Skip to content

Commit 4c0ab73

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 4c0ab73

1 file changed

Lines changed: 91 additions & 1 deletion

File tree

src/types.rs

Lines changed: 91 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,90 @@ 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 mut psbt_input = psbt::Input::default();
212+
psbt_input.non_witness_utxo = Some(prev_tx);
213+
214+
let utxo = Utxo::Foreign {
215+
outpoint,
216+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
217+
psbt_input: Box::new(psbt_input),
218+
};
219+
220+
let txout = utxo.txout();
221+
assert_eq!(txout.value, Amount::from_sat(2_000));
222+
}
223+
224+
#[test]
225+
fn test_foreign_utxo_txout_with_invalid_vout_panics_with_message() {
226+
let prev_tx = Transaction {
227+
version: Version::TWO,
228+
lock_time: LockTime::ZERO,
229+
input: vec![],
230+
output: vec![TxOut {
231+
value: Amount::from_sat(1_000),
232+
script_pubkey: ScriptBuf::new(),
233+
}],
234+
};
235+
236+
let outpoint = OutPoint {
237+
txid: prev_tx.compute_txid(),
238+
vout: 5, // out of bounds: only 1 output
239+
};
240+
241+
let mut psbt_input = psbt::Input::default();
242+
psbt_input.non_witness_utxo = Some(prev_tx);
243+
244+
let utxo = Utxo::Foreign {
245+
outpoint,
246+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
247+
psbt_input: Box::new(psbt_input),
248+
};
249+
250+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
251+
utxo.txout();
252+
}));
253+
let err = result.expect_err("txout() must panic for out-of-bounds vout");
254+
let msg = err
255+
.downcast_ref::<&str>()
256+
.copied()
257+
.or_else(|| err.downcast_ref::<String>().map(|s| s.as_str()))
258+
.expect("panic payload must be a string");
259+
assert!(
260+
msg.contains("outpoint vout must be in bounds"),
261+
"expected descriptive panic message, got: {msg}"
262+
);
263+
}
264+
}

0 commit comments

Comments
 (0)