Skip to content

Commit 07d2a63

Browse files
committed
feat(mergedmining): support dummy mining on coordinator
1 parent 23f764d commit 07d2a63

File tree

3 files changed

+73
-26
lines changed

3 files changed

+73
-26
lines changed

hathor/cli/merged_mining.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@ def create_parser() -> ArgumentParser:
3030
parser.add_argument('--debug-listen', help='Port to listen for Debug API', type=int, required=False)
3131
parser.add_argument('--hathor-api', help='Endpoint of the Hathor API (without version)', type=str, required=True)
3232
parser.add_argument('--hathor-address', help='Hathor address to send funds to', type=str, required=False)
33-
parser.add_argument('--bitcoin-rpc', help='Endpoint of the Bitcoin RPC', type=str, required=True)
33+
rpc = parser.add_mutually_exclusive_group(required=True)
34+
rpc.add_argument('--bitcoin-rpc', help='Endpoint of the Bitcoin RPC', type=str)
3435
parser.add_argument('--bitcoin-address', help='Bitcoin address to send funds to', type=str, required=False)
36+
rpc.add_argument('--dummy-merged-mining', help='Use zeroed bits to simulate a dummy merged mining',
37+
action='store_true')
38+
parser.add_argument('--dummy-merkle-len', help='Merkle path length to simulate when doing dummy merged mining',
39+
type=int, required=False)
3540
parser.add_argument('--min-diff', help='Minimum difficulty to set for jobs', type=int, required=False)
3641
return parser
3742

@@ -45,7 +50,14 @@ def execute(args: Namespace) -> None:
4550

4651
loop = asyncio.get_event_loop()
4752

48-
bitcoin_rpc = BitcoinRPC(args.bitcoin_rpc)
53+
bitcoin_rpc: BitcoinRPC | None
54+
if args.bitcoin_rpc is not None:
55+
# XXX: plain assert because argparse should already ensure it's correct
56+
assert not args.dummy_merged_mining
57+
bitcoin_rpc = BitcoinRPC(args.bitcoin_rpc)
58+
else:
59+
assert args.dummy_merged_mining
60+
bitcoin_rpc = None
4961
hathor_client = HathorClient(args.hathor_api)
5062
# TODO: validate addresses?
5163
merged_mining = MergedMiningCoordinator(
@@ -55,9 +67,11 @@ def execute(args: Namespace) -> None:
5567
payback_address_bitcoin=args.bitcoin_address,
5668
address_from_login=not (args.hathor_address and args.bitcoin_address),
5769
min_difficulty=args.min_diff,
70+
dummy_merkle_path_len=args.dummy_merkle_len,
5871
)
59-
logger.info('start Bitcoin RPC', url=args.bitcoin_rpc)
60-
loop.run_until_complete(bitcoin_rpc.start())
72+
if bitcoin_rpc is not None:
73+
logger.info('start Bitcoin RPC', url=args.bitcoin_rpc)
74+
loop.run_until_complete(bitcoin_rpc.start())
6175
logger.info('start Hathor Client', url=args.hathor_api)
6276
loop.run_until_complete(hathor_client.start())
6377
logger.info('start Merged Mining Server', listen=f'0.0.0.0:{args.port}')
@@ -89,7 +103,8 @@ def execute(args: Namespace) -> None:
89103
loop.run_until_complete(mm_server.wait_closed())
90104
loop.run_until_complete(merged_mining.stop())
91105
loop.run_until_complete(hathor_client.stop())
92-
loop.run_until_complete(bitcoin_rpc.stop())
106+
if bitcoin_rpc is not None:
107+
loop.run_until_complete(bitcoin_rpc.stop())
93108
loop.close()
94109
logger.info('bye')
95110

hathor/client.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,7 @@ async def submit(self, block: Block) -> Optional[BlockTemplate]:
251251
resp: Union[bool, dict] = await self._do_request('mining.submit', {
252252
'hexdata': bytes(block).hex(),
253253
})
254-
if resp:
255-
assert isinstance(resp, dict)
254+
if isinstance(resp, dict):
256255
error = resp.get('error')
257256
if error:
258257
raise APIError(error)

hathor/merged_mining/coordinator.py

+52-19
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@
7171
# PROPAGATION_FAILED = {'code': 33, 'message': 'Solution propagation failed'}
7272
DUPLICATE_SOLUTION = {'code': 34, 'message': 'Solution already submitted'}
7373

74+
ZEROED_4: bytes = b'\0' * 4
75+
ZEROED_32: bytes = b'\0' * 32
76+
7477

7578
class HathorCoordJob(NamedTuple):
7679
""" Data class used to send a job's work to Hathor Stratum.
@@ -690,6 +693,8 @@ async def submit_to_bitcoin(self, job: SingleMinerJob, work: SingleMinerWork) ->
690693
""" Submit work to Bitcoin RPC.
691694
"""
692695
bitcoin_rpc = self.coordinator.bitcoin_rpc
696+
if bitcoin_rpc is None:
697+
return
693698
# bitcoin_block = job.build_bitcoin_block(work) # XXX: too expensive for now
694699
bitcoin_block_header = job.build_bitcoin_block_header(work)
695700
block_hash = Hash(bitcoin_block_header.hash)
@@ -799,6 +804,27 @@ class BitcoinCoordJob(NamedTuple):
799804
witness_commitment: Optional[bytes] = None
800805
append_to_input: bool = True
801806

807+
@classmethod
808+
def zeroed(cls, merkle_path_len: int = 0) -> 'BitcoinCoordJob':
809+
if merkle_path_len > 0:
810+
transactions = [BitcoinRawTransaction(ZEROED_32, ZEROED_32, b'')] * (2 ** merkle_path_len - 1)
811+
merkle_path = tuple(build_merkle_path_for_coinbase([t.txid for t in transactions]))
812+
else:
813+
transactions = []
814+
merkle_path = tuple()
815+
return cls(
816+
version=0,
817+
previous_block_hash=ZEROED_32,
818+
coinbase_value=0,
819+
target=ZEROED_32,
820+
min_time=0,
821+
size_limit=0,
822+
bits=ZEROED_4,
823+
height=0,
824+
transactions=transactions,
825+
merkle_path=merkle_path,
826+
)
827+
802828
@classmethod
803829
def from_dict(cls, params: dict) -> 'BitcoinCoordJob':
804830
r""" Convert from dict of the properties returned from Bitcoin RPC.
@@ -970,7 +996,7 @@ def make_coinbase_transaction(self, hathor_block_hash: bytes, payback_script_bit
970996
if self.witness_commitment is not None:
971997
segwit_output = BitcoinTransactionOutput(0, self.witness_commitment)
972998
outputs.append(segwit_output)
973-
coinbase_input.script_witness.append(b'\0' * 32)
999+
coinbase_input.script_witness.append(ZEROED_32)
9741000

9751001
# append now because segwit presence may change this
9761002
inputs.append(coinbase_input)
@@ -1112,10 +1138,11 @@ class MergedMiningCoordinator:
11121138
MAX_XNONCE1 = 2**XNONCE1_SIZE - 1
11131139
MAX_RECONNECT_BACKOFF = 30
11141140

1115-
def __init__(self, bitcoin_rpc: IBitcoinRPC, hathor_client: IHathorClient,
1116-
payback_address_bitcoin: Optional[str], payback_address_hathor: Optional[str],
1117-
address_from_login: bool = True, min_difficulty: Optional[int] = None,
1118-
sequential_xnonce1: bool = False, rng: Optional[Random] = None):
1141+
def __init__(self, bitcoin_rpc: IBitcoinRPC | None, hathor_client: IHathorClient,
1142+
payback_address_bitcoin: str | None, payback_address_hathor: str | None,
1143+
address_from_login: bool = True, min_difficulty: int | None = None,
1144+
sequential_xnonce1: bool = False, rng: Random | None = None,
1145+
dummy_merkle_path_len: int | None = None):
11191146
self.log = logger.new()
11201147
if rng is None:
11211148
rng = Random()
@@ -1146,6 +1173,7 @@ def __init__(self, bitcoin_rpc: IBitcoinRPC, hathor_client: IHathorClient,
11461173
self.started_at = 0.0
11471174
self.strip_all_transactions = False
11481175
self.strip_segwit_transactions = False
1176+
self.dummy_merkle_path_len = dummy_merkle_path_len or 0
11491177

11501178
@property
11511179
def uptime(self) -> float:
@@ -1187,21 +1215,24 @@ async def start(self) -> None:
11871215
"""
11881216
loop = asyncio.get_event_loop()
11891217
self.started_at = time.time()
1190-
self.update_bitcoin_block_task = loop.create_task(self.update_bitcoin_block())
1218+
if self.bitcoin_rpc is not None:
1219+
self.update_bitcoin_block_task = loop.create_task(self.update_bitcoin_block())
1220+
else:
1221+
self.bitcoin_coord_job = BitcoinCoordJob.zeroed(self.dummy_merkle_path_len)
11911222
self.update_hathor_block_task = loop.create_task(self.update_hathor_block())
11921223

11931224
async def stop(self) -> None:
11941225
""" Stops the client, interrupting mining processes, stoping supervisor loop, and sending finished jobs.
11951226
"""
1196-
assert self.update_bitcoin_block_task is not None
1197-
self.update_bitcoin_block_task.cancel()
1227+
finals = []
1228+
if self.update_bitcoin_block_task is not None:
1229+
self.update_bitcoin_block_task.cancel()
1230+
finals.append(self.update_bitcoin_block_task)
11981231
assert self.update_hathor_block_task is not None
11991232
self.update_hathor_block_task.cancel()
1233+
finals.append(self.update_hathor_block_task)
12001234
try:
1201-
await asyncio.gather(
1202-
self.update_bitcoin_block_task,
1203-
self.update_hathor_block_task,
1204-
)
1235+
await asyncio.gather(*finals)
12051236
except asyncio.CancelledError:
12061237
pass
12071238
except Exception:
@@ -1212,6 +1243,7 @@ async def stop(self) -> None:
12121243
async def update_bitcoin_block(self) -> None:
12131244
""" Task that continuously polls block templates from bitcoin.get_block_template
12141245
"""
1246+
assert self.bitcoin_rpc is not None
12151247
backoff = 1
12161248
longpoll_id = None
12171249
while True:
@@ -1350,13 +1382,14 @@ async def update_merged_block(self) -> None:
13501382
merkle_root = build_merkle_root(list(tx.txid for tx in block_proposal.transactions))
13511383
if merkle_root != block_proposal.header.merkle_root:
13521384
self.log.warn('bad merkle root', expected=merkle_root.hex(), got=block_proposal.header.merkle_root.hex())
1353-
error = await self.bitcoin_rpc.verify_block_proposal(block=bytes(block_proposal))
1354-
if error is not None:
1355-
self.log.warn('proposed block is invalid, skipping update', error=error)
1356-
else:
1357-
self.next_merged_job = merged_job
1358-
self.update_jobs()
1359-
self.log.debug('merged job updated')
1385+
if self.bitcoin_rpc is not None:
1386+
error = await self.bitcoin_rpc.verify_block_proposal(block=bytes(block_proposal))
1387+
if error is not None:
1388+
self.log.warn('proposed block is invalid, skipping update', error=error)
1389+
return
1390+
self.next_merged_job = merged_job
1391+
self.update_jobs()
1392+
self.log.debug('merged job updated')
13601393

13611394
def status(self) -> dict[Any, Any]:
13621395
""" Build status dict with useful metrics for use in MM Status API.

0 commit comments

Comments
 (0)