Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc-schnorrmultisig-activation.py
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2019 The Bitcoin developers | # Copyright (c) 2019 The Bitcoin developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
""" | """ | ||||
This tests the activation of the upgraded CHECKMULTISIG mode that uses | This tests the CHECKMULTISIG mode that uses Schnorr transaction signatures and | ||||
Schnorr transaction signatures and repurposes the dummy element to indicate | repurposes the dummy element to indicate which signatures are being checked. | ||||
which signatures are being checked. | |||||
- acceptance both in mempool and blocks. | - acceptance both in mempool and blocks. | ||||
- check non-banning for peers who send invalid txns that would have been valid | - check non-banning for peers who send invalid txns that would have been valid | ||||
on the other side of the upgrade. | on the other side of the upgrade. | ||||
- check banning of peers for some fully-invalid transactions. | - check banning of peers for some fully-invalid transactions. | ||||
Derived from abc-schnorr.py | Derived from abc-schnorr.py | ||||
""" | """ | ||||
Show All 25 Lines | from test_framework.script import ( | ||||
OP_TRUE, | OP_TRUE, | ||||
SIGHASH_ALL, | SIGHASH_ALL, | ||||
SIGHASH_FORKID, | SIGHASH_FORKID, | ||||
SignatureHashForkId, | SignatureHashForkId, | ||||
) | ) | ||||
from test_framework.test_framework import BitcoinTestFramework | from test_framework.test_framework import BitcoinTestFramework | ||||
from test_framework.util import assert_equal, assert_raises_rpc_error | from test_framework.util import assert_equal, assert_raises_rpc_error | ||||
# the upgrade activation time, which we artificially set far into the future | |||||
GRAVITON_START_TIME = 2000000000 | |||||
# If we don't do this, autoreplay protection will activate before graviton and | # ECDSA checkmultisig with non-null dummy are invalid since the new mode | ||||
# all our sigs will mysteriously fail. | # refuses ECDSA. | ||||
REPLAY_PROTECTION_START_TIME = GRAVITON_START_TIME * 2 | ECDSA_NULLDUMMY_ERROR = 'mandatory-script-verify-flag-failed (Only Schnorr signatures allowed in this operation)' | ||||
# Before the upgrade, Schnorr checkmultisig is rejected. | |||||
PREUPGRADE_SCHNORR_MULTISIG_ERROR = 'mandatory-script-verify-flag-failed (Signature cannot be 65 bytes in CHECKMULTISIG)' | |||||
# After the upgrade, ECDSA checkmultisig with non-null dummy are invalid since | |||||
# the new mode refuses ECDSA. | |||||
POSTUPGRADE_ECDSA_NULLDUMMY_ERROR = 'mandatory-script-verify-flag-failed (Only Schnorr signatures allowed in this operation)' | |||||
# A mandatory (bannable) error occurs when people pass Schnorr signatures into | # A mandatory (bannable) error occurs when people pass Schnorr signatures into | ||||
# legacy OP_CHECKMULTISIG; this is the case on both sides of the upgrade. | # legacy OP_CHECKMULTISIG. | ||||
SCHNORR_LEGACY_MULTISIG_ERROR = 'mandatory-script-verify-flag-failed (Signature cannot be 65 bytes in CHECKMULTISIG)' | SCHNORR_LEGACY_MULTISIG_ERROR = 'mandatory-script-verify-flag-failed (Signature cannot be 65 bytes in CHECKMULTISIG)' | ||||
# Blocks with invalid scripts give this error: | # Blocks with invalid scripts give this error: | ||||
BADINPUTS_ERROR = 'blk-bad-inputs' | BADINPUTS_ERROR = 'blk-bad-inputs' | ||||
# This 64-byte signature is used to test exclusion & banning according to | # This 64-byte signature is used to test exclusion & banning according to | ||||
# the above error messages. | # the above error messages. | ||||
# Tests of real 64 byte ECDSA signatures can be found in script_tests. | # Tests of real 64 byte ECDSA signatures can be found in script_tests. | ||||
sig64 = b'\0' * 64 | sig64 = b'\0' * 64 | ||||
class SchnorrTest(BitcoinTestFramework): | class SchnorrMultisigTest(BitcoinTestFramework): | ||||
def set_test_params(self): | def set_test_params(self): | ||||
self.num_nodes = 1 | self.num_nodes = 1 | ||||
self.block_heights = {} | self.block_heights = {} | ||||
self.extra_args = [["-gravitonactivationtime={}".format( | |||||
GRAVITON_START_TIME), | |||||
"-replayprotectionactivationtime={}".format( | |||||
REPLAY_PROTECTION_START_TIME)]] | |||||
def bootstrap_p2p(self, *, num_connections=1): | def bootstrap_p2p(self, *, num_connections=1): | ||||
"""Add a P2P connection to the node. | """Add a P2P connection to the node. | ||||
Helper to connect and wait for version handshake.""" | Helper to connect and wait for version handshake.""" | ||||
for _ in range(num_connections): | for _ in range(num_connections): | ||||
self.nodes[0].add_p2p_connection(P2PDataStore()) | self.nodes[0].add_p2p_connection(P2PDataStore()) | ||||
Show All 34 Lines | class SchnorrMultisigTest(BitcoinTestFramework): | ||||
def check_for_ban_on_rejected_tx(self, tx, reject_reason=None): | def check_for_ban_on_rejected_tx(self, tx, reject_reason=None): | ||||
"""Check we are disconnected when sending a txn that the node rejects. | """Check we are disconnected when sending a txn that the node rejects. | ||||
(Can't actually get banned, since bitcoind won't ban local peers.)""" | (Can't actually get banned, since bitcoind won't ban local peers.)""" | ||||
self.nodes[0].p2p.send_txs_and_test( | self.nodes[0].p2p.send_txs_and_test( | ||||
[tx], self.nodes[0], success=False, expect_disconnect=True, reject_reason=reject_reason) | [tx], self.nodes[0], success=False, expect_disconnect=True, reject_reason=reject_reason) | ||||
self.reconnect_p2p() | self.reconnect_p2p() | ||||
def check_for_no_ban_on_rejected_tx(self, tx, reject_reason): | |||||
"""Check we are not disconnected when sending a txn that the node rejects.""" | |||||
self.nodes[0].p2p.send_txs_and_test( | |||||
[tx], self.nodes[0], success=False, reject_reason=reject_reason) | |||||
def check_for_ban_on_rejected_block(self, block, reject_reason=None): | def check_for_ban_on_rejected_block(self, block, reject_reason=None): | ||||
"""Check we are disconnected when sending a block that the node rejects. | """Check we are disconnected when sending a block that the node rejects. | ||||
(Can't actually get banned, since bitcoind won't ban local peers.)""" | (Can't actually get banned, since bitcoind won't ban local peers.)""" | ||||
self.nodes[0].p2p.send_blocks_and_test( | self.nodes[0].p2p.send_blocks_and_test( | ||||
[block], self.nodes[0], success=False, reject_reason=reject_reason, expect_disconnect=True) | [block], self.nodes[0], success=False, reject_reason=reject_reason, expect_disconnect=True) | ||||
self.reconnect_p2p() | self.reconnect_p2p() | ||||
▲ Show 20 Lines • Show All 55 Lines • ▼ Show 20 Lines | def run_test(self): | ||||
txsig = schnorr.sign(privkeybytes, sighash) + hashbyte | txsig = schnorr.sign(privkeybytes, sighash) + hashbyte | ||||
elif sigtype == 'ecdsa': | elif sigtype == 'ecdsa': | ||||
txsig = private_key.sign(sighash) + hashbyte | txsig = private_key.sign(sighash) + hashbyte | ||||
txspend.vin[0].scriptSig = CScript([dummy, txsig]) | txspend.vin[0].scriptSig = CScript([dummy, txsig]) | ||||
txspend.rehash() | txspend.rehash() | ||||
return txspend | return txspend | ||||
# two of these transactions, which are valid both before and after upgrade. | # This is valid. | ||||
ecdsa0tx = create_fund_and_spend_tx(OP_0, 'ecdsa') | ecdsa0tx = create_fund_and_spend_tx(OP_0, 'ecdsa') | ||||
ecdsa0tx_2 = create_fund_and_spend_tx(OP_0, 'ecdsa') | |||||
# two of these, which are nonstandard before upgrade and invalid after. | # This is invalid. | ||||
ecdsa1tx = create_fund_and_spend_tx(OP_1, 'ecdsa') | ecdsa1tx = create_fund_and_spend_tx(OP_1, 'ecdsa') | ||||
ecdsa1tx_2 = create_fund_and_spend_tx(OP_1, 'ecdsa') | |||||
# this one is always invalid. | # This is invalid. | ||||
schnorr0tx = create_fund_and_spend_tx(OP_0, 'schnorr') | schnorr0tx = create_fund_and_spend_tx(OP_0, 'schnorr') | ||||
# this one is only going to be valid after the upgrade. | # This is valid. | ||||
schnorr1tx = create_fund_and_spend_tx(OP_1, 'schnorr') | schnorr1tx = create_fund_and_spend_tx(OP_1, 'schnorr') | ||||
tip = self.build_block(tip, fundings) | tip = self.build_block(tip, fundings) | ||||
node.p2p.send_blocks_and_test([tip], node) | node.p2p.send_blocks_and_test([tip], node) | ||||
self.log.info("Start preupgrade tests") | self.log.info("Send a legacy ECDSA multisig into mempool.") | ||||
self.log.info("Sending rejected transactions via RPC") | |||||
assert_raises_rpc_error(-26, SCHNORR_LEGACY_MULTISIG_ERROR, | |||||
node.sendrawtransaction, ToHex(schnorr0tx)) | |||||
# Since MULTISIG_SCHNORR is in mandatory flags, we are not accepting | |||||
# non-null-dummy ECDSA transactions before the upgrade. We get a | |||||
# post-upgrade error since the mempool is using post-upgrade flags. | |||||
assert_raises_rpc_error(-26, POSTUPGRADE_ECDSA_NULLDUMMY_ERROR, | |||||
node.sendrawtransaction, ToHex(ecdsa1tx)) | |||||
# The Schnorr multisig almost gets accepted here but it finally gets | |||||
# caught in the block flags check. Note that "BUG! PLEASE REPORT | |||||
# THIS!" will appear in the log, since AcceptToMemoryPoolWorker expects | |||||
# that scriptVerifyFlags is more strict than nextBlockScriptVerifyFlags. | |||||
# For strictly subtractive ('soft forking') flags, it is fine if they | |||||
# are always part of scriptVerifyFlags and only sometimes appear in | |||||
# nextBlockScriptVerifyFlags, but for additive flags this kind of | |||||
# strange situation can be created. | |||||
# In practice, only new nodes will ever be in a pre-upgrade state, | |||||
# and they will also be in initial block download mode and hence | |||||
# not request transactions from peers. So, this weird log message | |||||
# could only be triggered by unsolicited submission of a tx, and | |||||
# it would be benign since the node is behaving correctly by rejecting | |||||
# the transaction (as tested here). | |||||
assert_raises_rpc_error(-26, PREUPGRADE_SCHNORR_MULTISIG_ERROR, | |||||
node.sendrawtransaction, ToHex(schnorr1tx)) | |||||
self.log.info( | |||||
"Sending rejected transactions via net (bannable)") | |||||
self.check_for_ban_on_rejected_tx( | |||||
schnorr0tx, SCHNORR_LEGACY_MULTISIG_ERROR) | |||||
self.check_for_ban_on_rejected_tx( | |||||
ecdsa1tx, POSTUPGRADE_ECDSA_NULLDUMMY_ERROR) | |||||
# If we are sent unsolicited post-upgrade transactions while before | |||||
# the upgrade block, the tx is to be rejected. | |||||
self.check_for_ban_on_rejected_tx( | |||||
schnorr1tx, PREUPGRADE_SCHNORR_MULTISIG_ERROR) | |||||
self.log.info( | |||||
"Sending invalid transactions in blocks (and get banned!)") | |||||
self.check_for_ban_on_rejected_block( | |||||
self.build_block(tip, [schnorr0tx]), BADINPUTS_ERROR) | |||||
self.check_for_ban_on_rejected_block( | |||||
self.build_block(tip, [schnorr1tx]), BADINPUTS_ERROR) | |||||
self.log.info("Sending valid transaction via net, then mining it") | |||||
node.p2p.send_txs_and_test([ecdsa0tx], node) | node.p2p.send_txs_and_test([ecdsa0tx], node) | ||||
assert_equal(node.getrawmempool(), [ecdsa0tx.hash]) | assert_equal(node.getrawmempool(), [ecdsa0tx.hash]) | ||||
tip = self.build_block(tip, [ecdsa0tx]) | |||||
node.p2p.send_blocks_and_test([tip], node) | |||||
assert_equal(node.getrawmempool(), []) | |||||
# Activation tests | |||||
self.log.info("Approach to just before upgrade activation") | self.log.info("Trying to mine a non-null-dummy ECDSA.") | ||||
# Move our clock to the uprade time so we will accept such future-timestamped blocks. | |||||
node.setmocktime(GRAVITON_START_TIME) | |||||
# Mine six blocks with timestamp starting at GRAVITON_START_TIME-1 | |||||
blocks = [] | |||||
for i in range(-1, 5): | |||||
tip = self.build_block(tip, nTime=GRAVITON_START_TIME + i) | |||||
blocks.append(tip) | |||||
node.p2p.send_blocks_and_test(blocks, node) | |||||
assert_equal(node.getblockchaininfo()[ | |||||
'mediantime'], GRAVITON_START_TIME - 1) | |||||
self.log.info( | |||||
"The next block will activate, but the activation block itself must follow old rules") | |||||
self.check_for_ban_on_rejected_block( | self.check_for_ban_on_rejected_block( | ||||
self.build_block(tip, [schnorr0tx]), BADINPUTS_ERROR) | self.build_block(tip, [ecdsa1tx]), BADINPUTS_ERROR) | ||||
self.log.info( | |||||
"Send a lecacy ECDSA multisig into mempool, we will check after upgrade to make sure it didn't get cleaned out unnecessarily.") | |||||
node.p2p.send_txs_and_test([ecdsa0tx_2], node) | |||||
assert_equal(node.getrawmempool(), [ecdsa0tx_2.hash]) | |||||
# save this tip for later | |||||
preupgrade_block = tip | |||||
self.log.info( | |||||
"Mine the activation block itself, including a non-null-dummy ECDSA at the last possible moment") | |||||
tip = self.build_block(tip, [ecdsa1tx]) | |||||
node.p2p.send_blocks_and_test([tip], node) | |||||
self.log.info("We have activated!") | |||||
assert_equal(node.getblockchaininfo()[ | |||||
'mediantime'], GRAVITON_START_TIME) | |||||
assert_equal(node.getrawmempool(), [ecdsa0tx_2.hash]) | |||||
# save this tip for later | |||||
upgrade_block = tip | |||||
self.log.info( | |||||
"Trying to mine a non-null-dummy ECDSA, but we are just barely too late") | |||||
self.check_for_ban_on_rejected_block( | |||||
self.build_block(tip, [ecdsa1tx_2]), BADINPUTS_ERROR) | |||||
self.log.info( | self.log.info( | ||||
"If we try to submit it by mempool or RPC, it is rejected and we are banned") | "If we try to submit it by mempool or RPC, it is rejected and we are banned") | ||||
assert_raises_rpc_error(-26, POSTUPGRADE_ECDSA_NULLDUMMY_ERROR, | assert_raises_rpc_error(-26, ECDSA_NULLDUMMY_ERROR, | ||||
node.sendrawtransaction, ToHex(ecdsa1tx_2)) | node.sendrawtransaction, ToHex(ecdsa1tx)) | ||||
self.check_for_ban_on_rejected_tx( | self.check_for_ban_on_rejected_tx( | ||||
ecdsa1tx_2, POSTUPGRADE_ECDSA_NULLDUMMY_ERROR) | ecdsa1tx, ECDSA_NULLDUMMY_ERROR) | ||||
self.log.info( | self.log.info( | ||||
"Submitting a new Schnorr-multisig via net, and mining it in a block") | "Submitting a Schnorr-multisig via net, and mining it in a block") | ||||
node.p2p.send_txs_and_test([schnorr1tx], node) | node.p2p.send_txs_and_test([schnorr1tx], node) | ||||
assert_equal(set(node.getrawmempool()), { | assert_equal(set(node.getrawmempool()), { | ||||
ecdsa0tx_2.hash, schnorr1tx.hash}) | ecdsa0tx.hash, schnorr1tx.hash}) | ||||
tip = self.build_block(tip, [schnorr1tx]) | tip = self.build_block(tip, [schnorr1tx]) | ||||
node.p2p.send_blocks_and_test([tip], node) | node.p2p.send_blocks_and_test([tip], node) | ||||
# save this tip for later | |||||
postupgrade_block = tip | |||||
self.log.info( | self.log.info( | ||||
"That legacy ECDSA multisig is still in mempool, let's mine it") | "That legacy ECDSA multisig is still in mempool, let's mine it") | ||||
assert_equal(node.getrawmempool(), [ecdsa0tx_2.hash]) | assert_equal(node.getrawmempool(), [ecdsa0tx.hash]) | ||||
tip = self.build_block(tip, [ecdsa0tx_2]) | tip = self.build_block(tip, [ecdsa0tx]) | ||||
node.p2p.send_blocks_and_test([tip], node) | node.p2p.send_blocks_and_test([tip], node) | ||||
assert_equal(node.getrawmempool(), []) | assert_equal(node.getrawmempool(), []) | ||||
self.log.info( | self.log.info( | ||||
"Trying Schnorr in legacy multisig remains invalid and banworthy as ever") | "Trying Schnorr in legacy multisig is invalid and banworthy.") | ||||
self.check_for_ban_on_rejected_tx( | self.check_for_ban_on_rejected_tx( | ||||
schnorr0tx, SCHNORR_LEGACY_MULTISIG_ERROR) | schnorr0tx, SCHNORR_LEGACY_MULTISIG_ERROR) | ||||
self.check_for_ban_on_rejected_block( | self.check_for_ban_on_rejected_block( | ||||
self.build_block(tip, [schnorr0tx]), BADINPUTS_ERROR) | self.build_block(tip, [schnorr0tx]), BADINPUTS_ERROR) | ||||
# Deactivation tests | |||||
self.log.info( | |||||
"Invalidating the post-upgrade blocks returns the transactions to mempool") | |||||
node.invalidateblock(postupgrade_block.hash) | |||||
assert_equal(set(node.getrawmempool()), { | |||||
ecdsa0tx_2.hash, schnorr1tx.hash}) | |||||
self.log.info( | |||||
"Invalidating the upgrade block evicts the transactions valid only after upgrade") | |||||
node.invalidateblock(upgrade_block.hash) | |||||
assert_equal(set(node.getrawmempool()), { | |||||
ecdsa0tx_2.hash}) | |||||
self.log.info("Return to our tip") | |||||
node.reconsiderblock(upgrade_block.hash) | |||||
node.reconsiderblock(postupgrade_block.hash) | |||||
assert_equal(node.getbestblockhash(), tip.hash) | |||||
assert_equal(node.getrawmempool(), []) | |||||
self.log.info( | |||||
"Create an empty-block reorg that forks from pre-upgrade") | |||||
tip = preupgrade_block | |||||
blocks = [] | |||||
for _ in range(10): | |||||
tip = self.build_block(tip) | |||||
blocks.append(tip) | |||||
node.p2p.send_blocks_and_test(blocks, node) | |||||
self.log.info("Transactions from orphaned blocks are sent into mempool ready to be mined again, including upgrade-dependent ones even though the fork deactivated and reactivated the upgrade.") | |||||
assert_equal(set(node.getrawmempool()), { | |||||
ecdsa0tx_2.hash, schnorr1tx.hash}) | |||||
node.generate(1) | |||||
tip = self.getbestblock(node) | |||||
assert set(tx.rehash() for tx in tip.vtx).issuperset( | |||||
{ecdsa0tx_2.hash, schnorr1tx.hash}) | |||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
SchnorrTest().main() | SchnorrMultisigTest().main() |