Skip to content

Commit

Permalink
refactor: implement Weight and Work types
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco committed Dec 12, 2024
1 parent 692a74c commit 215eecf
Show file tree
Hide file tree
Showing 74 changed files with 473 additions and 317 deletions.
8 changes: 5 additions & 3 deletions hathor/cli/events_simulator/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def simulate_reorg(simulator: 'Simulator', manager: 'HathorManager') -> None:
def simulate_unvoided_transaction(simulator: 'Simulator', manager: 'HathorManager') -> None:
from hathor.conf.get_settings import get_global_settings
from hathor.simulator.utils import add_new_block, add_new_blocks, gen_new_tx
from hathor.transaction.weight import Weight

settings = get_global_settings()
assert manager.wallet is not None
Expand All @@ -115,15 +116,15 @@ def simulate_unvoided_transaction(simulator: 'Simulator', manager: 'HathorManage

# A tx is created with weight 19.0005
tx = gen_new_tx(manager, address, 1000)
tx.weight = 19.0005
tx.weight = Weight(19.0005)
tx.update_hash()
assert manager.propagate_tx(tx, fails_silently=False)
simulator.run(60)

# A clone is created with a greater timestamp and a lower weight. It's a voided twin tx.
tx2 = tx.clone(include_metadata=False)
tx2.timestamp += 60
tx2.weight = 19
tx2.weight = Weight(19)
tx2.update_hash()
assert manager.propagate_tx(tx2, fails_silently=False)
simulator.run(60)
Expand Down Expand Up @@ -152,6 +153,7 @@ def simulate_invalid_mempool_transaction(simulator: 'Simulator', manager: 'Hatho
from hathor.conf.get_settings import get_global_settings
from hathor.simulator.utils import add_new_blocks, gen_new_tx
from hathor.transaction import Block
from hathor.transaction.weight import Weight

settings = get_global_settings()
assert manager.wallet is not None
Expand All @@ -174,7 +176,7 @@ def simulate_invalid_mempool_transaction(simulator: 'Simulator', manager: 'Hatho
block_to_replace = blocks[-2]
tb0 = manager.make_custom_block_template(block_to_replace.parents[0], block_to_replace.parents[1:])
b0: Block = tb0.generate_mining_block(manager.rng, storage=manager.tx_storage)
b0.weight = 10
b0.weight = Weight(10)
manager.cpu_mining_service.resolve(b0)
assert manager.propagate_tx(b0, fails_silently=False)
simulator.run(60)
Expand Down
5 changes: 3 additions & 2 deletions hathor/cli/generate_genesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class GenerateGenesisArgs(BaseModel):
def main() -> None:
from hathor.cli.util import create_parser
from hathor.transaction.genesis import generate_new_genesis
from hathor.transaction.weight import Weight

parser = create_parser()
parser.add_argument('--tokens', type=int, help='Amount of genesis tokens, including decimals', required=True)
Expand All @@ -43,8 +44,8 @@ def main() -> None:
tokens=args.tokens,
address=args.address,
block_timestamp=args.block_timestamp,
min_block_weight=args.min_block_weight,
min_tx_weight=args.min_tx_weight,
min_block_weight=Weight(args.min_block_weight),
min_tx_weight=Weight(args.min_tx_weight),
)

print('# Paste this output into your network\'s yaml configuration file')
Expand Down
6 changes: 3 additions & 3 deletions hathor/conf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ def GENESIS_TX2_TIMESTAMP(self) -> int:
GENESIS_TX2_HASH: bytes = bytes.fromhex('0002c187ab30d4f61c11a5dc43240bdf92dba4d19f40f1e883b0a5fdac54ef53')

# Weight of genesis and minimum weight of a tx/block
MIN_BLOCK_WEIGHT: int = 21
MIN_TX_WEIGHT: int = 14
MIN_SHARE_WEIGHT: int = 21
MIN_BLOCK_WEIGHT: float = 21.0
MIN_TX_WEIGHT: float = 14.0
MIN_SHARE_WEIGHT: float = 21.0

HATHOR_TOKEN_UID: bytes = HATHOR_TOKEN_UID

Expand Down
26 changes: 13 additions & 13 deletions hathor/consensus/block_consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@

from hathor.conf.get_settings import get_global_settings
from hathor.transaction import BaseTransaction, Block, Transaction
from hathor.transaction.weight import Weight, Work
from hathor.util import classproperty
from hathor.utils.weight import weight_to_work

if TYPE_CHECKING:
from hathor.consensus.context import ConsensusAlgorithmContext
Expand Down Expand Up @@ -99,7 +99,7 @@ def update_voided_info(self, block: Block) -> None:
When there are multiple best chains, all their heads will be voided.
"""
assert block.weight > 0, 'This algorithm assumes that block\'s weight is always greater than zero'
assert block.weight > Weight(0.0), 'This algorithm assumes that block\'s weight is always greater than zero'
if not block.parents:
assert block.is_genesis is True
self.update_score_and_mark_as_the_best_chain(block)
Expand All @@ -118,7 +118,7 @@ def update_voided_info(self, block: Block) -> None:
for h in voided_by:
tx = storage.get_transaction(h)
tx_meta = tx.get_metadata()
tx_meta.accumulated_weight += weight_to_work(block.weight)
tx_meta.accumulated_weight = tx_meta.accumulated_weight.add(block.weight)
self.context.save(tx)

# Check conflicts of the transactions voiding us.
Expand Down Expand Up @@ -162,7 +162,7 @@ def update_voided_info(self, block: Block) -> None:

# Get the score of the best chains.
heads = [cast(Block, storage.get_transaction(h)) for h in storage.get_best_block_tips()]
best_score: int | None = None
best_score: Work | None = None
for head in heads:
head_meta = head.get_metadata(force_reload=True)
if best_score is None:
Expand Down Expand Up @@ -286,11 +286,11 @@ def update_score_and_mark_as_the_best_chain_if_possible(self, block: Block) -> N
self.update_score_and_mark_as_the_best_chain(block)
self.remove_voided_by_from_chain(block)

best_score: int
best_score: Work
if self.update_voided_by_from_parents(block):
storage = block.storage
heads = [cast(Block, storage.get_transaction(h)) for h in storage.get_best_block_tips()]
best_score = 0
best_score = Work(0)
best_heads: list[Block]
for head in heads:
head_meta = head.get_metadata(force_reload=True)
Expand Down Expand Up @@ -447,7 +447,7 @@ def remove_first_block_markers(self, block: Block) -> None:
self.context.save(tx)

def _score_block_dfs(self, block: BaseTransaction, used: set[bytes],
mark_as_best_chain: bool, newest_timestamp: int) -> int:
mark_as_best_chain: bool, newest_timestamp: int) -> Work:
""" Internal method to run a DFS. It is used by `calculate_score()`.
"""
assert block.storage is not None
Expand All @@ -456,7 +456,7 @@ def _score_block_dfs(self, block: BaseTransaction, used: set[bytes],
storage = block.storage

from hathor.transaction import Block
score = weight_to_work(block.weight)
score = block.weight.to_work()
for parent in block.get_parents():
if parent.is_block:
assert isinstance(parent, Block)
Expand All @@ -465,7 +465,7 @@ def _score_block_dfs(self, block: BaseTransaction, used: set[bytes],
x = meta.score
else:
x = self._score_block_dfs(parent, used, mark_as_best_chain, newest_timestamp)
score += x
score = score.add(x)

else:
from hathor.transaction.storage.traversal import BFSTimestampWalk
Expand Down Expand Up @@ -493,7 +493,7 @@ def _score_block_dfs(self, block: BaseTransaction, used: set[bytes],
meta.first_block = block.hash
self.context.save(tx)

score += weight_to_work(tx.weight)
score = score.add(tx.weight)

# Always save the score when it is calculated.
meta = block.get_metadata()
Expand All @@ -510,7 +510,7 @@ def _score_block_dfs(self, block: BaseTransaction, used: set[bytes],

return score

def calculate_score(self, block: Block, *, mark_as_best_chain: bool = False) -> int:
def calculate_score(self, block: Block, *, mark_as_best_chain: bool = False) -> Work:
""" Calculate block's score, which is the accumulated work of the verified transactions and blocks.
:param: mark_as_best_chain: If `True`, the transactions' will point `meta.first_block` to
Expand All @@ -520,9 +520,9 @@ def calculate_score(self, block: Block, *, mark_as_best_chain: bool = False) ->
if block.is_genesis:
if mark_as_best_chain:
meta = block.get_metadata()
meta.score = weight_to_work(block.weight)
meta.score = block.weight.to_work()
self.context.save(block)
return weight_to_work(block.weight)
return block.weight.to_work()

parent = self._find_first_parent_in_best_chain(block)
newest_timestamp = parent.timestamp
Expand Down
5 changes: 3 additions & 2 deletions hathor/consensus/poa/poa.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from hathor.consensus.consensus_settings import PoaSettings
from hathor.crypto.util import get_public_key_from_bytes_compressed
from hathor.transaction import Block
from hathor.transaction.weight import Weight

if TYPE_CHECKING:
from hathor.transaction.poa import PoaBlock
Expand Down Expand Up @@ -66,10 +67,10 @@ def get_signer_index_distance(*, settings: PoaSettings, signer_index: int, heigh
return index_distance


def calculate_weight(settings: PoaSettings, block: PoaBlock, signer_index: int) -> float:
def calculate_weight(settings: PoaSettings, block: PoaBlock, signer_index: int) -> Weight:
"""Return the weight for the given block and signer."""
index_distance = get_signer_index_distance(settings=settings, signer_index=signer_index, height=block.get_height())
return BLOCK_WEIGHT_IN_TURN if index_distance == 0 else BLOCK_WEIGHT_OUT_OF_TURN / index_distance
return Weight(BLOCK_WEIGHT_IN_TURN) if index_distance == 0 else Weight(BLOCK_WEIGHT_OUT_OF_TURN / index_distance)


@dataclass(frozen=True, slots=True)
Expand Down
3 changes: 2 additions & 1 deletion hathor/consensus/poa/poa_block_producer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from hathor.crypto.util import get_public_key_bytes_compressed
from hathor.pubsub import EventArguments, HathorEvents
from hathor.reactor import ReactorProtocol
from hathor.transaction.weight import Weight
from hathor.util import not_none

if TYPE_CHECKING:
Expand Down Expand Up @@ -129,7 +130,7 @@ def _on_new_vertex(self, event: HathorEvents, args: EventArguments) -> None:
return

from hathor.transaction.poa import PoaBlock
if isinstance(block, PoaBlock) and not block.weight == poa.BLOCK_WEIGHT_IN_TURN:
if isinstance(block, PoaBlock) and not block.weight == Weight(poa.BLOCK_WEIGHT_IN_TURN):
self._log.info('received out of turn block', block=block.hash_hex, signer_id=block.signer_id)

self._schedule_block()
Expand Down
12 changes: 6 additions & 6 deletions hathor/consensus/transaction_consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

from hathor.conf.get_settings import get_global_settings
from hathor.transaction import BaseTransaction, Block, Transaction, TxInput
from hathor.transaction.weight import Work
from hathor.util import classproperty
from hathor.utils.weight import weight_to_work

if TYPE_CHECKING:
from hathor.consensus.context import ConsensusAlgorithmContext
Expand Down Expand Up @@ -194,13 +194,13 @@ def update_voided_info(self, tx: Transaction) -> None:
continue
tx2 = tx.storage.get_transaction(h)
tx2_meta = tx2.get_metadata()
tx2_meta.accumulated_weight += weight_to_work(tx.weight)
tx2_meta.accumulated_weight = tx2_meta.accumulated_weight.add(tx.weight)
self.context.save(tx2)

# Then, we add ourselves.
meta = tx.get_metadata()
assert not meta.voided_by or meta.voided_by == {tx.hash}
assert meta.accumulated_weight == weight_to_work(tx.weight)
assert meta.accumulated_weight == tx.weight.to_work()
if tx.hash in self.context.consensus.soft_voided_tx_ids:
voided_by.add(self._settings.SOFT_VOIDED_ID)
voided_by.add(tx.hash)
Expand Down Expand Up @@ -298,10 +298,10 @@ def check_conflicts(self, tx: Transaction) -> None:
if not tx_meta.voided_by:
candidate.update_accumulated_weight(stop_value=meta.accumulated_weight)
tx_meta = candidate.get_metadata()
d = tx_meta.accumulated_weight - meta.accumulated_weight
if d == 0:
d = tx_meta.accumulated_weight.sub(meta.accumulated_weight)
if d == Work(0):
tie_list.append(candidate)
elif d > 0:
elif d > Work(0):
is_highest = False
break
if not is_highest:
Expand Down
19 changes: 10 additions & 9 deletions hathor/daa.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from hathor.conf.settings import HathorSettings
from hathor.profiler import get_cpu_profiler
from hathor.transaction.weight import Weight
from hathor.types import VertexId
from hathor.util import iwindows

Expand Down Expand Up @@ -58,13 +59,13 @@ def __init__(self, *, settings: HathorSettings, test_mode: TestMode = TestMode.D
DifficultyAdjustmentAlgorithm.singleton = self

@cpu.profiler(key=lambda _, block: 'calculate_block_difficulty!{}'.format(block.hash.hex()))
def calculate_block_difficulty(self, block: 'Block', parent_block_getter: Callable[['Block'], 'Block']) -> float:
def calculate_block_difficulty(self, block: 'Block', parent_block_getter: Callable[['Block'], 'Block']) -> Weight:
""" Calculate block weight according to the ascendants of `block`, using calculate_next_weight."""
if self.TEST_MODE & TestMode.TEST_BLOCK_WEIGHT:
return 1.0
return Weight(1.0)

if block.is_genesis:
return self.MIN_BLOCK_WEIGHT
return Weight(self.MIN_BLOCK_WEIGHT)

parent_block = parent_block_getter(block)
return self.calculate_next_weight(parent_block, block.timestamp, parent_block_getter)
Expand Down Expand Up @@ -94,15 +95,15 @@ def calculate_next_weight(
parent_block: 'Block',
timestamp: int,
parent_block_getter: Callable[['Block'], 'Block'],
) -> float:
) -> Weight:
""" Calculate the next block weight, aka DAA/difficulty adjustment algorithm.
The algorithm used is described in [RFC 22](https://gitlab.com/HathorNetwork/rfcs/merge_requests/22).
The weight must not be less than `MIN_BLOCK_WEIGHT`.
"""
if self.TEST_MODE & TestMode.TEST_BLOCK_WEIGHT:
return 1.0
return Weight(1.0)

from hathor.transaction import sum_weights

Expand All @@ -112,7 +113,7 @@ def calculate_next_weight(
T = self.AVG_TIME_BETWEEN_BLOCKS
S = 5
if N < 10:
return self.MIN_BLOCK_WEIGHT
return Weight(self.MIN_BLOCK_WEIGHT)

blocks: list['Block'] = []
while len(blocks) < N + 1:
Expand Down Expand Up @@ -171,7 +172,7 @@ def get_weight_decay_amount(self, distance: int) -> float:
n_windows = 1 + (dt // self._settings.WEIGHT_DECAY_WINDOW_SIZE)
return n_windows * self._settings.WEIGHT_DECAY_AMOUNT

def minimum_tx_weight(self, tx: 'Transaction') -> float:
def minimum_tx_weight(self, tx: 'Transaction') -> Weight:
""" Returns the minimum weight for the param tx
The minimum is calculated by the following function:
Expand All @@ -188,10 +189,10 @@ def minimum_tx_weight(self, tx: 'Transaction') -> float:
# In test mode we don't validate the minimum weight for tx
# We do this to allow generating many txs for testing
if self.TEST_MODE & TestMode.TEST_TX_WEIGHT:
return 1.0
return Weight(1.0)

if tx.is_genesis:
return self._settings.MIN_TX_WEIGHT
return Weight(self._settings.MIN_TX_WEIGHT)

tx_size = len(tx.get_struct())

Expand Down
9 changes: 5 additions & 4 deletions hathor/dag_builder/vertex_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from hathor.transaction.base_transaction import TxInput, TxOutput
from hathor.transaction.scripts.p2pkh import P2PKH
from hathor.transaction.token_creation_tx import TokenCreationTransaction
from hathor.transaction.weight import Weight
from hathor.wallet import BaseWallet


Expand Down Expand Up @@ -66,7 +67,7 @@ def get_parent_block(self, block: Block) -> Block:
if block.parents[0] == self._settings.GENESIS_BLOCK_HASH:
genesis_block = Block(
timestamp=self._settings.GENESIS_BLOCK_TIMESTAMP,
weight=self._settings.MIN_BLOCK_WEIGHT,
weight=Weight(self._settings.MIN_BLOCK_WEIGHT),
)
genesis_block.get_height = lambda: 0 # type: ignore[method-assign]
return genesis_block
Expand Down Expand Up @@ -177,7 +178,7 @@ def create_vertex_token(self, node: DAGNode) -> TokenCreationTransaction:
vertex.timestamp = self.get_min_timestamp(node)
self.sign_all_inputs(node, vertex)
if 'weight' in node.attrs:
vertex.weight = float(node.attrs['weight'])
vertex.weight = Weight(float(node.attrs['weight']))
else:
vertex.weight = self._daa.minimum_tx_weight(vertex)
self.update_vertex_hash(vertex)
Expand All @@ -202,7 +203,7 @@ def create_vertex_block(self, node: DAGNode) -> Block:
blk.get_height = lambda: height # type: ignore[method-assign]
blk.update_hash() # the next call fails is blk.hash is None
if 'weight' in node.attrs:
blk.weight = float(node.attrs['weight'])
blk.weight = Weight(float(node.attrs['weight']))
else:
blk.weight = self._daa.calculate_block_difficulty(blk, self.get_parent_block)
self.update_vertex_hash(blk)
Expand All @@ -220,7 +221,7 @@ def create_vertex_transaction(self, node: DAGNode) -> Transaction:
tx.timestamp = self.get_min_timestamp(node)
self.sign_all_inputs(node, tx)
if 'weight' in node.attrs:
tx.weight = float(node.attrs['weight'])
tx.weight = Weight(float(node.attrs['weight']))
else:
tx.weight = self._daa.minimum_tx_weight(tx)
self.update_vertex_hash(tx)
Expand Down
4 changes: 2 additions & 2 deletions hathor/graphviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ def get_node_label(self, tx: BaseTransaction) -> str:
parts = [tx.hash.hex()[-4:]]

if self.show_weight:
parts.append('w: {:.2f}'.format(tx.weight))
parts.append('w: {:.2f}'.format(tx.weight.get()))
if self.show_acc_weight:
meta = tx.get_metadata()
parts.append('a: {:.2f}'.format(meta.accumulated_weight))
parts.append('a: {:.2f}'.format(meta.accumulated_weight.get()))
return '\n'.join(parts)

def get_node_attrs(self, tx: BaseTransaction) -> dict[str, str]:
Expand Down
Loading

0 comments on commit 215eecf

Please sign in to comment.