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 9067e43
Show file tree
Hide file tree
Showing 75 changed files with 476 additions and 335 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
28 changes: 14 additions & 14 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 All @@ -303,7 +303,7 @@ def update_score_and_mark_as_the_best_chain_if_possible(self, block: Block) -> N
else:
assert best_score == head_meta.score
best_heads.append(head)
assert isinstance(best_score, int) and best_score > 0
assert isinstance(best_score, Work) and best_score > Work(0)

assert len(best_heads) > 0
first_block = self._find_first_parent_in_best_chain(best_heads[0])
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
25 changes: 13 additions & 12 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 All @@ -125,7 +126,7 @@ def calculate_next_weight(

assert len(blocks) == N + 1
solvetimes, weights = zip(*(
(block.timestamp - prev_block.timestamp, block.weight)
(block.timestamp - prev_block.timestamp, block.weight.get())
for prev_block, block in iwindows(blocks, 2)
))
assert len(solvetimes) == len(weights) == N, f'got {len(solvetimes)}, {len(weights)} expected {N}'
Expand Down Expand Up @@ -156,7 +157,7 @@ def calculate_next_weight(
if weight < self.MIN_BLOCK_WEIGHT:
weight = self.MIN_BLOCK_WEIGHT

return weight
return Weight(weight)

def get_weight_decay_amount(self, distance: int) -> float:
"""Return the amount to be reduced in the weight of the block."""
Expand All @@ -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 All @@ -207,7 +208,7 @@ def minimum_tx_weight(self, tx: 'Transaction') -> float:
# Make sure the calculated weight is at least the minimum
weight = max(weight, self._settings.MIN_TX_WEIGHT)

return weight
return Weight(weight)

def get_tokens_issued_per_block(self, height: int) -> int:
"""Return the number of tokens issued (aka reward) per block of a given height."""
Expand Down
Loading

0 comments on commit 9067e43

Please sign in to comment.