Skip to content

feat(mergedmining): support dummy mining on coordinator #966

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

Merged
merged 1 commit into from
Aug 14, 2024
Merged
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
25 changes: 20 additions & 5 deletions hathor/cli/merged_mining.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand All @@ -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}')
Expand Down Expand Up @@ -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')

Expand Down
3 changes: 1 addition & 2 deletions hathor/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
79 changes: 60 additions & 19 deletions hathor/merged_mining/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
Loading