Skip to content

Commit 18855df

Browse files
committed
refactor: use get_nth_argument in implementation
1 parent 25be1e2 commit 18855df

2 files changed

Lines changed: 81 additions & 17 deletions

File tree

clarity/src/vm/docs/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1471,7 +1471,10 @@ passed directly to `verify-merkle-proof` as the leaf hash. The `script` is the r
14711471
P2WSH (`0x00 0x20 ...`), P2TR (`0x51 0x20 ...`), P2WPKH (`0x00 0x14 ...`), OP_RETURN
14721472
(`0x6a ...`), or any other output script.
14731473
1474-
Returns `(err u1)` if the transaction bytes are malformed or `vout` is out of range.
1474+
Returns one of three error codes on failure:
1475+
- `(err u1)` — `tx-bytes` did not deserialize as a Bitcoin transaction.
1476+
- `(err u2)` — `vout` is out of range for this transaction.
1477+
- `(err u3)` — the output's `scriptPubKey` exceeds the 1024-byte cap.
14751478
14761479
This builtin is intended to be paired with `verify-merkle-proof` and the burn-block header
14771480
data exposed by `get-burn-block-info?` to verify that a Bitcoin output exists on-chain

clarity/src/vm/functions/bitcoin.rs

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,52 @@ use crate::vm::{LocalContext, eval};
4646
/// Maximum supported merkle proof depth for `(verify-merkle-proof ...)`.
4747
const VERIFY_MERKLE_PROOF_MAX_DEPTH: u32 = 24;
4848

49+
/// Maximum supported `scriptPubKey` size for `(get-bitcoin-tx-output? ...)`.
50+
const GET_BITCOIN_TX_OUTPUT_MAX_SCRIPT_LEN: usize = 1024;
51+
52+
/// Failure modes of `(get-bitcoin-tx-output? ...)`. Mapped to Clarity `(err
53+
/// uN)` codes so callers can distinguish "the tx didn't parse" from "the tx
54+
/// parsed but the output you asked for doesn't exist" without re-parsing.
55+
#[derive(Debug, PartialEq, Eq)]
56+
enum ParseTxError {
57+
/// Tx bytes failed to deserialize as a Bitcoin transaction, or had
58+
/// trailing bytes after a successful parse.
59+
InvalidTx,
60+
/// `vout` is `>=` the number of outputs in the tx.
61+
VoutOutOfRange,
62+
/// The output's `scriptPubKey` is larger than
63+
/// `GET_BITCOIN_TX_OUTPUT_MAX_SCRIPT_LEN`.
64+
ScriptTooLarge,
65+
}
66+
67+
impl ParseTxError {
68+
/// Clarity `(err uN)` code that this failure is reported as.
69+
fn as_error_code(&self) -> u128 {
70+
match self {
71+
ParseTxError::InvalidTx => 1,
72+
ParseTxError::VoutOutOfRange => 2,
73+
ParseTxError::ScriptTooLarge => 3,
74+
}
75+
}
76+
}
77+
4978
/// Parse a Bitcoin transaction (SegWit or non-SegWit) and pluck the output at
5079
/// `vout`, along with the canonical (non-witness) txid in internal byte order.
51-
///
52-
/// Returns `None` if the bytes don't form a valid Bitcoin tx, if `vout` is
53-
/// out of range, or if there are trailing bytes after the tx.
54-
fn parse_tx_output(raw: &[u8], vout: u64) -> Option<(Vec<u8>, u64, [u8; 32])> {
55-
let tx: Transaction = btc_deserialize(raw).ok()?;
56-
let vout_idx = usize::try_from(vout).ok()?;
57-
let txout = tx.output.get(vout_idx)?;
80+
fn parse_tx_output(raw: &[u8], vout: u64) -> Result<(Vec<u8>, u64, [u8; 32]), ParseTxError> {
81+
let tx: Transaction = btc_deserialize(raw).map_err(|_| ParseTxError::InvalidTx)?;
82+
let vout_idx = usize::try_from(vout).map_err(|_| ParseTxError::VoutOutOfRange)?;
83+
let txout = tx
84+
.output
85+
.get(vout_idx)
86+
.ok_or(ParseTxError::VoutOutOfRange)?;
5887
let script_bytes = txout.script_pubkey.as_bytes();
59-
if script_bytes.len() > 1024 {
60-
return None;
88+
if script_bytes.len() > GET_BITCOIN_TX_OUTPUT_MAX_SCRIPT_LEN {
89+
return Err(ParseTxError::ScriptTooLarge);
6190
}
6291
let script = script_bytes.to_vec();
6392
let amount = txout.value;
6493
let txid = tx.txid().0;
65-
Some((script, amount, txid))
94+
Ok((script, amount, txid))
6695
}
6796

6897
/// Canonical Bitcoin merkle-tree depth for a block containing `tx_count`
@@ -244,7 +273,10 @@ pub fn special_verify_merkle_proof(
244273
/// `(get-bitcoin-tx-output? tx-bytes vout)` returns
245274
/// `(response { script: (buff 1024), amount: uint, txid: (buff 32) } uint)`,
246275
/// where the txid is in internal byte order (ready for `verify-merkle-proof`).
247-
/// On any parse failure or out-of-range vout, returns `(err u1)`.
276+
/// On failure, returns one of:
277+
/// - `(err u1)` — `tx-bytes` did not deserialize as a Bitcoin transaction.
278+
/// - `(err u2)` — `vout` is out of range for this tx.
279+
/// - `(err u3)` — the output's `scriptPubKey` exceeds the 1024-byte cap.
248280
pub fn special_get_bitcoin_tx_output(
249281
args: &[SymbolicExpression],
250282
exec_state: &mut ExecutionState,
@@ -285,11 +317,15 @@ pub fn special_get_bitcoin_tx_output(
285317

286318
let vout_u64 = match u64::try_from(vout) {
287319
Ok(v) => v,
288-
Err(_) => return Ok(Value::error(Value::UInt(1))?),
320+
Err(_) => {
321+
return Ok(Value::error(Value::UInt(
322+
ParseTxError::VoutOutOfRange.as_error_code(),
323+
))?);
324+
}
289325
};
290326

291327
let result = match parse_tx_output(&tx_bytes, vout_u64) {
292-
Some((script, amount, txid)) => {
328+
Ok((script, amount, txid)) => {
293329
let tuple = TupleData::from_data(vec![
294330
(
295331
ClarityName::from_literal("script"),
@@ -311,7 +347,7 @@ pub fn special_get_bitcoin_tx_output(
311347
})?;
312348
Value::okay(Value::Tuple(tuple))?
313349
}
314-
None => Value::error(Value::UInt(1))?,
350+
Err(e) => Value::err_uint(e.as_error_code()),
315351
};
316352

317353
Ok(result)
@@ -368,13 +404,38 @@ mod tests {
368404
#[test]
369405
fn parse_rejects_out_of_range_vout() {
370406
let raw = hex(SAMPLE_TX_HEX);
371-
assert!(parse_tx_output(&raw, 1).is_none());
407+
assert_eq!(parse_tx_output(&raw, 1), Err(ParseTxError::VoutOutOfRange));
372408
}
373409

374410
#[test]
375411
fn parse_rejects_truncated_tx() {
376412
let raw = hex(SAMPLE_TX_HEX);
377-
assert!(parse_tx_output(&raw[..raw.len() - 1], 0).is_none());
413+
assert_eq!(
414+
parse_tx_output(&raw[..raw.len() - 1], 0),
415+
Err(ParseTxError::InvalidTx),
416+
);
417+
}
418+
419+
#[test]
420+
fn parse_rejects_oversized_script() {
421+
// Build a tx with a single output whose scriptPubKey is 1025 bytes
422+
// (one byte over the cap). The serialized length prefix uses a
423+
// CompactSize: 0xfd 0x01 0x04 = 0x401 = 1025.
424+
let mut raw = hex(concat!(
425+
"01000000", // version
426+
"01", // n_in
427+
"0000000000000000000000000000000000000000000000000000000000000000", // prev txid
428+
"00000000", // prev vout
429+
"00", // scriptSig len
430+
"ffffffff", // sequence
431+
"01", // n_out
432+
"e803000000000000", // amount
433+
"fd0104", // script len = 1025
434+
));
435+
raw.extend(std::iter::repeat_n(0x51u8, 1025)); // 1025 bytes of OP_1
436+
raw.extend_from_slice(&hex("00000000")); // locktime
437+
438+
assert_eq!(parse_tx_output(&raw, 0), Err(ParseTxError::ScriptTooLarge),);
378439
}
379440

380441
#[test]

0 commit comments

Comments
 (0)