Skip to content

Commit df534c7

Browse files
committed
Merge rust-bitcoin#5402: primitives: reject transaction with duplicate inputs
af7a11b primitives: reject transaction with duplicate inputs (jrakibi) Pull request description: As part of adding validation rules to transactions, this check rejects transactions with duplicate inputs during the decoding stage Also updated a test case in the encode/decode roundtrip, the test was using duplicated inputs which causes it to fail with the current implementation. Addresses part of rust-bitcoin#5383 Related: CVE-2018-17144 ACKs for top commit: apoelstra: ACK af7a11b; successfully ran local tests tcharding: ACK af7a11b Tree-SHA512: d1d071973b855ee83af42c42c23bbda2207d8a6dda1f90208ce25d98f144ba7ce927e9dc1073ce57187e89aa7d37ac0b5eb1fe75962d3e15dd29c562ad2638be
2 parents 56d4bca + af7a11b commit df534c7

File tree

1 file changed

+50
-2
lines changed

1 file changed

+50
-2
lines changed

primitives/src/transaction.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,14 @@ impl Decoder for TransactionDecoder {
606606
return Err(E(Inner::CoinbaseScriptSigTooLarge(len)));
607607
}
608608
}
609+
// check for duplicate inputs (CVE-2018-17144).
610+
let mut outpoints: Vec<_> = tx.inputs.iter().map(|i| i.previous_output).collect();
611+
outpoints.sort_unstable();
612+
for pair in outpoints.windows(2) {
613+
if pair[0] == pair[1] {
614+
return Err(E(Inner::DuplicateInput(pair[0])));
615+
}
616+
}
609617
Ok(tx)
610618
}
611619
State::Errored => panic!("call to end() after decoder errored"),
@@ -715,6 +723,8 @@ enum TransactionDecoderErrorInner {
715723
CoinbaseScriptSigTooSmall(usize),
716724
/// Coinbase scriptSig is too large (must be at most 100 bytes).
717725
CoinbaseScriptSigTooLarge(usize),
726+
/// Transaction has duplicate inputs (this check prevents CVE-2018-17144 ).
727+
DuplicateInput(OutPoint),
718728
}
719729

720730
#[cfg(feature = "alloc")]
@@ -773,6 +783,8 @@ impl fmt::Display for TransactionDecoderError {
773783
write!(f, "coinbase scriptSig too small: {} bytes (min 2)", len),
774784
E::CoinbaseScriptSigTooLarge(len) =>
775785
write!(f, "coinbase scriptSig too large: {} bytes (max 100)", len),
786+
E::DuplicateInput(ref outpoint) =>
787+
write!(f, "duplicate input: {:?}:{}", outpoint.txid, outpoint.vout),
776788
}
777789
}
778790
}
@@ -795,6 +807,7 @@ impl std::error::Error for TransactionDecoderError {
795807
E::NullPrevoutInNonCoinbase(_) => None,
796808
E::CoinbaseScriptSigTooSmall(_) => None,
797809
E::CoinbaseScriptSigTooLarge(_) => None,
810+
E::DuplicateInput(_) => None,
798811
}
799812
}
800813
}
@@ -1608,10 +1621,15 @@ mod tests {
16081621
#[cfg(feature = "alloc")]
16091622
#[cfg(feature = "hex")]
16101623
fn transaction_encode_decode_roundtrip() {
1624+
// Create two different inputs to avoid duplicate input rejection
1625+
let tx_in_1 = segwit_tx_in();
1626+
let mut tx_in_2 = segwit_tx_in();
1627+
tx_in_2.previous_output.vout = 2;
1628+
16111629
let tx = Transaction {
16121630
version: Version::TWO,
16131631
lock_time: absolute::LockTime::ZERO,
1614-
inputs: vec![segwit_tx_in(), segwit_tx_in()],
1632+
inputs: vec![tx_in_1, tx_in_2],
16151633
outputs: vec![tx_out(), tx_out()],
16161634
};
16171635

@@ -1721,11 +1739,16 @@ mod tests {
17211739
#[test]
17221740
#[cfg(feature = "hex")]
17231741
fn transaction_from_hex_str_round_trip() {
1742+
// Create two different inputs to avoid duplicate input rejection
1743+
let tx_in_1 = segwit_tx_in();
1744+
let mut tx_in_2 = segwit_tx_in();
1745+
tx_in_2.previous_output.vout = 2;
1746+
17241747
// Create a transaction and convert it to a hex string
17251748
let tx = Transaction {
17261749
version: Version::TWO,
17271750
lock_time: absolute::LockTime::ZERO,
1728-
inputs: vec![segwit_tx_in(), segwit_tx_in()],
1751+
inputs: vec![tx_in_1, tx_in_2],
17291752
outputs: vec![tx_out(), tx_out()],
17301753
};
17311754

@@ -2452,4 +2475,29 @@ mod tests {
24522475

24532476
assert_eq!(tx.inputs[0].script_sig.len(), 100);
24542477
}
2478+
2479+
#[test]
2480+
#[cfg(all(feature = "alloc", feature = "hex"))]
2481+
fn reject_duplicate_inputs() {
2482+
// Test vector from Bitcoin Core tx_invalid.json:
2483+
// https://github.com/bitcoin/bitcoin/blob/master/src/test/data/tx_invalid.json#L50
2484+
// Transaction has two inputs both spending the same outpoint
2485+
let tx_bytes = hex!("01000000020001000000000000000000000000000000000000000000000000000000000000000000006c47304402204bb1197053d0d7799bf1b30cd503c44b58d6240cccbdc85b6fe76d087980208f02204beeed78200178ffc6c74237bb74b3f276bbb4098b5605d814304fe128bf1431012321039e8815e15952a7c3fada1905f8cf55419837133bd7756c0ef14fc8dfe50c0deaacffffffff0001000000000000000000000000000000000000000000000000000000000000000000006c47304402202306489afef52a6f62e90bf750bbcdf40c06f5c6b138286e6b6b86176bb9341802200dba98486ea68380f47ebb19a7df173b99e6bc9c681d6ccf3bde31465d1f16b3012321039e8815e15952a7c3fada1905f8cf55419837133bd7756c0ef14fc8dfe50c0deaacffffffff010000000000000000015100000000");
2486+
2487+
let mut decoder = Transaction::decoder();
2488+
let mut slice = tx_bytes.as_slice();
2489+
decoder.push_bytes(&mut slice).unwrap();
2490+
let err = decoder.end().expect_err("transaction with duplicate inputs should be rejected");
2491+
2492+
let expected_outpoint = OutPoint {
2493+
txid: Txid::from_byte_array([
2494+
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2495+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2496+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2497+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2498+
]),
2499+
vout: 0,
2500+
};
2501+
assert_eq!(err, TransactionDecoderError(TransactionDecoderErrorInner::DuplicateInput(expected_outpoint)));
2502+
}
24552503
}

0 commit comments

Comments
 (0)