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