#!/usr/bin/env python3 # Copyright (c) 2022 The Dash Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. import copy import struct from decimal import Decimal from io import BytesIO from test_framework.blocktools import ( create_block, create_coinbase, ) from test_framework.authproxy import JSONRPCException from test_framework.key import ECKey from test_framework.messages import ( CAssetLockTx, CAssetUnlockTx, COIN, COutPoint, CTransaction, CTxIn, CTxOut, FromHex, hash256, ser_string, ) from test_framework.script import ( CScript, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_RETURN, hash160, ) from test_framework.test_framework import DashTestFramework from test_framework.util import ( assert_equal, assert_greater_than, assert_greater_than_or_equal, ) llmq_type_test = 100 tiny_amount = int(Decimal("0.0007") * COIN) blocks_in_one_day = 576 class AssetLocksTest(DashTestFramework): def set_test_params(self): self.set_dash_test_params(5, 3) def skip_test_if_missing_module(self): self.skip_if_no_wallet() def create_assetlock(self, coin, amount, pubkey): node_wallet = self.nodes[0] inputs = [CTxIn(COutPoint(int(coin["txid"], 16), coin["vout"]))] credit_outputs = [] tmp_amount = amount if tmp_amount > COIN: tmp_amount -= COIN credit_outputs.append(CTxOut(COIN, CScript([OP_DUP, OP_HASH160, hash160(pubkey), OP_EQUALVERIFY, OP_CHECKSIG]))) credit_outputs.append(CTxOut(tmp_amount, CScript([OP_DUP, OP_HASH160, hash160(pubkey), OP_EQUALVERIFY, OP_CHECKSIG]))) lockTx_payload = CAssetLockTx(1, credit_outputs) remaining = int(COIN * coin['amount']) - tiny_amount - amount tx_output_ret = CTxOut(amount, CScript([OP_RETURN, b""])) tx_output = CTxOut(remaining, CScript([pubkey, OP_CHECKSIG])) lock_tx = CTransaction() lock_tx.vin = inputs lock_tx.vout = [tx_output, tx_output_ret] if remaining > 0 else [tx_output_ret] lock_tx.nVersion = 3 lock_tx.nType = 8 # asset lock type lock_tx.vExtraPayload = lockTx_payload.serialize() lock_tx = node_wallet.signrawtransactionwithwallet(lock_tx.serialize().hex()) self.log.info(f"next tx: {lock_tx} payload: {lockTx_payload}") return FromHex(CTransaction(), lock_tx["hex"]) def create_assetunlock(self, index, withdrawal, pubkey=None, fee=tiny_amount): node_wallet = self.nodes[0] mninfo = self.mninfo assert_greater_than(int(withdrawal), fee) tx_output = CTxOut(int(withdrawal) - fee, CScript([pubkey, OP_CHECKSIG])) # request ID = sha256("plwdtx", index) request_id_buf = ser_string(b"plwdtx") + struct.pack(" 0: self.log.info(f"Generating batch of blocks {amount} left") next = min(10, amount) amount -= next self.bump_mocktime(next) self.nodes[1].generate(next) self.sync_all() def run_test(self): node_wallet = self.nodes[0] node = self.nodes[1] self.set_sporks() self.activate_v20() self.mempool_size = 0 key = ECKey() key.generate() pubkey = key.get_pubkey().get_bytes() self.log.info("Testing asset lock...") locked_1 = 10 * COIN + 141421 locked_2 = 10 * COIN + 314159 coins = node_wallet.listunspent() coin = None while coin is None or COIN * coin['amount'] < locked_2: coin = coins.pop() asset_lock_tx = self.create_assetlock(coin, locked_1, pubkey) self.check_mempool_result(tx=asset_lock_tx, result_expected={'allowed': True}) self.validate_credit_pool_balance(0) txid_in_block = self.send_tx(asset_lock_tx) self.validate_credit_pool_balance(0) node.generate(1) assert_equal(self.get_credit_pool_balance(node=node_wallet), 0) assert_equal(self.get_credit_pool_balance(node=node), locked_1) self.log.info("Generate a number of blocks to ensure this is the longest chain for later in the test when we reconsiderblock") node.generate(12) self.sync_all() self.validate_credit_pool_balance(locked_1) # tx is mined, let's get blockhash self.log.info("Invalidate block with asset lock tx...") block_hash_1 = node_wallet.gettransaction(txid_in_block)['blockhash'] for inode in self.nodes: inode.invalidateblock(block_hash_1) assert_equal(self.get_credit_pool_balance(node=inode), 0) node.generate(3) self.sync_all() self.validate_credit_pool_balance(0) self.log.info("Resubmit asset lock tx to new chain...") # NEW tx appears asset_lock_tx_2 = self.create_assetlock(coin, locked_2, pubkey) txid_in_block = self.send_tx(asset_lock_tx_2) node.generate(1) self.sync_all() self.validate_credit_pool_balance(locked_2) self.log.info("Reconsider old blocks...") for inode in self.nodes: inode.reconsiderblock(block_hash_1) self.validate_credit_pool_balance(locked_1) self.sync_all() self.log.info('Mine block with incorrect credit-pool value...') extra_lock_tx = self.create_assetlock(coin, COIN, pubkey) self.create_and_check_block([extra_lock_tx], expected_error = 'bad-cbtx-assetlocked-amount') self.log.info("Mine a quorum...") self.mine_quorum() self.validate_credit_pool_balance(locked_1) self.log.info("Testing asset unlock...") self.log.info("Generating several txes by same quorum....") self.validate_credit_pool_balance(locked_1) asset_unlock_tx = self.create_assetunlock(101, COIN, pubkey) asset_unlock_tx_late = self.create_assetunlock(102, COIN, pubkey) asset_unlock_tx_too_late = self.create_assetunlock(103, COIN, pubkey) asset_unlock_tx_too_big_fee = self.create_assetunlock(104, COIN, pubkey, fee=int(Decimal("0.1") * COIN)) asset_unlock_tx_zero_fee = self.create_assetunlock(105, COIN, pubkey, fee=0) asset_unlock_tx_duplicate_index = copy.deepcopy(asset_unlock_tx) # modify this tx with duplicated index to make a hash of tx different, otherwise tx would be refused too early asset_unlock_tx_duplicate_index.vout[0].nValue += COIN too_late_height = node.getblockcount() + 48 self.check_mempool_result(tx=asset_unlock_tx, result_expected={'allowed': True}) self.check_mempool_result(tx=asset_unlock_tx_too_big_fee, result_expected={'allowed': False, 'reject-reason' : 'absurdly-high-fee'}) self.check_mempool_result(tx=asset_unlock_tx_zero_fee, result_expected={'allowed': False, 'reject-reason' : 'bad-txns-assetunlock-fee-outofrange'}) # not-verified is a correct faiure from mempool. Mempool knows nothing about CreditPool indexes and he just report that signature is not validated self.check_mempool_result(tx=asset_unlock_tx_duplicate_index, result_expected={'allowed': False, 'reject-reason' : 'bad-assetunlock-not-verified'}) self.log.info("Validating that we calculate payload hash correctly: ask quorum forcely by message hash...") asset_unlock_tx_payload = CAssetUnlockTx() asset_unlock_tx_payload.deserialize(BytesIO(asset_unlock_tx.vExtraPayload)) assert_equal(asset_unlock_tx_payload.quorumHash, int(self.mninfo[0].node.quorum("selectquorum", llmq_type_test, 'e6c7a809d79f78ea85b72d5df7e9bd592aecf151e679d6e976b74f053a7f9056')["quorumHash"], 16)) self.send_tx(asset_unlock_tx) self.mempool_size += 1 self.check_mempool_size() self.validate_credit_pool_balance(locked_1) node.generate(1) self.sync_all() self.validate_credit_pool_balance(locked_1 - COIN) self.mempool_size -= 1 self.check_mempool_size() block_asset_unlock = node.getrawtransaction(asset_unlock_tx.rehash(), 1)['blockhash'] self.send_tx(asset_unlock_tx, expected_error = "Transaction already in block chain", reason = "double copy") self.log.info("Mining next quorum to check tx 'asset_unlock_tx_late' is still valid...") self.mine_quorum() self.log.info("Checking credit pool amount is same...") self.validate_credit_pool_balance(locked_1 - 1 * COIN) self.check_mempool_result(tx=asset_unlock_tx_late, result_expected={'allowed': True}) self.log.info("Checking credit pool amount still is same...") self.validate_credit_pool_balance(locked_1 - 1 * COIN) self.send_tx(asset_unlock_tx_late) node.generate(1) self.sync_all() self.validate_credit_pool_balance(locked_1 - 2 * COIN) self.log.info("Generating many blocks to make quorum far behind (even still active)...") self.slowly_generate_batch(too_late_height - node.getblockcount() - 1) self.check_mempool_result(tx=asset_unlock_tx_too_late, result_expected={'allowed': True}) node.generate(1) self.sync_all() self.check_mempool_result(tx=asset_unlock_tx_too_late, result_expected={'allowed': False, 'reject-reason' : 'bad-assetunlock-too-late'}) self.log.info("Checking that two quorums later it is too late because quorum is not active...") self.mine_quorum() self.log.info("Expecting new reject-reason...") self.check_mempool_result(tx=asset_unlock_tx_too_late, result_expected={'allowed': False, 'reject-reason' : 'bad-assetunlock-not-active-quorum'}) block_to_reconsider = node.getbestblockhash() self.log.info("Test block invalidation with asset unlock tx...") for inode in self.nodes: inode.invalidateblock(block_asset_unlock) self.validate_credit_pool_balance(locked_1) self.slowly_generate_batch(50) self.validate_credit_pool_balance(locked_1) for inode in self.nodes: inode.reconsiderblock(block_to_reconsider) self.validate_credit_pool_balance(locked_1 - 2 * COIN) self.log.info("Forcibly mining asset_unlock_tx_too_late and ensure block is invalid...") self.create_and_check_block([asset_unlock_tx_too_late], expected_error = "bad-assetunlock-not-active-quorum") node.generate(1) self.sync_all() self.validate_credit_pool_balance(locked_1 - 2 * COIN) self.validate_credit_pool_balance(block_hash=block_hash_1, expected=locked_1) self.log.info("Checking too big withdrawal... expected to not be mined") asset_unlock_tx_full = self.create_assetunlock(201, 1 + self.get_credit_pool_balance(), pubkey) self.log.info("Checking that transaction with exceeding amount accepted by mempool...") # Mempool doesn't know about the size of the credit pool self.check_mempool_result(tx=asset_unlock_tx_full, result_expected={'allowed': True }) txid_in_block = self.send_tx(asset_unlock_tx_full) node.generate(1) self.sync_all() self.ensure_tx_is_not_mined(txid_in_block) self.log.info("Forcibly mine asset_unlock_tx_full and ensure block is invalid...") self.create_and_check_block([asset_unlock_tx_full], expected_error = "failed-creditpool-unlock-too-much") self.mempool_size += 1 asset_unlock_tx_full = self.create_assetunlock(301, self.get_credit_pool_balance(), pubkey) self.check_mempool_result(tx=asset_unlock_tx_full, result_expected={'allowed': True }) txid_in_block = self.send_tx(asset_unlock_tx_full) node.generate(1) self.sync_all() self.log.info("Check txid_in_block was mined...") block = node.getblock(node.getbestblockhash()) assert txid_in_block in block['tx'] self.validate_credit_pool_balance(0) self.log.info("Forcibly mine asset_unlock_tx_full and ensure block is invalid...") self.create_and_check_block([asset_unlock_tx_duplicate_index], expected_error = "bad-assetunlock-duplicated-index") self.log.info("Fast forward to the next day to reset all current unlock limits...") self.slowly_generate_batch(blocks_in_one_day + 1) self.mine_quorum() total = self.get_credit_pool_balance() while total <= 10_900 * COIN: self.log.info(f"Collecting coins in pool... Collected {total}/{10_900 * COIN}") coin = coins.pop() to_lock = int(coin['amount'] * COIN) - tiny_amount if to_lock > 50 * COIN: to_lock = 50 * COIN total += to_lock tx = self.create_assetlock(coin, to_lock, pubkey) self.send_tx_simple(tx) self.sync_mempools() node.generate(1) self.sync_all() credit_pool_balance_1 = self.get_credit_pool_balance() assert_greater_than(credit_pool_balance_1, 10_900 * COIN) limit_amount_1 = 1000 * COIN # take most of limit by one big tx for faster testing and # create several tiny withdrawal with exactly 1 *invalid* / causes spend above limit tx withdrawals = [600 * COIN, 131 * COIN, 131 * COIN, 131 * COIN, 131 * COIN] amount_to_withdraw_1 = sum(withdrawals) index = 400 for next_amount in withdrawals: index += 1 asset_unlock_tx = self.create_assetunlock(index, next_amount, pubkey) self.send_tx_simple(asset_unlock_tx) if index == 401: self.sync_mempools() node.generate(1) self.sync_mempools() node.generate(1) self.sync_all() new_total = self.get_credit_pool_balance() amount_actually_withdrawn = total - new_total block = node.getblock(node.getbestblockhash()) self.log.info("Testing that we tried to withdraw more than we could...") assert_greater_than(amount_to_withdraw_1, amount_actually_withdrawn) self.log.info("Checking that we tried to withdraw more than the limit...") assert_greater_than(amount_to_withdraw_1, limit_amount_1) self.log.info("Checking we didn't actually withdraw more than allowed by the limit...") assert_greater_than_or_equal(limit_amount_1, amount_actually_withdrawn) assert_equal(amount_actually_withdrawn, 993 * COIN) node.generate(1) self.sync_all() self.log.info("Checking that exactly 1 tx stayed in mempool...") self.mempool_size = 1 self.check_mempool_size() assert_equal(new_total, self.get_credit_pool_balance()) self.log.info("Fast forward to next day again...") self.slowly_generate_batch(blocks_in_one_day - 2) self.log.info("Checking mempool is empty now...") self.mempool_size = 0 self.check_mempool_size() self.log.info("Creating new asset-unlock tx. It should be mined exactly 1 block after") credit_pool_balance_2 = self.get_credit_pool_balance() limit_amount_2 = credit_pool_balance_2 // 10 index += 1 asset_unlock_tx = self.create_assetunlock(index, limit_amount_2, pubkey) self.send_tx(asset_unlock_tx) node.generate(1) self.sync_all() assert_equal(new_total, self.get_credit_pool_balance()) node.generate(1) self.sync_all() new_total -= limit_amount_2 assert_equal(new_total, self.get_credit_pool_balance()) self.log.info("Trying to withdraw more... expecting to fail") index += 1 asset_unlock_tx = self.create_assetunlock(index, COIN * 100, pubkey) self.send_tx(asset_unlock_tx) node.generate(1) self.sync_all() self.log.info("generate many blocks to be sure that mempool is empty afterwards...") self.slowly_generate_batch(60) self.log.info("Checking that credit pool is not changed...") assert_equal(new_total, self.get_credit_pool_balance()) self.check_mempool_size() self.activate_mn_rr(expected_activation_height=3090) self.log.info(f'height: {node.getblockcount()} credit: {self.get_credit_pool_balance()}') bt = node.getblocktemplate() platform_reward = bt['masternode'][0]['amount'] assert_equal(bt['masternode'][0]['script'], '6a') # empty OP_RETURN owner_reward = bt['masternode'][1]['amount'] operator_reward = bt['masternode'][2]['amount'] if len(bt['masternode']) == 3 else 0 all_mn_rewards = platform_reward + owner_reward + operator_reward assert_equal(all_mn_rewards, bt['coinbasevalue'] * 0.6) # 60/40 mn/miner reward split assert_equal(platform_reward, int(all_mn_rewards * 0.375)) # 0.375 platform share assert_equal(platform_reward, 2299859813) assert_equal(new_total, self.get_credit_pool_balance()) node.generate(1) self.sync_all() new_total += platform_reward assert_equal(new_total, self.get_credit_pool_balance()) coin = coins.pop() self.send_tx(self.create_assetlock(coin, COIN, pubkey)) new_total += platform_reward + COIN node.generate(1) self.sync_all() # part of fee is going to master node reward # these 2 conditions need to check a range assert_greater_than(self.get_credit_pool_balance(), new_total) assert_greater_than(new_total + tiny_amount, self.get_credit_pool_balance()) if __name__ == '__main__': AssetLocksTest().main()