Skip to content

Commit 2bd536f

Browse files
committed
fix(types): use bounds-checked access in Utxo::txout() for Foreign variant
Replace unchecked array indexing of `prev_tx.output[outpoint.vout]` with `.get().expect()` to provide a descriptive panic message instead of an opaque index-out-of-bounds if the outpoint vout is invalid. Co-Authored-By: HAL 9000 Signed-off-by: Elias Rohrer <dev@tnull.de>
1 parent fb7681a commit 2bd536f

1 file changed

Lines changed: 95 additions & 1 deletion

File tree

src/types.rs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ impl Utxo {
124124
..
125125
} => {
126126
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
127-
return &prev_tx.output[outpoint.vout as usize];
127+
return prev_tx
128+
.output
129+
.get(outpoint.vout as usize)
130+
.expect("outpoint vout must be in bounds");
128131
}
129132

130133
if let Some(txout) = &psbt_input.witness_utxo {
@@ -172,3 +175,94 @@ impl fmt::Display for IndexOutOfBoundsError {
172175
}
173176

174177
impl core::error::Error for IndexOutOfBoundsError {}
178+
179+
#[cfg(test)]
180+
mod test {
181+
use super::*;
182+
use alloc::string::String;
183+
184+
use bitcoin::absolute::LockTime;
185+
use bitcoin::transaction::{OutPoint, Sequence, TxOut, Version};
186+
use bitcoin::{psbt, Amount, ScriptBuf, Transaction};
187+
188+
#[test]
189+
fn test_foreign_utxo_txout_with_valid_vout() {
190+
let prev_tx = Transaction {
191+
version: Version::TWO,
192+
lock_time: LockTime::ZERO,
193+
input: vec![],
194+
output: vec![
195+
TxOut {
196+
value: Amount::from_sat(1_000),
197+
script_pubkey: ScriptBuf::new(),
198+
},
199+
TxOut {
200+
value: Amount::from_sat(2_000),
201+
script_pubkey: ScriptBuf::new(),
202+
},
203+
],
204+
};
205+
206+
let outpoint = OutPoint {
207+
txid: prev_tx.compute_txid(),
208+
vout: 1,
209+
};
210+
211+
let psbt_input = psbt::Input {
212+
non_witness_utxo: Some(prev_tx),
213+
..Default::default()
214+
};
215+
216+
let utxo = Utxo::Foreign {
217+
outpoint,
218+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
219+
psbt_input: Box::new(psbt_input),
220+
};
221+
222+
let txout = utxo.txout();
223+
assert_eq!(txout.value, Amount::from_sat(2_000));
224+
}
225+
226+
#[test]
227+
fn test_foreign_utxo_txout_with_invalid_vout_panics_with_message() {
228+
let prev_tx = Transaction {
229+
version: Version::TWO,
230+
lock_time: LockTime::ZERO,
231+
input: vec![],
232+
output: vec![TxOut {
233+
value: Amount::from_sat(1_000),
234+
script_pubkey: ScriptBuf::new(),
235+
}],
236+
};
237+
238+
let outpoint = OutPoint {
239+
txid: prev_tx.compute_txid(),
240+
vout: 5, // out of bounds: only 1 output
241+
};
242+
243+
let psbt_input = psbt::Input {
244+
non_witness_utxo: Some(prev_tx),
245+
..Default::default()
246+
};
247+
248+
let utxo = Utxo::Foreign {
249+
outpoint,
250+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
251+
psbt_input: Box::new(psbt_input),
252+
};
253+
254+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
255+
utxo.txout();
256+
}));
257+
let err = result.expect_err("txout() must panic for out-of-bounds vout");
258+
let msg = err
259+
.downcast_ref::<&str>()
260+
.copied()
261+
.or_else(|| err.downcast_ref::<String>().map(|s| s.as_str()))
262+
.expect("panic payload must be a string");
263+
assert!(
264+
msg.contains("outpoint vout must be in bounds"),
265+
"expected descriptive panic message, got: {msg}"
266+
);
267+
}
268+
}

0 commit comments

Comments
 (0)