From 47473dd1efe839f7fde085ab98a9edd2a186118f Mon Sep 17 00:00:00 2001 From: Jan Segre Date: Mon, 4 Mar 2024 16:18:18 -0300 Subject: [PATCH] feat(mergedmining): support dummy mining on coordinator --- hathor/cli/merged_mining.py | 25 +++++++-- hathor/client.py | 3 +- hathor/merged_mining/coordinator.py | 79 ++++++++++++++++++++++------- 3 files changed, 81 insertions(+), 26 deletions(-) diff --git a/hathor/cli/merged_mining.py b/hathor/cli/merged_mining.py index 14061ecb7..3fa11db06 100644 --- a/hathor/cli/merged_mining.py +++ b/hathor/cli/merged_mining.py @@ -30,8 +30,13 @@ def create_parser() -> ArgumentParser: parser.add_argument('--debug-listen', help='Port to listen for Debug API', type=int, required=False) parser.add_argument('--hathor-api', help='Endpoint of the Hathor API (without version)', type=str, required=True) parser.add_argument('--hathor-address', help='Hathor address to send funds to', type=str, required=False) - parser.add_argument('--bitcoin-rpc', help='Endpoint of the Bitcoin RPC', type=str, required=True) + rpc = parser.add_mutually_exclusive_group(required=True) + rpc.add_argument('--bitcoin-rpc', help='Endpoint of the Bitcoin RPC', type=str) parser.add_argument('--bitcoin-address', help='Bitcoin address to send funds to', type=str, required=False) + rpc.add_argument('--x-dummy-merged-mining', help='Use zeroed bits to simulate a dummy merged mining', + action='store_true') + parser.add_argument('--x-dummy-merkle-len', help='Merkle path length to simulate when doing dummy merged mining', + type=int, required=False) parser.add_argument('--min-diff', help='Minimum difficulty to set for jobs', type=int, required=False) return parser @@ -45,7 +50,14 @@ def execute(args: Namespace) -> None: loop = asyncio.get_event_loop() - bitcoin_rpc = BitcoinRPC(args.bitcoin_rpc) + bitcoin_rpc: BitcoinRPC | None + if args.bitcoin_rpc is not None: + # XXX: plain assert because argparse should already ensure it's correct + assert not args.x_dummy_merged_mining + bitcoin_rpc = BitcoinRPC(args.bitcoin_rpc) + else: + assert args.x_dummy_merged_mining + bitcoin_rpc = None hathor_client = HathorClient(args.hathor_api) # TODO: validate addresses? merged_mining = MergedMiningCoordinator( @@ -55,9 +67,11 @@ def execute(args: Namespace) -> None: payback_address_bitcoin=args.bitcoin_address, address_from_login=not (args.hathor_address and args.bitcoin_address), min_difficulty=args.min_diff, + dummy_merkle_path_len=args.x_dummy_merkle_len, ) - logger.info('start Bitcoin RPC', url=args.bitcoin_rpc) - loop.run_until_complete(bitcoin_rpc.start()) + if bitcoin_rpc is not None: + logger.info('start Bitcoin RPC', url=args.bitcoin_rpc) + loop.run_until_complete(bitcoin_rpc.start()) logger.info('start Hathor Client', url=args.hathor_api) loop.run_until_complete(hathor_client.start()) logger.info('start Merged Mining Server', listen=f'0.0.0.0:{args.port}') @@ -89,7 +103,8 @@ def execute(args: Namespace) -> None: loop.run_until_complete(mm_server.wait_closed()) loop.run_until_complete(merged_mining.stop()) loop.run_until_complete(hathor_client.stop()) - loop.run_until_complete(bitcoin_rpc.stop()) + if bitcoin_rpc is not None: + loop.run_until_complete(bitcoin_rpc.stop()) loop.close() logger.info('bye') diff --git a/hathor/client.py b/hathor/client.py index 1c68f6787..f6b1ece41 100644 --- a/hathor/client.py +++ b/hathor/client.py @@ -251,8 +251,7 @@ async def submit(self, block: Block) -> Optional[BlockTemplate]: resp: Union[bool, dict] = await self._do_request('mining.submit', { 'hexdata': bytes(block).hex(), }) - if resp: - assert isinstance(resp, dict) + if isinstance(resp, dict): error = resp.get('error') if error: raise APIError(error) diff --git a/hathor/merged_mining/coordinator.py b/hathor/merged_mining/coordinator.py index f65a181da..88905692a 100644 --- a/hathor/merged_mining/coordinator.py +++ b/hathor/merged_mining/coordinator.py @@ -71,6 +71,9 @@ # PROPAGATION_FAILED = {'code': 33, 'message': 'Solution propagation failed'} DUPLICATE_SOLUTION = {'code': 34, 'message': 'Solution already submitted'} +ZEROED_4: bytes = b'\0' * 4 +ZEROED_32: bytes = b'\0' * 32 + class HathorCoordJob(NamedTuple): """ Data class used to send a job's work to Hathor Stratum. @@ -688,6 +691,8 @@ async def submit_to_bitcoin(self, job: SingleMinerJob, work: SingleMinerWork) -> """ Submit work to Bitcoin RPC. """ bitcoin_rpc = self.coordinator.bitcoin_rpc + if bitcoin_rpc is None: + return # bitcoin_block = job.build_bitcoin_block(work) # XXX: too expensive for now bitcoin_block_header = job.build_bitcoin_block_header(work) block_hash = Hash(bitcoin_block_header.hash) @@ -797,6 +802,29 @@ class BitcoinCoordJob(NamedTuple): witness_commitment: Optional[bytes] = None append_to_input: bool = True + @classmethod + def create_dummy(cls, merkle_path_len: int = 0) -> 'BitcoinCoordJob': + """ Creates a dummy instance with zeroed values and optionally a merkle path with the given length. + """ + if merkle_path_len > 0: + transactions = [BitcoinRawTransaction(ZEROED_32, ZEROED_32, b'')] * (2 ** merkle_path_len - 1) + merkle_path = tuple(build_merkle_path_for_coinbase([t.txid for t in transactions])) + else: + transactions = [] + merkle_path = tuple() + return cls( + version=0, + previous_block_hash=ZEROED_32, + coinbase_value=0, + target=ZEROED_32, + min_time=0, + size_limit=0, + bits=ZEROED_4, + height=0, + transactions=transactions, + merkle_path=merkle_path, + ) + @classmethod def from_dict(cls, params: dict) -> 'BitcoinCoordJob': r""" Convert from dict of the properties returned from Bitcoin RPC. @@ -968,7 +996,7 @@ def make_coinbase_transaction(self, hathor_block_hash: bytes, payback_script_bit if self.witness_commitment is not None: segwit_output = BitcoinTransactionOutput(0, self.witness_commitment) outputs.append(segwit_output) - coinbase_input.script_witness.append(b'\0' * 32) + coinbase_input.script_witness.append(ZEROED_32) # append now because segwit presence may change this inputs.append(coinbase_input) @@ -1110,10 +1138,17 @@ class MergedMiningCoordinator: MAX_XNONCE1 = 2**XNONCE1_SIZE - 1 MAX_RECONNECT_BACKOFF = 30 - def __init__(self, bitcoin_rpc: IBitcoinRPC, hathor_client: IHathorClient, - payback_address_bitcoin: Optional[str], payback_address_hathor: Optional[str], - address_from_login: bool = True, min_difficulty: Optional[int] = None, - sequential_xnonce1: bool = False, rng: Optional[Random] = None): + def __init__(self, + bitcoin_rpc: IBitcoinRPC | None, + hathor_client: IHathorClient, + payback_address_bitcoin: str | None, + payback_address_hathor: str | None, + address_from_login: bool = True, + min_difficulty: int | None = None, + sequential_xnonce1: bool = False, + rng: Random | None = None, + dummy_merkle_path_len: int | None = None, + ): self.log = logger.new() if rng is None: rng = Random() @@ -1144,6 +1179,7 @@ def __init__(self, bitcoin_rpc: IBitcoinRPC, hathor_client: IHathorClient, self.started_at = 0.0 self.strip_all_transactions = False self.strip_segwit_transactions = False + self.dummy_merkle_path_len = dummy_merkle_path_len or 0 @property def uptime(self) -> float: @@ -1185,21 +1221,24 @@ async def start(self) -> None: """ loop = asyncio.get_event_loop() self.started_at = time.time() - self.update_bitcoin_block_task = loop.create_task(self.update_bitcoin_block()) + if self.bitcoin_rpc is not None: + self.update_bitcoin_block_task = loop.create_task(self.update_bitcoin_block()) + else: + self.bitcoin_coord_job = BitcoinCoordJob.create_dummy(self.dummy_merkle_path_len) self.update_hathor_block_task = loop.create_task(self.update_hathor_block()) async def stop(self) -> None: """ Stops the client, interrupting mining processes, stoping supervisor loop, and sending finished jobs. """ - assert self.update_bitcoin_block_task is not None - self.update_bitcoin_block_task.cancel() + finals = [] + if self.update_bitcoin_block_task is not None: + self.update_bitcoin_block_task.cancel() + finals.append(self.update_bitcoin_block_task) assert self.update_hathor_block_task is not None self.update_hathor_block_task.cancel() + finals.append(self.update_hathor_block_task) try: - await asyncio.gather( - self.update_bitcoin_block_task, - self.update_hathor_block_task, - ) + await asyncio.gather(*finals) except asyncio.CancelledError: pass except Exception: @@ -1210,6 +1249,7 @@ async def stop(self) -> None: async def update_bitcoin_block(self) -> None: """ Task that continuously polls block templates from bitcoin.get_block_template """ + assert self.bitcoin_rpc is not None backoff = 1 longpoll_id = None while True: @@ -1348,13 +1388,14 @@ async def update_merged_block(self) -> None: merkle_root = build_merkle_root(list(tx.txid for tx in block_proposal.transactions)) if merkle_root != block_proposal.header.merkle_root: self.log.warn('bad merkle root', expected=merkle_root.hex(), got=block_proposal.header.merkle_root.hex()) - error = await self.bitcoin_rpc.verify_block_proposal(block=bytes(block_proposal)) - if error is not None: - self.log.warn('proposed block is invalid, skipping update', error=error) - else: - self.next_merged_job = merged_job - self.update_jobs() - self.log.debug('merged job updated') + if self.bitcoin_rpc is not None: + error = await self.bitcoin_rpc.verify_block_proposal(block=bytes(block_proposal)) + if error is not None: + self.log.warn('proposed block is invalid, skipping update', error=error) + return + self.next_merged_job = merged_job + self.update_jobs() + self.log.debug('merged job updated') def status(self) -> dict[Any, Any]: """ Build status dict with useful metrics for use in MM Status API.