From f00598f8c9c6a80c8628c6d692e0719d4044f2db Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 22 Oct 2026 19:11:47 +0000 Subject: [PATCH 01/15] primitives%fix!: decode output values as signed amounts An output value that is negative or above the money range failed to decode. The reference reads any amount and rejects it in the check, so `TxOut::value` is an `i64` and the negative value has its own reason. --- docs/samples/solver/solver.rs | 5 +- pkgs/primitives/corpus/assetlock.json5 | 76 +++++++++++++++++++++++- pkgs/primitives/corpus/tx_check.json5 | 25 ++++++++ pkgs/primitives/src/block.rs | 8 +-- pkgs/primitives/src/payload/assetlock.rs | 42 +++++++++---- pkgs/primitives/src/transaction.rs | 68 +++++++++------------ 6 files changed, 166 insertions(+), 58 deletions(-) diff --git a/docs/samples/solver/solver.rs b/docs/samples/solver/solver.rs index fdb3b9c58..75fbdcd79 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/pkgs/primitives/corpus/assetlock.json5 b/pkgs/primitives/corpus/assetlock.json5 index 446bf6ff0..fa92c3c68 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,68 @@ ] } } - } + }, +/* +BOOST_AUTO_TEST_CASE(assetlock_check) +{ + const bool is_v24_active{true}; + const CScript p2pkh{GetScriptForDestination(PKHash{uint160{}})}; + 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}}}, + }) { + 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" }, + ], } diff --git a/pkgs/primitives/corpus/tx_check.json5 b/pkgs/primitives/corpus/tx_check.json5 index acf1c3c26..51351cb50 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 f3100dcfa..ce262fea3 100644 --- a/pkgs/primitives/src/block.rs +++ b/pkgs/primitives/src/block.rs @@ -254,7 +254,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, @@ -285,7 +285,6 @@ mod tests { use crate::transaction::{OutPoint, TxIn, TxOut}; use bitcoin_primitives::script::{ScriptPubKeyBuf, ScriptSigBuf}; - use bitcoin_units::Amount; use dash_dev::{assert_serde_rt, check_wire, Corpus}; use dash_script::Opcode; use rstest::rstest; @@ -331,10 +330,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![], ) diff --git a/pkgs/primitives/src/payload/assetlock.rs b/pkgs/primitives/src/payload/assetlock.rs index 9264132c8..103b92821 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}; @@ -61,17 +61,12 @@ 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 { - return Some(AssetLockInvalid::CreditOutOfRange { index: i }); - } - total = total.saturating_add(sat); - if total > max_money { + if out.value() == 0 || !money_range(out.value()) || !money_range(total + out.value()) { return Some(AssetLockInvalid::CreditOutOfRange { index: i }); } + total += out.value(); if !matches!( Recipient::from_script(out.script_pubkey().as_bytes()), Some(Recipient::PubKeyHash(_)) @@ -96,11 +91,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 +106,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/transaction.rs b/pkgs/primitives/src/transaction.rs index 91435b587..284384e00 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}; @@ -89,16 +89,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 +106,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 +132,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 +260,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 }); + } + if !money_range(value) { + return Some(TxInvalid::OutputTooLarge { index: i, value }); } - total = total.saturating_add(sat); - if total > max_money { + total += value; + if !money_range(total) { return Some(TxInvalid::OutputTotalTooLarge { total }); } } @@ -429,7 +416,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 +434,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,6 +465,7 @@ 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}"); } } From 94edd8fb7419c823281e93880eb8c8806bf31b3e Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 22 Oct 2026 19:15:04 +0000 Subject: [PATCH 02/15] primitives%fix: accept version 2 asset unlocks Version 2 shares the version 1 layout, and the reference accepts it in its context-free check. Refusing it before v24 is active needs the chain and is left to the caller. --- pkgs/primitives/corpus/assetunlock.json5 | 60 +++++++++++++++++++++- pkgs/primitives/src/payload/assetunlock.rs | 39 +++++++++++++- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/pkgs/primitives/corpus/assetunlock.json5 b/pkgs/primitives/corpus/assetunlock.json5 index 6d63fbcde..d284b938a 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,52 @@ "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" }, + ], } diff --git a/pkgs/primitives/src/payload/assetunlock.rs b/pkgs/primitives/src/payload/assetunlock.rs index e8b11035b..611938a28 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}"); + } + } } From 2b64a95313ee52a7136fac53dc14b4006b53c36c Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 22 Oct 2026 19:23:37 +0000 Subject: [PATCH 03/15] primitives%fix!: hash version 2 asset unlocks by their stable txid The txid of an asset unlock with a version 2 payload zeroes its quorum signing info, as the reference does, so re-signing keeps it. The hash of the whole serialization is `instance_hash()`. --- pkgs/primitives/corpus/assetunlock.json5 | 68 ++++++++++++++++++++++++ pkgs/primitives/src/transaction.rs | 62 ++++++++++++++++++++- 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/pkgs/primitives/corpus/assetunlock.json5 b/pkgs/primitives/corpus/assetunlock.json5 index d284b938a..f931c630e 100644 --- a/pkgs/primitives/corpus/assetunlock.json5 +++ b/pkgs/primitives/corpus/assetunlock.json5 @@ -299,4 +299,72 @@ BOOST_AUTO_TEST_CASE(assetunlock_check) // 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/src/transaction.rs b/pkgs/primitives/src/transaction.rs index 284384e00..1192e51ad 100644 --- a/pkgs/primitives/src/transaction.rs +++ b/pkgs/primitives/src/transaction.rs @@ -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 @@ -326,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()) } } @@ -341,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 @@ -396,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; @@ -469,4 +503,30 @@ pub(crate) mod tests { 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}"); + } + } } From d0bea8f2afb8a6897fb0e257706e5ea4c564270e Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 22 Oct 2026 19:29:12 +0000 Subject: [PATCH 04/15] primitives%fix!: accept version 2 asset locks and P2SH credits Version 2 credits may pay to a script hash; other scripts fail with the new `CreditBadScript`. Refusing version 2 before v24 is active needs the chain and is left to the caller. --- pkgs/primitives/corpus/assetlock.json5 | 32 ++++++++++++++++++++++++ pkgs/primitives/src/payload/assetlock.rs | 17 +++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/pkgs/primitives/corpus/assetlock.json5 b/pkgs/primitives/corpus/assetlock.json5 index fa92c3c68..647f3f330 100644 --- a/pkgs/primitives/corpus/assetlock.json5 +++ b/pkgs/primitives/corpus/assetlock.json5 @@ -277,6 +277,8 @@ 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}}}, @@ -288,6 +290,16 @@ BOOST_AUTO_TEST_CASE(assetlock_check) {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) { @@ -334,5 +346,25 @@ BOOST_AUTO_TEST_CASE(assetlock_check) { "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/src/payload/assetlock.rs b/pkgs/primitives/src/payload/assetlock.rs index 103b92821..affa7386e 100644 --- a/pkgs/primitives/src/payload/assetlock.rs +++ b/pkgs/primitives/src/payload/assetlock.rs @@ -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 }); } @@ -67,10 +72,12 @@ impl Checkable for AssetLock { return Some(AssetLockInvalid::CreditOutOfRange { index: i }); } total += out.value(); - if !matches!( - Recipient::from_script(out.script_pubkey().as_bytes()), - Some(Recipient::PubKeyHash(_)) - ) { + 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 }); } } From 309e0de5487eed20eea7e708b90d6b0e5d431c7e Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 22 Oct 2026 19:35:20 +0000 Subject: [PATCH 05/15] primitives%feat!: read the asset unlock root of version 4 CbTx Version 4 ends with the merkle root of the block's asset unlocks with a stable txid. It is read into `merkle_root_asset_unlocks`, and the check accepts versions 1 to 4 as the reference does. --- pkgs/primitives/corpus/cbtx.json5 | 107 +++++++++++++++++++++++++++- pkgs/primitives/src/payload/cbtx.rs | 62 +++++++++++++++- 2 files changed, 165 insertions(+), 4 deletions(-) diff --git a/pkgs/primitives/corpus/cbtx.json5 b/pkgs/primitives/corpus/cbtx.json5 index 51008c55f..287db85db 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/src/payload/cbtx.rs b/pkgs/primitives/src/payload/cbtx.rs index f9a58a5bf..7c3a162b7 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}"); + } + } } From 558f175a898039c112f7a13e20f0194134a16068 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 22 Oct 2026 21:02:14 +0000 Subject: [PATCH 06/15] primitives%fix!: read v3 ProRegTx and ProUpRegTx in the v24 layout From version 3 both carry a list of owner payouts in place of the payout script. ProRegTx then carries the collateral shares, their consent signatures and the early dissolution terms. --- clippy.toml | 2 +- maint/codeql/rust/lib/policy.qll | 4 + pkgs/primitives/corpus/proregtx.json5 | 266 +++++++++++++-------- pkgs/primitives/corpus/proupregtx.json5 | 67 +++++- pkgs/primitives/src/codec.rs | 29 +++ pkgs/primitives/src/provider/mod.rs | 4 +- pkgs/primitives/src/provider/proregtx.rs | 149 +++++++++++- pkgs/primitives/src/provider/proupregtx.rs | 96 +++++++- pkgs/primitives/src/provider/share.rs | 50 ++++ pkgs/primitives/src/provider/sig.rs | 10 +- 10 files changed, 561 insertions(+), 116 deletions(-) create mode 100644 pkgs/primitives/src/provider/share.rs diff --git a/clippy.toml b/clippy.toml index 1bd6147d8..0d4e02f0b 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -too-many-arguments-threshold = 15 +too-many-arguments-threshold = 20 diff --git a/maint/codeql/rust/lib/policy.qll b/maint/codeql/rust/lib/policy.qll index 1418ea260..5055e5c39 100644 --- a/maint/codeql/rust/lib/policy.qll +++ b/maint/codeql/rust/lib/policy.qll @@ -68,6 +68,10 @@ 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", // Serde artifact to deserialize a tagged enum. "__Seed" ] diff --git a/pkgs/primitives/corpus/proregtx.json5 b/pkgs/primitives/corpus/proregtx.json5 index b8abe7296..513d6daaa 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,161 @@ "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 }, + ], } diff --git a/pkgs/primitives/corpus/proupregtx.json5 b/pkgs/primitives/corpus/proupregtx.json5 index e2213212d..a9b3f1e41 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,59 @@ "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": [] }, + ], } diff --git a/pkgs/primitives/src/codec.rs b/pkgs/primitives/src/codec.rs index bfbb30a35..a665a6593 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/provider/mod.rs b/pkgs/primitives/src/provider/mod.rs index 050a79520..fa1318490 100644 --- a/pkgs/primitives/src/provider/mod.rs +++ b/pkgs/primitives/src/provider/mod.rs @@ -10,6 +10,7 @@ mod proregtx; mod proupregtx; mod prouprevtx; mod proupservtx; +mod share; mod sig; mod version; @@ -20,7 +21,8 @@ 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::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/proregtx.rs b/pkgs/primitives/src/provider/proregtx.rs index 1057faf59..4b5156179 100644 --- a/pkgs/primitives/src/provider/proregtx.rs +++ b/pkgs/primitives/src/provider/proregtx.rs @@ -6,8 +6,8 @@ //! ProRegTx registration payload (type 1). -use super::{CollateralSig, ProTxVersion}; -use crate::codec::impl_payload; +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 +33,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 +68,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, @@ -134,7 +154,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 +191,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 +223,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); @@ -327,6 +374,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 +396,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, @@ -365,11 +422,15 @@ 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::{assert_serde_rt, check_sptx, vec_from_hex, Corpus}; + use dash_types::Hashable; use rstest::rstest; + use serde::Deserialize; #[rstest] fn corpus_proregtx() { @@ -377,4 +438,78 @@ 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 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 7af42a76b..794cc2c67 100644 --- a/pkgs/primitives/src/provider/proupregtx.rs +++ b/pkgs/primitives/src/provider/proupregtx.rs @@ -6,28 +6,29 @@ //! ProUpRegTx registrar-update payload (type 3). -use super::{OwnerSig, ProTxVersion}; -use crate::codec::codec_payload; +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_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 +48,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 +63,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 { @@ -113,6 +161,8 @@ impl Checkable for ProUpRegTx { } } +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,33 @@ 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()); + } } diff --git a/pkgs/primitives/src/provider/share.rs b/pkgs/primitives/src/provider/share.rs new file mode 100644 index 000000000..7cf23d774 --- /dev/null +++ b/pkgs/primitives/src/provider/share.rs @@ -0,0 +1,50 @@ +// +// 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 crate::codec_type; + +use bitcoin_primitives::script::ScriptPubKeyBuf; +use dash_script::PubKeyHash; +use dash_types::type_id::TypeId; +use getset::{CopyGetters, Getters}; + +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, +} +} diff --git a/pkgs/primitives/src/provider/sig.rs b/pkgs/primitives/src/provider/sig.rs index 350a53983..4ee83ef29 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 { From da1deef528f537ebbf0566ff0ad231975b01d534 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 22 Oct 2026 21:08:40 +0000 Subject: [PATCH 07/15] primitives%fix!: check v3 payout and share lists as the reference does Owner payouts and collateral shares are checked for count, rewards or amounts, scripts and key reuse before the network info. ProUpRegTx also refuses a payout to its voting key in versions 1 and 2. --- pkgs/primitives/corpus/proregtx.json5 | 366 +++++++++++++++++++++ pkgs/primitives/corpus/proupregtx.json5 | 101 ++++++ pkgs/primitives/src/provider/proregtx.rs | 77 ++++- pkgs/primitives/src/provider/proupregtx.rs | 36 +- pkgs/primitives/src/provider/share.rs | 134 +++++++- pkgs/primitives/src/sptx.rs | 54 +++ 6 files changed, 744 insertions(+), 24 deletions(-) diff --git a/pkgs/primitives/corpus/proregtx.json5 b/pkgs/primitives/corpus/proregtx.json5 index 513d6daaa..91b207b61 100644 --- a/pkgs/primitives/corpus/proregtx.json5 +++ b/pkgs/primitives/corpus/proregtx.json5 @@ -707,4 +707,370 @@ BOOST_AUTO_TEST_CASE(proregtx_rejected) // 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" }, + ], } diff --git a/pkgs/primitives/corpus/proupregtx.json5 b/pkgs/primitives/corpus/proupregtx.json5 index a9b3f1e41..fbb4a5bbd 100644 --- a/pkgs/primitives/corpus/proupregtx.json5 +++ b/pkgs/primitives/corpus/proupregtx.json5 @@ -346,4 +346,105 @@ BOOST_AUTO_TEST_CASE(proupregtx_v24_layout) // 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/src/provider/proregtx.rs b/pkgs/primitives/src/provider/proregtx.rs index 4b5156179..34bd3f3c8 100644 --- a/pkgs/primitives/src/provider/proregtx.rs +++ b/pkgs/primitives/src/provider/proregtx.rs @@ -6,6 +6,7 @@ //! ProRegTx registration payload (type 1). +use super::share::{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::*; @@ -297,18 +298,31 @@ 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 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() { + 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(_)); @@ -341,12 +355,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, @@ -408,6 +416,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 { @@ -493,6 +523,25 @@ mod tests { 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}"); + } + } + #[derive(Deserialize)] struct RejectedVector { txid: String, diff --git a/pkgs/primitives/src/provider/proupregtx.rs b/pkgs/primitives/src/provider/proupregtx.rs index 794cc2c67..f75995fdd 100644 --- a/pkgs/primitives/src/provider/proupregtx.rs +++ b/pkgs/primitives/src/provider/proupregtx.rs @@ -6,6 +6,7 @@ //! ProUpRegTx registrar-update payload (type 3). +use super::share::{check_payouts, MAX_REWARD}; use super::{OwnerSig, PayoutShare, ProTxVersion}; use crate::codec::{decode_list, encode_list, impl_payload}; use crate::prelude::*; @@ -16,7 +17,7 @@ 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}; @@ -149,15 +150,11 @@ 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) } } @@ -241,4 +238,25 @@ mod tests { } 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}"); + } + } } diff --git a/pkgs/primitives/src/provider/share.rs b/pkgs/primitives/src/provider/share.rs index 7cf23d774..f0a5a15e3 100644 --- a/pkgs/primitives/src/provider/share.rs +++ b/pkgs/primitives/src/provider/share.rs @@ -7,12 +7,40 @@ //! Owner payouts and collateral shares of provider payloads. use crate::codec_type; +use crate::sptx::ProTxInvalid; use bitcoin_primitives::script::ScriptPubKeyBuf; -use dash_script::PubKeyHash; +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. +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)] @@ -48,3 +76,107 @@ pub struct CollateralShare { key_id_owner: PubKeyHash, } } + +/// Reads a payout script, which must pay to a key hash or a script hash. +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 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/sptx.rs b/pkgs/primitives/src/sptx.rs index b2b6ef4a9..5d2b10f41 100644 --- a/pkgs/primitives/src/sptx.rs +++ b/pkgs/primitives/src/sptx.rs @@ -119,6 +119,60 @@ 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 }, /// `bad-protx-operator-reward` #[error("bad-protx-operator-reward: {reward}")] OperatorRewardTooHigh { reward: u16 }, From 1c81d6fd578a4c865546cb2b662d46acccc4e232 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 22 Oct 2026 21:17:06 +0000 Subject: [PATCH 08/15] primitives%fix: build the v3 ProRegTx sign string over its payouts From version 3 the collateral key signs the owner payouts, each as its address or script and its reward, in place of the payout script. --- pkgs/primitives/corpus/proregtx.json5 | 57 ++++++++++++++++++++++++ pkgs/primitives/src/provider/proregtx.rs | 51 ++++++++++++++++++--- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/pkgs/primitives/corpus/proregtx.json5 b/pkgs/primitives/corpus/proregtx.json5 index 91b207b61..b394fe38c 100644 --- a/pkgs/primitives/corpus/proregtx.json5 +++ b/pkgs/primitives/corpus/proregtx.json5 @@ -1073,4 +1073,61 @@ BOOST_AUTO_TEST_CASE(proregtx_check) // 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/src/provider/proregtx.rs b/pkgs/primitives/src/provider/proregtx.rs index 34bd3f3c8..7263ad98d 100644 --- a/pkgs/primitives/src/provider/proregtx.rs +++ b/pkgs/primitives/src/provider/proregtx.rs @@ -139,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)?; @@ -253,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, @@ -457,7 +471,8 @@ mod tests { use super::*; use crate::Transaction; - use dash_dev::{assert_serde_rt, check_sptx, vec_from_hex, 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; @@ -542,6 +557,32 @@ mod tests { } } + #[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, From 5d5b5d761155b896b8d639c289886e9a20ffa80c Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 22 Oct 2026 21:27:13 +0000 Subject: [PATCH 09/15] primitives%feat!: type ProDisTx Type 10 dissolves a shared masternode. It is read with one signature per share, or one by the acting share's owner, and the check refuses none or more than eight. --- pkgs/primitives/corpus/prodistx.json5 | 60 ++++++++++ pkgs/primitives/src/provider/mod.rs | 2 + pkgs/primitives/src/provider/prodistx.rs | 138 +++++++++++++++++++++++ pkgs/primitives/src/provider/share.rs | 2 +- pkgs/primitives/src/sptx.rs | 11 +- 5 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 pkgs/primitives/corpus/prodistx.json5 create mode 100644 pkgs/primitives/src/provider/prodistx.rs diff --git a/pkgs/primitives/corpus/prodistx.json5 b/pkgs/primitives/corpus/prodistx.json5 new file mode 100644 index 000000000..bac4115a6 --- /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/src/provider/mod.rs b/pkgs/primitives/src/provider/mod.rs index fa1318490..f93166100 100644 --- a/pkgs/primitives/src/provider/mod.rs +++ b/pkgs/primitives/src/provider/mod.rs @@ -6,6 +6,7 @@ //! Provider transaction payloads. +mod prodistx; mod proregtx; mod proupregtx; mod prouprevtx; @@ -17,6 +18,7 @@ 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; diff --git a/pkgs/primitives/src/provider/prodistx.rs b/pkgs/primitives/src/provider/prodistx.rs new file mode 100644 index 000000000..cf930e617 --- /dev/null +++ b/pkgs/primitives/src/provider/prodistx.rs @@ -0,0 +1,138 @@ +// +// 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, ProTxVersion}; +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; + +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. + #[getset(get_copy = "pub")] + version: ProTxVersion, + /// 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: ProTxVersion::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.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.to_base()) + } +} + +#[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().to_base(), 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()); + } +} diff --git a/pkgs/primitives/src/provider/share.rs b/pkgs/primitives/src/provider/share.rs index f0a5a15e3..bfb28d23e 100644 --- a/pkgs/primitives/src/provider/share.rs +++ b/pkgs/primitives/src/provider/share.rs @@ -21,7 +21,7 @@ use core::iter; const MIN_SHARES: usize = 2; /// Most shares of a shared collateral, and most owner payouts. -const MAX_SHARES: usize = 8; +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; diff --git a/pkgs/primitives/src/sptx.rs b/pkgs/primitives/src/sptx.rs index 5d2b10f41..3cb5c7c0f 100644 --- a/pkgs/primitives/src/sptx.rs +++ b/pkgs/primitives/src/sptx.rs @@ -12,7 +12,7 @@ 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}; use crate::types::{NIError, NIPurpose, NITrait, NetInfoV2}; use dash_num::make_hash; @@ -61,6 +61,8 @@ 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", } } @@ -173,6 +175,9 @@ pub enum ProTxInvalid { /// `bad-protx-shares-penalty` #[error("bad-protx-shares-penalty: {penalty}")] BadEarlyPenalty { penalty: i64 }, + /// `bad-prodis-sig-count` + #[error("bad-prodis-sig-count: {count}")] + BadDissolveSigCount { count: usize }, /// `bad-protx-operator-reward` #[error("bad-protx-operator-reward: {reward}")] OperatorRewardTooHigh { reward: u16 }, @@ -233,6 +238,8 @@ pub enum SpecialPayload { AssetLock(AssetLock), /// Asset unlock: Platform to L1 (type 9). AssetUnlock(AssetUnlock), + /// Shared masternode dissolution (type 10). + ProviderDissolve(ProDisTx), /// Unrecognized type, stored as raw bytes. Unknown { /// The transaction type code. @@ -296,6 +303,7 @@ 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::Unknown { .. } => None, } } @@ -338,6 +346,7 @@ 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), unknown => { let bytes = data.to_vec(); *data = &[]; From 5bd7b665c2dbbca4c35f21c3aa29dae2775b0bca Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 22 Oct 2026 21:33:05 +0000 Subject: [PATCH 10/15] primitives%feat!: type ProUpShareTx and ProUpSharedRegTx Type 11 moves a share's rewards to a key or script hash under the share owner's signature. Type 12 replaces the operator and voting keys of a shared masternode with one to eight share owner signatures. --- maint/codeql/rust/lib/policy.qll | 2 + pkgs/primitives/corpus/proupsharedregtx.json5 | 85 +++++++++ pkgs/primitives/corpus/proupsharetx.json5 | 79 ++++++++ pkgs/primitives/src/provider/mod.rs | 4 + .../src/provider/proupsharedregtx.rs | 174 ++++++++++++++++++ pkgs/primitives/src/provider/proupsharetx.rs | 137 ++++++++++++++ pkgs/primitives/src/provider/share.rs | 2 +- pkgs/primitives/src/sptx.rs | 27 ++- 8 files changed, 508 insertions(+), 2 deletions(-) create mode 100644 pkgs/primitives/corpus/proupsharedregtx.json5 create mode 100644 pkgs/primitives/corpus/proupsharetx.json5 create mode 100644 pkgs/primitives/src/provider/proupsharedregtx.rs create mode 100644 pkgs/primitives/src/provider/proupsharetx.rs diff --git a/maint/codeql/rust/lib/policy.qll b/maint/codeql/rust/lib/policy.qll index 5055e5c39..3d9d0a516 100644 --- a/maint/codeql/rust/lib/policy.qll +++ b/maint/codeql/rust/lib/policy.qll @@ -72,6 +72,8 @@ predicate isSecretType(TypeItem t) { // 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/proupsharedregtx.json5 b/pkgs/primitives/corpus/proupsharedregtx.json5 new file mode 100644 index 000000000..1347845fd --- /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 000000000..37aa74da3 --- /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/src/provider/mod.rs b/pkgs/primitives/src/provider/mod.rs index f93166100..f755572c6 100644 --- a/pkgs/primitives/src/provider/mod.rs +++ b/pkgs/primitives/src/provider/mod.rs @@ -11,6 +11,8 @@ mod proregtx; mod proupregtx; mod prouprevtx; mod proupservtx; +mod proupsharedregtx; +mod proupsharetx; mod share; mod sig; mod version; @@ -23,6 +25,8 @@ pub use self::proregtx::ProRegTx; pub use self::proupregtx::ProUpRegTx; pub use self::prouprevtx::ProUpRevTx; pub use self::proupservtx::ProUpServTx; +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; diff --git a/pkgs/primitives/src/provider/proupsharedregtx.rs b/pkgs/primitives/src/provider/proupsharedregtx.rs new file mode 100644 index 000000000..62da78b76 --- /dev/null +++ b/pkgs/primitives/src/provider/proupsharedregtx.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 +// + +//! ProUpSharedRegTx shared registrar update payload (type 12). + +use super::share::MAX_SHARES; +use super::{CompactSig, ProTxVersion}; +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; + +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. + #[getset(get_copy = "pub")] + version: ProTxVersion, + /// 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: ProTxVersion::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.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.to_base()) + } +} + +#[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().to_base(), 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()); + } +} diff --git a/pkgs/primitives/src/provider/proupsharetx.rs b/pkgs/primitives/src/provider/proupsharetx.rs new file mode 100644 index 000000000..1eaa964ad --- /dev/null +++ b/pkgs/primitives/src/provider/proupsharetx.rs @@ -0,0 +1,137 @@ +// +// 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 super::ProTxVersion; +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; + +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. + #[getset(get_copy = "pub")] + version: ProTxVersion, + /// 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.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.to_base()) + } +} + +#[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().to_base(), 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()); + } +} diff --git a/pkgs/primitives/src/provider/share.rs b/pkgs/primitives/src/provider/share.rs index bfb28d23e..6c68502a2 100644 --- a/pkgs/primitives/src/provider/share.rs +++ b/pkgs/primitives/src/provider/share.rs @@ -78,7 +78,7 @@ pub struct CollateralShare { } /// Reads a payout script, which must pay to a key hash or a script hash. -fn payee(script: &ScriptPubKeyBuf) -> Option { +pub(super) fn payee(script: &ScriptPubKeyBuf) -> Option { Recipient::from_script(script.as_bytes()) .filter(|recipient| matches!(recipient, Recipient::PubKeyHash(_) | Recipient::ScriptHash(_))) } diff --git a/pkgs/primitives/src/sptx.rs b/pkgs/primitives/src/sptx.rs index 3cb5c7c0f..4fa4cffa9 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::{ProDisTx, 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; @@ -63,6 +65,10 @@ enum_map! { 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", } } @@ -178,6 +184,15 @@ pub enum ProTxInvalid { /// `bad-prodis-sig-count` #[error("bad-prodis-sig-count: {count}")] BadDissolveSigCount { count: usize }, + /// `bad-proupshare-sig-size` + #[error("bad-proupshare-sig-size: {size}")] + BadShareUpdateSigSize { size: usize }, + /// `bad-proupshare-payee` + #[error("bad-proupshare-payee")] + BadShareUpdateScript, + /// `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 }, @@ -240,6 +255,10 @@ pub enum SpecialPayload { 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. @@ -304,6 +323,8 @@ impl Checkable for SpecialPayload { 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, } } @@ -347,6 +368,10 @@ impl SpecialPayload { 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 = &[]; From e568ad1cd1d58739268322ea7c20e362d2c7e335 Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 23 Oct 2026 06:25:01 +0000 Subject: [PATCH 11/15] primitives%fix!: check the asset unlock root of version 4 CbTx A block whose version 4 coinbase payload does not commit to the instance hashes of its version 2 asset unlocks now fails its check, as in the reference implementation. --- pkgs/primitives/corpus/block_check.json5 | 162 ++++++++++++++++++++++- pkgs/primitives/src/block.rs | 150 +++++++++++++++++++-- 2 files changed, 295 insertions(+), 17 deletions(-) diff --git a/pkgs/primitives/corpus/block_check.json5 b/pkgs/primitives/corpus/block_check.json5 index cd3e443bd..98d68b08f 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/src/block.rs b/pkgs/primitives/src/block.rs index ce262fea3..b0c1f7377 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 { @@ -281,11 +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::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; @@ -353,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}"); From bb815ccfc8c036c09957daa8ca5ae9cb63a7de11 Mon Sep 17 00:00:00 2001 From: pasta Date: Sun, 25 Oct 2026 10:56:43 +0000 Subject: [PATCH 12/15] primitives%fix!: check that ProRegTx shares fit its encoding A ProRegTx constructed with a join signature count other than its share count, or with shares below version 3, does not round-trip through its encoding. Its check now refuses both. --- pkgs/primitives/src/provider/proregtx.rs | 41 ++++++++++++++++++++++++ pkgs/primitives/src/sptx.rs | 6 ++++ 2 files changed, 47 insertions(+) diff --git a/pkgs/primitives/src/provider/proregtx.rs b/pkgs/primitives/src/provider/proregtx.rs index 7263ad98d..2f8923ec9 100644 --- a/pkgs/primitives/src/provider/proregtx.rs +++ b/pkgs/primitives/src/provider/proregtx.rs @@ -313,6 +313,15 @@ impl Checkable for ProRegTx { } 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 shared && self.version < ProTxVersion::ExtAddr { + return Some(ProTxInvalid::SharesVersionTooLow { version: self.version }); + } if shared { if !self.collateral_hash.is_null() { return Some(ProTxInvalid::ExternalSharedCollateral); @@ -557,6 +566,38 @@ mod tests { } } + #[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 corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proregtx"); + let mut ptx = corpus + .vectors::("check") + .into_iter() + .filter(|v| v.reason.is_empty()) + .map(|v| ProRegTx::decode(&mut &vec_from_hex(&v.payload)[..]).unwrap()) + .find(|ptx| ptx.shares.len() == 2) + .unwrap(); + assert_eq!(ptx.version, ProTxVersion::ExtAddr); + assert_eq!(ptx.check(), None); + edit(&mut ptx); + assert_eq!(ptx.check(), Some(expected)); + } + #[derive(Deserialize)] struct SignStringVector { payload: String, diff --git a/pkgs/primitives/src/sptx.rs b/pkgs/primitives/src/sptx.rs index 4fa4cffa9..2764b116e 100644 --- a/pkgs/primitives/src/sptx.rs +++ b/pkgs/primitives/src/sptx.rs @@ -181,6 +181,12 @@ pub enum ProTxInvalid { /// `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 }, /// `bad-prodis-sig-count` #[error("bad-prodis-sig-count: {count}")] BadDissolveSigCount { count: usize }, From 7a163a6ab39060395ab0c8b7a4e5cd3423dbf2a0 Mon Sep 17 00:00:00 2001 From: pasta Date: Sun, 25 Oct 2026 11:26:00 +0000 Subject: [PATCH 13/15] primitives%fix!: check that provider payouts fit their version A ProRegTx or ProUpRegTx constructed with a payout script from version 3, or with payouts below it, does not round-trip through its encoding. Their checks now refuse both. --- pkgs/primitives/src/provider/proregtx.rs | 52 +++++++++++++++++++++- pkgs/primitives/src/provider/proupregtx.rs | 52 +++++++++++++++++++++- pkgs/primitives/src/provider/share.rs | 19 ++++++++ pkgs/primitives/src/sptx.rs | 3 ++ 4 files changed, 124 insertions(+), 2 deletions(-) diff --git a/pkgs/primitives/src/provider/proregtx.rs b/pkgs/primitives/src/provider/proregtx.rs index 2f8923ec9..4f248ac85 100644 --- a/pkgs/primitives/src/provider/proregtx.rs +++ b/pkgs/primitives/src/provider/proregtx.rs @@ -6,7 +6,7 @@ //! ProRegTx registration payload (type 1). -use super::share::{check_payouts, check_shares, EVO_COLLATERAL, MAX_REWARD, REGULAR_COLLATERAL}; +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::*; @@ -322,6 +322,9 @@ impl Checkable for ProRegTx { 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); @@ -598,6 +601,53 @@ mod tests { 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() + } + + /// A version 3 payload with one payout that the check vectors accept. + fn ext_addr() -> 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(|ptx| ptx.payouts.len() == 1) + .unwrap() + } + + #[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 })); + } + #[derive(Deserialize)] struct SignStringVector { payload: String, diff --git a/pkgs/primitives/src/provider/proupregtx.rs b/pkgs/primitives/src/provider/proupregtx.rs index f75995fdd..91dc86b55 100644 --- a/pkgs/primitives/src/provider/proupregtx.rs +++ b/pkgs/primitives/src/provider/proupregtx.rs @@ -6,7 +6,7 @@ //! ProUpRegTx registrar-update payload (type 3). -use super::share::{check_payouts, MAX_REWARD}; +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::*; @@ -142,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); @@ -259,4 +262,51 @@ mod tests { 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/share.rs b/pkgs/primitives/src/provider/share.rs index 6c68502a2..fadcab0ec 100644 --- a/pkgs/primitives/src/provider/share.rs +++ b/pkgs/primitives/src/provider/share.rs @@ -6,6 +6,7 @@ //! Owner payouts and collateral shares of provider payloads. +use super::ProTxVersion; use crate::codec_type; use crate::sptx::ProTxInvalid; @@ -119,6 +120,24 @@ pub(super) fn check_payouts( 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( diff --git a/pkgs/primitives/src/sptx.rs b/pkgs/primitives/src/sptx.rs index 2764b116e..3bb6520a0 100644 --- a/pkgs/primitives/src/sptx.rs +++ b/pkgs/primitives/src/sptx.rs @@ -187,6 +187,9 @@ pub enum ProTxInvalid { /// 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-sig-count` #[error("bad-prodis-sig-count: {count}")] BadDissolveSigCount { count: usize }, From ef41dc23b30123f0d310150cfa647b02d63ddb42 Mon Sep 17 00:00:00 2001 From: pasta Date: Sun, 25 Oct 2026 12:17:01 +0000 Subject: [PATCH 14/15] primitives%fix!: require version 1 for the three share payloads ProDisTx, ProUpShareTx and ProUpSharedRegTx read their version as an integer, and their checks refuse any version but 1 with the reference implementation's reasons. --- pkgs/primitives/src/provider/prodistx.rs | 48 ++++++++++++++++--- .../src/provider/proupsharedregtx.rs | 48 ++++++++++++++++--- pkgs/primitives/src/provider/proupsharetx.rs | 45 +++++++++++++++-- pkgs/primitives/src/sptx.rs | 9 ++++ 4 files changed, 133 insertions(+), 17 deletions(-) diff --git a/pkgs/primitives/src/provider/prodistx.rs b/pkgs/primitives/src/provider/prodistx.rs index cf930e617..92a927748 100644 --- a/pkgs/primitives/src/provider/prodistx.rs +++ b/pkgs/primitives/src/provider/prodistx.rs @@ -7,7 +7,7 @@ //! ProDisTx shared masternode dissolution payload (type 10). use super::share::MAX_SHARES; -use super::{CompactSig, ProTxVersion}; +use super::CompactSig; use crate::codec::{decode_list, encode_list, impl_payload}; use crate::prelude::*; use crate::sptx::ProTxInvalid; @@ -20,15 +20,18 @@ 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. + /// Payload version, read at any value. #[getset(get_copy = "pub")] - version: ProTxVersion, + version: u16, /// ProTx hash identifying the masternode. #[getset(get_copy = "pub")] pro_tx_hash: TxHash, @@ -47,7 +50,7 @@ impl_payload!(ProDisTx); impl BaseCodec for ProDisTx { fn decode(data: &mut &[u8]) -> Result { Ok(Self { - version: ProTxVersion::decode(data)?, + version: u16::decode(data)?, pro_tx_hash: TxHash::decode(data)?, actor_index: u16::decode(data)?, sigs: decode_list(data)?, @@ -66,6 +69,9 @@ 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() }); } @@ -77,7 +83,7 @@ hash_impl!(ProDisTx); impl fmt::Display for ProDisTx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "ProDisTx {{ v{} }}", self.version.to_base()) + write!(f, "ProDisTx {{ v{} }}", self.version) } } @@ -118,7 +124,7 @@ mod tests { 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().to_base(), v.json.version, "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(), @@ -135,4 +141,34 @@ mod tests { } 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/proupsharedregtx.rs b/pkgs/primitives/src/provider/proupsharedregtx.rs index 62da78b76..5f65a4191 100644 --- a/pkgs/primitives/src/provider/proupsharedregtx.rs +++ b/pkgs/primitives/src/provider/proupsharedregtx.rs @@ -7,7 +7,7 @@ //! ProUpSharedRegTx shared registrar update payload (type 12). use super::share::MAX_SHARES; -use super::{CompactSig, ProTxVersion}; +use super::CompactSig; use crate::codec::{decode_list, encode_list, impl_payload}; use crate::prelude::*; use crate::sptx::{InputsHash, ProTxInvalid}; @@ -22,15 +22,18 @@ 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. + /// Payload version, read at any value. #[getset(get_copy = "pub")] - version: ProTxVersion, + version: u16, /// ProTx hash identifying the masternode. #[getset(get_copy = "pub")] pro_tx_hash: TxHash, @@ -54,7 +57,7 @@ impl_payload!(ProUpSharedRegTx); impl BaseCodec for ProUpSharedRegTx { fn decode(data: &mut &[u8]) -> Result { Ok(Self { - version: ProTxVersion::decode(data)?, + version: u16::decode(data)?, pro_tx_hash: TxHash::decode(data)?, pub_key_operator: BlsPkBytes::decode(data)?, key_id_voting: PubKeyHash::decode(data)?, @@ -77,6 +80,9 @@ 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); } @@ -91,7 +97,7 @@ hash_impl!(ProUpSharedRegTx); impl fmt::Display for ProUpSharedRegTx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "ProUpSharedRegTx {{ v{} }}", self.version.to_base()) + write!(f, "ProUpSharedRegTx {{ v{} }}", self.version) } } @@ -140,7 +146,7 @@ mod tests { SpecialPayload::ProviderUpdateSharedRegistrar(ptx.clone()), "vector {i}" ); - assert_eq!(ptx.version().to_base(), v.json.version, "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(), @@ -171,4 +177,34 @@ mod tests { } 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 index 1eaa964ad..be9b06ac7 100644 --- a/pkgs/primitives/src/provider/proupsharetx.rs +++ b/pkgs/primitives/src/provider/proupsharetx.rs @@ -7,7 +7,6 @@ //! ProUpShareTx share reward update payload (type 11). use super::share::payee; -use super::ProTxVersion; use crate::codec::codec_payload; use crate::prelude::*; use crate::sptx::{InputsHash, ProTxInvalid}; @@ -21,15 +20,18 @@ 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. + /// Payload version, read at any value. #[getset(get_copy = "pub")] - version: ProTxVersion, + version: u16, /// ProTx hash identifying the masternode. #[getset(get_copy = "pub")] pro_tx_hash: TxHash, @@ -53,6 +55,9 @@ 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() }); } @@ -65,7 +70,7 @@ impl Checkable for ProUpShareTx { impl fmt::Display for ProUpShareTx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "ProUpShareTx {{ v{} }}", self.version.to_base()) + write!(f, "ProUpShareTx {{ v{} }}", self.version) } } @@ -110,7 +115,7 @@ mod tests { 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().to_base(), v.json.version, "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(), @@ -134,4 +139,34 @@ mod tests { } 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/sptx.rs b/pkgs/primitives/src/sptx.rs index 3bb6520a0..43b339e48 100644 --- a/pkgs/primitives/src/sptx.rs +++ b/pkgs/primitives/src/sptx.rs @@ -190,15 +190,24 @@ pub enum ProTxInvalid { /// 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 }, From 54e0bfe6fed029d3dd16cdd035095bfca074476c Mon Sep 17 00:00:00 2001 From: pasta Date: Sun, 25 Oct 2026 12:20:31 +0000 Subject: [PATCH 15/15] primitives%test: check the fields of structured ProRegTx errors The check vectors compare the reason only. Checks on constructed payloads now pin the counts, totals, reward, amount, period and penalty that the errors carry. --- pkgs/primitives/src/provider/proregtx.rs | 94 +++++++++++++++++++++--- 1 file changed, 83 insertions(+), 11 deletions(-) diff --git a/pkgs/primitives/src/provider/proregtx.rs b/pkgs/primitives/src/provider/proregtx.rs index 4f248ac85..d2c24dade 100644 --- a/pkgs/primitives/src/provider/proregtx.rs +++ b/pkgs/primitives/src/provider/proregtx.rs @@ -587,14 +587,7 @@ mod tests { ProTxInvalid::SharesVersionTooLow { version: ProTxVersion::BasicBls } )] fn check_rejects_inconsistent_shares(#[case] edit: fn(&mut ProRegTx), #[case] expected: ProTxInvalid) { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "proregtx"); - let mut ptx = corpus - .vectors::("check") - .into_iter() - .filter(|v| v.reason.is_empty()) - .map(|v| ProRegTx::decode(&mut &vec_from_hex(&v.payload)[..]).unwrap()) - .find(|ptx| ptx.shares.len() == 2) - .unwrap(); + let mut ptx = shared(); assert_eq!(ptx.version, ProTxVersion::ExtAddr); assert_eq!(ptx.check(), None); edit(&mut ptx); @@ -611,18 +604,28 @@ mod tests { .unwrap() } - /// A version 3 payload with one payout that the check vectors accept. - fn ext_addr() -> ProRegTx { + /// 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(|ptx| ptx.payouts.len() == 1) + .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(), @@ -648,6 +651,75 @@ mod tests { 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,