diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index 65b1e6c4e2c6..e65634cc3dd6 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -1235,3 +1235,32 @@ bool CheckProUpRevTx(const CTransaction& tx, gsl::not_null p return true; } + +bool IsStandardSpecialTx(const CTransaction& tx, std::string& reason) +{ + if (!tx.IsSpecialTxVersion()) return true; + + if (tx.nType != TRANSACTION_ASSET_LOCK) return true; + + // Each input is referenced by Platform's funding state transition; beyond this + // many inputs that state transition exceeds Platform's ~20 kB size limit. + static constexpr size_t MAX_STANDARD_ASSET_LOCK_INPUTS{100}; + if (tx.vin.size() > MAX_STANDARD_ASSET_LOCK_INPUTS) { + reason = "assetlocktx-too-many-inputs"; + return false; + } + + constexpr int max_tx_size_for_platform = 20480; + if (tx.GetTotalSize() > max_tx_size_for_platform) { + reason = "assetlocktx-too-big"; + return false; + } + + if (const auto opt_assetLockTx = GetTxPayload(tx); + opt_assetLockTx.has_value() && opt_assetLockTx->getVersion() >= 2) { + reason = "assetlocktx-version-2"; + return false; + } + + return true; +} diff --git a/src/evo/specialtxman.h b/src/evo/specialtxman.h index 2d5ab32096bb..a6164069a15e 100644 --- a/src/evo/specialtxman.h +++ b/src/evo/specialtxman.h @@ -104,4 +104,20 @@ bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null p bool CheckProUpRevTx(const CTransaction& tx, gsl::not_null pindexPrev, CDeterministicMNManager& dmnman, const ChainstateManager& chainman, TxValidationState& state, bool check_sigs); + +/** + * Asset lock transactions with more than 100 inputs (and so over ~20 kB) can not + * be processed by Platform, so Dash Core nodes should not relay them: they are + * marked non-standard, which keeps the network from propagating them over p2p. + * + * Asset lock v2 is enabled by the v24 fork, but Platform can not process it yet. + * It is kept non-standard so it can be enabled later without another hard fork. + * + * These are relay/mempool checks only: a rejected transaction stays valid inside + * a block. + * + * Returns false (with `reason` set) for a non-standard asset lock, and + * true for any transaction that is not an asset lock or not special-tx. + */ +bool IsStandardSpecialTx(const CTransaction& tx, std::string& reason); #endif // BITCOIN_EVO_SPECIALTXMAN_H diff --git a/src/validation.cpp b/src/validation.cpp index 9b99d0168343..06d45ca61bcb 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -782,6 +782,9 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) if (fRequireStandard && !IsStandardTx(tx, reason)) return state.Invalid(TxValidationResult::TX_NOT_STANDARD, reason); + if (fRequireStandard && !IsStandardSpecialTx(tx, reason)) + return state.Invalid(TxValidationResult::TX_NOT_STANDARD, reason); + // Do not work on transactions that are too small. // A transaction with 1 empty scriptSig input and 1 P2SH output has size of 83 bytes. // Transactions smaller than this are not relayed to mitigate CVE-2017-12842 by not relaying diff --git a/test/functional/feature_asset_locks.py b/test/functional/feature_asset_locks.py index 709cc386e7b5..887461016a2b 100755 --- a/test/functional/feature_asset_locks.py +++ b/test/functional/feature_asset_locks.py @@ -67,7 +67,8 @@ def create_assetlock(self, coin, amount, pubkey): node_wallet = self.nodes[0] - inputs = [CTxIn(COutPoint(int(coin["txid"], 16), coin["vout"]))] + coins = coin if isinstance(coin, list) else [coin] + inputs = [CTxIn(COutPoint(int(c["txid"], 16), c["vout"])) for c in coins] credit_outputs = [] tmp_amount = amount @@ -78,7 +79,7 @@ def create_assetlock(self, coin, amount, pubkey): lockTx_payload = CAssetLockTx(1, credit_outputs) - remaining = int(COIN * coin['amount']) - tiny_amount - amount + remaining = sum(int(COIN * c['amount']) for c in coins) - tiny_amount - amount tx_output_ret = CTxOut(amount, CScript([OP_RETURN, b""])) tx_output = CTxOut(remaining, key_to_p2pk_script(pubkey)) @@ -270,6 +271,7 @@ def run_test(self): self.test_mn_rr(node_wallet, node, pubkey) self.test_withdrawals_fork(node_wallet, node, pubkey) self.test_v24_fork(node_wallet, node, pubkey) + self.test_non_standard(node_wallet, node, pubkey) def test_asset_locks(self, node_wallet, node, pubkey): @@ -759,5 +761,33 @@ def test_v24_fork(self, node_wallet, node, pubkey): assert txid_in_block not in node.getblock(tip_hash)['tx'] + def test_non_standard(self, node_wallet, node, pubkey): + self.log.info("Split one coin into 101 outputs to build an asset lock with >100 inputs") + raw = node_wallet.createrawtransaction([], [{node_wallet.getnewaddress(): 1} for _ in range(101)]) + funded = node_wallet.fundrawtransaction(raw, {'change_position': 101})['hex'] + split_txid = node_wallet.sendrawtransaction(node_wallet.signrawtransactionwithwallet(funded)['hex']) + self.generate(node, 1) + many_coins = [{'txid': split_txid, 'vout': i, 'amount': 1} for i in range(101)] + tx_many_inputs = self.create_assetlock(many_coins, COIN, pubkey) + assert_equal(len(tx_many_inputs.vin), 101) + + self.log.info("A standard node (-acceptnonstdtxn=1) rejects them; the permissive node accepts them") + self.restart_node(1, self.extra_args[1] + ["-acceptnonstdtxn=1"]) + self.connect_nodes(1, 0) + + tx_hex = tx_many_inputs.serialize().hex() + assert_equal(node.testmempoolaccept([tx_hex])[0]['allowed'], True) + rejected = node_wallet.testmempoolaccept([tx_hex])[0] + assert_equal(rejected['allowed'], False) + assert_equal(rejected['reject-reason'], 'assetlocktx-too-many-inputs') + + txid = node.sendrawtransaction(tx_hex) + block_hash = self.generate(node, 1)[0] + for checked_node in self.nodes: + assert txid in checked_node.getblock(block_hash)['tx'] + self.restart_node(1, self.extra_args[1]) + self.connect_nodes(1, 0) + + if __name__ == '__main__': AssetLocksTest().main()