1pub mod policy;
57pub(crate) mod genesis;
58mod validation;
59
60pub use self::validation::VtxoValidationError;
61pub use self::policy::{Policy, VtxoPolicy, VtxoPolicyKind, ServerVtxoPolicy};
62pub(crate) use self::genesis::{GenesisItem, GenesisTransition};
63
64pub use self::policy::{
65 PubkeyVtxoPolicy, CheckpointVtxoPolicy, ExpiryVtxoPolicy, HarkLeafVtxoPolicy,
66 ServerHtlcRecvVtxoPolicy, ServerHtlcSendVtxoPolicy
67};
68pub use self::policy::clause::{
69 VtxoClause, DelayedSignClause, DelayedTimelockSignClause, HashDelaySignClause,
70 TapScriptClause,
71};
72
73pub type ServerVtxo<G = Bare> = Vtxo<G, ServerVtxoPolicy>;
75
76use std::borrow::Cow;
77use std::iter::FusedIterator;
78use std::{fmt, io};
79use std::str::FromStr;
80
81use bitcoin::{
82 taproot, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Weight, Witness
83};
84use bitcoin::absolute::LockTime;
85use bitcoin::hashes::{sha256, Hash};
86use bitcoin::secp256k1::{schnorr, PublicKey, XOnlyPublicKey};
87use bitcoin::taproot::TapTweakHash;
88
89use bitcoin_ext::{fee, BlockDelta, BlockHeight, TxOutExt};
90
91use crate::vtxo::policy::HarkForfeitVtxoPolicy;
92use crate::scripts;
93use crate::encode::{
94 LengthPrefixedVector, OversizedVectorError, ProtocolDecodingError, ProtocolEncoding, ReadExt,
95 WriteExt,
96};
97use crate::lightning::PaymentHash;
98use crate::tree::signed::{UnlockHash, UnlockPreimage};
99
100pub const EXIT_TX_WEIGHT: Weight = Weight::from_vb_unchecked(124);
102
103const VTXO_ENCODING_VERSION: u16 = 2;
105const VTXO_NO_FEE_AMOUNT_VERSION: u16 = 1;
107
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
110#[error("failed to parse vtxo id, must be 36 bytes")]
111pub struct VtxoIdParseError;
112
113#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
114pub struct VtxoId([u8; 36]);
115
116impl VtxoId {
117 pub const ENCODE_SIZE: usize = 36;
119
120 pub fn from_slice(b: &[u8]) -> Result<VtxoId, VtxoIdParseError> {
121 if b.len() == 36 {
122 let mut ret = [0u8; 36];
123 ret[..].copy_from_slice(&b[0..36]);
124 Ok(Self(ret))
125 } else {
126 Err(VtxoIdParseError)
127 }
128 }
129
130 pub fn utxo(self) -> OutPoint {
131 let vout = [self.0[32], self.0[33], self.0[34], self.0[35]];
132 OutPoint::new(Txid::from_slice(&self.0[0..32]).unwrap(), u32::from_le_bytes(vout))
133 }
134
135 pub fn to_bytes(self) -> [u8; 36] {
136 self.0
137 }
138}
139
140impl From<OutPoint> for VtxoId {
141 fn from(p: OutPoint) -> VtxoId {
142 let mut ret = [0u8; 36];
143 ret[0..32].copy_from_slice(&p.txid[..]);
144 ret[32..].copy_from_slice(&p.vout.to_le_bytes());
145 VtxoId(ret)
146 }
147}
148
149impl AsRef<[u8]> for VtxoId {
150 fn as_ref(&self) -> &[u8] {
151 &self.0
152 }
153}
154
155impl fmt::Display for VtxoId {
156 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
157 fmt::Display::fmt(&self.utxo(), f)
158 }
159}
160
161impl fmt::Debug for VtxoId {
162 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
163 fmt::Display::fmt(self, f)
164 }
165}
166
167impl FromStr for VtxoId {
168 type Err = VtxoIdParseError;
169 fn from_str(s: &str) -> Result<Self, Self::Err> {
170 Ok(OutPoint::from_str(s).map_err(|_| VtxoIdParseError)?.into())
171 }
172}
173
174impl serde::Serialize for VtxoId {
175 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
176 if s.is_human_readable() {
177 s.collect_str(self)
178 } else {
179 s.serialize_bytes(self.as_ref())
180 }
181 }
182}
183
184impl<'de> serde::Deserialize<'de> for VtxoId {
185 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
186 struct Visitor;
187 impl<'de> serde::de::Visitor<'de> for Visitor {
188 type Value = VtxoId;
189 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 write!(f, "a VtxoId")
191 }
192 fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
193 VtxoId::from_slice(v).map_err(serde::de::Error::custom)
194 }
195 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
196 VtxoId::from_str(v).map_err(serde::de::Error::custom)
197 }
198 }
199 if d.is_human_readable() {
200 d.deserialize_str(Visitor)
201 } else {
202 d.deserialize_bytes(Visitor)
203 }
204 }
205}
206
207impl ProtocolEncoding for VtxoId {
208 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
209 w.emit_slice(&self.0)
210 }
211 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
212 let array: [u8; 36] = r.read_byte_array()
213 .map_err(|_| ProtocolDecodingError::invalid("invalid vtxo id. Expected 36 bytes"))?;
214
215 Ok(VtxoId(array))
216 }
217}
218
219pub(crate) fn exit_clause(
221 user_pubkey: PublicKey,
222 exit_delta: BlockDelta,
223) -> ScriptBuf {
224 scripts::delayed_sign(exit_delta, user_pubkey.x_only_public_key().0)
225}
226
227pub fn create_exit_tx(
232 prevout: OutPoint,
233 output: TxOut,
234 signature: Option<&schnorr::Signature>,
235 fee: Amount,
236) -> Transaction {
237 Transaction {
238 version: bitcoin::transaction::Version(3),
239 lock_time: LockTime::ZERO,
240 input: vec![TxIn {
241 previous_output: prevout,
242 script_sig: ScriptBuf::new(),
243 sequence: Sequence::ZERO,
244 witness: {
245 let mut ret = Witness::new();
246 if let Some(sig) = signature {
247 ret.push(&sig[..]);
248 }
249 ret
250 },
251 }],
252 output: vec![output, fee::fee_anchor_with_amount(fee)],
253 }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub(crate) enum MaybePreimage {
261 Preimage([u8; 32]),
262 Hash(sha256::Hash),
263}
264
265impl MaybePreimage {
266 pub fn hash(&self) -> sha256::Hash {
268 match self {
269 Self::Preimage(p) => sha256::Hash::hash(p),
270 Self::Hash(h) => *h,
271 }
272 }
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
278pub struct VtxoTxIterItem {
279 pub tx: Transaction,
281 pub output_idx: usize,
283}
284
285pub struct VtxoTxIter<'a, P: Policy = VtxoPolicy> {
287 vtxo: &'a Vtxo<Full, P>,
288
289 prev: OutPoint,
290 genesis_idx: usize,
291 current_amount: Amount,
292}
293
294impl<'a, P: Policy> VtxoTxIter<'a, P> {
295 fn new(vtxo: &'a Vtxo<Full, P>) -> VtxoTxIter<'a, P> {
296 let onchain_amount = vtxo.chain_anchor_amount()
298 .expect("This should only fail if the VTXO is invalid.");
299 VtxoTxIter {
300 prev: vtxo.anchor_point,
301 vtxo: vtxo,
302 genesis_idx: 0,
303 current_amount: onchain_amount,
304 }
305 }
306}
307
308impl<'a, P: Policy> Iterator for VtxoTxIter<'a, P> {
309 type Item = VtxoTxIterItem;
310
311 fn next(&mut self) -> Option<Self::Item> {
312 let item = self.vtxo.genesis.items.get(self.genesis_idx)?;
313 let next_amount = self.current_amount.checked_sub(
314 item.other_output_sum().expect("we calculated this amount beforehand")
315 ).expect("we calculated this amount beforehand");
316
317 let next_output = if let Some(item) = self.vtxo.genesis.items.get(self.genesis_idx + 1) {
318 item.transition.input_txout(
319 next_amount,
320 self.vtxo.server_pubkey,
321 self.vtxo.expiry_height,
322 self.vtxo.exit_delta,
323 )
324 } else {
325 self.vtxo.policy.txout(
327 self.vtxo.amount,
328 self.vtxo.server_pubkey,
329 self.vtxo.exit_delta,
330 self.vtxo.expiry_height,
331 )
332 };
333
334 let tx = item.tx(self.prev, next_output, self.vtxo.server_pubkey, self.vtxo.expiry_height);
335 self.prev = OutPoint::new(tx.compute_txid(), item.output_idx as u32);
336 self.genesis_idx += 1;
337 self.current_amount = next_amount;
338 let output_idx = item.output_idx as usize;
339 Some(VtxoTxIterItem { tx, output_idx })
340 }
341
342 fn size_hint(&self) -> (usize, Option<usize>) {
343 let len = self.vtxo.genesis.items.len().saturating_sub(self.genesis_idx);
344 (len, Some(len))
345 }
346}
347
348impl<'a, P: Policy> ExactSizeIterator for VtxoTxIter<'a, P> {}
349impl<'a, P: Policy> FusedIterator for VtxoTxIter<'a, P> {}
350
351#[derive(Debug, Clone)]
353pub struct Bare;
354
355#[derive(Debug, Clone)]
357pub struct Full {
358 pub(crate) items: Vec<genesis::GenesisItem>,
359}
360
361#[derive(Debug, Clone)]
375pub struct Vtxo<G = Full, P = VtxoPolicy> {
376 pub(crate) policy: P,
377 pub(crate) amount: Amount,
378 pub(crate) expiry_height: BlockHeight,
379
380 pub(crate) server_pubkey: PublicKey,
381 pub(crate) exit_delta: BlockDelta,
382
383 pub(crate) anchor_point: OutPoint,
384 pub(crate) genesis: G,
386
387 pub(crate) point: OutPoint,
394}
395
396impl<G, P: Policy> Vtxo<G, P> {
397 pub fn id(&self) -> VtxoId {
401 self.point.into()
402 }
403
404 pub fn point(&self) -> OutPoint {
408 self.point
409 }
410
411 pub fn amount(&self) -> Amount {
413 self.amount
414 }
415
416 pub fn chain_anchor(&self) -> OutPoint {
420 self.anchor_point
421 }
422
423 pub fn policy(&self) -> &P {
425 &self.policy
426 }
427
428 pub fn policy_type(&self) -> VtxoPolicyKind {
430 self.policy.policy_type()
431 }
432
433 pub fn expiry_height(&self) -> BlockHeight {
435 self.expiry_height
436 }
437
438 pub fn server_pubkey(&self) -> PublicKey {
440 self.server_pubkey
441 }
442
443 pub fn exit_delta(&self) -> BlockDelta {
445 self.exit_delta
446 }
447
448 pub fn output_taproot(&self) -> taproot::TaprootSpendInfo {
450 self.policy.taproot(self.server_pubkey, self.exit_delta, self.expiry_height)
451 }
452
453 pub fn output_script_pubkey(&self) -> ScriptBuf {
455 self.policy.script_pubkey(self.server_pubkey, self.exit_delta, self.expiry_height)
456 }
457
458 pub fn txout(&self) -> TxOut {
460 self.policy.txout(self.amount, self.server_pubkey, self.exit_delta, self.expiry_height)
461 }
462
463 pub fn to_bare(&self) -> Vtxo<Bare, P> {
465 Vtxo {
466 point: self.point,
467 policy: self.policy.clone(),
468 amount: self.amount,
469 expiry_height: self.expiry_height,
470 server_pubkey: self.server_pubkey,
471 exit_delta: self.exit_delta,
472 anchor_point: self.anchor_point,
473 genesis: Bare,
474 }
475 }
476
477 pub fn into_bare(self) -> Vtxo<Bare, P> {
479 Vtxo {
480 point: self.point,
481 policy: self.policy,
482 amount: self.amount,
483 expiry_height: self.expiry_height,
484 server_pubkey: self.server_pubkey,
485 exit_delta: self.exit_delta,
486 anchor_point: self.anchor_point,
487 genesis: Bare,
488 }
489 }
490}
491
492impl<P: Policy> Vtxo<Full, P> {
493 pub fn exit_depth(&self) -> u16 {
495 self.genesis.items.len() as u16
496 }
497
498 pub fn past_arkoor_pubkeys(&self) -> Vec<Vec<PublicKey>> {
506 self.genesis.items.iter().filter_map(|g| {
507 match &g.transition {
508 GenesisTransition::Arkoor(inner) => Some(inner.client_cosigners().collect()),
511 _ => None,
512 }
513 }).collect()
514 }
515
516 pub fn has_all_witnesses(&self) -> bool {
521 self.genesis.items.iter().all(|g| g.transition.has_all_witnesses())
522 }
523
524 pub fn is_standard(&self) -> bool {
531 self.txout().is_standard() && self.genesis.items.iter()
532 .all(|i| i.other_outputs.iter().all(|o| o.is_standard()))
533 }
534
535 pub fn unlock_hash(&self) -> Option<UnlockHash> {
537 match self.genesis.items.last()?.transition {
538 GenesisTransition::HashLockedCosigned(ref inner) => Some(inner.unlock.hash()),
539 _ => None,
540 }
541 }
542
543 pub fn provide_unlock_signature(&mut self, signature: schnorr::Signature) -> bool {
547 match self.genesis.items.last_mut().map(|g| &mut g.transition) {
548 Some(GenesisTransition::HashLockedCosigned(inner)) => {
549 inner.signature.replace(signature);
550 true
551 },
552 _ => false,
553 }
554 }
555
556 pub fn provide_unlock_preimage(&mut self, preimage: UnlockPreimage) -> bool {
560 match self.genesis.items.last_mut().map(|g| &mut g.transition) {
561 Some(GenesisTransition::HashLockedCosigned(ref mut inner)) => {
562 if inner.unlock.hash() == UnlockHash::hash(&preimage) {
563 inner.unlock = MaybePreimage::Preimage(preimage);
564 true
565 } else {
566 false
567 }
568 },
569 _ => false,
570 }
571 }
572
573 pub fn transactions(&self) -> VtxoTxIter<'_, P> {
575 VtxoTxIter::new(self)
576 }
577
578 pub fn validate(
583 &self,
584 chain_anchor_tx: &Transaction,
585 ) -> Result<(), VtxoValidationError> {
586 self::validation::validate(self, chain_anchor_tx)
587 }
588
589 pub fn validate_unsigned(
591 &self,
592 chain_anchor_tx: &Transaction,
593 ) -> Result<(), VtxoValidationError> {
594 self::validation::validate_unsigned(self, chain_anchor_tx)
595 }
596
597 pub(crate) fn chain_anchor_amount(&self) -> Option<Amount> {
601 self.amount.checked_add(self.genesis.items.iter().try_fold(Amount::ZERO, |sum, i| {
602 i.other_output_sum().and_then(|amt| sum.checked_add(amt))
603 })?)
604 }
605}
606
607impl<G> Vtxo<G, VtxoPolicy> {
608 pub fn user_pubkey(&self) -> PublicKey {
610 self.policy.user_pubkey()
611 }
612
613 pub fn arkoor_pubkey(&self) -> Option<PublicKey> {
617 self.policy.arkoor_pubkey()
618 }
619}
620
621impl Vtxo<Full, VtxoPolicy> {
622 #[cfg(any(test, feature = "test-util"))]
624 pub fn finalize_hark_leaf(
625 &mut self,
626 user_key: &bitcoin::secp256k1::Keypair,
627 server_key: &bitcoin::secp256k1::Keypair,
628 chain_anchor: &Transaction,
629 unlock_preimage: UnlockPreimage,
630 ) {
631 use crate::tree::signed::{LeafVtxoCosignContext, LeafVtxoCosignResponse};
632
633 let (ctx, req) = LeafVtxoCosignContext::new(self, chain_anchor, user_key);
635 let cosign = LeafVtxoCosignResponse::new_cosign(&req, self, chain_anchor, server_key);
636 assert!(ctx.finalize(self, cosign));
637 assert!(self.provide_unlock_preimage(unlock_preimage));
639 }
640}
641
642impl<G> Vtxo<G, ServerVtxoPolicy> {
643 pub fn try_into_user_vtxo(self) -> Result<Vtxo<G, VtxoPolicy>, ServerVtxo<G>> {
647 if let Some(p) = self.policy.clone().into_user_policy() {
648 Ok(Vtxo {
649 policy: p,
650 amount: self.amount,
651 expiry_height: self.expiry_height,
652 server_pubkey: self.server_pubkey,
653 exit_delta: self.exit_delta,
654 anchor_point: self.anchor_point,
655 genesis: self.genesis,
656 point: self.point,
657 })
658 } else {
659 Err(self)
660 }
661 }
662}
663
664impl<G, P: Policy> PartialEq for Vtxo<G, P> {
665 fn eq(&self, other: &Self) -> bool {
666 PartialEq::eq(&self.id(), &other.id())
667 }
668}
669
670impl<G, P: Policy> Eq for Vtxo<G, P> {}
671
672impl<G, P: Policy> PartialOrd for Vtxo<G, P> {
673 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
674 PartialOrd::partial_cmp(&self.id(), &other.id())
675 }
676}
677
678impl<G, P: Policy> Ord for Vtxo<G, P> {
679 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
680 Ord::cmp(&self.id(), &other.id())
681 }
682}
683
684impl<G, P: Policy> std::hash::Hash for Vtxo<G, P> {
685 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
686 std::hash::Hash::hash(&self.id(), state)
687 }
688}
689
690impl<G, P: Policy> AsRef<Vtxo<G, P>> for Vtxo<G, P> {
691 fn as_ref(&self) -> &Vtxo<G, P> {
692 self
693 }
694}
695
696impl<G> From<Vtxo<G>> for ServerVtxo<G> {
697 fn from(vtxo: Vtxo<G>) -> ServerVtxo<G> {
698 ServerVtxo {
699 policy: vtxo.policy.into(),
700 amount: vtxo.amount,
701 expiry_height: vtxo.expiry_height,
702 server_pubkey: vtxo.server_pubkey,
703 exit_delta: vtxo.exit_delta,
704 anchor_point: vtxo.anchor_point,
705 genesis: vtxo.genesis,
706 point: vtxo.point,
707 }
708 }
709}
710
711pub trait VtxoRef<P: Policy = VtxoPolicy> {
713 fn vtxo_id(&self) -> VtxoId;
715
716 fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { None }
718
719 fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { None }
721
722 fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> where Self: Sized;
724}
725
726impl<P: Policy> VtxoRef<P> for VtxoId {
727 fn vtxo_id(&self) -> VtxoId { *self }
728 fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
729}
730
731impl<'a, P: Policy> VtxoRef<P> for &'a VtxoId {
732 fn vtxo_id(&self) -> VtxoId { **self }
733 fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
734}
735
736impl<P: Policy> VtxoRef<P> for Vtxo<Bare, P> {
737 fn vtxo_id(&self) -> VtxoId { self.id() }
738 fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Borrowed(self)) }
739 fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
740}
741
742impl<'a, P: Policy> VtxoRef<P> for &'a Vtxo<Bare, P> {
743 fn vtxo_id(&self) -> VtxoId { self.id() }
744 fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Borrowed(*self)) }
745 fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
746}
747
748impl<P: Policy> VtxoRef<P> for Vtxo<Full, P> {
749 fn vtxo_id(&self) -> VtxoId { self.id() }
750 fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Owned(self.to_bare())) }
751 fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { Some(self) }
752 fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { Some(self) }
753}
754
755impl<'a, P: Policy> VtxoRef<P> for &'a Vtxo<Full, P> {
756 fn vtxo_id(&self) -> VtxoId { self.id() }
757 fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Owned(self.to_bare())) }
758 fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { Some(*self) }
759 fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { Some(self.clone()) }
760}
761
762const VTXO_POLICY_PUBKEY: u8 = 0x00;
764
765const VTXO_POLICY_SERVER_HTLC_SEND: u8 = 0x01;
767
768const VTXO_POLICY_SERVER_HTLC_RECV: u8 = 0x02;
770
771const VTXO_POLICY_CHECKPOINT: u8 = 0x03;
773
774const VTXO_POLICY_EXPIRY: u8 = 0x04;
776
777const VTXO_POLICY_HARK_LEAF: u8 = 0x05;
779
780const VTXO_POLICY_HARK_FORFEIT: u8 = 0x06;
782
783impl ProtocolEncoding for VtxoPolicy {
784 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
785 match self {
786 Self::Pubkey(PubkeyVtxoPolicy { user_pubkey }) => {
787 w.emit_u8(VTXO_POLICY_PUBKEY)?;
788 user_pubkey.encode(w)?;
789 },
790 Self::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, payment_hash, htlc_expiry }) => {
791 w.emit_u8(VTXO_POLICY_SERVER_HTLC_SEND)?;
792 user_pubkey.encode(w)?;
793 payment_hash.to_sha256_hash().encode(w)?;
794 w.emit_u32(*htlc_expiry)?;
795 },
796 Self::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy {
797 user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
798 }) => {
799 w.emit_u8(VTXO_POLICY_SERVER_HTLC_RECV)?;
800 user_pubkey.encode(w)?;
801 payment_hash.to_sha256_hash().encode(w)?;
802 w.emit_u32(*htlc_expiry)?;
803 w.emit_u16(*htlc_expiry_delta)?;
804 },
805 }
806 Ok(())
807 }
808
809 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
810 let type_byte = r.read_u8()?;
811 decode_vtxo_policy(type_byte, r)
812 }
813}
814
815fn decode_vtxo_policy<R: io::Read + ?Sized>(
819 type_byte: u8,
820 r: &mut R,
821) -> Result<VtxoPolicy, ProtocolDecodingError> {
822 match type_byte {
823 VTXO_POLICY_PUBKEY => {
824 let user_pubkey = PublicKey::decode(r)?;
825 Ok(VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey }))
826 },
827 VTXO_POLICY_SERVER_HTLC_SEND => {
828 let user_pubkey = PublicKey::decode(r)?;
829 let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
830 let htlc_expiry = r.read_u32()?;
831 Ok(VtxoPolicy::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, payment_hash, htlc_expiry }))
832 },
833 VTXO_POLICY_SERVER_HTLC_RECV => {
834 let user_pubkey = PublicKey::decode(r)?;
835 let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
836 let htlc_expiry = r.read_u32()?;
837 let htlc_expiry_delta = r.read_u16()?;
838 Ok(VtxoPolicy::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy { user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta }))
839 },
840
841 v => Err(ProtocolDecodingError::invalid(format_args!(
846 "invalid VtxoPolicy type byte: {v:#x}",
847 ))),
848 }
849}
850
851impl ProtocolEncoding for ServerVtxoPolicy {
852 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
853 match self {
854 Self::User(p) => p.encode(w)?,
855 Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }) => {
856 w.emit_u8(VTXO_POLICY_CHECKPOINT)?;
857 user_pubkey.encode(w)?;
858 },
859 Self::Expiry(ExpiryVtxoPolicy { internal_key }) => {
860 w.emit_u8(VTXO_POLICY_EXPIRY)?;
861 internal_key.encode(w)?;
862 },
863 Self::HarkLeaf(HarkLeafVtxoPolicy { user_pubkey, unlock_hash }) => {
864 w.emit_u8(VTXO_POLICY_HARK_LEAF)?;
865 user_pubkey.encode(w)?;
866 unlock_hash.encode(w)?;
867 },
868 Self::HarkForfeit(HarkForfeitVtxoPolicy { user_pubkey, unlock_hash }) => {
869 w.emit_u8(VTXO_POLICY_HARK_FORFEIT)?;
870 user_pubkey.encode(w)?;
871 unlock_hash.encode(w)?;
872 },
873 }
874 Ok(())
875 }
876
877 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
878 let type_byte = r.read_u8()?;
879 match type_byte {
880 VTXO_POLICY_PUBKEY | VTXO_POLICY_SERVER_HTLC_SEND | VTXO_POLICY_SERVER_HTLC_RECV => {
881 Ok(Self::User(decode_vtxo_policy(type_byte, r)?))
882 },
883 VTXO_POLICY_CHECKPOINT => {
884 let user_pubkey = PublicKey::decode(r)?;
885 Ok(Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }))
886 },
887 VTXO_POLICY_EXPIRY => {
888 let internal_key = XOnlyPublicKey::decode(r)?;
889 Ok(Self::Expiry(ExpiryVtxoPolicy { internal_key }))
890 },
891 VTXO_POLICY_HARK_LEAF => {
892 let user_pubkey = PublicKey::decode(r)?;
893 let unlock_hash = sha256::Hash::decode(r)?;
894 Ok(Self::HarkLeaf(HarkLeafVtxoPolicy { user_pubkey, unlock_hash }))
895 },
896 VTXO_POLICY_HARK_FORFEIT => {
897 let user_pubkey = PublicKey::decode(r)?;
898 let unlock_hash = sha256::Hash::decode(r)?;
899 Ok(Self::HarkForfeit(HarkForfeitVtxoPolicy { user_pubkey, unlock_hash }))
900 },
901 v => Err(ProtocolDecodingError::invalid(format_args!(
902 "invalid ServerVtxoPolicy type byte: {v:#x}",
903 ))),
904 }
905 }
906}
907
908const GENESIS_TRANSITION_TYPE_COSIGNED: u8 = 1;
910
911const GENESIS_TRANSITION_TYPE_ARKOOR: u8 = 2;
913
914const GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED: u8 = 3;
916
917impl ProtocolEncoding for GenesisTransition {
918 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
919 match self {
920 Self::Cosigned(t) => {
921 w.emit_u8(GENESIS_TRANSITION_TYPE_COSIGNED)?;
922 LengthPrefixedVector::new(&t.pubkeys).encode(w)?;
923 t.signature.encode(w)?;
924 },
925 Self::HashLockedCosigned(t) => {
926 w.emit_u8(GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED)?;
927 t.user_pubkey.encode(w)?;
928 t.signature.encode(w)?;
929 match t.unlock {
930 MaybePreimage::Preimage(p) => {
931 w.emit_u8(0)?;
932 w.emit_slice(&p[..])?;
933 },
934 MaybePreimage::Hash(h) => {
935 w.emit_u8(1)?;
936 w.emit_slice(&h[..])?;
937 },
938 }
939 },
940 Self::Arkoor(t) => {
941 w.emit_u8(GENESIS_TRANSITION_TYPE_ARKOOR)?;
942 LengthPrefixedVector::new(&t.client_cosigners).encode(w)?;
943 t.tap_tweak.encode(w)?;
944 t.signature.encode(w)?;
945 },
946 }
947 Ok(())
948 }
949
950 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
951 match r.read_u8()? {
952 GENESIS_TRANSITION_TYPE_COSIGNED => {
953 let pubkeys = LengthPrefixedVector::decode(r)?.into_inner();
954 let signature = Option::<schnorr::Signature>::decode(r)?;
955 Ok(Self::new_cosigned(pubkeys, signature))
956 },
957 GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED => {
958 let user_pubkey = PublicKey::decode(r)?;
959 let signature = Option::<schnorr::Signature>::decode(r)?;
960 let unlock = match r.read_u8()? {
961 0 => MaybePreimage::Preimage(r.read_byte_array()?),
962 1 => MaybePreimage::Hash(ProtocolEncoding::decode(r)?),
963 v => return Err(ProtocolDecodingError::invalid(format_args!(
964 "invalid MaybePreimage type byte: {v:#x}",
965 ))),
966 };
967 Ok(Self::new_hash_locked_cosigned(user_pubkey, signature, unlock))
968 },
969 GENESIS_TRANSITION_TYPE_ARKOOR => {
970 let cosigners = LengthPrefixedVector::decode(r)?.into_inner();
971 let taptweak = TapTweakHash::decode(r)?;
972 let signature = Option::<schnorr::Signature>::decode(r)?;
973 Ok(Self::new_arkoor(cosigners, taptweak, signature))
974 },
975 v => Err(ProtocolDecodingError::invalid(format_args!(
976 "invalid GenesisTransistion type byte: {v:#x}",
977 ))),
978 }
979 }
980}
981
982trait VtxoVersionedEncoding: Sized {
985 fn encode<W: io::Write + ?Sized>(&self, w: &mut W, version: u16) -> Result<(), io::Error>;
986
987 fn decode<R: io::Read + ?Sized>(
988 r: &mut R,
989 version: u16,
990 ) -> Result<Self, ProtocolDecodingError>;
991}
992
993impl VtxoVersionedEncoding for Bare {
994 fn encode<W: io::Write + ?Sized>(&self, w: &mut W, _version: u16) -> Result<(), io::Error> {
995 w.emit_compact_size(0u64)?;
996 Ok(())
997 }
998
999 fn decode<R: io::Read + ?Sized>(
1000 r: &mut R,
1001 version: u16,
1002 ) -> Result<Self, ProtocolDecodingError> {
1003 let _full = Full::decode(r, version)?;
1006
1007 Ok(Bare)
1008 }
1009}
1010
1011impl VtxoVersionedEncoding for Full {
1012 fn encode<W: io::Write + ?Sized>(&self, w: &mut W, _version: u16) -> Result<(), io::Error> {
1013 w.emit_compact_size(self.items.len() as u64)?;
1014 for item in &self.items {
1015 item.transition.encode(w)?;
1016 let nb_outputs = item.other_outputs.len() + 1;
1017 w.emit_u8(nb_outputs.try_into()
1018 .map_err(|_| io::Error::other("too many outputs on genesis transaction"))?)?;
1019 w.emit_u8(item.output_idx)?;
1020 for txout in &item.other_outputs {
1021 txout.encode(w)?;
1022 }
1023 w.emit_u64(item.fee_amount.to_sat())?;
1024 }
1025 Ok(())
1026 }
1027
1028 fn decode<R: io::Read + ?Sized>(
1029 r: &mut R,
1030 version: u16,
1031 ) -> Result<Self, ProtocolDecodingError> {
1032 let nb_genesis_items = r.read_compact_size()? as usize;
1033 OversizedVectorError::check::<GenesisItem>(nb_genesis_items)?;
1034 let mut genesis = Vec::with_capacity(nb_genesis_items);
1035 for _ in 0..nb_genesis_items {
1036 let transition = GenesisTransition::decode(r)?;
1037 let nb_outputs = r.read_u8()? as usize;
1038 let output_idx = r.read_u8()?;
1039 let nb_other = nb_outputs.checked_sub(1)
1040 .ok_or_else(|| ProtocolDecodingError::invalid("genesis item with 0 outputs"))?;
1041 let mut other_outputs = Vec::with_capacity(nb_other);
1042 for _ in 0..nb_other {
1043 other_outputs.push(TxOut::decode(r)?);
1044 }
1045 let fee_amount = if version == VTXO_NO_FEE_AMOUNT_VERSION {
1046 Amount::ZERO
1048 } else {
1049 Amount::from_sat(r.read_u64()?)
1050 };
1051 genesis.push(GenesisItem { transition, output_idx, other_outputs, fee_amount });
1052 }
1053 Ok(Full { items: genesis })
1054 }
1055}
1056
1057impl<G: VtxoVersionedEncoding, P: Policy + ProtocolEncoding> ProtocolEncoding for Vtxo<G, P> {
1058 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1059 let version = VTXO_ENCODING_VERSION;
1060 w.emit_u16(version)?;
1061 w.emit_u64(self.amount.to_sat())?;
1062 w.emit_u32(self.expiry_height)?;
1063 self.server_pubkey.encode(w)?;
1064 w.emit_u16(self.exit_delta)?;
1065 self.anchor_point.encode(w)?;
1066
1067 self.genesis.encode(w, version)?;
1068
1069 self.policy.encode(w)?;
1070 self.point.encode(w)?;
1071 Ok(())
1072 }
1073
1074 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1075 let version = r.read_u16()?;
1076 if version != VTXO_ENCODING_VERSION && version != VTXO_NO_FEE_AMOUNT_VERSION {
1077 return Err(ProtocolDecodingError::invalid(format_args!(
1078 "invalid Vtxo encoding version byte: {version:#x}",
1079 )));
1080 }
1081
1082 let amount = Amount::from_sat(r.read_u64()?);
1083 let expiry_height = r.read_u32()?;
1084 let server_pubkey = PublicKey::decode(r)?;
1085 let exit_delta = r.read_u16()?;
1086 let anchor_point = OutPoint::decode(r)?;
1087
1088 let genesis = VtxoVersionedEncoding::decode(r, version)?;
1089
1090 let policy = P::decode(r)?;
1091 let point = OutPoint::decode(r)?;
1092
1093 Ok(Self {
1094 amount, expiry_height, server_pubkey, exit_delta, anchor_point, genesis, policy, point,
1095 })
1096 }
1097}
1098
1099
1100#[cfg(test)]
1101mod test {
1102 use bitcoin::consensus::encode::serialize_hex;
1103 use bitcoin::hex::DisplayHex;
1104
1105 use crate::test_util::encoding_roundtrip;
1106 use crate::test_util::dummy::{DUMMY_SERVER_KEY, DUMMY_USER_KEY};
1107 use crate::test_util::vectors::{
1108 generate_vtxo_vectors, VTXO_VECTORS, VTXO_NO_FEE_AMOUNT_VERSION_HEXES,
1109 };
1110
1111 use super::*;
1112
1113 #[test]
1114 fn test_generate_vtxo_vectors() {
1115 let g = generate_vtxo_vectors();
1116 println!("\n\ngenerated:");
1119 println!(" anchor_tx: {}", serialize_hex(&g.anchor_tx));
1120 println!(" board_vtxo: {}", g.board_vtxo.serialize().as_hex().to_string());
1121 println!(" arkoor_htlc_out_vtxo: {}", g.arkoor_htlc_out_vtxo.serialize().as_hex().to_string());
1122 println!(" arkoor2_vtxo: {}", g.arkoor2_vtxo.serialize().as_hex().to_string());
1123 println!(" round_tx: {}", serialize_hex(&g.round_tx));
1124 println!(" round1_vtxo: {}", g.round1_vtxo.serialize().as_hex().to_string());
1125 println!(" round2_vtxo: {}", g.round2_vtxo.serialize().as_hex().to_string());
1126 println!(" arkoor3_vtxo: {}", g.arkoor3_vtxo.serialize().as_hex().to_string());
1127
1128
1129 let v = &*VTXO_VECTORS;
1130 println!("\n\nstatic:");
1131 println!(" anchor_tx: {}", serialize_hex(&v.anchor_tx));
1132 println!(" board_vtxo: {}", v.board_vtxo.serialize().as_hex().to_string());
1133 println!(" arkoor_htlc_out_vtxo: {}", v.arkoor_htlc_out_vtxo.serialize().as_hex().to_string());
1134 println!(" arkoor2_vtxo: {}", v.arkoor2_vtxo.serialize().as_hex().to_string());
1135 println!(" round_tx: {}", serialize_hex(&v.round_tx));
1136 println!(" round1_vtxo: {}", v.round1_vtxo.serialize().as_hex().to_string());
1137 println!(" round2_vtxo: {}", v.round2_vtxo.serialize().as_hex().to_string());
1138 println!(" arkoor3_vtxo: {}", v.arkoor3_vtxo.serialize().as_hex().to_string());
1139
1140 assert_eq!(g.anchor_tx, v.anchor_tx, "anchor_tx does not match");
1141 assert_eq!(g.board_vtxo, v.board_vtxo, "board_vtxo does not match");
1142 assert_eq!(g.arkoor_htlc_out_vtxo, v.arkoor_htlc_out_vtxo, "arkoor_htlc_out_vtxo does not match");
1143 assert_eq!(g.arkoor2_vtxo, v.arkoor2_vtxo, "arkoor2_vtxo does not match");
1144 assert_eq!(g.round_tx, v.round_tx, "round_tx does not match");
1145 assert_eq!(g.round1_vtxo, v.round1_vtxo, "round1_vtxo does not match");
1146 assert_eq!(g.round2_vtxo, v.round2_vtxo, "round2_vtxo does not match");
1147 assert_eq!(g.arkoor3_vtxo, v.arkoor3_vtxo, "arkoor3_vtxo does not match");
1148
1149 assert_eq!(g, *v);
1151 }
1152
1153 #[test]
1154 fn test_vtxo_no_fee_amount_version_upgrade() {
1155 let hexes = &*VTXO_NO_FEE_AMOUNT_VERSION_HEXES;
1156 let v = hexes.deserialize_test_vectors();
1157
1158 v.validate_vtxos();
1160
1161 let board_hex = v.board_vtxo.serialize().as_hex().to_string();
1163 let arkoor_htlc_out_vtxo_hex = v.arkoor_htlc_out_vtxo.serialize().as_hex().to_string();
1164 let arkoor2_vtxo_hex = v.arkoor2_vtxo.serialize().as_hex().to_string();
1165 let round1_vtxo_hex = v.round1_vtxo.serialize().as_hex().to_string();
1166 let round2_vtxo_hex = v.round2_vtxo.serialize().as_hex().to_string();
1167 let arkoor3_vtxo_hex = v.arkoor3_vtxo.serialize().as_hex().to_string();
1168 assert_ne!(board_hex, hexes.board_vtxo);
1169 assert_ne!(arkoor_htlc_out_vtxo_hex, hexes.arkoor_htlc_out_vtxo);
1170 assert_ne!(arkoor2_vtxo_hex, hexes.arkoor2_vtxo);
1171 assert_ne!(round1_vtxo_hex, hexes.round1_vtxo);
1172 assert_ne!(round2_vtxo_hex, hexes.round2_vtxo);
1173 assert_ne!(arkoor3_vtxo_hex, hexes.arkoor3_vtxo);
1174
1175 let board_vtxo = Vtxo::<Full>::deserialize_hex(&board_hex).unwrap();
1181 assert_eq!(board_vtxo.serialize().as_hex().to_string(), board_hex);
1182 let arkoor_htlc_out_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor_htlc_out_vtxo_hex).unwrap();
1183 assert_eq!(arkoor_htlc_out_vtxo.serialize().as_hex().to_string(), arkoor_htlc_out_vtxo_hex);
1184 let arkoor2_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor2_vtxo_hex).unwrap();
1185 assert_eq!(arkoor2_vtxo.serialize().as_hex().to_string(), arkoor2_vtxo_hex);
1186 let round1_vtxo = Vtxo::<Full>::deserialize_hex(&round1_vtxo_hex).unwrap();
1187 assert_eq!(round1_vtxo.serialize().as_hex().to_string(), round1_vtxo_hex);
1188 let round2_vtxo = Vtxo::<Full>::deserialize_hex(&round2_vtxo_hex).unwrap();
1189 assert_eq!(round2_vtxo.serialize().as_hex().to_string(), round2_vtxo_hex);
1190 let arkoor3_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor3_vtxo_hex).unwrap();
1191 assert_eq!(arkoor3_vtxo.serialize().as_hex().to_string(), arkoor3_vtxo_hex);
1192 }
1193
1194 #[test]
1195 fn exit_depth() {
1196 let vtxos = &*VTXO_VECTORS;
1197 assert_eq!(vtxos.board_vtxo.exit_depth(), 1 );
1199
1200 assert_eq!(vtxos.round1_vtxo.exit_depth(), 3 );
1202
1203 assert_eq!(
1205 vtxos.arkoor_htlc_out_vtxo.exit_depth(),
1206 1 + 1 + 1 ,
1207 );
1208 assert_eq!(
1209 vtxos.arkoor2_vtxo.exit_depth(),
1210 1 + 2 + 2 ,
1211 );
1212 assert_eq!(
1213 vtxos.arkoor3_vtxo.exit_depth(),
1214 3 + 1 + 1 ,
1215 );
1216 }
1217
1218 #[test]
1219 fn test_genesis_length_257() {
1220 let vtxo: Vtxo<Full> = Vtxo {
1221 policy: VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key()),
1222 amount: Amount::from_sat(10_000),
1223 expiry_height: 101_010,
1224 server_pubkey: DUMMY_SERVER_KEY.public_key(),
1225 exit_delta: 2016,
1226 anchor_point: OutPoint::new(Txid::from_slice(&[1u8; 32]).unwrap(), 1),
1227 genesis: Full {
1228 items: (0..257).map(|_| {
1229 GenesisItem {
1230 transition: GenesisTransition::new_cosigned(
1231 vec![DUMMY_USER_KEY.public_key()],
1232 Some(schnorr::Signature::from_slice(&[2u8; 64]).unwrap()),
1233 ),
1234 output_idx: 0,
1235 other_outputs: vec![],
1236 fee_amount: Amount::ZERO,
1237 }
1238 }).collect(),
1239 },
1240 point: OutPoint::new(Txid::from_slice(&[3u8; 32]).unwrap(), 3),
1241 };
1242 assert_eq!(vtxo.genesis.items.len(), 257);
1243 encoding_roundtrip(&vtxo);
1244 }
1245
1246 mod genesis_transition_encoding {
1247 use bitcoin::hashes::{sha256, Hash};
1248 use bitcoin::secp256k1::{Keypair, PublicKey};
1249 use bitcoin::taproot::TapTweakHash;
1250 use std::str::FromStr;
1251
1252 use crate::test_util::encoding_roundtrip;
1253 use super::genesis::{
1254 GenesisTransition, CosignedGenesis, HashLockedCosignedGenesis, ArkoorGenesis,
1255 };
1256 use super::MaybePreimage;
1257
1258 fn test_pubkey() -> PublicKey {
1259 Keypair::from_str(
1260 "916da686cedaee9a9bfb731b77439f2a3f1df8664e16488fba46b8d2bfe15e92"
1261 ).unwrap().public_key()
1262 }
1263
1264 fn test_signature() -> bitcoin::secp256k1::schnorr::Signature {
1265 "cc8b93e9f6fbc2506bb85ae8bbb530b178daac49704f5ce2e3ab69c266fd5932\
1266 0b28d028eef212e3b9fdc42cfd2e0760a0359d3ea7d2e9e8cfe2040e3f1b71ea"
1267 .parse().unwrap()
1268 }
1269
1270 #[test]
1271 fn cosigned_with_signature() {
1272 let transition = GenesisTransition::Cosigned(CosignedGenesis {
1273 pubkeys: vec![test_pubkey()],
1274 signature: Some(test_signature()),
1275 });
1276 encoding_roundtrip(&transition);
1277 }
1278
1279 #[test]
1280 fn cosigned_without_signature() {
1281 let transition = GenesisTransition::Cosigned(CosignedGenesis {
1282 pubkeys: vec![test_pubkey()],
1283 signature: None,
1284 });
1285 encoding_roundtrip(&transition);
1286 }
1287
1288 #[test]
1289 fn cosigned_multiple_pubkeys() {
1290 let pk1 = test_pubkey();
1291 let pk2 = Keypair::from_str(
1292 "fab9e598081a3e74b2233d470c4ad87bcc285b6912ed929568e62ac0e9409879"
1293 ).unwrap().public_key();
1294
1295 let transition = GenesisTransition::Cosigned(CosignedGenesis {
1296 pubkeys: vec![pk1, pk2],
1297 signature: Some(test_signature()),
1298 });
1299 encoding_roundtrip(&transition);
1300 }
1301
1302 #[test]
1303 fn hash_locked_cosigned_with_preimage() {
1304 let preimage = [0x42u8; 32];
1305 let transition = GenesisTransition::HashLockedCosigned(HashLockedCosignedGenesis {
1306 user_pubkey: test_pubkey(),
1307 signature: Some(test_signature()),
1308 unlock: MaybePreimage::Preimage(preimage),
1309 });
1310 encoding_roundtrip(&transition);
1311 }
1312
1313 #[test]
1314 fn hash_locked_cosigned_with_hash() {
1315 let hash = sha256::Hash::hash(b"test preimage");
1316 let transition = GenesisTransition::HashLockedCosigned(HashLockedCosignedGenesis {
1317 user_pubkey: test_pubkey(),
1318 signature: Some(test_signature()),
1319 unlock: MaybePreimage::Hash(hash),
1320 });
1321 encoding_roundtrip(&transition);
1322 }
1323
1324 #[test]
1325 fn hash_locked_cosigned_without_signature() {
1326 let preimage = [0x42u8; 32];
1327 let transition = GenesisTransition::HashLockedCosigned(HashLockedCosignedGenesis {
1328 user_pubkey: test_pubkey(),
1329 signature: None,
1330 unlock: MaybePreimage::Preimage(preimage),
1331 });
1332 encoding_roundtrip(&transition);
1333 }
1334
1335 #[test]
1336 fn arkoor_with_signature() {
1337 let tap_tweak = TapTweakHash::from_slice(&[0xabu8; 32]).unwrap();
1338 let transition = GenesisTransition::Arkoor(ArkoorGenesis {
1339 client_cosigners: vec![test_pubkey()],
1340 tap_tweak,
1341 signature: Some(test_signature()),
1342 });
1343 encoding_roundtrip(&transition);
1344 }
1345
1346 #[test]
1347 fn arkoor_without_signature() {
1348 let tap_tweak = TapTweakHash::from_slice(&[0xabu8; 32]).unwrap();
1349 let transition = GenesisTransition::Arkoor(ArkoorGenesis {
1350 client_cosigners: vec![test_pubkey()],
1351 tap_tweak,
1352 signature: None,
1353 });
1354 encoding_roundtrip(&transition);
1355 }
1356 }
1357}