Skip to content

Commit 050f9bd

Browse files
committed
feat(elements): pass taproot annex to C FFI transaction construction
Previously the annex field was always null in the C FFI input struct. This extracts the annex from the witness stack and passes it through, with careful lifetime management using a boxed slice to prevent reallocation before the FFI call completes.
1 parent fe1f888 commit 050f9bd

1 file changed

Lines changed: 39 additions & 10 deletions

File tree

src/jet/elements/c_env.rs

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! High level APIs for creating C FFI compatible environment.
44
//!
55
6+
use bitcoin::taproot::TAPROOT_ANNEX_PREFIX;
67
use hashes::Hash;
78
use std::os::raw::c_uchar;
89

@@ -33,7 +34,6 @@ struct RawOutputData {
3334
/// passed to the C FFI.
3435
#[derive(Debug)]
3536
struct RawInputData {
36-
#[allow(dead_code)] // see FIXME below
3737
pub annex: Option<Vec<c_uchar>>,
3838
// pegin
3939
pub genesis_hash: Option<[c_uchar; 32]>,
@@ -73,10 +73,10 @@ fn new_raw_input<'raw>(
7373
inp: &'raw elements::TxIn,
7474
in_utxo: &'raw ElementsUtxo,
7575
inp_data: &'raw RawInputData,
76+
annex: *const c_elements::CRawBuffer,
7677
) -> c_elements::CRawInput<'raw> {
7778
c_elements::CRawInput {
78-
// FIXME actually pass the annex in; see https://github.com/BlockstreamResearch/simplicity/issues/311 for some difficulty here.
79-
annex: core::ptr::null(),
79+
annex,
8080
prev_txid: inp.previous_output.txid.as_ref(),
8181
pegin: inp_data.genesis_hash.as_ref(),
8282
issuance: if inp.has_issuance() {
@@ -114,7 +114,7 @@ fn new_tx_data(tx: &elements::Transaction, in_utxos: &[ElementsUtxo]) -> RawTran
114114
};
115115
for (inp, in_utxo) in tx.input.iter().zip(in_utxos.iter()) {
116116
let inp_data = RawInputData {
117-
annex: None, // Actually store annex
117+
annex: get_annex(&inp.witness).map(|s| s.to_vec()),
118118
genesis_hash: inp
119119
.pegin_data()
120120
.map(|x| x.genesis_hash.to_raw_hash().to_byte_array()),
@@ -148,15 +148,28 @@ pub(super) fn new_tx(
148148
) -> *mut c_elements::CTransaction {
149149
let mut raw_inputs = Vec::new();
150150
let mut raw_outputs = Vec::new();
151+
152+
// SAFETY: this allocation *must* live until after the `simplicity_mallocTransaction`
153+
// at the bottom of this function. We convert the vector to a boxed slice to ensure
154+
// it cannot be resized, which would potentially trigger a reallocation.
155+
let mut raw_annexes = Vec::from_iter((0..tx.input.len()).map(|_| None)).into_boxed_slice();
156+
151157
let txid = tx.txid();
152158
let tx_data = new_tx_data(tx, in_utxos);
153-
for ((inp, in_utxo), inp_data) in tx
154-
.input
155-
.iter()
159+
for (((raw_annex, inp), in_utxo), inp_data) in raw_annexes
160+
.iter_mut()
161+
.zip(tx.input.iter())
156162
.zip(in_utxos.iter())
157163
.zip(tx_data.inputs.iter())
158164
{
159-
let res = new_raw_input(inp, in_utxo, inp_data);
165+
*raw_annex = inp_data
166+
.annex
167+
.as_ref()
168+
.map(|annex| c_elements::CRawBuffer::new(annex));
169+
let annex_ptr = raw_annex
170+
.as_ref()
171+
.map_or(core::ptr::null(), |b| b as *const _);
172+
let res = new_raw_input(inp, in_utxo, inp_data, annex_ptr);
160173
raw_inputs.push(res);
161174
}
162175
for (out, out_data) in tx.output.iter().zip(tx_data.outputs.iter()) {
@@ -172,10 +185,16 @@ pub(super) fn new_tx(
172185
version: tx.version,
173186
locktime: tx.lock_time.to_consensus_u32(),
174187
};
175-
unsafe {
188+
let ret = unsafe {
176189
// SAFETY: this is a FFI call and we constructed its argument correctly.
177190
c_elements::simplicity_mallocTransaction(&c_raw_tx)
178-
}
191+
};
192+
193+
// Explicitly drop raw_annexes so Rust doesn't try any funny business dropping it early.
194+
// Drop raw_inputs first since it contains pointers into raw_annexes
195+
drop(raw_inputs);
196+
drop(raw_annexes);
197+
ret
179198
}
180199

181200
pub(super) fn new_tap_env(
@@ -256,3 +275,13 @@ fn serialize_surjection_proof(surjection_proof: &Option<Box<SurjectionProof>>) -
256275
.map(|x| x.serialize())
257276
.unwrap_or_default()
258277
}
278+
279+
/// If the last item in the witness stack is an annex, return the data following the 0x50 byte.
280+
fn get_annex(in_witness: &elements::TxInWitness) -> Option<&[u8]> {
281+
let last_item = in_witness.script_witness.last()?;
282+
if *last_item.first()? == TAPROOT_ANNEX_PREFIX {
283+
Some(&last_item[1..])
284+
} else {
285+
None
286+
}
287+
}

0 commit comments

Comments
 (0)