zcash/qa/rpc-tests/coinbase_funding_streams.py

226 lines
9.2 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2020-2024 The Zcash developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
from decimal import Decimal
from test_framework.test_framework import BitcoinTestFramework
from test_framework.blocktools import create_block, create_coinbase
from test_framework.mininode import (
fundingstream,
nuparams,
uint256_from_reversed_hex,
CTxOut,
COIN,
)
from test_framework.script import (
CScript,
OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160,
)
from test_framework.util import (
assert_equal,
bitcoind_processes,
connect_nodes,
start_node,
BLOSSOM_BRANCH_ID,
CANOPY_BRANCH_ID,
NU6_BRANCH_ID,
)
from base58 import b58decode_check
def redeem_script(addr):
raw_addr = b58decode_check(addr)
if len(raw_addr) == 22:
addr_hash = raw_addr[2:]
if raw_addr.startswith(b'\x1C\xBD') or raw_addr.startswith(b'\x1C\xBA'):
return CScript([OP_HASH160, addr_hash, OP_EQUAL])
elif raw_addr.startswith(b'\x1C\xB8') or raw_addr.startswith(b'\x1D\x25'):
return CScript([OP_DUP, OP_HASH160, addr_hash, OP_EQUALVERIFY, OP_CHECKSIG])
raise ValueError("unrecognized address type")
class CoinbaseFundingStreamsTest(BitcoinTestFramework):
def __init__(self):
super().__init__()
self.num_nodes = 2
self.cache_behavior = 'clean'
def start_node_with(self, index, extra_args=[]):
args = [
nuparams(BLOSSOM_BRANCH_ID, 2),
nuparams(CANOPY_BRANCH_ID, 3),
nuparams(NU6_BRANCH_ID, 4),
"-nurejectoldversions=false",
"-allowdeprecated=getnewaddress",
"-allowdeprecated=z_getnewaddress",
"-allowdeprecated=z_getbalance",
"-allowdeprecated=z_gettotalbalance",
]
return start_node(index, self.options.tmpdir, args + extra_args)
def setup_network(self, split=False):
self.nodes = []
self.nodes.append(self.start_node_with(0))
self.nodes.append(self.start_node_with(1))
connect_nodes(self.nodes[1], 0)
self.is_network_split = False
self.sync_all()
def run_test(self):
# Generate a shielded address for node 1 for miner rewards,
miner_addr = self.nodes[1].z_getnewaddress('sapling')
# Generate Sapling and transparent addresses (belonging to node 0) for funding stream rewards.
# For transparent funding stream outputs, strictly speaking the protocol spec only specifies
# paying to a P2SH multisig, but P2PKH funding addresses also work in zcashd.
fs_sapling = self.nodes[0].z_getnewaddress('sapling')
fs_transparent = self.nodes[0].getnewaddress()
print("Activating Blossom")
self.nodes[1].generate(2)
self.sync_all()
# We won't need to mine on node 1 from this point onward.
assert_equal(self.nodes[1].getblockchaininfo()['blocks'], 2)
# Restart both nodes with funding streams.
self.nodes[0].stop()
bitcoind_processes[0].wait()
self.nodes[1].stop()
bitcoind_processes[1].wait()
new_args = [
"-mineraddress=%s" % miner_addr,
"-minetolocalwallet=0",
fundingstream(0, 3, 4, [fs_sapling]), # FS_ZIP214_BP: 7%
fundingstream(1, 3, 4, [fs_sapling]), # FS_ZIP214_ZF: 5%
fundingstream(2, 3, 4, [fs_sapling]), # FS_ZIP214_MG: 8%
fundingstream(3, 4, 8, [fs_transparent, fs_transparent]), # FS_FPF_ZCG: 8%
fundingstream(4, 4, 8, ["DEFERRED_POOL", "DEFERRED_POOL"]), # FS_DEFERRED: 12%
]
self.nodes[0] = self.start_node_with(0, new_args)
self.nodes[1] = self.start_node_with(1, new_args)
connect_nodes(self.nodes[1], 0)
self.sync_all()
print("Activating Heartwood & Canopy")
self.nodes[0].generate(1)
self.sync_all()
assert_equal(self.nodes[1].getblockchaininfo()['blocks'], 3)
# All miner addresses belong to node 1; check balances
# Miner rewards are uniformly 5 zec per block (all test heights are below the first halving)
walletinfo = self.nodes[1].getwalletinfo()
assert_equal(Decimal(walletinfo['immature_balance']), Decimal('15'))
assert_equal(Decimal(walletinfo['balance']), Decimal('0'))
assert_equal(Decimal(self.nodes[1].z_getbalance(miner_addr, 0)), Decimal('5'))
assert_equal(Decimal(self.nodes[1].z_getbalance(miner_addr)), Decimal('5'))
# Check that the node 0 private balance has been augmented by the funding stream payment.
assert_equal(Decimal(self.nodes[0].z_getbalance(fs_sapling, 0)), Decimal('1.25'))
assert_equal(Decimal(self.nodes[0].z_getbalance(fs_sapling)), Decimal('1.25'))
assert_equal(Decimal(self.nodes[0].z_gettotalbalance()['private']), Decimal('1.25'))
assert_equal(Decimal(self.nodes[0].z_gettotalbalance()['total']), Decimal('1.25'))
print("Activating NU5 & NU6")
self.nodes[0].generate(1)
self.sync_all()
assert_equal(self.nodes[1].getblockchaininfo()['blocks'], 4)
# check that miner payments made it to node 1's wallet
walletinfo = self.nodes[1].getwalletinfo()
assert_equal(Decimal(walletinfo['immature_balance']), Decimal('15'))
assert_equal(Decimal(walletinfo['balance']), Decimal('0'))
assert_equal(Decimal(self.nodes[1].z_getbalance(miner_addr, 0)), Decimal('10'))
assert_equal(Decimal(self.nodes[1].z_getbalance(miner_addr)), Decimal('10'))
# Check that the node 0 *immature* transparent balance has been augmented by the funding
# stream payment:
# * 0.5 ZEC per block to fs_transparent
# * 0.75 ZEC per block to the lockbox
walletinfo = self.nodes[0].getwalletinfo()
assert_equal(Decimal(walletinfo['immature_balance']), Decimal('0.5'))
assert_equal(Decimal(walletinfo['shielded_balance']), Decimal('1.25'))
# Check that the node 0 lockbox balance has been augmented by the funding stream payment.
lastBlock = self.nodes[0].getblock('4')
def check_lockbox(blk, expected):
lockbox = next(elem for elem in blk['valuePools'] if elem['id'] == "lockbox")
assert_equal(Decimal(lockbox['chainValue']), expected)
check_lockbox(lastBlock, Decimal('0.75'))
# Invalidate the last block so that we can steal its `hashBlockCommitments` field.
self.nodes[0].invalidateblock(lastBlock['hash'])
self.nodes[1].invalidateblock(lastBlock['hash'])
self.sync_all()
assert_equal(self.nodes[1].getblockchaininfo()['blocks'], 3)
walletinfo = self.nodes[0].getwalletinfo()
assert_equal(Decimal(walletinfo['immature_balance']), Decimal('0'))
assert_equal(Decimal(walletinfo['shielded_balance']), Decimal('1.25'))
coinbaseTx = create_coinbase(
4,
after_blossom=True,
outputs=[CTxOut(int(0.5 * COIN), redeem_script(fs_transparent))],
lockboxvalue=int(0.75 * COIN),
)
block = create_block(
uint256_from_reversed_hex(lastBlock['previousblockhash']),
coinbaseTx,
lastBlock['time'],
hashBlockCommitments=uint256_from_reversed_hex(lastBlock['blockcommitments']),
)
# A coinbase transaction that claims less than the maximum amount should be invalid after NU6.
block.vtx[0].vout[0].nValue -= 1 # too little
block.vtx[0].rehash()
block.rehash()
block.solve()
assert_equal(self.nodes[0].submitblock(block.serialize().hex()), 'bad-cb-not-exact')
# A coinbase transaction that claims more than the maximum amount should also be invalid
# (with a different error).
block.vtx[0].vout[0].nValue += 2 # too much
block.vtx[0].rehash()
block.rehash()
block.solve()
assert_equal(self.nodes[0].submitblock(block.serialize().hex()), 'bad-cb-amount')
# A coinbase transaction that claims exactly the maximum amount should be valid.
block.vtx[0].vout[0].nValue -= 1 # just right
block.vtx[0].rehash()
block.rehash()
block.solve()
assert_equal(self.nodes[0].submitblock(block.serialize().hex()), None)
self.sync_all()
assert_equal(self.nodes[1].getblockchaininfo()['blocks'], 4)
walletinfo = self.nodes[0].getwalletinfo()
assert_equal(Decimal(walletinfo['immature_balance']), Decimal('0.5'))
assert_equal(Decimal(walletinfo['shielded_balance']), Decimal('1.25'))
check_lockbox(self.nodes[0].getblock('4'), Decimal('0.75'))
print("Mining past the end of the funding streams")
self.nodes[0].generate(4)
self.sync_all()
assert_equal(self.nodes[1].getblockchaininfo()['blocks'], 8)
walletinfo = self.nodes[0].getwalletinfo()
assert_equal(Decimal(walletinfo['immature_balance']), 4 * Decimal('0.5'))
assert_equal(Decimal(walletinfo['shielded_balance']), Decimal('1.25'))
check_lockbox(self.nodes[0].getblock('7'), 4 * Decimal('0.75'))
check_lockbox(self.nodes[0].getblock('8'), 4 * Decimal('0.75'))
if __name__ == '__main__':
CoinbaseFundingStreamsTest().main()