Skip to main content

server_rpc/
convert.rs

1
2use std::borrow::Borrow;
3use std::convert::TryFrom;
4use std::time::Duration;
5
6use bitcoin::hashes::sha256;
7use bitcoin::secp256k1::{schnorr, PublicKey};
8use bitcoin::{self, Amount, FeeRate, OutPoint, ScriptBuf, Transaction, Txid};
9
10use ark::{musig, ProtocolEncoding, SignedVtxoRequest, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
11use ark::vtxo::policy::check_block_delta;
12use ark::arkoor::{ArkoorCosignRequest, ArkoorCosignResponse, ArkoorDestination};
13use ark::arkoor::package::{ArkoorPackageCosignRequest, ArkoorPackageCosignResponse};
14use ark::attestations::{
15	ArkoorCosignAttestation, DelegatedRoundParticipationAttestation, LightningReceiveAttestation, OffboardRequestAttestation, RoundAttemptAttestation, VtxoStatusAttestation
16};
17use ark::board::BoardCosignResponse;
18use ark::fees::PpmFeeRate;
19use ark::forfeit::HashLockedForfeitBundle;
20use ark::lightning::{PaymentHash, Preimage};
21use ark::mailbox::BlindedMailboxIdentifier;
22use ark::offboard::OffboardRequest;
23use ark::rounds::{Challenge, RoundId};
24use ark::tree::signed::{LeafVtxoCosignRequest, LeafVtxoCosignResponse, VtxoTreeSpec};
25use ark::vtxo::{Bare, Full, VtxoRef};
26
27use crate::protos;
28
29
30#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
31#[error("rpc conversion error: {msg}")]
32pub struct ConvertError {
33	pub msg: &'static str,
34}
35
36impl From<&'static str> for ConvertError {
37	fn from(msg: &'static str) -> ConvertError {
38		ConvertError { msg }
39	}
40}
41
42impl From<ConvertError> for tonic::Status {
43	fn from(e: ConvertError) -> Self {
44		tonic::Status::invalid_argument(e.msg)
45	}
46}
47
48/// Trait to convert some types from byte slices.
49pub trait TryFromBytes: Sized {
50	fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError>;
51}
52
53macro_rules! impl_try_from_byte_array {
54	($ty:path, $exp:expr) => {
55		impl TryFromBytes for $ty {
56			fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
57				#[allow(unused)]
58				use bitcoin::hashes::Hash;
59
60				let array = TryFrom::try_from(b.as_ref())
61					.map_err(|_| concat!("invalid ", $exp))?;
62				Ok(<$ty>::from_byte_array(array))
63			}
64		}
65	};
66}
67impl_try_from_byte_array!(PaymentHash, "lightning payment hash");
68impl_try_from_byte_array!(Preimage, "lightning payment preimage");
69impl_try_from_byte_array!(sha256::Hash, "SHA-256 hash");
70impl_try_from_byte_array!(Txid, "transaction id");
71
72macro_rules! impl_try_from_byte_array_result {
73	($ty:path, $exp:expr) => {
74		impl TryFromBytes for $ty {
75			fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
76				Ok(TryFrom::try_from(b.as_ref()).ok()
77					.and_then(|b| <$ty>::from_byte_array(b).ok())
78					.ok_or(concat!("invalid ", $exp))?)
79			}
80		}
81	};
82}
83impl_try_from_byte_array_result!(musig::PublicNonce, "public musig nonce");
84impl_try_from_byte_array_result!(musig::PartialSignature, "partial musig signature");
85impl_try_from_byte_array_result!(musig::AggregatedNonce, "aggregated musig nonce");
86
87macro_rules! impl_try_from_byte_slice {
88	($ty:path, $exp:expr) => {
89		impl TryFromBytes for $ty {
90			fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
91				#[allow(unused)] // used for the impls of hash types
92				use bitcoin::hashes::Hash;
93
94				Ok(<$ty>::from_slice(b.as_ref()).map_err(|_| concat!("invalid ", $exp))?)
95			}
96		}
97	};
98}
99impl_try_from_byte_slice!(PublicKey, "public key");
100impl_try_from_byte_slice!(schnorr::Signature, "Schnorr signature");
101impl_try_from_byte_slice!(VtxoId, "VTXO ID");
102impl_try_from_byte_slice!(RoundId, "VTXO ID");
103
104impl TryFromBytes for bitcoin::bip32::Xpub {
105	fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
106		Ok(bitcoin::bip32::Xpub::decode(b.as_ref()).map_err(|_| "invalid xpub")?)
107	}
108}
109
110macro_rules! impl_try_from_bytes_protocol {
111	($ty:path, $exp:expr) => {
112		impl TryFromBytes for $ty {
113			fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
114				Ok(ProtocolEncoding::deserialize(b.as_ref())
115					.map_err(|_| concat!("invalid ", $exp))?)
116			}
117		}
118	};
119}
120impl_try_from_bytes_protocol!(OutPoint, "outpoint");
121impl_try_from_bytes_protocol!(Vtxo<Bare>, "bare VTXO");
122impl_try_from_bytes_protocol!(Vtxo<Full>, "full VTXO (with genesis)");
123impl_try_from_bytes_protocol!(VtxoPolicy, "VTXO policy");
124impl_try_from_bytes_protocol!(BlindedMailboxIdentifier, "a blinded VTXO mailbox identifier");
125impl_try_from_bytes_protocol!(HashLockedForfeitBundle, "hArk forfeit bundle");
126impl_try_from_bytes_protocol!(RoundAttemptAttestation, "round attempt attestation");
127impl_try_from_bytes_protocol!(DelegatedRoundParticipationAttestation,
128	"delegated round participation attestation"
129);
130impl_try_from_bytes_protocol!(LightningReceiveAttestation, "lightning receive attestation");
131impl_try_from_bytes_protocol!(VtxoStatusAttestation, "VTXO status attestation");
132impl_try_from_bytes_protocol!(OffboardRequestAttestation, "offboard request attestation");
133
134macro_rules! impl_try_from_bytes_bitcoin {
135	($ty:path, $exp:expr) => {
136		impl TryFromBytes for $ty {
137			fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
138				Ok(bitcoin::consensus::encode::deserialize(b.as_ref())
139					.map_err(|_| concat!("invalid ", $exp))?)
140			}
141		}
142	};
143}
144impl_try_from_bytes_bitcoin!(Transaction, "bitcoin transaction");
145
146
147impl From<ark::ArkInfo> for protos::ArkInfo {
148	#[allow(deprecated)] // vtxo_expiry_delta and offboard_feerate kept for old clients
149	fn from(v: ark::ArkInfo) -> Self {
150		protos::ArkInfo {
151			network: v.network.to_string(),
152			server_pubkey: v.server_pubkey.serialize().to_vec(),
153			mailbox_pubkey: v.mailbox_pubkey.serialize().to_vec(),
154			round_interval_secs: v.round_interval.as_secs() as u32,
155			nb_round_nonces: v.nb_round_nonces as u32,
156			vtxo_exit_delta: v.vtxo_exit_delta.into(),
157			vtxo_lifetime: v.vtxo_lifetime.into(),
158			// we serve the deprecated field from the new one so that it
159			// can never go stale for old clients
160			vtxo_expiry_delta: v.vtxo_lifetime.into(),
161			htlc_send_expiry_delta: v.htlc_send_expiry_delta.into(),
162			htlc_expiry_delta: v.htlc_expiry_delta.into(),
163			max_vtxo_amount: v.max_vtxo_amount.map(|v| v.to_sat()),
164			required_board_confirmations: v.required_board_confirmations as u32,
165			max_user_invoice_cltv_delta: v.max_user_invoice_cltv_delta.into(),
166			min_board_amount: v.min_board_amount.to_sat(),
167			offboard_feerate_sat_vkb: v.offboard_feerate.to_sat_per_kwu() * 4,
168			max_offboard_inputs: v.max_offboard_inputs as u32,
169			ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
170			fees: Some(v.fees.into()),
171			max_vtxo_exit_depth: v.max_vtxo_exit_depth as u32,
172			tos_link: v.tos_link,
173		}
174	}
175}
176
177impl TryFrom<protos::ArkInfo> for ark::ArkInfo {
178	type Error = ConvertError;
179	#[allow(deprecated)] // vtxo_expiry_delta and offboard_feerate kept for old clients
180	fn try_from(v: protos::ArkInfo) -> Result<Self, Self::Error> {
181		// Servers from before the rename only set vtxo_expiry_delta.
182		let vtxo_lifetime = check_block_delta(match v.vtxo_lifetime {
183			0 => v.vtxo_expiry_delta,
184			l => l,
185		}).map_err(|_| "invalid vtxo_lifetime")?;
186
187		Ok(ark::ArkInfo {
188			network: v.network.parse().map_err(|_| "invalid network")?,
189			server_pubkey: PublicKey::from_slice(&v.server_pubkey)
190				.map_err(|_| "invalid server pubkey")?,
191			mailbox_pubkey: PublicKey::from_slice(&v.mailbox_pubkey)
192				.map_err(|_| "invalid mailbox pubkey")?,
193			round_interval: Duration::from_secs(v.round_interval_secs as u64),
194			nb_round_nonces: v.nb_round_nonces as usize,
195			vtxo_exit_delta: check_block_delta(v.vtxo_exit_delta)
196				.map_err(|_| "invalid vtxo_exit_delta")?,
197			vtxo_lifetime,
198			vtxo_expiry_delta: vtxo_lifetime,
199			htlc_send_expiry_delta: check_block_delta(v.htlc_send_expiry_delta)
200				.map_err(|_| "invalid htlc_send_expiry_delta")?,
201			htlc_expiry_delta: check_block_delta(v.htlc_expiry_delta)
202				.map_err(|_| "invalid htlc_expiry_delta")?,
203			max_vtxo_amount: v.max_vtxo_amount.map(|v| Amount::from_sat(v)),
204			required_board_confirmations: check_block_delta(v.required_board_confirmations)
205				.map_err(|_| "invalid required_board_confirmations")?.to_u16() as usize,
206			max_user_invoice_cltv_delta: check_block_delta(v.max_user_invoice_cltv_delta)
207				.map_err(|_| "invalid max_user_invoice_cltv_delta")?,
208			min_board_amount: Amount::from_sat(v.min_board_amount),
209			offboard_feerate: FeeRate::from_sat_per_kwu(v.offboard_feerate_sat_vkb / 4),
210			max_offboard_inputs: v.max_offboard_inputs as usize,
211			ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
212			fees: v.fees.ok_or("missing fees")?.try_into()?,
213			max_vtxo_exit_depth: v.max_vtxo_exit_depth.try_into()
214				.map_err(|_| "invalid max_vtxo_exit_depth")?,
215			tos_link: v.tos_link,
216		})
217	}
218}
219
220impl From<ark::fees::PpmExpiryFeeEntry> for protos::PpmExpiryFeeEntry {
221	fn from(v: ark::fees::PpmExpiryFeeEntry) -> Self {
222		protos::PpmExpiryFeeEntry {
223			expiry_blocks_threshold: v.expiry_blocks_threshold,
224			ppm: v.ppm.0,
225		}
226	}
227}
228
229impl From<protos::PpmExpiryFeeEntry> for ark::fees::PpmExpiryFeeEntry {
230	fn from(v: protos::PpmExpiryFeeEntry) -> Self {
231		ark::fees::PpmExpiryFeeEntry {
232			expiry_blocks_threshold: v.expiry_blocks_threshold,
233			ppm: PpmFeeRate(v.ppm),
234		}
235	}
236}
237
238impl From<ark::fees::BoardFees> for protos::BoardFees {
239	fn from(v: ark::fees::BoardFees) -> Self {
240		protos::BoardFees {
241			min_fee_sat: v.min_fee.to_sat(),
242			base_fee_sat: v.base_fee.to_sat(),
243			ppm: v.ppm.0,
244		}
245	}
246}
247
248impl From<protos::BoardFees> for ark::fees::BoardFees {
249	fn from(v: protos::BoardFees) -> Self {
250		ark::fees::BoardFees {
251			min_fee: Amount::from_sat(v.min_fee_sat),
252			base_fee: Amount::from_sat(v.base_fee_sat),
253			ppm: PpmFeeRate(v.ppm),
254		}
255	}
256}
257
258impl From<ark::fees::OffboardFees> for protos::OffboardFees {
259	fn from(v: ark::fees::OffboardFees) -> Self {
260		protos::OffboardFees {
261			base_fee_sat: v.base_fee.to_sat(),
262			fixed_additional_vb: v.fixed_additional_vb,
263			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
264		}
265	}
266}
267
268impl From<protos::OffboardFees> for ark::fees::OffboardFees {
269	fn from(v: protos::OffboardFees) -> Self {
270		ark::fees::OffboardFees {
271			base_fee: Amount::from_sat(v.base_fee_sat),
272			fixed_additional_vb: v.fixed_additional_vb,
273			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
274		}
275	}
276}
277
278impl From<ark::fees::RefreshFees> for protos::RefreshFees {
279	fn from(v: ark::fees::RefreshFees) -> Self {
280		protos::RefreshFees {
281			base_fee_sat: v.base_fee.to_sat(),
282			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
283		}
284	}
285}
286
287impl From<protos::RefreshFees> for ark::fees::RefreshFees {
288	fn from(v: protos::RefreshFees) -> Self {
289		ark::fees::RefreshFees {
290			base_fee: Amount::from_sat(v.base_fee_sat),
291			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
292		}
293	}
294}
295
296impl From<ark::fees::LightningReceiveFees> for protos::LightningReceiveFees {
297	fn from(v: ark::fees::LightningReceiveFees) -> Self {
298		protos::LightningReceiveFees {
299			base_fee_sat: v.base_fee.to_sat(),
300			ppm: v.ppm.0,
301		}
302	}
303}
304
305impl From<protos::LightningReceiveFees> for ark::fees::LightningReceiveFees {
306	fn from(v: protos::LightningReceiveFees) -> Self {
307		ark::fees::LightningReceiveFees {
308			base_fee: Amount::from_sat(v.base_fee_sat),
309			ppm: PpmFeeRate(v.ppm),
310		}
311	}
312}
313
314impl From<ark::fees::LightningSendFees> for protos::LightningSendFees {
315	fn from(v: ark::fees::LightningSendFees) -> Self {
316		protos::LightningSendFees {
317			min_fee_sat: v.min_fee.to_sat(),
318			base_fee_sat: v.base_fee.to_sat(),
319			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
320		}
321	}
322}
323
324impl From<protos::LightningSendFees> for ark::fees::LightningSendFees {
325	fn from(v: protos::LightningSendFees) -> Self {
326		ark::fees::LightningSendFees {
327			min_fee: Amount::from_sat(v.min_fee_sat),
328			base_fee: Amount::from_sat(v.base_fee_sat),
329			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
330		}
331	}
332}
333
334impl From<ark::fees::FeeSchedule> for protos::FeeSchedule {
335	fn from(v: ark::fees::FeeSchedule) -> Self {
336		protos::FeeSchedule {
337			board: Some(v.board.into()),
338			offboard: Some(v.offboard.into()),
339			refresh: Some(v.refresh.into()),
340			lightning_receive: Some(v.lightning_receive.into()),
341			lightning_send: Some(v.lightning_send.into()),
342		}
343	}
344}
345
346impl TryFrom<protos::FeeSchedule> for ark::fees::FeeSchedule {
347	type Error = ConvertError;
348	fn try_from(v: protos::FeeSchedule) -> Result<Self, Self::Error> {
349		Ok(ark::fees::FeeSchedule {
350			board: v.board.ok_or("missing board fees")?.into(),
351			offboard: v.offboard.ok_or("missing offboard fees")?.into(),
352			refresh: v.refresh.ok_or("missing refresh fees")?.into(),
353			lightning_receive: v.lightning_receive.ok_or("missing lightning receive fees")?.into(),
354			lightning_send: v.lightning_send.ok_or("missing lightning send fees")?.into(),
355		})
356	}
357}
358
359impl<'a> From<&'a ark::rounds::RoundEvent> for protos::RoundEvent {
360	fn from(e: &'a ark::rounds::RoundEvent) -> Self {
361		protos::RoundEvent {
362			event: Some(match e {
363				ark::rounds::RoundEvent::Attempt(ark::rounds::RoundAttempt {
364					round_seq, attempt_seq, challenge,
365				}) => {
366					protos::round_event::Event::Attempt(protos::RoundAttempt {
367						round_seq: (*round_seq).into(),
368						attempt_seq: *attempt_seq as u64,
369						round_attempt_challenge: challenge.inner().to_vec(),
370					})
371				},
372				ark::rounds::RoundEvent::VtxoProposal(ark::rounds::VtxoProposal {
373					round_seq, attempt_seq, vtxos_spec, unsigned_round_tx, cosign_agg_nonces,
374				}) => {
375					protos::round_event::Event::VtxoProposal(protos::VtxoProposal {
376						round_seq: (*round_seq).into(),
377						attempt_seq: *attempt_seq as u64,
378						vtxos_spec: vtxos_spec.serialize(),
379						unsigned_round_tx: bitcoin::consensus::serialize(&unsigned_round_tx),
380						vtxos_agg_nonces: cosign_agg_nonces.into_iter()
381							.map(|n| n.serialize().to_vec())
382							.collect(),
383					})
384				},
385				ark::rounds::RoundEvent::Finished(ark::rounds::RoundFinished {
386					round_seq, attempt_seq, cosign_sigs, signed_round_tx,
387				}) => {
388					protos::round_event::Event::Finished(protos::RoundFinished {
389						round_seq: (*round_seq).into(),
390						attempt_seq: *attempt_seq as u64,
391						vtxo_cosign_signatures: cosign_sigs.into_iter()
392							.map(|s| s.serialize().to_vec()).collect(),
393						signed_round_tx: bitcoin::consensus::serialize(&signed_round_tx),
394					})
395				},
396				ark::rounds::RoundEvent::Failed(ark::rounds::RoundFailed {
397					round_seq,
398				}) => {
399					protos::round_event::Event::Failed(protos::RoundFailed {
400						round_seq: (*round_seq).into(),
401					})
402				},
403			})
404		}
405	}
406}
407
408impl TryFrom<protos::RoundEvent> for ark::rounds::RoundEvent {
409	type Error = ConvertError;
410
411	fn try_from(m: protos::RoundEvent) -> Result<ark::rounds::RoundEvent, Self::Error> {
412		Ok(match m.event.ok_or("unknown round event")? {
413			protos::round_event::Event::Attempt(m) => {
414				ark::rounds::RoundEvent::Attempt(ark::rounds::RoundAttempt {
415					round_seq: m.round_seq.into(),
416					attempt_seq: m.attempt_seq as usize,
417					challenge: Challenge::new(
418						m.round_attempt_challenge.try_into().map_err(|_| "invalid challenge")?
419					),
420				})
421			},
422			protos::round_event::Event::VtxoProposal(m) => {
423				ark::rounds::RoundEvent::VtxoProposal(ark::rounds::VtxoProposal {
424					round_seq: m.round_seq.into(),
425					attempt_seq: m.attempt_seq as usize,
426					unsigned_round_tx: bitcoin::consensus::deserialize(&m.unsigned_round_tx)
427						.map_err(|_| "invalid unsigned_round_tx")?,
428					vtxos_spec: VtxoTreeSpec::deserialize(&m.vtxos_spec)
429						.map_err(|_| "invalid vtxos_spec")?,
430					cosign_agg_nonces: m.vtxos_agg_nonces.into_iter().map(|n| {
431						musig::AggregatedNonce::from_bytes(&n)
432					}).collect::<Result<_, _>>()?,
433				})
434			},
435			protos::round_event::Event::Finished(m) => {
436				ark::rounds::RoundEvent::Finished(ark::rounds::RoundFinished {
437					round_seq: m.round_seq.into(),
438					attempt_seq: m.attempt_seq as usize,
439					cosign_sigs: m.vtxo_cosign_signatures.into_iter().map(|s| {
440						schnorr::Signature::from_slice(&s)
441							.map_err(|_| "invalid vtxo_cosign_signatures")
442					}).collect::<Result<_, _>>()?,
443					signed_round_tx: bitcoin::consensus::deserialize(&m.signed_round_tx)
444						.map_err(|_| "invalid signed_round_tx")?,
445				})
446			},
447			protos::round_event::Event::Failed(m) => {
448				ark::rounds::RoundEvent::Failed(ark::rounds::RoundFailed {
449					round_seq: m.round_seq.into(),
450				})
451			},
452		})
453	}
454}
455
456impl From<crate::WalletStatus> for protos::WalletStatus {
457	fn from(s: crate::WalletStatus) -> Self {
458		protos::WalletStatus {
459			address: s.address.assume_checked().to_string(),
460			total_balance: s.total_balance.to_sat(),
461			trusted_balance: s.trusted_balance.to_sat(),
462			untrusted_balance: s.untrusted_balance.to_sat(),
463			confirmed_utxos: s.confirmed_utxos.iter().map(|u| u.to_string()).collect(),
464			unconfirmed_utxos: s.unconfirmed_utxos.iter().map(|u| u.to_string()).collect(),
465		}
466	}
467}
468
469impl TryFrom<protos::WalletStatus> for crate::WalletStatus {
470	type Error = ConvertError;
471	fn try_from(s: protos::WalletStatus) -> Result<Self, Self::Error> {
472		Ok(crate::WalletStatus {
473			address: s.address.parse().map_err(|_| "invalid address")?,
474			total_balance: Amount::from_sat(s.total_balance),
475			trusted_balance: Amount::from_sat(s.trusted_balance),
476			untrusted_balance: Amount::from_sat(s.untrusted_balance),
477			confirmed_utxos: s.confirmed_utxos.iter().map(|u| {
478				u.parse().map_err(|_| "invalid outpoint")
479			}).collect::<Result<_, _>>()?,
480			unconfirmed_utxos: s.unconfirmed_utxos.iter().map(|u| {
481				u.parse().map_err(|_| "invalid outpoint")
482			}).collect::<Result<_, _>>()?,
483		})
484	}
485}
486
487
488impl<'a> From<&'a VtxoRequest> for protos::VtxoRequest {
489	fn from(v: &'a VtxoRequest) -> Self {
490		protos::VtxoRequest {
491			amount: v.amount.to_sat(),
492			policy: v.policy.serialize(),
493		}
494	}
495}
496
497impl TryFrom<protos::VtxoRequest> for VtxoRequest {
498	type Error = ConvertError;
499	fn try_from(v: protos::VtxoRequest) -> Result<Self, Self::Error> {
500		Ok(Self {
501			amount: Amount::from_sat(v.amount),
502			policy: VtxoPolicy::deserialize(&v.policy).map_err(|_| "invalid policy")?,
503		})
504	}
505}
506
507impl TryFrom<protos::ArkoorDestination> for ArkoorDestination {
508	type Error = ConvertError;
509	fn try_from(v: protos::ArkoorDestination) -> Result<Self, Self::Error> {
510		Ok(Self {
511			total_amount: Amount::from_sat(v.total_amount),
512			policy: VtxoPolicy::deserialize(&v.policy).map_err(|_| "invalid policy")?,
513		})
514	}
515}
516
517impl From<ArkoorDestination> for protos::ArkoorDestination {
518	fn from(v: ArkoorDestination) -> Self {
519		Self {
520			total_amount: v.total_amount.to_sat(),
521			policy: v.policy.serialize(),
522		}
523	}
524}
525
526impl From<SignedVtxoRequest> for protos::SignedVtxoRequest {
527	fn from(v: SignedVtxoRequest) -> Self {
528		protos::SignedVtxoRequest {
529			vtxo: Some(protos::VtxoRequest {
530				amount: v.vtxo.amount.to_sat(),
531				policy: v.vtxo.policy.serialize(),
532			}),
533			cosign_pubkey: v.cosign_pubkey.serialize().to_vec(),
534			public_nonces: v.nonces.iter().map(|n| n.serialize().to_vec()).collect(),
535		}
536	}
537}
538
539impl TryFrom<protos::SignedVtxoRequest> for SignedVtxoRequest {
540	type Error = ConvertError;
541	fn try_from(v: protos::SignedVtxoRequest) -> Result<Self, Self::Error> {
542		let vtxo = v.vtxo.ok_or("vtxo field missing")?;
543		Ok(SignedVtxoRequest {
544			vtxo: VtxoRequest {
545				amount: Amount::from_sat(vtxo.amount),
546				policy: VtxoPolicy::from_bytes(&vtxo.policy)?,
547			},
548			cosign_pubkey: PublicKey::from_bytes(&v.cosign_pubkey)?,
549			nonces: v.public_nonces.into_iter()
550				.map(|n| musig::PublicNonce::from_bytes(n))
551				.collect::<Result<_, _>>()?,
552		})
553	}
554}
555
556impl From<BoardCosignResponse> for protos::BoardCosignResponse {
557	fn from(v: BoardCosignResponse) -> Self {
558		Self {
559			pub_nonce: v.pub_nonce.serialize().to_vec(),
560			partial_sig: v.partial_signature.serialize().to_vec(),
561		}
562	}
563}
564
565impl TryFrom<protos::BoardCosignResponse> for BoardCosignResponse {
566	type Error = ConvertError;
567	fn try_from(v: protos::BoardCosignResponse) -> Result<Self, Self::Error> {
568		Ok(Self {
569			pub_nonce: musig::PublicNonce::from_bytes(&v.pub_nonce)?,
570			partial_signature: musig::PartialSignature::from_bytes(&v.partial_sig)?,
571		})
572	}
573}
574
575impl From<ark::integration::TokenType> for protos::intman::TokenType {
576	fn from(value: ark::integration::TokenType) -> Self {
577		match value {
578			ark::integration::TokenType::SingleUseBoard => protos::intman::TokenType::SingleUseBoard,
579		}
580	}
581}
582
583impl From<protos::intman::TokenType> for ark::integration::TokenType {
584	fn from(value: protos::intman::TokenType) -> Self {
585		match value {
586			protos::intman::TokenType::SingleUseBoard => ark::integration::TokenType::SingleUseBoard,
587		}
588	}
589}
590
591impl From<protos::intman::TokenStatus> for ark::integration::TokenStatus {
592	fn from(value: protos::intman::TokenStatus) -> Self {
593		match value {
594			protos::intman::TokenStatus::Unused => ark::integration::TokenStatus::Unused,
595			protos::intman::TokenStatus::Used => ark::integration::TokenStatus::Used,
596			protos::intman::TokenStatus::Abused => ark::integration::TokenStatus::Abused,
597			protos::intman::TokenStatus::Disabled => ark::integration::TokenStatus::Disabled,
598			// Setting to `Unused` since this status is an alias for `Unused` + expired.
599			protos::intman::TokenStatus::Expired => ark::integration::TokenStatus::Unused,
600		}
601	}
602}
603
604impl From<ark::integration::TokenStatus> for protos::intman::TokenStatus {
605	fn from(value: ark::integration::TokenStatus) -> Self {
606		match value {
607			ark::integration::TokenStatus::Unused => protos::intman::TokenStatus::Unused,
608			ark::integration::TokenStatus::Used => protos::intman::TokenStatus::Used,
609			ark::integration::TokenStatus::Abused => protos::intman::TokenStatus::Abused,
610			ark::integration::TokenStatus::Disabled => protos::intman::TokenStatus::Disabled,
611		}
612	}
613}
614
615// Serialization of a CosignRequest
616impl<V: VtxoRef> From<ArkoorCosignRequest<V>> for protos::ArkoorCosignRequest {
617	fn from(v: ArkoorCosignRequest<V>) -> Self {
618		Self {
619			input_vtxo_id: v.input.vtxo_id().serialize(),
620			user_pub_nonces: v.user_pub_nonces.into_iter()
621				.map(|n| n.serialize().to_vec())
622				.collect::<Vec<_>>(),
623			outputs: v.outputs.into_iter().map(|output| output.into()).collect::<Vec<_>>(),
624			isolated_outputs: v.isolated_outputs.into_iter()
625				.map(|output| output.into())
626				.collect::<Vec<_>>(),
627			use_checkpoint: v.use_checkpoint,
628			attestation: v.attestation.serialize().to_vec(),
629		}
630	}
631}
632
633// Deserialization of CosignRequest
634impl TryFrom<protos::ArkoorCosignRequest> for ArkoorCosignRequest<VtxoId> {
635	type Error = ConvertError;
636	fn try_from(v: protos::ArkoorCosignRequest) -> Result<Self, Self::Error> {
637		let req = Self::new_with_attestation(
638			v.user_pub_nonces.into_iter()
639				.map(|n| musig::PublicNonce::from_bytes(&n))
640				.collect::<Result<Vec<_>, _>>()?,
641			VtxoId::from_bytes(&v.input_vtxo_id)?,
642			v.outputs.into_iter()
643				.map(|output| ArkoorDestination::try_from(output))
644				.collect::<Result<Vec<_>, _>>()?,
645			v.isolated_outputs.into_iter()
646				.map(|output| ArkoorDestination::try_from(output))
647				.collect::<Result<Vec<_>, _>>()?,
648			v.use_checkpoint,
649			ArkoorCosignAttestation::deserialize(&v.attestation)
650				.map_err(|_| "Failed to parse attestation")?,
651		);
652		Ok(req)
653	}
654}
655
656impl<V: VtxoRef> From<ArkoorPackageCosignRequest<V>> for protos::ArkoorPackageCosignRequest {
657	fn from(v: ArkoorPackageCosignRequest<V>) -> Self {
658		Self {
659			parts: v.requests.into_iter().map(|p| p.into()).collect(),
660		}
661	}
662}
663
664impl<'a> TryFrom<protos::ArkoorPackageCosignRequest> for ArkoorPackageCosignRequest<VtxoId> {
665	type Error = ConvertError;
666
667	fn try_from(v: protos::ArkoorPackageCosignRequest) -> Result<Self, Self::Error> {
668		Ok(Self {
669			requests: v.parts.into_iter().map(|p| p.try_into()).collect::<Result<Vec<_>, _>>()?,
670		})
671	}
672}
673
674impl<'a> TryFrom<protos::LightningPayHtlcCosignRequest> for ArkoorPackageCosignRequest<VtxoId> {
675	type Error = ConvertError;
676
677	fn try_from(v: protos::LightningPayHtlcCosignRequest) -> Result<Self, Self::Error> {
678		Ok(Self {
679			requests: v.parts.into_iter().map(|p| p.try_into()).collect::<Result<Vec<_>, _>>()?,
680		})
681	}
682}
683
684impl From<ArkoorCosignResponse> for protos::ArkoorCosignResponse {
685	fn from(v: ArkoorCosignResponse) -> Self {
686		Self {
687			server_pub_nonces: v.server_pub_nonces.into_iter().map(|p| p.serialize().to_vec()).collect::<Vec<_>>(),
688			server_partial_sigs: v.server_partial_sigs.into_iter().map(|p| p.serialize().to_vec()).collect::<Vec<_>>(),
689		}
690	}
691}
692
693impl TryFrom<protos::ArkoorCosignResponse> for ArkoorCosignResponse {
694	type Error = ConvertError;
695	fn try_from(v: protos::ArkoorCosignResponse) -> Result<Self, Self::Error> {
696		Ok(Self {
697			server_pub_nonces: v.server_pub_nonces.into_iter().map(|n| musig::PublicNonce::from_bytes(&n)).collect::<Result<Vec<_>, _>>()?,
698			server_partial_sigs: v.server_partial_sigs.into_iter().map(|n| musig::PartialSignature::from_bytes(&n)).collect::<Result<Vec<_>, _>>()?,
699		})
700	}
701}
702
703impl From<ArkoorPackageCosignResponse> for protos::ArkoorPackageCosignResponse {
704	fn from(v: ArkoorPackageCosignResponse) -> Self {
705		Self {
706			parts: v.responses.into_iter().map(|p| p.into()).collect::<Vec<_>>(),
707		}
708	}
709}
710
711impl TryFrom<protos::ArkoorPackageCosignResponse> for ArkoorPackageCosignResponse {
712	type Error = ConvertError;
713	fn try_from(v: protos::ArkoorPackageCosignResponse) -> Result<Self, Self::Error> {
714		Ok(Self {
715			responses: v.parts.into_iter().map(|p| p.try_into()).collect::<Result<Vec<_>, _>>()?,
716		})
717	}
718}
719
720impl From<LeafVtxoCosignRequest> for protos::LeafVtxoCosignRequest {
721	fn from(v: LeafVtxoCosignRequest) -> Self {
722		protos::LeafVtxoCosignRequest {
723			vtxo_id: v.vtxo_id.to_bytes().to_vec(),
724			public_nonce: v.pub_nonce.serialize().to_vec(),
725		}
726	}
727}
728
729impl From<LeafVtxoCosignResponse> for protos::LeafVtxoCosignResponse {
730	fn from(v: LeafVtxoCosignResponse) -> Self {
731		protos::LeafVtxoCosignResponse {
732			public_nonce: v.public_nonce.serialize().to_vec(),
733			partial_signature: v.partial_signature.serialize().to_vec(),
734		}
735	}
736}
737
738impl TryFrom<protos::LeafVtxoCosignResponse> for LeafVtxoCosignResponse {
739	type Error = ConvertError;
740
741	fn try_from(v: protos::LeafVtxoCosignResponse) -> Result<Self, Self::Error> {
742		Ok(Self {
743			public_nonce: TryFromBytes::from_bytes(v.public_nonce)?,
744			partial_signature: TryFromBytes::from_bytes(v.partial_signature)?,
745		})
746	}
747}
748
749impl<V: Borrow<OffboardRequest>> From<V> for protos::OffboardRequest {
750	fn from(v: V) -> Self {
751		let v = v.borrow();
752	    protos::OffboardRequest {
753			offboard_spk: v.script_pubkey.to_bytes(),
754			net_amount_sat: v.net_amount.to_sat(),
755			deduct_fees_from_gross_amount: v.deduct_fees_from_gross_amount,
756			fee_rate_kwu: v.fee_rate.to_sat_per_kwu(),
757		}
758	}
759}
760
761impl TryFrom<protos::OffboardRequest> for OffboardRequest {
762	type Error = ConvertError;
763
764	fn try_from(v: protos::OffboardRequest) -> Result<Self, Self::Error> {
765		Ok(Self {
766			script_pubkey: ScriptBuf::from_bytes(v.offboard_spk),
767			net_amount: Amount::from_sat(v.net_amount_sat),
768			deduct_fees_from_gross_amount: v.deduct_fees_from_gross_amount,
769			fee_rate: FeeRate::from_sat_per_kwu(v.fee_rate_kwu),
770		})
771	}
772}
773
774#[cfg(test)]
775mod test {
776	use std::str::FromStr;
777	use bitcoin::hex::FromHex;
778	use super::*;
779
780	#[test]
781	fn test_preimage_bytes() {
782		let h = "ef2cb05d04819ddb2b9d960c7e0e295ea48ffb429712dc8f30aa48dfcc20c97e";
783		let b = Vec::<u8>::from_hex(h).unwrap();
784
785		let preimage = Preimage::from_str(h).unwrap();
786		assert_eq!(preimage, Preimage::from_bytes(&b).unwrap());
787		assert_eq!(preimage, Preimage::from_slice(&b).unwrap());
788		assert_eq!(preimage, Preimage::from_slice(&preimage.to_vec()).unwrap());
789		assert_eq!(preimage, Preimage::from_bytes(&preimage.to_vec()).unwrap());
790	}
791
792	/// Baseline proto with valid network and pubkeys, so TryFrom proceeds far
793	/// enough to exercise the delta validators. Fields after the deltas may be
794	/// invalid; these tests only assert delta-rejection short-circuits.
795	fn baseline_ark_info_proto() -> protos::ArkInfo {
796		let pk = PublicKey::from_str(
797			"02dfa52f6690299d2d6a08323083e290597b56fee125063e5f4e2957731639c42c",
798		).unwrap();
799		protos::ArkInfo {
800			network: "regtest".into(),
801			server_pubkey: pk.serialize().to_vec(),
802			mailbox_pubkey: pk.serialize().to_vec(),
803			round_interval_secs: 60,
804			nb_round_nonces: 1,
805			vtxo_exit_delta: 12,
806			vtxo_lifetime: 100,
807			#[allow(deprecated)]
808			vtxo_expiry_delta: 100,
809			htlc_send_expiry_delta: 100,
810			htlc_expiry_delta: 6,
811			max_vtxo_amount: None,
812			required_board_confirmations: 1,
813			max_user_invoice_cltv_delta: 50,
814			min_board_amount: 0,
815			ln_receive_anti_dos_required: false,
816			#[allow(deprecated)]
817			offboard_feerate_sat_vkb: 1000,
818			fees: None,
819			max_vtxo_exit_depth: 5,
820			max_offboard_inputs: 10,
821			tos_link: None,
822		}
823	}
824
825	#[test]
826	fn ark_info_rejects_oversized_vtxo_exit_delta() {
827		let mut proto = baseline_ark_info_proto();
828		proto.vtxo_exit_delta = ark::vtxo::policy::MAX_BLOCK_DELTA.to_u32() + 1;
829		assert!(ark::ArkInfo::try_from(proto).is_err());
830	}
831
832	#[test]
833	fn ark_info_rejects_oversized_htlc_expiry_delta() {
834		let mut proto = baseline_ark_info_proto();
835		proto.htlc_expiry_delta = ark::vtxo::policy::MAX_BLOCK_DELTA.to_u32() + 1;
836		assert!(ark::ArkInfo::try_from(proto).is_err());
837	}
838
839	#[test]
840	fn ark_info_rejects_oversized_max_user_invoice_cltv_delta() {
841		let mut proto = baseline_ark_info_proto();
842		proto.max_user_invoice_cltv_delta = ark::vtxo::policy::MAX_BLOCK_DELTA.to_u32() + 1;
843		assert!(ark::ArkInfo::try_from(proto).is_err());
844	}
845
846	#[test]
847	fn ark_info_rejects_oversized_required_board_confirmations() {
848		// A malicious server could otherwise push this near u32::MAX and overflow
849		// the client's `current_height + required` board arithmetic.
850		let mut proto = baseline_ark_info_proto();
851		proto.required_board_confirmations = ark::vtxo::policy::MAX_BLOCK_DELTA.to_u32() + 1;
852		assert!(ark::ArkInfo::try_from(proto).is_err());
853	}
854
855	#[test]
856	#[allow(deprecated)] // vtxo_expiry_delta kept for old peers
857	fn ark_info_vtxo_lifetime_falls_back_to_deprecated_field() {
858		// Servers from before the rename only set vtxo_expiry_delta.
859		let mut proto = baseline_ark_info_proto();
860		proto.fees = Some(ark::fees::FeeSchedule::default().into());
861		proto.vtxo_lifetime = 0;
862		proto.vtxo_expiry_delta = 42;
863
864		let info = ark::ArkInfo::try_from(proto).unwrap();
865		assert_eq!(info.vtxo_lifetime.to_u32(), 42);
866		assert_eq!(info.vtxo_expiry_delta.to_u32(), 42);
867	}
868
869	#[test]
870	#[allow(deprecated)] // vtxo_expiry_delta kept for old peers
871	fn ark_info_vtxo_lifetime_takes_precedence() {
872		let mut proto = baseline_ark_info_proto();
873		proto.fees = Some(ark::fees::FeeSchedule::default().into());
874		proto.vtxo_lifetime = 42;
875		proto.vtxo_expiry_delta = 100;
876
877		let info = ark::ArkInfo::try_from(proto).unwrap();
878		assert_eq!(info.vtxo_lifetime.to_u32(), 42);
879		assert_eq!(info.vtxo_expiry_delta.to_u32(), 42);
880
881		// and both fields are populated again on the way out
882		let proto = protos::ArkInfo::from(info);
883		assert_eq!(proto.vtxo_lifetime, 42);
884		assert_eq!(proto.vtxo_expiry_delta, 42);
885	}
886
887	#[test]
888	fn ark_info_rejects_u32_max_delta() {
889		// Pre-MR a u32 value above u16::MAX would have failed try_into; verify
890		// the new validator still rejects values that exceed the policy bound.
891		let mut proto = baseline_ark_info_proto();
892		proto.htlc_expiry_delta = u32::MAX;
893		assert!(ark::ArkInfo::try_from(proto).is_err());
894	}
895}