def pegin_test(self, sighash_ty): # Peg-in prep: # Hack: since we're not validating peg-ins in parent chain, just make # both the funding and claim tx on same chain (printing money) fund_info = self.nodes[0].getpeginaddress() peg_id = self.nodes[0].sendtoaddress(fund_info["mainchain_address"], 1) raw_peg_tx = self.nodes[0].gettransaction(peg_id)["hex"] peg_txid = self.nodes[0].sendrawtransaction(raw_peg_tx) self.nodes[0].generate(101) peg_prf = self.nodes[0].gettxoutproof([peg_txid]) claim_script = fund_info["claim_script"] # Create a pegin transaction # We have to manually supply claim script, otherwise the wallet will pick raw_claim = self.nodes[0].createrawpegin(raw_peg_tx, peg_prf, claim_script) raw_claim = FromHex(CTransaction(), raw_claim['hex']) # Create a taproot utxo tx, prev_vout, spk, sec, pub, tweak = self.create_taproot_utxo() # Spend the pegin and taproot tx together raw_claim.vin.append(CTxIn(COutPoint(tx.sha256, prev_vout))) raw_claim.vout.append( CTxOut(nValue=CTxOutValue(12 * 10**7), scriptPubKey=spk)) # send back to self signed = self.nodes[0].signrawtransactionwithwallet( raw_claim.serialize().hex()) raw_claim = FromHex(CTransaction(), signed['hex']) genesis_hash = uint256_from_str( bytes.fromhex(self.nodes[0].getblockhash(0))[::-1]) peg_utxo = CTxOut() peg_utxo.from_pegin_witness_data( raw_claim.wit.vtxinwit[0].peginWitness) msg = TaprootSignatureHash(raw_claim, [peg_utxo, tx.vout[prev_vout]], sighash_ty, genesis_hash, 1) # compute the tweak tweak_sk = tweak_add_privkey(sec, tweak) sig = sign_schnorr(tweak_sk, msg) raw_claim.wit.vtxinwit[1].scriptWitness.stack = [ taproot_pad_sighash_ty(sig, sighash_ty) ] pub_tweak = tweak_add_pubkey(pub, tweak)[0] assert (verify_schnorr(pub_tweak, sig, msg)) # Since we add in/outputs the min feerate is no longer maintained. self.nodes[0].sendrawtransaction(hexstring=raw_claim.serialize().hex()) self.nodes[0].generate(1) last_blk = self.nodes[0].getblock(self.nodes[0].getbestblockhash()) raw_claim.rehash() assert (raw_claim.hash in last_blk['tx'])
def issuance_test(self, sighash_ty): tx, prev_vout, spk, sec, pub, tweak = self.create_taproot_utxo() blind_addr = self.nodes[0].getnewaddress() nonblind_addr = self.nodes[0].validateaddress( blind_addr)['unconfidential'] raw_tx = self.nodes[0].createrawtransaction([], [{nonblind_addr: 1}]) raw_tx = FromHex(CTransaction(), raw_tx) # Need to taproot outputs later because fundrawtransaction cannot estimate fees # prev out has value 1.2 btc in_total = tx.vout[prev_vout].nValue.getAmount() fees = 100 raw_tx.vin.append(CTxIn(COutPoint(tx.sha256, prev_vout))) raw_tx.vout.append( CTxOut(nValue=CTxOutValue(in_total - fees - 10**8), scriptPubKey=spk)) # send back to self raw_tx.vout.append(CTxOut(nValue=CTxOutValue(fees))) # issued_tx = raw_tx.serialize().hex() blind_addr = self.nodes[0].getnewaddress() issue_addr = self.nodes[0].validateaddress( blind_addr)['unconfidential'] issued_tx = self.nodes[0].rawissueasset( raw_tx.serialize().hex(), [{ "asset_amount": 2, "asset_address": issue_addr, "blind": False }])[0]["hex"] # blind_tx = self.nodes[0].blindrawtransaction(issued_tx) # This is a no-op genesis_hash = uint256_from_str( bytes.fromhex(self.nodes[0].getblockhash(0))[::-1]) issued_tx = FromHex(CTransaction(), issued_tx) issued_tx.wit.vtxoutwit = [CTxOutWitness()] * len(issued_tx.vout) issued_tx.wit.vtxinwit = [CTxInWitness()] * len(issued_tx.vin) msg = TaprootSignatureHash(issued_tx, [tx.vout[prev_vout]], sighash_ty, genesis_hash, 0) # compute the tweak tweak_sk = tweak_add_privkey(sec, tweak) sig = sign_schnorr(tweak_sk, msg) issued_tx.wit.vtxinwit[0].scriptWitness.stack = [ taproot_pad_sighash_ty(sig, sighash_ty) ] pub_tweak = tweak_add_pubkey(pub, tweak)[0] assert (verify_schnorr(pub_tweak, sig, msg)) # Since we add in/outputs the min feerate is no longer maintained. self.nodes[0].sendrawtransaction(hexstring=issued_tx.serialize().hex()) self.nodes[0].generate(1) last_blk = self.nodes[0].getblock(self.nodes[0].getbestblockhash()) issued_tx.rehash() assert (issued_tx.hash in last_blk['tx'])
def run_test(self): node = self.nodes[0] address = node.get_deterministic_priv_key().address # Generate generate some blocks and mature the coinbases node.generatetoaddress(101, address) last_generated_block = node.getbestblockhash() txids = [ node.sendtoaddress(node.getnewaddress(), 1) for x in range(NUM_TRANSACTIONS) ] response = node.getrawunsolvedblock(address) block = FromHex(CBlock(), response['blockhex']) # coinbase and the 3 transactions we sent assert_equal(block.hashMerkleRoot, block.calc_merkle_root()) # Should have all the transactions, plus the coinbase tx assert_equal(len(block.vtx), NUM_TRANSACTIONS + 1) # Skip coinbase. Transactions should be sorted by Id txids.sort() for idx in range(NUM_TRANSACTIONS): # All transactions found and in order assert_equal(txids[idx], block.vtx[idx + 1].txid_hex) block.solve() node.submitblock(block.serialize().hex()) response = node.getbestblockhash() assert_equal(response, block.hash) # Ensure the transactions are confirmed and in the wallet txns = node.listsinceblock(last_generated_block)['transactions'] for idx in range(NUM_TRANSACTIONS): assert_array_result(txns, {'txid': txids[idx]}, { 'blockhash': block.hash, 'confirmations': 1 })
def create_test_block(self, version=536870912): self.bump_mocktime(5) bt = self.nodes[0].getblocktemplate() tip = int(bt['previousblockhash'], 16) nextheight = bt['height'] coinbase = create_coinbase(nextheight) coinbase.nVersion = 3 coinbase.nType = 5 # CbTx coinbase.vout[0].nValue = bt['coinbasevalue'] for mn in bt['masternode']: coinbase.vout.append( CTxOut(mn['amount'], CScript(hex_str_to_bytes(mn['script'])))) coinbase.vout[0].nValue -= mn['amount'] cbtx = FromHex(CCbTx(), bt['coinbase_payload']) coinbase.vExtraPayload = cbtx.serialize() coinbase.rehash() coinbase.calc_sha256() block = create_block(tip, coinbase, self.mocktime) block.nVersion = version # Add quorum commitments from template for tx in bt['transactions']: tx2 = FromHex(CTransaction(), tx['data']) if tx2.nType == 6: block.vtx.append(tx2) block.hashMerkleRoot = block.calc_merkle_root() block.rehash() block.solve() return block
def create_taproot_utxo(self, scripts=None, blind=False): # modify the transaction to add one output that should spend previous taproot # Create a taproot prevout addr = self.nodes[0].getnewaddress() sec = generate_privkey() pub = compute_xonly_pubkey(sec)[0] tap = taproot_construct(pub, scripts) spk = tap.scriptPubKey # No need to test blinding in this unit test unconf_addr = self.nodes[0].getaddressinfo(addr)['unconfidential'] raw_tx = self.nodes[0].createrawtransaction([], [{unconf_addr: 1.2}]) # edit spk directly, no way to get new address. # would need to implement bech32m in python tx = FromHex(CTransaction(), raw_tx) tx.vout[0].scriptPubKey = spk tx.vout[0].nValue = CTxOutValue(12 * 10**7) raw_hex = tx.serialize().hex() fund_tx = self.nodes[0].fundrawtransaction( raw_hex, False, )["hex"] fund_tx = FromHex(CTransaction(), fund_tx) # Createrawtransaction might rearrage txouts prev_vout = None for i, out in enumerate(fund_tx.vout): if spk == out.scriptPubKey: prev_vout = i tx = self.nodes[0].blindrawtransaction(fund_tx.serialize().hex()) signed_raw_tx = self.nodes[0].signrawtransactionwithwallet(tx) _txid = self.nodes[0].sendrawtransaction(signed_raw_tx['hex']) tx = FromHex(CTransaction(), signed_raw_tx['hex']) tx.rehash() self.nodes[0].generate(1) last_blk = self.nodes[0].getblock(self.nodes[0].getbestblockhash()) assert (tx.hash in last_blk['tx']) return tx, prev_vout, spk, sec, pub, tap
def run_test(self): parent = self.nodes[0] #parent2 = self.nodes[1] sidechain = self.nodes[2] sidechain2 = self.nodes[3] # If we're testing post-transition, force a fedpegscript transition and # getting rid of old fedpegscript by making at least another epoch pass by WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"] # We just randomize the keys a bit to get another valid fedpegscript new_fedpegscript = sidechain.tweakfedpegscript("f00dbabe")["script"] if self.options.post_transition: print("Running test post-transition") for _ in range(30): block_hex = sidechain.getnewblockhex( 0, { "signblockscript": WSH_OP_TRUE, "max_block_witness": 10, "fedpegscript": new_fedpegscript, "extension_space": [] }) sidechain.submitblock(block_hex) assert_equal(sidechain.getsidechaininfo()["current_fedpegscripts"], [new_fedpegscript] * 2) if self.options.pre_transition: print( "Running test pre-transition, dynafed activated from first block" ) for node in self.nodes: node.importprivkey(privkey=node.get_deterministic_priv_key().key, label="mining") util.node_fastmerkle = sidechain parent.generate(101) sidechain.generate(101) self.log.info("sidechain info: {}".format( sidechain.getsidechaininfo())) addrs = sidechain.getpeginaddress() addr = addrs["mainchain_address"] assert_equal( sidechain.decodescript(addrs["claim_script"])["type"], "witness_v0_keyhash") txid1 = parent.sendtoaddress(addr, 24) vout = find_vout_for_address(parent, txid1, addr) # 10+2 confirms required to get into mempool and confirm assert_equal(sidechain.getsidechaininfo()["pegin_confirmation_depth"], 10) parent.generate(1) time.sleep(2) proof = parent.gettxoutproof([txid1]) raw = parent.gettransaction(txid1)["hex"] # Create a wallet in order to test that multi-wallet support works correctly for claimpegin # (Regression test for https://github.com/ElementsProject/elements/issues/812 .) sidechain.createwallet("throwaway") # Set up our sidechain RPCs to use the first wallet (with empty name). We do this by # overriding the RPC object in a hacky way, to avoid breaking a different hack on TestNode # that enables generate() to work despite the deprecation of the generate RPC. sidechain.rpc = sidechain.get_wallet_rpc("") print("Attempting peg-ins") # First attempt fails the consensus check but gives useful result try: pegtxid = sidechain.claimpegin(raw, proof) raise Exception( "Peg-in should not be mature enough yet, need another block.") except JSONRPCException as e: assert ( "Peg-in Bitcoin transaction needs more confirmations to be sent." in e.error["message"]) # Second attempt simply doesn't hit mempool bar parent.generate(10) try: pegtxid = sidechain.claimpegin(raw, proof) raise Exception( "Peg-in should not be mature enough yet, need another block.") except JSONRPCException as e: assert ( "Peg-in Bitcoin transaction needs more confirmations to be sent." in e.error["message"]) try: pegtxid = sidechain.createrawpegin(raw, proof, 'AEIOU') raise Exception("Peg-in with non-hex claim_script should fail.") except JSONRPCException as e: assert ("Given claim_script is not hex." in e.error["message"]) # Should fail due to non-matching wallet address try: scriptpubkey = sidechain.getaddressinfo( get_new_unconfidential_address(sidechain))["scriptPubKey"] pegtxid = sidechain.claimpegin(raw, proof, scriptpubkey) raise Exception( "Peg-in with non-matching claim_script should fail.") except JSONRPCException as e: assert ( "Given claim_script does not match the given Bitcoin transaction." in e.error["message"]) # 12 confirms allows in mempool parent.generate(1) # Make sure that a tx with a duplicate pegin claim input gets rejected. raw_pegin = sidechain.createrawpegin(raw, proof)["hex"] raw_pegin = FromHex(CTransaction(), raw_pegin) raw_pegin.vin.append(raw_pegin.vin[0]) # duplicate the pegin input raw_pegin = sidechain.signrawtransactionwithwallet( bytes_to_hex_str(raw_pegin.serialize()))["hex"] assert_raises_rpc_error(-26, "bad-txns-inputs-duplicate", sidechain.sendrawtransaction, raw_pegin) # Also try including this tx in a block manually and submitting it. doublespendblock = FromHex(CBlock(), sidechain.getnewblockhex()) doublespendblock.vtx.append(FromHex(CTransaction(), raw_pegin)) doublespendblock.hashMerkleRoot = doublespendblock.calc_merkle_root() add_witness_commitment(doublespendblock) doublespendblock.solve() block_hex = bytes_to_hex_str(doublespendblock.serialize(True)) assert_raises_rpc_error(-25, "bad-txns-inputs-duplicate", sidechain.testproposedblock, block_hex, True) # Should succeed via wallet lookup for address match, and when given raw_pegin = sidechain.createrawpegin(raw, proof)['hex'] signed_pegin = sidechain.signrawtransactionwithwallet(raw_pegin) # Find the address that the peg-in used outputs = [] for pegin_vout in sidechain.decoderawtransaction(raw_pegin)['vout']: if pegin_vout['scriptPubKey']['type'] == 'witness_v0_keyhash': outputs.append({ pegin_vout['scriptPubKey']['addresses'][0]: pegin_vout['value'] }) elif pegin_vout['scriptPubKey']['type'] == 'fee': outputs.append({"fee": pegin_vout['value']}) # Check the createrawtransaction makes the same unsigned peg-in transaction raw_pegin2 = sidechain.createrawtransaction( [{ "txid": txid1, "vout": vout, "pegin_bitcoin_tx": raw, "pegin_txout_proof": proof, "pegin_claim_script": addrs["claim_script"] }], outputs) assert_equal(raw_pegin, raw_pegin2) # Check that createpsbt makes the correct unsigned peg-in pegin_psbt = sidechain.createpsbt( [{ "txid": txid1, "vout": vout, "pegin_bitcoin_tx": raw, "pegin_txout_proof": proof, "pegin_claim_script": addrs["claim_script"] }], outputs) decoded_psbt = sidechain.decodepsbt(pegin_psbt) # Check that pegin_bitcoin_tx == raw, but due to stripping witnesses, we need to compare their txids txid1 = parent.decoderawtransaction( decoded_psbt['inputs'][0]['pegin_bitcoin_tx'])['txid'] txid2 = parent.decoderawtransaction(raw)['txid'] assert_equal(txid1, txid2) # Check the rest assert_equal(decoded_psbt['inputs'][0]['pegin_claim_script'], addrs["claim_script"]) assert_equal(decoded_psbt['inputs'][0]['pegin_txout_proof'], proof) assert_equal(decoded_psbt['inputs'][0]['pegin_genesis_hash'], parent.getblockhash(0)) # Make a psbt without those peg-in data and merge them merge_pegin_psbt = sidechain.createpsbt([{ "txid": txid1, "vout": vout }], outputs) decoded_psbt = sidechain.decodepsbt(merge_pegin_psbt) assert 'pegin_bitcoin_tx' not in decoded_psbt['inputs'][0] assert 'pegin_claim_script' not in decoded_psbt['inputs'][0] assert 'pegin_txout_proof' not in decoded_psbt['inputs'][0] assert 'pegin_genesis_hash' not in decoded_psbt['inputs'][0] merged_pegin_psbt = sidechain.combinepsbt( [pegin_psbt, merge_pegin_psbt]) assert_equal(pegin_psbt, merged_pegin_psbt) # Now sign the psbt signed_psbt = sidechain.walletsignpsbt(pegin_psbt) # Finalize and extract and compare fin_psbt = sidechain.finalizepsbt(signed_psbt['psbt']) assert_equal(fin_psbt, signed_pegin) # Try funding a psbt with the peg-in assert_equal(sidechain.getbalance()['bitcoin'], 50) out_bal = 0 outputs.append({sidechain.getnewaddress(): 49.999}) for out in outputs: for val in out.values(): out_bal += Decimal(val) assert_greater_than(out_bal, 50) pegin_psbt = sidechain.walletcreatefundedpsbt( [{ "txid": txid1, "vout": vout, "pegin_bitcoin_tx": raw, "pegin_txout_proof": proof, "pegin_claim_script": addrs["claim_script"] }], outputs) signed_psbt = sidechain.walletsignpsbt(pegin_psbt['psbt']) fin_psbt = sidechain.finalizepsbt(signed_psbt['psbt']) assert fin_psbt['complete'] sample_pegin_struct = FromHex(CTransaction(), signed_pegin["hex"]) # Round-trip peg-in transaction using python serialization assert_equal(signed_pegin["hex"], bytes_to_hex_str(sample_pegin_struct.serialize())) # Store this for later (evil laugh) sample_pegin_witness = sample_pegin_struct.wit.vtxinwit[0].peginWitness pegtxid1 = sidechain.claimpegin(raw, proof) # Make sure a second pegin claim does not get accepted in the mempool when # another mempool tx already claims that pegin. assert_raises_rpc_error(-4, "txn-mempool-conflict", sidechain.claimpegin, raw, proof) # Will invalidate the block that confirms this transaction later self.sync_all(self.node_groups) blockhash = sidechain2.generate(1) self.sync_all(self.node_groups) sidechain.generate(5) tx1 = sidechain.gettransaction(pegtxid1) if "confirmations" in tx1 and tx1["confirmations"] == 6: print("Peg-in is confirmed: Success!") else: raise Exception("Peg-in confirmation has failed.") # Look at pegin fields decoded = sidechain.decoderawtransaction(tx1["hex"]) assert decoded["vin"][0]["is_pegin"] == True assert len(decoded["vin"][0]["pegin_witness"]) > 0 # Check that there's sufficient fee for the peg-in vsize = decoded["vsize"] fee_output = decoded["vout"][1] fallbackfee_pervbyte = Decimal("0.00001") / Decimal("1000") assert fee_output["scriptPubKey"]["type"] == "fee" assert fee_output["value"] >= fallbackfee_pervbyte * vsize # Quick reorg checks of pegs sidechain.invalidateblock(blockhash[0]) if sidechain.gettransaction(pegtxid1)["confirmations"] != 0: raise Exception( "Peg-in didn't unconfirm after invalidateblock call.") # Re-org causes peg-ins to get booted(wallet will resubmit in 10 minutes) assert_equal(sidechain.getrawmempool(), []) sidechain.sendrawtransaction(tx1["hex"]) # Create duplicate claim, put it in block along with current one in mempool # to test duplicate-in-block claims between two txs that are in the same block. raw_pegin = sidechain.createrawpegin(raw, proof)["hex"] raw_pegin = sidechain.signrawtransactionwithwallet(raw_pegin)["hex"] raw_pegin = FromHex(CTransaction(), raw_pegin) doublespendblock = FromHex(CBlock(), sidechain.getnewblockhex()) assert (len(doublespendblock.vtx) == 2) # coinbase and pegin doublespendblock.vtx.append(raw_pegin) doublespendblock.hashMerkleRoot = doublespendblock.calc_merkle_root() add_witness_commitment(doublespendblock) doublespendblock.solve() block_hex = bytes_to_hex_str(doublespendblock.serialize(True)) assert_raises_rpc_error(-25, "bad-txns-double-pegin", sidechain.testproposedblock, block_hex, True) # Re-enters block sidechain.generate(1) if sidechain.gettransaction(pegtxid1)["confirmations"] != 1: raise Exception("Peg-in should have one confirm on side block.") sidechain.reconsiderblock(blockhash[0]) if sidechain.gettransaction(pegtxid1)["confirmations"] != 6: raise Exception("Peg-in should be back to 6 confirms.") # Now the pegin is already claimed in a confirmed tx. # In that case, a duplicate claim should (1) not be accepted in the mempool # and (2) not be accepted in a block. assert_raises_rpc_error(-4, "pegin-already-claimed", sidechain.claimpegin, raw, proof) # For case (2), manually craft a block and include the tx. doublespendblock = FromHex(CBlock(), sidechain.getnewblockhex()) doublespendblock.vtx.append(raw_pegin) doublespendblock.hashMerkleRoot = doublespendblock.calc_merkle_root() add_witness_commitment(doublespendblock) doublespendblock.solve() block_hex = bytes_to_hex_str(doublespendblock.serialize(True)) assert_raises_rpc_error(-25, "bad-txns-double-pegin", sidechain.testproposedblock, block_hex, True) # Do multiple claims in mempool n_claims = 6 print("Flooding mempool with a few claims") pegtxs = [] sidechain.generate(101) # Do mixture of raw peg-in and automatic peg-in tx construction # where raw creation is done on another node for i in range(n_claims): addrs = sidechain.getpeginaddress() txid = parent.sendtoaddress(addrs["mainchain_address"], 1) parent.generate(1) proof = parent.gettxoutproof([txid]) raw = parent.gettransaction(txid)["hex"] if i % 2 == 0: parent.generate(11) pegtxs += [sidechain.claimpegin(raw, proof)] else: # The raw API doesn't check for the additional 2 confirmation buffer # So we only get 10 confirms then send off. Miners will add to block anyways. # Don't mature whole way yet to test signing immature peg-in input parent.generate(8) # Wallet in sidechain2 gets funds instead of sidechain raw_pegin = sidechain2.createrawpegin( raw, proof, addrs["claim_script"])["hex"] # First node should also be able to make a valid transaction with or without 3rd arg # since this wallet originated the claim_script itself sidechain.createrawpegin(raw, proof, addrs["claim_script"]) sidechain.createrawpegin(raw, proof) signed_pegin = sidechain.signrawtransactionwithwallet( raw_pegin) assert (signed_pegin["complete"]) assert ("warning" in signed_pegin) # warning for immature peg-in # fully mature them now parent.generate(1) pegtxs += [sidechain.sendrawtransaction(signed_pegin["hex"])] self.sync_all(self.node_groups) sidechain2.generate(1) for i, pegtxid in enumerate(pegtxs): if i % 2 == 0: tx = sidechain.gettransaction(pegtxid) else: tx = sidechain2.gettransaction(pegtxid) if "confirmations" not in tx or tx["confirmations"] == 0: raise Exception("Peg-in confirmation has failed.") print("Test pegouts") self.test_pegout(get_new_unconfidential_address(parent, "legacy"), sidechain) self.test_pegout(get_new_unconfidential_address(parent, "p2sh-segwit"), sidechain) self.test_pegout(get_new_unconfidential_address(parent, "bech32"), sidechain) print("Test pegout P2SH") parent_chain_addr = get_new_unconfidential_address(parent) parent_pubkey = parent.getaddressinfo(parent_chain_addr)["pubkey"] parent_chain_p2sh_addr = parent.createmultisig( 1, [parent_pubkey])["address"] self.test_pegout(parent_chain_p2sh_addr, sidechain) print("Test pegout Garbage") parent_chain_addr = "garbage" try: self.test_pegout(parent_chain_addr, sidechain) raise Exception("A garbage address should fail.") except JSONRPCException as e: assert ("Invalid Bitcoin address" in e.error["message"]) print("Test pegout Garbage valid") prev_txid = sidechain.sendtoaddress(sidechain.getnewaddress(), 1) sidechain.generate(1) pegout_chain = 'a' * 64 pegout_hex = 'b' * 500 inputs = [{"txid": prev_txid, "vout": 0}] outputs = {"vdata": [pegout_chain, pegout_hex]} rawtx = sidechain.createrawtransaction(inputs, outputs) raw_pegout = sidechain.decoderawtransaction(rawtx) assert 'vout' in raw_pegout and len(raw_pegout['vout']) > 0 pegout_tested = False for output in raw_pegout['vout']: scriptPubKey = output['scriptPubKey'] if 'type' in scriptPubKey and scriptPubKey['type'] == 'nulldata': assert ('pegout_hex' in scriptPubKey and 'pegout_asm' in scriptPubKey and 'pegout_type' in scriptPubKey) assert ('pegout_chain' in scriptPubKey and 'pegout_reqSigs' not in scriptPubKey and 'pegout_addresses' not in scriptPubKey) assert scriptPubKey['pegout_type'] == 'nonstandard' assert scriptPubKey['pegout_chain'] == pegout_chain assert scriptPubKey['pegout_hex'] == pegout_hex pegout_tested = True break assert pegout_tested print( "Now test failure to validate peg-ins based on intermittent bitcoind rpc failure" ) self.stop_node(1) txid = parent.sendtoaddress(addr, 1) parent.generate(12) proof = parent.gettxoutproof([txid]) raw = parent.gettransaction(txid)["hex"] sidechain.claimpegin(raw, proof) # stuck peg sidechain.generate(1) print("Waiting to ensure block is being rejected by sidechain2") time.sleep(5) assert (sidechain.getblockcount() != sidechain2.getblockcount()) print("Restarting parent2") self.start_node(1) connect_nodes_bi(self.nodes, 0, 1) # Don't make a block, race condition when pegin-invalid block # is awaiting further validation, nodes reject subsequent blocks # even ones they create print( "Now waiting for node to re-evaluate peg-in witness failed block... should take a few seconds" ) self.sync_all(self.node_groups) print("Completed!\n") print("Now send funds out in two stages, partial, and full") some_btc_addr = get_new_unconfidential_address(parent) bal_1 = sidechain.getwalletinfo()["balance"]['bitcoin'] try: sidechain.sendtomainchain(some_btc_addr, bal_1 + 1) raise Exception("Sending out too much; should have failed") except JSONRPCException as e: assert ("Insufficient funds" in e.error["message"]) assert (sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) try: sidechain.sendtomainchain(some_btc_addr + "b", bal_1 - 1) raise Exception("Sending to invalid address; should have failed") except JSONRPCException as e: assert ("Invalid Bitcoin address" in e.error["message"]) assert (sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) try: sidechain.sendtomainchain("1Nro9WkpaKm9axmcfPVp79dAJU1Gx7VmMZ", bal_1 - 1) raise Exception( "Sending to mainchain address when should have been testnet; should have failed" ) except JSONRPCException as e: assert ("Invalid Bitcoin address" in e.error["message"]) assert (sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) # Test superfluous peg-in witness data on regular spend before we have no funds raw_spend = sidechain.createrawtransaction( [], {sidechain.getnewaddress(): 1}) fund_spend = sidechain.fundrawtransaction(raw_spend) sign_spend = sidechain.signrawtransactionwithwallet(fund_spend["hex"]) signed_struct = FromHex(CTransaction(), sign_spend["hex"]) # Non-witness tx has no witness serialized yet if len(signed_struct.wit.vtxinwit) == 0: signed_struct.wit.vtxinwit = [CTxInWitness()] signed_struct.wit.vtxinwit[ 0].peginWitness.stack = sample_pegin_witness.stack assert_equal( sidechain.testmempoolaccept( [bytes_to_hex_str(signed_struct.serialize())])[0]["allowed"], False) assert_equal( sidechain.testmempoolaccept([ bytes_to_hex_str(signed_struct.serialize()) ])[0]["reject-reason"], "68: extra-pegin-witness") signed_struct.wit.vtxinwit[0].peginWitness.stack = [b'\x00' * 100000 ] # lol assert_equal( sidechain.testmempoolaccept( [bytes_to_hex_str(signed_struct.serialize())])[0]["allowed"], False) assert_equal( sidechain.testmempoolaccept([ bytes_to_hex_str(signed_struct.serialize()) ])[0]["reject-reason"], "68: extra-pegin-witness") peg_out_txid = sidechain.sendtomainchain(some_btc_addr, 1) peg_out_details = sidechain.decoderawtransaction( sidechain.getrawtransaction(peg_out_txid)) # peg-out, change, fee assert (len(peg_out_details["vout"]) == 3) found_pegout_value = False for output in peg_out_details["vout"]: if "value" in output and output["value"] == 1: found_pegout_value = True assert (found_pegout_value) bal_2 = sidechain.getwalletinfo()["balance"]["bitcoin"] # Make sure balance went down assert (bal_2 + 1 < bal_1) # Send rest of coins using subtractfee from output arg sidechain.sendtomainchain(some_btc_addr, bal_2, True) assert (sidechain.getwalletinfo()["balance"]['bitcoin'] == 0) print('Test coinbase peg-in maturity rules') # Have bitcoin output go directly into a claim output pegin_info = sidechain.getpeginaddress() mainchain_addr = pegin_info["mainchain_address"] # Watch the address so we can get tx without txindex parent.importaddress(mainchain_addr) claim_block = parent.generatetoaddress(50, mainchain_addr)[0] self.sync_all(self.node_groups) block_coinbase = parent.getblock(claim_block, 2)["tx"][0] claim_txid = block_coinbase["txid"] claim_tx = block_coinbase["hex"] claim_proof = parent.gettxoutproof([claim_txid], claim_block) # Can't claim something even though it has 50 confirms since it's coinbase assert_raises_rpc_error( -8, "Peg-in Bitcoin transaction needs more confirmations to be sent.", sidechain.claimpegin, claim_tx, claim_proof) # If done via raw API, still doesn't work coinbase_pegin = sidechain.createrawpegin(claim_tx, claim_proof) assert_equal(coinbase_pegin["mature"], False) signed_pegin = sidechain.signrawtransactionwithwallet( coinbase_pegin["hex"])["hex"] assert_raises_rpc_error( -26, "bad-pegin-witness, Needs more confirmations.", sidechain.sendrawtransaction, signed_pegin) # 50 more blocks to allow wallet to make it succeed by relay and consensus parent.generatetoaddress(50, parent.getnewaddress()) self.sync_all(self.node_groups) # Wallet still doesn't want to for 2 more confirms assert_equal( sidechain.createrawpegin(claim_tx, claim_proof)["mature"], False) # But we can just shoot it off claim_txid = sidechain.sendrawtransaction(signed_pegin) sidechain.generatetoaddress(1, sidechain.getnewaddress()) self.sync_all(self.node_groups) assert_equal(sidechain.gettransaction(claim_txid)["confirmations"], 1) # Test a confidential pegin. print("Performing a confidential pegin.") # start pegin pegin_addrs = sidechain.getpeginaddress() assert_equal( sidechain.decodescript(pegin_addrs["claim_script"])["type"], "witness_v0_keyhash") pegin_addr = addrs["mainchain_address"] txid_fund = parent.sendtoaddress(pegin_addr, 10) # 10+2 confirms required to get into mempool and confirm parent.generate(11) self.sync_all(self.node_groups) proof = parent.gettxoutproof([txid_fund]) raw = parent.gettransaction(txid_fund)["hex"] raw_pegin = sidechain.createrawpegin(raw, proof)['hex'] pegin = FromHex(CTransaction(), raw_pegin) # add new blinding pubkey for the pegin output pegin.vout[0].nNonce = CTxOutNonce( hex_str_to_bytes( sidechain.getaddressinfo(sidechain.getnewaddress( "", "blech32"))["confidential_key"])) # now add an extra input and output from listunspent; we need a blinded output for this blind_addr = sidechain.getnewaddress("", "blech32") sidechain.sendtoaddress(blind_addr, 15) sidechain.generate(6) # Make sure sidechain2 knows about the same input self.sync_all(self.node_groups) unspent = [ u for u in sidechain.listunspent(6, 6) if u["amount"] == 15 ][0] assert (unspent["spendable"]) assert ("amountcommitment" in unspent) pegin.vin.append( CTxIn(COutPoint(int(unspent["txid"], 16), unspent["vout"]))) # insert corresponding output before fee output new_destination = sidechain.getaddressinfo( sidechain.getnewaddress("", "blech32")) new_dest_script_pk = hex_str_to_bytes(new_destination["scriptPubKey"]) new_dest_nonce = CTxOutNonce( hex_str_to_bytes(new_destination["confidential_key"])) new_dest_asset = pegin.vout[0].nAsset pegin.vout.insert( 1, CTxOut( int(unspent["amount"] * COIN) - 10000, new_dest_script_pk, new_dest_asset, new_dest_nonce)) # add the 10 ksat fee pegin.vout[2].nValue.setToAmount(pegin.vout[2].nValue.getAmount() + 10000) pegin_hex = ToHex(pegin) # test with both blindraw and rawblindraw raw_pegin_blinded1 = sidechain.blindrawtransaction(pegin_hex) raw_pegin_blinded2 = sidechain.rawblindrawtransaction( pegin_hex, ["", unspent["amountblinder"]], [10, 15], [unspent["asset"]] * 2, ["", unspent["assetblinder"]], "", False) pegin_signed1 = sidechain.signrawtransactionwithwallet( raw_pegin_blinded1) pegin_signed2 = sidechain.signrawtransactionwithwallet( raw_pegin_blinded2) for pegin_signed in [pegin_signed1, pegin_signed2]: final_decoded = sidechain.decoderawtransaction(pegin_signed["hex"]) assert (final_decoded["vin"][0]["is_pegin"]) assert (not final_decoded["vin"][1]["is_pegin"]) assert ("assetcommitment" in final_decoded["vout"][0]) assert ("valuecommitment" in final_decoded["vout"][0]) assert ("commitmentnonce" in final_decoded["vout"][0]) assert ("value" not in final_decoded["vout"][0]) assert ("asset" not in final_decoded["vout"][0]) assert (final_decoded["vout"][0]["commitmentnonce_fully_valid"]) assert ("assetcommitment" in final_decoded["vout"][1]) assert ("valuecommitment" in final_decoded["vout"][1]) assert ("commitmentnonce" in final_decoded["vout"][1]) assert ("value" not in final_decoded["vout"][1]) assert ("asset" not in final_decoded["vout"][1]) assert (final_decoded["vout"][1]["commitmentnonce_fully_valid"]) assert ("value" in final_decoded["vout"][2]) assert ("asset" in final_decoded["vout"][2]) # check that it is accepted in either mempool accepted = sidechain.testmempoolaccept([pegin_signed["hex"]])[0] if not accepted["allowed"]: raise Exception(accepted["reject-reason"]) accepted = sidechain2.testmempoolaccept([pegin_signed["hex"]])[0] if not accepted["allowed"]: raise Exception(accepted["reject-reason"]) print("Blinded transaction looks ok!" ) # need this print to distinguish failures in for loop print('Success!') # Manually stop sidechains first, then the parent chains. self.stop_node(2) self.stop_node(3) self.stop_node(0) self.stop_node(1)
def run_test(self): (node, ) = self.nodes node.add_p2p_connection(P2PDataStore()) tip = self.getbestblock(node) self.log.info("Create some blocks with OP_1 coinbase for spending.") blocks = [] for _ in range(600): tip = self.build_block(tip) blocks.append(tip) node.p2p.send_blocks_and_test(blocks, node, success=True) self.spendable_outputs = deque(block.vtx[0] for block in blocks) self.log.info( "Mature the blocks and get out of IBD; also make some coinbases for the node wallet." ) node.generate(120) tip = self.getbestblock(node) self.log.info( 'PART 1: testing of priority under congestion (mempool eviction priority and mining priority).' ) self.log.info("Put a regular transaction in mempool at 1 sat/byte.") txid1 = node.sendtoaddress(node.getnewaddress(), 0.001) self.log.info( "Mempool accepts 1 sat/byte txes even with very many sigops") script_4000_sigops = bytes([OP_CHECKMULTISIG] * 200) # vsize = 200000, so 0.0015 sat/vbyte. ctx = create_var_transaction(self.spendable_outputs.popleft(), script_4000_sigops, 300, 300) txid_parent = node.sendrawtransaction(ToHex(ctx)) self.log.info( "Create a 1 sat/byte compensating child of a high-sigop parent") # package vsize = 200000, so 0.5015 sat/vbyte. ctx_child = create_var_transaction(ctx, b'', 100000, 100000) txid_child = node.sendrawtransaction(ToHex(ctx_child)) # make one with 270 sat/byte, but only 0.4 sat/vbyte. ctx = create_var_transaction(self.spendable_outputs.popleft(), script_4000_sigops, 300, 80000) txid_highsigops = node.sendrawtransaction(ToHex(ctx)) self.log.info( "Flood the mempool with 10-kB transactions @ 9.9 sat/byte and 0.495 sat/vbyte, until it is full" ) # About 500 should fill up our 5 MB mempool. # Note that even starting from an empty pool, somewhat fewer than 500 # would be accepted depending on mempool data structure sizes. for i in range(500): spendfrom = self.spendable_outputs.popleft() ctx = create_var_transaction(spendfrom, script_4000_sigops, 10000, 99000) try: node.sendrawtransaction(ToHex(ctx)) except JSONRPCException as e: if 'mempool full' in e.error[ 'message'] or 'mempool min fee not met' in e.error[ 'message']: self.log.info( "Mempool filled after {} transactions".format(i, )) break raise else: raise RuntimeError("didn't fill mempool") self.log.info( 'The flooding only caused eviction of lower low sat/vbyte packages' ) mempool_txids = set(node.getrawmempool()) # The 9.9 sat/byte txes caused the 270 sat/byte tx to get evicted, because # the latter was more dense in sigops. assert txid_highsigops not in mempool_txids # Other txes did get kept, including txid_parent which had ultralow # virtual feerate (but its child supplied enough fee). settxids = set([txid1, txid_parent, txid_child]) assert_equal(settxids.difference(mempool_txids), set()) self.log.info('Mempool fee floor has jumped to 1.495 sat/vbyte') # The removed txes have feerate 495 sat/kvbyte and the fee floor jumps by # 1000 sat/kbyte (MEMPOOL_FULL_FEE_INCREMENT) on top of that: assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001495')) self.log.info( 'Broadcasting a regular tx still works, because wallet knows what fee to use.' ) txid2 = node.sendtoaddress(node.getnewaddress(), 0.001) settxids.add(txid2) self.log.info( "But, a regular 1 sat/byte transaction can't get in now.") spendfrom = self.spendable_outputs.popleft() assert_raises_rpc_error( -26, "mempool min fee not met", node.sendrawtransaction, ToHex(create_var_transaction(spendfrom, b'', 500, 500))) self.log.info( "Broadcasting regular transactions will push out the high-sigops txns." ) # We can broadcast a bunch of regular txes. They need to pay a bit more # fee (1.5 sat/vbyte) than the floor. for i in range(15): spendfrom = self.spendable_outputs.popleft() ctx = create_var_transaction(spendfrom, b'', 100000, 150000) settxids.add(node.sendrawtransaction(ToHex(ctx))) self.log.info("Mining picks all the 'good' txes first") # These slightly higher-fee transactions also have more priority. [ lastblockhash, ] = node.generate(1) blocktxes = set(node.getblock(lastblockhash, 1)['tx']) assert_equal(settxids.difference(blocktxes), set()) # there are still hundreds of the flooded txes left in mempool. assert len(node.getrawmempool()) > 200 self.log.info( 'PART 2: The following tests focus on mineability according to blockmintxfee.' ) self.log.info('Clear the mempool by mining out everything.') node.generate(100) assert_equal(node.getrawmempool(), []) self.log.info( 'Reset the mempool fee floor (currently, restarting the node achieves this).' ) # could also be done by setting mocktime in future (the floor decays # over a period of hours) self.restart_node(0, self.extra_args[0]) (node, ) = self.nodes node.add_p2p_connection(P2PDataStore()) assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) self.log.info( 'Get a 0.998 sat/byte transaction into mempool by having it mined then reorged out.' ) self.log.info(node.getbestblockhash()) tip = self.getbestblock(node) self.log.info(tip.hash) ctx = create_var_transaction(self.spendable_outputs.popleft(), b'', 1000, 998) stucktxid = ctx.hash # check that indeed it can't get in normally: assert_raises_rpc_error(-26, "min relay fee not met", node.sendrawtransaction, ToHex(ctx)) block = self.build_block(tip, [ctx]) node.p2p.send_blocks_and_test([block], node, success=True) node.invalidateblock(block.hash) assert stucktxid in node.getrawmempool(), "should be returned to pool" self.log.info( "Since blockmintxfee is 1 sat/byte, that 0.998 sat/byte tx won't get mined by default, and is stuck permanently." ) node.generate(1) assert stucktxid in node.getrawmempool(), "should be stuck" self.log.info( 'Get wallet to make an normal tx sending money to 12 people, which is just under 1 sat/vbyte.' ) txid = node.sendmany("", {node.getnewaddress(): 0.01 for _ in range(12)}) # analyze the constructed tx just to make sure it's as expected ctx = FromHex(CTransaction(), node.getrawtransaction(txid)) assert len(ctx.vin) == 1, "only a single input should be needed" assert len(ctx.vout) == 13, "12 recipients and 1 change" # tx size should be ~ 599 bytes and fees should be 599 sats. # 13 sigops and 50 sigops / byte = 650 vbytes. txsize = len(ctx.serialize()) txvsize = max(txsize, 50 * len(ctx.vout)) assert txvsize > txsize, "transactions is dense-sigops" fee_sats = node.getmempoolentry(txid)['fee'] / SATOSHI assert_equal(fee_sats, 599) assert fee_sats / txsize >= 1.0, "just over 1 sat/byte" assert fee_sats / txvsize < 0.95, "just under 1 sat/vbyte" self.log.info( "Even though another transaction is stuck permanently, it should not prevent a normal tx from being mined." ) [ bhash, ] = node.generate(1) blocktxes = set(node.getblock(bhash, 1)['tx']) assert txid in blocktxes, "normal tx is not stuck" assert stucktxid not in blocktxes, "lowfee tx is still stuck"
def run_test(self): node = self.nodes[0] self.log.info('Start with empty mempool, and 200 blocks') self.mempool_size = 0 assert_equal(node.getblockcount(), 200) assert_equal(node.getmempoolinfo()['size'], self.mempool_size) coins = node.listunspent() self.log.info('Should not accept garbage to testmempoolaccept') assert_raises_rpc_error(-3, 'Expected type array, got string', lambda: node.testmempoolaccept(rawtxs='ff00baar')) assert_raises_rpc_error(-8, 'Array must contain exactly one raw transaction for now', lambda: node.testmempoolaccept(rawtxs=['ff00baar', 'ff22'])) assert_raises_rpc_error(-22, 'TX decode failed', lambda: node.testmempoolaccept(rawtxs=['ff00baar'])) self.log.info('A transaction already in the blockchain') # Pick a random coin(base) to spend coin = coins.pop() raw_tx_in_block = node.signrawtransactionwithwallet(node.createrawtransaction( inputs=[{'txid': coin['txid'], 'vout': coin['vout']}], outputs=[{node.getnewaddress(): 0.3}, {node.getnewaddress(): 49}], ))['hex'] txid_in_block = node.sendrawtransaction( hexstring=raw_tx_in_block, maxfeerate=0) node.generate(1) self.mempool_size = 0 self.check_mempool_result( result_expected=[{'txid': txid_in_block, 'allowed': False, 'reject-reason': 'txn-already-known'}], rawtxs=[raw_tx_in_block], ) self.log.info('A transaction not in the mempool') fee = 0.00000700 raw_tx_0 = node.signrawtransactionwithwallet(node.createrawtransaction( inputs=[{"txid": txid_in_block, "vout": 0, "sequence": 0xfffffffd}], outputs=[{node.getnewaddress(): 0.3 - fee}], ))['hex'] tx = FromHex(CTransaction(), raw_tx_0) txid_0 = tx.rehash() self.check_mempool_result( result_expected=[{'txid': txid_0, 'allowed': True}], rawtxs=[raw_tx_0], ) self.log.info('A final transaction not in the mempool') # Pick a random coin(base) to spend coin = coins.pop() raw_tx_final = node.signrawtransactionwithwallet(node.createrawtransaction( inputs=[{'txid': coin['txid'], 'vout': coin['vout'], "sequence": 0xffffffff}], # SEQUENCE_FINAL outputs=[{node.getnewaddress(): 0.025}], locktime=node.getblockcount() + 2000, # Can be anything ))['hex'] tx = FromHex(CTransaction(), raw_tx_final) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': True}], rawtxs=[tx.serialize().hex()], maxfeerate=0, ) node.sendrawtransaction(hexstring=raw_tx_final, maxfeerate=0) self.mempool_size += 1 self.log.info('A transaction in the mempool') node.sendrawtransaction(hexstring=raw_tx_0) self.mempool_size += 1 self.check_mempool_result( result_expected=[{'txid': txid_0, 'allowed': False, 'reject-reason': 'txn-already-in-mempool'}], rawtxs=[raw_tx_0], ) # Removed RBF test # self.log.info('A transaction that replaces a mempool transaction') # ... self.log.info('A transaction that conflicts with an unconfirmed tx') # Send the transaction that conflicts with the mempool transaction node.sendrawtransaction(hexstring=tx.serialize().hex(), maxfeerate=0) # take original raw_tx_0 tx = FromHex(CTransaction(), raw_tx_0) tx.vout[0].nValue -= int(4 * fee * COIN) # Set more fee # skip re-signing the tx self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'txn-mempool-conflict'}], rawtxs=[tx.serialize().hex()], maxfeerate=0, ) self.log.info('A transaction with missing inputs, that never existed') tx = FromHex(CTransaction(), raw_tx_0) tx.vin[0].prevout = COutPoint(hash=int('ff' * 32, 16), n=14) # skip re-signing the tx self.check_mempool_result( result_expected=[ {'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'missing-inputs'}], rawtxs=[ToHex(tx)], ) self.log.info( 'A transaction with missing inputs, that existed once in the past') tx = FromHex(CTransaction(), raw_tx_0) # Set vout to 1, to spend the other outpoint (49 coins) of the # in-chain-tx we want to double spend tx.vin[0].prevout.n = 1 raw_tx_1 = node.signrawtransactionwithwallet( ToHex(tx))['hex'] txid_1 = node.sendrawtransaction(hexstring=raw_tx_1, maxfeerate=0) # Now spend both to "clearly hide" the outputs, ie. remove the coins # from the utxo set by spending them raw_tx_spend_both = node.signrawtransactionwithwallet(node.createrawtransaction( inputs=[ {'txid': txid_0, 'vout': 0}, {'txid': txid_1, 'vout': 0}, ], outputs=[{node.getnewaddress(): 0.1}] ))['hex'] txid_spend_both = node.sendrawtransaction( hexstring=raw_tx_spend_both, maxfeerate=0) node.generate(1) self.mempool_size = 0 # Now see if we can add the coins back to the utxo set by sending the # exact txs again self.check_mempool_result( result_expected=[ {'txid': txid_0, 'allowed': False, 'reject-reason': 'missing-inputs'}], rawtxs=[raw_tx_0], ) self.check_mempool_result( result_expected=[ {'txid': txid_1, 'allowed': False, 'reject-reason': 'missing-inputs'}], rawtxs=[raw_tx_1], ) self.log.info('Create a signed "reference" tx for later use') raw_tx_reference = node.signrawtransactionwithwallet(node.createrawtransaction( inputs=[{'txid': txid_spend_both, 'vout': 0}], outputs=[{node.getnewaddress(): 0.05}], ))['hex'] tx = FromHex(CTransaction(), raw_tx_reference) # Reference tx should be valid on itself self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': True}], rawtxs=[ToHex(tx)], maxfeerate=0, ) self.log.info('A transaction with no outputs') tx = FromHex(CTransaction(), raw_tx_reference) tx.vout = [] # Skip re-signing the transaction for context independent checks from now on # FromHex(tx, node.signrawtransactionwithwallet(ToHex(tx))['hex']) self.check_mempool_result( result_expected=[{'txid': tx.rehash( ), 'allowed': False, 'reject-reason': 'bad-txns-vout-empty'}], rawtxs=[ToHex(tx)], ) self.log.info('A really large transaction') tx = FromHex(CTransaction(), raw_tx_reference) tx.vin = [tx.vin[0]] * (1 + MAX_BLOCK_BASE_SIZE // len(tx.vin[0].serialize())) self.check_mempool_result( result_expected=[ {'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-oversize'}], rawtxs=[ToHex(tx)], ) self.log.info('A transaction with negative output value') tx = FromHex(CTransaction(), raw_tx_reference) tx.vout[0].nValue *= -1 self.check_mempool_result( result_expected=[{'txid': tx.rehash( ), 'allowed': False, 'reject-reason': 'bad-txns-vout-negative'}], rawtxs=[ToHex(tx)], ) # The following two validations prevent overflow of the output amounts # (see CVE-2010-5139). self.log.info('A transaction with too large output value') tx = FromHex(CTransaction(), raw_tx_reference) tx.vout[0].nValue = 21000000 * COIN + 1 self.check_mempool_result( result_expected=[{'txid': tx.rehash( ), 'allowed': False, 'reject-reason': 'bad-txns-vout-toolarge'}], rawtxs=[ToHex(tx)], ) self.log.info('A transaction with too large sum of output values') tx = FromHex(CTransaction(), raw_tx_reference) tx.vout = [tx.vout[0]] * 2 tx.vout[0].nValue = 21000000 * COIN self.check_mempool_result( result_expected=[{'txid': tx.rehash( ), 'allowed': False, 'reject-reason': 'bad-txns-txouttotal-toolarge'}], rawtxs=[ToHex(tx)], ) self.log.info('A transaction with duplicate inputs') tx = FromHex(CTransaction(), raw_tx_reference) tx.vin = [tx.vin[0]] * 2 self.check_mempool_result( result_expected=[{'txid': tx.rehash( ), 'allowed': False, 'reject-reason': 'bad-txns-inputs-duplicate'}], rawtxs=[ToHex(tx)], ) self.log.info('A coinbase transaction') # Pick the input of the first tx we signed, so it has to be a coinbase # tx raw_tx_coinbase_spent = node.getrawtransaction( txid=node.decoderawtransaction(hexstring=raw_tx_in_block)['vin'][0]['txid']) tx = FromHex(CTransaction(), raw_tx_coinbase_spent) self.check_mempool_result( result_expected=[ {'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-tx-coinbase'}], rawtxs=[ToHex(tx)], ) self.log.info('Some nonstandard transactions') tx = FromHex(CTransaction(), raw_tx_reference) tx.nVersion = 3 # A version currently non-standard self.check_mempool_result( result_expected=[ {'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'version'}], rawtxs=[ToHex(tx)], ) tx = FromHex(CTransaction(), raw_tx_reference) tx.vout[0].scriptPubKey = CScript([OP_0]) # Some non-standard script self.check_mempool_result( result_expected=[ {'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'scriptpubkey'}], rawtxs=[ToHex(tx)], ) tx = FromHex(CTransaction(), raw_tx_reference) # Some not-pushonly scriptSig tx.vin[0].scriptSig = CScript([OP_HASH160]) self.check_mempool_result( result_expected=[{'txid': tx.rehash( ), 'allowed': False, 'reject-reason': 'scriptsig-not-pushonly'}], rawtxs=[ToHex(tx)], ) tx = FromHex(CTransaction(), raw_tx_reference) output_p2sh_burn = CTxOut(nValue=540, scriptPubKey=CScript( [OP_HASH160, hash160(b'burn'), OP_EQUAL])) # Use enough outputs to make the tx too large for our policy num_scripts = 100000 // len(output_p2sh_burn.serialize()) tx.vout = [output_p2sh_burn] * num_scripts self.check_mempool_result( result_expected=[ {'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'tx-size'}], rawtxs=[ToHex(tx)], ) tx = FromHex(CTransaction(), raw_tx_reference) tx.vout[0] = output_p2sh_burn # Make output smaller, such that it is dust for our policy tx.vout[0].nValue -= 1 self.check_mempool_result( result_expected=[ {'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'dust'}], rawtxs=[ToHex(tx)], ) tx = FromHex(CTransaction(), raw_tx_reference) tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'\xff']) tx.vout = [tx.vout[0]] * 2 self.check_mempool_result( result_expected=[ {'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'multi-op-return'}], rawtxs=[ToHex(tx)], ) self.log.info('A timelocked transaction') tx = FromHex(CTransaction(), raw_tx_reference) # Should be non-max, so locktime is not ignored tx.vin[0].nSequence -= 1 tx.nLockTime = node.getblockcount() + 1 self.check_mempool_result( result_expected=[ {'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-nonfinal'}], rawtxs=[ToHex(tx)], ) self.log.info('A transaction that is locked by BIP68 sequence logic') tx = FromHex(CTransaction(), raw_tx_reference) # We could include it in the second block mined from now, but not the # very next one tx.vin[0].nSequence = 2 # Can skip re-signing the tx because of early rejection self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'non-BIP68-final'}], rawtxs=[tx.serialize().hex()], maxfeerate=0, )
def run_test(self): # Turn off node 1 while node 0 mines blocks to generate stakes, # so that we can later try starting node 1 with an orphan proof. self.stop_node(1) node = self.nodes[0] addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(100, addrkey0.address) self.log.info( "Make build a valid proof and restart the node to use it") privkey = ECKey() privkey.set( bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747" ), True) wif_privkey = bytes_to_wif(privkey.get_bytes()) def get_hex_pubkey(privkey): return privkey.get_pubkey().get_bytes().hex() proof_master = get_hex_pubkey(privkey) proof_sequence = 11 proof_expiration = 12 stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) proof = node.buildavalancheproof(proof_sequence, proof_expiration, wif_privkey, stakes) self.log.info("Test decodeavalancheproof RPC") proofobj = FromHex(LegacyAvalancheProof(), proof) decodedproof = node.decodeavalancheproof(proof) limited_id_hex = f"{proofobj.limited_proofid:0{64}x}" assert_equal(decodedproof["sequence"], proof_sequence) assert_equal(decodedproof["expiration"], proof_expiration) assert_equal(decodedproof["master"], proof_master) assert_equal(decodedproof["payoutscript"]["hex"], "") assert "signature" not in decodedproof.keys() assert_equal(decodedproof["proofid"], f"{proofobj.proofid:0{64}x}") assert_equal(decodedproof["limitedid"], limited_id_hex) assert_equal(decodedproof["stakes"][0]["txid"], stakes[0]["txid"]) assert_equal(decodedproof["stakes"][0]["vout"], stakes[0]["vout"]) assert_equal(decodedproof["stakes"][0]["height"], stakes[0]["height"]) assert_equal(decodedproof["stakes"][0]["iscoinbase"], stakes[0]["iscoinbase"]) assert_equal(decodedproof["stakes"][0]["signature"], base64.b64encode(proofobj.stakes[0].sig).decode("ascii")) # Invalid hex (odd number of hex digits) assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", node.decodeavalancheproof, proof[:-1]) # Valid hex but invalid proof assert_raises_rpc_error(-22, "Proof has invalid format", node.decodeavalancheproof, proof[:-2]) self.log.info( "Testing decodeavalancheproof with legacyavaproof disabled") self.restart_node(0, self.extra_args[0] + ["-legacyavaproof=0"]) regular_proof = node.buildavalancheproof(proof_sequence, proof_expiration, wif_privkey, stakes, ADDRESS_ECREG_UNSPENDABLE) decoded_regular_proof = node.decodeavalancheproof(regular_proof) assert_equal(decoded_regular_proof["sequence"], decodedproof["sequence"]) assert_equal(decoded_regular_proof["expiration"], decodedproof["expiration"]) assert_equal(decoded_regular_proof["master"], decodedproof["master"]) assert_equal( decoded_regular_proof["payoutscript"], { "asm": "OP_DUP OP_HASH160 0000000000000000000000000000000000000000 OP_EQUALVERIFY OP_CHECKSIG", "hex": "76a914000000000000000000000000000000000000000088ac", "reqSigs": 1, "type": "pubkeyhash", "addresses": [ADDRESS_ECREG_UNSPENDABLE], }) regular_proof_obj = FromHex(AvalancheProof(), regular_proof) assert_equal( decoded_regular_proof["signature"], base64.b64encode(regular_proof_obj.signature).decode("ascii")) assert_equal(decoded_regular_proof["proofid"], f"{regular_proof_obj.proofid:0{64}x}") assert_equal(decoded_regular_proof["limitedid"], f"{regular_proof_obj.limited_proofid:0{64}x}") assert_equal(decoded_regular_proof["stakes"][0]["txid"], decodedproof["stakes"][0]["txid"]) assert_equal(decoded_regular_proof["stakes"][0]["vout"], decodedproof["stakes"][0]["vout"]) assert_equal(decoded_regular_proof["stakes"][0]["height"], decodedproof["stakes"][0]["height"]) assert_equal(decoded_regular_proof["stakes"][0]["iscoinbase"], decodedproof["stakes"][0]["iscoinbase"]) assert_equal( decoded_regular_proof["stakes"][0]["signature"], base64.b64encode(regular_proof_obj.stakes[0].sig).decode("ascii")) # Restart the node with this proof self.restart_node( 0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) self.log.info("The proof is registered at first chaintip update") assert_equal(len(node.getavalanchepeerinfo()), 0) node.generate(1) self.wait_until(lambda: len(node.getavalanchepeerinfo()) == 1, timeout=5) # This case will occur for users building proofs with a third party # tool and then starting a new node that is not yet aware of the # transactions used for stakes. self.log.info("Start a node with an orphan proof") self.start_node( 1, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) # Mine a block to trigger an attempt at registering the proof self.nodes[1].generate(1) wait_for_proof(self.nodes[1], f"{proofobj.proofid:0{64}x}", expect_orphan=True) self.log.info("Connect to an up-to-date node to unorphan the proof") self.connect_nodes(1, node.index) self.sync_all() wait_for_proof(self.nodes[1], f"{proofobj.proofid:0{64}x}", expect_orphan=False) self.log.info("Generate delegations for the proof") # Stack up a few delegation levels def gen_privkey(): pk = ECKey() pk.generate() return pk delegator_privkey = privkey delegation = None for _ in range(10): delegated_privkey = gen_privkey() delegation = node.delegateavalancheproof( limited_id_hex, bytes_to_wif(delegator_privkey.get_bytes()), get_hex_pubkey(delegated_privkey), delegation, ) delegator_privkey = delegated_privkey random_privkey = gen_privkey() random_pubkey = get_hex_pubkey(random_privkey) # Invalid proof no_stake = node.buildavalancheproof(proof_sequence, proof_expiration, wif_privkey, []) # Invalid privkey assert_raises_rpc_error( -5, "The private key is invalid", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(bytes(32)), random_pubkey, ) # Invalid delegation bad_dg = AvalancheDelegation() assert_raises_rpc_error( -8, "The delegation does not match the proof", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Still invalid, but with a matching proofid bad_dg.limited_proofid = proofobj.limited_proofid bad_dg.proof_master = proofobj.master bad_dg.levels = [AvalancheDelegationLevel()] assert_raises_rpc_error( -8, "The delegation is invalid", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Wrong privkey, match the proof but does not match the delegation assert_raises_rpc_error( -5, "The private key does not match the delegation", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, delegation, ) # Delegation not hex assert_raises_rpc_error( -22, "Delegation must be an hexadecimal string.", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, "f00", ) # Delegation is hex but ill-formed assert_raises_rpc_error( -22, "Delegation has invalid format", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, "dead", ) # Test invalid proofs dust = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0")) dust2 = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, create_coinbase_stakes( node, [blockhashes[0]], addrkey0.key, amount=f"{PROOF_DUST_THRESHOLD * 0.9999:.2f}")) missing_stake = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10000000, 'height': 42, 'iscoinbase': False, 'privatekey': addrkey0.key, }]) duplicate_stake = ( "0b000000000000000c0000000000000021030b4c866585dd868" "a9d62348a9cd008d6a312937048fff31670e7e920cfc7a74402" "05c5f72f5d6da3085583e75ee79340eb4eff208c89988e7ed0e" "fb30b87298fa30000000000f2052a0100000003000000210227" "d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2" "fc839ef5d3f86076def2e8bc3c40671c1a0eb505da5857a950a" "0cf4625a80018cdd75ac62e61273ff8142f747de67e73f6368c" "8648942b0ef6c065d72a81ad7438a23c11cca05c5f72f5d6da3" "085583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30" "000000000f2052a0100000003000000210227d85ba011276cf2" "5b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f860" "76def2e8bc3c40671c1a0eb505da5857a950a0cf4625a80018c" "dd75ac62e61273ff8142f747de67e73f6368c8648942b0ef6c0" "65d72a81ad7438a23c11cca") bad_sig = ( "0b000000000000000c0000000000000021030b4c866585dd868a9d62348" "a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085" "583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20" "52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3" "8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0" "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") wrong_order = ( "c964aa6fde575e4ce8404581c7be874e21023beefdde700a6bc0203" "6335b4df141c8bc67bb05a971f5ac2745fd683797dde30305d427b7" "06705a5d4b6a368a231d6db62abacf8c29bc32b61e7f65a0a6976aa" "8b86b687bc0260e821e4f0200b9d3bf6d2102449fb5237efe8f647d" "32e8b64f06c22d1d40368eaca2a71ffc6a13ecc8bce68052365271b" "6c71189f5cd7e3b694b77b579080f0b35bae567b96590ab6aa3019b" "018ff9f061f52f1426bdb195d4b6d4dff5114cee90e33dabf0c588e" "badf7774418f54247f6390791706af36fac782302479898b5273f9e" "51a92cb1fb5af43deeb6c8c269403d30ffcb380300134398c42103e" "49f9df52de2dea81cf7838b82521b69f2ea360f1c4eed9e6c89b7d0" "f9e645efa08e97ea0c60e1f0a064fbf08989c084707082727e85dcb" "9f79bb503f76ee6c8dad42a07ef15c89b3750a5631d604b21fafff0" "f4de354ade95c2f28160ae549af0d4ce48c4ca9d0714b1fa5192027" "0f8575e0af610f07b4e602a018ecdbb649b64fff614c0026e9fc8e0" "030092533d422103aac52f4cfca700e7e9824298e0184755112e32f" "359c832f5f6ad2ef62a2c024af812d6d7f2ecc6223a774e19bce1fb" "20d94d6b01ea693638f55c74fdaa5358fa9239d03e4caf3d817e8f7" "48ccad55a27b9d365db06ad5a0b779ac385f3dc8710") self.log.info( "Check the verifyavalancheproof and sendavalancheproof RPCs") if self.is_wallet_compiled(): self.log.info( "Check a proof with the maximum number of UTXO is valid") new_blocks = node.generate(AVALANCHE_MAX_PROOF_STAKES // 10 + 1) # confirm the coinbase UTXOs node.generate(101) too_many_stakes = create_stakes(node, new_blocks, AVALANCHE_MAX_PROOF_STAKES + 1) maximum_stakes = too_many_stakes[:-1] good_proof = node.buildavalancheproof(proof_sequence, proof_expiration, wif_privkey, maximum_stakes) too_many_utxos = node.buildavalancheproof(proof_sequence, proof_expiration, wif_privkey, too_many_stakes) assert node.verifyavalancheproof(good_proof) for rpc in [node.verifyavalancheproof, node.sendavalancheproof]: assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", rpc, "f00") assert_raises_rpc_error(-22, "Proof has invalid format", rpc, "f00d") def check_rpc_failure(proof, message): assert_raises_rpc_error(-8, "The proof is invalid: " + message, rpc, proof) check_rpc_failure(no_stake, "no-stake") check_rpc_failure(dust, "amount-below-dust-threshold") check_rpc_failure(duplicate_stake, "duplicated-stake") check_rpc_failure(missing_stake, "utxo-missing-or-spent") check_rpc_failure(bad_sig, "invalid-stake-signature") check_rpc_failure(wrong_order, "wrong-stake-ordering") if self.is_wallet_compiled(): check_rpc_failure(too_many_utxos, "too-many-utxos") conflicting_utxo = node.buildavalancheproof(proof_sequence + 1, proof_expiration, wif_privkey, stakes) assert_raises_rpc_error( -8, "The proof has conflicting utxo with an existing proof", node.sendavalancheproof, conflicting_utxo) # Clear the proof pool self.restart_node(0) # Good proof assert node.verifyavalancheproof(proof) peer = node.add_p2p_connection(P2PInterface()) proofid = FromHex(LegacyAvalancheProof(), proof).proofid node.sendavalancheproof(proof) assert proofid in get_proof_ids(node) def inv_found(): with p2p_lock: return peer.last_message.get( "inv") and peer.last_message["inv"].inv[-1].hash == proofid self.wait_until(inv_found) self.log.info("Check the getrawproof RPC") raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], False) assert_raises_rpc_error(-8, "Proof not found", node.getrawavalancheproof, '0' * 64) # Orphan the proof by sending the stake raw_tx = node.createrawtransaction([{ "txid": stakes[-1]["txid"], "vout": 0 }], { ADDRESS_ECREG_UNSPENDABLE: stakes[-1]["amount"] - Decimal('10000') }) signed_tx = node.signrawtransactionwithkey(raw_tx, [addrkey0.key]) node.sendrawtransaction(signed_tx["hex"]) node.generate(1) self.wait_until(lambda: proofid not in get_proof_ids(node)) raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], True) self.log.info("Bad proof should be rejected at startup") self.stop_node(0) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avasessionkey=0", ], expected_msg="Error: The avalanche session key is invalid.", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), ], expected_msg= "Error: The avalanche master key is missing for the avalanche proof.", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=0", ], expected_msg="Error: The avalanche master key is invalid.", ) def check_proof_init_error(proof, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: " + message, ) check_proof_init_error(no_stake, "The avalanche proof has no stake.") check_proof_init_error(dust, "The avalanche proof stake is too low.") check_proof_init_error(dust2, "The avalanche proof stake is too low.") check_proof_init_error(duplicate_stake, "The avalanche proof has duplicated stake.") check_proof_init_error( bad_sig, "The avalanche proof has invalid stake signatures.") if self.is_wallet_compiled(): # The too many utxos case creates a proof which is that large that it # cannot fit on the command line append_config(node.datadir, ["avaproof={}".format(too_many_utxos)]) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: The avalanche proof has too many utxos.", match=ErrorMatch.PARTIAL_REGEX, ) # Master private key mismatch random_privkey = ECKey() random_privkey.generate() node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(random_privkey.get_bytes())), ], expected_msg= "Error: The master key does not match the proof public key.", ) self.log.info("Bad delegation should be rejected at startup") def check_delegation_init_error(delegation, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avadelegation={}".format(delegation), "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(delegated_privkey.get_bytes())), ], expected_msg="Error: " + message, ) check_delegation_init_error( AvalancheDelegation().serialize().hex(), "The delegation does not match the proof.") bad_level_sig = FromHex(AvalancheDelegation(), delegation) # Tweak some key to cause the signature to mismatch bad_level_sig.levels[-2].pubkey = bytes.fromhex(proof_master) check_delegation_init_error( bad_level_sig.serialize().hex(), "The avalanche delegation has invalid signatures.") node.assert_start_raises_init_error( self.extra_args[0] + [ "-avadelegation={}".format(delegation), "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(random_privkey.get_bytes())), ], expected_msg= "Error: The master key does not match the delegation public key.", )
def run_test(self): self.nodes[0].add_p2p_connection(P2PDataStore()) self.nodeaddress = self.nodes[0].getnewaddress() self.pubkey = self.nodes[0].getaddressinfo(self.nodeaddress)["pubkey"] self.log.info("Mining %d blocks", CHAIN_HEIGHT) self.coinbase_txids = [ self.nodes[0].getblock(b)['tx'][0] for b in self.nodes[0].generate( CHAIN_HEIGHT, self.signblockprivkey_wif) ] ## P2PKH transaction ######################## self.log.info("Test using a P2PKH transaction") spendtx = create_transaction(self.nodes[0], self.coinbase_txids[0], self.nodeaddress, amount=10) spendtx.rehash() copy_spendTx = CTransaction(spendtx) #cache hashes hash = spendtx.hash hashMalFix = spendtx.hashMalFix #malleate unDERify(spendtx) spendtx.rehash() # verify that hashMalFix remains the same even when signature is malleated and hash changes assert_not_equal(hash, spendtx.hash) assert_equal(hashMalFix, spendtx.hashMalFix) # verify that hash is spendtx.serialize() hash = encode(hash256(spendtx.serialize())[::-1], 'hex_codec').decode('ascii') assert_equal(hash, spendtx.hash) # verify that hashMalFix is spendtx.serialize(with_scriptsig=False) hashMalFix = encode( hash256(spendtx.serialize(with_scriptsig=False))[::-1], 'hex_codec').decode('ascii') assert_equal(hashMalFix, spendtx.hashMalFix) assert_not_equal(hash, hashMalFix) #as this transaction does not have witness data the following is true assert_equal(spendtx.serialize(), spendtx.serialize(with_witness=True, with_scriptsig=True)) assert_equal(spendtx.serialize(with_witness=False), spendtx.serialize(with_witness=True, with_scriptsig=True)) assert_not_equal( spendtx.serialize(with_witness=False), spendtx.serialize(with_witness=True, with_scriptsig=False)) assert_equal(spendtx.serialize(with_witness=False), spendtx.serialize_without_witness(with_scriptsig=True)) assert_equal(spendtx.serialize_with_witness(with_scriptsig=True), spendtx.serialize_without_witness(with_scriptsig=True)) assert_equal(spendtx.serialize_with_witness(with_scriptsig=False), spendtx.serialize_without_witness(with_scriptsig=False)) #Create block with only non-DER signature P2PKH transaction tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblockheader(tip)['mediantime'] + 1 block = create_block(int(tip, 16), create_coinbase(CHAIN_HEIGHT + 1), block_time) block.vtx.append(spendtx) block.hashMerkleRoot = block.calc_merkle_root() block.hashImMerkleRoot = block.calc_immutable_merkle_root() block.rehash() block.solve(self.signblockprivkey) # serialize with and without witness block remains the same assert_equal(block.serialize(with_witness=True), block.serialize()) assert_equal(block.serialize(with_witness=True), block.serialize(with_witness=False)) assert_equal(block.serialize(with_witness=True), block.serialize(with_witness=False, with_scriptsig=True)) self.log.info("Reject block with non-DER signature") self.nodes[0].p2p.send_and_ping(msg_block(block)) assert_equal(self.nodes[0].getbestblockhash(), tip) wait_until(lambda: "reject" in self.nodes[0].p2p.last_message.keys(), lock=mininode_lock) with mininode_lock: assert_equal(self.nodes[0].p2p.last_message["reject"].code, REJECT_INVALID) assert_equal(self.nodes[0].p2p.last_message["reject"].data, block.sha256) assert_equal(self.nodes[0].p2p.last_message["reject"].reason, b'block-validation-failed') self.log.info("Accept block with DER signature") #recreate block with DER sig transaction block = create_block(int(tip, 16), create_coinbase(CHAIN_HEIGHT + 1), block_time) block.vtx.append(copy_spendTx) block.hashMerkleRoot = block.calc_merkle_root() block.hashImMerkleRoot = block.calc_immutable_merkle_root() block.rehash() block.solve(self.signblockprivkey) self.nodes[0].p2p.send_and_ping(msg_block(block)) assert_equal(self.nodes[0].getbestblockhash(), block.hash) ## P2SH transaction ######################## self.log.info("Test using P2SH transaction ") REDEEM_SCRIPT_1 = CScript([OP_1, OP_DROP]) P2SH_1 = CScript([OP_HASH160, hash160(REDEEM_SCRIPT_1), OP_EQUAL]) tx = CTransaction() tx.vin.append( CTxIn(COutPoint(int(self.coinbase_txids[1], 16), 0), b"", 0xffffffff)) tx.vout.append(CTxOut(10, P2SH_1)) tx.rehash() spendtx_raw = self.nodes[0].signrawtransactionwithwallet( ToHex(tx), [], "ALL", self.options.scheme)["hex"] spendtx = FromHex(spendtx, spendtx_raw) spendtx.rehash() copy_spendTx = CTransaction(spendtx) #cache hashes hash = spendtx.hash hashMalFix = spendtx.hashMalFix #malleate spendtxcopy = spendtx unDERify(spendtxcopy) spendtxcopy.rehash() # verify that hashMalFix remains the same even when signature is malleated and hash changes assert_not_equal(hash, spendtxcopy.hash) assert_equal(hashMalFix, spendtxcopy.hashMalFix) # verify that hash is spendtx.serialize() hash = encode( hash256(spendtx.serialize(with_witness=False))[::-1], 'hex_codec').decode('ascii') assert_equal(hash, spendtx.hash) # verify that hashMalFix is spendtx.serialize(with_scriptsig=False) hashMalFix = encode( hash256(spendtx.serialize(with_witness=False, with_scriptsig=False))[::-1], 'hex_codec').decode('ascii') assert_equal(hashMalFix, spendtx.hashMalFix) assert_not_equal(hash, hashMalFix) #as this transaction does not have witness data the following is true assert_equal(spendtx.serialize(), spendtx.serialize(with_witness=True, with_scriptsig=True)) assert_equal(spendtx.serialize(with_witness=False), spendtx.serialize(with_witness=True, with_scriptsig=True)) assert_not_equal( spendtx.serialize(with_witness=False), spendtx.serialize(with_witness=True, with_scriptsig=False)) assert_equal(spendtx.serialize(with_witness=False), spendtx.serialize_without_witness(with_scriptsig=True)) assert_equal(spendtx.serialize_with_witness(with_scriptsig=True), spendtx.serialize_without_witness(with_scriptsig=True)) assert_equal(spendtx.serialize_with_witness(with_scriptsig=False), spendtx.serialize_without_witness(with_scriptsig=False)) #Create block with only non-DER signature P2SH transaction tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblockheader(tip)['mediantime'] + 1 block = create_block(int(tip, 16), create_coinbase(CHAIN_HEIGHT + 2), block_time) block.vtx.append(spendtx) block.hashMerkleRoot = block.calc_merkle_root() block.hashImMerkleRoot = block.calc_immutable_merkle_root() block.rehash() block.solve(self.signblockprivkey) # serialize with and without witness block remains the same assert_equal(block.serialize(with_witness=True), block.serialize()) assert_equal(block.serialize(with_witness=True), block.serialize(with_witness=False)) assert_equal(block.serialize(with_witness=True), block.serialize(with_witness=True, with_scriptsig=True)) self.log.info("Reject block with non-DER signature") self.nodes[0].p2p.send_and_ping(msg_block(block)) assert_equal(self.nodes[0].getbestblockhash(), tip) wait_until(lambda: "reject" in self.nodes[0].p2p.last_message.keys(), lock=mininode_lock) with mininode_lock: assert_equal(self.nodes[0].p2p.last_message["reject"].code, REJECT_INVALID) assert_equal(self.nodes[0].p2p.last_message["reject"].data, block.sha256) assert_equal(self.nodes[0].p2p.last_message["reject"].reason, b'block-validation-failed') self.log.info("Accept block with DER signature") #recreate block with DER sig transaction block = create_block(int(tip, 16), create_coinbase(CHAIN_HEIGHT + 2), block_time) block.vtx.append(copy_spendTx) block.hashMerkleRoot = block.calc_merkle_root() block.hashImMerkleRoot = block.calc_immutable_merkle_root() block.rehash() block.solve(self.signblockprivkey) self.nodes[0].p2p.send_and_ping(msg_block(block)) assert_equal(self.nodes[0].getbestblockhash(), block.hash) ## redeem previous P2SH ######################### self.log.info("Test using P2SH redeem transaction ") tx = CTransaction() tx.vout.append(CTxOut(1, CScript([OP_TRUE]))) tx.vin.append(CTxIn(COutPoint(block.vtx[1].malfixsha256, 0), b'')) (sighash, err) = SignatureHash(REDEEM_SCRIPT_1, tx, 1, SIGHASH_ALL) signKey = CECKey() signKey.set_secretbytes(b"horsebattery") sig = signKey.sign(sighash) + bytes(bytearray([SIGHASH_ALL])) scriptSig = CScript([sig, REDEEM_SCRIPT_1]) tx.vin[0].scriptSig = scriptSig tx.rehash() spendtx_raw = self.nodes[0].signrawtransactionwithwallet( ToHex(tx), [], "ALL", self.options.scheme)["hex"] spendtx = FromHex(spendtx, spendtx_raw) spendtx.rehash() #cache hashes hash = spendtx.hash hashMalFix = spendtx.hashMalFix #malleate spendtxcopy = spendtx unDERify(spendtxcopy) spendtxcopy.rehash() # verify that hashMalFix remains the same even when signature is malleated and hash changes assert_not_equal(hash, spendtxcopy.hash) assert_equal(hashMalFix, spendtxcopy.hashMalFix) # verify that hash is spendtx.serialize() hash = encode( hash256(spendtx.serialize(with_witness=False))[::-1], 'hex_codec').decode('ascii') assert_equal(hash, spendtx.hash) # verify that hashMalFix is spendtx.serialize(with_scriptsig=False) hashMalFix = encode( hash256(spendtx.serialize(with_witness=False, with_scriptsig=False))[::-1], 'hex_codec').decode('ascii') assert_equal(hashMalFix, spendtx.hashMalFix) assert_not_equal(hash, hashMalFix) #as this transaction does not have witness data the following is true assert_equal(spendtx.serialize(), spendtx.serialize(with_witness=True, with_scriptsig=True)) assert_equal(spendtx.serialize(with_witness=False), spendtx.serialize(with_witness=True, with_scriptsig=True)) assert_not_equal( spendtx.serialize(with_witness=False), spendtx.serialize(with_witness=True, with_scriptsig=False)) assert_equal(spendtx.serialize(with_witness=False), spendtx.serialize_without_witness(with_scriptsig=True)) assert_equal(spendtx.serialize_with_witness(with_scriptsig=True), spendtx.serialize_without_witness(with_scriptsig=True)) assert_equal(spendtx.serialize_with_witness(with_scriptsig=False), spendtx.serialize_without_witness(with_scriptsig=False)) #Create block with only non-DER signature P2SH redeem transaction tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblockheader(tip)['mediantime'] + 1 block = create_block(int(tip, 16), create_coinbase(CHAIN_HEIGHT + 3), block_time) block.vtx.append(spendtx) block.hashMerkleRoot = block.calc_merkle_root() block.hashImMerkleRoot = block.calc_immutable_merkle_root() block.rehash() block.solve(self.signblockprivkey) # serialize with and without witness block remains the same assert_equal(block.serialize(with_witness=True), block.serialize()) assert_equal(block.serialize(with_witness=True), block.serialize(with_witness=False)) assert_equal(block.serialize(with_witness=True), block.serialize(with_witness=True, with_scriptsig=True)) self.log.info("Accept block with P2SH redeem transaction") self.nodes[0].p2p.send_and_ping(msg_block(block)) assert_equal(self.nodes[0].getbestblockhash(), block.hash) ## p2sh_p2wpkh transaction ############################## self.log.info("Test using p2sh_p2wpkh transaction ") spendtxStr = create_witness_tx(self.nodes[0], True, getInput(self.coinbase_txids[4]), self.pubkey, amount=1.0) #get CTRansaction object from above hex spendtx = CTransaction() spendtx.deserialize(BytesIO(hex_str_to_bytes(spendtxStr))) spendtx.rehash() #cache hashes spendtx.rehash() hash = spendtx.hash hashMalFix = spendtx.hashMalFix withash = spendtx.calc_sha256(True) # malleate unDERify(spendtx) spendtx.rehash() withash2 = spendtx.calc_sha256(True) # verify that hashMalFix remains the same even when signature is malleated and hash changes assert_equal(withash, withash2) assert_equal(hash, spendtx.hash) assert_equal(hashMalFix, spendtx.hashMalFix) # verify that hash is spendtx.serialize() hash = encode(hash256(spendtx.serialize())[::-1], 'hex_codec').decode('ascii') assert_equal(hash, spendtx.hash) # verify that hashMalFix is spendtx.serialize(with_scriptsig=False) hashMalFix = encode( hash256(spendtx.serialize(with_scriptsig=False))[::-1], 'hex_codec').decode('ascii') assert_equal(hashMalFix, spendtx.hashMalFix) assert_not_equal(hash, hashMalFix) #as this transaction does not have witness data the following is true assert_equal(spendtx.serialize(), spendtx.serialize(with_witness=True, with_scriptsig=True)) assert_equal(spendtx.serialize(with_witness=False), spendtx.serialize(with_witness=True, with_scriptsig=True)) assert_not_equal( spendtx.serialize(with_witness=False), spendtx.serialize(with_witness=True, with_scriptsig=False)) assert_equal(spendtx.serialize(with_witness=False), spendtx.serialize_without_witness(with_scriptsig=True)) assert_equal(spendtx.serialize_with_witness(with_scriptsig=True), spendtx.serialize_without_witness(with_scriptsig=True)) assert_equal(spendtx.serialize_with_witness(with_scriptsig=False), spendtx.serialize_without_witness(with_scriptsig=False)) #Create block with only non-DER signature p2sh_p2wpkh transaction spendtxStr = self.nodes[0].signrawtransactionwithwallet( spendtxStr, [], "ALL", self.options.scheme) assert ("errors" not in spendtxStr or len(["errors"]) == 0) spendtxStr = spendtxStr["hex"] spendtx = CTransaction() spendtx.deserialize(BytesIO(hex_str_to_bytes(spendtxStr))) spendtx.rehash() tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblockheader(tip)['mediantime'] + 1 block = create_block(int(tip, 16), create_coinbase(CHAIN_HEIGHT + 4), block_time) block.vtx.append(spendtx) add_witness_commitment(block) block.hashMerkleRoot = block.calc_merkle_root() block.hashImMerkleRoot = block.calc_immutable_merkle_root() block.rehash() block.solve(self.signblockprivkey) # serialize with and without witness assert_equal(block.serialize(with_witness=False), block.serialize()) assert_not_equal(block.serialize(with_witness=True), block.serialize(with_witness=False)) assert_not_equal( block.serialize(with_witness=True), block.serialize(with_witness=False, with_scriptsig=True)) self.log.info( "Reject block with p2sh_p2wpkh transaction and witness commitment") assert_raises_rpc_error( -22, "Block does not start with a coinbase", self.nodes[0].submitblock, bytes_to_hex_str(block.serialize(with_witness=True))) assert_equal(self.nodes[0].getbestblockhash(), tip) block = create_block(int(tip, 16), create_coinbase(CHAIN_HEIGHT + 4), block_time) block.vtx.append(spendtx) block.hashMerkleRoot = block.calc_merkle_root() block.hashImMerkleRoot = block.calc_immutable_merkle_root() block.rehash() block.solve(self.signblockprivkey) self.log.info("Accept block with p2sh_p2wpkh transaction") self.nodes[0].submitblock( bytes_to_hex_str(block.serialize(with_witness=True))) assert_equal(self.nodes[0].getbestblockhash(), block.hash) ## p2sh_p2wsh transaction ############################## self.log.info("Test using p2sh_p2wsh transaction") spendtxStr = create_witness_tx(self.nodes[0], True, getInput(self.coinbase_txids[5]), self.pubkey, amount=1.0) #get CTRansaction object from above hex spendtx = CTransaction() spendtx.deserialize(BytesIO(hex_str_to_bytes(spendtxStr))) spendtx.rehash() #cache hashes spendtx.rehash() hash = spendtx.hash hashMalFix = spendtx.hashMalFix withash = spendtx.calc_sha256(True) # malleate unDERify(spendtx) spendtx.rehash() withash2 = spendtx.calc_sha256(True) # verify that hashMalFix remains the same even when signature is malleated and hash changes assert_equal(withash, withash2) assert_equal(hash, spendtx.hash) assert_equal(hashMalFix, spendtx.hashMalFix) # verify that hash is spendtx.serialize() hash = encode(hash256(spendtx.serialize())[::-1], 'hex_codec').decode('ascii') assert_equal(hash, spendtx.hash) # verify that hashMalFix is spendtx.serialize(with_scriptsig=False) hashMalFix = encode( hash256(spendtx.serialize(with_scriptsig=False))[::-1], 'hex_codec').decode('ascii') assert_equal(hashMalFix, spendtx.hashMalFix) assert_not_equal(hash, hashMalFix) #as this transaction does not have witness data the following is true assert_equal(spendtx.serialize(), spendtx.serialize(with_witness=True, with_scriptsig=True)) assert_equal(spendtx.serialize(with_witness=False), spendtx.serialize(with_witness=True, with_scriptsig=True)) assert_not_equal( spendtx.serialize(with_witness=False), spendtx.serialize(with_witness=True, with_scriptsig=False)) assert_equal(spendtx.serialize(with_witness=False), spendtx.serialize_without_witness(with_scriptsig=True)) assert_equal(spendtx.serialize_with_witness(with_scriptsig=True), spendtx.serialize_without_witness(with_scriptsig=True)) assert_equal(spendtx.serialize_with_witness(with_scriptsig=False), spendtx.serialize_without_witness(with_scriptsig=False)) #Create block with only non-DER signature p2sh_p2wsh transaction spendtxStr = self.nodes[0].signrawtransactionwithwallet( spendtxStr, [], "ALL", self.options.scheme) assert ("errors" not in spendtxStr or len(["errors"]) == 0) spendtxStr = spendtxStr["hex"] spendtx = CTransaction() spendtx.deserialize(BytesIO(hex_str_to_bytes(spendtxStr))) spendtx.rehash() tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblockheader(tip)['mediantime'] + 1 block = create_block(int(tip, 16), create_coinbase(CHAIN_HEIGHT + 5), block_time) block.vtx.append(spendtx) add_witness_commitment(block) block.hashMerkleRoot = block.calc_merkle_root() block.hashImMerkleRoot = block.calc_immutable_merkle_root() block.rehash() block.solve(self.signblockprivkey) # serialize with and without witness assert_equal(block.serialize(with_witness=False), block.serialize()) assert_not_equal(block.serialize(with_witness=True), block.serialize(with_witness=False)) assert_not_equal( block.serialize(with_witness=True), block.serialize(with_witness=False, with_scriptsig=True)) self.log.info( "Reject block with p2sh_p2wsh transaction and witness commitment") assert_raises_rpc_error( -22, "Block does not start with a coinbase", self.nodes[0].submitblock, bytes_to_hex_str(block.serialize(with_witness=True))) assert_equal(self.nodes[0].getbestblockhash(), tip) block = create_block(int(tip, 16), create_coinbase(CHAIN_HEIGHT + 5), block_time) block.vtx.append(spendtx) block.hashMerkleRoot = block.calc_merkle_root() block.hashImMerkleRoot = block.calc_immutable_merkle_root() block.rehash() block.solve(self.signblockprivkey) self.log.info("Accept block with p2sh_p2wsh transaction") self.nodes[0].submitblock( bytes_to_hex_str(block.serialize(with_witness=True))) assert_equal(self.nodes[0].getbestblockhash(), block.hash)
def tapscript_satisfy_test(self, script, inputs=[], add_issuance=False, add_pegin=False, fail=None, add_prevout=False, add_asset=False, add_value=False, add_spk=False, seq=0, add_out_spk=None, add_out_asset=None, add_out_value=None, add_out_nonce=None, ver=2, locktime=0, add_num_outputs=False, add_weight=False, blind=False): # Create a taproot utxo scripts = [("s0", script)] prev_tx, prev_vout, spk, sec, pub, tap = self.create_taproot_utxo( scripts) if add_pegin: fund_info = self.nodes[0].getpeginaddress() peg_id = self.nodes[0].sendtoaddress( fund_info["mainchain_address"], 1) raw_peg_tx = self.nodes[0].gettransaction(peg_id)["hex"] peg_txid = self.nodes[0].sendrawtransaction(raw_peg_tx) self.nodes[0].generate(101) peg_prf = self.nodes[0].gettxoutproof([peg_txid]) claim_script = fund_info["claim_script"] raw_claim = self.nodes[0].createrawpegin(raw_peg_tx, peg_prf, claim_script) tx = FromHex(CTransaction(), raw_claim['hex']) else: tx = CTransaction() tx.nVersion = ver tx.nLockTime = locktime # Spend the pegin and taproot tx together in_total = prev_tx.vout[prev_vout].nValue.getAmount() fees = 1000 tap_in_pos = 0 if blind: # Add an unrelated output key = ECKey() key.generate() tx.vout.append( CTxOut(nValue=CTxOutValue(10000), scriptPubKey=spk, nNonce=CTxOutNonce(key.get_pubkey().get_bytes()))) tx_hex = self.nodes[0].fundrawtransaction(tx.serialize().hex()) tx = FromHex(CTransaction(), tx_hex['hex']) tx.vin.append( CTxIn(COutPoint(prev_tx.sha256, prev_vout), nSequence=seq)) tx.vout.append( CTxOut(nValue=CTxOutValue(in_total - fees), scriptPubKey=spk)) # send back to self tx.vout.append(CTxOut(CTxOutValue(fees))) if add_issuance: blind_addr = self.nodes[0].getnewaddress() issue_addr = self.nodes[0].validateaddress( blind_addr)['unconfidential'] # Issuances only require one fee output and that output must the last # one. However the way, the current code is structured, it is not possible # to this in a super clean without special casing. if add_pegin: tx.vout.pop() tx.vout.pop() tx.vout.insert(0, CTxOut(nValue=CTxOutValue(in_total), scriptPubKey=spk)) # send back to self) issued_tx = self.nodes[0].rawissueasset( tx.serialize().hex(), [{ "asset_amount": 2, "asset_address": issue_addr, "blind": False }])[0]["hex"] tx = FromHex(CTransaction(), issued_tx) # Sign inputs if add_pegin: signed = self.nodes[0].signrawtransactionwithwallet( tx.serialize().hex()) tx = FromHex(CTransaction(), signed['hex']) tap_in_pos += 1 else: # Need to create empty witness when not deserializing from rpc tx.wit.vtxinwit.append(CTxInWitness()) if blind: tx.vin[0], tx.vin[1] = tx.vin[1], tx.vin[0] utxo = self.get_utxo(tx, 1) zero_str = "0" * 64 blinded_raw = self.nodes[0].rawblindrawtransaction( tx.serialize().hex(), [zero_str, utxo["amountblinder"]], [1.2, utxo['amount']], [utxo['asset'], utxo['asset']], [zero_str, utxo['assetblinder']]) tx = FromHex(CTransaction(), blinded_raw) signed_raw_tx = self.nodes[0].signrawtransactionwithwallet( tx.serialize().hex()) tx = FromHex(CTransaction(), signed_raw_tx['hex']) suffix_annex = [] control_block = bytes([ tap.leaves["s0"].version + tap.negflag ]) + tap.inner_pubkey + tap.leaves["s0"].merklebranch # Add the prevout to the top of inputs. The witness script will check for equality. if add_prevout: inputs = [ prev_vout.to_bytes(4, 'little'), ser_uint256(prev_tx.sha256) ] if add_asset: assert blind # only used with blinding in testing utxo = self.nodes[0].gettxout( ser_uint256(tx.vin[1].prevout.hash)[::-1].hex(), tx.vin[1].prevout.n) if "assetcommitment" in utxo: asset = bytes.fromhex(utxo["assetcommitment"]) else: asset = b"\x01" + bytes.fromhex(utxo["asset"])[::-1] inputs = [asset[0:1], asset[1:33]] if add_value: utxo = self.nodes[0].gettxout( ser_uint256(tx.vin[1].prevout.hash)[::-1].hex(), tx.vin[1].prevout.n) if "valuecommitment" in utxo: value = bytes.fromhex(utxo["valuecommitment"]) inputs = [value[0:1], value[1:33]] else: value = b"\x01" + int( satoshi_round(utxo["value"]) * COIN).to_bytes(8, 'little') inputs = [value[0:1], value[1:9]] if add_spk: ver = CScriptOp.decode_op_n(int.from_bytes(spk[0:1], 'little')) inputs = [CScriptNum.encode(CScriptNum(ver))[1:], spk[2:len(spk)]] # always segwit # Add witness for outputs if add_out_asset is not None: asset = tx.vout[add_out_asset].nAsset.vchCommitment inputs = [asset[0:1], asset[1:33]] if add_out_value is not None: value = tx.vout[add_out_value].nValue.vchCommitment if len(value) == 9: inputs = [value[0:1], value[1:9][::-1]] else: inputs = [value[0:1], value[1:33]] if add_out_nonce is not None: nonce = tx.vout[add_out_nonce].nNonce.vchCommitment if len(nonce) == 1: inputs = [b''] else: inputs = [nonce] if add_out_spk is not None: out_spk = tx.vout[add_out_spk].scriptPubKey if len(out_spk) == 0: # Python upstream encoding CScriptNum interesting behaviour where it also encodes the length # This assumes the implicit wallet behaviour of using segwit outputs. # This is useful while sending scripts, but not while using CScriptNums in constructing scripts inputs = [ CScriptNum.encode(CScriptNum(-1))[1:], sha256(out_spk) ] else: ver = CScriptOp.decode_op_n( int.from_bytes(out_spk[0:1], 'little')) inputs = [ CScriptNum.encode(CScriptNum(ver))[1:], out_spk[2:len(out_spk)] ] # always segwit if add_num_outputs: num_outs = len(tx.vout) inputs = [CScriptNum.encode(CScriptNum(num_outs))[1:]] if add_weight: # Add a dummy input and check the overall weight inputs = [int(5).to_bytes(8, 'little')] wit = inputs + [bytes(tap.leaves["s0"].script), control_block ] + suffix_annex tx.wit.vtxinwit[tap_in_pos].scriptWitness.stack = wit exp_weight = self.nodes[0].decoderawtransaction( tx.serialize().hex())["weight"] inputs = [exp_weight.to_bytes(8, 'little')] wit = inputs + [bytes(tap.leaves["s0"].script), control_block ] + suffix_annex tx.wit.vtxinwit[tap_in_pos].scriptWitness.stack = wit if fail: assert_raises_rpc_error(-26, fail, self.nodes[0].sendrawtransaction, tx.serialize().hex()) return self.nodes[0].sendrawtransaction(hexstring=tx.serialize().hex()) self.nodes[0].generate(1) last_blk = self.nodes[0].getblock(self.nodes[0].getbestblockhash()) tx.rehash() assert (tx.hash in last_blk['tx'])
def run_test(self): parent = self.nodes[0] #parent2 = self.nodes[1] sidechain = self.nodes[2] sidechain2 = self.nodes[3] for node in self.nodes: node.importprivkey(privkey=node.get_deterministic_priv_key().key, label="mining") util.node_fastmerkle = sidechain parent.generate(101) sidechain.generate(101) self.log.info("sidechain info: {}".format(sidechain.getsidechaininfo())) addrs = sidechain.getpeginaddress() addr = addrs["mainchain_address"] assert_equal(sidechain.decodescript(addrs["claim_script"])["type"], "witness_v0_keyhash") txid1 = parent.sendtoaddress(addr, 24) # 10+2 confirms required to get into mempool and confirm parent.generate(1) time.sleep(2) proof = parent.gettxoutproof([txid1]) raw = parent.gettransaction(txid1)["hex"] print("Attempting peg-ins") # First attempt fails the consensus check but gives useful result try: pegtxid = sidechain.claimpegin(raw, proof) raise Exception("Peg-in should not be mature enough yet, need another block.") except JSONRPCException as e: assert("Peg-in Bitcoin transaction needs more confirmations to be sent." in e.error["message"]) # Second attempt simply doesn't hit mempool bar parent.generate(10) try: pegtxid = sidechain.claimpegin(raw, proof) raise Exception("Peg-in should not be mature enough yet, need another block.") except JSONRPCException as e: assert("Peg-in Bitcoin transaction needs more confirmations to be sent." in e.error["message"]) try: pegtxid = sidechain.createrawpegin(raw, proof, 'AEIOU') raise Exception("Peg-in with non-hex claim_script should fail.") except JSONRPCException as e: assert("Given claim_script is not hex." in e.error["message"]) # Should fail due to non-matching wallet address try: scriptpubkey = sidechain.getaddressinfo(get_new_unconfidential_address(sidechain))["scriptPubKey"] pegtxid = sidechain.claimpegin(raw, proof, scriptpubkey) raise Exception("Peg-in with non-matching claim_script should fail.") except JSONRPCException as e: assert("Given claim_script does not match the given Bitcoin transaction." in e.error["message"]) # 12 confirms allows in mempool parent.generate(1) # Make sure that a tx with a duplicate pegin claim input gets rejected. raw_pegin = sidechain.createrawpegin(raw, proof)["hex"] raw_pegin = FromHex(CTransaction(), raw_pegin) raw_pegin.vin.append(raw_pegin.vin[0]) # duplicate the pegin input raw_pegin = sidechain.signrawtransactionwithwallet(raw_pegin.serialize().hex())["hex"] assert_raises_rpc_error(-26, "bad-txns-inputs-duplicate", sidechain.sendrawtransaction, raw_pegin) # Also try including this tx in a block manually and submitting it. doublespendblock = FromHex(CBlock(), sidechain.getnewblockhex()) doublespendblock.vtx.append(FromHex(CTransaction(), raw_pegin)) doublespendblock.hashMerkleRoot = doublespendblock.calc_merkle_root() add_witness_commitment(doublespendblock) doublespendblock.solve() block_hex = bytes_to_hex_str(doublespendblock.serialize(True)) assert_raises_rpc_error(-25, "bad-txns-inputs-duplicate", sidechain.testproposedblock, block_hex, True) # Should succeed via wallet lookup for address match, and when given raw_pegin = sidechain.createrawpegin(raw, proof)['hex'] signed_pegin = sidechain.signrawtransactionwithwallet(raw_pegin) sample_pegin_struct = FromHex(CTransaction(), signed_pegin["hex"]) # Round-trip peg-in transaction using python serialization assert_equal(signed_pegin["hex"], sample_pegin_struct.serialize().hex()) # Store this for later (evil laugh) sample_pegin_witness = sample_pegin_struct.wit.vtxinwit[0].peginWitness pegtxid1 = sidechain.claimpegin(raw, proof) # Make sure a second pegin claim does not get accepted in the mempool when # another mempool tx already claims that pegin. assert_raises_rpc_error(-4, "txn-mempool-conflict", sidechain.claimpegin, raw, proof) # Will invalidate the block that confirms this transaction later self.sync_all(self.node_groups) blockhash = sidechain2.generate(1) self.sync_all(self.node_groups) sidechain.generate(5) tx1 = sidechain.gettransaction(pegtxid1) if "confirmations" in tx1 and tx1["confirmations"] == 6: print("Peg-in is confirmed: Success!") else: raise Exception("Peg-in confirmation has failed.") # Look at pegin fields decoded = sidechain.decoderawtransaction(tx1["hex"]) assert decoded["vin"][0]["is_pegin"] == True assert len(decoded["vin"][0]["pegin_witness"]) > 0 # Check that there's sufficient fee for the peg-in vsize = decoded["vsize"] fee_output = decoded["vout"][1] fallbackfee_pervbyte = Decimal("0.00001")/Decimal("1000") assert fee_output["scriptPubKey"]["type"] == "fee" assert fee_output["value"] >= fallbackfee_pervbyte*vsize # Quick reorg checks of pegs sidechain.invalidateblock(blockhash[0]) if sidechain.gettransaction(pegtxid1)["confirmations"] != 0: raise Exception("Peg-in didn't unconfirm after invalidateblock call.") # Create duplicate claim, put it in block along with current one in mempool # to test duplicate-in-block claims between two txs that are in the same block. raw_pegin = sidechain.createrawpegin(raw, proof)["hex"] raw_pegin = sidechain.signrawtransactionwithwallet(raw_pegin)["hex"] raw_pegin = FromHex(CTransaction(), raw_pegin) doublespendblock = FromHex(CBlock(), sidechain.getnewblockhex()) assert(len(doublespendblock.vtx) == 2) # coinbase and pegin doublespendblock.vtx.append(raw_pegin) doublespendblock.hashMerkleRoot = doublespendblock.calc_merkle_root() add_witness_commitment(doublespendblock) doublespendblock.solve() block_hex = bytes_to_hex_str(doublespendblock.serialize(True)) assert_raises_rpc_error(-25, "bad-txns-double-pegin", sidechain.testproposedblock, block_hex, True) # Re-enters block sidechain.generate(1) if sidechain.gettransaction(pegtxid1)["confirmations"] != 1: raise Exception("Peg-in should have one confirm on side block.") sidechain.reconsiderblock(blockhash[0]) if sidechain.gettransaction(pegtxid1)["confirmations"] != 6: raise Exception("Peg-in should be back to 6 confirms.") # Now the pegin is already claimed in a confirmed tx. # In that case, a duplicate claim should (1) not be accepted in the mempool # and (2) not be accepted in a block. assert_raises_rpc_error(-4, "pegin-already-claimed", sidechain.claimpegin, raw, proof) # For case (2), manually craft a block and include the tx. doublespendblock = FromHex(CBlock(), sidechain.getnewblockhex()) doublespendblock.vtx.append(raw_pegin) doublespendblock.hashMerkleRoot = doublespendblock.calc_merkle_root() add_witness_commitment(doublespendblock) doublespendblock.solve() block_hex = bytes_to_hex_str(doublespendblock.serialize(True)) assert_raises_rpc_error(-25, "bad-txns-double-pegin", sidechain.testproposedblock, block_hex, True) # Do multiple claims in mempool n_claims = 6 print("Flooding mempool with a few claims") pegtxs = [] sidechain.generate(101) # Do mixture of raw peg-in and automatic peg-in tx construction # where raw creation is done on another node for i in range(n_claims): addrs = sidechain.getpeginaddress() txid = parent.sendtoaddress(addrs["mainchain_address"], 1) parent.generate(1) proof = parent.gettxoutproof([txid]) raw = parent.gettransaction(txid)["hex"] if i % 2 == 0: parent.generate(11) pegtxs += [sidechain.claimpegin(raw, proof)] else: # The raw API doesn't check for the additional 2 confirmation buffer # So we only get 10 confirms then send off. Miners will add to block anyways. # Don't mature whole way yet to test signing immature peg-in input parent.generate(8) # Wallet in sidechain2 gets funds instead of sidechain raw_pegin = sidechain2.createrawpegin(raw, proof, addrs["claim_script"])["hex"] # First node should also be able to make a valid transaction with or without 3rd arg # since this wallet originated the claim_script itself sidechain.createrawpegin(raw, proof, addrs["claim_script"]) sidechain.createrawpegin(raw, proof) signed_pegin = sidechain.signrawtransactionwithwallet(raw_pegin) assert(signed_pegin["complete"]) assert("warning" in signed_pegin) # warning for immature peg-in # fully mature them now parent.generate(1) pegtxs += [sidechain.sendrawtransaction(signed_pegin["hex"])] self.sync_all(self.node_groups) sidechain2.generate(1) for i, pegtxid in enumerate(pegtxs): if i % 2 == 0: tx = sidechain.gettransaction(pegtxid) else: tx = sidechain2.gettransaction(pegtxid) if "confirmations" not in tx or tx["confirmations"] == 0: raise Exception("Peg-in confirmation has failed.") print("Test pegouts") self.test_pegout(get_new_unconfidential_address(parent, "legacy"), sidechain) self.test_pegout(get_new_unconfidential_address(parent, "p2sh-segwit"), sidechain) self.test_pegout(get_new_unconfidential_address(parent, "bech32"), sidechain) print("Test pegout P2SH") parent_chain_addr = get_new_unconfidential_address(parent) parent_pubkey = parent.getaddressinfo(parent_chain_addr)["pubkey"] parent_chain_p2sh_addr = parent.createmultisig(1, [parent_pubkey])["address"] self.test_pegout(parent_chain_p2sh_addr, sidechain) print("Test pegout Garbage") parent_chain_addr = "garbage" try: self.test_pegout(parent_chain_addr, sidechain) raise Exception("A garbage address should fail.") except JSONRPCException as e: assert("Invalid Bitcoin address" in e.error["message"]) print("Test pegout Garbage valid") prev_txid = sidechain.sendtoaddress(sidechain.getnewaddress(), 1) sidechain.generate(1) pegout_chain = 'a' * 64 pegout_hex = 'b' * 500 inputs = [{"txid": prev_txid, "vout": 0}] outputs = {"vdata": [pegout_chain, pegout_hex]} rawtx = sidechain.createrawtransaction(inputs, outputs) raw_pegout = sidechain.decoderawtransaction(rawtx) assert 'vout' in raw_pegout and len(raw_pegout['vout']) > 0 pegout_tested = False for output in raw_pegout['vout']: scriptPubKey = output['scriptPubKey'] if 'type' in scriptPubKey and scriptPubKey['type'] == 'nulldata': assert ('pegout_hex' in scriptPubKey and 'pegout_asm' in scriptPubKey and 'pegout_type' in scriptPubKey) assert ('pegout_chain' in scriptPubKey and 'pegout_reqSigs' not in scriptPubKey and 'pegout_addresses' not in scriptPubKey) assert scriptPubKey['pegout_type'] == 'nonstandard' assert scriptPubKey['pegout_chain'] == pegout_chain assert scriptPubKey['pegout_hex'] == pegout_hex pegout_tested = True break assert pegout_tested print("Now test failure to validate peg-ins based on intermittent bitcoind rpc failure") self.stop_node(1) txid = parent.sendtoaddress(addr, 1) parent.generate(12) proof = parent.gettxoutproof([txid]) raw = parent.gettransaction(txid)["hex"] sidechain.claimpegin(raw, proof) # stuck peg sidechain.generate(1) print("Waiting to ensure block is being rejected by sidechain2") time.sleep(5) assert(sidechain.getblockcount() != sidechain2.getblockcount()) print("Restarting parent2") self.start_node(1) connect_nodes_bi(self.nodes, 0, 1) # Don't make a block, race condition when pegin-invalid block # is awaiting further validation, nodes reject subsequent blocks # even ones they create print("Now waiting for node to re-evaluate peg-in witness failed block... should take a few seconds") self.sync_all(self.node_groups) print("Completed!\n") print("Now send funds out in two stages, partial, and full") some_btc_addr = get_new_unconfidential_address(parent) bal_1 = sidechain.getwalletinfo()["balance"]['bitcoin'] try: sidechain.sendtomainchain(some_btc_addr, bal_1 + 1) raise Exception("Sending out too much; should have failed") except JSONRPCException as e: assert("Insufficient funds" in e.error["message"]) assert(sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) try: sidechain.sendtomainchain(some_btc_addr+"b", bal_1 - 1) raise Exception("Sending to invalid address; should have failed") except JSONRPCException as e: assert("Invalid Bitcoin address" in e.error["message"]) assert(sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) try: sidechain.sendtomainchain("1Nro9WkpaKm9axmcfPVp79dAJU1Gx7VmMZ", bal_1 - 1) raise Exception("Sending to mainchain address when should have been testnet; should have failed") except JSONRPCException as e: assert("Invalid Bitcoin address" in e.error["message"]) assert(sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) # Test superfluous peg-in witness data on regular spend before we have no funds raw_spend = sidechain.createrawtransaction([], {sidechain.getnewaddress():1}) fund_spend = sidechain.fundrawtransaction(raw_spend) sign_spend = sidechain.signrawtransactionwithwallet(fund_spend["hex"]) signed_struct = FromHex(CTransaction(), sign_spend["hex"]) # Non-witness tx has no witness serialized yet if len(signed_struct.wit.vtxinwit) == 0: signed_struct.wit.vtxinwit = [CTxInWitness()] signed_struct.wit.vtxinwit[0].peginWitness.stack = sample_pegin_witness.stack assert_equal(sidechain.testmempoolaccept([signed_struct.serialize().hex()])[0]["allowed"], False) assert_equal(sidechain.testmempoolaccept([signed_struct.serialize().hex()])[0]["reject-reason"], "68: extra-pegin-witness") signed_struct.wit.vtxinwit[0].peginWitness.stack = [b'\x00'*100000] # lol assert_equal(sidechain.testmempoolaccept([signed_struct.serialize().hex()])[0]["allowed"], False) assert_equal(sidechain.testmempoolaccept([signed_struct.serialize().hex()])[0]["reject-reason"], "68: extra-pegin-witness") peg_out_txid = sidechain.sendtomainchain(some_btc_addr, 1) peg_out_details = sidechain.decoderawtransaction(sidechain.getrawtransaction(peg_out_txid)) # peg-out, change, fee assert(len(peg_out_details["vout"]) == 3) found_pegout_value = False for output in peg_out_details["vout"]: if "value" in output and output["value"] == 1: found_pegout_value = True assert(found_pegout_value) bal_2 = sidechain.getwalletinfo()["balance"]["bitcoin"] # Make sure balance went down assert(bal_2 + 1 < bal_1) # Send rest of coins using subtractfee from output arg sidechain.sendtomainchain(some_btc_addr, bal_2, True) assert(sidechain.getwalletinfo()["balance"]['bitcoin'] == 0) print('Test coinbase peg-in maturity rules') # Have bitcoin output go directly into a claim output pegin_info = sidechain.getpeginaddress() mainchain_addr = pegin_info["mainchain_address"] # Watch the address so we can get tx without txindex parent.importaddress(mainchain_addr) claim_block = parent.generatetoaddress(50, mainchain_addr)[0] block_coinbase = parent.getblock(claim_block, 2)["tx"][0] claim_txid = block_coinbase["txid"] claim_tx = block_coinbase["hex"] claim_proof = parent.gettxoutproof([claim_txid], claim_block) # Can't claim something even though it has 50 confirms since it's coinbase assert_raises_rpc_error(-8, "Peg-in Bitcoin transaction needs more confirmations to be sent.", sidechain.claimpegin, claim_tx, claim_proof) # If done via raw API, still doesn't work coinbase_pegin = sidechain.createrawpegin(claim_tx, claim_proof) assert_equal(coinbase_pegin["mature"], False) signed_pegin = sidechain.signrawtransactionwithwallet(coinbase_pegin["hex"])["hex"] assert_raises_rpc_error(-26, "bad-pegin-witness, Needs more confirmations.", sidechain.sendrawtransaction, signed_pegin) # 50 more blocks to allow wallet to make it succeed by relay and consensus parent.generatetoaddress(50, parent.getnewaddress()) # Wallet still doesn't want to for 2 more confirms assert_equal(sidechain.createrawpegin(claim_tx, claim_proof)["mature"], False) # But we can just shoot it off claim_txid = sidechain.sendrawtransaction(signed_pegin) sidechain.generatetoaddress(1, sidechain.getnewaddress()) assert_equal(sidechain.gettransaction(claim_txid)["confirmations"], 1) print('Success!') # Manually stop sidechains first, then the parent chains. self.stop_node(2) self.stop_node(3) self.stop_node(0) self.stop_node(1)
def run_test(self): parent = self.nodes[0] #parent2 = self.nodes[1] sidechain = self.nodes[2] sidechain2 = self.nodes[3] parent.generate(101) sidechain.generate(101) addrs = sidechain.getpeginaddress() addr = addrs["mainchain_address"] assert_equal(sidechain.decodescript(addrs["claim_script"])["type"], "witness_v0_keyhash") txid1 = parent.sendtoaddress(addr, 24) # 10+2 confirms required to get into mempool and confirm parent.generate(1) time.sleep(2) proof = parent.gettxoutproof([txid1]) raw = parent.getrawtransaction(txid1) print("Attempting peg-ins") # First attempt fails the consensus check but gives useful result try: pegtxid = sidechain.claimpegin(raw, proof) raise Exception("Peg-in should not be mature enough yet, need another block.") except JSONRPCException as e: assert("Peg-in Bitcoin transaction needs more confirmations to be sent." in e.error["message"]) # Second attempt simply doesn't hit mempool bar parent.generate(10) try: pegtxid = sidechain.claimpegin(raw, proof) raise Exception("Peg-in should not be mature enough yet, need another block.") except JSONRPCException as e: assert("Peg-in Bitcoin transaction needs more confirmations to be sent." in e.error["message"]) try: pegtxid = sidechain.createrawpegin(raw, proof, 'AEIOU') raise Exception("Peg-in with non-hex claim_script should fail.") except JSONRPCException as e: assert("Given claim_script is not hex." in e.error["message"]) # Should fail due to non-matching wallet address try: scriptpubkey = sidechain.getaddressinfo(get_new_unconfidential_address(sidechain))["scriptPubKey"] pegtxid = sidechain.claimpegin(raw, proof, scriptpubkey) raise Exception("Peg-in with non-matching claim_script should fail.") except JSONRPCException as e: assert("Given claim_script does not match the given Bitcoin transaction." in e.error["message"]) # 12 confirms allows in mempool parent.generate(1) # Should succeed via wallet lookup for address match, and when given raw_pegin = sidechain.createrawpegin(raw, proof)['hex'] signed_pegin = sidechain.signrawtransactionwithwallet(raw_pegin) sample_pegin_struct = FromHex(CTransaction(), signed_pegin["hex"]) # Round-trip peg-in transaction using python serialization assert_equal(signed_pegin["hex"], sample_pegin_struct.serialize().hex()) # Store this for later (evil laugh) sample_pegin_witness = sample_pegin_struct.wit.vtxinwit[0].peginWitness pegtxid1 = sidechain.claimpegin(raw, proof) # Will invalidate the block that confirms this transaction later self.sync_all(self.node_groups) blockhash = sidechain2.generate(1) self.sync_all(self.node_groups) sidechain.generate(5) tx1 = sidechain.gettransaction(pegtxid1) if "confirmations" in tx1 and tx1["confirmations"] == 6: print("Peg-in is confirmed: Success!") else: raise Exception("Peg-in confirmation has failed.") # Look at pegin fields decoded = sidechain.decoderawtransaction(tx1["hex"]) assert decoded["vin"][0]["is_pegin"] == True assert len(decoded["vin"][0]["pegin_witness"]) > 0 # Check that there's sufficient fee for the peg-in vsize = decoded["vsize"] fee_output = decoded["vout"][1] fallbackfee_pervbyte = Decimal("0.00001")/Decimal("1000") assert fee_output["scriptPubKey"]["type"] == "fee" assert fee_output["value"] >= fallbackfee_pervbyte*vsize # Quick reorg checks of pegs sidechain.invalidateblock(blockhash[0]) if sidechain.gettransaction(pegtxid1)["confirmations"] != 0: raise Exception("Peg-in didn't unconfirm after invalidateblock call.") # Re-enters block sidechain.generate(1) if sidechain.gettransaction(pegtxid1)["confirmations"] != 1: raise Exception("Peg-in should have one confirm on side block.") sidechain.reconsiderblock(blockhash[0]) if sidechain.gettransaction(pegtxid1)["confirmations"] != 6: raise Exception("Peg-in should be back to 6 confirms.") # Do multiple claims in mempool n_claims = 6 print("Flooding mempool with a few claims") pegtxs = [] sidechain.generate(101) # Do mixture of raw peg-in and automatic peg-in tx construction # where raw creation is done on another node for i in range(n_claims): addrs = sidechain.getpeginaddress() txid = parent.sendtoaddress(addrs["mainchain_address"], 1) parent.generate(1) proof = parent.gettxoutproof([txid]) raw = parent.getrawtransaction(txid) if i % 2 == 0: parent.generate(11) pegtxs += [sidechain.claimpegin(raw, proof)] else: # The raw API doesn't check for the additional 2 confirmation buffer # So we only get 10 confirms then send off. Miners will add to block anyways. # Don't mature whole way yet to test signing immature peg-in input parent.generate(8) # Wallet in sidechain2 gets funds instead of sidechain raw_pegin = sidechain2.createrawpegin(raw, proof, addrs["claim_script"])["hex"] # First node should also be able to make a valid transaction with or without 3rd arg # since this wallet originated the claim_script itself sidechain.createrawpegin(raw, proof, addrs["claim_script"]) sidechain.createrawpegin(raw, proof) signed_pegin = sidechain.signrawtransactionwithwallet(raw_pegin) assert(signed_pegin["complete"]) assert("warning" in signed_pegin) # warning for immature peg-in # fully mature them now parent.generate(1) pegtxs += [sidechain.sendrawtransaction(signed_pegin["hex"])] self.sync_all(self.node_groups) sidechain2.generate(1) for i, pegtxid in enumerate(pegtxs): if i % 2 == 0: tx = sidechain.gettransaction(pegtxid) else: tx = sidechain2.gettransaction(pegtxid) if "confirmations" not in tx or tx["confirmations"] == 0: raise Exception("Peg-in confirmation has failed.") print("Test pegouts") self.test_pegout(get_new_unconfidential_address(parent, "legacy"), sidechain) self.test_pegout(get_new_unconfidential_address(parent, "p2sh-segwit"), sidechain) self.test_pegout(get_new_unconfidential_address(parent, "bech32"), sidechain) print("Test pegout P2SH") parent_chain_addr = get_new_unconfidential_address(parent) parent_pubkey = parent.getaddressinfo(parent_chain_addr)["pubkey"] parent_chain_p2sh_addr = parent.createmultisig(1, [parent_pubkey])["address"] self.test_pegout(parent_chain_p2sh_addr, sidechain) print("Test pegout Garbage") parent_chain_addr = "garbage" try: self.test_pegout(parent_chain_addr, sidechain) raise Exception("A garbage address should fail.") except JSONRPCException as e: assert("Invalid Bitcoin address" in e.error["message"]) print("Test pegout Garbage valid") prev_txid = sidechain.sendtoaddress(sidechain.getnewaddress(), 1) sidechain.generate(1) pegout_chain = 'a' * 64 pegout_hex = 'b' * 500 inputs = [{"txid": prev_txid, "vout": 0}] outputs = {"vdata": [pegout_chain, pegout_hex]} rawtx = sidechain.createrawtransaction(inputs, outputs) raw_pegout = sidechain.decoderawtransaction(rawtx) assert 'vout' in raw_pegout and len(raw_pegout['vout']) > 0 pegout_tested = False for output in raw_pegout['vout']: scriptPubKey = output['scriptPubKey'] if 'type' in scriptPubKey and scriptPubKey['type'] == 'nulldata': assert ('pegout_hex' in scriptPubKey and 'pegout_asm' in scriptPubKey and 'pegout_type' in scriptPubKey) assert ('pegout_chain' in scriptPubKey and 'pegout_reqSigs' not in scriptPubKey and 'pegout_addresses' not in scriptPubKey) assert scriptPubKey['pegout_type'] == 'nonstandard' assert scriptPubKey['pegout_chain'] == pegout_chain assert scriptPubKey['pegout_hex'] == pegout_hex pegout_tested = True break assert pegout_tested print("Now test failure to validate peg-ins based on intermittent bitcoind rpc failure") self.stop_node(1) txid = parent.sendtoaddress(addr, 1) parent.generate(12) proof = parent.gettxoutproof([txid]) raw = parent.getrawtransaction(txid) sidechain.claimpegin(raw, proof) # stuck peg sidechain.generate(1) print("Waiting to ensure block is being rejected by sidechain2") time.sleep(5) assert(sidechain.getblockcount() != sidechain2.getblockcount()) print("Restarting parent2") self.start_node(1) connect_nodes_bi(self.nodes, 0, 1) # Don't make a block, race condition when pegin-invalid block # is awaiting further validation, nodes reject subsequent blocks # even ones they create print("Now waiting for node to re-evaluate peg-in witness failed block... should take a few seconds") self.sync_all(self.node_groups) print("Completed!\n") print("Now send funds out in two stages, partial, and full") some_btc_addr = get_new_unconfidential_address(parent) bal_1 = sidechain.getwalletinfo()["balance"]['bitcoin'] try: sidechain.sendtomainchain(some_btc_addr, bal_1 + 1) raise Exception("Sending out too much; should have failed") except JSONRPCException as e: assert("Insufficient funds" in e.error["message"]) assert(sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) try: sidechain.sendtomainchain(some_btc_addr+"b", bal_1 - 1) raise Exception("Sending to invalid address; should have failed") except JSONRPCException as e: assert("Invalid Bitcoin address" in e.error["message"]) assert(sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) try: sidechain.sendtomainchain("1Nro9WkpaKm9axmcfPVp79dAJU1Gx7VmMZ", bal_1 - 1) raise Exception("Sending to mainchain address when should have been testnet; should have failed") except JSONRPCException as e: assert("Invalid Bitcoin address" in e.error["message"]) assert(sidechain.getwalletinfo()["balance"]["bitcoin"] == bal_1) # Test superfluous peg-in witness data on regular spend before we have no funds raw_spend = sidechain.createrawtransaction([], {sidechain.getnewaddress():1}) fund_spend = sidechain.fundrawtransaction(raw_spend) sign_spend = sidechain.signrawtransactionwithwallet(fund_spend["hex"]) signed_struct = FromHex(CTransaction(), sign_spend["hex"]) # Non-witness tx has no witness serialized yet if len(signed_struct.wit.vtxinwit) == 0: signed_struct.wit.vtxinwit = [CTxInWitness()] signed_struct.wit.vtxinwit[0].peginWitness.stack = sample_pegin_witness.stack assert_equal(sidechain.testmempoolaccept([signed_struct.serialize().hex()])[0]["allowed"], False) assert_equal(sidechain.testmempoolaccept([signed_struct.serialize().hex()])[0]["reject-reason"], "68: extra-pegin-witness") signed_struct.wit.vtxinwit[0].peginWitness.stack = [b'\x00'*100000] # lol assert_equal(sidechain.testmempoolaccept([signed_struct.serialize().hex()])[0]["allowed"], False) assert_equal(sidechain.testmempoolaccept([signed_struct.serialize().hex()])[0]["reject-reason"], "68: extra-pegin-witness") peg_out_txid = sidechain.sendtomainchain(some_btc_addr, 1) peg_out_details = sidechain.decoderawtransaction(sidechain.getrawtransaction(peg_out_txid)) # peg-out, change, fee assert(len(peg_out_details["vout"]) == 3) found_pegout_value = False for output in peg_out_details["vout"]: if "value" in output and output["value"] == 1: found_pegout_value = True assert(found_pegout_value) bal_2 = sidechain.getwalletinfo()["balance"]["bitcoin"] # Make sure balance went down assert(bal_2 + 1 < bal_1) # Send rest of coins using subtractfee from output arg sidechain.sendtomainchain(some_btc_addr, bal_2, True) assert(sidechain.getwalletinfo()["balance"]['bitcoin'] == 0) print('Test coinbase peg-in maturity rules') # Have bitcoin output go directly into a claim output pegin_info = sidechain.getpeginaddress() mainchain_addr = pegin_info["mainchain_address"] # Watch the address so we can get tx without txindex parent.importaddress(mainchain_addr) claim_block = parent.generatetoaddress(50, mainchain_addr)[0] block_coinbase = parent.getblock(claim_block, 2)["tx"][0] claim_txid = block_coinbase["txid"] claim_tx = block_coinbase["hex"] claim_proof = parent.gettxoutproof([claim_txid], claim_block) # Can't claim something even though it has 50 confirms since it's coinbase assert_raises_rpc_error(-8, "Peg-in Bitcoin transaction needs more confirmations to be sent.", sidechain.claimpegin, claim_tx, claim_proof) # If done via raw API, still doesn't work coinbase_pegin = sidechain.createrawpegin(claim_tx, claim_proof) assert_equal(coinbase_pegin["mature"], False) signed_pegin = sidechain.signrawtransactionwithwallet(coinbase_pegin["hex"])["hex"] assert_raises_rpc_error(-26, "bad-pegin-witness, Needs more confirmations.", sidechain.sendrawtransaction, signed_pegin) # 50 more blocks to allow wallet to make it succeed by relay and consensus parent.generatetoaddress(50, parent.getnewaddress()) # Wallet still doesn't want to for 2 more confirms assert_equal(sidechain.createrawpegin(claim_tx, claim_proof)["mature"], False) # But we can just shoot it off claim_txid = sidechain.sendrawtransaction(signed_pegin) sidechain.generatetoaddress(1, sidechain.getnewaddress()) assert_equal(sidechain.gettransaction(claim_txid)["confirmations"], 1) print('Success!') # Manually stop sidechains first, then the parent chains. self.stop_node(2) self.stop_node(3) self.stop_node(0) self.stop_node(1)
def run_test(self): # Turn off node 1 while node 0 mines blocks to generate stakes, # so that we can later try starting node 1 with an orphan proof. self.stop_node(1) node = self.nodes[0] addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(100, addrkey0.address) self.log.info( "Make build a valid proof and restart the node to use it") privkey = ECKey() privkey.set( bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747" ), True) def get_hex_pubkey(privkey): return privkey.get_pubkey().get_bytes().hex() proof_master = get_hex_pubkey(privkey) proof_sequence = 11 proof_expiration = 12 stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) proof = node.buildavalancheproof(proof_sequence, proof_expiration, proof_master, stakes) self.log.info("Test decodeavalancheproof RPC") proofobj = FromHex(AvalancheProof(), proof) decodedproof = node.decodeavalancheproof(proof) limited_id_hex = f"{proofobj.limited_proofid:0{64}x}" assert_equal(decodedproof["sequence"], proof_sequence) assert_equal(decodedproof["expiration"], proof_expiration) assert_equal(decodedproof["master"], proof_master) assert_equal(decodedproof["proofid"], f"{proofobj.proofid:0{64}x}") assert_equal(decodedproof["limitedid"], limited_id_hex) assert_equal(decodedproof["stakes"][0]["txid"], stakes[0]["txid"]) assert_equal(decodedproof["stakes"][0]["vout"], stakes[0]["vout"]) assert_equal(decodedproof["stakes"][0]["height"], stakes[0]["height"]) assert_equal(decodedproof["stakes"][0]["iscoinbase"], stakes[0]["iscoinbase"]) assert_equal(decodedproof["stakes"][0]["signature"], base64.b64encode(proofobj.stakes[0].sig).decode("ascii")) # Invalid hex (odd number of hex digits) assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", node.decodeavalancheproof, proof[:-1]) # Valid hex but invalid proof assert_raises_rpc_error(-22, "Proof has invalid format", node.decodeavalancheproof, proof[:-2]) # Restart the node with this proof self.restart_node( 0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) self.log.info("The proof is registered at first chaintip update") assert_equal(len(node.getavalanchepeerinfo()), 0) node.generate(1) wait_until(lambda: len(node.getavalanchepeerinfo()) == 1, timeout=5) # This case will occur for users building proofs with a third party # tool and then starting a new node that is not yet aware of the # transactions used for stakes. self.log.info("Start a node with an orphan proof") self.start_node( 1, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) # Mine a block to trigger an attempt at registering the proof self.nodes[1].generate(1) wait_for_proof(self.nodes[1], f"{proofobj.proofid:0{64}x}", expect_orphan=True) self.log.info("Connect to an up-to-date node to unorphan the proof") connect_nodes(self.nodes[1], node) self.sync_all() wait_for_proof(self.nodes[1], f"{proofobj.proofid:0{64}x}", expect_orphan=False) self.log.info("Generate delegations for the proof") # Stack up a few delegation levels def gen_privkey(): pk = ECKey() pk.generate() return pk delegator_privkey = privkey delegation = None for _ in range(10): delegated_privkey = gen_privkey() delegation = node.delegateavalancheproof( limited_id_hex, bytes_to_wif(delegator_privkey.get_bytes()), get_hex_pubkey(delegated_privkey), delegation, ) delegator_privkey = delegated_privkey random_privkey = gen_privkey() random_pubkey = get_hex_pubkey(random_privkey) # Invalid proof no_stake = node.buildavalancheproof(proof_sequence, proof_expiration, proof_master, []) # Invalid privkey assert_raises_rpc_error( -5, "The private key is invalid", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(bytes(32)), random_pubkey, ) # Invalid delegation bad_dg = AvalancheDelegation() assert_raises_rpc_error( -8, "The delegation does not match the proof", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Still invalid, but with a matching proofid bad_dg.limited_proofid = proofobj.limited_proofid bad_dg.proof_master = proofobj.master bad_dg.levels = [AvalancheDelegationLevel()] assert_raises_rpc_error( -8, "The delegation is invalid", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Wrong privkey, match the proof but does not match the delegation assert_raises_rpc_error( -5, "The private key does not match the delegation", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, delegation, ) # Delegation not hex assert_raises_rpc_error( -22, "Delegation must be an hexadecimal string.", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, "f00", ) # Delegation is hex but ill-formed assert_raises_rpc_error( -22, "Delegation has invalid format", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, "dead", ) # Test invalid proofs dust = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0")) dust_amount = Decimal(f"{PROOF_DUST_THRESHOLD * 0.9999:.4f}") dust2 = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount=str(dust_amount))) duplicate_stake = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]] * 2, addrkey0.key)) missing_stake = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10000000, 'height': 42, 'iscoinbase': False, 'privatekey': addrkey0.key, }]) bad_sig = ( "0b000000000000000c0000000000000021030b4c866585dd868a9d62348" "a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085" "583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20" "52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3" "8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0" "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") self.log.info( "Check the verifyavalancheproof and sendavalancheproof RPCs") if self.is_wallet_compiled(): self.log.info( "Check a proof with the maximum number of UTXO is valid") new_blocks = node.generate(AVALANCHE_MAX_PROOF_STAKES // 10 + 1) # confirm the coinbase UTXOs node.generate(101) too_many_stakes = create_stakes(node, new_blocks, AVALANCHE_MAX_PROOF_STAKES + 1) maximum_stakes = too_many_stakes[:-1] good_proof = node.buildavalancheproof(proof_sequence, proof_expiration, proof_master, maximum_stakes) too_many_utxos = node.buildavalancheproof(proof_sequence, proof_expiration, proof_master, too_many_stakes) assert node.verifyavalancheproof(good_proof) for rpc in [node.verifyavalancheproof, node.sendavalancheproof]: assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", rpc, "f00") assert_raises_rpc_error(-22, "Proof has invalid format", rpc, "f00d") def check_rpc_failure(proof, message): assert_raises_rpc_error(-8, "The proof is invalid: " + message, rpc, proof) check_rpc_failure(no_stake, "no-stake") check_rpc_failure(dust, "amount-below-dust-threshold") check_rpc_failure(duplicate_stake, "duplicated-stake") check_rpc_failure(missing_stake, "utxo-missing-or-spent") check_rpc_failure(bad_sig, "invalid-signature") if self.is_wallet_compiled(): check_rpc_failure(too_many_utxos, "too-many-utxos") conflicting_utxo = node.buildavalancheproof(proof_sequence + 1, proof_expiration, proof_master, stakes) assert_raises_rpc_error( -8, "The proof has conflicting utxo with an existing proof", node.sendavalancheproof, conflicting_utxo) # Good proof assert node.verifyavalancheproof(proof) peer = node.add_p2p_connection(P2PInterface()) proofid = FromHex(AvalancheProof(), proof).proofid node.sendavalancheproof(proof) assert proofid in get_proof_ids(node) def inv_found(): with p2p_lock: return peer.last_message.get( "inv") and peer.last_message["inv"].inv[-1].hash == proofid wait_until(inv_found) self.log.info("Check the getrawproof RPC") raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], False) assert_raises_rpc_error(-8, "Proof not found", node.getrawavalancheproof, '0' * 64) # Orphan the proof by sending the stake raw_tx = node.createrawtransaction([{ "txid": stakes[-1]["txid"], "vout": 0 }], { ADDRESS_BCHREG_UNSPENDABLE: stakes[-1]["amount"] - Decimal('10000') }) signed_tx = node.signrawtransactionwithkey(raw_tx, [addrkey0.key]) node.sendrawtransaction(signed_tx["hex"]) node.generate(1) wait_until(lambda: proofid not in get_proof_ids(node)) raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], True) self.log.info("Bad proof should be rejected at startup") self.stop_node(0) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avasessionkey=0", ], expected_msg="Error: The avalanche session key is invalid.", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), ], expected_msg= "Error: The avalanche master key is missing for the avalanche proof.", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=0", ], expected_msg="Error: The avalanche master key is invalid.", ) def check_proof_init_error(proof, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: " + message, ) check_proof_init_error(no_stake, "The avalanche proof has no stake.") check_proof_init_error(dust, "The avalanche proof stake is too low.") check_proof_init_error(dust2, "The avalanche proof stake is too low.") check_proof_init_error(duplicate_stake, "The avalanche proof has duplicated stake.") check_proof_init_error( bad_sig, "The avalanche proof has invalid stake signatures.") if self.is_wallet_compiled(): # The too many utxos case creates a proof which is that large that it # cannot fit on the command line append_config(node.datadir, ["avaproof={}".format(too_many_utxos)]) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: The avalanche proof has too many utxos.", match=ErrorMatch.PARTIAL_REGEX, ) # Master private key mismatch random_privkey = ECKey() random_privkey.generate() node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(random_privkey.get_bytes())), ], expected_msg= "Error: The master key does not match the proof public key.", ) self.log.info("Bad delegation should be rejected at startup") def check_delegation_init_error(delegation, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avadelegation={}".format(delegation), "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(delegated_privkey.get_bytes())), ], expected_msg="Error: " + message, ) check_delegation_init_error( AvalancheDelegation().serialize().hex(), "The delegation does not match the proof.") bad_level_sig = FromHex(AvalancheDelegation(), delegation) # Tweak some key to cause the signature to mismatch bad_level_sig.levels[-2].pubkey = bytes.fromhex(proof_master) check_delegation_init_error( bad_level_sig.serialize().hex(), "The avalanche delegation has invalid signatures.") node.assert_start_raises_init_error( self.extra_args[0] + [ "-avadelegation={}".format(delegation), "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(random_privkey.get_bytes())), ], expected_msg= "Error: The master key does not match the delegation public key.", )
def mine_block(self, node, vtx=None, mn_payee=None, mn_amount=None, use_mnmerkleroot_from_tip=False, expected_error=None): if vtx is None: vtx = [] bt = node.getblocktemplate({'rules': ['segwit']}) height = bt['height'] tip_hash = bt['previousblockhash'] tip_block = node.getblock(tip_hash, 2)["tx"][0] coinbasevalue = 50 * COIN halvings = int(height / 150) # regtest coinbasevalue >>= halvings miner_script = self.nodes[0].getaddressinfo( self.nodes[0].getnewaddress())['scriptPubKey'] if mn_payee is None: if isinstance(bt['masternode'], list): mn_payee = bt['masternode'][0]['script'] else: mn_payee = bt['masternode']['script'] # we can't take the masternode payee amount from the template here as we might have additional fees in vtx new_fees = 0 for tx in vtx: in_value = 0 out_value = 0 for txin in tx.vin: txout = node.gettxout("%064x" % txin.prevout.hash, txin.prevout.n, False) in_value += int(txout['value'] * COIN) for txout in tx.vout: out_value += txout.nValue new_fees += in_value - out_value if mn_amount is None: mn_amount = get_masternode_payment( height, coinbasevalue, bt['masternode_collateral_height']) + new_fees / 2 miner_amount = int(coinbasevalue * 0.25) miner_amount += new_fees / 2 coinbase = CTransaction() coinbase.vout.append( CTxOut(int(miner_amount), hex_str_to_bytes(miner_script))) coinbase.vout.append(CTxOut(int(mn_amount), hex_str_to_bytes(mn_payee))) coinbase.vin = create_coinbase(height).vin # Recreate mn root as using one in BT would result in invalid merkle roots for masternode lists coinbase.nVersion = bt['version_coinbase'] if len(bt['default_witness_commitment_extra']) != 0: if use_mnmerkleroot_from_tip: cbtx = FromHex(CCbTx(version=2), bt['default_witness_commitment_extra']) if 'cbTx' in tip_block: cbtx.merkleRootMNList = int( tip_block['cbTx']['merkleRootMNList'], 16) else: cbtx.merkleRootMNList = 0 coinbase.extraData = cbtx.serialize() else: coinbase.extraData = hex_str_to_bytes( bt['default_witness_commitment_extra']) coinbase.calc_sha256(with_witness=True) block = create_block(int(tip_hash, 16), coinbase) block.nVersion = 4 block.vtx += vtx block.hashMerkleRoot = block.calc_merkle_root() add_witness_commitment(block) block.solve() result = node.submitblock(ToHex(block)) if expected_error is not None and result != expected_error: raise AssertionError( 'mining the block should have failed with error %s, but submitblock returned %s' % (expected_error, result)) elif expected_error is None and result is not None: raise AssertionError('submitblock returned %s' % (result))