1use crate::Error;
2use crate::ErrorContext;
3use crate::VTXO_CONDITION_KEY;
4use crate::VTXO_TAPROOT_KEY;
5use bitcoin::absolute;
6use bitcoin::base64;
7use bitcoin::base64::Engine;
8use bitcoin::hashes::sha256;
9use bitcoin::hashes::Hash;
10use bitcoin::opcodes::all::*;
11use bitcoin::psbt;
12use bitcoin::psbt::PsbtSighashType;
13use bitcoin::secp256k1;
14use bitcoin::secp256k1::schnorr;
15use bitcoin::secp256k1::PublicKey;
16use bitcoin::sighash::Prevouts;
17use bitcoin::sighash::SighashCache;
18use bitcoin::taproot;
19use bitcoin::transaction::Version;
20use bitcoin::Amount;
21use bitcoin::OutPoint;
22use bitcoin::Psbt;
23use bitcoin::ScriptBuf;
24use bitcoin::Sequence;
25use bitcoin::TapLeafHash;
26use bitcoin::TapSighashType;
27use bitcoin::Transaction;
28use bitcoin::TxIn;
29use bitcoin::TxOut;
30use bitcoin::Txid;
31use bitcoin::Witness;
32use bitcoin::XOnlyPublicKey;
33use serde::Deserialize;
34use serde::Serialize;
35
36#[derive(Clone, Debug)]
37pub struct Input {
38 outpoint: OutPoint,
40 sequence: Sequence,
42 locktime: absolute::LockTime,
44 witness_utxo: TxOut,
45 tapscripts: Vec<ScriptBuf>,
47 spend_info: (ScriptBuf, taproot::ControlBlock),
48 is_onchain: bool,
49 is_swept: bool,
50 extra_witness: Option<Vec<Vec<u8>>>,
53}
54
55impl Input {
56 pub fn new(
57 outpoint: OutPoint,
58 sequence: Sequence,
59 locktime: Option<absolute::LockTime>,
60 witness_utxo: TxOut,
61 tapscripts: Vec<ScriptBuf>,
62 spend_info: (ScriptBuf, taproot::ControlBlock),
63 is_onchain: bool,
64 is_swept: bool,
65 ) -> Self {
66 Self {
67 outpoint,
68 sequence,
69 locktime: locktime.unwrap_or(absolute::LockTime::ZERO),
70 witness_utxo,
71 tapscripts,
72 spend_info,
73 is_onchain,
74 is_swept,
75 extra_witness: None,
76 }
77 }
78
79 pub fn new_with_extra_witness(
81 outpoint: OutPoint,
82 sequence: Sequence,
83 locktime: Option<absolute::LockTime>,
84 witness_utxo: TxOut,
85 tapscripts: Vec<ScriptBuf>,
86 spend_info: (ScriptBuf, taproot::ControlBlock),
87 is_onchain: bool,
88 is_swept: bool,
89 extra_witness: Vec<Vec<u8>>,
90 ) -> Self {
91 Self {
92 outpoint,
93 sequence,
94 locktime: locktime.unwrap_or(absolute::LockTime::ZERO),
95 witness_utxo,
96 tapscripts,
97 spend_info,
98 is_onchain,
99 is_swept,
100 extra_witness: Some(extra_witness),
101 }
102 }
103
104 pub fn script_pubkey(&self) -> &ScriptBuf {
105 &self.witness_utxo.script_pubkey
106 }
107
108 pub fn amount(&self) -> Amount {
109 self.witness_utxo.value
110 }
111
112 pub fn spend_info(&self) -> &(ScriptBuf, taproot::ControlBlock) {
113 &self.spend_info
114 }
115
116 pub fn outpoint(&self) -> OutPoint {
117 self.outpoint
118 }
119
120 pub fn tapscripts(&self) -> &[ScriptBuf] {
121 &self.tapscripts
122 }
123
124 pub fn is_swept(&self) -> bool {
125 self.is_swept
126 }
127
128 pub fn extra_witness(&self) -> Option<&[Vec<u8>]> {
129 self.extra_witness.as_deref()
130 }
131}
132
133#[derive(Debug, Clone)]
134pub enum Output {
135 Offchain(TxOut),
137 Onchain(TxOut),
139}
140
141#[derive(Debug, Clone)]
142pub struct Intent {
143 pub proof: Psbt,
144 message: IntentMessage,
145}
146
147impl Intent {
148 pub fn new(proof: Psbt, message: IntentMessage) -> Self {
149 Self { proof, message }
150 }
151
152 pub fn serialize_proof(&self) -> String {
153 let base64 = base64::engine::GeneralPurpose::new(
154 &base64::alphabet::STANDARD,
155 base64::engine::GeneralPurposeConfig::new(),
156 );
157
158 let bytes = self.proof.serialize();
159
160 base64.encode(&bytes)
161 }
162
163 pub fn serialize_message(&self) -> Result<String, Error> {
164 self.message.encode()
165 }
166}
167
168pub fn make_intent<SV, SO>(
169 sign_for_vtxo_fn: SV,
170 sign_for_onchain_fn: SO,
171 inputs: Vec<Input>,
172 outputs: Vec<Output>,
173 message: IntentMessage,
174) -> Result<Intent, Error>
175where
176 SV: Fn(
177 &mut psbt::Input,
178 secp256k1::Message,
179 ) -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, Error>,
180 SO: Fn(
181 &mut psbt::Input,
182 secp256k1::Message,
183 ) -> Result<(schnorr::Signature, XOnlyPublicKey), Error>,
184{
185 let (mut proof_psbt, fake_input) = build_proof_psbt(&message, &inputs, &outputs)?;
186
187 for (i, proof_input) in proof_psbt.inputs.iter_mut().enumerate() {
188 if i == 0 {
189 let (script, control_block) = inputs[0].spend_info.clone();
190
191 proof_input
192 .tap_scripts
193 .insert(control_block, (script, taproot::LeafVersion::TapScript));
194 } else {
195 let (script, control_block) = inputs[i - 1].spend_info.clone();
196
197 let tap_tree = taptree::TapTree(inputs[i - 1].tapscripts.clone());
198 let bytes = tap_tree
199 .encode()
200 .map_err(Error::ad_hoc)
201 .with_context(|| format!("failed to encode taptree for input {i}"))?;
202
203 proof_input.unknown.insert(
204 psbt::raw::Key {
205 type_value: 222,
206 key: VTXO_TAPROOT_KEY.to_vec(),
207 },
208 bytes,
209 );
210 proof_input
211 .tap_scripts
212 .insert(control_block, (script, taproot::LeafVersion::TapScript));
213 };
214 }
215
216 let prevouts = proof_psbt
217 .inputs
218 .iter()
219 .filter_map(|i| i.witness_utxo.clone())
220 .collect::<Vec<_>>();
221
222 let inputs = [inputs, vec![fake_input]].concat();
223
224 for (i, proof_input) in proof_psbt.inputs.iter_mut().enumerate() {
225 let input = inputs
226 .iter()
227 .find(|input| input.outpoint == proof_psbt.unsigned_tx.input[i].previous_output)
228 .expect("witness utxo");
229
230 let prevouts = Prevouts::All(&prevouts);
231
232 let (_, (script, leaf_version)) =
233 proof_input.tap_scripts.first_key_value().expect("a value");
234
235 let leaf_hash = TapLeafHash::from_script(script, *leaf_version);
236
237 let tap_sighash = SighashCache::new(&proof_psbt.unsigned_tx)
238 .taproot_script_spend_signature_hash(i, &prevouts, leaf_hash, TapSighashType::Default)
239 .map_err(Error::crypto)
240 .with_context(|| format!("failed to compute sighash for proof of funds input {i}"))?;
241
242 let msg = secp256k1::Message::from_digest(tap_sighash.to_raw_hash().to_byte_array());
243
244 if let Some(extra_witness) = input.extra_witness() {
246 let encoded = encode_witness(extra_witness);
249 proof_input.unknown.insert(
250 psbt::raw::Key {
251 type_value: 222,
252 key: VTXO_CONDITION_KEY.to_vec(),
253 },
254 encoded,
255 );
256 } else {
257 let sigs = match input.is_onchain {
259 true => vec![sign_for_onchain_fn(proof_input, msg)?],
260 false => sign_for_vtxo_fn(proof_input, msg)?,
261 };
262
263 for (sig, pk) in sigs {
264 let sig = taproot::Signature {
265 signature: sig,
266 sighash_type: TapSighashType::Default,
267 };
268 proof_input.tap_script_sigs.insert((pk, leaf_hash), sig);
269 }
270 }
271 }
272
273 Ok(Intent {
274 proof: proof_psbt,
275 message,
276 })
277}
278
279pub(crate) fn build_proof_psbt(
280 message: &IntentMessage,
281 inputs: &[Input],
282 outputs: &[Output],
283) -> Result<(Psbt, Input), Error> {
284 if inputs.is_empty() {
285 return Err(Error::ad_hoc("missing inputs"));
286 }
287
288 let message = message
289 .encode()
290 .map_err(Error::ad_hoc)
291 .context("failed to encode intent message")?;
292
293 let first_input = inputs[0].clone();
294 let script_pubkey = first_input.witness_utxo.script_pubkey.clone();
295
296 let to_spend_tx = {
297 let hash = message_hash(message.as_bytes());
298
299 let script_sig = ScriptBuf::builder()
300 .push_opcode(OP_PUSHBYTES_0)
301 .push_slice(hash.as_byte_array())
302 .into_script();
303
304 let output = TxOut {
305 value: Amount::ZERO,
306 script_pubkey,
307 };
308
309 Transaction {
310 version: Version::non_standard(0),
311 lock_time: absolute::LockTime::ZERO,
312 input: vec![TxIn {
313 previous_output: OutPoint {
314 txid: Txid::all_zeros(),
315 vout: 0xFFFFFFFF,
316 },
317 script_sig,
318 sequence: Sequence::ZERO,
319 witness: Witness::default(),
320 }],
321 output: vec![output],
322 }
323 };
324
325 let fake_outpoint = OutPoint {
326 txid: to_spend_tx.compute_txid(),
327 vout: 0,
328 };
329
330 let to_sign_psbt = {
331 let mut to_sign_inputs = Vec::with_capacity(inputs.len() + 1);
332
333 to_sign_inputs.push(TxIn {
334 previous_output: fake_outpoint,
335 script_sig: ScriptBuf::new(),
336 sequence: first_input.sequence,
337 witness: Witness::default(),
338 });
339
340 for input in inputs.iter() {
341 to_sign_inputs.push(TxIn {
342 previous_output: input.outpoint,
343 script_sig: ScriptBuf::new(),
344 sequence: input.sequence,
345 witness: Witness::default(),
346 });
347 }
348
349 let outputs = match outputs.len() {
350 0 => vec![TxOut {
351 value: Amount::ZERO,
352 script_pubkey: ScriptBuf::new_op_return([]),
353 }],
354 _ => outputs
355 .iter()
356 .map(|o| match o {
357 Output::Offchain(txout) | Output::Onchain(txout) => txout.clone(),
358 })
359 .collect::<Vec<_>>(),
360 };
361
362 let tx = Transaction {
363 version: Version::TWO,
364 lock_time: inputs
365 .iter()
366 .map(|i| i.locktime)
367 .max_by(|a, b| a.to_consensus_u32().cmp(&b.to_consensus_u32()))
368 .unwrap_or(absolute::LockTime::ZERO),
369 input: to_sign_inputs,
370 output: outputs,
371 };
372
373 let mut psbt = Psbt::from_unsigned_tx(tx)
374 .map_err(Error::ad_hoc)
375 .context("failed to build proof of funds PSBT")?;
376
377 psbt.inputs[0].witness_utxo = Some(to_spend_tx.output[0].clone());
378 psbt.inputs[0].sighash_type = Some(PsbtSighashType::from_u32(1));
379 psbt.inputs[0].witness_script = Some(inputs[0].spend_info.0.clone());
380
381 for (i, input) in inputs.iter().enumerate() {
382 psbt.inputs[i + 1].witness_utxo = Some(input.witness_utxo.clone());
383 psbt.inputs[i + 1].sighash_type = Some(PsbtSighashType::from_u32(1));
384 psbt.inputs[i + 1].witness_script = Some(input.spend_info.0.clone());
385 }
386
387 psbt
388 };
389
390 let mut first_input_modified = first_input;
391 first_input_modified.outpoint = fake_outpoint;
392
393 Ok((to_sign_psbt, first_input_modified))
394}
395
396fn message_hash(message: &[u8]) -> sha256::Hash {
397 const TAG: &[u8] = b"ark-intent-proof-message";
398
399 let hashed_tag = sha256::Hash::hash(TAG);
400
401 let mut v = Vec::new();
402 v.extend_from_slice(hashed_tag.as_byte_array());
403 v.extend_from_slice(hashed_tag.as_byte_array());
404 v.extend_from_slice(message);
405
406 sha256::Hash::hash(&v)
407}
408
409#[derive(Serialize, Deserialize, Debug, Clone)]
410#[serde(tag = "type")]
411pub enum IntentMessage {
412 #[serde(rename = "register")]
413 Register {
414 onchain_output_indexes: Vec<usize>,
415 valid_at: u64,
416 expire_at: u64,
417 #[serde(rename = "cosigners_public_keys")]
418 own_cosigner_pks: Vec<PublicKey>,
419 },
420 #[serde(rename = "delete")]
421 Delete { expire_at: u64 },
422 #[serde(rename = "estimate-intent-fee")]
423 EstimateIntentFee {
424 onchain_output_indexes: Vec<usize>,
425 valid_at: u64,
426 expire_at: u64,
427 #[serde(rename = "cosigners_public_keys")]
428 own_cosigner_pks: Vec<PublicKey>,
429 },
430 #[serde(rename = "get-pending-tx")]
431 GetPendingTx { expire_at: u64 },
432}
433
434impl IntentMessage {
435 pub fn encode(&self) -> Result<String, Error> {
436 serde_json::to_string(self)
437 .map_err(Error::ad_hoc)
438 .context("failed to serialize intent message to JSON")
439 }
440}
441
442fn encode_witness(elements: &[Vec<u8>]) -> Vec<u8> {
446 let mut result = Vec::new();
447
448 write_compact_size(&mut result, elements.len() as u64);
450
451 for elem in elements {
453 write_compact_size(&mut result, elem.len() as u64);
454 result.extend_from_slice(elem);
455 }
456
457 result
458}
459
460fn write_compact_size(w: &mut Vec<u8>, val: u64) {
462 if val < 253 {
463 w.push(val as u8);
464 } else if val < 0x10000 {
465 w.push(253);
466 w.extend_from_slice(&(val as u16).to_le_bytes());
467 } else if val < 0x100000000 {
468 w.push(254);
469 w.extend_from_slice(&(val as u32).to_le_bytes());
470 } else {
471 w.push(255);
472 w.extend_from_slice(&val.to_le_bytes());
473 }
474}
475
476pub(crate) mod taptree {
477 use bitcoin::ScriptBuf;
478 use std::io::Write;
479 use std::io::{self};
480
481 pub struct TapTree(pub Vec<ScriptBuf>);
482
483 impl TapTree {
484 pub fn encode(&self) -> io::Result<Vec<u8>> {
485 let mut tapscripts_bytes = Vec::new();
486 for tapscript in &self.0 {
487 tapscripts_bytes.push(1);
489
490 tapscripts_bytes.push(0xc0);
492
493 write_compact_size_uint(&mut tapscripts_bytes, tapscript.len() as u64)?;
495 tapscripts_bytes.extend(tapscript.as_bytes());
496 }
497
498 Ok(tapscripts_bytes)
499 }
500
501 #[cfg(test)]
502 pub fn decode(data: &[u8]) -> io::Result<Self> {
503 use std::io::Cursor;
504 use std::io::Read;
505
506 let mut buf = Cursor::new(data);
507 let mut leaves = Vec::new();
508
509 while buf.position() < data.len() as u64 {
511 let mut depth = [0u8; 1];
513 buf.read_exact(&mut depth)?;
514
515 let mut lv = [0u8; 1];
517 buf.read_exact(&mut lv)?;
518
519 let script_len = read_compact_size_uint(&mut buf)? as usize;
521
522 let mut script_bytes = vec![0u8; script_len];
524 buf.read_exact(&mut script_bytes)?;
525
526 leaves.push(ScriptBuf::from_bytes(script_bytes));
527 }
528
529 Ok(TapTree(leaves))
530 }
531 }
532
533 fn write_compact_size_uint<W: Write>(w: &mut W, val: u64) -> io::Result<()> {
535 if val < 253 {
536 w.write_all(&[val as u8])
537 } else if val < 0x10000 {
538 w.write_all(&[253])?;
539 w.write_all(&(val as u16).to_le_bytes())
540 } else if val < 0x100000000 {
541 w.write_all(&[254])?;
542 w.write_all(&(val as u32).to_le_bytes())
543 } else {
544 w.write_all(&[255])?;
545 w.write_all(&val.to_le_bytes())
546 }
547 }
548
549 #[cfg(test)]
550 fn read_compact_size_uint<R: io::Read>(r: &mut R) -> io::Result<u64> {
552 let mut first = [0u8; 1];
553 r.read_exact(&mut first)?;
554 match first[0] {
555 253 => {
556 let mut buf = [0u8; 2];
557 r.read_exact(&mut buf)?;
558 Ok(u16::from_le_bytes(buf) as u64)
559 }
560 254 => {
561 let mut buf = [0u8; 4];
562 r.read_exact(&mut buf)?;
563 Ok(u32::from_le_bytes(buf) as u64)
564 }
565 255 => {
566 let mut buf = [0u8; 8];
567 r.read_exact(&mut buf)?;
568 Ok(u64::from_le_bytes(buf))
569 }
570 v => Ok(v as u64),
571 }
572 }
573
574 #[cfg(test)]
575 mod tests {
576 use super::*;
577 use bitcoin::opcodes::OP_FALSE;
578 use bitcoin::opcodes::OP_TRUE;
579
580 #[test]
581 fn tap_tree_encode_decode_roundtrip() {
582 let scripts = vec![ScriptBuf::builder().push_opcode(OP_TRUE).into_script()];
583
584 let tree = TapTree(scripts.clone());
585 let encoded = tree.encode().unwrap();
586 let decoded = TapTree::decode(&encoded).unwrap();
587 assert_eq!(decoded.0, scripts);
588 }
589
590 #[test]
591 fn tap_tree_multiple_leaves() {
592 let scripts = vec![
593 ScriptBuf::builder().push_opcode(OP_TRUE).into_script(),
594 ScriptBuf::builder().push_opcode(OP_FALSE).into_script(),
595 ];
596 let tree = TapTree(scripts.clone());
597 let encoded = tree.encode().unwrap();
598 let decoded = TapTree::decode(&encoded).unwrap();
599 assert_eq!(decoded.0, scripts);
600 }
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use std::str::FromStr;
608
609 #[test]
610 fn intent_message_register_serialization() {
611 let pk = PublicKey::from_str(
612 "027b763fdd0d6d96d1ce6fb95e09e381fdae2bcbe3ed7d1a2bd95702524d5dcd8a",
613 )
614 .unwrap();
615 let msg = IntentMessage::Register {
616 onchain_output_indexes: vec![],
617 valid_at: 1762861934,
618 expire_at: 1762862054,
619 own_cosigner_pks: vec![pk],
620 };
621 let encoded = msg.encode().unwrap();
622 assert_eq!(
623 encoded,
624 r#"{"type":"register","onchain_output_indexes":[],"valid_at":1762861934,"expire_at":1762862054,"cosigners_public_keys":["027b763fdd0d6d96d1ce6fb95e09e381fdae2bcbe3ed7d1a2bd95702524d5dcd8a"]}"#
625 );
626 }
627
628 #[test]
629 fn intent_message_estimate_fee_serialization() {
630 let pk = PublicKey::from_str(
631 "027b763fdd0d6d96d1ce6fb95e09e381fdae2bcbe3ed7d1a2bd95702524d5dcd8a",
632 )
633 .unwrap();
634 let msg = IntentMessage::EstimateIntentFee {
635 onchain_output_indexes: vec![],
636 valid_at: 1762861934,
637 expire_at: 1762862054,
638 own_cosigner_pks: vec![pk],
639 };
640 let encoded = msg.encode().unwrap();
641 assert_eq!(
642 encoded,
643 r#"{"type":"estimate-intent-fee","onchain_output_indexes":[],"valid_at":1762861934,"expire_at":1762862054,"cosigners_public_keys":["027b763fdd0d6d96d1ce6fb95e09e381fdae2bcbe3ed7d1a2bd95702524d5dcd8a"]}"#
644 );
645 }
646
647 #[test]
648 fn intent_message_delete_serialization() {
649 let msg = IntentMessage::Delete {
650 expire_at: 1762862054,
651 };
652 let encoded = msg.encode().unwrap();
653 assert_eq!(encoded, r#"{"type":"delete","expire_at":1762862054}"#);
654 }
655
656 #[test]
657 fn intent_message_get_pending_tx_serialization() {
658 let msg = IntentMessage::GetPendingTx {
659 expire_at: 1762862054,
660 };
661 let encoded = msg.encode().unwrap();
662 assert_eq!(
663 encoded,
664 r#"{"type":"get-pending-tx","expire_at":1762862054}"#
665 );
666 }
667}