def test_independent(self): self.log.info("Test multiple independent transactions in a package") node = self.nodes[0] # For independent transactions, order doesn't matter. self.assert_testres_equal(self.independent_txns_hex, self.independent_txns_testres) self.log.info("Test an otherwise valid package with an extra garbage tx appended") garbage_tx = node.createrawtransaction([{"txid": "00" * 32, "vout": 5}], {self.address: 1}) tx = CTransaction() tx.deserialize(BytesIO(hex_str_to_bytes(garbage_tx))) # Only the txid and wtxids are returned because validation is incomplete for the independent txns. # Package validation is atomic: if the node cannot find a UTXO for any single tx in the package, # it terminates immediately to avoid unnecessary, expensive signature verification. package_bad = self.independent_txns_hex + [garbage_tx] testres_bad = self.independent_txns_testres_blank + [{"txid": tx.rehash(), "wtxid": tx.getwtxid(), "allowed": False, "reject-reason": "missing-inputs"}] self.assert_testres_equal(package_bad, testres_bad) self.log.info("Check testmempoolaccept tells us when some transactions completed validation successfully") coin = self.coins.pop() tx_bad_sig_hex = node.createrawtransaction([{"txid": coin["txid"], "vout": 0}], {self.address : coin["amount"] - Decimal("0.0001")}) tx_bad_sig = CTransaction() tx_bad_sig.deserialize(BytesIO(hex_str_to_bytes(tx_bad_sig_hex))) testres_bad_sig = node.testmempoolaccept(self.independent_txns_hex + [tx_bad_sig_hex]) # By the time the signature for the last transaction is checked, all the other transactions # have been fully validated, which is why the node returns full validation results for all # transactions here but empty results in other cases. assert_equal(testres_bad_sig, self.independent_txns_testres + [{ "txid": tx_bad_sig.rehash(), "wtxid": tx_bad_sig.getwtxid(), "allowed": False, "reject-reason": "mandatory-script-verify-flag-failed (Operation not valid with the current stack size)" }]) self.log.info("Check testmempoolaccept reports txns in packages that exceed max feerate") coin = self.coins.pop() tx_high_fee_raw = node.createrawtransaction([{"txid": coin["txid"], "vout": 0}], {self.address : coin["amount"] - Decimal("0.999")}) tx_high_fee_signed = node.signrawtransactionwithkey(hexstring=tx_high_fee_raw, privkeys=self.privkeys) assert tx_high_fee_signed["complete"] tx_high_fee = CTransaction() tx_high_fee.deserialize(BytesIO(hex_str_to_bytes(tx_high_fee_signed["hex"]))) testres_high_fee = node.testmempoolaccept([tx_high_fee_signed["hex"]]) assert_equal(testres_high_fee, [ {"txid": tx_high_fee.rehash(), "wtxid": tx_high_fee.getwtxid(), "allowed": False, "reject-reason": "max-fee-exceeded"} ]) package_high_fee = [tx_high_fee_signed["hex"]] + self.independent_txns_hex testres_package_high_fee = node.testmempoolaccept(package_high_fee) assert_equal(testres_package_high_fee, testres_high_fee + self.independent_txns_testres_blank)
def test_conflicting(self): node = self.nodes[0] prevtx = self.coins.pop() inputs = [{"txid": prevtx["txid"], "vout": 0}] output1 = {node.get_deterministic_priv_key().address: 50 - 0.00125} output2 = {ADDRESS_BCRT1_P2WSH_OP_TRUE: 50 - 0.00125} # tx1 and tx2 share the same inputs rawtx1 = node.createrawtransaction(inputs, output1) rawtx2 = node.createrawtransaction(inputs, output2) signedtx1 = node.signrawtransactionwithkey(hexstring=rawtx1, privkeys=self.privkeys) signedtx2 = node.signrawtransactionwithkey(hexstring=rawtx2, privkeys=self.privkeys) tx1 = CTransaction() tx1.deserialize(BytesIO(hex_str_to_bytes(signedtx1["hex"]))) tx2 = CTransaction() tx2.deserialize(BytesIO(hex_str_to_bytes(signedtx2["hex"]))) assert signedtx1["complete"] assert signedtx2["complete"] # Ensure tx1 and tx2 are valid by themselves assert node.testmempoolaccept([signedtx1["hex"]])[0]["allowed"] assert node.testmempoolaccept([signedtx2["hex"]])[0]["allowed"] self.log.info("Test duplicate transactions in the same package") testres = node.testmempoolaccept([signedtx1["hex"], signedtx1["hex"]]) assert_equal(testres, [{ "txid": tx1.rehash(), "wtxid": tx1.getwtxid(), "package-error": "conflict-in-package" }, { "txid": tx1.rehash(), "wtxid": tx1.getwtxid(), "package-error": "conflict-in-package" }]) self.log.info("Test conflicting transactions in the same package") testres = node.testmempoolaccept([signedtx1["hex"], signedtx2["hex"]]) assert_equal(testres, [{ "txid": tx1.rehash(), "wtxid": tx1.getwtxid(), "package-error": "conflict-in-package" }, { "txid": tx2.rehash(), "wtxid": tx2.getwtxid(), "package-error": "conflict-in-package" }])
def create_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node=None, utxo_to_spend=None, mempool_valid=True, locktime=0, sequence=0): """Create and return a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed. Checking mempool validity via the testmempoolaccept RPC can be skipped by setting mempool_valid to False.""" from_node = from_node or self._test_node utxo_to_spend = utxo_to_spend or self.get_utxo() if self._priv_key is None: vsize = Decimal(104) # anyone-can-spend else: vsize = Decimal( 168 ) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other) send_value = int(COIN * (utxo_to_spend['value'] - fee_rate * (vsize / 1000))) assert send_value > 0 tx = CTransaction() tx.vin = [ CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=sequence) ] tx.vout = [CTxOut(send_value, self._scriptPubKey)] tx.nLockTime = locktime if not self._address: # raw script if self._priv_key is not None: # P2PK, need to sign self.sign_tx(tx) else: # anyone-can-spend tx.vin[0].scriptSig = CScript([OP_NOP] * 43) # pad to identical size else: tx.wit.vtxinwit = [CTxInWitness()] tx.wit.vtxinwit[0].scriptWitness.stack = [ CScript([OP_TRUE]), bytes([LEAF_VERSION_TAPSCRIPT]) + self._internal_key ] tx_hex = tx.serialize().hex() if mempool_valid: tx_info = from_node.testmempoolaccept([tx_hex])[0] assert_equal(tx_info['allowed'], True) assert_equal(tx_info['vsize'], vsize) assert_equal(tx_info['fees']['base'], utxo_to_spend['value'] - Decimal(send_value) / COIN) return { 'txid': tx.rehash(), 'wtxid': tx.getwtxid(), 'hex': tx_hex, 'tx': tx }
def test_rbf(self): node = self.nodes[0] coin = self.coins.pop() inputs = [{"txid": coin["txid"], "vout": 0, "sequence": BIP125_SEQUENCE_NUMBER}] fee = Decimal('0.00125000') output = {node.get_deterministic_priv_key().address: 50 - fee} raw_replaceable_tx = node.createrawtransaction(inputs, output) signed_replaceable_tx = node.signrawtransactionwithkey(hexstring=raw_replaceable_tx, privkeys=self.privkeys) testres_replaceable = node.testmempoolaccept([signed_replaceable_tx["hex"]]) replaceable_tx = CTransaction() replaceable_tx.deserialize(BytesIO(hex_str_to_bytes(signed_replaceable_tx["hex"]))) assert_equal(testres_replaceable, [ {"txid": replaceable_tx.rehash(), "wtxid": replaceable_tx.getwtxid(), "allowed": True, "vsize": replaceable_tx.get_vsize(), "fees": { "base": fee }} ]) # Replacement transaction is identical except has double the fee replacement_tx = CTransaction() replacement_tx.deserialize(BytesIO(hex_str_to_bytes(signed_replaceable_tx["hex"]))) replacement_tx.vout[0].nValue -= int(fee * COIN) # Doubled fee signed_replacement_tx = node.signrawtransactionwithkey(replacement_tx.serialize().hex(), self.privkeys) replacement_tx.deserialize(BytesIO(hex_str_to_bytes(signed_replacement_tx["hex"]))) self.log.info("Test that transactions within a package cannot replace each other") testres_rbf_conflicting = node.testmempoolaccept([signed_replaceable_tx["hex"], signed_replacement_tx["hex"]]) assert_equal(testres_rbf_conflicting, [ {"txid": replaceable_tx.rehash(), "wtxid": replaceable_tx.getwtxid(), "package-error": "conflict-in-package"}, {"txid": replacement_tx.rehash(), "wtxid": replacement_tx.getwtxid(), "package-error": "conflict-in-package"} ]) self.log.info("Test that packages cannot conflict with mempool transactions, even if a valid BIP125 RBF") node.sendrawtransaction(signed_replaceable_tx["hex"]) testres_rbf_single = node.testmempoolaccept([signed_replacement_tx["hex"]]) # This transaction is a valid BIP125 replace-by-fee assert testres_rbf_single[0]["allowed"] testres_rbf_package = self.independent_txns_testres_blank + [{ "txid": replacement_tx.rehash(), "wtxid": replacement_tx.getwtxid(), "allowed": False, "reject-reason": "txn-mempool-conflict" }] self.assert_testres_equal(self.independent_txns_hex + [signed_replacement_tx["hex"]], testres_rbf_package)
def create_self_transfer(self, *, fee_rate=Decimal("0.003"), fee=Decimal("0"), utxo_to_spend=None, locktime=0, sequence=0, target_weight=0): """Create and return a tx with the specified fee. If fee is 0, use fee_rate, where the resulting fee may be exact or at most one satoshi higher than needed.""" utxo_to_spend = utxo_to_spend or self.get_utxo() assert fee_rate >= 0 assert fee >= 0 if self._mode in (MiniWalletMode.RAW_OP_TRUE, MiniWalletMode.ADDRESS_OP_TRUE): vsize = Decimal(104) # anyone-can-spend elif self._mode == MiniWalletMode.RAW_P2PK: vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other) else: assert False send_value = utxo_to_spend["value"] - (fee or (fee_rate * vsize / 1000)) assert send_value > 0 tx = CTransaction() tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=sequence)] tx.vout = [CTxOut(int(COIN * send_value), bytearray(self._scriptPubKey))] tx.nLockTime = locktime if self._mode == MiniWalletMode.RAW_P2PK: self.sign_tx(tx) elif self._mode == MiniWalletMode.RAW_OP_TRUE: tx.vin[0].scriptSig = CScript([OP_NOP] * 43) # pad to identical size elif self._mode == MiniWalletMode.ADDRESS_OP_TRUE: tx.wit.vtxinwit = [CTxInWitness()] tx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE]), bytes([LEAF_VERSION_TAPSCRIPT]) + self._internal_key] else: assert False assert_equal(tx.get_vsize(), vsize) if target_weight: self._bulk_tx(tx, target_weight) tx_hex = tx.serialize().hex() new_utxo = self._create_utxo(txid=tx.rehash(), vout=0, value=send_value, height=0) return {"txid": new_utxo["txid"], "wtxid": tx.getwtxid(), "hex": tx_hex, "tx": tx, "new_utxo": new_utxo}
def run_test(self): node = self.nodes[0] self.log.info('Start with empty mempool and 101 blocks') # The last 100 coinbase transactions are premature blockhash = node.generate(101)[0] txid = node.getblock(blockhash=blockhash, verbosity=2)["tx"][0]["txid"] assert_equal(node.getmempoolinfo()['size'], 0) self.log.info("Submit parent with multiple script branches to mempool") hashlock = hash160(b'Preimage') witness_script = CScript([ OP_IF, OP_HASH160, hashlock, OP_EQUAL, OP_ELSE, OP_TRUE, OP_ENDIF ]) witness_program = sha256(witness_script) script_pubkey = CScript([OP_0, witness_program]) parent = CTransaction() parent.vin.append(CTxIn(COutPoint(int(txid, 16), 0), b"")) parent.vout.append(CTxOut(int(9.99998 * COIN), script_pubkey)) parent.rehash() privkeys = [node.get_deterministic_priv_key().key] raw_parent = node.signrawtransactionwithkey( hexstring=parent.serialize().hex(), privkeys=privkeys)['hex'] parent_txid = node.sendrawtransaction(hexstring=raw_parent, maxfeerate=0) node.generate(1) peer_wtxid_relay = node.add_p2p_connection(P2PTxInvStore()) # Create a new transaction with witness solving first branch child_witness_script = CScript([OP_TRUE]) child_witness_program = sha256(child_witness_script) child_script_pubkey = CScript([OP_0, child_witness_program]) child_one = CTransaction() child_one.vin.append(CTxIn(COutPoint(int(parent_txid, 16), 0), b"")) child_one.vout.append(CTxOut(int(9.99996 * COIN), child_script_pubkey)) child_one.wit.vtxinwit.append(CTxInWitness()) child_one.wit.vtxinwit[0].scriptWitness.stack = [ b'Preimage', b'\x01', witness_script ] child_one_wtxid = child_one.getwtxid() child_one_txid = child_one.rehash() # Create another identical transaction with witness solving second branch child_two = deepcopy(child_one) child_two.wit.vtxinwit[0].scriptWitness.stack = [b'', witness_script] child_two_wtxid = child_two.getwtxid() child_two_txid = child_two.rehash() assert_equal(child_one_txid, child_two_txid) assert child_one_wtxid != child_two_wtxid self.log.info("Submit child_one to the mempool") txid_submitted = node.sendrawtransaction(child_one.serialize().hex()) assert_equal( node.getrawmempool(True)[txid_submitted]['wtxid'], child_one_wtxid) peer_wtxid_relay.wait_for_broadcast([child_one_wtxid]) assert_equal(node.getmempoolinfo()["unbroadcastcount"], 0) # testmempoolaccept reports the "already in mempool" error assert_equal(node.testmempoolaccept([child_one.serialize().hex()]), [{ "txid": child_one_txid, "wtxid": child_one_wtxid, "allowed": False, "reject-reason": "txn-already-in-mempool" }]) assert_equal( node.testmempoolaccept([child_two.serialize().hex()])[0], { "txid": child_two_txid, "wtxid": child_two_wtxid, "allowed": False, "reject-reason": "txn-same-nonwitness-data-in-mempool" }) # sendrawtransaction will not throw but quits early when the exact same transaction is already in mempool node.sendrawtransaction(child_one.serialize().hex()) self.log.info("Connect another peer that hasn't seen child_one before") peer_wtxid_relay_2 = node.add_p2p_connection(P2PTxInvStore()) self.log.info("Submit child_two to the mempool") # sendrawtransaction will not throw but quits early when a transaction with the same non-witness data is already in mempool node.sendrawtransaction(child_two.serialize().hex()) # The node should rebroadcast the transaction using the wtxid of the correct transaction # (child_one, which is in its mempool). peer_wtxid_relay_2.wait_for_broadcast([child_one_wtxid]) assert_equal(node.getmempoolinfo()["unbroadcastcount"], 0)