diff --git a/clippy.toml b/clippy.toml index 1bd6147d..0d4e02f0 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -too-many-arguments-threshold = 15 +too-many-arguments-threshold = 20 diff --git a/docs/samples/solver/solver.rs b/docs/samples/solver/solver.rs index fdb3b9c5..75fbdcd7 100644 --- a/docs/samples/solver/solver.rs +++ b/docs/samples/solver/solver.rs @@ -50,7 +50,10 @@ fn build_coinbase(script_sig: Vec, script_pubkey: Vec, amount_duffs: &st ScriptSigBuf::from_bytes(script_sig), 0xFFFF_FFFF, )], - vec![TxOut::new(value, ScriptPubKeyBuf::from_bytes(script_pubkey))], + vec![TxOut::new( + value.to_signed().to_sat(), + ScriptPubKeyBuf::from_bytes(script_pubkey), + )], 0, Vec::new(), )) diff --git a/maint/codeql/rust/lib/policy.qll b/maint/codeql/rust/lib/policy.qll index 1418ea26..3d9d0a51 100644 --- a/maint/codeql/rust/lib/policy.qll +++ b/maint/codeql/rust/lib/policy.qll @@ -68,6 +68,12 @@ predicate isSecretType(TypeItem t) { // The identifier a share is issued against is the participant's, known to every member of the // quorum; only the scalar the share carries is secret. "BlsShareId", + // A masternode's owner payouts and the shares of its collateral hold public scripts, key ids + // and amounts. + "PayoutShare", + "CollateralShare", + // A share's reward update holds a public script and signature. + "ProUpShareTx", // Serde artifact to deserialize a tagged enum. "__Seed" ] diff --git a/pkgs/primitives/corpus/assetlock.json5 b/pkgs/primitives/corpus/assetlock.json5 index 446bf6ff..647f3f33 100644 --- a/pkgs/primitives/corpus/assetlock.json5 +++ b/pkgs/primitives/corpus/assetlock.json5 @@ -1,3 +1,14 @@ +// +// Asset lock vectors. Sections with a program were generated with the +// reference implementation (dashpay/dash v24.0.0-rc.3, 0364ed626e). +// +// To run a program, paste it at the end of the `evo_assetlocks_tests` suite +// in `src/test/evo_assetlocks_tests.cpp` at the specified revision, build it +// with `make -C src test/test_dash` and run +// `./src/test/test_dash --run_test=evo_assetlocks_tests/`. The output +// should match the section. +// + { "assetlock": { // Block 0000000000000017863c74745bfd506478eac05e032e2d546fe5f003c7a20bea @@ -260,5 +271,100 @@ ] } } - } + }, +/* +BOOST_AUTO_TEST_CASE(assetlock_check) +{ + const bool is_v24_active{true}; + const CScript p2pkh{GetScriptForDestination(PKHash{uint160{}})}; + const CScript p2sh{GetScriptForDestination(ScriptHash{uint160{}})}; + const CScript other{CScript() << OP_TRUE}; + for (const auto& [version, credits] : std::vector>>{ + {1, {{1, p2pkh}}}, + {1, {{0, p2pkh}}}, + {1, {{-1, p2pkh}}}, + {1, {{MAX_MONEY, p2pkh}}}, + {1, {{MAX_MONEY + 1, p2pkh}}}, + {1, {{MAX_MONEY, p2pkh}, {1, p2pkh}}}, + {1, {{1, p2pkh}, {-1, p2pkh}}}, + {1, {{CAmount{0x7fffffffffffffff}, p2pkh}}}, + {1, {{-CAmount{0x7fffffffffffffff} - 1, p2pkh}}}, + {1, {{MAX_MONEY, p2pkh}, {CAmount{0x7fffffffffffffff}, p2pkh}}}, + {0, {{1, p2pkh}}}, + {2, {{1, p2pkh}}}, + {3, {{1, p2pkh}}}, + {1, {{1, p2sh}}}, + {2, {{1, p2sh}}}, + {1, {{1, other}}}, + {2, {{1, other}}}, + {2, {{1, p2pkh}, {1, p2sh}}}, + {2, {{1, p2sh}, {1, other}}}, + {2, {{0, other}}}, + }) { + CAmount total{0}; + for (const CTxOut& credit : credits) { + if (MoneyRange(credit.nValue)) total += credit.nValue; + } + CMutableTransaction tx; + tx.nVersion = 3; + tx.nType = TRANSACTION_ASSET_LOCK; + tx.vout.emplace_back(total > 0 && MoneyRange(total) ? total : 1, CScript() << OP_RETURN << ParseHex("")); + SetTxPayload(tx, CAssetLockPayload{credits, version}); + TxValidationState state; + CheckAssetLockTx(CTransaction{tx}, state, is_v24_active); + std::printf(" { \"version\": %u, \"values\": [", version); + for (size_t i = 0; i < credits.size(); ++i) std::printf("%s%lld", i ? ", " : "", (long long)credits[i].nValue); + std::printf("], \"payload\": \"%s\", \"reason\": \"%s\" },\n", HexStr(tx.vExtraPayload).c_str(), + state.GetRejectReason().c_str()); + } +} +*/ + // Each vector is an asset lock payload of `version` with one credit output + // per value. `payload` is its serialization, which holds the credit + // scripts, and `reason` the rejection reason, empty when the payload + // passes. The transaction around it has an OP_RETURN output of the total + // of the credits in the money range, or of 1 when that total is 0 or out + // of range. + "check": [ + // One credit of one duff passes. + { "version": 1, "values": [1], "payload": "010101000000000000001976a914000000000000000000000000000000000000000088ac", "reason": "" }, + // A zero credit is out of range. + { "version": 1, "values": [0], "payload": "010100000000000000001976a914000000000000000000000000000000000000000088ac", "reason": "bad-assetlocktx-credit-outofrange" }, + // A negative credit decodes and is out of range. + { "version": 1, "values": [-1], "payload": "0101ffffffffffffffff1976a914000000000000000000000000000000000000000088ac", "reason": "bad-assetlocktx-credit-outofrange" }, + // A credit of exactly the money range passes. + { "version": 1, "values": [2100000000000000], "payload": "01010040075af07507001976a914000000000000000000000000000000000000000088ac", "reason": "" }, + // A credit one duff above the money range. + { "version": 1, "values": [2100000000000001], "payload": "01010140075af07507001976a914000000000000000000000000000000000000000088ac", "reason": "bad-assetlocktx-credit-outofrange" }, + // Two credits in range whose total exceeds the money range. + { "version": 1, "values": [2100000000000000, 1], "payload": "01020040075af07507001976a914000000000000000000000000000000000000000088ac01000000000000001976a914000000000000000000000000000000000000000088ac", "reason": "bad-assetlocktx-credit-outofrange" }, + // A negative credit after a valid one. + { "version": 1, "values": [1, -1], "payload": "010201000000000000001976a914000000000000000000000000000000000000000088acffffffffffffffff1976a914000000000000000000000000000000000000000088ac", "reason": "bad-assetlocktx-credit-outofrange" }, + // The highest 64-bit credit is out of range. + { "version": 1, "values": [9223372036854775807], "payload": "0101ffffffffffffff7f1976a914000000000000000000000000000000000000000088ac", "reason": "bad-assetlocktx-credit-outofrange" }, + // The lowest 64-bit credit is out of range. + { "version": 1, "values": [-9223372036854775808], "payload": "010100000000000000801976a914000000000000000000000000000000000000000088ac", "reason": "bad-assetlocktx-credit-outofrange" }, + // The range check of a credit comes before it is added to the total. + { "version": 1, "values": [2100000000000000, 9223372036854775807], "payload": "01020040075af07507001976a914000000000000000000000000000000000000000088acffffffffffffff7f1976a914000000000000000000000000000000000000000088ac", "reason": "bad-assetlocktx-credit-outofrange" }, + // Version 0 is invalid. + { "version": 0, "values": [1], "payload": "000101000000000000001976a914000000000000000000000000000000000000000088ac", "reason": "bad-assetlocktx-version" }, + // Version 2, the current version, passes. + { "version": 2, "values": [1], "payload": "020101000000000000001976a914000000000000000000000000000000000000000088ac", "reason": "" }, + // Version 3 is above the current version. + { "version": 3, "values": [1], "payload": "030101000000000000001976a914000000000000000000000000000000000000000088ac", "reason": "bad-assetlocktx-version" }, + // Version 1 rejects a P2SH credit. + { "version": 1, "values": [1], "payload": "0101010000000000000017a914000000000000000000000000000000000000000087", "reason": "bad-assetlocktx-pubKeyHash" }, + // Version 2 accepts a P2SH credit. + { "version": 2, "values": [1], "payload": "0201010000000000000017a914000000000000000000000000000000000000000087", "reason": "" }, + // Version 1 rejects a credit that is neither P2PKH nor P2SH. + { "version": 1, "values": [1], "payload": "010101000000000000000151", "reason": "bad-assetlocktx-pubKeyHash" }, + // Version 2 rejects it with its own reason. + { "version": 2, "values": [1], "payload": "020101000000000000000151", "reason": "bad-assetlocktx-script-pubkey" }, + // Version 2 accepts P2PKH and P2SH credits together. + { "version": 2, "values": [1, 1], "payload": "020201000000000000001976a914000000000000000000000000000000000000000088ac010000000000000017a914000000000000000000000000000000000000000087", "reason": "" }, + // A bad script after a valid credit. + { "version": 2, "values": [1, 1], "payload": "0202010000000000000017a91400000000000000000000000000000000000000008701000000000000000151", "reason": "bad-assetlocktx-script-pubkey" }, + // The range check comes before the script check. + { "version": 2, "values": [0], "payload": "020100000000000000000151", "reason": "bad-assetlocktx-credit-outofrange" }, + ], } diff --git a/pkgs/primitives/corpus/assetunlock.json5 b/pkgs/primitives/corpus/assetunlock.json5 index 6d63fbcd..f931c630 100644 --- a/pkgs/primitives/corpus/assetunlock.json5 +++ b/pkgs/primitives/corpus/assetunlock.json5 @@ -1,3 +1,14 @@ +// +// Asset unlock vectors. Sections with a program were generated with the +// reference implementation (dashpay/dash v24.0.0-rc.3, 0364ed626e). +// +// To run a program, paste it at the end of the `evo_assetlocks_tests` suite +// in `src/test/evo_assetlocks_tests.cpp` at the specified revision, build it +// with `make -C src test/test_dash` and run +// `./src/test/test_dash --run_test=evo_assetlocks_tests/`. The output +// should match the section. +// + { "assetunlock": { // Block 000000000000001d34d9b48fcc7570106ec674aca8c73fc01b0363646520d6c6 @@ -240,5 +251,120 @@ "quorumSig": "b6181477febad821ac60f1493f117d7600bbeb5d83d2fdde31d1101233355ad1f60ee45215e0b1f5db91be71868798c117976ddb6ec0a3f13fc19b8555623d3de16f0d882f2ae39daf54022c70405da1698662ecd72428abc88b782b48239fbd" } } - } + }, +/* +BOOST_AUTO_TEST_CASE(assetunlock_check) +{ + LOCK(cs_main); + const bool is_v24_active{true}; + const auto& blockman{Assert(m_node.chainman)->m_blockman}; + const auto& qman{*Assert(m_node.llmq_ctx)->qman}; + const CBlockIndex* tip{m_node.chainman->ActiveChain().Tip()}; + for (const auto& [version, fee] : std::vector>{ + {1, 1}, {2, 1}, {0, 1}, {3, 1}, {255, 1}, {1, 0}, {2, 0}}) { + CMutableTransaction tx; + tx.nVersion = 3; + tx.nType = TRANSACTION_ASSET_UNLOCK; + tx.vout.emplace_back(1, GetScriptForDestination(PKHash{uint160{}})); + SetTxPayload(tx, CAssetUnlockPayload{version, 1, fee, 1, uint256::ONE, CBLSSignature{}}); + TxValidationState state; + CheckAssetUnlockTx(blockman, qman, CTransaction{tx}, tip, std::nullopt, is_v24_active, state); + TxValidationState fee_state; + CAmount tx_fee; + GetAssetUnlockFee(CTransaction{tx}, tx_fee, fee_state); + std::printf(" { \"version\": %u, \"fee\": %u, \"payload\": \"%s\", \"reason\": \"%s\", \"fee_reason\": \"%s\" },\n", + version, fee, HexStr(tx.vExtraPayload).c_str(), state.GetRejectReason().c_str(), + fee_state.GetRejectReason().c_str()); + } +} +*/ + // Each vector is an asset unlock payload of `version` and `fee`, serialized + // as `payload`. `reason` is the rejection reason of the asset unlock check + // with v24 active and `fee_reason` that of the fee check, each empty when + // it passes. The quorum hash is not in the chain, so a payload that passes + // the version check fails on its quorum hash. + "check": [ + // Version 1 passes the version check. + { "version": 1, "fee": 1, "payload": "01010000000000000001000000010000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-assetunlock-quorum-hash", "fee_reason": "" }, + // Version 2, the current version, passes the version check. + { "version": 2, "fee": 1, "payload": "02010000000000000001000000010000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-assetunlock-quorum-hash", "fee_reason": "" }, + // Version 0 is invalid. + { "version": 0, "fee": 1, "payload": "00010000000000000001000000010000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-assetunlocktx-version", "fee_reason": "" }, + // Version 3 is above the current version. + { "version": 3, "fee": 1, "payload": "03010000000000000001000000010000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-assetunlocktx-version", "fee_reason": "" }, + // The highest version byte is above the current version. + { "version": 255, "fee": 1, "payload": "ff010000000000000001000000010000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-assetunlocktx-version", "fee_reason": "" }, + // A zero fee is out of range. + { "version": 1, "fee": 0, "payload": "01010000000000000000000000010000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-assetunlock-quorum-hash", "fee_reason": "bad-txns-assetunlock-fee-outofrange" }, + // A zero fee is out of range in version 2 as well. + { "version": 2, "fee": 0, "payload": "02010000000000000000000000010000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-assetunlock-quorum-hash", "fee_reason": "bad-txns-assetunlock-fee-outofrange" }, + ], +/* +BOOST_AUTO_TEST_CASE(stable_txid) +{ + struct UnlockShape { + int16_t tx_version; + uint16_t type; + uint8_t version; + uint64_t index; + uint32_t requested_height; + uint256 quorum_hash; + int resize; + }; + uint160 pubkey_hash; + std::fill(pubkey_hash.begin(), pubkey_hash.end(), 0x11); + for (const UnlockShape& s : std::vector{ + {3, TRANSACTION_ASSET_UNLOCK, 1, 101, 500, uint256::ONE, 0}, + {3, TRANSACTION_ASSET_UNLOCK, 1, 101, 700, uint256::TWO, 0}, + {3, TRANSACTION_ASSET_UNLOCK, 2, 101, 500, uint256::ONE, 0}, + {3, TRANSACTION_ASSET_UNLOCK, 2, 101, 700, uint256::TWO, 0}, + {3, TRANSACTION_ASSET_UNLOCK, 2, 123456789, 500, uint256::ONE, 0}, + {3, TRANSACTION_ASSET_UNLOCK, 3, 101, 500, uint256::ONE, 0}, + {3, TRANSACTION_ASSET_UNLOCK, 2, 101, 500, uint256::ONE, -1}, + {3, TRANSACTION_ASSET_UNLOCK, 2, 101, 500, uint256::ONE, 1}, + {3, TRANSACTION_ASSET_LOCK, 2, 101, 500, uint256::ONE, 0}, + {2, TRANSACTION_ASSET_UNLOCK, 2, 101, 500, uint256::ONE, 0}, + }) { + CMutableTransaction mtx; + mtx.nVersion = s.tx_version; + mtx.nType = s.type; + mtx.vout.emplace_back(100000000, GetScriptForDestination(PKHash{pubkey_hash})); + SetTxPayload(mtx, CAssetUnlockPayload{s.version, s.index, 70000, s.requested_height, s.quorum_hash, {}}); + mtx.vExtraPayload.resize(mtx.vExtraPayload.size() + s.resize); + const CTransaction tx{mtx}; + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << tx; + std::printf(" { \"raw\": \"%s\", \"payload\": \"%s\", \"txid\": \"%s\", \"instance\": \"%s\", \"stable\": %s },\n", + HexStr(ss).c_str(), HexStr(tx.vExtraPayload).c_str(), tx.GetHash().ToString().c_str(), + tx.GetInstanceHash().ToString().c_str(), IsAssetUnlockWithStableTxid(tx) ? "true" : "false"); + } +} +*/ + // Each vector is a transaction with one output and an asset unlock + // payload. `raw` is its serialization and `payload` the payload it holds, + // which transaction version 2 does not serialize. `txid` is its hash, + // `instance` the hash of the whole serialization, and `stable` whether the + // txid zeroes the quorum signing info at the end of the payload. + "stable_txid": [ + // A version 1 payload hashes in full. + { "raw": "03000900000100e1f505000000001976a914111111111111111111111111111111111111111188ac000000009101650000000000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "payload": "01650000000000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "txid": "6e74a48b7b4a6de0a032b68cc353217e437fa3ac2dcd4c80e76a12efbb2fe1dc", "instance": "6e74a48b7b4a6de0a032b68cc353217e437fa3ac2dcd4c80e76a12efbb2fe1dc", "stable": false }, + // Version 1 with other quorum signing info has another txid. + { "raw": "03000900000100e1f505000000001976a914111111111111111111111111111111111111111188ac000000009101650000000000000070110100bc0200000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "payload": "01650000000000000070110100bc0200000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "txid": "4f9a0d89dabb821774bbcf6c073d04b26acd2e1f5a2fa0e56a8f7b9e3e54767e", "instance": "4f9a0d89dabb821774bbcf6c073d04b26acd2e1f5a2fa0e56a8f7b9e3e54767e", "stable": false }, + // Version 2 zeroes the quorum signing info for the txid. + { "raw": "03000900000100e1f505000000001976a914111111111111111111111111111111111111111188ac000000009102650000000000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "payload": "02650000000000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "txid": "3c4db73c8356407a5d7c78df5045bd280f2dc4fd644b06c4bfbdead3d5ae41cf", "instance": "7df5cfea6865226753795ccdd8536781a179f2767febf4281e2236e178dc4a50", "stable": true }, + // Version 2 with other quorum signing info keeps the txid. + { "raw": "03000900000100e1f505000000001976a914111111111111111111111111111111111111111188ac000000009102650000000000000070110100bc0200000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "payload": "02650000000000000070110100bc0200000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "txid": "3c4db73c8356407a5d7c78df5045bd280f2dc4fd644b06c4bfbdead3d5ae41cf", "instance": "6c4017f7a880668e7e4506f15841f3139697dad0ba2b67db0ca7f2542643aea1", "stable": true }, + // Version 2 with another withdrawal index has another txid. + { "raw": "03000900000100e1f505000000001976a914111111111111111111111111111111111111111188ac00000000910215cd5b070000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "payload": "0215cd5b070000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "txid": "a67e1107ae6e04b813bc8e81348266f5206d1ca93d305dc4323940e18cdbaf34", "instance": "740f83dd8cd91e10fadccb89fdd609452570ff4e289b28cb9737c87bf509552b", "stable": true }, + // Version 3 has a stable txid too. + { "raw": "03000900000100e1f505000000001976a914111111111111111111111111111111111111111188ac000000009103650000000000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "payload": "03650000000000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "txid": "617c6f3859b15f8ca6b49b39d659fe95f4558b8735983a8739b3919a0836460e", "instance": "b2f736fb81769031cddcca650857d7fa135aa7c9203a1ac26b53ef7d1371bc34", "stable": true }, + // A payload one byte short hashes in full. + { "raw": "03000900000100e1f505000000001976a914111111111111111111111111111111111111111188ac000000009002650000000000000070110100f401000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "payload": "02650000000000000070110100f401000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "txid": "8132cb9be949190775f5ce44c1b0618939f30fc93537e955cf89efb50894ef14", "instance": "8132cb9be949190775f5ce44c1b0618939f30fc93537e955cf89efb50894ef14", "stable": false }, + // A payload one byte long hashes in full. + { "raw": "03000900000100e1f505000000001976a914111111111111111111111111111111111111111188ac000000009202650000000000000070110100f4010000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "payload": "02650000000000000070110100f4010000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "txid": "206a814b3e19b86538266d92dad5b7ed9f59d521144c35c956b421e1113e1554", "instance": "206a814b3e19b86538266d92dad5b7ed9f59d521144c35c956b421e1113e1554", "stable": false }, + // An asset lock with the same payload hashes in full. + { "raw": "03000800000100e1f505000000001976a914111111111111111111111111111111111111111188ac000000009102650000000000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "payload": "02650000000000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "txid": "80b04f7ccf6c25aff2ed8cda2938bce2ad5d20455275dddabe0d703170ea2ae2", "instance": "80b04f7ccf6c25aff2ed8cda2938bce2ad5d20455275dddabe0d703170ea2ae2", "stable": false }, + // Transaction version 2 does not serialize its payload and hashes in full. + { "raw": "02000900000100e1f505000000001976a914111111111111111111111111111111111111111188ac00000000", "payload": "02650000000000000070110100f40100000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "txid": "d49f37259befb25864cb4d2fc89c406273c1f943dd748e193e6c666b266da871", "instance": "d49f37259befb25864cb4d2fc89c406273c1f943dd748e193e6c666b266da871", "stable": false }, + ], } diff --git a/pkgs/primitives/corpus/block_check.json5 b/pkgs/primitives/corpus/block_check.json5 index cd3e443b..98d68b08 100644 --- a/pkgs/primitives/corpus/block_check.json5 +++ b/pkgs/primitives/corpus/block_check.json5 @@ -2,14 +2,15 @@ // Context-free block check vectors. Generated with the reference // implementation (dashpay/dash v24.0.0-rc.3, 0364ed626e). // -// To run the program, paste it at the end of the `validation_tests` suite in -// `src/test/validation_tests.cpp` at the specified revision, build it with +// To run a program, paste it at the end of the test suite in the file named +// above it at the specified revision, build it with // `make -C src test/test_dash` and run -// `./src/test/test_dash --run_test=validation_tests/block_check_shapes`. The -// output should match the section. +// `./src/test/test_dash --run_test=` with the case named there. The +// output should match the section that follows the program. // { + // `src/test/validation_tests.cpp`, case `validation_tests/block_check_shapes` /* struct BlockTxShape { std::vector inputs; @@ -172,4 +173,157 @@ BOOST_AUTO_TEST_CASE(block_check_shapes) // The mutation check runs before the size check. { "txs": [{ "inputs": [0], "lock_time": 0, "sigops": 0, "pad": 199656 }, { "inputs": [1], "lock_time": 0, "sigops": 0, "pad": 600000 }, { "inputs": [2], "lock_time": 0, "sigops": 0, "pad": 600000 }, { "inputs": [2], "lock_time": 0, "sigops": 0, "pad": 600000 }], "merkle_root": "61a04aac6a1cf6a65764a53e6ee30b222d484e6824a99235ee73f87118d4dce1", "size": 2000001, "reason": "bad-txns-duplicate" }, ], + // `src/test/evo_assetlocks_tests.cpp`, case + // `evo_assetlocks_tests/block_check_asset_unlock_roots` +/* +struct UnlockShape { + uint8_t version; + uint64_t index; + uint8_t quorum; +}; + +struct AssetUnlockRootShape { + uint8_t coinbase_input; + uint16_t cbtx_version; + std::vector unlocks; + std::vector committed; +}; + +static CTransactionRef BuildUnlock(const UnlockShape& u) +{ + CMutableTransaction tx; + tx.nVersion = 3; + tx.nType = TRANSACTION_ASSET_UNLOCK; + tx.vout.emplace_back(1, CScript{}); + uint256 quorum_hash; + std::fill(quorum_hash.begin(), quorum_hash.end(), u.quorum); + SetTxPayload(tx, CAssetUnlockPayload{u.version, u.index, 1, 0, quorum_hash, CBLSSignature{}}); + return MakeTransactionRef(std::move(tx)); +} + +static uint256 TxidMerkleRoot(const CBlock& block) +{ + std::vector hashes; + for (const CTransactionRef& tx : block.vtx) hashes.push_back(tx->GetHash()); + while (hashes.size() > 1) { + if (hashes.size() % 2) hashes.push_back(hashes.back()); + for (size_t i = 0; i < hashes.size() / 2; ++i) hashes[i] = Hash(hashes[2 * i], hashes[2 * i + 1]); + hashes.resize(hashes.size() / 2); + } + return hashes.empty() ? uint256{} : hashes[0]; +} + +static CBlock BuildUnlockBlock(const AssetUnlockRootShape& s) +{ + CBlock committed; + for (const UnlockShape& u : s.committed) committed.vtx.push_back(BuildUnlock(u)); + CCbTx cbtx; + cbtx.nVersion = static_cast(s.cbtx_version); + cbtx.merkleRootAssetUnlocks = CalcCbTxMerkleRootAssetUnlocks(committed); + CMutableTransaction coinbase; + coinbase.nVersion = 3; + coinbase.nType = TRANSACTION_COINBASE; + COutPoint prevout; + if (s.coinbase_input) { + uint256 hash; + std::fill(hash.begin(), hash.end(), s.coinbase_input); + prevout = COutPoint{hash, 0}; + } + coinbase.vin.emplace_back(prevout); + coinbase.vin[0].scriptSig.resize(2); + coinbase.vout.emplace_back(1, CScript{}); + SetTxPayload(coinbase, cbtx); + CBlock block; + block.vtx.push_back(MakeTransactionRef(std::move(coinbase))); + for (const UnlockShape& u : s.unlocks) block.vtx.push_back(BuildUnlock(u)); + block.hashMerkleRoot = TxidMerkleRoot(block); + return block; +} + +BOOST_FIXTURE_TEST_CASE(block_check_asset_unlock_roots, BasicTestingSetup) +{ + for (const AssetUnlockRootShape& s : std::vector{ + {0, 4, {}, {}}, + {0, 4, {{2, 1, 1}}, {{2, 1, 1}}}, + {0, 4, {{2, 1, 2}}, {{2, 1, 1}}}, + {0, 4, {{2, 1, 1}}, {}}, + {0, 4, {{1, 1, 1}}, {}}, + {0, 4, {{2, 1, 1}, {2, 2, 1}}, {{2, 1, 1}, {2, 2, 1}}}, + {0, 4, {{2, 1, 1}, {2, 2, 1}}, {{2, 2, 1}, {2, 1, 1}}}, + {0, 4, {{1, 1, 1}, {2, 2, 1}}, {{2, 2, 1}}}, + {0, 3, {{2, 1, 2}}, {{2, 1, 1}}}, + {0, 4, {{2, 1, 1}, {2, 2, 1}, {2, 2, 1}}, {{2, 1, 1}, {2, 2, 1}}}, + {1, 4, {{2, 1, 2}}, {{2, 1, 1}}}, + {1, 4, {{2, 1, 1}}, {{2, 1, 1}}}, + }) { + const CBlock block{BuildUnlockBlock(s)}; + BlockValidationState state; + CheckBlock(block, state, Params().GetConsensus(), false, true); + std::printf(" { \"coinbase_input\": %u, \"cbtx_version\": %u, \"unlocks\": [", s.coinbase_input, + s.cbtx_version); + for (size_t i = 0; i < s.unlocks.size(); ++i) { + std::printf("%s{ \"version\": %u, \"index\": %llu, \"quorum\": %u }", i ? ", " : "", s.unlocks[i].version, + static_cast(s.unlocks[i].index), s.unlocks[i].quorum); + } + std::printf("]"); + if (s.cbtx_version >= 4) { + std::printf(", \"asset_unlock_root\": \"%s\"", + GetTxPayload(*block.vtx[0])->merkleRootAssetUnlocks.ToString().c_str()); + } else { + std::printf(", \"asset_unlock_root\": null"); + } + std::printf(", \"merkle_root\": \"%s\", \"size\": %zu, \"reason\": \"%s\" },\n", + block.hashMerkleRoot.ToString().c_str(), ::GetSerializeSize(block, PROTOCOL_VERSION), + state.GetRejectReason().c_str()); + } +} +*/ + // Each vector lists a block of a coinbase followed by asset unlocks. The + // coinbase has version 3 and one input, which spends the null outpoint when + // `coinbase_input` is 0 and otherwise output 0 of the hash filled with that + // byte, with a `script_sig` of two zero bytes. Its payload has version + // `cbtx_version`, zero fields and the asset unlock root, absent below + // version 4, that the program computes over the unlocks it commits to. Each + // unlock has version 3, no inputs, and a payload with the given `version` + // and `index`, fee 1, requested height 0, a quorum hash filled with the + // `quorum` byte and a zero signature. Every transaction has one output of + // value 1 with an empty script. The header commits to the transactions and + // its proof of work is not checked. `size` is the serialized size and + // `reason` the rejection reason, empty when the block passes. The root is + // checked in + // https://github.com/dashpay/dash/blob/0364ed626ec2eaa518bf1ae4a0566ce638c7fe18/src/validation.cpp#L4351-L4360 + // and computed in + // https://github.com/dashpay/dash/blob/0364ed626ec2eaa518bf1ae4a0566ce638c7fe18/src/evo/cbtx.cpp#L158-L171 + "asset_unlock_root": [ + // A version 4 coinbase in a block without asset unlocks commits to the + // null root. + { "coinbase_input": 0, "cbtx_version": 4, "unlocks": [], "asset_unlock_root": "0000000000000000000000000000000000000000000000000000000000000000", "merkle_root": "5195831cafd27cb2da79c3ae908bd6a643a9a126920b18ac5db9cdc84ece7bc3", "size": 351, "reason": "" }, + // The root over a single version 2 unlock is its instance hash. + { "coinbase_input": 0, "cbtx_version": 4, "unlocks": [{ "version": 2, "index": 1, "quorum": 1 }], "asset_unlock_root": "86009e16d6da83ba3bcddcc36a33c71cc1c89817f5cb41b2166970b95ad3b666", "merkle_root": "fb412f55a79f7e87180fd83bc67142d7e0a8016ebb7e07535d1ac7294bca00e7", "size": 516, "reason": "" }, + // Altered signing bytes in a version 2 unlock keep its txid and the + // header root of the vector above but change its instance hash, so the + // committed root no longer matches. + { "coinbase_input": 0, "cbtx_version": 4, "unlocks": [{ "version": 2, "index": 1, "quorum": 2 }], "asset_unlock_root": "86009e16d6da83ba3bcddcc36a33c71cc1c89817f5cb41b2166970b95ad3b666", "merkle_root": "fb412f55a79f7e87180fd83bc67142d7e0a8016ebb7e07535d1ac7294bca00e7", "size": 516, "reason": "bad-cbtx-assetunlockmerkleroot" }, + // An unlock that the asset unlock root leaves out. + { "coinbase_input": 0, "cbtx_version": 4, "unlocks": [{ "version": 2, "index": 1, "quorum": 1 }], "asset_unlock_root": "0000000000000000000000000000000000000000000000000000000000000000", "merkle_root": "be474bcbc54e1a10f8e718c727ba89ffb702e6e5c18a0863b297587407bb3cbd", "size": 516, "reason": "bad-cbtx-assetunlockmerkleroot" }, + // A version 1 unlock is left out of the root. + { "coinbase_input": 0, "cbtx_version": 4, "unlocks": [{ "version": 1, "index": 1, "quorum": 1 }], "asset_unlock_root": "0000000000000000000000000000000000000000000000000000000000000000", "merkle_root": "5127e8e985a90d910afb5a6f6650d93febde48f5ae0e29479904b66b26105cb3", "size": 516, "reason": "" }, + // Two unlocks are committed to in block order. + { "coinbase_input": 0, "cbtx_version": 4, "unlocks": [{ "version": 2, "index": 1, "quorum": 1 }, { "version": 2, "index": 2, "quorum": 1 }], "asset_unlock_root": "942c9820f23e84ef77b5a5d8e3d6ba0f59f49036d8ef72118b930bfb0f3cc163", "merkle_root": "fd67369eb72d1797e29ed69d49b5772ff2071d247c5385a18a286f95126a652e", "size": 681, "reason": "" }, + // A root over the same two unlocks in the other order. + { "coinbase_input": 0, "cbtx_version": 4, "unlocks": [{ "version": 2, "index": 1, "quorum": 1 }, { "version": 2, "index": 2, "quorum": 1 }], "asset_unlock_root": "fe4b0dfba61f3d24b3b79b747b5f1be71cb88e24ec0fdc028390d9429ce69915", "merkle_root": "7ddf58af3f9bc8b61d972c1538c9c83c28094e668dc46121f660d1fdbb06a615", "size": 681, "reason": "bad-cbtx-assetunlockmerkleroot" }, + // Only the version 2 unlock of a mixed pair is committed to. + { "coinbase_input": 0, "cbtx_version": 4, "unlocks": [{ "version": 1, "index": 1, "quorum": 1 }, { "version": 2, "index": 2, "quorum": 1 }], "asset_unlock_root": "4bf322c24222b600bc4f814c7823aadcbc9390b256d2c16b4c004cc7e0258592", "merkle_root": "46df6ae617ddf72284f74a34fc7a79bb9536f7953de8bb72b1b19998b93b19af", "size": 681, "reason": "" }, + // A version 3 coinbase has no asset unlock root, so altered signing + // bytes pass. + { "coinbase_input": 0, "cbtx_version": 3, "unlocks": [{ "version": 2, "index": 1, "quorum": 2 }], "asset_unlock_root": null, "merkle_root": "5d036ecd714edde37ad6946f15086b19d38ffa77384900603d91b928b2b480ed", "size": 484, "reason": "" }, + // The mutation check runs before the asset unlock root check. + { "coinbase_input": 0, "cbtx_version": 4, "unlocks": [{ "version": 2, "index": 1, "quorum": 1 }, { "version": 2, "index": 2, "quorum": 1 }, { "version": 2, "index": 2, "quorum": 1 }], "asset_unlock_root": "942c9820f23e84ef77b5a5d8e3d6ba0f59f49036d8ef72118b930bfb0f3cc163", "merkle_root": "fd67369eb72d1797e29ed69d49b5772ff2071d247c5385a18a286f95126a652e", "size": 846, "reason": "bad-txns-duplicate" }, + // The asset unlock root is checked on a first transaction of the + // coinbase type before its input is checked. + { "coinbase_input": 1, "cbtx_version": 4, "unlocks": [{ "version": 2, "index": 1, "quorum": 2 }], "asset_unlock_root": "86009e16d6da83ba3bcddcc36a33c71cc1c89817f5cb41b2166970b95ad3b666", "merkle_root": "2835ea7bd4c7649c28340afa6bb87b242094a78591649df5ba035a7a74d75716", "size": 516, "reason": "bad-cbtx-assetunlockmerkleroot" }, + // With a matching root, the same first transaction fails the coinbase + // check. + { "coinbase_input": 1, "cbtx_version": 4, "unlocks": [{ "version": 2, "index": 1, "quorum": 1 }], "asset_unlock_root": "86009e16d6da83ba3bcddcc36a33c71cc1c89817f5cb41b2166970b95ad3b666", "merkle_root": "2835ea7bd4c7649c28340afa6bb87b242094a78591649df5ba035a7a74d75716", "size": 516, "reason": "bad-cb-missing" }, + ], } diff --git a/pkgs/primitives/corpus/cbtx.json5 b/pkgs/primitives/corpus/cbtx.json5 index 51008c55..287db85d 100644 --- a/pkgs/primitives/corpus/cbtx.json5 +++ b/pkgs/primitives/corpus/cbtx.json5 @@ -1,3 +1,14 @@ +// +// CbTx vectors. Sections with a program were generated with the +// reference implementation (dashpay/dash v24.0.0-rc.3, 0364ed626e). +// +// To run a program, paste it at the end of the `evo_cbtx_tests` suite +// in `src/test/evo_cbtx_tests.cpp` at the specified revision, build it +// with `make -C src test/test_dash` and run +// `./src/test/test_dash --run_test=evo_cbtx_tests/`. The output +// should match the section. +// + { "cbtx": { // Block 000000000000003bb55596232b23ffe0dd9f88b8d1c5cc4123184a61b132dafa @@ -260,5 +271,99 @@ "creditPoolBalance": 2281172585982 } } - } + }, +/* +BOOST_AUTO_TEST_CASE(cbtx_versions) +{ + struct Shape { + uint16_t version; + bool root; + uint32_t cl_height_diff; + }; + for (const Shape& s : std::vector{ + {1, false, 0}, + {1, true, 0}, + {2, false, 0}, + {2, true, 0}, + {3, false, 0}, + {3, true, 0}, + {4, true, 0}, + {4, false, 0}, + {5, true, 0}, + {5, false, 0}, + {0, false, 0}, + {3, false, 0x02000000}, + {3, false, 0x02000001}, + {4, true, 0x02000000}, + {4, true, 0x02000001}, + }) { + CCbTx cbtx; + cbtx.nVersion = static_cast(s.version); + cbtx.nHeight = 1000; + cbtx.merkleRootMNList = uint256::ONE; + cbtx.merkleRootQuorums = uint256::TWO; + cbtx.bestCLHeightDiff = s.cl_height_diff; + cbtx.creditPoolBalance = 5000; + for (size_t i = 0; i < uint256::size(); ++i) cbtx.merkleRootAssetUnlocks.begin()[i] = i; + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << cbtx; + std::vector payload(UCharCast(ss.data()), UCharCast(ss.data() + ss.size())); + const bool has_root{cbtx.nVersion >= CCbTx::Version::MERKLE_ROOT_ASSETUNLOCKS}; + if (s.root && !has_root) { + payload.insert(payload.end(), cbtx.merkleRootAssetUnlocks.begin(), cbtx.merkleRootAssetUnlocks.end()); + } else if (!s.root && has_root) { + payload.resize(payload.size() - uint256::size()); + } + const auto decoded{GetTxPayload(payload)}; + TxValidationState state; + std::string root{"null"}; + if (decoded) { + CheckCbTx(*decoded, nullptr, true, state); + if (decoded->nVersion >= CCbTx::Version::MERKLE_ROOT_ASSETUNLOCKS) { + root = "\"" + decoded->merkleRootAssetUnlocks.ToString() + "\""; + } + } + std::printf(" { \"version\": %u, \"payload\": \"%s\", \"decodes\": %s, \"root\": %s, \"reason\": \"%s\" },\n", + s.version, HexStr(payload).c_str(), decoded ? "true" : "false", root.c_str(), + state.GetRejectReason().c_str()); + } +} +*/ + // Each vector is a CbTx payload of `version`, serialized from the fields + // of that version and then given or stripped of a trailing asset unlock + // root. `decodes` tells whether it reads with no bytes left over, `root` + // is the asset unlock root it holds, and `reason` the rejection reason of + // the check without chain context, empty when it passes. + "versions": [ + // Version 1 without the root. + { "version": 1, "payload": "0100e80300000100000000000000000000000000000000000000000000000000000000000000", "decodes": true, "root": null, "reason": "" }, + // Version 1 followed by a root has trailing bytes. + { "version": 1, "payload": "0100e80300000100000000000000000000000000000000000000000000000000000000000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "decodes": false, "root": null, "reason": "" }, + // Version 2 without the root. + { "version": 2, "payload": "0200e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000", "decodes": true, "root": null, "reason": "" }, + // Version 2 followed by a root has trailing bytes. + { "version": 2, "payload": "0200e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "decodes": false, "root": null, "reason": "" }, + // Version 3 without the root. + { "version": 3, "payload": "0300e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008813000000000000", "decodes": true, "root": null, "reason": "" }, + // Version 3 followed by a root has trailing bytes. + { "version": 3, "payload": "0300e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008813000000000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "decodes": false, "root": null, "reason": "" }, + // Version 4 ends with the root. + { "version": 4, "payload": "0400e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008813000000000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "decodes": true, "root": "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", "reason": "" }, + // Version 4 without the root is truncated. + { "version": 4, "payload": "0400e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008813000000000000", "decodes": false, "root": null, "reason": "" }, + // Version 5 reads like version 4 and is unknown. + { "version": 5, "payload": "0500e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008813000000000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "decodes": true, "root": "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", "reason": "bad-cbtx-version" }, + // Version 5 without the root is truncated. + { "version": 5, "payload": "0500e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008813000000000000", "decodes": false, "root": null, "reason": "" }, + // Version 0 holds only the base fields and is invalid. + { "version": 0, "payload": "0000e80300000100000000000000000000000000000000000000000000000000000000000000", "decodes": true, "root": null, "reason": "bad-cbtx-version" }, + // A ChainLock height difference at the size limit. + { "version": 3, "payload": "0300e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000fe000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008813000000000000", "decodes": true, "root": null, "reason": "" }, + // A ChainLock height difference one above the size limit. + { "version": 3, "payload": "0300e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000fe010000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008813000000000000", "decodes": false, "root": null, "reason": "" }, + // The size limit in version 4. + { "version": 4, "payload": "0400e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000fe000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008813000000000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "decodes": true, "root": "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", "reason": "" }, + // One above the size limit in version 4. + { "version": 4, "payload": "0400e803000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000fe010000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008813000000000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "decodes": false, "root": null, "reason": "" }, + ], } diff --git a/pkgs/primitives/corpus/prodistx.json5 b/pkgs/primitives/corpus/prodistx.json5 new file mode 100644 index 00000000..bac4115a --- /dev/null +++ b/pkgs/primitives/corpus/prodistx.json5 @@ -0,0 +1,60 @@ +// +// ProDisTx vectors. Sections with a program were generated with the +// reference implementation (dashpay/dash v24.0.0-rc.3, 0364ed626e). +// +// To run a program, paste it at the end of the `evo_sharedmn_tests` suite +// in `src/test/evo_sharedmn_tests.cpp` at the specified revision, build it +// with `make -C src test/test_dash` and run +// `./src/test/test_dash --run_test=evo_sharedmn_tests/`. The output +// should match the section. +// + +{ +/* +BOOST_AUTO_TEST_CASE(prodistx_check) +{ + const auto sigs = [](size_t count) { + std::vector ret(count); + for (size_t i = 0; i < count; ++i) { + std::fill(ret[i].begin(), ret[i].end(), 0x20 + i); + } + return ret; + }; + uint256 pro_tx_hash; + for (size_t i = 0; i < pro_tx_hash.size(); ++i) pro_tx_hash.begin()[i] = i; + for (const auto& [actor, count] : std::vector>{ + {0, 1}, {1, 2}, {7, 8}, {300, 1}, {0, 0}, {0, 9}}) { + CProDisTx ptx; + ptx.proTxHash = pro_tx_hash; + ptx.actorIndex = actor; + ptx.vchSigs = sigs(count); + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << ptx; + const std::vector payload(UCharCast(ss.data()), UCharCast(ss.data() + ss.size())); + const auto decoded{GetTxPayload(payload)}; + BOOST_REQUIRE(decoded); + TxValidationState state; + decoded->IsTriviallyValid(state); + std::printf(" { \"payload\": \"%s\", \"json\": %s, \"reason\": \"%s\" },\n", HexStr(payload).c_str(), + decoded->ToJson().write().c_str(), state.GetRejectReason().c_str()); + } +} +*/ + // Each vector is a ProDisTx payload, `json` the reference implementation's + // JSON form of it, and `reason` the rejection reason of its check without + // chain context, empty when it passes. + "check": [ + // One signature, by the acting share's owner. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0000012020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","actorIndex":0,"sigCount":1}, "reason": "" }, + // Two signatures, one per share. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f01000220202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","actorIndex":1,"sigCount":2}, "reason": "" }, + // Eight signatures, the most allowed. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f07000820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222223232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252526262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","actorIndex":7,"sigCount":8}, "reason": "" }, + // An actor index wider than a byte. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2c01012020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","actorIndex":300,"sigCount":1}, "reason": "" }, + // No signatures. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f000000", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","actorIndex":0,"sigCount":0}, "reason": "bad-prodis-sig-count" }, + // Nine signatures, one more than allowed. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f000009202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202021212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232324242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262627272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","actorIndex":0,"sigCount":9}, "reason": "bad-prodis-sig-count" }, + ], +} diff --git a/pkgs/primitives/corpus/proregtx.json5 b/pkgs/primitives/corpus/proregtx.json5 index b8abe729..b394fe38 100644 --- a/pkgs/primitives/corpus/proregtx.json5 +++ b/pkgs/primitives/corpus/proregtx.json5 @@ -1,3 +1,14 @@ +// +// ProRegTx vectors. Sections with a program were generated with the +// reference implementation (dashpay/dash v24.0.0-rc.3, 0364ed626e). +// +// To run a program, paste it at the end of the `evo_sharedmn_tests` suite +// in `src/test/evo_sharedmn_tests.cpp` at the specified revision, build it +// with `make -C src test/test_dash` and run +// `./src/test/test_dash --run_test=evo_sharedmn_tests/`. The output +// should match the section. +// + { "proregtx": { // Block 000000000000001c2e2bdb3dd588d2e794a85f4638bd2f5cb74222f4c1d923b1 @@ -539,104 +550,584 @@ "platformHTTPPort": null, "vchSig": "1fbeb3d72e590341119141f2e56b00c99c43377ebc587b8b02984633d0814916c52b9a0dbf2d8eab115e87a6762981cf8c6b8a332807cc579eb00b871051a28b19" } - }, - // Synthetic ExtAddr from rpc_netinfo.py, Evonode with single IPv4 per purpose - "f137aa6f6e0a8895086f5eb18a3de18acf2cba5a1a598a13722a7d8cbdfb1261": { - "raw": "030001000103549432f45ab131b382207c2b8188e50e75af6baf17fe9c6e308ee57f660f7f010000006a47304402205058745ff7b9addc793390c58a5bd54bf7a0563c298eae6e166604f924af512102203b662d789e8852122ff122bd1753b75743a55e6b9e394c7cb2941dc8317cf656012102de469765cb9273617bc34cf5cbf1a0a919db9b14b3d9d7880289139b34e291d6feffffff01ce54f405000000001976a91469b366dadd041a9afb5a59b277f7164027c1285388ac00000000fd370103000100000003549432f45ab131b382207c2b8188e50e75af6baf17fe9c6e308ee57f660f7f00000000010300010101047f000001334501010101047f00000156b802010101047f00000156b9a8de9990c0a0676a95450ee59c6032c0222060dca326c971a901c71247043152a691a5a460c79ac9ff83fd876e3c1a6fa3b6734ecc72961438ddfc67049f941b3dfbedea19badc75f739727264b6848edef4e2f97ae303eb00001976a9142528448b4447518b5eafe16f574661c8e07bb18588ac3aaefd39eb12cdbcc1c40bc2d4ad000b4cafc9e0fa3e901e7571c9ff7c77ff72e5992efdc3d9e31931fb19590b33aaa1b90bb9584120c21b280fb2e8f72f9c46d7af4f78e50f6f6c31f0aeb080c6042879027a096a2b2f29047d0871e506e93c277e37e3ac52a2823cd205fe6051be5539b798d0f08f", - "details": { - "version": 3, - "mnType": 1, - "mode": 0, - "collateralHash": "7f0f667fe58e306e9cfe17af6baf750ee588812b7c2082b331b15af432945403", - "collateralIndex": 0, - "netInfo": { - "extended": { - "version": 1, - "entries": [ - [0, [{"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 13125}}]], - [1, [{"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 22200}}]], - [2, [{"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 22201}}]] - ] - } - }, - "keyIdOwner": "dc602022c032609ce50e45956a67a0c09099dea8", - "pubKeyOperator": "a326c971a901c71247043152a691a5a460c79ac9ff83fd876e3c1a6fa3b6734ecc72961438ddfc67049f941b3dfbedea", - "keyIdVoting": "eb03e37af9e2f4de8e84b664727239f775dcba19", - "operatorReward": 0, - "scriptPayout": "76a9142528448b4447518b5eafe16f574661c8e07bb18588ac", - "inputsHash": "72ff777cffc971751e903efae0c9af4c0b00add4c20bc4c1bccd12eb39fdae3a", - "platformNodeId": "58b90bb9a1aa330b5919fb3119e3d9c3fd2e99e5", - "platformP2PPort": null, - "platformHTTPPort": null, - "vchSig": "20c21b280fb2e8f72f9c46d7af4f78e50f6f6c31f0aeb080c6042879027a096a2b2f29047d0871e506e93c277e37e3ac52a2823cd205fe6051be5539b798d0f08f" - } - }, - // Synthetic ExtAddr from rpc_netinfo.py, Evonode with dual IPv4+IPv6 per purpose - "a51324799403f291cef55feb42d68d4e226187b83faf5e44b38112dc3bfbf5dc": { - "raw": "0300010001cd3f92989da4770b690bb464f2faaa3dc21f7eed750922fd4a75ef821df9ddd3010000006a473044022034a3d407f02004877cdec6ab5481b9d871433f76b123a0852ecd42f68da6240f0220596b64b09edcbf0273c3ce8725a1ec2812b630fdafda49ea04f104e10fdbc9ab012102c28ef58e2f72d4785500e96965c1f8dcd86db1ed38fc87b00c8aae4cc225c547feffffff01c1c5f205000000001976a914d5b798dfb6a4fc4820ee449561e8871b3b11a98688ac00000000fd7601030001000000cd3f92989da4770b690bb464f2faaa3dc21f7eed750922fd4a75ef821df9ddd300000000010300020101047f000001334501021000000000000000000000000000000001334501020101047f00000256b80102100000000000000000000000000000000256b802020101047f00000356b90102100000000000000000000000000000000356b9fd553fc926287871a7dcd71968ad659026c093f795d86735440a8f1f650c1efae172fa21e5ea6cb5edb374e2cb6c3399184a5904f36a35d5969a01c9a15f49d3d4075b731441c5334f5decfbff85f3e60b1073ee2597c75000001976a9146fcb03df8b448707f27f4adb99bb1ddbf11a561a88ac2b4dff8a08c996556b81a270b7e337d6a86ecb6f1ac1702820b5348900797a8344442971737accd3a941166d9b3dc35f619ca10f4120c9582a842b570f95c68246ed563f992328354d3e0ee0d64bba7344278ca2ee93519761d07d315b0108bd4e95487c5e6fdc07970fdf9b528c747b1292cfbe25c7", - "details": { - "version": 3, - "mnType": 1, - "mode": 0, - "collateralHash": "d3ddf91d82ef754afd220975ed7e1fc23daafaf264b40b690b77a49d98923fcd", - "collateralIndex": 0, - "netInfo": { - "extended": { - "version": 1, - "entries": [ - [0, [ - {"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 13125}}, - {"service": {"addr": {"Ipv6": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "port": 13125}} - ]], - [1, [ - {"service": {"addr": {"Ipv4": [127, 0, 0, 2]}, "port": 22200}}, - {"service": {"addr": {"Ipv6": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]}, "port": 22200}} - ]], - [2, [ - {"service": {"addr": {"Ipv4": [127, 0, 0, 3]}, "port": 22201}}, - {"service": {"addr": {"Ipv6": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3]}, "port": 22201}} - ]] - ] - } - }, - "keyIdOwner": "f793c0269065ad6819d7dca771782826c93f55fd", - "pubKeyOperator": "95d86735440a8f1f650c1efae172fa21e5ea6cb5edb374e2cb6c3399184a5904f36a35d5969a01c9a15f49d3d4075b73", - "keyIdVoting": "50c79725ee73100be6f385fffbec5d4f33c54114", - "operatorReward": 0, - "scriptPayout": "76a9146fcb03df8b448707f27f4adb99bb1ddbf11a561a88ac", - "inputsHash": "837a79008934b5202870c11a6fcb6ea8d637e3b770a2816b5596c9088aff4d2b", - "platformNodeId": "0fa19c615fc33d9b6d1641a9d3cc7a7371294444", - "platformP2PPort": null, - "platformHTTPPort": null, - "vchSig": "20c9582a842b570f95c68246ed563f992328354d3e0ee0d64bba7344278ca2ee93519761d07d315b0108bd4e95487c5e6fdc07970fdf9b528c747b1292cfbe25c7" - } - }, - // Synthetic ExtAddr from rpc_netinfo.py, Evonode with blanked services (later updated with ProUpServTx) - "ecf23a8aee664a60783d5db8b0bdd4d345dd4f71f9c9dcf0bf500f855ea800c0": { - "raw": "0300010001b94ba65d1dde0f74f6fc8f4b78527820833f49c6cc58332206834b67429f84a4010000006a473044022039729c72931e81af2d055fb497ccd3b42bdf14544770f622316184b0de0cec2902206e0de7c5797fb73db6aa9ee50420f7adb7bb2821c368688fa1e59f5a5f5191e501210375add90cceec34273c5072e07679f827eb3cc64e16a545e38ff24a42fb924d8efeffffff017e58f405000000001976a91412035297dd4327017e0e5e8caf62f27f94aee03d88ac00000000fd1601030001000000b94ba65d1dde0f74f6fc8f4b78527820833f49c6cc58332206834b67429f84a400000000010015dde7c373a14ad16c091f409198b0f40f831b1f87dc4da5b388e0287ef12d48630e441e36baf164e6a2d24f986a4d0584144f879725722d0336c35c4ceaa36aeb607ae0f69375ea93a7fd20b3e2c0e24df9b3aa82613c9600001976a9144eb62894923a194f0f56ed20e37ba336b680264c88ac77a9fe84dd644ce20f0bd98282626bedd85fc87997e4b7f223e5229e25ae85ead769470503e89d55001d813fcbb8b3a770f97fe0411fbeb28ed0e786225ff0bed8b2eb86a3d95929737b25bd0083ecf0efeeabe74b9127a5436866bc2a38af290d64d05d8b77b8ee428337af47a4ca1cd97c3af59297", - "details": { - "version": 3, - "mnType": 1, - "mode": 0, - "collateralHash": "a4849f42674b8306223358ccc6493f83207852784b8ffcf6740fde1d5da64bb9", - "collateralIndex": 0, - "netInfo": { - "extended": { - "version": 1, - "entries": [] - } - }, - "keyIdOwner": "1f1b830ff4b09891401f096cd14aa173c3e7dd15", - "pubKeyOperator": "87dc4da5b388e0287ef12d48630e441e36baf164e6a2d24f986a4d0584144f879725722d0336c35c4ceaa36aeb607ae0", - "keyIdVoting": "963c6182aab3f94de2c0e2b320fda793ea7593f6", - "operatorReward": 0, - "scriptPayout": "76a9144eb62894923a194f0f56ed20e37ba336b680264c88ac", - "inputsHash": "ea85ae259e22e523f2b7e49779c85fd8ed6b628282d90b0fe24c64dd84fea977", - "platformNodeId": "e07ff970a7b3b8cb3f811d00559de803054769d7", - "platformP2PPort": null, - "platformHTTPPort": null, - "vchSig": "1fbeb28ed0e786225ff0bed8b2eb86a3d95929737b25bd0083ecf0efeeabe74b9127a5436866bc2a38af290d64d05d8b77b8ee428337af47a4ca1cd97c3af59297" - } } - } + }, +/* +BOOST_AUTO_TEST_CASE(proregtx_v24_layout) +{ + const auto key_id = [](uint8_t seed) { + uint160 id; + std::fill(id.begin(), id.end(), seed); + return CKeyID{id}; + }; + const auto pay_to_key = [&](uint8_t seed) { return GetScriptForDestination(PKHash{key_id(seed)}); }; + const auto pay_to_script = [&](uint8_t seed) { return GetScriptForDestination(ScriptHash{uint160{key_id(seed)}}); }; + const CBLSSecretKey operator_key{MakeUCharSpan(std::vector(32, 1))}; + const auto base = [&](MnType type) { + CProRegTx ptx; + ptx.nVersion = ProTxVersion::ExtAddr; + ptx.nType = type; + ptx.collateralOutpoint = COutPoint(uint256::ONE, 1); + ptx.netInfo = NetInfoInterface::MakeNetInfo(ptx.nVersion); + ptx.keyIDOwner = key_id(0x11); + ptx.pubKeyOperator.Set(operator_key.GetPublicKey(), false); + ptx.keyIDVoting = key_id(0x22); + ptx.nOperatorReward = 500; + ptx.inputsHash = uint256::TWO; + if (type == MnType::Evo) ptx.platformNodeID = uint160{key_id(0x33)}; + return ptx; + }; + const auto shared = [&](const std::vector& amounts) { + CProRegTx ptx{base(MnType::Regular)}; + ptx.collateralOutpoint = COutPoint(uint256(), 0); + ptx.keyIDOwner = CKeyID{}; + for (size_t i = 0; i < amounts.size(); ++i) { + const uint8_t seed = 0x40 + i; + ptx.shares.emplace_back(amounts[i], pay_to_key(seed), CScript(), key_id(seed + 0x10)); + CompactSignature sig; + std::fill(sig.begin(), sig.end(), seed + 0x20); + ptx.vchJoinSigs.push_back(sig); + } + return ptx; + }; + + std::vector shapes; + { + CProRegTx ptx{base(MnType::Regular)}; + ptx.payouts = {{pay_to_key(0x60), 10000}}; + shapes.push_back(ptx); + } + { + CProRegTx ptx{base(MnType::Regular)}; + ptx.payouts = {{pay_to_key(0x60), 5000}, {pay_to_script(0x61), 3000}, {pay_to_key(0x62), 2000}}; + ptx.vchSig.assign(CPubKey::COMPACT_SIGNATURE_SIZE, 0x1f); + shapes.push_back(ptx); + } + { + CProRegTx ptx{base(MnType::Evo)}; + ptx.payouts = {{pay_to_script(0x61), 7500}, {pay_to_key(0x60), 2500}}; + shapes.push_back(ptx); + } + { + CProRegTx ptx{base(MnType::Evo)}; + ptx.payouts = {{pay_to_key(0x60), 10000}}; + BOOST_REQUIRE(ptx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9999") == NetInfoStatus::Success); + BOOST_REQUIRE(ptx.netInfo->AddEntry(NetInfoPurpose::PLATFORM_P2P, "1.1.1.2:26656") == NetInfoStatus::Success); + BOOST_REQUIRE(ptx.netInfo->AddEntry(NetInfoPurpose::PLATFORM_HTTPS, "1.1.1.3:443") == NetInfoStatus::Success); + shapes.push_back(ptx); + } + { + CProRegTx ptx{base(MnType::Regular)}; + shapes.push_back(ptx); + } + { + CProRegTx ptx{shared({600 * COIN, 400 * COIN})}; + ptx.shares[1].scriptReward = pay_to_script(0x63); + ptx.nEarlyPeriodBlocks = 1000; + ptx.nEarlyPenalty = 50 * COIN; + shapes.push_back(ptx); + } + { + shapes.push_back(shared(std::vector(8, 125 * COIN))); + } + + for (const CProRegTx& shape : shapes) { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << shape; + const std::vector payload(UCharCast(ss.data()), UCharCast(ss.data() + ss.size())); + const auto ptx{GetTxPayload(payload)}; + BOOST_REQUIRE(ptx); + std::string payouts, shares, sigs; + for (const auto& payout : ptx->payouts) { + payouts += strprintf("%s[\"%s\", %u]", payouts.empty() ? "" : ", ", HexStr(payout.scriptPayout), + payout.reward); + } + for (const auto& share : ptx->shares) { + shares += strprintf("%s[%d, \"%s\", \"%s\", \"%s\"]", shares.empty() ? "" : ", ", share.amount, + HexStr(share.scriptRefund), HexStr(share.scriptReward), HexStr(share.keyIDOwner)); + } + for (const auto& sig : ptx->vchJoinSigs) { + sigs += strprintf("%s\"%s\"", sigs.empty() ? "" : ", ", HexStr(sig)); + } + std::printf("%s\n", strprintf(" { \"payload\": \"%s\", \"payouts\": [%s], \"shares\": [%s], " + "\"join_sigs\": [%s], \"early_period_blocks\": %u, \"early_penalty\": %d },", + HexStr(payload), payouts, shares, sigs, ptx->nEarlyPeriodBlocks, + ptx->nEarlyPenalty) + .c_str()); + } +} +*/ + // Each vector is a version 3 ProRegTx payload. `payouts` lists the owner + // payouts as [script, reward], `shares` the collateral shares as [amount, + // refund script, reward script, owner key id], `join_sigs` the consent + // signatures, and `early_period_blocks` and `early_penalty` the early + // dissolution terms, all as the reference implementation reads them. + "v24_layout": [ + // One payout of the whole owner reward. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222f401011976a914606060606060606060606060606060606060606088ac102700000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "payouts": [["76a914606060606060606060606060606060606060606088ac", 10000]], "shares": [], "join_sigs": [], "early_period_blocks": 0, "early_penalty": 0 }, + // Three payouts to P2PKH and P2SH scripts, with a 65-byte signature. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222f401031976a914606060606060606060606060606060606060606088ac881317a914616161616161616161616161616161616161616187b80b1976a914626262626262626262626262626262626262626288acd007000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000411f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f", "payouts": [["76a914606060606060606060606060606060606060606088ac", 5000], ["a914616161616161616161616161616161616161616187", 3000], ["76a914626262626262626262626262626262626262626288ac", 2000]], "shares": [], "join_sigs": [], "early_period_blocks": 0, "early_penalty": 0 }, + // An Evo registration, whose platform node id follows the inputs hash. + { "payload": "03000100000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222f4010217a9146161616161616161616161616161616161616161874c1d1976a914606060606060606060606060606060606060606088acc409000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000333333333333333333333333333333333333333300", "payouts": [["a914616161616161616161616161616161616161616187", 7500], ["76a914606060606060606060606060606060606060606088ac", 2500]], "shares": [], "join_sigs": [], "early_period_blocks": 0, "early_penalty": 0 }, + // An Evo registration with core and platform addresses ahead of its payouts. + { "payload": "0300010000000100000000000000000000000000000000000000000000000000000000000000010000000103000101010401010101270f010101010401010102682002010101040101010301bb1111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222f401011976a914606060606060606060606060606060606060606088ac1027000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000333333333333333333333333333333333333333300", "payouts": [["76a914606060606060606060606060606060606060606088ac", 10000]], "shares": [], "join_sigs": [], "early_period_blocks": 0, "early_penalty": 0 }, + // No payouts. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222f4010000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "payouts": [], "shares": [], "join_sigs": [], "early_period_blocks": 0, "early_penalty": 0 }, + // Two shares, one with its own reward script, and early dissolution terms. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222f4010002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac17a914636363636363636363636363636363636363636387515151515151515151515151515151515151515160606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "payouts": [], "shares": [[60000000000, "76a914404040404040404040404040404040404040404088ac", "", "5050505050505050505050505050505050505050"], [40000000000, "76a914414141414141414141414141414141414141414188ac", "a914636363636363636363636363636363636363636387", "5151515151515151515151515151515151515151"]], "join_sigs": ["6060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060", "6161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161"], "early_period_blocks": 1000, "early_penalty": 5000000000 }, + // Eight shares and no early dissolution terms. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222f401000800dd0ee9020000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000dd0ee9020000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100dd0ee9020000001976a914424242424242424242424242424242424242424288ac00525252525252525252525252525252525252525200dd0ee9020000001976a914434343434343434343434343434343434343434388ac00535353535353535353535353535353535353535300dd0ee9020000001976a914444444444444444444444444444444444444444488ac00545454545454545454545454545454545454545400dd0ee9020000001976a914454545454545454545454545454545454545454588ac00555555555555555555555555555555555555555500dd0ee9020000001976a914464646464646464646464646464646464646464688ac00565656565656565656565656565656565656565600dd0ee9020000001976a914474747474747474747474747474747474747474788ac00575757575757575757575757575757575757575760606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626263636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656566666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "payouts": [], "shares": [[12500000000, "76a914404040404040404040404040404040404040404088ac", "", "5050505050505050505050505050505050505050"], [12500000000, "76a914414141414141414141414141414141414141414188ac", "", "5151515151515151515151515151515151515151"], [12500000000, "76a914424242424242424242424242424242424242424288ac", "", "5252525252525252525252525252525252525252"], [12500000000, "76a914434343434343434343434343434343434343434388ac", "", "5353535353535353535353535353535353535353"], [12500000000, "76a914444444444444444444444444444444444444444488ac", "", "5454545454545454545454545454545454545454"], [12500000000, "76a914454545454545454545454545454545454545454588ac", "", "5555555555555555555555555555555555555555"], [12500000000, "76a914464646464646464646464646464646464646464688ac", "", "5656565656565656565656565656565656565656"], [12500000000, "76a914474747474747474747474747474747474747474788ac", "", "5757575757575757575757575757575757575757"]], "join_sigs": ["6060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060", "6161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161", "6262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262", "6363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363", "6464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464", "6565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565656565", "6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666", "6767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767"], "early_period_blocks": 0, "early_penalty": 0 }, + ], +/* +BOOST_AUTO_TEST_CASE(proregtx_rejected) +{ + for (const char* raw : { + "030001000103549432f45ab131b382207c2b8188e50e75af6baf17fe9c6e308ee57f660f7f010000006a47304402205058745ff7b9addc793390c58a5bd54bf7a0563c298eae6e166604f924af512102203b662d789e8852122ff122bd1753b75743a55e6b9e394c7cb2941dc8317cf656012102de469765cb9273617bc34cf5cbf1a0a919db9b14b3d9d7880289139b34e291d6feffffff01ce54f405000000001976a91469b366dadd041a9afb5a59b277f7164027c1285388ac00000000fd370103000100000003549432f45ab131b382207c2b8188e50e75af6baf17fe9c6e308ee57f660f7f00000000010300010101047f000001334501010101047f00000156b802010101047f00000156b9a8de9990c0a0676a95450ee59c6032c0222060dca326c971a901c71247043152a691a5a460c79ac9ff83fd876e3c1a6fa3b6734ecc72961438ddfc67049f941b3dfbedea19badc75f739727264b6848edef4e2f97ae303eb00001976a9142528448b4447518b5eafe16f574661c8e07bb18588ac3aaefd39eb12cdbcc1c40bc2d4ad000b4cafc9e0fa3e901e7571c9ff7c77ff72e5992efdc3d9e31931fb19590b33aaa1b90bb9584120c21b280fb2e8f72f9c46d7af4f78e50f6f6c31f0aeb080c6042879027a096a2b2f29047d0871e506e93c277e37e3ac52a2823cd205fe6051be5539b798d0f08f", + "0300010001cd3f92989da4770b690bb464f2faaa3dc21f7eed750922fd4a75ef821df9ddd3010000006a473044022034a3d407f02004877cdec6ab5481b9d871433f76b123a0852ecd42f68da6240f0220596b64b09edcbf0273c3ce8725a1ec2812b630fdafda49ea04f104e10fdbc9ab012102c28ef58e2f72d4785500e96965c1f8dcd86db1ed38fc87b00c8aae4cc225c547feffffff01c1c5f205000000001976a914d5b798dfb6a4fc4820ee449561e8871b3b11a98688ac00000000fd7601030001000000cd3f92989da4770b690bb464f2faaa3dc21f7eed750922fd4a75ef821df9ddd300000000010300020101047f000001334501021000000000000000000000000000000001334501020101047f00000256b80102100000000000000000000000000000000256b802020101047f00000356b90102100000000000000000000000000000000356b9fd553fc926287871a7dcd71968ad659026c093f795d86735440a8f1f650c1efae172fa21e5ea6cb5edb374e2cb6c3399184a5904f36a35d5969a01c9a15f49d3d4075b731441c5334f5decfbff85f3e60b1073ee2597c75000001976a9146fcb03df8b448707f27f4adb99bb1ddbf11a561a88ac2b4dff8a08c996556b81a270b7e337d6a86ecb6f1ac1702820b5348900797a8344442971737accd3a941166d9b3dc35f619ca10f4120c9582a842b570f95c68246ed563f992328354d3e0ee0d64bba7344278ca2ee93519761d07d315b0108bd4e95487c5e6fdc07970fdf9b528c747b1292cfbe25c7", + "0300010001b94ba65d1dde0f74f6fc8f4b78527820833f49c6cc58332206834b67429f84a4010000006a473044022039729c72931e81af2d055fb497ccd3b42bdf14544770f622316184b0de0cec2902206e0de7c5797fb73db6aa9ee50420f7adb7bb2821c368688fa1e59f5a5f5191e501210375add90cceec34273c5072e07679f827eb3cc64e16a545e38ff24a42fb924d8efeffffff017e58f405000000001976a91412035297dd4327017e0e5e8caf62f27f94aee03d88ac00000000fd1601030001000000b94ba65d1dde0f74f6fc8f4b78527820833f49c6cc58332206834b67429f84a400000000010015dde7c373a14ad16c091f409198b0f40f831b1f87dc4da5b388e0287ef12d48630e441e36baf164e6a2d24f986a4d0584144f879725722d0336c35c4ceaa36aeb607ae0f69375ea93a7fd20b3e2c0e24df9b3aa82613c9600001976a9144eb62894923a194f0f56ed20e37ba336b680264c88ac77a9fe84dd644ce20f0bd98282626bedd85fc87997e4b7f223e5229e25ae85ead769470503e89d55001d813fcbb8b3a770f97fe0411fbeb28ed0e786225ff0bed8b2eb86a3d95929737b25bd0083ecf0efeeabe74b9127a5436866bc2a38af290d64d05d8b77b8ee428337af47a4ca1cd97c3af59297", + }) { + CMutableTransaction mtx; + CDataStream ss(ParseHex(raw), SER_NETWORK, PROTOCOL_VERSION); + ss >> mtx; + const CTransaction tx{mtx}; + const bool decodes{GetTxPayload(tx).has_value()}; + std::printf(" { \"txid\": \"%s\", \"raw\": \"%s\", \"decodes\": %s },\n", tx.GetHash().ToString().c_str(), + raw, decodes ? "true" : "false"); + } +} +*/ + // Each vector is a transaction holding a version 3 ProRegTx payload with + // its txid. `decodes` tells whether the reference implementation reads the + // payload. + "rejected": [ + // An Evo registration with one IPv4 address per purpose. + { "txid": "f137aa6f6e0a8895086f5eb18a3de18acf2cba5a1a598a13722a7d8cbdfb1261", "raw": "030001000103549432f45ab131b382207c2b8188e50e75af6baf17fe9c6e308ee57f660f7f010000006a47304402205058745ff7b9addc793390c58a5bd54bf7a0563c298eae6e166604f924af512102203b662d789e8852122ff122bd1753b75743a55e6b9e394c7cb2941dc8317cf656012102de469765cb9273617bc34cf5cbf1a0a919db9b14b3d9d7880289139b34e291d6feffffff01ce54f405000000001976a91469b366dadd041a9afb5a59b277f7164027c1285388ac00000000fd370103000100000003549432f45ab131b382207c2b8188e50e75af6baf17fe9c6e308ee57f660f7f00000000010300010101047f000001334501010101047f00000156b802010101047f00000156b9a8de9990c0a0676a95450ee59c6032c0222060dca326c971a901c71247043152a691a5a460c79ac9ff83fd876e3c1a6fa3b6734ecc72961438ddfc67049f941b3dfbedea19badc75f739727264b6848edef4e2f97ae303eb00001976a9142528448b4447518b5eafe16f574661c8e07bb18588ac3aaefd39eb12cdbcc1c40bc2d4ad000b4cafc9e0fa3e901e7571c9ff7c77ff72e5992efdc3d9e31931fb19590b33aaa1b90bb9584120c21b280fb2e8f72f9c46d7af4f78e50f6f6c31f0aeb080c6042879027a096a2b2f29047d0871e506e93c277e37e3ac52a2823cd205fe6051be5539b798d0f08f", "decodes": false }, + // An Evo registration with an IPv4 and an IPv6 address per purpose. + { "txid": "a51324799403f291cef55feb42d68d4e226187b83faf5e44b38112dc3bfbf5dc", "raw": "0300010001cd3f92989da4770b690bb464f2faaa3dc21f7eed750922fd4a75ef821df9ddd3010000006a473044022034a3d407f02004877cdec6ab5481b9d871433f76b123a0852ecd42f68da6240f0220596b64b09edcbf0273c3ce8725a1ec2812b630fdafda49ea04f104e10fdbc9ab012102c28ef58e2f72d4785500e96965c1f8dcd86db1ed38fc87b00c8aae4cc225c547feffffff01c1c5f205000000001976a914d5b798dfb6a4fc4820ee449561e8871b3b11a98688ac00000000fd7601030001000000cd3f92989da4770b690bb464f2faaa3dc21f7eed750922fd4a75ef821df9ddd300000000010300020101047f000001334501021000000000000000000000000000000001334501020101047f00000256b80102100000000000000000000000000000000256b802020101047f00000356b90102100000000000000000000000000000000356b9fd553fc926287871a7dcd71968ad659026c093f795d86735440a8f1f650c1efae172fa21e5ea6cb5edb374e2cb6c3399184a5904f36a35d5969a01c9a15f49d3d4075b731441c5334f5decfbff85f3e60b1073ee2597c75000001976a9146fcb03df8b448707f27f4adb99bb1ddbf11a561a88ac2b4dff8a08c996556b81a270b7e337d6a86ecb6f1ac1702820b5348900797a8344442971737accd3a941166d9b3dc35f619ca10f4120c9582a842b570f95c68246ed563f992328354d3e0ee0d64bba7344278ca2ee93519761d07d315b0108bd4e95487c5e6fdc07970fdf9b528c747b1292cfbe25c7", "decodes": false }, + // An Evo registration with no addresses. + { "txid": "ecf23a8aee664a60783d5db8b0bdd4d345dd4f71f9c9dcf0bf500f855ea800c0", "raw": "0300010001b94ba65d1dde0f74f6fc8f4b78527820833f49c6cc58332206834b67429f84a4010000006a473044022039729c72931e81af2d055fb497ccd3b42bdf14544770f622316184b0de0cec2902206e0de7c5797fb73db6aa9ee50420f7adb7bb2821c368688fa1e59f5a5f5191e501210375add90cceec34273c5072e07679f827eb3cc64e16a545e38ff24a42fb924d8efeffffff017e58f405000000001976a91412035297dd4327017e0e5e8caf62f27f94aee03d88ac00000000fd1601030001000000b94ba65d1dde0f74f6fc8f4b78527820833f49c6cc58332206834b67429f84a400000000010015dde7c373a14ad16c091f409198b0f40f831b1f87dc4da5b388e0287ef12d48630e441e36baf164e6a2d24f986a4d0584144f879725722d0336c35c4ceaa36aeb607ae0f69375ea93a7fd20b3e2c0e24df9b3aa82613c9600001976a9144eb62894923a194f0f56ed20e37ba336b680264c88ac77a9fe84dd644ce20f0bd98282626bedd85fc87997e4b7f223e5229e25ae85ead769470503e89d55001d813fcbb8b3a770f97fe0411fbeb28ed0e786225ff0bed8b2eb86a3d95929737b25bd0083ecf0efeeabe74b9127a5436866bc2a38af290d64d05d8b77b8ee428337af47a4ca1cd97c3af59297", "decodes": false }, + ], +/* +BOOST_AUTO_TEST_CASE(proregtx_check) +{ + const auto key_id = [](uint8_t seed) { + uint160 id; + std::fill(id.begin(), id.end(), seed); + return CKeyID{id}; + }; + const auto pay_to_key = [&](uint8_t seed) { return GetScriptForDestination(PKHash{key_id(seed)}); }; + const auto pay_to_script = [&](uint8_t seed) { return GetScriptForDestination(ScriptHash{uint160{key_id(seed)}}); }; + const CScript op_true{CScript() << OP_TRUE}; + const CBLSSecretKey operator_key{MakeUCharSpan(std::vector(32, 1))}; + const auto base = [&](MnType type) { + CProRegTx ptx; + ptx.nVersion = ProTxVersion::ExtAddr; + ptx.nType = type; + ptx.collateralOutpoint = COutPoint(uint256::ONE, 1); + ptx.netInfo = NetInfoInterface::MakeNetInfo(ptx.nVersion); + ptx.keyIDOwner = key_id(0x11); + ptx.pubKeyOperator.Set(operator_key.GetPublicKey(), false); + ptx.keyIDVoting = key_id(0x22); + ptx.inputsHash = uint256::TWO; + ptx.payouts = {{pay_to_key(0x60), 10000}}; + if (type == MnType::Evo) ptx.platformNodeID = uint160{key_id(0x33)}; + return ptx; + }; + const auto payouts = [&](const std::vector& rewards) { + CProRegTx ptx{base(MnType::Regular)}; + ptx.payouts.clear(); + for (size_t i = 0; i < rewards.size(); ++i) { + ptx.payouts.push_back({pay_to_key(0x60 + i), rewards[i]}); + } + return ptx; + }; + const auto shared = [&](MnType type, const std::vector& amounts) { + CProRegTx ptx{base(type)}; + ptx.collateralOutpoint = COutPoint(uint256(), 0); + ptx.keyIDOwner = CKeyID{}; + ptx.payouts.clear(); + for (size_t i = 0; i < amounts.size(); ++i) { + const uint8_t seed = 0x40 + i; + ptx.shares.emplace_back(amounts[i], pay_to_key(seed), CScript(), key_id(seed + 0x10)); + } + ptx.vchJoinSigs.resize(amounts.size()); + ptx.nEarlyPeriodBlocks = 1000; + ptx.nEarlyPenalty = 50 * COIN; + return ptx; + }; + const auto two_shares = [&]() { return shared(MnType::Regular, {600 * COIN, 400 * COIN}); }; + + std::vector shapes; + shapes.push_back(base(MnType::Regular)); + shapes.push_back(payouts({})); + shapes.push_back(payouts(std::vector(9, 1000))); + shapes.push_back(payouts(std::vector(8, 1250))); + shapes.push_back(payouts({99, 9901})); + shapes.push_back(payouts({10001})); + shapes.push_back(payouts({100, 9900})); + shapes.push_back(payouts({5000, 4999})); + { + CProRegTx ptx{base(MnType::Regular)}; + ptx.payouts[0].scriptPayout = pay_to_script(0x60); + shapes.push_back(ptx); + } + { + CProRegTx ptx{base(MnType::Regular)}; + ptx.payouts[0].scriptPayout = op_true; + shapes.push_back(ptx); + } + { + CProRegTx ptx{payouts({5000, 5000})}; + ptx.payouts[1].scriptPayout = ptx.payouts[0].scriptPayout; + shapes.push_back(ptx); + } + { + CProRegTx ptx{payouts({5000, 5000})}; + ptx.payouts[1].scriptPayout = pay_to_key(0x11); + shapes.push_back(ptx); + } + { + CProRegTx ptx{payouts({5000, 5000})}; + ptx.payouts[1].scriptPayout = pay_to_key(0x22); + shapes.push_back(ptx); + } + { + CProRegTx ptx{base(MnType::Regular)}; + ptx.payouts[0].scriptPayout = pay_to_key(0x22); + BOOST_REQUIRE(ptx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9999") == NetInfoStatus::Success); + BOOST_REQUIRE(ptx.netInfo->AddEntry(NetInfoPurpose::PLATFORM_P2P, "1.1.1.2:26656") == NetInfoStatus::Success); + shapes.push_back(ptx); + } + { + CProRegTx ptx{base(MnType::Regular)}; + ptx.nEarlyPenalty = 1; + shapes.push_back(ptx); + } + { + CProRegTx ptx{base(MnType::Regular)}; + ptx.nEarlyPeriodBlocks = 1; + shapes.push_back(ptx); + } + { + CProRegTx ptx{base(MnType::Regular)}; + ptx.keyIDOwner = CKeyID{}; + shapes.push_back(ptx); + } + shapes.push_back(two_shares()); + { + CProRegTx ptx{two_shares()}; + ptx.collateralOutpoint = COutPoint(uint256::ONE, 0); + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.keyIDOwner = key_id(0x11); + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.payouts = {{pay_to_key(0x60), 10000}}; + shapes.push_back(ptx); + } + shapes.push_back(shared(MnType::Regular, {1000 * COIN})); + shapes.push_back(shared(MnType::Regular, std::vector(9, 100 * COIN))); + shapes.push_back(shared(MnType::Regular, std::vector(8, 125 * COIN))); + { + CProRegTx ptx{two_shares()}; + ptx.nEarlyPeriodBlocks = CProRegTx::MAX_EARLY_PERIOD_BLOCKS; + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.nEarlyPeriodBlocks = CProRegTx::MAX_EARLY_PERIOD_BLOCKS + 1; + shapes.push_back(ptx); + } + shapes.push_back(shared(MnType::Regular, {99 * COIN, 901 * COIN})); + shapes.push_back(shared(MnType::Regular, {1001 * COIN, 400 * COIN})); + { + CProRegTx ptx{two_shares()}; + ptx.shares[1].keyIDOwner = CKeyID{}; + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[1].keyIDOwner = ptx.shares[0].keyIDOwner; + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[1].scriptRefund = ptx.shares[0].scriptRefund; + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[0].scriptRefund = op_true; + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[0].scriptReward = op_true; + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[0].scriptRefund = pay_to_script(0x40); + ptx.shares[1].scriptReward = pay_to_script(0x63); + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[0].scriptReward = pay_to_key(0x64); + ptx.shares[1].scriptReward = pay_to_key(0x64); + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[0].scriptReward = GetScriptForDestination(PKHash{ptx.shares[1].keyIDOwner}); + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[0].scriptReward = GetScriptForDestination(PKHash{ptx.shares[0].keyIDOwner}); + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[1].scriptRefund = pay_to_key(0x22); + shapes.push_back(ptx); + } + shapes.push_back(shared(MnType::Regular, {599 * COIN, 400 * COIN})); + { + CProRegTx ptx{two_shares()}; + ptx.nEarlyPenalty = -1; + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.nEarlyPenalty = 400 * COIN; + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.nEarlyPenalty = 400 * COIN - 1; + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.nEarlyPeriodBlocks = 0; + ptx.nEarlyPenalty = 1; + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.nEarlyPeriodBlocks = 0; + ptx.nEarlyPenalty = 0; + shapes.push_back(ptx); + } + shapes.push_back(shared(MnType::Regular, {100 * COIN, 900 * COIN})); + shapes.push_back(shared(MnType::Regular, {1000 * COIN, 100 * COIN})); + { + CProRegTx ptx{payouts({5000, 5000})}; + ptx.payouts[0].scriptPayout = pay_to_script(0x11); + ptx.payouts[1].scriptPayout = pay_to_script(0x22); + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[0].scriptRefund = pay_to_script(0x51); + ptx.shares[1].scriptReward = pay_to_script(0x22); + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[0].scriptReward = pay_to_key(0x22); + shapes.push_back(ptx); + } + { + CProRegTx ptx{two_shares()}; + ptx.shares[0].scriptRefund = GetScriptForDestination(PKHash{ptx.shares[1].keyIDOwner}); + shapes.push_back(ptx); + } + shapes.push_back(shared(MnType::Evo, {2500 * COIN, 1500 * COIN})); + shapes.push_back(shared(MnType::Evo, {600 * COIN, 400 * COIN})); + + for (const CProRegTx& shape : shapes) { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << shape; + const std::vector payload(UCharCast(ss.data()), UCharCast(ss.data() + ss.size())); + const auto ptx{GetTxPayload(payload)}; + BOOST_REQUIRE(ptx); + TxValidationState state; + ptx->IsTriviallyValid(state); + std::printf(" { \"payload\": \"%s\", \"reason\": \"%s\" },\n", HexStr(payload).c_str(), + state.GetRejectReason().c_str()); + } +} +*/ + // Each vector is a version 3 ProRegTx payload and `reason` the rejection + // reason of the reference implementation's check without chain context, + // empty when it passes. + "check": [ + // One payout of the whole owner reward passes. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000011976a914606060606060606060606060606060606060606088ac102700000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // No payouts. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payouts-count" }, + // Nine payouts, one more than allowed. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000091976a914606060606060606060606060606060606060606088ace8031976a914616161616161616161616161616161616161616188ace8031976a914626262626262626262626262626262626262626288ace8031976a914636363636363636363636363636363636363636388ace8031976a914646464646464646464646464646464646464646488ace8031976a914656565656565656565656565656565656565656588ace8031976a914666666666666666666666666666666666666666688ace8031976a914676767676767676767676767676767676767676788ace8031976a914686868686868686868686868686868686868686888ace80300000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payouts-count" }, + // Eight payouts, the most allowed. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000081976a914606060606060606060606060606060606060606088ace2041976a914616161616161616161616161616161616161616188ace2041976a914626262626262626262626262626262626262626288ace2041976a914636363636363636363636363636363636363636388ace2041976a914646464646464646464646464646464646464646488ace2041976a914656565656565656565656565656565656565656588ace2041976a914666666666666666666666666666666666666666688ace2041976a914676767676767676767676767676767676767676788ace20400000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // A payout below the smallest reward. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000021976a914606060606060606060606060606060606060606088ac63001976a914616161616161616161616161616161616161616188acad2600000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payout-reward" }, + // A payout above the whole owner reward. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000011976a914606060606060606060606060606060606060606088ac112700000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payout-reward" }, + // A payout of the smallest reward. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000021976a914606060606060606060606060606060606060606088ac64001976a914616161616161616161616161616161616161616188acac2600000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // Rewards that sum to less than the whole owner reward. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000021976a914606060606060606060606060606060606060606088ac88131976a914616161616161616161616161616161616161616188ac871300000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payout-reward-sum" }, + // A payout to a P2SH script. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000117a914606060606060606060606060606060606060606087102700000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // A payout to a script that is neither P2PKH nor P2SH. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000010151102700000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payee" }, + // Two payouts to the same script. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000021976a914606060606060606060606060606060606060606088ac88131976a914606060606060606060606060606060606060606088ac881300000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payee-dup" }, + // A payout to the owner key. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000021976a914606060606060606060606060606060606060606088ac88131976a914111111111111111111111111111111111111111188ac881300000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payee-reuse" }, + // A payout to the voting key. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000021976a914606060606060606060606060606060606060606088ac88131976a914222222222222222222222222222222222222222288ac881300000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payee-reuse" }, + // A payout to the voting key fails before the invalid platform address. + { "payload": "0300000000000100000000000000000000000000000000000000000000000000000000000000010000000102000101010401010101270f01010101040101010268201111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000011976a914222222222222222222222222222222222222222288ac102700000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payee-reuse" }, + // An early penalty without shares. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000011976a914606060606060606060606060606060606060606088ac102700000000000100000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-empty-fields" }, + // An early period without shares. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000011976a914606060606060606060606060606060606060606088ac102700010000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-empty-fields" }, + // A null owner key without shares. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000011976a914606060606060606060606060606060606060606088ac102700000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-key-null" }, + // Two shares of 600 and 400 DASH with early dissolution terms pass. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // Shares with an external collateral. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-external" }, + // Shares with an owner key. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-owner-key" }, + // Shares with owner payouts. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000011976a914606060606060606060606060606060606060606088ac102702005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-payouts" }, + // A single share. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000000100e87648170000001976a914404040404040404040404040404040404040404088ac0050505050505050505050505050505050505050500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-count" }, + // Nine shares, one more than allowed. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000000900e40b54020000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000e40b54020000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100e40b54020000001976a914424242424242424242424242424242424242424288ac00525252525252525252525252525252525252525200e40b54020000001976a914434343434343434343434343434343434343434388ac00535353535353535353535353535353535353535300e40b54020000001976a914444444444444444444444444444444444444444488ac00545454545454545454545454545454545454545400e40b54020000001976a914454545454545454545454545454545454545454588ac00555555555555555555555555555555555555555500e40b54020000001976a914464646464646464646464646464646464646464688ac00565656565656565656565656565656565656565600e40b54020000001976a914474747474747474747474747474747474747474788ac00575757575757575757575757575757575757575700e40b54020000001976a914484848484848484848484848484848484848484888ac005858585858585858585858585858585858585858000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-count" }, + // Eight shares, the most allowed. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000000800dd0ee9020000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000dd0ee9020000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100dd0ee9020000001976a914424242424242424242424242424242424242424288ac00525252525252525252525252525252525252525200dd0ee9020000001976a914434343434343434343434343434343434343434388ac00535353535353535353535353535353535353535300dd0ee9020000001976a914444444444444444444444444444444444444444488ac00545454545454545454545454545454545454545400dd0ee9020000001976a914454545454545454545454545454545454545454588ac00555555555555555555555555555555555555555500dd0ee9020000001976a914464646464646464646464646464646464646464688ac00565656565656565656565656565656565656565600dd0ee9020000001976a914474747474747474747474747474747474747474788ac00575757575757575757575757575757575757575700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // The longest early period. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000806a060000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // An early period one block longer than allowed. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000816a060000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-early-period" }, + // A share below 100 DASH. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222000000020003164e020000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000e560fa140000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-amount" }, + // A share above the collateral. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000000200c96c4e170000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-amount" }, + // A share with a null owner key. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-key-null" }, + // Two shares with the same owner key. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00505050505050505050505050505050505050505000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-dup-key" }, + // Two shares with the same refund script. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914404040404040404040404040404040404040404088ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-dup-refund" }, + // A refund script that is neither P2PKH nor P2SH. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d000000015100505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-payee" }, + // A reward script that is neither P2PKH nor P2SH. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac0151505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-payee" }, + // P2SH refund and reward scripts. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d00000017a91440404040404040404040404040404040404040408700505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac17a914636363636363636363636363636363636363636387515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // Two shares with the same reward script. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac1976a914646464646464646464646464646464646464646488ac505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac1976a914646464646464646464646464646464646464646488ac515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // A reward script paying another share's owner key. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac1976a914515151515151515151515151515151515151515188ac505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-payee-reuse" }, + // A reward script paying the share's own owner key. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac1976a914505050505050505050505050505050505050505088ac505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-payee-reuse" }, + // A refund script paying the voting key. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914222222222222222222222222222222222222222288ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-payee-reuse" }, + // Shares that sum to less than the collateral. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002007751f20d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-amount-sum" }, + // A negative early penalty. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000ffffffffffffffff020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-penalty" }, + // An early penalty equal to the smallest share. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000902f5009000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-penalty" }, + // An early penalty just below the smallest share. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000ff8f2f5009000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // An early penalty without an early period. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-penalty" }, + // Shares without early dissolution terms. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // A share of exactly 100 DASH passes. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000000200e40b54020000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000046bf4140000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // A share of the whole collateral is in range, so the sum fails. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220000000200e87648170000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000e40b54020000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-amount-sum" }, + // P2SH payouts whose hashes are the owner and voting key ids pass. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000217a914111111111111111111111111111111111111111187881317a914222222222222222222222222222222222222222287881300000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // P2SH share scripts whose hashes are an owner and the voting key id pass. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d00000017a91451515151515151515151515151515151515151518700505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac17a914222222222222222222222222222222222222222287515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // A reward script paying the voting key. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac1976a914222222222222222222222222222222222222222288ac505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-payee-reuse" }, + // A refund script paying another share's owner key. + { "payload": "03000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914515151515151515151515151515151515151515188ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a01000000020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-shares-payee-reuse" }, + // Evo shares that sum to the Evo collateral. + { "payload": "03000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002004429353a0000001976a914404040404040404040404040404040404040404088ac005050505050505050505050505050505050505050005cb2ec220000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a010000000200000000000000000000000000000000000000000000000000000000000000333333333333333333333333333333333333333300", "reason": "" }, + // Evo shares that sum to the regular collateral. + { "payload": "03000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200000002005847f80d0000001976a914404040404040404040404040404040404040404088ac00505050505050505050505050505050505050505000902f50090000001976a914414141414141414141414141414141414141414188ac00515151515151515151515151515151515151515100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e803000000f2052a010000000200000000000000000000000000000000000000000000000000000000000000333333333333333333333333333333333333333300", "reason": "bad-protx-shares-amount-sum" }, + ], +/* +BOOST_AUTO_TEST_CASE(proregtx_sign_string) +{ + const auto key_id = [](uint8_t seed) { + uint160 id; + std::fill(id.begin(), id.end(), seed); + return CKeyID{id}; + }; + const auto pay_to_key = [&](uint8_t seed) { return GetScriptForDestination(PKHash{key_id(seed)}); }; + const auto pay_to_script = [&](uint8_t seed) { return GetScriptForDestination(ScriptHash{uint160{key_id(seed)}}); }; + const CBLSSecretKey operator_key{MakeUCharSpan(std::vector(32, 1))}; + const std::vector secret(32, 0x77); + CKey collateral_key; + collateral_key.Set(secret.begin(), secret.end(), true); + const auto base = [&](MnType type, const MasternodePayoutShares& payouts) { + CProRegTx ptx; + ptx.nVersion = ProTxVersion::ExtAddr; + ptx.nType = type; + ptx.collateralOutpoint = COutPoint(uint256::ONE, 1); + ptx.netInfo = NetInfoInterface::MakeNetInfo(ptx.nVersion); + ptx.keyIDOwner = key_id(0x11); + ptx.pubKeyOperator.Set(operator_key.GetPublicKey(), false); + ptx.keyIDVoting = key_id(0x22); + ptx.nOperatorReward = 1234; + ptx.payouts = payouts; + ptx.inputsHash = uint256::TWO; + if (type == MnType::Evo) ptx.platformNodeID = uint160{key_id(0x33)}; + return ptx; + }; + + for (CProRegTx ptx : { + base(MnType::Regular, {{pay_to_key(0x60), 10000}}), + base(MnType::Regular, {{pay_to_key(0x60), 7000}, {pay_to_script(0x61), 3000}}), + base(MnType::Evo, {{pay_to_script(0x61), 2500}, {pay_to_key(0x62), 7500}}), + }) { + const std::string sign_string{ptx.MakeSignString()}; + BOOST_REQUIRE(CMessageSigner::SignMessage(sign_string, ptx.vchSig, collateral_key)); + std::string err; + BOOST_REQUIRE(CMessageSigner::VerifyMessage(collateral_key.GetPubKey().GetID(), ptx.vchSig, sign_string, err)); + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << ptx; + std::printf(" { \"payload\": \"%s\", \"sign_string\": \"%s\", \"collateral_key\": \"%s\" },\n", + HexStr(ss).c_str(), sign_string.c_str(), HexStr(collateral_key.GetPubKey().GetID()).c_str()); + } +} +*/ + // Each vector is a version 3 ProRegTx payload signed with its collateral + // key, `sign_string` the message the reference implementation signs, and + // `collateral_key` the key id of the collateral key. + "sign_string": [ + // One payout of the whole owner reward. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222d204011976a914606060606060606060606060606060606060606088ac1027000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000411fb7365c21b795623e63d17d5ded1de1a3383083be0f5289d2791aef07754557dc7612a0e5bcadb13a4e55bd6520c7b7cf79cfcac5d6c9aa228bc809ec56af9ab4", "sign_string": "XjUSA2LA85DUMf8c527PYTbpKQmmvemY8H:10000|1234|XcF5mKwWsiv3k394GBQNpYAuk3CVJ48Xnp|XdoKhQE8njcWL8gXfUVXnYf2ejpHSdUeQa|49493e9d0d755a38662fc8942005a4f0ef8e52710b82b18590fa920461ae201e", "collateral_key": "0c15a4a3e98104afbf77fd7b9256164d41d3cfe4" }, + // A P2PKH and a P2SH payout, joined in payout order. + { "payload": "03000000000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222d204021976a914606060606060606060606060606060606060606088ac581b17a914616161616161616161616161616161616161616187b80b00000000000000000000000000020000000000000000000000000000000000000000000000000000000000000041205e6fba79a56d9ab2d71b2922a1c93cf07fb1d13ac0ba982c9469a99b46c9beac7f0e0173ad441365d0b365e32269b8eb19a2887aee80d3e09df2cd036ed33e76", "sign_string": "XjUSA2LA85DUMf8c527PYTbpKQmmvemY8H:7000,7bHYxTHcaNH3g8PNoTFPoHmZYqETh9dt3D:3000|1234|XcF5mKwWsiv3k394GBQNpYAuk3CVJ48Xnp|XdoKhQE8njcWL8gXfUVXnYf2ejpHSdUeQa|6f78c267238bfc0fdc2b7b05a3dd61bf25e9307652b04e2eaffffea3eff9ad74", "collateral_key": "0c15a4a3e98104afbf77fd7b9256164d41d3cfe4" }, + // An Evo registration whose first payout is to a P2SH script. + { "payload": "03000100000001000000000000000000000000000000000000000000000000000000000000000100000001001111111111111111111111111111111111111111aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222d2040217a914616161616161616161616161616161616161616187c4091976a914626262626262626262626262626262626262626288ac4c1d0000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003333333333333333333333333333333333333333411fe6cd62c65a2c0ddafe5b315c602721a03bf207a632d56beb14dab196b8ebd7cd6f077401509e24f2ba5e44f9853424d8318d3464c47abc380bedac2d8a300e28", "sign_string": "7bHYxTHcaNH3g8PNoTFPoHmZYqETh9dt3D:2500,Xjf3uv1eEJwm8cNYUEMeFAbiJncLfh7XZN:7500|1234|XcF5mKwWsiv3k394GBQNpYAuk3CVJ48Xnp|XdoKhQE8njcWL8gXfUVXnYf2ejpHSdUeQa|db84bb8a1ae0a259c8ffa80c8b831f16c5c5a6aab41768cf9814e7abdfca5061", "collateral_key": "0c15a4a3e98104afbf77fd7b9256164d41d3cfe4" }, + ], } diff --git a/pkgs/primitives/corpus/proupregtx.json5 b/pkgs/primitives/corpus/proupregtx.json5 index e2213212..fbb4a5bb 100644 --- a/pkgs/primitives/corpus/proupregtx.json5 +++ b/pkgs/primitives/corpus/proupregtx.json5 @@ -1,3 +1,14 @@ +// +// ProUpRegTx vectors. Sections with a program were generated with the +// reference implementation (dashpay/dash v24.0.0-rc.3, 0364ed626e). +// +// To run a program, paste it at the end of the `evo_sharedmn_tests` suite +// in `src/test/evo_sharedmn_tests.cpp` at the specified revision, build it +// with `make -C src test/test_dash` and run +// `./src/test/test_dash --run_test=evo_sharedmn_tests/`. The output +// should match the section. +// + { "proupregtx": { // Block 000000000000000e35e5d1afa6f9b2c115982082d528da1e1749d80bf982ac94 @@ -280,5 +291,160 @@ "vchSig": "1fa040d812d6c71d5028070dfcbe4670f102a9cee4ec0506fb76c53a1afc32c318259daf122c87332706b330d4f4c5e27ba415a9b1a2ec446949b113f44e427def" } } - } + }, +/* +BOOST_AUTO_TEST_CASE(proupregtx_v24_layout) +{ + const auto key_id = [](uint8_t seed) { + uint160 id; + std::fill(id.begin(), id.end(), seed); + return CKeyID{id}; + }; + const auto pay_to_key = [&](uint8_t seed) { return GetScriptForDestination(PKHash{key_id(seed)}); }; + const auto pay_to_script = [&](uint8_t seed) { return GetScriptForDestination(ScriptHash{uint160{key_id(seed)}}); }; + const CBLSSecretKey operator_key{MakeUCharSpan(std::vector(32, 1))}; + const auto base = [&](const MasternodePayoutShares& payouts) { + CProUpRegTx ptx; + ptx.nVersion = ProTxVersion::ExtAddr; + ptx.proTxHash = uint256::ONE; + ptx.pubKeyOperator.Set(operator_key.GetPublicKey(), false); + ptx.keyIDVoting = key_id(0x22); + ptx.payouts = payouts; + ptx.inputsHash = uint256::TWO; + return ptx; + }; + + std::vector shapes{ + base({{pay_to_key(0x60), 10000}}), + base({{pay_to_key(0x60), 5000}, {pay_to_script(0x61), 3000}, {pay_to_key(0x62), 2000}}), + base({}), + }; + shapes[1].vchSig.assign(CPubKey::COMPACT_SIGNATURE_SIZE, 0x1f); + + for (const CProUpRegTx& shape : shapes) { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << shape; + const std::vector payload(UCharCast(ss.data()), UCharCast(ss.data() + ss.size())); + const auto ptx{GetTxPayload(payload)}; + BOOST_REQUIRE(ptx); + std::string payouts; + for (const auto& payout : ptx->payouts) { + payouts += strprintf("%s[\"%s\", %u]", payouts.empty() ? "" : ", ", HexStr(payout.scriptPayout), + payout.reward); + } + std::printf("%s\n", strprintf(" { \"payload\": \"%s\", \"payouts\": [%s] },", HexStr(payload), payouts).c_str()); + } +} +*/ + // Each vector is a version 3 ProUpRegTx payload. `payouts` lists the owner + // payouts as [script, reward], as the reference implementation reads them. + "v24_layout": [ + // One payout of the whole owner reward. + { "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222011976a914606060606060606060606060606060606060606088ac1027020000000000000000000000000000000000000000000000000000000000000000", "payouts": [["76a914606060606060606060606060606060606060606088ac", 10000]] }, + // Three payouts to P2PKH and P2SH scripts, with a 65-byte signature. + { "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222031976a914606060606060606060606060606060606060606088ac881317a914616161616161616161616161616161616161616187b80b1976a914626262626262626262626262626262626262626288acd0070200000000000000000000000000000000000000000000000000000000000000411f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f", "payouts": [["76a914606060606060606060606060606060606060606088ac", 5000], ["a914616161616161616161616161616161616161616187", 3000], ["76a914626262626262626262626262626262626262626288ac", 2000]] }, + // No payouts. + { "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200020000000000000000000000000000000000000000000000000000000000000000", "payouts": [] }, + ], +/* +BOOST_AUTO_TEST_CASE(proupregtx_check) +{ + const auto key_id = [](uint8_t seed) { + uint160 id; + std::fill(id.begin(), id.end(), seed); + return CKeyID{id}; + }; + const auto pay_to_key = [&](uint8_t seed) { return GetScriptForDestination(PKHash{key_id(seed)}); }; + const CBLSSecretKey operator_key{MakeUCharSpan(std::vector(32, 1))}; + const auto base = [&](const std::vector& rewards) { + CProUpRegTx ptx; + ptx.nVersion = ProTxVersion::ExtAddr; + ptx.proTxHash = uint256::ONE; + ptx.pubKeyOperator.Set(operator_key.GetPublicKey(), false); + ptx.keyIDVoting = key_id(0x22); + for (size_t i = 0; i < rewards.size(); ++i) { + ptx.payouts.push_back({pay_to_key(0x60 + i), rewards[i]}); + } + ptx.inputsHash = uint256::TWO; + return ptx; + }; + const auto legacy = [&](const CScript& script) { + CProUpRegTx ptx{base({})}; + ptx.nVersion = ProTxVersion::BasicBLS; + ptx.scriptPayout = script; + return ptx; + }; + + std::vector shapes; + shapes.push_back(base({10000})); + shapes.push_back(base({})); + shapes.push_back(base(std::vector(9, 1000))); + shapes.push_back(base(std::vector(8, 1250))); + shapes.push_back(base({99, 9901})); + shapes.push_back(base({5001, 5000})); + { + CProUpRegTx ptx{base({10000})}; + ptx.payouts[0].scriptPayout = CScript() << OP_TRUE; + shapes.push_back(ptx); + } + { + CProUpRegTx ptx{base({5000, 5000})}; + ptx.payouts[1].scriptPayout = ptx.payouts[0].scriptPayout; + shapes.push_back(ptx); + } + { + CProUpRegTx ptx{base({5000, 5000})}; + ptx.payouts[1].scriptPayout = pay_to_key(0x22); + shapes.push_back(ptx); + } + { + CProUpRegTx ptx{base({10000})}; + ptx.payouts[0].scriptPayout = pay_to_key(0x00); + shapes.push_back(ptx); + } + shapes.push_back(legacy(pay_to_key(0x60))); + shapes.push_back(legacy(pay_to_key(0x22))); + + for (const CProUpRegTx& shape : shapes) { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << shape; + const std::vector payload(UCharCast(ss.data()), UCharCast(ss.data() + ss.size())); + const auto ptx{GetTxPayload(payload)}; + BOOST_REQUIRE(ptx); + TxValidationState state; + ptx->IsTriviallyValid(state); + std::printf(" { \"version\": %u, \"payload\": \"%s\", \"reason\": \"%s\" },\n", ptx->nVersion, + HexStr(payload).c_str(), state.GetRejectReason().c_str()); + } +} +*/ + // Each vector is a ProUpRegTx payload of `version` and `reason` the + // rejection reason of the reference implementation's check without chain + // context, empty when it passes. + "check": [ + // One payout of the whole owner reward passes. + { "version": 3, "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222011976a914606060606060606060606060606060606060606088ac1027020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // No payouts. + { "version": 3, "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222200020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payouts-count" }, + // Nine payouts, one more than allowed. + { "version": 3, "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222091976a914606060606060606060606060606060606060606088ace8031976a914616161616161616161616161616161616161616188ace8031976a914626262626262626262626262626262626262626288ace8031976a914636363636363636363636363636363636363636388ace8031976a914646464646464646464646464646464646464646488ace8031976a914656565656565656565656565656565656565656588ace8031976a914666666666666666666666666666666666666666688ace8031976a914676767676767676767676767676767676767676788ace8031976a914686868686868686868686868686868686868686888ace803020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payouts-count" }, + // Eight payouts, the most allowed. + { "version": 3, "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222081976a914606060606060606060606060606060606060606088ace2041976a914616161616161616161616161616161616161616188ace2041976a914626262626262626262626262626262626262626288ace2041976a914636363636363636363636363636363636363636388ace2041976a914646464646464646464646464646464646464646488ace2041976a914656565656565656565656565656565656565656588ace2041976a914666666666666666666666666666666666666666688ace2041976a914676767676767676767676767676767676767676788ace204020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // A payout below the smallest reward. + { "version": 3, "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222021976a914606060606060606060606060606060606060606088ac63001976a914616161616161616161616161616161616161616188acad26020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payout-reward" }, + // Rewards that sum to more than the whole owner reward. + { "version": 3, "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222021976a914606060606060606060606060606060606060606088ac89131976a914616161616161616161616161616161616161616188ac8813020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payout-reward-sum" }, + // A payout to a script that is neither P2PKH nor P2SH. + { "version": 3, "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220101511027020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payee" }, + // Two payouts to the same script. + { "version": 3, "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222021976a914606060606060606060606060606060606060606088ac88131976a914606060606060606060606060606060606060606088ac8813020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payee-dup" }, + // A payout to the voting key. + { "version": 3, "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222021976a914606060606060606060606060606060606060606088ac88131976a914222222222222222222222222222222222222222288ac8813020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payee-reuse" }, + // A payout to the null key id, as there is no owner key to compare with. + { "version": 3, "payload": "030001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222011976a914000000000000000000000000000000000000000088ac1027020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // A version 2 payout script passes. + { "version": 2, "payload": "020001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222221976a914606060606060606060606060606060606060606088ac020000000000000000000000000000000000000000000000000000000000000000", "reason": "" }, + // A version 2 payout script paying the voting key. + { "version": 2, "payload": "020001000000000000000000000000000000000000000000000000000000000000000000aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222221976a914222222222222222222222222222222222222222288ac020000000000000000000000000000000000000000000000000000000000000000", "reason": "bad-protx-payee-reuse" }, + ], } diff --git a/pkgs/primitives/corpus/proupsharedregtx.json5 b/pkgs/primitives/corpus/proupsharedregtx.json5 new file mode 100644 index 00000000..1347845f --- /dev/null +++ b/pkgs/primitives/corpus/proupsharedregtx.json5 @@ -0,0 +1,85 @@ +// +// ProUpSharedRegTx vectors. Sections with a program were generated with the +// reference implementation (dashpay/dash v24.0.0-rc.3, 0364ed626e). +// +// To run a program, paste it at the end of the `evo_sharedmn_tests` suite +// in `src/test/evo_sharedmn_tests.cpp` at the specified revision, build it +// with `make -C src test/test_dash` and run +// `./src/test/test_dash --run_test=evo_sharedmn_tests/`. The output +// should match the section. +// + +{ +/* +BOOST_AUTO_TEST_CASE(proupsharedregtx_check) +{ + const auto key_id = [](uint8_t seed) { + uint160 id; + std::fill(id.begin(), id.end(), seed); + return CKeyID{id}; + }; + const auto sigs = [](size_t count) { + std::vector ret(count); + for (size_t i = 0; i < count; ++i) { + std::fill(ret[i].begin(), ret[i].end(), 0x20 + i); + } + return ret; + }; + const CBLSSecretKey operator_key{MakeUCharSpan(std::vector(32, 1))}; + uint256 pro_tx_hash; + for (size_t i = 0; i < pro_tx_hash.size(); ++i) pro_tx_hash.begin()[i] = i; + struct Shape { + bool operator_key; + uint8_t voting; + size_t sig_count; + }; + for (const Shape& s : std::vector{ + {true, 0x22, 2}, + {true, 0x22, 8}, + {true, 0x22, 1}, + {true, 0x22, 0}, + {true, 0x22, 9}, + {false, 0x22, 2}, + {true, 0x00, 2}, + {false, 0x22, 0}, + }) { + CProUpSharedRegTx ptx; + ptx.proTxHash = pro_tx_hash; + ptx.pubKeyOperator.Set(s.operator_key ? operator_key.GetPublicKey() : CBLSPublicKey(), false); + ptx.keyIDVoting = key_id(s.voting); + ptx.inputsHash = uint256::TWO; + ptx.vchSigs = sigs(s.sig_count); + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << ptx; + const std::vector payload(UCharCast(ss.data()), UCharCast(ss.data() + ss.size())); + const auto decoded{GetTxPayload(payload)}; + BOOST_REQUIRE(decoded); + TxValidationState state; + decoded->IsTriviallyValid(state); + std::printf(" { \"payload\": \"%s\", \"json\": %s, \"reason\": \"%s\" },\n", HexStr(payload).c_str(), + decoded->ToJson().write().c_str(), state.GetRejectReason().c_str()); + } +} +*/ + // Each vector is a ProUpSharedRegTx payload, `json` the reference + // implementation's JSON form of it, and `reason` the rejection reason of + // its check without chain context, empty when it passes. + "check": [ + // Two signatures pass. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1faa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222202000000000000000000000000000000000000000000000000000000000000000220202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","votingAddress":"XdoKhQE8njcWL8gXfUVXnYf2ejpHSdUeQa","pubKeyOperator":"aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002","sigCount":2}, "reason": "" }, + // Eight signatures, the most allowed. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1faa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1222222222222222222222222222222222222222202000000000000000000000000000000000000000000000000000000000000000820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222223232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252526262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","votingAddress":"XdoKhQE8njcWL8gXfUVXnYf2ejpHSdUeQa","pubKeyOperator":"aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002","sigCount":8}, "reason": "" }, + // A single signature passes. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1faa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac122222222222222222222222222222222222222220200000000000000000000000000000000000000000000000000000000000000012020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","votingAddress":"XdoKhQE8njcWL8gXfUVXnYf2ejpHSdUeQa","pubKeyOperator":"aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002","sigCount":1}, "reason": "" }, + // No signatures. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1faa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222020000000000000000000000000000000000000000000000000000000000000000", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","votingAddress":"XdoKhQE8njcWL8gXfUVXnYf2ejpHSdUeQa","pubKeyOperator":"aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002","sigCount":0}, "reason": "bad-proupsharedreg-sig-count" }, + // Nine signatures, one more than allowed. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1faa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac12222222222222222222222222222222222222222020000000000000000000000000000000000000000000000000000000000000009202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202021212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232324242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525252525262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262627272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272727272828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","votingAddress":"XdoKhQE8njcWL8gXfUVXnYf2ejpHSdUeQa","pubKeyOperator":"aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002","sigCount":9}, "reason": "bad-proupsharedreg-sig-count" }, + // A null operator key. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000222222222222222222222222222222222222222202000000000000000000000000000000000000000000000000000000000000000220202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","votingAddress":"XdoKhQE8njcWL8gXfUVXnYf2ejpHSdUeQa","pubKeyOperator":"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002","sigCount":2}, "reason": "bad-protx-key-null" }, + // A null voting key. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1faa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000220202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121212121", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","votingAddress":"XagqqFetxiDb9wbartKDrXgnqLah6SqX2S","pubKeyOperator":"aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002","sigCount":2}, "reason": "bad-protx-key-null" }, + // A null operator key and no signatures fail on the key first. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002222222222222222222222222222222222222222020000000000000000000000000000000000000000000000000000000000000000", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","votingAddress":"XdoKhQE8njcWL8gXfUVXnYf2ejpHSdUeQa","pubKeyOperator":"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002","sigCount":0}, "reason": "bad-protx-key-null" }, + ], +} diff --git a/pkgs/primitives/corpus/proupsharetx.json5 b/pkgs/primitives/corpus/proupsharetx.json5 new file mode 100644 index 00000000..37aa74da --- /dev/null +++ b/pkgs/primitives/corpus/proupsharetx.json5 @@ -0,0 +1,79 @@ +// +// ProUpShareTx vectors. Sections with a program were generated with the +// reference implementation (dashpay/dash v24.0.0-rc.3, 0364ed626e). +// +// To run a program, paste it at the end of the `evo_sharedmn_tests` suite +// in `src/test/evo_sharedmn_tests.cpp` at the specified revision, build it +// with `make -C src test/test_dash` and run +// `./src/test/test_dash --run_test=evo_sharedmn_tests/`. The output +// should match the section. +// + +{ +/* +BOOST_AUTO_TEST_CASE(proupsharetx_check) +{ + const auto key_id = [](uint8_t seed) { + uint160 id; + std::fill(id.begin(), id.end(), seed); + return CKeyID{id}; + }; + const auto pay_to_key = [&](uint8_t seed) { return GetScriptForDestination(PKHash{key_id(seed)}); }; + const auto pay_to_script = [&](uint8_t seed) { return GetScriptForDestination(ScriptHash{uint160{key_id(seed)}}); }; + uint256 pro_tx_hash; + for (size_t i = 0; i < pro_tx_hash.size(); ++i) pro_tx_hash.begin()[i] = i; + struct Shape { + uint16_t share_index; + CScript script_reward; + size_t sig_size; + }; + for (const Shape& s : std::vector{ + {0, pay_to_key(0x60), 65}, + {7, pay_to_script(0x61), 65}, + {1, pay_to_key(0x60), 0}, + {1, pay_to_key(0x60), 64}, + {1, pay_to_key(0x60), 66}, + {1, CScript() << OP_TRUE, 65}, + {1, CScript(), 65}, + {1, CScript(), 0}, + }) { + CProUpShareTx ptx; + ptx.proTxHash = pro_tx_hash; + ptx.shareIndex = s.share_index; + ptx.scriptReward = s.script_reward; + ptx.inputsHash = uint256::TWO; + ptx.vchSig.assign(s.sig_size, 0x1f); + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << ptx; + const std::vector payload(UCharCast(ss.data()), UCharCast(ss.data() + ss.size())); + const auto decoded{GetTxPayload(payload)}; + BOOST_REQUIRE(decoded); + TxValidationState state; + decoded->IsTriviallyValid(state); + std::printf(" { \"payload\": \"%s\", \"json\": %s, \"reason\": \"%s\" },\n", HexStr(payload).c_str(), + decoded->ToJson().write().c_str(), state.GetRejectReason().c_str()); + } +} +*/ + // Each vector is a ProUpShareTx payload, `json` the reference + // implementation's JSON form of it, and `reason` the rejection reason of + // its check without chain context, empty when it passes. + "check": [ + // A P2PKH reward script with a 65-byte signature passes. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f00001976a914606060606060606060606060606060606060606088ac0200000000000000000000000000000000000000000000000000000000000000411f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","shareIndex":0,"rewardAddress":"XjUSA2LA85DUMf8c527PYTbpKQmmvemY8H","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002"}, "reason": "" }, + // A P2SH reward script for share 7 passes. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f070017a9146161616161616161616161616161616161616161870200000000000000000000000000000000000000000000000000000000000000411f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","shareIndex":7,"rewardAddress":"7bHYxTHcaNH3g8PNoTFPoHmZYqETh9dt3D","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002"}, "reason": "" }, + // No signature. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f01001976a914606060606060606060606060606060606060606088ac020000000000000000000000000000000000000000000000000000000000000000", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","shareIndex":1,"rewardAddress":"XjUSA2LA85DUMf8c527PYTbpKQmmvemY8H","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002"}, "reason": "bad-proupshare-sig-size" }, + // A signature one byte short. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f01001976a914606060606060606060606060606060606060606088ac0200000000000000000000000000000000000000000000000000000000000000401f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","shareIndex":1,"rewardAddress":"XjUSA2LA85DUMf8c527PYTbpKQmmvemY8H","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002"}, "reason": "bad-proupshare-sig-size" }, + // A signature one byte long. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f01001976a914606060606060606060606060606060606060606088ac0200000000000000000000000000000000000000000000000000000000000000421f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","shareIndex":1,"rewardAddress":"XjUSA2LA85DUMf8c527PYTbpKQmmvemY8H","inputsHash":"0000000000000000000000000000000000000000000000000000000000000002"}, "reason": "bad-proupshare-sig-size" }, + // A reward script that is neither P2PKH nor P2SH. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f010001510200000000000000000000000000000000000000000000000000000000000000411f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","shareIndex":1,"inputsHash":"0000000000000000000000000000000000000000000000000000000000000002"}, "reason": "bad-proupshare-payee" }, + // An empty reward script. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0100000200000000000000000000000000000000000000000000000000000000000000411f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","shareIndex":1,"inputsHash":"0000000000000000000000000000000000000000000000000000000000000002"}, "reason": "bad-proupshare-payee" }, + // An empty reward script and no signature fail on the signature first. + { "payload": "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f010000020000000000000000000000000000000000000000000000000000000000000000", "json": {"version":1,"proTxHash":"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100","shareIndex":1,"inputsHash":"0000000000000000000000000000000000000000000000000000000000000002"}, "reason": "bad-proupshare-sig-size" }, + ], +} diff --git a/pkgs/primitives/corpus/tx_check.json5 b/pkgs/primitives/corpus/tx_check.json5 index acf1c3c2..51351cb5 100644 --- a/pkgs/primitives/corpus/tx_check.json5 +++ b/pkgs/primitives/corpus/tx_check.json5 @@ -80,6 +80,14 @@ BOOST_AUTO_TEST_CASE(tx_check_shapes) {0, {1, 1}, 0, {1}, 0, MAX_LEGACY_BLOCK_SIZE + 1}, {0, {0, 1}, 0, {1}, 0, 0}, {0, {0, 1}, 2, {1}, 0, 0}, + {0, {1}, 0, {-1}, 0, 0}, + {0, {1}, 0, {MAX_MONEY + 1}, 0, 0}, + {0, {1}, 0, {1, -1}, 0, 0}, + {0, {1}, 0, {MAX_MONEY, -1}, 0, 0}, + {0, {1}, 0, {MAX_MONEY + 1, -1}, 0, 0}, + {0, {1}, 0, {CAmount{0x7fffffffffffffff}}, 0, 0}, + {0, {1}, 0, {-CAmount{0x7fffffffffffffff} - 1}, 0, 0}, + {0, {1}, 0, {MAX_MONEY, CAmount{0x7fffffffffffffff}}, 0, 0}, }) { const auto size = [&](size_t pad) { return ::GetSerializeSize(BuildTx(s, pad), PROTOCOL_VERSION); }; size_t pad{0}; @@ -177,5 +185,22 @@ BOOST_AUTO_TEST_CASE(tx_check_shapes) // Two inputs with a null first prevout and a valid coinbase script length // are still not a coinbase. { "type": 0, "inputs": [0, 1], "script_sig": 2, "values": [1], "pad": 0, "payload": 0, "size": 103, "reason": "bad-txns-prevout-null" }, + // A negative output value decodes and fails the sign check. + { "type": 0, "inputs": [1], "script_sig": 0, "values": [-1], "pad": 0, "payload": 0, "size": 60, "reason": "bad-txns-vout-negative" }, + // An output one duff above the money range. + { "type": 0, "inputs": [1], "script_sig": 0, "values": [2100000000000001], "pad": 0, "payload": 0, "size": 60, "reason": "bad-txns-vout-toolarge" }, + // A negative output after a valid one. + { "type": 0, "inputs": [1], "script_sig": 0, "values": [1, -1], "pad": 0, "payload": 0, "size": 69, "reason": "bad-txns-vout-negative" }, + // A negative output fails even when the total stays in range. + { "type": 0, "inputs": [1], "script_sig": 0, "values": [2100000000000000, -1], "pad": 0, "payload": 0, "size": 69, "reason": "bad-txns-vout-negative" }, + // Outputs are checked in order, so the cap of the first fails before the + // sign of the second. + { "type": 0, "inputs": [1], "script_sig": 0, "values": [2100000000000001, -1], "pad": 0, "payload": 0, "size": 69, "reason": "bad-txns-vout-toolarge" }, + // The highest 64-bit value is above the money range. + { "type": 0, "inputs": [1], "script_sig": 0, "values": [9223372036854775807], "pad": 0, "payload": 0, "size": 60, "reason": "bad-txns-vout-toolarge" }, + // The lowest 64-bit value is negative. + { "type": 0, "inputs": [1], "script_sig": 0, "values": [-9223372036854775808], "pad": 0, "payload": 0, "size": 60, "reason": "bad-txns-vout-negative" }, + // The range check of an output comes before it is added to the total. + { "type": 0, "inputs": [1], "script_sig": 0, "values": [2100000000000000, 9223372036854775807], "pad": 0, "payload": 0, "size": 69, "reason": "bad-txns-vout-toolarge" }, ], } diff --git a/pkgs/primitives/src/block.rs b/pkgs/primitives/src/block.rs index f3100dcf..b0c1f737 100644 --- a/pkgs/primitives/src/block.rs +++ b/pkgs/primitives/src/block.rs @@ -7,7 +7,7 @@ //! Dash block (header + transactions). use crate::prelude::*; -use crate::sptx::TxType; +use crate::sptx::{SpecialPayload, TxType}; use crate::transaction::{OutPoint, Transaction, TxHash, TxIn, TxInvalid, TxOut}; use crate::{codec_base, codec_type, hash_impl}; @@ -104,6 +104,10 @@ pub enum BlockInvalid { /// `bad-txns-duplicate` #[error("bad-txns-duplicate")] DuplicateTransactions, + /// The asset unlock root of a version 4 coinbase payload does not match the + /// block. + #[error("bad-cbtx-assetunlockmerkleroot")] + BadAssetUnlockMerkleRoot, /// `bad-blk-length` #[error("bad-blk-length: {size} bytes")] BadBlockLength { size: usize }, @@ -152,6 +156,13 @@ impl Checkable for Block { if mutated { return Some(BlockInvalid::DuplicateTransactions); } + if let Some(Ok(SpecialPayload::CoinbaseCommitment(cbtx))) = + self.transactions.first().and_then(Transaction::decode_payload) + { + if cbtx.version() >= 4 && cbtx.merkle_root_asset_unlocks() != Some(self.asset_unlock_merkle_root()) { + return Some(BlockInvalid::BadAssetUnlockMerkleRoot); + } + } let size = codec::encoded_size(self); if self.transactions.is_empty() || size > MAX_DIP0001_BLOCK_SIZE { @@ -235,6 +246,20 @@ impl Block { let leaves: Vec = self.transactions.iter().map(|tx| tx.hash()).collect(); compute_merkle_root(&leaves) } + + /// Computes the merkle root that a version 4 coinbase payload commits to: + /// the [`Transaction::instance_hash`] of each transaction with a + /// [stable txid](Transaction::has_stable_txid), in block order. It is the + /// null root when there is none. + pub fn asset_unlock_merkle_root(&self) -> MerkleRoot { + let leaves: Vec = self + .transactions + .iter() + .filter(|tx| tx.has_stable_txid()) + .map(Transaction::instance_hash) + .collect(); + compute_merkle_root(&leaves).0 + } } impl fmt::Display for Block { @@ -254,7 +279,7 @@ impl From<&GenesisParams> for Block { 0xFFFF_FFFF, )], vec![TxOut::new( - params.reward, + params.reward.to_signed().to_sat(), ScriptPubKeyBuf::from_bytes(params.script_pubkey.to_vec()), )], 0, @@ -281,12 +306,14 @@ impl From<&GenesisParams> for Block { #[expect(clippy::panic, clippy::unwrap_used, reason = "test code")] mod tests { use super::*; - use crate::sptx::TxType; + use crate::payload::{AssetUnlock, CoinbaseCommitment}; + use crate::sptx::{QuorumHash, TxType}; use crate::transaction::{OutPoint, TxIn, TxOut}; use bitcoin_primitives::script::{ScriptPubKeyBuf, ScriptSigBuf}; - use bitcoin_units::Amount; + use bitcoin_units::BlockHeight; use dash_dev::{assert_serde_rt, check_wire, Corpus}; + use dash_pkc::bls::{BlsSigBytes, BLS_SIG_LEN}; use dash_script::Opcode; use rstest::rstest; use serde::Deserialize; @@ -331,10 +358,7 @@ mod tests { 3, TxType::Spend, inputs.collect(), - vec![TxOut::new( - Amount::from_sat(1).unwrap(), - ScriptPubKeyBuf::from_bytes(script), - )], + vec![TxOut::new(1, ScriptPubKeyBuf::from_bytes(script))], self.lock_time, vec![], ) @@ -357,22 +381,118 @@ mod tests { } } + /// Builds a block that commits to `transactions` and passes the proof of + /// work. + fn mine(transactions: Vec) -> Block { + let mut block = Block { + header: BlockHeader { + bits: 0x207f_ffff, + ..BlockHeader::default() + }, + transactions, + }; + block.header.merkle_root = block.merkle().0; + while block.check() == Some(BlockInvalid::BadProofOfWork) { + block.header.nonce += 1; + } + block + } + #[rstest] fn check_matches_vectors() { let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "block_check"); for (i, v) in corpus.vectors::("check").into_iter().enumerate() { - let mut block = Block { - header: BlockHeader { - bits: 0x207f_ffff, - ..BlockHeader::default() - }, - transactions: v.txs.iter().map(TxShape::build).collect(), - }; - block.header.merkle_root = block.merkle().0; + let block = mine(v.txs.iter().map(TxShape::build).collect()); + assert_eq!(block.header.merkle_root, v.merkle_root, "vector {i}"); + let mut buf = Vec::new(); + block.encode(&mut buf); + assert_eq!(buf.len(), v.size, "vector {i}"); + assert_eq!(reason(block.check()), v.reason, "vector {i}"); + } + } + + #[derive(Deserialize)] + struct UnlockShape { + version: u8, + index: u64, + quorum: u8, + } + + impl UnlockShape { + fn build(&self) -> Transaction { + let payload = AssetUnlock::new( + self.version, + self.index, + 1, + 0, + QuorumHash::from_lendian([self.quorum; 32]), + BlsSigBytes::from_bytes([0; BLS_SIG_LEN]), + ); + let mut extra_payload = Vec::new(); + payload.encode(&mut extra_payload); + Transaction::new( + 3, + TxType::AssetUnlock, + vec![], + vec![TxOut::new(1, ScriptPubKeyBuf::new())], + 0, + extra_payload, + ) + } + } + + #[derive(Deserialize)] + struct AssetUnlockRootVector { + coinbase_input: u8, + cbtx_version: u16, + unlocks: Vec, + asset_unlock_root: Option, + merkle_root: MerkleRoot, + size: usize, + reason: String, + } + + impl AssetUnlockRootVector { + fn coinbase(&self) -> Transaction { + let payload = CoinbaseCommitment::new( + self.cbtx_version, + BlockHeight::from_u32(0), + MerkleRoot::default(), + Some(MerkleRoot::default()), + Some(0), + Some(BlsSigBytes::from_bytes([0; BLS_SIG_LEN])), + Some(0), + self.asset_unlock_root, + ); + let mut extra_payload = Vec::new(); + payload.encode(&mut extra_payload); + let seed = self.coinbase_input; + Transaction::new( + 3, + TxType::CoinbaseCommitment, + vec![TxIn::new( + OutPoint::new(TxHash::from_lendian([seed; 32]), if seed == 0 { u32::MAX } else { 0 }), + ScriptSigBuf::from_bytes(vec![0; 2]), + u32::MAX, + )], + vec![TxOut::new(1, ScriptPubKeyBuf::new())], + 0, + extra_payload, + ) + } + } + + #[rstest] + fn check_matches_asset_unlock_root_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "block_check"); + for (i, v) in corpus + .vectors::("asset_unlock_root") + .into_iter() + .enumerate() + { + let unlocks = v.unlocks.iter().map(UnlockShape::build); + let block = mine(core::iter::once(v.coinbase()).chain(unlocks).collect()); assert_eq!(block.header.merkle_root, v.merkle_root, "vector {i}"); - while block.check() == Some(BlockInvalid::BadProofOfWork) { - block.header.nonce += 1; - } let mut buf = Vec::new(); block.encode(&mut buf); assert_eq!(buf.len(), v.size, "vector {i}"); diff --git a/pkgs/primitives/src/codec.rs b/pkgs/primitives/src/codec.rs index bfbb30a3..a665a659 100644 --- a/pkgs/primitives/src/codec.rs +++ b/pkgs/primitives/src/codec.rs @@ -6,9 +6,38 @@ //! Codec helpers for primitives payload types. +use crate::prelude::*; + +use dash_types::codec::{BaseCodec, DecodeError, EncodeBuf}; + /// Maximum special-transaction payload size over the wire (10 KiB). pub const MAX_SPTX_PAYLOAD_SIZE: usize = 10_240; +/// Reads `count` values written back to back. +pub(crate) fn decode_items(data: &mut &[u8], count: u8) -> Result, DecodeError> { + (0..count).map(|_| T::decode(data)).collect() +} + +/// Reads a list whose length is a single byte. +pub(crate) fn decode_list(data: &mut &[u8]) -> Result, DecodeError> { + let count = u8::decode(data)?; + decode_items(data, count) +} + +/// Writes values back to back. +pub(crate) fn encode_items(items: &[T], buf: &mut impl EncodeBuf) { + for item in items { + item.encode(buf); + } +} + +/// Writes a list whose length is a single byte, which wraps past 255 as the +/// reference implementation's does. +pub(crate) fn encode_list(items: &[T], buf: &mut impl EncodeBuf) { + (items.len() as u8).encode(buf); + encode_items(items, buf); +} + /// Blanket `Hashable` via SHA256d of wire encoding. /// /// Also asserts that the type implements `TypeId`, producing a diff --git a/pkgs/primitives/src/payload/assetlock.rs b/pkgs/primitives/src/payload/assetlock.rs index 9264132c..affa7386 100644 --- a/pkgs/primitives/src/payload/assetlock.rs +++ b/pkgs/primitives/src/payload/assetlock.rs @@ -8,7 +8,7 @@ use crate::codec::codec_payload; use crate::prelude::*; -use crate::transaction::TxOut; +use crate::transaction::{money_range, TxOut}; use dash_script::Recipient; use dash_types::type_id::{TypeId, Unencodable}; @@ -47,13 +47,18 @@ pub enum AssetLockInvalid { /// `bad-assetlocktx-pubKeyHash` #[error("bad-assetlocktx-pubKeyHash: output {index}")] CreditNotP2pkh { index: usize }, + /// `bad-assetlocktx-script-pubkey` + #[error("bad-assetlocktx-script-pubkey: output {index}")] + CreditBadScript { index: usize }, } impl Checkable for AssetLock { type Error = AssetLockInvalid; + /// Checks the rules that need no chain context. Refusing version 2 before + /// the v24 deployment is active is left to the caller. fn check(&self) -> Option { - if self.version == 0 || self.version > 1 { + if self.version == 0 || self.version > 2 { return Some(AssetLockInvalid::BadVersion { version: self.version }); } @@ -61,21 +66,18 @@ impl Checkable for AssetLock { return Some(AssetLockInvalid::EmptyCreditOutputs); } - let max_money = bitcoin_units::Amount::MAX_MONEY.to_sat(); - let mut total: u64 = 0; + let mut total: i64 = 0; for (i, out) in self.credit_outputs.iter().enumerate() { - let sat = out.value().to_sat(); - if sat == 0 || sat > max_money { + if out.value() == 0 || !money_range(out.value()) || !money_range(total + out.value()) { return Some(AssetLockInvalid::CreditOutOfRange { index: i }); } - total = total.saturating_add(sat); - if total > max_money { - return Some(AssetLockInvalid::CreditOutOfRange { index: i }); - } - if !matches!( - Recipient::from_script(out.script_pubkey().as_bytes()), - Some(Recipient::PubKeyHash(_)) - ) { + total += out.value(); + let recipient = Recipient::from_script(out.script_pubkey().as_bytes()); + if self.version >= 2 { + if !matches!(recipient, Some(Recipient::PubKeyHash(_) | Recipient::ScriptHash(_))) { + return Some(AssetLockInvalid::CreditBadScript { index: i }); + } + } else if !matches!(recipient, Some(Recipient::PubKeyHash(_))) { return Some(AssetLockInvalid::CreditNotP2pkh { index: i }); } } @@ -96,11 +98,14 @@ impl fmt::Display for AssetLock { } #[cfg(all(test, feature = "serde"))] +#[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; - use dash_dev::{assert_serde_rt, check_sptx, Corpus}; + use dash_dev::{assert_serde_rt, check_sptx, vec_from_hex, Corpus}; + use dash_types::codec::BaseCodec; use rstest::rstest; + use serde::Deserialize; #[rstest] fn corpus_assetlock() { @@ -108,4 +113,28 @@ mod tests { let items = corpus.entries::("assetlock", check_sptx); assert_serde_rt("assetlock", &items); } + + #[derive(Deserialize)] + struct CheckVector { + version: u8, + values: Vec, + payload: String, + reason: String, + } + + #[rstest] + fn check_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "assetlock"); + for (i, v) in corpus.vectors::("check").into_iter().enumerate() { + let raw = vec_from_hex(&v.payload); + let mut cursor = &raw[..]; + let lock = AssetLock::decode(&mut cursor).unwrap(); + assert!(cursor.is_empty(), "vector {i}"); + assert_eq!(lock.version, v.version, "vector {i}"); + let values: Vec = lock.credit_outputs.iter().map(TxOut::value).collect(); + assert_eq!(values, v.values, "vector {i}"); + let reason = lock.check().map(|e| e.to_string()).unwrap_or_default(); + assert_eq!(reason.split(':').next().unwrap_or_default(), v.reason, "vector {i}"); + } + } } diff --git a/pkgs/primitives/src/payload/assetunlock.rs b/pkgs/primitives/src/payload/assetunlock.rs index e8b11035..611938a2 100644 --- a/pkgs/primitives/src/payload/assetunlock.rs +++ b/pkgs/primitives/src/payload/assetunlock.rs @@ -57,8 +57,10 @@ pub enum AssetUnlockInvalid { impl Checkable for AssetUnlock { type Error = AssetUnlockInvalid; + /// Checks the rules that need no chain context. Refusing version 2 before + /// the v24 deployment is active is left to the caller. fn check(&self) -> Option { - if self.version == 0 || self.version > 1 { + if self.version == 0 || self.version > 2 { return Some(AssetUnlockInvalid::BadVersion { version: self.version }); } @@ -77,11 +79,15 @@ impl fmt::Display for AssetUnlock { } #[cfg(all(test, feature = "serde"))] +#[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; + use crate::prelude::*; - use dash_dev::{assert_serde_rt, check_sptx, Corpus}; + use dash_dev::{assert_serde_rt, check_sptx, vec_from_hex, Corpus}; + use dash_types::codec::BaseCodec; use rstest::rstest; + use serde::Deserialize; #[rstest] fn corpus_assetunlock() { @@ -89,4 +95,33 @@ mod tests { let items = corpus.entries::("assetunlock", check_sptx); assert_serde_rt("assetunlock", &items); } + + #[derive(Deserialize)] + struct CheckVector { + version: u8, + fee: u32, + payload: String, + reason: String, + fee_reason: String, + } + + /// The quorum hash lookup after the version check needs the chain, so its + /// reason counts as a pass and the fee reason applies. + #[rstest] + fn check_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "assetunlock"); + for (i, v) in corpus.vectors::("check").into_iter().enumerate() { + let raw = vec_from_hex(&v.payload); + let mut cursor = &raw[..]; + let unlock = AssetUnlock::decode(&mut cursor).unwrap(); + assert!(cursor.is_empty(), "vector {i}"); + assert_eq!((unlock.version, unlock.fee), (v.version, v.fee), "vector {i}"); + let expected = match v.reason.as_str() { + "bad-assetunlock-quorum-hash" => &v.fee_reason, + reason => reason, + }; + let reason = unlock.check().map(|e| e.to_string()).unwrap_or_default(); + assert_eq!(reason.split(':').next().unwrap_or_default(), expected, "vector {i}"); + } + } } diff --git a/pkgs/primitives/src/payload/cbtx.rs b/pkgs/primitives/src/payload/cbtx.rs index f9a58a5b..7c3a162b 100644 --- a/pkgs/primitives/src/payload/cbtx.rs +++ b/pkgs/primitives/src/payload/cbtx.rs @@ -24,11 +24,12 @@ impl_new! { /// - v1: base fields (version, height, merkle_root_mn_list) /// - v2: adds merkle_root_quorums /// - v3: adds ChainLock proof and credit pool balance +/// - v4: adds merkle_root_asset_unlocks #[derive(Clone, CopyGetters, Debug, Eq, Hash, PartialEq, TypeId)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct CoinbaseCommitment { - /// Payload version (1, 2, or 3). + /// Payload version (1 to 4). #[getset(get_copy = "pub")] version: u16, /// Block height. @@ -49,6 +50,10 @@ pub struct CoinbaseCommitment { /// Credit pool balance in duffs (v3+). #[getset(get_copy = "pub")] credit_pool_balance: Option, + /// Merkle root of the instance hashes of the block's asset unlocks with a + /// stable txid (v4+). + #[getset(get_copy = "pub")] + merkle_root_asset_unlocks: Option, } } @@ -73,6 +78,11 @@ impl BaseCodec for CoinbaseCommitment { } else { (None, None, None) }; + let merkle_root_asset_unlocks = if version >= 4 { + Some(MerkleRoot::decode(data)?) + } else { + None + }; Ok(Self { version, @@ -82,6 +92,7 @@ impl BaseCodec for CoinbaseCommitment { best_cl_height_diff, best_cl_signature, credit_pool_balance, + merkle_root_asset_unlocks, }) } @@ -101,6 +112,9 @@ impl BaseCodec for CoinbaseCommitment { sig.encode(buf); bal.encode(buf); } + if let Some(root) = self.merkle_root_asset_unlocks { + root.encode(buf); + } } } @@ -121,8 +135,10 @@ pub enum CbTxInvalid { impl Checkable for CoinbaseCommitment { type Error = CbTxInvalid; + /// Checks the rules that need no chain context. The version that the active + /// deployments require is left to the caller. fn check(&self) -> Option { - if self.version == 0 || self.version >= 4 { + if self.version == 0 || self.version >= 5 { return Some(CbTxInvalid::BadVersion { version: self.version }); } @@ -145,6 +161,14 @@ impl Checkable for CoinbaseCommitment { return Some(CbTxInvalid::UnexpectedField); } + if self.version >= 4 { + if self.merkle_root_asset_unlocks.is_none() { + return Some(CbTxInvalid::MissingField); + } + } else if self.merkle_root_asset_unlocks.is_some() { + return Some(CbTxInvalid::UnexpectedField); + } + None } } @@ -160,9 +184,11 @@ impl fmt::Display for CoinbaseCommitment { #[cfg(all(test, feature = "serde"))] mod tests { use super::*; + use crate::prelude::*; - use dash_dev::{assert_serde_rt, check_sptx, Corpus}; + use dash_dev::{assert_serde_rt, check_sptx, vec_from_hex, Corpus}; use rstest::rstest; + use serde::Deserialize; #[rstest] fn corpus_cbtx() { @@ -170,4 +196,34 @@ mod tests { let items = corpus.entries::("cbtx", check_sptx); assert_serde_rt("cbtx", &items); } + + #[derive(Deserialize)] + struct VersionVector { + version: u16, + payload: String, + decodes: bool, + root: Option, + reason: String, + } + + #[rstest] + fn versions_match_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "cbtx"); + for (i, v) in corpus.vectors::("versions").into_iter().enumerate() { + let raw = vec_from_hex(&v.payload); + let mut cursor = &raw[..]; + let decoded = CoinbaseCommitment::decode(&mut cursor) + .ok() + .filter(|_| cursor.is_empty()); + assert_eq!(decoded.is_some(), v.decodes, "vector {i}"); + let Some(cbtx) = decoded else { continue }; + assert_eq!(cbtx.version, v.version, "vector {i}"); + assert_eq!(cbtx.merkle_root_asset_unlocks, v.root, "vector {i}"); + let mut buf = Vec::new(); + cbtx.encode(&mut buf); + assert_eq!(buf, raw, "vector {i}"); + let reason = cbtx.check().map(|e| e.to_string()).unwrap_or_default(); + assert_eq!(reason.split(':').next().unwrap_or_default(), v.reason, "vector {i}"); + } + } } diff --git a/pkgs/primitives/src/provider/mod.rs b/pkgs/primitives/src/provider/mod.rs index 050a7952..f755572c 100644 --- a/pkgs/primitives/src/provider/mod.rs +++ b/pkgs/primitives/src/provider/mod.rs @@ -6,21 +6,29 @@ //! Provider transaction payloads. +mod prodistx; mod proregtx; mod proupregtx; mod prouprevtx; mod proupservtx; +mod proupsharedregtx; +mod proupsharetx; +mod share; mod sig; mod version; #[cfg(feature = "serde")] pub(crate) use self::sig::compact_hex; +pub use self::prodistx::ProDisTx; pub use self::proregtx::ProRegTx; pub use self::proupregtx::ProUpRegTx; pub use self::prouprevtx::ProUpRevTx; pub use self::proupservtx::ProUpServTx; -pub use self::sig::{CollateralSig, OperatorSig, OwnerSig}; +pub use self::proupsharedregtx::ProUpSharedRegTx; +pub use self::proupsharetx::ProUpShareTx; +pub use self::share::{CollateralShare, PayoutShare}; +pub use self::sig::{CollateralSig, CompactSig, OperatorSig, OwnerSig}; pub use self::version::ProTxVersion; pub use dash_pkc::__private::__EddsaPkHash as PlatformNodeId; diff --git a/pkgs/primitives/src/provider/prodistx.rs b/pkgs/primitives/src/provider/prodistx.rs new file mode 100644 index 00000000..92a92774 --- /dev/null +++ b/pkgs/primitives/src/provider/prodistx.rs @@ -0,0 +1,174 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! ProDisTx shared masternode dissolution payload (type 10). + +use super::share::MAX_SHARES; +use super::CompactSig; +use crate::codec::{decode_list, encode_list, impl_payload}; +use crate::prelude::*; +use crate::sptx::ProTxInvalid; +use crate::{hash_impl, TxHash}; + +use dash_types::codec::{BaseCodec, DecodeError, EncodeBuf}; +use dash_types::type_id::TypeId; +use dash_types::{impl_new, Checkable}; +use getset::{CopyGetters, Getters}; + +use core::fmt; + +/// The payload version `check` accepts. +const VERSION: u16 = 1; + +impl_new! { +/// ProDisTx -- dissolve a shared masternode (type 10). +#[derive(Clone, CopyGetters, Debug, Eq, Getters, Hash, PartialEq, TypeId)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct ProDisTx { + /// Payload version, read at any value. + #[getset(get_copy = "pub")] + version: u16, + /// ProTx hash identifying the masternode. + #[getset(get_copy = "pub")] + pro_tx_hash: TxHash, + /// Index of the acting share. + #[getset(get_copy = "pub")] + actor_index: u16, + /// One signature by the acting share's owner, or one per share in share + /// order. + #[getset(get = "pub")] + sigs: Vec, +} +} + +impl_payload!(ProDisTx); + +impl BaseCodec for ProDisTx { + fn decode(data: &mut &[u8]) -> Result { + Ok(Self { + version: u16::decode(data)?, + pro_tx_hash: TxHash::decode(data)?, + actor_index: u16::decode(data)?, + sigs: decode_list(data)?, + }) + } + + fn encode(&self, buf: &mut impl EncodeBuf) { + self.version.encode(buf); + self.pro_tx_hash.encode(buf); + self.actor_index.encode(buf); + encode_list(&self.sigs, buf); + } +} + +impl Checkable for ProDisTx { + type Error = ProTxInvalid; + + fn check(&self) -> Option { + if self.version != VERSION { + return Some(ProTxInvalid::BadDissolveVersion { version: self.version }); + } + if self.sigs.is_empty() || self.sigs.len() > MAX_SHARES { + return Some(ProTxInvalid::BadDissolveSigCount { count: self.sigs.len() }); + } + None + } +} + +hash_impl!(ProDisTx); + +impl fmt::Display for ProDisTx { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ProDisTx {{ v{} }}", self.version) + } +} + +#[cfg(all(test, feature = "serde"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::sptx::{SpecialPayload, TxType}; + + use dash_dev::{assert_serde_rt, vec_from_hex, Corpus}; + use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Json { + version: u16, + pro_tx_hash: String, + actor_index: u16, + sig_count: usize, + } + + #[derive(Deserialize)] + struct CheckVector { + payload: String, + json: Json, + reason: String, + } + + #[rstest] + fn check_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "prodistx"); + let mut decoded = Vec::new(); + for (i, v) in corpus.vectors::("check").into_iter().enumerate() { + let raw = vec_from_hex(&v.payload); + let mut cursor = &raw[..]; + let payload = SpecialPayload::decode(TxType::ProviderDissolve, &mut cursor).unwrap(); + assert!(cursor.is_empty(), "vector {i}"); + let ptx = ProDisTx::decode(&mut &raw[..]).unwrap(); + assert_eq!(payload, SpecialPayload::ProviderDissolve(ptx.clone()), "vector {i}"); + assert_eq!(ptx.version(), v.json.version, "vector {i}"); + assert_eq!( + ptx.pro_tx_hash(), + TxHash::from_hex(&v.json.pro_tx_hash).unwrap(), + "vector {i}" + ); + assert_eq!(ptx.actor_index(), v.json.actor_index, "vector {i}"); + assert_eq!(ptx.sigs().len(), v.json.sig_count, "vector {i}"); + let reason = payload.check().map(|e| e.to_string()).unwrap_or_default(); + assert_eq!(reason.split(':').next().unwrap_or_default(), v.reason, "vector {i}"); + let mut buf = Vec::new(); + ptx.encode(&mut buf); + assert_eq!(buf, raw, "vector {i}"); + decoded.push((format!("{i}"), ptx)); + } + assert_serde_rt("check", &decoded.into_iter().collect()); + } + + #[rstest] + #[case::version_0("", 0, Some(ProTxInvalid::BadDissolveVersion { version: 0 }))] + #[case::version_1("", 1, None)] + #[case::version_2("", 2, Some(ProTxInvalid::BadDissolveVersion { version: 2 }))] + #[case::version_2_before_sig_count( + "bad-prodis-sig-count", + 2, + Some(ProTxInvalid::BadDissolveVersion { version: 2 }) + )] + fn check_requires_version_1(#[case] reason: &str, #[case] version: u16, #[case] expected: Option) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "prodistx"); + let vector = corpus + .vectors::("check") + .into_iter() + .find(|v| v.reason == reason) + .unwrap(); + let mut raw = vec_from_hex(&vector.payload); + raw[..2].copy_from_slice(&version.to_le_bytes()); + let mut cursor = &raw[..]; + let payload = SpecialPayload::decode(TxType::ProviderDissolve, &mut cursor).unwrap(); + assert!(cursor.is_empty()); + let ptx = ProDisTx::decode(&mut &raw[..]).unwrap(); + assert_eq!(payload, SpecialPayload::ProviderDissolve(ptx.clone())); + assert_eq!(ptx.version(), version); + assert_eq!(ptx.check(), expected); + let mut buf = Vec::new(); + ptx.encode(&mut buf); + assert_eq!(buf, raw); + } +} diff --git a/pkgs/primitives/src/provider/proregtx.rs b/pkgs/primitives/src/provider/proregtx.rs index 1057faf5..d2c24dad 100644 --- a/pkgs/primitives/src/provider/proregtx.rs +++ b/pkgs/primitives/src/provider/proregtx.rs @@ -6,8 +6,9 @@ //! ProRegTx registration payload (type 1). -use super::{CollateralSig, ProTxVersion}; -use crate::codec::impl_payload; +use super::share::{check_payout_version, check_payouts, check_shares, EVO_COLLATERAL, MAX_REWARD, REGULAR_COLLATERAL}; +use super::{CollateralShare, CollateralSig, CompactSig, PayoutShare, ProTxVersion}; +use crate::codec::{decode_items, decode_list, encode_items, encode_list, impl_payload}; use crate::prelude::*; use crate::provider::PlatformNodeId; use crate::sign::{message_digest, unsigned_digest, verify_compact}; @@ -33,7 +34,7 @@ use core::fmt; /// /// - v1: LegacyBLS /// - v2: BasicBLS -/// - v3: ExtAddr (extended network info) +/// - v3: ExtAddr (extended network info, owner payouts, collateral shares) #[derive(Clone, CopyGetters, Debug, Eq, Getters, Hash, PartialEq, TypeId)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] @@ -68,9 +69,29 @@ pub struct ProRegTx { /// Operator reward in basis points (0-10000). #[getset(get_copy = "pub")] operator_reward: u16, - /// Payout script. + /// Payout script (version < 3 only). #[getset(get = "pub")] script_payout: ScriptPubKeyBuf, + /// Owner payouts (version 3). + #[cfg_attr(feature = "serde", serde(default))] + #[getset(get = "pub")] + payouts: Vec, + /// Collateral shares, empty unless the collateral is shared (version 3). + #[cfg_attr(feature = "serde", serde(default))] + #[getset(get = "pub")] + shares: Vec, + /// Consent signatures, one per share in share order (version 3). + #[cfg_attr(feature = "serde", serde(default))] + #[getset(get = "pub")] + join_sigs: Vec, + /// Early dissolution period in blocks (version 3). + #[cfg_attr(feature = "serde", serde(default))] + #[getset(get_copy = "pub")] + early_period_blocks: u32, + /// Early dissolution penalty in duffs (version 3). + #[cfg_attr(feature = "serde", serde(default))] + #[getset(get_copy = "pub")] + early_penalty: i64, /// Hash of all inputs. #[getset(get_copy = "pub")] inputs_hash: InputsHash, @@ -118,6 +139,14 @@ pub(super) fn check_platform_fields( None } +/// The address a payout script pays, or the script in hex. +fn payee_string(script: &ScriptPubKeyBuf, ctx: &AddrParams) -> String { + let script = script.as_bytes(); + Recipient::from_script(script) + .and_then(|r| r.to_base58c(ctx)) + .unwrap_or_else(|| script.to_lower_hex_string()) +} + impl BaseCodec for ProRegTx { fn decode(data: &mut &[u8]) -> Result { let version = ProTxVersion::decode(data)?; @@ -134,7 +163,19 @@ impl BaseCodec for ProRegTx { let pub_key_operator = BlsPkBytes::decode(data)?; let key_id_voting = PubKeyHash::decode(data)?; let operator_reward = u16::decode(data)?; - let script_payout = ScriptPubKeyBuf::decode(data)?; + let (script_payout, payouts) = if version >= ProTxVersion::ExtAddr { + (ScriptPubKeyBuf::new(), decode_list(data)?) + } else { + (ScriptPubKeyBuf::decode(data)?, Vec::new()) + }; + let (shares, join_sigs, early_period_blocks, early_penalty) = if version >= ProTxVersion::ExtAddr { + let count = u8::decode(data)?; + let shares = decode_items(data, count)?; + let join_sigs = decode_items(data, count)?; + (shares, join_sigs, u32::decode(data)?, i64::decode(data)?) + } else { + (Vec::new(), Vec::new(), 0, 0) + }; let inputs_hash = InputsHash::decode(data)?; let (platform_node_id, platform_p2p_port, platform_http_port) = if mn_type == MnType::Evo { let node_id = PlatformNodeId::decode(data)?; @@ -159,6 +200,11 @@ impl BaseCodec for ProRegTx { key_id_voting, operator_reward, script_payout, + payouts, + shares, + join_sigs, + early_period_blocks, + early_penalty, inputs_hash, platform_node_id, platform_p2p_port, @@ -186,7 +232,17 @@ impl BaseCodec for ProRegTx { self.pub_key_operator.encode(buf); self.key_id_voting.encode(buf); self.operator_reward.encode(buf); - self.script_payout.encode(buf); + if self.version >= ProTxVersion::ExtAddr { + encode_list(&self.payouts, buf); + // The signatures reuse the share count, so a signature list of another + // length does not read back. + encode_list(&self.shares, buf); + encode_items(&self.join_sigs, buf); + self.early_period_blocks.encode(buf); + self.early_penalty.encode(buf); + } else { + self.script_payout.encode(buf); + } self.inputs_hash.encode(buf); if self.mn_type == MnType::Evo { self.platform_node_id.unwrap_or_default().encode(buf); @@ -205,10 +261,16 @@ impl Signable for ProRegTx { type Error = EcdsaError; fn payload(&self, ctx: &AddrParams) -> Vec { - let payout = self.script_payout.as_bytes(); - let payout = Recipient::from_script(payout) - .and_then(|r| r.to_base58c(ctx)) - .unwrap_or_else(|| payout.to_lower_hex_string()); + let payout = if self.version >= ProTxVersion::ExtAddr { + let payouts: Vec = self + .payouts + .iter() + .map(|p| format!("{}:{}", payee_string(p.script_payout(), ctx), p.reward())) + .collect(); + payouts.join(",") + } else { + payee_string(&self.script_payout, ctx) + }; let msg = format!( "{payout}|{}|{}|{}|{}", self.operator_reward, @@ -250,18 +312,43 @@ impl Checkable for ProRegTx { return Some(ProTxInvalid::BadMode { mode: self.mode }); } - if self.key_id_owner.is_null() || self.key_id_voting.is_null() { - return Some(ProTxInvalid::NullKey); + let shared = !self.shares.is_empty(); + if self.join_sigs.len() != self.shares.len() { + return Some(ProTxInvalid::JoinSigCountMismatch { + sigs: self.join_sigs.len(), + shares: self.shares.len(), + }); } - if self.pub_key_operator.is_null() { + if shared && self.version < ProTxVersion::ExtAddr { + return Some(ProTxInvalid::SharesVersionTooLow { version: self.version }); + } + if let Some(e) = check_payout_version(self.version, &self.script_payout, &self.payouts) { + return Some(e); + } + if shared { + if !self.collateral_hash.is_null() { + return Some(ProTxInvalid::ExternalSharedCollateral); + } + if !self.key_id_owner.is_null() { + return Some(ProTxInvalid::SharedOwnerKey); + } + if !self.payouts.is_empty() { + return Some(ProTxInvalid::SharedPayouts); + } + } else { + if self.early_period_blocks != 0 || self.early_penalty != 0 { + return Some(ProTxInvalid::UnsharedEarlyTerms); + } + if self.key_id_owner.is_null() { + return Some(ProTxInvalid::NullKey); + } + } + if self.pub_key_operator.is_null() || self.key_id_voting.is_null() { return Some(ProTxInvalid::NullKey); } - let payout = self.script_payout.as_bytes(); - let recipient = Recipient::from_script(payout); - match &recipient { - Some(Recipient::PubKeyHash(_) | Recipient::ScriptHash(_)) => {} - _ => return Some(ProTxInvalid::BadPayoutScript), + if let Some(e) = self.check_owner_rewards() { + return Some(e); } let is_extended = matches!(self.net_info, NetInfo::Extended(_)); @@ -294,12 +381,6 @@ impl Checkable for ProRegTx { return Some(e); } - if let Some(Recipient::PubKeyHash(hash)) = &recipient { - if *hash == self.key_id_owner || *hash == self.key_id_voting { - return Some(ProTxInvalid::PayoutKeyReuse); - } - } - if self.operator_reward > MAX_OPERATOR_REWARD { return Some(ProTxInvalid::OperatorRewardTooHigh { reward: self.operator_reward, @@ -327,6 +408,11 @@ impl ProRegTx { key_id_voting: PubKeyHash, operator_reward: u16, script_payout: ScriptPubKeyBuf, + payouts: Vec, + shares: Vec, + join_sigs: Vec, + early_period_blocks: u32, + early_penalty: i64, inputs_hash: InputsHash, platform_node_id: Option, platform_p2p_port: Option, @@ -344,6 +430,11 @@ impl ProRegTx { key_id_voting, operator_reward, script_payout, + payouts, + shares, + join_sigs, + early_period_blocks, + early_penalty, inputs_hash, platform_node_id, platform_p2p_port, @@ -351,6 +442,28 @@ impl ProRegTx { sig: CollateralSig::default(), } } + + /// Checks the collateral shares, or the owner payouts when unshared. + fn check_owner_rewards(&self) -> Option { + if !self.shares.is_empty() { + let collateral = match self.mn_type { + MnType::Evo => EVO_COLLATERAL, + _ => REGULAR_COLLATERAL, + }; + return check_shares( + &self.shares, + collateral, + self.early_period_blocks, + self.early_penalty, + &self.key_id_voting, + ); + } + if self.version >= ProTxVersion::ExtAddr { + return check_payouts(&self.payouts, Some(&self.key_id_owner), &self.key_id_voting); + } + let payout = PayoutShare::new(self.script_payout.clone(), MAX_REWARD); + check_payouts(&[payout], Some(&self.key_id_owner), &self.key_id_voting) + } } impl fmt::Display for ProRegTx { @@ -365,11 +478,16 @@ impl fmt::Display for ProRegTx { } #[cfg(all(test, feature = "serde"))] +#[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; + use crate::Transaction; - use dash_dev::{assert_serde_rt, check_sptx, Corpus}; + use dash_dev::{arr_from_hex, assert_serde_rt, check_sptx, vec_from_hex, Corpus}; + use dash_params::Network; + use dash_types::Hashable; use rstest::rstest; + use serde::Deserialize; #[rstest] fn corpus_proregtx() { @@ -377,4 +495,274 @@ mod tests { let items = corpus.entries::("proregtx", check_sptx); assert_serde_rt("proregtx", &items); } + + #[derive(Deserialize)] + struct LayoutVector { + payload: String, + payouts: Vec<(String, u16)>, + shares: Vec<(i64, String, String, String)>, + join_sigs: Vec, + early_period_blocks: u32, + early_penalty: i64, + } + + #[rstest] + fn v24_layout_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proregtx"); + let mut decoded = Vec::new(); + for (i, v) in corpus.vectors::("v24_layout").into_iter().enumerate() { + let raw = vec_from_hex(&v.payload); + let mut cursor = &raw[..]; + let ptx = ProRegTx::decode(&mut cursor).unwrap(); + assert!(cursor.is_empty(), "vector {i}"); + let payouts: Vec<_> = ptx + .payouts() + .iter() + .map(|p| (p.script_payout().as_bytes().to_lower_hex_string(), p.reward())) + .collect(); + assert_eq!(payouts, v.payouts, "vector {i}"); + let shares: Vec<_> = ptx + .shares() + .iter() + .map(|s| { + ( + s.amount(), + s.script_refund().as_bytes().to_lower_hex_string(), + s.script_reward().as_bytes().to_lower_hex_string(), + s.key_id_owner().as_bytes().to_lower_hex_string(), + ) + }) + .collect(); + assert_eq!(shares, v.shares, "vector {i}"); + let join_sigs: Vec<_> = ptx + .join_sigs() + .iter() + .map(|s| s.as_bytes().to_lower_hex_string()) + .collect(); + assert_eq!(join_sigs, v.join_sigs, "vector {i}"); + assert_eq!(ptx.early_period_blocks(), v.early_period_blocks, "vector {i}"); + assert_eq!(ptx.early_penalty(), v.early_penalty, "vector {i}"); + let mut buf = Vec::new(); + ptx.encode(&mut buf); + assert_eq!(buf, raw, "vector {i}"); + decoded.push((format!("{i}"), ptx)); + } + assert_serde_rt("v24_layout", &decoded.into_iter().collect()); + } + + #[derive(Deserialize)] + struct CheckVector { + payload: String, + reason: String, + } + + #[rstest] + fn check_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proregtx"); + for (i, v) in corpus.vectors::("check").into_iter().enumerate() { + let raw = vec_from_hex(&v.payload); + let mut cursor = &raw[..]; + let ptx = ProRegTx::decode(&mut cursor).unwrap(); + assert!(cursor.is_empty(), "vector {i}"); + let reason = ptx.check().map(|e| e.to_string()).unwrap_or_default(); + assert_eq!(reason.split(':').next().unwrap_or_default(), v.reason, "vector {i}"); + } + } + + #[rstest] + #[case::sig_missing( + |ptx: &mut ProRegTx| ptx.join_sigs.truncate(1), + ProTxInvalid::JoinSigCountMismatch { sigs: 1, shares: 2 } + )] + #[case::sig_extra( + |ptx: &mut ProRegTx| ptx.join_sigs.push(ptx.join_sigs[0]), + ProTxInvalid::JoinSigCountMismatch { sigs: 3, shares: 2 } + )] + #[case::sigs_unshared( + |ptx: &mut ProRegTx| ptx.shares.clear(), + ProTxInvalid::JoinSigCountMismatch { sigs: 2, shares: 0 } + )] + #[case::shares_basic_bls( + |ptx: &mut ProRegTx| ptx.version = ProTxVersion::BasicBls, + ProTxInvalid::SharesVersionTooLow { version: ProTxVersion::BasicBls } + )] + fn check_rejects_inconsistent_shares(#[case] edit: fn(&mut ProRegTx), #[case] expected: ProTxInvalid) { + let mut ptx = shared(); + assert_eq!(ptx.version, ProTxVersion::ExtAddr); + assert_eq!(ptx.check(), None); + edit(&mut ptx); + assert_eq!(ptx.check(), Some(expected)); + } + + /// A version 2 payload from the chain. + fn basic_bls() -> ProRegTx { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proregtx"); + let entries = corpus.entries::("proregtx", |_, _, _| {}); + entries + .into_values() + .find(|ptx| ptx.version == ProTxVersion::BasicBls) + .unwrap() + } + + /// The first payload the check vectors accept that `pick` selects. + fn accepted(pick: fn(&ProRegTx) -> bool) -> ProRegTx { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proregtx"); + corpus + .vectors::("check") + .into_iter() + .filter(|v| v.reason.is_empty()) + .map(|v| ProRegTx::decode(&mut &vec_from_hex(&v.payload)[..]).unwrap()) + .find(pick) + .unwrap() + } + + /// A version 3 payload with one payout that the check vectors accept. + fn ext_addr() -> ProRegTx { + accepted(|ptx| ptx.payouts.len() == 1) + } + + /// A version 3 payload with two shares that the check vectors accept. + fn shared() -> ProRegTx { + accepted(|ptx| ptx.shares.len() == 2) + } + + #[rstest] + #[case::script_payout_ext_addr( + ext_addr(), + |ptx: &mut ProRegTx| ptx.script_payout = ptx.payouts[0].script_payout().clone(), + ProTxVersion::ExtAddr + )] + #[case::payouts_basic_bls( + basic_bls(), + |ptx: &mut ProRegTx| { + let script_payout = ptx.script_payout.clone(); + ptx.payouts.push(PayoutShare::new(script_payout, MAX_REWARD)); + }, + ProTxVersion::BasicBls + )] + fn check_rejects_payouts_of_another_version( + #[case] mut ptx: ProRegTx, + #[case] edit: fn(&mut ProRegTx), + #[case] version: ProTxVersion, + ) { + assert_eq!(ptx.version, version); + assert_eq!(ptx.check(), None); + edit(&mut ptx); + assert_eq!(ptx.check(), Some(ProTxInvalid::PayoutVersionMismatch { version })); + } + + /// `payout` with its reward set to `reward`. + fn with_reward(payout: &PayoutShare, reward: u16) -> PayoutShare { + PayoutShare::new(payout.script_payout().clone(), reward) + } + + /// `share` with its amount set to `amount`. + fn with_amount(share: &CollateralShare, amount: i64) -> CollateralShare { + CollateralShare::new( + amount, + share.script_refund().clone(), + share.script_reward().clone(), + share.key_id_owner(), + ) + } + + #[rstest] + #[case::payout_count( + ext_addr(), + |ptx: &mut ProRegTx| ptx.payouts = vec![ptx.payouts[0].clone(); 9], + ProTxInvalid::BadPayoutCount { count: 9 } + )] + #[case::payout_reward( + ext_addr(), + |ptx: &mut ProRegTx| ptx.payouts[0] = with_reward(&ptx.payouts[0], 99), + ProTxInvalid::BadPayoutReward { reward: 99 } + )] + #[case::payout_reward_sum( + accepted(|ptx| ptx.payouts.len() == 2), + |ptx: &mut ProRegTx| ptx.payouts[0] = with_reward(&ptx.payouts[0], ptx.payouts[0].reward() + 1), + ProTxInvalid::BadPayoutRewardSum { total: 10_001 } + )] + #[case::share_count( + shared(), + |ptx: &mut ProRegTx| { + ptx.shares.truncate(1); + ptx.join_sigs.truncate(1); + }, + ProTxInvalid::BadShareCount { count: 1 } + )] + #[case::early_period( + shared(), + |ptx: &mut ProRegTx| ptx.early_period_blocks = 420_481, + ProTxInvalid::BadEarlyPeriod { blocks: 420_481 } + )] + #[case::share_amount( + shared(), + |ptx: &mut ProRegTx| ptx.shares[0] = with_amount(&ptx.shares[0], 1), + ProTxInvalid::BadShareAmount { amount: 1 } + )] + #[case::share_amount_sum( + shared(), + |ptx: &mut ProRegTx| ptx.shares[0] = with_amount(&ptx.shares[0], ptx.shares[0].amount() - 1), + ProTxInvalid::BadShareAmountSum { total: REGULAR_COLLATERAL - 1 } + )] + #[case::early_penalty( + shared(), + |ptx: &mut ProRegTx| ptx.early_penalty = -1, + ProTxInvalid::BadEarlyPenalty { penalty: -1 } + )] + fn check_reports_error_fields( + #[case] mut ptx: ProRegTx, + #[case] edit: fn(&mut ProRegTx), + #[case] expected: ProTxInvalid, + ) { + assert_eq!(ptx.check(), None); + edit(&mut ptx); + assert_eq!(ptx.check(), Some(expected)); + } + + #[derive(Deserialize)] + struct SignStringVector { + payload: String, + sign_string: String, + collateral_key: String, + } + + #[rstest] + fn sign_string_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proregtx"); + for (i, v) in corpus + .vectors::("sign_string") + .into_iter() + .enumerate() + { + let ptx = ProRegTx::decode(&mut &vec_from_hex(&v.payload)[..]).unwrap(); + let key = PubKeyHash::from_lendian(arr_from_hex(&v.collateral_key)); + assert_eq!( + ptx.payload(Network::Main.addr()), + message_digest(v.sign_string.as_bytes()), + "vector {i}" + ); + assert_eq!(ptx.verify(&key, Network::Main.addr()), None, "vector {i}"); + } + } + + #[derive(Deserialize)] + struct RejectedVector { + txid: String, + raw: String, + decodes: bool, + } + + #[rstest] + fn rejected_match_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proregtx"); + for (i, v) in corpus.vectors::("rejected").into_iter().enumerate() { + let raw = vec_from_hex(&v.raw); + let tx = Transaction::decode(&mut &raw[..]).unwrap(); + assert_eq!(tx.hash(), TxHash::from_hex(&v.txid).unwrap(), "vector {i}"); + let mut cursor = &tx.extra_payload()[..]; + let decodes = ProRegTx::decode(&mut cursor).is_ok() && cursor.is_empty(); + assert_eq!(decodes, v.decodes, "vector {i}"); + } + } } diff --git a/pkgs/primitives/src/provider/proupregtx.rs b/pkgs/primitives/src/provider/proupregtx.rs index 7af42a76..91dc86b5 100644 --- a/pkgs/primitives/src/provider/proupregtx.rs +++ b/pkgs/primitives/src/provider/proupregtx.rs @@ -6,28 +6,30 @@ //! ProUpRegTx registrar-update payload (type 3). -use super::{OwnerSig, ProTxVersion}; -use crate::codec::codec_payload; +use super::share::{check_payout_version, check_payouts, MAX_REWARD}; +use super::{OwnerSig, PayoutShare, ProTxVersion}; +use crate::codec::{decode_list, encode_list, impl_payload}; use crate::prelude::*; use crate::sign::{unsigned_digest, verify_compact}; use crate::sptx::{InputsHash, ProTxInvalid}; -use crate::TxHash; +use crate::{hash_impl, TxHash}; use bitcoin_primitives::script::ScriptPubKeyBuf; use dash_pkc::bls::BlsPkBytes; use dash_pkc::ecdsa::{EcdsaError, ECDSA_REC_SIG_LEN}; -use dash_script::{PubKeyHash, Recipient}; +use dash_script::PubKeyHash; +use dash_types::codec::{BaseCodec, DecodeError, EncodeBuf}; use dash_types::type_id::TypeId; use dash_types::{Checkable, Signable}; use getset::{CopyGetters, Getters}; use core::fmt; -codec_payload! { @no_new /// ProUpRegTx -- update MN keys/payout (type 3). /// /// - v1: LegacyBLS /// - v2: BasicBLS +/// - v3: ExtAddr (owner payouts) #[derive(Clone, CopyGetters, Debug, Eq, Getters, Hash, PartialEq, TypeId)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] @@ -47,9 +49,13 @@ pub struct ProUpRegTx { /// Voting key id (20 bytes). #[getset(get_copy = "pub")] key_id_voting: PubKeyHash, - /// Payout script. + /// Payout script (version < 3 only). #[getset(get = "pub")] script_payout: ScriptPubKeyBuf, + /// Owner payouts (version 3). + #[cfg_attr(feature = "serde", serde(default))] + #[getset(get = "pub")] + payouts: Vec, /// Hash of all inputs. #[getset(get_copy = "pub")] inputs_hash: InputsHash, @@ -58,6 +64,49 @@ pub struct ProUpRegTx { #[getset(get = "pub")] sig: OwnerSig, } + +impl_payload!(ProUpRegTx); + +impl BaseCodec for ProUpRegTx { + fn decode(data: &mut &[u8]) -> Result { + let version = ProTxVersion::decode(data)?; + let pro_tx_hash = TxHash::decode(data)?; + let mode = u16::decode(data)?; + let pub_key_operator = BlsPkBytes::decode(data)?; + let key_id_voting = PubKeyHash::decode(data)?; + let (script_payout, payouts) = if version >= ProTxVersion::ExtAddr { + (ScriptPubKeyBuf::new(), decode_list(data)?) + } else { + (ScriptPubKeyBuf::decode(data)?, Vec::new()) + }; + + Ok(Self { + version, + pro_tx_hash, + mode, + pub_key_operator, + key_id_voting, + script_payout, + payouts, + inputs_hash: InputsHash::decode(data)?, + sig: OwnerSig::decode(data)?, + }) + } + + fn encode(&self, buf: &mut impl EncodeBuf) { + self.version.encode(buf); + self.pro_tx_hash.encode(buf); + self.mode.encode(buf); + self.pub_key_operator.encode(buf); + self.key_id_voting.encode(buf); + if self.version >= ProTxVersion::ExtAddr { + encode_list(&self.payouts, buf); + } else { + self.script_payout.encode(buf); + } + self.inputs_hash.encode(buf); + self.sig.encode(buf); + } } impl Signable for ProUpRegTx { @@ -93,6 +142,9 @@ impl Checkable for ProUpRegTx { if self.mode != 0 { return Some(ProTxInvalid::BadMode { mode: self.mode }); } + if let Some(e) = check_payout_version(self.version, &self.script_payout, &self.payouts) { + return Some(e); + } if self.pub_key_operator.is_null() { return Some(ProTxInvalid::NullKey); @@ -101,18 +153,16 @@ impl Checkable for ProUpRegTx { return Some(ProTxInvalid::NullKey); } - let payout = self.script_payout.as_bytes(); - if !matches!( - Recipient::from_script(payout), - Some(Recipient::PubKeyHash(_) | Recipient::ScriptHash(_)) - ) { - return Some(ProTxInvalid::BadPayoutScript); + if self.version >= ProTxVersion::ExtAddr { + return check_payouts(&self.payouts, None, &self.key_id_voting); } - - None + let payout = PayoutShare::new(self.script_payout.clone(), MAX_REWARD); + check_payouts(&[payout], None, &self.key_id_voting) } } +hash_impl!(ProUpRegTx); + impl ProUpRegTx { /// Constructs an unsigned payload. The signature is set through /// [`Signable::set_signature`]. @@ -123,6 +173,7 @@ impl ProUpRegTx { pub_key_operator: BlsPkBytes, key_id_voting: PubKeyHash, script_payout: ScriptPubKeyBuf, + payouts: Vec, inputs_hash: InputsHash, ) -> Self { Self { @@ -132,6 +183,7 @@ impl ProUpRegTx { pub_key_operator, key_id_voting, script_payout, + payouts, inputs_hash, sig: OwnerSig::default(), } @@ -145,11 +197,14 @@ impl fmt::Display for ProUpRegTx { } #[cfg(all(test, feature = "serde"))] +#[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; - use dash_dev::{assert_serde_rt, check_sptx, Corpus}; + use dash_dev::{assert_serde_rt, check_sptx, vec_from_hex, Corpus}; + use hex_conservative::DisplayHex; use rstest::rstest; + use serde::Deserialize; #[rstest] fn corpus_proupregtx() { @@ -157,4 +212,101 @@ mod tests { let items = corpus.entries::("proupregtx", check_sptx); assert_serde_rt("proupregtx", &items); } + + #[derive(Deserialize)] + struct LayoutVector { + payload: String, + payouts: Vec<(String, u16)>, + } + + #[rstest] + fn v24_layout_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proupregtx"); + let mut decoded = Vec::new(); + for (i, v) in corpus.vectors::("v24_layout").into_iter().enumerate() { + let raw = vec_from_hex(&v.payload); + let mut cursor = &raw[..]; + let ptx = ProUpRegTx::decode(&mut cursor).unwrap(); + assert!(cursor.is_empty(), "vector {i}"); + let payouts: Vec<_> = ptx + .payouts() + .iter() + .map(|p| (p.script_payout().as_bytes().to_lower_hex_string(), p.reward())) + .collect(); + assert_eq!(payouts, v.payouts, "vector {i}"); + let mut buf = Vec::new(); + ptx.encode(&mut buf); + assert_eq!(buf, raw, "vector {i}"); + decoded.push((format!("{i}"), ptx)); + } + assert_serde_rt("v24_layout", &decoded.into_iter().collect()); + } + + #[derive(Deserialize)] + struct CheckVector { + version: u16, + payload: String, + reason: String, + } + + #[rstest] + fn check_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proupregtx"); + for (i, v) in corpus.vectors::("check").into_iter().enumerate() { + let raw = vec_from_hex(&v.payload); + let mut cursor = &raw[..]; + let ptx = ProUpRegTx::decode(&mut cursor).unwrap(); + assert!(cursor.is_empty(), "vector {i}"); + assert_eq!(ptx.version().to_base(), v.version, "vector {i}"); + let reason = ptx.check().map(|e| e.to_string()).unwrap_or_default(); + assert_eq!(reason.split(':').next().unwrap_or_default(), v.reason, "vector {i}"); + } + } + + /// A version 2 payload from the chain. + fn basic_bls() -> ProUpRegTx { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proupregtx"); + let entries = corpus.entries::("proupregtx", |_, _, _| {}); + entries + .into_values() + .find(|ptx| ptx.version == ProTxVersion::BasicBls) + .unwrap() + } + + /// A version 3 payload with one payout that the check vectors accept. + fn ext_addr() -> ProUpRegTx { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proupregtx"); + corpus + .vectors::("check") + .into_iter() + .filter(|v| v.reason.is_empty()) + .map(|v| ProUpRegTx::decode(&mut &vec_from_hex(&v.payload)[..]).unwrap()) + .find(|ptx| ptx.payouts.len() == 1) + .unwrap() + } + + #[rstest] + #[case::script_payout_ext_addr( + ext_addr(), + |ptx: &mut ProUpRegTx| ptx.script_payout = ptx.payouts[0].script_payout().clone(), + ProTxVersion::ExtAddr + )] + #[case::payouts_basic_bls( + basic_bls(), + |ptx: &mut ProUpRegTx| { + let script_payout = ptx.script_payout.clone(); + ptx.payouts.push(PayoutShare::new(script_payout, MAX_REWARD)); + }, + ProTxVersion::BasicBls + )] + fn check_rejects_payouts_of_another_version( + #[case] mut ptx: ProUpRegTx, + #[case] edit: fn(&mut ProUpRegTx), + #[case] version: ProTxVersion, + ) { + assert_eq!(ptx.version, version); + assert_eq!(ptx.check(), None); + edit(&mut ptx); + assert_eq!(ptx.check(), Some(ProTxInvalid::PayoutVersionMismatch { version })); + } } diff --git a/pkgs/primitives/src/provider/proupsharedregtx.rs b/pkgs/primitives/src/provider/proupsharedregtx.rs new file mode 100644 index 00000000..5f65a419 --- /dev/null +++ b/pkgs/primitives/src/provider/proupsharedregtx.rs @@ -0,0 +1,210 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! ProUpSharedRegTx shared registrar update payload (type 12). + +use super::share::MAX_SHARES; +use super::CompactSig; +use crate::codec::{decode_list, encode_list, impl_payload}; +use crate::prelude::*; +use crate::sptx::{InputsHash, ProTxInvalid}; +use crate::{hash_impl, TxHash}; + +use dash_pkc::bls::BlsPkBytes; +use dash_script::PubKeyHash; +use dash_types::codec::{BaseCodec, DecodeError, EncodeBuf}; +use dash_types::type_id::TypeId; +use dash_types::{impl_new, Checkable}; +use getset::{CopyGetters, Getters}; + +use core::fmt; + +/// The payload version `check` accepts. +const VERSION: u16 = 1; + +impl_new! { +/// ProUpSharedRegTx -- update the keys of a shared masternode (type 12). +#[derive(Clone, CopyGetters, Debug, Eq, Getters, Hash, PartialEq, TypeId)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct ProUpSharedRegTx { + /// Payload version, read at any value. + #[getset(get_copy = "pub")] + version: u16, + /// ProTx hash identifying the masternode. + #[getset(get_copy = "pub")] + pro_tx_hash: TxHash, + /// Operator BLS public key (48 bytes, basic scheme). + #[getset(get_copy = "pub")] + pub_key_operator: BlsPkBytes, + /// Voting key id (20 bytes). + #[getset(get_copy = "pub")] + key_id_voting: PubKeyHash, + /// Hash of all inputs. + #[getset(get_copy = "pub")] + inputs_hash: InputsHash, + /// Share owner signatures, one per share in share order. + #[getset(get = "pub")] + sigs: Vec, +} +} + +impl_payload!(ProUpSharedRegTx); + +impl BaseCodec for ProUpSharedRegTx { + fn decode(data: &mut &[u8]) -> Result { + Ok(Self { + version: u16::decode(data)?, + pro_tx_hash: TxHash::decode(data)?, + pub_key_operator: BlsPkBytes::decode(data)?, + key_id_voting: PubKeyHash::decode(data)?, + inputs_hash: InputsHash::decode(data)?, + sigs: decode_list(data)?, + }) + } + + fn encode(&self, buf: &mut impl EncodeBuf) { + self.version.encode(buf); + self.pro_tx_hash.encode(buf); + self.pub_key_operator.encode(buf); + self.key_id_voting.encode(buf); + self.inputs_hash.encode(buf); + encode_list(&self.sigs, buf); + } +} + +impl Checkable for ProUpSharedRegTx { + type Error = ProTxInvalid; + + fn check(&self) -> Option { + if self.version != VERSION { + return Some(ProTxInvalid::BadSharedRegistrarVersion { version: self.version }); + } + if self.pub_key_operator.is_null() || self.key_id_voting.is_null() { + return Some(ProTxInvalid::NullKey); + } + if self.sigs.is_empty() || self.sigs.len() > MAX_SHARES { + return Some(ProTxInvalid::BadSharedRegistrarSigCount { count: self.sigs.len() }); + } + None + } +} + +hash_impl!(ProUpSharedRegTx); + +impl fmt::Display for ProUpSharedRegTx { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ProUpSharedRegTx {{ v{} }}", self.version) + } +} + +#[cfg(all(test, feature = "serde"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::sptx::{SpecialPayload, TxType}; + + use dash_dev::{assert_serde_rt, vec_from_hex, Corpus}; + use dash_params::Network; + use hex_conservative::DisplayHex; + use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Json { + version: u16, + pro_tx_hash: String, + voting_address: String, + pub_key_operator: String, + inputs_hash: String, + sig_count: usize, + } + + #[derive(Deserialize)] + struct CheckVector { + payload: String, + json: Json, + reason: String, + } + + #[rstest] + fn check_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proupsharedregtx"); + let mut decoded = Vec::new(); + for (i, v) in corpus.vectors::("check").into_iter().enumerate() { + let raw = vec_from_hex(&v.payload); + let mut cursor = &raw[..]; + let payload = SpecialPayload::decode(TxType::ProviderUpdateSharedRegistrar, &mut cursor).unwrap(); + assert!(cursor.is_empty(), "vector {i}"); + let ptx = ProUpSharedRegTx::decode(&mut &raw[..]).unwrap(); + assert_eq!( + payload, + SpecialPayload::ProviderUpdateSharedRegistrar(ptx.clone()), + "vector {i}" + ); + assert_eq!(ptx.version(), v.json.version, "vector {i}"); + assert_eq!( + ptx.pro_tx_hash(), + TxHash::from_hex(&v.json.pro_tx_hash).unwrap(), + "vector {i}" + ); + assert_eq!( + ptx.key_id_voting().to_base58c(Network::Main.addr().pubkey_addr), + v.json.voting_address, + "vector {i}" + ); + assert_eq!( + ptx.pub_key_operator().as_bytes().to_lower_hex_string(), + v.json.pub_key_operator, + "vector {i}" + ); + assert_eq!( + ptx.inputs_hash(), + InputsHash::from_hex(&v.json.inputs_hash).unwrap(), + "vector {i}" + ); + assert_eq!(ptx.sigs().len(), v.json.sig_count, "vector {i}"); + let reason = payload.check().map(|e| e.to_string()).unwrap_or_default(); + assert_eq!(reason.split(':').next().unwrap_or_default(), v.reason, "vector {i}"); + let mut buf = Vec::new(); + ptx.encode(&mut buf); + assert_eq!(buf, raw, "vector {i}"); + decoded.push((format!("{i}"), ptx)); + } + assert_serde_rt("check", &decoded.into_iter().collect()); + } + + #[rstest] + #[case::version_0("", 0, Some(ProTxInvalid::BadSharedRegistrarVersion { version: 0 }))] + #[case::version_1("", 1, None)] + #[case::version_2("", 2, Some(ProTxInvalid::BadSharedRegistrarVersion { version: 2 }))] + #[case::version_2_before_key_null( + "bad-protx-key-null", + 2, + Some(ProTxInvalid::BadSharedRegistrarVersion { version: 2 }) + )] + fn check_requires_version_1(#[case] reason: &str, #[case] version: u16, #[case] expected: Option) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proupsharedregtx"); + let vector = corpus + .vectors::("check") + .into_iter() + .find(|v| v.reason == reason) + .unwrap(); + let mut raw = vec_from_hex(&vector.payload); + raw[..2].copy_from_slice(&version.to_le_bytes()); + let mut cursor = &raw[..]; + let payload = SpecialPayload::decode(TxType::ProviderUpdateSharedRegistrar, &mut cursor).unwrap(); + assert!(cursor.is_empty()); + let ptx = ProUpSharedRegTx::decode(&mut &raw[..]).unwrap(); + assert_eq!(payload, SpecialPayload::ProviderUpdateSharedRegistrar(ptx.clone())); + assert_eq!(ptx.version(), version); + assert_eq!(ptx.check(), expected); + let mut buf = Vec::new(); + ptx.encode(&mut buf); + assert_eq!(buf, raw); + } +} diff --git a/pkgs/primitives/src/provider/proupsharetx.rs b/pkgs/primitives/src/provider/proupsharetx.rs new file mode 100644 index 00000000..be9b06ac --- /dev/null +++ b/pkgs/primitives/src/provider/proupsharetx.rs @@ -0,0 +1,172 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! ProUpShareTx share reward update payload (type 11). + +use super::share::payee; +use crate::codec::codec_payload; +use crate::prelude::*; +use crate::sptx::{InputsHash, ProTxInvalid}; +use crate::TxHash; + +use bitcoin_primitives::script::ScriptPubKeyBuf; +use dash_pkc::ecdsa::ECDSA_REC_SIG_LEN; +use dash_types::type_id::TypeId; +use dash_types::Checkable; +use getset::{CopyGetters, Getters}; + +use core::fmt; + +/// The payload version `check` accepts. +const VERSION: u16 = 1; + +codec_payload! { +/// ProUpShareTx -- update the reward script of a share (type 11). +#[derive(Clone, CopyGetters, Debug, Eq, Getters, Hash, PartialEq, TypeId)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct ProUpShareTx { + /// Payload version, read at any value. + #[getset(get_copy = "pub")] + version: u16, + /// ProTx hash identifying the masternode. + #[getset(get_copy = "pub")] + pro_tx_hash: TxHash, + /// Index of the share. + #[getset(get_copy = "pub")] + share_index: u16, + /// Script the share's rewards are paid to. + #[getset(get = "pub")] + script_reward: ScriptPubKeyBuf, + /// Hash of all inputs. + #[getset(get_copy = "pub")] + inputs_hash: InputsHash, + /// Share owner ECDSA signature, read at any length. + #[cfg_attr(feature = "serde", serde(rename = "vchSig", with = "dash_types::serialize::hex"))] + #[getset(get = "pub")] + sig: Vec, +} +} + +impl Checkable for ProUpShareTx { + type Error = ProTxInvalid; + + fn check(&self) -> Option { + if self.version != VERSION { + return Some(ProTxInvalid::BadShareUpdateVersion { version: self.version }); + } + if self.sig.len() != ECDSA_REC_SIG_LEN { + return Some(ProTxInvalid::BadShareUpdateSigSize { size: self.sig.len() }); + } + if payee(&self.script_reward).is_none() { + return Some(ProTxInvalid::BadShareUpdateScript); + } + None + } +} + +impl fmt::Display for ProUpShareTx { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ProUpShareTx {{ v{} }}", self.version) + } +} + +#[cfg(all(test, feature = "serde"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::sptx::{SpecialPayload, TxType}; + + use dash_dev::{assert_serde_rt, vec_from_hex, Corpus}; + use dash_params::Network; + use dash_script::Recipient; + use dash_types::codec::BaseCodec; + use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Json { + version: u16, + pro_tx_hash: String, + share_index: u16, + reward_address: Option, + inputs_hash: String, + } + + #[derive(Deserialize)] + struct CheckVector { + payload: String, + json: Json, + reason: String, + } + + #[rstest] + fn check_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proupsharetx"); + let mut decoded = Vec::new(); + for (i, v) in corpus.vectors::("check").into_iter().enumerate() { + let raw = vec_from_hex(&v.payload); + let mut cursor = &raw[..]; + let payload = SpecialPayload::decode(TxType::ProviderUpdateShare, &mut cursor).unwrap(); + assert!(cursor.is_empty(), "vector {i}"); + let ptx = ProUpShareTx::decode(&mut &raw[..]).unwrap(); + assert_eq!(payload, SpecialPayload::ProviderUpdateShare(ptx.clone()), "vector {i}"); + assert_eq!(ptx.version(), v.json.version, "vector {i}"); + assert_eq!( + ptx.pro_tx_hash(), + TxHash::from_hex(&v.json.pro_tx_hash).unwrap(), + "vector {i}" + ); + assert_eq!(ptx.share_index(), v.json.share_index, "vector {i}"); + let address = + Recipient::from_script(ptx.script_reward().as_bytes()).and_then(|r| r.to_base58c(Network::Main.addr())); + assert_eq!(address, v.json.reward_address, "vector {i}"); + assert_eq!( + ptx.inputs_hash(), + InputsHash::from_hex(&v.json.inputs_hash).unwrap(), + "vector {i}" + ); + let reason = payload.check().map(|e| e.to_string()).unwrap_or_default(); + assert_eq!(reason.split(':').next().unwrap_or_default(), v.reason, "vector {i}"); + let mut buf = Vec::new(); + ptx.encode(&mut buf); + assert_eq!(buf, raw, "vector {i}"); + decoded.push((format!("{i}"), ptx)); + } + assert_serde_rt("check", &decoded.into_iter().collect()); + } + + #[rstest] + #[case::version_0("", 0, Some(ProTxInvalid::BadShareUpdateVersion { version: 0 }))] + #[case::version_1("", 1, None)] + #[case::version_2("", 2, Some(ProTxInvalid::BadShareUpdateVersion { version: 2 }))] + #[case::version_2_before_sig_size( + "bad-proupshare-sig-size", + 2, + Some(ProTxInvalid::BadShareUpdateVersion { version: 2 }) + )] + fn check_requires_version_1(#[case] reason: &str, #[case] version: u16, #[case] expected: Option) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proupsharetx"); + let vector = corpus + .vectors::("check") + .into_iter() + .find(|v| v.reason == reason) + .unwrap(); + let mut raw = vec_from_hex(&vector.payload); + raw[..2].copy_from_slice(&version.to_le_bytes()); + let mut cursor = &raw[..]; + let payload = SpecialPayload::decode(TxType::ProviderUpdateShare, &mut cursor).unwrap(); + assert!(cursor.is_empty()); + let ptx = ProUpShareTx::decode(&mut &raw[..]).unwrap(); + assert_eq!(payload, SpecialPayload::ProviderUpdateShare(ptx.clone())); + assert_eq!(ptx.version(), version); + assert_eq!(ptx.check(), expected); + let mut buf = Vec::new(); + ptx.encode(&mut buf); + assert_eq!(buf, raw); + } +} diff --git a/pkgs/primitives/src/provider/share.rs b/pkgs/primitives/src/provider/share.rs new file mode 100644 index 00000000..fadcab0e --- /dev/null +++ b/pkgs/primitives/src/provider/share.rs @@ -0,0 +1,201 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Owner payouts and collateral shares of provider payloads. + +use super::ProTxVersion; +use crate::codec_type; +use crate::sptx::ProTxInvalid; + +use bitcoin_primitives::script::ScriptPubKeyBuf; +use bitcoin_units::SignedAmount; +use dash_script::{PubKeyHash, Recipient}; +use dash_types::type_id::TypeId; +use getset::{CopyGetters, Getters}; + +use core::iter; + +/// Fewest shares of a shared collateral. +const MIN_SHARES: usize = 2; + +/// Most shares of a shared collateral, and most owner payouts. +pub(super) const MAX_SHARES: usize = 8; + +/// Smallest part of the owner reward a payout may take, in basis points. +const MIN_REWARD: u16 = 100; + +/// The whole owner reward in basis points. +pub(super) const MAX_REWARD: u16 = 10_000; + +/// Smallest collateral share in duffs. +const MIN_SHARE_AMOUNT: i64 = SignedAmount::from_btc_i16(100).to_sat(); + +/// Longest early dissolution period in blocks. +const MAX_EARLY_PERIOD_BLOCKS: u32 = 420_480; + +/// Collateral of a regular masternode in duffs. +pub(super) const REGULAR_COLLATERAL: i64 = SignedAmount::from_btc_i16(1000).to_sat(); + +/// Collateral of an Evo masternode in duffs. +pub(super) const EVO_COLLATERAL: i64 = SignedAmount::from_btc_i16(4000).to_sat(); + +codec_type! { +/// An owner payout, a script and its part of the owner reward. +#[derive(Clone, CopyGetters, Debug, Eq, Getters, Hash, PartialEq, TypeId)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct PayoutShare { + /// Payout script. + #[getset(get = "pub")] + script_payout: ScriptPubKeyBuf, + /// Part of the owner reward in basis points. + #[getset(get_copy = "pub")] + reward: u16, +} +} + +codec_type! { +/// One participant's part of a shared collateral. +#[derive(Clone, CopyGetters, Debug, Eq, Getters, Hash, PartialEq, TypeId)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct CollateralShare { + /// Amount in duffs. + #[getset(get_copy = "pub")] + amount: i64, + /// Script the amount is refunded to. + #[getset(get = "pub")] + script_refund: ScriptPubKeyBuf, + /// Script rewards are paid to, the refund script when empty. + #[getset(get = "pub")] + script_reward: ScriptPubKeyBuf, + /// Owner key id of the participant (20 bytes). + #[getset(get_copy = "pub")] + key_id_owner: PubKeyHash, +} +} + +/// Reads a payout script, which must pay to a key hash or a script hash. +pub(super) fn payee(script: &ScriptPubKeyBuf) -> Option { + Recipient::from_script(script.as_bytes()) + .filter(|recipient| matches!(recipient, Recipient::PubKeyHash(_) | Recipient::ScriptHash(_))) +} + +/// Checks owner payouts that must not pay the owner or voting key. +pub(super) fn check_payouts( + payouts: &[PayoutShare], + key_id_owner: Option<&PubKeyHash>, + key_id_voting: &PubKeyHash, +) -> Option { + if payouts.is_empty() || payouts.len() > MAX_SHARES { + return Some(ProTxInvalid::BadPayoutCount { count: payouts.len() }); + } + + let mut total = 0u32; + for (i, payout) in payouts.iter().enumerate() { + if !(MIN_REWARD..=MAX_REWARD).contains(&payout.reward) { + return Some(ProTxInvalid::BadPayoutReward { reward: payout.reward }); + } + total += u32::from(payout.reward); + + let Some(recipient) = payee(&payout.script_payout) else { + return Some(ProTxInvalid::BadPayoutScript); + }; + if payouts[..i].iter().any(|p| p.script_payout == payout.script_payout) { + return Some(ProTxInvalid::DuplicatePayout); + } + if let Recipient::PubKeyHash(hash) = recipient { + if key_id_owner == Some(&hash) || hash == *key_id_voting { + return Some(ProTxInvalid::PayoutKeyReuse); + } + } + } + + if total != u32::from(MAX_REWARD) { + return Some(ProTxInvalid::BadPayoutRewardSum { total }); + } + None +} + +/// Checks that only the payout form of `version` is set. Version 3 carries the +/// payout list, and earlier versions the payout script. +pub(super) fn check_payout_version( + version: ProTxVersion, + script_payout: &ScriptPubKeyBuf, + payouts: &[PayoutShare], +) -> Option { + let stray = if version >= ProTxVersion::ExtAddr { + !script_payout.is_empty() + } else { + !payouts.is_empty() + }; + if stray { + return Some(ProTxInvalid::PayoutVersionMismatch { version }); + } + None +} + +/// Checks the shares of a `collateral` and the early dissolution terms. No +/// share may pay the voting key or any share's owner key. +pub(super) fn check_shares( + shares: &[CollateralShare], + collateral: i64, + early_period_blocks: u32, + early_penalty: i64, + key_id_voting: &PubKeyHash, +) -> Option { + if !(MIN_SHARES..=MAX_SHARES).contains(&shares.len()) { + return Some(ProTxInvalid::BadShareCount { count: shares.len() }); + } + if early_period_blocks > MAX_EARLY_PERIOD_BLOCKS { + return Some(ProTxInvalid::BadEarlyPeriod { + blocks: early_period_blocks, + }); + } + + let mut total = 0i64; + let mut min_amount = i64::MAX; + for (i, share) in shares.iter().enumerate() { + // Each amount is at most the collateral, so the total cannot overflow. + if !(MIN_SHARE_AMOUNT..=collateral).contains(&share.amount) { + return Some(ProTxInvalid::BadShareAmount { amount: share.amount }); + } + total += share.amount; + min_amount = min_amount.min(share.amount); + + if share.key_id_owner.is_null() { + return Some(ProTxInvalid::NullShareKey); + } + let earlier = &shares[..i]; + if earlier.iter().any(|s| s.key_id_owner == share.key_id_owner) { + return Some(ProTxInvalid::DuplicateShareKey); + } + if earlier.iter().any(|s| s.script_refund == share.script_refund) { + return Some(ProTxInvalid::DuplicateShareRefund); + } + + // An empty reward script leaves rewards to the refund script. + let reward = Some(&share.script_reward).filter(|script| !script.is_empty()); + for script in iter::once(&share.script_refund).chain(reward) { + let Some(recipient) = payee(script) else { + return Some(ProTxInvalid::BadShareScript); + }; + if let Recipient::PubKeyHash(hash) = recipient { + if hash == *key_id_voting || shares.iter().any(|s| s.key_id_owner == hash) { + return Some(ProTxInvalid::ShareKeyReuse); + } + } + } + } + + if total != collateral { + return Some(ProTxInvalid::BadShareAmountSum { total }); + } + if !(0..min_amount).contains(&early_penalty) || (early_period_blocks == 0 && early_penalty != 0) { + return Some(ProTxInvalid::BadEarlyPenalty { penalty: early_penalty }); + } + None +} diff --git a/pkgs/primitives/src/provider/sig.rs b/pkgs/primitives/src/provider/sig.rs index 350a5398..4ee83ef2 100644 --- a/pkgs/primitives/src/provider/sig.rs +++ b/pkgs/primitives/src/provider/sig.rs @@ -7,6 +7,7 @@ //! Provider transaction signatures. use super::ProTxVersion; +use crate::hash_impl; use crate::prelude::*; use crate::sign::verify_bls; @@ -18,7 +19,7 @@ use dash_types::codec::{BaseCodec, DecodeError, EncodeBuf}; #[cfg(feature = "serde")] use dash_types::serialize::hex as serde_hex; use dash_types::type_id::TypeId; -use dash_types::{CompactSize, Hashable, Numeric}; +use dash_types::{make_bytes, CompactSize, Hashable, Numeric}; #[cfg(feature = "serde")] use serde::de::Error as _; #[cfg(feature = "serde")] @@ -200,6 +201,13 @@ impl Default for OperatorSig { } } +make_bytes! { + /// Compact recoverable ECDSA signature, written without a length. + CompactSig, ECDSA_REC_SIG_LEN +} + +hash_impl!(CompactSig); + #[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { diff --git a/pkgs/primitives/src/sptx.rs b/pkgs/primitives/src/sptx.rs index b2b6ef4a..43b339e4 100644 --- a/pkgs/primitives/src/sptx.rs +++ b/pkgs/primitives/src/sptx.rs @@ -12,7 +12,9 @@ use crate::payload::{ FinalCommitment, MnHardFork, MnHardForkInvalid, }; use crate::prelude::*; -use crate::provider::{ProRegTx, ProTxVersion, ProUpRegTx, ProUpRevTx, ProUpServTx}; +use crate::provider::{ + ProDisTx, ProRegTx, ProTxVersion, ProUpRegTx, ProUpRevTx, ProUpServTx, ProUpShareTx, ProUpSharedRegTx, +}; use crate::types::{NIError, NIPurpose, NITrait, NetInfoV2}; use dash_num::make_hash; @@ -61,6 +63,12 @@ enum_map! { AssetLock = 8 => "asset_lock", /// Asset unlock: platform to L1 (type 9). AssetUnlock = 9 => "asset_unlock", + /// Shared masternode dissolution (type 10). + ProviderDissolve = 10 => "provider_dissolve", + /// Share reward script update (type 11). + ProviderUpdateShare = 11 => "provider_update_share", + /// Shared masternode registrar update (type 12). + ProviderUpdateSharedRegistrar = 12 => "provider_update_shared_registrar", } } @@ -119,6 +127,90 @@ pub enum ProTxInvalid { /// `bad-protx-payee-reuse` #[error("bad-protx-payee-reuse")] PayoutKeyReuse, + /// `bad-protx-payouts-count` + #[error("bad-protx-payouts-count: {count}")] + BadPayoutCount { count: usize }, + /// `bad-protx-payout-reward` + #[error("bad-protx-payout-reward: {reward}")] + BadPayoutReward { reward: u16 }, + /// `bad-protx-payee-dup` + #[error("bad-protx-payee-dup")] + DuplicatePayout, + /// `bad-protx-payout-reward-sum` + #[error("bad-protx-payout-reward-sum: {total}")] + BadPayoutRewardSum { total: u32 }, + /// `bad-protx-shares-external` + #[error("bad-protx-shares-external")] + ExternalSharedCollateral, + /// `bad-protx-shares-owner-key` + #[error("bad-protx-shares-owner-key")] + SharedOwnerKey, + /// `bad-protx-shares-payouts` + #[error("bad-protx-shares-payouts")] + SharedPayouts, + /// `bad-protx-shares-empty-fields` + #[error("bad-protx-shares-empty-fields")] + UnsharedEarlyTerms, + /// `bad-protx-shares-count` + #[error("bad-protx-shares-count: {count}")] + BadShareCount { count: usize }, + /// `bad-protx-shares-early-period` + #[error("bad-protx-shares-early-period: {blocks}")] + BadEarlyPeriod { blocks: u32 }, + /// `bad-protx-shares-amount` + #[error("bad-protx-shares-amount: {amount}")] + BadShareAmount { amount: i64 }, + /// `bad-protx-shares-key-null` + #[error("bad-protx-shares-key-null")] + NullShareKey, + /// `bad-protx-shares-dup-key` + #[error("bad-protx-shares-dup-key")] + DuplicateShareKey, + /// `bad-protx-shares-dup-refund` + #[error("bad-protx-shares-dup-refund")] + DuplicateShareRefund, + /// `bad-protx-shares-payee` + #[error("bad-protx-shares-payee")] + BadShareScript, + /// `bad-protx-shares-payee-reuse` + #[error("bad-protx-shares-payee-reuse")] + ShareKeyReuse, + /// `bad-protx-shares-amount-sum` + #[error("bad-protx-shares-amount-sum: {total}")] + BadShareAmountSum { total: i64 }, + /// `bad-protx-shares-penalty` + #[error("bad-protx-shares-penalty: {penalty}")] + BadEarlyPenalty { penalty: i64 }, + /// The join signatures are not one per share. + #[error("join signature count {sigs} does not match share count {shares}")] + JoinSigCountMismatch { sigs: usize, shares: usize }, + /// Shares in a version that does not carry them. + #[error("shares in version {version}")] + SharesVersionTooLow { version: ProTxVersion }, + /// A payout form that the version does not carry. + #[error("payout form does not match version {version}")] + PayoutVersionMismatch { version: ProTxVersion }, + /// `bad-prodis-version` + #[error("bad-prodis-version: {version}")] + BadDissolveVersion { version: u16 }, + /// `bad-prodis-sig-count` + #[error("bad-prodis-sig-count: {count}")] + BadDissolveSigCount { count: usize }, + /// `bad-proupshare-version` + #[error("bad-proupshare-version: {version}")] + BadShareUpdateVersion { version: u16 }, + /// `bad-proupshare-sig-size` + #[error("bad-proupshare-sig-size: {size}")] + BadShareUpdateSigSize { size: usize }, + /// `bad-proupshare-payee` + #[error("bad-proupshare-payee")] + BadShareUpdateScript, + /// `bad-proupsharedreg-version` + #[error("bad-proupsharedreg-version: {version}")] + BadSharedRegistrarVersion { version: u16 }, + /// `bad-proupsharedreg-sig-count` + #[error("bad-proupsharedreg-sig-count: {count}")] + BadSharedRegistrarSigCount { count: usize }, /// `bad-protx-operator-reward` #[error("bad-protx-operator-reward: {reward}")] OperatorRewardTooHigh { reward: u16 }, @@ -179,6 +271,12 @@ pub enum SpecialPayload { AssetLock(AssetLock), /// Asset unlock: Platform to L1 (type 9). AssetUnlock(AssetUnlock), + /// Shared masternode dissolution (type 10). + ProviderDissolve(ProDisTx), + /// Share reward script update (type 11). + ProviderUpdateShare(ProUpShareTx), + /// Shared masternode registrar update (type 12). + ProviderUpdateSharedRegistrar(ProUpSharedRegTx), /// Unrecognized type, stored as raw bytes. Unknown { /// The transaction type code. @@ -242,6 +340,9 @@ impl Checkable for SpecialPayload { Self::MnhfSignal(p) => p.check().map(PayloadInvalid::MnHardFork), Self::AssetLock(p) => p.check().map(PayloadInvalid::AssetLock), Self::AssetUnlock(p) => p.check().map(PayloadInvalid::AssetUnlock), + Self::ProviderDissolve(p) => p.check().map(PayloadInvalid::ProTx), + Self::ProviderUpdateShare(p) => p.check().map(PayloadInvalid::ProTx), + Self::ProviderUpdateSharedRegistrar(p) => p.check().map(PayloadInvalid::ProTx), Self::Unknown { .. } => None, } } @@ -284,6 +385,11 @@ impl SpecialPayload { TxType::MnhfSignal => MnHardFork::decode(data).map(Self::MnhfSignal).map_err(err), TxType::AssetLock => AssetLock::decode(data).map(Self::AssetLock).map_err(err), TxType::AssetUnlock => AssetUnlock::decode(data).map(Self::AssetUnlock).map_err(err), + TxType::ProviderDissolve => ProDisTx::decode(data).map(Self::ProviderDissolve).map_err(err), + TxType::ProviderUpdateShare => ProUpShareTx::decode(data).map(Self::ProviderUpdateShare).map_err(err), + TxType::ProviderUpdateSharedRegistrar => ProUpSharedRegTx::decode(data) + .map(Self::ProviderUpdateSharedRegistrar) + .map_err(err), unknown => { let bytes = data.to_vec(); *data = &[]; diff --git a/pkgs/primitives/src/transaction.rs b/pkgs/primitives/src/transaction.rs index 91435b58..1192e51a 100644 --- a/pkgs/primitives/src/transaction.rs +++ b/pkgs/primitives/src/transaction.rs @@ -14,7 +14,7 @@ use crate::{codec_type, hash_impl}; use bitcoin_hashes::sha256d; use bitcoin_primitives::script::{ScriptPubKeyBuf, ScriptSigBuf}; -use bitcoin_units::Amount; +use bitcoin_units::SignedAmount; use dash_num::make_hash; use dash_types::codec::{self, BaseCodec, DecodeError, EncodeBuf}; use dash_types::type_id::{TypeId, Unencodable}; @@ -29,6 +29,13 @@ pub const MAX_TX_EXTRA_PAYLOAD: usize = 10_000; /// Maximum coinbase script size in bytes. pub const MAX_COINBASE_SCRIPT_SIZE: usize = 100; +/// Size of the quorum signing info that ends an asset unlock payload, made +/// of the requested height, quorum hash and quorum signature. +const ASSET_UNLOCK_QUORUM_INFO_SIZE: usize = 4 + 32 + 96; + +/// Size of an asset unlock payload. +const ASSET_UNLOCK_PAYLOAD_SIZE: usize = 1 + 8 + 4 + ASSET_UNLOCK_QUORUM_INFO_SIZE; + make_hash! { /// SHA256d hash of a serialized transaction. TxHash, 32 @@ -89,16 +96,16 @@ impl fmt::Display for TxIn { } } -impl_new! { +codec_type! { /// A transaction output. #[derive(Clone, CopyGetters, Debug, Eq, Getters, Hash, PartialEq, TypeId)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct TxOut { - /// Output value in duffs. - #[cfg_attr(feature = "serde", serde(with = "crate::serialize::amount"))] + /// Output value in duffs. Any value decodes; the transaction check rejects + /// one outside `0..=MAX_MONEY`. #[getset(get_copy = "pub")] - value: Amount, + value: i64, /// Locking script. #[cfg_attr(feature = "serde", serde(rename = "scriptPubKey"))] #[getset(get = "pub")] @@ -106,35 +113,17 @@ pub struct TxOut { } } -impl_type!(TxOut); - -impl BaseCodec for TxOut { - fn decode(data: &mut &[u8]) -> Result { - let raw = u64::decode(data)?; - let value = Amount::from_sat(raw).map_err(|_| DecodeError::InvalidValue { - expected: vec![Amount::MAX_MONEY.to_sat()], - actual: raw, - })?; - Ok(Self { - value, - script_pubkey: ScriptPubKeyBuf::decode(data)?, - }) - } - - fn encode(&self, buf: &mut impl EncodeBuf) { - self.value.to_sat().encode(buf); - self.script_pubkey.encode(buf); - } -} - -hash_impl!(TxOut); - impl fmt::Display for TxOut { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "TxOut {{ value: {} }}", self.value.to_sat()) + write!(f, "TxOut {{ value: {} }}", self.value) } } +/// Returns `true` for a value in `0..=MAX_MONEY`. +pub(crate) fn money_range(value: i64) -> bool { + (0..=SignedAmount::MAX_MONEY.to_sat()).contains(&value) +} + /// Transaction validation failure. #[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error, Unencodable)] pub enum TxInvalid { @@ -150,12 +139,15 @@ pub enum TxInvalid { /// `bad-txns-payload-oversize` #[error("bad-txns-payload-oversize: {size} bytes")] PayloadOversize { size: usize }, + /// `bad-txns-vout-negative` + #[error("bad-txns-vout-negative: output {index} value {value}")] + OutputNegative { index: usize, value: i64 }, /// `bad-txns-vout-toolarge` #[error("bad-txns-vout-toolarge: output {index} value {value}")] - OutputTooLarge { index: usize, value: u64 }, + OutputTooLarge { index: usize, value: i64 }, /// `bad-txns-txouttotal-toolarge` #[error("bad-txns-txouttotal-toolarge: {total}")] - OutputTotalTooLarge { total: u64 }, + OutputTotalTooLarge { total: i64 }, /// `bad-txns-inputs-duplicate` #[error("bad-txns-inputs-duplicate: {outpoint}")] DuplicateInputs { outpoint: OutPoint }, @@ -275,15 +267,17 @@ impl Checkable for Transaction { }); } - let max_money = bitcoin_units::Amount::MAX_MONEY.to_sat(); - let mut total: u64 = 0; + let mut total: i64 = 0; for (i, output) in self.outputs.iter().enumerate() { - let sat = output.value.to_sat(); - if sat > max_money { - return Some(TxInvalid::OutputTooLarge { index: i, value: sat }); + let value = output.value; + if value < 0 { + return Some(TxInvalid::OutputNegative { index: i, value }); } - total = total.saturating_add(sat); - if total > max_money { + if !money_range(value) { + return Some(TxInvalid::OutputTooLarge { index: i, value }); + } + total += value; + if !money_range(total) { return Some(TxInvalid::OutputTotalTooLarge { total }); } } @@ -339,9 +333,15 @@ impl Checkable for Transaction { impl Hashable for Transaction { type Hash = TxHash; + /// Returns the txid. An asset unlock with a stable txid is hashed with its + /// quorum signing info zeroed; see [`Transaction::has_stable_txid`]. fn hash(&self) -> TxHash { let mut buf = Vec::new(); self.encode(&mut buf); + if self.has_stable_txid() { + let start = buf.len() - ASSET_UNLOCK_QUORUM_INFO_SIZE; + buf[start..].fill(0); + } TxHash::from_lendian(sha256d::Hash::hash(&buf).to_byte_array()) } } @@ -354,6 +354,27 @@ impl Transaction { v | t } + /// Returns the hash of the whole serialization. It differs from the txid + /// only for an asset unlock with a stable txid. + pub fn instance_hash(&self) -> TxHash { + let mut buf = Vec::new(); + self.encode(&mut buf); + TxHash::from_lendian(sha256d::Hash::hash(&buf).to_byte_array()) + } + + /// Returns `true` for an asset unlock whose payload has the size of a valid + /// one and a version of at least 2. + /// + /// Its txid leaves out the quorum signing info, so re-signing it keeps the + /// txid. Only the payload size and first byte are read, before any + /// validation. + pub fn has_stable_txid(&self) -> bool { + self.has_extra_payload() + && self.tx_type == TxType::AssetUnlock + && self.extra_payload.len() == ASSET_UNLOCK_PAYLOAD_SIZE + && self.extra_payload[0] >= 2 + } + /// Returns true when this transaction carries an extra payload. fn has_extra_payload(&self) -> bool { self.version >= 3 && self.tx_type != TxType::Spend @@ -409,7 +430,7 @@ impl fmt::Display for Transaction { pub(crate) mod tests { use super::*; - use dash_dev::{assert_serde_rt, check_tx, Corpus}; + use dash_dev::{assert_serde_rt, check_tx, vec_from_hex, Corpus}; use rstest::rstest; use serde::Deserialize; @@ -429,7 +450,7 @@ pub(crate) mod tests { tx_type: u16, inputs: Vec, script_sig: usize, - values: Vec, + values: Vec, pad: usize, payload: usize, size: usize, @@ -447,7 +468,7 @@ pub(crate) mod tests { sequence: u32::MAX, }); let outputs = self.values.iter().enumerate().map(|(i, &value)| TxOut { - value: Amount::from_sat(value).unwrap(), + value, script_pubkey: ScriptPubKeyBuf::from_bytes(vec![0; if i == 0 { self.pad } else { 0 }]), }); Transaction { @@ -478,7 +499,34 @@ pub(crate) mod tests { let mut buf = Vec::new(); tx.encode(&mut buf); assert_eq!(buf.len(), v.size, "vector {i}"); + assert_eq!(Transaction::decode(&mut &buf[..]).unwrap(), tx, "vector {i}"); assert_eq!(reason(tx.check()), v.reason, "vector {i}"); } } + + #[derive(Deserialize)] + struct StableTxidVector { + raw: String, + payload: String, + txid: TxHash, + instance: TxHash, + stable: bool, + } + + #[rstest] + fn stable_txid_matches_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "assetunlock"); + for (i, v) in corpus + .vectors::("stable_txid") + .into_iter() + .enumerate() + { + let raw = vec_from_hex(&v.raw); + let mut tx = Transaction::decode(&mut &raw[..]).unwrap(); + tx.extra_payload = vec_from_hex(&v.payload); + assert_eq!(tx.hash(), v.txid, "vector {i}"); + assert_eq!(tx.instance_hash(), v.instance, "vector {i}"); + assert_eq!(tx.has_stable_txid(), v.stable, "vector {i}"); + } + } }