diff --git a/jmclient/jmclient/blockchaininterface.py b/jmclient/jmclient/blockchaininterface.py index a01f55e..71e43ae 100644 --- a/jmclient/jmclient/blockchaininterface.py +++ b/jmclient/jmclient/blockchaininterface.py @@ -4,6 +4,7 @@ import random import sys import time from decimal import Decimal +import binascii from twisted.internet import reactor, task import jmbitcoin as btc @@ -406,6 +407,30 @@ class BitcoinCoreInterface(BlockchainInterface): except JsonRpcError: return self.rpc('getblock', [blockhash])['time'] + def get_tx_merkle_branch(self, txid, blockhash=None): + if not blockhash: + tx = self.rpc("gettransaction", [txid]) + if tx["confirmations"] < 1: + raise ValueError("Transaction not in block") + blockhash = tx["blockhash"] + try: + core_proof = self.rpc("gettxoutproof", [[txid], blockhash]) + except JsonRpcError: + raise ValueError("Block containing transaction is pruned") + return self.core_proof_to_merkle_branch(core_proof) + + def core_proof_to_merkle_branch(self, core_proof): + core_proof = binascii.unhexlify(core_proof) + #first 80 bytes of a proof given by core are just a block header + #so we can save space by replacing it with a 4-byte block height + return core_proof[80:] + + def verify_tx_merkle_branch(self, txid, block_height, merkle_branch): + block_hash = self.rpc("getblockhash", [block_height]) + core_proof = self.rpc("getblockheader", [block_hash, False]) + \ + binascii.hexlify(merkle_branch).decode() + ret = self.rpc("verifytxoutproof", [core_proof]) + return len(ret) == 1 and ret[0] == txid class RegtestBitcoinCoreMixin(): """ diff --git a/jmclient/jmclient/wallet_service.py b/jmclient/jmclient/wallet_service.py index c21fc70..0c92fb2 100644 --- a/jmclient/jmclient/wallet_service.py +++ b/jmclient/jmclient/wallet_service.py @@ -537,6 +537,46 @@ class WalletService(Service): jmprint(restart_msg, "important") sys.exit(EXIT_SUCCESS) + def sync_burner_outputs(self, burner_txes): + mixdepth = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH + address_type = FidelityBondMixin.BIP32_BURN_ID + self.wallet.set_next_index(mixdepth, address_type, self.wallet.gap_limit, + force=True) + highest_used_index = 0 + + known_burner_outputs = self.wallet.get_burner_outputs() + for index in range(self.wallet.gap_limit): + path = self.wallet.get_path(mixdepth, address_type, index) + path_privkey, engine = self.wallet._get_priv_from_path(path) + path_pubkey = engine.privkey_to_pubkey(path_privkey) + path_pubkeyhash = btc.bin_hash160(path_pubkey) + + for burner_tx in burner_txes: + burner_pubkeyhash, gettx = burner_tx + if burner_pubkeyhash != path_pubkeyhash: + continue + highest_used_index = index + path_repr = self.wallet.get_path_repr(path) + if path_repr.encode() in known_burner_outputs: + continue + txid = gettx["txid"] + jlog.info("Found a burner transaction txid=" + txid + " path = " + + path_repr) + try: + merkle_branch = self.bci.get_tx_merkle_branch(txid, gettx["blockhash"]) + except ValueError as e: + jlog.warning(repr(e)) + jlog.warning("Merkle branch likely not available, use " + + "wallet-tool `addtxoutproof`") + merkle_branch = None + block_height = self.bci.rpc("getblockheader", [gettx["blockhash"]])["height"] + if merkle_branch: + assert self.bci.verify_tx_merkle_branch(txid, block_height, merkle_branch) + self.wallet.add_burner_output(path_repr, gettx["hex"], block_height, + merkle_branch, gettx["blockindex"]) + + self.wallet.set_next_index(mixdepth, address_type, highest_used_index + 1) + def sync_addresses(self): """ Triggered by use of --recoversync option in scripts, attempts a full scan of the blockchain without assuming