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