clementine_tx_sender/citrea/
reveal_scripts.rs1use bitcoin::blockdata::opcodes::all::{OP_CHECKSIGVERIFY, OP_ENDIF, OP_IF, OP_NIP};
4use bitcoin::blockdata::opcodes::OP_FALSE;
5use bitcoin::blockdata::script;
6use bitcoin::script::PushBytesBuf;
7use bitcoin::secp256k1::{All, Secp256k1};
8use bitcoin::taproot::{ControlBlock, LeafVersion, TaprootBuilder};
9use bitcoin::{Address, Network, ScriptBuf, XOnlyPublicKey};
10
11use crate::citrea::TransactionKind;
12use crate::signer::TxSenderSigningKey;
13use crate::TxSender;
14
15#[derive(Debug, Clone)]
17pub struct CitreaSigningData {
18 pub reveal_script: ScriptBuf,
19 pub control_block: ControlBlock,
20 pub commit_address: Address,
21}
22
23impl TxSender {
24 pub fn create_reveal_script(
45 &self,
46 transaction_kind: TransactionKind,
47 body: &[u8],
48 ) -> CitreaSigningData {
49 create_reveal_script(
50 self.xonly_public_key(),
51 &self.da_signer,
52 self.network,
53 transaction_kind,
54 body,
55 )
56 }
57}
58
59fn create_reveal_script(
60 public_key: XOnlyPublicKey,
61 da_signer: &TxSenderSigningKey,
62 network: Network,
63 transaction_kind: TransactionKind,
64 body: &[u8],
65) -> CitreaSigningData {
66 let kind_bytes = transaction_kind.to_bytes();
67
68 let nonce: i64 = 16;
70
71 let needs_signature = matches!(
73 transaction_kind,
74 TransactionKind::Complete
75 | TransactionKind::SequencerCommitment
76 | TransactionKind::Aggregate
77 );
78
79 let mut reveal_script_builder = script::Builder::new()
80 .push_x_only_key(&public_key)
81 .push_opcode(OP_CHECKSIGVERIFY)
82 .push_slice(PushBytesBuf::from(kind_bytes))
83 .push_opcode(OP_FALSE)
84 .push_opcode(OP_IF);
85
86 if needs_signature {
88 let (signature, signer_public_key) = da_signer.sign_blob(body);
89 reveal_script_builder = reveal_script_builder
90 .push_slice(PushBytesBuf::try_from(signature).expect("Cannot push signature"))
91 .push_slice(
92 PushBytesBuf::try_from(signer_public_key).expect("Cannot push signer public key"),
93 );
94 }
95
96 for chunk in body.chunks(520) {
98 reveal_script_builder = reveal_script_builder
99 .push_slice(PushBytesBuf::try_from(chunk.to_vec()).expect("Cannot push body chunk"));
100 }
101
102 reveal_script_builder = reveal_script_builder
104 .push_opcode(OP_ENDIF)
105 .push_slice(nonce.to_le_bytes())
106 .push_opcode(OP_NIP);
107
108 let reveal_script = reveal_script_builder.into_script();
109
110 let secp = Secp256k1::<All>::new();
112 let taproot_spend_info = TaprootBuilder::new()
113 .add_leaf(0, reveal_script.clone())
114 .expect("Cannot add reveal script to taptree")
115 .finalize(&secp, public_key)
116 .expect("Cannot finalize taptree");
117
118 let control_block = taproot_spend_info
119 .control_block(&(reveal_script.clone(), LeafVersion::TapScript))
120 .expect("Cannot create control block");
121
122 let merkle_root = taproot_spend_info.merkle_root();
123 let commit_address = Address::p2tr(&secp, public_key, merkle_root, network);
124
125 CitreaSigningData {
126 reveal_script,
127 control_block,
128 commit_address,
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use bitcoin::absolute::LockTime;
135 use bitcoin::hashes::Hash as _;
136 use bitcoin::secp256k1::SecretKey;
137 use bitcoin::transaction::Version;
138 use bitcoin::{
139 Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness,
140 };
141 use clementine_primitives::MIN_TAPROOT_AMOUNT;
142
143 use crate::citrea::data_serialization::DataOnDa;
144 use crate::citrea::{TransactionKind, MAX_CHUNK_SIZE};
145 use crate::signer::TxSenderSigningKey;
146
147 use super::create_reveal_script;
148
149 const TAPROOT_INPUT_COUNT: usize = 10;
150 const TAPROOT_OUTPUT_COUNT: usize = 2;
151 const STANDARD_TX_MAX_VBYTES: u64 = 100_000;
152
153 #[test]
154 fn max_chunk_reveal_transaction_stays_under_standard_weight() {
155 let secret_key = SecretKey::from_slice(&[1u8; 32]).expect("valid test secret key");
156 let signer = TxSenderSigningKey::new(secret_key, Network::Regtest);
157
158 let raw_chunk = vec![0u8; MAX_CHUNK_SIZE as usize];
159 let reveal_body =
160 borsh::to_vec(&DataOnDa::Chunk(raw_chunk)).expect("max chunk DA body must serialize");
161 assert!(
162 reveal_body.len() > MAX_CHUNK_SIZE as usize,
163 "test must cover the final Borsh-wrapped reveal body"
164 );
165
166 let signing_data = create_reveal_script(
167 signer.xonly_public_key(),
168 &signer,
169 Network::Regtest,
170 TransactionKind::Chunks,
171 &reveal_body,
172 );
173
174 let mut inputs: Vec<TxIn> = (0..TAPROOT_INPUT_COUNT)
175 .map(|vout| TxIn {
176 previous_output: OutPoint {
177 txid: Txid::all_zeros(),
178 vout: vout as u32,
179 },
180 script_sig: ScriptBuf::new(),
181 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
182 witness: Witness::new(),
183 })
184 .collect();
185
186 let mut reveal_witness = Witness::new();
187 reveal_witness.push([0u8; 65]);
188 reveal_witness.push(signing_data.reveal_script);
189 reveal_witness.push(signing_data.control_block.serialize());
190 inputs[0].witness = reveal_witness;
191
192 for input in inputs.iter_mut().skip(1) {
193 input.witness.push([0u8; 65]);
194 }
195
196 let script_pubkey = signer.address().script_pubkey();
197 let outputs = (0..TAPROOT_OUTPUT_COUNT)
198 .map(|_| TxOut {
199 value: MIN_TAPROOT_AMOUNT,
200 script_pubkey: script_pubkey.clone(),
201 })
202 .collect();
203
204 let reveal_tx = Transaction {
205 version: Version::TWO,
206 lock_time: LockTime::ZERO,
207 input: inputs,
208 output: outputs,
209 };
210
211 let weight = reveal_tx.weight().to_wu();
212 let vbytes = reveal_tx.weight().to_vbytes_ceil();
213 println!("weight: {weight}, vbytes: {vbytes}");
214 assert!(
215 vbytes < STANDARD_TX_MAX_VBYTES,
216 "max chunk reveal transaction is {weight} WU / {vbytes} vB"
217 );
218 }
219}