Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 77 additions & 3 deletions dash/src/blockdata/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,9 @@ impl Encodable for Transaction {
if self.tx_type() == TransactionType::Coinbase {
have_witness = false;
}
if matches!(self.tx_type(), TransactionType::Unknown(_)) {
have_witness = false;
}
if !have_witness {
len += self.input.consensus_encode(w)?;
len += self.output.consensus_encode(w)?;
Expand Down Expand Up @@ -606,9 +609,10 @@ impl Decodable for Transaction {
let version = u16::consensus_decode_from_finite_reader(r)?;
let special_transaction_type_u16 = u16::consensus_decode(r)?;
let special_transaction_type = if version != 0 {
TransactionType::try_from(special_transaction_type_u16).map_err(|_| {
encode::Error::UnknownSpecialTransactionType(special_transaction_type_u16)
})?
// Keep types we don't know as raw payloads, so a block carrying a newer special
// transaction type still decodes.
TransactionType::try_from(special_transaction_type_u16)
.unwrap_or(TransactionType::Unknown(special_transaction_type_u16))
} else if special_transaction_type_u16 == 0 {
TransactionType::Classic
} else {
Expand All @@ -634,6 +638,9 @@ impl Decodable for Transaction {
if special_transaction_type == TransactionType::Coinbase {
segwit = false;
}
if matches!(special_transaction_type, TransactionType::Unknown(_)) {
segwit = false;
}
if segwit {
let segwit_flag = u8::consensus_decode_from_finite_reader(r)?;
match segwit_flag {
Expand Down Expand Up @@ -909,6 +916,7 @@ mod tests {
use crate::consensus::encode::{deserialize, serialize};
use crate::internal_macros::hex;
use crate::network::message::{NetworkMessage, RawNetworkMessage};
use crate::test_utils::UNASSIGNED_SPECIAL_TX_TYPE;

#[test]
fn test_is_coinbase() {
Expand Down Expand Up @@ -1078,6 +1086,72 @@ mod tests {
);
}

fn unknown_special_tx(input: Vec<TxIn>) -> Transaction {
Transaction {
version: 3,
lock_time: 0,
input,
output: vec![TxOut {
value: 1_000,
script_pubkey: ScriptBuf::new_op_return(&[0xab; 4]),
}],
special_transaction_payload: Some(TransactionPayload::UnknownPayloadType(
UNASSIGNED_SPECIAL_TX_TYPE,
vec![0x01, 0x00, 0xde, 0xad, 0xbe, 0xef],
)),
}
}

#[test]
fn unknown_special_transaction_type_round_trips() {
let tx = unknown_special_tx(vec![TxIn::default()]);
let bytes = serialize(&tx);

// nTxType on the wire, and the payload is its length prefix plus the raw bytes.
assert_eq!(&bytes[2..4], &UNASSIGNED_SPECIAL_TX_TYPE.to_le_bytes());
assert_eq!(&bytes[bytes.len() - 7..], &[0x06, 0x01, 0x00, 0xde, 0xad, 0xbe, 0xef]);

let decoded: Transaction = deserialize(&bytes).expect("unknown types must decode");
assert_eq!(decoded.tx_type(), TransactionType::Unknown(UNASSIGNED_SPECIAL_TX_TYPE));
assert_eq!(decoded, tx);
assert_eq!(serialize(&decoded), bytes);
assert_eq!(decoded.txid(), Txid::hash(&bytes));
assert_eq!(decoded.size(), bytes.len());
}

#[test]
fn unknown_special_transaction_type_keeps_the_stream_aligned() {
// Back to back, as inside a block: the transaction after the unknown one must
// decode from the right offset.
let unknown = unknown_special_tx(vec![TxIn::default()]);
let classic = Transaction {
special_transaction_payload: None,
..unknown_special_tx(vec![TxIn::default()])
};
let mut bytes = serialize(&unknown);
bytes.extend(serialize(&classic));

let mut cursor = io::Cursor::new(&bytes);
assert_eq!(Transaction::consensus_decode(&mut cursor).unwrap(), unknown);
assert_eq!(Transaction::consensus_decode(&mut cursor).unwrap(), classic);
assert_eq!(cursor.position() as usize, bytes.len());
}

#[test]
fn unknown_special_transaction_type_without_inputs_is_not_segwit() {
let tx = unknown_special_tx(vec![]);
let bytes = serialize(&tx);
// No BIP144 marker: the input count (0) and output count (1) follow the version and
// type, then straight away the output value.
assert_eq!(&bytes[4..6], &[0x00, 0x01]);
assert_eq!(&bytes[6..14], &1_000u64.to_le_bytes());
assert_eq!(bytes.len(), tx.size());

let decoded: Transaction = deserialize(&bytes).expect("input-less unknown type decodes");
assert_eq!(decoded, tx);
assert_eq!(serialize(&decoded), bytes);
}

#[test]
fn tx_no_input_deserialization() {
let tx_bytes = Vec::from_hex(
Expand Down
26 changes: 22 additions & 4 deletions dash/src/blockdata/transaction/special_transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ use crate::blockdata::transaction::special_transaction::TransactionPayload::{
ClassicalWithNonStandardVersionTypeBytesPayloadType, CoinbasePayloadType,
MnhfSignalPayloadType, ProviderRegistrationPayloadType, ProviderUpdateRegistrarPayloadType,
ProviderUpdateRevocationPayloadType, ProviderUpdateServicePayloadType,
QuorumCommitmentPayloadType,
QuorumCommitmentPayloadType, UnknownPayloadType,
};
use crate::blockdata::transaction::special_transaction::TransactionType::{
AssetLock, AssetUnlock, Classic, ClassicalWithNonStandardVersionTypeBytes, Coinbase,
MnhfSignal, ProviderRegistration, ProviderUpdateRegistrar, ProviderUpdateRevocation,
ProviderUpdateService, QuorumCommitment,
ProviderUpdateService, QuorumCommitment, Unknown,
};
use crate::blockdata::transaction::special_transaction::asset_lock::AssetLockPayload;
use crate::blockdata::transaction::special_transaction::asset_unlock::qualified_asset_unlock::AssetUnlockPayload;
Expand Down Expand Up @@ -92,6 +92,10 @@ pub enum TransactionPayload {
/// `consensus_encode` and `txid` continue to round-trip the on-wire bytes faithfully.
/// This variant has no payload section on the wire.
ClassicalWithNonStandardVersionTypeBytesPayloadType(u16),
/// The raw payload of a special transaction type this crate does not know, together
/// with its `nTxType`. Keeping the bytes lets blocks containing newer special
/// transaction types decode, and re-encode to the same bytes and txid.
UnknownPayloadType(u16, Vec<u8>),
}

impl Encodable for TransactionPayload {
Expand All @@ -107,6 +111,10 @@ impl Encodable for TransactionPayload {
AssetLockPayloadType(p) => p.consensus_encode(w),
AssetUnlockPayloadType(p) => p.consensus_encode(w),
ClassicalWithNonStandardVersionTypeBytesPayloadType(_) => Ok(0),
UnknownPayloadType(_, bytes) => {
w.write_all(bytes)?;
Ok(bytes.len())
}
}
}
}
Expand All @@ -127,6 +135,7 @@ impl TransactionPayload {
ClassicalWithNonStandardVersionTypeBytesPayloadType(raw) => {
ClassicalWithNonStandardVersionTypeBytes(*raw)
}
UnknownPayloadType(raw, _) => Unknown(*raw),
}
}

Expand All @@ -146,6 +155,7 @@ impl TransactionPayload {
AssetUnlockPayloadType(p) => 1 + p.size(),
// Pre-DIP-0002 transactions have no payload section on the wire.
ClassicalWithNonStandardVersionTypeBytesPayloadType(_) => 0,
UnknownPayloadType(_, bytes) => VarInt(bytes.len() as u64).len() + bytes.len(),
}
}

Expand Down Expand Up @@ -318,6 +328,9 @@ pub enum TransactionType {
/// bytes were non-zero. The wrapped value is the original u16 read from the wire,
/// which must be re-emitted verbatim during serialization to preserve the txid.
ClassicalWithNonStandardVersionTypeBytes(u16),
/// A special transaction type this crate does not know. The wrapped value is the
/// on-wire `nTxType`; the payload is kept as raw bytes.
Unknown(u16),
}

impl Debug for TransactionType {
Expand All @@ -336,6 +349,7 @@ impl Debug for TransactionType {
ClassicalWithNonStandardVersionTypeBytes(raw) => {
write!(f, "Classic Transaction (pre-DIP-0002, raw type bytes 0x{raw:04x})")
}
Unknown(raw) => write!(f, "Unknown Special Transaction (type {raw})"),
}
}
}
Expand All @@ -356,6 +370,7 @@ impl Display for TransactionType {
ClassicalWithNonStandardVersionTypeBytes(raw) => {
write!(f, "Classic (pre-DIP-0002, raw 0x{raw:04x})")
}
Unknown(raw) => write!(f, "Unknown ({raw})"),
}
}
}
Expand Down Expand Up @@ -415,6 +430,7 @@ impl TransactionType {
AssetLock => 8,
AssetUnlock => 9,
ClassicalWithNonStandardVersionTypeBytes(raw) => raw,
Unknown(raw) => raw,
}
}

Expand All @@ -424,9 +440,10 @@ impl TransactionType {
d: &mut R,
) -> Result<Option<TransactionPayload>, encode::Error> {
// Pre-DIP-0002 transactions and Classic transactions have no payload section
// on the wire — there isn't even a length prefix to consume.
// on the wire — there isn't even a length prefix to consume. An unknown type's
// length prefix is read together with its bytes below.
let _len = match self {
Classic | ClassicalWithNonStandardVersionTypeBytes(_) => VarInt(0),
Classic | ClassicalWithNonStandardVersionTypeBytes(_) | Unknown(_) => VarInt(0),
_ => VarInt::consensus_decode(d)?,
};

Expand Down Expand Up @@ -454,6 +471,7 @@ impl TransactionType {
MnhfSignal => Some(MnhfSignalPayloadType(MnhfSignalPayload::consensus_decode(d)?)),
AssetLock => Some(AssetLockPayloadType(AssetLockPayload::consensus_decode(d)?)),
AssetUnlock => Some(AssetUnlockPayloadType(AssetUnlockPayload::consensus_decode(d)?)),
Unknown(raw) => Some(UnknownPayloadType(raw, Vec::<u8>::consensus_decode(d)?)),
})
}
}
Expand Down
2 changes: 2 additions & 0 deletions dash/src/test_utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ mod instantlock;
mod network;
mod sml;
mod transaction;

pub use transaction::UNASSIGNED_SPECIAL_TX_TYPE;
3 changes: 3 additions & 0 deletions dash/src/test_utils/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ use std::ops::Range;

use crate::{Address, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness};

/// A special transaction type no Dash Core version assigns
pub const UNASSIGNED_SPECIAL_TX_TYPE: u16 = 0xffff;

impl Transaction {
/// Creates a transaction with no inputs or outputs.
pub fn dummy_empty() -> Transaction {
Expand Down
56 changes: 22 additions & 34 deletions key-wallet/src/transaction_checking/transaction_router/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
mod tests;

use crate::managed_account::managed_account_type::ManagedAccountType;
use dashcore::blockdata::transaction::special_transaction::TransactionPayload;
use dashcore::blockdata::transaction::special_transaction::TransactionType as TxType;
use dashcore::blockdata::transaction::Transaction;

/// Classification of transaction types for routing
Expand Down Expand Up @@ -41,41 +41,29 @@ pub struct TransactionRouter;
impl TransactionRouter {
/// Classify a transaction based on its type and payload
pub fn classify_transaction(tx: &Transaction) -> TransactionType {
// Check if it's a special transaction
let special_classification = tx.special_transaction_payload.as_ref().and_then(|payload| {
match payload {
TransactionPayload::ProviderRegistrationPayloadType(_) => {
Some(TransactionType::ProviderRegistration)
match tx.tx_type() {
TxType::ProviderRegistration => TransactionType::ProviderRegistration,
TxType::ProviderUpdateRegistrar => TransactionType::ProviderUpdateRegistrar,
TxType::ProviderUpdateService => TransactionType::ProviderUpdateService,
TxType::ProviderUpdateRevocation => TransactionType::ProviderUpdateRevocation,
TxType::AssetLock => TransactionType::AssetLock,
TxType::AssetUnlock => TransactionType::AssetUnlock,
TxType::Coinbase => TransactionType::Coinbase,
TxType::QuorumCommitment | TxType::MnhfSignal => TransactionType::Ignored,
// Classic and pre-DIP-0002 transactions are classified by shape. So are special
// types we can't parse yet: they may still spend or pay wallet coins, so they are
// checked across every fund-bearing account.
TxType::Classic
| TxType::ClassicalWithNonStandardVersionTypeBytes(_)
| TxType::Unknown(_) => {
if tx.is_coin_base() {
TransactionType::Coinbase
} else if Self::is_coinjoin_transaction(tx) {
TransactionType::CoinJoin
} else {
TransactionType::Standard
}
TransactionPayload::ProviderUpdateRegistrarPayloadType(_) => {
Some(TransactionType::ProviderUpdateRegistrar)
}
TransactionPayload::ProviderUpdateServicePayloadType(_) => {
Some(TransactionType::ProviderUpdateService)
}
TransactionPayload::ProviderUpdateRevocationPayloadType(_) => {
Some(TransactionType::ProviderUpdateRevocation)
}
TransactionPayload::AssetLockPayloadType(_) => Some(TransactionType::AssetLock),
TransactionPayload::AssetUnlockPayloadType(_) => Some(TransactionType::AssetUnlock),
TransactionPayload::CoinbasePayloadType(_) => Some(TransactionType::Coinbase),
TransactionPayload::QuorumCommitmentPayloadType(_) => {
Some(TransactionType::Ignored)
}
TransactionPayload::MnhfSignalPayloadType(_) => Some(TransactionType::Ignored),
// Pre-DIP-0002 transactions are logically Classic — fall through to the
// standard / coinbase / coinjoin classification below.
TransactionPayload::ClassicalWithNonStandardVersionTypeBytesPayloadType(_) => None,
}
});
if let Some(classification) = special_classification {
classification
} else if tx.is_coin_base() {
TransactionType::Coinbase
} else if Self::is_coinjoin_transaction(tx) {
TransactionType::CoinJoin
} else {
TransactionType::Standard
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use dashcore::blockdata::transaction::Transaction;
use dashcore::bls_sig_utils::{BLSPublicKey, BLSSignature};
use dashcore::hash_types::{MerkleRootMasternodeList, MerkleRootQuorums};
use dashcore::hashes::Hash;
use dashcore::test_utils::UNASSIGNED_SPECIAL_TX_TYPE;
use dashcore::Txid;

#[test]
Expand Down Expand Up @@ -170,6 +171,16 @@ fn test_classify_asset_unlock_transaction() {
assert_eq!(TransactionRouter::classify_transaction(&tx), TransactionType::AssetUnlock);
}

#[test]
fn test_classify_unknown_special_transaction_as_standard() {
let addr = test_addr();
let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]);
tx.special_transaction_payload =
Some(TransactionPayload::UnknownPayloadType(UNASSIGNED_SPECIAL_TX_TYPE, vec![0x01, 0x00]));

assert_eq!(TransactionRouter::classify_transaction(&tx), TransactionType::Standard);
}

#[test]
fn test_classify_coinbase_transaction() {
let addr = test_addr();
Expand Down
Loading