Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc-schnorrmultisig-activation.py
- This file was added.
Property | Old Value | New Value |
---|---|---|
File Mode | null | 100755 |
#!/usr/bin/env python3 | |||||
# Copyright (c) 2019 The Bitcoin developers | |||||
# Distributed under the MIT software license, see the accompanying | |||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | |||||
""" | |||||
This tests the activation of the upgraded CHECKMULTISIG mode that uses | |||||
Schnorr transaction signatures and repurposes the dummy element to indicate | |||||
which signatures are being checked. | |||||
- acceptance both in mempool and blocks. | |||||
- check non-banning for peers who send invalid txns that would have been valid | |||||
on the other side of the upgrade. | |||||
- check banning of peers for some fully-invalid transactions. | |||||
Derived from abc-schnorr.py | |||||
""" | |||||
from test_framework.blocktools import ( | |||||
create_block, | |||||
create_coinbase, | |||||
create_transaction, | |||||
make_conform_to_ctor, | |||||
) | |||||
from test_framework.key import CECKey | |||||
from test_framework.messages import ( | |||||
CBlock, | |||||
COutPoint, | |||||
CTransaction, | |||||
CTxIn, | |||||
CTxOut, | |||||
FromHex, | |||||
ToHex, | |||||
) | |||||
from test_framework.mininode import ( | |||||
network_thread_join, | |||||
network_thread_start, | |||||
P2PDataStore, | |||||
) | |||||
from test_framework import schnorr | |||||
from test_framework.script import ( | |||||
CScript, | |||||
OP_0, | |||||
OP_1, | |||||
OP_CHECKMULTISIG, | |||||
OP_TRUE, | |||||
SIGHASH_ALL, | |||||
SIGHASH_FORKID, | |||||
SignatureHashForkId, | |||||
) | |||||
from test_framework.test_framework import BitcoinTestFramework | |||||
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 | |||||
# all our sigs will mysteriously fail. | |||||
REPLAY_PROTECTION_START_TIME = GRAVITON_START_TIME * 2 | |||||
# Before the upgrade, Schnorr checkmultisig is rejected but forgiven if it would have been valid after the upgrade. | |||||
PREUPGRADE_SCHNORR_MULTISIG_ERROR = dict(reject_code=16, | |||||
reject_reason=b'upgrade-conditional-script-failure (Dummy CHECKMULTISIG argument must be zero)') | |||||
# Before the upgrade, ECDSA checkmultisig with non-null dummy are rejected with a non-mandatory error. | |||||
PREUPGRADE_ECDSA_NULLDUMMY_ERROR = dict(reject_code=64, | |||||
reject_reason=b'non-mandatory-script-verify-flag (Dummy CHECKMULTISIG argument must be zero)') | |||||
# After the upgrade, ECDSA checkmultisig with non-null dummy are invalid since the new mode refuses ECDSA, but still do not result in ban. | |||||
POSTUPGRADE_ECDSA_NULLDUMMY_ERROR = dict(reject_code=16, | |||||
reject_reason=b'upgrade-conditional-script-failure (Only Schnorr signatures allowed in this operation)') | |||||
# A mandatory (bannable) error occurs when people pass Schnorr signatures into legacy OP_CHECKMULTISIG; this is the case on both sides of the upgrade. | |||||
SCHNORR_LEGACY_MULTISIG_ERROR = dict(reject_code=16, | |||||
reject_reason=b'mandatory-script-verify-flag-failed (Signature cannot be 65 bytes in CHECKMULTISIG)') | |||||
# Blocks with invalid scripts give this error: | |||||
BADINPUTS_ERROR = dict(reject_code=16, | |||||
reject_reason=b'blk-bad-inputs') | |||||
def rpc_error(*, reject_code, reject_reason): | |||||
# RPC indicates rejected items in a slightly different way than p2p. | |||||
return '{:s} (code {:d})'.format(reject_reason.decode(), reject_code) | |||||
# This 64-byte signature is used to test exclusion & banning according to | |||||
# the above error messages. | |||||
# Tests of real 64 byte ECDSA signatures can be found in script_tests. | |||||
sig64 = b'\0'*64 | |||||
markblundeberg: this can be removed (leftover from copy) | |||||
class SchnorrTest(BitcoinTestFramework): | |||||
def set_test_params(self): | |||||
self.num_nodes = 1 | |||||
self.block_heights = {} | |||||
self.extra_args = [["-gravitonactivationtime={}".format( | |||||
GRAVITON_START_TIME), | |||||
"-replayprotectionactivationtime={}".format( | |||||
REPLAY_PROTECTION_START_TIME)]] | |||||
def bootstrap_p2p(self, *, num_connections=1): | |||||
"""Add a P2P connection to the node. | |||||
Helper to connect and wait for version handshake.""" | |||||
for _ in range(num_connections): | |||||
self.nodes[0].add_p2p_connection(P2PDataStore()) | |||||
network_thread_start() | |||||
self.nodes[0].p2p.wait_for_verack() | |||||
def reconnect_p2p(self, **kwargs): | |||||
"""Tear down and bootstrap the P2P connection to the node. | |||||
The node gets disconnected several times in this test. This helper | |||||
method reconnects the p2p and restarts the network thread.""" | |||||
self.nodes[0].disconnect_p2ps() | |||||
network_thread_join() | |||||
self.bootstrap_p2p(**kwargs) | |||||
def getbestblock(self, node): | |||||
"""Get the best block. Register its height so we can use build_block.""" | |||||
block_height = node.getblockcount() | |||||
blockhash = node.getblockhash(block_height) | |||||
block = FromHex(CBlock(), node.getblock(blockhash, 0)) | |||||
block.calc_sha256() | |||||
self.block_heights[block.sha256] = block_height | |||||
return block | |||||
def build_block(self, parent, transactions=(), nTime=None): | |||||
"""Make a new block with an OP_1 coinbase output. | |||||
Requires parent to have its height registered.""" | |||||
parent.calc_sha256() | |||||
block_height = self.block_heights[parent.sha256] + 1 | |||||
block_time = (parent.nTime + 1) if nTime is None else nTime | |||||
block = create_block( | |||||
parent.sha256, create_coinbase(block_height), block_time) | |||||
block.vtx.extend(transactions) | |||||
make_conform_to_ctor(block) | |||||
block.hashMerkleRoot = block.calc_merkle_root() | |||||
block.solve() | |||||
self.block_heights[block.sha256] = block_height | |||||
return block | |||||
def check_for_ban_on_rejected_tx(self, tx, reject_code=None, reject_reason=None): | |||||
"""Check we are disconnected when sending a txn that the node rejects. | |||||
(Can't actually get banned, since bitcoind won't ban local peers.)""" | |||||
self.nodes[0].p2p.send_txs_and_test( | |||||
[tx], self.nodes[0], success=False, expect_disconnect=True, reject_code=reject_code, reject_reason=reject_reason) | |||||
self.reconnect_p2p() | |||||
def check_for_no_ban_on_rejected_tx(self, tx, reject_code, 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_code=reject_code, reject_reason=reject_reason) | |||||
def check_for_ban_on_rejected_block(self, block, reject_code=None, reject_reason=None): | |||||
"""Check we are disconnected when sending a block that the node rejects. | |||||
(Can't actually get banned, since bitcoind won't ban local peers.)""" | |||||
self.nodes[0].p2p.send_blocks_and_test( | |||||
[block], self.nodes[0], success=False, reject_code=reject_code, reject_reason=reject_reason) | |||||
self.nodes[0].p2p.wait_for_disconnect() | |||||
self.reconnect_p2p() | |||||
def run_test(self): | |||||
node, = self.nodes | |||||
self.bootstrap_p2p() | |||||
tip = self.getbestblock(node) | |||||
self.log.info("Create some blocks with OP_1 coinbase for spending.") | |||||
blocks = [] | |||||
for _ in range(10): | |||||
tip = self.build_block(tip) | |||||
blocks.append(tip) | |||||
node.p2p.send_blocks_and_test(blocks, node, success=True) | |||||
spendable_outputs = [block.vtx[0] for block in blocks] | |||||
self.log.info("Mature the blocks and get out of IBD.") | |||||
node.generate(100) | |||||
tip = self.getbestblock(node) | |||||
self.log.info("Setting up spends to test and mining the fundings.") | |||||
fundings = [] | |||||
# Generate a key pair | |||||
privkeybytes = b"Schnorr!" * 4 | |||||
private_key = CECKey() | |||||
private_key.set_secretbytes(privkeybytes) | |||||
# get uncompressed public key serialization | |||||
public_key = private_key.get_pubkey() | |||||
def create_fund_and_spend_tx(dummy=OP_0, sigtype='ecdsa'): | |||||
spendfrom = spendable_outputs.pop() | |||||
script = CScript([OP_1, public_key, OP_1, OP_CHECKMULTISIG]) | |||||
value = spendfrom.vout[0].nValue | |||||
# Fund transaction | |||||
txfund = create_transaction(spendfrom, 0, b'', value, script) | |||||
txfund.rehash() | |||||
fundings.append(txfund) | |||||
# Spend transaction | |||||
txspend = CTransaction() | |||||
txspend.vout.append( | |||||
CTxOut(value-1000, CScript([OP_TRUE]))) | |||||
txspend.vin.append( | |||||
CTxIn(COutPoint(txfund.sha256, 0), b'')) | |||||
# Sign the transaction | |||||
sighashtype = SIGHASH_ALL | SIGHASH_FORKID | |||||
hashbyte = bytes([sighashtype & 0xff]) | |||||
sighash = SignatureHashForkId( | |||||
script, txspend, 0, sighashtype, value) | |||||
if sigtype == 'schnorr': | |||||
txsig = schnorr.sign(privkeybytes, sighash) + hashbyte | |||||
elif sigtype == 'ecdsa': | |||||
txsig = private_key.sign(sighash) + hashbyte | |||||
deadalnixUnsubmitted Not Done Inline ActionsYou probably want to have an else that throw an error. deadalnix: You probably want to have an else that throw an error. | |||||
txspend.vin[0].scriptSig = CScript([dummy, txsig]) | |||||
txspend.rehash() | |||||
return txspend | |||||
# two of these transactions, which are valid both before and after upgrade. | |||||
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. | |||||
ecdsa1tx = create_fund_and_spend_tx(OP_1, 'ecdsa') | |||||
ecdsa1tx_2 = create_fund_and_spend_tx(OP_1, 'ecdsa') | |||||
# this one is always invalid. | |||||
schnorr0tx = create_fund_and_spend_tx(OP_0, 'schnorr') | |||||
# this one is only going to be valid after the upgrade. | |||||
schnorr1tx = create_fund_and_spend_tx(OP_1, 'schnorr') | |||||
tip = self.build_block(tip, fundings) | |||||
node.p2p.send_blocks_and_test([tip], node) | |||||
self.log.info("Start preupgrade tests") | |||||
self.log.info("Sending rejected transactions via RPC") | |||||
assert_raises_rpc_error(-26, rpc_error(**PREUPGRADE_ECDSA_NULLDUMMY_ERROR), | |||||
node.sendrawtransaction, ToHex(ecdsa1tx)) | |||||
assert_raises_rpc_error(-26, rpc_error(**SCHNORR_LEGACY_MULTISIG_ERROR), | |||||
node.sendrawtransaction, ToHex(schnorr0tx)) | |||||
assert_raises_rpc_error(-26, rpc_error(**PREUPGRADE_SCHNORR_MULTISIG_ERROR), | |||||
node.sendrawtransaction, ToHex(schnorr1tx)) | |||||
self.log.info( | |||||
"Sending rejected transactions via net (banning depending on situation)") | |||||
self.check_for_no_ban_on_rejected_tx( | |||||
ecdsa1tx, **PREUPGRADE_ECDSA_NULLDUMMY_ERROR) | |||||
self.check_for_ban_on_rejected_tx( | |||||
schnorr0tx, **SCHNORR_LEGACY_MULTISIG_ERROR) | |||||
self.check_for_no_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) | |||||
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") | |||||
# 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.build_block(tip, [schnorr0tx]), **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 legacy nulldummy violation 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 legacy nulldummy violation, 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( | |||||
"If we try to submit it by mempool or RPC, the error code has changed but we still aren't banned") | |||||
assert_raises_rpc_error(-26, rpc_error(**POSTUPGRADE_ECDSA_NULLDUMMY_ERROR), | |||||
node.sendrawtransaction, ToHex(ecdsa1tx_2)) | |||||
self.check_for_no_ban_on_rejected_tx( | |||||
ecdsa1tx_2, **POSTUPGRADE_ECDSA_NULLDUMMY_ERROR) | |||||
self.log.info( | |||||
"Submitting a new Schnorr-multisig via net, and mining it in a block") | |||||
node.p2p.send_txs_and_test([schnorr1tx], node) | |||||
assert_equal(set(node.getrawmempool()), { | |||||
ecdsa0tx_2.hash, schnorr1tx.hash}) | |||||
tip = self.build_block(tip, [schnorr1tx]) | |||||
node.p2p.send_blocks_and_test([tip], node) | |||||
# save this tip for later | |||||
postupgrade_block = tip | |||||
self.log.info( | |||||
"That legacy ECDSA multisig is still in mempool, let's mine it") | |||||
assert_equal(node.getrawmempool(), [ecdsa0tx_2.hash]) | |||||
tip = self.build_block(tip, [ecdsa0tx_2]) | |||||
node.p2p.send_blocks_and_test([tip], node) | |||||
assert_equal(node.getrawmempool(), []) | |||||
self.log.info( | |||||
"Trying Schnorr in legacy multisig remains invalid and banworthy as ever") | |||||
self.check_for_ban_on_rejected_tx( | |||||
schnorr0tx, **SCHNORR_LEGACY_MULTISIG_ERROR) | |||||
self.check_for_ban_on_rejected_block( | |||||
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__': | |||||
SchnorrTest().main() |
this can be removed (leftover from copy)