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