mirror of
https://github.com/dashpay/dash.git
synced 2024-12-26 12:32:48 +01:00
338 lines
12 KiB
Python
338 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) 2015-2020 The Bitcoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
"""Utilities for manipulating blocks and transactions."""
|
|
|
|
from decimal import Decimal
|
|
import io
|
|
import struct
|
|
import time
|
|
import unittest
|
|
|
|
from .messages import (
|
|
CBlock,
|
|
CCbTx,
|
|
COIN,
|
|
COutPoint,
|
|
CTransaction,
|
|
CTxIn,
|
|
CTxOut,
|
|
tx_from_hex,
|
|
from_hex,
|
|
uint256_to_string,
|
|
)
|
|
from .script import CScript, CScriptNum, CScriptOp, OP_TRUE, OP_CHECKSIG
|
|
from .util import assert_equal
|
|
from io import BytesIO
|
|
|
|
MAX_BLOCK_SIGOPS = 40000
|
|
|
|
# Genesis block time (regtest)
|
|
TIME_GENESIS_BLOCK = 1417713337
|
|
|
|
MAX_FUTURE_BLOCK_TIME = 2 * 60 * 60
|
|
|
|
# Coinbase transaction outputs can only be spent after this number of new blocks (network rule)
|
|
COINBASE_MATURITY = 100
|
|
|
|
NORMAL_GBT_REQUEST_PARAMS = {"rules": []} # type: ignore[var-annotated]
|
|
|
|
VERSIONBITS_LAST_OLD_BLOCK_VERSION = 4
|
|
|
|
def create_block(hashprev=None, coinbase=None, ntime=None, *, version=None, tmpl=None, txlist=None, dip4_activated=False, v20_activated=False):
|
|
"""Create a block (with regtest difficulty)."""
|
|
block = CBlock()
|
|
if tmpl is None:
|
|
tmpl = {}
|
|
block.nVersion = version or tmpl.get('version') or VERSIONBITS_LAST_OLD_BLOCK_VERSION
|
|
block.nTime = ntime or tmpl.get('curtime') or int(time.time() + 600)
|
|
block.hashPrevBlock = hashprev or int(tmpl['previousblockhash'], 0x10)
|
|
if tmpl and not tmpl.get('bits') is None:
|
|
block.nBits = struct.unpack('>I', bytes.fromhex(tmpl['bits']))[0]
|
|
else:
|
|
block.nBits = 0x207fffff # difficulty retargeting is disabled in REGTEST chainparams
|
|
if coinbase is None:
|
|
coinbase = create_coinbase(height=tmpl['height'], dip4_activated=dip4_activated, v20_activated=v20_activated)
|
|
block.vtx.append(coinbase)
|
|
if txlist:
|
|
for tx in txlist:
|
|
if not hasattr(tx, 'calc_sha256'):
|
|
txo = CTransaction()
|
|
txo.deserialize(io.BytesIO(tx))
|
|
tx = txo
|
|
block.vtx.append(tx)
|
|
block.hashMerkleRoot = block.calc_merkle_root()
|
|
block.calc_sha256()
|
|
return block
|
|
|
|
# TODO: imlement MN_RR support here or remove it
|
|
def create_block_with_mnpayments(mninfo, node, vtx=None, mn_payee=None, mn_amount=None):
|
|
if vtx is None:
|
|
vtx = []
|
|
bt = node.getblocktemplate()
|
|
height = bt['height']
|
|
tip_hash = bt['previousblockhash']
|
|
coinbasevalue = bt['coinbasevalue']
|
|
|
|
assert len(bt['masternode']) <= 2
|
|
if mn_payee is None:
|
|
mn_payee = bt['masternode'][0]['payee']
|
|
|
|
mn_operator_payee = None
|
|
if len(bt['masternode']) == 2:
|
|
mn_operator_payee = bt['masternode'][1]['payee']
|
|
# we can't take the masternode payee amount from the template here as we might have additional fees in vtx
|
|
|
|
# calculate fees that the block template included (we'll have to remove it from the coinbase as we won't
|
|
# include the template's transactions
|
|
bt_fees = 0
|
|
for tx in bt['transactions']:
|
|
bt_fees += tx['fee']
|
|
|
|
new_fees = 0
|
|
for tx in vtx:
|
|
in_value = 0
|
|
out_value = 0
|
|
for txin in tx.vin:
|
|
txout = node.gettxout(uint256_to_string(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
|
|
|
|
# fix fees
|
|
coinbasevalue -= bt_fees
|
|
coinbasevalue += new_fees
|
|
|
|
operator_reward = 0
|
|
if mn_operator_payee is not None:
|
|
for mn in mninfo:
|
|
if mn.rewards_address == mn_payee:
|
|
operator_reward = mn.operator_reward
|
|
break
|
|
assert operator_reward > 0
|
|
|
|
mn_operator_amount = 0
|
|
if mn_amount is None:
|
|
v20_info = node.getblockchaininfo()['softforks']['v20']
|
|
mn_amount_total = get_masternode_payment(height, coinbasevalue, v20_info['active'])
|
|
mn_operator_amount = mn_amount_total * operator_reward // 100
|
|
mn_amount = mn_amount_total - mn_operator_amount
|
|
miner_amount = coinbasevalue - mn_amount - mn_operator_amount
|
|
|
|
miner_address = node.get_deterministic_priv_key().address
|
|
outputs = {miner_address: str(Decimal(miner_amount) / COIN)}
|
|
if mn_amount > 0:
|
|
outputs[mn_payee] = str(Decimal(mn_amount) / COIN)
|
|
if mn_operator_amount > 0:
|
|
outputs[mn_operator_payee] = str(Decimal(mn_operator_amount) / COIN)
|
|
|
|
coinbase = tx_from_hex(node.createrawtransaction([], outputs))
|
|
coinbase.vin = create_coinbase(height).vin
|
|
|
|
# We can't really use this one as it would result in invalid merkle roots for masternode lists
|
|
if len(bt['coinbase_payload']) != 0:
|
|
tip_block = node.getblock(tip_hash)
|
|
cbtx = from_hex(CCbTx(version=1), bt['coinbase_payload'])
|
|
if 'cbTx' in tip_block:
|
|
cbtx.merkleRootMNList = int(tip_block['cbTx']['merkleRootMNList'], 16)
|
|
else:
|
|
cbtx.merkleRootMNList = 0
|
|
coinbase.nVersion = 3
|
|
coinbase.nType = 5 # CbTx
|
|
coinbase.vExtraPayload = cbtx.serialize()
|
|
|
|
coinbase.calc_sha256()
|
|
|
|
block = create_block(int(tip_hash, 16), coinbase, ntime=bt['curtime'], version=bt['version'])
|
|
block.vtx += vtx
|
|
|
|
# Add quorum commitments from template
|
|
for tx in bt['transactions']:
|
|
tx2 = tx_from_hex(tx['data'])
|
|
if tx2.nType == 6:
|
|
block.vtx.append(tx2)
|
|
|
|
block.hashMerkleRoot = block.calc_merkle_root()
|
|
block.solve()
|
|
return block
|
|
|
|
def script_BIP34_coinbase_height(height):
|
|
if height <= 16:
|
|
res = CScriptOp.encode_op_n(height)
|
|
# Append dummy to increase scriptSig size above 2 (see bad-cb-length consensus rule)
|
|
return CScript([res, OP_TRUE])
|
|
return CScript([CScriptNum(height)])
|
|
|
|
|
|
def create_coinbase(height, pubkey=None, dip4_activated=False, v20_activated=False, nValue=500):
|
|
"""Create a coinbase transaction, assuming no miner fees.
|
|
|
|
If pubkey is passed in, the coinbase output will be a P2PK output;
|
|
otherwise an anyone-can-spend output."""
|
|
coinbase = CTransaction()
|
|
coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), 0xffffffff))
|
|
coinbaseoutput = CTxOut()
|
|
coinbaseoutput.nValue = nValue * COIN
|
|
if nValue == 500:
|
|
halvings = int(height / 150) # regtest
|
|
coinbaseoutput.nValue >>= halvings
|
|
if (pubkey is not None):
|
|
coinbaseoutput.scriptPubKey = CScript([pubkey, OP_CHECKSIG])
|
|
else:
|
|
coinbaseoutput.scriptPubKey = CScript([OP_TRUE])
|
|
coinbase.vout = [coinbaseoutput]
|
|
if dip4_activated:
|
|
coinbase.nVersion = 3
|
|
coinbase.nType = 5
|
|
cbtx_version = 3 if v20_activated else 2
|
|
cbtx_payload = CCbTx(cbtx_version, height, 0, 0, 0)
|
|
coinbase.vExtraPayload = cbtx_payload.serialize()
|
|
coinbase.calc_sha256()
|
|
return coinbase
|
|
|
|
def create_tx_with_script(prevtx, n, script_sig=b"", *, amount, script_pub_key=CScript()):
|
|
"""Return one-input, one-output transaction object
|
|
spending the prevtx's n-th output with the given amount.
|
|
|
|
Can optionally pass scriptPubKey and scriptSig, default is anyone-can-spend output.
|
|
"""
|
|
tx = CTransaction()
|
|
assert n < len(prevtx.vout)
|
|
tx.vin.append(CTxIn(COutPoint(prevtx.sha256, n), script_sig, 0xffffffff))
|
|
tx.vout.append(CTxOut(amount, script_pub_key))
|
|
tx.calc_sha256()
|
|
return tx
|
|
|
|
def create_transaction(node, txid, to_address, *, amount):
|
|
""" Return signed transaction spending the first output of the
|
|
input txid. Note that the node must have a wallet that can
|
|
sign for the output that is being spent.
|
|
"""
|
|
raw_tx = create_raw_transaction(node, txid, to_address, amount=amount)
|
|
tx = CTransaction()
|
|
tx.deserialize(BytesIO(bytes.fromhex(raw_tx)))
|
|
return tx
|
|
|
|
def create_raw_transaction(node, txid, to_address, *, amount):
|
|
""" Return raw signed transaction spending the first output of the
|
|
input txid. Note that the node must have a wallet that can sign
|
|
for the output that is being spent.
|
|
"""
|
|
psbt = node.createpsbt(inputs=[{"txid": txid, "vout": 0}], outputs={to_address: amount})
|
|
for _ in range(2):
|
|
for w in node.listwallets():
|
|
wrpc = node.get_wallet_rpc(w)
|
|
signed_psbt = wrpc.walletprocesspsbt(psbt)
|
|
psbt = signed_psbt['psbt']
|
|
final_psbt = node.finalizepsbt(psbt)
|
|
assert_equal(final_psbt["complete"], True)
|
|
return final_psbt['hex']
|
|
|
|
def get_legacy_sigopcount_block(block, accurate=True):
|
|
count = 0
|
|
for tx in block.vtx:
|
|
count += get_legacy_sigopcount_tx(tx, accurate)
|
|
return count
|
|
|
|
def get_legacy_sigopcount_tx(tx, accurate=True):
|
|
count = 0
|
|
for i in tx.vout:
|
|
count += i.scriptPubKey.GetSigOpCount(accurate)
|
|
for j in tx.vin:
|
|
# scriptSig might be of type bytes, so convert to CScript for the moment
|
|
count += CScript(j.scriptSig).GetSigOpCount(accurate)
|
|
return count
|
|
|
|
"""
|
|
Dash chaintips rpc returns extra info in each tip (difficulty, chainwork, and
|
|
forkpoint). Filter down to relevant ones checked in this test.
|
|
"""
|
|
def filter_tip_keys(chaintips):
|
|
check_keys = ["hash", "height", "branchlen", "status"]
|
|
filtered_tips = []
|
|
for tip in chaintips:
|
|
filtered_tips.append({k: tip[k] for k in check_keys})
|
|
return filtered_tips
|
|
|
|
# Identical to GetMasternodePayment in C++ code
|
|
# TODO: remove it or make **proper** tests for various height
|
|
def get_masternode_payment(nHeight, blockValue, fV20Active):
|
|
ret = int(blockValue / 5)
|
|
|
|
nMNPIBlock = 350
|
|
nMNPIPeriod = 10
|
|
nReallocActivationHeight = 1
|
|
|
|
if nHeight > nMNPIBlock:
|
|
ret += int(blockValue / 20)
|
|
if nHeight > nMNPIBlock+(nMNPIPeriod* 1):
|
|
ret += int(blockValue / 20)
|
|
if nHeight > nMNPIBlock+(nMNPIPeriod* 2):
|
|
ret += int(blockValue / 20)
|
|
if nHeight > nMNPIBlock+(nMNPIPeriod* 3):
|
|
ret += int(blockValue / 40)
|
|
if nHeight > nMNPIBlock+(nMNPIPeriod* 4):
|
|
ret += int(blockValue / 40)
|
|
if nHeight > nMNPIBlock+(nMNPIPeriod* 5):
|
|
ret += int(blockValue / 40)
|
|
if nHeight > nMNPIBlock+(nMNPIPeriod* 6):
|
|
ret += int(blockValue / 40)
|
|
if nHeight > nMNPIBlock+(nMNPIPeriod* 7):
|
|
ret += int(blockValue / 40)
|
|
if nHeight > nMNPIBlock+(nMNPIPeriod* 9):
|
|
ret += int(blockValue / 40)
|
|
|
|
if nHeight < nReallocActivationHeight:
|
|
# Block Reward Realocation is not activated yet, nothing to do
|
|
return ret
|
|
|
|
nSuperblockCycle = 20
|
|
# Actual realocation starts in the cycle next to one activation happens in
|
|
nReallocStart = nReallocActivationHeight - nReallocActivationHeight % nSuperblockCycle + nSuperblockCycle
|
|
|
|
if nHeight < nReallocStart:
|
|
# Activated but we have to wait for the next cycle to start realocation, nothing to do
|
|
return ret
|
|
|
|
if fV20Active:
|
|
# Once MNRewardReallocated activates, block reward is 80% of block subsidy (+ tx fees) since treasury is 20%
|
|
# Since the MN reward needs to be equal to 60% of the block subsidy (according to the proposal), MN reward is set to 75% of the block reward.
|
|
# Previous reallocation periods are dropped.
|
|
return blockValue * 3 // 4
|
|
|
|
# Periods used to reallocate the masternode reward from 50% to 60%
|
|
vecPeriods = [
|
|
513, # Period 1: 51.3%
|
|
526, # Period 2: 52.6%
|
|
533, # Period 3: 53.3%
|
|
540, # Period 4: 54%
|
|
546, # Period 5: 54.6%
|
|
552, # Period 6: 55.2%
|
|
557, # Period 7: 55.7%
|
|
562, # Period 8: 56.2%
|
|
567, # Period 9: 56.7%
|
|
572, # Period 10: 57.2%
|
|
577, # Period 11: 57.7%
|
|
582, # Period 12: 58.2%
|
|
585, # Period 13: 58.5%
|
|
588, # Period 14: 58.8%
|
|
591, # Period 15: 59.1%
|
|
594, # Period 16: 59.4%
|
|
597, # Period 17: 59.7%
|
|
599, # Period 18: 59.9%
|
|
600 # Period 19: 60%
|
|
]
|
|
|
|
nReallocCycle = nSuperblockCycle * 3
|
|
nCurrentPeriod = min(int((nHeight - nReallocStart) / nReallocCycle), len(vecPeriods) - 1)
|
|
|
|
return int(blockValue * vecPeriods[nCurrentPeriod] / 1000)
|
|
|
|
class TestFrameworkBlockTools(unittest.TestCase):
|
|
def test_create_coinbase(self):
|
|
height = 20
|
|
coinbase_tx = create_coinbase(height=height)
|
|
assert_equal(CScriptNum.decode(coinbase_tx.vin[0].scriptSig), height)
|