Skip to content

Commit

Permalink
feat(side-dag): implement PoaBlockProducer (#1061)
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco authored Jul 15, 2024
1 parent 7c7c261 commit 5a788d0
Show file tree
Hide file tree
Showing 17 changed files with 420 additions and 50 deletions.
13 changes: 13 additions & 0 deletions hathor/builder/cli_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from structlog import get_logger

from hathor.cli.run_node_args import RunNodeArgs
from hathor.cli.side_dag import SideDagArgs
from hathor.consensus import ConsensusAlgorithm
from hathor.daa import DifficultyAdjustmentAlgorithm
from hathor.event import EventManager
Expand Down Expand Up @@ -336,6 +337,18 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
vertex_handler=vertex_handler,
)

if settings.CONSENSUS_ALGORITHM.is_poa():
assert isinstance(self._args, SideDagArgs)
if self._args.poa_signer_file:
from hathor.consensus.poa import PoaBlockProducer, PoaSignerFile
poa_signer_file = PoaSignerFile.parse_file(self._args.poa_signer_file)
self.manager.poa_block_producer = PoaBlockProducer(
settings=settings,
reactor=reactor,
manager=self.manager,
poa_signer=poa_signer_file.get_signer(),
)

if self._args.x_ipython_kernel:
self.check_or_raise(self._args.x_asyncio_reactor,
'--x-ipython-kernel must be used with --x-asyncio-reactor')
Expand Down
79 changes: 79 additions & 0 deletions hathor/cli/generate_genesis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright 2024 Hathor Labs
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import sys

import base58


def main():
from hathor.cli.util import create_parser
from hathor.conf.get_settings import get_global_settings
from hathor.mining.cpu_mining_service import CpuMiningService
from hathor.transaction import Block, Transaction, TxOutput
from hathor.transaction.scripts import P2PKH

parser = create_parser()
parser.add_argument('--config-yaml', type=str, help='Configuration yaml filepath')
parser.add_argument('--genesis-address', type=str, help='Address for genesis tokens')
parser.add_argument('--genesis-block-timestamp', type=int, help='Timestamp for the genesis block')

args = parser.parse_args(sys.argv[1:])
if not args.config_yaml:
raise Exception('`--config-yaml` is required')
if not args.genesis_address:
raise Exception('`--genesis-address` is required')
if not args.genesis_block_timestamp:
raise Exception('`--genesis-block-timestamp` is required')

os.environ['HATHOR_CONFIG_YAML'] = args.config_yaml
settings = get_global_settings()
output_script = P2PKH.create_output_script(address=base58.b58decode(args.genesis_address))
block_timestamp = int(args.genesis_block_timestamp)
mining_service = CpuMiningService()

block = Block(
timestamp=block_timestamp,
weight=settings.MIN_BLOCK_WEIGHT,
outputs=[
TxOutput(settings.GENESIS_TOKENS, output_script),
],
)
mining_service.start_mining(block)
block.update_hash()

tx1 = Transaction(
timestamp=settings.GENESIS_TX1_TIMESTAMP,
weight=settings.MIN_TX_WEIGHT,
)
mining_service.start_mining(tx1)
tx1.update_hash()

tx2 = Transaction(
timestamp=settings.GENESIS_TX2_TIMESTAMP,
weight=settings.MIN_TX_WEIGHT,
)
mining_service.start_mining(tx2)
tx2.update_hash()

# The output format is compatible with the yaml config file
print('GENESIS_OUTPUT_SCRIPT:', output_script.hex())
print('GENESIS_BLOCK_TIMESTAMP:', block.timestamp)
print('GENESIS_BLOCK_HASH:', block.hash_hex)
print('GENESIS_BLOCK_NONCE:', block.nonce)
print('GENESIS_TX1_HASH:', tx1.hash_hex)
print('GENESIS_TX1_NONCE:', tx1.nonce)
print('GENESIS_TX2_HASH:', tx2.hash_hex)
print('GENESIS_TX2_NONCE:', tx2.nonce)
2 changes: 2 additions & 0 deletions hathor/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(self) -> None:
from . import (
db_export,
db_import,
generate_genesis,
generate_poa_keys,
generate_valid_words,
load_from_logs,
Expand Down Expand Up @@ -74,6 +75,7 @@ def __init__(self) -> None:
self.add_cmd('side-dag', 'run_node_with_side_dag', side_dag, 'Run a side-dag')
self.add_cmd('side-dag', 'gen_poa_keys', generate_poa_keys, 'Generate a private/public key pair and its '
'address to be used in Proof-of-Authority')
self.add_cmd('side-dag', 'gen_genesis', generate_genesis, 'Generate a new genesis')
self.add_cmd('docs', 'generate_openapi_json', openapi_json, 'Generate OpenAPI json for API docs')
self.add_cmd('multisig', 'gen_multisig_address', multisig_address, 'Generate a new multisig address')
self.add_cmd('multisig', 'spend_multisig_output', multisig_spend, 'Generate tx that spends a multisig output')
Expand Down
7 changes: 5 additions & 2 deletions hathor/cli/run_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,6 @@ def check_python_version(self) -> None:
]))

def __init__(self, *, argv=None):
from hathor.cli.run_node_args import RunNodeArgs
from hathor.conf import NANO_TESTNET_SETTINGS_FILEPATH, TESTNET_SETTINGS_FILEPATH
from hathor.conf.get_settings import get_global_settings
self.log = logger.new()
Expand All @@ -469,7 +468,7 @@ def __init__(self, *, argv=None):
self.parser = self.create_parser()
raw_args = self.parse_args(argv)

self._args = RunNodeArgs.parse_obj(vars(raw_args))
self._args = self._parse_args_obj(vars(raw_args))

if self._args.config_yaml:
os.environ['HATHOR_CONFIG_YAML'] = self._args.config_yaml
Expand Down Expand Up @@ -531,6 +530,10 @@ def init_sysctl(self, description: str, sysctl_init_file: Optional[str] = None)
def parse_args(self, argv: list[str]) -> Namespace:
return self.parser.parse_args(argv)

def _parse_args_obj(self, args: dict[str, Any]) -> 'RunNodeArgs':
from hathor.cli.run_node_args import RunNodeArgs
return RunNodeArgs.parse_obj(args)

def run(self) -> None:
self.reactor.run()

Expand Down
25 changes: 22 additions & 3 deletions hathor/cli/side_dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@
import signal
import sys
import traceback
from argparse import ArgumentParser
from dataclasses import dataclass
from enum import Enum
from multiprocessing import Pipe, Process
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from typing_extensions import assert_never
from typing_extensions import assert_never, override

from hathor.cli.run_node_args import RunNodeArgs # skip-cli-import-custom-check

if TYPE_CHECKING:
from hathor.cli.util import LoggingOutput
Expand All @@ -46,6 +49,10 @@
HATHOR_NODE_INIT_WAIT_PERIOD: int = 10


class SideDagArgs(RunNodeArgs):
poa_signer_file: str | None


@dataclass(frozen=True, slots=True)
class HathorProcessInitFail:
reason: str
Expand All @@ -69,6 +76,17 @@ class SideDagProcessTerminated:
class SideDagRunNode(RunNode):
env_vars_prefix = 'hathor_side_dag_'

@override
def _parse_args_obj(self, args: dict[str, Any]) -> RunNodeArgs:
return SideDagArgs.parse_obj(args)

@classmethod
@override
def create_parser(cls) -> ArgumentParser:
parser = super().create_parser()
parser.add_argument('--poa-signer-file', help='File containing the Proof-of-Authority signer private key.')
return parser


def main(capture_stdout: bool) -> None:
"""
Expand Down Expand Up @@ -201,7 +219,7 @@ def _run_side_dag_node(argv: list[str], *, hathor_node_process: Process, conn: '
try:
side_dag = SideDagRunNode(argv=argv)
except (BaseException, Exception):
logger.critical('terminating hathor node...')
logger.exception('terminating hathor node due to exception in side-dag node...')
conn.send(SideDagProcessTerminated())
hathor_node_process.terminate()
return
Expand Down Expand Up @@ -252,6 +270,7 @@ def _run_hathor_node(
setup_logging(logging_output=logging_output, logging_options=log_options, capture_stdout=capture_stdout)
hathor_node = run_node_cmd(argv=argv)
except (BaseException, Exception):
logger.exception('hathor process terminated due to exception in initialization')
conn.send(HathorProcessInitFail(traceback.format_exc()))
return

Expand Down
12 changes: 9 additions & 3 deletions hathor/consensus/consensus_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,19 @@ class PoaSettings(_BaseConsensusSettings):
type: Literal[ConsensusType.PROOF_OF_AUTHORITY] = ConsensusType.PROOF_OF_AUTHORITY

# A list of Proof-of-Authority signer public keys that have permission to produce blocks.
signers: tuple[bytes, ...] = ()
signers: tuple[bytes, ...]

@validator('signers', each_item=True)
def parse_hex_str(cls, hex_str: str | bytes) -> bytes:
@validator('signers', each_item=True, pre=True)
def _parse_hex_str(cls, hex_str: str | bytes) -> bytes:
from hathor.conf.settings import parse_hex_str
return parse_hex_str(hex_str)

@validator('signers')
def _validate_signers(cls, signers: tuple[bytes, ...]) -> tuple[bytes, ...]:
if len(signers) == 0:
raise ValueError('At least one signer must be provided in PoA networks')
return signers

@override
def _get_valid_vertex_versions(self) -> set[TxVersion]:
return {
Expand Down
16 changes: 15 additions & 1 deletion hathor/consensus/poa/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
from .poa import BLOCK_WEIGHT_IN_TURN, BLOCK_WEIGHT_OUT_OF_TURN, SIGNER_ID_LEN, get_hashed_poa_data
from .poa import (
BLOCK_WEIGHT_IN_TURN,
BLOCK_WEIGHT_OUT_OF_TURN,
SIGNER_ID_LEN,
calculate_weight,
get_hashed_poa_data,
is_in_turn,
)
from .poa_block_producer import PoaBlockProducer
from .poa_signer import PoaSigner, PoaSignerFile

__all__ = [
'BLOCK_WEIGHT_IN_TURN',
'BLOCK_WEIGHT_OUT_OF_TURN',
'SIGNER_ID_LEN',
'get_hashed_poa_data',
'is_in_turn',
'calculate_weight',
'PoaBlockProducer',
'PoaSigner',
'PoaSignerFile',
]
12 changes: 12 additions & 0 deletions hathor/consensus/poa/poa.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import hashlib
from typing import TYPE_CHECKING

from hathor.consensus.consensus_settings import PoaSettings
from hathor.transaction import Block

if TYPE_CHECKING:
Expand All @@ -34,3 +35,14 @@ def get_hashed_poa_data(block: PoaBlock) -> bytes:
poa_data += block.get_struct_nonce()
hashed_poa_data = hashlib.sha256(poa_data).digest()
return hashed_poa_data


def is_in_turn(*, settings: PoaSettings, height: int, signer_index: int) -> bool:
"""Return whether the given signer is in turn for the given height."""
return height % len(settings.signers) == signer_index


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

0 comments on commit 5a788d0

Please sign in to comment.