Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: implement Weight and Work types #1195

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading