From 7c4e1efe089a07243cc5e78b371339c7bf966f46 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 19:09:48 -0400 Subject: [PATCH 01/22] feat(ffor): verify committed voucher books --- lightning/src/chain/channelmonitor.rs | 40 +++ lightning/src/ln/channel.rs | 2 + lightning/src/ln/channel/ffor.rs | 147 +++++++++ lightning/src/ln/channelmanager.rs | 38 +++ lightning/src/ln/ffor.rs | 237 +++++++++++++ lightning/src/ln/ffor_tests.rs | 457 ++++++++++++++++++++++++++ lightning/src/ln/mod.rs | 3 + 7 files changed, 924 insertions(+) create mode 100644 lightning/src/ln/channel/ffor.rs create mode 100644 lightning/src/ln/ffor.rs create mode 100644 lightning/src/ln/ffor_tests.rs diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 7727366c2ad..022bc3a2495 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -56,6 +56,7 @@ use crate::ln::channel_keys::{ RevocationKey, }; use crate::ln::channelmanager::{HTLCSource, PaymentClaimDetails, SentHTLCId}; +use crate::ln::ffor::{FFORCommitmentError, FFORMonitorSnapshot}; use crate::ln::msgs::DecodeError; use crate::ln::types::ChannelId; use crate::sign::{ @@ -2104,6 +2105,45 @@ impl ChannelMonitor { self.inner.lock().unwrap().get_latest_update_id() } + /// Captures canonical commitment identities and signed claim material for FFOR verification. + /// + /// Release any [`ChainMonitor::get_monitor`] guard before passing the result to + /// [`ChannelManager::ffor_voucher_commitments`], so no monitor lock is held while acquiring the + /// manager's channel lock. The manager rejects snapshots that do not match its current state. + /// This snapshot alone neither proves completed persistence nor activates offline receiving. + /// + /// [`ChainMonitor::get_monitor`]: crate::chain::chainmonitor::ChainMonitor::get_monitor + /// [`ChannelManager::ffor_voucher_commitments`]: crate::ln::channelmanager::ChannelManager::ffor_voucher_commitments + pub fn ffor_commitment_snapshot(&self) -> Result { + let inner = self.inner.lock().unwrap(); + if inner.funding_spend_seen || inner.lockdown_from_offchain || inner.holder_tx_signed { + return Err(FFORCommitmentError::ChannelUnavailable); + } + if !inner.pending_funding.is_empty() { + return Err(FFORCommitmentError::PendingUpdates); + } + let counterparty_txid = inner + .funding + .current_counterparty_commitment_txid + .ok_or(FFORCommitmentError::MonitorMismatch)?; + let htlcs = inner + .funding + .counterparty_claimable_outpoints + .get(&counterparty_txid) + .ok_or(FFORCommitmentError::MonitorMismatch)?; + Ok(FFORMonitorSnapshot { + channel_id: inner.channel_id(), + funding_txo: inner.get_funding_txo(), + update_id: inner.latest_update_id, + holder: inner.funding.current_holder_commitment_tx.clone(), + holder_number: inner.current_holder_commitment_number, + counterparty_txid, + counterparty_number: inner.current_counterparty_commitment_number, + counterparty_htlcs: htlcs.iter().map(|(htlc, _)| htlc.clone()).collect(), + revoked_through: inner.commitment_secrets.get_min_seen_secret(), + }) + } + /// Gets the funding transaction outpoint of the channel this ChannelMonitor is monitoring for. pub fn get_funding_txo(&self) -> OutPoint { self.inner.lock().unwrap().get_funding_txo() diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index a89e240edc1..c38333b4c8e 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -99,6 +99,8 @@ use core::{cmp, fmt, mem}; use super::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}; +mod ffor; + #[cfg(any(test, feature = "_test_utils"))] #[allow(unused)] pub struct ChannelValueStat { diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs new file mode 100644 index 00000000000..b83b56af029 --- /dev/null +++ b/lightning/src/ln/channel/ffor.rs @@ -0,0 +1,147 @@ +use super::*; +use crate::ln::ffor::{ + self as verification, FFORCommitmentError, FFORMonitorSnapshot, FFORSettlementParty, + FFORVoucher, FFORVoucherCommitments, +}; + +impl FundedChannel +where + SP::Target: SignerProvider, +{ + pub(crate) fn ffor_voucher_commitments( + &self, settlement_party: FFORSettlementParty, vouchers: &[FFORVoucher], + monitor: &FFORMonitorSnapshot, logger: &L, + ) -> Result + where + L::Target: Logger, + { + self.check_ffor_synchronized()?; + verification::validate_vouchers(vouchers)?; + self.check_ffor_htlc_ids(settlement_party, vouchers)?; + let context = &self.context; + if monitor.channel_id != context.channel_id() + || Some(monitor.funding_txo) != self.funding.get_funding_txo() + || monitor.update_id != context.latest_monitor_update_id + { + return Err(FFORCommitmentError::MonitorMismatch); + } + + let holder_point = self + .holder_commitment_point + .current_point() + .ok_or(FFORCommitmentError::PendingUpdates)?; + let counterparty_point = context + .counterparty_current_commitment_point + .ok_or(FFORCommitmentError::PendingUpdates)?; + let holder = context + .build_commitment_transaction( + &self.funding, + self.holder_commitment_point.current_transaction_number(), + &holder_point, + true, + false, + logger, + ) + .tx; + let counterparty = context + .build_commitment_transaction( + &self.funding, + context.counterparty_next_commitment_transaction_number + 1, + &counterparty_point, + false, + true, + logger, + ) + .tx; + let holder_offered = settlement_party == FFORSettlementParty::Holder; + verification::verify_outputs(&holder, holder_offered, vouchers)?; + verification::verify_outputs(&counterparty, !holder_offered, vouchers)?; + if monitor.holder_number != holder.commitment_number() + || monitor.holder.commitment_number() != holder.commitment_number() + || monitor.holder.trust().built_transaction().transaction + != holder.trust().built_transaction().transaction + || monitor.holder.trust().txid() != holder.trust().txid() + || monitor.holder.nondust_htlcs() != holder.nondust_htlcs() + || monitor.counterparty_number != counterparty.commitment_number() + || monitor.counterparty_txid != counterparty.trust().txid() + || &monitor.counterparty_htlcs != counterparty.nondust_htlcs() + || monitor.revoked_through != counterparty.commitment_number() + 1 + || context.commitment_secrets.get_min_seen_secret() != monitor.revoked_through + { + return Err(FFORCommitmentError::MonitorMismatch); + } + verification::verify_claim_signatures( + &holder, + &monitor.holder, + &self.funding.channel_transaction_parameters, + )?; + Ok(FFORVoucherCommitments { + holder: verification::commitment_identity(&holder), + counterparty: verification::commitment_identity(&counterparty), + }) + } + + fn check_ffor_synchronized(&self) -> Result<(), FFORCommitmentError> { + let context = &self.context; + let channel_type = self.funding.get_channel_type(); + if !channel_type.supports_static_remote_key() + || !channel_type.supports_anchors_zero_fee_htlc_tx() + || channel_type.supports_taproot() + || channel_type.supports_anchor_zero_fee_commitments() + { + return Err(FFORCommitmentError::UnsupportedChannelType); + } + if !context.is_usable() { + return Err(FFORCommitmentError::ChannelUnavailable); + } + if context.is_waiting_on_peer_pending_channel_update() + || context.is_monitor_or_signer_pending_channel_update() + || !context.holding_cell_htlc_updates.is_empty() + || context.holding_cell_update_fee.is_some() + || !context.blocked_monitor_updates.is_empty() + || context.monitor_pending_revoke_and_ack + || context.monitor_pending_commitment_signed + || context.interactive_tx_signing_session.is_some() + || self.pending_splice.is_some() + || matches!(self.quiescent_action, Some(QuiescentAction::Splice(_))) + { + return Err(FFORCommitmentError::PendingUpdates); + } + Ok(()) + } + + fn check_ffor_htlc_ids( + &self, settlement_party: FFORSettlementParty, vouchers: &[FFORVoucher], + ) -> Result<(), FFORCommitmentError> { + let context = &self.context; + let actual: Vec = match settlement_party { + FFORSettlementParty::Holder if context.pending_inbound_htlcs.is_empty() => context + .pending_outbound_htlcs + .iter() + .map(|h| FFORVoucher { + htlc_id: h.htlc_id, + payment_hash: h.payment_hash, + amount_msat: h.amount_msat, + cltv_expiry: h.cltv_expiry, + }) + .collect(), + FFORSettlementParty::Counterparty if context.pending_outbound_htlcs.is_empty() => { + context + .pending_inbound_htlcs + .iter() + .map(|h| FFORVoucher { + htlc_id: h.htlc_id, + payment_hash: h.payment_hash, + amount_msat: h.amount_msat, + cltv_expiry: h.cltv_expiry, + }) + .collect() + }, + _ => return Err(FFORCommitmentError::InvalidVoucherBook), + }; + if actual.len() != vouchers.len() || actual.iter().any(|htlc| !vouchers.contains(htlc)) { + return Err(FFORCommitmentError::InvalidVoucherBook); + } + Ok(()) + } +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index dbfe949dd25..d9b2cf745aa 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -65,6 +65,10 @@ use crate::ln::channel::{ WithChannelContext, }; use crate::ln::channel_state::ChannelDetails; +use crate::ln::ffor::{ + FFORCommitmentError, FFORMonitorSnapshot, FFORSettlementParty, FFORVoucher, + FFORVoucherCommitments, +}; use crate::ln::funding::SpliceContribution; use crate::ln::inbound_payment; use crate::ln::interactivetxs::InteractiveTxMessageSend; @@ -4262,6 +4266,40 @@ where self.list_funded_channels_with_filter(|&(_, _, ref channel)| channel.context().is_live()) } + /// Verifies that both current commitments contain exactly the supplied Variant D voucher book. + /// + /// Vouchers must be in slot order, with consecutive settlement-peer HTLC IDs, unique hashes and + /// a uniform expiry. Both commitment rounds must be complete, with no other HTLCs, trimmed + /// vouchers, pending fee updates, signer operations, monitor persistence, or splices. The method + /// rebuilds both transactions and verifies the monitor's holder commitment and HTLC signatures. + /// + /// Obtain `monitor` from [`ChannelMonitor::ffor_commitment_snapshot`] and release any monitor + /// guard before calling this method. Stale update IDs or transaction identities are rejected. + /// Completed persistence relies on the application's correct [`Watch`] implementation. + /// + /// This is a read-only, point-in-time check. It does not authenticate the supplied book, park + /// HTLCs, freeze the channel, or activate an epoch. It never authorizes invoice exposure or + /// preimage release. Future durable activation must recheck state while serializing channel + /// updates under the same authority. + pub fn ffor_voucher_commitments( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + settlement_party: FFORSettlementParty, vouchers: &[FFORVoucher], + monitor: &FFORMonitorSnapshot, + ) -> Result { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer = per_peer_state + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(channel_id) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + channel.ffor_voucher_commitments(settlement_party, vouchers, monitor, &self.logger) + } + /// Gets the list of channels we have with a given counterparty, in random order. pub fn list_channels_with_counterparty( &self, counterparty_node_id: &PublicKey, diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs new file mode 100644 index 00000000000..f3cd9272434 --- /dev/null +++ b/lightning/src/ln/ffor.rs @@ -0,0 +1,237 @@ +//! Point-in-time verification of the committed voucher book for FFOR Variant D. +//! +//! These checks do not park HTLCs, freeze a channel, authenticate an epoch, or activate offline +//! receiving. A successful result must never by itself authorize invoice exposure or preimage +//! release. Future activation must recheck the commitments while atomically freezing channel state. + +use alloc::collections::{BTreeMap, BTreeSet}; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::{Message, Secp256k1}; +use bitcoin::sighash::{EcdsaSighashType, SighashCache}; +use bitcoin::Txid; +use core::fmt; + +use crate::chain::transaction::OutPoint; +use crate::ln::chan_utils::{ + self, ChannelTransactionParameters, CommitmentTransaction, HTLCOutputInCommitment, + HolderCommitmentTransaction, +}; +use crate::ln::channel::INITIAL_COMMITMENT_NUMBER; +use crate::ln::types::ChannelId; +use crate::prelude::*; +use crate::types::payment::PaymentHash; + +/// Which channel participant offers every voucher HTLC in the book. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FFORSettlementParty { + /// The local node is the settlement peer and offers the voucher HTLCs. + Holder, + /// The remote node is the settlement peer and offers the voucher HTLCs. + Counterparty, +} + +/// The public terms of one voucher from an authenticated FFOR Variant D book. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FFORVoucher { + /// The settlement peer's offered HTLC ID. + pub htlc_id: u64, + /// The voucher's payment hash. This API neither needs nor returns a preimage. + pub payment_hash: PaymentHash, + /// The exact value in millisatoshis, before on-chain rounding. + pub amount_msat: u64, + /// The absolute CLTV expiry height shared by every voucher in the book. + pub cltv_expiry: u32, +} + +/// An actual current commitment transaction identified for the FFOR activation transcript. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FFORCommitment { + /// The forward-counting BOLT commitment number, starting at zero. + pub number: u64, + /// The commitment transaction ID. FFOR hashes its internal byte order. + pub txid: Txid, +} + +/// Verified identities of both commitment views at one instant. +/// +/// This result does not reserve the channel or establish an active FFOR epoch. Ordinary channel +/// updates may invalidate it immediately after the verification returns. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FFORVoucherCommitments { + /// The local node's commitment. + pub holder: FFORCommitment, + /// The remote node's commitment. + pub counterparty: FFORCommitment, +} + +/// Why the current channel state cannot prove an enforceable Variant D voucher book. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FFORCommitmentError { + /// The channel does not exist or is not fully established and open. + ChannelUnavailable, + /// Only ECDSA static-remote-key channels with zero-fee-HTLC anchors are supported. + UnsupportedChannelType, + /// A channel update, signer operation, monitor persistence operation, or splice is pending. + PendingUpdates, + /// The expected book is empty, oversized, ambiguous, or differs from the full HTLC set. + InvalidVoucherBook, + /// At least one expected voucher is trimmed from a commitment transaction. + TrimmedVoucher, + /// The monitor snapshot is stale, belongs to another channel, or differs from rebuilt state. + MonitorMismatch, + /// The holder commitment or one of its second-stage HTLC signatures is invalid or missing. + InvalidClaimMaterial, +} + +impl fmt::Display for FFORCommitmentError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::ChannelUnavailable => "channel unavailable for FFOR verification", + Self::UnsupportedChannelType => "unsupported FFOR channel type", + Self::PendingUpdates => "FFOR vouchers are not fully committed", + Self::InvalidVoucherBook => "invalid committed FFOR voucher book", + Self::TrimmedVoucher => "FFOR voucher is trimmed", + Self::MonitorMismatch => "FFOR monitor snapshot does not match channel state", + Self::InvalidClaimMaterial => "invalid FFOR claim signatures", + } + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for FFORCommitmentError {} + +/// Opaque commitment and claim material captured by a channel monitor. +/// +/// Obtain this from [`ChannelMonitor::ffor_commitment_snapshot`], release any monitor lock, then +/// pass it to [`ChannelManager::ffor_voucher_commitments`]. Its presence alone does not prove that +/// monitor persistence has completed. The manager also checks update IDs and pending operations. +/// Applications remain responsible for honoring the [`Watch`] persistence completion contract. +/// +/// [`ChannelMonitor::ffor_commitment_snapshot`]: crate::chain::channelmonitor::ChannelMonitor::ffor_commitment_snapshot +/// [`ChannelManager::ffor_voucher_commitments`]: crate::ln::channelmanager::ChannelManager::ffor_voucher_commitments +/// [`Watch`]: crate::chain::Watch +pub struct FFORMonitorSnapshot { + pub(crate) channel_id: ChannelId, + pub(crate) funding_txo: OutPoint, + pub(crate) update_id: u64, + pub(crate) holder: HolderCommitmentTransaction, + pub(crate) holder_number: u64, + pub(crate) counterparty_txid: Txid, + pub(crate) counterparty_number: u64, + pub(crate) counterparty_htlcs: Vec, + pub(crate) revoked_through: u64, +} + +pub(crate) fn validate_vouchers(vouchers: &[FFORVoucher]) -> Result<(), FFORCommitmentError> { + let first = vouchers.first().ok_or(FFORCommitmentError::InvalidVoucherBook)?; + if vouchers.len() > 483 { + return Err(FFORCommitmentError::InvalidVoucherBook); + } + let mut hashes = BTreeSet::new(); + for (index, voucher) in vouchers.iter().enumerate() { + if first.htlc_id.checked_add(index as u64) != Some(voucher.htlc_id) + || voucher.amount_msat == 0 + || voucher.cltv_expiry == 0 + || voucher.cltv_expiry != first.cltv_expiry + || !hashes.insert(voucher.payment_hash.0) + { + return Err(FFORCommitmentError::InvalidVoucherBook); + } + } + Ok(()) +} + +pub(crate) fn verify_outputs( + commitment: &CommitmentTransaction, offered: bool, vouchers: &[FFORVoucher], +) -> Result<(), FFORCommitmentError> { + if commitment.nondust_htlcs().len() != vouchers.len() { + return Err(FFORCommitmentError::TrimmedVoucher); + } + let mut by_hash: BTreeMap<_, _> = vouchers.iter().map(|v| (v.payment_hash.0, v)).collect(); + let trusted = commitment.trust(); + let tx = &trusted.built_transaction().transaction; + let mut output_indices = BTreeSet::new(); + for htlc in commitment.nondust_htlcs() { + let voucher = + by_hash.remove(&htlc.payment_hash.0).ok_or(FFORCommitmentError::InvalidVoucherBook)?; + if htlc.offered != offered + || htlc.amount_msat != voucher.amount_msat + || htlc.cltv_expiry != voucher.cltv_expiry + { + return Err(FFORCommitmentError::InvalidVoucherBook); + } + let index = htlc.transaction_output_index.ok_or(FFORCommitmentError::TrimmedVoucher)?; + let output = tx.output.get(index as usize).ok_or(FFORCommitmentError::TrimmedVoucher)?; + let script = chan_utils::get_htlc_redeemscript( + htlc, + trusted.channel_type_features(), + trusted.keys(), + ); + if !output_indices.insert(index) + || output.value != htlc.to_bitcoin_amount() + || output.script_pubkey != script.to_p2wsh() + { + return Err(FFORCommitmentError::InvalidVoucherBook); + } + } + Ok(()) +} + +pub(crate) fn verify_claim_signatures( + holder: &CommitmentTransaction, stored: &HolderCommitmentTransaction, + parameters: &ChannelTransactionParameters, +) -> Result<(), FFORCommitmentError> { + let counterparty = parameters + .counterparty_parameters + .as_ref() + .ok_or(FFORCommitmentError::InvalidClaimMaterial)?; + let trusted = holder.trust(); + let built = trusted.built_transaction(); + let funding_script = parameters.make_funding_redeemscript(); + let secp_ctx = Secp256k1::verification_only(); + let sighash = built.get_sighash_all(&funding_script, parameters.channel_value_satoshis); + secp_ctx + .verify_ecdsa(&sighash, &stored.counterparty_sig, &counterparty.pubkeys.funding_pubkey) + .map_err(|_| FFORCommitmentError::InvalidClaimMaterial)?; + if stored.counterparty_htlc_sigs.len() != holder.nondust_htlcs().len() { + return Err(FFORCommitmentError::InvalidClaimMaterial); + } + let keys = trusted.keys(); + for (htlc, signature) in holder.nondust_htlcs().iter().zip(&stored.counterparty_htlc_sigs) { + let tx = chan_utils::build_htlc_transaction( + &built.txid, + holder.negotiated_feerate_per_kw(), + counterparty.selected_contest_delay, + htlc, + ¶meters.channel_type_features, + &keys.broadcaster_delayed_payment_key, + &keys.revocation_key, + ); + let script = + chan_utils::get_htlc_redeemscript(htlc, ¶meters.channel_type_features, keys); + let hash = SighashCache::new(&tx) + .p2wsh_signature_hash( + 0, + &script, + htlc.to_bitcoin_amount(), + EcdsaSighashType::SinglePlusAnyoneCanPay, + ) + .map_err(|_| FFORCommitmentError::InvalidClaimMaterial)?; + let message = Message::from_digest(hash.to_byte_array()); + secp_ctx + .verify_ecdsa(&message, signature, &keys.countersignatory_htlc_key.to_public_key()) + .map_err(|_| FFORCommitmentError::InvalidClaimMaterial)?; + } + Ok(()) +} + +pub(crate) fn commitment_identity(commitment: &CommitmentTransaction) -> FFORCommitment { + FFORCommitment { + number: INITIAL_COMMITMENT_NUMBER - commitment.commitment_number(), + txid: commitment.trust().txid(), + } +} diff --git a/lightning/src/ln/ffor_tests.rs b/lightning/src/ln/ffor_tests.rs new file mode 100644 index 00000000000..90d60d402c0 --- /dev/null +++ b/lightning/src/ln/ffor_tests.rs @@ -0,0 +1,457 @@ +use crate::chain::ChannelMonitorUpdateStatus; +use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; +use crate::ln::ffor::*; +use crate::ln::functional_test_utils::*; +use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; +use crate::ln::types::ChannelId; +use crate::types::payment::{PaymentHash, PaymentSecret}; +use crate::util::config::UserConfig; +use crate::util::ser::Writeable; + +fn anchor_config() -> UserConfig { + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.manually_accept_inbound_channels = true; + config +} + +fn snapshot(node: &Node, channel_id: ChannelId) -> FFORMonitorSnapshot { + get_monitor!(node, channel_id).ffor_commitment_snapshot().unwrap() +} + +fn verify( + node: &Node, counterparty: &Node, channel_id: ChannelId, party: FFORSettlementParty, + vouchers: &[FFORVoucher], +) -> Result { + let monitor = snapshot(node, channel_id); + node.node.ffor_voucher_commitments( + &channel_id, + &counterparty.node.get_our_node_id(), + party, + vouchers, + &monitor, + ) +} + +fn offer_voucher( + sender: &Node, receiver: &Node, amount_msat: u64, +) -> (msgs::CommitmentUpdate, FFORVoucher, PaymentSecret) { + let (mut route, payment_hash, _, payment_secret) = + get_route_and_payment_hash!(sender, receiver, amount_msat); + route.paths[0].hops[0].cltv_expiry_delta = 144; + sender + .node + .send_payment_with_route( + route, + payment_hash, + RecipientOnionFields::secret_only(payment_secret), + PaymentId(payment_hash.0), + ) + .unwrap(); + check_added_monitors(sender, 1); + let update = get_htlc_update_msgs!(sender, receiver.node.get_our_node_id()); + let add = &update.update_add_htlcs[0]; + let voucher = FFORVoucher { + htlc_id: add.htlc_id, + payment_hash: add.payment_hash, + amount_msat: add.amount_msat, + cltv_expiry: add.cltv_expiry, + }; + (update, voucher, payment_secret) +} + +fn commit_voucher(sender: &Node, receiver: &Node, amount_msat: u64) -> FFORVoucher { + let (update, voucher, secret) = offer_voucher(sender, receiver, amount_msat); + receiver + .node + .handle_update_add_htlc(sender.node.get_our_node_id(), &update.update_add_htlcs[0]); + commitment_signed_dance!(receiver, sender, update.commitment_signed, false); + expect_and_process_pending_htlcs(receiver, false); + expect_payment_claimable!(receiver, voucher.payment_hash, secret, amount_msat); + voucher +} + +#[test] +fn ffor_requires_both_commitment_rounds_and_completed_persistence() { + for pending_persistence in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let sender = &nodes[0]; + let receiver = &nodes[1]; + let sender_id = sender.node.get_our_node_id(); + let receiver_id = receiver.node.get_our_node_id(); + let (update, voucher, secret) = offer_voucher(sender, receiver, 2_000_000); + let book = [voucher]; + assert_eq!( + verify(sender, receiver, channel_id, FFORSettlementParty::Holder, &book), + Err(FFORCommitmentError::PendingUpdates) + ); + receiver.node.handle_update_add_htlc(sender_id, &update.update_add_htlcs[0]); + assert_eq!( + verify(receiver, sender, channel_id, FFORSettlementParty::Counterparty, &book), + Err(FFORCommitmentError::PendingUpdates) + ); + receiver.node.handle_commitment_signed_batch_test(sender_id, &update.commitment_signed); + check_added_monitors(receiver, 1); + let (revoke, commitment) = get_revoke_commit_msgs!(receiver, sender_id); + sender.node.handle_revoke_and_ack(receiver_id, &revoke); + check_added_monitors(sender, 1); + assert_eq!( + verify(sender, receiver, channel_id, FFORSettlementParty::Holder, &book), + Err(FFORCommitmentError::PendingUpdates) + ); + sender.node.handle_commitment_signed_batch_test(receiver_id, &commitment); + check_added_monitors(sender, 1); + let revoke = get_event_msg!(sender, MessageSendEvent::SendRevokeAndACK, receiver_id); + assert_eq!( + verify(receiver, sender, channel_id, FFORSettlementParty::Counterparty, &book), + Err(FFORCommitmentError::PendingUpdates) + ); + if pending_persistence { + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + } + receiver.node.handle_revoke_and_ack(sender_id, &revoke); + check_added_monitors(receiver, 1); + if pending_persistence { + assert_eq!( + verify(receiver, sender, channel_id, FFORSettlementParty::Counterparty, &book), + Err(FFORCommitmentError::PendingUpdates) + ); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + let update_id = get_monitor!(receiver, channel_id).get_latest_update_id(); + receiver + .chain_monitor + .chain_monitor + .channel_monitor_updated(channel_id, update_id) + .unwrap(); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + } + expect_and_process_pending_htlcs(receiver, false); + expect_payment_claimable!(receiver, voucher.payment_hash, secret, voucher.amount_msat); + let local = + verify(sender, receiver, channel_id, FFORSettlementParty::Holder, &book).unwrap(); + let remote = + verify(receiver, sender, channel_id, FFORSettlementParty::Counterparty, &book).unwrap(); + assert_eq!(local.holder, remote.counterparty); + assert_eq!(local.counterparty, remote.holder); + assert_eq!(local.holder.number, 1); + assert_eq!(local.counterparty.number, 1); + } +} + +#[test] +fn ffor_checks_complete_book_claim_material_and_stale_snapshots() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let first = commit_voucher(&nodes[0], &nodes[1], 2_000_123); + let stale = snapshot(&nodes[1], channel_id); + let second = commit_voucher(&nodes[0], &nodes[1], 3_000_456); + let book = [first, second]; + let check = |vouchers: &[FFORVoucher], monitor: &FFORMonitorSnapshot| { + nodes[1].node.ffor_voucher_commitments( + &channel_id, + &nodes[0].node.get_our_node_id(), + FFORSettlementParty::Counterparty, + vouchers, + monitor, + ) + }; + assert_eq!(check(&book, &stale), Err(FFORCommitmentError::MonitorMismatch)); + let monitor = snapshot(&nodes[1], channel_id); + assert!(check(&book, &monitor).is_ok()); + assert_eq!(check(&book[..1], &monitor), Err(FFORCommitmentError::InvalidVoucherBook)); + assert_eq!(check(&[], &monitor), Err(FFORCommitmentError::InvalidVoucherBook)); + for mutation in 0..6 { + let mut wrong = book; + match mutation { + 0 => wrong[0].htlc_id += 1, + 1 => wrong[0].payment_hash = PaymentHash([42; 32]), + 2 => wrong[0].amount_msat += 1, + 3 => wrong[0].cltv_expiry += 1, + 4 => wrong[1].payment_hash = wrong[0].payment_hash, + _ => wrong[0].amount_msat = 0, + } + assert_eq!(check(&wrong, &monitor), Err(FFORCommitmentError::InvalidVoucherBook)); + } + let mut missing = snapshot(&nodes[1], channel_id); + missing.holder.counterparty_htlc_sigs.pop(); + assert_eq!(check(&book, &missing), Err(FFORCommitmentError::InvalidClaimMaterial)); + let mut invalid = snapshot(&nodes[1], channel_id); + invalid.holder.counterparty_htlc_sigs[0] = invalid.holder.counterparty_sig; + assert_eq!(check(&book, &invalid), Err(FFORCommitmentError::InvalidClaimMaterial)); + let mut wrong_id = snapshot(&nodes[1], channel_id); + wrong_id.update_id -= 1; + assert_eq!(check(&book, &wrong_id), Err(FFORCommitmentError::MonitorMismatch)); + assert_eq!( + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Holder, &book), + Err(FFORCommitmentError::InvalidVoucherBook) + ); +} + +#[test] +fn ffor_rejects_trimmed_vouchers_and_unsupported_channel_types() { + for anchors in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = anchor_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = anchors; + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let book = [commit_voucher(&nodes[0], &nodes[1], 100_000)]; + let expected = if anchors { + FFORCommitmentError::TrimmedVoucher + } else { + FFORCommitmentError::UnsupportedChannelType + }; + assert_eq!( + verify(&nodes[0], &nodes[1], channel_id, FFORSettlementParty::Holder, &book), + Err(expected) + ); + assert_eq!( + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Counterparty, &book), + Err(expected) + ); + } +} + +#[test] +fn ffor_commitments_survive_manager_and_monitor_reload() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let reloaded; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let book = [commit_voucher(&nodes[0], &nodes[1], 2_000_000)]; + let before = + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Counterparty, &book).unwrap(); + let monitor_encoded = get_monitor!(nodes[1], channel_id).encode(); + let manager_encoded = nodes[1].node.encode(); + nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id()); + reload_node!( + nodes[1], + config, + &manager_encoded, + &[&monitor_encoded], + persister, + chain_monitor, + reloaded + ); + reconnect_nodes(ReconnectArgs::new(&nodes[0], &nodes[1])); + let after = + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Counterparty, &book).unwrap(); + assert_eq!(before, after); +} + +#[test] +fn ffor_rejects_extra_reverse_htlcs_and_remains_read_only() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let book = [commit_voucher(&nodes[0], &nodes[1], 2_000_000)]; + let before = + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Counterparty, &book).unwrap(); + let stale = snapshot(&nodes[1], channel_id); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + let after = + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Counterparty, &book).unwrap(); + assert!(after.holder.number > before.holder.number); + assert!(after.counterparty.number > before.counterparty.number); + assert_eq!( + nodes[1].node.ffor_voucher_commitments( + &channel_id, + &nodes[0].node.get_our_node_id(), + FFORSettlementParty::Counterparty, + &book, + &stale, + ), + Err(FFORCommitmentError::MonitorMismatch) + ); + commit_voucher(&nodes[1], &nodes[0], 1_000_000); + assert_eq!( + verify(&nodes[0], &nodes[1], channel_id, FFORSettlementParty::Holder, &book), + Err(FFORCommitmentError::InvalidVoucherBook) + ); + assert_eq!( + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Counterparty, &book), + Err(FFORCommitmentError::InvalidVoucherBook) + ); +} + +#[test] +fn ffor_validates_book_id_overflow_slot_limit_and_uniform_expiry() { + let voucher = FFORVoucher { + htlc_id: u64::MAX, + payment_hash: PaymentHash([1; 32]), + amount_msat: 1_000_000, + cltv_expiry: 200, + }; + assert!(validate_vouchers(&[voucher]).is_ok()); + let mut next = voucher; + next.htlc_id = 0; + next.payment_hash = PaymentHash([2; 32]); + assert_eq!(validate_vouchers(&[voucher, next]), Err(FFORCommitmentError::InvalidVoucherBook)); + let oversized = vec![voucher; 484]; + assert_eq!(validate_vouchers(&oversized), Err(FFORCommitmentError::InvalidVoucherBook)); + let mut book: Vec<_> = (0..483u64) + .map(|index| { + let mut hash = [0; 32]; + hash[..8].copy_from_slice(&index.to_be_bytes()); + FFORVoucher { htlc_id: index, payment_hash: PaymentHash(hash), ..voucher } + }) + .collect(); + assert!(validate_vouchers(&book).is_ok()); + book[482].cltv_expiry += 1; + assert_eq!(validate_vouchers(&book), Err(FFORCommitmentError::InvalidVoucherBook)); +} + +#[test] +fn ffor_rejects_partial_round_after_reload_until_reestablishment_completes() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let reloaded; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (update, voucher, secret) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + let sender_id = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + nodes[1].node.handle_update_add_htlc(sender_id, &update.update_add_htlcs[0]); + nodes[1].node.handle_commitment_signed_batch_test(sender_id, &update.commitment_signed); + check_added_monitors(&nodes[1], 1); + let (revoke, _) = get_revoke_commit_msgs!(&nodes[1], sender_id); + nodes[0].node.handle_revoke_and_ack(receiver_id, &revoke); + check_added_monitors(&nodes[0], 1); + let monitor_encoded = get_monitor!(nodes[0], channel_id).encode(); + let manager_encoded = nodes[0].node.encode(); + nodes[1].node.peer_disconnected(sender_id); + reload_node!( + nodes[0], + config, + &manager_encoded, + &[&monitor_encoded], + persister, + chain_monitor, + reloaded + ); + assert!( + verify(&nodes[0], &nodes[1], channel_id, FFORSettlementParty::Holder, &[voucher]).is_err() + ); + let mut reconnect = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect.pending_responding_commitment_signed = (true, false); + reconnect.send_announcement_sigs = (true, false); + reconnect_nodes(reconnect); + expect_and_process_pending_htlcs(&nodes[1], false); + expect_payment_claimable!(&nodes[1], voucher.payment_hash, secret, voucher.amount_msat); + assert!( + verify(&nodes[0], &nodes[1], channel_id, FFORSettlementParty::Holder, &[voucher]).is_ok() + ); + assert!(verify( + &nodes[1], + &nodes[0], + channel_id, + FFORSettlementParty::Counterparty, + &[voucher] + ) + .is_ok()); +} + +#[test] +fn ffor_receiver_funder_uses_both_contest_delays_and_dust_limits() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut recipient_config = anchor_config(); + recipient_config.channel_handshake_config.our_to_self_delay = 144; + let mut settlement_config = anchor_config(); + settlement_config.channel_handshake_config.our_to_self_delay = 288; + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(recipient_config), Some(settlement_config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + // LDK's public configuration uses a fixed dust limit. Configure the fixture's two matching + // views before the voucher round to model peers negotiating different dust limits. + for index in 0..2 { + let peer_id = nodes[1 - index].node.get_our_node_id(); + let peers = nodes[index].node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&peer_id).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get_mut(&channel_id).unwrap(); + channel.context_mut().holder_dust_limit_satoshis = if index == 0 { 1000 } else { 354 }; + channel.context_mut().counterparty_dust_limit_satoshis = + if index == 0 { 354 } else { 1000 }; + } + let first = commit_voucher(&nodes[1], &nodes[0], 2_000_123); + let recipient = + verify(&nodes[0], &nodes[1], channel_id, FFORSettlementParty::Counterparty, &[first]) + .unwrap(); + let settlement = + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Holder, &[first]).unwrap(); + assert_eq!(recipient.holder, settlement.counterparty); + assert_eq!(recipient.counterparty, settlement.holder); + + let second = commit_voucher(&nodes[1], &nodes[0], 400_000); + assert_eq!(snapshot(&nodes[0], channel_id).holder.nondust_htlcs().len(), 1); + assert_eq!(snapshot(&nodes[1], channel_id).holder.nondust_htlcs().len(), 2); + let book = [first, second]; + assert_eq!( + verify(&nodes[0], &nodes[1], channel_id, FFORSettlementParty::Counterparty, &book), + Err(FFORCommitmentError::TrimmedVoucher) + ); + assert_eq!( + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Holder, &book), + Err(FFORCommitmentError::TrimmedVoucher) + ); +} + +#[test] +fn ffor_rejects_pending_fee_updates() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let book = [commit_voucher(&nodes[0], &nodes[1], 2_000_000)]; + *chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap() += 20; + nodes[0].node.timer_tick_occurred(); + check_added_monitors(&nodes[0], 1); + let update = get_htlc_update_msgs!(&nodes[0], nodes[1].node.get_our_node_id()); + assert_eq!( + verify(&nodes[0], &nodes[1], channel_id, FFORSettlementParty::Holder, &book), + Err(FFORCommitmentError::PendingUpdates) + ); + nodes[1] + .node + .handle_update_fee(nodes[0].node.get_our_node_id(), update.update_fee.as_ref().unwrap()); + assert_eq!( + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Counterparty, &book), + Err(FFORCommitmentError::PendingUpdates) + ); + commitment_signed_dance!(&nodes[1], &nodes[0], update.commitment_signed, false); + assert!(verify(&nodes[0], &nodes[1], channel_id, FFORSettlementParty::Holder, &book).is_ok()); + assert!( + verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Counterparty, &book).is_ok() + ); +} diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 2e6b279965f..25f950949d4 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -18,6 +18,9 @@ pub mod channel_keys; pub mod channel_state; pub mod channelmanager; mod features; +pub mod ffor; +#[cfg(test)] +mod ffor_tests; pub mod funding; pub mod inbound_payment; pub mod msgs; From 40973f317e3bb8d3068d3e225666e63e8a98769d Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 19:48:35 -0400 Subject: [PATCH 02/22] feat(ffor): park receiver vouchers through channel recovery --- FFOR.md | 79 +++++ lightning/src/ln/channel.rs | 18 + lightning/src/ln/channel/ffor.rs | 386 ++++++++++++++++++++- lightning/src/ln/channelmanager.rs | 120 ++++++- lightning/src/ln/ffor.rs | 152 ++++++++- lightning/src/ln/ffor_tests.rs | 527 ++++++++++++++++++++++++++++- lightning/src/sign/ffor.rs | 165 +++++++++ lightning/src/sign/mod.rs | 25 ++ 8 files changed, 1462 insertions(+), 10 deletions(-) create mode 100644 FFOR.md create mode 100644 lightning/src/sign/ffor.rs diff --git a/FFOR.md b/FFOR.md new file mode 100644 index 00000000000..f391ab34d28 --- /dev/null +++ b/FFOR.md @@ -0,0 +1,79 @@ +# Experimental FFOR receiver parking + +This fork targets rust-lightning v0.2.5. The current APIs provide receiver voucher +parking and point-in-time verification of both commitment views for FFOR Variant D. +They do not provide an active offline receiving epoch. + +`register_ffor_receiver_book` takes an already authenticated and validated public +book on an empty, connected, synchronized anchor channel. It checks the book's +structural invariants and exact first incoming HTLC ID. The application must persist +the registration before allowing the settlement peer to send vouchers. Registration +requests manager persistence but does not wait for a durable write. + +The stock channel owns voucher identity through the ordinary commitment rounds. +Exact vouchers never enter invoice, keysend, or forwarding processing. The receiver +only derives encrypted failure material from their onion keys. `Registered` reports +progress; `Parked` additionally requires the complete monitor-backed commitment proof, +including both current commitment transactions and holder claim signatures. + +An explicit abort, mismatch, disconnect, or restart unwinds committed vouchers using +ordinary HTLC failure rounds. Partial rounds finish before their HTLCs are failed. +`Aborting` and `Aborted` distinguish pending removal from a synchronized channel with +all owned HTLCs drained. Ordinary payments work after drain. Registered voucher +hashes remain excluded from ordinary receiving. + +There is deliberately only one registration per channel. Its epoch and public book +remain as a permanent tombstone, and attempts to register either the same or another +epoch are refused. The required even channel TLV 65534 is retained after abort; +older readers must reject the channel. Restore validates live voucher ownership +against stock inbound HTLCs before accepting the channel, and checks that committed +vouchers retain either failure material or their deferred add. + +These are consistency checks, not an authenticated storage envelope. Arbitrarily +deleting a mismatching add's ownership record after abort can make its nonreserved +hash indistinguishable from an ordinary post-abort payment. No valid writer creates +that state: the ownership record and stock HTLC are written together. The current +checks do not claim to detect every arbitrary alteration of local storage. + +No feature bit, wire activation, invoice readiness, commitment freeze, or automatic +setup deadline is implemented here. The caller must abort a setup that times out. +Ordinary channel updates can invalidate a previously returned `Parked` proof. + +`NodeSigner::sign_ffor_message` supplies the protocol's single-SHA256 `ffor/msg` +signature domain without exporting node keys. KeysManager uses its existing ECDSA +implementation with auxiliary entropy; phantom wallets use their actual local node +identity. External signers refuse this operation by default. Signing requests check +only the allowed envelope type and size; protocol and transition validation remain +required before requesting a signature. + +## Validation + +The FFOR tests exercise real two-node commitment rounds, both funding directions, +asymmetric dust limits and contest delays, signature corruption, stale monitors, +monitor persistence delays, exact and partial books, mismatching and extra adds, +and ordinary payments after abort. The parking crash matrix reloads at registration, +uncommitted add, first commitment, both commitments before interception, parked, +explicit abort, failure commitment sent, and fully drained tombstone. + +Run the focused suite with: + +```sh +cargo test -p lightning --lib ffor_ --offline +``` + +The compatibility test can export ordinary and registered channel fixtures with +`FFOR_LEGACY_FIXTURE_DIR`. An unmodified v0.2.5 channel reader accepted the ordinary +fixture and rejected the registered fixture with `UnknownRequiredFeature`. +Existing payment, reload, monitor, and quiescence suites also pass. +Three signer tests additionally verify the public Appendix D activation signature, +domain/type separation, local node identity, low-S output and default refusal. + +## Next boundary + +Reusable epochs require durable retired epoch IDs and voucher hashes, with one +current signed transcript record under the same channel authority. Activation must +atomically verify commitment evidence, record exact signed setup and activation +bytes, and freeze ordinary mutations. A manager persistence-completion barrier must +precede acknowledgement release or invoice eligibility. The record must also survive +channel removal and remain available during on-chain resolution, including witness +and mailbox recovery. None of these later guarantees can be inferred from parking. diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index c38333b4c8e..98afa93262e 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2974,6 +2974,8 @@ where holding_cell_update_fee: Option, next_holder_htlc_id: u64, pub(super) next_counterparty_htlc_id: u64, + // Required serialized compatibility fence whenever experimental voucher parking is used. + ffor_receiver_book: Option, pub(super) feerate_per_kw: u32, /// The timestamp set on our latest `channel_update` message for this channel. It is updated @@ -3649,6 +3651,7 @@ where holding_cell_update_fee: None, next_holder_htlc_id: 0, next_counterparty_htlc_id: 0, + ffor_receiver_book: None, update_time_counter: 1, resend_order: RAACommitmentOrder::CommitmentFirst, @@ -3887,6 +3890,7 @@ where holding_cell_update_fee: None, next_holder_htlc_id: 0, next_counterparty_htlc_id: 0, + ffor_receiver_book: None, update_time_counter: 1, resend_order: RAACommitmentOrder::CommitmentFirst, @@ -7788,6 +7792,7 @@ where .try_for_each(|funding| self.context.validate_update_add_htlc(funding, msg, fee_estimator))?; // Now update local state: + self.ffor_record_update_add(msg); self.context.next_counterparty_htlc_id += 1; self.context.pending_inbound_htlcs.push(InboundHTLCOutput { htlc_id: msg.htlc_id, @@ -9283,6 +9288,10 @@ where return Err(()) } + if let Some(book) = self.context.ffor_receiver_book.as_mut() { + book.abort(crate::ln::ffor::FFORReceiverAbortReason::Disconnected); + } + // We only clear `peer_disconnected` if we were able to reestablish the channel. We always // reset our awaiting response in case we failed reestablishment and are disconnecting. self.context.sent_message_awaiting_response = None; @@ -14930,6 +14939,8 @@ where (65, self.quiescent_action, option), // Added in 0.2 (67, pending_outbound_held_htlc_flags, optional_vec), // Added in 0.2 (69, holding_cell_held_htlc_flags, optional_vec), // Added in 0.2 + // Experimental required field: old readers must refuse the entire parked channel. + (65534, self.context.ffor_receiver_book, option), }); Ok(()) @@ -15297,6 +15308,7 @@ where let mut pending_outbound_held_htlc_flags_opt: Option>> = None; let mut holding_cell_held_htlc_flags_opt: Option>> = None; + let mut ffor_receiver_book: Option = None; read_tlv_fields!(reader, { (0, announcement_sigs, option), @@ -15344,8 +15356,13 @@ where (65, quiescent_action, upgradable_option), // Added in 0.2 (67, pending_outbound_held_htlc_flags_opt, optional_vec), // Added in 0.2 (69, holding_cell_held_htlc_flags_opt, optional_vec), // Added in 0.2 + (65534, ffor_receiver_book, option), }); + if let Some(book) = ffor_receiver_book.as_mut() { + book.restored(&pending_inbound_htlcs)?; + } + let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); let mut iter = preimages.into_iter(); @@ -15673,6 +15690,7 @@ where holding_cell_update_fee, next_holder_htlc_id, next_counterparty_htlc_id, + ffor_receiver_book, update_time_counter, feerate_per_kw, diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index b83b56af029..5a40fb3ca69 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -1,13 +1,320 @@ use super::*; use crate::ln::ffor::{ - self as verification, FFORCommitmentError, FFORMonitorSnapshot, FFORSettlementParty, - FFORVoucher, FFORVoucherCommitments, + self as verification, FFORCommitmentError, FFORMonitorSnapshot, FFORReceiverAbortReason, + FFORReceiverError, FFORReceiverStatus, FFORSettlementParty, FFORVoucher, + FFORVoucherCommitments, FFORVoucherFailure, }; +/// Channel-owned identity survives the stock inbound HTLC's transition to `Committed`. +pub(super) struct FFORReceiverBook { + epoch_id: [u8; 32], + vouchers: Vec, + received: Vec, + abort_reason: Option, +} + +struct FFORReceivedVoucher { + voucher: FFORVoucher, + failure: Option, +} + +impl_writeable_tlv_based!(FFORReceivedVoucher, { + (0, voucher, required), + (2, failure, option), +}); + +impl_writeable_tlv_based!(FFORReceiverBook, { + (0, epoch_id, required), + (2, vouchers, required_vec), + (4, received, required_vec), + (6, abort_reason, option), +}); + +impl FFORReceiverBook { + pub(super) fn abort(&mut self, reason: FFORReceiverAbortReason) { + self.abort_reason.get_or_insert(reason); + } + + pub(super) fn restored( + &mut self, inbound_htlcs: &[InboundHTLCOutput], + ) -> Result<(), DecodeError> { + verification::validate_vouchers(&self.vouchers).map_err(|_| DecodeError::InvalidValue)?; + let mut ids = alloc::collections::BTreeSet::new(); + if self.received.len() > 483 + || self.received.iter().any(|received| !ids.insert(received.voucher.htlc_id)) + { + return Err(DecodeError::InvalidValue); + } + for received in &self.received { + let valid_failure = match received.failure.as_ref() { + None => true, + // The fixed temporary_node_failure packet has 256 payload/padding bytes, + // two length fields, and a 32-byte HMAC. + Some(FFORVoucherFailure::Relay { packet }) => packet.len() == 292, + Some(FFORVoucherFailure::Malformed { sha256_of_onion, failure_code }) => { + *failure_code == LocalHTLCFailureReason::InvalidOnionKey.failure_code() + || *failure_code + == LocalHTLCFailureReason::InvalidOnionVersion.failure_code() + || (*failure_code + == LocalHTLCFailureReason::InvalidOnionBlinding.failure_code() + && *sha256_of_onion == [0; 32]) + }, + }; + if !valid_failure { + return Err(DecodeError::InvalidValue); + } + } + for htlc in inbound_htlcs { + let received = + self.received.iter().find(|received| received.voucher.htlc_id == htlc.htlc_id); + if let Some(received) = received { + let voucher = &received.voucher; + if voucher.payment_hash != htlc.payment_hash + || voucher.amount_msat != htlc.amount_msat + || voucher.cltv_expiry != htlc.cltv_expiry + { + return Err(DecodeError::InvalidValue); + } + } else if self.abort_reason.is_none() + || self.vouchers.iter().any(|voucher| voucher.payment_hash == htlc.payment_hash) + { + // Never restore a live voucher whose channel-owned interception record was lost. + return Err(DecodeError::InvalidValue); + } + } + self.abort(FFORReceiverAbortReason::Restarted); + Ok(()) + } + + fn owns(&self, htlc_id: u64) -> bool { + self.received.iter().any(|received| received.voucher.htlc_id == htlc_id) + } +} + impl FundedChannel where SP::Target: SignerProvider, { + pub(crate) fn register_ffor_receiver_book( + &mut self, epoch_id: [u8; 32], vouchers: &[FFORVoucher], + ) -> Result<(), FFORReceiverError> { + if self.context.ffor_receiver_book.is_some() { + return Err(FFORReceiverError::AlreadyRegistered); + } + self.check_ffor_synchronized()?; + if !self.context.is_connected() { + return Err(FFORCommitmentError::ChannelUnavailable.into()); + } + verification::validate_vouchers(vouchers)?; + if !self.context.pending_inbound_htlcs.is_empty() + || !self.context.pending_outbound_htlcs.is_empty() + || vouchers[0].htlc_id != self.context.next_counterparty_htlc_id + { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + self.context.ffor_receiver_book = Some(FFORReceiverBook { + epoch_id, + vouchers: vouchers.to_vec(), + received: Vec::new(), + abort_reason: None, + }); + Ok(()) + } + + pub(crate) fn ffor_receiver_book_status( + &self, monitor: &FFORMonitorSnapshot, logger: &L, + ) -> Result + where + L::Target: Logger, + { + let book = + self.context.ffor_receiver_book.as_ref().ok_or(FFORReceiverError::NotRegistered)?; + if let Some(reason) = book.abort_reason { + let pending = + self.context.pending_inbound_htlcs.iter().any(|htlc| book.owns(htlc.htlc_id)); + return Ok( + if pending + || !self.context.is_connected() + || self.check_ffor_synchronized().is_err() + { + FFORReceiverStatus::Aborting { reason } + } else { + FFORReceiverStatus::Aborted { reason } + }, + ); + } + let parked = book.received.iter().filter(|received| received.failure.is_some()).count(); + if parked != book.vouchers.len() { + return Ok(FFORReceiverStatus::Registered { + parked_vouchers: parked as u16, + total_vouchers: book.vouchers.len() as u16, + }); + } + let commitments = self.ffor_voucher_commitments( + FFORSettlementParty::Counterparty, + &book.vouchers, + monitor, + logger, + )?; + Ok(FFORReceiverStatus::Parked { commitments }) + } + + pub(crate) fn abort_ffor_receiver_book( + &mut self, epoch_id: [u8; 32], + ) -> Result<(), FFORReceiverError> { + let book = + self.context.ffor_receiver_book.as_mut().ok_or(FFORReceiverError::NotRegistered)?; + if book.epoch_id != epoch_id { + return Err(FFORReceiverError::UnknownEpoch); + } + book.abort(FFORReceiverAbortReason::Requested); + Ok(()) + } + + /// Called only after normal channel validation has accepted an incoming add. + pub(super) fn ffor_record_update_add(&mut self, msg: &msgs::UpdateAddHTLC) { + let owned_pending = self + .context + .ffor_receiver_book + .as_ref() + .map(|book| { + self.context.pending_inbound_htlcs.iter().any(|htlc| book.owns(htlc.htlc_id)) + }) + .unwrap_or(false); + let book = match self.context.ffor_receiver_book.as_mut() { + Some(book) => book, + None => return, + }; + // Bound retained received records to the live HTLC set. The expected book itself is the + // permanent tombstone, including hashes that must never reenter ordinary payment handling. + book.received.retain(|received| { + self.context + .pending_inbound_htlcs + .iter() + .any(|htlc| htlc.htlc_id == received.voucher.htlc_id) + }); + let reserved_hash = + book.vouchers.iter().any(|voucher| voucher.payment_hash == msg.payment_hash); + if book.abort_reason.is_some() && !reserved_hash && !owned_pending { + return; + } + let voucher = FFORVoucher { + htlc_id: msg.htlc_id, + payment_hash: msg.payment_hash, + amount_msat: msg.amount_msat, + cltv_expiry: msg.cltv_expiry, + }; + if !book.vouchers.contains(&voucher) + || msg.blinding_point.is_some() + || msg.hold_htlc.is_some() + || msg.skimmed_fee_msat.is_some() + || msg.onion_routing_packet.version != 0 + || msg.onion_routing_packet.public_key.is_err() + { + book.abort(FFORReceiverAbortReason::VoucherMismatch); + } + if let Some(received) = + book.received.iter_mut().find(|received| received.voucher.htlc_id == msg.htlc_id) + { + // An uncommitted add can be dropped on disconnect and this ID then reused. The aborted + // registration retains ownership, but its old encrypted failure must not be reused. + received.voucher = voucher; + received.failure = None; + } else { + book.received.push(FFORReceivedVoucher { voucher, failure: None }); + } + } + + pub(crate) fn ffor_owns_received_htlc(&self, htlc_id: u64) -> bool { + self.context.ffor_receiver_book.as_ref().map(|book| book.owns(htlc_id)).unwrap_or(false) + } + + /// A committed voucher needs either its failure packet or the stock deferred add from which + /// one can still be derived. Validate this after the manager's deferred queue has been read. + pub(crate) fn ffor_validate_unwind_material( + &self, deferred_adds: &[msgs::UpdateAddHTLC], + ) -> Result<(), DecodeError> { + let book = match self.context.ffor_receiver_book.as_ref() { + Some(book) => book, + None => return Ok(()), + }; + for received in &book.received { + if received.failure.is_some() { + continue; + } + let voucher = &received.voucher; + let committed = self.context.pending_inbound_htlcs.iter().any(|htlc| { + htlc.htlc_id == voucher.htlc_id && matches!(htlc.state, InboundHTLCState::Committed) + }); + let has_deferred_add = deferred_adds + .iter() + .chain(self.context.monitor_pending_update_adds.iter()) + .any(|add| { + add.channel_id == self.context.channel_id() + && add.htlc_id == voucher.htlc_id + && add.payment_hash == voucher.payment_hash + && add.amount_msat == voucher.amount_msat + && add.cltv_expiry == voucher.cltv_expiry + }); + if committed && !has_deferred_add { + return Err(DecodeError::InvalidValue); + } + } + Ok(()) + } + + pub(crate) fn ffor_park_received_htlc(&mut self, htlc_id: u64, failure: FFORVoucherFailure) { + if let Some(book) = self.context.ffor_receiver_book.as_mut() { + if let Some(received) = + book.received.iter_mut().find(|received| received.voucher.htlc_id == htlc_id) + { + received.failure = Some(failure); + } + } + } + + /// Queue only irrevocably committed vouchers; partial rounds keep their stock deferred add. + pub(crate) fn ffor_queue_aborted_vouchers(&mut self, logger: &L) + where + L::Target: Logger, + { + let book = match self.context.ffor_receiver_book.as_ref() { + Some(book) if book.abort_reason.is_some() => book, + _ => return, + }; + let failures: Vec<_> = book + .received + .iter() + .filter_map(|received| { + let committed = self.context.pending_inbound_htlcs.iter().any(|htlc| { + htlc.htlc_id == received.voucher.htlc_id + && matches!(htlc.state, InboundHTLCState::Committed) + }); + if committed { + received.failure.clone().map(|failure| (received.voucher.htlc_id, failure)) + } else { + None + } + }) + .collect(); + for (htlc_id, failure) in failures { + match failure { + FFORVoucherFailure::Relay { packet } => { + let packet = msgs::OnionErrorPacket { data: packet, attribution_data: None }; + let _ = self.queue_fail_htlc(htlc_id, packet, logger); + }, + FFORVoucherFailure::Malformed { sha256_of_onion, failure_code } => { + let _ = self.queue_fail_malformed_htlc( + htlc_id, + failure_code, + sha256_of_onion, + logger, + ); + }, + } + } + } + pub(crate) fn ffor_voucher_commitments( &self, settlement_party: FFORSettlementParty, vouchers: &[FFORVoucher], monitor: &FFORMonitorSnapshot, logger: &L, @@ -145,3 +452,78 @@ where Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ln::ffor_tests::{ + anchor_config, deliver_parked_voucher, offer_voucher, register_book, + }; + use crate::ln::functional_test_utils::*; + + #[test] + fn ffor_parking_rejects_inconsistent_stored_ownership_and_unwind_material() { + for mutation in 0..7 { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let sender_id = nodes[0].node.get_our_node_id(); + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_book(&nodes[1], &nodes[0], channel_id, &[voucher]); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + let encoded = { + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&sender_id).unwrap().lock().unwrap(); + let channel = + peer.channel_by_id.get_mut(&channel_id).unwrap().as_funded_mut().unwrap(); + let book = channel.context.ffor_receiver_book.as_mut().unwrap(); + let original_book = book.encode(); + match mutation { + 0 => book.received.clear(), + 1 => book.received[0].voucher.payment_hash.0[0] ^= 1, + 2 => book.received[0].voucher.amount_msat += 1, + 3 => book.received[0].voucher.cltv_expiry += 1, + 4 => { + book.received[0].failure = Some(FFORVoucherFailure::Malformed { + sha256_of_onion: [0; 32], + failure_code: 0, + }) + }, + 5 => book.received[0].failure = None, + _ => { + book.received[0].failure = + Some(FFORVoucherFailure::Relay { packet: Vec::new() }) + }, + } + let damaged = channel.encode(); + channel.context.ffor_receiver_book = + Some(FFORReceiverBook::read(&mut &original_book[..]).unwrap()); + damaged + }; + let features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + let restored = FundedChannel::read( + &mut &encoded[..], + (&nodes[1].keys_manager, &nodes[1].keys_manager, &features), + ); + if mutation == 5 { + // The manager checks this once its deferred adds have also been read. A parked + // voucher whose packet and original add were both lost cannot be unwound. + let restored = restored.unwrap(); + assert_eq!( + restored.ffor_validate_unwind_material(&[]), + Err(DecodeError::InvalidValue) + ); + } else { + assert!( + matches!(restored, Err(DecodeError::InvalidValue)), + "mutation {}", + mutation + ); + } + } + } +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d9b2cf745aa..927a00b3266 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -66,8 +66,8 @@ use crate::ln::channel::{ }; use crate::ln::channel_state::ChannelDetails; use crate::ln::ffor::{ - FFORCommitmentError, FFORMonitorSnapshot, FFORSettlementParty, FFORVoucher, - FFORVoucherCommitments, + FFORCommitmentError, FFORMonitorSnapshot, FFORReceiverError, FFORReceiverStatus, + FFORSettlementParty, FFORVoucher, FFORVoucherCommitments, }; use crate::ln::funding::SpliceContribution; use crate::ln::inbound_payment; @@ -4300,6 +4300,89 @@ where channel.ffor_voucher_commitments(settlement_party, vouchers, monitor, &self.logger) } + /// Register an authenticated Variant D voucher book for experimental receiver-side parking. + /// + /// The channel must be connected, synchronized, empty, and use a supported anchor channel type. + /// The first voucher must use the peer's exact next incoming HTLC ID. Matching vouchers are kept + /// out of ordinary payment processing, including `PaymentClaimable` and forwarding events. + /// The caller is responsible for authenticating the epoch and persisting the manager before + /// instructing the peer to send vouchers. This method requests persistence but does not await it. + /// + /// This is a private experimental protocol boundary, with no feature advertisement or activation. + /// It does not freeze commitment updates or authorize invoices. Disconnects and restarts abort + /// the registration and fail its vouchers using the ordinary channel commitment protocol. + /// Only one registration is supported for the lifetime of a channel, even after abort. Its + /// permanent tombstone prevents epoch reuse until a future durable epoch-history design exists. + /// Serialized channels using this API require a reader that understands voucher parking. + pub fn register_ffor_receiver_book( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + vouchers: &[FFORVoucher], + ) -> Result<(), FFORReceiverError> { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let per_peer_state = self.per_peer_state.read().unwrap(); + let mut peer = per_peer_state + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + channel.register_ffor_receiver_book(epoch_id, vouchers) + } + + /// Inspect an experimental receiver registration. `Parked` requires the complete two-view + /// commitment proof, including completed monitor updates and valid holder claim signatures. + /// + /// Obtain `monitor` from [`ChannelMonitor::ffor_commitment_snapshot`] and release its monitor + /// lock before calling this method, just as for [`Self::ffor_voucher_commitments`]. The result + /// does not freeze the channel or establish durable activation. It never authorizes an invoice. + pub fn ffor_receiver_book_status( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + monitor: &FFORMonitorSnapshot, + ) -> Result { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer = per_peer_state + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(channel_id) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + channel.ffor_receiver_book_status(monitor, &self.logger) + } + + /// Irreversibly abort an experimental receiver registration and unwind its stock voucher HTLCs. + /// + /// Continue normal event and peer-message processing until status is `Aborted`. HTLCs in partial + /// commitment rounds are failed only after those rounds finish. A disconnect or restart retains + /// the abort and resumes the unwind on reconnection. Ordinary payments can proceed after drain, + /// but further FFOR registrations on this channel remain refused, including a different epoch. + pub fn abort_ffor_receiver_book( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result<(), FFORReceiverError> { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let per_peer_state = self.per_peer_state.read().unwrap(); + let mut peer = per_peer_state + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + channel.abort_ffor_receiver_book(epoch_id)?; + channel.ffor_queue_aborted_vouchers(&self.logger); + Ok(()) + } + /// Gets the list of channels we have with a given counterparty, in random order. pub fn list_channels_with_counterparty( &self, counterparty_node_id: &PublicKey, @@ -6974,6 +7057,31 @@ where let mut htlc_forwards = Vec::new(); let mut htlc_fails = Vec::new(); for update_add_htlc in &update_add_htlcs { + let is_voucher = self.do_funded_channel_callback(incoming_scid_alias, |chan| { + chan.ffor_owns_received_htlc(update_add_htlc.htlc_id) + }); + if is_voucher == Some(true) { + // Voucher identity belongs to the channel. Do not decode its payload into an + // ordinary payment, even after abort or restart. Only derive unwind material. + match crate::ln::ffor::voucher_failure(update_add_htlc, &*self.node_signer) { + Ok(failure) => { + self.do_funded_channel_callback(incoming_scid_alias, |chan| { + chan.ffor_park_received_htlc( + update_add_htlc.htlc_id, + failure.clone(), + ); + }); + }, + Err(()) => { + // A temporarily unavailable signer cannot turn a voucher into a payment. + self.push_decode_update_add_htlcs(( + incoming_scid_alias, + vec![update_add_htlc.clone()], + )); + }, + } + continue; + } let (next_hop, next_packet_details_opt) = match decode_incoming_update_add_htlc_onion( &update_add_htlc, @@ -12233,6 +12341,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }) { let counterparty_node_id = chan.context.get_counterparty_node_id(); let funding_txo = chan.funding.get_funding_txo(); + chan.ffor_queue_aborted_vouchers(&self.logger); let (monitor_opt, holding_cell_failed_htlcs) = chan .maybe_free_holding_cell_htlcs( &self.fee_estimator, @@ -17234,6 +17343,13 @@ where (21, async_receive_offer_cache, (default_value, async_receive_offer_cache)), }); let mut decode_update_add_htlcs = decode_update_add_htlcs.unwrap_or_else(|| new_hash_map()); + for peer in per_peer_state.values() { + for channel in peer.lock().unwrap().channel_by_id.values().filter_map(Channel::as_funded) { + let deferred_adds = decode_update_add_htlcs.get(&channel.context.outbound_scid_alias()) + .map(Vec::as_slice).unwrap_or(&[]); + channel.ffor_validate_unwind_material(deferred_adds)?; + } + } let peer_storage_dir: Vec<(PublicKey, Vec)> = peer_storage_dir.unwrap_or_else(Vec::new); if fake_scid_rand_bytes.is_none() { fake_scid_rand_bytes = Some(args.entropy_source.get_secure_random_bytes()); diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index f3cd9272434..a756ee95777 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -1,8 +1,9 @@ -//! Point-in-time verification of the committed voucher book for FFOR Variant D. +//! Experimental receiver parking and commitment verification for FFOR Variant D. //! -//! These checks do not park HTLCs, freeze a channel, authenticate an epoch, or activate offline -//! receiving. A successful result must never by itself authorize invoice exposure or preimage -//! release. Future activation must recheck the commitments while atomically freezing channel state. +//! Parking keeps registered vouchers out of ordinary payment processing. It does not authenticate +//! an epoch, freeze commitment state, or activate offline receiving. Neither parking nor commitment +//! verification authorizes invoice exposure or preimage release. Future activation must recheck +//! the commitments while atomically freezing channel state. use alloc::collections::{BTreeMap, BTreeSet}; use bitcoin::hashes::Hash; @@ -18,7 +19,9 @@ use crate::ln::chan_utils::{ }; use crate::ln::channel::INITIAL_COMMITMENT_NUMBER; use crate::ln::types::ChannelId; +use crate::ln::{msgs, onion_utils}; use crate::prelude::*; +use crate::sign::{NodeSigner, Recipient}; use crate::types::payment::PaymentHash; /// Which channel participant offers every voucher HTLC in the book. @@ -43,6 +46,147 @@ pub struct FFORVoucher { pub cltv_expiry: u32, } +impl_writeable_tlv_based!(FFORVoucher, { + (0, htlc_id, required), + (2, payment_hash, required), + (4, amount_msat, required), + (6, cltv_expiry, required), +}); + +/// Why a receiver registration was irreversibly aborted. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FFORReceiverAbortReason { + /// The application explicitly requested an unwind. + Requested, + /// An incoming HTLC differed from the registered book or used an unsupported onion header. + VoucherMismatch, + /// The channel disconnected before a durable activation mechanism existed. + Disconnected, + /// The channel was restored from storage before a durable activation mechanism existed. + Restarted, +} + +impl_writeable_tlv_based_enum!(FFORReceiverAbortReason, + (0, Requested) => {}, + (2, VoucherMismatch) => {}, + (4, Disconnected) => {}, + (6, Restarted) => {}, +); + +/// Receiver-side state of one experimental voucher registration. +/// +/// Even `Parked` is only point-in-time evidence. It does not freeze commitments or authorize an +/// offline invoice. A disconnect or restart aborts this experimental registration. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FFORReceiverStatus { + /// The book is registered, but not all vouchers have been irrevocably committed and parked. + Registered { + /// Number of fully committed vouchers intercepted before ordinary payment processing. + parked_vouchers: u16, + /// Number of vouchers in the registered book. + total_vouchers: u16, + }, + /// Every voucher is parked and the supplied monitor proves both current commitment views. + Parked { + /// Verified current commitment identities, subject to subsequent channel updates. + commitments: FFORVoucherCommitments, + }, + /// Failure updates are pending, or the channel is not yet synchronized after an abort. + Aborting { + /// The first reason the registration was aborted. + reason: FFORReceiverAbortReason, + }, + /// All owned HTLCs have drained and the channel is synchronized again. + Aborted { + /// The first reason the registration was aborted. + reason: FFORReceiverAbortReason, + }, +} + +/// Why an experimental receiver registration or status operation could not be performed. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FFORReceiverError { + /// The channel or supplied commitment evidence does not meet the required conditions. + ChannelState(FFORCommitmentError), + /// This channel already has a registration, including a permanently retained aborted one. + AlreadyRegistered, + /// This channel has no receiver registration. + NotRegistered, + /// The supplied epoch does not identify this channel's registration. + UnknownEpoch, +} + +impl From for FFORReceiverError { + fn from(error: FFORCommitmentError) -> Self { + Self::ChannelState(error) + } +} + +impl fmt::Display for FFORReceiverError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ChannelState(error) => error.fmt(f), + Self::AlreadyRegistered => f.write_str("FFOR receiver already registered on channel"), + Self::NotRegistered => f.write_str("no FFOR receiver registration on channel"), + Self::UnknownEpoch => f.write_str("unknown FFOR receiver epoch"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for FFORReceiverError {} + +#[derive(Clone)] +pub(crate) enum FFORVoucherFailure { + Relay { packet: Vec }, + Malformed { sha256_of_onion: [u8; 32], failure_code: u16 }, +} + +impl_writeable_tlv_based_enum!(FFORVoucherFailure, + (0, Relay) => { (0, packet, required_vec) }, + (2, Malformed) => { + (0, sha256_of_onion, required), + (2, failure_code, required), + }, +); + +/// Derive only the material needed to unwind a voucher. Do not decode or act on its payload. +pub(crate) fn voucher_failure( + msg: &msgs::UpdateAddHTLC, node_signer: &NS, +) -> Result { + use onion_utils::LocalHTLCFailureReason as Failure; + let malformed = if msg.blinding_point.is_some() { + Some((Failure::InvalidOnionBlinding, [0; 32])) + } else if msg.onion_routing_packet.public_key.is_err() { + Some(( + Failure::InvalidOnionKey, + bitcoin::hashes::sha256::Hash::hash(&msg.onion_routing_packet.hop_data).to_byte_array(), + )) + } else if msg.onion_routing_packet.version != 0 { + Some(( + Failure::InvalidOnionVersion, + bitcoin::hashes::sha256::Hash::hash(&msg.onion_routing_packet.hop_data).to_byte_array(), + )) + } else { + None + }; + if let Some((reason, sha256_of_onion)) = malformed { + return Ok(FFORVoucherFailure::Malformed { + sha256_of_onion, + failure_code: reason.failure_code(), + }); + } + let public_key = msg.onion_routing_packet.public_key.as_ref().map_err(|_| ())?; + let shared_secret = node_signer.ecdh(Recipient::Node, public_key, None)?; + let packet = onion_utils::build_failure_packet( + &shared_secret.secret_bytes(), + Failure::TemporaryNodeFailure, + &[], + 0, + ); + Ok(FFORVoucherFailure::Relay { packet: packet.data }) +} + /// An actual current commitment transaction identified for the FFOR activation transcript. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FFORCommitment { diff --git a/lightning/src/ln/ffor_tests.rs b/lightning/src/ln/ffor_tests.rs index 90d60d402c0..a437e742897 100644 --- a/lightning/src/ln/ffor_tests.rs +++ b/lightning/src/ln/ffor_tests.rs @@ -8,7 +8,7 @@ use crate::types::payment::{PaymentHash, PaymentSecret}; use crate::util::config::UserConfig; use crate::util::ser::Writeable; -fn anchor_config() -> UserConfig { +pub(super) fn anchor_config() -> UserConfig { let mut config = test_default_channel_config(); config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; config.manually_accept_inbound_channels = true; @@ -33,7 +33,7 @@ fn verify( ) } -fn offer_voucher( +pub(super) fn offer_voucher( sender: &Node, receiver: &Node, amount_msat: u64, ) -> (msgs::CommitmentUpdate, FFORVoucher, PaymentSecret) { let (mut route, payment_hash, _, payment_secret) = @@ -189,6 +189,9 @@ fn ffor_checks_complete_book_claim_material_and_stale_snapshots() { let mut invalid = snapshot(&nodes[1], channel_id); invalid.holder.counterparty_htlc_sigs[0] = invalid.holder.counterparty_sig; assert_eq!(check(&book, &invalid), Err(FFORCommitmentError::InvalidClaimMaterial)); + let mut invalid_funding = snapshot(&nodes[1], channel_id); + invalid_funding.holder.counterparty_sig = invalid_funding.holder.counterparty_htlc_sigs[0]; + assert_eq!(check(&book, &invalid_funding), Err(FFORCommitmentError::InvalidClaimMaterial)); let mut wrong_id = snapshot(&nodes[1], channel_id); wrong_id.update_id -= 1; assert_eq!(check(&book, &wrong_id), Err(FFORCommitmentError::MonitorMismatch)); @@ -455,3 +458,523 @@ fn ffor_rejects_pending_fee_updates() { verify(&nodes[1], &nodes[0], channel_id, FFORSettlementParty::Counterparty, &book).is_ok() ); } + +const RECEIVER_EPOCH: [u8; 32] = [91; 32]; + +pub(super) fn register_book( + receiver: &Node, sender: &Node, channel_id: ChannelId, book: &[FFORVoucher], +) { + receiver + .node + .register_ffor_receiver_book( + &channel_id, + &sender.node.get_our_node_id(), + RECEIVER_EPOCH, + book, + ) + .unwrap(); +} + +fn receiver_status(receiver: &Node, sender: &Node, channel_id: ChannelId) -> FFORReceiverStatus { + let monitor = snapshot(receiver, channel_id); + receiver + .node + .ffor_receiver_book_status(&channel_id, &sender.node.get_our_node_id(), &monitor) + .unwrap() +} + +pub(super) fn deliver_parked_voucher( + sender: &Node, receiver: &Node, update: msgs::CommitmentUpdate, +) { + for add in update.update_add_htlcs.iter() { + receiver.node.handle_update_add_htlc(sender.node.get_our_node_id(), add); + } + commitment_signed_dance!(receiver, sender, update.commitment_signed, false); + expect_and_process_pending_htlcs(receiver, false); + assert!(receiver.node.get_and_clear_pending_events().is_empty()); +} + +fn drain_voucher_failures(sender: &Node, receiver: &Node, hashes: &[PaymentHash]) { + let update = get_htlc_update_msgs!(receiver, sender.node.get_our_node_id()); + check_added_monitors(receiver, 1); + assert_eq!( + update.update_fail_htlcs.len() + update.update_fail_malformed_htlcs.len(), + hashes.len() + ); + assert!(update.update_add_htlcs.is_empty()); + assert!(update.update_fulfill_htlcs.is_empty()); + for fail in update.update_fail_htlcs.iter() { + sender.node.handle_update_fail_htlc(receiver.node.get_our_node_id(), fail); + } + for fail in update.update_fail_malformed_htlcs.iter() { + sender.node.handle_update_fail_malformed_htlc(receiver.node.get_our_node_id(), fail); + } + commitment_signed_dance!(sender, receiver, update.commitment_signed, false); + for hash in hashes { + expect_payment_failed!(sender, *hash, false); + } + assert!(receiver.node.get_and_clear_pending_events().is_empty()); +} + +#[test] +fn ffor_parking_proves_both_views_and_abort_restores_ordinary_receiving() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_book(&nodes[1], &nodes[0], channel_id, &[voucher]); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Registered { parked_vouchers: 0, total_vouchers: 1 } + ); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + assert!(matches!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Parked { .. } + )); + assert_eq!( + nodes[1].node.abort_ffor_receiver_book( + &channel_id, + &nodes[0].node.get_our_node_id(), + [0; 32], + ), + Err(FFORReceiverError::UnknownEpoch) + ); + nodes[1] + .node + .abort_ffor_receiver_book(&channel_id, &nodes[0].node.get_our_node_id(), RECEIVER_EPOCH) + .unwrap(); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborting { reason: FFORReceiverAbortReason::Requested } + ); + drain_voucher_failures(&nodes[0], &nodes[1], &[voucher.payment_hash]); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborted { reason: FFORReceiverAbortReason::Requested } + ); + for epoch in [RECEIVER_EPOCH, [92; 32]] { + assert_eq!( + nodes[1].node.register_ffor_receiver_book( + &channel_id, + &nodes[0].node.get_our_node_id(), + epoch, + &[voucher], + ), + Err(FFORReceiverError::AlreadyRegistered) + ); + } + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + +#[test] +fn ffor_parking_requires_empty_connected_channel_and_exact_next_id() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let voucher = FFORVoucher { + htlc_id: 1, + payment_hash: PaymentHash([1; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let register = |voucher| { + nodes[1].node.register_ffor_receiver_book( + &channel_id, + &nodes[0].node.get_our_node_id(), + RECEIVER_EPOCH, + &[voucher], + ) + }; + assert_eq!(register(voucher), Err(FFORCommitmentError::InvalidVoucherBook.into())); + nodes[1].node.peer_disconnected(nodes[0].node.get_our_node_id()); + nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id()); + assert_eq!( + register(FFORVoucher { htlc_id: 0, ..voucher }), + Err(FFORCommitmentError::ChannelUnavailable.into()) + ); + pump_ffor_reconnection(&nodes[0], &nodes[1]); + commit_voucher(&nodes[0], &nodes[1], 2_000_000); + assert_eq!(register(voucher), Err(FFORCommitmentError::InvalidVoucherBook.into())); +} + +#[test] +fn ffor_parking_mismatches_abort_without_payment_events() { + for mutation in 0..5 { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (mut update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + let mut expected = voucher; + match mutation { + 0 => expected.amount_msat += 1, + 1 => expected.payment_hash = PaymentHash([42; 32]), + 2 => expected.cltv_expiry += 1, + 3 => update.update_add_htlcs[0].hold_htlc = Some(()), + _ => update.update_add_htlcs[0].skimmed_fee_msat = Some(1), + } + register_book(&nodes[1], &nodes[0], channel_id, &[expected]); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborting { reason: FFORReceiverAbortReason::VoucherMismatch } + ); + drain_voucher_failures(&nodes[0], &nodes[1], &[voucher.payment_hash]); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborted { reason: FFORReceiverAbortReason::VoucherMismatch } + ); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + } +} + +#[test] +fn ffor_parking_partial_book_never_reports_parked() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + let second = FFORVoucher { htlc_id: 1, payment_hash: PaymentHash([2; 32]), ..voucher }; + register_book(&nodes[1], &nodes[0], channel_id, &[voucher, second]); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Registered { parked_vouchers: 1, total_vouchers: 2 } + ); + nodes[1] + .node + .abort_ffor_receiver_book(&channel_id, &nodes[0].node.get_our_node_id(), RECEIVER_EPOCH) + .unwrap(); + drain_voucher_failures(&nodes[0], &nodes[1], &[voucher.payment_hash]); + // An unused voucher slot must not prevent an ordinary payment after the abort drains. + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + +/// Complete real peer messages, deferred adds, and failure commitment rounds after a crash. +fn pump_ffor_reconnection<'a, 'b, 'c>(sender: &Node<'a, 'b, 'c>, receiver: &Node<'a, 'b, 'c>) { + connect_nodes(sender, receiver); + for _ in 0..20 { + let mut progressed = false; + for (from, to) in [(sender, receiver), (receiver, sender)] { + for event in from.node.get_and_clear_pending_msg_events() { + progressed = true; + let from_id = from.node.get_our_node_id(); + match event { + MessageSendEvent::SendChannelReestablish { msg, .. } => { + to.node.handle_channel_reestablish(from_id, &msg) + }, + MessageSendEvent::SendChannelReady { msg, .. } => { + to.node.handle_channel_ready(from_id, &msg) + }, + MessageSendEvent::SendRevokeAndACK { msg, .. } => { + to.node.handle_revoke_and_ack(from_id, &msg) + }, + MessageSendEvent::SendAnnouncementSignatures { msg, .. } => { + to.node.handle_announcement_signatures(from_id, &msg) + }, + MessageSendEvent::UpdateHTLCs { updates, .. } => { + assert!(updates.update_fulfill_htlcs.is_empty()); + assert!(updates.update_fee.is_none()); + for add in updates.update_add_htlcs { + to.node.handle_update_add_htlc(from_id, &add); + } + for fail in updates.update_fail_htlcs { + to.node.handle_update_fail_htlc(from_id, &fail); + } + for fail in updates.update_fail_malformed_htlcs { + to.node.handle_update_fail_malformed_htlc(from_id, &fail); + } + to.node.handle_commitment_signed_batch_test( + from_id, + &updates.commitment_signed, + ); + }, + MessageSendEvent::BroadcastChannelAnnouncement { .. } + | MessageSendEvent::BroadcastChannelUpdate { .. } + | MessageSendEvent::SendChannelUpdate { .. } => {}, + other => panic!("Unexpected peer event during voucher unwind: {:?}", other), + } + } + if from.node.needs_pending_htlc_processing() { + from.node.process_pending_htlc_forwards(); + progressed = true; + } + from.chain_monitor.added_monitors.lock().unwrap().clear(); + } + assert!(receiver.node.get_and_clear_pending_events().is_empty()); + if !progressed { + return; + } + } + panic!("Voucher unwind did not converge"); +} + +#[test] +fn ffor_parking_crash_boundaries_preserve_identity_and_drain_stock_htlcs() { + // Registration, uncommitted add, first commitment, both commitments before interception, + // parked, explicit abort, failure commitment sent, and fully drained tombstone. + for crash_at in 0..8 { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let reloaded; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let sender_id = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_book(&nodes[1], &nodes[0], channel_id, &[voucher]); + if crash_at >= 1 { + nodes[1].node.handle_update_add_htlc(sender_id, &update.update_add_htlcs[0]); + } + if crash_at == 2 { + nodes[1].node.handle_commitment_signed_batch_test(sender_id, &update.commitment_signed); + check_added_monitors(&nodes[1], 1); + let _undelivered = get_revoke_commit_msgs!(&nodes[1], sender_id); + } + if crash_at >= 3 { + commitment_signed_dance!(&nodes[1], &nodes[0], update.commitment_signed, false); + } + if crash_at >= 4 { + expect_and_process_pending_htlcs(&nodes[1], false); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + } + if crash_at >= 5 { + nodes[1] + .node + .abort_ffor_receiver_book(&channel_id, &sender_id, RECEIVER_EPOCH) + .unwrap(); + } + if crash_at == 6 { + let _undelivered = get_htlc_update_msgs!(&nodes[1], sender_id); + check_added_monitors(&nodes[1], 1); + } + if crash_at == 7 { + drain_voucher_failures(&nodes[0], &nodes[1], &[voucher.payment_hash]); + } + let monitor_encoded = get_monitor!(nodes[1], channel_id).encode(); + let manager_encoded = nodes[1].node.encode(); + nodes[0].node.peer_disconnected(receiver_id); + reload_node!( + nodes[1], + config, + &manager_encoded, + &[&monitor_encoded], + persister, + chain_monitor, + reloaded + ); + assert_eq!( + nodes[1].node.register_ffor_receiver_book( + &channel_id, + &sender_id, + RECEIVER_EPOCH, + &[voucher] + ), + Err(FFORReceiverError::AlreadyRegistered) + ); + pump_ffor_reconnection(&nodes[0], &nodes[1]); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborted { + reason: if crash_at >= 5 { + FFORReceiverAbortReason::Requested + } else { + FFORReceiverAbortReason::Restarted + }, + }, + "crash checkpoint {}", + crash_at + ); + if crash_at != 7 { + expect_payment_failed!(&nodes[0], voucher.payment_hash, false); + } + // Both stock commitment sets have drained. The one-registration tombstone remains. + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + } +} + +#[test] +fn ffor_parking_serialization_requires_the_channel_compatibility_fence() { + use crate::ln::channel::FundedChannel; + use crate::types::features::ChannelTypeFeatures; + use crate::util::ser::ReadableArgs; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let serialize_channel = || { + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let peer = peers.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + peer.channel_by_id.get(&channel_id).unwrap().as_funded().unwrap().encode() + }; + let ordinary = serialize_channel(); + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([1; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + register_book(&nodes[1], &nodes[0], channel_id, &[voucher]); + let registered = serialize_channel(); + let features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + let read_channel = |bytes: &[u8]| { + FundedChannel::read( + &mut &bytes[..], + (&nodes[1].keys_manager, &nodes[1].keys_manager, &features), + ) + }; + assert!(read_channel(&ordinary).is_ok()); + assert!(read_channel(®istered).is_ok()); + // This deliberately simple book contains no 0xfdfffe sequence itself, so the last occurrence + // is the channel's required type. Model a reader that does not recognize that even type. + let mut unknown = registered.clone(); + let offset = unknown.windows(3).rposition(|bytes| bytes == [0xfd, 0xff, 0xfe]).unwrap(); + unknown[offset + 2] = 0xfc; + assert!(matches!(read_channel(&unknown), Err(msgs::DecodeError::UnknownRequiredFeature))); + if let Ok(directory) = std::env::var("FFOR_LEGACY_FIXTURE_DIR") { + std::fs::write(std::path::Path::new(&directory).join("ordinary-channel.bin"), &ordinary) + .unwrap(); + std::fs::write( + std::path::Path::new(&directory).join("registered-channel.bin"), + ®istered, + ) + .unwrap(); + } +} + +#[test] +fn ffor_parking_waits_for_both_rounds_and_monitor_completion() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let sender_id = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_book(&nodes[1], &nodes[0], channel_id, &[voucher]); + let assert_unparked = || { + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Registered { parked_vouchers: 0, total_vouchers: 1 } + ); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + }; + nodes[1].node.handle_update_add_htlc(sender_id, &update.update_add_htlcs[0]); + assert_unparked(); + nodes[1].node.handle_commitment_signed_batch_test(sender_id, &update.commitment_signed); + check_added_monitors(&nodes[1], 1); + let (revoke, commitment) = get_revoke_commit_msgs!(&nodes[1], sender_id); + assert_unparked(); + nodes[0].node.handle_revoke_and_ack(receiver_id, &revoke); + check_added_monitors(&nodes[0], 1); + nodes[0].node.handle_commitment_signed_batch_test(receiver_id, &commitment); + check_added_monitors(&nodes[0], 1); + let revoke = get_event_msg!(&nodes[0], MessageSendEvent::SendRevokeAndACK, receiver_id); + assert_unparked(); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + nodes[1].node.handle_revoke_and_ack(sender_id, &revoke); + check_added_monitors(&nodes[1], 1); + assert_unparked(); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + let update_id = get_monitor!(nodes[1], channel_id).get_latest_update_id(); + nodes[1].chain_monitor.chain_monitor.channel_monitor_updated(channel_id, update_id).unwrap(); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + expect_and_process_pending_htlcs(&nodes[1], false); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + assert!(matches!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Parked { .. } + )); +} + +#[test] +fn ffor_parking_multiple_rounds_and_extra_add_abort_the_entire_book() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (first_update, first, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + let (mut route, second_hash, _, secret) = + get_route_and_payment_hash!(&nodes[0], &nodes[1], 3_000_000); + route.paths[0].hops[0].cltv_expiry_delta = 144; + let second = FFORVoucher { + htlc_id: first.htlc_id + 1, + payment_hash: second_hash, + amount_msat: 3_000_000, + ..first + }; + register_book(&nodes[1], &nodes[0], channel_id, &[first, second]); + deliver_parked_voucher(&nodes[0], &nodes[1], first_update); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Registered { parked_vouchers: 1, total_vouchers: 2 } + ); + nodes[0] + .node + .send_payment_with_route( + route, + second_hash, + RecipientOnionFields::secret_only(secret), + PaymentId(second_hash.0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + let second_update = get_htlc_update_msgs!(&nodes[0], nodes[1].node.get_our_node_id()); + deliver_parked_voucher(&nodes[0], &nodes[1], second_update); + assert!(matches!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Parked { .. } + )); + let (extra, _, _) = offer_voucher(&nodes[0], &nodes[1], 1_000_000); + nodes[1] + .node + .handle_update_add_htlc(nodes[0].node.get_our_node_id(), &extra.update_add_htlcs[0]); + // Abort queues the older, committed vouchers even while this extra add is still in flight. + // Use the same real message pump after disconnect to exercise their combined unwind safely. + nodes[1].node.handle_commitment_signed_batch_test( + nodes[0].node.get_our_node_id(), + &extra.commitment_signed, + ); + nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id()); + nodes[1].node.peer_disconnected(nodes[0].node.get_our_node_id()); + pump_ffor_reconnection(&nodes[0], &nodes[1]); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborted { reason: FFORReceiverAbortReason::VoucherMismatch } + ); + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!( + events + .iter() + .filter(|event| matches!(event, crate::events::Event::PaymentFailed { .. })) + .count(), + 3 + ); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} diff --git a/lightning/src/sign/ffor.rs b/lightning/src/sign/ffor.rs new file mode 100644 index 00000000000..1e03e410ffa --- /dev/null +++ b/lightning/src/sign/ffor.rs @@ -0,0 +1,165 @@ +//! Domain-bound signing requests for the experimental FFOR Variant D protocol. +//! +//! This module constructs no protocol messages and owns no channel transitions. The protocol +//! adapter must validate canonical fields and lifecycle authority before using the node signer. + +use bitcoin::hashes::{sha256, Hash, HashEngine}; + +/// A bounded FFOR signing request containing the wire type and complete unsigned body. +/// +/// Construction checks only the allowed message type and envelope bounds. It does not parse +/// protocol fields, authenticate a peer, or authorize a channel transition. Bytes are borrowed +/// immutably so they cannot change between signer inspection and digest construction. +/// +/// Use [`crate::sign::NodeSigner::sign_ffor_message`] to keep private key material in the signer. +pub struct FFORSigningRequest<'a> { + unsigned_wire: &'a [u8], +} + +impl<'a> FFORSigningRequest<'a> { + /// Check the unsigned envelope's type and bounds before sending it to a signer. + /// + /// `unsigned_wire` contains the two-byte message type, the channel and epoch IDs, and all + /// canonical fields/TLVs, excluding the final 64-byte signature. Only the seven signed Variant + /// D setup and lifecycle message types are accepted. The completed message must fit BOLT 8's + /// 65,535-byte plaintext limit. Callers must separately validate the full protocol structure. + pub fn new(unsigned_wire: &'a [u8]) -> Result { + if unsigned_wire.len() < 2 + 32 + 32 || unsigned_wire.len() > 65_535 - 64 { + return Err(()); + } + let message_type = u16::from_be_bytes([unsigned_wire[0], unsigned_wire[1]]); + if !matches!(message_type, 55001 | 55003 | 55045 | 55047 | 55049 | 55051 | 55053) { + return Err(()); + } + Ok(Self { unsigned_wire }) + } + + /// The exact type and body that a signer may inspect against its own policy. + pub fn unsigned_wire(&self) -> &'a [u8] { + self.unsigned_wire + } + + /// The single SHA256 digest specified by FFOR section 7: `SHA256("ffor/msg" || wire)`. + /// + /// The wire already includes its two-byte type. No Lightning signed-message prefix, double + /// hash, tagged-hash construction or signature bytes are included. + pub fn digest(&self) -> [u8; 32] { + let mut engine = sha256::Hash::engine(); + engine.input(b"ffor/msg"); + engine.input(self.unsigned_wire); + sha256::Hash::from_engine(engine).to_byte_array() + } +} + +#[cfg(test)] +mod tests { + use super::FFORSigningRequest; + use crate::prelude::*; + use crate::sign::{KeysManager, NodeSigner, PhantomKeysManager, Recipient}; + use crate::util::test_utils::TestNodeSigner; + use bitcoin::hashes::{sha256, sha256d, Hash}; + use bitcoin::secp256k1::{ecdsa::Signature, Message, PublicKey, Secp256k1, SecretKey}; + + // Public Appendix D.1 ff_activate, FFOR d719161f42d1eeb6bd6c3856d564222f03c2205e. + // The published signature is verified here; this fixture contains no signing key. + const ACTIVATE: &str = "d705bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489fc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff15ba34be8afe89e4457543a6e7279f86054f1d6e9038234b786b740e9c905c001f8ae276dd460c7b2030287a608677bc51eb9cdef5b7cfc1f73d005e463076510a723024696ad20b4758b228284620d18fd30750b7051cc64efca23a1417cf1321000c0df003aa4c0c160338dfb3783120761fb4a6053836307e196fe3597032f28715b9a021074ec06ceb9c5f06af9854f8b21ff1b6b74ff3ae326a049de8270f2c8dc559"; + + fn hex(value: &str) -> Vec { + (0..value.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&value[i..i + 2], 16).unwrap()) + .collect() + } + + #[test] + fn published_activation_uses_the_exact_single_hash_domain() { + let wire = hex(ACTIVATE); + let unsigned = &wire[..wire.len() - 64]; + let request = FFORSigningRequest::new(unsigned).unwrap(); + assert_eq!(request.unsigned_wire(), unsigned); + assert_eq!( + request.digest().as_slice(), + hex("678df08e2efce5c2ac54c389dbf8df5e3846ccbd6ca78301e2bf6ac925716ebc") + ); + let signature = Signature::from_compact(&wire[wire.len() - 64..]).unwrap(); + let receiver = PublicKey::from_slice(&hex( + "039fca7f8157aa768708894ffd92550fe970edd18526a5f936583ea3b54dab3228", + )) + .unwrap(); + let secp = Secp256k1::verification_only(); + secp.verify_ecdsa(&Message::from_digest(request.digest()), &signature, &receiver).unwrap(); + let mut changed = unsigned.to_vec(); + changed[1] ^= 2; + let wrong_type = FFORSigningRequest::new(&changed).unwrap(); + assert!(secp + .verify_ecdsa(&Message::from_digest(wrong_type.digest()), &signature, &receiver) + .is_err()); + let mut wrong_domain = b"Lightning Signed Message:".to_vec(); + wrong_domain.extend_from_slice(unsigned); + assert!(secp + .verify_ecdsa( + &Message::from_digest(sha256::Hash::hash(&wrong_domain).to_byte_array()), + &signature, + &receiver + ) + .is_err()); + let mut double_hash = b"ffor/msg".to_vec(); + double_hash.extend_from_slice(unsigned); + assert!(secp + .verify_ecdsa( + &Message::from_digest(sha256d::Hash::hash(&double_hash).to_byte_array()), + &signature, + &receiver + ) + .is_err()); + } + + #[test] + fn signer_uses_local_identity_and_low_s_with_no_key_export() { + let wire = hex(ACTIVATE); + let request = FFORSigningRequest::new(&wire[..wire.len() - 64]).unwrap(); + let keys = KeysManager::new(&[7; 32], 0, 0, true); + let phantom = PhantomKeysManager::new(&[7; 32], 0, 0, &[9; 32], true); + let secp = Secp256k1::verification_only(); + let digest = Message::from_digest(request.digest()); + for signer in [&keys as &dyn NodeSigner, &phantom as &dyn NodeSigner] { + let signature = signer.sign_ffor_message(&request).unwrap(); + let mut normalized = signature; + normalized.normalize_s(); + assert_eq!(signature, normalized); + let identity = signer.get_node_id(Recipient::Node).unwrap(); + secp.verify_ecdsa(&digest, &signature, &identity).unwrap(); + assert!(secp + .verify_ecdsa( + &digest, + &signature, + &phantom.get_node_id(Recipient::PhantomNode).unwrap() + ) + .is_err()); + } + } + + #[test] + fn unsupported_signers_and_invalid_envelopes_fail_without_signing() { + let wire = hex(ACTIVATE); + let unsigned = &wire[..wire.len() - 64]; + let request = FFORSigningRequest::new(unsigned).unwrap(); + let external = TestNodeSigner::new(SecretKey::from_slice(&[1; 32]).unwrap()); + assert!(external.sign_ffor_message(&request).is_err()); + for length in 0..66 { + assert!(FFORSigningRequest::new(&unsigned[..length]).is_err()); + } + let mut bytes = vec![0; 65_535 - 64 + 1]; + bytes[..2].copy_from_slice(&55001_u16.to_be_bytes()); + assert!(FFORSigningRequest::new(&bytes).is_err()); + bytes.pop(); + assert!(FFORSigningRequest::new(&bytes).is_ok()); + for message_type in 0_u16..=u16::MAX { + bytes[..2].copy_from_slice(&message_type.to_be_bytes()); + assert_eq!( + FFORSigningRequest::new(&bytes).is_ok(), + matches!(message_type, 55001 | 55003 | 55045 | 55047 | 55049 | 55051 | 55053) + ); + } + } +} diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index ac24a4a4040..515d7e8912c 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -77,6 +77,7 @@ use musig2::types::{PartialSignature, PublicNonce}; pub(crate) mod type_resolver; pub mod ecdsa; +pub mod ffor; #[cfg(taproot)] pub mod taproot; pub mod tx_builder; @@ -988,6 +989,21 @@ pub trait NodeSigner { /// An `Err` can be returned to signal that the signer is unavailable / cannot produce a valid /// signature. fn sign_message(&self, msg: &[u8]) -> Result; + + /// Sign an experimental FFOR Variant D message with the local node identity. + /// + /// The compact low-S ECDSA signature must authenticate the request's single SHA256 digest, + /// including the `ffor/msg` domain and the two-byte wire type. Do not use [`Self::sign_message`], + /// which uses a different prefix. Implementers may inspect the complete unsigned wire bytes + /// and refuse requests outside their policy. The caller must validate canonical protocol + /// fields and the intended channel transition before requesting a signature. + /// + /// This operation does not change channel state or establish durable activation. It never + /// uses a phantom identity. The default refuses signing, preserving compatibility for + /// external signers that have not explicitly implemented this experimental protocol. + fn sign_ffor_message(&self, _request: &ffor::FFORSigningRequest<'_>) -> Result { + Err(()) + } } /// A trait that describes a wallet capable of creating a spending [`Transaction`] from a set of @@ -2404,6 +2420,11 @@ impl NodeSigner for KeysManager { fn sign_message(&self, msg: &[u8]) -> Result { Ok(crate::util::message_signing::sign(msg, &self.node_secret)) } + + fn sign_ffor_message(&self, request: &ffor::FFORSigningRequest<'_>) -> Result { + let message = hash_to_message!(&request.digest()); + Ok(sign_with_aux_rand(&self.secp_ctx, &message, &self.node_secret, &self)) + } } impl OutputSpender for KeysManager { @@ -2573,6 +2594,10 @@ impl NodeSigner for PhantomKeysManager { fn sign_message(&self, msg: &[u8]) -> Result { self.inner.sign_message(msg) } + + fn sign_ffor_message(&self, request: &ffor::FFORSigningRequest<'_>) -> Result { + self.inner.sign_ffor_message(request) + } } impl OutputSpender for PhantomKeysManager { From 364d8f905743a2bd7a91c1757ef85454c1d258a6 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 20:08:07 -0400 Subject: [PATCH 03/22] feat(ffor): share validated variant d protocol primitives --- .github/workflows/ffor-protocol.yml | 47 ++ Cargo.toml | 1 + lightning-ffor/Cargo.toml | 20 + lightning-ffor/README.md | 173 ++++++ lightning-ffor/ci/check-msrv.sh | 37 ++ lightning-ffor/fuzz/.gitignore | 5 + lightning-ffor/fuzz/Cargo.toml | 23 + lightning-ffor/fuzz/README.md | 22 + lightning-ffor/fuzz/fuzz_targets/wire.rs | 49 ++ lightning-ffor/fuzz/seed_corpus.py | 23 + lightning-ffor/src/amounts.rs | 104 ++++ lightning-ffor/src/book.rs | 169 ++++++ lightning-ffor/src/lib.rs | 20 + lightning-ffor/src/reestablish.rs | 124 ++++ lightning-ffor/src/setup.rs | 321 ++++++++++ lightning-ffor/src/transcript.rs | 132 +++++ lightning-ffor/src/wire/codec.rs | 83 +++ lightning-ffor/src/wire/lifecycle.rs | 122 ++++ lightning-ffor/src/wire/mod.rs | 171 ++++++ lightning-ffor/src/wire/setup.rs | 195 +++++++ lightning-ffor/src/wire/tlv.rs | 170 ++++++ lightning-ffor/src/wire/types.rs | 128 ++++ lightning-ffor/tests/amounts.rs | 127 ++++ lightning-ffor/tests/book.rs | 151 +++++ lightning-ffor/tests/data/appendix-d.json | 92 +++ .../tests/data/beignet-lifecycle.json | 72 +++ .../tests/data/extract_appendix_d.py | 94 +++ .../tests/data/generate_beignet_lifecycle.cjs | 50 ++ lightning-ffor/tests/reestablish.rs | 84 +++ lightning-ffor/tests/reference_lifecycle.rs | 37 ++ lightning-ffor/tests/setup.rs | 373 ++++++++++++ lightning-ffor/tests/transcript.rs | 161 +++++ lightning-ffor/tests/wire.rs | 551 ++++++++++++++++++ lightning/Cargo.toml | 3 +- 34 files changed, 3933 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ffor-protocol.yml create mode 100644 lightning-ffor/Cargo.toml create mode 100644 lightning-ffor/README.md create mode 100755 lightning-ffor/ci/check-msrv.sh create mode 100644 lightning-ffor/fuzz/.gitignore create mode 100644 lightning-ffor/fuzz/Cargo.toml create mode 100644 lightning-ffor/fuzz/README.md create mode 100644 lightning-ffor/fuzz/fuzz_targets/wire.rs create mode 100644 lightning-ffor/fuzz/seed_corpus.py create mode 100644 lightning-ffor/src/amounts.rs create mode 100644 lightning-ffor/src/book.rs create mode 100644 lightning-ffor/src/lib.rs create mode 100644 lightning-ffor/src/reestablish.rs create mode 100644 lightning-ffor/src/setup.rs create mode 100644 lightning-ffor/src/transcript.rs create mode 100644 lightning-ffor/src/wire/codec.rs create mode 100644 lightning-ffor/src/wire/lifecycle.rs create mode 100644 lightning-ffor/src/wire/mod.rs create mode 100644 lightning-ffor/src/wire/setup.rs create mode 100644 lightning-ffor/src/wire/tlv.rs create mode 100644 lightning-ffor/src/wire/types.rs create mode 100644 lightning-ffor/tests/amounts.rs create mode 100644 lightning-ffor/tests/book.rs create mode 100644 lightning-ffor/tests/data/appendix-d.json create mode 100644 lightning-ffor/tests/data/beignet-lifecycle.json create mode 100644 lightning-ffor/tests/data/extract_appendix_d.py create mode 100644 lightning-ffor/tests/data/generate_beignet_lifecycle.cjs create mode 100644 lightning-ffor/tests/reestablish.rs create mode 100644 lightning-ffor/tests/reference_lifecycle.rs create mode 100644 lightning-ffor/tests/setup.rs create mode 100644 lightning-ffor/tests/transcript.rs create mode 100644 lightning-ffor/tests/wire.rs diff --git a/.github/workflows/ffor-protocol.yml b/.github/workflows/ffor-protocol.yml new file mode 100644 index 00000000000..b44448181f0 --- /dev/null +++ b/.github/workflows/ffor-protocol.yml @@ -0,0 +1,47 @@ +name: FFOR protocol primitives + +on: + pull_request: + paths: + - 'lightning-ffor/**' + - 'Cargo.toml' + - 'lightning/Cargo.toml' + - '.github/workflows/ffor-protocol.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - run: cargo fmt -p lightning-ffor -- --check + - run: cargo test -p lightning-ffor + - run: cargo check -p lightning-ffor --lib --no-default-features + - run: cargo clippy -p lightning-ffor --all-targets -- -D warnings + + msrv: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.63.0 + - run: lightning-ffor/ci/check-msrv.sh + + fuzz: + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: lightning-ffor + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - run: cargo install cargo-fuzz --version 0.13.2 --locked + - run: python3 fuzz/seed_corpus.py + - run: cargo fuzz run wire -- -max_len=131072 -max_total_time=30 diff --git a/Cargo.toml b/Cargo.toml index e262cd09058..414ba2ffb79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ resolver = "2" # as in `ci/ci-tests.sh`. members = [ "lightning", + "lightning-ffor", "lightning-types", "lightning-block-sync", "lightning-invoice", diff --git a/lightning-ffor/Cargo.toml b/lightning-ffor/Cargo.toml new file mode 100644 index 00000000000..ca022aee25a --- /dev/null +++ b/lightning-ffor/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "lightning-ffor" +version = "0.1.0" +edition = "2021" +rust-version = "1.63" +license = "MIT OR Apache-2.0" +description = "Experimental FFOR Variant D arithmetic and transcript primitives" +publish = false + +[features] +default = ["std"] +std = ["bitcoin/std"] + +[dependencies] +bitcoin = { version = "0.32.7", default-features = false } + +[dev-dependencies] +proptest = "1.0.0" +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.128" diff --git a/lightning-ffor/README.md b/lightning-ffor/README.md new file mode 100644 index 00000000000..052deac806e --- /dev/null +++ b/lightning-ffor/README.md @@ -0,0 +1,173 @@ +# FFOR protocol foundation + +This unpublished crate implements checked Variant D amount and anchor-channel book +calculations, signed setup and lifecycle codecs, reconnect reports, canonical books +and authenticated transcript checks from draft v0.9.4. It is shared protocol data +for the native channel engine and LDK Node. It cannot prepare an offline invoice +or receive a payment by itself. The channel engine owns live state and persistence. + +Work is tracked in [ldk-node #117](https://github.com/synonymdev/ldk-node/issues/117). + +## Build and portability + +The runtime supports Rust 1.63 and `no_std` with `alloc`. The default `std` feature +enables Bitcoin's standard-library support and the standard error trait. The crate +contains no I/O or operating-system integration. Fixture and property-test +dependencies are checked with stable Rust separately from the runtime MSRV. + +```sh +cargo check -p lightning-ffor --lib --no-default-features +# Requires stable and 1.63.0 toolchains; checks both runtime feature configurations. +lightning-ffor/ci/check-msrv.sh +``` + +The MSRV script creates a temporary standalone runtime build and vendors its +dependencies for Cargo 1.63. It pins compatible versions of the C build tools, +without changing the workspace lockfile. Native workspace lockfiles are ignored. + +## Channel-engine baseline + +The receiver port targets rust-lightning **v0.2.5**, pinned to +[`5bc1dc84b3a1b084f84de4b7ece3d978b678d894`](https://github.com/lightningdevkit/rust-lightning/commit/5bc1dc84b3a1b084f84de4b7ece3d978b678d894). +This retains the current LDK Node dependency generation and its maintenance fixes. +The Synonym fork's `main` snapshot uses `0.3.0+git` and requires a separate API +migration. It is not the baseline for this port. + +## Reference and verification scope + +The normative source is FFOR at +[`d719161f42d1eeb6bd6c3856d564222f03c2205e`](https://github.com/coreyphillips/ffor/tree/d719161f42d1eeb6bd6c3856d564222f03c2205e). +`tests/data/appendix-d.json` contains the public Appendix D transcript fixtures. +The extraction script reconstructs the abbreviated D.5 messages and book using the +published deterministic inputs and existing signatures, then checks the published +wire hashes and book hash before writing the fixture. No signing keys are needed. + +```sh +python3 lightning-ffor/tests/data/extract_appendix_d.py /path/to/ffor/ffor-variant-d-vectors.md +cargo test -p lightning-ffor +cargo clippy -p lightning-ffor --all-targets -- -D warnings +cargo fmt -p lightning-ffor -- --check +``` + +Tests cover the six published transcript scenarios, including the 483-slot book, +verification of their public node signatures, both funder roles, both commitment +dust limits, an unfunded receiver, exact budgets, fee-spike reserves, negotiated +limits, deadlines, overflow and public-channel fee selection. Property tests compare +arithmetic with a wide-integer oracle and mutate budgets, amounts and transcript +domains. They do not construct or broadcast commitment transactions. + +The wire parser supports `ff_init`, `ff_accept`, `ff_activate`, `ff_activate_ack`, +`ff_abort`, `ff_close` and `ff_close_ack`. It bounds message sizes and collection +counts, rejects noncanonical BigSize values, unknown mandatory TLVs and invalid +compressed points, and preserves unknown optional fields in the signed bytes. +Low-S compact signatures are verified against expected channel peer identities. +Unsigned digest construction does not require a placeholder valid signature. + +`AuthenticatedSetup` derives the book exclusively from authenticated setup messages. +It checks fee bounds, unique hashes, requested hash chains, activation transcripts, +height agreement and close preimages, including prefix settlement for chained books. +It is immutable protocol data, not an epoch state machine or proof of live capacity. + +`tests/data/beignet-lifecycle.json` adds five signed reference lifecycle fixtures, +including both absent and explicitly empty preimage TLVs when no payment settled. +Regenerate them with `generate_beignet_lifecycle.cjs` against the pinned Beignet +checkout, using that checkout's `ts-node/register`. Both encodings are preserved +exactly so authentication never depends on normalizing received signed bytes. + +The same fixture file includes all seven reconnect states from Beignet. The +`reestablish` module reads the exact 67-byte TLV 55001 value and rejects unknown +states and nonzero Variant D sequences. It preserves the peer's reported hash as +untrusted data. The pinned Beignet sender includes its computed hash in `ACTIVATING`, +although section 11.1 specifies zeros before `ACTIVE`; local engine writers must +follow the specification. Neither form proves activation without the signed ack. + +This crate originated in `ldk-node/crates/ffor-protocol` and moved into the native +workspace so the channel engine and Node share one parser and authenticated setup +implementation. The Appendix D and Beignet JSON fixtures are preserved byte for +byte. The original fixture and coverage measurements below predate that move. + +The standalone [fuzz target](fuzz/README.md) exercises parsing, canonical round trips, +signatures and authenticated setup with real signed fixture seeds. A bounded local +run completed 6,359,059 inputs in 121 seconds without a crash; a run including the +reconnect parser completed 2,118,228 inputs in 31 seconds without a crash. The current +suite has 66 tests and four doctests. Nightly LLVM instrumentation measured 865/892 +source lines (96.97%) and 198/200 branches (99.00%) covered across this crate. Uncovered +code includes diagnostic formatting and defensive paths whose preconditions are +excluded by prior validated bounds. These are measured results, not exhaustive +proofs of correctness. After migration, the seeded fuzz target completed 2,299,173 +inputs in 31 seconds without a crash. + +There is no signer, persistence implementation or channel state machine in this +crate. Crash injection, monitor recovery and cross-engine regtest remain requirements +for the engine port. Passing pure protocol tests is not evidence of offline payment +settlement or recovery. + +## Implementation references + +The receiver and settlement port should be compared with these immutable source +revisions, in addition to the normative specification: + +- [Beignet 0.21.10, `8aee31d18e596fe49a0d195b325a6e757d7a009b`](https://github.com/coreyphillips/beignet/tree/8aee31d18e596fe49a0d195b325a6e757d7a009b). + `src/lightning/channel/channel.ts` owns voucher matching, both-view commitment + verification, activation, freeze and drain. `src/lightning/ffor/` contains the + wire, transcript and witness code. The Variant D setup and settlement tests + cover acknowledgement loss, restart, rejected updates and cooperative return. +- [beignet-umbrel, `12d483462ca2eabd8ae9cc105e69affa420459f9`](https://github.com/coreyphillips/beignet-umbrel/tree/12d483462ca2eabd8ae9cc105e69affa420459f9). + `manager/ui/src/pages/tabs/ReceiveTab.jsx` and the receive routes demonstrate + explicit offline intent, stable request identity, existing-channel capacity + and rejecting a response that is not explicitly offline-capable. The + `scripts/lfbw-regtest/` FFOR scenarios exercise process-stopped receiving and + return through the manager and daemon APIs. + +These sources implement their own channel engine. They do not supply FFOR APIs +to rust-lightning or LND. Port the protocol invariants into each engine's own +commitment and persistence boundary. Do not copy deployment defaults such as +channel headroom or invoice lifetime into Bitkit as protocol guarantees. Passing +reference tests alone cannot qualify the native port. + +## Trust boundaries + +- Amounts, peer data and public policy are untrusted. Overflow, underpayment, + overpayment and inconsistent budgets must fail before committing vouchers. +- Only the channel engine may supply channel type, negotiated limits, balances, + funder identity and dust limits. Application aggregate inbound liquidity is not + sufficient. Existing ordinary HTLCs must be drained before book validation. +- A public fee exception requires an authenticated local public channel to the + correct receiver, all four announcement signatures, correct node/funding-key + bindings and the onion's actual SCID. This crate only compares fee amounts. +- Hash functions operate on public transcript bytes and authenticate nothing on + their own. Callers must validate canonical encodings and low-S signatures. The + existing signer must retain all private keys. +- A valid book is a proposed reservation, not received money. Neither amount checks + nor matching transcript hashes imply durable activation or invoice readiness. + +There is no new unsafe code, secret storage, nonce generation or custom crypto. +SHA256 and signature verification use the existing Bitcoin dependency and its +secp256k1 implementation. No production signing API is introduced. + +## Required integration + +[rust-lightning #4](https://github.com/synonymdev/rust-lightning/pull/4) adds a +point-in-time verifier for actual committed vouchers and monitor claim signatures, +receiver voucher parking and signer support on the v0.2.5 baseline. This protocol +crate is a dependency of that engine, but does not itself transition live channels. +The remaining engine integration must own authenticated setup, persistent ACTIVE +freeze, activation acknowledgement replay, cooperative drain and on-chain +enforcement inside rust-lightning. A second channel state machine in +ldk-node would duplicate signing authority and is not an acceptable substitute. + +After that integration, ldk-node must provide exact-amount eligibility, durable and +retry-safe preparation, invoice exposure, recovery and status through generated +Kotlin/Swift bindings. Readiness must require the agreed witness acknowledgements. +The Bitkit provider adapters must only be enabled after these real APIs exist and +the interoperability tests demonstrate payer success with the receiver stopped. + +The first app profile is a positive fixed amount in whole satoshis, single-part +BOLT 11, one eligible anchor channel, with a `Receive Offline` checkbox. Amountless +invoices, just-in-time liquidity and aggregate multi-channel capacity cannot qualify. +The node must enforce the selected offline window, claim margin, chain watching and +fee funding. No production window or witness deployment is chosen by this crate. + +The requested settlement baseline is LND v0.21.3-beta. LND implementation work is +kept local. This unpublished workspace crate does not change a binding version or +a release. diff --git a/lightning-ffor/ci/check-msrv.sh b/lightning-ffor/ci/check-msrv.sh new file mode 100755 index 00000000000..4e00fe43fc5 --- /dev/null +++ b/lightning-ffor/ci/check-msrv.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Check runtime dependencies separately from property-test and fixture dependencies. +# Resolve and vendor with stable Cargo because Cargo 1.63 predates the sparse index. +crate_dir="$(cd "$(dirname "$0")/.." && pwd)" +check_dir="$(mktemp -d "${TMPDIR:-/tmp}/lightning-ffor-msrv.XXXXXX")" +trap 'rm -rf "$check_dir"' EXIT + +if [ -n "${FFOR_MSRV_BIN:-}" ]; then + msrv_cargo="$FFOR_MSRV_BIN/cargo" + msrv_rustc="$FFOR_MSRV_BIN/rustc" +else + msrv_cargo="$(rustup which --toolchain 1.63.0 cargo)" + msrv_rustc="$(rustup which --toolchain 1.63.0 rustc)" +fi + +cp -R "$crate_dir/src" "$check_dir/src" +sed '/^\[dev-dependencies\]/,$d' "$crate_dir/Cargo.toml" > "$check_dir/Cargo.toml" +printf '\n[workspace]\n' >> "$check_dir/Cargo.toml" +cd "$check_dir" +cargo +stable generate-lockfile +# Later build-tool releases require Rust 1.65; neither pin changes runtime APIs. +cargo +stable update -p cc --precise 1.2.67 +cargo +stable update -p find-msvc-tools --precise 0.1.9 +mkdir .cargo +cargo +stable vendor --locked vendor > .cargo/config.toml +# Version 4 uses the same entries here, but Cargo 1.63 accepts lockfile version 3. +python3 - <<'PY' +from pathlib import Path +lockfile = Path("Cargo.lock") +lockfile.write_text(lockfile.read_text().replace("version = 4\n", "version = 3\n", 1)) +PY + +"$msrv_rustc" --version +RUSTC="$msrv_rustc" "$msrv_cargo" check --lib --no-default-features --locked --offline +RUSTC="$msrv_rustc" "$msrv_cargo" check --lib --locked --offline diff --git a/lightning-ffor/fuzz/.gitignore b/lightning-ffor/fuzz/.gitignore new file mode 100644 index 00000000000..a0e83e5bfa1 --- /dev/null +++ b/lightning-ffor/fuzz/.gitignore @@ -0,0 +1,5 @@ +/target/ +/corpus/ +/artifacts/ +/coverage/ +/Cargo.lock diff --git a/lightning-ffor/fuzz/Cargo.toml b/lightning-ffor/fuzz/Cargo.toml new file mode 100644 index 00000000000..4162513fd8f --- /dev/null +++ b/lightning-ffor/fuzz/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "lightning-ffor-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +bitcoin = "0.32.7" +lightning-ffor = { path = ".." } + +[workspace] +members = ["."] + +[[bin]] +name = "wire" +path = "fuzz_targets/wire.rs" +test = false +doc = false +bench = false diff --git a/lightning-ffor/fuzz/README.md b/lightning-ffor/fuzz/README.md new file mode 100644 index 00000000000..8cc2d0c63be --- /dev/null +++ b/lightning-ffor/fuzz/README.md @@ -0,0 +1,22 @@ +# Wire parser and setup fuzzing + +The `wire` target checks the 65,535-byte envelope bound, canonical decode/encode round trips, +signature verification using the Appendix D public keys, and authenticated setup construction. +It accepts either one raw message or `[u16 init_length][init][accept]` so valid signed setup +pairs can seed the authenticated boundary. It contains no signing operation or private keys. + +From this directory, run: + +```sh +python3 seed_corpus.py +cargo +nightly fuzz run wire -- -max_len=131072 -max_total_time=60 +``` + +The seed script writes the six Appendix D scenarios' messages and length-prefixed setup +pairs from `../tests/data/appendix-d.json`, plus the Beignet lifecycle fixtures. Generated +corpora, crash artifacts, coverage data and the standalone workspace lockfile are ignored. + +A bounded run detects regressions but does not prove exhaustive parser coverage. The parent +crate's property tests exercise bounded arbitrary bytes, extension lengths, and settled +bitmaps independently of this target. Live network partitions, persistence and channel +transitions are outside this pure codec and require later engine integration tests. diff --git a/lightning-ffor/fuzz/fuzz_targets/wire.rs b/lightning-ffor/fuzz/fuzz_targets/wire.rs new file mode 100644 index 00000000000..9a49133b498 --- /dev/null +++ b/lightning-ffor/fuzz/fuzz_targets/wire.rs @@ -0,0 +1,49 @@ +#![no_main] + +use bitcoin::secp256k1::PublicKey; +use lightning_ffor::reestablish::Reestablish; +use lightning_ffor::setup::AuthenticatedSetup; +use lightning_ffor::wire::{Message, MAX_MESSAGE_LEN}; +use libfuzzer_sys::fuzz_target; + +fn keys() -> (PublicKey, PublicKey) { + let receiver = [ + 3, 159, 202, 127, 129, 87, 170, 118, 135, 8, 137, 79, 253, 146, 85, 15, 233, 112, 237, 209, + 133, 38, 165, 249, 54, 88, 62, 163, 181, 77, 171, 50, 40, + ]; + let settlement = [ + 2, 8, 123, 125, 27, 71, 137, 23, 15, 110, 55, 79, 10, 14, 88, 161, 183, 168, 153, 227, 73, + 41, 121, 83, 20, 171, 105, 100, 230, 150, 9, 233, 192, + ]; + (PublicKey::from_slice(&receiver).unwrap(), PublicKey::from_slice(&settlement).unwrap()) +} + +fuzz_target!(|data: &[u8]| { + if data.len() > 2 * MAX_MESSAGE_LEN + 2 { + return; + } + let (receiver, settlement) = keys(); + if let Ok(report) = Reestablish::decode(data) { + assert_eq!(report.encode().as_slice(), data); + } + if let Ok(message) = Message::decode(data) { + assert_eq!(message.encode().unwrap(), data); + let _ = message.verify_signature(&receiver); + let _ = message.verify_signature(&settlement); + } + // Two-byte first-message length also permits real signed setup pairs in the corpus. + if data.len() < 2 { + return; + } + let split = usize::from(u16::from_be_bytes([data[0], data[1]])); + let Some(first) = data.get(2..2 + split) else { + return; + }; + let Some(second) = data.get(2 + split..) else { + return; + }; + if let (Ok(init), Ok(accept)) = (Message::decode(first), Message::decode(second)) { + let _ = AuthenticatedSetup::new(&init, &accept, receiver, settlement); + let _ = AuthenticatedSetup::new(&init, &accept, settlement, receiver); + } +}); diff --git a/lightning-ffor/fuzz/seed_corpus.py b/lightning-ffor/fuzz/seed_corpus.py new file mode 100644 index 00000000000..5eb982898ee --- /dev/null +++ b/lightning-ffor/fuzz/seed_corpus.py @@ -0,0 +1,23 @@ +"""Seed protocol fuzzing with pinned public signed fixtures, without a signing key.""" + +import json +from pathlib import Path + +crate = Path(__file__).resolve().parents[1] +corpus = crate / "fuzz" / "corpus" / "wire" +corpus.mkdir(parents=True, exist_ok=True) +fixtures = json.loads((crate / "tests/data/appendix-d.json").read_text()) +for index, fixture in enumerate(fixtures): + for field in ("init_wire", "accept_wire", "activate_wire", "ack_wire"): + (corpus / f"appendix-{index}-{field}").write_bytes(bytes.fromhex(fixture[field])) + init = bytes.fromhex(fixture["init_wire"]) + accept = bytes.fromhex(fixture["accept_wire"]) + (corpus / f"appendix-{index}-pair").write_bytes(len(init).to_bytes(2, "big") + init + accept) + +reference = json.loads((crate / "tests/data/beignet-lifecycle.json").read_text()) +for fixture in reference["fixtures"]: + (corpus / fixture["name"]).write_bytes(bytes.fromhex(fixture["wire"])) +for fixture in reference["reestablish"]: + (corpus / f"reestablish-{fixture['state']}").write_bytes(bytes.fromhex(fixture["value"])) + +print("Seeded 35 signed reference messages/setup pairs and 7 reconnect reports") diff --git a/lightning-ffor/src/amounts.rs b/lightning-ffor/src/amounts.rs new file mode 100644 index 00000000000..17082360901 --- /dev/null +++ b/lightning-ffor/src/amounts.rs @@ -0,0 +1,104 @@ +//! Checked fixed-amount arithmetic from FFOR v0.9.4 section 7.6. + +use core::fmt; + +const MILLION: u64 = 1_000_000; + +/// Invalid amount arithmetic. Errors must prevent setup or settlement. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AmountError { + /// A fixed-amount voucher cannot have a zero value. + ZeroAmount, + /// A multiplication, fee, or gross amount exceeds the protocol's u64 bound. + Overflow, + /// A plaintext hop attempts to credit a different amount than the signed voucher. + WrongPayeeAmount, + /// The upstream HTLC does not cover the forwarding amount and required fee. + InsufficientFee, +} + +impl fmt::Display for AmountError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::ZeroAmount => "FFOR voucher amount must be positive", + Self::Overflow => "FFOR amount exceeds u64", + Self::WrongPayeeAmount => "forwarding amount differs from voucher amount", + Self::InsufficientFee => "incoming amount does not cover forwarding fee", + }) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for AmountError {} + +/// BOLT 7 forwarding terms, applied to the voucher's payee amount in millisatoshis. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct FeePolicy { + /// Fixed forwarding fee in millisatoshis. + pub base_msat: u32, + /// Proportional fee in millionths of the payee amount. + pub proportional_millionths: u32, +} + +impl FeePolicy { + /// Calculate the fee without reducing the voucher amount. + /// + /// Section 7.6 rejects a product exceeding u64 even if division would make it fit. + /// This check must also run during setup, before committing the voucher. + /// + /// ``` + /// use lightning_ffor::amounts::FeePolicy; + /// let fees = FeePolicy { base_msat: 1_000, proportional_millionths: 5_000 }; + /// assert_eq!(fees.fee_msat(1_000_000), Ok(6_000)); + /// ``` + pub fn fee_msat(self, payee_msat: u64) -> Result { + let product = payee_msat + .checked_mul(u64::from(self.proportional_millionths)) + .ok_or(AmountError::Overflow)?; + (product / MILLION).checked_add(u64::from(self.base_msat)).ok_or(AmountError::Overflow) + } + + /// Calculate the book-priced upstream amount with all protocol overflow checks. + pub fn gross_msat(self, payee_msat: u64) -> Result { + payee_msat.checked_add(self.fee_msat(payee_msat)?).ok_or(AmountError::Overflow) + } +} + +/// Check the two plaintext amount conditions for a Variant D payment. +/// +/// `qualifying_public_policy` may only be supplied after the engine verifies section 7.6's +/// public channel announcement, both node/funding-key bindings and the onion's actual SCID. +/// Private channels, aliases and unverified announcements require `None`. This function +/// cannot authenticate channel gossip. Never use it for blinded payments. +/// +/// Successful amount checks do not authorize preimage release: the channel engine must also +/// verify the slot, durable epoch, deadlines and irrevocable upstream HTLC before settlement. +pub fn check_plaintext_amount( + payee_msat: u64, forward_msat: u64, incoming_msat: u64, book_policy: FeePolicy, + qualifying_public_policy: Option, +) -> Result<(), AmountError> { + if payee_msat == 0 { + return Err(AmountError::ZeroAmount); + } + if forward_msat != payee_msat { + return Err(AmountError::WrongPayeeAmount); + } + let book_fee = book_policy.gross_msat(payee_msat)? - payee_msat; + // Compare complete policies. Mixing the cheapest base and rate would undercharge. + let required_fee = match qualifying_public_policy { + Some(public) => { + // The setup product restriction applies to the signed book. A later public policy + // increase must never prevent settlement at the valid book fee. + let public_fee = u128::from(public.base_msat) + + u128::from(payee_msat) * u128::from(public.proportional_millionths) + / u128::from(MILLION); + u128::from(book_fee).min(public_fee) as u64 + }, + None => book_fee, + }; + let paid_fee = incoming_msat.checked_sub(forward_msat).ok_or(AmountError::InsufficientFee)?; + if paid_fee < required_fee { + return Err(AmountError::InsufficientFee); + } + Ok(()) +} diff --git a/lightning-ffor/src/book.rs b/lightning-ffor/src/book.rs new file mode 100644 index 00000000000..9d3172cc9ac --- /dev/null +++ b/lightning-ffor/src/book.rs @@ -0,0 +1,169 @@ +//! Arithmetic eligibility for ECDSA zero-fee-HTLC anchor channels. +//! +//! The caller must derive every limit from the same current channel state, with no other +//! pending HTLCs. This module does not establish negotiated support or sign commitments. + +use alloc::vec::Vec; + +use core::fmt; + +use crate::amounts::{AmountError, FeePolicy}; + +/// BOLT 2 upper bound, not a promise of available channel capacity. +pub const MAX_VOUCHERS: usize = 483; +const ANCHOR_COMMITMENT_WEIGHT: u64 = 1124; +const HTLC_OUTPUT_WEIGHT: u64 = 172; +const BOTH_ANCHORS_SAT: u64 = 660; + +/// Proposed fixed-amount book. All amounts are millisatoshis and deadlines are block heights. +#[derive(Clone, Debug)] +pub struct BookTerms { + /// Exact amount of every voucher, in hash-set order. + pub amounts_msat: Vec, + /// Signed sum of all voucher amounts, excluding forwarding fees. + pub budget_msat: u64, + /// Signed minimum voucher amount. + pub minimum_payment_msat: u64, + /// Signed forwarding policy. + pub fees: FeePolicy, + /// Last height at which the peer can admit delegated payments. + pub settlement_deadline: u32, + /// Uniform absolute HTLC expiry. + pub voucher_expiry: u32, +} + +/// Current limits for a channel already verified to use zero-fee-HTLC anchors. +/// +/// These values must come from the channel engine, not application balances or peer claims. +/// Normal pending HTLCs must be drained before testing a new Variant D book. +#[derive(Clone, Copy, Debug)] +pub struct AnchorChannelLimits { + /// Receiver's negotiated maximum accepted HTLC count. + pub max_accepted_htlcs: u16, + /// Receiver's negotiated maximum in-flight value in millisatoshis. + pub max_in_flight_msat: u64, + /// Minimum HTLC amount for offers from settlement peer to receiver. + pub htlc_minimum_msat: u64, + /// Dust limit in the receiver's commitment view. + pub receiver_dust_sat: u64, + /// Dust limit in the settlement peer's commitment view. + pub settlement_dust_sat: u64, + /// Settlement peer's balance before the voucher round, before commitment fee deduction. + pub settlement_balance_msat: u64, + /// Receiver's balance before the voucher round, before commitment fee deduction. + pub receiver_balance_msat: u64, + /// Reserve the settlement peer must retain in satoshis. + pub settlement_reserve_sat: u64, + /// Reserve the receiver must retain in satoshis. + pub receiver_reserve_sat: u64, + /// Whether the settlement peer funds the channel and pays commitment fees. + pub settlement_is_funder: bool, + /// Frozen commitment feerate in satoshis per 1000 weight units. + pub feerate_sat_per_kw: u32, +} + +/// An arithmetic condition preventing voucher setup. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BookError { + /// Empty book, protocol count exceeded, or negotiated count exceeded. + SlotCount, + /// A zero amount, signed minimum violation, or channel HTLC minimum violation. + MinimumAmount, + /// At least one voucher would be trimmed in a commitment view. + Dust, + /// The signed budget differs from the voucher sum. + BudgetMismatch, + /// The voucher sum exceeds the receiver's negotiated in-flight bound. + InFlightLimit, + /// Admission has already ended, or the required claim margin does not fit. + Deadline, + /// The settlement peer cannot fund the book and retain its reserve. + SettlementReserve, + /// The funder cannot retain its reserve, both anchors and the doubled-feerate buffer. + FeeReserve, + /// A required sum, product, or upstream amount exceeds its protocol bound. + Overflow, +} + +impl fmt::Display for BookError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ineligible FFOR voucher book: {self:?}") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for BookError {} + +impl From for BookError { + fn from(_: AmountError) -> Self { + Self::Overflow + } +} + +/// Validate the arithmetic conditions for a proposed Variant D anchor-channel book. +/// +/// `claim_margin_blocks` is a deployment policy, not an assumed offline window. Section 7.1 +/// recommends at least 1008 blocks. The caller must additionally validate channel delay, +/// chain watching, negotiated variant, signatures, unique hashes, HTLC identities, actual +/// commitment outputs, and persistence. At activation, recheck these conditions using the +/// retained pre-round balances and current policy. Do not pass post-round balances here: +/// this function subtracts the book budget, which would debit the vouchers twice. Actual +/// committed outputs and remaining balances require separate engine validation. +/// +/// This function has no side effects and never means an offline invoice is ready. +pub fn validate_anchor_book( + terms: &BookTerms, channel: &AnchorChannelLimits, current_height: u32, claim_margin_blocks: u32, +) -> Result<(), BookError> { + let count = terms.amounts_msat.len(); + if count == 0 || count > MAX_VOUCHERS || count > usize::from(channel.max_accepted_htlcs) { + return Err(BookError::SlotCount); + } + let required_expiry = terms.settlement_deadline.checked_add(claim_margin_blocks); + if terms.settlement_deadline <= current_height + || claim_margin_blocks == 0 + || required_expiry.map_or(true, |expiry| terms.voucher_expiry < expiry) + { + return Err(BookError::Deadline); + } + let mut total_msat = 0_u64; + for &amount in &terms.amounts_msat { + if amount == 0 || amount < terms.minimum_payment_msat || amount < channel.htlc_minimum_msat + { + return Err(BookError::MinimumAmount); + } + if amount / 1000 < channel.receiver_dust_sat.max(channel.settlement_dust_sat) { + return Err(BookError::Dust); + } + terms.fees.gross_msat(amount)?; + total_msat = total_msat.checked_add(amount).ok_or(BookError::Overflow)?; + } + if total_msat != terms.budget_msat { + return Err(BookError::BudgetMismatch); + } + if total_msat > channel.max_in_flight_msat { + return Err(BookError::InFlightLimit); + } + let settlement_after_msat = channel + .settlement_balance_msat + .checked_sub(total_msat) + .ok_or(BookError::SettlementReserve)?; + if settlement_after_msat / 1000 < channel.settlement_reserve_sat { + return Err(BookError::SettlementReserve); + } + let (funder_after_sat, funder_reserve_sat) = if channel.settlement_is_funder { + (settlement_after_msat / 1000, channel.settlement_reserve_sat) + } else { + (channel.receiver_balance_msat / 1000, channel.receiver_reserve_sat) + }; + // Count is bounded by 483 and feerate by u32, so this u64 product cannot overflow. + let weight = ANCHOR_COMMITMENT_WEIGHT + HTLC_OUTPUT_WEIGHT * count as u64; + let spike_fee_sat = u64::from(channel.feerate_sat_per_kw) * 2 * weight / 1000; + let required_sat = funder_reserve_sat + .checked_add(spike_fee_sat) + .and_then(|amount| amount.checked_add(BOTH_ANCHORS_SAT)) + .ok_or(BookError::Overflow)?; + if funder_after_sat < required_sat { + return Err(BookError::FeeReserve); + } + Ok(()) +} diff --git a/lightning-ffor/src/lib.rs b/lightning-ffor/src/lib.rs new file mode 100644 index 00000000000..07e6d75b472 --- /dev/null +++ b/lightning-ffor/src/lib.rs @@ -0,0 +1,20 @@ +//! Pure primitives for the draft FFOR Variant D protocol. +//! +//! These primitives do not implement a receiver or settlement peer. In particular, successful +//! arithmetic, signature verification or transcript construction is not proof of channel +//! commitments, durable activation, or readiness to expose an offline invoice. +//! +//! The channel engine must retain sole ownership of commitment transitions and signing keys. + +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +extern crate alloc; + +pub mod amounts; +pub mod book; +pub mod reestablish; +pub mod setup; +pub mod transcript; +pub mod wire; diff --git a/lightning-ffor/src/reestablish.rs b/lightning-ffor/src/reestablish.rs new file mode 100644 index 00000000000..84302c03117 --- /dev/null +++ b/lightning-ffor/src/reestablish.rs @@ -0,0 +1,124 @@ +//! The Variant D value carried by `channel_reestablish` TLV 55001 (FFOR section 11.1). +//! +//! This is an unsigned peer report, not evidence of activation or payment. The channel engine +//! must compare it with its durable epoch and authenticate any replayed acknowledgement. In +//! particular, Variant D's zero sequence number never permits discarding an active epoch. + +use core::fmt; + +use crate::transcript::Digest; + +/// The TLV type within a standard BOLT 2 `channel_reestablish` message. +pub const TLV_TYPE: u64 = 55001; +/// The exact value length, excluding the enclosing TLV type and length. +pub const VALUE_LEN: usize = 67; + +/// A reported lifecycle phase, without authority to perform a channel transition. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum ReportedState { + /// Setup has started but its vouchers are not yet committed. + Negotiating = 0, + /// The complete book exists in both commitment views. + VouchersCommitted = 1, + /// The receiver is waiting for the signed activation acknowledgement. + Activating = 2, + /// The reporting peer says it durably accepted activation. + Active = 3, + /// The reporting peer says it accepted the close acknowledgement. + Draining = 4, + /// All vouchers have been irrevocably resolved. + Closed = 5, + /// Setup was aborted before activation. + Aborted = 6, +} + +/// The reported epoch and phase on reconnect. +/// +/// No signature is included here. Parsing preserves the reported hash even in a pre-active +/// state, where it has no activation authority. This also permits reading the pinned Beignet +/// implementation, which retains its computed hash while reporting `Activating`. +/// +/// Locally generated reports must use a zero hash before `Active`, as required by section 11.1. +/// `last_seq` is not exposed because Variant D always encodes zero. Ordinary BOLT 2 commitment +/// and revocation counters still need their normal validation by the channel engine. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Reestablish { + /// The epoch being reported, scoped by the enclosing message's channel ID. + pub epoch_id: Digest, + /// The peer's reported lifecycle state. + pub state: ReportedState, + /// Reported activation digest, meaningful only with appropriate authenticated evidence. + pub activation_hash: Digest, +} + +impl Reestablish { + /// Decode one exact TLV value. Reject truncation, trailing bytes, unknown states and A/B sequences. + /// + /// This function allocates nothing and does not authorize any channel transition. + pub fn decode(value: &[u8]) -> Result { + let value: &[u8; VALUE_LEN] = value.try_into().map_err(|_| ReestablishError::Length)?; + let state = match value[32] { + 0 => ReportedState::Negotiating, + 1 => ReportedState::VouchersCommitted, + 2 => ReportedState::Activating, + 3 => ReportedState::Active, + 4 => ReportedState::Draining, + 5 => ReportedState::Closed, + 6 => ReportedState::Aborted, + _ => return Err(ReestablishError::State), + }; + if value[33..35] != [0, 0] { + return Err(ReestablishError::Sequence); + } + let mut epoch_id = [0; 32]; + epoch_id.copy_from_slice(&value[..32]); + let mut activation_hash = [0; 32]; + activation_hash.copy_from_slice(&value[35..]); + Ok(Self { epoch_id, state, activation_hash }) + } + + /// Encode this report exactly, with Variant D's zero sequence number. + /// + /// Use only engine-owned durable state to construct an outbound report. For an active receiver, + /// a missing or conflicting peer report is not permission to discard vouchers or lift the freeze. + /// + /// ``` + /// use lightning_ffor::reestablish::{Reestablish, ReportedState}; + /// let report = Reestablish { + /// epoch_id: [1; 32], state: ReportedState::Activating, activation_hash: [0; 32], + /// }; + /// assert_eq!(Reestablish::decode(&report.encode()).unwrap(), report); + /// ``` + pub fn encode(&self) -> [u8; VALUE_LEN] { + let mut value = [0; VALUE_LEN]; + value[..32].copy_from_slice(&self.epoch_id); + value[32] = self.state as u8; + value[35..].copy_from_slice(&self.activation_hash); + value + } +} + +/// Why a reported reconnect value is not a supported Variant D encoding. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReestablishError { + /// The input was not exactly 67 bytes. + Length, + /// The state byte did not identify one of the seven specified states. + State, + /// Variant D requires `last_seq` to equal zero. + Sequence, +} + +impl fmt::Display for ReestablishError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Length => "FFOR reconnect value must be exactly 67 bytes", + Self::State => "unknown FFOR reconnect state", + Self::Sequence => "Variant D reconnect sequence must be zero", + }) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ReestablishError {} diff --git a/lightning-ffor/src/setup.rs b/lightning-ffor/src/setup.rs new file mode 100644 index 00000000000..c5c9cea6d4b --- /dev/null +++ b/lightning-ffor/src/setup.rs @@ -0,0 +1,321 @@ +//! Authenticated Variant D setup transcripts and their canonical voucher books. +//! +//! These are immutable protocol data, not a second channel state machine. Construction checks +//! node signatures and signed agreement. Live capacity, revealed commitment secrets, voucher +//! parking, persisted claim material, freeze and invoice exposure remain engine responsibilities. + +use alloc::vec::Vec; + +use alloc::collections::BTreeSet; +use core::fmt; + +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::PublicKey; + +use crate::amounts::FeePolicy; +use crate::book::BookTerms; +use crate::transcript::{self, Digest}; +use crate::wire::{Header, Init, Message, Payload, WireError}; + +/// A signed statement is inconsistent with the agreed setup or expected peer. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SetupError { + /// The envelope is invalid or its expected peer's signature does not verify. + Wire(WireError), + /// Receiver and settlement peer must be distinct authenticated nodes. + Roles, + /// This operation received the wrong message type. + MessageType, + /// The channel or epoch identifier differs from the authenticated setup. + Identity, + /// A payment hash was assigned to more than one voucher. + DuplicateHash, + /// Requested chained vouchers do not satisfy the signed hash-chain relation. + HashChain, + /// A signed voucher's fee product or upstream gross amount exceeds u64. + FeeOverflow, + /// The claimed transcript, book, activation or actual commitment digest differs. + Transcript, + /// The activation height is too far from the local tip or admission has ended. + Height, + /// A close acknowledgement does not describe exactly this book's slots and hashes. + CloseBook, +} + +impl fmt::Display for SetupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid FFOR setup statement: {self:?}") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for SetupError {} + +impl From for SetupError { + fn from(error: WireError) -> Self { + Self::Wire(error) + } +} + +/// One entry in an authenticated, fixed-amount Variant D book. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Voucher { + /// One-based position in the signed payment hash list. + pub slot: u16, + /// Settlement-generated hash; no preimage is part of setup. + pub payment_hash: [u8; 32], + /// Exact promised payee amount, with forwarding fees charged separately. + pub amount_msat: u64, + /// Absolute HTLC expiry shared by the entire book. + pub expiry: u32, + /// Admission deadline shared by the entire book. + pub deadline: u32, + /// Settlement peer's actual outgoing HTLC identifier to use for this voucher. + pub htlc_id: u64, +} + +/// Immutable signed agreement and canonical book derived exclusively from init and accept. +/// +/// The public messages used to construct this value are copied. Mutating them afterwards cannot +/// alter its hashes or vouchers. This value neither owns a channel nor authorizes activation. +#[derive(Clone, Debug)] +pub struct AuthenticatedSetup { + init: Message, + accept: Message, + receiver: PublicKey, + settlement: PublicKey, + vouchers: Vec, + setup_hash: Digest, + book_hash: Digest, + canonical_book: Vec, +} + +impl AuthenticatedSetup { + /// Verify both signatures and signed agreement, then derive the canonical slot book. + /// + /// The node keys must come from the channel's authenticated identities. Check channel/epoch + /// uniqueness and reject payment hashes matching disclosed commitment secrets in the engine. + /// This constructor has no I/O and cannot make an invoice ready. + pub fn new( + init: &Message, accept: &Message, receiver: PublicKey, settlement: PublicKey, + ) -> Result { + if receiver == settlement { + return Err(SetupError::Roles); + } + let (terms, accepted) = match (&init.payload, &accept.payload) { + (Payload::Init(terms), Payload::Accept(accepted)) => (terms, accepted), + _ => return Err(SetupError::MessageType), + }; + init.verify_signature(&receiver)?; + accept.verify_signature(&settlement)?; + accept.validate_accept(init)?; + let mut seen = BTreeSet::new(); + let mut vouchers = Vec::with_capacity(terms.amounts_msat.len()); + let fees = FeePolicy { + base_msat: terms.fee_base_msat, + proportional_millionths: terms.fee_proportional_millionths, + }; + for (index, (&amount, &hash)) in + terms.amounts_msat.iter().zip(&accepted.payment_hashes).enumerate() + { + fees.gross_msat(amount).map_err(|_| SetupError::FeeOverflow)?; + if !seen.insert(hash) { + return Err(SetupError::DuplicateHash); + } + if terms.hash_chain + && index > 0 && sha256::Hash::hash(&hash).to_byte_array() + != accepted.payment_hashes[index - 1] + { + return Err(SetupError::HashChain); + } + // Encoding validated the 483-slot bound and the entire HTLC id interval. + vouchers.push(Voucher { + slot: index as u16 + 1, + payment_hash: hash, + amount_msat: amount, + expiry: terms.voucher_expiry, + deadline: terms.settlement_deadline, + htlc_id: accepted.s_htlc_id_base + index as u64, + }); + } + let canonical_book = encode_book(init.header.epoch_id, &vouchers); + let init_hash = transcript::init_hash(&init.encode()?); + Ok(Self { + init: init.clone(), + accept: accept.clone(), + receiver, + settlement, + vouchers, + setup_hash: transcript::setup_hash(&init_hash, &accept.encode()?), + book_hash: transcript::book_hash(&canonical_book), + canonical_book, + }) + } + + /// The common channel and epoch identifiers authenticated by both nodes. + pub fn header(&self) -> Header { + self.init.header + } + + /// Original init, including all signed optional extensions. + pub fn init(&self) -> &Message { + &self.init + } + + /// Original accept, including all signed optional extensions. + pub fn accept(&self) -> &Message { + &self.accept + } + + /// Canonical slot order, with one exact payment hash and amount per HTLC identifier. + pub fn vouchers(&self) -> &[Voucher] { + &self.vouchers + } + + /// Section 7.5.3 bytes with variant 4 and fixed-amount profile 1. + pub fn canonical_book(&self) -> &[u8] { + &self.canonical_book + } + + /// T_setup over the complete signed init and accept. + pub fn setup_hash(&self) -> Digest { + self.setup_hash + } + + /// H_book over the canonical voucher entries, without a wire-message wrapper. + pub fn book_hash(&self) -> Digest { + self.book_hash + } + + /// Signed arithmetic terms, to validate against limits read from the actual channel. + pub fn terms(&self) -> BookTerms { + let init = self.init_terms(); + BookTerms { + amounts_msat: init.amounts_msat.clone(), + budget_msat: init.budget_msat, + minimum_payment_msat: init.min_payment_msat, + fees: FeePolicy { + base_msat: init.fee_base_msat, + proportional_millionths: init.fee_proportional_millionths, + }, + settlement_deadline: init.settlement_deadline, + voucher_expiry: init.voucher_expiry, + } + } + + /// Authenticate activation against this setup and the channel engine's actual H_commit. + /// + /// The caller must obtain `commitment_hash` from both fully signed, irrevocably committed + /// transaction views under quiescence. A peer-supplied digest is not acceptable evidence. + /// The returned H_act is only a digest: persist the complete epoch and enforce the channel + /// freeze before acknowledging activation or exposing an invoice. + pub fn validate_activation( + &self, message: &Message, commitment_hash: Digest, current_height: u32, + ) -> Result { + self.authenticate(message, &self.receiver)?; + let activation = match &message.payload { + Payload::Activate(activation) => activation, + _ => return Err(SetupError::MessageType), + }; + if activation.setup_hash != self.setup_hash + || activation.book_hash != self.book_hash + || activation.commit_hash != commitment_hash + { + return Err(SetupError::Transcript); + } + let deadline = self.init_terms().settlement_deadline; + if activation.epoch_start_height.abs_diff(current_height) > 6 + || current_height >= deadline + || activation.epoch_start_height >= deadline + { + return Err(SetupError::Height); + } + Ok(transcript::activation_hash( + &self.setup_hash, + &self.book_hash, + &commitment_hash, + activation.epoch_start_height, + )) + } + + /// Verify the settlement peer's acknowledgement of the locally validated H_act. + /// + /// An authenticated acknowledgement does not establish persistence or recovery readiness. + pub fn validate_activation_ack( + &self, message: &Message, activation_hash: Digest, + ) -> Result<(), SetupError> { + self.authenticate(message, &self.settlement)?; + match message.payload { + Payload::ActivateAck(hash) if hash == activation_hash => Ok(()), + Payload::ActivateAck(_) => Err(SetupError::Transcript), + _ => Err(SetupError::MessageType), + } + } + + /// Verify close accounting and every disclosed preimage against this signed book. + /// + /// An unset bit never permits discarding a preimage learned from another source. Retain + /// contradictory evidence and fulfil every known claim through the engine's drain path. + pub fn validate_close_ack( + &self, message: &Message, activation_hash: Digest, + ) -> Result<(), SetupError> { + self.authenticate(message, &self.settlement)?; + let close = match &message.payload { + Payload::CloseAck(close) => close, + _ => return Err(SetupError::MessageType), + }; + if close.activation_hash != activation_hash { + return Err(SetupError::Transcript); + } + if usize::from(close.num_slots) != self.vouchers.len() { + return Err(SetupError::CloseBook); + } + if self.init_terms().hash_chain + && close + .preimages + .iter() + .enumerate() + .any(|(index, preimage)| usize::from(preimage.slot) != index + 1) + { + return Err(SetupError::CloseBook); + } + for preimage in &close.preimages { + let voucher = &self.vouchers[usize::from(preimage.slot) - 1]; + if sha256::Hash::hash(&preimage.value).to_byte_array() != voucher.payment_hash { + return Err(SetupError::CloseBook); + } + } + Ok(()) + } + + fn authenticate(&self, message: &Message, signer: &PublicKey) -> Result<(), SetupError> { + message.verify_signature(signer)?; + if message.header != self.header() { + return Err(SetupError::Identity); + } + Ok(()) + } + + fn init_terms(&self) -> &Init { + match &self.init.payload { + Payload::Init(init) => init, + _ => unreachable!("invariant: constructor validated immutable init payload"), + } + } +} + +fn encode_book(epoch_id: [u8; 32], vouchers: &[Voucher]) -> Vec { + let mut bytes = Vec::with_capacity(36 + 58 * vouchers.len()); + bytes.extend_from_slice(&epoch_id); + bytes.extend_from_slice(&[4, 1]); + bytes.extend_from_slice(&(vouchers.len() as u16).to_be_bytes()); + for voucher in vouchers { + bytes.extend_from_slice(&voucher.slot.to_be_bytes()); + bytes.extend_from_slice(&voucher.payment_hash); + bytes.extend_from_slice(&voucher.amount_msat.to_be_bytes()); + bytes.extend_from_slice(&voucher.expiry.to_be_bytes()); + bytes.extend_from_slice(&voucher.deadline.to_be_bytes()); + bytes.extend_from_slice(&voucher.htlc_id.to_be_bytes()); + } + bytes +} diff --git a/lightning-ffor/src/transcript.rs b/lightning-ffor/src/transcript.rs new file mode 100644 index 00000000000..f2f477bf0f0 --- /dev/null +++ b/lightning-ffor/src/transcript.rs @@ -0,0 +1,132 @@ +//! Domain-separated SHA256 transcript primitives from FFOR section 7.5. +//! +//! Inputs are public transcript data. The caller must validate canonical wire encodings and +//! signatures before using these digests. Hashing alone authenticates nothing. + +use bitcoin::hashes::{sha256, Hash, HashEngine}; +use bitcoin::secp256k1::ecdsa::Signature; +use bitcoin::secp256k1::{Message, PublicKey, Secp256k1}; +use core::fmt; + +/// A 32-byte SHA256 transcript digest in raw byte order. +pub type Digest = [u8; 32]; + +/// Failure to authenticate a compact node-key signature from FFOR section 7. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SignatureError { + /// The signature is not a valid compact ECDSA encoding. + Malformed, + /// The signature uses a noncanonical high-S scalar. + HighS, + /// The expected node did not sign this message type and exact unsigned body. + Invalid, +} + +impl fmt::Display for SignatureError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid FFOR message signature: {self:?}") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for SignatureError {} + +/// Authenticate the exact unsigned body under the expected peer's node key. +/// +/// Uses the single SHA256 digest and low-S compact ECDSA encoding required by section 7. +/// The body includes the channel and epoch identifiers and every TLV, without a message type +/// prefix or trailing signature. Do not reconstruct it while discarding unknown odd TLVs. +/// The expected key must come from the authenticated channel peer, not the message itself. +/// +/// This verifies identity and byte integrity only. The caller must still decode a canonical +/// supported message, check the sender's role, validate the transcript and enforce epoch state. +/// No signing key is used or exposed. +/// +/// ``` +/// use bitcoin::secp256k1::PublicKey; +/// use lightning_ffor::transcript::verify_message_signature; +/// let node = PublicKey::from_slice(&[ +/// 2, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, +/// 0x95, 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, +/// 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98, +/// ]).unwrap(); +/// assert!(verify_message_signature(55001, &[0; 64], &[0; 64], &node).is_err()); +/// ``` +pub fn verify_message_signature( + message_type: u16, unsigned_body: &[u8], compact_signature: &[u8; 64], + expected_signer: &PublicKey, +) -> Result<(), SignatureError> { + let signature = + Signature::from_compact(compact_signature).map_err(|_| SignatureError::Malformed)?; + let mut normalized = signature; + normalized.normalize_s(); + if normalized != signature { + return Err(SignatureError::HighS); + } + let message = Message::from_digest(message_digest(message_type, unsigned_body)); + Secp256k1::verification_only() + .verify_ecdsa(&message, &signature, expected_signer) + .map_err(|_| SignatureError::Invalid) +} + +fn hash_parts(tag: &[u8], parts: &[&[u8]]) -> Digest { + let mut engine = sha256::Hash::engine(); + engine.input(tag); + for part in parts { + engine.input(part); + } + sha256::Hash::from_engine(engine).to_byte_array() +} + +/// Digest signed by a node key: one SHA256 over domain, type and unsigned body. +/// +/// The unsigned body includes channel id, epoch id, fixed fields and every TLV, but excludes +/// the final 64-byte signature. Signature verification must separately reject high-S values. +pub fn message_digest(message_type: u16, unsigned_body: &[u8]) -> Digest { + hash_parts(b"ffor/msg", &[&message_type.to_be_bytes(), unsigned_body]) +} + +/// Hash the complete, validated `ff_init` wire message including type and signature. +pub fn init_hash(init_wire: &[u8]) -> Digest { + hash_parts(b"ffor/tr/init", &[init_wire]) +} + +/// Bind the complete `ff_accept` wire message to its preceding signed init. +pub fn setup_hash(init: &Digest, accept_wire: &[u8]) -> Digest { + hash_parts(b"ffor/tr/setup", &[init, accept_wire]) +} + +/// Hash the canonical voucher book, including its epoch, variant, profile and ordered entries. +/// +/// Canonical encoding and book validation are the caller's responsibility. +pub fn book_hash(canonical_book: &[u8]) -> Digest { + hash_parts(b"ffor/book", &[canonical_book]) +} + +/// Bind the current commitment numbers and both transaction ids. +/// +/// Transaction ids must be in internal byte order, the reverse of display hex. The caller +/// must obtain them from the actual signed, fully committed voucher transactions. +pub fn commitment_hash( + receiver_number: u64, receiver_txid_internal: &[u8; 32], settlement_number: u64, + settlement_txid_internal: &[u8; 32], +) -> Digest { + hash_parts( + b"ffor/commit", + &[ + &receiver_number.to_be_bytes(), + receiver_txid_internal, + &settlement_number.to_be_bytes(), + settlement_txid_internal, + ], + ) +} + +/// Bind setup, voucher book, both commitments and the agreed activation height. +/// +/// A matching digest does not establish durable ACTIVE state or invoice readiness. +pub fn activation_hash( + setup: &Digest, book: &Digest, commitment: &Digest, start_height: u32, +) -> Digest { + hash_parts(b"ffor/activate", &[setup, book, commitment, &start_height.to_be_bytes()]) +} diff --git a/lightning-ffor/src/wire/codec.rs b/lightning-ffor/src/wire/codec.rs new file mode 100644 index 00000000000..032c802b6b0 --- /dev/null +++ b/lightning-ffor/src/wire/codec.rs @@ -0,0 +1,83 @@ +use alloc::vec::Vec; + +use bitcoin::secp256k1::ecdsa::Signature; + +use super::tlv::{check_extensions, read_tlvs, write_tlvs, Reader, Writer}; +use super::{lifecycle, setup, Header, Message, Payload, WireError, MAX_MESSAGE_LEN}; + +pub(super) fn decode(wire: &[u8]) -> Result { + if wire.len() > MAX_MESSAGE_LEN { + return Err(WireError::SizeLimit); + } + if wire.len() < 2 + 64 + 64 { + return Err(WireError::Truncated); + } + let signature_start = wire.len() - 64; + let signature = wire[signature_start..].try_into().map_err(|_| WireError::Truncated)?; + validate_signature_encoding(&signature)?; + let mut reader = Reader::new(&wire[..signature_start]); + let kind = reader.u16()?; + let header = Header { channel_id: reader.array()?, epoch_id: reader.array()? }; + let (payload, extensions) = match kind { + 55001 => setup::read_init(&mut reader)?, + 55003 => setup::read_accept(&mut reader)?, + 55045 | 55047 | 55049 | 55051 | 55053 => lifecycle::read(kind, &mut reader)?, + _ => return Err(WireError::UnsupportedMessage(kind)), + }; + check_extensions(&extensions)?; + Ok(Message { header, payload, extensions, signature }) +} + +pub(super) fn encode(message: &Message) -> Result, WireError> { + validate_signature_encoding(&message.signature)?; + let mut writer = Writer(encode_unsigned(message)?); + writer.put(&message.signature)?; + Ok(writer.0) +} + +pub(super) fn encode_unsigned(message: &Message) -> Result, WireError> { + check_extensions(&message.extensions)?; + let mut writer = Writer::new(); + writer.u16(message.message_type())?; + writer.put(&message.header.channel_id)?; + writer.put(&message.header.epoch_id)?; + let known = match &message.payload { + Payload::Init(init) => setup::write_init(init, &mut writer)?, + Payload::Accept(accept) => setup::write_accept(accept, &mut writer)?, + payload => lifecycle::write(payload, &mut writer)?, + }; + if matches!(message.payload, Payload::Init(_)) { + for tlv in &message.extensions { + if matches!(tlv.kind, 1 | 3 | 5 | 9 | 13 | 15) { + return Err(WireError::InvalidTlv(tlv.kind)); + } + } + } + if matches!(message.payload, Payload::CloseAck(_)) + && message.extensions.iter().any(|tlv| tlv.kind == 1) + { + return Err(WireError::InvalidTlv(1)); + } + write_tlvs(&mut writer, known, &message.extensions)?; + if writer.0.len() > MAX_MESSAGE_LEN - 64 { + return Err(WireError::SizeLimit); + } + Ok(writer.0) +} + +fn validate_signature_encoding(bytes: &[u8; 64]) -> Result<(), WireError> { + if bytes[..32].iter().all(|byte| *byte == 0) || bytes[32..].iter().all(|byte| *byte == 0) { + return Err(WireError::InvalidSignature); + } + let signature = Signature::from_compact(bytes).map_err(|_| WireError::InvalidSignature)?; + let mut normalized = signature; + normalized.normalize_s(); + if normalized != signature { + return Err(WireError::InvalidSignature); + } + Ok(()) +} + +pub(super) fn remaining_tlvs(reader: &mut Reader<'_>) -> Result, WireError> { + read_tlvs(reader) +} diff --git a/lightning-ffor/src/wire/lifecycle.rs b/lightning-ffor/src/wire/lifecycle.rs new file mode 100644 index 00000000000..d4c92f570b2 --- /dev/null +++ b/lightning-ffor/src/wire/lifecycle.rs @@ -0,0 +1,122 @@ +use alloc::vec; +use alloc::vec::Vec; + +use super::codec::remaining_tlvs; +use super::setup::check_count; +use super::tlv::{optional, Reader, Writer}; +use super::{Abort, Activate, CloseAck, Payload, Preimage, Tlv, WireError, MAX_MESSAGE_LEN}; + +pub(super) fn read(kind: u16, reader: &mut Reader<'_>) -> Result<(Payload, Vec), WireError> { + let payload = match kind { + 55045 => Payload::Activate(Activate { + setup_hash: reader.array()?, + book_hash: reader.array()?, + commit_hash: reader.array()?, + epoch_start_height: reader.u32()?, + }), + 55047 => Payload::ActivateAck(reader.array()?), + 55049 => { + let transcript_hash = reader.array()?; + let reason = reader.u16()?; + if reason > 7 { + return Err(WireError::InvalidField); + } + let data_len = usize::from(reader.u16()?); + Payload::Abort(Abort { transcript_hash, reason, data: reader.take(data_len)?.to_vec() }) + }, + 55051 => Payload::Close(reader.array()?), + 55053 => return read_close_ack(reader), + _ => return Err(WireError::UnsupportedMessage(kind)), + }; + Ok((payload, remaining_tlvs(reader)?)) +} + +fn read_close_ack(reader: &mut Reader<'_>) -> Result<(Payload, Vec), WireError> { + let activation_hash = reader.array()?; + let num_slots = reader.u16()?; + check_count(usize::from(num_slots))?; + let settled = reader.take((usize::from(num_slots) + 7) / 8)?.to_vec(); + let mut tlvs = remaining_tlvs(reader)?; + let value = optional(&mut tlvs, 1); + let preimages_tlv_present = value.is_some(); + let bytes = value.unwrap_or_default(); + if bytes.len() % 34 != 0 || bytes.len() / 34 > usize::from(num_slots) { + return Err(WireError::InvalidTlv(1)); + } + let mut preimages = Vec::new(); + let mut records = Reader::new(&bytes); + while !records.is_empty() { + preimages.push(Preimage { slot: records.u16()?, value: records.array()? }); + } + let ack = CloseAck { activation_hash, num_slots, settled, preimages, preimages_tlv_present }; + validate_close_ack(&ack)?; + Ok((Payload::CloseAck(ack), tlvs)) +} + +pub(super) fn write(payload: &Payload, writer: &mut Writer) -> Result, WireError> { + match payload { + Payload::Activate(activate) => { + writer.put(&activate.setup_hash)?; + writer.put(&activate.book_hash)?; + writer.put(&activate.commit_hash)?; + writer.u32(activate.epoch_start_height)?; + }, + Payload::ActivateAck(hash) | Payload::Close(hash) => writer.put(hash)?, + Payload::Abort(abort) => { + if abort.reason > 7 { + return Err(WireError::InvalidField); + } + if abort.data.len() > MAX_MESSAGE_LEN { + return Err(WireError::SizeLimit); + } + writer.put(&abort.transcript_hash)?; + writer.u16(abort.reason)?; + writer.u16(abort.data.len() as u16)?; + writer.put(&abort.data)?; + }, + Payload::CloseAck(ack) => return write_close_ack(ack, writer), + _ => return Err(WireError::InvalidField), + } + Ok(Vec::new()) +} + +fn write_close_ack(ack: &CloseAck, writer: &mut Writer) -> Result, WireError> { + validate_close_ack(ack)?; + writer.put(&ack.activation_hash)?; + writer.u16(ack.num_slots)?; + writer.put(&ack.settled)?; + let mut preimages = Writer::new(); + for record in &ack.preimages { + preimages.u16(record.slot)?; + preimages.put(&record.value)?; + } + Ok(if ack.preimages_tlv_present { + vec![Tlv { kind: 1, value: preimages.0 }] + } else { + Vec::new() + }) +} + +fn validate_close_ack(ack: &CloseAck) -> Result<(), WireError> { + let count = usize::from(ack.num_slots); + check_count(count)?; + if ack.settled.len() != (count + 7) / 8 { + return Err(WireError::InvalidField); + } + // Bits outside the book cannot describe slots and must not create alternate encodings. + let used_bits = count % 8; + if used_bits != 0 && ack.settled[count / 8] >> used_bits != 0 { + return Err(WireError::InvalidField); + } + let expected = (1..=ack.num_slots).filter(|slot| { + let bit = usize::from(slot - 1); + ack.settled[bit / 8] & (1 << (bit % 8)) != 0 + }); + if !expected.eq(ack.preimages.iter().map(|record| record.slot)) { + return Err(WireError::InvalidTlv(1)); + } + if !ack.preimages_tlv_present && !ack.preimages.is_empty() { + return Err(WireError::MissingTlv(1)); + } + Ok(()) +} diff --git a/lightning-ffor/src/wire/mod.rs b/lightning-ffor/src/wire/mod.rs new file mode 100644 index 00000000000..91504fb9aae --- /dev/null +++ b/lightning-ffor/src/wire/mod.rs @@ -0,0 +1,171 @@ +//! Bounded canonical encodings for the signed Variant D setup and lifecycle messages. +//! +//! Peer bytes are untrusted. Decoding checks structure, low-S signature encoding and +//! message-local invariants, but does not authenticate the sender or establish channel +//! readiness. Call [`Message::verify_signature`] with the expected channel peer before +//! acting, then validate the transcript and live channel state. Unknown odd TLVs are +//! retained byte-for-byte; unknown even TLVs are rejected. No signing keys are accepted. +//! +//! ``` +//! use lightning_ffor::wire::{Message, WireError}; +//! assert_eq!(Message::decode(&[]), Err(WireError::Truncated)); +//! ``` + +mod codec; +mod lifecycle; +mod setup; +mod tlv; +mod types; + +use alloc::vec::Vec; + +use core::fmt; + +use bitcoin::secp256k1::PublicKey; +pub use types::{Abort, Accept, Activate, CloseAck, Header, Init, Payload, Preimage, Tlv}; + +use crate::transcript::{self, Digest}; + +/// BOLT 8 maximum plaintext peer message, including its two-byte type. +pub const MAX_MESSAGE_LEN: usize = 65_535; +/// Maximum number of TLVs that can fit a peer message even with empty values. +pub const MAX_TLV_COUNT: usize = MAX_MESSAGE_LEN / 2; + +/// Structural or authentication failure. No error authorizes a channel transition. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WireError { + /// The message, field, or BigSize value ended early. + Truncated, + /// A message or bounded collection exceeds its protocol limit. + SizeLimit, + /// This message type is not implemented by this Variant D codec. + UnsupportedMessage(u16), + /// The init selected a variant other than D. + UnsupportedVariant(u8), + /// A BigSize value used a longer-than-minimal representation. + NonCanonicalBigSize, + /// TLV types were duplicated or not strictly increasing. + TlvOrder, + /// An unknown mandatory even TLV appeared. + UnknownEvenTlv(u64), + /// A required known TLV is absent. + MissingTlv(u64), + /// A known TLV has an invalid value length or is forbidden for this message. + InvalidTlv(u64), + /// A fixed field or count violates the message's local invariants. + InvalidField, + /// A compressed secp256k1 point is invalid or is not compressed. + InvalidPoint, + /// The compact signature is malformed, high-S, or fails authentication. + InvalidSignature, + /// An accept does not bind the supplied init's header, amounts, or transcript. + SetupMismatch, +} + +impl fmt::Display for WireError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid FFOR wire message: {self:?}") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for WireError {} + +/// A parsed signed envelope. Public fields remain untrusted until signature verification. +/// +/// Encoding revalidates all fields and produces canonical bytes. It never signs or repairs +/// a signature: changing any header, payload or extension requires a new external signature. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Message { + /// Channel and epoch to which the signed message belongs. + pub header: Header, + /// Known message fields and known TLVs. + pub payload: Payload, + /// Unknown odd TLVs, in strictly increasing type order. + pub extensions: Vec, + /// Unverified compact low-S ECDSA signature in the final 64 wire bytes. + pub signature: [u8; 64], +} + +impl Message { + /// Decode one complete peer message with no trailing framing bytes. + /// + /// All input-dependent allocation is bounded by [`MAX_MESSAGE_LEN`]. Successful + /// decoding does not verify the signature, live limits, or lifecycle ordering. + pub fn decode(wire: &[u8]) -> Result { + codec::decode(wire) + } + + /// Encode canonical wire bytes after rechecking message-local invariants. + /// + /// An existing signature is retained unchanged, even if fields were modified. Call + /// [`Self::verify_signature`] before transmitting or acting on a reconstructed message. + pub fn encode(&self) -> Result, WireError> { + codec::encode(self) + } + + /// The assigned two-byte message type, without authenticating the message. + pub fn message_type(&self) -> u16 { + match self.payload { + Payload::Init(_) => 55001, + Payload::Accept(_) => 55003, + Payload::Activate(_) => 55045, + Payload::ActivateAck(_) => 55047, + Payload::Abort(_) => 55049, + Payload::Close(_) => 55051, + Payload::CloseAck(_) => 55053, + } + } + + /// The existing transcript module's domain-separated digest over the unsigned body. + /// + /// The signature field is intentionally ignored so the channel's existing signer can + /// sign newly constructed terms. All unsigned fields and the final envelope size are checked. + pub fn signature_digest(&self) -> Result { + let wire = self.unsigned_wire()?; + Ok(transcript::message_digest(self.message_type(), &wire[2..])) + } + + /// Canonical type and unsigned body for an existing node signer to inspect and sign. + /// + /// The final 64-byte signature is excluded and the current signature field is ignored. All + /// unsigned fields and the completed envelope size are checked. This exposes public message + /// bytes, never signing keys, and does not authorize the proposed channel transition. + pub fn unsigned_wire(&self) -> Result, WireError> { + codec::encode_unsigned(self) + } + + /// Authenticate the canonical envelope with the expected channel peer's node key. + /// + /// This proves only the signed bytes, not the correctness of the peer's claims. + pub fn verify_signature(&self, expected_signer: &PublicKey) -> Result<(), WireError> { + let wire = self.encode()?; + transcript::verify_message_signature( + self.message_type(), + &wire[2..wire.len() - 64], + &self.signature, + expected_signer, + ) + .map_err(|_| WireError::InvalidSignature) + } + + /// Check this accept against the exact canonical signed init it answers. + /// + /// Verify both signatures first. Live channel capacity, negotiated limits, deadline + /// margins and previously disclosed commitment secrets remain the engine's checks. + pub fn validate_accept(&self, init: &Message) -> Result<(), WireError> { + let (accept, terms) = match (&self.payload, &init.payload) { + (Payload::Accept(accept), Payload::Init(terms)) => (accept, terms), + _ => return Err(WireError::SetupMismatch), + }; + self.encode()?; + let init_wire = init.encode()?; + if self.header != init.header + || accept.amounts_msat != terms.amounts_msat + || accept.init_hash != transcript::init_hash(&init_wire) + { + return Err(WireError::SetupMismatch); + } + Ok(()) + } +} diff --git a/lightning-ffor/src/wire/setup.rs b/lightning-ffor/src/wire/setup.rs new file mode 100644 index 00000000000..6da8f04e9e6 --- /dev/null +++ b/lightning-ffor/src/wire/setup.rs @@ -0,0 +1,195 @@ +use alloc::vec; +use alloc::vec::Vec; + +use bitcoin::secp256k1::PublicKey; + +use super::codec::remaining_tlvs; +use super::tlv::{fixed, optional, required, Reader, Writer}; +use super::{Accept, Init, Payload, Tlv, WireError, MAX_MESSAGE_LEN}; +use crate::book::MAX_VOUCHERS; + +pub(super) fn read_init(reader: &mut Reader<'_>) -> Result<(Payload, Vec), WireError> { + let variant = reader.u8()?; + if variant != 4 { + return Err(WireError::UnsupportedVariant(variant)); + } + let budget_msat = reader.u64()?; + let count = usize::from(reader.u16()?); + check_count(count)?; + let min_payment_msat = reader.u64()?; + let settlement_deadline = reader.u32()?; + let voucher_expiry = reader.u32()?; + let fee_base_msat = reader.u32()?; + let fee_proportional_millionths = reader.u32()?; + if reader.u64()? != 0 || reader.u16()? != 0 { + return Err(WireError::InvalidField); + } + let mut tlvs = remaining_tlvs(reader)?; + if let Some(forbidden) = tlvs.iter().find(|entry| matches!(entry.kind, 1 | 3 | 5)) { + return Err(WireError::InvalidTlv(forbidden.kind)); + } + let amounts_msat = read_amounts(required(&mut tlvs, 9)?)?; + if amounts_msat.len() != count { + return Err(WireError::InvalidTlv(9)); + } + let witness_peers = optional(&mut tlvs, 13).map(read_points).transpose()?; + let hash_chain = match optional(&mut tlvs, 15) { + None => false, + Some(value) if value == [1] => true, + Some(_) => return Err(WireError::InvalidTlv(15)), + }; + let init = Init { + budget_msat, + min_payment_msat, + settlement_deadline, + voucher_expiry, + fee_base_msat, + fee_proportional_millionths, + amounts_msat, + witness_peers, + hash_chain, + }; + validate_init(&init)?; + Ok((Payload::Init(init), tlvs)) +} + +pub(super) fn read_accept(reader: &mut Reader<'_>) -> Result<(Payload, Vec), WireError> { + let s_commitment_number = reader.u64()?; + let mut tlvs = remaining_tlvs(reader)?; + let hashes = required(&mut tlvs, 1)?; + if hashes.len() % 32 != 0 { + return Err(WireError::InvalidTlv(1)); + } + check_count(hashes.len() / 32)?; + let payment_hashes = hashes + .chunks_exact(32) + .map(|hash| hash.try_into().map_err(|_| WireError::InvalidTlv(1))) + .collect::>()?; + let s_htlc_id_base = u64::from_be_bytes(fixed(required(&mut tlvs, 7)?, 7)?); + let amounts_msat = read_amounts(required(&mut tlvs, 9)?)?; + let init_hash = fixed(required(&mut tlvs, 11)?, 11)?; + let accept = + Accept { s_commitment_number, payment_hashes, s_htlc_id_base, amounts_msat, init_hash }; + validate_accept(&accept)?; + Ok((Payload::Accept(accept), tlvs)) +} + +pub(super) fn write_init(init: &Init, writer: &mut Writer) -> Result, WireError> { + validate_init(init)?; + writer.put(&[4])?; + writer.u64(init.budget_msat)?; + writer.u16(init.amounts_msat.len() as u16)?; + writer.u64(init.min_payment_msat)?; + writer.u32(init.settlement_deadline)?; + writer.u32(init.voucher_expiry)?; + writer.u32(init.fee_base_msat)?; + writer.u32(init.fee_proportional_millionths)?; + writer.u64(0)?; + writer.u16(0)?; + let mut tlvs = vec![Tlv { kind: 9, value: amount_bytes(&init.amounts_msat) }]; + if let Some(peers) = &init.witness_peers { + if peers.len() > MAX_MESSAGE_LEN / 33 { + return Err(WireError::SizeLimit); + } + let mut points = Writer::new(); + points.u16(peers.len() as u16)?; + for peer in peers { + points.put(&peer.serialize())?; + } + tlvs.push(Tlv { kind: 13, value: points.0 }); + } + if init.hash_chain { + tlvs.push(Tlv { kind: 15, value: vec![1] }); + } + Ok(tlvs) +} + +pub(super) fn write_accept(accept: &Accept, writer: &mut Writer) -> Result, WireError> { + validate_accept(accept)?; + writer.u64(accept.s_commitment_number)?; + Ok(vec![ + Tlv { kind: 1, value: accept.payment_hashes.concat() }, + Tlv { kind: 7, value: accept.s_htlc_id_base.to_be_bytes().to_vec() }, + Tlv { kind: 9, value: amount_bytes(&accept.amounts_msat) }, + Tlv { kind: 11, value: accept.init_hash.to_vec() }, + ]) +} + +fn validate_init(init: &Init) -> Result<(), WireError> { + check_count(init.amounts_msat.len())?; + if init.voucher_expiry <= init.settlement_deadline { + return Err(WireError::InvalidField); + } + let mut total = 0u64; + for &amount in &init.amounts_msat { + if amount == 0 || amount < init.min_payment_msat { + return Err(WireError::InvalidTlv(9)); + } + if init.hash_chain && amount != init.amounts_msat[0] { + return Err(WireError::InvalidTlv(15)); + } + total = total.checked_add(amount).ok_or(WireError::InvalidField)?; + } + if total != init.budget_msat { + return Err(WireError::InvalidField); + } + Ok(()) +} + +fn validate_accept(accept: &Accept) -> Result<(), WireError> { + check_count(accept.payment_hashes.len())?; + if accept.amounts_msat.len() != accept.payment_hashes.len() || accept.amounts_msat.contains(&0) + { + return Err(WireError::InvalidTlv(9)); + } + accept + .s_htlc_id_base + .checked_add(accept.payment_hashes.len() as u64 - 1) + .ok_or(WireError::InvalidTlv(7))?; + Ok(()) +} + +pub(super) fn check_count(count: usize) -> Result<(), WireError> { + if count == 0 || count > MAX_VOUCHERS { + return Err(WireError::InvalidField); + } + Ok(()) +} + +fn read_amounts(bytes: Vec) -> Result, WireError> { + if bytes.len() % 8 != 0 { + return Err(WireError::InvalidTlv(9)); + } + check_count(bytes.len() / 8)?; + bytes + .chunks_exact(8) + .map(|amount| { + Ok(u64::from_be_bytes(amount.try_into().map_err(|_| WireError::InvalidTlv(9))?)) + }) + .collect() +} + +fn amount_bytes(amounts: &[u64]) -> Vec { + amounts.iter().flat_map(|amount| amount.to_be_bytes()).collect() +} + +fn read_points(bytes: Vec) -> Result, WireError> { + let mut reader = Reader::new(&bytes); + let count = usize::from(reader.u16()?); + if count > MAX_MESSAGE_LEN / 33 { + return Err(WireError::SizeLimit); + } + let mut peers = Vec::new(); + for _ in 0..count { + let bytes = reader.take(33)?; + let point = PublicKey::from_slice(bytes).map_err(|_| WireError::InvalidPoint)?; + if point.serialize() != bytes { + return Err(WireError::InvalidPoint); + } + peers.push(point); + } + if !reader.is_empty() { + return Err(WireError::InvalidTlv(13)); + } + Ok(peers) +} diff --git a/lightning-ffor/src/wire/tlv.rs b/lightning-ffor/src/wire/tlv.rs new file mode 100644 index 00000000000..e74add02eab --- /dev/null +++ b/lightning-ffor/src/wire/tlv.rs @@ -0,0 +1,170 @@ +use alloc::vec::Vec; + +use super::{Tlv, WireError, MAX_MESSAGE_LEN, MAX_TLV_COUNT}; + +pub(super) struct Reader<'a> { + remaining: &'a [u8], +} + +impl<'a> Reader<'a> { + pub(super) fn new(bytes: &'a [u8]) -> Self { + Self { remaining: bytes } + } + + pub(super) fn take(&mut self, size: usize) -> Result<&'a [u8], WireError> { + let value = self.remaining.get(..size).ok_or(WireError::Truncated)?; + self.remaining = &self.remaining[size..]; + Ok(value) + } + + pub(super) fn array(&mut self) -> Result<[u8; N], WireError> { + self.take(N)?.try_into().map_err(|_| WireError::Truncated) + } + + pub(super) fn u8(&mut self) -> Result { + Ok(self.array::<1>()?[0]) + } + pub(super) fn u16(&mut self) -> Result { + Ok(u16::from_be_bytes(self.array()?)) + } + pub(super) fn u32(&mut self) -> Result { + Ok(u32::from_be_bytes(self.array()?)) + } + pub(super) fn u64(&mut self) -> Result { + Ok(u64::from_be_bytes(self.array()?)) + } + pub(super) fn is_empty(&self) -> bool { + self.remaining.is_empty() + } + + fn bigsize(&mut self) -> Result { + let (value, minimum) = match self.u8()? { + 0xfd => (u64::from(self.u16()?), 0xfd), + 0xfe => (u64::from(self.u32()?), 0x1_0000), + 0xff => (self.u64()?, 0x1_0000_0000), + value => return Ok(u64::from(value)), + }; + if value < minimum { + return Err(WireError::NonCanonicalBigSize); + } + Ok(value) + } +} + +pub(super) fn read_tlvs(reader: &mut Reader<'_>) -> Result, WireError> { + let mut entries = Vec::new(); + let mut last = None; + while !reader.is_empty() { + let kind = reader.bigsize()?; + if last.map_or(false, |previous| kind <= previous) { + return Err(WireError::TlvOrder); + } + let len = usize::try_from(reader.bigsize()?).map_err(|_| WireError::SizeLimit)?; + if len > MAX_MESSAGE_LEN || entries.len() >= MAX_TLV_COUNT { + return Err(WireError::SizeLimit); + } + let value = reader.take(len)?.to_vec(); + entries.push(Tlv { kind, value }); + last = Some(kind); + } + Ok(entries) +} + +pub(super) fn required(entries: &mut Vec, kind: u64) -> Result, WireError> { + optional(entries, kind).ok_or(WireError::MissingTlv(kind)) +} + +pub(super) fn optional(entries: &mut Vec, kind: u64) -> Option> { + let position = entries.iter().position(|entry| entry.kind == kind)?; + Some(entries.remove(position).value) +} + +pub(super) fn check_extensions(entries: &[Tlv]) -> Result<(), WireError> { + if entries.len() > MAX_TLV_COUNT { + return Err(WireError::SizeLimit); + } + let mut previous = None; + let mut total = 0usize; + for entry in entries { + if previous.map_or(false, |last| entry.kind <= last) { + return Err(WireError::TlvOrder); + } + if entry.kind % 2 == 0 { + return Err(WireError::UnknownEvenTlv(entry.kind)); + } + total = total + .checked_add(entry.value.len()) + .and_then(|n| n.checked_add(2)) + .ok_or(WireError::SizeLimit)?; + if total > MAX_MESSAGE_LEN { + return Err(WireError::SizeLimit); + } + previous = Some(entry.kind); + } + Ok(()) +} + +pub(super) struct Writer(pub(super) Vec); + +impl Writer { + pub(super) fn new() -> Self { + Self(Vec::new()) + } + pub(super) fn put(&mut self, bytes: &[u8]) -> Result<(), WireError> { + if bytes.len() > MAX_MESSAGE_LEN - self.0.len() { + return Err(WireError::SizeLimit); + } + self.0.extend_from_slice(bytes); + Ok(()) + } + pub(super) fn u16(&mut self, value: u16) -> Result<(), WireError> { + self.put(&value.to_be_bytes()) + } + pub(super) fn u32(&mut self, value: u32) -> Result<(), WireError> { + self.put(&value.to_be_bytes()) + } + pub(super) fn u64(&mut self, value: u64) -> Result<(), WireError> { + self.put(&value.to_be_bytes()) + } + + fn bigsize(&mut self, value: u64) -> Result<(), WireError> { + match value { + 0..=0xfc => self.put(&[value as u8]), + 0xfd..=0xffff => { + self.put(&[0xfd])?; + self.u16(value as u16) + }, + 0x1_0000..=0xffff_ffff => { + self.put(&[0xfe])?; + self.u32(value as u32) + }, + _ => { + self.put(&[0xff])?; + self.u64(value) + }, + } + } +} + +pub(super) fn write_tlvs( + writer: &mut Writer, known: Vec, extensions: &[Tlv], +) -> Result<(), WireError> { + check_extensions(extensions)?; + for extension in extensions { + if known.iter().any(|tlv| tlv.kind == extension.kind) { + return Err(WireError::InvalidTlv(extension.kind)); + } + } + let mut entries: Vec<_> = known.iter().chain(extensions).collect(); + entries.sort_by_key(|entry| entry.kind); + for entry in entries { + writer.bigsize(entry.kind)?; + writer.bigsize(entry.value.len() as u64)?; + writer.put(&entry.value)?; + } + Ok(()) +} + +pub(super) fn fixed(value: Vec, kind: u64) -> Result<[u8; N], WireError> { + value.try_into().map_err(|_| WireError::InvalidTlv(kind)) +} diff --git a/lightning-ffor/src/wire/types.rs b/lightning-ffor/src/wire/types.rs new file mode 100644 index 00000000000..5828f41e83e --- /dev/null +++ b/lightning-ffor/src/wire/types.rs @@ -0,0 +1,128 @@ +use alloc::vec::Vec; + +use bitcoin::secp256k1::PublicKey; + +use crate::transcript::Digest; + +/// Identifiers signed by every supported message; neither is a trusted channel lookup key. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Header { + /// BOLT channel identifier. + pub channel_id: [u8; 32], + /// Receiver-selected epoch identifier, whose uniqueness the engine must enforce. + pub epoch_id: [u8; 32], +} + +/// An unknown optional extension retained in the signed canonical TLV stream. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Tlv { + /// Odd type number; duplicate or known types cannot be supplied as extensions. + pub kind: u64, + /// Exact extension bytes, bounded by the peer message limit. + pub value: Vec, +} + +/// Signed Variant D init terms. G and receiver point count are fixed to zero on wire. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Init { + /// Exact sum of the proposed voucher amounts, excluding forwarding fees. + pub budget_msat: u64, + /// Minimum permitted amount for every voucher. + pub min_payment_msat: u64, + /// Absolute height after which new settlement is forbidden. + pub settlement_deadline: u32, + /// Uniform voucher HTLC expiry height, strictly after the deadline. + pub voucher_expiry: u32, + /// Proposed base forwarding fee in millisatoshis. + pub fee_base_msat: u32, + /// Proposed proportional forwarding fee in millionths. + pub fee_proportional_millionths: u32, + /// Required TLV 9 amounts in slot order; length encodes K, from 1 to 483. + pub amounts_msat: Vec, + /// Optional TLV 13 witness peer restriction, preserving peer order. + pub witness_peers: Option>, + /// TLV 15 requests uniform hash-chained vouchers; chain verification needs the accept. + pub hash_chain: bool, +} + +/// Signed accept fields. Cross-message agreement is checked by `Message::validate_accept`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Accept { + /// Peer commitment number before the voucher round. + pub s_commitment_number: u64, + /// Required TLV 1 payment hashes in slot order. + pub payment_hashes: Vec<[u8; 32]>, + /// Required TLV 7 first voucher HTLC id. + pub s_htlc_id_base: u64, + /// Required TLV 9 exact amounts, with one amount per payment hash. + pub amounts_msat: Vec, + /// Required TLV 11 digest of the exact signed init wire bytes. + pub init_hash: Digest, +} + +/// Receiver's proposed activation commitments, still requiring live engine validation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Activate { + /// T_setup binding the init and accept. + pub setup_hash: Digest, + /// H_book for the canonical slot book. + pub book_hash: Digest, + /// H_commit for the real committed transaction pair. + pub commit_hash: Digest, + /// Receiver's activation height; the peer must check the six-block agreement rule. + pub epoch_start_height: u32, +} + +/// Signed pre-activation abort request; lifecycle permission is an engine concern. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Abort { + /// T_setup if accept was exchanged, otherwise T_init. + pub transcript_hash: Digest, + /// Normative reason code from 0 through 7. + pub reason: u16, + /// Bounded uninterpreted text or evidence; not assumed to be UTF-8. + pub data: Vec, +} + +/// A slot-numbered preimage in a Variant D close acknowledgement. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Preimage { + /// One-based slot index, in ascending order in the preimage list. + pub slot: u16, + /// Preimage whose SHA256 must match the corresponding authenticated book entry. + pub value: [u8; 32], +} + +/// Signed peer close report. Bitmap padding must be zero and every set bit has a preimage. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CloseAck { + /// H_act of the active epoch. + pub activation_hash: Digest, + /// K from the agreed book, checked against that book by the engine. + pub num_slots: u16, + /// LSB-first settled bitmap, exactly ceil(K/8) bytes. + pub settled: Vec, + /// Exactly one record per set bit in TLV 1; missing is permitted only when no slots settled. + pub preimages: Vec, + /// Preserve explicit empty TLV 1 versus absence when the bitmap is empty. + pub preimages_tlv_present: bool, +} + +/// Supported Variant D signed messages; undefined and unsupported types are rejected. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Payload { + /// ff_init, type 55001. + Init(Init), + /// ff_accept, type 55003. + Accept(Accept), + /// ff_activate, type 55045. + Activate(Activate), + /// ff_activate_ack, type 55047, carrying H_act. + ActivateAck(Digest), + /// ff_abort, type 55049. + Abort(Abort), + /// ff_close, type 55051, carrying H_act. + Close(Digest), + /// ff_close_ack, type 55053. + CloseAck(CloseAck), +} diff --git a/lightning-ffor/tests/amounts.rs b/lightning-ffor/tests/amounts.rs new file mode 100644 index 00000000000..a6cb8020dc7 --- /dev/null +++ b/lightning-ffor/tests/amounts.rs @@ -0,0 +1,127 @@ +use lightning_ffor::amounts::{check_plaintext_amount, AmountError, FeePolicy}; +use proptest::prelude::*; + +#[test] +fn appendix_amounts_preserve_invoice_value() { + let fees = FeePolicy { base_msat: 1000, proportional_millionths: 5000 }; + for (amount, fee, gross) in [ + (1_000_000, 6000, 1_006_000), + (994_000, 5970, 999_970), + (546_250, 3731, 549_981), + (49_749_000, 249_745, 49_998_745), + (546_000, 3730, 549_730), + ] { + assert_eq!(fees.fee_msat(amount), Ok(fee)); + assert_eq!(fees.gross_msat(amount), Ok(gross)); + assert_eq!(check_plaintext_amount(amount, amount, gross, fees, None), Ok(())); + assert_eq!( + check_plaintext_amount(amount, amount, gross - 1, fees, None), + Err(AmountError::InsufficientFee) + ); + } +} + +#[test] +fn rejects_overflow_before_division_and_at_gross_sum() { + let product = FeePolicy { base_msat: 0, proportional_millionths: 2 }; + assert_eq!(product.fee_msat(u64::MAX / 2 + 1), Err(AmountError::Overflow)); + assert!(product.fee_msat(u64::MAX / 2).is_ok()); + let base = FeePolicy { base_msat: 1, proportional_millionths: 0 }; + assert_eq!(base.gross_msat(u64::MAX), Err(AmountError::Overflow)); + assert_eq!(FeePolicy::default().gross_msat(u64::MAX), Ok(u64::MAX)); +} + +#[test] +fn public_policy_compares_complete_fees() { + let book = FeePolicy { base_msat: 1000, proportional_millionths: 0 }; + let public = FeePolicy { base_msat: 0, proportional_millionths: 2000 }; + assert_eq!( + check_plaintext_amount(1_000_000, 1_000_000, 1_000_999, book, Some(public)), + Err(AmountError::InsufficientFee) + ); + assert_eq!(check_plaintext_amount(1_000_000, 1_000_000, 1_001_000, book, Some(public)), Ok(())); + assert_eq!( + check_plaintext_amount(1_000_000, 1_000_000, 1_000_000, book, Some(FeePolicy::default())), + Ok(()) + ); + assert_eq!( + check_plaintext_amount(1_000_000, 1_000_000, 1_000_000, book, None), + Err(AmountError::InsufficientFee) + ); +} + +#[test] +fn public_policy_increase_cannot_disable_book_pricing() { + let amount = u64::MAX / 2; + let public = FeePolicy { base_msat: u32::MAX, proportional_millionths: u32::MAX }; + assert_eq!( + check_plaintext_amount(amount, amount, amount, FeePolicy::default(), Some(public)), + Ok(()) + ); +} + +#[test] +fn cheaper_public_policy_can_have_product_above_book_setup_bound() { + let book = FeePolicy { base_msat: 5000, proportional_millionths: u32::MAX - 1 }; + let public = FeePolicy { base_msat: 0, proportional_millionths: u32::MAX }; + let amount = 4_294_967_298; + let public_fee = 18_446_744_078_004; + assert_eq!( + check_plaintext_amount(amount, amount, amount + public_fee, book, Some(public)), + Ok(()) + ); + assert_eq!( + check_plaintext_amount(amount, amount, amount + public_fee - 1, book, Some(public)), + Err(AmountError::InsufficientFee) + ); +} + +#[test] +fn rejects_mismatched_payee_and_incoming_underflow() { + for forward in [999, 1001] { + assert_eq!( + check_plaintext_amount(1000, forward, 2000, FeePolicy::default(), None), + Err(AmountError::WrongPayeeAmount) + ); + } + assert_eq!( + check_plaintext_amount(1000, 1000, 999, FeePolicy::default(), None), + Err(AmountError::InsufficientFee) + ); +} + +#[test] +fn public_discount_cannot_admit_invalid_signed_book_amounts() { + let fees = FeePolicy { base_msat: 1, proportional_millionths: 0 }; + assert_eq!( + check_plaintext_amount(u64::MAX, u64::MAX, u64::MAX, fees, Some(FeePolicy::default())), + Err(AmountError::Overflow) + ); + assert_eq!( + check_plaintext_amount(0, 0, 0, FeePolicy::default(), None), + Err(AmountError::ZeroAmount) + ); +} + +proptest! { + #[test] + fn fee_bounds_match_wide_integer_oracle(amount in any::(), base in any::(), ppm in any::()) { + let policy = FeePolicy { base_msat: base, proportional_millionths: ppm }; + let product = u128::from(amount) * u128::from(ppm); + let gross = u128::from(amount) + u128::from(base) + product / 1_000_000; + if product > u128::from(u64::MAX) || gross > u128::from(u64::MAX) { + prop_assert_eq!(policy.gross_msat(amount), Err(AmountError::Overflow)); + } else { + prop_assert_eq!(policy.gross_msat(amount), Ok(gross as u64)); + } + } + + #[test] + fn gross_payment_passes_and_payee_changes_fail(amount in 1_u64..1_000_000_000, base in any::(), ppm in 0_u32..1_000_000) { + let policy = FeePolicy { base_msat: base, proportional_millionths: ppm }; + let gross = policy.gross_msat(amount).unwrap(); + prop_assert_eq!(check_plaintext_amount(amount, amount, gross, policy, None), Ok(())); + prop_assert_eq!(check_plaintext_amount(amount, amount - 1, gross, policy, None), Err(AmountError::WrongPayeeAmount)); + prop_assert_eq!(check_plaintext_amount(amount, amount + 1, gross, policy, None), Err(AmountError::WrongPayeeAmount)); + } +} diff --git a/lightning-ffor/tests/book.rs b/lightning-ffor/tests/book.rs new file mode 100644 index 00000000000..56392d02487 --- /dev/null +++ b/lightning-ffor/tests/book.rs @@ -0,0 +1,151 @@ +use lightning_ffor::amounts::FeePolicy; +use lightning_ffor::book::{validate_anchor_book, AnchorChannelLimits, BookError, BookTerms}; +use proptest::prelude::*; + +fn terms(amounts_msat: Vec) -> BookTerms { + BookTerms { + budget_msat: amounts_msat.iter().sum(), + amounts_msat, + minimum_payment_msat: 546_000, + fees: FeePolicy { base_msat: 1000, proportional_millionths: 5000 }, + settlement_deadline: 798_992, + voucher_expiry: 800_000, + } +} + +fn channel() -> AnchorChannelLimits { + AnchorChannelLimits { + max_accepted_htlcs: 483, + max_in_flight_msat: 5_000_000_000, + htlc_minimum_msat: 1, + receiver_dust_sat: 546, + settlement_dust_sat: 546, + settlement_balance_msat: 7_000_000_000, + receiver_balance_msat: 3_000_000_000, + settlement_reserve_sat: 10_000, + receiver_reserve_sat: 10_000, + settlement_is_funder: true, + feerate_sat_per_kw: 2500, + } +} + +fn validate(book: &BookTerms, channel: &AnchorChannelLimits) -> Result<(), BookError> { + validate_anchor_book(book, channel, 790_000, 1008) +} + +#[test] +fn appendix_d_books_fit_both_funder_roles() { + for amounts in + [vec![1_000_000], vec![994_000, 546_250, 49_749_000], vec![546_000], vec![546_000; 483]] + { + for settlement_is_funder in [false, true] { + let channel = AnchorChannelLimits { settlement_is_funder, ..channel() }; + assert_eq!(validate(&terms(amounts.clone()), &channel), Ok(())); + } + } +} + +#[test] +fn unfunded_receiver_can_receive_in_settlement_funded_channel() { + let mut channel = channel(); + channel.receiver_balance_msat = 0; + assert_eq!(validate(&terms(vec![1_000_000]), &channel), Ok(())); + channel.settlement_is_funder = false; + assert_eq!(validate(&terms(vec![1_000_000]), &channel), Err(BookError::FeeReserve)); +} + +#[test] +fn requires_fee_spike_buffer_including_both_anchors() { + let book = terms(vec![1_000_000]); + let mut channel = channel(); + // D.1 fee at twice 2500 sat/kw is 6480 sat, plus 660 sat for anchors. + channel.settlement_balance_msat = book.budget_msat + (10_000 + 6480 + 660) * 1000; + assert_eq!(validate(&book, &channel), Ok(())); + channel.settlement_balance_msat -= 1; + assert_eq!(validate(&book, &channel), Err(BookError::FeeReserve)); + channel.settlement_balance_msat = book.budget_msat + channel.settlement_reserve_sat * 1000 - 1; + assert_eq!(validate(&book, &channel), Err(BookError::SettlementReserve)); + channel.settlement_balance_msat = book.budget_msat - 1; + assert_eq!(validate(&book, &channel), Err(BookError::SettlementReserve)); +} + +#[test] +fn validates_both_commitment_dust_limits() { + let book = terms(vec![546_999]); + let mut channel = channel(); + channel.receiver_dust_sat = 547; + assert_eq!(validate(&book, &channel), Err(BookError::Dust)); + channel.receiver_dust_sat = 546; + channel.settlement_dust_sat = 547; + assert_eq!(validate(&book, &channel), Err(BookError::Dust)); + channel.settlement_dust_sat = u64::MAX; + assert_eq!(validate(&book, &channel), Err(BookError::Dust)); +} + +#[test] +fn rejects_budget_and_channel_limit_violations() { + let mut book = terms(vec![1_000_000]); + let mut channel = channel(); + book.budget_msat += 1; + assert_eq!(validate(&book, &channel), Err(BookError::BudgetMismatch)); + book.budget_msat -= 1; + channel.max_in_flight_msat = book.budget_msat - 1; + assert_eq!(validate(&book, &channel), Err(BookError::InFlightLimit)); + channel.max_in_flight_msat = book.budget_msat; + channel.htlc_minimum_msat = book.budget_msat + 1; + assert_eq!(validate(&book, &channel), Err(BookError::MinimumAmount)); + channel.htlc_minimum_msat = 1; + book.minimum_payment_msat = book.budget_msat + 1; + assert_eq!(validate(&book, &channel), Err(BookError::MinimumAmount)); + book.minimum_payment_msat = 0; + book.amounts_msat = vec![0]; + assert_eq!(validate(&book, &channel), Err(BookError::MinimumAmount)); +} + +#[test] +fn bounds_slot_count_before_processing_amounts() { + for count in [0, 484] { + assert_eq!(validate(&terms(vec![546_000; count]), &channel()), Err(BookError::SlotCount)); + } + let channel = AnchorChannelLimits { max_accepted_htlcs: 1, ..channel() }; + assert_eq!(validate(&terms(vec![546_000; 2]), &channel), Err(BookError::SlotCount)); +} + +#[test] +fn rejects_expired_or_overflowing_deadlines() { + let mut book = terms(vec![1_000_000]); + book.settlement_deadline = 790_000; + assert_eq!(validate(&book, &channel()), Err(BookError::Deadline)); + book.settlement_deadline = u32::MAX; + assert_eq!(validate(&book, &channel()), Err(BookError::Deadline)); + book.settlement_deadline = 798_993; + assert_eq!(validate(&book, &channel()), Err(BookError::Deadline)); + assert_eq!( + validate_anchor_book(&terms(vec![1_000_000]), &channel(), 790_000, 0), + Err(BookError::Deadline) + ); +} + +#[test] +fn rejects_arithmetic_overflow_without_wrapping() { + let mut book = terms(vec![1_000_000]); + book.amounts_msat = vec![u64::MAX]; + assert_eq!(validate(&book, &channel()), Err(BookError::Overflow)); + book.fees = FeePolicy::default(); + book.amounts_msat = vec![u64::MAX, 546_000]; + assert_eq!(validate(&book, &channel()), Err(BookError::Overflow)); + let mut channel = channel(); + channel.settlement_is_funder = false; + channel.receiver_reserve_sat = u64::MAX; + assert_eq!(validate(&terms(vec![1_000_000]), &channel), Err(BookError::Overflow)); +} + +proptest! { + #[test] + fn valid_books_cannot_exceed_signed_budget(amounts in prop::collection::vec(546_000_u64..2_000_000, 1..100)) { + let mut book = terms(amounts); + prop_assert_eq!(validate(&book, &channel()), Ok(())); + book.budget_msat += 1; + prop_assert_eq!(validate(&book, &channel()), Err(BookError::BudgetMismatch)); + } +} diff --git a/lightning-ffor/tests/data/appendix-d.json b/lightning-ffor/tests/data/appendix-d.json new file mode 100644 index 00000000000..356ae81cb1e --- /dev/null +++ b/lightning-ffor/tests/data/appendix-d.json @@ -0,0 +1,92 @@ +[ + { + "scenario": "D.1", + "init_hash": "343331edc28e500ea03cbce26983583e66fff33dc9f096a3d6924b8239f68eb8", + "setup_hash": "ba34be8afe89e4457543a6e7279f86054f1d6e9038234b786b740e9c905c001f", + "book_hash": "8ae276dd460c7b2030287a608677bc51eb9cdef5b7cfc1f73d005e463076510a", + "commitment_hash": "723024696ad20b4758b228284620d18fd30750b7051cc64efca23a1417cf1321", + "activation_hash": "580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e716", + "init_wire": "d6d9bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489fc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff150400000000000f4240000100000000000854d0000c3110000c3500000003e80000138800000000000000000000090800000000000f4240b650a5af1658bafe768dc0d5b07af4556ee5776b010bd4da19cd64aefd40f1a0184923774f95b2d3d9e024283db263075e8ed6f793ba4553b9960e638fd90933", + "accept_wire": "d6dbbef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489fc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff15000000000000002a0120a0a62adc5092cd3c202aaea0900b5826c242194c5f9984070e833e58b273eda507080000000000000000090800000000000f42400b20343331edc28e500ea03cbce26983583e66fff33dc9f096a3d6924b8239f68eb898fbd933af0a181f69b015f155a48608d1295327607d85bdb03a9598b54dc58f6ac01b0421309cebbbaa596e62113de7e6fe2aafaccc281f535eda535525b3ba", + "activate_wire": "d705bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489fc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff15ba34be8afe89e4457543a6e7279f86054f1d6e9038234b786b740e9c905c001f8ae276dd460c7b2030287a608677bc51eb9cdef5b7cfc1f73d005e463076510a723024696ad20b4758b228284620d18fd30750b7051cc64efca23a1417cf1321000c0df003aa4c0c160338dfb3783120761fb4a6053836307e196fe3597032f28715b9a021074ec06ceb9c5f06af9854f8b21ff1b6b74ff3ae326a049de8270f2c8dc559", + "ack_wire": "d707bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489fc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff15580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e7161a20d93b0e00046483cffe0d9b54b0747a351ccaa3971a92aaf9245eb7cde5734f20a6ed584ccd30d81e833d9e89d9d9e42878b541cce5913a647b5ffc1ca9a3", + "book": "fc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff15040100010001a0a62adc5092cd3c202aaea0900b5826c242194c5f9984070e833e58b273eda500000000000f4240000c3500000c31100000000000000000", + "receiver_txid": "c56247558caf290d78e2fe7d948d9605320c012b20fa181db87fc847ce69159b", + "settlement_txid": "d23aaf74371cdda6cbaf75b7390fdd75b3247dfe90c98d2e19e9802783c349b9" + }, + { + "scenario": "D.2", + "init_hash": "19644c62830070c3d7ca623b582f274e6e94669d4938e54262a1c6e6e6ed1791", + "setup_hash": "8465b03bd1ab3001129cd91fffb6e5129460cf82fb396cad2ebbb0ede8916396", + "book_hash": "09e4e3f8cef88225f7401bdcf539cf557932eeb035a265d602afba2083125eec", + "commitment_hash": "e7e996cb809a52736589d78d22b1a7c2a76b2ba07a8ac1f957aabc2651b0b9c5", + "activation_hash": "1d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4", + "init_wire": "d6d9bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884892436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d60400000000030e9ca2000300000000000854d0000c3110000c3500000003e80000138800000000000000000000091800000000000f2ad000000000000855ca0000000002f71c081950e84703f6ab745d5e93ed17fadd4cb4750b1b4bc04b01b18dd4051b0fc58114f5b24aa52b2a1c5c391d98621ce6a37512049c6dac1cbf048f61cb52868547", + "accept_wire": "d6dbbef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884892436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d6000000000000002a016024e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd993a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf707132596934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f607080000000000000007091800000000000f2ad000000000000855ca0000000002f71c080b2019644c62830070c3d7ca623b582f274e6e94669d4938e54262a1c6e6e6ed179105ec065a191d90cbb8a6adbeb0d1fd2838be33fa511588e2a3ccb83ce7313a0a1946d43ead676cd1be4a6780b5dd6feb03dc71f0c8d98056ce61304ab243d91b", + "activate_wire": "d705bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884892436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d68465b03bd1ab3001129cd91fffb6e5129460cf82fb396cad2ebbb0ede891639609e4e3f8cef88225f7401bdcf539cf557932eeb035a265d602afba2083125eece7e996cb809a52736589d78d22b1a7c2a76b2ba07a8ac1f957aabc2651b0b9c5000c0df01b5ca88c942d833703ab7702576abcbe1694524b37ef6b762f61358c9e2c31710cd2d600c69ef3a810d29a4b25705759de647e7b666909f0baec2a7af766eca4", + "ack_wire": "d707bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884892436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d61d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d40e976c39f969145764a509691cd84aafedaddd873be2e3b35fbb42b55d43087b79aadcd67eb097ea539931314d7b8aee65758147094f78cd43736c0e496d2e66", + "book": "2436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d604010003000124e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd99300000000000f2ad0000c3500000c311000000000000000070002a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf70713259600000000000855ca000c3500000c311000000000000000080003934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f60000000002f71c08000c3500000c31100000000000000009", + "receiver_txid": "2cc9f550a3c31100426d327e5308e0ee875b1c33d2382097027f0db822320b83", + "settlement_txid": "69e73334f97a3fe4ddcde4f3161ba1112d6d5bc6953e95199f0ec0bb1b552e2c" + }, + { + "scenario": "D.3", + "init_hash": "196ce823fe02a2daea0a6a544f05f7b1c51520c0c323836663b31af3318a9cf0", + "setup_hash": "43bce577aa29c966a8c9a8bc614a9edfc4994f8ee4954414bc635cb6dcd745ff", + "book_hash": "5bb093a59800241c8f94a2ab083fadadda85a5d08e81b046e0dcf4623310819b", + "commitment_hash": "c034dadab45e153536220113ba43e4aeaf2a87391cda1fa33b9d7d89472ce5ad", + "activation_hash": "bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03", + "init_wire": "d6d9bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884898685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecb0400000000030e9ca2000300000000000854d0000c3110000c3500000003e80000138800000000000000000000091800000000000f2ad000000000000855ca0000000002f71c08b1ab264efb4d47b962fe9f5aeb72b5c1d67e5f0cd7709835f07a6bd58450fa9a048758571653c17a8c91eacd8750cbc7efe0bd648caca5c1861ed14ccad2c885", + "accept_wire": "d6dbbef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884898685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecb000000000000002a016021f2705152e9753f5d63b5a0bdbb1d3cd0da4d56d83e3d3853cd44c12dfe6f8458bdb1620d8f37d85a846d3f80a4e43ca084cbe1743d043d429de8dcfb0843c0dfe77b118b59852036e38b7409770e502899c35e8aced4a5d405ebecc167d0ea07080000000000000000091800000000000f2ad000000000000855ca0000000002f71c080b20196ce823fe02a2daea0a6a544f05f7b1c51520c0c323836663b31af3318a9cf025b2bb680e71adfd82af8c7ff8a10a871775112ca01901a0dc56895b5e1217567c4f5dada7a4da2f3e4211d7faf6db8664f59d1062d31b31ba83d4233462b07d", + "activate_wire": "d705bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884898685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecb43bce577aa29c966a8c9a8bc614a9edfc4994f8ee4954414bc635cb6dcd745ff5bb093a59800241c8f94a2ab083fadadda85a5d08e81b046e0dcf4623310819bc034dadab45e153536220113ba43e4aeaf2a87391cda1fa33b9d7d89472ce5ad000c0df0872c5320024fbba9ee9ee119fc2b61b48107ee6aed280b508eae7d233e8bc9db501f0541eb37c0649ef9526e75f035d355a6dede257c86d1222a728814cd8d82", + "ack_wire": "d707bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884898685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecbbcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03b07d07776548693e5a9ceace1cca137794ef60f13c53d67ad6ac4baa64518e8576fa49931dd22c60ccd8b66d1319834cb6993d41df0b281dd45930f1b5e475b4", + "book": "8685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecb04010003000121f2705152e9753f5d63b5a0bdbb1d3cd0da4d56d83e3d3853cd44c12dfe6f8400000000000f2ad0000c3500000c31100000000000000000000258bdb1620d8f37d85a846d3f80a4e43ca084cbe1743d043d429de8dcfb0843c000000000000855ca000c3500000c311000000000000000010003dfe77b118b59852036e38b7409770e502899c35e8aced4a5d405ebecc167d0ea0000000002f71c08000c3500000c31100000000000000002", + "receiver_txid": "a351e3b1b081a909e61b6106a8c4684aebbb40c32f597aa2d2a0494eea7468c6", + "settlement_txid": "37f499ed459db4049a86c452b91740a254f550f678a0e85b6bbf063b1dbedada" + }, + { + "scenario": "D.4", + "init_hash": "b4a90a0eb194b5d7d283a45707f5778bfca2eda67c762e3f548520024f0cc257", + "setup_hash": "29b3ef77eec90177a34bf4d9785afd1fd6ffb2a28bad4ed571f295fe711c35a9", + "book_hash": "6beceabe7c2571353e03fdb7f82e3f4241c2467967ba67c2b9efebf32a4a6839", + "commitment_hash": "06602b655a1dbeb1059e87e9f8c0fefd89bed2a6501a5df25724e54d1c2e56fe", + "activation_hash": "982ee14fdd19b21558b89adda15b53f54122fc1c5a72bae5ef4261ee154cc776", + "init_wire": "d6d9bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848987cb2b4c1f4e6b9246bc30f7a7cb145dff0ad8b75153061fe08a31344ec15a0d0400000000000854d0000100000000000854d0000c3110000c3500000003e80000138800000000000000000000090800000000000854d0babbf4d69c60af8d49aaca2364c217375b270927d0340b18f630396bb6d9b1cd5490997cf98fc8ea3d38ef2f517e3a6e26e0ec9ce8c33ae503625e40a46fa451", + "accept_wire": "d6dbbef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848987cb2b4c1f4e6b9246bc30f7a7cb145dff0ad8b75153061fe08a31344ec15a0d000000000000002a0120ec5abb4d6d027af6f9bc6451f6cc98d95ad28fb9c20d67d3e51cb30614c15a6e07080000000000000000090800000000000854d00b20b4a90a0eb194b5d7d283a45707f5778bfca2eda67c762e3f548520024f0cc2575f178313b04c050db7aa8053d8fd271677247d8058ac49f3096d0f7abc1d09f75c82c95bcb0702117007a7ca143b106392a006f81cde971644bdad587af2cc7f", + "activate_wire": "d705bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848987cb2b4c1f4e6b9246bc30f7a7cb145dff0ad8b75153061fe08a31344ec15a0d29b3ef77eec90177a34bf4d9785afd1fd6ffb2a28bad4ed571f295fe711c35a96beceabe7c2571353e03fdb7f82e3f4241c2467967ba67c2b9efebf32a4a683906602b655a1dbeb1059e87e9f8c0fefd89bed2a6501a5df25724e54d1c2e56fe000c0df02938ff94475f468d79e2503c5159adc276699ea1681343cf53169b350b3046ae09015bb2005c24c4f8d9f19649523ed164abbe7f367ff2687cf8a27ebacfff91", + "ack_wire": "d707bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848987cb2b4c1f4e6b9246bc30f7a7cb145dff0ad8b75153061fe08a31344ec15a0d982ee14fdd19b21558b89adda15b53f54122fc1c5a72bae5ef4261ee154cc776e551b9a3347022ed74ca206fe49684de31f1150e6d3aace73807bf6055b24942176c45fb7eb36a13f2c1e3866e223cef4617b6740621c114f7278f68d283d000", + "book": "87cb2b4c1f4e6b9246bc30f7a7cb145dff0ad8b75153061fe08a31344ec15a0d040100010001ec5abb4d6d027af6f9bc6451f6cc98d95ad28fb9c20d67d3e51cb30614c15a6e00000000000854d0000c3500000c31100000000000000000", + "receiver_txid": "6c5524cdfb79105aa2431263458cc44849c91f1af2218c5bede450935b40b8e3", + "settlement_txid": "6fd405cb69cf32914b5de6c56cf73a034c29a1b8472edf5ef6a4d840535ed995" + }, + { + "scenario": "D.5", + "init_hash": "e0c80af58e7643d04abbd00763600d69e7ad33429020676adae4d4d341201b3e", + "setup_hash": "e012e565964e65d584adbc7138f1d115f6e00c27a90903e6c1b1657982fe5cff", + "book_hash": "556ddfc432a327526e84bbfe843070d8fca46b05d25b3a0dfccd406ec4f03a29", + "commitment_hash": "61034010d3510c0a44a53e0ef4a086226bfa039708f944ce4da9833f1ad5e5f3", + "activation_hash": "7d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a233365", + "init_wire": "d6d9bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848957d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da604000000000fb8047001e300000000000854d0000c3110000c3500000003e8000013880000000000000000000009fd0f1800000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d017a3e152d4dca7cfe072f02634463aa789ac5e5a0c4dbaed0ce8cb7baf418ffa25f7b1a4f8d10e36916250b3cb6098ca4fb177335d1c24c78ed9e69cc3939805", + "accept_wire": "d6dbbef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848957d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da6000000000000002a01fd3c607305b11a874bbd995b29b11d109eff432402136904a3bab1f7bd9f869109cfc1cbf141d9ad3b41938c25083328862acf711291bb47ab53b33dc37898ef9326b462e42ded68912761a397087a1310af972f9022330fbbbf24c580b9e71479fd87754bccfbb8e3ee44224e0290582ada7cb1dcb05ad968df63ac0aa01e81298f1b7c23775d75a9e3e878e5e33ba9cf7da51c78a4d54431bc1981c8524eda36bb0dbcd5fb4815309c1de8fbc2867eb93635d8764c565d952917457874fc6eceda18e3ae54a63b8f9526f7955db3417b5a1f6743b99ad0ca7cc92c3fb199c149b768bd471262b242310a4d1b298156a5fc3469395eb2fa3d28be02b88d8d0c970811397b027bfa95da000fac9dac5d41825bb54a0cd0071a262be00d6d4445f3e2e9db17a978e9e05effc0d7f4c33f7fb78898deec1a0b44212bf943818091fbc09e0fc65290b128e63d598e654d4a7bbe14cc3ef946f6fb6fe0d5b46cb9e0e263883f951c45254175c92549460b5a89d5d3c61a950e7e366f992dac9f7f7a7e6ce7fa9d083023788850024c7eb588fae1c7542604d23cc12f44ddd568c2320a754f3aa43187fba488b5530dbc366257f82058801f6983509716a9e9bf00db680f187404ba142c5ad30fb6769e7f6a84843d4c437bd50254a72815c8b75c1deb0c4b28ced94a2fc573f8c251838edfc807be39d1b910d18ede82aa7a127c5888b3a3a6a8f52194b94c6d0b521756e5c2c20ab5b6da8a2483e44aeb48447112c068042014e1e38afe84f6da72dbab22108ec05f60555504bac35bbfc7c62d1ac3621113667fb8817a03dd2f5fd97325ffaec561f597256d58742291c569ee251257fb30ec738e4babdd6d0b18b82a7f5a7415074cd283260197c4efe5d439671c9abc3e9e411c4bcb405bda68f31ef974cf91e5dc7a530fa8580048c4bc742805125a7aacb62cd9c823ddf004ca04fb936c3093029ddceaef569190534039885b1b8da9e7ea66fc54f78b5a4441c5ca6ebaa742485894509dc379783c52d881fad9e4063c74693de53572427dd71747f7a47f95a72b83b95f62b60be7479516593c53454a89ae1311664f5155c45544265e5b33911a120db3b683522ac590e64528b2db1d9b5644e6d6b7befb0f222e567bef23ec977fd3d5a6f146b6b195f48384e454755b708745f22c50b1ded9d0f24bc9b87d93d95046accde5e0d0c1b27ee3a01b29bd0c8af340c2ede7c9552fd9410b4c731a47156f54ae2fe2ef6ab170a3f93b68781e8a122e02fdbade74d727eedafc80663a79c32726b7d555e730f6b00c508a679821f8e0b736552664ffd03360eb8e08acc5ac5c653ce280885222ca80599fd0cf5ce9487aae5390f0aa7e2f55bbf36c7fab020879c262c74f0659b80d1d35e8a88f39201d36bb5d8603822ee851e4f936cc9d941eda8f0c132be674ed8b0edea50f2efd820966577036831199400b94a6a5ddc1efde5caa77d770fd938267950cb8f56aee3a11f312d601c4e2f49e229db76eebe9df97b467478fe8043e52b8a3ff4d5f870fa11ba4e1a7ffc5f35310840f33f01da137e5802ad25db1bb7507ed8a6155c9d7d58e9798f1822e143da862250702f237267f91bf1f6bc33dbe9c22ae8f8733c3a60d21c30bf7e51a304f53bac7c4a2c6e3cbe61157679598a89d04d4471120348d82a09c6588128fe927d38dfe26d98932961fc842764036a20a0e1e99e0e8f0f9d389acb4ce07130e58276e847f52bf8d17c2bf36fc33f5d5256f0c02a8e76d9de795b0a9a6624e694f2df4d106bfe32fa46b60400a3308853d9812b63820a7c738e5fc15ab6181cd391a059c248b700657d4b21b4e5db49ad03151b291227dc42af60dd203c373a9533afdc9875f4c02d08560c4533fdc43a956e852c354169466af263dd38c4f278c18fb54d209d9b65ef1828cbec21c3da0878d813c406978c150f03595e9a26d582ad426d14ef279edd0e8f452fbdccf935988fc435b5713186e04f48e53fe514466473a48ad5e0afad0d22fe464e113b733aec9e1d33dfd95556e546977a6b907fda9164c86f41dff868d4a2cc3f6fd342210a6de76e74742ecb52b84f482aeed8d2e0b0a843cd8167b4554f5d6882620e0b4f1feee1c51202c3b9623135d41499777d1341f63730f98dad4702cba08838987d86618a0dc670de584e3cb9602dd2f60278ea817a31d7f311e634d3f4fce46408992a463e8d153537aa72ed50c22cd6c76648191a9cb4b43de2b9b2f030755b807abfb879b08abe7a8b15adcd9efd2f419214fa5d02d099fda96106918f0ebfa087f6b72399aca062f7bde3577c44fd288090b9ff2718fc6f5cc81f70d71f8c06e27b949b91c969119a7cb85a4779bef0397a4d2a6847b49d128dd31f229a547af63a7d20a9603285eb3793ed667b25f4a22bd8cd94344665f31a8d872bb09a3b355b08b3bf759b436cbc36f6d645fc01dccf919454235b8cf509b7bf97f96e2d82c0396dd60d8a32ca53a9034f4fafb590a6f8b3f02e3d16a26bd1d44995dc923077293887ec5dab71deac2055a37fdf727d5a2c0d216bbb5a25f6fc5c81c3b0c8b25375f88a324a38ad92d72a73d2771886704e24ed7ab53595164a7f4d976600e0d3670d51fd1f1a08937a5247cd7314e78fc85a6c692cee5b1304c70efe0cdf3be30823623953ef905d37831a080ae3ae1d0c261f7921ab76752f4c9863d3ae76c166247d9225b19a416ad39f7f46ff197bc95f695201dafe676fbeb3ff3afa2700541928f23f11d59f8a56d40abd6b10086d70ff568c1b39d83a811fe4aa924595c69a33b636f0410f8da6c59683048f6a63d011eecd0fc74ab8a8b2d7759b68431f7539de4e7c40fcf22a15cbb3d472dd20a31d690bd6960c1305b0e61a9ea2da6d693234a815de4e712aa7a38fecf60291bb0e0af44276a434b51045e65ddc0caf75b30495e146c0076fc28951d7396a23ecff1690e2f8a9df059ca0da40a29d8afeebcfcd02cefa7cdef598a2673065f7d55b6617884c0ca418d14c97d01e4c0aad582a5174c09a21638342681bbcdf02ef3ff5f56cab237594556a4f9feebc7b9efaf166d36bc7011be23886a2c89e590f9ad2a4665a5ad51420b09a07509e900aaa4fcc6cf6bace255fef4d89c31727806446edbba51c733ad0dee32d9a52a4d62c9c96186e1afd55d35f54ddc21fa980f771e17fd7f464b71d7e34e559d518b268791aa82b54cec4fc2aefa34cc66fe9f21eb90948acc2d776c5d23a365ffcb4604ee65a09cce259c871dab39b04939d4f3d2252a3d6469912e6512ff7a494d00d8ab8977f1378479cdd5919a687a6b4912af46667e378f6cfb89eef63eaf08aa5d3f5d83c90698ba63169104be2a28e46f9d1ccca709e07c300817fd060a676575d6376ea8deaccbe2eec3714954f953826781831caf5d250305bc69ba1cd8af664d439f6469f65361e3f5fe870b765f99bf9c840aaf472cbaa57978e18a7beb1aab7b3231d10ca16d6900093ded5c8ac76aee2fdcd208f7cc7c79c678fa3d3af63e43f8145e12c509786de6d484f0fa9eefd4df65e28a412244c366997dc906ad005406191314722626a355dbbaa982075b3195b9164032bafa294351f3da469060dabc9ed49a700436b28e33ba98eb19575777b2f7c31d1c01ea89f750fd13cea775b223d7b33fb72787d312a5491919dbf1ccbe25364a6509a5cad019874190a2d2575fed1814db0ec63349e0d45da1ddef18ccd67bbaecdc0ceda6c4c73f713e4f2b0720eb3d477f514148f2dac77cc50ea510f6bb6e89201ad4cf91179cb56daa171bd244803fd2d2552f81d9e8613b9a3991e8f85ca405596250eaa741554d424ed40359b7bc9a29df7116ecf508708d8a70c87b12700480cfac14a3b6abeee0ce1529d0c8dcfe84a8ba767e9766247665362caeafc4b9bf93574c0f8d4bc94f57a85d9f23efc58c769e716f0d7b18b38dd969126f4e713f05ba71906ad30b5673030fe0d3f82d874741e2dc94bdcea49e102bb80bd902fbb168b3121206e194f44db5ef0cbaaa3198ecb971466d128f17514082cad26a5c16ddc9f61d81b789669510a05281c338443b02ba356ea887c02f265d9a20fafd71d0aa9696c1aaa52a8b570d3cd3aecff66d6a2ccbfa71e14d39ae6967ae7b6549b26551f919039be40948601cf75d580bd8a7273102944d505d98476373841a91019fc58c72deef72db55794552bbe06b99a0cc70b545c257c782011b8a630ed5a1d31aed2f71d4007347e50c49d0273abc634d77b7fd732c570ad83b5a1329af2453f9a24a11729b565921757b15b24342cfa0cd68926626d9e98244eb7266bac47dfe83ad32ea0aa6c2a71c6c6efeeb6fc94dbec2888836a0c0bd14f5ac63c549c1df54d26ebd93b57f93f3610f87dc19b15d6178e97a3c87c56dcdbbffe2ac6cd828e1885083843d6dff6883b91a547ed66ab4315d7e3bf72e89e3b0df85fbe1eff77b8841b90b0fbc491812b75ac167e0df33b212447b539ed51626c3998d5bb14679fe39d2fc267375317eec386d12d9805e45bd9082d5071eda545451d9d4b94201fb5b5f6623d970d3839e46f870d7fa3bf2d2cf7ece93865c1b6f4070c9bc30c2f29286d8eea729414d2828c3827d7add206c22b5d8fdf7662574899c10d0b4b4119824dc80a26595b4b44284ab1a6d95bf48da05e3f6e8deab009f70f621e12f36a033dc1bf7ce211100ec76ebbbdfbe1a8a296bbf3c96ed2d4e91b99750437ed50e90544e3ace2456c7fdef5e6d81d1c69797041264507da352e9f70ab879dbc4a2aa073630b6d61dd0e1eaf5bc215510ae551ed0523085244700d8bc42a08c99dab05caa6d4e00a0a3e974355e6a5cadd6a86049843e8fb4fde1fb78042e83c43096cc68ab22b2d41b945781c674dff62520d3cfd4c3b0a4e860f9d09f8e28525c44e2b32684d8ba68228a3f1f79db0598b2eea7bfbb7a54fc65f2cc087f91d4a54e89c6a9189994fdf807cbace173f150527f420940c82246dec163f6bcea0066dc1b8e42153a9a89cb87335bc874fef74954ae10fe330651d32c31fcab4753b4467cac95586300c264136f8f86aac3721fb165a82c392b6da093a4bd54e33100ea7cd77b5bdcf688b1fb691b474a10e0bc36362f743009fa6915ebadbfbfa27399c3a30f9b29b48e39e8e76a6bef920de3a577815323725370ca6a62de58f815237acb314bc3c4ca763f5c260923f0dd2b4fcc6727a4ae8c2ebb1d033eb969abd7476e90dc6b40b54f75b5f4d6d755679f6c097e07701c7cde480625ca923bebaa832bc17a6abed269b7b1d3601175c2ff6cfa4173d50ab54b95deae539126064ca1a69a831ac4bfc2c9cc419c6f1da7572f51c8bba7fb3d037affa75e8ab2b6784f404e9ef50f7a76b9382874b24086d5b5dd1cae9402e611c095147a803f5b408d27ebaa9344cb951fb073666fd308fa5574fad15542780583e26b83691532044687dce16a82ea097c13d339da899a5ee02a245471ab91354bcc2c2bb09ab2a2b96840d5447ba9e3b9db177f63bf1894ab759a09a07adac2cf5ee978ef0898161ab40e666963976b0dcbbbaae8a0d7f8b28014e2ab39cda053ec7cb75fdda214d03f85c30d9d137083645ec85d70119906c2768ff392d5d959b68e4eab28b75c719679c43ccc65060d8d9d1b55c56e410b12aed573b2efbc90729cc17c11f1947952c29ba79945746be3a3635cac70dcbef9b5f95315fb041ea28586d613d2a976d82d348d960989c7b9416fb3d56104da1640d95f0c5da8d32977131dc41a05a92f1a6876ca043a638240c2b1c8521f1114e58f57a865ff75144bdd08549c83c656431b680ab2e57f55501308f826216b83d7f2320258354c20a9069262030e08e266332394343063bcea0cb1869f329ea267c32697927595a930d79d6af1b4d0ef7eb48eca60a66b0b9295e281b05534527954935a783c0293c7338c17e95ca0e84642aee0b1357adda4264d19d6dfc5a1649f6f69ab5b4062727281bfc562602b8d23866140ff820bae3629c28d2b68bb8646a8a5d0c0a6d6abfc5db6d6f7f0675e2e502248d2c9868363efc858272cc637ccf66214c602f0155d3350d8410b27f835f10d7ed7480d38697ee5928580aa4dc5c25b4e7e181bacc5dc94f96cea194f4ad582ff9082e49f43af951f5f2c72b4a423a735be5b23a37b7e502b1d730cdf511b5f5dcd7e3dac491dbf42454bd7d0300ea2db54c07c732c435c8b390eff7af09d1fec7d4183f26c11af6f06ae8406eb3dd61b8993afd0aad49eaab19f9b363acc565df1eee863fd0cf189ae9e79e2f3402d5ec19a1b0c2df4f3bb8f8375692869e820a1e4071468c85bbfd561e1a4210d942e844c1b72a9418b1b4b3c4b0f6e514fe3b0f75e3d7df746e95326e400608445dec5823810557eb56baddb365cdba4e465f2498abe69cd50b0333f3fcd2a4d3bb7d1b6be07dd1f3c90d3cca5f5b3cedb97379673782f5afb23c30200254a9a75cdf945525320029f7e8c5aba45dfd5b4a426f346603348a66b7dd45325074aff4e140b03cc239486412dd0a130e3ec93b2648b95569ae55e5782af05d8a8b5c8d165c407816b8eb5e89adb271a1078cdca4f29315161593ec86f7695aa968f2a69bb072521c766134d9c1521b5d49a368b0dd00ea3b4e21883f7c55692d8e3576e981f7ef9fafcb12635311778fa6e9bd7544bb502e310113869bb2bfce131a771b67463a1bf5b20192031a7780c14e21a74d4ac5a0c4468a45998ae52267d49a6543212b263beea088dfe20227f97d1cc1a3b935bbfa0e11e47548413496ffa2340f37f578504f289f12a5fc253e2e534f83ed2878e342bd0187c8ac168e856e6948c8eb949d48e53f1c64266341a91c7995547f441d3f2bb7db66efd09c7a336cc28e97b5ff1e5c83d05c3e68257ddb2587b0c20338833512dcdce256bb41df47f9dc594b20b7aed566d3457cd3feab6a2ac54021ac61a65b74cd51304a8cec2aaa0fb13195fc09a06d571fcc8ac066797be25754ce7da18783c4682e93cabb272fbed2f0954226d7871dadbcc06fb772436fe4bd5e79a509dc33a67f4fce731aef9e25e456361ebaa95b8dd448180b3780a57dd5e996e155fc12aee0a1488e1db16efd197fe3307bb1ef3fecc70be961fb5d83f57293b23897427d8b7f44f79f3825b0898063c9be52e8430a4fc6556380971e45ba2bff87d41f30cd85fbd33a658c370afb29f2c452a9c176e6b15b85cb999c849c3e5fcc3571672fb1ed9a9b17e17dc9252ab1b6c57842adef570518d2ddb9ca68fbd6cd6864f0e18954a79e2d97b37db47861208d7191933965b8a66a8fa2192c2f037065395f2891d485e7fd8fb3acffbe35e13ee5fbc7ab4bd4b856819d6cff8e9b46054c1b17223b7dc16a717a0143c327abdf0ecec7a56ce9d4a42a187228d4764f3adaae664a80ba167ef9a42fe98f9e60997922edf02ab62b9515f3cc04cd742bbc0a01dc5c03a9ee981ff70177b153af6073d60b5dd62f266b099e2a9a4d4db5228b51cb37a70492764ccb4e974c7d238592775eccce4089735e69dddad210d68642d0821c9c1ef4a9e69d5f508ce86029fdc4601e2a1faeed4a8bbc0617640f5cea6be06ffb02722e4f7b8b02d33806acdf4fc6050e20ae54bb1955aafd407a8be9d7518052cc5fc266960839ca65bbdcb933b36eaf9b2f61c37192e6ba5effca7669ea401167af230c9b049bd02d00e1636f21319cdab24c46937d04d8a4ddb41eb1f1b2f6031fd9c8380f15b2928f042e9645332d6e0bc2f55e06778108ece17ffb8410e7035d2c12ede9ebc2c6bc81af83b27d172db05b66b10c7c7f48c57222e1b3fb423b0daed330e204543b2fab0fa9aafff31b387535ad322f5743c7b78d82c9d6f58fd85711958fcfd2824474818828d5ecb641bdeed41c988b6eec5c22e015bda7aab18ccb03f23a253bd3b94ec400a67d057cadeb092167e45001856df35eab6ff950e9ec80d4563255908a7ea09a5ae5dfbd0e3c2944295709ecd7e35957cfa2365934f72f6559f95997c60304b0a98e3c9e0dc99ab7ea6554bb0a2611012ec8ec892231dbc8ce89d60f3bb9b375718e460e7b5bd1b8e84631e291a9bca5d940ca7b8fb92c4baecb5a6e464d3c3b5aa5c12e67d9e70650327b858ebcc9d5412f7b492ac0203d7a82aabe23f8aa7776d173df7da0afc42147476614d9111d3ab9e16a2c977acd6d131284d2fc8c0d1af94dd6191d6a84344bc47221354eb9a2b0b9a696572dfc2adebd5f9177abda3769f294babd21480a6839801474b078be2982828fed4bb25781dc9bccaa624f6058c722eac222b4a659e9489edbe819151088a9e7aada910f638ca1a248bc94b4878b1031a41c101af8f35c9aef1f28de95ff9677f064bb695651008dd14631262188ce64c392f192d6efdf8d4f19ea9e5ffbeda1fd8846698475c057567d1f2c133c6d548f3e9722b3b8b9ec6960689a54e3457da2e4ec2a9b07cad6b256469eab859e2e8354b27bbb9f08c3ca7460f9ac1863a0d05133befb41a245dba3e136fe3e9de43e1000316991d135c420006cb6b8ace8eb6f74e97e484bb283a123132eb4976f0c665de5394ed0b02217b38bdf8c89b9b0db4eb62ff10e98308e9ce8647200922d8d790b7dac81bef8e81157bfa3420a29f5124ddf629161d0b986616e8aaac86278b838b7caadd8508b67b3796c8da1c71ac82a89c2fa7a8b40e2ee560c6df7ac42cc99e51645918c138b2fb62c7aa54b15597b8422142cd49a776092d55f92d5dad31e417844c63b31e7701adb1fc46ca8f1c24d0e24cc40fe7d9daff7deeb63211fbf5bc1c123c02e2cb25607821cfb7e67028ef96056f6e3f81f72db1bf8851432472936bbc6476d9392204247dfded154f8863582e44f0acd0a30a3bc8a38ab35ab744b73ca18192e42b479904bc5abf82fbb467577afe7f53632f5a8e89be6791a36b49c326f54412ac310f1fbc90220101c29f748304f9496740c3eee14b64c8fc17155835275fdd380ed620c5abc8a4160f18375928c0ed6a926043eaf91365dfdefddef93f0c8edc3204a6c47c4e9115755a24c31763efa78db17bfaa9645aabeb4972c89a83e02383dcaff8714d79fc6df28563584f79be4435bb3632958eb98649d214e3e136d56b460bf32ae200ce0fe980817a2bbc78112c9969c70db679c2aa30d73b4cd753d127fca8a62042ce2dae9cf035561530b3e2e95f60fe74245ce293dfb62ee0ed4ba29122e130bfd9a32626fd46ac15ce11642d63e5c596b7d23d6b142392b59998dda902b1005f23746254e625f4a70103d02b0ee3cd7cf6f19425188d040c9ed7f1af1ab9d41814211ca770b40c250364d4a26bd0d57b4d6de3d406c8cf54bb912bf745b8761c10c6ed566edc96e82ad8e7a9bc3fcd22fb0e3974cfb4a81886dd94579a2ebe3c14379490aec6fd8bc8bcd7981094f23011e1690bf6a853b474acbb9539a6631f51144498802e9eeb237d8ba7d11ac050c4a392129afcc08ec36aaac6cb7b82a482059dd448f61a9b25249bc5d4e6be5517baf5f5e242693de4032c7083eed7fb1803c6d5624a7e32d3107bd7a3939cc89a083388264bf9756396a45f40c30da3a0a67d4dc1838b20a098e5351a1e4f344dbbe00dcb86780b34031a24731ecf7e0e6844097a8e62b43267d565dd6d3a4886e556206789bcc1624846430d2de342f54d7b8e61d264bfa39528a1fcc63532b291c994c2ab7a217a4e347a614aa9e651bd234eb0c3d49830a6a804e7ede79c77959ce14f731d361f1504f6625c8289222276f9bceafa3281738e27e15f6547884c9ca7a5dcfe2501594d57b4c25674b575507428defccdccfe69543a182575dd4817b9179d45eebf5678f6a340be93a51d2e263e764839c2454a10bf8853fac76861923dc3a5d81abfe6b8ac5e302b89b6057f782aad54a65adb51ed77e50d3a366db9d7a20e6556de4b9b77a9d866d3bda9f4a87208766b0cc701414a4deaac8da2acfe5abc0a8294b1cc8edafc68b3a95d9263f1499a02d63c93a0fe30633ad556a5b446a1078aac0d34309b68c23dcc3d7da53e7f88c24e6ed76850021ab5ac808d6b1a412173ca93c1a3db66b0695504c0151d21f1e65e417d5671dde59931256ac4fde601a904450d4f2e168e917bf4438d900cd17f571719d60bb900fb0e0740d65251a8a26ec67cb3ba4b95f2ac4be486745285e9434dcebd8f7df9f743dbd36530d20396497388e26286a2a9bfef575a2269ea85b36b4005d294302aa053758620abd37177996abc69e2ca24b121f45b32718a095250c0d533a89785fbc1692f819491b3619b64e09337ff2b31ed0a98c07d47c910346fa6e43ff4da9c16a43170fa38ff7b2f735c6d7ed6697968cc2792e9ed4c2cc9957e2152c773a23e109690d82876021b20bad6dd3833506b1e2c7ddb85e8fc754b11f0099c558ad676db0d3f7a7c70b38e38652fe97203085488d82bc013586ad8a093aeceb4deaf2ee93bc0e5f3123292708627ace19965a0fb622f8eef55e33dd21febe02b2fb6dc2025e9c9a099ac9f0603aa645a573caf5b99f8eb03b28e881010b96d87e1ed55ce341d875edf3e4221594917fca299d02b1c3f1990f6069f944330c3b2ad9edcba7b48640f071c6bcd7c1a94314bd8615a6f6493919fc216a555b9dabca0121775f5457e830ff6e6eae43268bb92d5fecdbf8832a37b575285b1e113b29ea7e5f8c955b2b3b16fa963985019f4a6cac17b51fd164b85efd22180221311ba043e1688eed5a265e23b6fee64a6800737f859959582dae01bf804141b7dc199139dc15a7920b76551c9c0381e120a166e200e20e3b2e0d54267bb3c9f69a0a374af5f53550cb6ae690524fbf6a6845f2df5da2d7e4aacc218263bc216db4be3c269ae52b3ee1187678512ea1324b3e746df8c76d52008263fd5ed6ee3307608b02a7e66c0c14618d3cc34a24260fcb5c7819db74e65d8eca279451104abcf31eb27a7fe64108c7261a446b472fa45a0c29af4b821210c8aabe6c113d292b6300224dc2cbcf946cf2868a4749a156c578a4a6e40a2a7bc1716b35880eb9d87a4748ae09673465d6084beef0010d1ac1e7e2fcbc6783d854f3927e6d51f272c52bcc6bda16d175c4b0f2a9dbe8437a34789c0343991526bc83bdece3c71963f86cc51183a34b71da18c6f995f91aac176268d6ac0a3a2acf2717801ebf23b9d91926e9144f380ea1d68711d9db0ab45e3397dbd39b5e728dcec8d55930f6f2affb0ca12c5834434e1f82adc0b7ac0bd9ec0d4179d36e2a611ede605a42977de8a9e466c453479e0daf8b53f8c04ba0eddac3848dd88ae4cafeecc59bdec10b881101c883b61b82153bc8cf58c32d180d4f46da8f40ca6d8a3fff5ed4d5ba16a1fa73d4e67d00cfde8a915c23cda075f4a88dc0ae74792bd8ece23dcfdba72f56b9813b0ebd559de901dbb27927b10cbb56d57e3b244a9f5c248ac8d85221547ba9dabfb3dc68cdfd7846524e752cbb7ba1aa0e495352325399e03d91a7016e3671fd964d591141f42588c3501522fa211cdffd484c7889c2cc6716de25654169609657a49e6ef1453006abcf1da0dfe1572ef43a2082f91f230b0f14ce55a6361e8650b9212266945cef53747caa27b2ecb733afa5afa42bac3afee722206d2d0c5abb811c7ca46af876947a45686fa1f11f8842a6ba14ef982a40f5138bf420c6f7317623f1661f5435f16624c0e51a7a644ece8fe3d7f84af61a410787b30feeeb966d0cadaf4cd6e8d42db0682daba7a65e6a51434b843b5cf75213ea4427e0319f4efbc7d18e01e7f39891798b254ec5fbfd4066bfa49c634f8e3eac8fb5329aeb7439cc3be9f123da1cbb918809e10bee97bdc9fa805b06c557bea2210471fa33078f1a7aae9140ccfd98127b56044d9104e9de5fbcd682859598eef0d6f9ca92aabfaa9ff70179a7b0eff8bef5d45379521f1110e8db1ee03b96d579764c8c344c607d1b09149d5c5275f85a10d8cd9202c9400ca2def4ad6791075291b4c3c5df3ca26248920d3f7592d14d56435f07ccaba5650dfb6a88e4e39075376402718a3dee81eaa499f230eefdac7766ac46fda3d15ce50ba634d5dddaa37ed8b27706fcddf241b4264b637a0ac3315b33d20ad2b4ee78abe9b109cd2ff12910527db052073acd612d2f8b24c44a0d791c8e458cb4209a578c5e54059a80bf078ea62f2f2bc07ca3c0917130c0549bb4528b3d30787e8a58e1a4e395f797ee1d5313c861adb3bb2a1d5a92c063c04d8e4ccba90f31d32705ab9e5bf9937abcae9bd411316d6fc2b34c08eb4c4047768a720a11c236f83a193eddbefb5f177811e1cb748f7ef1e699a40d95dcd4c155bfe82b2458ffc85aad59e9a7b9a38b6f8e20129cbccd2d776df1d26ee861943082701bbb4b69249fadd8f143b597ac73e2af5c1c556ceceb7b569011a449af0c305c8dbd28c6f9703aed425f8373c8497188b2b9eb2f5712de794c52523a62778e83e0e59a1bbc6f06f97b7789670f0e5039ea9a1693cec364193e49f8f6aebf42955d1b49acda6c888a3a6e2ee274e372a45494b4ebc0b53de00bce7dc1119c28cbf0648dc7ad74c976e4b49b1e5ab6d372fe7c87dc46f6965e74bbaeb7789ee532264e843103a39d7a9d75132de7e9b5ef02a5300b1c69e8c2aee8a42f849b56150603e0541ba97656911b2bec92fb6b80e1c47be8e25448c1ed1d4cf6851269c121e1c45e3cd58292714ca7a0afcd8c6813dc142ef09220765bb717aad3c2d4dcfe180e8c0ebd8421df820386626d3a85e4d29595e3312d192af31f4735e7352203a2b3e1ff5c8712247ea76b8d6521328a281bb01550134b55966c3b8a175688d7fbd913988eb37701053abdebcd07b2c0ee2c8b44418ae4ad010eb4a16252265eb6e1ffbb8238ae74055e93170d5aed29599996d1a0367ed61db81a131b5b163de5d6f2f693055b87a3cc7a0c6a15bb72ef826b6c2e89d781417985751c1fde5cd6069167fce4c56138370bcaa24d03ce24b9e7af1b72e21a2785359677d3c101e9029b117a43dba617f8ea56d7dd022d8d72d2e1c3b68cf4b0e8bd492eb5741cf78a3b36847fa0a97c3304a95dcc7a8a1440e06ed3926bb95bf57bc7ee2c9e5f1c5bd661bacb1ce5a1a2deabe6736c83678a6abd820de07f5cda1819cd9a9146f0adf4d8dcc60e3cab50bc5e1abc14891e27ac610ff9d6969b53d28f85a92a1fad522b3bee82f76f010f45ef0a3e4c7c15d7255670d53527fa3d330df360ac6c98d49a6a5ce3f6fdf1de4ca700844c8fa3e1d6d6e357f1227e5e942653388260e8238b5612f4927fbfdce258bc4d0b7594354e9dbc5cf7149868716b45dbdecdc449d0b39b5eafd69e3b49a7ed29d4459aee285b863674b89d72cfb884f5cd5eb523ac0b493661c58f7851c5547a959ed81819532ccb84729849fbc4cce0ac5f8236350c3b67b316338e15cc62b954d30b4d23d76e2a5311b80e76c5229b01cb07f2bb85646fdcb4c62adc82f198b047bebf661b595023ecb62cbf93a64d234dbf889166b48fa8eb394bb9fbe841e1cd5660513cfb510f00b170cf080b3ce2b850cec369d35c0550eff4b8fb2b6c91dfda8b9d1d054cf18aae245c31e11c0d122ed5aadd8a3b35ad39dfe30ac8798a46d23e06b5899ba907b2923ca8677fa61c7f0943cdd7c1c40def5d83aad630bc00eda293692db9029a74afbc68d94e574decdd9aa7f388e8aa344379018c4da18256e6c2b3b309d75e679971f71daae4c69990da8707c359765d2c1f74a56ea45a25191b688dbb1263e7d9c8898a7356dd313e14f066a6db7dbd8a05af623cfb142305d9daa987405ea5c478cc99679df047e1cca5d28b2ae550d88570c9a7b361b462d4bf0f568c72724e502c4ea615d9bdfe76205af4649db3381bb039a6c81d5e7387afecf394d4357604f540282bbd442f84f456c5ff810d7964941b31bc12f5fb19741e64da87111ec3ab8a714db12a89a0e19cf667a0049a9dcd72aa5c30cfe041c4bfedd2c41b363acd3fa746b0e65820abf0259851616c147a62539828606bd4d8149d173108a9685d7305f1624dae2c8dda08e475d90494ff93e3b68d3f06807c6439794cc48e36826e845ad919d758bc0308611f2b2063f35e59badcc28b94b58b11fd0a16ba852395acedb03563c9992103fbb9c4541c0140ad25bba0d98f301ca34257ff0bb60f8a29e8c2e75569f12c2e757cfcd86d441987754db98d33a298ca334510f32a78b0d175ef81aa1283c5a43a0a36997337a188cf28df16535f74d7b79af5ab10020827567deb1d7feb0d68c97b24375444fc91685c3d4cd31ecebd30f110cd6faeddf43207ca9ce128876a99d882d1eb2956ac66baeb680dc9f243390f1e5ff5f824b3558f3a71558ee2b76b6dc85b8767d8c3a762a9935e665b8a599899c0e0c8e20d270917ab23a1c5ec1392031cda458edb1c95f47f390439757563240273ed69da2e0b49158ffac612227c538ab4df21e668f072475199d7a5f1d5702a5e97030d9b85af5459e59ece07c34cf4eba44fc2fbe75f388eb334984eb5a14d76478f8c7e277804cb813c573fe185e1269a0b1b3a7c4af2934a43425a844e0cfcdce1f191ac83c216aabfe9cfae82ad8fb80e5ed32fd953e4182cabfe4bca69a2b2d26394b851e6f313fd7400e34f66b207fa7997336eebeca3c2b2abf74149413af897d503c0940a04e65488cdca79adc80f3a1022fa7358cc7525792ec60cad166479beed863fbda52d75a2b3b21b44cb3948c74c3e5084911fc1e172537175b5cb1ed8e0ecae73beda0b4e5e13b826819e1dfeab3d9415167f1a6ff33eb7eb3a208895a8bc11057d6fc0e3d3cde21eb23a9e8f0428f6bd1174aa697898fbf5fa97167d246f94cc930add143f6b5f3ba1c189160fe99630f10f3c50d48328bf9fb5ecc61598d7fb7da587fb7821e4b42151afc4ee36ddc396d879f620ece1af21eb2ae7af3ccb63520b7e5c2c86ff9704ecfeae2897336074edd4eb00de98fd3f92fa28e76e19abdccc7183a427e0f80f25450992490329bd6b4a05c40900c16d963d4cb2c32cd3b866fd2704809359d4e7be2b4d3c9e0d278e14a883573033cdcdbfe68747f69c02a77aee2631a53854877cd977f8ccc619d317d853e8a8c9605c6b2016c5b77d1ed0ce2764865af5bb58da60d25fe091da59c8e15c2d0b67a0c03241ed38bcaa08afe611e374ff56a04b1493496c051d3aca70ad269740eb8143d40c40ccd9655137e1a797db99c19f0c74496eb02e8b3f8186945c665a130ce8bb5022be66ee9a82d58bd04995dc2ed8817b9cf5382bbf5437a38dff5538c0853418b67eed524fd6ec6730af6e83fd7e1819aa9a9126f1cdc86e71f3a885ba96d926e619de1dc600a5e917556b59349709a09202f42f8ff596b8d24eb09eadb09653801d690e24c1fd00201534185e5d76616e75138866d0eaecd45ba609331dc127de1cc6de73bedf6c8857b39d7bca574d5a959fc5edceefdd5ba830ee696fa281bfaff5f19f03a517bcc14f2a723cb43db17bcabd47e25adb0dc27d1c5a063dd640ee22ac5716a1dcdbe152c27c2a4925deee344080f378bc7003840f1d01437c268fb20c7f9bf4b38cfaf1b18bbcdf7368fc5130157d7beaaf191672619dffdb99204c2ab01b35d299c29b4a4c9f1364570df139c7812864191abffe378364b46c4929ce9f354e57994e56ef8690a9adbdc6b76b343cf768eea6ea9430c144d1f833d2fd19f01a0b8d04e09ad0625c0c1ce47f182f63a888b037893c449126093bb70f97b7360b903cc02705619b8232cac97ddb46b199bd616225b980e5cf964c647898a44241ca45335f679143b0097d4869d4246b117a38112ba1bd600e78490ba170a1d1307043f3c83c2a1a2d50106c905f692aacc895ec966a4c41045525845352c1993f7d5fad4f836f286565c62b9051eb76af4a34d66106771401412f01f4ac661594140355541f815f5e1e74903b7e76d9101f61768d4ea6470dbf632941ad250c20d2ed9b3a9b62fc2ae398c0e5cf571d20737997ec5f0f856dc59a1104d5b153b9377cab0a3107dcef4ee1c340bc27dbed94378bb13a6b1bc1a61789be9572d1d42a7611338d5a3e1351768d00e63f7f71c19b7eda6368a15dc5dc2e7295541888e60738db15b3cb792926170144788ed1842d11dfc84d06d656f2b8390e11778066f21d5f97626bd72b475fdc4642f1fc647f5c8efe7acc255ba6bab55d280a095546aaca38921df154937581e52b4f91a493d81e0ad67231f835c8f1ec2fbab50d1f4e5e324504000ee2445b037cfbfe23395592f82f1ee8d5eab8a2277d8e3644908c775b16396774338f5d3781d0b608c534512544055182fe979fc8d439f5c49b64cc948bd6f7b6314df800b2593788c198f7321ce6424338ef7e83123e241aa7fbe8dc38e7c1e3de86297b0f44eae74f3a3bfba8c15b94663e93067a575289e7034c8ca6fb8fed4d1cd87ab47f0d8959d60eb4147714356816f6fc95b0fb1f92abfb087f9fdd9721a6015c8f1a325c0740647aa9af8d6f41b9b364f2d6509cba4c318784ab5f98664fddd3765c9c21ac14cced443eed9254bb76cf595dd19763bdad8f3762118180c3305dc8d8d03f1d1b79383291c9b91c6dbed7b5868793a94d86f8cd0b02ea9dffb9285e8ae6dac04fec83f99de4cfd81186130119af91f3af4c2c2ed9556cd73b85f9b9c5c46848b51684d15e7a018c16c7433251d4e05bf0a6fbb1c22e26624eb356f2fcca2d099ddf402b0f28922c8be5276c406fb6a262a10e1ba2d37c1310d2361231644dae69b1d58f83ac1795d89062f1d20f1e18bc58a38beeab8a143d5fc410231f56ded5634a2b827172d7ff54326b4d9ba55aff6f76dc0c1d8abe734a3ce42369319689c90263bcf693655b2b5f73426e4299bb3edd27a61fcea84d6529f4cfceb408d8e4db8a93a81948458df679039b6b286ef8e2d6b392e584f7b652a19f302298f2ddedc5bdde42af0c1617f981d5942d33bd7bb436780ef950578799b9c840639e4d0d50fd3e6e4728c62c0bd0378096552acbbd4096572acb88eb8760d4cf5b9077c2871390b10c3af43aa24de7873c145ee0efb6549c7d6ec662d3d9dff9fc1daee14bac71957f258d155d3cf5351507f7f9c75f9fdf97501e0dd1b9635b59c05c79911a8d80bff18b7575e6429c8d709a869e6a06e04c30bfc1ae8163736881a06d397844694f4c1b611b2ec421e3e85830e6a0b06e76264103f4ec21762e94c3a6176166b0112f1dec7ed9cec100950fed6956966a114a747369da11f17dbfdebe6329cef69ae7150b9003b2a03efda39443b9958f8154c93127c2a56004f8d0a7a8fe5866174cea89697bf41df5e122ce7a23215e2d60425e789b4b54bce1b239c8f012e156269e109a865bb46cf6d615412a6bd1e060a15d04d386172988a10fb74a66f2edb3798cae4ae180cbf1b110e1ce447d1cdedd14b28e04b1008b7716368751b7281fd1c56efb14c4bda843b2e3e56a6dd167b08135c514ba440467ae403e0a65e81f359ae4241789416377f7502ce739f6c304d1153cb1b5b24b1d021059395131e760ff239f5ac04487fabed4265acb7f43a59c506c7f1424e8f6a02cbb4b0488a8253ed13971bd35d9857273fc03d9c2e3edefb781b52260a9d6fd63130f1b8d6d603e4247eaae108d635e2e5bc04c5382e5335d54f7383e77089b8fdd6652f3fef12fb0f70fb2a84d2affb193bb1b7095fbf3e5a3c2868086d150869804d22d5da63dba8992833908943ce66f90dad171afed9687c86989821940a117f7fc601ff678677a12178775caca21a09249da22af8ce826220eef839cd84d2b9df12a0f0ee5806b4aa19c173a92308a9f08454311921efda69bc2cf34f1e47081b44b6400291eb927f4980a94f3134b111a2523237c353aaaec137e0c9db2d33427135e6f8473ea35384b6a3eb206af450916ed1f3a2a2e0cebabac99e3d67147da573f39fcab1746d2a05d2e22a060af92efe969375113bf780682741b62602e90cb9b3a50132ef3443924c0b93b7f3c0ea80a6b5085704b4b6b823059acda09858529e4e9fb542b52c668b392bc2900378b079da08774a6ff7e6968eddad0fe921d39051caaf3c2bc2aa047102baa574852e7ee6006b57c070a434ba17e69fc30dfdd88fc3faf7c00465df22f22fc575051deaeb004427e0c3795eeb74ae52d8eb908d6395d93e90d93f113a941d728c7a186625cce4babf6d48d362e642b15f5234e346d0ad56e01eff852be1d4bad5da4d9f7de1734b52094a5045f4d3a962b0811c40148ab37a8cb8949ada98b9bb2ef98ef6bb35d733dafb8788f89a35fc734c6fae0c79c7ef79d9ab79a619331d5991bef608ebfa1bda8ea7044d7614459001856762aea6fa2a8da8a84f63b35b784aa95efd4cd712e8d819797cbdd2e5eb14ab8479581992c5cb4fefb71acd21259c98e469abb620208408b16c28a5b22a408e86745059c187e533738786c8321e62d655b7d42391295649268e33972b9b05e3863b06e927b1b6821f6be5c22cd5b323a71224eb2c47f79b4b7897332b4edf21831b271dd31265ea82a56ee423fbc51d625238678138161a8a222110d7fae260b0d53dd718d8f0bbebc6dbf9f7c603d0998478e02c1ccb514f538e60d2d42bead26565df3239926c7ab2ba9e2556375ccadb273ea32290f6b387130461ebd022c6b559d4485f2dded4dc866794b1952edcc463d2f12315dd333110aec4216d3ad68442ea011ebbf65cc03d6fc4def28d26af9c7b0ad6986d2014ee1c344720f7e5ecb5912d4533e3db9cec5abf3766fc37b0243c9147d7cdb24ab677cb997793184b4c6cbe078a2339c7050553debf100b65bf566728aa8f0bd3b5f8b03efad99fe4d86570c3792a009bbd23aed885167c0f33fb634f5b1cfad7cc130da55119a2fc282e4975d40204af4084a9df275310d4665d3a1c11e4bd872b0b47d822fefc75e8010f2766febba4206548da19e4e5998fdf3f51aedac8c428ebbe458f8039f8da6d996381998888846d7d7eac934aa1e7b220a2ccb3d28ae1fc383aa4319dcb8df1a554f98e360ccb1b630674b00296a7c4204e0380ac993850b4041042fcff74542ba713574070c2982e5fe06d1bcebf698d2ebc669f1e2e3ebbdb2bc9745616b0dddf8503965e9ce1bfbfbd4317ca3153cfcf2c91ab678671c0193785a38fd13243f23bacf712297c1c3161df42aa6f3b6b844e832587ca38676960f99da8a7bf1cd0861f9476e8148056dc7b1677bf1f8138392b8a896be86688d8e2a852c94abed179d515ea75cbb0f011445d07f5c149c59191cf2e949908b485c269c7237df230bcc66bff9d221b912af14af01101f2fadf75503b90fa5519fabc113be039f7cac24d6a425ff6f4d46c91b75ae35825e628b1d386a81c380eac87e5063ea2fb581058a44e67647606faecb3ace4f6e0b4d2504edf42d9fa09490ca53c59401c5ac8eae407a0f0347dc2b4da0319bb42c74cb71cc24608d303045b7c66b21c9cad1bcefa23190ea9008afde61dacb664d45941c172a551ede9e667bcf095113c15bac9217e1069c4549d3a6fea6aac4b9e2c07142170421a6f4f81ba464c8ddc49887d9f0f7826314b772e1df4dfc3f008e8b2348c48574f1c8e555a987162ec8347d793d0ceda160c9528e793bdc7a9b90cfd1a09081912fe24f588947146e99c46f58754cb16f81f6cd9ce19f510bda9ca936f1ce98af2c35aed4c87e98c38927a095b9b94f741305f94c7930f425336449c2c3208d0e0b74d37e1eb5c1c0311e9a77bf00bf103daa2dfc7d9c3fecba42d41c4ceb332133da641fb6cdc531897be0caabbc3261cac5c59871ca2ac3fcb9449b1cd3903798aa08e0e751d505d9aadec2d2b912094dd05c5d1dac0d405505faf085050662a9b20fe2e3398fd01a9b011dda41a55c740485c8aca06e52f2ee6f62f413b23f3bac9b4da3e93a2fb788f34c085be0128a293e650562444dd139b2bc45aa2064e6f945e1b4ff89ceef30842195d7e3766b5a254a3112af2adcf6c56ac46a4cbf93312e2b6b7ea1848185a85273a7b11251c2179fa802b543ca6922c0656320b0761b883b9c1d75228ac9a7e9626ec76a7c84820b2fcc13e833dd5bdd5d3c8ecb1745f50d831619c895ba3dcd24597bc52e0f6751ddb4c5c55ed2746c69088f47da9cf8450cae2cba98e8a997381709e234c8115f88327f3873e70e5742dba0a8a41ab4555ef75ccbd49812534d625a38086465c877f5cb86d90f3acfe194d244ff85604036251e73be7453d043470e8e87d1df11b6468631457a999e13d8556d28875f80d475e4080a822606e53ab4521e612fae27506829355cd7edd93156f5d3d78038a3cfa19d1b94502097914558d7a99d3e922eb6004f428a319dce0b00be94d9d65e8a891e0da8092cd17eb57f42719d72cf19e8b9cdbbce8788a37b61803d8c5ac9b705b451a16cf3fd4e44c98a697846d4c389a327e75e07e26dde46d96ec5f9cbe652798c6e4c4f071e518ca431ae4ea874c9bfb2c727a572b1bb77a3911ddfe4890f0f4cc152b67e4490953fa74133078da4bda050f5f723492d3f63a9f465a3f8352414b9b24747052268e8fd9f00cbf24109afca71dc10ec95847ccd30693a340ac17a1c438de32c9d5ec779ce6d1eebb68e4560bfc706060c2557d03e5fe55e5c76f1b0ef8e8b9102af101e1b660f5505127a1c513a7bf51a506bbd38413db23366690dde3b7a64ada8b38c3bf5f2a77c993b3c8c2860b9b6f661657e2ed9c3fb8a7f07b9f86e6908ff61e5040beeced096c0d29cb3117bd673c5a13fcc06788b4db4d7d24a2bb143cd675772293861f3b9c8f22a3fc7f39be4019cbee78624119caa0ac94b509b691e516835c3083a24fa948cd62be1d683b9cacaba9117c6a95ba3af20f8df2bedefbe34563d12e24a4fc1c9116fc4eeead881d06ea1a7ecfbe5b50a60da676d03906784c355c572a6639f35bec2e4ac4d0afad8f0b1a5b56f379ba18bb0f5a2239bed56845fe0dd02a29705d5066285c49d0f62b8b7703247af1ca3f3ef80c72d2e73d4b09270a3d63c0ee9b56b60d8422820e7d421d7cb5edd90bd36f171f3a248ca8ba22a0ad7dd4c1094a8d2cad3b5f58c2f6521a82fcda6145f50a67031c99fac5ee33bbc593f697857610ae607fcc8aaadfbfa9ccc8b666c1610fe58638c4b81b39d62ef8ef79e3f3125048e96e178148b987ad8ce75ddb86f226c9422f88311a1e54584d870aff1afb86ba69ddc59fe8f846915e32928ec2b41ecfdcec2ddf6e03483f220ce62505abf5bd046d810d0a73121491fc5845984c6fc49d95fff620acf0239fd782432147be7b01479d1bbf685ad71220496c5fbe49751f43661ad97ffa452dbbf443cb66f47d94219064c1e11d02a35eee965b7f853b387676a12fc1bbc92ca5e3fea2af39b5290131d48ae5ac6b8b3da7f8939f3416427aa7bf23c495532077ccb94f30ba0e14ae25792e182fd3a9c89a136112a7875a582877d067c9994fd79bcbb666c872f3cf82331b4890fd26895337b42a7f957e3da716d9dedeb9563d83c0644780a2f7dc8441b391e23ad1a9d86a23b965931c13d66e41314ea356c15452b8070d12716b40768018b3315c33b00a40c54fc825a77e16189b5850d2f5e73eeac5e1263a0df8a53ba2b1deb5b8e7815c61bd8d70015ed1912fa27498c3f78d908282db2e0ad1a117dad769c93569014fa2500d266959dc58f9d70708000000000000000009fd0f1800000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d000000000000854d00b20e0c80af58e7643d04abbd00763600d69e7ad33429020676adae4d4d341201b3e8f809b84eb46a8e2f6891931ceb07e375c8f7df92beae625f3a2193c77a1f5da52bf64dd15d0231dbb13c5594a519c0eaee2ed23ff4d269a9919b204c3aae789", + "activate_wire": "d705bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848957d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da6e012e565964e65d584adbc7138f1d115f6e00c27a90903e6c1b1657982fe5cff556ddfc432a327526e84bbfe843070d8fca46b05d25b3a0dfccd406ec4f03a2961034010d3510c0a44a53e0ef4a086226bfa039708f944ce4da9833f1ad5e5f3000c0df05779c6f246b9d57721a604df34edb76ca1669844ee5799dc2eadacb6ba6d531254e954c09d705cdf51e5ea9728528cccc4af0bdebe4dd29853e8315e96edca63", + "ack_wire": "d707bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848957d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da67d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a2333658196bbac6ca9362246f559f4c15d5645ef908635dc460bdaea0f3e8e860460215c584b017c77a3a2254a39d5832206367aa74cd262def4e8a819c60a2360b988", + "book": "57d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da6040101e300017305b11a874bbd995b29b11d109eff432402136904a3bab1f7bd9f869109cfc100000000000854d0000c3500000c311000000000000000000002cbf141d9ad3b41938c25083328862acf711291bb47ab53b33dc37898ef9326b400000000000854d0000c3500000c31100000000000000001000362e42ded68912761a397087a1310af972f9022330fbbbf24c580b9e71479fd8700000000000854d0000c3500000c311000000000000000020004754bccfbb8e3ee44224e0290582ada7cb1dcb05ad968df63ac0aa01e81298f1b00000000000854d0000c3500000c3110000000000000000300057c23775d75a9e3e878e5e33ba9cf7da51c78a4d54431bc1981c8524eda36bb0d00000000000854d0000c3500000c311000000000000000040006bcd5fb4815309c1de8fbc2867eb93635d8764c565d952917457874fc6eceda1800000000000854d0000c3500000c311000000000000000050007e3ae54a63b8f9526f7955db3417b5a1f6743b99ad0ca7cc92c3fb199c149b76800000000000854d0000c3500000c311000000000000000060008bd471262b242310a4d1b298156a5fc3469395eb2fa3d28be02b88d8d0c97081100000000000854d0000c3500000c311000000000000000070009397b027bfa95da000fac9dac5d41825bb54a0cd0071a262be00d6d4445f3e2e900000000000854d0000c3500000c31100000000000000008000adb17a978e9e05effc0d7f4c33f7fb78898deec1a0b44212bf943818091fbc09e00000000000854d0000c3500000c31100000000000000009000b0fc65290b128e63d598e654d4a7bbe14cc3ef946f6fb6fe0d5b46cb9e0e2638800000000000854d0000c3500000c3110000000000000000a000c3f951c45254175c92549460b5a89d5d3c61a950e7e366f992dac9f7f7a7e6ce700000000000854d0000c3500000c3110000000000000000b000dfa9d083023788850024c7eb588fae1c7542604d23cc12f44ddd568c2320a754f00000000000854d0000c3500000c3110000000000000000c000e3aa43187fba488b5530dbc366257f82058801f6983509716a9e9bf00db680f1800000000000854d0000c3500000c3110000000000000000d000f7404ba142c5ad30fb6769e7f6a84843d4c437bd50254a72815c8b75c1deb0c4b00000000000854d0000c3500000c3110000000000000000e001028ced94a2fc573f8c251838edfc807be39d1b910d18ede82aa7a127c5888b3a300000000000854d0000c3500000c3110000000000000000f0011a6a8f52194b94c6d0b521756e5c2c20ab5b6da8a2483e44aeb48447112c0680400000000000854d0000c3500000c3110000000000000001000122014e1e38afe84f6da72dbab22108ec05f60555504bac35bbfc7c62d1ac3621100000000000854d0000c3500000c31100000000000000011001313667fb8817a03dd2f5fd97325ffaec561f597256d58742291c569ee251257fb00000000000854d0000c3500000c31100000000000000012001430ec738e4babdd6d0b18b82a7f5a7415074cd283260197c4efe5d439671c9abc00000000000854d0000c3500000c3110000000000000001300153e9e411c4bcb405bda68f31ef974cf91e5dc7a530fa8580048c4bc742805125a00000000000854d0000c3500000c3110000000000000001400167aacb62cd9c823ddf004ca04fb936c3093029ddceaef569190534039885b1b8d00000000000854d0000c3500000c311000000000000000150017a9e7ea66fc54f78b5a4441c5ca6ebaa742485894509dc379783c52d881fad9e400000000000854d0000c3500000c311000000000000000160018063c74693de53572427dd71747f7a47f95a72b83b95f62b60be7479516593c5300000000000854d0000c3500000c311000000000000000170019454a89ae1311664f5155c45544265e5b33911a120db3b683522ac590e64528b200000000000854d0000c3500000c31100000000000000018001adb1d9b5644e6d6b7befb0f222e567bef23ec977fd3d5a6f146b6b195f48384e400000000000854d0000c3500000c31100000000000000019001b54755b708745f22c50b1ded9d0f24bc9b87d93d95046accde5e0d0c1b27ee3a000000000000854d0000c3500000c3110000000000000001a001c1b29bd0c8af340c2ede7c9552fd9410b4c731a47156f54ae2fe2ef6ab170a3f900000000000854d0000c3500000c3110000000000000001b001d3b68781e8a122e02fdbade74d727eedafc80663a79c32726b7d555e730f6b00c00000000000854d0000c3500000c3110000000000000001c001e508a679821f8e0b736552664ffd03360eb8e08acc5ac5c653ce280885222ca8000000000000854d0000c3500000c3110000000000000001d001f599fd0cf5ce9487aae5390f0aa7e2f55bbf36c7fab020879c262c74f0659b80d00000000000854d0000c3500000c3110000000000000001e00201d35e8a88f39201d36bb5d8603822ee851e4f936cc9d941eda8f0c132be674ed00000000000854d0000c3500000c3110000000000000001f00218b0edea50f2efd820966577036831199400b94a6a5ddc1efde5caa77d770fd9300000000000854d0000c3500000c3110000000000000002000228267950cb8f56aee3a11f312d601c4e2f49e229db76eebe9df97b467478fe80400000000000854d0000c3500000c3110000000000000002100233e52b8a3ff4d5f870fa11ba4e1a7ffc5f35310840f33f01da137e5802ad25db100000000000854d0000c3500000c311000000000000000220024bb7507ed8a6155c9d7d58e9798f1822e143da862250702f237267f91bf1f6bc300000000000854d0000c3500000c3110000000000000002300253dbe9c22ae8f8733c3a60d21c30bf7e51a304f53bac7c4a2c6e3cbe61157679500000000000854d0000c3500000c31100000000000000024002698a89d04d4471120348d82a09c6588128fe927d38dfe26d98932961fc842764000000000000854d0000c3500000c31100000000000000025002736a20a0e1e99e0e8f0f9d389acb4ce07130e58276e847f52bf8d17c2bf36fc3300000000000854d0000c3500000c311000000000000000260028f5d5256f0c02a8e76d9de795b0a9a6624e694f2df4d106bfe32fa46b60400a3300000000000854d0000c3500000c31100000000000000027002908853d9812b63820a7c738e5fc15ab6181cd391a059c248b700657d4b21b4e5d00000000000854d0000c3500000c31100000000000000028002ab49ad03151b291227dc42af60dd203c373a9533afdc9875f4c02d08560c4533f00000000000854d0000c3500000c31100000000000000029002bdc43a956e852c354169466af263dd38c4f278c18fb54d209d9b65ef1828cbec200000000000854d0000c3500000c3110000000000000002a002c1c3da0878d813c406978c150f03595e9a26d582ad426d14ef279edd0e8f452fb00000000000854d0000c3500000c3110000000000000002b002ddccf935988fc435b5713186e04f48e53fe514466473a48ad5e0afad0d22fe46400000000000854d0000c3500000c3110000000000000002c002ee113b733aec9e1d33dfd95556e546977a6b907fda9164c86f41dff868d4a2cc300000000000854d0000c3500000c3110000000000000002d002ff6fd342210a6de76e74742ecb52b84f482aeed8d2e0b0a843cd8167b4554f5d600000000000854d0000c3500000c3110000000000000002e0030882620e0b4f1feee1c51202c3b9623135d41499777d1341f63730f98dad4702c00000000000854d0000c3500000c3110000000000000002f0031ba08838987d86618a0dc670de584e3cb9602dd2f60278ea817a31d7f311e634d00000000000854d0000c3500000c3110000000000000003000323f4fce46408992a463e8d153537aa72ed50c22cd6c76648191a9cb4b43de2b9b00000000000854d0000c3500000c3110000000000000003100332f030755b807abfb879b08abe7a8b15adcd9efd2f419214fa5d02d099fda961000000000000854d0000c3500000c3110000000000000003200346918f0ebfa087f6b72399aca062f7bde3577c44fd288090b9ff2718fc6f5cc8100000000000854d0000c3500000c311000000000000000330035f70d71f8c06e27b949b91c969119a7cb85a4779bef0397a4d2a6847b49d128dd00000000000854d0000c3500000c31100000000000000034003631f229a547af63a7d20a9603285eb3793ed667b25f4a22bd8cd94344665f31a800000000000854d0000c3500000c311000000000000000350037d872bb09a3b355b08b3bf759b436cbc36f6d645fc01dccf919454235b8cf509b00000000000854d0000c3500000c3110000000000000003600387bf97f96e2d82c0396dd60d8a32ca53a9034f4fafb590a6f8b3f02e3d16a26bd00000000000854d0000c3500000c3110000000000000003700391d44995dc923077293887ec5dab71deac2055a37fdf727d5a2c0d216bbb5a25f00000000000854d0000c3500000c31100000000000000038003a6fc5c81c3b0c8b25375f88a324a38ad92d72a73d2771886704e24ed7ab53595100000000000854d0000c3500000c31100000000000000039003b64a7f4d976600e0d3670d51fd1f1a08937a5247cd7314e78fc85a6c692cee5b100000000000854d0000c3500000c3110000000000000003a003c304c70efe0cdf3be30823623953ef905d37831a080ae3ae1d0c261f7921ab76700000000000854d0000c3500000c3110000000000000003b003d52f4c9863d3ae76c166247d9225b19a416ad39f7f46ff197bc95f695201dafe600000000000854d0000c3500000c3110000000000000003c003e76fbeb3ff3afa2700541928f23f11d59f8a56d40abd6b10086d70ff568c1b39d00000000000854d0000c3500000c3110000000000000003d003f83a811fe4aa924595c69a33b636f0410f8da6c59683048f6a63d011eecd0fc7400000000000854d0000c3500000c3110000000000000003e0040ab8a8b2d7759b68431f7539de4e7c40fcf22a15cbb3d472dd20a31d690bd696000000000000854d0000c3500000c3110000000000000003f0041c1305b0e61a9ea2da6d693234a815de4e712aa7a38fecf60291bb0e0af44276a00000000000854d0000c3500000c311000000000000000400042434b51045e65ddc0caf75b30495e146c0076fc28951d7396a23ecff1690e2f8a00000000000854d0000c3500000c3110000000000000004100439df059ca0da40a29d8afeebcfcd02cefa7cdef598a2673065f7d55b6617884c000000000000854d0000c3500000c311000000000000000420044ca418d14c97d01e4c0aad582a5174c09a21638342681bbcdf02ef3ff5f56cab200000000000854d0000c3500000c31100000000000000043004537594556a4f9feebc7b9efaf166d36bc7011be23886a2c89e590f9ad2a4665a500000000000854d0000c3500000c311000000000000000440046ad51420b09a07509e900aaa4fcc6cf6bace255fef4d89c31727806446edbba5100000000000854d0000c3500000c311000000000000000450047c733ad0dee32d9a52a4d62c9c96186e1afd55d35f54ddc21fa980f771e17fd7f00000000000854d0000c3500000c311000000000000000460048464b71d7e34e559d518b268791aa82b54cec4fc2aefa34cc66fe9f21eb90948a00000000000854d0000c3500000c311000000000000000470049cc2d776c5d23a365ffcb4604ee65a09cce259c871dab39b04939d4f3d2252a3d00000000000854d0000c3500000c31100000000000000048004a6469912e6512ff7a494d00d8ab8977f1378479cdd5919a687a6b4912af46667e00000000000854d0000c3500000c31100000000000000049004b378f6cfb89eef63eaf08aa5d3f5d83c90698ba63169104be2a28e46f9d1ccca700000000000854d0000c3500000c3110000000000000004a004c09e07c300817fd060a676575d6376ea8deaccbe2eec3714954f953826781831c00000000000854d0000c3500000c3110000000000000004b004daf5d250305bc69ba1cd8af664d439f6469f65361e3f5fe870b765f99bf9c840a00000000000854d0000c3500000c3110000000000000004c004eaf472cbaa57978e18a7beb1aab7b3231d10ca16d6900093ded5c8ac76aee2fdc00000000000854d0000c3500000c3110000000000000004d004fd208f7cc7c79c678fa3d3af63e43f8145e12c509786de6d484f0fa9eefd4df6500000000000854d0000c3500000c3110000000000000004e0050e28a412244c366997dc906ad005406191314722626a355dbbaa982075b3195b900000000000854d0000c3500000c3110000000000000004f0051164032bafa294351f3da469060dabc9ed49a700436b28e33ba98eb19575777b200000000000854d0000c3500000c311000000000000000500052f7c31d1c01ea89f750fd13cea775b223d7b33fb72787d312a5491919dbf1ccbe00000000000854d0000c3500000c31100000000000000051005325364a6509a5cad019874190a2d2575fed1814db0ec63349e0d45da1ddef18cc00000000000854d0000c3500000c311000000000000000520054d67bbaecdc0ceda6c4c73f713e4f2b0720eb3d477f514148f2dac77cc50ea51000000000000854d0000c3500000c311000000000000000530055f6bb6e89201ad4cf91179cb56daa171bd244803fd2d2552f81d9e8613b9a399100000000000854d0000c3500000c311000000000000000540056e8f85ca405596250eaa741554d424ed40359b7bc9a29df7116ecf508708d8a7000000000000854d0000c3500000c311000000000000000550057c87b12700480cfac14a3b6abeee0ce1529d0c8dcfe84a8ba767e97662476653600000000000854d0000c3500000c3110000000000000005600582caeafc4b9bf93574c0f8d4bc94f57a85d9f23efc58c769e716f0d7b18b38dd900000000000854d0000c3500000c31100000000000000057005969126f4e713f05ba71906ad30b5673030fe0d3f82d874741e2dc94bdcea49e1000000000000854d0000c3500000c31100000000000000058005a2bb80bd902fbb168b3121206e194f44db5ef0cbaaa3198ecb971466d128f175100000000000854d0000c3500000c31100000000000000059005b4082cad26a5c16ddc9f61d81b789669510a05281c338443b02ba356ea887c02f00000000000854d0000c3500000c3110000000000000005a005c265d9a20fafd71d0aa9696c1aaa52a8b570d3cd3aecff66d6a2ccbfa71e14d3900000000000854d0000c3500000c3110000000000000005b005dae6967ae7b6549b26551f919039be40948601cf75d580bd8a7273102944d505d00000000000854d0000c3500000c3110000000000000005c005e98476373841a91019fc58c72deef72db55794552bbe06b99a0cc70b545c257c700000000000854d0000c3500000c3110000000000000005d005f82011b8a630ed5a1d31aed2f71d4007347e50c49d0273abc634d77b7fd732c5700000000000854d0000c3500000c3110000000000000005e00600ad83b5a1329af2453f9a24a11729b565921757b15b24342cfa0cd68926626d900000000000854d0000c3500000c3110000000000000005f0061e98244eb7266bac47dfe83ad32ea0aa6c2a71c6c6efeeb6fc94dbec2888836a000000000000854d0000c3500000c311000000000000000600062c0bd14f5ac63c549c1df54d26ebd93b57f93f3610f87dc19b15d6178e97a3c8700000000000854d0000c3500000c311000000000000000610063c56dcdbbffe2ac6cd828e1885083843d6dff6883b91a547ed66ab4315d7e3bf700000000000854d0000c3500000c3110000000000000006200642e89e3b0df85fbe1eff77b8841b90b0fbc491812b75ac167e0df33b212447b5300000000000854d0000c3500000c3110000000000000006300659ed51626c3998d5bb14679fe39d2fc267375317eec386d12d9805e45bd9082d500000000000854d0000c3500000c311000000000000000640066071eda545451d9d4b94201fb5b5f6623d970d3839e46f870d7fa3bf2d2cf7ece00000000000854d0000c3500000c31100000000000000065006793865c1b6f4070c9bc30c2f29286d8eea729414d2828c3827d7add206c22b5d800000000000854d0000c3500000c311000000000000000660068fdf7662574899c10d0b4b4119824dc80a26595b4b44284ab1a6d95bf48da05e300000000000854d0000c3500000c311000000000000000670069f6e8deab009f70f621e12f36a033dc1bf7ce211100ec76ebbbdfbe1a8a296bbf00000000000854d0000c3500000c31100000000000000068006a3c96ed2d4e91b99750437ed50e90544e3ace2456c7fdef5e6d81d1c69797041200000000000854d0000c3500000c31100000000000000069006b64507da352e9f70ab879dbc4a2aa073630b6d61dd0e1eaf5bc215510ae551ed000000000000854d0000c3500000c3110000000000000006a006c523085244700d8bc42a08c99dab05caa6d4e00a0a3e974355e6a5cadd6a8604900000000000854d0000c3500000c3110000000000000006b006d843e8fb4fde1fb78042e83c43096cc68ab22b2d41b945781c674dff62520d3cf00000000000854d0000c3500000c3110000000000000006c006ed4c3b0a4e860f9d09f8e28525c44e2b32684d8ba68228a3f1f79db0598b2eea700000000000854d0000c3500000c3110000000000000006d006fbfbb7a54fc65f2cc087f91d4a54e89c6a9189994fdf807cbace173f150527f4200000000000854d0000c3500000c3110000000000000006e00700940c82246dec163f6bcea0066dc1b8e42153a9a89cb87335bc874fef74954ae00000000000854d0000c3500000c3110000000000000006f007110fe330651d32c31fcab4753b4467cac95586300c264136f8f86aac3721fb16500000000000854d0000c3500000c311000000000000000700072a82c392b6da093a4bd54e33100ea7cd77b5bdcf688b1fb691b474a10e0bc363600000000000854d0000c3500000c3110000000000000007100732f743009fa6915ebadbfbfa27399c3a30f9b29b48e39e8e76a6bef920de3a57700000000000854d0000c3500000c311000000000000000720074815323725370ca6a62de58f815237acb314bc3c4ca763f5c260923f0dd2b4fcc00000000000854d0000c3500000c3110000000000000007300756727a4ae8c2ebb1d033eb969abd7476e90dc6b40b54f75b5f4d6d755679f6c0900000000000854d0000c3500000c3110000000000000007400767e07701c7cde480625ca923bebaa832bc17a6abed269b7b1d3601175c2ff6cfa00000000000854d0000c3500000c3110000000000000007500774173d50ab54b95deae539126064ca1a69a831ac4bfc2c9cc419c6f1da7572f5100000000000854d0000c3500000c311000000000000000760078c8bba7fb3d037affa75e8ab2b6784f404e9ef50f7a76b9382874b24086d5b5dd00000000000854d0000c3500000c3110000000000000007700791cae9402e611c095147a803f5b408d27ebaa9344cb951fb073666fd308fa557400000000000854d0000c3500000c31100000000000000078007afad15542780583e26b83691532044687dce16a82ea097c13d339da899a5ee02a00000000000854d0000c3500000c31100000000000000079007b245471ab91354bcc2c2bb09ab2a2b96840d5447ba9e3b9db177f63bf1894ab7500000000000854d0000c3500000c3110000000000000007a007c9a09a07adac2cf5ee978ef0898161ab40e666963976b0dcbbbaae8a0d7f8b28000000000000854d0000c3500000c3110000000000000007b007d14e2ab39cda053ec7cb75fdda214d03f85c30d9d137083645ec85d70119906c200000000000854d0000c3500000c3110000000000000007c007e768ff392d5d959b68e4eab28b75c719679c43ccc65060d8d9d1b55c56e410b1200000000000854d0000c3500000c3110000000000000007d007faed573b2efbc90729cc17c11f1947952c29ba79945746be3a3635cac70dcbef900000000000854d0000c3500000c3110000000000000007e0080b5f95315fb041ea28586d613d2a976d82d348d960989c7b9416fb3d56104da1600000000000854d0000c3500000c3110000000000000007f008140d95f0c5da8d32977131dc41a05a92f1a6876ca043a638240c2b1c8521f111400000000000854d0000c3500000c311000000000000000800082e58f57a865ff75144bdd08549c83c656431b680ab2e57f55501308f826216b8300000000000854d0000c3500000c311000000000000000810083d7f2320258354c20a9069262030e08e266332394343063bcea0cb1869f329ea200000000000854d0000c3500000c31100000000000000082008467c32697927595a930d79d6af1b4d0ef7eb48eca60a66b0b9295e281b055345200000000000854d0000c3500000c3110000000000000008300857954935a783c0293c7338c17e95ca0e84642aee0b1357adda4264d19d6dfc5a100000000000854d0000c3500000c311000000000000000840086649f6f69ab5b4062727281bfc562602b8d23866140ff820bae3629c28d2b68bb00000000000854d0000c3500000c3110000000000000008500878646a8a5d0c0a6d6abfc5db6d6f7f0675e2e502248d2c9868363efc858272cc600000000000854d0000c3500000c31100000000000000086008837ccf66214c602f0155d3350d8410b27f835f10d7ed7480d38697ee5928580aa00000000000854d0000c3500000c3110000000000000008700894dc5c25b4e7e181bacc5dc94f96cea194f4ad582ff9082e49f43af951f5f2c7200000000000854d0000c3500000c31100000000000000088008ab4a423a735be5b23a37b7e502b1d730cdf511b5f5dcd7e3dac491dbf42454bd700000000000854d0000c3500000c31100000000000000089008bd0300ea2db54c07c732c435c8b390eff7af09d1fec7d4183f26c11af6f06ae8400000000000854d0000c3500000c3110000000000000008a008c06eb3dd61b8993afd0aad49eaab19f9b363acc565df1eee863fd0cf189ae9e7900000000000854d0000c3500000c3110000000000000008b008de2f3402d5ec19a1b0c2df4f3bb8f8375692869e820a1e4071468c85bbfd561e100000000000854d0000c3500000c3110000000000000008c008ea4210d942e844c1b72a9418b1b4b3c4b0f6e514fe3b0f75e3d7df746e95326e400000000000854d0000c3500000c3110000000000000008d008f00608445dec5823810557eb56baddb365cdba4e465f2498abe69cd50b0333f3f00000000000854d0000c3500000c3110000000000000008e0090cd2a4d3bb7d1b6be07dd1f3c90d3cca5f5b3cedb97379673782f5afb23c3020000000000000854d0000c3500000c3110000000000000008f0091254a9a75cdf945525320029f7e8c5aba45dfd5b4a426f346603348a66b7dd45300000000000854d0000c3500000c31100000000000000090009225074aff4e140b03cc239486412dd0a130e3ec93b2648b95569ae55e5782af0500000000000854d0000c3500000c311000000000000000910093d8a8b5c8d165c407816b8eb5e89adb271a1078cdca4f29315161593ec86f769500000000000854d0000c3500000c311000000000000000920094aa968f2a69bb072521c766134d9c1521b5d49a368b0dd00ea3b4e21883f7c55600000000000854d0000c3500000c31100000000000000093009592d8e3576e981f7ef9fafcb12635311778fa6e9bd7544bb502e310113869bb2b00000000000854d0000c3500000c311000000000000000940096fce131a771b67463a1bf5b20192031a7780c14e21a74d4ac5a0c4468a45998ae00000000000854d0000c3500000c31100000000000000095009752267d49a6543212b263beea088dfe20227f97d1cc1a3b935bbfa0e11e47548400000000000854d0000c3500000c31100000000000000096009813496ffa2340f37f578504f289f12a5fc253e2e534f83ed2878e342bd0187c8a00000000000854d0000c3500000c311000000000000000970099c168e856e6948c8eb949d48e53f1c64266341a91c7995547f441d3f2bb7db66e00000000000854d0000c3500000c31100000000000000098009afd09c7a336cc28e97b5ff1e5c83d05c3e68257ddb2587b0c20338833512dcdce00000000000854d0000c3500000c31100000000000000099009b256bb41df47f9dc594b20b7aed566d3457cd3feab6a2ac54021ac61a65b74cd500000000000854d0000c3500000c3110000000000000009a009c1304a8cec2aaa0fb13195fc09a06d571fcc8ac066797be25754ce7da18783c4600000000000854d0000c3500000c3110000000000000009b009d82e93cabb272fbed2f0954226d7871dadbcc06fb772436fe4bd5e79a509dc33a00000000000854d0000c3500000c3110000000000000009c009e67f4fce731aef9e25e456361ebaa95b8dd448180b3780a57dd5e996e155fc12a00000000000854d0000c3500000c3110000000000000009d009fee0a1488e1db16efd197fe3307bb1ef3fecc70be961fb5d83f57293b2389742700000000000854d0000c3500000c3110000000000000009e00a0d8b7f44f79f3825b0898063c9be52e8430a4fc6556380971e45ba2bff87d41f300000000000854d0000c3500000c3110000000000000009f00a10cd85fbd33a658c370afb29f2c452a9c176e6b15b85cb999c849c3e5fcc3571600000000000854d0000c3500000c311000000000000000a000a272fb1ed9a9b17e17dc9252ab1b6c57842adef570518d2ddb9ca68fbd6cd6864f00000000000854d0000c3500000c311000000000000000a100a30e18954a79e2d97b37db47861208d7191933965b8a66a8fa2192c2f03706539500000000000854d0000c3500000c311000000000000000a200a4f2891d485e7fd8fb3acffbe35e13ee5fbc7ab4bd4b856819d6cff8e9b46054c100000000000854d0000c3500000c311000000000000000a300a5b17223b7dc16a717a0143c327abdf0ecec7a56ce9d4a42a187228d4764f3adaa00000000000854d0000c3500000c311000000000000000a400a6e664a80ba167ef9a42fe98f9e60997922edf02ab62b9515f3cc04cd742bbc0a000000000000854d0000c3500000c311000000000000000a500a71dc5c03a9ee981ff70177b153af6073d60b5dd62f266b099e2a9a4d4db5228b500000000000854d0000c3500000c311000000000000000a600a81cb37a70492764ccb4e974c7d238592775eccce4089735e69dddad210d68642d00000000000854d0000c3500000c311000000000000000a700a90821c9c1ef4a9e69d5f508ce86029fdc4601e2a1faeed4a8bbc0617640f5cea600000000000854d0000c3500000c311000000000000000a800aabe06ffb02722e4f7b8b02d33806acdf4fc6050e20ae54bb1955aafd407a8be9d00000000000854d0000c3500000c311000000000000000a900ab7518052cc5fc266960839ca65bbdcb933b36eaf9b2f61c37192e6ba5effca76600000000000854d0000c3500000c311000000000000000aa00ac9ea401167af230c9b049bd02d00e1636f21319cdab24c46937d04d8a4ddb41eb00000000000854d0000c3500000c311000000000000000ab00ad1f1b2f6031fd9c8380f15b2928f042e9645332d6e0bc2f55e06778108ece17ff00000000000854d0000c3500000c311000000000000000ac00aeb8410e7035d2c12ede9ebc2c6bc81af83b27d172db05b66b10c7c7f48c57222e00000000000854d0000c3500000c311000000000000000ad00af1b3fb423b0daed330e204543b2fab0fa9aafff31b387535ad322f5743c7b78d800000000000854d0000c3500000c311000000000000000ae00b02c9d6f58fd85711958fcfd2824474818828d5ecb641bdeed41c988b6eec5c22e00000000000854d0000c3500000c311000000000000000af00b1015bda7aab18ccb03f23a253bd3b94ec400a67d057cadeb092167e45001856df00000000000854d0000c3500000c311000000000000000b000b235eab6ff950e9ec80d4563255908a7ea09a5ae5dfbd0e3c2944295709ecd7e3500000000000854d0000c3500000c311000000000000000b100b3957cfa2365934f72f6559f95997c60304b0a98e3c9e0dc99ab7ea6554bb0a26100000000000854d0000c3500000c311000000000000000b200b41012ec8ec892231dbc8ce89d60f3bb9b375718e460e7b5bd1b8e84631e291a9b00000000000854d0000c3500000c311000000000000000b300b5ca5d940ca7b8fb92c4baecb5a6e464d3c3b5aa5c12e67d9e70650327b858ebcc00000000000854d0000c3500000c311000000000000000b400b69d5412f7b492ac0203d7a82aabe23f8aa7776d173df7da0afc42147476614d9100000000000854d0000c3500000c311000000000000000b500b711d3ab9e16a2c977acd6d131284d2fc8c0d1af94dd6191d6a84344bc4722135400000000000854d0000c3500000c311000000000000000b600b8eb9a2b0b9a696572dfc2adebd5f9177abda3769f294babd21480a6839801474b00000000000854d0000c3500000c311000000000000000b700b9078be2982828fed4bb25781dc9bccaa624f6058c722eac222b4a659e9489edbe00000000000854d0000c3500000c311000000000000000b800ba819151088a9e7aada910f638ca1a248bc94b4878b1031a41c101af8f35c9aef100000000000854d0000c3500000c311000000000000000b900bbf28de95ff9677f064bb695651008dd14631262188ce64c392f192d6efdf8d4f100000000000854d0000c3500000c311000000000000000ba00bc9ea9e5ffbeda1fd8846698475c057567d1f2c133c6d548f3e9722b3b8b9ec69600000000000854d0000c3500000c311000000000000000bb00bd0689a54e3457da2e4ec2a9b07cad6b256469eab859e2e8354b27bbb9f08c3ca700000000000854d0000c3500000c311000000000000000bc00be460f9ac1863a0d05133befb41a245dba3e136fe3e9de43e1000316991d135c4200000000000854d0000c3500000c311000000000000000bd00bf0006cb6b8ace8eb6f74e97e484bb283a123132eb4976f0c665de5394ed0b022100000000000854d0000c3500000c311000000000000000be00c07b38bdf8c89b9b0db4eb62ff10e98308e9ce8647200922d8d790b7dac81bef8e00000000000854d0000c3500000c311000000000000000bf00c181157bfa3420a29f5124ddf629161d0b986616e8aaac86278b838b7caadd850800000000000854d0000c3500000c311000000000000000c000c2b67b3796c8da1c71ac82a89c2fa7a8b40e2ee560c6df7ac42cc99e51645918c100000000000854d0000c3500000c311000000000000000c100c338b2fb62c7aa54b15597b8422142cd49a776092d55f92d5dad31e417844c63b300000000000854d0000c3500000c311000000000000000c200c41e7701adb1fc46ca8f1c24d0e24cc40fe7d9daff7deeb63211fbf5bc1c123c0200000000000854d0000c3500000c311000000000000000c300c5e2cb25607821cfb7e67028ef96056f6e3f81f72db1bf8851432472936bbc647600000000000854d0000c3500000c311000000000000000c400c6d9392204247dfded154f8863582e44f0acd0a30a3bc8a38ab35ab744b73ca18100000000000854d0000c3500000c311000000000000000c500c792e42b479904bc5abf82fbb467577afe7f53632f5a8e89be6791a36b49c326f500000000000854d0000c3500000c311000000000000000c600c84412ac310f1fbc90220101c29f748304f9496740c3eee14b64c8fc171558352700000000000854d0000c3500000c311000000000000000c700c95fdd380ed620c5abc8a4160f18375928c0ed6a926043eaf91365dfdefddef93f00000000000854d0000c3500000c311000000000000000c800ca0c8edc3204a6c47c4e9115755a24c31763efa78db17bfaa9645aabeb4972c89a00000000000854d0000c3500000c311000000000000000c900cb83e02383dcaff8714d79fc6df28563584f79be4435bb3632958eb98649d214e300000000000854d0000c3500000c311000000000000000ca00cce136d56b460bf32ae200ce0fe980817a2bbc78112c9969c70db679c2aa30d73b00000000000854d0000c3500000c311000000000000000cb00cd4cd753d127fca8a62042ce2dae9cf035561530b3e2e95f60fe74245ce293dfb600000000000854d0000c3500000c311000000000000000cc00ce2ee0ed4ba29122e130bfd9a32626fd46ac15ce11642d63e5c596b7d23d6b142300000000000854d0000c3500000c311000000000000000cd00cf92b59998dda902b1005f23746254e625f4a70103d02b0ee3cd7cf6f19425188d00000000000854d0000c3500000c311000000000000000ce00d0040c9ed7f1af1ab9d41814211ca770b40c250364d4a26bd0d57b4d6de3d406c800000000000854d0000c3500000c311000000000000000cf00d1cf54bb912bf745b8761c10c6ed566edc96e82ad8e7a9bc3fcd22fb0e3974cfb400000000000854d0000c3500000c311000000000000000d000d2a81886dd94579a2ebe3c14379490aec6fd8bc8bcd7981094f23011e1690bf6a800000000000854d0000c3500000c311000000000000000d100d353b474acbb9539a6631f51144498802e9eeb237d8ba7d11ac050c4a392129afc00000000000854d0000c3500000c311000000000000000d200d4c08ec36aaac6cb7b82a482059dd448f61a9b25249bc5d4e6be5517baf5f5e24200000000000854d0000c3500000c311000000000000000d300d5693de4032c7083eed7fb1803c6d5624a7e32d3107bd7a3939cc89a083388264b00000000000854d0000c3500000c311000000000000000d400d6f9756396a45f40c30da3a0a67d4dc1838b20a098e5351a1e4f344dbbe00dcb8600000000000854d0000c3500000c311000000000000000d500d7780b34031a24731ecf7e0e6844097a8e62b43267d565dd6d3a4886e55620678900000000000854d0000c3500000c311000000000000000d600d8bcc1624846430d2de342f54d7b8e61d264bfa39528a1fcc63532b291c994c2ab00000000000854d0000c3500000c311000000000000000d700d97a217a4e347a614aa9e651bd234eb0c3d49830a6a804e7ede79c77959ce14f7300000000000854d0000c3500000c311000000000000000d800da1d361f1504f6625c8289222276f9bceafa3281738e27e15f6547884c9ca7a5dc00000000000854d0000c3500000c311000000000000000d900dbfe2501594d57b4c25674b575507428defccdccfe69543a182575dd4817b9179d00000000000854d0000c3500000c311000000000000000da00dc45eebf5678f6a340be93a51d2e263e764839c2454a10bf8853fac76861923dc300000000000854d0000c3500000c311000000000000000db00dda5d81abfe6b8ac5e302b89b6057f782aad54a65adb51ed77e50d3a366db9d7a200000000000854d0000c3500000c311000000000000000dc00de0e6556de4b9b77a9d866d3bda9f4a87208766b0cc701414a4deaac8da2acfe5a00000000000854d0000c3500000c311000000000000000dd00dfbc0a8294b1cc8edafc68b3a95d9263f1499a02d63c93a0fe30633ad556a5b44600000000000854d0000c3500000c311000000000000000de00e0a1078aac0d34309b68c23dcc3d7da53e7f88c24e6ed76850021ab5ac808d6b1a00000000000854d0000c3500000c311000000000000000df00e1412173ca93c1a3db66b0695504c0151d21f1e65e417d5671dde59931256ac4fd00000000000854d0000c3500000c311000000000000000e000e2e601a904450d4f2e168e917bf4438d900cd17f571719d60bb900fb0e0740d65200000000000854d0000c3500000c311000000000000000e100e351a8a26ec67cb3ba4b95f2ac4be486745285e9434dcebd8f7df9f743dbd3653000000000000854d0000c3500000c311000000000000000e200e4d20396497388e26286a2a9bfef575a2269ea85b36b4005d294302aa05375862000000000000854d0000c3500000c311000000000000000e300e5abd37177996abc69e2ca24b121f45b32718a095250c0d533a89785fbc1692f8100000000000854d0000c3500000c311000000000000000e400e69491b3619b64e09337ff2b31ed0a98c07d47c910346fa6e43ff4da9c16a4317000000000000854d0000c3500000c311000000000000000e500e7fa38ff7b2f735c6d7ed6697968cc2792e9ed4c2cc9957e2152c773a23e10969000000000000854d0000c3500000c311000000000000000e600e8d82876021b20bad6dd3833506b1e2c7ddb85e8fc754b11f0099c558ad676db0d00000000000854d0000c3500000c311000000000000000e700e93f7a7c70b38e38652fe97203085488d82bc013586ad8a093aeceb4deaf2ee93b00000000000854d0000c3500000c311000000000000000e800eac0e5f3123292708627ace19965a0fb622f8eef55e33dd21febe02b2fb6dc202500000000000854d0000c3500000c311000000000000000e900ebe9c9a099ac9f0603aa645a573caf5b99f8eb03b28e881010b96d87e1ed55ce3400000000000854d0000c3500000c311000000000000000ea00ec1d875edf3e4221594917fca299d02b1c3f1990f6069f944330c3b2ad9edcba7b00000000000854d0000c3500000c311000000000000000eb00ed48640f071c6bcd7c1a94314bd8615a6f6493919fc216a555b9dabca0121775f500000000000854d0000c3500000c311000000000000000ec00ee457e830ff6e6eae43268bb92d5fecdbf8832a37b575285b1e113b29ea7e5f8c900000000000854d0000c3500000c311000000000000000ed00ef55b2b3b16fa963985019f4a6cac17b51fd164b85efd22180221311ba043e168800000000000854d0000c3500000c311000000000000000ee00f0eed5a265e23b6fee64a6800737f859959582dae01bf804141b7dc199139dc15a00000000000854d0000c3500000c311000000000000000ef00f17920b76551c9c0381e120a166e200e20e3b2e0d54267bb3c9f69a0a374af5f5300000000000854d0000c3500000c311000000000000000f000f2550cb6ae690524fbf6a6845f2df5da2d7e4aacc218263bc216db4be3c269ae5200000000000854d0000c3500000c311000000000000000f100f3b3ee1187678512ea1324b3e746df8c76d52008263fd5ed6ee3307608b02a7e6600000000000854d0000c3500000c311000000000000000f200f4c0c14618d3cc34a24260fcb5c7819db74e65d8eca279451104abcf31eb27a7fe00000000000854d0000c3500000c311000000000000000f300f564108c7261a446b472fa45a0c29af4b821210c8aabe6c113d292b6300224dc2c00000000000854d0000c3500000c311000000000000000f400f6bcf946cf2868a4749a156c578a4a6e40a2a7bc1716b35880eb9d87a4748ae09600000000000854d0000c3500000c311000000000000000f500f773465d6084beef0010d1ac1e7e2fcbc6783d854f3927e6d51f272c52bcc6bda100000000000854d0000c3500000c311000000000000000f600f86d175c4b0f2a9dbe8437a34789c0343991526bc83bdece3c71963f86cc51183a00000000000854d0000c3500000c311000000000000000f700f934b71da18c6f995f91aac176268d6ac0a3a2acf2717801ebf23b9d91926e914400000000000854d0000c3500000c311000000000000000f800faf380ea1d68711d9db0ab45e3397dbd39b5e728dcec8d55930f6f2affb0ca12c500000000000854d0000c3500000c311000000000000000f900fb834434e1f82adc0b7ac0bd9ec0d4179d36e2a611ede605a42977de8a9e466c4500000000000854d0000c3500000c311000000000000000fa00fc3479e0daf8b53f8c04ba0eddac3848dd88ae4cafeecc59bdec10b881101c883b00000000000854d0000c3500000c311000000000000000fb00fd61b82153bc8cf58c32d180d4f46da8f40ca6d8a3fff5ed4d5ba16a1fa73d4e6700000000000854d0000c3500000c311000000000000000fc00fed00cfde8a915c23cda075f4a88dc0ae74792bd8ece23dcfdba72f56b9813b0eb00000000000854d0000c3500000c311000000000000000fd00ffd559de901dbb27927b10cbb56d57e3b244a9f5c248ac8d85221547ba9dabfb3d00000000000854d0000c3500000c311000000000000000fe0100c68cdfd7846524e752cbb7ba1aa0e495352325399e03d91a7016e3671fd964d500000000000854d0000c3500000c311000000000000000ff010191141f42588c3501522fa211cdffd484c7889c2cc6716de25654169609657a4900000000000854d0000c3500000c311000000000000001000102e6ef1453006abcf1da0dfe1572ef43a2082f91f230b0f14ce55a6361e8650b9200000000000854d0000c3500000c31100000000000000101010312266945cef53747caa27b2ecb733afa5afa42bac3afee722206d2d0c5abb81100000000000854d0000c3500000c311000000000000001020104c7ca46af876947a45686fa1f11f8842a6ba14ef982a40f5138bf420c6f73176200000000000854d0000c3500000c3110000000000000010301053f1661f5435f16624c0e51a7a644ece8fe3d7f84af61a410787b30feeeb966d000000000000854d0000c3500000c311000000000000001040106cadaf4cd6e8d42db0682daba7a65e6a51434b843b5cf75213ea4427e0319f4ef00000000000854d0000c3500000c311000000000000001050107bc7d18e01e7f39891798b254ec5fbfd4066bfa49c634f8e3eac8fb5329aeb74300000000000854d0000c3500000c3110000000000000010601089cc3be9f123da1cbb918809e10bee97bdc9fa805b06c557bea2210471fa3307800000000000854d0000c3500000c311000000000000001070109f1a7aae9140ccfd98127b56044d9104e9de5fbcd682859598eef0d6f9ca92aab00000000000854d0000c3500000c31100000000000000108010afaa9ff70179a7b0eff8bef5d45379521f1110e8db1ee03b96d579764c8c344c600000000000854d0000c3500000c31100000000000000109010b07d1b09149d5c5275f85a10d8cd9202c9400ca2def4ad6791075291b4c3c5df300000000000854d0000c3500000c3110000000000000010a010cca26248920d3f7592d14d56435f07ccaba5650dfb6a88e4e39075376402718a300000000000854d0000c3500000c3110000000000000010b010ddee81eaa499f230eefdac7766ac46fda3d15ce50ba634d5dddaa37ed8b27706f00000000000854d0000c3500000c3110000000000000010c010ecddf241b4264b637a0ac3315b33d20ad2b4ee78abe9b109cd2ff12910527db0500000000000854d0000c3500000c3110000000000000010d010f2073acd612d2f8b24c44a0d791c8e458cb4209a578c5e54059a80bf078ea62f200000000000854d0000c3500000c3110000000000000010e0110f2bc07ca3c0917130c0549bb4528b3d30787e8a58e1a4e395f797ee1d5313c8600000000000854d0000c3500000c3110000000000000010f01111adb3bb2a1d5a92c063c04d8e4ccba90f31d32705ab9e5bf9937abcae9bd411300000000000854d0000c3500000c31100000000000000110011216d6fc2b34c08eb4c4047768a720a11c236f83a193eddbefb5f177811e1cb74800000000000854d0000c3500000c311000000000000001110113f7ef1e699a40d95dcd4c155bfe82b2458ffc85aad59e9a7b9a38b6f8e20129cb00000000000854d0000c3500000c311000000000000001120114ccd2d776df1d26ee861943082701bbb4b69249fadd8f143b597ac73e2af5c1c500000000000854d0000c3500000c31100000000000000113011556ceceb7b569011a449af0c305c8dbd28c6f9703aed425f8373c8497188b2b9e00000000000854d0000c3500000c311000000000000001140116b2f5712de794c52523a62778e83e0e59a1bbc6f06f97b7789670f0e5039ea9a100000000000854d0000c3500000c311000000000000001150117693cec364193e49f8f6aebf42955d1b49acda6c888a3a6e2ee274e372a45494b00000000000854d0000c3500000c3110000000000000011601184ebc0b53de00bce7dc1119c28cbf0648dc7ad74c976e4b49b1e5ab6d372fe7c800000000000854d0000c3500000c3110000000000000011701197dc46f6965e74bbaeb7789ee532264e843103a39d7a9d75132de7e9b5ef02a5300000000000854d0000c3500000c31100000000000000118011a00b1c69e8c2aee8a42f849b56150603e0541ba97656911b2bec92fb6b80e1c4700000000000854d0000c3500000c31100000000000000119011bbe8e25448c1ed1d4cf6851269c121e1c45e3cd58292714ca7a0afcd8c6813dc100000000000854d0000c3500000c3110000000000000011a011c42ef09220765bb717aad3c2d4dcfe180e8c0ebd8421df820386626d3a85e4d2900000000000854d0000c3500000c3110000000000000011b011d595e3312d192af31f4735e7352203a2b3e1ff5c8712247ea76b8d6521328a28100000000000854d0000c3500000c3110000000000000011c011ebb01550134b55966c3b8a175688d7fbd913988eb37701053abdebcd07b2c0ee200000000000854d0000c3500000c3110000000000000011d011fc8b44418ae4ad010eb4a16252265eb6e1ffbb8238ae74055e93170d5aed2959900000000000854d0000c3500000c3110000000000000011e0120996d1a0367ed61db81a131b5b163de5d6f2f693055b87a3cc7a0c6a15bb72ef800000000000854d0000c3500000c3110000000000000011f012126b6c2e89d781417985751c1fde5cd6069167fce4c56138370bcaa24d03ce24b00000000000854d0000c3500000c3110000000000000012001229e7af1b72e21a2785359677d3c101e9029b117a43dba617f8ea56d7dd022d8d700000000000854d0000c3500000c3110000000000000012101232d2e1c3b68cf4b0e8bd492eb5741cf78a3b36847fa0a97c3304a95dcc7a8a14400000000000854d0000c3500000c3110000000000000012201240e06ed3926bb95bf57bc7ee2c9e5f1c5bd661bacb1ce5a1a2deabe6736c8367800000000000854d0000c3500000c311000000000000001230125a6abd820de07f5cda1819cd9a9146f0adf4d8dcc60e3cab50bc5e1abc14891e200000000000854d0000c3500000c3110000000000000012401267ac610ff9d6969b53d28f85a92a1fad522b3bee82f76f010f45ef0a3e4c7c15d00000000000854d0000c3500000c3110000000000000012501277255670d53527fa3d330df360ac6c98d49a6a5ce3f6fdf1de4ca700844c8fa3e00000000000854d0000c3500000c3110000000000000012601281d6d6e357f1227e5e942653388260e8238b5612f4927fbfdce258bc4d0b7594300000000000854d0000c3500000c31100000000000000127012954e9dbc5cf7149868716b45dbdecdc449d0b39b5eafd69e3b49a7ed29d4459ae00000000000854d0000c3500000c31100000000000000128012ae285b863674b89d72cfb884f5cd5eb523ac0b493661c58f7851c5547a959ed8100000000000854d0000c3500000c31100000000000000129012b819532ccb84729849fbc4cce0ac5f8236350c3b67b316338e15cc62b954d30b400000000000854d0000c3500000c3110000000000000012a012cd23d76e2a5311b80e76c5229b01cb07f2bb85646fdcb4c62adc82f198b047beb00000000000854d0000c3500000c3110000000000000012b012df661b595023ecb62cbf93a64d234dbf889166b48fa8eb394bb9fbe841e1cd56600000000000854d0000c3500000c3110000000000000012c012e0513cfb510f00b170cf080b3ce2b850cec369d35c0550eff4b8fb2b6c91dfda800000000000854d0000c3500000c3110000000000000012d012fb9d1d054cf18aae245c31e11c0d122ed5aadd8a3b35ad39dfe30ac8798a46d2300000000000854d0000c3500000c3110000000000000012e0130e06b5899ba907b2923ca8677fa61c7f0943cdd7c1c40def5d83aad630bc00eda00000000000854d0000c3500000c3110000000000000012f0131293692db9029a74afbc68d94e574decdd9aa7f388e8aa344379018c4da18256e00000000000854d0000c3500000c3110000000000000013001326c2b3b309d75e679971f71daae4c69990da8707c359765d2c1f74a56ea45a25100000000000854d0000c3500000c31100000000000000131013391b688dbb1263e7d9c8898a7356dd313e14f066a6db7dbd8a05af623cfb1423000000000000854d0000c3500000c3110000000000000013201345d9daa987405ea5c478cc99679df047e1cca5d28b2ae550d88570c9a7b361b4600000000000854d0000c3500000c3110000000000000013301352d4bf0f568c72724e502c4ea615d9bdfe76205af4649db3381bb039a6c81d5e700000000000854d0000c3500000c311000000000000001340136387afecf394d4357604f540282bbd442f84f456c5ff810d7964941b31bc12f5f00000000000854d0000c3500000c311000000000000001350137b19741e64da87111ec3ab8a714db12a89a0e19cf667a0049a9dcd72aa5c30cfe00000000000854d0000c3500000c311000000000000001360138041c4bfedd2c41b363acd3fa746b0e65820abf0259851616c147a6253982860600000000000854d0000c3500000c311000000000000001370139bd4d8149d173108a9685d7305f1624dae2c8dda08e475d90494ff93e3b68d3f000000000000854d0000c3500000c31100000000000000138013a6807c6439794cc48e36826e845ad919d758bc0308611f2b2063f35e59badcc2800000000000854d0000c3500000c31100000000000000139013bb94b58b11fd0a16ba852395acedb03563c9992103fbb9c4541c0140ad25bba0d00000000000854d0000c3500000c3110000000000000013a013c98f301ca34257ff0bb60f8a29e8c2e75569f12c2e757cfcd86d441987754db9800000000000854d0000c3500000c3110000000000000013b013dd33a298ca334510f32a78b0d175ef81aa1283c5a43a0a36997337a188cf28df100000000000854d0000c3500000c3110000000000000013c013e6535f74d7b79af5ab10020827567deb1d7feb0d68c97b24375444fc91685c3d400000000000854d0000c3500000c3110000000000000013d013fcd31ecebd30f110cd6faeddf43207ca9ce128876a99d882d1eb2956ac66baeb600000000000854d0000c3500000c3110000000000000013e014080dc9f243390f1e5ff5f824b3558f3a71558ee2b76b6dc85b8767d8c3a762a9900000000000854d0000c3500000c3110000000000000013f014135e665b8a599899c0e0c8e20d270917ab23a1c5ec1392031cda458edb1c95f4700000000000854d0000c3500000c311000000000000001400142f390439757563240273ed69da2e0b49158ffac612227c538ab4df21e668f072400000000000854d0000c3500000c31100000000000000141014375199d7a5f1d5702a5e97030d9b85af5459e59ece07c34cf4eba44fc2fbe75f300000000000854d0000c3500000c31100000000000000142014488eb334984eb5a14d76478f8c7e277804cb813c573fe185e1269a0b1b3a7c4af00000000000854d0000c3500000c3110000000000000014301452934a43425a844e0cfcdce1f191ac83c216aabfe9cfae82ad8fb80e5ed32fd9500000000000854d0000c3500000c3110000000000000014401463e4182cabfe4bca69a2b2d26394b851e6f313fd7400e34f66b207fa7997336ee00000000000854d0000c3500000c311000000000000001450147beca3c2b2abf74149413af897d503c0940a04e65488cdca79adc80f3a1022fa700000000000854d0000c3500000c311000000000000001460148358cc7525792ec60cad166479beed863fbda52d75a2b3b21b44cb3948c74c3e500000000000854d0000c3500000c311000000000000001470149084911fc1e172537175b5cb1ed8e0ecae73beda0b4e5e13b826819e1dfeab3d900000000000854d0000c3500000c31100000000000000148014a415167f1a6ff33eb7eb3a208895a8bc11057d6fc0e3d3cde21eb23a9e8f0428f00000000000854d0000c3500000c31100000000000000149014b6bd1174aa697898fbf5fa97167d246f94cc930add143f6b5f3ba1c189160fe9900000000000854d0000c3500000c3110000000000000014a014c630f10f3c50d48328bf9fb5ecc61598d7fb7da587fb7821e4b42151afc4ee36d00000000000854d0000c3500000c3110000000000000014b014ddc396d879f620ece1af21eb2ae7af3ccb63520b7e5c2c86ff9704ecfeae2897300000000000854d0000c3500000c3110000000000000014c014e36074edd4eb00de98fd3f92fa28e76e19abdccc7183a427e0f80f2545099249000000000000854d0000c3500000c3110000000000000014d014f329bd6b4a05c40900c16d963d4cb2c32cd3b866fd2704809359d4e7be2b4d3c900000000000854d0000c3500000c3110000000000000014e0150e0d278e14a883573033cdcdbfe68747f69c02a77aee2631a53854877cd977f8c00000000000854d0000c3500000c3110000000000000014f0151cc619d317d853e8a8c9605c6b2016c5b77d1ed0ce2764865af5bb58da60d25fe00000000000854d0000c3500000c311000000000000001500152091da59c8e15c2d0b67a0c03241ed38bcaa08afe611e374ff56a04b1493496c000000000000854d0000c3500000c31100000000000000151015351d3aca70ad269740eb8143d40c40ccd9655137e1a797db99c19f0c74496eb0200000000000854d0000c3500000c311000000000000001520154e8b3f8186945c665a130ce8bb5022be66ee9a82d58bd04995dc2ed8817b9cf5300000000000854d0000c3500000c31100000000000000153015582bbf5437a38dff5538c0853418b67eed524fd6ec6730af6e83fd7e1819aa9a900000000000854d0000c3500000c311000000000000001540156126f1cdc86e71f3a885ba96d926e619de1dc600a5e917556b59349709a09202f00000000000854d0000c3500000c31100000000000000155015742f8ff596b8d24eb09eadb09653801d690e24c1fd00201534185e5d76616e75100000000000854d0000c3500000c31100000000000000156015838866d0eaecd45ba609331dc127de1cc6de73bedf6c8857b39d7bca574d5a95900000000000854d0000c3500000c311000000000000001570159fc5edceefdd5ba830ee696fa281bfaff5f19f03a517bcc14f2a723cb43db17bc00000000000854d0000c3500000c31100000000000000158015aabd47e25adb0dc27d1c5a063dd640ee22ac5716a1dcdbe152c27c2a4925deee300000000000854d0000c3500000c31100000000000000159015b44080f378bc7003840f1d01437c268fb20c7f9bf4b38cfaf1b18bbcdf7368fc500000000000854d0000c3500000c3110000000000000015a015c130157d7beaaf191672619dffdb99204c2ab01b35d299c29b4a4c9f1364570df00000000000854d0000c3500000c3110000000000000015b015d139c7812864191abffe378364b46c4929ce9f354e57994e56ef8690a9adbdc6b00000000000854d0000c3500000c3110000000000000015c015e76b343cf768eea6ea9430c144d1f833d2fd19f01a0b8d04e09ad0625c0c1ce4700000000000854d0000c3500000c3110000000000000015d015ff182f63a888b037893c449126093bb70f97b7360b903cc02705619b8232cac9700000000000854d0000c3500000c3110000000000000015e0160ddb46b199bd616225b980e5cf964c647898a44241ca45335f679143b0097d48600000000000854d0000c3500000c3110000000000000015f01619d4246b117a38112ba1bd600e78490ba170a1d1307043f3c83c2a1a2d50106c900000000000854d0000c3500000c31100000000000000160016205f692aacc895ec966a4c41045525845352c1993f7d5fad4f836f286565c62b900000000000854d0000c3500000c311000000000000001610163051eb76af4a34d66106771401412f01f4ac661594140355541f815f5e1e7490300000000000854d0000c3500000c311000000000000001620164b7e76d9101f61768d4ea6470dbf632941ad250c20d2ed9b3a9b62fc2ae398c0e00000000000854d0000c3500000c3110000000000000016301655cf571d20737997ec5f0f856dc59a1104d5b153b9377cab0a3107dcef4ee1c3400000000000854d0000c3500000c3110000000000000016401660bc27dbed94378bb13a6b1bc1a61789be9572d1d42a7611338d5a3e1351768d000000000000854d0000c3500000c3110000000000000016501670e63f7f71c19b7eda6368a15dc5dc2e7295541888e60738db15b3cb79292617000000000000854d0000c3500000c311000000000000001660168144788ed1842d11dfc84d06d656f2b8390e11778066f21d5f97626bd72b475fd00000000000854d0000c3500000c311000000000000001670169c4642f1fc647f5c8efe7acc255ba6bab55d280a095546aaca38921df1549375800000000000854d0000c3500000c31100000000000000168016a1e52b4f91a493d81e0ad67231f835c8f1ec2fbab50d1f4e5e324504000ee244500000000000854d0000c3500000c31100000000000000169016bb037cfbfe23395592f82f1ee8d5eab8a2277d8e3644908c775b16396774338f500000000000854d0000c3500000c3110000000000000016a016cd3781d0b608c534512544055182fe979fc8d439f5c49b64cc948bd6f7b6314df00000000000854d0000c3500000c3110000000000000016b016d800b2593788c198f7321ce6424338ef7e83123e241aa7fbe8dc38e7c1e3de86200000000000854d0000c3500000c3110000000000000016c016e97b0f44eae74f3a3bfba8c15b94663e93067a575289e7034c8ca6fb8fed4d1cd00000000000854d0000c3500000c3110000000000000016d016f87ab47f0d8959d60eb4147714356816f6fc95b0fb1f92abfb087f9fdd9721a6000000000000854d0000c3500000c3110000000000000016e017015c8f1a325c0740647aa9af8d6f41b9b364f2d6509cba4c318784ab5f98664fd00000000000854d0000c3500000c3110000000000000016f0171dd3765c9c21ac14cced443eed9254bb76cf595dd19763bdad8f3762118180c3300000000000854d0000c3500000c31100000000000000170017205dc8d8d03f1d1b79383291c9b91c6dbed7b5868793a94d86f8cd0b02ea9dffb00000000000854d0000c3500000c3110000000000000017101739285e8ae6dac04fec83f99de4cfd81186130119af91f3af4c2c2ed9556cd73b800000000000854d0000c3500000c3110000000000000017201745f9b9c5c46848b51684d15e7a018c16c7433251d4e05bf0a6fbb1c22e26624eb00000000000854d0000c3500000c311000000000000001730175356f2fcca2d099ddf402b0f28922c8be5276c406fb6a262a10e1ba2d37c1310d00000000000854d0000c3500000c3110000000000000017401762361231644dae69b1d58f83ac1795d89062f1d20f1e18bc58a38beeab8a143d500000000000854d0000c3500000c311000000000000001750177fc410231f56ded5634a2b827172d7ff54326b4d9ba55aff6f76dc0c1d8abe73400000000000854d0000c3500000c311000000000000001760178a3ce42369319689c90263bcf693655b2b5f73426e4299bb3edd27a61fcea84d600000000000854d0000c3500000c311000000000000001770179529f4cfceb408d8e4db8a93a81948458df679039b6b286ef8e2d6b392e584f7b00000000000854d0000c3500000c31100000000000000178017a652a19f302298f2ddedc5bdde42af0c1617f981d5942d33bd7bb436780ef950500000000000854d0000c3500000c31100000000000000179017b78799b9c840639e4d0d50fd3e6e4728c62c0bd0378096552acbbd4096572acb800000000000854d0000c3500000c3110000000000000017a017c8eb8760d4cf5b9077c2871390b10c3af43aa24de7873c145ee0efb6549c7d6ec00000000000854d0000c3500000c3110000000000000017b017d662d3d9dff9fc1daee14bac71957f258d155d3cf5351507f7f9c75f9fdf9750100000000000854d0000c3500000c3110000000000000017c017ee0dd1b9635b59c05c79911a8d80bff18b7575e6429c8d709a869e6a06e04c30b00000000000854d0000c3500000c3110000000000000017d017ffc1ae8163736881a06d397844694f4c1b611b2ec421e3e85830e6a0b06e7626400000000000854d0000c3500000c3110000000000000017e0180103f4ec21762e94c3a6176166b0112f1dec7ed9cec100950fed6956966a114a700000000000854d0000c3500000c3110000000000000017f018147369da11f17dbfdebe6329cef69ae7150b9003b2a03efda39443b9958f8154c00000000000854d0000c3500000c31100000000000000180018293127c2a56004f8d0a7a8fe5866174cea89697bf41df5e122ce7a23215e2d60400000000000854d0000c3500000c31100000000000000181018325e789b4b54bce1b239c8f012e156269e109a865bb46cf6d615412a6bd1e060a00000000000854d0000c3500000c31100000000000000182018415d04d386172988a10fb74a66f2edb3798cae4ae180cbf1b110e1ce447d1cded00000000000854d0000c3500000c311000000000000001830185d14b28e04b1008b7716368751b7281fd1c56efb14c4bda843b2e3e56a6dd167b00000000000854d0000c3500000c31100000000000000184018608135c514ba440467ae403e0a65e81f359ae4241789416377f7502ce739f6c3000000000000854d0000c3500000c3110000000000000018501874d1153cb1b5b24b1d021059395131e760ff239f5ac04487fabed4265acb7f43a00000000000854d0000c3500000c31100000000000000186018859c506c7f1424e8f6a02cbb4b0488a8253ed13971bd35d9857273fc03d9c2e3e00000000000854d0000c3500000c311000000000000001870189defb781b52260a9d6fd63130f1b8d6d603e4247eaae108d635e2e5bc04c5382e00000000000854d0000c3500000c31100000000000000188018a5335d54f7383e77089b8fdd6652f3fef12fb0f70fb2a84d2affb193bb1b7095f00000000000854d0000c3500000c31100000000000000189018bbf3e5a3c2868086d150869804d22d5da63dba8992833908943ce66f90dad171a00000000000854d0000c3500000c3110000000000000018a018cfed9687c86989821940a117f7fc601ff678677a12178775caca21a09249da22a00000000000854d0000c3500000c3110000000000000018b018df8ce826220eef839cd84d2b9df12a0f0ee5806b4aa19c173a92308a9f084543100000000000854d0000c3500000c3110000000000000018c018e1921efda69bc2cf34f1e47081b44b6400291eb927f4980a94f3134b111a2523200000000000854d0000c3500000c3110000000000000018d018f37c353aaaec137e0c9db2d33427135e6f8473ea35384b6a3eb206af450916ed100000000000854d0000c3500000c3110000000000000018e0190f3a2a2e0cebabac99e3d67147da573f39fcab1746d2a05d2e22a060af92efe9600000000000854d0000c3500000c3110000000000000018f01919375113bf780682741b62602e90cb9b3a50132ef3443924c0b93b7f3c0ea80a600000000000854d0000c3500000c311000000000000001900192b5085704b4b6b823059acda09858529e4e9fb542b52c668b392bc2900378b07900000000000854d0000c3500000c311000000000000001910193da08774a6ff7e6968eddad0fe921d39051caaf3c2bc2aa047102baa574852e7e00000000000854d0000c3500000c311000000000000001920194e6006b57c070a434ba17e69fc30dfdd88fc3faf7c00465df22f22fc575051dea00000000000854d0000c3500000c311000000000000001930195eb004427e0c3795eeb74ae52d8eb908d6395d93e90d93f113a941d728c7a186600000000000854d0000c3500000c31100000000000000194019625cce4babf6d48d362e642b15f5234e346d0ad56e01eff852be1d4bad5da4d9f00000000000854d0000c3500000c3110000000000000019501977de1734b52094a5045f4d3a962b0811c40148ab37a8cb8949ada98b9bb2ef98e00000000000854d0000c3500000c311000000000000001960198f6bb35d733dafb8788f89a35fc734c6fae0c79c7ef79d9ab79a619331d5991be00000000000854d0000c3500000c311000000000000001970199f608ebfa1bda8ea7044d7614459001856762aea6fa2a8da8a84f63b35b784aa900000000000854d0000c3500000c31100000000000000198019a5efd4cd712e8d819797cbdd2e5eb14ab8479581992c5cb4fefb71acd21259c9800000000000854d0000c3500000c31100000000000000199019be469abb620208408b16c28a5b22a408e86745059c187e533738786c8321e62d600000000000854d0000c3500000c3110000000000000019a019c55b7d42391295649268e33972b9b05e3863b06e927b1b6821f6be5c22cd5b32300000000000854d0000c3500000c3110000000000000019b019da71224eb2c47f79b4b7897332b4edf21831b271dd31265ea82a56ee423fbc51d00000000000854d0000c3500000c3110000000000000019c019e625238678138161a8a222110d7fae260b0d53dd718d8f0bbebc6dbf9f7c603d000000000000854d0000c3500000c3110000000000000019d019f998478e02c1ccb514f538e60d2d42bead26565df3239926c7ab2ba9e2556375c00000000000854d0000c3500000c3110000000000000019e01a0cadb273ea32290f6b387130461ebd022c6b559d4485f2dded4dc866794b1952e00000000000854d0000c3500000c3110000000000000019f01a1dcc463d2f12315dd333110aec4216d3ad68442ea011ebbf65cc03d6fc4def28d00000000000854d0000c3500000c311000000000000001a001a226af9c7b0ad6986d2014ee1c344720f7e5ecb5912d4533e3db9cec5abf3766fc00000000000854d0000c3500000c311000000000000001a101a337b0243c9147d7cdb24ab677cb997793184b4c6cbe078a2339c7050553debf1000000000000854d0000c3500000c311000000000000001a201a40b65bf566728aa8f0bd3b5f8b03efad99fe4d86570c3792a009bbd23aed8851600000000000854d0000c3500000c311000000000000001a301a57c0f33fb634f5b1cfad7cc130da55119a2fc282e4975d40204af4084a9df275300000000000854d0000c3500000c311000000000000001a401a610d4665d3a1c11e4bd872b0b47d822fefc75e8010f2766febba4206548da19e400000000000854d0000c3500000c311000000000000001a501a7e5998fdf3f51aedac8c428ebbe458f8039f8da6d996381998888846d7d7eac9300000000000854d0000c3500000c311000000000000001a601a84aa1e7b220a2ccb3d28ae1fc383aa4319dcb8df1a554f98e360ccb1b630674b000000000000854d0000c3500000c311000000000000001a701a90296a7c4204e0380ac993850b4041042fcff74542ba713574070c2982e5fe06d00000000000854d0000c3500000c311000000000000001a801aa1bcebf698d2ebc669f1e2e3ebbdb2bc9745616b0dddf8503965e9ce1bfbfbd4300000000000854d0000c3500000c311000000000000001a901ab17ca3153cfcf2c91ab678671c0193785a38fd13243f23bacf712297c1c3161df00000000000854d0000c3500000c311000000000000001aa01ac42aa6f3b6b844e832587ca38676960f99da8a7bf1cd0861f9476e8148056dc7b00000000000854d0000c3500000c311000000000000001ab01ad1677bf1f8138392b8a896be86688d8e2a852c94abed179d515ea75cbb0f0114400000000000854d0000c3500000c311000000000000001ac01ae5d07f5c149c59191cf2e949908b485c269c7237df230bcc66bff9d221b912af100000000000854d0000c3500000c311000000000000001ad01af4af01101f2fadf75503b90fa5519fabc113be039f7cac24d6a425ff6f4d46c9100000000000854d0000c3500000c311000000000000001ae01b0b75ae35825e628b1d386a81c380eac87e5063ea2fb581058a44e67647606faec00000000000854d0000c3500000c311000000000000001af01b1b3ace4f6e0b4d2504edf42d9fa09490ca53c59401c5ac8eae407a0f0347dc2b400000000000854d0000c3500000c311000000000000001b001b2da0319bb42c74cb71cc24608d303045b7c66b21c9cad1bcefa23190ea9008afd00000000000854d0000c3500000c311000000000000001b101b3e61dacb664d45941c172a551ede9e667bcf095113c15bac9217e1069c4549d3a00000000000854d0000c3500000c311000000000000001b201b46fea6aac4b9e2c07142170421a6f4f81ba464c8ddc49887d9f0f7826314b772e00000000000854d0000c3500000c311000000000000001b301b51df4dfc3f008e8b2348c48574f1c8e555a987162ec8347d793d0ceda160c952800000000000854d0000c3500000c311000000000000001b401b6e793bdc7a9b90cfd1a09081912fe24f588947146e99c46f58754cb16f81f6cd900000000000854d0000c3500000c311000000000000001b501b7ce19f510bda9ca936f1ce98af2c35aed4c87e98c38927a095b9b94f741305f9400000000000854d0000c3500000c311000000000000001b601b8c7930f425336449c2c3208d0e0b74d37e1eb5c1c0311e9a77bf00bf103daa2df00000000000854d0000c3500000c311000000000000001b701b9c7d9c3fecba42d41c4ceb332133da641fb6cdc531897be0caabbc3261cac5c5900000000000854d0000c3500000c311000000000000001b801ba871ca2ac3fcb9449b1cd3903798aa08e0e751d505d9aadec2d2b912094dd05c500000000000854d0000c3500000c311000000000000001b901bbd1dac0d405505faf085050662a9b20fe2e3398fd01a9b011dda41a55c740485c00000000000854d0000c3500000c311000000000000001ba01bc8aca06e52f2ee6f62f413b23f3bac9b4da3e93a2fb788f34c085be0128a293e600000000000854d0000c3500000c311000000000000001bb01bd50562444dd139b2bc45aa2064e6f945e1b4ff89ceef30842195d7e3766b5a25400000000000854d0000c3500000c311000000000000001bc01bea3112af2adcf6c56ac46a4cbf93312e2b6b7ea1848185a85273a7b11251c217900000000000854d0000c3500000c311000000000000001bd01bffa802b543ca6922c0656320b0761b883b9c1d75228ac9a7e9626ec76a7c8482000000000000854d0000c3500000c311000000000000001be01c0b2fcc13e833dd5bdd5d3c8ecb1745f50d831619c895ba3dcd24597bc52e0f67500000000000854d0000c3500000c311000000000000001bf01c11ddb4c5c55ed2746c69088f47da9cf8450cae2cba98e8a997381709e234c811500000000000854d0000c3500000c311000000000000001c001c2f88327f3873e70e5742dba0a8a41ab4555ef75ccbd49812534d625a38086465c00000000000854d0000c3500000c311000000000000001c101c3877f5cb86d90f3acfe194d244ff85604036251e73be7453d043470e8e87d1df100000000000854d0000c3500000c311000000000000001c201c41b6468631457a999e13d8556d28875f80d475e4080a822606e53ab4521e612fa00000000000854d0000c3500000c311000000000000001c301c5e27506829355cd7edd93156f5d3d78038a3cfa19d1b94502097914558d7a99d300000000000854d0000c3500000c311000000000000001c401c6e922eb6004f428a319dce0b00be94d9d65e8a891e0da8092cd17eb57f42719d700000000000854d0000c3500000c311000000000000001c501c72cf19e8b9cdbbce8788a37b61803d8c5ac9b705b451a16cf3fd4e44c98a6978400000000000854d0000c3500000c311000000000000001c601c86d4c389a327e75e07e26dde46d96ec5f9cbe652798c6e4c4f071e518ca431ae400000000000854d0000c3500000c311000000000000001c701c9ea874c9bfb2c727a572b1bb77a3911ddfe4890f0f4cc152b67e4490953fa741300000000000854d0000c3500000c311000000000000001c801ca3078da4bda050f5f723492d3f63a9f465a3f8352414b9b24747052268e8fd9f000000000000854d0000c3500000c311000000000000001c901cb0cbf24109afca71dc10ec95847ccd30693a340ac17a1c438de32c9d5ec779ce600000000000854d0000c3500000c311000000000000001ca01ccd1eebb68e4560bfc706060c2557d03e5fe55e5c76f1b0ef8e8b9102af101e1b600000000000854d0000c3500000c311000000000000001cb01cd60f5505127a1c513a7bf51a506bbd38413db23366690dde3b7a64ada8b38c3bf00000000000854d0000c3500000c311000000000000001cc01ce5f2a77c993b3c8c2860b9b6f661657e2ed9c3fb8a7f07b9f86e6908ff61e504000000000000854d0000c3500000c311000000000000001cd01cfbeeced096c0d29cb3117bd673c5a13fcc06788b4db4d7d24a2bb143cd675772200000000000854d0000c3500000c311000000000000001ce01d093861f3b9c8f22a3fc7f39be4019cbee78624119caa0ac94b509b691e516835c00000000000854d0000c3500000c311000000000000001cf01d13083a24fa948cd62be1d683b9cacaba9117c6a95ba3af20f8df2bedefbe3456300000000000854d0000c3500000c311000000000000001d001d2d12e24a4fc1c9116fc4eeead881d06ea1a7ecfbe5b50a60da676d03906784c3500000000000854d0000c3500000c311000000000000001d101d35c572a6639f35bec2e4ac4d0afad8f0b1a5b56f379ba18bb0f5a2239bed5684500000000000854d0000c3500000c311000000000000001d201d4fe0dd02a29705d5066285c49d0f62b8b7703247af1ca3f3ef80c72d2e73d4b0900000000000854d0000c3500000c311000000000000001d301d5270a3d63c0ee9b56b60d8422820e7d421d7cb5edd90bd36f171f3a248ca8ba2200000000000854d0000c3500000c311000000000000001d401d6a0ad7dd4c1094a8d2cad3b5f58c2f6521a82fcda6145f50a67031c99fac5ee3300000000000854d0000c3500000c311000000000000001d501d7bbc593f697857610ae607fcc8aaadfbfa9ccc8b666c1610fe58638c4b81b39d600000000000854d0000c3500000c311000000000000001d601d82ef8ef79e3f3125048e96e178148b987ad8ce75ddb86f226c9422f88311a1e5400000000000854d0000c3500000c311000000000000001d701d9584d870aff1afb86ba69ddc59fe8f846915e32928ec2b41ecfdcec2ddf6e034800000000000854d0000c3500000c311000000000000001d801da3f220ce62505abf5bd046d810d0a73121491fc5845984c6fc49d95fff620acf000000000000854d0000c3500000c311000000000000001d901db239fd782432147be7b01479d1bbf685ad71220496c5fbe49751f43661ad97ffa00000000000854d0000c3500000c311000000000000001da01dc452dbbf443cb66f47d94219064c1e11d02a35eee965b7f853b387676a12fc1bb00000000000854d0000c3500000c311000000000000001db01ddc92ca5e3fea2af39b5290131d48ae5ac6b8b3da7f8939f3416427aa7bf23c49500000000000854d0000c3500000c311000000000000001dc01de532077ccb94f30ba0e14ae25792e182fd3a9c89a136112a7875a582877d067c900000000000854d0000c3500000c311000000000000001dd01df994fd79bcbb666c872f3cf82331b4890fd26895337b42a7f957e3da716d9dede00000000000854d0000c3500000c311000000000000001de01e0b9563d83c0644780a2f7dc8441b391e23ad1a9d86a23b965931c13d66e41314e00000000000854d0000c3500000c311000000000000001df01e1a356c15452b8070d12716b40768018b3315c33b00a40c54fc825a77e16189b5800000000000854d0000c3500000c311000000000000001e001e250d2f5e73eeac5e1263a0df8a53ba2b1deb5b8e7815c61bd8d70015ed1912fa200000000000854d0000c3500000c311000000000000001e101e37498c3f78d908282db2e0ad1a117dad769c93569014fa2500d266959dc58f9d700000000000854d0000c3500000c311000000000000001e2", + "receiver_txid": "31d842bb6d8a1529dd54d013c7a439827864c4ff4867a8af5080a68b0f25aed4", + "settlement_txid": "b772ab617557851d0f05d96026f51ff9b7baf43c6ef3ee72153495c41c8c2b09" + }, + { + "scenario": "D.6", + "init_hash": "d290b308abba2f4346a5fb0e2477d54eada86eb455d298a14570a8c90aae4d7d", + "setup_hash": "eb4e7e4a56d8612ce7dddd7ed5b8b31d22a9c394560909d5f0a8a7cfc6fb5a71", + "book_hash": "dc2a6687c21e580e3cacc642c8f877ded7278fc9b87e7b388ecee30142ecebb7", + "commitment_hash": "8f19b491169952d5c5852cb9c722ec7f9e52b4bf1627f2c80377f0e7bdbe9287", + "activation_hash": "65129deb07cd484e12e9badcbc11dace2e724e9a1ccba6b40ec92d57dbaaf028", + "init_wire": "d6d9bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848975ac6c4567bb853d69517b2fe7660700e83262c129fb80c5f3e7b8705d2b2f930400000000000f4240000100000000000854d0000c3110000c3500000003e80000138800000000000000000000090800000000000f4240e0e99ff77a29e85142624109ed949a2beb6a35de044c32f884e22a67f72308526065b8e983625c675361332e2a10fa506af6b402b069471921b4a366765edf7f", + "accept_wire": "d6dbbef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848975ac6c4567bb853d69517b2fe7660700e83262c129fb80c5f3e7b8705d2b2f93000000000000002a012064c356aaa2f7789a1ddf82024f18b37dfc22b4fb3ec7583ab7b09f8a9ad1312a07080000000000000000090800000000000f42400b20d290b308abba2f4346a5fb0e2477d54eada86eb455d298a14570a8c90aae4d7d5ff3c9076c82f18847c629529938cb5d03cf3136d8b87be432f9ed0bc81a40e231a83a0146d5c1d0473cc11990623ca31ab3defa22e9c96142fc6340e55aac23", + "activate_wire": "d705bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848975ac6c4567bb853d69517b2fe7660700e83262c129fb80c5f3e7b8705d2b2f93eb4e7e4a56d8612ce7dddd7ed5b8b31d22a9c394560909d5f0a8a7cfc6fb5a71dc2a6687c21e580e3cacc642c8f877ded7278fc9b87e7b388ecee30142ecebb78f19b491169952d5c5852cb9c722ec7f9e52b4bf1627f2c80377f0e7bdbe9287000c0df09b032fc561367edc1f08074a67a670db9d5220826d2f5ea2768ea0ae8485bf9367549039a160219ba6393387728aeb66cdf27647fe07d3d201b0c7d2df813051", + "ack_wire": "d707bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848975ac6c4567bb853d69517b2fe7660700e83262c129fb80c5f3e7b8705d2b2f9365129deb07cd484e12e9badcbc11dace2e724e9a1ccba6b40ec92d57dbaaf0281b127837288c4ae30686b772f13791954b42dafb2b318eba7f91545e3a09089543a8e85aedf462ecd5a1338602329a036c49c41473420ac1f2d2b911898baacc", + "book": "75ac6c4567bb853d69517b2fe7660700e83262c129fb80c5f3e7b8705d2b2f9304010001000164c356aaa2f7789a1ddf82024f18b37dfc22b4fb3ec7583ab7b09f8a9ad1312a00000000000f4240000c3500000c31100000000000000000", + "receiver_txid": "5a294b75a1b592d11670560d0e7976fabaac0779292f2aaa4096e81f73e591c7", + "settlement_txid": "d76e272a1353cd34eb8ee2eeefe8783f24b77a0d5424ffb253961af387d7de82" + } +] diff --git a/lightning-ffor/tests/data/beignet-lifecycle.json b/lightning-ffor/tests/data/beignet-lifecycle.json new file mode 100644 index 00000000000..10981d3c190 --- /dev/null +++ b/lightning-ffor/tests/data/beignet-lifecycle.json @@ -0,0 +1,72 @@ +{ + "source_revision": "8aee31d18e596fe49a0d195b325a6e757d7a009b", + "fixtures": [ + { + "name": "abort", + "message_type": 55049, + "signer": "035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c", + "wire": "d70901010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202080808080808080808080808080808080808080808080808080808080808080800060019646973636f6e6e656374656420647572696e672073657475708e68c4763d4ebe74817028d6a02fc833da4ca71b584623466ede149efeaa17c86ed6adfcca9e8c3770b5ca620ca3b15bc4d28a033357d2dfc640ee2b81a77893" + }, + { + "name": "close", + "message_type": 55051, + "signer": "035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c", + "wire": "d70b010101010101010101010101010101010101010101010101010101010101010102020202020202020202020202020202020202020202020202020202020202020909090909090909090909090909090909090909090909090909090909090909b8da4a492327808e2b591f2db8be5451b07c3d54feb5d719937804e3126501e4092d425044ce969609d8583736e9693602bc6da36eebfa5bf12c99bc57a5440c" + }, + { + "name": "close_ack_paid", + "message_type": 55053, + "signer": "035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c", + "wire": "d70d010101010101010101010101010101010101010101010101010101010101010102020202020202020202020202020202020202020202020202020202020202020909090909090909090909090909090909090909090909090909090909090909000201012200010505050505050505050505050505050505050505050505050505050505050505b7ddea980cd1041b4dd42c200b4a1f23e84bf9c78526f25a60b46bac63d9694e27170e46aa38740eeda54b20324eafa15074324b53f43339f54375eee3c93683" + }, + { + "name": "close_ack_unpaid_absent_tlv", + "message_type": 55053, + "signer": "035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c", + "wire": "d70d010101010101010101010101010101010101010101010101010101010101010102020202020202020202020202020202020202020202020202020202020202020909090909090909090909090909090909090909090909090909090909090909000200b5ef97faf9628a1baf0bb5911c52cc21482c750da7107d10f12c064b2ae0e7f04bfda970e26140b436fb5a5855c0374b502b21448f92ae990d202d120670beed" + }, + { + "name": "close_ack_unpaid_empty_tlv", + "message_type": 55053, + "signer": "035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c", + "wire": "d70d010101010101010101010101010101010101010101010101010101010101010102020202020202020202020202020202020202020202020202020202020202020909090909090909090909090909090909090909090909090909090909090909000200010050a03c5bd10fffb8e15f81cdf2868320fcb86f235654c6f3ee60d56deda66a4e756c3f5f01cf83f9c15791f8f2df269167fd4a06f3477d8041ac2bf2079e0458" + } + ], + "reestablish": [ + { + "state": 0, + "type": 55001, + "value": "02020202020202020202020202020202020202020202020202020202020202020000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "state": 1, + "type": 55001, + "value": "02020202020202020202020202020202020202020202020202020202020202020100000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "state": 2, + "type": 55001, + "value": "02020202020202020202020202020202020202020202020202020202020202020200000909090909090909090909090909090909090909090909090909090909090909" + }, + { + "state": 3, + "type": 55001, + "value": "02020202020202020202020202020202020202020202020202020202020202020300000909090909090909090909090909090909090909090909090909090909090909" + }, + { + "state": 4, + "type": 55001, + "value": "02020202020202020202020202020202020202020202020202020202020202020400000909090909090909090909090909090909090909090909090909090909090909" + }, + { + "state": 5, + "type": 55001, + "value": "02020202020202020202020202020202020202020202020202020202020202020500000909090909090909090909090909090909090909090909090909090909090909" + }, + { + "state": 6, + "type": 55001, + "value": "02020202020202020202020202020202020202020202020202020202020202020600000000000000000000000000000000000000000000000000000000000000000000" + } + ] +} diff --git a/lightning-ffor/tests/data/extract_appendix_d.py b/lightning-ffor/tests/data/extract_appendix_d.py new file mode 100644 index 00000000000..215a8be4bff --- /dev/null +++ b/lightning-ffor/tests/data/extract_appendix_d.py @@ -0,0 +1,94 @@ +"""Extract pinned public Appendix D fixtures, reconstructing abbreviated D.5 bytes. + +Usage: python3 extract_appendix_d.py /path/to/ffor-variant-d-vectors.md +Writes appendix-d.json beside this script. Every reconstruction is checked against +the independently published hashes, and uses published signatures without signing. +""" + +import hashlib +import json +import pathlib +import re +import struct +import sys + + +def sha(data): + return hashlib.sha256(data).digest() + + +def field(section, name): + return re.search(r"\| `" + re.escape(name) + r"` \| `([0-9a-f]+)`", section)[1] + + +def tlv(kind, value): + length = len(value) + encoded_length = bytes([length]) if length < 253 else b"\xfd" + struct.pack(">H", length) + return bytes([kind]) + encoded_length + value + + +def extract(source): + fixtures = [] + for number in range(1, 7): + section = source.split(f"## D.{number} ")[1].split("\n## D.")[0] + fixture = {"scenario": f"D.{number}"} + for label, name in [ + ("init_hash", "T_init"), ("setup_hash", "T_setup"), + ("book_hash", "H_book"), ("commitment_hash", "H_commit"), + ("activation_hash", "H_act"), + ]: + fixture[label] = field(section, name) + for label, name in [ + ("init_wire", "ff_init"), ("accept_wire", "ff_accept"), + ("activate_wire", "ff_activate"), ("ack_wire", "ff_activate_ack"), + ]: + message = section.split(f"**`{name}` (type ")[1].split("**`ff_")[0] + if number == 5 and name in ("ff_init", "ff_accept"): + header = bytes.fromhex(fixtures[0]["init_wire"])[2:34] + epoch = bytes.fromhex(field(section, "epoch_id")) + amounts = struct.pack(">Q", 546000) * 483 + hashes = b"".join( + sha(sha(b"ffor/vector/D.5/preimage" + struct.pack(">H", k))) + for k in range(1, 484) + ) + if name == "ff_init": + wire = struct.pack(">H", 55001) + header + epoch + wire += struct.pack(">BQHQIIIIQH", 4, 263718000, 483, 546000, + 798992, 800000, 1000, 5000, 0, 0) + wire += tlv(9, amounts) + else: + wire = struct.pack(">H", 55003) + header + epoch + struct.pack(">Q", 42) + wire += tlv(1, hashes) + tlv(7, struct.pack(">Q", 0)) + wire += tlv(9, amounts) + tlv(11, bytes.fromhex(fixture["init_hash"])) + signature = re.search(r"signature \(final 64 bytes\) \| `([0-9a-f]+)`", message)[1] + wire += bytes.fromhex(signature) + else: + wire = bytes.fromhex(re.search(r"Wire bytes:\s*```\s*([0-9a-f]+)\s*```", message)[1]) + expected = re.search(r"SHA256\(wire bytes\) \| `([0-9a-f]+)`", message)[1] + assert sha(wire).hex() == expected, (number, name) + fixture[label] = wire.hex() + if number == 5: + book = epoch + struct.pack(">BBH", 4, 1, 483) + for k in range(1, 484): + payment_hash = hashes[(k - 1) * 32:k * 32] + book += struct.pack(">H", k) + payment_hash + book += struct.pack(">QIIQ", 546000, 800000, 798992, k - 1) + else: + book_section = section.split(f"### D.{number}.4 ")[1].split(f"### D.{number}.5 ")[0] + book = bytes.fromhex(re.search(r"```\s*([0-9a-f]+)\s*```", book_section)[1]) + assert sha(b"ffor/book" + book).hex() == fixture["book_hash"] + fixture["book"] = book.hex() + txids = re.findall( + r"\| `n_[RS]\^act` / `txid\(C\^[RS]\)` internal byte order \| 43 / `([0-9a-f]+)`", section + ) + assert len(txids) == 2 + fixture["receiver_txid"], fixture["settlement_txid"] = txids + fixtures.append(fixture) + return fixtures + + +if __name__ == "__main__": + fixture_data = extract(pathlib.Path(sys.argv[1]).read_text()) + pathlib.Path(__file__).with_name("appendix-d.json").write_text( + json.dumps(fixture_data, indent=2) + "\n" + ) diff --git a/lightning-ffor/tests/data/generate_beignet_lifecycle.cjs b/lightning-ffor/tests/data/generate_beignet_lifecycle.cjs new file mode 100644 index 00000000000..c3851ef1b2c --- /dev/null +++ b/lightning-ffor/tests/data/generate_beignet_lifecycle.cjs @@ -0,0 +1,50 @@ +// Reproduce lifecycle interoperability fixtures using the pinned Beignet implementation. +// Usage: node -r /path/to/beignet/node_modules/ts-node/register \ +// generate_beignet_lifecycle.cjs /path/to/beignet /path/to/output.json +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); + +const reference = path.resolve(process.argv[2]); +const expectedRevision = '8aee31d18e596fe49a0d195b325a6e757d7a009b'; +const revision = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: reference, encoding: 'utf8' }).trim(); +if (revision !== expectedRevision) throw new Error('Beignet revision differs from the fixture baseline'); +const codecs = require(path.join(reference, 'src/lightning/ffor/messages.ts')); +const { getPublicKey } = require(path.join(reference, 'src/lightning/crypto/ecdh.ts')); + +// Deterministic test-only signing material. These fixtures contain no wallet keys or funds. +const secret = Buffer.alloc(32, 42); +const publicKey = getPublicKey(secret); +const header = { channelId: Buffer.alloc(32, 1), epochId: Buffer.alloc(32, 2) }; +const activation = { ...header, activationHash: Buffer.alloc(32, 9) }; +const emptyClose = codecs.encodeFforCloseAckUnsigned({ + ...activation, numSlots: 2, settled: Buffer.from([0]), preimages: [] +}); +const cases = [ + ['abort', 55049, codecs.encodeFforAbortUnsigned({ + ...header, transcriptHash: Buffer.alloc(32, 8), reason: 6, + data: Buffer.from('disconnected during setup', 'utf8') + })], + ['close', 55051, codecs.encodeFforCloseUnsigned(activation)], + ['close_ack_paid', 55053, codecs.encodeFforCloseAckUnsigned({ + ...activation, numSlots: 2, settled: Buffer.from([1]), + preimages: [{ k: 1, preimage: Buffer.alloc(32, 5) }] + })], + ['close_ack_unpaid_absent_tlv', 55053, emptyClose], + ['close_ack_unpaid_empty_tlv', 55053, Buffer.concat([emptyClose, Buffer.from([1, 0])])] +]; +const fixtures = cases.map(([name, type, unsigned]) => { + const body = codecs.signFforMessage(type, unsigned, secret); + if (!codecs.verifyFforMessage(type, body, publicKey)) throw new Error('Fixture authentication failed'); + return { name, message_type: type, signer: publicKey.toString('hex'), wire: codecs.fforWireBytes(type, body).toString('hex') }; +}); +const reestablish = Array.from({ length: 7 }, (_, state) => { + // Beignet retains hAct while ACTIVATING, even though section 11.1 says zero before ACTIVE. + // Preserve that input as an interoperability case, without treating it as activation proof. + const activationHash = Buffer.alloc(32, state >= 2 && state <= 5 ? 9 : 0); + const tlv = codecs.encodeFforReestablishTlv({ epochId: header.epochId, state, lastSeq: 0, activationHash }); + const decoded = codecs.decodeFforReestablishTlv(tlv.value); + if (decoded.state !== state || !decoded.activationHash.equals(activationHash)) throw new Error('Reconnect fixture mismatch'); + return { state, type: Number(tlv.type), value: tlv.value.toString('hex') }; +}); +fs.writeFileSync(process.argv[3], JSON.stringify({ source_revision: revision, fixtures, reestablish }, null, 2) + '\n'); diff --git a/lightning-ffor/tests/reestablish.rs b/lightning-ffor/tests/reestablish.rs new file mode 100644 index 00000000000..a89b917b0b7 --- /dev/null +++ b/lightning-ffor/tests/reestablish.rs @@ -0,0 +1,84 @@ +use lightning_ffor::reestablish::{ + Reestablish, ReestablishError, ReportedState, TLV_TYPE, VALUE_LEN, +}; +use proptest::prelude::*; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Reference { + source_revision: String, + reestablish: Vec, +} + +#[derive(Deserialize)] +struct Fixture { + state: u8, + r#type: u64, + value: String, +} + +#[test] +fn all_states_match_the_pinned_reference() { + let reference: Reference = + serde_json::from_str(include_str!("data/beignet-lifecycle.json")).unwrap(); + assert_eq!(reference.source_revision, "8aee31d18e596fe49a0d195b325a6e757d7a009b"); + assert_eq!(reference.reestablish.len(), 7); + for fixture in reference.reestablish { + assert_eq!(fixture.r#type, TLV_TYPE); + let bytes: Vec = (0..fixture.value.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&fixture.value[i..i + 2], 16).unwrap()) + .collect(); + let report = Reestablish::decode(&bytes).unwrap(); + assert_eq!(report.epoch_id, [2; 32]); + assert_eq!(report.state as u8, fixture.state); + assert_eq!(report.encode().as_slice(), bytes); + if report.state == ReportedState::Activating { + // This unsigned report is deliberately preserved, not accepted as activation evidence. + assert_eq!(report.activation_hash, [9; 32]); + } + } +} + +#[test] +fn every_length_and_unknown_state_boundary_is_rejected() { + let value = [0; VALUE_LEN]; + for length in 0..VALUE_LEN { + assert_eq!(Reestablish::decode(&value[..length]), Err(ReestablishError::Length)); + } + assert_eq!(Reestablish::decode(&[0; VALUE_LEN + 1]), Err(ReestablishError::Length)); + for state in 7..=255 { + let mut value = value; + value[32] = state; + assert_eq!(Reestablish::decode(&value), Err(ReestablishError::State)); + } +} + +#[test] +fn nonzero_sequence_is_never_variant_d_settlement_evidence() { + for sequence in 1_u16..=u16::MAX { + let mut value = [0; VALUE_LEN]; + value[32] = ReportedState::Active as u8; + value[33..35].copy_from_slice(&sequence.to_be_bytes()); + assert_eq!(Reestablish::decode(&value), Err(ReestablishError::Sequence)); + } +} + +proptest! { + #[test] + fn round_trip_preserves_all_report_bytes(epoch in any::<[u8; 32]>(), hash in any::<[u8; 32]>(), state in 0_u8..7) { + let mut bytes = [0; VALUE_LEN]; + bytes[..32].copy_from_slice(&epoch); + bytes[32] = state; + bytes[35..].copy_from_slice(&hash); + let report = Reestablish::decode(&bytes).unwrap(); + prop_assert_eq!(report.encode(), bytes); + } + + #[test] + fn arbitrary_inputs_never_panic(bytes in proptest::collection::vec(any::(), 0..256)) { + if let Ok(report) = Reestablish::decode(&bytes) { + prop_assert_eq!(report.encode().to_vec(), bytes); + } + } +} diff --git a/lightning-ffor/tests/reference_lifecycle.rs b/lightning-ffor/tests/reference_lifecycle.rs new file mode 100644 index 00000000000..0b6f582f103 --- /dev/null +++ b/lightning-ffor/tests/reference_lifecycle.rs @@ -0,0 +1,37 @@ +use bitcoin::secp256k1::PublicKey; +use lightning_ffor::wire::Message; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Reference { + source_revision: String, + fixtures: Vec, +} + +#[derive(Deserialize)] +struct Fixture { + name: String, + message_type: u16, + signer: String, + wire: String, +} + +fn hex(value: &str) -> Vec { + (0..value.len()).step_by(2).map(|i| u8::from_str_radix(&value[i..i + 2], 16).unwrap()).collect() +} + +#[test] +fn beignet_lifecycle_messages_preserve_signed_wire_bytes() { + let reference: Reference = + serde_json::from_str(include_str!("data/beignet-lifecycle.json")).unwrap(); + assert_eq!(reference.source_revision, "8aee31d18e596fe49a0d195b325a6e757d7a009b"); + assert_eq!(reference.fixtures.len(), 5); + for fixture in reference.fixtures { + let wire = hex(&fixture.wire); + let decoded = + Message::decode(&wire).unwrap_or_else(|error| panic!("{}: {error}", fixture.name)); + assert_eq!(decoded.message_type(), fixture.message_type); + decoded.verify_signature(&PublicKey::from_slice(&hex(&fixture.signer)).unwrap()).unwrap(); + assert_eq!(decoded.encode().unwrap(), wire, "{}", fixture.name); + } +} diff --git a/lightning-ffor/tests/setup.rs b/lightning-ffor/tests/setup.rs new file mode 100644 index 00000000000..de5b0f32d41 --- /dev/null +++ b/lightning-ffor/tests/setup.rs @@ -0,0 +1,373 @@ +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::{Message as SecpMessage, PublicKey, Secp256k1, SecretKey}; +use lightning_ffor::setup::{AuthenticatedSetup, SetupError}; +use lightning_ffor::transcript; +use lightning_ffor::wire::{ + Accept, Activate, CloseAck, Header, Init, Message, Payload, Preimage, Tlv, +}; +use proptest::prelude::*; +use serde::Deserialize; + +fn key(value: u8) -> PublicKey { + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[value; 32]).unwrap()) +} + +fn sign(message: &mut Message, value: u8) { + let digest = message.signature_digest().unwrap(); + message.signature = Secp256k1::new() + .sign_ecdsa( + &SecpMessage::from_digest(digest), + &SecretKey::from_slice(&[value; 32]).unwrap(), + ) + .serialize_compact(); +} + +fn message(payload: Payload, signer: u8) -> Message { + let mut message = Message { + header: Header { channel_id: [1; 32], epoch_id: [2; 32] }, + payload, + extensions: Vec::new(), + signature: [0; 64], + }; + sign(&mut message, signer); + message +} + +fn setup_messages() -> (Message, Message) { + let init = message( + Payload::Init(Init { + budget_msat: 2_000_000, + min_payment_msat: 354_000, + settlement_deadline: 1000, + voucher_expiry: 2008, + fee_base_msat: 1000, + fee_proportional_millionths: 100, + amounts_msat: vec![1_000_000; 2], + witness_peers: None, + hash_chain: false, + }), + 42, + ); + let accept = message( + Payload::Accept(Accept { + s_commitment_number: 3, + payment_hashes: vec![ + sha256::Hash::hash(&[5; 32]).to_byte_array(), + sha256::Hash::hash(&[6; 32]).to_byte_array(), + ], + s_htlc_id_base: 8, + amounts_msat: vec![1_000_000; 2], + init_hash: transcript::init_hash(&init.encode().unwrap()), + }), + 43, + ); + (init, accept) +} + +fn setup() -> AuthenticatedSetup { + let (init, accept) = setup_messages(); + AuthenticatedSetup::new(&init, &accept, key(42), key(43)).unwrap() +} + +fn activate(setup: &AuthenticatedSetup, height: u32) -> Message { + message( + Payload::Activate(Activate { + setup_hash: setup.setup_hash(), + book_hash: setup.book_hash(), + commit_hash: [9; 32], + epoch_start_height: height, + }), + 42, + ) +} + +fn refresh_accept(init: &Message, accept: &mut Message) { + let accepted = match &mut accept.payload { + Payload::Accept(accepted) => accepted, + _ => unreachable!(), + }; + accepted.init_hash = transcript::init_hash(&init.encode().unwrap()); + sign(accept, 43); +} + +fn hex(value: &str) -> Vec { + (0..value.len()).step_by(2).map(|i| u8::from_str_radix(&value[i..i + 2], 16).unwrap()).collect() +} + +#[derive(Deserialize)] +struct Fixture { + init_wire: String, + accept_wire: String, + activate_wire: String, + ack_wire: String, + book: String, + book_hash: String, + setup_hash: String, + commitment_hash: String, + activation_hash: String, +} + +#[test] +fn all_public_books_and_activation_statements_authenticate() { + let fixtures: Vec = + serde_json::from_str(include_str!("data/appendix-d.json")).unwrap(); + assert_eq!(fixtures.len(), 6); + let receiver = PublicKey::from_slice(&hex( + "039fca7f8157aa768708894ffd92550fe970edd18526a5f936583ea3b54dab3228", + )) + .unwrap(); + let settlement = PublicKey::from_slice(&hex( + "02087b7d1b4789170f6e374f0a0e58a1b7a899e34929795314ab6964e69609e9c0", + )) + .unwrap(); + for fixture in fixtures { + let init = Message::decode(&hex(&fixture.init_wire)).unwrap(); + let accept = Message::decode(&hex(&fixture.accept_wire)).unwrap(); + let setup = AuthenticatedSetup::new(&init, &accept, receiver, settlement).unwrap(); + assert_eq!(setup.canonical_book(), hex(&fixture.book)); + assert_eq!(setup.book_hash().as_slice(), hex(&fixture.book_hash)); + assert_eq!(setup.setup_hash().as_slice(), hex(&fixture.setup_hash)); + let activation = Message::decode(&hex(&fixture.activate_wire)).unwrap(); + let expected_commitment = hex(&fixture.commitment_hash).try_into().unwrap(); + let hash = setup.validate_activation(&activation, expected_commitment, 790_000).unwrap(); + assert_eq!(hash.as_slice(), hex(&fixture.activation_hash)); + let ack = Message::decode(&hex(&fixture.ack_wire)).unwrap(); + setup.validate_activation_ack(&ack, hash).unwrap(); + } +} + +#[test] +fn setup_requires_distinct_expected_signers_and_unique_hashes() { + let (init, mut accept) = setup_messages(); + assert!(matches!( + AuthenticatedSetup::new(&init, &accept, key(42), key(42)), + Err(SetupError::Roles) + )); + assert!(matches!( + AuthenticatedSetup::new(&init, &accept, key(43), key(42)), + Err(SetupError::Wire(_)) + )); + let accepted = match &mut accept.payload { + Payload::Accept(accepted) => accepted, + _ => unreachable!(), + }; + accepted.payment_hashes[1] = accepted.payment_hashes[0]; + sign(&mut accept, 43); + assert!(matches!( + AuthenticatedSetup::new(&init, &accept, key(42), key(43)), + Err(SetupError::DuplicateHash) + )); +} + +#[test] +fn setup_rejects_signed_fee_overflow() { + let (mut init, mut accept) = setup_messages(); + let terms = match &mut init.payload { + Payload::Init(terms) => terms, + _ => unreachable!(), + }; + terms.amounts_msat = vec![u64::MAX]; + terms.budget_msat = u64::MAX; + terms.fee_proportional_millionths = 2; + sign(&mut init, 42); + let accepted = match &mut accept.payload { + Payload::Accept(accepted) => accepted, + _ => unreachable!(), + }; + accepted.amounts_msat = vec![u64::MAX]; + accepted.payment_hashes.truncate(1); + refresh_accept(&init, &mut accept); + assert!(matches!( + AuthenticatedSetup::new(&init, &accept, key(42), key(43)), + Err(SetupError::FeeOverflow) + )); +} + +#[test] +fn setup_copies_signed_inputs_and_preserves_optional_extensions() { + let (mut init, mut accept) = setup_messages(); + init.extensions.push(Tlv { kind: 99, value: vec![1, 2, 3] }); + sign(&mut init, 42); + refresh_accept(&init, &mut accept); + let setup = AuthenticatedSetup::new(&init, &accept, key(42), key(43)).unwrap(); + let original_wire = setup.init().encode().unwrap(); + init.extensions[0].value[0] = 255; + accept.header.epoch_id = [8; 32]; + assert_eq!(setup.init().encode().unwrap(), original_wire); + assert_eq!(setup.accept().header, setup.header()); + assert_eq!(setup.vouchers()[1].htlc_id, 9); + assert_eq!(setup.terms().fees.base_msat, 1000); +} + +#[test] +fn requested_hash_chain_must_link_every_slot() { + let (mut init, mut accept) = setup_messages(); + let terms = match &mut init.payload { + Payload::Init(terms) => terms, + _ => unreachable!(), + }; + terms.hash_chain = true; + sign(&mut init, 42); + refresh_accept(&init, &mut accept); + assert!(matches!( + AuthenticatedSetup::new(&init, &accept, key(42), key(43)), + Err(SetupError::HashChain) + )); + let accepted = match &mut accept.payload { + Payload::Accept(accepted) => accepted, + _ => unreachable!(), + }; + accepted.payment_hashes[0] = sha256::Hash::hash(&accepted.payment_hashes[1]).to_byte_array(); + sign(&mut accept, 43); + AuthenticatedSetup::new(&init, &accept, key(42), key(43)).unwrap(); +} + +#[test] +fn activation_checks_actual_commitments_identity_and_height_boundaries() { + let setup = setup(); + let activation = activate(&setup, 990); + assert_eq!(setup.validate_activation(&activation, [8; 32], 990), Err(SetupError::Transcript)); + for height in [983, 997, 1000, u32::MAX] { + assert_eq!( + setup.validate_activation(&activation, [9; 32], height), + Err(SetupError::Height) + ); + } + for height in [984, 990, 996] { + setup.validate_activation(&activation, [9; 32], height).unwrap(); + } + let mut other_epoch = activation; + other_epoch.header.epoch_id[0] ^= 1; + sign(&mut other_epoch, 42); + assert_eq!(setup.validate_activation(&other_epoch, [9; 32], 990), Err(SetupError::Identity)); + let at_deadline = activate(&setup, 1000); + assert_eq!(setup.validate_activation(&at_deadline, [9; 32], 999), Err(SetupError::Height)); +} + +#[test] +fn activation_ack_is_bound_to_role_type_and_activation_hash() { + let setup = setup(); + let hash = setup.validate_activation(&activate(&setup, 990), [9; 32], 990).unwrap(); + let mut ack = message(Payload::ActivateAck(hash), 43); + setup.validate_activation_ack(&ack, hash).unwrap(); + assert_eq!(setup.validate_activation_ack(&ack, [0; 32]), Err(SetupError::Transcript)); + sign(&mut ack, 42); + assert!(matches!(setup.validate_activation_ack(&ack, hash), Err(SetupError::Wire(_)))); + let close = message(Payload::Close(hash), 43); + assert_eq!(setup.validate_activation_ack(&close, hash), Err(SetupError::MessageType)); +} + +#[test] +fn lifecycle_validation_rejects_signed_wrong_message_types() { + let setup = setup(); + let receiver_close = message(Payload::Close([9; 32]), 42); + let settlement_close = message(Payload::Close([9; 32]), 43); + assert_eq!( + setup.validate_activation(&receiver_close, [9; 32], 990), + Err(SetupError::MessageType) + ); + assert_eq!(setup.validate_close_ack(&settlement_close, [9; 32]), Err(SetupError::MessageType)); + assert!(matches!( + AuthenticatedSetup::new(&receiver_close, &settlement_close, key(42), key(43)), + Err(SetupError::MessageType) + )); +} + +#[test] +fn activation_cannot_admit_at_local_deadline_even_when_peer_tip_is_close() { + let setup = setup(); + assert_eq!( + setup.validate_activation(&activate(&setup, 999), [9; 32], 1000), + Err(SetupError::Height) + ); +} + +#[test] +fn close_ack_verifies_book_size_and_preimages() { + let setup = setup(); + let mut ack = message( + Payload::CloseAck(CloseAck { + activation_hash: [9; 32], + num_slots: 2, + settled: vec![1], + preimages_tlv_present: true, + preimages: vec![Preimage { slot: 1, value: [5; 32] }], + }), + 43, + ); + setup.validate_close_ack(&ack, [9; 32]).unwrap(); + let mut missing_preimage_tlv = ack.clone(); + let close = match &mut missing_preimage_tlv.payload { + Payload::CloseAck(close) => close, + _ => unreachable!(), + }; + close.preimages_tlv_present = false; + assert!(missing_preimage_tlv.signature_digest().is_err()); + assert!(missing_preimage_tlv.encode().is_err()); + assert_eq!(setup.validate_close_ack(&ack, [8; 32]), Err(SetupError::Transcript)); + let close = match &mut ack.payload { + Payload::CloseAck(close) => close, + _ => unreachable!(), + }; + close.preimages[0].value = [6; 32]; + sign(&mut ack, 43); + assert_eq!(setup.validate_close_ack(&ack, [9; 32]), Err(SetupError::CloseBook)); + let close = match &mut ack.payload { + Payload::CloseAck(close) => close, + _ => unreachable!(), + }; + close.num_slots = 3; + sign(&mut ack, 43); + assert_eq!(setup.validate_close_ack(&ack, [9; 32]), Err(SetupError::CloseBook)); +} + +#[test] +fn chained_close_cannot_claim_a_later_slot_without_every_prior_slot() { + let (mut init, mut accept) = setup_messages(); + let terms = match &mut init.payload { + Payload::Init(terms) => terms, + _ => unreachable!(), + }; + terms.hash_chain = true; + sign(&mut init, 42); + let first_preimage = sha256::Hash::hash(&[6; 32]).to_byte_array(); + let accepted = match &mut accept.payload { + Payload::Accept(accepted) => accepted, + _ => unreachable!(), + }; + accepted.payment_hashes[0] = sha256::Hash::hash(&first_preimage).to_byte_array(); + refresh_accept(&init, &mut accept); + let setup = AuthenticatedSetup::new(&init, &accept, key(42), key(43)).unwrap(); + let mut ack = message( + Payload::CloseAck(CloseAck { + activation_hash: [9; 32], + num_slots: 2, + settled: vec![2], + preimages_tlv_present: true, + preimages: vec![Preimage { slot: 2, value: [6; 32] }], + }), + 43, + ); + assert_eq!(setup.validate_close_ack(&ack, [9; 32]), Err(SetupError::CloseBook)); + let close = match &mut ack.payload { + Payload::CloseAck(close) => close, + _ => unreachable!(), + }; + close.settled[0] = 3; + close.preimages.insert(0, Preimage { slot: 1, value: first_preimage }); + sign(&mut ack, 43); + setup.validate_close_ack(&ack, [9; 32]).unwrap(); +} + +proptest! { + #[test] + fn signed_wrong_activation_hashes_are_rejected(field in 0_u8..3, index in 0_usize..32, bit in 0_u8..8) { + let setup = setup(); + let mut activation = activate(&setup, 990); + let contents = match &mut activation.payload { Payload::Activate(contents) => contents, _ => unreachable!() }; + let hash = match field { 0 => &mut contents.setup_hash, 1 => &mut contents.book_hash, _ => &mut contents.commit_hash }; + hash[index] ^= 1 << bit; + sign(&mut activation, 42); + prop_assert_eq!(setup.validate_activation(&activation, [9; 32], 990), Err(SetupError::Transcript)); + } +} diff --git a/lightning-ffor/tests/transcript.rs b/lightning-ffor/tests/transcript.rs new file mode 100644 index 00000000000..8ef3a8d7bba --- /dev/null +++ b/lightning-ffor/tests/transcript.rs @@ -0,0 +1,161 @@ +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, SecretKey}; +use lightning_ffor::transcript; +use lightning_ffor::transcript::{verify_message_signature, SignatureError}; +use proptest::prelude::*; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Fixture { + scenario: String, + init_wire: String, + accept_wire: String, + activate_wire: String, + ack_wire: String, + book: String, + init_hash: String, + setup_hash: String, + book_hash: String, + commitment_hash: String, + activation_hash: String, + receiver_txid: String, + settlement_txid: String, +} + +fn hex(value: &str) -> Vec { + assert_eq!(value.len() % 2, 0); + (0..value.len()).step_by(2).map(|i| u8::from_str_radix(&value[i..i + 2], 16).unwrap()).collect() +} + +fn digest(value: &str) -> [u8; 32] { + hex(value).try_into().unwrap() +} + +#[test] +fn appendix_d_all_six_transcripts_match_published_bytes() { + let fixtures: Vec = + serde_json::from_str(include_str!("data/appendix-d.json")).unwrap(); + assert_eq!(fixtures.len(), 6); + let receiver_key = PublicKey::from_slice(&hex( + "039fca7f8157aa768708894ffd92550fe970edd18526a5f936583ea3b54dab3228", + )) + .unwrap(); + let settlement_key = PublicKey::from_slice(&hex( + "02087b7d1b4789170f6e374f0a0e58a1b7a899e34929795314ab6964e69609e9c0", + )) + .unwrap(); + for fixture in fixtures { + let init = transcript::init_hash(&hex(&fixture.init_wire)); + assert_eq!(init, digest(&fixture.init_hash), "{}", fixture.scenario); + let setup = transcript::setup_hash(&init, &hex(&fixture.accept_wire)); + assert_eq!(setup, digest(&fixture.setup_hash)); + let book = transcript::book_hash(&hex(&fixture.book)); + assert_eq!(book, digest(&fixture.book_hash)); + let commitment = transcript::commitment_hash( + 43, + &digest(&fixture.receiver_txid), + 43, + &digest(&fixture.settlement_txid), + ); + assert_eq!(commitment, digest(&fixture.commitment_hash)); + assert_eq!( + transcript::activation_hash(&setup, &book, &commitment, 790_000), + digest(&fixture.activation_hash) + ); + for (encoded, key) in [ + (&fixture.init_wire, receiver_key), + (&fixture.accept_wire, settlement_key), + (&fixture.activate_wire, receiver_key), + (&fixture.ack_wire, settlement_key), + ] { + let wire = hex(encoded); + let signature_start = wire.len() - 64; + let signature = wire[signature_start..].try_into().unwrap(); + let kind = u16::from_be_bytes(wire[..2].try_into().unwrap()); + let unsigned = &wire[2..signature_start]; + verify_message_signature(kind, unsigned, &signature, &key).unwrap(); + assert_eq!( + verify_message_signature(kind + 2, unsigned, &signature, &key), + Err(SignatureError::Invalid) + ); + } + } +} + +fn test_key() -> (SecretKey, PublicKey) { + let secret = SecretKey::from_slice(&[42; 32]).unwrap(); + let public = PublicKey::from_secret_key(&Secp256k1::new(), &secret); + (secret, public) +} + +fn sign_digest(digest: [u8; 32]) -> [u8; 64] { + Secp256k1::new().sign_ecdsa(&Message::from_digest(digest), &test_key().0).serialize_compact() +} + +#[test] +fn signature_rejects_invalid_compact_scalars() { + assert_eq!( + verify_message_signature(55001, b"body", &[255; 64], &test_key().1), + Err(SignatureError::Malformed) + ); + assert_eq!( + verify_message_signature(55001, b"body", &[0; 64], &test_key().1), + Err(SignatureError::Invalid) + ); +} + +#[test] +fn signature_rejects_high_s_without_normalizing_received_bytes() { + let body = [7; 128]; + let mut signature = sign_digest(transcript::message_digest(55001, &body)); + // Negating s modulo the curve order preserves ECDSA validity but violates canonical low-S. + let order = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + let mut borrow = 0_i16; + for index in (0..32).rev() { + let value = i16::from(order[index]) - i16::from(signature[32 + index]) - borrow; + signature[32 + index] = value.rem_euclid(256) as u8; + borrow = i16::from(value < 0); + } + assert_eq!( + verify_message_signature(55001, &body, &signature, &test_key().1), + Err(SignatureError::HighS) + ); +} + +#[test] +fn signature_requires_single_hash_and_ffor_domain() { + let body = [9; 128]; + let digest = transcript::message_digest(55001, &body); + for wrong_digest in + [sha256::Hash::hash(&body).to_byte_array(), sha256::Hash::hash(&digest).to_byte_array()] + { + assert_eq!( + verify_message_signature(55001, &body, &sign_digest(wrong_digest), &test_key().1), + Err(SignatureError::Invalid) + ); + } +} + +proptest! { + #[test] + fn signed_bytes_and_expected_peer_cannot_be_substituted(mut body in prop::collection::vec(any::(), 64..2048), changed_index in any::()) { + let (_, public) = test_key(); + let signature = sign_digest(transcript::message_digest(55001, &body)); + prop_assert_eq!(verify_message_signature(55001, &body, &signature, &public), Ok(())); + let other_secret = SecretKey::from_slice(&[43; 32]).unwrap(); + let other_public = PublicKey::from_secret_key(&Secp256k1::new(), &other_secret); + prop_assert_eq!(verify_message_signature(55001, &body, &signature, &other_public), Err(SignatureError::Invalid)); + let index = changed_index % body.len(); + body[index] ^= 1; + prop_assert_eq!(verify_message_signature(55001, &body, &signature, &public), Err(SignatureError::Invalid)); + } + + #[test] + fn transcript_fields_and_domains_are_bound(bytes in prop::collection::vec(any::(), 0..2048), height in 0_u32..u32::MAX) { + let init = transcript::init_hash(&bytes); + let book = transcript::book_hash(&bytes); + prop_assert_ne!(init, book); + prop_assert_ne!(transcript::message_digest(55001, &bytes), transcript::message_digest(55003, &bytes)); + prop_assert_ne!(transcript::activation_hash(&init, &book, &init, height), transcript::activation_hash(&init, &book, &init, height + 1)); + } +} diff --git a/lightning-ffor/tests/wire.rs b/lightning-ffor/tests/wire.rs new file mode 100644 index 00000000000..bf7d5973026 --- /dev/null +++ b/lightning-ffor/tests/wire.rs @@ -0,0 +1,551 @@ +use bitcoin::secp256k1::PublicKey; +use lightning_ffor::transcript; +use lightning_ffor::wire::{ + Abort, CloseAck, Message, Payload, Preimage, Tlv, WireError, MAX_MESSAGE_LEN, MAX_TLV_COUNT, +}; +use proptest::prelude::*; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Fixture { + scenario: String, + init_wire: String, + accept_wire: String, + activate_wire: String, + ack_wire: String, +} + +fn fixtures() -> Vec { + serde_json::from_str(include_str!("data/appendix-d.json")).unwrap() +} + +fn hex(value: &str) -> Vec { + (0..value.len()).step_by(2).map(|i| u8::from_str_radix(&value[i..i + 2], 16).unwrap()).collect() +} + +fn receiver() -> PublicKey { + PublicKey::from_slice(&hex( + "039fca7f8157aa768708894ffd92550fe970edd18526a5f936583ea3b54dab3228", + )) + .unwrap() +} + +fn settlement() -> PublicKey { + PublicKey::from_slice(&hex( + "02087b7d1b4789170f6e374f0a0e58a1b7a899e34929795314ab6964e69609e9c0", + )) + .unwrap() +} + +fn init() -> Message { + Message::decode(&hex(&fixtures()[0].init_wire)).unwrap() +} +fn ack() -> Message { + Message::decode(&hex(&fixtures()[0].ack_wire)).unwrap() +} + +// Modified fixture signatures remain untrusted bytes, never newly signed messages. +fn append_tlv(mut wire: Vec, tlv: &[u8]) -> Vec { + wire.splice(wire.len() - 64..wire.len() - 64, tlv.iter().copied()); + wire +} + +fn close_ack() -> Message { + let mut message = ack(); + message.payload = Payload::CloseAck(CloseAck { + activation_hash: [3; 32], + num_slots: 9, + settled: vec![1, 1], + preimages: vec![Preimage { slot: 1, value: [1; 32] }, Preimage { slot: 9, value: [9; 32] }], + preimages_tlv_present: true, + }); + message +} + +#[test] +fn appendix_d_all_published_setup_and_activation_messages_round_trip() { + for fixture in fixtures() { + let initial = Message::decode(&hex(&fixture.init_wire)).unwrap(); + let accept = Message::decode(&hex(&fixture.accept_wire)).unwrap(); + accept.validate_accept(&initial).unwrap(); + for (wire, signer) in [ + (fixture.init_wire, receiver()), + (fixture.accept_wire, settlement()), + (fixture.activate_wire, receiver()), + (fixture.ack_wire, settlement()), + ] { + let bytes = hex(&wire); + let message = Message::decode(&bytes).unwrap(); + assert_eq!(message.encode().unwrap(), bytes, "{}", fixture.scenario); + message.verify_signature(&signer).unwrap(); + assert_eq!( + message.signature_digest().unwrap(), + transcript::message_digest(message.message_type(), &bytes[2..bytes.len() - 64]) + ); + } + } +} + +#[test] +fn truncation_and_oversized_peer_messages_are_rejected() { + let fixture = &fixtures()[0]; + for encoded in + [&fixture.init_wire, &fixture.accept_wire, &fixture.activate_wire, &fixture.ack_wire] + { + let bytes = hex(encoded); + for end in 0..bytes.len() { + assert!(Message::decode(&bytes[..end]).is_err(), "prefix {end}"); + } + } + assert_eq!(Message::decode(&vec![0; MAX_MESSAGE_LEN + 1]), Err(WireError::SizeLimit)); +} + +#[test] +fn unsupported_variants_and_unassigned_message_types_are_rejected() { + for variant in [0, 1, 2, 3, 5, 255] { + let mut bytes = init().encode().unwrap(); + bytes[66] = variant; + assert_eq!(Message::decode(&bytes), Err(WireError::UnsupportedVariant(variant))); + } + for kind in [55011u16, 55021, 55055, 0, u16::MAX] { + let mut bytes = init().encode().unwrap(); + bytes[..2].copy_from_slice(&kind.to_be_bytes()); + assert_eq!(Message::decode(&bytes), Err(WireError::UnsupportedMessage(kind))); + } +} + +#[test] +fn variant_d_disallows_escape_ladders_and_receiver_points() { + for offset in [108, 110] { + let mut bytes = init().encode().unwrap(); + bytes[offset] = 1; + assert_eq!(Message::decode(&bytes), Err(WireError::InvalidField)); + } + for kind in [1, 3, 5] { + let mut bytes = init().encode().unwrap(); + bytes.splice(111..111, [kind, 0]); + assert_eq!(Message::decode(&bytes), Err(WireError::InvalidTlv(u64::from(kind)))); + } +} + +#[test] +fn slot_count_amount_lengths_minimum_budget_and_deadline_are_checked() { + for count in [0u16, 484, u16::MAX] { + let mut bytes = init().encode().unwrap(); + bytes[75..77].copy_from_slice(&count.to_be_bytes()); + assert!(Message::decode(&bytes).is_err()); + } + let mut bytes = init().encode().unwrap(); + bytes[76] = 2; + assert_eq!(Message::decode(&bytes), Err(WireError::InvalidTlv(9))); + for mutate in [0, 1, 2, 3] { + let mut message = init(); + let terms = match &mut message.payload { + Payload::Init(terms) => terms, + _ => panic!(), + }; + match mutate { + 0 => terms.amounts_msat[0] = 0, + 1 => terms.min_payment_msat = u64::MAX, + 2 => terms.budget_msat += 1, + _ => terms.voucher_expiry = terms.settlement_deadline, + } + assert!(message.encode().is_err()); + } +} + +#[test] +fn amount_sum_overflow_is_rejected() { + let mut message = init(); + let terms = match &mut message.payload { + Payload::Init(terms) => terms, + _ => panic!(), + }; + terms.amounts_msat = vec![u64::MAX, 1]; + terms.budget_msat = 0; + terms.min_payment_msat = 1; + assert_eq!(message.encode(), Err(WireError::InvalidField)); +} + +#[test] +fn required_tlv_and_canonical_bigsize_rules_are_enforced() { + let mut no_amounts = init().encode().unwrap(); + no_amounts.drain(111..no_amounts.len() - 64); + assert_eq!(Message::decode(&no_amounts), Err(WireError::MissingTlv(9))); + for tlv in [vec![9, 0], vec![7, 0]] { + assert_eq!( + Message::decode(&append_tlv(init().encode().unwrap(), &tlv)), + Err(WireError::TlvOrder) + ); + } + assert_eq!( + Message::decode(&append_tlv(init().encode().unwrap(), &[10, 0])), + Err(WireError::UnknownEvenTlv(10)) + ); + for nonminimal in [ + vec![0xfd, 0, 11, 0], + vec![11, 0xfd, 0, 0], + vec![0xfe, 0, 0, 1, 1, 0], + vec![0xff, 0, 0, 0, 0, 0, 1, 0, 0, 0], + ] { + assert_eq!( + Message::decode(&append_tlv(init().encode().unwrap(), &nonminimal)), + Err(WireError::NonCanonicalBigSize) + ); + } + assert_eq!( + Message::decode(&append_tlv( + init().encode().unwrap(), + &[11, 0xff, 255, 255, 255, 255, 255, 255, 255, 255] + )), + Err(WireError::SizeLimit) + ); + assert!(Message::decode(&append_tlv(init().encode().unwrap(), &[11, 2, 1])).is_err()); +} + +#[test] +fn unknown_odd_extensions_preserve_exact_bytes_and_are_signed() { + let mut message = init(); + message.extensions = + vec![Tlv { kind: 257, value: vec![7; 253] }, Tlv { kind: u64::MAX, value: vec![] }]; + let wire = message.encode().unwrap(); + assert_eq!(Message::decode(&wire).unwrap(), message); + assert_eq!(message.verify_signature(&receiver()), Err(WireError::InvalidSignature)); + assert_ne!(message.signature_digest().unwrap(), init().signature_digest().unwrap()); +} + +#[test] +fn witness_points_require_compressed_valid_curve_points_and_exact_lengths() { + let mut message = init(); + let terms = match &mut message.payload { + Payload::Init(terms) => terms, + _ => panic!(), + }; + terms.witness_peers = Some(vec![receiver(), settlement()]); + let wire = message.encode().unwrap(); + assert_eq!(Message::decode(&wire).unwrap(), message); + let mut invalid = vec![13, 35, 0, 1]; + invalid.extend_from_slice(&[0; 33]); + assert_eq!( + Message::decode(&append_tlv(init().encode().unwrap(), &invalid)), + Err(WireError::InvalidPoint) + ); + assert!(Message::decode(&append_tlv(init().encode().unwrap(), &[13, 2, 0, 1])).is_err()); + assert_eq!( + Message::decode(&append_tlv(init().encode().unwrap(), &[13, 3, 0, 0, 0])), + Err(WireError::InvalidTlv(13)) + ); +} + +#[test] +fn hash_chain_profile_has_one_canonical_encoding_and_requires_uniform_amounts() { + for value in [vec![], vec![0], vec![2], vec![1, 1]] { + let mut tlv = vec![15, value.len() as u8]; + tlv.extend(value); + assert_eq!( + Message::decode(&append_tlv(init().encode().unwrap(), &tlv)), + Err(WireError::InvalidTlv(15)) + ); + } + let mut message = init(); + let terms = match &mut message.payload { + Payload::Init(terms) => terms, + _ => panic!(), + }; + terms.hash_chain = true; + assert_eq!(Message::decode(&message.encode().unwrap()).unwrap(), message); + let terms = match &mut message.payload { + Payload::Init(terms) => terms, + _ => panic!(), + }; + terms.amounts_msat.push(terms.amounts_msat[0] + 1); + terms.budget_msat = terms.amounts_msat.iter().sum(); + assert_eq!(message.encode(), Err(WireError::InvalidTlv(15))); +} + +#[test] +fn accept_requires_each_known_tlv_and_consistent_slot_identity() { + let fixture = &fixtures()[0]; + let original = Message::decode(&hex(&fixture.accept_wire)).unwrap(); + for mutate in [0, 1, 2] { + let mut message = original.clone(); + let accept = match &mut message.payload { + Payload::Accept(accept) => accept, + _ => panic!(), + }; + match mutate { + 0 => accept.amounts_msat.clear(), + 1 => accept.amounts_msat[0] = 0, + _ => { + accept.payment_hashes.push([2; 32]); + accept.amounts_msat.push(1); + accept.s_htlc_id_base = u64::MAX; + }, + } + assert!(message.encode().is_err()); + } + for (start, len, kind) in [(74, 34, 1), (108, 10, 7), (118, 10, 9), (128, 34, 11)] { + let mut bytes = original.encode().unwrap(); + bytes.drain(start..start + len); + assert_eq!(Message::decode(&bytes), Err(WireError::MissingTlv(kind))); + } +} + +#[test] +fn accept_binds_header_exact_amounts_and_complete_signed_init_bytes() { + let mut initial = init(); + let accept = Message::decode(&hex(&fixtures()[0].accept_wire)).unwrap(); + accept.validate_accept(&initial).unwrap(); + initial.extensions.push(Tlv { kind: 17, value: vec![1] }); + assert_eq!(accept.validate_accept(&initial), Err(WireError::SetupMismatch)); + let mut other = accept.clone(); + other.header.epoch_id[0] ^= 1; + assert_eq!(other.validate_accept(&init()), Err(WireError::SetupMismatch)); + let terms = match &mut other.payload { + Payload::Accept(terms) => terms, + _ => panic!(), + }; + terms.amounts_msat[0] += 1; + other.header = init().header; + assert_eq!(other.validate_accept(&init()), Err(WireError::SetupMismatch)); + assert_eq!(init().validate_accept(&accept), Err(WireError::SetupMismatch)); +} + +#[test] +fn lifecycle_structure_round_trips_but_changed_messages_do_not_authenticate() { + for payload in [ + Payload::Abort(Abort { transcript_hash: [2; 32], reason: 7, data: vec![0, 255, 1] }), + Payload::Close([3; 32]), + close_ack().payload, + ] { + let mut message = ack(); + message.payload = payload; + let bytes = message.encode().unwrap(); + assert_eq!(Message::decode(&bytes).unwrap(), message); + assert_eq!(message.verify_signature(&settlement()), Err(WireError::InvalidSignature)); + } +} + +#[test] +fn close_ack_requires_preimages_matching_every_set_bit_in_order() { + for mutate in [0, 1, 2, 3, 4, 5] { + let mut message = close_ack(); + let close = match &mut message.payload { + Payload::CloseAck(close) => close, + _ => panic!(), + }; + match mutate { + 0 => close.preimages.clear(), + 1 => close.preimages.swap(0, 1), + 2 => close.preimages[1].slot = 1, + 3 => close.settled[1] = 0x81, + 4 => close.num_slots = 0, + _ => close.settled.push(0), + } + assert!(message.encode().is_err()); + } + let mut message = close_ack(); + let close = match &mut message.payload { + Payload::CloseAck(close) => close, + _ => panic!(), + }; + close.settled = vec![0, 0]; + close.preimages.clear(); + let mut bytes = message.encode().unwrap(); + assert_eq!(Message::decode(&bytes).unwrap(), message); + let end = bytes.len() - 64; + bytes.drain(end - 2..end); + let absent = Message::decode(&bytes).unwrap(); + let close = match &absent.payload { + Payload::CloseAck(close) => close, + _ => panic!(), + }; + assert!(!close.preimages_tlv_present); + assert_eq!(absent.encode().unwrap(), bytes); +} + +#[test] +fn encoder_rejects_oversized_data_and_noncanonical_extensions() { + let mut message = ack(); + message.payload = Payload::Abort(Abort { transcript_hash: [2; 32], reason: 8, data: vec![] }); + assert_eq!(message.encode(), Err(WireError::InvalidField)); + let abort = match &mut message.payload { + Payload::Abort(abort) => abort, + _ => panic!(), + }; + abort.reason = 0; + abort.data = vec![0; MAX_MESSAGE_LEN]; + assert_eq!(message.encode(), Err(WireError::SizeLimit)); + for extensions in [ + vec![Tlv { kind: 17, value: vec![0; MAX_MESSAGE_LEN] }], + vec![Tlv { kind: 13, value: vec![] }], + vec![Tlv { kind: 15, value: vec![1] }], + vec![Tlv { kind: 17, value: vec![] }, Tlv { kind: 17, value: vec![] }], + vec![Tlv { kind: 19, value: vec![] }, Tlv { kind: 17, value: vec![] }], + vec![Tlv { kind: 18, value: vec![] }], + ] { + let mut message = init(); + message.extensions = extensions; + assert!(message.encode().is_err()); + } +} + +#[test] +fn zero_out_of_range_and_high_s_signatures_are_rejected() { + for bytes in [[0; 64], [255; 64]] { + let mut message = ack(); + message.signature = bytes; + assert_eq!(message.encode(), Err(WireError::InvalidSignature)); + } + let mut message = ack(); + let order = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + let mut borrow = 0i16; + for i in (0..32).rev() { + let value = i16::from(order[i]) - i16::from(message.signature[i + 32]) - borrow; + message.signature[i + 32] = value.rem_euclid(256) as u8; + borrow = i16::from(value < 0); + } + assert_eq!(message.encode(), Err(WireError::InvalidSignature)); +} + +#[test] +fn unsigned_digest_can_be_requested_without_manufacturing_a_signature() { + let mut message = init(); + let digest = message.signature_digest().unwrap(); + let signed = message.encode().unwrap(); + let unsigned = message.unsigned_wire().unwrap(); + assert_eq!(unsigned, signed[..signed.len() - 64]); + message.signature = [0; 64]; + assert_eq!(message.signature_digest().unwrap(), digest); + assert_eq!(message.unsigned_wire().unwrap(), unsigned); + assert_eq!(message.encode(), Err(WireError::InvalidSignature)); + message.extensions.push(Tlv { kind: 17, value: vec![1] }); + assert_ne!(message.signature_digest().unwrap(), digest); +} + +#[test] +fn encoder_reserves_signature_space_and_bounds_collections_before_processing() { + let mut message = ack(); + message.payload = Payload::Abort(Abort { + transcript_hash: [2; 32], + reason: 0, + data: vec![7; MAX_MESSAGE_LEN - 166], + }); + let bytes = message.encode().unwrap(); + assert_eq!(bytes.len(), MAX_MESSAGE_LEN); + assert_eq!(Message::decode(&bytes).unwrap(), message); + let abort = match &mut message.payload { + Payload::Abort(abort) => abort, + _ => panic!(), + }; + abort.data.push(1); + assert_eq!(message.signature_digest(), Err(WireError::SizeLimit)); + let mut oversized_extensions = init(); + oversized_extensions.extensions = vec![Tlv { kind: 17, value: vec![] }; MAX_TLV_COUNT + 1]; + assert_eq!(oversized_extensions.encode(), Err(WireError::SizeLimit)); + let mut oversized_points = init(); + let terms = match &mut oversized_points.payload { + Payload::Init(terms) => terms, + _ => panic!(), + }; + terms.witness_peers = Some(vec![receiver(); MAX_MESSAGE_LEN / 33 + 1]); + assert_eq!(oversized_points.encode(), Err(WireError::SizeLimit)); + assert_eq!( + Message::decode(&append_tlv(init().encode().unwrap(), &[13, 2, 255, 255])), + Err(WireError::SizeLimit) + ); +} + +#[test] +fn malformed_known_tlv_value_lengths_are_rejected_on_the_wire() { + let mut amounts = init().encode().unwrap(); + amounts[112] = 7; + amounts.remove(120); + assert_eq!(Message::decode(&amounts), Err(WireError::InvalidTlv(9))); + let mut hashes = Message::decode(&hex(&fixtures()[0].accept_wire)).unwrap().encode().unwrap(); + hashes[75] = 31; + hashes.remove(107); + assert_eq!(Message::decode(&hashes), Err(WireError::InvalidTlv(1))); + let mut preimages = close_ack().encode().unwrap(); + preimages[103] = 67; + preimages.remove(171); + assert_eq!(Message::decode(&preimages), Err(WireError::InvalidTlv(1))); + let mut one_slot = close_ack(); + let close = match &mut one_slot.payload { + Payload::CloseAck(close) => close, + _ => panic!(), + }; + close.num_slots = 1; + close.settled = vec![1]; + close.preimages.truncate(1); + let mut too_many_preimages = one_slot.encode().unwrap(); + too_many_preimages[102] = 68; + too_many_preimages.splice(137..137, [0; 34]); + assert_eq!(Message::decode(&too_many_preimages), Err(WireError::InvalidTlv(1))); + let mut absent_preimages = one_slot.encode().unwrap(); + absent_preimages.drain(101..137); + assert!(Message::decode(&absent_preimages).is_err()); +} + +#[test] +fn invalid_wire_abort_reason_and_oversized_constructed_abort_are_rejected() { + let mut message = ack(); + message.payload = Payload::Abort(Abort { transcript_hash: [2; 32], reason: 0, data: vec![] }); + let mut wire = message.encode().unwrap(); + wire[99] = 8; + assert_eq!(Message::decode(&wire), Err(WireError::InvalidField)); + let abort = match &mut message.payload { + Payload::Abort(abort) => abort, + _ => panic!(), + }; + abort.data = vec![0; MAX_MESSAGE_LEN + 1]; + assert_eq!(message.encode(), Err(WireError::SizeLimit)); +} + +#[test] +fn known_extension_collisions_and_zero_s_are_rejected() { + let mut accept = Message::decode(&hex(&fixtures()[0].accept_wire)).unwrap(); + accept.extensions.push(Tlv { kind: 1, value: vec![1] }); + assert_eq!(accept.encode(), Err(WireError::InvalidTlv(1))); + let mut close = close_ack(); + close.extensions.push(Tlv { kind: 1, value: vec![] }); + assert_eq!(close.encode(), Err(WireError::InvalidTlv(1))); + let mut message = ack(); + message.signature[32..].fill(0); + assert_eq!(message.encode(), Err(WireError::InvalidSignature)); + assert!(WireError::InvalidSignature.to_string().contains("InvalidSignature")); +} + +proptest! { + #[test] + fn arbitrary_bounded_input_never_panics_and_accepted_bytes_are_canonical( + bytes in prop::collection::vec(any::(), 0..=MAX_MESSAGE_LEN + 1) + ) { + if let Ok(message) = Message::decode(&bytes) { + prop_assert_eq!(message.encode().unwrap(), bytes); + } + } + + #[test] + fn unknown_odd_tlv_values_round_trip_without_normalization(value in prop::collection::vec(any::(), 0..2048)) { + let mut message = init(); + message.extensions = vec![Tlv { kind: 65_537, value }]; + let bytes = message.encode().unwrap(); + prop_assert_eq!(Message::decode(&bytes).unwrap(), message); + } + + #[test] + fn close_bitmaps_round_trip_with_exactly_the_corresponding_preimages(bits in prop::collection::vec(any::(), 1..=483)) { + let mut close = CloseAck { activation_hash: [3; 32], num_slots: bits.len() as u16, + settled: vec![0; (bits.len() + 7) / 8], preimages: Vec::new(), preimages_tlv_present: true }; + for (index, bit) in bits.into_iter().enumerate() { + if bit { + close.settled[index / 8] |= 1 << (index % 8); + close.preimages.push(Preimage { slot: index as u16 + 1, value: [index as u8; 32] }); + } + } + let mut message = ack(); + message.payload = Payload::CloseAck(close); + prop_assert_eq!(Message::decode(&message.encode().unwrap()).unwrap(), message); + } +} diff --git a/lightning/Cargo.toml b/lightning/Cargo.toml index 2558cd9c68c..19c8c1fd54d 100644 --- a/lightning/Cargo.toml +++ b/lightning/Cargo.toml @@ -23,7 +23,7 @@ _externalize_tests = ["inventory", "_test_utils"] # This is unsafe to use in production because it may result in the counterparty publishing taking our funds. unsafe_revoked_tx_signing = [] -std = [] +std = ["lightning-ffor/std"] dnssec = ["dnssec-prover/validation"] @@ -33,6 +33,7 @@ grind_signatures = [] default = ["std", "grind_signatures"] [dependencies] +lightning-ffor = { version = "0.1.0", path = "../lightning-ffor", default-features = false } lightning-types = { version = "0.3.5", path = "../lightning-types", default-features = false } lightning-invoice = { version = "0.34.0", path = "../lightning-invoice", default-features = false } lightning-macros = { version = "0.2.1", path = "../lightning-macros" } From 9ce29d19de27cbea70698e2ba5531b72d2351c77 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 20:49:00 -0400 Subject: [PATCH 04/22] feat(ffor): persist authenticated receiver setup and recovery --- FFOR.md | 74 +- lightning-background-processor/src/lib.rs | 45 +- .../src/tests/ffor.rs | 270 +++++++ lightning-ffor/src/book.rs | 9 +- lightning-ffor/tests/book.rs | 13 + lightning/src/ln/channel.rs | 9 +- lightning/src/ln/channel/ffor.rs | 19 +- lightning/src/ln/channel/ffor/setup.rs | 419 ++++++++++ lightning/src/ln/channel/ffor/setup/tests.rs | 716 ++++++++++++++++++ lightning/src/ln/channelmanager.rs | 162 +++- .../ln/channelmanager/ffor_recovery_tests.rs | 274 +++++++ lightning/src/ln/ffor.rs | 14 +- lightning/src/ln/ffor_persistence.rs | 144 ++++ lightning/src/ln/ffor_recovery.rs | 227 ++++++ lightning/src/ln/ffor_recovery/tests.rs | 316 ++++++++ lightning/src/ln/ffor_tests.rs | 195 +++++ lightning/src/ln/mod.rs | 2 + lightning/src/util/test_utils.rs | 5 + 18 files changed, 2883 insertions(+), 30 deletions(-) create mode 100644 lightning-background-processor/src/tests/ffor.rs create mode 100644 lightning/src/ln/channel/ffor/setup.rs create mode 100644 lightning/src/ln/channel/ffor/setup/tests.rs create mode 100644 lightning/src/ln/channelmanager/ffor_recovery_tests.rs create mode 100644 lightning/src/ln/ffor_persistence.rs create mode 100644 lightning/src/ln/ffor_recovery.rs create mode 100644 lightning/src/ln/ffor_recovery/tests.rs diff --git a/FFOR.md b/FFOR.md index f391ab34d28..4a3657ee80c 100644 --- a/FFOR.md +++ b/FFOR.md @@ -1,14 +1,43 @@ -# Experimental FFOR receiver parking +# Experimental FFOR receiver setup and parking This fork targets rust-lightning v0.2.5. The current APIs provide receiver voucher parking and point-in-time verification of both commitment views for FFOR Variant D. They do not provide an active offline receiving epoch. -`register_ffor_receiver_book` takes an already authenticated and validated public -book on an empty, connected, synchronized anchor channel. It checks the book's -structural invariants and exact first incoming HTLC ID. The application must persist -the registration before allowing the settlement peer to send vouchers. Registration -requests manager persistence but does not wait for a durable write. +`register_ffor_receiver_setup` authenticates exact signed init and accept messages +against the manager's actual node identity, chain and current height. It checks the +empty, connected, synchronized anchor channel, funding terms, commitment number, +exact next incoming HTLC ID, fees, dust and reserves before installing the book. +Admission currently refuses channels with more than 4096 revealed peer commitments +until a bounded persistent history index exists. It rejects reuse of a revealed +commitment secret, including the secret disclosed by the voucher commitment round. + +The caller supplies a positive claim margin from local deployment policy. The +voucher expiry must be a block height and at least the settlement deadline plus +that margin. This does not select a production reconciliation floor or establish +watcher-free eligibility. The setup remains ineligible for offline invoices. + +`register_ffor_receiver_book` is the lower-level experimental parking API. It checks +the public book's structural invariants and exact first incoming HTLC ID, but does +not retain signed setup evidence and cannot authorize later activation. Both APIs +request manager persistence without waiting for a durable write. The application +must persist registration before allowing the settlement peer to send vouchers. + +Registration returns an opaque persistence requirement. The background processor +captures a token before encoding the manager and completes it only after its +ordered store reports success, both in its processing loop and during shutdown. +`is_ffor_state_persisted` remains false while the write is pending or has failed or +been cancelled. A completed older snapshot cannot release a later mutation, and +tokens from another manager instance or a restart are refused. Custom persistence +loops must follow the same capture, encode, durable write, completion sequence. +After consuming a notification, a custom loop must explicitly retry a cancelled +write by checking `get_and_clear_needs_persistence`; notifications are edge-triggered. +Storage completion alone does not prove that the channel still has the same phase. + +The shared `lightning-ffor` crate owns bounded wire parsing, signature verification, +authenticated setup derivation and transcript hashes. Its runtime supports +`no_std` with `alloc` and Rust 1.63. Node consumes this same crate from the pinned +fork revision. The crate has no channel mutation or invoice authority. The stock channel owns voucher identity through the ordinary commitment rounds. Exact vouchers never enter invoice, keysend, or forwarding processing. The receiver @@ -29,11 +58,23 @@ older readers must reject the channel. Restore validates live voucher ownership against stock inbound HTLCs before accepting the channel, and checks that committed vouchers retain either failure material or their deferred add. +Authenticated setup also enters a manager-owned recovery registry in required TLV +22. Its exact signed messages, canonical book and original admission context survive +explicit closure, monitor-triggered closure and disposal of a stale manager channel. +Restore reauthenticates records, checks the manager's identity and chain, and requires +every retained channel setup to match its archive entry. The registry is bounded to +64 records, 8 MiB total and 192 KiB per record; capacity is reserved before channel +mutation. The setup-only archive version retains no activation or claim authority. +Fully drained channel tombstones permit subsequent ordinary splicing while retaining +the original funding context in their archive. + These are consistency checks, not an authenticated storage envelope. Arbitrarily deleting a mismatching add's ownership record after abort can make its nonreserved hash indistinguishable from an ordinary post-abort payment. No valid writer creates that state: the ownership record and stock HTLC are written together. The current checks do not claim to detect every arbitrary alteration of local storage. +Similarly, an archive without its original live channel cannot independently prove +the historical funding context against arbitrary local storage alteration. No feature bit, wire activation, invoice readiness, commitment freeze, or automatic setup deadline is implemented here. The caller must abort a setup that times out. @@ -64,16 +105,27 @@ cargo test -p lightning --lib ffor_ --offline The compatibility test can export ordinary and registered channel fixtures with `FFOR_LEGACY_FIXTURE_DIR`. An unmodified v0.2.5 channel reader accepted the ordinary fixture and rejected the registered fixture with `UnknownRequiredFeature`. +The same unmodified release reader accepted an ordinary manager fixture and +rejected an archive-only manager fixture with `UnknownRequiredFeature`, after its +last live channel had already been removed. Existing payment, reload, monitor, and quiescence suites also pass. Three signer tests additionally verify the public Appendix D activation signature, domain/type separation, local node identity, low-S output and default refusal. +The manager tests cover revision ordering, stale instances and signer retry after +restart. Background-processor tests inject delayed, failed and cancelled writes, +then verify successful retries and both synchronous and asynchronous final writes. +Authenticated setup tests cover signature and identity mismatches, funding and +commitment mismatches, history bounds, block-height expiry and revealed-secret reuse. +Registry and manager recovery tests cover closure retention, stale channel disposal, +missing or conflicting records, capacity refusal and archive-only serialization. ## Next boundary Reusable epochs require durable retired epoch IDs and voucher hashes, with one current signed transcript record under the same channel authority. Activation must -atomically verify commitment evidence, record exact signed setup and activation -bytes, and freeze ordinary mutations. A manager persistence-completion barrier must -precede acknowledgement release or invoice eligibility. The record must also survive -channel removal and remain available during on-chain resolution, including witness -and mailbox recovery. None of these later guarantees can be inferred from parking. +atomically verify commitment evidence, retain exact signed activation bytes beside +the existing setup archive, and freeze ordinary mutations. Acknowledgement release +and invoice eligibility must use the persistence-completion barrier and recheck the +channel phase. The archive must be extended with active commitment and claim evidence +for on-chain resolution, including witness and mailbox recovery. None of these later +guarantees can be inferred from setup retention or parking. diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index cdf1b2e5aa3..cb5d9c325b6 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -39,6 +39,7 @@ use lightning::events::EventHandler; use lightning::events::EventsProvider; use lightning::events::ReplayEvent; use lightning::events::{Event, PathFailure}; +use lightning::io; use lightning::util::ser::Writeable; use lightning::ln::channelmanager::AChannelManager; @@ -1080,6 +1081,7 @@ where log_trace!(logger, "Persisting ChannelManager..."); let fut = async { + let persistence_token = channel_manager.get_cm().capture_ffor_persistence(); kv_store .write( CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, @@ -1087,7 +1089,15 @@ where CHANNEL_MANAGER_PERSISTENCE_KEY, channel_manager.get_cm().encode(), ) - .await + .await?; + channel_manager.get_cm().ffor_persistence_completed(persistence_token).map_err( + |_| { + io::Error::new( + io::ErrorKind::InvalidData, + "FFOR persistence token belongs to another manager", + ) + }, + ) }; // TODO: Once our MSRV is 1.68 we should be able to drop the Box let mut fut = Box::pin(fut); @@ -1309,6 +1319,7 @@ where // After we exit, ensure we persist the ChannelManager one final time - this avoids // some races where users quit while channel updates were in-flight, with // ChannelMonitor update(s) persisted without a corresponding ChannelManager update. + let persistence_token = channel_manager.get_cm().capture_ffor_persistence(); kv_store .write( CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, @@ -1317,6 +1328,12 @@ where channel_manager.get_cm().encode(), ) .await?; + channel_manager.get_cm().ffor_persistence_completed(persistence_token).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "FFOR persistence token belongs to another manager", + ) + })?; if let Some(ref scorer) = scorer { kv_store .write( @@ -1636,12 +1653,22 @@ impl BackgroundProcessor { } if channel_manager.get_cm().get_and_clear_needs_persistence() { log_trace!(logger, "Persisting ChannelManager..."); + let persistence_token = channel_manager.get_cm().capture_ffor_persistence(); (kv_store.write( CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_KEY, channel_manager.get_cm().encode(), ))?; + channel_manager + .get_cm() + .ffor_persistence_completed(persistence_token) + .map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "FFOR persistence token belongs to another manager", + ) + })?; log_trace!(logger, "Done persisting ChannelManager."); } @@ -1744,12 +1771,21 @@ impl BackgroundProcessor { // After we exit, ensure we persist the ChannelManager one final time - this avoids // some races where users quit while channel updates were in-flight, with // ChannelMonitor update(s) persisted without a corresponding ChannelManager update. + let persistence_token = channel_manager.get_cm().capture_ffor_persistence(); kv_store.write( CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_KEY, channel_manager.get_cm().encode(), )?; + channel_manager.get_cm().ffor_persistence_completed(persistence_token).map_err( + |_| { + io::Error::new( + io::ErrorKind::InvalidData, + "FFOR persistence token belongs to another manager", + ) + }, + )?; if let Some(ref scorer) = scorer { kv_store.write( SCORER_PERSISTENCE_PRIMARY_NAMESPACE, @@ -1878,6 +1914,8 @@ mod tests { use std::time::Duration; use std::{env, fs}; + mod ffor; + const EVENT_DEADLINE: Duration = Duration::from_millis(5 * (FRESHNESS_TIMER.as_millis() as u64)); @@ -2064,6 +2102,7 @@ mod tests { } struct Persister { + manager_write_gate: Option, graph_error: Option<(std::io::ErrorKind, &'static str)>, graph_persistence_notifier: Option>, manager_error: Option<(std::io::ErrorKind, &'static str)>, @@ -2075,6 +2114,7 @@ mod tests { fn new(data_dir: PathBuf) -> Self { let kv_store = FilesystemStore::new(data_dir); Self { + manager_write_gate: None, graph_error: None, graph_persistence_notifier: None, manager_error: None, @@ -2118,6 +2158,9 @@ mod tests { && secondary_namespace == CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE && key == CHANNEL_MANAGER_PERSISTENCE_KEY { + if let Some(gate) = &self.manager_write_gate { + gate.wait()?; + } if let Some((error, message)) = self.manager_error { return Err(std::io::Error::new(error, message).into()); } diff --git a/lightning-background-processor/src/tests/ffor.rs b/lightning-background-processor/src/tests/ffor.rs new file mode 100644 index 00000000000..389705b9525 --- /dev/null +++ b/lightning-background-processor/src/tests/ffor.rs @@ -0,0 +1,270 @@ +//! Storage fault injection for the actual background-processor completion hooks. + +use super::*; +use core::future::Future; +use core::pin::Pin; +use lightning::sign::ChangeDestinationSourceSyncWrapper; +use lightning::util::async_poll::AsyncResult; +use std::sync::{mpsc, Mutex}; +use tokio::sync::{mpsc as async_mpsc, oneshot, watch}; + +type WriteResult = Result<(), std::io::ErrorKind>; + +pub(super) struct SyncWriteGate { + started: mpsc::SyncSender<()>, + complete: Mutex>, +} + +impl SyncWriteGate { + pub(super) fn wait(&self) -> lightning::io::Result<()> { + self.started.send(()).unwrap(); + self.complete + .lock() + .unwrap() + .recv_timeout(EVENT_DEADLINE) + .unwrap() + .map_err(|kind| std::io::Error::new(kind, "injected write failure").into()) + } +} + +#[test] +fn ffor_sync_completion_waits_for_storage_and_preserves_later_revision_on_error() { + let (_, nodes) = create_nodes(1, "ffor_sync_persistence"); + let node = &nodes[0]; + let first = node.node.testing_request_ffor_persistence(); + let (started, writes) = mpsc::sync_channel(1); + let (complete, results) = mpsc::sync_channel(1); + let mut store = Persister::new(node.kv_store.get_data_dir()); + store.manager_write_gate = Some(SyncWriteGate { started, complete: Mutex::new(results) }); + let processor = BackgroundProcessor::start( + Arc::new(store), + |_| Ok(()), + Arc::clone(&node.chain_monitor), + Arc::clone(&node.node), + Some(Arc::clone(&node.messenger)), + node.no_gossip_sync(), + Arc::clone(&node.peer_manager), + Some(Arc::clone(&node.liquidity_manager)), + Some(Arc::clone(&node.sweeper)), + Arc::clone(&node.logger), + Some(Arc::clone(&node.scorer)), + ); + writes.recv_timeout(EVENT_DEADLINE).unwrap(); + assert!(!node.node.is_ffor_state_persisted(&first)); + assert!(read_manager(node).is_err()); + let later = node.node.testing_request_ffor_persistence(); + complete.send(Ok(())).unwrap(); + writes.recv_timeout(EVENT_DEADLINE).unwrap(); + assert!(node.node.is_ffor_state_persisted(&first)); + assert!(read_manager(node).is_ok()); + assert!(!node.node.is_ffor_state_persisted(&later)); + complete.send(Err(std::io::ErrorKind::Other)).unwrap(); + assert_eq!(processor.join().unwrap_err().kind(), std::io::ErrorKind::Other); + assert!(!node.node.is_ffor_state_persisted(&later)); + assert!(node.node.get_and_clear_needs_persistence()); +} + +fn read_manager(node: &Node) -> lightning::io::Result> { + node.kv_store.read( + CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, + CHANNEL_MANAGER_PERSISTENCE_KEY, + ) +} + +struct AsyncStore { + inner: Arc, + writes: async_mpsc::UnboundedSender>, +} + +impl lightning::util::persist::KVStore for AsyncStore { + fn read( + &self, primary: &str, secondary: &str, key: &str, + ) -> AsyncResult<'static, Vec, lightning::io::Error> { + let result = self.inner.read(primary, secondary, key); + Box::pin(async move { result }) + } + + fn write( + &self, primary: &str, secondary: &str, key: &str, buf: Vec, + ) -> AsyncResult<'static, (), lightning::io::Error> { + let inner = Arc::clone(&self.inner); + let names = (primary.to_owned(), secondary.to_owned(), key.to_owned()); + let gate = if primary == CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE + && secondary == CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE + && key == CHANNEL_MANAGER_PERSISTENCE_KEY + { + let (complete, gate) = oneshot::channel(); + self.writes.send(complete).unwrap(); + Some(gate) + } else { + None + }; + Box::pin(async move { + if let Some(gate) = gate { + gate.await + .unwrap() + .map_err(|kind| std::io::Error::new(kind, "injected write failure"))?; + } + inner.write(&names.0, &names.1, &names.2, buf) + }) + } + + fn remove( + &self, primary: &str, secondary: &str, key: &str, lazy: bool, + ) -> AsyncResult<'static, (), lightning::io::Error> { + let result = self.inner.remove(primary, secondary, key, lazy); + Box::pin(async move { result }) + } + + fn list( + &self, primary: &str, secondary: &str, + ) -> AsyncResult<'static, Vec, lightning::io::Error> { + let result = self.inner.list(primary, secondary); + Box::pin(async move { result }) + } +} + +type AsyncSweeper = OutputSweeper< + Arc, + ChangeDestinationSourceSyncWrapper>, + Arc, + Arc, + Arc, + Arc, + Arc, +>; + +async fn run_async( + node: &Node, store: Arc, stop: watch::Receiver, +) -> lightning::io::Result<()> { + super::super::process_events_async( + store, + |_| async { Ok(()) }, + Arc::clone(&node.chain_monitor), + Arc::clone(&node.node), + Some(Arc::clone(&node.messenger)), + node.no_gossip_sync(), + Arc::clone(&node.peer_manager), + Some(node.liquidity_manager.get_lm_async()), + None::>, + Arc::clone(&node.logger), + Some(Arc::clone(&node.scorer)), + move |duration| { + let mut stop = stop.clone(); + Box::pin(async move { + if *stop.borrow() { + return true; + } + tokio::select! { + _ = tokio::time::sleep(duration) => false, + _ = stop.changed() => true, + } + }) + }, + false, + || Some(Duration::ZERO), + ) + .await +} + +async fn next_write>>( + processor: Pin<&mut F>, + writes: &mut async_mpsc::UnboundedReceiver>, +) -> oneshot::Sender { + tokio::select! { + result = processor => panic!("processor exited before expected write: {:?}", result), + write = tokio::time::timeout(EVENT_DEADLINE, writes.recv()) => write.unwrap().unwrap(), + } +} + +#[tokio::test] +async fn ffor_async_completion_waits_for_storage_and_preserves_later_revision_on_error() { + let (_, nodes) = create_nodes(1, "ffor_async_persistence"); + let node = &nodes[0]; + let first = node.node.testing_request_ffor_persistence(); + let (writes_tx, mut writes) = async_mpsc::unbounded_channel(); + let store = Arc::new(AsyncStore { inner: Arc::clone(&node.kv_store), writes: writes_tx }); + let (_stop_tx, stop) = watch::channel(false); + let mut processor = Box::pin(run_async(node, store, stop)); + let complete = next_write(processor.as_mut(), &mut writes).await; + assert!(!node.node.is_ffor_state_persisted(&first)); + assert!(read_manager(node).is_err()); + let later = node.node.testing_request_ffor_persistence(); + complete.send(Ok(())).unwrap(); + let complete = next_write(processor.as_mut(), &mut writes).await; + assert!(node.node.is_ffor_state_persisted(&first)); + assert!(read_manager(node).is_ok()); + assert!(!node.node.is_ffor_state_persisted(&later)); + complete.send(Err(std::io::ErrorKind::Other)).unwrap(); + let error = tokio::time::timeout(EVENT_DEADLINE, processor).await.unwrap().unwrap_err(); + assert_eq!(error.kind(), lightning::io::ErrorKind::Other); + assert!(!node.node.is_ffor_state_persisted(&later)); + assert!(node.node.get_and_clear_needs_persistence()); +} + +#[tokio::test] +async fn ffor_cancelled_async_write_requires_retry_and_final_shutdown_write() { + let (_, nodes) = create_nodes(1, "ffor_cancelled_persistence"); + let node = &nodes[0]; + let requirement = node.node.testing_request_ffor_persistence(); + let (writes_tx, mut writes) = async_mpsc::unbounded_channel(); + let store = Arc::new(AsyncStore { inner: Arc::clone(&node.kv_store), writes: writes_tx }); + let (stop_tx, stop) = watch::channel(false); + let mut processor = Box::pin(run_async(node, Arc::clone(&store), stop.clone())); + let cancelled = next_write(processor.as_mut(), &mut writes).await; + drop(processor); + assert!(cancelled.send(Ok(())).is_err()); + assert!(read_manager(node).is_err()); + assert!(!node.node.is_ffor_state_persisted(&requirement)); + assert!(node.node.get_and_clear_needs_persistence()); + + let mut processor = Box::pin(run_async(node, store, stop)); + let retry = next_write(processor.as_mut(), &mut writes).await; + let later = node.node.testing_request_ffor_persistence(); + stop_tx.send(true).unwrap(); + retry.send(Ok(())).unwrap(); + let final_write = next_write(processor.as_mut(), &mut writes).await; + assert!(node.node.is_ffor_state_persisted(&requirement)); + assert!(!node.node.is_ffor_state_persisted(&later)); + assert!(read_manager(node).is_ok()); + final_write.send(Ok(())).unwrap(); + tokio::time::timeout(EVENT_DEADLINE, processor).await.unwrap().unwrap(); + assert!(node.node.is_ffor_state_persisted(&later)); + assert!(read_manager(node).is_ok()); +} + +#[test] +fn ffor_sync_shutdown_completes_only_its_final_durable_snapshot() { + let (_, nodes) = create_nodes(1, "ffor_sync_shutdown_persistence"); + let node = &nodes[0]; + let first = node.node.testing_request_ffor_persistence(); + let (started, writes) = mpsc::sync_channel(1); + let (complete, results) = mpsc::sync_channel(1); + let mut store = Persister::new(node.kv_store.get_data_dir()); + store.manager_write_gate = Some(SyncWriteGate { started, complete: Mutex::new(results) }); + let processor = BackgroundProcessor::start( + Arc::new(store), + |_| Ok(()), + Arc::clone(&node.chain_monitor), + Arc::clone(&node.node), + Some(Arc::clone(&node.messenger)), + node.no_gossip_sync(), + Arc::clone(&node.peer_manager), + Some(Arc::clone(&node.liquidity_manager)), + Some(Arc::clone(&node.sweeper)), + Arc::clone(&node.logger), + Some(Arc::clone(&node.scorer)), + ); + writes.recv_timeout(EVENT_DEADLINE).unwrap(); + let later = node.node.testing_request_ffor_persistence(); + processor.stop_thread.store(true, std::sync::atomic::Ordering::Release); + complete.send(Ok(())).unwrap(); + writes.recv_timeout(EVENT_DEADLINE).unwrap(); + assert!(node.node.is_ffor_state_persisted(&first)); + assert!(!node.node.is_ffor_state_persisted(&later)); + complete.send(Ok(())).unwrap(); + processor.join().unwrap(); + assert!(node.node.is_ffor_state_persisted(&later)); + assert!(read_manager(node).is_ok()); +} diff --git a/lightning-ffor/src/book.rs b/lightning-ffor/src/book.rs index 9d3172cc9ac..c5fb7d4d1a3 100644 --- a/lightning-ffor/src/book.rs +++ b/lightning-ffor/src/book.rs @@ -7,6 +7,8 @@ use alloc::vec::Vec; use core::fmt; +use bitcoin::locktime::absolute::LOCK_TIME_THRESHOLD; + use crate::amounts::{AmountError, FeePolicy}; /// BOLT 2 upper bound, not a promise of available channel capacity. @@ -28,7 +30,7 @@ pub struct BookTerms { pub fees: FeePolicy, /// Last height at which the peer can admit delegated payments. pub settlement_deadline: u32, - /// Uniform absolute HTLC expiry. + /// Uniform absolute HTLC expiry height, strictly below Bitcoin's locktime threshold. pub voucher_expiry: u32, } @@ -75,7 +77,7 @@ pub enum BookError { BudgetMismatch, /// The voucher sum exceeds the receiver's negotiated in-flight bound. InFlightLimit, - /// Admission has already ended, or the required claim margin does not fit. + /// Admission has ended, the claim margin does not fit, or expiry is not a block height. Deadline, /// The settlement peer cannot fund the book and retain its reserve. SettlementReserve, @@ -119,7 +121,8 @@ pub fn validate_anchor_book( return Err(BookError::SlotCount); } let required_expiry = terms.settlement_deadline.checked_add(claim_margin_blocks); - if terms.settlement_deadline <= current_height + if terms.voucher_expiry >= LOCK_TIME_THRESHOLD + || terms.settlement_deadline <= current_height || claim_margin_blocks == 0 || required_expiry.map_or(true, |expiry| terms.voucher_expiry < expiry) { diff --git a/lightning-ffor/tests/book.rs b/lightning-ffor/tests/book.rs index 56392d02487..31888ea97e7 100644 --- a/lightning-ffor/tests/book.rs +++ b/lightning-ffor/tests/book.rs @@ -126,6 +126,19 @@ fn rejects_expired_or_overflowing_deadlines() { ); } +#[test] +fn voucher_expiry_must_be_a_block_height() { + let mut book = terms(vec![1_000_000]); + book.voucher_expiry = 499_999_999; + book.settlement_deadline = book.voucher_expiry - 1008; + assert_eq!(validate(&book, &channel()), Ok(())); + for expiry in [500_000_000, u32::MAX] { + book.voucher_expiry = expiry; + book.settlement_deadline = expiry - 1008; + assert_eq!(validate(&book, &channel()), Err(BookError::Deadline)); + } +} + #[test] fn rejects_arithmetic_overflow_without_wrapping() { let mut book = terms(vec![1_000_000]); diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 98afa93262e..30e8eb68764 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -100,6 +100,9 @@ use core::{cmp, fmt, mem}; use super::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}; mod ffor; +#[cfg(test)] +pub(crate) use ffor::ffor_setup_test_messages; +pub(crate) use ffor::FFORReceiverSetup; #[cfg(any(test, feature = "_test_utils"))] #[allow(unused)] @@ -15616,7 +15619,7 @@ where } } - Ok(FundedChannel { + let mut channel = FundedChannel { funding: FundingScope { value_to_self_msat, counterparty_selected_channel_reserve_satoshis, @@ -15755,7 +15758,9 @@ where holder_commitment_point, pending_splice, quiescent_action, - }) + }; + channel.ffor_restored()?; + Ok(channel) } } diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index 5a40fb3ca69..3b20aaaf299 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -1,9 +1,14 @@ use super::*; + +mod setup; use crate::ln::ffor::{ self as verification, FFORCommitmentError, FFORMonitorSnapshot, FFORReceiverAbortReason, FFORReceiverError, FFORReceiverStatus, FFORSettlementParty, FFORVoucher, FFORVoucherCommitments, FFORVoucherFailure, }; +#[cfg(test)] +pub(crate) use setup::ffor_setup_test_messages; +pub(crate) use setup::FFORReceiverSetup; /// Channel-owned identity survives the stock inbound HTLC's transition to `Committed`. pub(super) struct FFORReceiverBook { @@ -11,6 +16,7 @@ pub(super) struct FFORReceiverBook { vouchers: Vec, received: Vec, abort_reason: Option, + setup: Option, } struct FFORReceivedVoucher { @@ -28,6 +34,8 @@ impl_writeable_tlv_based!(FFORReceiverBook, { (2, vouchers, required_vec), (4, received, required_vec), (6, abort_reason, option), + // Readers predating authenticated setup must refuse this record. + (8, setup, option), }); impl FFORReceiverBook { @@ -82,7 +90,9 @@ impl FFORReceiverBook { return Err(DecodeError::InvalidValue); } } - self.abort(FFORReceiverAbortReason::Restarted); + if let Some(setup) = self.setup.as_ref() { + setup.validate_book(self).map_err(|_| DecodeError::InvalidValue)?; + } Ok(()) } @@ -117,6 +127,7 @@ where vouchers: vouchers.to_vec(), received: Vec::new(), abort_reason: None, + setup: None, }); Ok(()) } @@ -150,6 +161,9 @@ where total_vouchers: book.vouchers.len() as u16, }); } + if self.ffor_revealed_secret_matches()? { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } let commitments = self.ffor_voucher_commitments( FFORSettlementParty::Counterparty, &book.vouchers, @@ -234,6 +248,7 @@ where pub(crate) fn ffor_validate_unwind_material( &self, deferred_adds: &[msgs::UpdateAddHTLC], ) -> Result<(), DecodeError> { + self.ffor_validate_receiver_setup()?; let book = match self.context.ffor_receiver_book.as_ref() { Some(book) => book, None => return Ok(()), @@ -264,6 +279,7 @@ where } pub(crate) fn ffor_park_received_htlc(&mut self, htlc_id: u64, failure: FFORVoucherFailure) { + self.ffor_abort_revealed_secret_reuse(); if let Some(book) = self.context.ffor_receiver_book.as_mut() { if let Some(received) = book.received.iter_mut().find(|received| received.voucher.htlc_id == htlc_id) @@ -278,6 +294,7 @@ where where L::Target: Logger, { + self.ffor_abort_revealed_secret_reuse(); let book = match self.context.ffor_receiver_book.as_ref() { Some(book) if book.abort_reason.is_some() => book, _ => return, diff --git a/lightning/src/ln/channel/ffor/setup.rs b/lightning/src/ln/channel/ffor/setup.rs new file mode 100644 index 00000000000..96e4b41f4e9 --- /dev/null +++ b/lightning/src/ln/channel/ffor/setup.rs @@ -0,0 +1,419 @@ +//! Signed setup retained by the channel's experimental receiver parking record. +//! +//! Admission currently supports at most 4096 revealed peer commitments. Longer histories are +//! refused until a bounded native history index exists. A setup remains preactivation evidence, +//! with the parking record's one-registration limit and conservative abort on restart. + +use super::*; +use bitcoin::locktime::absolute::LOCK_TIME_THRESHOLD; +use lightning_ffor::book::{validate_anchor_book, AnchorChannelLimits}; +use lightning_ffor::setup::AuthenticatedSetup; +use lightning_ffor::wire::{Message, Payload, MAX_MESSAGE_LEN}; + +// This experimental admission path scans shachain under the channel lock. Older histories +// require a future durable digest index; refuse them before doing unbounded work. +const MAX_SETUP_COMMITMENT_HISTORY: u64 = 4096; + +fn checked_history_start(minimum: u64) -> Result { + let count = (INITIAL_COMMITMENT_NUMBER + 1).checked_sub(minimum).ok_or_else(invalid_setup)?; + if count > MAX_SETUP_COMMITMENT_HISTORY { + return Err(invalid_setup()); + } + Ok(minimum) +} + +/// Admission context is historical evidence, never a replacement for live channel authority. +/// The signed messages retain all optional extensions in their exact canonical wire encoding. +#[derive(Clone)] +pub(crate) struct FFORReceiverSetup { + init_wire: Vec, + accept_wire: Vec, + receiver: PublicKey, + settlement: PublicKey, + chain_hash: ChainHash, + funding_txo: OutPoint, + channel_value_sat: u64, + receiver_balance_msat: u64, + feerate_sat_per_kw: u32, + settlement_is_funder: bool, + admission_height: u32, + claim_margin_blocks: u32, +} + +impl_writeable_tlv_based!(FFORReceiverSetup, { + (0, init_wire, required_vec), + (2, accept_wire, required_vec), + (4, receiver, required), + (6, settlement, required), + (8, chain_hash, required), + (10, funding_txo, required), + (12, channel_value_sat, required), + (14, receiver_balance_msat, required), + (16, feerate_sat_per_kw, required), + (18, settlement_is_funder, required), + (20, admission_height, required), + (22, claim_margin_blocks, required), +}); + +fn invalid_setup() -> FFORReceiverError { + FFORCommitmentError::InvalidVoucherBook.into() +} + +impl FFORReceiverSetup { + fn authenticate(&self) -> Result { + let init = Message::decode(&self.init_wire).map_err(|_| invalid_setup())?; + let accept = Message::decode(&self.accept_wire).map_err(|_| invalid_setup())?; + AuthenticatedSetup::new(&init, &accept, self.receiver, self.settlement) + .map_err(|_| invalid_setup()) + } + + pub(super) fn validate_book( + &self, book: &FFORReceiverBook, + ) -> Result { + let setup = self.validate_record()?; + if setup.header().epoch_id != book.epoch_id || setup.vouchers().len() != book.vouchers.len() + { + return Err(invalid_setup()); + } + for (signed, parked) in setup.vouchers().iter().zip(&book.vouchers) { + if signed.htlc_id != parked.htlc_id + || signed.payment_hash != parked.payment_hash.0 + || signed.amount_msat != parked.amount_msat + || signed.expiry != parked.cltv_expiry + { + return Err(invalid_setup()); + } + } + + Ok(setup) + } + + /// Reauthenticate historical signed evidence and its expiry height without claiming current + /// channel eligibility. Timestamp locktimes are invalid even for fully drained tombstones. + pub(crate) fn validate_recovery(&self) -> Result { + self.validate_record().map_err(|_| DecodeError::InvalidValue) + } + + fn validate_record(&self) -> Result { + let setup = self.authenticate()?; + let terms = setup.terms(); + let maximum_balance = self.channel_value_sat.checked_mul(1000).ok_or_else(invalid_setup)?; + if terms.voucher_expiry >= LOCK_TIME_THRESHOLD + || self.receiver_balance_msat > maximum_balance + || self.claim_margin_blocks == 0 + || self.admission_height >= terms.settlement_deadline + || terms + .settlement_deadline + .checked_add(self.claim_margin_blocks) + .map_or(true, |expiry| expiry > terms.voucher_expiry) + { + return Err(invalid_setup()); + } + Ok(setup) + } + + pub(crate) fn receiver(&self) -> PublicKey { + self.receiver + } + pub(crate) fn chain_hash(&self) -> ChainHash { + self.chain_hash + } + pub(crate) fn funding_txo(&self) -> OutPoint { + self.funding_txo + } +} + +impl FundedChannel +where + SP::Target: SignerProvider, +{ + /// Authenticate a complete setup before any vouchers are offered. Identity, chain and height + /// arguments are supplied exclusively from the containing manager, under its consistency lock. + /// This retains no activation acknowledgement and grants no invoice or mutation authority. + #[cfg(test)] + pub(crate) fn register_ffor_receiver_setup( + &mut self, init_wire: &[u8], accept_wire: &[u8], our_node_id: PublicKey, + chain_hash: ChainHash, current_height: u32, claim_margin_blocks: u32, + ) -> Result<(), FFORReceiverError> { + let prepared = self.prepare_ffor_receiver_setup( + init_wire, + accept_wire, + our_node_id, + chain_hash, + current_height, + claim_margin_blocks, + )?; + self.install_prepared_ffor_receiver_setup(prepared) + } + + pub(crate) fn prepare_ffor_receiver_setup( + &self, init_wire: &[u8], accept_wire: &[u8], our_node_id: PublicKey, chain_hash: ChainHash, + current_height: u32, claim_margin_blocks: u32, + ) -> Result { + if self.context.ffor_receiver_book.is_some() { + return Err(FFORReceiverError::AlreadyRegistered); + } + self.check_ffor_synchronized()?; + if init_wire.len() > MAX_MESSAGE_LEN || accept_wire.len() > MAX_MESSAGE_LEN { + return Err(invalid_setup()); + } + + let record = FFORReceiverSetup { + init_wire: init_wire.to_vec(), + accept_wire: accept_wire.to_vec(), + receiver: our_node_id, + settlement: self.context.counterparty_node_id, + chain_hash, + funding_txo: self.funding.get_funding_txo().ok_or_else(invalid_setup)?, + channel_value_sat: self.funding.get_value_satoshis(), + receiver_balance_msat: self.funding.value_to_self_msat, + feerate_sat_per_kw: self.context.feerate_per_kw, + settlement_is_funder: !self.funding.is_outbound(), + admission_height: current_height, + claim_margin_blocks, + }; + let setup = record.authenticate()?; + if setup.header().channel_id != self.context.channel_id().0 { + return Err(invalid_setup()); + } + let accept = match &setup.accept().payload { + Payload::Accept(accept) => accept, + _ => return Err(invalid_setup()), + }; + if accept.s_commitment_number != self.ffor_counterparty_commitment_number()? { + return Err(invalid_setup()); + } + let limits = self.ffor_setup_limits(&record)?; + validate_anchor_book(&setup.terms(), &limits, current_height, claim_margin_blocks) + .map_err(|_| invalid_setup())?; + self.ffor_check_revealed_history(setup.vouchers()[0].payment_hash)?; + + if !self.context.is_connected() + || !self.context.pending_inbound_htlcs.is_empty() + || !self.context.pending_outbound_htlcs.is_empty() + || accept.s_htlc_id_base != self.context.next_counterparty_htlc_id + { + return Err(invalid_setup()); + } + Ok(record) + } + + /// Install immediately after preparing while retaining the same manager and peer locks. + /// Recheck mutable authority so an internal stale record cannot reserve different state. + pub(crate) fn install_prepared_ffor_receiver_setup( + &mut self, record: FFORReceiverSetup, + ) -> Result<(), FFORReceiverError> { + let setup = record.validate_record()?; + let accept = match &setup.accept().payload { + Payload::Accept(accept) => accept, + _ => return Err(invalid_setup()), + }; + if record.settlement != self.context.counterparty_node_id + || setup.header().channel_id != self.context.channel_id().0 + || record.funding_txo != self.funding.get_funding_txo().ok_or_else(invalid_setup)? + || record.channel_value_sat != self.funding.get_value_satoshis() + || record.receiver_balance_msat != self.funding.value_to_self_msat + || record.feerate_sat_per_kw != self.context.feerate_per_kw + || record.settlement_is_funder == self.funding.is_outbound() + || accept.s_commitment_number != self.ffor_counterparty_commitment_number()? + { + return Err(invalid_setup()); + } + self.ffor_check_revealed_history(setup.vouchers()[0].payment_hash)?; + let limits = self.ffor_setup_limits(&record)?; + validate_anchor_book( + &setup.terms(), + &limits, + record.admission_height, + record.claim_margin_blocks, + ) + .map_err(|_| invalid_setup())?; + let vouchers: Vec<_> = setup + .vouchers() + .iter() + .map(|voucher| FFORVoucher { + htlc_id: voucher.htlc_id, + payment_hash: PaymentHash(voucher.payment_hash), + amount_msat: voucher.amount_msat, + cltv_expiry: voucher.expiry, + }) + .collect(); + self.register_ffor_receiver_book(setup.header().epoch_id, &vouchers)?; + self.context.ffor_receiver_book.as_mut().unwrap().setup = Some(record); + Ok(()) + } + + fn ffor_check_revealed_history(&self, first_hash: [u8; 32]) -> Result<(), FFORReceiverError> { + // The bound is checked before iterating any state-derived index range. A future durable + // digest index can admit busier channels without scanning their complete shachain history. + let start = checked_history_start(self.context.commitment_secrets.get_min_seen_secret())?; + for index in start..=INITIAL_COMMITMENT_NUMBER { + if self + .context + .commitment_secrets + .get_secret(index) + .map_or(false, |secret| Sha256::hash(&secret).to_byte_array() == first_hash) + { + return Err(invalid_setup()); + } + } + Ok(()) + } + + fn ffor_counterparty_commitment_number(&self) -> Result { + let current = self + .context + .counterparty_next_commitment_transaction_number + .checked_add(1) + .ok_or_else(invalid_setup)?; + INITIAL_COMMITMENT_NUMBER.checked_sub(current).ok_or_else(invalid_setup) + } + + fn ffor_setup_limits( + &self, record: &FFORReceiverSetup, + ) -> Result { + let settlement_balance_msat = record + .channel_value_sat + .checked_mul(1000) + .and_then(|total| total.checked_sub(record.receiver_balance_msat)) + .ok_or_else(invalid_setup)?; + Ok(AnchorChannelLimits { + max_accepted_htlcs: self.context.holder_max_accepted_htlcs, + max_in_flight_msat: self.context.holder_max_htlc_value_in_flight_msat, + htlc_minimum_msat: self.context.holder_htlc_minimum_msat, + receiver_dust_sat: self.context.holder_dust_limit_satoshis, + settlement_dust_sat: self.context.counterparty_dust_limit_satoshis, + settlement_balance_msat, + receiver_balance_msat: record.receiver_balance_msat, + settlement_reserve_sat: self.funding.holder_selected_channel_reserve_satoshis, + receiver_reserve_sat: self + .funding + .counterparty_selected_channel_reserve_satoshis + .ok_or_else(invalid_setup)?, + settlement_is_funder: record.settlement_is_funder, + feerate_sat_per_kw: record.feerate_sat_per_kw, + }) + } + + /// Check n0 again once the stock voucher round has revealed it. Never derive or expose it + /// outside this channel, and do not accept caller-provided revocation evidence. + pub(super) fn ffor_revealed_secret_matches(&self) -> Result { + let book = match self.context.ffor_receiver_book.as_ref() { + Some(book) if book.abort_reason.is_none() => book, + _ => return Ok(false), + }; + let record = match book.setup.as_ref() { + Some(record) => record, + None => return Ok(false), + }; + let setup = record.validate_book(book)?; + let number = match &setup.accept().payload { + Payload::Accept(accept) => accept.s_commitment_number, + _ => return Err(invalid_setup()), + }; + let index = INITIAL_COMMITMENT_NUMBER.checked_sub(number).ok_or_else(invalid_setup)?; + if index < self.context.commitment_secrets.get_min_seen_secret() { + return Ok(false); + } + let secret = self.context.commitment_secrets.get_secret(index).ok_or_else(invalid_setup)?; + Ok(Sha256::hash(&secret).to_byte_array() == setup.vouchers()[0].payment_hash) + } + + pub(super) fn ffor_abort_revealed_secret_reuse(&mut self) { + if self.ffor_revealed_secret_matches().unwrap_or(true) { + if let Some(book) = self.context.ffor_receiver_book.as_mut() { + book.abort(FFORReceiverAbortReason::CommitmentSecretReused); + } + } + } + + /// Export a validated immutable setup for the containing manager's retained evidence registry. + pub(crate) fn ffor_receiver_setup_record( + &self, + ) -> Result, DecodeError> { + self.ffor_validate_receiver_setup()?; + Ok(self.context.ffor_receiver_book.as_ref().and_then(|book| book.setup.clone())) + } + + /// Rebind the saved receiver and network to the manager restoring this channel. + pub(crate) fn ffor_validate_receiver_identity( + &self, our_node_id: PublicKey, chain_hash: ChainHash, + ) -> Result<(), DecodeError> { + if let Some(record) = + self.context.ffor_receiver_book.as_ref().and_then(|book| book.setup.as_ref()) + { + if record.receiver != our_node_id || record.chain_hash != chain_hash { + return Err(DecodeError::InvalidValue); + } + } + Ok(()) + } + + /// Validate before the reader converts a live setup into an aborted restart tombstone. + pub(in crate::ln::channel) fn ffor_restored(&mut self) -> Result<(), DecodeError> { + self.ffor_validate_receiver_setup()?; + if let Some(book) = self.context.ffor_receiver_book.as_mut() { + book.abort(FFORReceiverAbortReason::Restarted); + } + Ok(()) + } + + pub(crate) fn ffor_validate_receiver_setup(&self) -> Result<(), DecodeError> { + let book = match self.context.ffor_receiver_book.as_ref() { + Some(book) => book, + None => return Ok(()), + }; + let record = match book.setup.as_ref() { + Some(record) => record, + None => return Ok(()), + }; + let setup = record.validate_book(book).map_err(|_| DecodeError::InvalidValue)?; + if record.settlement != self.context.counterparty_node_id + || setup.header().channel_id != self.context.channel_id().0 + { + return Err(DecodeError::InvalidValue); + } + let pending = self.context.pending_inbound_htlcs.iter().any(|htlc| book.owns(htlc.htlc_id)); + if book.abort_reason.is_none() || pending { + let channel_type = self.funding.get_channel_type(); + if record.funding_txo + != self.funding.get_funding_txo().ok_or(DecodeError::InvalidValue)? + || record.channel_value_sat != self.funding.get_value_satoshis() + || record.settlement_is_funder == self.funding.is_outbound() + || !channel_type.supports_static_remote_key() + || !channel_type.supports_anchors_zero_fee_htlc_tx() + || channel_type.supports_taproot() + || channel_type.supports_anchor_zero_fee_commitments() + { + return Err(DecodeError::InvalidValue); + } + let accept = match &setup.accept().payload { + Payload::Accept(accept) => accept, + _ => return Err(DecodeError::InvalidValue), + }; + if accept.s_commitment_number + > self + .ffor_counterparty_commitment_number() + .map_err(|_| DecodeError::InvalidValue)? + || accept.s_htlc_id_base > self.context.next_counterparty_htlc_id + { + return Err(DecodeError::InvalidValue); + } + let limits = self.ffor_setup_limits(record).map_err(|_| DecodeError::InvalidValue)?; + validate_anchor_book( + &setup.terms(), + &limits, + record.admission_height, + record.claim_margin_blocks, + ) + .map_err(|_| DecodeError::InvalidValue)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests; + +#[cfg(test)] +pub(crate) use tests::messages as ffor_setup_test_messages; diff --git a/lightning/src/ln/channel/ffor/setup/tests.rs b/lightning/src/ln/channel/ffor/setup/tests.rs new file mode 100644 index 00000000000..b5deddd9726 --- /dev/null +++ b/lightning/src/ln/channel/ffor/setup/tests.rs @@ -0,0 +1,716 @@ +use super::*; +use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; +use crate::ln::functional_test_utils::*; +use crate::sign::ffor::FFORSigningRequest; +use crate::sign::{KeysManager, NodeSigner, Recipient}; +use lightning_ffor::transcript; +use lightning_ffor::wire::{Accept, Header, Init, Tlv}; + +fn chain_hash() -> ChainHash { + ChainHash::using_genesis_block(bitcoin::Network::Testnet) +} + +fn sign(message: &mut Message, node: &Node) { + let keys = KeysManager::new(&node.node_seed, 0, 0, true); + assert_eq!(keys.get_node_id(Recipient::Node).unwrap(), node.node.get_our_node_id()); + let wire = message.unsigned_wire().unwrap(); + message.signature = keys + .sign_ffor_message(&FFORSigningRequest::new(&wire).unwrap()) + .unwrap() + .serialize_compact(); +} + +pub(crate) fn messages( + sender: &Node, receiver: &Node, channel_id: ChannelId, voucher: FFORVoucher, +) -> (Message, Message) { + let number = { + let peers = sender.node.per_peer_state.read().unwrap(); + let peer = peers.get(&receiver.node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get(&channel_id).unwrap().as_funded().unwrap(); + // The sender's own current view supplies n0 independently of the receiver's peer counter. + INITIAL_COMMITMENT_NUMBER - channel.holder_commitment_point.current_transaction_number() + }; + let mut init = Message { + header: Header { channel_id: channel_id.0, epoch_id: [81; 32] }, + payload: Payload::Init(Init { + budget_msat: voucher.amount_msat, + min_payment_msat: voucher.amount_msat, + settlement_deadline: voucher.cltv_expiry - 20, + voucher_expiry: voucher.cltv_expiry, + fee_base_msat: 0, + fee_proportional_millionths: 0, + amounts_msat: vec![voucher.amount_msat], + witness_peers: None, + hash_chain: false, + }), + extensions: vec![Tlv { kind: 101, value: vec![9, 8, 7] }], + signature: [0; 64], + }; + sign(&mut init, receiver); + let mut accept = Message { + header: init.header, + payload: Payload::Accept(Accept { + s_commitment_number: number, + payment_hashes: vec![voucher.payment_hash.0], + s_htlc_id_base: voucher.htlc_id, + amounts_msat: vec![voucher.amount_msat], + init_hash: transcript::init_hash(&init.encode().unwrap()), + }), + extensions: vec![Tlv { kind: 103, value: vec![6, 5] }], + signature: [0; 64], + }; + sign(&mut accept, sender); + (init, accept) +} + +#[test] +fn ffor_setup_authenticates_both_funder_roles_and_persists_exact_wire() { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let (funder, peer) = if receiver_funds { (1, 0) } else { (0, 1) }; + let channel_id = create_announced_chan_between_nodes_with_value( + &nodes, funder, peer, 100_000, 40_000_000, + ) + .2; + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + let (init, accept) = messages(&nodes[0], &nodes[1], channel_id, voucher); + let init_wire = init.encode().unwrap(); + let accept_wire = accept.encode().unwrap(); + let requirement = nodes[1] + .node + .register_ffor_receiver_setup( + &channel_id, + &nodes[0].node.get_our_node_id(), + &init_wire, + &accept_wire, + 20, + ) + .unwrap(); + assert!(!nodes[1].node.is_ffor_state_persisted(&requirement)); + let token = nodes[1].node.capture_ffor_persistence(); + let _persisted = nodes[1].node.encode(); + nodes[1].node.ffor_persistence_completed(token).unwrap(); + assert!(nodes[1].node.is_ffor_state_persisted(&requirement)); + + { + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get_mut(&channel_id).unwrap().as_funded_mut().unwrap(); + channel + .ffor_validate_receiver_identity(nodes[1].node.get_our_node_id(), chain_hash()) + .unwrap(); + assert!(channel + .ffor_validate_receiver_identity(nodes[0].node.get_our_node_id(), chain_hash()) + .is_err()); + assert!(channel + .ffor_validate_receiver_identity( + nodes[1].node.get_our_node_id(), + ChainHash::using_genesis_block(bitcoin::Network::Bitcoin) + ) + .is_err()); + let features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + let encoded = channel.encode(); + let restored = FundedChannel::read( + &mut &encoded[..], + (&nodes[1].keys_manager, &nodes[1].keys_manager, &features), + ) + .unwrap(); + let book = restored.context.ffor_receiver_book.as_ref().unwrap(); + assert_eq!(book.abort_reason, Some(FFORReceiverAbortReason::Restarted)); + let record = book.setup.as_ref().unwrap(); + assert_eq!(record.init_wire, init_wire); + assert_eq!(record.accept_wire, accept_wire); + assert_eq!(record.settlement_is_funder, !receiver_funds); + } + deliver_parked_voucher(&nodes[0], &nodes[1], update); + let monitor = get_monitor!(nodes[1], channel_id).ffor_commitment_snapshot().unwrap(); + assert!(matches!( + nodes[1] + .node + .ffor_receiver_book_status(&channel_id, &nodes[0].node.get_our_node_id(), &monitor) + .unwrap(), + FFORReceiverStatus::Parked { .. } + )); + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get_mut(&channel_id).unwrap().as_funded_mut().unwrap(); + let encoded = channel.encode(); + let features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + let mut restored = FundedChannel::read( + &mut &encoded[..], + (&nodes[1].keys_manager, &nodes[1].keys_manager, &features), + ) + .unwrap(); + assert_eq!( + restored.context.ffor_receiver_book.as_ref().unwrap().setup.as_ref().unwrap().init_wire, + init_wire + ); + assert_eq!( + restored.register_ffor_receiver_setup( + &init_wire, + &accept_wire, + nodes[1].node.get_our_node_id(), + chain_hash(), + nodes[1].node.current_best_block().height, + 20 + ), + Err(FFORReceiverError::AlreadyRegistered) + ); + } +} + +#[test] +fn ffor_setup_rejects_signed_mismatches_without_registering() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([55; 32]), + amount_msat: 2_000_000, + cltv_expiry: 300, + }; + for mutation in 0..8 { + let (mut init, mut accept) = messages(&nodes[0], &nodes[1], channel_id, voucher); + match mutation { + 0 => { + init.header.channel_id[0] ^= 1; + accept.header = init.header; + }, + 1 => { + if let Payload::Accept(terms) = &mut accept.payload { + terms.s_htlc_id_base += 1; + } + }, + 2 => { + if let Payload::Accept(terms) = &mut accept.payload { + terms.s_commitment_number += 1; + } + }, + 3 => { + if let Payload::Accept(terms) = &mut accept.payload { + terms.amounts_msat[0] += 1; + } + }, + 4 => { + if let Payload::Init(terms) = &mut init.payload { + terms.voucher_expiry -= 1; + } + }, + _ => {}, + } + sign(&mut init, &nodes[1]); + if let Payload::Accept(terms) = &mut accept.payload { + terms.init_hash = transcript::init_hash(&init.encode().unwrap()); + } + sign(&mut accept, &nodes[0]); + if mutation == 5 { + sign(&mut accept, &nodes[1]); + } + let mut init_wire = init.encode().unwrap(); + if mutation == 6 { + let last = init_wire.len() - 1; + init_wire[last] ^= 1; + } + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get_mut(&channel_id).unwrap().as_funded_mut().unwrap(); + let result = channel.register_ffor_receiver_setup( + &init_wire, + &accept.encode().unwrap(), + nodes[1].node.get_our_node_id(), + chain_hash(), + nodes[1].node.current_best_block().height, + if mutation == 7 { 0 } else { 20 }, + ); + assert_eq!(result, Err(invalid_setup()), "mutation {}", mutation); + assert!(channel.context.ffor_receiver_book.is_none()); + } +} + +#[test] +fn ffor_setup_rejects_timestamp_expiry_before_registration_and_restore() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([55; 32]), + amount_msat: 2_000_000, + cltv_expiry: 499_999_999, + }; + crate::ln::ffor::validate_vouchers(&[voucher]).unwrap(); + assert!(matches!( + nodes[1].node.register_ffor_receiver_book( + &channel_id, + &nodes[0].node.get_our_node_id(), + [81; 32], + &[FFORVoucher { cltv_expiry: 500_000_000, ..voucher }], + ), + Err(FFORReceiverError::ChannelState(FFORCommitmentError::InvalidVoucherBook)) + )); + let (init, accept) = messages(&nodes[0], &nodes[1], channel_id, voucher); + let (timestamp_init, timestamp_accept) = messages( + &nodes[0], + &nodes[1], + channel_id, + FFORVoucher { cltv_expiry: 500_000_000, ..voucher }, + ); + let timestamp_init_wire = timestamp_init.encode().unwrap(); + let timestamp_accept_wire = timestamp_accept.encode().unwrap(); + // Both statements are authentic and mutually bound; only native height eligibility fails. + AuthenticatedSetup::new( + ×tamp_init, + ×tamp_accept, + nodes[1].node.get_our_node_id(), + nodes[0].node.get_our_node_id(), + ) + .unwrap(); + assert!(matches!( + nodes[1].node.register_ffor_receiver_setup( + &channel_id, + &nodes[0].node.get_our_node_id(), + ×tamp_init_wire, + ×tamp_accept_wire, + 20, + ), + Err(FFORReceiverError::ChannelState(FFORCommitmentError::InvalidVoucherBook)) + )); + // Refusing the invalid setup leaves the same channel available for a valid registration. + nodes[1] + .node + .register_ffor_receiver_setup( + &channel_id, + &nodes[0].node.get_our_node_id(), + &init.encode().unwrap(), + &accept.encode().unwrap(), + 20, + ) + .unwrap(); + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get_mut(&channel_id).unwrap().as_funded_mut().unwrap(); + let original = channel.context.ffor_receiver_book.as_ref().unwrap().encode(); + let record = channel.context.ffor_receiver_book.as_ref().unwrap().setup.as_ref().unwrap(); + record.validate_recovery().unwrap(); + let features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + FundedChannel::read( + &mut &channel.encode()[..], + (&nodes[1].keys_manager, &nodes[1].keys_manager, &features), + ) + .unwrap(); + for abort_reason in [None, Some(FFORReceiverAbortReason::Requested)] { + let book = channel.context.ffor_receiver_book.as_mut().unwrap(); + book.abort_reason = abort_reason; + book.vouchers[0].cltv_expiry = 500_000_000; + let record = book.setup.as_mut().unwrap(); + record.init_wire = timestamp_init_wire.clone(); + record.accept_wire = timestamp_accept_wire.clone(); + assert!(matches!(record.validate_recovery(), Err(DecodeError::InvalidValue))); + let encoded = channel.encode(); + channel.context.ffor_receiver_book = + Some(FFORReceiverBook::read(&mut &original[..]).unwrap()); + assert!(matches!( + FundedChannel::read( + &mut &encoded[..], + (&nodes[1].keys_manager, &nodes[1].keys_manager, &features), + ), + Err(DecodeError::InvalidValue) + )); + } +} + +#[test] +fn ffor_setup_history_range_is_bounded_before_scanning() { + let end = INITIAL_COMMITMENT_NUMBER + 1; + assert_eq!(checked_history_start(end), Ok(end)); + assert_eq!( + checked_history_start(end - MAX_SETUP_COMMITMENT_HISTORY), + Ok(end - MAX_SETUP_COMMITMENT_HISTORY) + ); + assert_eq!(checked_history_start(end - MAX_SETUP_COMMITMENT_HISTORY - 1), Err(invalid_setup())); + assert_eq!(checked_history_start(0), Err(invalid_setup())); + assert_eq!(checked_history_start(end + 1), Err(invalid_setup())); +} + +#[test] +fn ffor_setup_rejects_revealed_history_and_unwinds_n0_reuse() { + use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; + use crate::ln::msgs::ChannelMessageHandler; + for revealed_before_setup in [true, false] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let secret = if revealed_before_setup { + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let peer = peers.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get(&channel_id).unwrap().as_funded().unwrap(); + channel + .context + .commitment_secrets + .get_secret(channel.context.commitment_secrets.get_min_seen_secret()) + .unwrap() + } else { + let peers = nodes[0].node.per_peer_state.read().unwrap(); + let peer = peers.get(&nodes[1].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get(&channel_id).unwrap().as_funded().unwrap(); + // Only this test reconstructs the sender's known fixture seed to imitate a malicious + // sender. The receiver production path can access only already revealed secrets. + let keys = KeysManager::new(&nodes[0].node_seed, 0, 0, true); + let signer = keys.derive_channel_signer(channel.context.channel_keys_id); + let index = channel.holder_commitment_point.current_transaction_number(); + let secret = chan_utils::build_commitment_secret(&signer.commitment_seed, index); + assert_eq!( + PublicKey::from_secret_key( + &Secp256k1::new(), + &SecretKey::from_slice(&secret).unwrap() + ), + channel.holder_commitment_point.current_point().unwrap() + ); + secret + }; + let hash = PaymentHash(Sha256::hash(&secret).to_byte_array()); + let (mut route, _, _, payment_secret) = + get_route_and_payment_hash!(&nodes[0], &nodes[1], 2_000_000); + route.paths[0].hops[0].cltv_expiry_delta = 144; + nodes[0] + .node + .send_payment_with_route( + route, + hash, + RecipientOnionFields::secret_only(payment_secret), + PaymentId(hash.0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + let update = get_htlc_update_msgs!(&nodes[0], nodes[1].node.get_our_node_id()); + let add = &update.update_add_htlcs[0]; + let voucher = FFORVoucher { + htlc_id: add.htlc_id, + payment_hash: hash, + amount_msat: add.amount_msat, + cltv_expiry: add.cltv_expiry, + }; + let (init, accept) = messages(&nodes[0], &nodes[1], channel_id, voucher); + let result = nodes[1].node.register_ffor_receiver_setup( + &channel_id, + &nodes[0].node.get_our_node_id(), + &init.encode().unwrap(), + &accept.encode().unwrap(), + 20, + ); + if revealed_before_setup { + assert!(matches!( + result, + Err(FFORReceiverError::ChannelState(FFORCommitmentError::InvalidVoucherBook)) + )); + // The test sender has already offered an add; finish it through an explicitly + // aborted raw registration to leave its ordinary commitment round well formed. + nodes[1] + .node + .register_ffor_receiver_book( + &channel_id, + &nodes[0].node.get_our_node_id(), + init.header.epoch_id, + &[voucher], + ) + .unwrap(); + nodes[1] + .node + .abort_ffor_receiver_book( + &channel_id, + &nodes[0].node.get_our_node_id(), + init.header.epoch_id, + ) + .unwrap(); + } else { + result.unwrap(); + } + deliver_parked_voucher(&nodes[0], &nodes[1], update); + let monitor = get_monitor!(nodes[1], channel_id).ffor_commitment_snapshot().unwrap(); + let status = nodes[1] + .node + .ffor_receiver_book_status(&channel_id, &nodes[0].node.get_our_node_id(), &monitor) + .unwrap(); + assert_eq!( + status, + FFORReceiverStatus::Aborting { + reason: if revealed_before_setup { + FFORReceiverAbortReason::Requested + } else { + FFORReceiverAbortReason::CommitmentSecretReused + } + } + ); + let update = get_htlc_update_msgs!(&nodes[1], nodes[0].node.get_our_node_id()); + check_added_monitors(&nodes[1], 1); + assert_eq!(update.update_fail_htlcs.len(), 1); + nodes[0] + .node + .handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &update.update_fail_htlcs[0]); + commitment_signed_dance!(&nodes[0], &nodes[1], update.commitment_signed, false); + expect_payment_failed!(&nodes[0], hash, false); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + } +} + +#[test] +fn ffor_setup_rechecks_live_limits_and_refuses_stale_preparation() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([55; 32]), + amount_msat: 2_000_000, + cltv_expiry: 300, + }; + let (init, accept) = messages(&nodes[0], &nodes[1], channel_id, voucher); + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get_mut(&channel_id).unwrap().as_funded_mut().unwrap(); + let prepare = |channel: &FundedChannel<_>| { + channel.prepare_ffor_receiver_setup( + &init.encode().unwrap(), + &accept.encode().unwrap(), + nodes[1].node.get_our_node_id(), + chain_hash(), + nodes[1].node.current_best_block().height, + 20, + ) + }; + let original_secrets = channel.context.commitment_secrets.encode(); + let mut corrupt_secrets = original_secrets.clone(); + corrupt_secrets[32..40].copy_from_slice(&0_u64.to_be_bytes()); + channel.context.commitment_secrets = + CounterpartyCommitmentSecrets::read(&mut &corrupt_secrets[..]).unwrap(); + assert!(matches!( + prepare(channel), + Err(FFORReceiverError::ChannelState(FFORCommitmentError::InvalidVoucherBook)) + )); + channel.context.commitment_secrets = + CounterpartyCommitmentSecrets::read(&mut &original_secrets[..]).unwrap(); + for mutation in 0..8 { + let prepared = prepare(channel).unwrap(); + let original = ( + channel.context.holder_max_accepted_htlcs, + channel.context.holder_max_htlc_value_in_flight_msat, + channel.context.holder_htlc_minimum_msat, + channel.context.holder_dust_limit_satoshis, + channel.context.counterparty_dust_limit_satoshis, + channel.funding.holder_selected_channel_reserve_satoshis, + channel.context.feerate_per_kw, + channel.funding.value_to_self_msat, + ); + match mutation { + 0 => channel.context.holder_max_accepted_htlcs = 0, + 1 => channel.context.holder_max_htlc_value_in_flight_msat = voucher.amount_msat - 1, + 2 => channel.context.holder_htlc_minimum_msat = voucher.amount_msat + 1, + 3 => channel.context.holder_dust_limit_satoshis = voucher.amount_msat / 1000 + 1, + 4 => channel.context.counterparty_dust_limit_satoshis = voucher.amount_msat / 1000 + 1, + 5 => { + channel.funding.holder_selected_channel_reserve_satoshis = + channel.funding.get_value_satoshis() + }, + 6 => channel.context.feerate_per_kw += 1, + _ => channel.funding.value_to_self_msat += 1, + } + assert_eq!( + channel.install_prepared_ffor_receiver_setup(prepared), + Err(invalid_setup()), + "mutation {}", + mutation + ); + assert!(channel.context.ffor_receiver_book.is_none()); + ( + channel.context.holder_max_accepted_htlcs, + channel.context.holder_max_htlc_value_in_flight_msat, + channel.context.holder_htlc_minimum_msat, + channel.context.holder_dust_limit_satoshis, + channel.context.counterparty_dust_limit_satoshis, + channel.funding.holder_selected_channel_reserve_satoshis, + channel.context.feerate_per_kw, + channel.funding.value_to_self_msat, + ) = original; + } +} + +#[test] +fn ffor_setup_restore_reauthenticates_before_restart_and_requires_its_fence() { + struct OldBook { + epoch_id: [u8; 32], + vouchers: Vec, + received: Vec, + abort_reason: Option, + } + impl_writeable_tlv_based!(OldBook, { (0, epoch_id, required), (2, vouchers, required_vec), (4, received, required_vec), (6, abort_reason, option) }); + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([55; 32]), + amount_msat: 2_000_000, + cltv_expiry: 300, + }; + let (init, accept) = messages(&nodes[0], &nodes[1], channel_id, voucher); + nodes[1] + .node + .register_ffor_receiver_setup( + &channel_id, + &nodes[0].node.get_our_node_id(), + &init.encode().unwrap(), + &accept.encode().unwrap(), + 20, + ) + .unwrap(); + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get_mut(&channel_id).unwrap().as_funded_mut().unwrap(); + let original = channel.context.ffor_receiver_book.as_ref().unwrap().encode(); + assert!(matches!(OldBook::read(&mut &original[..]), Err(DecodeError::UnknownRequiredFeature))); + for mutation in 0..6 { + let book = channel.context.ffor_receiver_book.as_mut().unwrap(); + let record = book.setup.as_mut().unwrap(); + match mutation { + 0 => record.receiver = nodes[0].node.get_our_node_id(), + 1 => { + let len = record.accept_wire.len(); + record.accept_wire[len - 1] ^= 1; + }, + 2 => book.vouchers[0].payment_hash.0[0] ^= 1, + 3 => record.funding_txo.index ^= 1, + 4 => record.receiver_balance_msat = u64::MAX, + _ => record.claim_margin_blocks = 0, + } + let encoded = channel.encode(); + channel.context.ffor_receiver_book = + Some(FFORReceiverBook::read(&mut &original[..]).unwrap()); + let features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + let restored = FundedChannel::read( + &mut &encoded[..], + (&nodes[1].keys_manager, &nodes[1].keys_manager, &features), + ); + assert!(matches!(restored, Err(DecodeError::InvalidValue)), "mutation {}", mutation); + } + // A fully drained, explicitly aborted tombstone retains its old funding scope, allowing + // ordinary splicing to proceed without turning the old record back into live evidence. + channel.context.ffor_receiver_book.as_mut().unwrap().abort(FFORReceiverAbortReason::Requested); + let old_txo = channel.funding.channel_transaction_parameters.funding_outpoint; + channel.funding.channel_transaction_parameters.funding_outpoint.as_mut().unwrap().index ^= 1; + let encoded = channel.encode(); + channel.funding.channel_transaction_parameters.funding_outpoint = old_txo; + let features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + let mut restored = FundedChannel::read( + &mut &encoded[..], + (&nodes[1].keys_manager, &nodes[1].keys_manager, &features), + ) + .unwrap(); + assert_eq!( + restored.context.ffor_receiver_book.as_ref().unwrap().abort_reason, + Some(FFORReceiverAbortReason::Requested) + ); + assert_eq!( + restored.register_ffor_receiver_setup( + &init.encode().unwrap(), + &accept.encode().unwrap(), + nodes[1].node.get_our_node_id(), + chain_hash(), + nodes[1].node.current_best_block().height, + 20 + ), + Err(FFORReceiverError::AlreadyRegistered) + ); +} + +#[test] +fn ffor_setup_manager_restore_rebinds_actual_node_and_chain() { + use crate::ln::channelmanager::ChannelManagerReadArgs; + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([55; 32]), + amount_msat: 2_000_000, + cltv_expiry: 300, + }; + let (init, accept) = messages(&nodes[0], &nodes[1], channel_id, voucher); + nodes[1] + .node + .register_ffor_receiver_setup( + &channel_id, + &nodes[0].node.get_our_node_id(), + &init.encode().unwrap(), + &accept.encode().unwrap(), + 20, + ) + .unwrap(); + let encoded = nodes[1].node.encode(); + let monitor_bytes = get_monitor!(nodes[1], channel_id).encode(); + let (_, monitor) = + <(BlockHash, ChannelMonitor)>::read( + &mut &monitor_bytes[..], + (nodes[1].keys_manager, nodes[1].keys_manager), + ) + .unwrap(); + for mutation in 0..3 { + let mut bytes = encoded.clone(); + if mutation == 2 { + // The manager's chain hash is the first field following its two version bytes. + bytes[2..34] + .copy_from_slice(&ChainHash::using_genesis_block(bitcoin::Network::Bitcoin)[..]); + } + let mut channel_monitors = new_hash_map(); + channel_monitors.insert(channel_id, &monitor); + let node = &nodes[1]; + let result = <(BlockHash, TestChannelManager)>::read( + &mut &bytes[..], + ChannelManagerReadArgs { + config: config.clone(), + entropy_source: node.keys_manager, + node_signer: if mutation == 1 { nodes[0].keys_manager } else { node.keys_manager }, + signer_provider: node.keys_manager, + fee_estimator: node.fee_estimator, + router: node.router, + message_router: node.message_router, + chain_monitor: node.chain_monitor, + tx_broadcaster: node.tx_broadcaster, + logger: node.logger, + channel_monitors, + }, + ); + if mutation == 0 { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(DecodeError::InvalidValue)), "mutation {}", mutation); + } + } +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 927a00b3266..acf066e343b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -69,6 +69,9 @@ use crate::ln::ffor::{ FFORCommitmentError, FFORMonitorSnapshot, FFORReceiverError, FFORReceiverStatus, FFORSettlementParty, FFORVoucher, FFORVoucherCommitments, }; +use crate::ln::ffor_persistence::FFORPersistenceBarrier; +pub use crate::ln::ffor_persistence::{FFORPersistenceRequirement, FFORPersistenceToken}; +use crate::ln::ffor_recovery::FFORRecoveryRegistry; use crate::ln::funding::SpliceContribution; use crate::ln::inbound_payment; use crate::ln::interactivetxs::InteractiveTxMessageSend; @@ -2900,6 +2903,10 @@ pub struct ChannelManager< event_persist_notifier: Notifier, needs_persist_flag: AtomicBool, + // Completion metadata is process-local; an old token cannot prove a restored state durable. + ffor_persistence: Mutex, + // Evidence outlives channels, including force-close and stale-manager restoration. + ffor_recovery: Mutex, /// Tracks the message events that are to be broadcasted when we are connected to some peer. pending_broadcast_messages: Mutex>, @@ -4022,6 +4029,8 @@ where background_events_processed_since_startup: AtomicBool::new(false), event_persist_notifier: Notifier::new(), needs_persist_flag: AtomicBool::new(false), + ffor_persistence: Mutex::new(FFORPersistenceBarrier::new()), + ffor_recovery: Mutex::new(FFORRecoveryRegistry::new()), funding_batch_states: Mutex::new(BTreeMap::new()), pending_broadcast_messages: Mutex::new(Vec::new()), @@ -4305,8 +4314,9 @@ where /// The channel must be connected, synchronized, empty, and use a supported anchor channel type. /// The first voucher must use the peer's exact next incoming HTLC ID. Matching vouchers are kept /// out of ordinary payment processing, including `PaymentClaimable` and forwarding events. - /// The caller is responsible for authenticating the epoch and persisting the manager before - /// instructing the peer to send vouchers. This method requests persistence but does not await it. + /// The caller is responsible for authenticating the epoch and awaiting the returned persistence + /// requirement before instructing the peer to send vouchers. Query it with + /// [`Self::is_ffor_state_persisted`]. This method requests persistence but does not await it. /// /// This is a private experimental protocol boundary, with no feature advertisement or activation. /// It does not freeze commitment updates or authorize invoices. Disconnects and restarts abort @@ -4317,8 +4327,46 @@ where pub fn register_ffor_receiver_book( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], vouchers: &[FFORVoucher], - ) -> Result<(), FFORReceiverError> { + ) -> Result { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let per_peer_state = self.per_peer_state.read().unwrap(); + let mut peer = per_peer_state + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.register_ffor_receiver_book(epoch_id, vouchers)?; + Ok(requirement) + } + + /// Authenticate and retain a signed Variant D setup before receiving its voucher book. + /// + /// The manager supplies the actual local identity, chain and current height. The channel checks + /// peer identity, commitment numbering, funding terms and the exact next incoming HTLC ID. + /// `claim_margin_blocks` is local deployment policy and must not come from the peer. Invalid + /// signatures, incompatible terms, or unsupported channel history leave the book unregistered. + /// + /// Await the returned persistence requirement before telling the peer to send vouchers. This + /// retains setup evidence but does not activate or freeze the channel, or authorize an invoice. + /// The same abort, restart and one-registration limits as [`Self::register_ffor_receiver_book`] + /// apply. Raw-book registration does not supply the authenticated evidence retained here. + pub fn register_ffor_receiver_setup( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, init_wire: &[u8], + accept_wire: &[u8], claim_margin_blocks: u32, + ) -> Result { let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let current_height = self.best_block.read().unwrap().height; let per_peer_state = self.per_peer_state.read().unwrap(); let mut peer = per_peer_state .get(counterparty_node_id) @@ -4330,7 +4378,26 @@ where .get_mut(channel_id) .and_then(Channel::as_funded_mut) .ok_or(FFORCommitmentError::ChannelUnavailable)?; - channel.register_ffor_receiver_book(epoch_id, vouchers) + let setup = channel.prepare_ffor_receiver_setup( + init_wire, + accept_wire, + self.our_network_pubkey, + self.chain_hash, + current_height, + claim_margin_blocks, + )?; + let mut recovery = self.ffor_recovery.lock().unwrap(); + let insertion = + recovery.prepare_insert(&setup).map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.install_prepared_ffor_receiver_setup(setup)?; + insertion.commit(); + Ok(requirement) } /// Inspect an experimental receiver registration. `Parked` requires the complete two-view @@ -14766,7 +14833,56 @@ where /// See [`Self::get_event_or_persistence_needed_future`] for retrieving a [`Future`] that /// indicates this should be checked. pub fn get_and_clear_needs_persistence(&self) -> bool { + // A failed or cancelled write must not clear an outstanding FFOR durability barrier. self.needs_persist_flag.swap(false, Ordering::AcqRel) + | self.ffor_persistence.lock().unwrap().needs_persistence() + } + + /// Capture a completion token immediately before serializing this manager for persistence. + /// + /// Use a single ordered persister for all manager writes. After capturing, encode this manager, + /// durably write those bytes, then call [`Self::ffor_persistence_completed`] with the token. + /// Never capture after encoding, acknowledge a failed write, or race unordered writes to the + /// same storage key. The usual monitor persistence requirements also continue to apply. + /// + /// Capturing waits for in-flight protected mutations to finish. Any later mutation remains + /// conservatively blocked until a subsequent token completes, even if it entered the snapshot. + /// This does not itself release messages or establish activation readiness. + pub fn capture_ffor_persistence(&self) -> FFORPersistenceToken { + let _consistency_lock = self.total_consistency_lock.write().unwrap(); + self.ffor_persistence.lock().unwrap().capture() + } + + /// Confirm the durable write paired with a token from [`Self::capture_ffor_persistence`]. + /// + /// Call only after that write succeeds. A token from a different or restored manager is refused. + /// Older completions are idempotent, but the writes themselves must still be ordered. This + /// records only storage completion; channel phase and authenticated peer evidence remain + /// separate requirements for any later action. + pub fn ffor_persistence_completed(&self, token: FFORPersistenceToken) -> Result<(), ()> { + let advanced = { + let _consistency_lock = self.total_consistency_lock.read().unwrap(); + self.ffor_persistence.lock().unwrap().complete(token)? + }; + if advanced { + self.event_persist_notifier.notify(); + } + Ok(()) + } + + /// Whether a snapshot covering this protected mutation or a later state has reached storage. + /// + /// False is returned for another manager instance, including one restored from the same bytes. + /// A true result does not mean the channel's phase is unchanged or that an invoice is ready. + pub fn is_ffor_state_persisted(&self, requirement: &FFORPersistenceRequirement) -> bool { + self.ffor_persistence.lock().unwrap().is_complete(requirement) + } + + /// Request a protected revision without channel setup for persistence integration tests. + #[cfg(any(test, feature = "_test_utils"))] + pub fn testing_request_ffor_persistence(&self) -> FFORPersistenceRequirement { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + self.ffor_persistence.lock().unwrap().request().unwrap() } #[cfg(any(test, feature = "_test_utils"))] @@ -16605,6 +16721,8 @@ where } } + let ffor_recovery = self.ffor_recovery.lock().unwrap(); + let ffor_recovery = if ffor_recovery.is_empty() { None } else { Some(&*ffor_recovery) }; write_tlv_fields!(writer, { (1, pending_outbound_payments_no_retry, required), (2, pending_intercepted_htlcs, option), @@ -16623,6 +16741,7 @@ where (17, in_flight_monitor_updates, option), (19, peer_storage_dir, optional_vec), (21, WithoutLength(&self.flow.writeable_async_receive_offer_cache()), required), + (22, ffor_recovery, option), }); // Remove the SpliceFailed events added earlier. @@ -16913,6 +17032,8 @@ where let chain_hash: ChainHash = Readable::read(reader)?; let best_block_height: u32 = Readable::read(reader)?; let best_block_hash: BlockHash = Readable::read(reader)?; + let our_network_pubkey = + args.node_signer.get_node_id(Recipient::Node).map_err(|_| DecodeError::InvalidValue)?; let empty_peer_state = || PeerState { channel_by_id: new_hash_map(), @@ -16928,6 +17049,7 @@ where }; let mut failed_htlcs = Vec::new(); + let mut ffor_channel_setups = Vec::new(); let channel_count: u64 = Readable::read(reader)?; let mut channel_id_set = hash_set_with_capacity(cmp::min(channel_count as usize, 128)); let mut per_peer_state = hash_map_with_capacity(cmp::min( @@ -16946,6 +17068,10 @@ where &provided_channel_type_features(&args.config), ), )?; + channel.ffor_validate_receiver_identity(our_network_pubkey, chain_hash)?; + if let Some(setup) = channel.ffor_receiver_setup_record()? { + ffor_channel_setups.push(setup); + } let logger = WithChannelContext::from(&args.logger, &channel.context, None); let channel_id = channel.context.channel_id(); channel_id_set.insert(channel_id); @@ -17323,6 +17449,7 @@ where let mut inbound_payment_id_secret = None; let mut peer_storage_dir: Option)>> = None; let mut async_receive_offer_cache: AsyncReceiveOfferCache = AsyncReceiveOfferCache::new(); + let mut ffor_recovery = None; read_tlv_fields!(reader, { (1, pending_outbound_payments_no_retry, option), (2, pending_intercepted_htlcs, option), @@ -17341,12 +17468,24 @@ where (17, in_flight_monitor_updates, option), (19, peer_storage_dir, optional_vec), (21, async_receive_offer_cache, (default_value, async_receive_offer_cache)), + (22, ffor_recovery, option), }); + let ffor_recovery = ffor_recovery.unwrap_or_else(FFORRecoveryRegistry::new); + ffor_recovery.validate_identity(our_network_pubkey, chain_hash)?; + for setup in &ffor_channel_setups { + if !ffor_recovery.contains_exact(setup) { + return Err(DecodeError::InvalidValue); + } + } let mut decode_update_add_htlcs = decode_update_add_htlcs.unwrap_or_else(|| new_hash_map()); for peer in per_peer_state.values() { - for channel in peer.lock().unwrap().channel_by_id.values().filter_map(Channel::as_funded) { - let deferred_adds = decode_update_add_htlcs.get(&channel.context.outbound_scid_alias()) - .map(Vec::as_slice).unwrap_or(&[]); + for channel in + peer.lock().unwrap().channel_by_id.values().filter_map(Channel::as_funded) + { + let deferred_adds = decode_update_add_htlcs + .get(&channel.context.outbound_scid_alias()) + .map(Vec::as_slice) + .unwrap_or(&[]); channel.ffor_validate_unwind_material(deferred_adds)?; } } @@ -18104,10 +18243,6 @@ where let mut secp_ctx = Secp256k1::new(); secp_ctx.seeded_randomize(&args.entropy_source.get_secure_random_bytes()); - let our_network_pubkey = match args.node_signer.get_node_id(Recipient::Node) { - Ok(key) => key, - Err(()) => return Err(DecodeError::InvalidValue), - }; if let Some(network_pubkey) = received_network_pubkey { if network_pubkey != our_network_pubkey { log_error!(args.logger, "Key that was generated does not match the existing key."); @@ -18313,6 +18448,8 @@ where event_persist_notifier: Notifier::new(), needs_persist_flag: AtomicBool::new(false), + ffor_persistence: Mutex::new(FFORPersistenceBarrier::new()), + ffor_recovery: Mutex::new(ffor_recovery), funding_batch_states: Mutex::new(BTreeMap::new()), @@ -18613,6 +18750,9 @@ where } } +#[cfg(test)] +mod ffor_recovery_tests; + #[cfg(test)] mod tests { use crate::events::{ClosureReason, Event, HTLCHandlingFailureType}; diff --git a/lightning/src/ln/channelmanager/ffor_recovery_tests.rs b/lightning/src/ln/channelmanager/ffor_recovery_tests.rs new file mode 100644 index 00000000000..e9a8de02c41 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_recovery_tests.rs @@ -0,0 +1,274 @@ +use super::*; +use crate::chain::channelmonitor::ChannelMonitor; +use crate::ln::channel::{ffor_setup_test_messages, FFORReceiverSetup}; +use crate::ln::ffor_recovery::tests::full_registry; +use crate::ln::ffor_tests::anchor_config; +use crate::ln::functional_test_utils::*; +use crate::util::test_channel_signer::TestChannelSigner; + +fn setup_messages(sender: &Node, receiver: &Node, channel_id: ChannelId) -> (Vec, Vec) { + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([91; 32]), + amount_msat: 2_000_000, + cltv_expiry: 300, + }; + let (init, accept) = ffor_setup_test_messages(sender, receiver, channel_id, voucher); + (init.encode().unwrap(), accept.encode().unwrap()) +} + +fn prepare_record( + sender: &Node, receiver: &Node, channel_id: ChannelId, margin: u32, +) -> FFORReceiverSetup { + let (init, accept) = setup_messages(sender, receiver, channel_id); + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get(&channel_id).unwrap().as_funded().unwrap(); + channel + .prepare_ffor_receiver_setup( + &init, + &accept, + receiver.node.get_our_node_id(), + receiver.node.chain_hash, + receiver.node.current_best_block().height, + margin, + ) + .unwrap() +} + +fn register(sender: &Node, receiver: &Node, channel_id: ChannelId) -> FFORReceiverSetup { + let (init, accept) = setup_messages(sender, receiver, channel_id); + receiver + .node + .register_ffor_receiver_setup( + &channel_id, + &sender.node.get_our_node_id(), + &init, + &accept, + 20, + ) + .unwrap(); + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + peer.channel_by_id + .get(&channel_id) + .unwrap() + .as_funded() + .unwrap() + .ffor_receiver_setup_record() + .unwrap() + .unwrap() +} + +fn restore<'a, 'b, 'c>( + node: &Node<'a, 'b, 'c>, manager: &[u8], monitor: &[u8], +) -> Result, DecodeError> { + let (_, monitor) = <(BlockHash, ChannelMonitor)>::read( + &mut &monitor[..], + (node.keys_manager, node.keys_manager), + )?; + let (_, restored) = <(BlockHash, TestChannelManager<'b, 'c>)>::read( + &mut &manager[..], + ChannelManagerReadArgs::new( + node.keys_manager, + node.keys_manager, + node.keys_manager, + node.fee_estimator, + node.chain_monitor, + node.tx_broadcaster, + node.router, + node.message_router, + node.logger, + anchor_config(), + vec![&monitor], + ), + )?; + Ok(restored) +} + +fn drain_close_events(node: &Node) { + node.node.get_and_clear_pending_msg_events(); + let events = node.node.get_and_clear_pending_events(); + assert!(events.iter().any(|event| matches!(event, Event::ChannelClosed { .. }))); + node.chain_monitor.added_monitors.lock().unwrap().clear(); +} + +#[test] +fn ffor_recovery_explicit_and_monitor_closure_retain_setup_after_reload() { + for monitor_closes in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let record = register(&nodes[0], &nodes[1], channel_id); + let evidence = nodes[1].node.ffor_recovery.lock().unwrap().encode(); + if monitor_closes { + get_monitor!(nodes[1], channel_id).broadcast_latest_holder_commitment_txn( + &nodes[1].tx_broadcaster, + &nodes[1].fee_estimator, + &nodes[1].logger, + ); + // The normal message poll processes monitor events under manager consistency. + nodes[1].node.get_and_clear_pending_msg_events(); + } else { + nodes[1] + .node + .force_close_broadcasting_latest_txn( + &channel_id, + &nodes[0].node.get_our_node_id(), + "recovery retention test".into(), + ) + .unwrap(); + } + assert!(nodes[1].node.list_channels().is_empty()); + drain_close_events(&nodes[1]); + let manager = nodes[1].node.encode(); + let monitor = get_monitor!(nodes[1], channel_id).encode(); + let restored = restore(&nodes[1], &manager, &monitor).unwrap(); + assert!(restored.list_channels().is_empty()); + assert_eq!(restored.ffor_recovery.lock().unwrap().encode(), evidence); + assert!(restored.ffor_recovery.lock().unwrap().contains_exact(&record)); + } +} + +#[test] +fn ffor_recovery_stale_manager_disposal_retains_the_removed_channel_evidence() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let record = register(&nodes[0], &nodes[1], channel_id); + let stale_manager = nodes[1].node.encode(); + let evidence = nodes[1].node.ffor_recovery.lock().unwrap().encode(); + nodes[1] + .node + .force_close_broadcasting_latest_txn( + &channel_id, + &nodes[0].node.get_our_node_id(), + "newer monitor test".into(), + ) + .unwrap(); + drain_close_events(&nodes[1]); + let monitor = get_monitor!(nodes[1], channel_id).encode(); + let restored = restore(&nodes[1], &stale_manager, &monitor).unwrap(); + assert!(restored.list_channels().is_empty()); + assert_eq!(restored.ffor_recovery.lock().unwrap().encode(), evidence); + assert!(restored.ffor_recovery.lock().unwrap().contains_exact(&record)); + assert!(restored.get_and_clear_pending_events().iter().any(|event| matches!( + event, + Event::ChannelClosed { reason: ClosureReason::OutdatedChannelManager, .. } + ))); + nodes[1].node.get_and_clear_pending_msg_events(); + nodes[1].node.get_and_clear_pending_events(); + nodes[1].chain_monitor.added_monitors.lock().unwrap().clear(); +} + +#[test] +fn ffor_recovery_live_channel_refuses_missing_or_conflicting_archived_setup() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let different_admission = prepare_record(&nodes[0], &nodes[1], channel_id, 19); + register(&nodes[0], &nodes[1], channel_id); + let monitor = get_monitor!(nodes[1], channel_id).encode(); + for conflicting in [false, true] { + let mut invalid = FFORRecoveryRegistry::new(); + if conflicting { + invalid.prepare_insert(&different_admission).unwrap().commit(); + } + let original = + core::mem::replace(&mut *nodes[1].node.ffor_recovery.lock().unwrap(), invalid); + let encoded = nodes[1].node.encode(); + *nodes[1].node.ffor_recovery.lock().unwrap() = original; + assert!(matches!(restore(&nodes[1], &encoded, &monitor), Err(DecodeError::InvalidValue))); + } +} + +#[test] +fn ffor_recovery_capacity_failure_does_not_register_the_live_channel() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (init, accept) = setup_messages(&nodes[0], &nodes[1], channel_id); + let original = + core::mem::replace(&mut *nodes[1].node.ffor_recovery.lock().unwrap(), full_registry()); + let before = nodes[1].node.ffor_recovery.lock().unwrap().encode(); + let result = nodes[1].node.register_ffor_receiver_setup( + &channel_id, + &nodes[0].node.get_our_node_id(), + &init, + &accept, + 20, + ); + assert!(matches!(result, Err(FFORReceiverError::RecoveryUnavailable))); + assert_eq!(nodes[1].node.ffor_recovery.lock().unwrap().encode(), before); + *nodes[1].node.ffor_recovery.lock().unwrap() = original; + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let peer = peers.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get(&channel_id).unwrap().as_funded().unwrap(); + assert!(channel.ffor_receiver_setup_record().unwrap().is_none()); + drop(peer); + drop(peers); + // A valid request remains possible after storage becomes available. + register(&nodes[0], &nodes[1], channel_id); +} + +#[test] +fn ffor_recovery_archive_only_manager_preserves_required_tlv_and_exports_legacy_fixture() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + register(&nodes[0], &nodes[1], channel_id); + nodes[1] + .node + .force_close_broadcasting_latest_txn( + &channel_id, + &nodes[0].node.get_our_node_id(), + "archive compatibility fixture".into(), + ) + .unwrap(); + drain_close_events(&nodes[1]); + let archive = nodes[1].node.encode(); + let monitor = get_monitor!(nodes[1], channel_id).encode(); + let evidence = core::mem::replace( + &mut *nodes[1].node.ffor_recovery.lock().unwrap(), + FFORRecoveryRegistry::new(), + ); + let ordinary = nodes[1].node.encode(); + *nodes[1].node.ffor_recovery.lock().unwrap() = evidence; + assert!(archive.len() > ordinary.len()); + assert!(restore(&nodes[1], &ordinary, &monitor) + .unwrap() + .ffor_recovery + .lock() + .unwrap() + .is_empty()); + assert!(!restore(&nodes[1], &archive, &monitor) + .unwrap() + .ffor_recovery + .lock() + .unwrap() + .is_empty()); + if let Ok(directory) = std::env::var("FFOR_RECOVERY_FIXTURE_DIR") { + let path = std::path::Path::new(&directory); + std::fs::create_dir_all(path).unwrap(); + std::fs::write(path.join("archive-only-manager.bin"), &archive).unwrap(); + std::fs::write(path.join("ordinary-manager.bin"), &ordinary).unwrap(); + std::fs::write(path.join("closed-monitor.bin"), &monitor).unwrap(); + std::fs::write(path.join("test-node-seed.bin"), &nodes[1].node_seed).unwrap(); + } +} diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index a756ee95777..5828862a8ac 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -7,6 +7,7 @@ use alloc::collections::{BTreeMap, BTreeSet}; use bitcoin::hashes::Hash; +use bitcoin::locktime::absolute::LOCK_TIME_THRESHOLD; use bitcoin::secp256k1::{Message, Secp256k1}; use bitcoin::sighash::{EcdsaSighashType, SighashCache}; use bitcoin::Txid; @@ -42,7 +43,8 @@ pub struct FFORVoucher { pub payment_hash: PaymentHash, /// The exact value in millisatoshis, before on-chain rounding. pub amount_msat: u64, - /// The absolute CLTV expiry height shared by every voucher in the book. + /// The absolute CLTV expiry height shared by every voucher, strictly below Bitcoin's + /// locktime threshold. Timestamp locktimes are not valid voucher expiries. pub cltv_expiry: u32, } @@ -64,6 +66,8 @@ pub enum FFORReceiverAbortReason { Disconnected, /// The channel was restored from storage before a durable activation mechanism existed. Restarted, + /// The first voucher hash reused a revealed counterparty commitment secret. + CommitmentSecretReused, } impl_writeable_tlv_based_enum!(FFORReceiverAbortReason, @@ -71,6 +75,7 @@ impl_writeable_tlv_based_enum!(FFORReceiverAbortReason, (2, VoucherMismatch) => {}, (4, Disconnected) => {}, (6, Restarted) => {}, + (8, CommitmentSecretReused) => {}, ); /// Receiver-side state of one experimental voucher registration. @@ -114,6 +119,10 @@ pub enum FFORReceiverError { NotRegistered, /// The supplied epoch does not identify this channel's registration. UnknownEpoch, + /// No unused persistence revision remains in this manager instance. + PersistenceUnavailable, + /// Retained recovery storage is full or conflicts with an existing setup. + RecoveryUnavailable, } impl From for FFORReceiverError { @@ -129,6 +138,8 @@ impl fmt::Display for FFORReceiverError { Self::AlreadyRegistered => f.write_str("FFOR receiver already registered on channel"), Self::NotRegistered => f.write_str("no FFOR receiver registration on channel"), Self::UnknownEpoch => f.write_str("unknown FFOR receiver epoch"), + Self::PersistenceUnavailable => f.write_str("FFOR persistence revision exhausted"), + Self::RecoveryUnavailable => f.write_str("FFOR recovery record cannot be retained"), } } } @@ -280,6 +291,7 @@ pub(crate) fn validate_vouchers(vouchers: &[FFORVoucher]) -> Result<(), FFORComm if first.htlc_id.checked_add(index as u64) != Some(voucher.htlc_id) || voucher.amount_msat == 0 || voucher.cltv_expiry == 0 + || voucher.cltv_expiry >= LOCK_TIME_THRESHOLD || voucher.cltv_expiry != first.cltv_expiry || !hashes.insert(voucher.payment_hash.0) { diff --git a/lightning/src/ln/ffor_persistence.rs b/lightning/src/ln/ffor_persistence.rs new file mode 100644 index 00000000000..f06b12a1a77 --- /dev/null +++ b/lightning/src/ln/ffor_persistence.rs @@ -0,0 +1,144 @@ +//! In-memory completion tracking for FFOR state written with the channel manager. +//! +//! A token must be captured before serialization and completed only after the same ordered +//! persister has durably stored that serialization. It is deliberately unusable after restart. + +use crate::sync::Arc; + +/// An opaque receipt identifying FFOR state that a forthcoming manager snapshot must include. +/// +/// Capture this before encoding the channel manager, then pass it back only after the write +/// succeeds. Capturing or dropping it proves nothing about storage. Tokens cannot be transferred +/// between manager instances, including instances restored from the same serialized bytes. +#[must_use] +pub struct FFORPersistenceToken { + identity: Arc<()>, + revision: u64, +} + +/// An opaque requirement returned by a protected FFOR channel mutation. +/// +/// The requirement can be queried after the manager's persistence callback completes. It is +/// specific to one manager instance and does not assert that a channel remains in the same state. +#[derive(Clone, Debug)] +pub struct FFORPersistenceRequirement { + identity: Arc<()>, + revision: u64, +} + +impl PartialEq for FFORPersistenceRequirement { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.identity, &other.identity) && self.revision == other.revision + } +} + +impl Eq for FFORPersistenceRequirement {} + +pub(super) struct FFORPersistenceBarrier { + identity: Arc<()>, + requested: u64, + completed: u64, +} + +impl FFORPersistenceBarrier { + pub(super) fn new() -> Self { + Self { identity: Arc::new(()), requested: 0, completed: 0 } + } + + /// Reserve a new revision before changing protected state, while holding manager consistency. + /// Exhaustion is an error before the state transition, never a wrap to an already durable ID. + pub(super) fn request(&mut self) -> Result { + self.requested = self.requested.checked_add(1).ok_or(())?; + Ok(FFORPersistenceRequirement { + identity: Arc::clone(&self.identity), + revision: self.requested, + }) + } + + pub(super) fn capture(&self) -> FFORPersistenceToken { + FFORPersistenceToken { identity: Arc::clone(&self.identity), revision: self.requested } + } + + pub(super) fn complete(&mut self, token: FFORPersistenceToken) -> Result { + if !Arc::ptr_eq(&self.identity, &token.identity) { + return Err(()); + } + // Older completions are idempotent. Storage writes themselves must remain ordered. + let advanced = token.revision > self.completed; + self.completed = self.completed.max(token.revision); + Ok(advanced) + } + + pub(super) fn is_complete(&self, requirement: &FFORPersistenceRequirement) -> bool { + Arc::ptr_eq(&self.identity, &requirement.identity) + && requirement.revision != 0 + && requirement.revision <= self.completed + } + + pub(super) fn needs_persistence(&self) -> bool { + self.requested > self.completed + } +} + +#[cfg(test)] +mod tests { + use super::FFORPersistenceBarrier; + + #[test] + fn ffor_persistence_does_not_release_later_mutations() { + let mut barrier = FFORPersistenceBarrier::new(); + assert!(!barrier.needs_persistence()); + let first = barrier.request().unwrap(); + let token = barrier.capture(); + let later = barrier.request().unwrap(); + barrier.complete(token).unwrap(); + assert!(barrier.is_complete(&first)); + assert!(!barrier.is_complete(&later)); + assert!(barrier.needs_persistence()); + let token = barrier.capture(); + barrier.complete(token).unwrap(); + assert!(barrier.is_complete(&later)); + assert!(!barrier.needs_persistence()); + } + + #[test] + fn ffor_persistence_failure_and_cancellation_leave_work_pending() { + let mut barrier = FFORPersistenceBarrier::new(); + let revision = barrier.request().unwrap(); + drop(barrier.capture()); + assert!(barrier.needs_persistence()); + assert!(!barrier.is_complete(&revision)); + let token = barrier.capture(); + barrier.complete(token).unwrap(); + assert!(barrier.is_complete(&revision)); + } + + #[test] + fn ffor_persistence_tokens_are_instance_bound_and_cannot_regress() { + let mut first = FFORPersistenceBarrier::new(); + let mut restored = FFORPersistenceBarrier::new(); + let original = first.request().unwrap(); + let revision = restored.request().unwrap(); + assert!(restored.complete(first.capture()).is_err()); + assert!(!restored.is_complete(&revision)); + assert!(!restored.is_complete(&original)); + let older = restored.capture(); + let newer = restored.request().unwrap(); + let token = restored.capture(); + restored.complete(token).unwrap(); + restored.complete(older).unwrap(); + assert!(restored.is_complete(&newer)); + assert!(!restored.needs_persistence()); + } + + #[test] + fn ffor_persistence_revision_exhaustion_fails_before_wraparound() { + let mut barrier = FFORPersistenceBarrier::new(); + barrier.requested = u64::MAX - 1; + barrier.completed = u64::MAX - 1; + let last = barrier.request().unwrap(); + assert!(barrier.request().is_err()); + assert!(barrier.needs_persistence()); + assert!(!barrier.is_complete(&last)); + } +} diff --git a/lightning/src/ln/ffor_recovery.rs b/lightning/src/ln/ffor_recovery.rs new file mode 100644 index 00000000000..7edaec65988 --- /dev/null +++ b/lightning/src/ln/ffor_recovery.rs @@ -0,0 +1,227 @@ +//! Bounded retained setup evidence, independent of the lifetime of a live channel. +//! +//! This registry proves neither activation nor readiness. The manager must retain it in required +//! TLV 22, under the same consistency lock as channel admission and persistence. Admission uses +//! a borrowed insertion permit so capacity failure cannot leave a channel without its evidence. + +use alloc::vec::Vec; +use bitcoin::constants::ChainHash; +use bitcoin::secp256k1::PublicKey; + +use crate::io; +use crate::ln::channel::FFORReceiverSetup; +use crate::ln::msgs::DecodeError; +use crate::ln::types::ChannelId; +use crate::util::ser::{FixedLengthReader, Readable, Writeable, Writer}; + +// A later format containing activation evidence must use a different required version. A +// setup-only reader must never silently discard evidence for a channel that may be active. +const SETUP_ONLY_VERSION: u8 = 0; +const MAX_RECORDS: usize = 64; +const MAX_ENCODED_BYTES: usize = 8 * 1024 * 1024; +// Two maximum wire messages, a 483-slot canonical book, and fixed admission fields fit here. +const MAX_RECORD_BYTES: usize = 192 * 1024; +const HEADER_BYTES: usize = 3; +const RECORD_LENGTH_BYTES: usize = 4; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct FFORRecoveryKey { + pub(crate) channel_id: ChannelId, + pub(crate) epoch_id: [u8; 32], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum FFORRecoveryError { + InvalidRecord, + ConflictingRecord, + CapacityExceeded, +} + +struct StoredSetup { + setup: FFORReceiverSetup, + canonical_book: Vec, +} + +impl_writeable_tlv_based!(StoredSetup, { + (0, setup, required), + (2, canonical_book, required_vec), +}); + +struct Entry { + key: FFORRecoveryKey, + record: StoredSetup, + encoded_bytes: usize, +} + +impl Entry { + fn new(record: StoredSetup) -> Result { + let authenticated = + record.setup.validate_recovery().map_err(|_| FFORRecoveryError::InvalidRecord)?; + if record.canonical_book != authenticated.canonical_book() { + return Err(FFORRecoveryError::InvalidRecord); + } + let header = authenticated.header(); + let encoded_bytes = record.serialized_length(); + if encoded_bytes > MAX_RECORD_BYTES { + return Err(FFORRecoveryError::CapacityExceeded); + } + Ok(Self { + key: FFORRecoveryKey { + channel_id: ChannelId(header.channel_id), + epoch_id: header.epoch_id, + }, + record, + encoded_bytes, + }) + } + + fn has_same_identity(&self, other: &Self) -> bool { + self.key.channel_id == other.key.channel_id + || (self.record.setup.chain_hash() == other.record.setup.chain_hash() + && self.record.setup.funding_txo() == other.record.setup.funding_txo()) + } +} + +pub(crate) struct FFORRecoveryRegistry { + entries: Vec, + encoded_bytes: usize, +} + +/// Exclusive capacity reservation. Dropping it changes nothing; commit cannot fail. +pub(crate) struct FFORRecoveryInsertion<'a> { + registry: &'a mut FFORRecoveryRegistry, + entry: Option, +} + +impl FFORRecoveryInsertion<'_> { + pub(crate) fn commit(self) { + if let Some(entry) = self.entry { + self.registry.encoded_bytes += RECORD_LENGTH_BYTES + entry.encoded_bytes; + self.registry.entries.push(entry); + } + } +} + +impl FFORRecoveryRegistry { + pub(crate) fn new() -> Self { + Self { entries: Vec::new(), encoded_bytes: HEADER_BYTES } + } + + pub(crate) fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub(crate) fn get(&self, key: &FFORRecoveryKey) -> Option<&FFORReceiverSetup> { + self.entries.iter().find(|entry| entry.key == *key).map(|entry| &entry.record.setup) + } + + pub(crate) fn contains_exact(&self, record: &FFORReceiverSetup) -> bool { + let authenticated = match record.validate_recovery() { + Ok(authenticated) => authenticated, + Err(_) => return false, + }; + let header = authenticated.header(); + let key = + FFORRecoveryKey { channel_id: ChannelId(header.channel_id), epoch_id: header.epoch_id }; + self.get(&key).map_or(false, |stored| stored.encode() == record.encode()) + } + + /// Must be checked against the actual restoring manager, including archive-only snapshots. + pub(crate) fn validate_identity( + &self, receiver: PublicKey, chain_hash: ChainHash, + ) -> Result<(), DecodeError> { + if self.entries.iter().any(|entry| { + entry.record.setup.receiver() != receiver + || entry.record.setup.chain_hash() != chain_hash + }) { + return Err(DecodeError::InvalidValue); + } + Ok(()) + } + + /// Reauthenticate and reserve storage before changing the channel. Retain this permit while + /// installing the identical prepared setup under the peer lock, then commit it on success. + /// Exact retries are idempotent; the current format permits only one setup per channel/funding. + pub(crate) fn prepare_insert( + &mut self, setup: &FFORReceiverSetup, + ) -> Result, FFORRecoveryError> { + let authenticated = + setup.validate_recovery().map_err(|_| FFORRecoveryError::InvalidRecord)?; + let entry = Entry::new(StoredSetup { + setup: setup.clone(), + canonical_book: authenticated.canonical_book().to_vec(), + })?; + if let Some(existing) = + self.entries.iter().find(|existing| existing.has_same_identity(&entry)) + { + if existing.key != entry.key || existing.record.encode() != entry.record.encode() { + return Err(FFORRecoveryError::ConflictingRecord); + } + return Ok(FFORRecoveryInsertion { registry: self, entry: None }); + } + self.check_capacity(entry.encoded_bytes)?; + Ok(FFORRecoveryInsertion { registry: self, entry: Some(entry) }) + } + + fn check_capacity(&self, record_bytes: usize) -> Result<(), FFORRecoveryError> { + let total = self + .encoded_bytes + .checked_add(RECORD_LENGTH_BYTES) + .and_then(|total| total.checked_add(record_bytes)); + if self.entries.len() >= MAX_RECORDS + || record_bytes > MAX_RECORD_BYTES + || total.map_or(true, |total| total > MAX_ENCODED_BYTES) + { + return Err(FFORRecoveryError::CapacityExceeded); + } + Ok(()) + } +} + +impl Writeable for FFORRecoveryRegistry { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + SETUP_ONLY_VERSION.write(writer)?; + (self.entries.len() as u16).write(writer)?; + for entry in &self.entries { + (entry.encoded_bytes as u32).write(writer)?; + entry.record.write(writer)?; + } + Ok(()) + } +} + +impl Readable for FFORRecoveryRegistry { + fn read(reader: &mut R) -> Result { + if u8::read(reader)? != SETUP_ONLY_VERSION { + return Err(DecodeError::UnknownRequiredFeature); + } + let count = u16::read(reader)? as usize; + if count > MAX_RECORDS { + return Err(DecodeError::InvalidValue); + } + let mut registry = Self::new(); + for _ in 0..count { + let length = u32::read(reader)? as usize; + registry.check_capacity(length).map_err(|_| DecodeError::InvalidValue)?; + let mut bounded = FixedLengthReader::new(reader, length as u64); + let record = StoredSetup::read(&mut bounded)?; + if bounded.bytes_remain() { + return Err(DecodeError::InvalidValue); + } + let entry = Entry::new(record).map_err(|_| DecodeError::InvalidValue)?; + // Reject ignored storage extensions and alternate framing as well as duplicate keys. + // Exact signed optional wire extensions remain inside the retained message bytes. + if entry.encoded_bytes != length + || registry.entries.iter().any(|existing| existing.has_same_identity(&entry)) + { + return Err(DecodeError::InvalidValue); + } + registry.encoded_bytes += RECORD_LENGTH_BYTES + length; + registry.entries.push(entry); + } + Ok(registry) + } +} + +#[cfg(test)] +pub(crate) mod tests; diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs new file mode 100644 index 00000000000..64db91a97fa --- /dev/null +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -0,0 +1,316 @@ +use super::*; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::{Message as SecpMessage, Secp256k1, SecretKey}; +use bitcoin::{Network, Txid}; +use lightning_ffor::transcript; +use lightning_ffor::wire::{Accept, Header, Init, Message, Payload, Tlv, MAX_MESSAGE_LEN}; + +use crate::chain::transaction::OutPoint; + +struct Fixture { + init_wire: Vec, + accept_wire: Vec, + receiver: PublicKey, + settlement: PublicKey, + chain_hash: ChainHash, + funding_txo: OutPoint, + channel_value_sat: u64, + receiver_balance_msat: u64, + feerate_sat_per_kw: u32, + settlement_is_funder: bool, + admission_height: u32, + claim_margin_blocks: u32, +} + +impl_writeable_tlv_based!(Fixture, { + (0, init_wire, required_vec), + (2, accept_wire, required_vec), + (4, receiver, required), + (6, settlement, required), + (8, chain_hash, required), + (10, funding_txo, required), + (12, channel_value_sat, required), + (14, receiver_balance_msat, required), + (16, feerate_sat_per_kw, required), + (18, settlement_is_funder, required), + (20, admission_height, required), + (22, claim_margin_blocks, required), +}); + +impl Fixture { + fn record(&self) -> FFORReceiverSetup { + FFORReceiverSetup::read(&mut &self.encode()[..]).unwrap() + } +} + +fn key(value: u8) -> PublicKey { + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[value; 32]).unwrap()) +} + +fn sign(message: &mut Message, value: u8) { + message.signature = Secp256k1::new() + .sign_ecdsa( + &SecpMessage::from_digest(message.signature_digest().unwrap()), + &SecretKey::from_slice(&[value; 32]).unwrap(), + ) + .serialize_compact(); +} + +fn message(header: Header, payload: Payload, signer: u8, maximum_size: bool) -> Message { + let mut message = Message { header, payload, extensions: Vec::new(), signature: [0; 64] }; + sign(&mut message, signer); + if maximum_size { + let padding = MAX_MESSAGE_LEN - message.encode().unwrap().len() - 4; + message.extensions.push(Tlv { kind: 101, value: vec![11; padding] }); + sign(&mut message, signer); + assert_eq!(message.encode().unwrap().len(), MAX_MESSAGE_LEN); + } + message +} + +fn fixture(identity: u8, maximum_size: bool) -> Fixture { + let header = Header { channel_id: [identity; 32], epoch_id: [identity.wrapping_add(1); 32] }; + let count = if maximum_size { 483 } else { 2 }; + let init = message( + header, + Payload::Init(Init { + budget_msat: 1_000_000 * count as u64, + min_payment_msat: 1_000_000, + settlement_deadline: 1000, + voucher_expiry: 2000, + fee_base_msat: 10, + fee_proportional_millionths: 10, + amounts_msat: vec![1_000_000; count], + witness_peers: None, + hash_chain: false, + }), + 41, + maximum_size, + ); + let hashes = (0..count) + .map(|index| { + let mut hash = [identity; 32]; + hash[..2].copy_from_slice(&(index as u16).to_be_bytes()); + hash + }) + .collect(); + let accept = message( + header, + Payload::Accept(Accept { + s_commitment_number: 3, + payment_hashes: hashes, + s_htlc_id_base: 8, + amounts_msat: vec![1_000_000; count], + init_hash: transcript::init_hash(&init.encode().unwrap()), + }), + 42, + maximum_size, + ); + Fixture { + init_wire: init.encode().unwrap(), + accept_wire: accept.encode().unwrap(), + receiver: key(41), + settlement: key(42), + chain_hash: ChainHash::using_genesis_block(Network::Regtest), + funding_txo: OutPoint { txid: Txid::from_byte_array([identity; 32]), index: 0 }, + channel_value_sat: 1_000_000, + receiver_balance_msat: 5_000_000, + feerate_sat_per_kw: 253, + settlement_is_funder: true, + admission_height: 500, + claim_margin_blocks: 12, + } +} + +fn stored(record: FFORReceiverSetup) -> StoredSetup { + let book = record.validate_recovery().unwrap().canonical_book().to_vec(); + StoredSetup { setup: record, canonical_book: book } +} + +fn frame(records: &[Vec]) -> Vec { + let mut bytes = Vec::new(); + SETUP_ONLY_VERSION.write(&mut bytes).unwrap(); + (records.len() as u16).write(&mut bytes).unwrap(); + for record in records { + (record.len() as u32).write(&mut bytes).unwrap(); + bytes.extend_from_slice(record); + } + bytes +} + +fn decode(bytes: &[u8]) -> Result { + FFORRecoveryRegistry::read(&mut &bytes[..]) +} + +pub(crate) fn full_registry() -> FFORRecoveryRegistry { + let mut registry = FFORRecoveryRegistry::new(); + for identity in 1..=MAX_RECORDS { + registry.prepare_insert(&fixture(identity as u8, false).record()).unwrap().commit(); + } + registry +} + +fn without_book(setup: FFORReceiverSetup) -> Result, io::Error> { + let mut bytes = Vec::new(); + write_tlv_fields!(&mut bytes, { (0, setup, required) }); + Ok(bytes) +} + +#[test] +fn ffor_recovery_roundtrip_retains_exact_signed_setup_and_native_identity() { + let record = fixture(1, true).record(); + let mut registry = FFORRecoveryRegistry::new(); + registry.prepare_insert(&record).unwrap().commit(); + let encoded = registry.encode(); + let restored = decode(&encoded).unwrap(); + assert_eq!(restored.encode(), encoded); + assert!(restored.contains_exact(&record)); + assert_eq!(restored.encoded_bytes, encoded.len()); + assert!(restored.validate_identity(record.receiver(), record.chain_hash()).is_ok()); + assert_eq!( + restored.validate_identity(key(43), record.chain_hash()), + Err(DecodeError::InvalidValue) + ); + assert_eq!( + restored + .validate_identity(record.receiver(), ChainHash::using_genesis_block(Network::Bitcoin)), + Err(DecodeError::InvalidValue) + ); +} + +#[test] +fn ffor_recovery_cancelled_admission_and_duplicate_retry_do_not_mutate_registry() { + let record = fixture(2, false).record(); + let mut registry = FFORRecoveryRegistry::new(); + drop(registry.prepare_insert(&record).unwrap()); + assert!(registry.is_empty()); + registry.prepare_insert(&record).unwrap().commit(); + let encoded = registry.encode(); + registry.prepare_insert(&record).unwrap().commit(); + assert_eq!(registry.encode(), encoded); +} + +#[test] +fn ffor_recovery_conflicting_channel_or_funding_cannot_replace_evidence() { + let original = fixture(3, false); + let mut registry = FFORRecoveryRegistry::new(); + registry.prepare_insert(&original.record()).unwrap().commit(); + let encoded = registry.encode(); + let mut changed = fixture(3, false); + changed.feerate_sat_per_kw += 1; + assert!(matches!( + registry.prepare_insert(&changed.record()), + Err(FFORRecoveryError::ConflictingRecord) + )); + assert!(!registry.contains_exact(&changed.record())); + let mut changed = fixture(4, false); + changed.funding_txo = original.funding_txo; + assert!(matches!( + registry.prepare_insert(&changed.record()), + Err(FFORRecoveryError::ConflictingRecord) + )); + assert_eq!(registry.encode(), encoded); +} + +#[test] +fn ffor_recovery_capacity_refuses_new_records_without_eviction() { + let mut registry = FFORRecoveryRegistry::new(); + let first = fixture(1, false).record(); + for identity in 1..=MAX_RECORDS { + registry.prepare_insert(&fixture(identity as u8, false).record()).unwrap().commit(); + } + let before = registry.encode(); + assert!(matches!( + registry.prepare_insert(&fixture(90, false).record()), + Err(FFORRecoveryError::CapacityExceeded) + )); + registry.prepare_insert(&first).unwrap().commit(); + assert_eq!(registry.encode(), before); + assert!(registry.contains_exact(&first)); +} + +#[test] +fn ffor_recovery_total_byte_limit_applies_before_record_count_limit() { + let mut registry = FFORRecoveryRegistry::new(); + let mut hit_limit = false; + for identity in 1..=MAX_RECORDS { + let record = fixture(identity as u8, true).record(); + let before = registry.encoded_bytes; + match registry.prepare_insert(&record) { + Ok(permit) => permit.commit(), + Err(FFORRecoveryError::CapacityExceeded) => { + hit_limit = true; + }, + Err(other) => panic!("unexpected insertion error: {:?}", other), + } + if hit_limit { + assert_eq!(registry.encoded_bytes, before); + assert!(registry.entries.len() < MAX_RECORDS); + break; + } + } + assert!(hit_limit); + assert!(registry.encoded_bytes <= MAX_ENCODED_BYTES); + assert!(decode(®istry.encode()).is_ok()); +} + +#[test] +fn ffor_recovery_reader_rejects_duplicate_and_conflicting_identity_keys() { + let record = stored(fixture(5, false).record()).encode(); + assert!(decode(&frame(&[record.clone(), record.clone()])).is_err()); + let mut changed = fixture(5, false); + changed.feerate_sat_per_kw += 1; + assert!(decode(&frame(&[record.clone(), stored(changed.record()).encode()])).is_err()); + let mut changed = fixture(6, false); + changed.funding_txo = fixture(5, false).funding_txo; + assert!(decode(&frame(&[record, stored(changed.record()).encode()])).is_err()); +} + +#[test] +fn ffor_recovery_reader_reauthenticates_signatures_and_canonical_book() { + let mut record = stored(fixture(7, false).record()); + record.canonical_book[40] ^= 1; + assert!(decode(&frame(&[record.encode()])).is_err()); + let mut broken = fixture(7, false); + let last = broken.accept_wire.len() - 1; + broken.accept_wire[last] ^= 1; + let invalid = broken.record(); + assert!(matches!( + FFORRecoveryRegistry::new().prepare_insert(&invalid), + Err(FFORRecoveryError::InvalidRecord) + )); + let mut record = stored(fixture(7, false).record()); + record.setup = invalid; + assert!(decode(&frame(&[record.encode()])).is_err()); +} + +#[test] +fn ffor_recovery_reader_refuses_future_schema_missing_fields_and_truncation() { + let record = stored(fixture(8, false).record()).encode(); + let framed = frame(&[record]); + for length in 0..framed.len() { + assert!(decode(&framed[..length]).is_err(), "accepted prefix {}", length); + } + let mut future = framed.clone(); + future[0] = 1; + assert!(matches!(decode(&future), Err(DecodeError::UnknownRequiredFeature))); + // Empty TLV records omit both mandatory fields. + assert!(decode(&frame(&[vec![0]])).is_err()); + // No canonical book, although the setup itself is valid. + let missing = without_book(fixture(8, false).record()).unwrap(); + assert!(decode(&frame(&[missing])).is_err()); +} + +#[test] +fn ffor_recovery_reader_bounds_declared_counts_and_lengths_before_reading_records() { + assert!(decode(&[SETUP_ONLY_VERSION, 0, MAX_RECORDS as u8 + 1]).is_err()); + for length in [MAX_RECORD_BYTES + 1, u32::MAX as usize] { + let mut bytes = vec![SETUP_ONLY_VERSION, 0, 1]; + bytes.extend_from_slice(&(length as u32).to_be_bytes()); + assert!(matches!(decode(&bytes), Err(DecodeError::InvalidValue))); + } + let mut registry = FFORRecoveryRegistry::new(); + registry.encoded_bytes = MAX_ENCODED_BYTES; + assert_eq!(registry.check_capacity(0), Err(FFORRecoveryError::CapacityExceeded)); + assert_eq!(registry.check_capacity(usize::MAX), Err(FFORRecoveryError::CapacityExceeded)); +} diff --git a/lightning/src/ln/ffor_tests.rs b/lightning/src/ln/ffor_tests.rs index a437e742897..78281717f94 100644 --- a/lightning/src/ln/ffor_tests.rs +++ b/lightning/src/ln/ffor_tests.rs @@ -7,6 +7,7 @@ use crate::ln::types::ChannelId; use crate::types::payment::{PaymentHash, PaymentSecret}; use crate::util::config::UserConfig; use crate::util::ser::Writeable; +use core::sync::atomic::Ordering; pub(super) fn anchor_config() -> UserConfig { let mut config = test_default_channel_config(); @@ -978,3 +979,197 @@ fn ffor_parking_multiple_rounds_and_extra_add_abort_the_entire_book() { assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); send_payment(&nodes[0], &[&nodes[1]], 1_000_000); } + +#[test] +fn ffor_parking_retries_unavailable_node_signer_before_and_after_restart() { + // Recovery while registered, after explicit abort, and after a persisted restart abort. + for recovery in 0..3 { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let reloaded; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let sender_id = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_book(&nodes[1], &nodes[0], channel_id, &[voucher]); + nodes[1].node.handle_update_add_htlc(sender_id, &update.update_add_htlcs[0]); + commitment_signed_dance!(&nodes[1], &nodes[0], update.commitment_signed, false); + nodes[1].keys_manager.unavailable_node_ecdh.store(true, Ordering::Release); + + for _ in 0..3 { + assert!(nodes[1].node.needs_pending_htlc_processing()); + nodes[1].node.process_pending_htlc_forwards(); + assert!(nodes[1].node.needs_pending_htlc_processing()); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Registered { parked_vouchers: 0, total_vouchers: 1 } + ); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + } + + if recovery == 1 { + nodes[1] + .node + .abort_ffor_receiver_book(&channel_id, &sender_id, RECEIVER_EPOCH) + .unwrap(); + } else if recovery == 2 { + let monitor_encoded = get_monitor!(nodes[1], channel_id).encode(); + let manager_encoded = nodes[1].node.encode(); + nodes[0].node.peer_disconnected(receiver_id); + reload_node!( + nodes[1], + config, + &manager_encoded, + &[&monitor_encoded], + persister, + chain_monitor, + reloaded + ); + } + + if recovery != 0 { + nodes[1].node.process_pending_htlc_forwards(); + assert!(nodes[1].node.needs_pending_htlc_processing()); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborting { + reason: if recovery == 1 { + FFORReceiverAbortReason::Requested + } else { + FFORReceiverAbortReason::Restarted + } + } + ); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + } + + nodes[1].keys_manager.unavailable_node_ecdh.store(false, Ordering::Release); + if recovery == 2 { + pump_ffor_reconnection(&nodes[0], &nodes[1]); + expect_payment_failed!(&nodes[0], voucher.payment_hash, false); + } else { + nodes[1].node.process_pending_htlc_forwards(); + if recovery == 0 { + assert!(matches!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Parked { .. } + )); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + nodes[1] + .node + .abort_ffor_receiver_book(&channel_id, &sender_id, RECEIVER_EPOCH) + .unwrap(); + } + drain_voucher_failures(&nodes[0], &nodes[1], &[voucher.payment_hash]); + } + assert!(!nodes[1].node.needs_pending_htlc_processing()); + assert!(matches!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborted { .. } + )); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + } +} + +#[test] +fn ffor_registration_waits_for_its_own_durable_snapshot() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs( + 3, + &node_cfgs, + &[Some(config.clone()), Some(config.clone()), Some(config)], + ); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let first_channel = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let second_channel = create_announced_chan_between_nodes(&nodes, 2, 1).2; + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([3; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let receiver = nodes[1].node; + let first = receiver + .register_ffor_receiver_book( + &first_channel, + &nodes[0].node.get_our_node_id(), + [4; 32], + &[voucher], + ) + .unwrap(); + assert!(!receiver.is_ffor_state_persisted(&first)); + let earlier_snapshot = receiver.capture_ffor_persistence(); + let second = receiver + .register_ffor_receiver_book( + &second_channel, + &nodes[2].node.get_our_node_id(), + [5; 32], + &[voucher], + ) + .unwrap(); + // This later serialization contains both registrations, but the earlier captured token + // deliberately acknowledges only the first. A failed/cancelled write acknowledges neither. + let stored = receiver.encode(); + assert!(!stored.is_empty()); + assert!(!receiver.is_ffor_state_persisted(&first)); + assert!(!receiver.is_ffor_state_persisted(&second)); + for _ in 0..3 { + assert!(receiver.get_and_clear_needs_persistence()); + } + receiver.ffor_persistence_completed(earlier_snapshot).unwrap(); + assert!(receiver.is_ffor_state_persisted(&first)); + assert!(!receiver.is_ffor_state_persisted(&second)); + assert!(receiver.get_and_clear_needs_persistence()); + let latest = receiver.capture_ffor_persistence(); + let _stored = receiver.encode(); + receiver.ffor_persistence_completed(latest).unwrap(); + assert!(receiver.is_ffor_state_persisted(&second)); + assert!(!receiver.get_and_clear_needs_persistence()); + assert!(!nodes[0].node.is_ffor_state_persisted(&first)); + assert!(receiver.ffor_persistence_completed(nodes[0].node.capture_ffor_persistence()).is_err()); +} + +#[test] +fn ffor_persistence_tokens_do_not_authorize_a_restored_manager() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let reloaded; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([7; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let requirement = nodes[1] + .node + .register_ffor_receiver_book( + &channel_id, + &nodes[0].node.get_our_node_id(), + [8; 32], + &[voucher], + ) + .unwrap(); + let old_token = nodes[1].node.capture_ffor_persistence(); + let manager = nodes[1].node.encode(); + let monitor = get_monitor!(nodes[1], channel_id).encode(); + nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id()); + reload_node!(nodes[1], config, &manager, &[&monitor], persister, chain_monitor, reloaded); + assert!(nodes[1].node.ffor_persistence_completed(old_token).is_err()); + assert!(!nodes[1].node.is_ffor_state_persisted(&requirement)); +} diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 25f950949d4..b84cccd9814 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -19,6 +19,8 @@ pub mod channel_state; pub mod channelmanager; mod features; pub mod ffor; +mod ffor_persistence; +mod ffor_recovery; #[cfg(test)] mod ffor_tests; pub mod funding; diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index db0b0503118..5f1728b80b9 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -1801,6 +1801,7 @@ impl NodeSigner for TestNodeSigner { } pub struct TestKeysInterface { + pub unavailable_node_ecdh: AtomicBool, pub backing: DynKeysInterface, pub override_random_bytes: Mutex>, pub disable_revocation_policy_check: bool, @@ -1830,6 +1831,9 @@ impl NodeSigner for TestKeysInterface { fn ecdh( &self, recipient: Recipient, other_key: &PublicKey, tweak: Option<&Scalar>, ) -> Result { + if self.unavailable_node_ecdh.load(Ordering::Acquire) { + return Err(()); + } self.backing.ecdh(recipient, other_key, tweak) } @@ -1951,6 +1955,7 @@ impl TestKeysInterface { fn build(backing: Box>) -> Self { Self { backing: DynKeysInterface::new(backing), + unavailable_node_ecdh: AtomicBool::new(false), override_random_bytes: Mutex::new(None), disable_revocation_policy_check: false, disable_all_state_policy_checks: false, From 272535300e6dc1908b923a06441e5d7943d67548 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 21:17:52 -0400 Subject: [PATCH 05/22] feat(ffor): own receiver quiescence and revalidate voucher evidence --- FFOR.md | 20 + lightning/src/ln/channel.rs | 18 +- lightning/src/ln/channel/ffor.rs | 2 + lightning/src/ln/channel/ffor/quiescence.rs | 197 ++++++++ lightning/src/ln/channelmanager.rs | 126 +++++- lightning/src/ln/ffor.rs | 15 + lightning/src/ln/ffor_tests.rs | 2 + lightning/src/ln/ffor_tests/quiescence.rs | 478 ++++++++++++++++++++ 8 files changed, 843 insertions(+), 15 deletions(-) create mode 100644 lightning/src/ln/channel/ffor/quiescence.rs create mode 100644 lightning/src/ln/ffor_tests/quiescence.rs diff --git a/FFOR.md b/FFOR.md index 4a3657ee80c..b72264b9cb3 100644 --- a/FFOR.md +++ b/FFOR.md @@ -45,6 +45,22 @@ only derives encrypted failure material from their onion keys. `Registered` repo progress; `Parked` additionally requires the complete monitor-backed commitment proof, including both current commitment transactions and holder claim signatures. +`request_ffor_receiver_quiescence` starts an owned STFU handshake only for a +complete authenticated book and peers that support quiescence. It retains the +monitor proof and checks it again after both STFU messages, including the receiver's +initiator role and the settlement deadline. An intervening commitment or monitor +update invalidates the proof. `ffor_receiver_quiescence_status` distinguishes a +pending handshake from completed, still-valid quiescence. Neither state authorizes +an invoice. Stock quiescence ends on disconnect and is not an activation freeze. + +The owned handshake excludes a competing splice. Explicit abort after sending +STFU requests a disconnect before voucher failures can drain on reconnection. +Timeout, disconnect and restart also unwind setup; force-close remains available. +Only the epoch identity is serialized in a required action variant. Transient +monitor evidence is discarded on restart, which aborts the setup. The runtime must +enforce its handshake deadline through explicit abort, alongside the existing +stock peer timeout. + An explicit abort, mismatch, disconnect, or restart unwinds committed vouchers using ordinary HTLC failure rounds. Partial rounds finish before their HTLCs are failed. `Aborting` and `Aborted` distinguish pending removal from a synchronized channel with @@ -118,6 +134,10 @@ Authenticated setup tests cover signature and identity mismatches, funding and commitment mismatches, history bounds, block-height expiry and revealed-secret reuse. Registry and manager recovery tests cover closure retention, stale channel disposal, missing or conflicting records, capacity refusal and archive-only serialization. +Quiescence tests cover real STFU exchange, unsupported peers, competing actions, +partial rounds, delayed monitors, changed commitments, deadline equality, initiator +tie loss, pending abort, disconnect, restart and force-close. The ordinary +quiescence and splice suites also pass with these channel changes. ## Next boundary diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 30e8eb68764..4d1259afa77 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2820,6 +2820,7 @@ impl_writeable_tlv_based!(SpliceInstructions, { pub(crate) enum QuiescentAction { Splice(SpliceInstructions), + FFORReceiver(ffor::FFORReceiverQuiescence), #[cfg(any(test, fuzzing))] DoNothing, } @@ -2827,16 +2828,19 @@ pub(crate) enum QuiescentAction { pub(crate) enum StfuResponse { Stfu(msgs::Stfu), SpliceInit(msgs::SpliceInit), + FFORReceiver { is_initiator: bool }, } #[cfg(any(test, fuzzing))] impl_writeable_tlv_based_enum_upgradable!(QuiescentAction, (0, DoNothing) => {}, {1, Splice} => (), + {2, FFORReceiver} => (), ); #[cfg(not(any(test, fuzzing)))] impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,, {1, Splice} => (), + {2, FFORReceiver} => (), ); /// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`]. @@ -7012,7 +7016,6 @@ where contributed_outputs: outputs, }) }, - #[cfg(any(test, fuzzing))] Some(quiescent_action) => { self.quiescent_action = Some(quiescent_action); None @@ -9294,6 +9297,7 @@ where if let Some(book) = self.context.ffor_receiver_book.as_mut() { book.abort(crate::ln::ffor::FFORReceiverAbortReason::Disconnected); } + self.release_ffor_receiver_quiescence(); // We only clear `peer_disconnected` if we were able to reestablish the channel. We always // reset our awaiting response in case we failed reestablishment and are disconnecting. @@ -12135,6 +12139,11 @@ where pub fn validate_splice_init( &self, msg: &msgs::SpliceInit, our_funding_contribution: SignedAmount, ) -> Result { + if matches!(self.quiescent_action, Some(QuiescentAction::FFORReceiver(_))) { + return Err(ChannelError::WarnAndDisconnect( + "FFOR receiver owns this quiescence session".to_owned(), + )); + } if self.holder_commitment_point.current_point().is_none() { return Err(ChannelError::WarnAndDisconnect(format!( "Channel {} commitment point needs to be advanced once before spliced", @@ -13316,6 +13325,11 @@ where "Received counterparty stfu, channel is now quiescent and we are{} the initiator", if !is_holder_quiescence_initiator { " not" } else { "" } ); + if matches!(self.quiescent_action, Some(QuiescentAction::FFORReceiver(_))) { + return Ok(Some(StfuResponse::FFORReceiver { + is_initiator: is_holder_quiescence_initiator, + })); + } if is_holder_quiescence_initiator { match self.quiescent_action.take() { @@ -13340,6 +13354,7 @@ where let splice_init = self.send_splice_init(instructions); return Ok(Some(StfuResponse::SpliceInit(splice_init))); }, + Some(QuiescentAction::FFORReceiver(_)) => unreachable!(), #[cfg(any(test, fuzzing))] Some(QuiescentAction::DoNothing) => { // In quiescence test we want to just hang out here, letting the test manually @@ -15760,6 +15775,7 @@ where quiescent_action, }; channel.ffor_restored()?; + channel.restore_ffor_receiver_quiescence()?; Ok(channel) } } diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index 3b20aaaf299..20b330ed5ab 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -1,11 +1,13 @@ use super::*; +mod quiescence; mod setup; use crate::ln::ffor::{ self as verification, FFORCommitmentError, FFORMonitorSnapshot, FFORReceiverAbortReason, FFORReceiverError, FFORReceiverStatus, FFORSettlementParty, FFORVoucher, FFORVoucherCommitments, FFORVoucherFailure, }; +pub(crate) use quiescence::FFORReceiverQuiescence; #[cfg(test)] pub(crate) use setup::ffor_setup_test_messages; pub(crate) use setup::FFORReceiverSetup; diff --git a/lightning/src/ln/channel/ffor/quiescence.rs b/lightning/src/ln/channel/ffor/quiescence.rs new file mode 100644 index 00000000000..1baaf2a1f9b --- /dev/null +++ b/lightning/src/ln/channel/ffor/quiescence.rs @@ -0,0 +1,197 @@ +//! Connection-scoped STFU ownership for a complete authenticated receiver book. +//! +//! Only the epoch identity is serialized. A restart aborts setup and discards the transient +//! proof rather than resuming quiescence. This state never authorizes offline receiving. + +use super::*; +use crate::ln::ffor::FFORReceiverQuiescenceStatus; + +pub(crate) struct FFORReceiverQuiescence { + epoch_id: [u8; 32], + monitor: Option, + completed: bool, +} + +impl_writeable_tlv_based!(FFORReceiverQuiescence, { + (0, epoch_id, required), + (_unused, monitor, (static_value, None)), + (_unused, completed, (static_value, false)), +}); + +impl FundedChannel +where + SP::Target: SignerProvider, +{ + pub(crate) fn request_ffor_receiver_quiescence( + &mut self, epoch_id: [u8; 32], monitor: FFORMonitorSnapshot, current_height: u32, + logger: &L, + ) -> Result, FFORReceiverError> + where + L::Target: Logger, + { + self.check_ffor_quiescence_book(epoch_id, &monitor, current_height, logger)?; + if !self.context.is_connected() + || self.quiescent_action.is_some() + || self.context.channel_state.is_awaiting_quiescence() + || self.context.channel_state.is_local_stfu_sent() + || self.context.channel_state.is_remote_stfu_sent() + || self.context.channel_state.is_quiescent() + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let action = QuiescentAction::FFORReceiver(FFORReceiverQuiescence { + epoch_id, + monitor: Some(monitor), + completed: false, + }); + match self.propose_quiescence(logger, action) { + Ok(Some(message)) => Ok(Some(message)), + _ => { + // Admission requires a live, synchronized channel, so postponing the STFU is + // not supported. Never leave an owned action that can start implicitly later. + self.release_ffor_receiver_quiescence(); + Err(FFORCommitmentError::PendingUpdates.into()) + }, + } + } + + fn check_ffor_quiescence_book( + &self, epoch_id: [u8; 32], monitor: &FFORMonitorSnapshot, current_height: u32, logger: &L, + ) -> Result<(), FFORReceiverError> + where + L::Target: Logger, + { + let book = + self.context.ffor_receiver_book.as_ref().ok_or(FFORReceiverError::NotRegistered)?; + if book.epoch_id != epoch_id { + return Err(FFORReceiverError::UnknownEpoch); + } + let record = book.setup.as_ref().ok_or(FFORCommitmentError::InvalidVoucherBook)?; + let setup = + record.validate_recovery().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if current_height >= setup.terms().settlement_deadline { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + self.ffor_validate_receiver_setup().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + match self.ffor_receiver_book_status(monitor, logger)? { + FFORReceiverStatus::Parked { .. } => Ok(()), + _ => Err(FFORCommitmentError::PendingUpdates.into()), + } + } + + pub(crate) fn ffor_receiver_quiescence_status( + &self, epoch_id: [u8; 32], current_height: u32, logger: &L, + ) -> Result + where + L::Target: Logger, + { + let request = match self.quiescent_action.as_ref() { + Some(QuiescentAction::FFORReceiver(request)) => request, + _ => return Err(FFORCommitmentError::ChannelUnavailable.into()), + }; + if request.epoch_id != epoch_id { + return Err(FFORReceiverError::UnknownEpoch); + } + if !request.completed { + let book = + self.context.ffor_receiver_book.as_ref().ok_or(FFORReceiverError::NotRegistered)?; + if book.abort_reason.is_some() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + return Ok(FFORReceiverQuiescenceStatus::Negotiating); + } + let monitor = request.monitor.as_ref().ok_or(FFORCommitmentError::MonitorMismatch)?; + self.check_ffor_quiescence_book(epoch_id, monitor, current_height, logger)?; + if !self.context.channel_state.is_quiescent() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + Ok(FFORReceiverQuiescenceStatus::Quiescent) + } + + pub(crate) fn complete_ffor_receiver_quiescence( + &mut self, is_initiator: bool, current_height: u32, logger: &L, + ) -> Result<(), ChannelError> + where + L::Target: Logger, + { + let valid = match self.quiescent_action.as_ref() { + Some(QuiescentAction::FFORReceiver(request)) + if is_initiator && self.context.channel_state.is_quiescent() => + { + request.monitor.as_ref().map_or(false, |monitor| { + self.check_ffor_quiescence_book( + request.epoch_id, + monitor, + current_height, + logger, + ) + .is_ok() + }) + }, + _ => false, + }; + if !valid { + if let Some(book) = self.context.ffor_receiver_book.as_mut() { + book.abort(FFORReceiverAbortReason::QuiescenceFailed); + } + self.release_ffor_receiver_quiescence(); + return Err(ChannelError::WarnAndDisconnect( + "FFOR quiescence no longer matches its authenticated voucher book".to_owned(), + )); + } + if let Some(QuiescentAction::FFORReceiver(request)) = self.quiescent_action.as_mut() { + request.completed = true; + } + Ok(()) + } + + /// Drop only FFOR ownership. Stock STFU flags must survive until a disconnect has also + /// ended the peer's quiescence; otherwise queued failures would be sent into that session. + pub(crate) fn release_ffor_receiver_quiescence(&mut self) -> bool { + if !matches!(self.quiescent_action, Some(QuiescentAction::FFORReceiver(_))) { + return false; + } + self.quiescent_action = None; + self.context.channel_state.clear_awaiting_quiescence(); + self.context.channel_state.is_local_stfu_sent() + || self.context.channel_state.is_remote_stfu_sent() + || self.context.channel_state.is_quiescent() + } + + pub(in crate::ln::channel) fn restore_ffor_receiver_quiescence( + &mut self, + ) -> Result<(), DecodeError> { + if let Some(QuiescentAction::FFORReceiver(request)) = self.quiescent_action.as_ref() { + let book = self.context.ffor_receiver_book.as_ref().ok_or(DecodeError::InvalidValue)?; + if book.epoch_id != request.epoch_id || book.setup.is_none() { + return Err(DecodeError::InvalidValue); + } + self.release_ffor_receiver_quiescence(); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::ser::MaybeReadable; + + #[test] + fn ffor_quiescence_action_requires_new_reader() { + enum PreviousAction { + Splice(SpliceInstructions), + } + impl_writeable_tlv_based_enum_upgradable!(PreviousAction,, {1, Splice} => (),); + let action = QuiescentAction::FFORReceiver(FFORReceiverQuiescence { + epoch_id: [81; 32], + monitor: None, + completed: false, + }); + let encoded = action.encode(); + assert!(matches!( + PreviousAction::read(&mut &encoded[..]), + Err(DecodeError::UnknownRequiredFeature) + )); + } +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index acf066e343b..e6ced0d8787 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -66,8 +66,8 @@ use crate::ln::channel::{ }; use crate::ln::channel_state::ChannelDetails; use crate::ln::ffor::{ - FFORCommitmentError, FFORMonitorSnapshot, FFORReceiverError, FFORReceiverStatus, - FFORSettlementParty, FFORVoucher, FFORVoucherCommitments, + FFORCommitmentError, FFORMonitorSnapshot, FFORReceiverError, FFORReceiverQuiescenceStatus, + FFORReceiverStatus, FFORSettlementParty, FFORVoucher, FFORVoucherCommitments, }; use crate::ln::ffor_persistence::FFORPersistenceBarrier; pub use crate::ln::ffor_persistence::{FFORPersistenceRequirement, FFORPersistenceToken}; @@ -4400,6 +4400,76 @@ where Ok(requirement) } + /// Request a connection-scoped STFU handshake for an authenticated receiver setup. + /// + /// Both peers must advertise quiescence support. The channel must have exactly the complete, + /// irrevocably committed voucher book, with no pending updates, splices or competing handshake. + /// Obtain `monitor` from [`ChannelMonitor::ffor_commitment_snapshot`] and release its monitor + /// guard before calling. This consumes and retains the proof, rechecking it after both STFU + /// messages. Any intervening commitment or monitor update invalidates that proof. + /// + /// Quiescence ownership lasts until abort or disconnect. This does not activate an epoch or + /// authorize invoices. A restart aborts setup. The runtime must abort if its handshake timeout + /// expires; normal peer processing also enforces the existing stock quiescence timeout. + pub fn request_ffor_receiver_quiescence( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + monitor: FFORMonitorSnapshot, + ) -> Result<(), FFORReceiverError> { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let current_height = self.best_block.read().unwrap().height; + let per_peer_state = self.per_peer_state.read().unwrap(); + let mut peer = per_peer_state + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + if !self.init_features().supports_quiescence() + || !peer.latest_features.supports_quiescence() + { + return Err(FFORCommitmentError::UnsupportedChannelType.into()); + } + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + channel + .ffor_validate_receiver_identity(self.our_network_pubkey, self.chain_hash) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if let Some(msg) = channel.request_ffor_receiver_quiescence( + epoch_id, + monitor, + current_height, + &self.logger, + )? { + peer.pending_msg_events + .push(MessageSendEvent::SendStfu { node_id: *counterparty_node_id, msg }); + } + Ok(()) + } + + /// Inspect an owned receiver STFU handshake, rechecking its proof when quiescent. + /// + /// This status is connection-scoped and never establishes activation or invoice readiness. + /// A changed book, monitor update or expired deadline makes the completed proof invalid. + pub fn ffor_receiver_quiescence_status( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result { + let current_height = self.best_block.read().unwrap().height; + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer = per_peer_state + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(channel_id) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + channel.ffor_receiver_quiescence_status(epoch_id, current_height, &self.logger) + } + /// Inspect an experimental receiver registration. `Parked` requires the complete two-view /// commitment proof, including completed monitor updates and valid holder claim signatures. /// @@ -4430,6 +4500,8 @@ where /// commitment rounds are failed only after those rounds finish. A disconnect or restart retains /// the abort and resumes the unwind on reconnection. Ordinary payments can proceed after drain, /// but further FFOR registrations on this channel remain refused, including a different epoch. + /// If STFU was sent, abort requests a peer disconnect to end both sides' quiescence. Reconnect + /// and continue message processing to drain; failures are not sent while the peer is quiescent. pub fn abort_ffor_receiver_book( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], ) -> Result<(), FFORReceiverError> { @@ -4446,7 +4518,20 @@ where .and_then(Channel::as_funded_mut) .ok_or(FFORCommitmentError::ChannelUnavailable)?; channel.abort_ffor_receiver_book(epoch_id)?; + let disconnect = channel.release_ffor_receiver_quiescence(); channel.ffor_queue_aborted_vouchers(&self.logger); + if disconnect { + peer.pending_msg_events.push(MessageSendEvent::HandleError { + node_id: *counterparty_node_id, + action: msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id: *channel_id, + data: "Aborting FFOR receiver quiescence before draining vouchers" + .to_owned(), + }, + }, + }); + } Ok(()) } @@ -11796,7 +11881,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } #[rustfmt::skip] - fn internal_stfu(&self, counterparty_node_id: &PublicKey, msg: &msgs::Stfu) -> Result { + fn internal_stfu(&self, counterparty_node_id: &PublicKey, msg: &msgs::Stfu, persist_ffor: &mut bool) -> Result { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { debug_assert!(false); @@ -11825,6 +11910,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let resp = try_channel_entry!(self, peer_state, res, chan_entry); match resp { None => Ok(false), + Some(StfuResponse::FFORReceiver { is_initiator }) => { + *persist_ffor = true; + let current_height = self.best_block.read().unwrap().height; + let chan = chan_entry.get_mut().as_funded_mut().unwrap(); + let res = chan.complete_ffor_receiver_quiescence(is_initiator, current_height, &&logger); + try_channel_entry!(self, peer_state, res, chan_entry); + Ok(false) + }, Some(StfuResponse::Stfu(msg)) => { peer_state.pending_msg_events.push(MessageSendEvent::SendStfu { node_id: *counterparty_node_id, @@ -15070,17 +15163,22 @@ where fn handle_stfu(&self, counterparty_node_id: PublicKey, msg: &msgs::Stfu) { let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { - let res = self.internal_stfu(&counterparty_node_id, msg); - let persist = match &res { - Err(e) if e.closes_channel() => NotifyOption::DoPersist, - Err(_) => NotifyOption::SkipPersistHandleEvents, - Ok(responded) => { - if *responded { - NotifyOption::SkipPersistHandleEvents - } else { - NotifyOption::SkipPersistNoEvents - } - }, + let mut persist_ffor = false; + let res = self.internal_stfu(&counterparty_node_id, msg, &mut persist_ffor); + let persist = if persist_ffor { + NotifyOption::DoPersist + } else { + match &res { + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(responded) => { + if *responded { + NotifyOption::SkipPersistHandleEvents + } else { + NotifyOption::SkipPersistNoEvents + } + }, + } }; let _ = handle_error!(self, res, counterparty_node_id); persist diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index 5828862a8ac..78e046aba33 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -68,6 +68,8 @@ pub enum FFORReceiverAbortReason { Restarted, /// The first voucher hash reused a revealed counterparty commitment secret. CommitmentSecretReused, + /// The owned quiescence handshake no longer matched its voucher evidence or deadline. + QuiescenceFailed, } impl_writeable_tlv_based_enum!(FFORReceiverAbortReason, @@ -76,8 +78,21 @@ impl_writeable_tlv_based_enum!(FFORReceiverAbortReason, (4, Disconnected) => {}, (6, Restarted) => {}, (8, CommitmentSecretReused) => {}, + (10, QuiescenceFailed) => {}, ); +/// State of the receiver's experimental, connection-scoped STFU handshake. +/// +/// Neither state means that an epoch is activated or an invoice can be exposed. Quiescence +/// ends on disconnect; a later activation mechanism must establish its own durable freeze. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FFORReceiverQuiescenceStatus { + /// The receiver has requested quiescence and is awaiting the peer's STFU. + Negotiating, + /// Both STFU messages were exchanged and the retained voucher proof still matches. + Quiescent, +} + /// Receiver-side state of one experimental voucher registration. /// /// Even `Parked` is only point-in-time evidence. It does not freeze commitments or authorize an diff --git a/lightning/src/ln/ffor_tests.rs b/lightning/src/ln/ffor_tests.rs index 78281717f94..bc9c1c04257 100644 --- a/lightning/src/ln/ffor_tests.rs +++ b/lightning/src/ln/ffor_tests.rs @@ -9,6 +9,8 @@ use crate::util::config::UserConfig; use crate::util::ser::Writeable; use core::sync::atomic::Ordering; +mod quiescence; + pub(super) fn anchor_config() -> UserConfig { let mut config = test_default_channel_config(); config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; diff --git a/lightning/src/ln/ffor_tests/quiescence.rs b/lightning/src/ln/ffor_tests/quiescence.rs new file mode 100644 index 00000000000..91a7998acf6 --- /dev/null +++ b/lightning/src/ln/ffor_tests/quiescence.rs @@ -0,0 +1,478 @@ +use super::*; +use crate::ln::channel::{ffor_setup_test_messages, DISCONNECT_PEER_AWAITING_RESPONSE_TICKS}; + +const EPOCH: [u8; 32] = [81; 32]; + +fn register_signed(sender: &Node, receiver: &Node, channel_id: ChannelId, voucher: FFORVoucher) { + let (init, accept) = ffor_setup_test_messages(sender, receiver, channel_id, voucher); + let requirement = receiver + .node + .register_ffor_receiver_setup( + &channel_id, + &sender.node.get_our_node_id(), + &init.encode().unwrap(), + &accept.encode().unwrap(), + 20, + ) + .unwrap(); + let token = receiver.node.capture_ffor_persistence(); + let _persisted = receiver.node.encode(); + receiver.node.ffor_persistence_completed(token).unwrap(); + assert!(receiver.node.is_ffor_state_persisted(&requirement)); +} + +fn request(sender: &Node, receiver: &Node, channel_id: ChannelId) -> Result<(), FFORReceiverError> { + receiver.node.request_ffor_receiver_quiescence( + &channel_id, + &sender.node.get_our_node_id(), + EPOCH, + snapshot(receiver, channel_id), + ) +} + +fn status( + sender: &Node, receiver: &Node, channel_id: ChannelId, +) -> Result { + receiver.node.ffor_receiver_quiescence_status( + &channel_id, + &sender.node.get_our_node_id(), + EPOCH, + ) +} + +fn complete_handshake(sender: &Node, receiver: &Node) { + let receiver_id = receiver.node.get_our_node_id(); + let sender_id = sender.node.get_our_node_id(); + let proposed = get_event_msg!(receiver, MessageSendEvent::SendStfu, sender_id); + assert!(proposed.initiator); + sender.node.handle_stfu(receiver_id, &proposed); + let response = get_event_msg!(sender, MessageSendEvent::SendStfu, receiver_id); + assert!(!response.initiator); + receiver.node.handle_stfu(sender_id, &response); +} + +fn expect_disconnect(node: &Node) { + let events = node.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1, "{events:?}"); + assert!(matches!( + events[0], + MessageSendEvent::HandleError { + action: msgs::ErrorAction::DisconnectPeerWithWarning { .. }, + .. + } + )); + check_added_monitors(node, 0); +} + +#[test] +fn ffor_quiescence_exchanges_stfu_and_retains_ownership_until_abort() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_signed(&nodes[0], &nodes[1], channel_id, voucher); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + request(&nodes[0], &nodes[1], channel_id).unwrap(); + assert_eq!( + status(&nodes[0], &nodes[1], channel_id), + Ok(FFORReceiverQuiescenceStatus::Negotiating) + ); + complete_handshake(&nodes[0], &nodes[1]); + assert_eq!( + status(&nodes[0], &nodes[1], channel_id), + Ok(FFORReceiverQuiescenceStatus::Quiescent) + ); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + assert!(request(&nodes[0], &nodes[1], channel_id).is_err()); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + nodes[1] + .node + .abort_ffor_receiver_book(&channel_id, &nodes[0].node.get_our_node_id(), EPOCH) + .unwrap(); + expect_disconnect(&nodes[1]); + assert!(status(&nodes[0], &nodes[1], channel_id).is_err()); + nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id()); + nodes[1].node.peer_disconnected(nodes[0].node.get_our_node_id()); + pump_ffor_reconnection(&nodes[0], &nodes[1]); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborted { reason: FFORReceiverAbortReason::Requested } + ); + expect_payment_failed!(&nodes[0], voucher.payment_hash, false); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + +#[test] +fn ffor_quiescence_rejects_raw_setup_missing_support_and_competing_action() { + for rejection in 0..3 { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + if rejection == 1 { + let mut features = node_chanmgrs[0].init_features(); + features.clear_quiescence(); + *node_cfgs[0].override_init_features.borrow_mut() = Some(features); + } + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + if rejection == 0 { + register_book(&nodes[1], &nodes[0], channel_id, &[voucher]); + } else { + register_signed(&nodes[0], &nodes[1], channel_id, voucher); + } + deliver_parked_voucher(&nodes[0], &nodes[1], update); + if rejection == 2 { + nodes[1] + .node + .maybe_propose_quiescence(&nodes[0].node.get_our_node_id(), &channel_id) + .unwrap(); + } + assert!(nodes[1] + .node + .request_ffor_receiver_quiescence( + &channel_id, + &nodes[0].node.get_our_node_id(), + if rejection == 0 { RECEIVER_EPOCH } else { EPOCH }, + snapshot(&nodes[1], channel_id), + ) + .is_err()); + if rejection == 2 { + complete_handshake(&nodes[0], &nodes[1]); + assert!(nodes[0] + .node + .exit_quiescence(&nodes[1].node.get_our_node_id(), &channel_id) + .unwrap()); + assert!(nodes[1] + .node + .exit_quiescence(&nodes[0].node.get_our_node_id(), &channel_id) + .unwrap()); + } else { + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + } + } +} + +#[test] +fn ffor_quiescence_rejects_partial_rounds_and_stale_monitor_evidence() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_signed(&nodes[0], &nodes[1], channel_id, voucher); + let before_round = snapshot(&nodes[1], channel_id); + assert!(request(&nodes[0], &nodes[1], channel_id).is_err()); + nodes[1] + .node + .handle_update_add_htlc(nodes[0].node.get_our_node_id(), &update.update_add_htlcs[0]); + assert!(request(&nodes[0], &nodes[1], channel_id).is_err()); + let sender_id = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + nodes[1].node.handle_commitment_signed_batch_test(sender_id, &update.commitment_signed); + check_added_monitors(&nodes[1], 1); + assert!(request(&nodes[0], &nodes[1], channel_id).is_err()); + let (revoke, commitment) = get_revoke_commit_msgs!(&nodes[1], sender_id); + nodes[0].node.handle_revoke_and_ack(receiver_id, &revoke); + check_added_monitors(&nodes[0], 1); + nodes[0].node.handle_commitment_signed_batch_test(receiver_id, &commitment); + check_added_monitors(&nodes[0], 1); + let revoke = get_event_msg!(&nodes[0], MessageSendEvent::SendRevokeAndACK, receiver_id); + assert!(request(&nodes[0], &nodes[1], channel_id).is_err()); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + nodes[1].node.handle_revoke_and_ack(sender_id, &revoke); + check_added_monitors(&nodes[1], 1); + assert!(request(&nodes[0], &nodes[1], channel_id).is_err()); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + let update_id = get_monitor!(nodes[1], channel_id).get_latest_update_id(); + nodes[1].chain_monitor.chain_monitor.channel_monitor_updated(channel_id, update_id).unwrap(); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + expect_and_process_pending_htlcs(&nodes[1], false); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[1] + .node + .request_ffor_receiver_quiescence( + &channel_id, + &nodes[0].node.get_our_node_id(), + EPOCH, + before_round, + ) + .is_err()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + request(&nodes[0], &nodes[1], channel_id).unwrap(); + complete_handshake(&nodes[0], &nodes[1]); + assert_eq!( + status(&nodes[0], &nodes[1], channel_id), + Ok(FFORReceiverQuiescenceStatus::Quiescent) + ); +} + +#[test] +fn ffor_quiescence_finishes_inflight_round_then_rejects_changed_book() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let sender_id = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_signed(&nodes[0], &nodes[1], channel_id, voucher); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + request(&nodes[0], &nodes[1], channel_id).unwrap(); + let proposed = get_event_msg!(&nodes[1], MessageSendEvent::SendStfu, sender_id); + // The peer has not received our STFU, so its already-started stock round is still legal. + let (update, extra, _) = offer_voucher(&nodes[0], &nodes[1], 1_000_000); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + assert_eq!(nodes[1].node.list_channels().len(), 1); + nodes[0].node.handle_stfu(receiver_id, &proposed); + let response = get_event_msg!(&nodes[0], MessageSendEvent::SendStfu, receiver_id); + nodes[1].node.handle_stfu(sender_id, &response); + expect_disconnect(&nodes[1]); + assert!(status(&nodes[0], &nodes[1], channel_id).is_err()); + nodes[0].node.peer_disconnected(receiver_id); + nodes[1].node.peer_disconnected(sender_id); + pump_ffor_reconnection(&nodes[0], &nodes[1]); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborted { reason: FFORReceiverAbortReason::VoucherMismatch } + ); + let mut failures = [Vec::new(), Vec::new()]; + for event in nodes[0].node.get_and_clear_pending_events() { + let hash = match &event { + crate::events::Event::PaymentPathFailed { payment_hash, .. } => *payment_hash, + crate::events::Event::PaymentFailed { payment_hash: Some(payment_hash), .. } => { + *payment_hash + }, + _ => panic!("Unexpected event {event:?}"), + }; + let index = if hash == voucher.payment_hash { + 0 + } else { + assert_eq!(hash, extra.payment_hash); + 1 + }; + failures[index].push(event); + } + for (events, hash) in failures.into_iter().zip([voucher.payment_hash, extra.payment_hash]) { + expect_payment_failed_conditions_event(events, hash, false, PaymentFailedConditions::new()); + } + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + +#[test] +fn ffor_quiescence_disconnect_timeout_and_restart_abort_and_drain() { + for recovery in 0..3 { + for completed in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let reloaded; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let sender_id = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_signed(&nodes[0], &nodes[1], channel_id, voucher); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + request(&nodes[0], &nodes[1], channel_id).unwrap(); + if completed { + complete_handshake(&nodes[0], &nodes[1]); + } else { + let _undelivered = get_event_msg!(&nodes[1], MessageSendEvent::SendStfu, sender_id); + } + if recovery == 1 { + for _ in 0..DISCONNECT_PEER_AWAITING_RESPONSE_TICKS { + nodes[1].node.timer_tick_occurred(); + } + expect_disconnect(&nodes[1]); + } + nodes[0].node.peer_disconnected(receiver_id); + if recovery == 2 { + let monitor_encoded = get_monitor!(nodes[1], channel_id).encode(); + let manager_encoded = nodes[1].node.encode(); + reload_node!( + nodes[1], + config, + &manager_encoded, + &[&monitor_encoded], + persister, + chain_monitor, + reloaded + ); + } else { + nodes[1].node.peer_disconnected(sender_id); + } + pump_ffor_reconnection(&nodes[0], &nodes[1]); + assert!(status(&nodes[0], &nodes[1], channel_id).is_err()); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborted { + reason: if recovery == 2 { + FFORReceiverAbortReason::Restarted + } else { + FFORReceiverAbortReason::Disconnected + }, + } + ); + expect_payment_failed!(&nodes[0], voucher.payment_hash, false); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + } + } +} + +#[test] +fn ffor_quiescence_rechecks_deadline_and_same_book_after_fee_round() { + for change in 0..3 { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let sender_id = nodes[0].node.get_our_node_id(); + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_signed(&nodes[0], &nodes[1], channel_id, voucher); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + if change != 0 { + request(&nodes[0], &nodes[1], channel_id).unwrap(); + } + if change < 2 { + let deadline = voucher.cltv_expiry - 20; + let blocks = deadline - nodes[1].node.current_best_block().height; + connect_blocks(&nodes[1], blocks); + assert_eq!(nodes[1].node.current_best_block().height, deadline); + } else { + // This stock fee round is legal before the sender has received our STFU. It keeps + // the exact vouchers but invalidates the retained commitment and monitor proof. + *chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap() += 20; + nodes[0].node.timer_tick_occurred(); + check_added_monitors(&nodes[0], 1); + let update = get_htlc_update_msgs!(&nodes[0], nodes[1].node.get_our_node_id()); + nodes[1].node.handle_update_fee(sender_id, update.update_fee.as_ref().unwrap()); + // Consume the pending request before the commitment dance inspects outgoing messages. + let proposed = get_event_msg!(&nodes[1], MessageSendEvent::SendStfu, sender_id); + commitment_signed_dance!(&nodes[1], &nodes[0], update.commitment_signed, false); + assert!(matches!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Parked { .. } + )); + nodes[0].node.handle_stfu(nodes[1].node.get_our_node_id(), &proposed); + let response = get_event_msg!( + &nodes[0], + MessageSendEvent::SendStfu, + nodes[1].node.get_our_node_id() + ); + nodes[1].node.handle_stfu(sender_id, &response); + } + if change == 0 { + assert!(request(&nodes[0], &nodes[1], channel_id).is_err()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[1].node.abort_ffor_receiver_book(&channel_id, &sender_id, EPOCH).unwrap(); + drain_voucher_failures(&nodes[0], &nodes[1], &[voucher.payment_hash]); + } else { + if change == 1 { + complete_handshake(&nodes[0], &nodes[1]); + } + expect_disconnect(&nodes[1]); + nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id()); + nodes[1].node.peer_disconnected(sender_id); + pump_ffor_reconnection(&nodes[0], &nodes[1]); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborted { reason: FFORReceiverAbortReason::QuiescenceFailed } + ); + expect_payment_failed!(&nodes[0], voucher.payment_hash, false); + } + } +} + +#[test] +fn ffor_quiescence_losing_initiator_tie_aborts_and_pending_abort_drains() { + for tie in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let sender_id = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_signed(&nodes[0], &nodes[1], channel_id, voucher); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + request(&nodes[0], &nodes[1], channel_id).unwrap(); + let proposed = get_event_msg!(&nodes[1], MessageSendEvent::SendStfu, sender_id); + if tie { + nodes[0].node.maybe_propose_quiescence(&receiver_id, &channel_id).unwrap(); + let competing = get_event_msg!(&nodes[0], MessageSendEvent::SendStfu, receiver_id); + assert!(competing.initiator && proposed.initiator); + nodes[0].node.handle_stfu(receiver_id, &proposed); + nodes[1].node.handle_stfu(sender_id, &competing); + } else { + nodes[1].node.abort_ffor_receiver_book(&channel_id, &sender_id, EPOCH).unwrap(); + } + expect_disconnect(&nodes[1]); + nodes[0].node.peer_disconnected(receiver_id); + nodes[1].node.peer_disconnected(sender_id); + pump_ffor_reconnection(&nodes[0], &nodes[1]); + assert_eq!( + receiver_status(&nodes[1], &nodes[0], channel_id), + FFORReceiverStatus::Aborted { + reason: if tie { + FFORReceiverAbortReason::QuiescenceFailed + } else { + FFORReceiverAbortReason::Requested + } + } + ); + expect_payment_failed!(&nodes[0], voucher.payment_hash, false); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + } +} + +#[test] +fn ffor_quiescence_preserves_force_close() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (update, voucher, _) = offer_voucher(&nodes[0], &nodes[1], 2_000_000); + register_signed(&nodes[0], &nodes[1], channel_id, voucher); + deliver_parked_voucher(&nodes[0], &nodes[1], update); + request(&nodes[0], &nodes[1], channel_id).unwrap(); + complete_handshake(&nodes[0], &nodes[1]); + nodes[1] + .node + .force_close_broadcasting_latest_txn( + &channel_id, + &nodes[0].node.get_our_node_id(), + "quiescence force close test".into(), + ) + .unwrap(); + assert!(nodes[1].node.list_channels().is_empty()); + assert!(nodes[1] + .node + .get_and_clear_pending_events() + .iter() + .any(|event| matches!(event, crate::events::Event::ChannelClosed { .. }))); + handle_bump_close_event(&nodes[1]); + assert!(!nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().is_empty()); + nodes[1].node.get_and_clear_pending_msg_events(); + check_added_monitors(&nodes[1], 1); +} From 25d2c0d07935f4aa3ca5f728e75327f43b85915c Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 21:20:41 -0400 Subject: [PATCH 06/22] feat(ffor): verify witness provisioning and acknowledgement correlation --- .github/workflows/ffor-protocol.yml | 1 + lightning-ffor/README.md | 33 ++ lightning-ffor/fuzz/Cargo.toml | 7 + lightning-ffor/fuzz/README.md | 7 + lightning-ffor/fuzz/fuzz_targets/witness.rs | 51 +++ lightning-ffor/fuzz/seed_corpus.py | 13 + lightning-ffor/src/lib.rs | 1 + lightning-ffor/src/transcript.rs | 18 +- lightning-ffor/src/witness/correlation.rs | 107 +++++ lightning-ffor/src/witness/manifest.rs | 212 +++++++++ lightning-ffor/src/witness/messages.rs | 148 +++++++ lightning-ffor/src/witness/mod.rs | 131 ++++++ .../tests/data/beignet-witness.json | 65 +++ .../tests/data/generate_beignet_witness.cjs | 71 +++ lightning-ffor/tests/witness.rs | 411 ++++++++++++++++++ 15 files changed, 1272 insertions(+), 4 deletions(-) create mode 100644 lightning-ffor/fuzz/fuzz_targets/witness.rs create mode 100644 lightning-ffor/src/witness/correlation.rs create mode 100644 lightning-ffor/src/witness/manifest.rs create mode 100644 lightning-ffor/src/witness/messages.rs create mode 100644 lightning-ffor/src/witness/mod.rs create mode 100644 lightning-ffor/tests/data/beignet-witness.json create mode 100644 lightning-ffor/tests/data/generate_beignet_witness.cjs create mode 100644 lightning-ffor/tests/witness.rs diff --git a/.github/workflows/ffor-protocol.yml b/.github/workflows/ffor-protocol.yml index b44448181f0..8dee1f48dad 100644 --- a/.github/workflows/ffor-protocol.yml +++ b/.github/workflows/ffor-protocol.yml @@ -45,3 +45,4 @@ jobs: - run: cargo install cargo-fuzz --version 0.13.2 --locked - run: python3 fuzz/seed_corpus.py - run: cargo fuzz run wire -- -max_len=131072 -max_total_time=30 + - run: cargo fuzz run witness -- -max_len=196609 -max_total_time=30 diff --git a/lightning-ffor/README.md b/lightning-ffor/README.md index 052deac806e..71c2766bfa3 100644 --- a/lightning-ffor/README.md +++ b/lightning-ffor/README.md @@ -168,6 +168,39 @@ invoices, just-in-time liquidity and aggregate multi-channel capacity cannot qua The node must enforce the selected offline window, claim margin, chain watching and fee funding. No production window or witness deployment is chosen by this crate. +## Receiver witness provisioning primitives + +The `witness` module implements only version 1/D-R manifest construction, receiver-side +decoding against `AuthenticatedSetup`, fetch-key signature authentication, and Appendix F.1 +`ff_witness_provision`/`ff_witness_ack`. It reuses the authenticated canonical book and transcript +hashes. Timestamp-style voucher expiries, short retention, mismatched setup/book/activation +digests, malformed keys and noncanonical signatures are refused. There is no extension stream +in these formats: unknown versions/profiles, non-0/1 success flags and trailing bytes fail. +Refusal data is bounded opaque bytes. The 65,535-byte wire limit includes the message type. + +`UnsignedManifest` supplies canonical bytes and their `ffor/witness/manifest` signing digest +to an external fetch-key signer. It exports no private key and implements no signing operation. +`PendingProvision` associates exact signed manifest bytes and request ID with a caller-owned +authenticated witness connection token. `check_acknowledgement` checks the request, actual +source node and connection, claimed witness identity and retention promise before returning +an immutable `CheckedAcknowledgement`. Request IDs must never be reused for different +manifests. A reconnect requires fresh request correlation with the same retained manifest. + +This acknowledgement has no independent signature or H_act field on the wire. The caller must +supply transport identity from its actual handshake, persist registration keys and the exact +manifest before sending, persist the correlated acknowledgement, and bind it to the engine's +current ACTIVE epoch before any invoice use. The protocol object cannot establish those facts, +or that a witness will honor its promise. Missing/failed/short acknowledgements remain incomplete. +Fetch/receipt decryption, witness selection and invoice paths, durable receiver recovery, and a +witness service remain outside this slice. + +`tests/data/beignet-witness.json` is generated from Beignet `8aee31d1` using its actual manifest +and provisioning codecs over the public Appendix D setups, with deterministic test-only fetch +key material. `generate_beignet_witness.cjs` verifies the source revision and records the +Appendix D input digest. The fixtures are new cross-implementation examples, not published +Appendix F vectors. Focused tests cover exact bytes, all truncated prefixes, signature domains, +retention/height bounds, maximum books, connection changes, retries and bounded arbitrary input. + The requested settlement baseline is LND v0.21.3-beta. LND implementation work is kept local. This unpublished workspace crate does not change a binding version or a release. diff --git a/lightning-ffor/fuzz/Cargo.toml b/lightning-ffor/fuzz/Cargo.toml index 4162513fd8f..83b7bb9f2e4 100644 --- a/lightning-ffor/fuzz/Cargo.toml +++ b/lightning-ffor/fuzz/Cargo.toml @@ -21,3 +21,10 @@ path = "fuzz_targets/wire.rs" test = false doc = false bench = false + +[[bin]] +name = "witness" +path = "fuzz_targets/witness.rs" +test = false +doc = false +bench = false diff --git a/lightning-ffor/fuzz/README.md b/lightning-ffor/fuzz/README.md index 8cc2d0c63be..f7416c061a5 100644 --- a/lightning-ffor/fuzz/README.md +++ b/lightning-ffor/fuzz/README.md @@ -20,3 +20,10 @@ A bounded run detects regressions but does not prove exhaustive parser coverage. crate's property tests exercise bounded arbitrary bytes, extension lengths, and settled bitmaps independently of this target. Live network partitions, persistence and channel transitions are outside this pure codec and require later engine integration tests. + +The `witness` target checks strict provision/ack encodings and authenticated receiver manifest +decoding. It accepts raw acknowledgement bytes or `[u16 init_length][u16 accept_length][init] +[accept][candidate]`, with candidate being a manifest or complete provision. Its seeds use the +six public Appendix D setups and manifests generated by the pinned Beignet implementation. +Run `cargo +nightly fuzz run witness -- -max_len=196609 -max_total_time=60` after seeding. +Neither fuzz target signs messages or accesses private keys. diff --git a/lightning-ffor/fuzz/fuzz_targets/witness.rs b/lightning-ffor/fuzz/fuzz_targets/witness.rs new file mode 100644 index 00000000000..efcf04bf929 --- /dev/null +++ b/lightning-ffor/fuzz/fuzz_targets/witness.rs @@ -0,0 +1,51 @@ +#![no_main] + +use bitcoin::secp256k1::PublicKey; +use libfuzzer_sys::fuzz_target; +use lightning_ffor::setup::AuthenticatedSetup; +use lightning_ffor::wire::Message; +use lightning_ffor::witness::{Acknowledgement, Provision, SignedManifest, MAX_MESSAGE_LEN}; + +fuzz_target!(|data: &[u8]| { + if let Ok(ack) = Acknowledgement::decode(data) { + assert_eq!(ack.encode(), data); + } + // Public signed setup plus candidate, so the receiver-specific manifest decoder is exercised + // without a private key or a second independent canonical-book implementation. + if data.len() < 4 || data.len() > 3 * MAX_MESSAGE_LEN + 4 { + return; + } + let init_length = usize::from(u16::from_be_bytes([data[0], data[1]])); + let accept_length = usize::from(u16::from_be_bytes([data[2], data[3]])); + let Some(init) = data.get(4..4 + init_length) else { + return; + }; + let Some(accept) = data.get(4 + init_length..4 + init_length + accept_length) else { + return; + }; + let Some(candidate) = data.get(4 + init_length + accept_length..) else { + return; + }; + let (Ok(init), Ok(accept)) = (Message::decode(init), Message::decode(accept)) else { + return; + }; + let receiver = PublicKey::from_slice(&[ + 3, 159, 202, 127, 129, 87, 170, 118, 135, 8, 137, 79, 253, 146, 85, 15, 233, 112, 237, 209, + 133, 38, 165, 249, 54, 88, 62, 163, 181, 77, 171, 50, 40, + ]) + .unwrap(); + let settlement = PublicKey::from_slice(&[ + 2, 8, 123, 125, 27, 71, 137, 23, 15, 110, 55, 79, 10, 14, 88, 161, 183, 168, 153, 227, 73, + 41, 121, 83, 20, 171, 105, 100, 230, 150, 9, 233, 192, + ]) + .unwrap(); + let Ok(setup) = AuthenticatedSetup::new(&init, &accept, receiver, settlement) else { + return; + }; + if let Ok(manifest) = SignedManifest::decode(candidate, &setup) { + assert_eq!(manifest.encode(), candidate); + } + if let Ok(provision) = Provision::decode(candidate, &setup) { + assert_eq!(provision.encode(), candidate); + } +}); diff --git a/lightning-ffor/fuzz/seed_corpus.py b/lightning-ffor/fuzz/seed_corpus.py index 5eb982898ee..bf07c87f02b 100644 --- a/lightning-ffor/fuzz/seed_corpus.py +++ b/lightning-ffor/fuzz/seed_corpus.py @@ -21,3 +21,16 @@ (corpus / f"reestablish-{fixture['state']}").write_bytes(bytes.fromhex(fixture["value"])) print("Seeded 35 signed reference messages/setup pairs and 7 reconnect reports") + +witness_corpus = crate / "fuzz" / "corpus" / "witness" +witness_corpus.mkdir(parents=True, exist_ok=True) +witnesses = json.loads((crate / "tests/data/beignet-witness.json").read_text()) +for index, (setup, witness) in enumerate(zip(fixtures, witnesses["fixtures"])): + init = bytes.fromhex(setup["init_wire"]) + accept = bytes.fromhex(setup["accept_wire"]) + prefix = len(init).to_bytes(2, "big") + len(accept).to_bytes(2, "big") + init + accept + for field in ("manifest", "provision"): + (witness_corpus / f"witness-{index}-{field}").write_bytes(prefix + bytes.fromhex(witness[field])) + for field in ("acknowledgement", "refusal"): + (witness_corpus / f"witness-{index}-{field}").write_bytes(bytes.fromhex(witness[field])) +print("Seeded 24 witness manifest/provision/acknowledgement cases") diff --git a/lightning-ffor/src/lib.rs b/lightning-ffor/src/lib.rs index 07e6d75b472..e46b6c0bc43 100644 --- a/lightning-ffor/src/lib.rs +++ b/lightning-ffor/src/lib.rs @@ -18,3 +18,4 @@ pub mod reestablish; pub mod setup; pub mod transcript; pub mod wire; +pub mod witness; diff --git a/lightning-ffor/src/transcript.rs b/lightning-ffor/src/transcript.rs index f2f477bf0f0..39be618a950 100644 --- a/lightning-ffor/src/transcript.rs +++ b/lightning-ffor/src/transcript.rs @@ -11,14 +11,14 @@ use core::fmt; /// A 32-byte SHA256 transcript digest in raw byte order. pub type Digest = [u8; 32]; -/// Failure to authenticate a compact node-key signature from FFOR section 7. +/// Failure to authenticate a compact ECDSA signature in an FFOR domain. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SignatureError { /// The signature is not a valid compact ECDSA encoding. Malformed, /// The signature uses a noncanonical high-S scalar. HighS, - /// The expected node did not sign this message type and exact unsigned body. + /// The expected public key did not sign the exact domain-bound digest. Invalid, } @@ -55,6 +55,16 @@ impl std::error::Error for SignatureError {} pub fn verify_message_signature( message_type: u16, unsigned_body: &[u8], compact_signature: &[u8; 64], expected_signer: &PublicKey, +) -> Result<(), SignatureError> { + verify_digest_signature( + message_digest(message_type, unsigned_body), + compact_signature, + expected_signer, + ) +} + +pub(crate) fn verify_digest_signature( + digest: Digest, compact_signature: &[u8; 64], expected_signer: &PublicKey, ) -> Result<(), SignatureError> { let signature = Signature::from_compact(compact_signature).map_err(|_| SignatureError::Malformed)?; @@ -63,13 +73,13 @@ pub fn verify_message_signature( if normalized != signature { return Err(SignatureError::HighS); } - let message = Message::from_digest(message_digest(message_type, unsigned_body)); + let message = Message::from_digest(digest); Secp256k1::verification_only() .verify_ecdsa(&message, &signature, expected_signer) .map_err(|_| SignatureError::Invalid) } -fn hash_parts(tag: &[u8], parts: &[&[u8]]) -> Digest { +pub(crate) fn hash_parts(tag: &[u8], parts: &[&[u8]]) -> Digest { let mut engine = sha256::Hash::engine(); engine.input(tag); for part in parts { diff --git a/lightning-ffor/src/witness/correlation.rs b/lightning-ffor/src/witness/correlation.rs new file mode 100644 index 00000000000..57eaeb3ba0b --- /dev/null +++ b/lightning-ffor/src/witness/correlation.rs @@ -0,0 +1,107 @@ +use bitcoin::secp256k1::PublicKey; + +use super::{Acknowledgement, AcknowledgementResult, Provision, WitnessError}; + +/// Caller-supplied evidence identifying one authenticated transport connection. +/// +/// This crate cannot authenticate a socket. `node_id` must come from the real peer handshake, +/// and `identity` must distinguish connection instances, including reconnects. Never populate +/// either field from acknowledgement bytes. An engine can use its opaque connection token as C. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WitnessConnection { + /// Node key authenticated by the transport handshake. + pub node_id: PublicKey, + /// Caller-owned identity for this particular connection instance. + pub identity: C, +} + +/// One immutable request and the authenticated connection on which its reply is expected. +/// +/// Request IDs must not be reused for a different manifest. The caller must persist the signed +/// manifest and recovery keys before sending. A reconnect needs a newly correlated request; +/// re-provision the identical manifest rather than allocating a new mailbox after an uncertain +/// result. This value is deliberately not a durable or ACTIVE state machine. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PendingProvision { + provision: Provision, + connection: WitnessConnection, +} + +impl PendingProvision { + /// Bind the exact request, signed manifest and intended witness connection before sending. + pub fn new(provision: Provision, connection: WitnessConnection) -> Self { + Self { provision, connection } + } + + /// Exact request to persist and send; its manifest cannot be changed through this reference. + pub fn provision(&self) -> &Provision { + &self.provision + } + + /// Expected transport witness and connection generation. + pub fn connection(&self) -> &WitnessConnection { + &self.connection + } +} + +impl PendingProvision { + /// Correlate a canonical response with this request and its actual authenticated source. + /// + /// The ack contains no H_act or manifest hash. Its request ID is meaningful only alongside + /// this exact retained manifest and connection. A refusal, short retention or mismatch leaves + /// the pending request unchanged. Even success requires caller persistence before any use. + pub fn check_acknowledgement( + &self, acknowledgement: &Acknowledgement, source: &WitnessConnection, + ) -> Result, WitnessError> { + if acknowledgement.request_id() != self.provision.request_id() { + return Err(WitnessError::Request); + } + if source.identity != self.connection.identity { + return Err(WitnessError::Connection); + } + if source.node_id != self.connection.node_id { + return Err(WitnessError::Witness); + } + let (witness, retention_until) = match acknowledgement.result() { + AcknowledgementResult::Accepted { witness, retention_until } => { + (*witness, *retention_until) + }, + AcknowledgementResult::Refused(_) => return Err(WitnessError::Refused), + }; + if witness != self.connection.node_id { + return Err(WitnessError::Witness); + } + if retention_until < self.provision.manifest().unsigned().parameters().retention_until { + return Err(WitnessError::Retention); + } + Ok(CheckedAcknowledgement { pending: self.clone(), retention_until }) + } +} + +/// Immutable, exactly correlated witness acknowledgement, not proof of invoice readiness. +/// +/// Its private fields can only be constructed by successful pending-request checks. The caller +/// must durably associate this result with the engine's current epoch and recheck ACTIVE before +/// exposing anything. A dishonest witness may still fail its storage promise. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CheckedAcknowledgement { + pending: PendingProvision, + retention_until: u32, +} + +impl CheckedAcknowledgement { + /// The exact request and signed manifest this response acknowledged. + pub fn provision(&self) -> &Provision { + self.pending.provision() + } + + /// The actual expected witness connection that was checked. + pub fn connection(&self) -> &WitnessConnection { + self.pending.connection() + } + + /// Witness retention promise, checked against the full pending manifest requirement. + pub fn retention_until(&self) -> u32 { + self.retention_until + } +} diff --git a/lightning-ffor/src/witness/manifest.rs b/lightning-ffor/src/witness/manifest.rs new file mode 100644 index 00000000000..de0206023c9 --- /dev/null +++ b/lightning-ffor/src/witness/manifest.rs @@ -0,0 +1,212 @@ +use alloc::vec::Vec; + +use bitcoin::locktime::absolute::LOCK_TIME_THRESHOLD; +use bitcoin::secp256k1::PublicKey; + +use crate::setup::AuthenticatedSetup; +use crate::transcript::{self, Digest}; + +use super::{Reader, WitnessError, MAX_MESSAGE_LEN, RETENTION_MARGIN_BLOCKS}; + +// The manifest must also fit inside type + request_id + manifest. +const MAX_MANIFEST_LEN: usize = MAX_MESSAGE_LEN - 2 - 16; + +/// Public receiver-selected provisioning fields, with no secret key material. +/// +/// The engine supplies the already-validated commitment digest and activation height. The +/// caller generates fresh mailbox/fetch identities per witness and epoch, and an encryption +/// key for the epoch. This type cannot establish freshness, actual activation or persistence. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ManifestParameters { + /// Random, unlinkable 32-byte mailbox identity for this witness and epoch. + pub mailbox_id: [u8; 32], + /// H_commit from the engine's verified, fully signed voucher commitments. + pub commitment_hash: Digest, + /// Height bound into the epoch's signed activation. + pub epoch_start_height: u32, + /// Per-witness/per-epoch key authorizing the manifest, fetch and close requests. + pub fetch_public_key: PublicKey, + /// Public key to which the witness encrypts records; its private half stays local. + pub encryption_public_key: PublicKey, + /// Block height through which the witness must retain every record. + pub retention_until: u32, + /// Requested guardian receipt count; zero requests local witness durability only. + pub minimum_receipts: u8, +} + +/// Canonical manifest bytes awaiting an externally produced fetch-key signature. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UnsignedManifest { + parameters: ManifestParameters, + setup_hash: Digest, + activation_hash: Digest, + book: Vec, +} + +impl UnsignedManifest { + /// Build a version 1/D-R manifest from an existing authenticated canonical setup. + /// + /// The caller must separately match the commitment digest and height to its persisted ACTIVE + /// epoch. Recomputing H_act here does not establish channel-state or storage authority. + pub fn new( + setup: &AuthenticatedSetup, parameters: ManifestParameters, + ) -> Result { + let terms = setup.terms(); + if terms.voucher_expiry >= LOCK_TIME_THRESHOLD { + return Err(WitnessError::Height); + } + let minimum_retention = terms + .voucher_expiry + .checked_add(RETENTION_MARGIN_BLOCKS) + .ok_or(WitnessError::Retention)?; + if parameters.retention_until < minimum_retention { + return Err(WitnessError::Retention); + } + if parameters.epoch_start_height >= terms.settlement_deadline { + return Err(WitnessError::Height); + } + let setup_hash = setup.setup_hash(); + let activation_hash = transcript::activation_hash( + &setup_hash, + &setup.book_hash(), + ¶meters.commitment_hash, + parameters.epoch_start_height, + ); + Ok(Self { parameters, setup_hash, activation_hash, book: setup.canonical_book().to_vec() }) + } + + /// Public provisioning fields that will be bound by the fetch-key signature. + pub fn parameters(&self) -> &ManifestParameters { + &self.parameters + } + + /// T_setup already authenticated by the channel's receiver and settlement keys. + pub fn setup_hash(&self) -> Digest { + self.setup_hash + } + + /// H_act recomputed from this setup, book, commitment digest and activation height. + pub fn activation_hash(&self) -> Digest { + self.activation_hash + } + + /// Exact shared section 7.5.3 book, with no independently reconstructed entries. + pub fn canonical_book(&self) -> &[u8] { + &self.book + } + + /// Exact section 9.6.4 manifest bytes, excluding only its final signature. + pub fn unsigned_bytes(&self) -> Vec { + let p = &self.parameters; + let mut bytes = Vec::with_capacity(207 + self.book.len()); + bytes.extend_from_slice(&[1, 1]); + bytes.extend_from_slice(&p.mailbox_id); + bytes.extend_from_slice(&self.setup_hash); + bytes.extend_from_slice(&p.commitment_hash); + bytes.extend_from_slice(&p.epoch_start_height.to_be_bytes()); + bytes.extend_from_slice(&self.activation_hash); + bytes.extend_from_slice(&p.fetch_public_key.serialize()); + bytes.extend_from_slice(&p.encryption_public_key.serialize()); + bytes.extend_from_slice(&p.retention_until.to_be_bytes()); + bytes.push(p.minimum_receipts); + // AuthenticatedSetup restricts books to 483 fixed-size entries, well below u16::MAX. + bytes.extend_from_slice(&(self.book.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&self.book); + bytes + } + + /// Single SHA256 over `ffor/witness/manifest` and the exact unsigned manifest. + /// + /// This is a fetch-key domain, not the `ffor/msg` node-key domain. No wire message type or + /// request identifier is included. An external signer retains the private key. + pub fn signing_digest(&self) -> Digest { + transcript::hash_parts(b"ffor/witness/manifest", &[&self.unsigned_bytes()]) + } + + /// Authenticate an external compact low-S signature without signing or exporting keys. + pub fn authenticate(self, signature: [u8; 64]) -> Result { + transcript::verify_digest_signature( + self.signing_digest(), + &signature, + &self.parameters.fetch_public_key, + )?; + Ok(SignedManifest { manifest: self, signature }) + } +} + +/// Immutable canonical manifest whose signature and setup binding were checked. +/// +/// This authenticates fetch-key control only. A witness does not learn the receiver's node key +/// from the manifest, and this object cannot prove real commitment state or durable activation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignedManifest { + manifest: UnsignedManifest, + signature: [u8; 64], +} + +impl SignedManifest { + /// Decode against the receiver's authenticated setup and verify every signed byte. + /// + /// No trailing fields are defined for version 1. The entire canonical book must equal the + /// supplied setup's book; this decoder never trusts a separately supplied list of vouchers. + pub fn decode(bytes: &[u8], setup: &AuthenticatedSetup) -> Result { + if bytes.len() > MAX_MANIFEST_LEN { + return Err(WitnessError::SizeLimit); + } + let mut reader = Reader(bytes); + if reader.byte()? != 1 { + return Err(WitnessError::Version); + } + if reader.byte()? != 1 { + return Err(WitnessError::Profile); + } + let mailbox_id = reader.array()?; + let setup_hash = reader.array()?; + let commitment_hash = reader.array()?; + let epoch_start_height = reader.u32()?; + let activation_hash = reader.array()?; + let fetch_public_key = reader.public_key()?; + let encryption_public_key = reader.public_key()?; + let retention_until = reader.u32()?; + let minimum_receipts = reader.byte()?; + let length = reader.u16()? as usize; + if reader.take(length)? != setup.canonical_book() { + return Err(WitnessError::Book); + } + let signature = reader.array()?; + reader.finish()?; + let manifest = UnsignedManifest::new( + setup, + ManifestParameters { + mailbox_id, + commitment_hash, + epoch_start_height, + fetch_public_key, + encryption_public_key, + retention_until, + minimum_receipts, + }, + )?; + if manifest.setup_hash != setup_hash || manifest.activation_hash != activation_hash { + return Err(WitnessError::Transcript); + } + manifest.authenticate(signature) + } + + /// The immutable validated fields and exact canonical unsigned encoding. + pub fn unsigned(&self) -> &UnsignedManifest { + &self.manifest + } + + /// Original compact low-S fetch-key signature, in fixed 64-byte form. + pub fn signature(&self) -> &[u8; 64] { + &self.signature + } + + /// Exact canonical manifest including the original signature, suitable for durable retries. + pub fn encode(&self) -> Vec { + let mut bytes = self.manifest.unsigned_bytes(); + bytes.extend_from_slice(&self.signature); + bytes + } +} diff --git a/lightning-ffor/src/witness/messages.rs b/lightning-ffor/src/witness/messages.rs new file mode 100644 index 00000000000..9b29cbf6759 --- /dev/null +++ b/lightning-ffor/src/witness/messages.rs @@ -0,0 +1,148 @@ +use alloc::vec::Vec; + +use bitcoin::secp256k1::PublicKey; + +use crate::setup::AuthenticatedSetup; + +use super::{ + Reader, SignedManifest, WitnessError, ACK_MESSAGE_TYPE, MAX_MESSAGE_LEN, PROVISION_MESSAGE_TYPE, +}; + +const MAX_REFUSAL_BYTES: usize = MAX_MESSAGE_LEN - 2 - 16 - 1 - 2; + +/// Immutable `ff_witness_provision`: one request identity and its exact signed manifest. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Provision { + request_id: [u8; 16], + manifest: SignedManifest, +} + +impl Provision { + /// Associate a freshly allocated request ID with an already authenticated manifest. + /// + /// Keep this exact value through uncertain send results. The caller must persist its manifest + /// and protected recovery keys before transport. Constructing a request performs no I/O. + pub fn new(request_id: [u8; 16], manifest: SignedManifest) -> Self { + Self { request_id, manifest } + } + + /// Decode the complete BOLT 8 plaintext, including type, against the receiver's own setup. + pub fn decode(bytes: &[u8], setup: &AuthenticatedSetup) -> Result { + if bytes.len() > MAX_MESSAGE_LEN { + return Err(WitnessError::SizeLimit); + } + let mut reader = Reader(bytes); + if reader.u16()? != PROVISION_MESSAGE_TYPE { + return Err(WitnessError::MessageType); + } + let request_id = reader.array()?; + let manifest = SignedManifest::decode(reader.0, setup)?; + Ok(Self { request_id, manifest }) + } + + /// Canonical type, request identity and exact signed manifest bytes. + pub fn encode(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&PROVISION_MESSAGE_TYPE.to_be_bytes()); + bytes.extend_from_slice(&self.request_id); + bytes.extend_from_slice(&self.manifest.encode()); + bytes + } + + /// Request identity that an acknowledgement must echo exactly. + pub fn request_id(&self) -> [u8; 16] { + self.request_id + } + + /// Immutable manifest whose acknowledgement is being requested. + pub fn manifest(&self) -> &SignedManifest { + &self.manifest + } +} + +/// The witness's unsigned response, requiring authenticated transport correlation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AcknowledgementResult { + /// Claimed witness identity and record retention promise. + Accepted { + /// Must equal both the requested witness and the actual authenticated response peer. + witness: PublicKey, + /// Must be at least the pending manifest's retention promise. + retention_until: u32, + }, + /// Bounded opaque diagnostic bytes. + Refused(Vec), +} + +/// Canonical `ff_witness_ack`, which alone proves neither identity nor storage. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Acknowledgement { + request_id: [u8; 16], + result: AcknowledgementResult, +} + +impl Acknowledgement { + /// Construct a bounded response; success identity is still an unauthenticated claim here. + pub fn new(request_id: [u8; 16], result: AcknowledgementResult) -> Result { + if matches!(&result, AcknowledgementResult::Refused(error) if error.len() > MAX_REFUSAL_BYTES) + { + return Err(WitnessError::SizeLimit); + } + Ok(Self { request_id, result }) + } + + /// Decode complete plaintext with a strict 0/1 result and no trailing extension fields. + pub fn decode(bytes: &[u8]) -> Result { + if bytes.len() > MAX_MESSAGE_LEN { + return Err(WitnessError::SizeLimit); + } + let mut reader = Reader(bytes); + if reader.u16()? != ACK_MESSAGE_TYPE { + return Err(WitnessError::MessageType); + } + let request_id = reader.array()?; + let result = match reader.byte()? { + 1 => AcknowledgementResult::Accepted { + witness: reader.public_key()?, + retention_until: reader.u32()?, + }, + 0 => { + let length = reader.u16()? as usize; + AcknowledgementResult::Refused(reader.take(length)?.to_vec()) + }, + _ => return Err(WitnessError::NonCanonical), + }; + reader.finish()?; + Self::new(request_id, result) + } + + /// Canonical acknowledgement, including its two-byte type. + pub fn encode(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&ACK_MESSAGE_TYPE.to_be_bytes()); + bytes.extend_from_slice(&self.request_id); + match &self.result { + AcknowledgementResult::Accepted { witness, retention_until } => { + bytes.push(1); + bytes.extend_from_slice(&witness.serialize()); + bytes.extend_from_slice(&retention_until.to_be_bytes()); + }, + AcknowledgementResult::Refused(error) => { + bytes.push(0); + bytes.extend_from_slice(&(error.len() as u16).to_be_bytes()); + bytes.extend_from_slice(error); + }, + } + bytes + } + + /// The response correlation identifier, not part of a signature. + pub fn request_id(&self) -> [u8; 16] { + self.request_id + } + + /// The untrusted success claim or bounded refusal. + pub fn result(&self) -> &AcknowledgementResult { + &self.result + } +} diff --git a/lightning-ffor/src/witness/mod.rs b/lightning-ffor/src/witness/mod.rs new file mode 100644 index 00000000000..9f2e2714058 --- /dev/null +++ b/lightning-ffor/src/witness/mod.rs @@ -0,0 +1,131 @@ +//! Receiver-side D-R manifest and provisioning primitives from section 9.6 and Appendix F.1. +//! +//! A manifest proves fetch-key control and agreement with a supplied authenticated setup. A +//! checked acknowledgement only correlates a response with one immutable provisioning request. +//! Neither value proves live commitment state, durable storage, ACTIVE, or invoice readiness. +//! The caller owns authenticated transport, signing, protected key storage and persistence. +//! +//! Version 1/profile 1 manifests and the two provisioning messages define no extension stream. +//! Decoders reject every trailing byte, unknown version/profile, and noncanonical success flag. +//! Refusal text is bounded opaque bytes and need not be UTF-8. +//! +//! ``` +//! use lightning_ffor::witness::{Acknowledgement, AcknowledgementResult}; +//! let refusal = Acknowledgement::new( +//! [1; 16], AcknowledgementResult::Refused(b"capacity unavailable".to_vec()), +//! ).unwrap(); +//! assert_eq!(Acknowledgement::decode(&refusal.encode()).unwrap(), refusal); +//! ``` + +use core::fmt; + +use bitcoin::secp256k1::PublicKey; + +use crate::transcript::SignatureError; + +mod correlation; +mod manifest; +mod messages; + +pub use correlation::{CheckedAcknowledgement, PendingProvision, WitnessConnection}; +pub use manifest::{ManifestParameters, SignedManifest, UnsignedManifest}; +pub use messages::{Acknowledgement, AcknowledgementResult, Provision}; + +/// Appendix F.1 receiver-to-witness provisioning message, including its two-byte type. +pub const PROVISION_MESSAGE_TYPE: u16 = 55055; +/// Appendix F.1 witness-to-receiver provisioning acknowledgement. +pub const ACK_MESSAGE_TYPE: u16 = 55057; +/// Minimum promised record retention beyond voucher expiry, in blocks. +pub const RETENTION_MARGIN_BLOCKS: u32 = 144; +/// BOLT 8's plaintext limit, including the two-byte message type. +pub const MAX_MESSAGE_LEN: usize = crate::wire::MAX_MESSAGE_LEN; + +/// A malformed manifest/message or a response that cannot acknowledge this pending request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WitnessError { + /// The input ends before a declared field is complete. + Truncated, + /// A message exceeds the bounded BOLT 8 envelope. + SizeLimit, + /// The wire type is outside the two supported provisioning messages. + MessageType, + /// Only manifest version 1 is supported. + Version, + /// Only the D-R profile, value 1, is supported. + Profile, + /// An unsupported trailing field or noncanonical flag was encountered. + NonCanonical, + /// A compressed secp256k1 public key is invalid. + PublicKey, + /// The supplied fetch-key signature does not authenticate the exact manifest. + Signature(SignatureError), + /// The manifest differs from the authenticated setup's canonical book. + Book, + /// The setup or recomputed activation digest differs. + Transcript, + /// Activation is not before admission closes, or voucher expiry is timestamp-style. + Height, + /// Retention is too short or its required lower bound overflows. + Retention, + /// This response echoes a different pending request identifier. + Request, + /// This response arrived on a different authenticated transport connection. + Connection, + /// The actual response peer or acknowledgement names another witness. + Witness, + /// The witness explicitly refused the provisioning request. + Refused, +} + +impl fmt::Display for WitnessError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid FFOR witness provisioning: {self:?}") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for WitnessError {} + +impl From for WitnessError { + fn from(error: SignatureError) -> Self { + Self::Signature(error) + } +} + +struct Reader<'a>(&'a [u8]); + +impl<'a> Reader<'a> { + fn take(&mut self, length: usize) -> Result<&'a [u8], WitnessError> { + let value = self.0.get(..length).ok_or(WitnessError::Truncated)?; + self.0 = &self.0[length..]; + Ok(value) + } + + fn array(&mut self) -> Result<[u8; N], WitnessError> { + self.take(N)?.try_into().map_err(|_| WitnessError::Truncated) + } + + fn byte(&mut self) -> Result { + Ok(self.array::<1>()?[0]) + } + + fn u16(&mut self) -> Result { + Ok(u16::from_be_bytes(self.array()?)) + } + + fn u32(&mut self) -> Result { + Ok(u32::from_be_bytes(self.array()?)) + } + + fn public_key(&mut self) -> Result { + PublicKey::from_slice(self.take(33)?).map_err(|_| WitnessError::PublicKey) + } + + fn finish(self) -> Result<(), WitnessError> { + if self.0.is_empty() { + Ok(()) + } else { + Err(WitnessError::NonCanonical) + } + } +} diff --git a/lightning-ffor/tests/data/beignet-witness.json b/lightning-ffor/tests/data/beignet-witness.json new file mode 100644 index 00000000000..caf701a7672 --- /dev/null +++ b/lightning-ffor/tests/data/beignet-witness.json @@ -0,0 +1,65 @@ +{ + "source_revision": "8aee31d18e596fe49a0d195b325a6e757d7a009b", + "specification_revision": "d719161f42d1eeb6bd6c3856d564222f03c2205e", + "source_file": "src/lightning/ffor/witness-messages.ts", + "appendix_d_sha256": "beaf507dcde419fb10b3611babb3344616712b467de4b6decbfb9f8b7b3c9be9", + "fetch_public_key": "035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c", + "encryption_public_key": "028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60", + "witness_public_key": "02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2", + "fixtures": [ + { + "scenario": "D.1", + "unsigned": "01010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0bba34be8afe89e4457543a6e7279f86054f1d6e9038234b786b740e9c905c001f723024696ad20b4758b228284620d18fd30750b7051cc64efca23a1417cf1321000c0df0580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e716035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c359000005efc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff15040100010001a0a62adc5092cd3c202aaea0900b5826c242194c5f9984070e833e58b273eda500000000000f4240000c3500000c31100000000000000000", + "digest": "74d917acf7990b4651b9a1f6897b53bf4f6ecca6e828e0793168dab9e9f1b315", + "manifest": "01010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0bba34be8afe89e4457543a6e7279f86054f1d6e9038234b786b740e9c905c001f723024696ad20b4758b228284620d18fd30750b7051cc64efca23a1417cf1321000c0df0580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e716035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c359000005efc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff15040100010001a0a62adc5092cd3c202aaea0900b5826c242194c5f9984070e833e58b273eda500000000000f4240000c3500000c3110000000000000000068c460b699c82f1694ff36a6e872b82c566d529e13c49f63dd6db9217279044f6e5b46f06c9abf252aec4701b251aa871de7c4dfe48d0eb3b2c84e96a2fb7753", + "provision": "d70f0101010101010101010101010101010101010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0bba34be8afe89e4457543a6e7279f86054f1d6e9038234b786b740e9c905c001f723024696ad20b4758b228284620d18fd30750b7051cc64efca23a1417cf1321000c0df0580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e716035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c359000005efc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff15040100010001a0a62adc5092cd3c202aaea0900b5826c242194c5f9984070e833e58b273eda500000000000f4240000c3500000c3110000000000000000068c460b699c82f1694ff36a6e872b82c566d529e13c49f63dd6db9217279044f6e5b46f06c9abf252aec4701b251aa871de7c4dfe48d0eb3b2c84e96a2fb7753", + "acknowledgement": "d711010101010101010101010101010101010102bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2000c3590", + "refusal": "d7110101010101010101010101010101010100000e63616e6e6f742072657365727665" + }, + { + "scenario": "D.2", + "unsigned": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c8465b03bd1ab3001129cd91fffb6e5129460cf82fb396cad2ebbb0ede8916396e7e996cb809a52736589d78d22b1a7c2a76b2ba07a8ac1f957aabc2651b0b9c5000c0df01d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c35900000d22436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d604010003000124e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd99300000000000f2ad0000c3500000c311000000000000000070002a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf70713259600000000000855ca000c3500000c311000000000000000080003934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f60000000002f71c08000c3500000c31100000000000000009", + "digest": "1f714d4da0de1aacaec6d26736cea6216f3edb59ec9509a457b5ade72b76ac81", + "manifest": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c8465b03bd1ab3001129cd91fffb6e5129460cf82fb396cad2ebbb0ede8916396e7e996cb809a52736589d78d22b1a7c2a76b2ba07a8ac1f957aabc2651b0b9c5000c0df01d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c35900000d22436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d604010003000124e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd99300000000000f2ad0000c3500000c311000000000000000070002a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf70713259600000000000855ca000c3500000c311000000000000000080003934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f60000000002f71c08000c3500000c31100000000000000009ab2800d9b36f24db717e37c55b739581b3697a6b298520240117cbc6f71130dd07dbdd77a581b1804cd3db0c1f656aa75ea6326f08fafeef1a4460f70f0a84a0", + "provision": "d70f0202020202020202020202020202020201010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c8465b03bd1ab3001129cd91fffb6e5129460cf82fb396cad2ebbb0ede8916396e7e996cb809a52736589d78d22b1a7c2a76b2ba07a8ac1f957aabc2651b0b9c5000c0df01d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c35900000d22436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d604010003000124e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd99300000000000f2ad0000c3500000c311000000000000000070002a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf70713259600000000000855ca000c3500000c311000000000000000080003934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f60000000002f71c08000c3500000c31100000000000000009ab2800d9b36f24db717e37c55b739581b3697a6b298520240117cbc6f71130dd07dbdd77a581b1804cd3db0c1f656aa75ea6326f08fafeef1a4460f70f0a84a0", + "acknowledgement": "d711020202020202020202020202020202020102bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2000c3590", + "refusal": "d7110202020202020202020202020202020200000e63616e6e6f742072657365727665" + }, + { + "scenario": "D.3", + "unsigned": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d43bce577aa29c966a8c9a8bc614a9edfc4994f8ee4954414bc635cb6dcd745ffc034dadab45e153536220113ba43e4aeaf2a87391cda1fa33b9d7d89472ce5ad000c0df0bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c35900000d28685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecb04010003000121f2705152e9753f5d63b5a0bdbb1d3cd0da4d56d83e3d3853cd44c12dfe6f8400000000000f2ad0000c3500000c31100000000000000000000258bdb1620d8f37d85a846d3f80a4e43ca084cbe1743d043d429de8dcfb0843c000000000000855ca000c3500000c311000000000000000010003dfe77b118b59852036e38b7409770e502899c35e8aced4a5d405ebecc167d0ea0000000002f71c08000c3500000c31100000000000000002", + "digest": "db7abfb63cb2b1867636cae55d6df0e66c2b3c3dddd3a5258cfe03d3da656060", + "manifest": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d43bce577aa29c966a8c9a8bc614a9edfc4994f8ee4954414bc635cb6dcd745ffc034dadab45e153536220113ba43e4aeaf2a87391cda1fa33b9d7d89472ce5ad000c0df0bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c35900000d28685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecb04010003000121f2705152e9753f5d63b5a0bdbb1d3cd0da4d56d83e3d3853cd44c12dfe6f8400000000000f2ad0000c3500000c31100000000000000000000258bdb1620d8f37d85a846d3f80a4e43ca084cbe1743d043d429de8dcfb0843c000000000000855ca000c3500000c311000000000000000010003dfe77b118b59852036e38b7409770e502899c35e8aced4a5d405ebecc167d0ea0000000002f71c08000c3500000c311000000000000000021b65e81359a7e634cfbd376ca2d78e801577cad0c8282c4012b585d2ed715dc319771afcd946f75c6fda619be90040726b863a718d928b9bb39c92e30f5e6d55", + "provision": "d70f0303030303030303030303030303030301010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d43bce577aa29c966a8c9a8bc614a9edfc4994f8ee4954414bc635cb6dcd745ffc034dadab45e153536220113ba43e4aeaf2a87391cda1fa33b9d7d89472ce5ad000c0df0bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c35900000d28685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecb04010003000121f2705152e9753f5d63b5a0bdbb1d3cd0da4d56d83e3d3853cd44c12dfe6f8400000000000f2ad0000c3500000c31100000000000000000000258bdb1620d8f37d85a846d3f80a4e43ca084cbe1743d043d429de8dcfb0843c000000000000855ca000c3500000c311000000000000000010003dfe77b118b59852036e38b7409770e502899c35e8aced4a5d405ebecc167d0ea0000000002f71c08000c3500000c311000000000000000021b65e81359a7e634cfbd376ca2d78e801577cad0c8282c4012b585d2ed715dc319771afcd946f75c6fda619be90040726b863a718d928b9bb39c92e30f5e6d55", + "acknowledgement": "d711030303030303030303030303030303030102bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2000c3590", + "refusal": "d7110303030303030303030303030303030300000e63616e6e6f742072657365727665" + }, + { + "scenario": "D.4", + "unsigned": "01010e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e29b3ef77eec90177a34bf4d9785afd1fd6ffb2a28bad4ed571f295fe711c35a906602b655a1dbeb1059e87e9f8c0fefd89bed2a6501a5df25724e54d1c2e56fe000c0df0982ee14fdd19b21558b89adda15b53f54122fc1c5a72bae5ef4261ee154cc776035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c359000005e87cb2b4c1f4e6b9246bc30f7a7cb145dff0ad8b75153061fe08a31344ec15a0d040100010001ec5abb4d6d027af6f9bc6451f6cc98d95ad28fb9c20d67d3e51cb30614c15a6e00000000000854d0000c3500000c31100000000000000000", + "digest": "07d800e99706fa7dc8b9c37db277ddb61a3c536166ff0fdccbc7cbeb1bd48998", + "manifest": "01010e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e29b3ef77eec90177a34bf4d9785afd1fd6ffb2a28bad4ed571f295fe711c35a906602b655a1dbeb1059e87e9f8c0fefd89bed2a6501a5df25724e54d1c2e56fe000c0df0982ee14fdd19b21558b89adda15b53f54122fc1c5a72bae5ef4261ee154cc776035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c359000005e87cb2b4c1f4e6b9246bc30f7a7cb145dff0ad8b75153061fe08a31344ec15a0d040100010001ec5abb4d6d027af6f9bc6451f6cc98d95ad28fb9c20d67d3e51cb30614c15a6e00000000000854d0000c3500000c311000000000000000003a2a3a91c28c313cd2b96ca91e155e985f287729ce9adf284215604af48550c13a7cabe77d552dd23180a8b7ac1844eadc064f1b1c499fc0944e3c74de265f80", + "provision": "d70f0404040404040404040404040404040401010e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e29b3ef77eec90177a34bf4d9785afd1fd6ffb2a28bad4ed571f295fe711c35a906602b655a1dbeb1059e87e9f8c0fefd89bed2a6501a5df25724e54d1c2e56fe000c0df0982ee14fdd19b21558b89adda15b53f54122fc1c5a72bae5ef4261ee154cc776035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c359000005e87cb2b4c1f4e6b9246bc30f7a7cb145dff0ad8b75153061fe08a31344ec15a0d040100010001ec5abb4d6d027af6f9bc6451f6cc98d95ad28fb9c20d67d3e51cb30614c15a6e00000000000854d0000c3500000c311000000000000000003a2a3a91c28c313cd2b96ca91e155e985f287729ce9adf284215604af48550c13a7cabe77d552dd23180a8b7ac1844eadc064f1b1c499fc0944e3c74de265f80", + "acknowledgement": "d711040404040404040404040404040404040102bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2000c3590", + "refusal": "d7110404040404040404040404040404040400000e63616e6e6f742072657365727665" + }, + { + "scenario": "D.5", + "unsigned": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0fe012e565964e65d584adbc7138f1d115f6e00c27a90903e6c1b1657982fe5cff61034010d3510c0a44a53e0ef4a086226bfa039708f944ce4da9833f1ad5e5f3000c0df07d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a233365035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c3590006d9257d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da6040101e300017305b11a874bbd995b29b11d109eff432402136904a3bab1f7bd9f869109cfc100000000000854d0000c3500000c311000000000000000000002cbf141d9ad3b41938c25083328862acf711291bb47ab53b33dc37898ef9326b400000000000854d0000c3500000c31100000000000000001000362e42ded68912761a397087a1310af972f9022330fbbbf24c580b9e71479fd8700000000000854d0000c3500000c311000000000000000020004754bccfbb8e3ee44224e0290582ada7cb1dcb05ad968df63ac0aa01e81298f1b00000000000854d0000c3500000c3110000000000000000300057c23775d75a9e3e878e5e33ba9cf7da51c78a4d54431bc1981c8524eda36bb0d00000000000854d0000c3500000c311000000000000000040006bcd5fb4815309c1de8fbc2867eb93635d8764c565d952917457874fc6eceda1800000000000854d0000c3500000c311000000000000000050007e3ae54a63b8f9526f7955db3417b5a1f6743b99ad0ca7cc92c3fb199c149b76800000000000854d0000c3500000c311000000000000000060008bd471262b242310a4d1b298156a5fc3469395eb2fa3d28be02b88d8d0c97081100000000000854d0000c3500000c311000000000000000070009397b027bfa95da000fac9dac5d41825bb54a0cd0071a262be00d6d4445f3e2e900000000000854d0000c3500000c31100000000000000008000adb17a978e9e05effc0d7f4c33f7fb78898deec1a0b44212bf943818091fbc09e00000000000854d0000c3500000c31100000000000000009000b0fc65290b128e63d598e654d4a7bbe14cc3ef946f6fb6fe0d5b46cb9e0e2638800000000000854d0000c3500000c3110000000000000000a000c3f951c45254175c92549460b5a89d5d3c61a950e7e366f992dac9f7f7a7e6ce700000000000854d0000c3500000c3110000000000000000b000dfa9d083023788850024c7eb588fae1c7542604d23cc12f44ddd568c2320a754f00000000000854d0000c3500000c3110000000000000000c000e3aa43187fba488b5530dbc366257f82058801f6983509716a9e9bf00db680f1800000000000854d0000c3500000c3110000000000000000d000f7404ba142c5ad30fb6769e7f6a84843d4c437bd50254a72815c8b75c1deb0c4b00000000000854d0000c3500000c3110000000000000000e001028ced94a2fc573f8c251838edfc807be39d1b910d18ede82aa7a127c5888b3a300000000000854d0000c3500000c3110000000000000000f0011a6a8f52194b94c6d0b521756e5c2c20ab5b6da8a2483e44aeb48447112c0680400000000000854d0000c3500000c3110000000000000001000122014e1e38afe84f6da72dbab22108ec05f60555504bac35bbfc7c62d1ac3621100000000000854d0000c3500000c31100000000000000011001313667fb8817a03dd2f5fd97325ffaec561f597256d58742291c569ee251257fb00000000000854d0000c3500000c31100000000000000012001430ec738e4babdd6d0b18b82a7f5a7415074cd283260197c4efe5d439671c9abc00000000000854d0000c3500000c3110000000000000001300153e9e411c4bcb405bda68f31ef974cf91e5dc7a530fa8580048c4bc742805125a00000000000854d0000c3500000c3110000000000000001400167aacb62cd9c823ddf004ca04fb936c3093029ddceaef569190534039885b1b8d00000000000854d0000c3500000c311000000000000000150017a9e7ea66fc54f78b5a4441c5ca6ebaa742485894509dc379783c52d881fad9e400000000000854d0000c3500000c311000000000000000160018063c74693de53572427dd71747f7a47f95a72b83b95f62b60be7479516593c5300000000000854d0000c3500000c311000000000000000170019454a89ae1311664f5155c45544265e5b33911a120db3b683522ac590e64528b200000000000854d0000c3500000c31100000000000000018001adb1d9b5644e6d6b7befb0f222e567bef23ec977fd3d5a6f146b6b195f48384e400000000000854d0000c3500000c31100000000000000019001b54755b708745f22c50b1ded9d0f24bc9b87d93d95046accde5e0d0c1b27ee3a000000000000854d0000c3500000c3110000000000000001a001c1b29bd0c8af340c2ede7c9552fd9410b4c731a47156f54ae2fe2ef6ab170a3f900000000000854d0000c3500000c3110000000000000001b001d3b68781e8a122e02fdbade74d727eedafc80663a79c32726b7d555e730f6b00c00000000000854d0000c3500000c3110000000000000001c001e508a679821f8e0b736552664ffd03360eb8e08acc5ac5c653ce280885222ca8000000000000854d0000c3500000c3110000000000000001d001f599fd0cf5ce9487aae5390f0aa7e2f55bbf36c7fab020879c262c74f0659b80d00000000000854d0000c3500000c3110000000000000001e00201d35e8a88f39201d36bb5d8603822ee851e4f936cc9d941eda8f0c132be674ed00000000000854d0000c3500000c3110000000000000001f00218b0edea50f2efd820966577036831199400b94a6a5ddc1efde5caa77d770fd9300000000000854d0000c3500000c3110000000000000002000228267950cb8f56aee3a11f312d601c4e2f49e229db76eebe9df97b467478fe80400000000000854d0000c3500000c3110000000000000002100233e52b8a3ff4d5f870fa11ba4e1a7ffc5f35310840f33f01da137e5802ad25db100000000000854d0000c3500000c311000000000000000220024bb7507ed8a6155c9d7d58e9798f1822e143da862250702f237267f91bf1f6bc300000000000854d0000c3500000c3110000000000000002300253dbe9c22ae8f8733c3a60d21c30bf7e51a304f53bac7c4a2c6e3cbe61157679500000000000854d0000c3500000c31100000000000000024002698a89d04d4471120348d82a09c6588128fe927d38dfe26d98932961fc842764000000000000854d0000c3500000c31100000000000000025002736a20a0e1e99e0e8f0f9d389acb4ce07130e58276e847f52bf8d17c2bf36fc3300000000000854d0000c3500000c311000000000000000260028f5d5256f0c02a8e76d9de795b0a9a6624e694f2df4d106bfe32fa46b60400a3300000000000854d0000c3500000c31100000000000000027002908853d9812b63820a7c738e5fc15ab6181cd391a059c248b700657d4b21b4e5d00000000000854d0000c3500000c31100000000000000028002ab49ad03151b291227dc42af60dd203c373a9533afdc9875f4c02d08560c4533f00000000000854d0000c3500000c31100000000000000029002bdc43a956e852c354169466af263dd38c4f278c18fb54d209d9b65ef1828cbec200000000000854d0000c3500000c3110000000000000002a002c1c3da0878d813c406978c150f03595e9a26d582ad426d14ef279edd0e8f452fb00000000000854d0000c3500000c3110000000000000002b002ddccf935988fc435b5713186e04f48e53fe514466473a48ad5e0afad0d22fe46400000000000854d0000c3500000c3110000000000000002c002ee113b733aec9e1d33dfd95556e546977a6b907fda9164c86f41dff868d4a2cc300000000000854d0000c3500000c3110000000000000002d002ff6fd342210a6de76e74742ecb52b84f482aeed8d2e0b0a843cd8167b4554f5d600000000000854d0000c3500000c3110000000000000002e0030882620e0b4f1feee1c51202c3b9623135d41499777d1341f63730f98dad4702c00000000000854d0000c3500000c3110000000000000002f0031ba08838987d86618a0dc670de584e3cb9602dd2f60278ea817a31d7f311e634d00000000000854d0000c3500000c3110000000000000003000323f4fce46408992a463e8d153537aa72ed50c22cd6c76648191a9cb4b43de2b9b00000000000854d0000c3500000c3110000000000000003100332f030755b807abfb879b08abe7a8b15adcd9efd2f419214fa5d02d099fda961000000000000854d0000c3500000c3110000000000000003200346918f0ebfa087f6b72399aca062f7bde3577c44fd288090b9ff2718fc6f5cc8100000000000854d0000c3500000c311000000000000000330035f70d71f8c06e27b949b91c969119a7cb85a4779bef0397a4d2a6847b49d128dd00000000000854d0000c3500000c31100000000000000034003631f229a547af63a7d20a9603285eb3793ed667b25f4a22bd8cd94344665f31a800000000000854d0000c3500000c311000000000000000350037d872bb09a3b355b08b3bf759b436cbc36f6d645fc01dccf919454235b8cf509b00000000000854d0000c3500000c3110000000000000003600387bf97f96e2d82c0396dd60d8a32ca53a9034f4fafb590a6f8b3f02e3d16a26bd00000000000854d0000c3500000c3110000000000000003700391d44995dc923077293887ec5dab71deac2055a37fdf727d5a2c0d216bbb5a25f00000000000854d0000c3500000c31100000000000000038003a6fc5c81c3b0c8b25375f88a324a38ad92d72a73d2771886704e24ed7ab53595100000000000854d0000c3500000c31100000000000000039003b64a7f4d976600e0d3670d51fd1f1a08937a5247cd7314e78fc85a6c692cee5b100000000000854d0000c3500000c3110000000000000003a003c304c70efe0cdf3be30823623953ef905d37831a080ae3ae1d0c261f7921ab76700000000000854d0000c3500000c3110000000000000003b003d52f4c9863d3ae76c166247d9225b19a416ad39f7f46ff197bc95f695201dafe600000000000854d0000c3500000c3110000000000000003c003e76fbeb3ff3afa2700541928f23f11d59f8a56d40abd6b10086d70ff568c1b39d00000000000854d0000c3500000c3110000000000000003d003f83a811fe4aa924595c69a33b636f0410f8da6c59683048f6a63d011eecd0fc7400000000000854d0000c3500000c3110000000000000003e0040ab8a8b2d7759b68431f7539de4e7c40fcf22a15cbb3d472dd20a31d690bd696000000000000854d0000c3500000c3110000000000000003f0041c1305b0e61a9ea2da6d693234a815de4e712aa7a38fecf60291bb0e0af44276a00000000000854d0000c3500000c311000000000000000400042434b51045e65ddc0caf75b30495e146c0076fc28951d7396a23ecff1690e2f8a00000000000854d0000c3500000c3110000000000000004100439df059ca0da40a29d8afeebcfcd02cefa7cdef598a2673065f7d55b6617884c000000000000854d0000c3500000c311000000000000000420044ca418d14c97d01e4c0aad582a5174c09a21638342681bbcdf02ef3ff5f56cab200000000000854d0000c3500000c31100000000000000043004537594556a4f9feebc7b9efaf166d36bc7011be23886a2c89e590f9ad2a4665a500000000000854d0000c3500000c311000000000000000440046ad51420b09a07509e900aaa4fcc6cf6bace255fef4d89c31727806446edbba5100000000000854d0000c3500000c311000000000000000450047c733ad0dee32d9a52a4d62c9c96186e1afd55d35f54ddc21fa980f771e17fd7f00000000000854d0000c3500000c311000000000000000460048464b71d7e34e559d518b268791aa82b54cec4fc2aefa34cc66fe9f21eb90948a00000000000854d0000c3500000c311000000000000000470049cc2d776c5d23a365ffcb4604ee65a09cce259c871dab39b04939d4f3d2252a3d00000000000854d0000c3500000c31100000000000000048004a6469912e6512ff7a494d00d8ab8977f1378479cdd5919a687a6b4912af46667e00000000000854d0000c3500000c31100000000000000049004b378f6cfb89eef63eaf08aa5d3f5d83c90698ba63169104be2a28e46f9d1ccca700000000000854d0000c3500000c3110000000000000004a004c09e07c300817fd060a676575d6376ea8deaccbe2eec3714954f953826781831c00000000000854d0000c3500000c3110000000000000004b004daf5d250305bc69ba1cd8af664d439f6469f65361e3f5fe870b765f99bf9c840a00000000000854d0000c3500000c3110000000000000004c004eaf472cbaa57978e18a7beb1aab7b3231d10ca16d6900093ded5c8ac76aee2fdc00000000000854d0000c3500000c3110000000000000004d004fd208f7cc7c79c678fa3d3af63e43f8145e12c509786de6d484f0fa9eefd4df6500000000000854d0000c3500000c3110000000000000004e0050e28a412244c366997dc906ad005406191314722626a355dbbaa982075b3195b900000000000854d0000c3500000c3110000000000000004f0051164032bafa294351f3da469060dabc9ed49a700436b28e33ba98eb19575777b200000000000854d0000c3500000c311000000000000000500052f7c31d1c01ea89f750fd13cea775b223d7b33fb72787d312a5491919dbf1ccbe00000000000854d0000c3500000c31100000000000000051005325364a6509a5cad019874190a2d2575fed1814db0ec63349e0d45da1ddef18cc00000000000854d0000c3500000c311000000000000000520054d67bbaecdc0ceda6c4c73f713e4f2b0720eb3d477f514148f2dac77cc50ea51000000000000854d0000c3500000c311000000000000000530055f6bb6e89201ad4cf91179cb56daa171bd244803fd2d2552f81d9e8613b9a399100000000000854d0000c3500000c311000000000000000540056e8f85ca405596250eaa741554d424ed40359b7bc9a29df7116ecf508708d8a7000000000000854d0000c3500000c311000000000000000550057c87b12700480cfac14a3b6abeee0ce1529d0c8dcfe84a8ba767e97662476653600000000000854d0000c3500000c3110000000000000005600582caeafc4b9bf93574c0f8d4bc94f57a85d9f23efc58c769e716f0d7b18b38dd900000000000854d0000c3500000c31100000000000000057005969126f4e713f05ba71906ad30b5673030fe0d3f82d874741e2dc94bdcea49e1000000000000854d0000c3500000c31100000000000000058005a2bb80bd902fbb168b3121206e194f44db5ef0cbaaa3198ecb971466d128f175100000000000854d0000c3500000c31100000000000000059005b4082cad26a5c16ddc9f61d81b789669510a05281c338443b02ba356ea887c02f00000000000854d0000c3500000c3110000000000000005a005c265d9a20fafd71d0aa9696c1aaa52a8b570d3cd3aecff66d6a2ccbfa71e14d3900000000000854d0000c3500000c3110000000000000005b005dae6967ae7b6549b26551f919039be40948601cf75d580bd8a7273102944d505d00000000000854d0000c3500000c3110000000000000005c005e98476373841a91019fc58c72deef72db55794552bbe06b99a0cc70b545c257c700000000000854d0000c3500000c3110000000000000005d005f82011b8a630ed5a1d31aed2f71d4007347e50c49d0273abc634d77b7fd732c5700000000000854d0000c3500000c3110000000000000005e00600ad83b5a1329af2453f9a24a11729b565921757b15b24342cfa0cd68926626d900000000000854d0000c3500000c3110000000000000005f0061e98244eb7266bac47dfe83ad32ea0aa6c2a71c6c6efeeb6fc94dbec2888836a000000000000854d0000c3500000c311000000000000000600062c0bd14f5ac63c549c1df54d26ebd93b57f93f3610f87dc19b15d6178e97a3c8700000000000854d0000c3500000c311000000000000000610063c56dcdbbffe2ac6cd828e1885083843d6dff6883b91a547ed66ab4315d7e3bf700000000000854d0000c3500000c3110000000000000006200642e89e3b0df85fbe1eff77b8841b90b0fbc491812b75ac167e0df33b212447b5300000000000854d0000c3500000c3110000000000000006300659ed51626c3998d5bb14679fe39d2fc267375317eec386d12d9805e45bd9082d500000000000854d0000c3500000c311000000000000000640066071eda545451d9d4b94201fb5b5f6623d970d3839e46f870d7fa3bf2d2cf7ece00000000000854d0000c3500000c31100000000000000065006793865c1b6f4070c9bc30c2f29286d8eea729414d2828c3827d7add206c22b5d800000000000854d0000c3500000c311000000000000000660068fdf7662574899c10d0b4b4119824dc80a26595b4b44284ab1a6d95bf48da05e300000000000854d0000c3500000c311000000000000000670069f6e8deab009f70f621e12f36a033dc1bf7ce211100ec76ebbbdfbe1a8a296bbf00000000000854d0000c3500000c31100000000000000068006a3c96ed2d4e91b99750437ed50e90544e3ace2456c7fdef5e6d81d1c69797041200000000000854d0000c3500000c31100000000000000069006b64507da352e9f70ab879dbc4a2aa073630b6d61dd0e1eaf5bc215510ae551ed000000000000854d0000c3500000c3110000000000000006a006c523085244700d8bc42a08c99dab05caa6d4e00a0a3e974355e6a5cadd6a8604900000000000854d0000c3500000c3110000000000000006b006d843e8fb4fde1fb78042e83c43096cc68ab22b2d41b945781c674dff62520d3cf00000000000854d0000c3500000c3110000000000000006c006ed4c3b0a4e860f9d09f8e28525c44e2b32684d8ba68228a3f1f79db0598b2eea700000000000854d0000c3500000c3110000000000000006d006fbfbb7a54fc65f2cc087f91d4a54e89c6a9189994fdf807cbace173f150527f4200000000000854d0000c3500000c3110000000000000006e00700940c82246dec163f6bcea0066dc1b8e42153a9a89cb87335bc874fef74954ae00000000000854d0000c3500000c3110000000000000006f007110fe330651d32c31fcab4753b4467cac95586300c264136f8f86aac3721fb16500000000000854d0000c3500000c311000000000000000700072a82c392b6da093a4bd54e33100ea7cd77b5bdcf688b1fb691b474a10e0bc363600000000000854d0000c3500000c3110000000000000007100732f743009fa6915ebadbfbfa27399c3a30f9b29b48e39e8e76a6bef920de3a57700000000000854d0000c3500000c311000000000000000720074815323725370ca6a62de58f815237acb314bc3c4ca763f5c260923f0dd2b4fcc00000000000854d0000c3500000c3110000000000000007300756727a4ae8c2ebb1d033eb969abd7476e90dc6b40b54f75b5f4d6d755679f6c0900000000000854d0000c3500000c3110000000000000007400767e07701c7cde480625ca923bebaa832bc17a6abed269b7b1d3601175c2ff6cfa00000000000854d0000c3500000c3110000000000000007500774173d50ab54b95deae539126064ca1a69a831ac4bfc2c9cc419c6f1da7572f5100000000000854d0000c3500000c311000000000000000760078c8bba7fb3d037affa75e8ab2b6784f404e9ef50f7a76b9382874b24086d5b5dd00000000000854d0000c3500000c3110000000000000007700791cae9402e611c095147a803f5b408d27ebaa9344cb951fb073666fd308fa557400000000000854d0000c3500000c31100000000000000078007afad15542780583e26b83691532044687dce16a82ea097c13d339da899a5ee02a00000000000854d0000c3500000c31100000000000000079007b245471ab91354bcc2c2bb09ab2a2b96840d5447ba9e3b9db177f63bf1894ab7500000000000854d0000c3500000c3110000000000000007a007c9a09a07adac2cf5ee978ef0898161ab40e666963976b0dcbbbaae8a0d7f8b28000000000000854d0000c3500000c3110000000000000007b007d14e2ab39cda053ec7cb75fdda214d03f85c30d9d137083645ec85d70119906c200000000000854d0000c3500000c3110000000000000007c007e768ff392d5d959b68e4eab28b75c719679c43ccc65060d8d9d1b55c56e410b1200000000000854d0000c3500000c3110000000000000007d007faed573b2efbc90729cc17c11f1947952c29ba79945746be3a3635cac70dcbef900000000000854d0000c3500000c3110000000000000007e0080b5f95315fb041ea28586d613d2a976d82d348d960989c7b9416fb3d56104da1600000000000854d0000c3500000c3110000000000000007f008140d95f0c5da8d32977131dc41a05a92f1a6876ca043a638240c2b1c8521f111400000000000854d0000c3500000c311000000000000000800082e58f57a865ff75144bdd08549c83c656431b680ab2e57f55501308f826216b8300000000000854d0000c3500000c311000000000000000810083d7f2320258354c20a9069262030e08e266332394343063bcea0cb1869f329ea200000000000854d0000c3500000c31100000000000000082008467c32697927595a930d79d6af1b4d0ef7eb48eca60a66b0b9295e281b055345200000000000854d0000c3500000c3110000000000000008300857954935a783c0293c7338c17e95ca0e84642aee0b1357adda4264d19d6dfc5a100000000000854d0000c3500000c311000000000000000840086649f6f69ab5b4062727281bfc562602b8d23866140ff820bae3629c28d2b68bb00000000000854d0000c3500000c3110000000000000008500878646a8a5d0c0a6d6abfc5db6d6f7f0675e2e502248d2c9868363efc858272cc600000000000854d0000c3500000c31100000000000000086008837ccf66214c602f0155d3350d8410b27f835f10d7ed7480d38697ee5928580aa00000000000854d0000c3500000c3110000000000000008700894dc5c25b4e7e181bacc5dc94f96cea194f4ad582ff9082e49f43af951f5f2c7200000000000854d0000c3500000c31100000000000000088008ab4a423a735be5b23a37b7e502b1d730cdf511b5f5dcd7e3dac491dbf42454bd700000000000854d0000c3500000c31100000000000000089008bd0300ea2db54c07c732c435c8b390eff7af09d1fec7d4183f26c11af6f06ae8400000000000854d0000c3500000c3110000000000000008a008c06eb3dd61b8993afd0aad49eaab19f9b363acc565df1eee863fd0cf189ae9e7900000000000854d0000c3500000c3110000000000000008b008de2f3402d5ec19a1b0c2df4f3bb8f8375692869e820a1e4071468c85bbfd561e100000000000854d0000c3500000c3110000000000000008c008ea4210d942e844c1b72a9418b1b4b3c4b0f6e514fe3b0f75e3d7df746e95326e400000000000854d0000c3500000c3110000000000000008d008f00608445dec5823810557eb56baddb365cdba4e465f2498abe69cd50b0333f3f00000000000854d0000c3500000c3110000000000000008e0090cd2a4d3bb7d1b6be07dd1f3c90d3cca5f5b3cedb97379673782f5afb23c3020000000000000854d0000c3500000c3110000000000000008f0091254a9a75cdf945525320029f7e8c5aba45dfd5b4a426f346603348a66b7dd45300000000000854d0000c3500000c31100000000000000090009225074aff4e140b03cc239486412dd0a130e3ec93b2648b95569ae55e5782af0500000000000854d0000c3500000c311000000000000000910093d8a8b5c8d165c407816b8eb5e89adb271a1078cdca4f29315161593ec86f769500000000000854d0000c3500000c311000000000000000920094aa968f2a69bb072521c766134d9c1521b5d49a368b0dd00ea3b4e21883f7c55600000000000854d0000c3500000c31100000000000000093009592d8e3576e981f7ef9fafcb12635311778fa6e9bd7544bb502e310113869bb2b00000000000854d0000c3500000c311000000000000000940096fce131a771b67463a1bf5b20192031a7780c14e21a74d4ac5a0c4468a45998ae00000000000854d0000c3500000c31100000000000000095009752267d49a6543212b263beea088dfe20227f97d1cc1a3b935bbfa0e11e47548400000000000854d0000c3500000c31100000000000000096009813496ffa2340f37f578504f289f12a5fc253e2e534f83ed2878e342bd0187c8a00000000000854d0000c3500000c311000000000000000970099c168e856e6948c8eb949d48e53f1c64266341a91c7995547f441d3f2bb7db66e00000000000854d0000c3500000c31100000000000000098009afd09c7a336cc28e97b5ff1e5c83d05c3e68257ddb2587b0c20338833512dcdce00000000000854d0000c3500000c31100000000000000099009b256bb41df47f9dc594b20b7aed566d3457cd3feab6a2ac54021ac61a65b74cd500000000000854d0000c3500000c3110000000000000009a009c1304a8cec2aaa0fb13195fc09a06d571fcc8ac066797be25754ce7da18783c4600000000000854d0000c3500000c3110000000000000009b009d82e93cabb272fbed2f0954226d7871dadbcc06fb772436fe4bd5e79a509dc33a00000000000854d0000c3500000c3110000000000000009c009e67f4fce731aef9e25e456361ebaa95b8dd448180b3780a57dd5e996e155fc12a00000000000854d0000c3500000c3110000000000000009d009fee0a1488e1db16efd197fe3307bb1ef3fecc70be961fb5d83f57293b2389742700000000000854d0000c3500000c3110000000000000009e00a0d8b7f44f79f3825b0898063c9be52e8430a4fc6556380971e45ba2bff87d41f300000000000854d0000c3500000c3110000000000000009f00a10cd85fbd33a658c370afb29f2c452a9c176e6b15b85cb999c849c3e5fcc3571600000000000854d0000c3500000c311000000000000000a000a272fb1ed9a9b17e17dc9252ab1b6c57842adef570518d2ddb9ca68fbd6cd6864f00000000000854d0000c3500000c311000000000000000a100a30e18954a79e2d97b37db47861208d7191933965b8a66a8fa2192c2f03706539500000000000854d0000c3500000c311000000000000000a200a4f2891d485e7fd8fb3acffbe35e13ee5fbc7ab4bd4b856819d6cff8e9b46054c100000000000854d0000c3500000c311000000000000000a300a5b17223b7dc16a717a0143c327abdf0ecec7a56ce9d4a42a187228d4764f3adaa00000000000854d0000c3500000c311000000000000000a400a6e664a80ba167ef9a42fe98f9e60997922edf02ab62b9515f3cc04cd742bbc0a000000000000854d0000c3500000c311000000000000000a500a71dc5c03a9ee981ff70177b153af6073d60b5dd62f266b099e2a9a4d4db5228b500000000000854d0000c3500000c311000000000000000a600a81cb37a70492764ccb4e974c7d238592775eccce4089735e69dddad210d68642d00000000000854d0000c3500000c311000000000000000a700a90821c9c1ef4a9e69d5f508ce86029fdc4601e2a1faeed4a8bbc0617640f5cea600000000000854d0000c3500000c311000000000000000a800aabe06ffb02722e4f7b8b02d33806acdf4fc6050e20ae54bb1955aafd407a8be9d00000000000854d0000c3500000c311000000000000000a900ab7518052cc5fc266960839ca65bbdcb933b36eaf9b2f61c37192e6ba5effca76600000000000854d0000c3500000c311000000000000000aa00ac9ea401167af230c9b049bd02d00e1636f21319cdab24c46937d04d8a4ddb41eb00000000000854d0000c3500000c311000000000000000ab00ad1f1b2f6031fd9c8380f15b2928f042e9645332d6e0bc2f55e06778108ece17ff00000000000854d0000c3500000c311000000000000000ac00aeb8410e7035d2c12ede9ebc2c6bc81af83b27d172db05b66b10c7c7f48c57222e00000000000854d0000c3500000c311000000000000000ad00af1b3fb423b0daed330e204543b2fab0fa9aafff31b387535ad322f5743c7b78d800000000000854d0000c3500000c311000000000000000ae00b02c9d6f58fd85711958fcfd2824474818828d5ecb641bdeed41c988b6eec5c22e00000000000854d0000c3500000c311000000000000000af00b1015bda7aab18ccb03f23a253bd3b94ec400a67d057cadeb092167e45001856df00000000000854d0000c3500000c311000000000000000b000b235eab6ff950e9ec80d4563255908a7ea09a5ae5dfbd0e3c2944295709ecd7e3500000000000854d0000c3500000c311000000000000000b100b3957cfa2365934f72f6559f95997c60304b0a98e3c9e0dc99ab7ea6554bb0a26100000000000854d0000c3500000c311000000000000000b200b41012ec8ec892231dbc8ce89d60f3bb9b375718e460e7b5bd1b8e84631e291a9b00000000000854d0000c3500000c311000000000000000b300b5ca5d940ca7b8fb92c4baecb5a6e464d3c3b5aa5c12e67d9e70650327b858ebcc00000000000854d0000c3500000c311000000000000000b400b69d5412f7b492ac0203d7a82aabe23f8aa7776d173df7da0afc42147476614d9100000000000854d0000c3500000c311000000000000000b500b711d3ab9e16a2c977acd6d131284d2fc8c0d1af94dd6191d6a84344bc4722135400000000000854d0000c3500000c311000000000000000b600b8eb9a2b0b9a696572dfc2adebd5f9177abda3769f294babd21480a6839801474b00000000000854d0000c3500000c311000000000000000b700b9078be2982828fed4bb25781dc9bccaa624f6058c722eac222b4a659e9489edbe00000000000854d0000c3500000c311000000000000000b800ba819151088a9e7aada910f638ca1a248bc94b4878b1031a41c101af8f35c9aef100000000000854d0000c3500000c311000000000000000b900bbf28de95ff9677f064bb695651008dd14631262188ce64c392f192d6efdf8d4f100000000000854d0000c3500000c311000000000000000ba00bc9ea9e5ffbeda1fd8846698475c057567d1f2c133c6d548f3e9722b3b8b9ec69600000000000854d0000c3500000c311000000000000000bb00bd0689a54e3457da2e4ec2a9b07cad6b256469eab859e2e8354b27bbb9f08c3ca700000000000854d0000c3500000c311000000000000000bc00be460f9ac1863a0d05133befb41a245dba3e136fe3e9de43e1000316991d135c4200000000000854d0000c3500000c311000000000000000bd00bf0006cb6b8ace8eb6f74e97e484bb283a123132eb4976f0c665de5394ed0b022100000000000854d0000c3500000c311000000000000000be00c07b38bdf8c89b9b0db4eb62ff10e98308e9ce8647200922d8d790b7dac81bef8e00000000000854d0000c3500000c311000000000000000bf00c181157bfa3420a29f5124ddf629161d0b986616e8aaac86278b838b7caadd850800000000000854d0000c3500000c311000000000000000c000c2b67b3796c8da1c71ac82a89c2fa7a8b40e2ee560c6df7ac42cc99e51645918c100000000000854d0000c3500000c311000000000000000c100c338b2fb62c7aa54b15597b8422142cd49a776092d55f92d5dad31e417844c63b300000000000854d0000c3500000c311000000000000000c200c41e7701adb1fc46ca8f1c24d0e24cc40fe7d9daff7deeb63211fbf5bc1c123c0200000000000854d0000c3500000c311000000000000000c300c5e2cb25607821cfb7e67028ef96056f6e3f81f72db1bf8851432472936bbc647600000000000854d0000c3500000c311000000000000000c400c6d9392204247dfded154f8863582e44f0acd0a30a3bc8a38ab35ab744b73ca18100000000000854d0000c3500000c311000000000000000c500c792e42b479904bc5abf82fbb467577afe7f53632f5a8e89be6791a36b49c326f500000000000854d0000c3500000c311000000000000000c600c84412ac310f1fbc90220101c29f748304f9496740c3eee14b64c8fc171558352700000000000854d0000c3500000c311000000000000000c700c95fdd380ed620c5abc8a4160f18375928c0ed6a926043eaf91365dfdefddef93f00000000000854d0000c3500000c311000000000000000c800ca0c8edc3204a6c47c4e9115755a24c31763efa78db17bfaa9645aabeb4972c89a00000000000854d0000c3500000c311000000000000000c900cb83e02383dcaff8714d79fc6df28563584f79be4435bb3632958eb98649d214e300000000000854d0000c3500000c311000000000000000ca00cce136d56b460bf32ae200ce0fe980817a2bbc78112c9969c70db679c2aa30d73b00000000000854d0000c3500000c311000000000000000cb00cd4cd753d127fca8a62042ce2dae9cf035561530b3e2e95f60fe74245ce293dfb600000000000854d0000c3500000c311000000000000000cc00ce2ee0ed4ba29122e130bfd9a32626fd46ac15ce11642d63e5c596b7d23d6b142300000000000854d0000c3500000c311000000000000000cd00cf92b59998dda902b1005f23746254e625f4a70103d02b0ee3cd7cf6f19425188d00000000000854d0000c3500000c311000000000000000ce00d0040c9ed7f1af1ab9d41814211ca770b40c250364d4a26bd0d57b4d6de3d406c800000000000854d0000c3500000c311000000000000000cf00d1cf54bb912bf745b8761c10c6ed566edc96e82ad8e7a9bc3fcd22fb0e3974cfb400000000000854d0000c3500000c311000000000000000d000d2a81886dd94579a2ebe3c14379490aec6fd8bc8bcd7981094f23011e1690bf6a800000000000854d0000c3500000c311000000000000000d100d353b474acbb9539a6631f51144498802e9eeb237d8ba7d11ac050c4a392129afc00000000000854d0000c3500000c311000000000000000d200d4c08ec36aaac6cb7b82a482059dd448f61a9b25249bc5d4e6be5517baf5f5e24200000000000854d0000c3500000c311000000000000000d300d5693de4032c7083eed7fb1803c6d5624a7e32d3107bd7a3939cc89a083388264b00000000000854d0000c3500000c311000000000000000d400d6f9756396a45f40c30da3a0a67d4dc1838b20a098e5351a1e4f344dbbe00dcb8600000000000854d0000c3500000c311000000000000000d500d7780b34031a24731ecf7e0e6844097a8e62b43267d565dd6d3a4886e55620678900000000000854d0000c3500000c311000000000000000d600d8bcc1624846430d2de342f54d7b8e61d264bfa39528a1fcc63532b291c994c2ab00000000000854d0000c3500000c311000000000000000d700d97a217a4e347a614aa9e651bd234eb0c3d49830a6a804e7ede79c77959ce14f7300000000000854d0000c3500000c311000000000000000d800da1d361f1504f6625c8289222276f9bceafa3281738e27e15f6547884c9ca7a5dc00000000000854d0000c3500000c311000000000000000d900dbfe2501594d57b4c25674b575507428defccdccfe69543a182575dd4817b9179d00000000000854d0000c3500000c311000000000000000da00dc45eebf5678f6a340be93a51d2e263e764839c2454a10bf8853fac76861923dc300000000000854d0000c3500000c311000000000000000db00dda5d81abfe6b8ac5e302b89b6057f782aad54a65adb51ed77e50d3a366db9d7a200000000000854d0000c3500000c311000000000000000dc00de0e6556de4b9b77a9d866d3bda9f4a87208766b0cc701414a4deaac8da2acfe5a00000000000854d0000c3500000c311000000000000000dd00dfbc0a8294b1cc8edafc68b3a95d9263f1499a02d63c93a0fe30633ad556a5b44600000000000854d0000c3500000c311000000000000000de00e0a1078aac0d34309b68c23dcc3d7da53e7f88c24e6ed76850021ab5ac808d6b1a00000000000854d0000c3500000c311000000000000000df00e1412173ca93c1a3db66b0695504c0151d21f1e65e417d5671dde59931256ac4fd00000000000854d0000c3500000c311000000000000000e000e2e601a904450d4f2e168e917bf4438d900cd17f571719d60bb900fb0e0740d65200000000000854d0000c3500000c311000000000000000e100e351a8a26ec67cb3ba4b95f2ac4be486745285e9434dcebd8f7df9f743dbd3653000000000000854d0000c3500000c311000000000000000e200e4d20396497388e26286a2a9bfef575a2269ea85b36b4005d294302aa05375862000000000000854d0000c3500000c311000000000000000e300e5abd37177996abc69e2ca24b121f45b32718a095250c0d533a89785fbc1692f8100000000000854d0000c3500000c311000000000000000e400e69491b3619b64e09337ff2b31ed0a98c07d47c910346fa6e43ff4da9c16a4317000000000000854d0000c3500000c311000000000000000e500e7fa38ff7b2f735c6d7ed6697968cc2792e9ed4c2cc9957e2152c773a23e10969000000000000854d0000c3500000c311000000000000000e600e8d82876021b20bad6dd3833506b1e2c7ddb85e8fc754b11f0099c558ad676db0d00000000000854d0000c3500000c311000000000000000e700e93f7a7c70b38e38652fe97203085488d82bc013586ad8a093aeceb4deaf2ee93b00000000000854d0000c3500000c311000000000000000e800eac0e5f3123292708627ace19965a0fb622f8eef55e33dd21febe02b2fb6dc202500000000000854d0000c3500000c311000000000000000e900ebe9c9a099ac9f0603aa645a573caf5b99f8eb03b28e881010b96d87e1ed55ce3400000000000854d0000c3500000c311000000000000000ea00ec1d875edf3e4221594917fca299d02b1c3f1990f6069f944330c3b2ad9edcba7b00000000000854d0000c3500000c311000000000000000eb00ed48640f071c6bcd7c1a94314bd8615a6f6493919fc216a555b9dabca0121775f500000000000854d0000c3500000c311000000000000000ec00ee457e830ff6e6eae43268bb92d5fecdbf8832a37b575285b1e113b29ea7e5f8c900000000000854d0000c3500000c311000000000000000ed00ef55b2b3b16fa963985019f4a6cac17b51fd164b85efd22180221311ba043e168800000000000854d0000c3500000c311000000000000000ee00f0eed5a265e23b6fee64a6800737f859959582dae01bf804141b7dc199139dc15a00000000000854d0000c3500000c311000000000000000ef00f17920b76551c9c0381e120a166e200e20e3b2e0d54267bb3c9f69a0a374af5f5300000000000854d0000c3500000c311000000000000000f000f2550cb6ae690524fbf6a6845f2df5da2d7e4aacc218263bc216db4be3c269ae5200000000000854d0000c3500000c311000000000000000f100f3b3ee1187678512ea1324b3e746df8c76d52008263fd5ed6ee3307608b02a7e6600000000000854d0000c3500000c311000000000000000f200f4c0c14618d3cc34a24260fcb5c7819db74e65d8eca279451104abcf31eb27a7fe00000000000854d0000c3500000c311000000000000000f300f564108c7261a446b472fa45a0c29af4b821210c8aabe6c113d292b6300224dc2c00000000000854d0000c3500000c311000000000000000f400f6bcf946cf2868a4749a156c578a4a6e40a2a7bc1716b35880eb9d87a4748ae09600000000000854d0000c3500000c311000000000000000f500f773465d6084beef0010d1ac1e7e2fcbc6783d854f3927e6d51f272c52bcc6bda100000000000854d0000c3500000c311000000000000000f600f86d175c4b0f2a9dbe8437a34789c0343991526bc83bdece3c71963f86cc51183a00000000000854d0000c3500000c311000000000000000f700f934b71da18c6f995f91aac176268d6ac0a3a2acf2717801ebf23b9d91926e914400000000000854d0000c3500000c311000000000000000f800faf380ea1d68711d9db0ab45e3397dbd39b5e728dcec8d55930f6f2affb0ca12c500000000000854d0000c3500000c311000000000000000f900fb834434e1f82adc0b7ac0bd9ec0d4179d36e2a611ede605a42977de8a9e466c4500000000000854d0000c3500000c311000000000000000fa00fc3479e0daf8b53f8c04ba0eddac3848dd88ae4cafeecc59bdec10b881101c883b00000000000854d0000c3500000c311000000000000000fb00fd61b82153bc8cf58c32d180d4f46da8f40ca6d8a3fff5ed4d5ba16a1fa73d4e6700000000000854d0000c3500000c311000000000000000fc00fed00cfde8a915c23cda075f4a88dc0ae74792bd8ece23dcfdba72f56b9813b0eb00000000000854d0000c3500000c311000000000000000fd00ffd559de901dbb27927b10cbb56d57e3b244a9f5c248ac8d85221547ba9dabfb3d00000000000854d0000c3500000c311000000000000000fe0100c68cdfd7846524e752cbb7ba1aa0e495352325399e03d91a7016e3671fd964d500000000000854d0000c3500000c311000000000000000ff010191141f42588c3501522fa211cdffd484c7889c2cc6716de25654169609657a4900000000000854d0000c3500000c311000000000000001000102e6ef1453006abcf1da0dfe1572ef43a2082f91f230b0f14ce55a6361e8650b9200000000000854d0000c3500000c31100000000000000101010312266945cef53747caa27b2ecb733afa5afa42bac3afee722206d2d0c5abb81100000000000854d0000c3500000c311000000000000001020104c7ca46af876947a45686fa1f11f8842a6ba14ef982a40f5138bf420c6f73176200000000000854d0000c3500000c3110000000000000010301053f1661f5435f16624c0e51a7a644ece8fe3d7f84af61a410787b30feeeb966d000000000000854d0000c3500000c311000000000000001040106cadaf4cd6e8d42db0682daba7a65e6a51434b843b5cf75213ea4427e0319f4ef00000000000854d0000c3500000c311000000000000001050107bc7d18e01e7f39891798b254ec5fbfd4066bfa49c634f8e3eac8fb5329aeb74300000000000854d0000c3500000c3110000000000000010601089cc3be9f123da1cbb918809e10bee97bdc9fa805b06c557bea2210471fa3307800000000000854d0000c3500000c311000000000000001070109f1a7aae9140ccfd98127b56044d9104e9de5fbcd682859598eef0d6f9ca92aab00000000000854d0000c3500000c31100000000000000108010afaa9ff70179a7b0eff8bef5d45379521f1110e8db1ee03b96d579764c8c344c600000000000854d0000c3500000c31100000000000000109010b07d1b09149d5c5275f85a10d8cd9202c9400ca2def4ad6791075291b4c3c5df300000000000854d0000c3500000c3110000000000000010a010cca26248920d3f7592d14d56435f07ccaba5650dfb6a88e4e39075376402718a300000000000854d0000c3500000c3110000000000000010b010ddee81eaa499f230eefdac7766ac46fda3d15ce50ba634d5dddaa37ed8b27706f00000000000854d0000c3500000c3110000000000000010c010ecddf241b4264b637a0ac3315b33d20ad2b4ee78abe9b109cd2ff12910527db0500000000000854d0000c3500000c3110000000000000010d010f2073acd612d2f8b24c44a0d791c8e458cb4209a578c5e54059a80bf078ea62f200000000000854d0000c3500000c3110000000000000010e0110f2bc07ca3c0917130c0549bb4528b3d30787e8a58e1a4e395f797ee1d5313c8600000000000854d0000c3500000c3110000000000000010f01111adb3bb2a1d5a92c063c04d8e4ccba90f31d32705ab9e5bf9937abcae9bd411300000000000854d0000c3500000c31100000000000000110011216d6fc2b34c08eb4c4047768a720a11c236f83a193eddbefb5f177811e1cb74800000000000854d0000c3500000c311000000000000001110113f7ef1e699a40d95dcd4c155bfe82b2458ffc85aad59e9a7b9a38b6f8e20129cb00000000000854d0000c3500000c311000000000000001120114ccd2d776df1d26ee861943082701bbb4b69249fadd8f143b597ac73e2af5c1c500000000000854d0000c3500000c31100000000000000113011556ceceb7b569011a449af0c305c8dbd28c6f9703aed425f8373c8497188b2b9e00000000000854d0000c3500000c311000000000000001140116b2f5712de794c52523a62778e83e0e59a1bbc6f06f97b7789670f0e5039ea9a100000000000854d0000c3500000c311000000000000001150117693cec364193e49f8f6aebf42955d1b49acda6c888a3a6e2ee274e372a45494b00000000000854d0000c3500000c3110000000000000011601184ebc0b53de00bce7dc1119c28cbf0648dc7ad74c976e4b49b1e5ab6d372fe7c800000000000854d0000c3500000c3110000000000000011701197dc46f6965e74bbaeb7789ee532264e843103a39d7a9d75132de7e9b5ef02a5300000000000854d0000c3500000c31100000000000000118011a00b1c69e8c2aee8a42f849b56150603e0541ba97656911b2bec92fb6b80e1c4700000000000854d0000c3500000c31100000000000000119011bbe8e25448c1ed1d4cf6851269c121e1c45e3cd58292714ca7a0afcd8c6813dc100000000000854d0000c3500000c3110000000000000011a011c42ef09220765bb717aad3c2d4dcfe180e8c0ebd8421df820386626d3a85e4d2900000000000854d0000c3500000c3110000000000000011b011d595e3312d192af31f4735e7352203a2b3e1ff5c8712247ea76b8d6521328a28100000000000854d0000c3500000c3110000000000000011c011ebb01550134b55966c3b8a175688d7fbd913988eb37701053abdebcd07b2c0ee200000000000854d0000c3500000c3110000000000000011d011fc8b44418ae4ad010eb4a16252265eb6e1ffbb8238ae74055e93170d5aed2959900000000000854d0000c3500000c3110000000000000011e0120996d1a0367ed61db81a131b5b163de5d6f2f693055b87a3cc7a0c6a15bb72ef800000000000854d0000c3500000c3110000000000000011f012126b6c2e89d781417985751c1fde5cd6069167fce4c56138370bcaa24d03ce24b00000000000854d0000c3500000c3110000000000000012001229e7af1b72e21a2785359677d3c101e9029b117a43dba617f8ea56d7dd022d8d700000000000854d0000c3500000c3110000000000000012101232d2e1c3b68cf4b0e8bd492eb5741cf78a3b36847fa0a97c3304a95dcc7a8a14400000000000854d0000c3500000c3110000000000000012201240e06ed3926bb95bf57bc7ee2c9e5f1c5bd661bacb1ce5a1a2deabe6736c8367800000000000854d0000c3500000c311000000000000001230125a6abd820de07f5cda1819cd9a9146f0adf4d8dcc60e3cab50bc5e1abc14891e200000000000854d0000c3500000c3110000000000000012401267ac610ff9d6969b53d28f85a92a1fad522b3bee82f76f010f45ef0a3e4c7c15d00000000000854d0000c3500000c3110000000000000012501277255670d53527fa3d330df360ac6c98d49a6a5ce3f6fdf1de4ca700844c8fa3e00000000000854d0000c3500000c3110000000000000012601281d6d6e357f1227e5e942653388260e8238b5612f4927fbfdce258bc4d0b7594300000000000854d0000c3500000c31100000000000000127012954e9dbc5cf7149868716b45dbdecdc449d0b39b5eafd69e3b49a7ed29d4459ae00000000000854d0000c3500000c31100000000000000128012ae285b863674b89d72cfb884f5cd5eb523ac0b493661c58f7851c5547a959ed8100000000000854d0000c3500000c31100000000000000129012b819532ccb84729849fbc4cce0ac5f8236350c3b67b316338e15cc62b954d30b400000000000854d0000c3500000c3110000000000000012a012cd23d76e2a5311b80e76c5229b01cb07f2bb85646fdcb4c62adc82f198b047beb00000000000854d0000c3500000c3110000000000000012b012df661b595023ecb62cbf93a64d234dbf889166b48fa8eb394bb9fbe841e1cd56600000000000854d0000c3500000c3110000000000000012c012e0513cfb510f00b170cf080b3ce2b850cec369d35c0550eff4b8fb2b6c91dfda800000000000854d0000c3500000c3110000000000000012d012fb9d1d054cf18aae245c31e11c0d122ed5aadd8a3b35ad39dfe30ac8798a46d2300000000000854d0000c3500000c3110000000000000012e0130e06b5899ba907b2923ca8677fa61c7f0943cdd7c1c40def5d83aad630bc00eda00000000000854d0000c3500000c3110000000000000012f0131293692db9029a74afbc68d94e574decdd9aa7f388e8aa344379018c4da18256e00000000000854d0000c3500000c3110000000000000013001326c2b3b309d75e679971f71daae4c69990da8707c359765d2c1f74a56ea45a25100000000000854d0000c3500000c31100000000000000131013391b688dbb1263e7d9c8898a7356dd313e14f066a6db7dbd8a05af623cfb1423000000000000854d0000c3500000c3110000000000000013201345d9daa987405ea5c478cc99679df047e1cca5d28b2ae550d88570c9a7b361b4600000000000854d0000c3500000c3110000000000000013301352d4bf0f568c72724e502c4ea615d9bdfe76205af4649db3381bb039a6c81d5e700000000000854d0000c3500000c311000000000000001340136387afecf394d4357604f540282bbd442f84f456c5ff810d7964941b31bc12f5f00000000000854d0000c3500000c311000000000000001350137b19741e64da87111ec3ab8a714db12a89a0e19cf667a0049a9dcd72aa5c30cfe00000000000854d0000c3500000c311000000000000001360138041c4bfedd2c41b363acd3fa746b0e65820abf0259851616c147a6253982860600000000000854d0000c3500000c311000000000000001370139bd4d8149d173108a9685d7305f1624dae2c8dda08e475d90494ff93e3b68d3f000000000000854d0000c3500000c31100000000000000138013a6807c6439794cc48e36826e845ad919d758bc0308611f2b2063f35e59badcc2800000000000854d0000c3500000c31100000000000000139013bb94b58b11fd0a16ba852395acedb03563c9992103fbb9c4541c0140ad25bba0d00000000000854d0000c3500000c3110000000000000013a013c98f301ca34257ff0bb60f8a29e8c2e75569f12c2e757cfcd86d441987754db9800000000000854d0000c3500000c3110000000000000013b013dd33a298ca334510f32a78b0d175ef81aa1283c5a43a0a36997337a188cf28df100000000000854d0000c3500000c3110000000000000013c013e6535f74d7b79af5ab10020827567deb1d7feb0d68c97b24375444fc91685c3d400000000000854d0000c3500000c3110000000000000013d013fcd31ecebd30f110cd6faeddf43207ca9ce128876a99d882d1eb2956ac66baeb600000000000854d0000c3500000c3110000000000000013e014080dc9f243390f1e5ff5f824b3558f3a71558ee2b76b6dc85b8767d8c3a762a9900000000000854d0000c3500000c3110000000000000013f014135e665b8a599899c0e0c8e20d270917ab23a1c5ec1392031cda458edb1c95f4700000000000854d0000c3500000c311000000000000001400142f390439757563240273ed69da2e0b49158ffac612227c538ab4df21e668f072400000000000854d0000c3500000c31100000000000000141014375199d7a5f1d5702a5e97030d9b85af5459e59ece07c34cf4eba44fc2fbe75f300000000000854d0000c3500000c31100000000000000142014488eb334984eb5a14d76478f8c7e277804cb813c573fe185e1269a0b1b3a7c4af00000000000854d0000c3500000c3110000000000000014301452934a43425a844e0cfcdce1f191ac83c216aabfe9cfae82ad8fb80e5ed32fd9500000000000854d0000c3500000c3110000000000000014401463e4182cabfe4bca69a2b2d26394b851e6f313fd7400e34f66b207fa7997336ee00000000000854d0000c3500000c311000000000000001450147beca3c2b2abf74149413af897d503c0940a04e65488cdca79adc80f3a1022fa700000000000854d0000c3500000c311000000000000001460148358cc7525792ec60cad166479beed863fbda52d75a2b3b21b44cb3948c74c3e500000000000854d0000c3500000c311000000000000001470149084911fc1e172537175b5cb1ed8e0ecae73beda0b4e5e13b826819e1dfeab3d900000000000854d0000c3500000c31100000000000000148014a415167f1a6ff33eb7eb3a208895a8bc11057d6fc0e3d3cde21eb23a9e8f0428f00000000000854d0000c3500000c31100000000000000149014b6bd1174aa697898fbf5fa97167d246f94cc930add143f6b5f3ba1c189160fe9900000000000854d0000c3500000c3110000000000000014a014c630f10f3c50d48328bf9fb5ecc61598d7fb7da587fb7821e4b42151afc4ee36d00000000000854d0000c3500000c3110000000000000014b014ddc396d879f620ece1af21eb2ae7af3ccb63520b7e5c2c86ff9704ecfeae2897300000000000854d0000c3500000c3110000000000000014c014e36074edd4eb00de98fd3f92fa28e76e19abdccc7183a427e0f80f2545099249000000000000854d0000c3500000c3110000000000000014d014f329bd6b4a05c40900c16d963d4cb2c32cd3b866fd2704809359d4e7be2b4d3c900000000000854d0000c3500000c3110000000000000014e0150e0d278e14a883573033cdcdbfe68747f69c02a77aee2631a53854877cd977f8c00000000000854d0000c3500000c3110000000000000014f0151cc619d317d853e8a8c9605c6b2016c5b77d1ed0ce2764865af5bb58da60d25fe00000000000854d0000c3500000c311000000000000001500152091da59c8e15c2d0b67a0c03241ed38bcaa08afe611e374ff56a04b1493496c000000000000854d0000c3500000c31100000000000000151015351d3aca70ad269740eb8143d40c40ccd9655137e1a797db99c19f0c74496eb0200000000000854d0000c3500000c311000000000000001520154e8b3f8186945c665a130ce8bb5022be66ee9a82d58bd04995dc2ed8817b9cf5300000000000854d0000c3500000c31100000000000000153015582bbf5437a38dff5538c0853418b67eed524fd6ec6730af6e83fd7e1819aa9a900000000000854d0000c3500000c311000000000000001540156126f1cdc86e71f3a885ba96d926e619de1dc600a5e917556b59349709a09202f00000000000854d0000c3500000c31100000000000000155015742f8ff596b8d24eb09eadb09653801d690e24c1fd00201534185e5d76616e75100000000000854d0000c3500000c31100000000000000156015838866d0eaecd45ba609331dc127de1cc6de73bedf6c8857b39d7bca574d5a95900000000000854d0000c3500000c311000000000000001570159fc5edceefdd5ba830ee696fa281bfaff5f19f03a517bcc14f2a723cb43db17bc00000000000854d0000c3500000c31100000000000000158015aabd47e25adb0dc27d1c5a063dd640ee22ac5716a1dcdbe152c27c2a4925deee300000000000854d0000c3500000c31100000000000000159015b44080f378bc7003840f1d01437c268fb20c7f9bf4b38cfaf1b18bbcdf7368fc500000000000854d0000c3500000c3110000000000000015a015c130157d7beaaf191672619dffdb99204c2ab01b35d299c29b4a4c9f1364570df00000000000854d0000c3500000c3110000000000000015b015d139c7812864191abffe378364b46c4929ce9f354e57994e56ef8690a9adbdc6b00000000000854d0000c3500000c3110000000000000015c015e76b343cf768eea6ea9430c144d1f833d2fd19f01a0b8d04e09ad0625c0c1ce4700000000000854d0000c3500000c3110000000000000015d015ff182f63a888b037893c449126093bb70f97b7360b903cc02705619b8232cac9700000000000854d0000c3500000c3110000000000000015e0160ddb46b199bd616225b980e5cf964c647898a44241ca45335f679143b0097d48600000000000854d0000c3500000c3110000000000000015f01619d4246b117a38112ba1bd600e78490ba170a1d1307043f3c83c2a1a2d50106c900000000000854d0000c3500000c31100000000000000160016205f692aacc895ec966a4c41045525845352c1993f7d5fad4f836f286565c62b900000000000854d0000c3500000c311000000000000001610163051eb76af4a34d66106771401412f01f4ac661594140355541f815f5e1e7490300000000000854d0000c3500000c311000000000000001620164b7e76d9101f61768d4ea6470dbf632941ad250c20d2ed9b3a9b62fc2ae398c0e00000000000854d0000c3500000c3110000000000000016301655cf571d20737997ec5f0f856dc59a1104d5b153b9377cab0a3107dcef4ee1c3400000000000854d0000c3500000c3110000000000000016401660bc27dbed94378bb13a6b1bc1a61789be9572d1d42a7611338d5a3e1351768d000000000000854d0000c3500000c3110000000000000016501670e63f7f71c19b7eda6368a15dc5dc2e7295541888e60738db15b3cb79292617000000000000854d0000c3500000c311000000000000001660168144788ed1842d11dfc84d06d656f2b8390e11778066f21d5f97626bd72b475fd00000000000854d0000c3500000c311000000000000001670169c4642f1fc647f5c8efe7acc255ba6bab55d280a095546aaca38921df1549375800000000000854d0000c3500000c31100000000000000168016a1e52b4f91a493d81e0ad67231f835c8f1ec2fbab50d1f4e5e324504000ee244500000000000854d0000c3500000c31100000000000000169016bb037cfbfe23395592f82f1ee8d5eab8a2277d8e3644908c775b16396774338f500000000000854d0000c3500000c3110000000000000016a016cd3781d0b608c534512544055182fe979fc8d439f5c49b64cc948bd6f7b6314df00000000000854d0000c3500000c3110000000000000016b016d800b2593788c198f7321ce6424338ef7e83123e241aa7fbe8dc38e7c1e3de86200000000000854d0000c3500000c3110000000000000016c016e97b0f44eae74f3a3bfba8c15b94663e93067a575289e7034c8ca6fb8fed4d1cd00000000000854d0000c3500000c3110000000000000016d016f87ab47f0d8959d60eb4147714356816f6fc95b0fb1f92abfb087f9fdd9721a6000000000000854d0000c3500000c3110000000000000016e017015c8f1a325c0740647aa9af8d6f41b9b364f2d6509cba4c318784ab5f98664fd00000000000854d0000c3500000c3110000000000000016f0171dd3765c9c21ac14cced443eed9254bb76cf595dd19763bdad8f3762118180c3300000000000854d0000c3500000c31100000000000000170017205dc8d8d03f1d1b79383291c9b91c6dbed7b5868793a94d86f8cd0b02ea9dffb00000000000854d0000c3500000c3110000000000000017101739285e8ae6dac04fec83f99de4cfd81186130119af91f3af4c2c2ed9556cd73b800000000000854d0000c3500000c3110000000000000017201745f9b9c5c46848b51684d15e7a018c16c7433251d4e05bf0a6fbb1c22e26624eb00000000000854d0000c3500000c311000000000000001730175356f2fcca2d099ddf402b0f28922c8be5276c406fb6a262a10e1ba2d37c1310d00000000000854d0000c3500000c3110000000000000017401762361231644dae69b1d58f83ac1795d89062f1d20f1e18bc58a38beeab8a143d500000000000854d0000c3500000c311000000000000001750177fc410231f56ded5634a2b827172d7ff54326b4d9ba55aff6f76dc0c1d8abe73400000000000854d0000c3500000c311000000000000001760178a3ce42369319689c90263bcf693655b2b5f73426e4299bb3edd27a61fcea84d600000000000854d0000c3500000c311000000000000001770179529f4cfceb408d8e4db8a93a81948458df679039b6b286ef8e2d6b392e584f7b00000000000854d0000c3500000c31100000000000000178017a652a19f302298f2ddedc5bdde42af0c1617f981d5942d33bd7bb436780ef950500000000000854d0000c3500000c31100000000000000179017b78799b9c840639e4d0d50fd3e6e4728c62c0bd0378096552acbbd4096572acb800000000000854d0000c3500000c3110000000000000017a017c8eb8760d4cf5b9077c2871390b10c3af43aa24de7873c145ee0efb6549c7d6ec00000000000854d0000c3500000c3110000000000000017b017d662d3d9dff9fc1daee14bac71957f258d155d3cf5351507f7f9c75f9fdf9750100000000000854d0000c3500000c3110000000000000017c017ee0dd1b9635b59c05c79911a8d80bff18b7575e6429c8d709a869e6a06e04c30b00000000000854d0000c3500000c3110000000000000017d017ffc1ae8163736881a06d397844694f4c1b611b2ec421e3e85830e6a0b06e7626400000000000854d0000c3500000c3110000000000000017e0180103f4ec21762e94c3a6176166b0112f1dec7ed9cec100950fed6956966a114a700000000000854d0000c3500000c3110000000000000017f018147369da11f17dbfdebe6329cef69ae7150b9003b2a03efda39443b9958f8154c00000000000854d0000c3500000c31100000000000000180018293127c2a56004f8d0a7a8fe5866174cea89697bf41df5e122ce7a23215e2d60400000000000854d0000c3500000c31100000000000000181018325e789b4b54bce1b239c8f012e156269e109a865bb46cf6d615412a6bd1e060a00000000000854d0000c3500000c31100000000000000182018415d04d386172988a10fb74a66f2edb3798cae4ae180cbf1b110e1ce447d1cded00000000000854d0000c3500000c311000000000000001830185d14b28e04b1008b7716368751b7281fd1c56efb14c4bda843b2e3e56a6dd167b00000000000854d0000c3500000c31100000000000000184018608135c514ba440467ae403e0a65e81f359ae4241789416377f7502ce739f6c3000000000000854d0000c3500000c3110000000000000018501874d1153cb1b5b24b1d021059395131e760ff239f5ac04487fabed4265acb7f43a00000000000854d0000c3500000c31100000000000000186018859c506c7f1424e8f6a02cbb4b0488a8253ed13971bd35d9857273fc03d9c2e3e00000000000854d0000c3500000c311000000000000001870189defb781b52260a9d6fd63130f1b8d6d603e4247eaae108d635e2e5bc04c5382e00000000000854d0000c3500000c31100000000000000188018a5335d54f7383e77089b8fdd6652f3fef12fb0f70fb2a84d2affb193bb1b7095f00000000000854d0000c3500000c31100000000000000189018bbf3e5a3c2868086d150869804d22d5da63dba8992833908943ce66f90dad171a00000000000854d0000c3500000c3110000000000000018a018cfed9687c86989821940a117f7fc601ff678677a12178775caca21a09249da22a00000000000854d0000c3500000c3110000000000000018b018df8ce826220eef839cd84d2b9df12a0f0ee5806b4aa19c173a92308a9f084543100000000000854d0000c3500000c3110000000000000018c018e1921efda69bc2cf34f1e47081b44b6400291eb927f4980a94f3134b111a2523200000000000854d0000c3500000c3110000000000000018d018f37c353aaaec137e0c9db2d33427135e6f8473ea35384b6a3eb206af450916ed100000000000854d0000c3500000c3110000000000000018e0190f3a2a2e0cebabac99e3d67147da573f39fcab1746d2a05d2e22a060af92efe9600000000000854d0000c3500000c3110000000000000018f01919375113bf780682741b62602e90cb9b3a50132ef3443924c0b93b7f3c0ea80a600000000000854d0000c3500000c311000000000000001900192b5085704b4b6b823059acda09858529e4e9fb542b52c668b392bc2900378b07900000000000854d0000c3500000c311000000000000001910193da08774a6ff7e6968eddad0fe921d39051caaf3c2bc2aa047102baa574852e7e00000000000854d0000c3500000c311000000000000001920194e6006b57c070a434ba17e69fc30dfdd88fc3faf7c00465df22f22fc575051dea00000000000854d0000c3500000c311000000000000001930195eb004427e0c3795eeb74ae52d8eb908d6395d93e90d93f113a941d728c7a186600000000000854d0000c3500000c31100000000000000194019625cce4babf6d48d362e642b15f5234e346d0ad56e01eff852be1d4bad5da4d9f00000000000854d0000c3500000c3110000000000000019501977de1734b52094a5045f4d3a962b0811c40148ab37a8cb8949ada98b9bb2ef98e00000000000854d0000c3500000c311000000000000001960198f6bb35d733dafb8788f89a35fc734c6fae0c79c7ef79d9ab79a619331d5991be00000000000854d0000c3500000c311000000000000001970199f608ebfa1bda8ea7044d7614459001856762aea6fa2a8da8a84f63b35b784aa900000000000854d0000c3500000c31100000000000000198019a5efd4cd712e8d819797cbdd2e5eb14ab8479581992c5cb4fefb71acd21259c9800000000000854d0000c3500000c31100000000000000199019be469abb620208408b16c28a5b22a408e86745059c187e533738786c8321e62d600000000000854d0000c3500000c3110000000000000019a019c55b7d42391295649268e33972b9b05e3863b06e927b1b6821f6be5c22cd5b32300000000000854d0000c3500000c3110000000000000019b019da71224eb2c47f79b4b7897332b4edf21831b271dd31265ea82a56ee423fbc51d00000000000854d0000c3500000c3110000000000000019c019e625238678138161a8a222110d7fae260b0d53dd718d8f0bbebc6dbf9f7c603d000000000000854d0000c3500000c3110000000000000019d019f998478e02c1ccb514f538e60d2d42bead26565df3239926c7ab2ba9e2556375c00000000000854d0000c3500000c3110000000000000019e01a0cadb273ea32290f6b387130461ebd022c6b559d4485f2dded4dc866794b1952e00000000000854d0000c3500000c3110000000000000019f01a1dcc463d2f12315dd333110aec4216d3ad68442ea011ebbf65cc03d6fc4def28d00000000000854d0000c3500000c311000000000000001a001a226af9c7b0ad6986d2014ee1c344720f7e5ecb5912d4533e3db9cec5abf3766fc00000000000854d0000c3500000c311000000000000001a101a337b0243c9147d7cdb24ab677cb997793184b4c6cbe078a2339c7050553debf1000000000000854d0000c3500000c311000000000000001a201a40b65bf566728aa8f0bd3b5f8b03efad99fe4d86570c3792a009bbd23aed8851600000000000854d0000c3500000c311000000000000001a301a57c0f33fb634f5b1cfad7cc130da55119a2fc282e4975d40204af4084a9df275300000000000854d0000c3500000c311000000000000001a401a610d4665d3a1c11e4bd872b0b47d822fefc75e8010f2766febba4206548da19e400000000000854d0000c3500000c311000000000000001a501a7e5998fdf3f51aedac8c428ebbe458f8039f8da6d996381998888846d7d7eac9300000000000854d0000c3500000c311000000000000001a601a84aa1e7b220a2ccb3d28ae1fc383aa4319dcb8df1a554f98e360ccb1b630674b000000000000854d0000c3500000c311000000000000001a701a90296a7c4204e0380ac993850b4041042fcff74542ba713574070c2982e5fe06d00000000000854d0000c3500000c311000000000000001a801aa1bcebf698d2ebc669f1e2e3ebbdb2bc9745616b0dddf8503965e9ce1bfbfbd4300000000000854d0000c3500000c311000000000000001a901ab17ca3153cfcf2c91ab678671c0193785a38fd13243f23bacf712297c1c3161df00000000000854d0000c3500000c311000000000000001aa01ac42aa6f3b6b844e832587ca38676960f99da8a7bf1cd0861f9476e8148056dc7b00000000000854d0000c3500000c311000000000000001ab01ad1677bf1f8138392b8a896be86688d8e2a852c94abed179d515ea75cbb0f0114400000000000854d0000c3500000c311000000000000001ac01ae5d07f5c149c59191cf2e949908b485c269c7237df230bcc66bff9d221b912af100000000000854d0000c3500000c311000000000000001ad01af4af01101f2fadf75503b90fa5519fabc113be039f7cac24d6a425ff6f4d46c9100000000000854d0000c3500000c311000000000000001ae01b0b75ae35825e628b1d386a81c380eac87e5063ea2fb581058a44e67647606faec00000000000854d0000c3500000c311000000000000001af01b1b3ace4f6e0b4d2504edf42d9fa09490ca53c59401c5ac8eae407a0f0347dc2b400000000000854d0000c3500000c311000000000000001b001b2da0319bb42c74cb71cc24608d303045b7c66b21c9cad1bcefa23190ea9008afd00000000000854d0000c3500000c311000000000000001b101b3e61dacb664d45941c172a551ede9e667bcf095113c15bac9217e1069c4549d3a00000000000854d0000c3500000c311000000000000001b201b46fea6aac4b9e2c07142170421a6f4f81ba464c8ddc49887d9f0f7826314b772e00000000000854d0000c3500000c311000000000000001b301b51df4dfc3f008e8b2348c48574f1c8e555a987162ec8347d793d0ceda160c952800000000000854d0000c3500000c311000000000000001b401b6e793bdc7a9b90cfd1a09081912fe24f588947146e99c46f58754cb16f81f6cd900000000000854d0000c3500000c311000000000000001b501b7ce19f510bda9ca936f1ce98af2c35aed4c87e98c38927a095b9b94f741305f9400000000000854d0000c3500000c311000000000000001b601b8c7930f425336449c2c3208d0e0b74d37e1eb5c1c0311e9a77bf00bf103daa2df00000000000854d0000c3500000c311000000000000001b701b9c7d9c3fecba42d41c4ceb332133da641fb6cdc531897be0caabbc3261cac5c5900000000000854d0000c3500000c311000000000000001b801ba871ca2ac3fcb9449b1cd3903798aa08e0e751d505d9aadec2d2b912094dd05c500000000000854d0000c3500000c311000000000000001b901bbd1dac0d405505faf085050662a9b20fe2e3398fd01a9b011dda41a55c740485c00000000000854d0000c3500000c311000000000000001ba01bc8aca06e52f2ee6f62f413b23f3bac9b4da3e93a2fb788f34c085be0128a293e600000000000854d0000c3500000c311000000000000001bb01bd50562444dd139b2bc45aa2064e6f945e1b4ff89ceef30842195d7e3766b5a25400000000000854d0000c3500000c311000000000000001bc01bea3112af2adcf6c56ac46a4cbf93312e2b6b7ea1848185a85273a7b11251c217900000000000854d0000c3500000c311000000000000001bd01bffa802b543ca6922c0656320b0761b883b9c1d75228ac9a7e9626ec76a7c8482000000000000854d0000c3500000c311000000000000001be01c0b2fcc13e833dd5bdd5d3c8ecb1745f50d831619c895ba3dcd24597bc52e0f67500000000000854d0000c3500000c311000000000000001bf01c11ddb4c5c55ed2746c69088f47da9cf8450cae2cba98e8a997381709e234c811500000000000854d0000c3500000c311000000000000001c001c2f88327f3873e70e5742dba0a8a41ab4555ef75ccbd49812534d625a38086465c00000000000854d0000c3500000c311000000000000001c101c3877f5cb86d90f3acfe194d244ff85604036251e73be7453d043470e8e87d1df100000000000854d0000c3500000c311000000000000001c201c41b6468631457a999e13d8556d28875f80d475e4080a822606e53ab4521e612fa00000000000854d0000c3500000c311000000000000001c301c5e27506829355cd7edd93156f5d3d78038a3cfa19d1b94502097914558d7a99d300000000000854d0000c3500000c311000000000000001c401c6e922eb6004f428a319dce0b00be94d9d65e8a891e0da8092cd17eb57f42719d700000000000854d0000c3500000c311000000000000001c501c72cf19e8b9cdbbce8788a37b61803d8c5ac9b705b451a16cf3fd4e44c98a6978400000000000854d0000c3500000c311000000000000001c601c86d4c389a327e75e07e26dde46d96ec5f9cbe652798c6e4c4f071e518ca431ae400000000000854d0000c3500000c311000000000000001c701c9ea874c9bfb2c727a572b1bb77a3911ddfe4890f0f4cc152b67e4490953fa741300000000000854d0000c3500000c311000000000000001c801ca3078da4bda050f5f723492d3f63a9f465a3f8352414b9b24747052268e8fd9f000000000000854d0000c3500000c311000000000000001c901cb0cbf24109afca71dc10ec95847ccd30693a340ac17a1c438de32c9d5ec779ce600000000000854d0000c3500000c311000000000000001ca01ccd1eebb68e4560bfc706060c2557d03e5fe55e5c76f1b0ef8e8b9102af101e1b600000000000854d0000c3500000c311000000000000001cb01cd60f5505127a1c513a7bf51a506bbd38413db23366690dde3b7a64ada8b38c3bf00000000000854d0000c3500000c311000000000000001cc01ce5f2a77c993b3c8c2860b9b6f661657e2ed9c3fb8a7f07b9f86e6908ff61e504000000000000854d0000c3500000c311000000000000001cd01cfbeeced096c0d29cb3117bd673c5a13fcc06788b4db4d7d24a2bb143cd675772200000000000854d0000c3500000c311000000000000001ce01d093861f3b9c8f22a3fc7f39be4019cbee78624119caa0ac94b509b691e516835c00000000000854d0000c3500000c311000000000000001cf01d13083a24fa948cd62be1d683b9cacaba9117c6a95ba3af20f8df2bedefbe3456300000000000854d0000c3500000c311000000000000001d001d2d12e24a4fc1c9116fc4eeead881d06ea1a7ecfbe5b50a60da676d03906784c3500000000000854d0000c3500000c311000000000000001d101d35c572a6639f35bec2e4ac4d0afad8f0b1a5b56f379ba18bb0f5a2239bed5684500000000000854d0000c3500000c311000000000000001d201d4fe0dd02a29705d5066285c49d0f62b8b7703247af1ca3f3ef80c72d2e73d4b0900000000000854d0000c3500000c311000000000000001d301d5270a3d63c0ee9b56b60d8422820e7d421d7cb5edd90bd36f171f3a248ca8ba2200000000000854d0000c3500000c311000000000000001d401d6a0ad7dd4c1094a8d2cad3b5f58c2f6521a82fcda6145f50a67031c99fac5ee3300000000000854d0000c3500000c311000000000000001d501d7bbc593f697857610ae607fcc8aaadfbfa9ccc8b666c1610fe58638c4b81b39d600000000000854d0000c3500000c311000000000000001d601d82ef8ef79e3f3125048e96e178148b987ad8ce75ddb86f226c9422f88311a1e5400000000000854d0000c3500000c311000000000000001d701d9584d870aff1afb86ba69ddc59fe8f846915e32928ec2b41ecfdcec2ddf6e034800000000000854d0000c3500000c311000000000000001d801da3f220ce62505abf5bd046d810d0a73121491fc5845984c6fc49d95fff620acf000000000000854d0000c3500000c311000000000000001d901db239fd782432147be7b01479d1bbf685ad71220496c5fbe49751f43661ad97ffa00000000000854d0000c3500000c311000000000000001da01dc452dbbf443cb66f47d94219064c1e11d02a35eee965b7f853b387676a12fc1bb00000000000854d0000c3500000c311000000000000001db01ddc92ca5e3fea2af39b5290131d48ae5ac6b8b3da7f8939f3416427aa7bf23c49500000000000854d0000c3500000c311000000000000001dc01de532077ccb94f30ba0e14ae25792e182fd3a9c89a136112a7875a582877d067c900000000000854d0000c3500000c311000000000000001dd01df994fd79bcbb666c872f3cf82331b4890fd26895337b42a7f957e3da716d9dede00000000000854d0000c3500000c311000000000000001de01e0b9563d83c0644780a2f7dc8441b391e23ad1a9d86a23b965931c13d66e41314e00000000000854d0000c3500000c311000000000000001df01e1a356c15452b8070d12716b40768018b3315c33b00a40c54fc825a77e16189b5800000000000854d0000c3500000c311000000000000001e001e250d2f5e73eeac5e1263a0df8a53ba2b1deb5b8e7815c61bd8d70015ed1912fa200000000000854d0000c3500000c311000000000000001e101e37498c3f78d908282db2e0ad1a117dad769c93569014fa2500d266959dc58f9d700000000000854d0000c3500000c311000000000000001e2", + "digest": "2f9eff37ac4516070d391bdf681aeb15fd21d78b7530174a8330ea906149f0a7", + "manifest": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0fe012e565964e65d584adbc7138f1d115f6e00c27a90903e6c1b1657982fe5cff61034010d3510c0a44a53e0ef4a086226bfa039708f944ce4da9833f1ad5e5f3000c0df07d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a233365035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c3590006d9257d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da6040101e300017305b11a874bbd995b29b11d109eff432402136904a3bab1f7bd9f869109cfc100000000000854d0000c3500000c311000000000000000000002cbf141d9ad3b41938c25083328862acf711291bb47ab53b33dc37898ef9326b400000000000854d0000c3500000c31100000000000000001000362e42ded68912761a397087a1310af972f9022330fbbbf24c580b9e71479fd8700000000000854d0000c3500000c311000000000000000020004754bccfbb8e3ee44224e0290582ada7cb1dcb05ad968df63ac0aa01e81298f1b00000000000854d0000c3500000c3110000000000000000300057c23775d75a9e3e878e5e33ba9cf7da51c78a4d54431bc1981c8524eda36bb0d00000000000854d0000c3500000c311000000000000000040006bcd5fb4815309c1de8fbc2867eb93635d8764c565d952917457874fc6eceda1800000000000854d0000c3500000c311000000000000000050007e3ae54a63b8f9526f7955db3417b5a1f6743b99ad0ca7cc92c3fb199c149b76800000000000854d0000c3500000c311000000000000000060008bd471262b242310a4d1b298156a5fc3469395eb2fa3d28be02b88d8d0c97081100000000000854d0000c3500000c311000000000000000070009397b027bfa95da000fac9dac5d41825bb54a0cd0071a262be00d6d4445f3e2e900000000000854d0000c3500000c31100000000000000008000adb17a978e9e05effc0d7f4c33f7fb78898deec1a0b44212bf943818091fbc09e00000000000854d0000c3500000c31100000000000000009000b0fc65290b128e63d598e654d4a7bbe14cc3ef946f6fb6fe0d5b46cb9e0e2638800000000000854d0000c3500000c3110000000000000000a000c3f951c45254175c92549460b5a89d5d3c61a950e7e366f992dac9f7f7a7e6ce700000000000854d0000c3500000c3110000000000000000b000dfa9d083023788850024c7eb588fae1c7542604d23cc12f44ddd568c2320a754f00000000000854d0000c3500000c3110000000000000000c000e3aa43187fba488b5530dbc366257f82058801f6983509716a9e9bf00db680f1800000000000854d0000c3500000c3110000000000000000d000f7404ba142c5ad30fb6769e7f6a84843d4c437bd50254a72815c8b75c1deb0c4b00000000000854d0000c3500000c3110000000000000000e001028ced94a2fc573f8c251838edfc807be39d1b910d18ede82aa7a127c5888b3a300000000000854d0000c3500000c3110000000000000000f0011a6a8f52194b94c6d0b521756e5c2c20ab5b6da8a2483e44aeb48447112c0680400000000000854d0000c3500000c3110000000000000001000122014e1e38afe84f6da72dbab22108ec05f60555504bac35bbfc7c62d1ac3621100000000000854d0000c3500000c31100000000000000011001313667fb8817a03dd2f5fd97325ffaec561f597256d58742291c569ee251257fb00000000000854d0000c3500000c31100000000000000012001430ec738e4babdd6d0b18b82a7f5a7415074cd283260197c4efe5d439671c9abc00000000000854d0000c3500000c3110000000000000001300153e9e411c4bcb405bda68f31ef974cf91e5dc7a530fa8580048c4bc742805125a00000000000854d0000c3500000c3110000000000000001400167aacb62cd9c823ddf004ca04fb936c3093029ddceaef569190534039885b1b8d00000000000854d0000c3500000c311000000000000000150017a9e7ea66fc54f78b5a4441c5ca6ebaa742485894509dc379783c52d881fad9e400000000000854d0000c3500000c311000000000000000160018063c74693de53572427dd71747f7a47f95a72b83b95f62b60be7479516593c5300000000000854d0000c3500000c311000000000000000170019454a89ae1311664f5155c45544265e5b33911a120db3b683522ac590e64528b200000000000854d0000c3500000c31100000000000000018001adb1d9b5644e6d6b7befb0f222e567bef23ec977fd3d5a6f146b6b195f48384e400000000000854d0000c3500000c31100000000000000019001b54755b708745f22c50b1ded9d0f24bc9b87d93d95046accde5e0d0c1b27ee3a000000000000854d0000c3500000c3110000000000000001a001c1b29bd0c8af340c2ede7c9552fd9410b4c731a47156f54ae2fe2ef6ab170a3f900000000000854d0000c3500000c3110000000000000001b001d3b68781e8a122e02fdbade74d727eedafc80663a79c32726b7d555e730f6b00c00000000000854d0000c3500000c3110000000000000001c001e508a679821f8e0b736552664ffd03360eb8e08acc5ac5c653ce280885222ca8000000000000854d0000c3500000c3110000000000000001d001f599fd0cf5ce9487aae5390f0aa7e2f55bbf36c7fab020879c262c74f0659b80d00000000000854d0000c3500000c3110000000000000001e00201d35e8a88f39201d36bb5d8603822ee851e4f936cc9d941eda8f0c132be674ed00000000000854d0000c3500000c3110000000000000001f00218b0edea50f2efd820966577036831199400b94a6a5ddc1efde5caa77d770fd9300000000000854d0000c3500000c3110000000000000002000228267950cb8f56aee3a11f312d601c4e2f49e229db76eebe9df97b467478fe80400000000000854d0000c3500000c3110000000000000002100233e52b8a3ff4d5f870fa11ba4e1a7ffc5f35310840f33f01da137e5802ad25db100000000000854d0000c3500000c311000000000000000220024bb7507ed8a6155c9d7d58e9798f1822e143da862250702f237267f91bf1f6bc300000000000854d0000c3500000c3110000000000000002300253dbe9c22ae8f8733c3a60d21c30bf7e51a304f53bac7c4a2c6e3cbe61157679500000000000854d0000c3500000c31100000000000000024002698a89d04d4471120348d82a09c6588128fe927d38dfe26d98932961fc842764000000000000854d0000c3500000c31100000000000000025002736a20a0e1e99e0e8f0f9d389acb4ce07130e58276e847f52bf8d17c2bf36fc3300000000000854d0000c3500000c311000000000000000260028f5d5256f0c02a8e76d9de795b0a9a6624e694f2df4d106bfe32fa46b60400a3300000000000854d0000c3500000c31100000000000000027002908853d9812b63820a7c738e5fc15ab6181cd391a059c248b700657d4b21b4e5d00000000000854d0000c3500000c31100000000000000028002ab49ad03151b291227dc42af60dd203c373a9533afdc9875f4c02d08560c4533f00000000000854d0000c3500000c31100000000000000029002bdc43a956e852c354169466af263dd38c4f278c18fb54d209d9b65ef1828cbec200000000000854d0000c3500000c3110000000000000002a002c1c3da0878d813c406978c150f03595e9a26d582ad426d14ef279edd0e8f452fb00000000000854d0000c3500000c3110000000000000002b002ddccf935988fc435b5713186e04f48e53fe514466473a48ad5e0afad0d22fe46400000000000854d0000c3500000c3110000000000000002c002ee113b733aec9e1d33dfd95556e546977a6b907fda9164c86f41dff868d4a2cc300000000000854d0000c3500000c3110000000000000002d002ff6fd342210a6de76e74742ecb52b84f482aeed8d2e0b0a843cd8167b4554f5d600000000000854d0000c3500000c3110000000000000002e0030882620e0b4f1feee1c51202c3b9623135d41499777d1341f63730f98dad4702c00000000000854d0000c3500000c3110000000000000002f0031ba08838987d86618a0dc670de584e3cb9602dd2f60278ea817a31d7f311e634d00000000000854d0000c3500000c3110000000000000003000323f4fce46408992a463e8d153537aa72ed50c22cd6c76648191a9cb4b43de2b9b00000000000854d0000c3500000c3110000000000000003100332f030755b807abfb879b08abe7a8b15adcd9efd2f419214fa5d02d099fda961000000000000854d0000c3500000c3110000000000000003200346918f0ebfa087f6b72399aca062f7bde3577c44fd288090b9ff2718fc6f5cc8100000000000854d0000c3500000c311000000000000000330035f70d71f8c06e27b949b91c969119a7cb85a4779bef0397a4d2a6847b49d128dd00000000000854d0000c3500000c31100000000000000034003631f229a547af63a7d20a9603285eb3793ed667b25f4a22bd8cd94344665f31a800000000000854d0000c3500000c311000000000000000350037d872bb09a3b355b08b3bf759b436cbc36f6d645fc01dccf919454235b8cf509b00000000000854d0000c3500000c3110000000000000003600387bf97f96e2d82c0396dd60d8a32ca53a9034f4fafb590a6f8b3f02e3d16a26bd00000000000854d0000c3500000c3110000000000000003700391d44995dc923077293887ec5dab71deac2055a37fdf727d5a2c0d216bbb5a25f00000000000854d0000c3500000c31100000000000000038003a6fc5c81c3b0c8b25375f88a324a38ad92d72a73d2771886704e24ed7ab53595100000000000854d0000c3500000c31100000000000000039003b64a7f4d976600e0d3670d51fd1f1a08937a5247cd7314e78fc85a6c692cee5b100000000000854d0000c3500000c3110000000000000003a003c304c70efe0cdf3be30823623953ef905d37831a080ae3ae1d0c261f7921ab76700000000000854d0000c3500000c3110000000000000003b003d52f4c9863d3ae76c166247d9225b19a416ad39f7f46ff197bc95f695201dafe600000000000854d0000c3500000c3110000000000000003c003e76fbeb3ff3afa2700541928f23f11d59f8a56d40abd6b10086d70ff568c1b39d00000000000854d0000c3500000c3110000000000000003d003f83a811fe4aa924595c69a33b636f0410f8da6c59683048f6a63d011eecd0fc7400000000000854d0000c3500000c3110000000000000003e0040ab8a8b2d7759b68431f7539de4e7c40fcf22a15cbb3d472dd20a31d690bd696000000000000854d0000c3500000c3110000000000000003f0041c1305b0e61a9ea2da6d693234a815de4e712aa7a38fecf60291bb0e0af44276a00000000000854d0000c3500000c311000000000000000400042434b51045e65ddc0caf75b30495e146c0076fc28951d7396a23ecff1690e2f8a00000000000854d0000c3500000c3110000000000000004100439df059ca0da40a29d8afeebcfcd02cefa7cdef598a2673065f7d55b6617884c000000000000854d0000c3500000c311000000000000000420044ca418d14c97d01e4c0aad582a5174c09a21638342681bbcdf02ef3ff5f56cab200000000000854d0000c3500000c31100000000000000043004537594556a4f9feebc7b9efaf166d36bc7011be23886a2c89e590f9ad2a4665a500000000000854d0000c3500000c311000000000000000440046ad51420b09a07509e900aaa4fcc6cf6bace255fef4d89c31727806446edbba5100000000000854d0000c3500000c311000000000000000450047c733ad0dee32d9a52a4d62c9c96186e1afd55d35f54ddc21fa980f771e17fd7f00000000000854d0000c3500000c311000000000000000460048464b71d7e34e559d518b268791aa82b54cec4fc2aefa34cc66fe9f21eb90948a00000000000854d0000c3500000c311000000000000000470049cc2d776c5d23a365ffcb4604ee65a09cce259c871dab39b04939d4f3d2252a3d00000000000854d0000c3500000c31100000000000000048004a6469912e6512ff7a494d00d8ab8977f1378479cdd5919a687a6b4912af46667e00000000000854d0000c3500000c31100000000000000049004b378f6cfb89eef63eaf08aa5d3f5d83c90698ba63169104be2a28e46f9d1ccca700000000000854d0000c3500000c3110000000000000004a004c09e07c300817fd060a676575d6376ea8deaccbe2eec3714954f953826781831c00000000000854d0000c3500000c3110000000000000004b004daf5d250305bc69ba1cd8af664d439f6469f65361e3f5fe870b765f99bf9c840a00000000000854d0000c3500000c3110000000000000004c004eaf472cbaa57978e18a7beb1aab7b3231d10ca16d6900093ded5c8ac76aee2fdc00000000000854d0000c3500000c3110000000000000004d004fd208f7cc7c79c678fa3d3af63e43f8145e12c509786de6d484f0fa9eefd4df6500000000000854d0000c3500000c3110000000000000004e0050e28a412244c366997dc906ad005406191314722626a355dbbaa982075b3195b900000000000854d0000c3500000c3110000000000000004f0051164032bafa294351f3da469060dabc9ed49a700436b28e33ba98eb19575777b200000000000854d0000c3500000c311000000000000000500052f7c31d1c01ea89f750fd13cea775b223d7b33fb72787d312a5491919dbf1ccbe00000000000854d0000c3500000c31100000000000000051005325364a6509a5cad019874190a2d2575fed1814db0ec63349e0d45da1ddef18cc00000000000854d0000c3500000c311000000000000000520054d67bbaecdc0ceda6c4c73f713e4f2b0720eb3d477f514148f2dac77cc50ea51000000000000854d0000c3500000c311000000000000000530055f6bb6e89201ad4cf91179cb56daa171bd244803fd2d2552f81d9e8613b9a399100000000000854d0000c3500000c311000000000000000540056e8f85ca405596250eaa741554d424ed40359b7bc9a29df7116ecf508708d8a7000000000000854d0000c3500000c311000000000000000550057c87b12700480cfac14a3b6abeee0ce1529d0c8dcfe84a8ba767e97662476653600000000000854d0000c3500000c3110000000000000005600582caeafc4b9bf93574c0f8d4bc94f57a85d9f23efc58c769e716f0d7b18b38dd900000000000854d0000c3500000c31100000000000000057005969126f4e713f05ba71906ad30b5673030fe0d3f82d874741e2dc94bdcea49e1000000000000854d0000c3500000c31100000000000000058005a2bb80bd902fbb168b3121206e194f44db5ef0cbaaa3198ecb971466d128f175100000000000854d0000c3500000c31100000000000000059005b4082cad26a5c16ddc9f61d81b789669510a05281c338443b02ba356ea887c02f00000000000854d0000c3500000c3110000000000000005a005c265d9a20fafd71d0aa9696c1aaa52a8b570d3cd3aecff66d6a2ccbfa71e14d3900000000000854d0000c3500000c3110000000000000005b005dae6967ae7b6549b26551f919039be40948601cf75d580bd8a7273102944d505d00000000000854d0000c3500000c3110000000000000005c005e98476373841a91019fc58c72deef72db55794552bbe06b99a0cc70b545c257c700000000000854d0000c3500000c3110000000000000005d005f82011b8a630ed5a1d31aed2f71d4007347e50c49d0273abc634d77b7fd732c5700000000000854d0000c3500000c3110000000000000005e00600ad83b5a1329af2453f9a24a11729b565921757b15b24342cfa0cd68926626d900000000000854d0000c3500000c3110000000000000005f0061e98244eb7266bac47dfe83ad32ea0aa6c2a71c6c6efeeb6fc94dbec2888836a000000000000854d0000c3500000c311000000000000000600062c0bd14f5ac63c549c1df54d26ebd93b57f93f3610f87dc19b15d6178e97a3c8700000000000854d0000c3500000c311000000000000000610063c56dcdbbffe2ac6cd828e1885083843d6dff6883b91a547ed66ab4315d7e3bf700000000000854d0000c3500000c3110000000000000006200642e89e3b0df85fbe1eff77b8841b90b0fbc491812b75ac167e0df33b212447b5300000000000854d0000c3500000c3110000000000000006300659ed51626c3998d5bb14679fe39d2fc267375317eec386d12d9805e45bd9082d500000000000854d0000c3500000c311000000000000000640066071eda545451d9d4b94201fb5b5f6623d970d3839e46f870d7fa3bf2d2cf7ece00000000000854d0000c3500000c31100000000000000065006793865c1b6f4070c9bc30c2f29286d8eea729414d2828c3827d7add206c22b5d800000000000854d0000c3500000c311000000000000000660068fdf7662574899c10d0b4b4119824dc80a26595b4b44284ab1a6d95bf48da05e300000000000854d0000c3500000c311000000000000000670069f6e8deab009f70f621e12f36a033dc1bf7ce211100ec76ebbbdfbe1a8a296bbf00000000000854d0000c3500000c31100000000000000068006a3c96ed2d4e91b99750437ed50e90544e3ace2456c7fdef5e6d81d1c69797041200000000000854d0000c3500000c31100000000000000069006b64507da352e9f70ab879dbc4a2aa073630b6d61dd0e1eaf5bc215510ae551ed000000000000854d0000c3500000c3110000000000000006a006c523085244700d8bc42a08c99dab05caa6d4e00a0a3e974355e6a5cadd6a8604900000000000854d0000c3500000c3110000000000000006b006d843e8fb4fde1fb78042e83c43096cc68ab22b2d41b945781c674dff62520d3cf00000000000854d0000c3500000c3110000000000000006c006ed4c3b0a4e860f9d09f8e28525c44e2b32684d8ba68228a3f1f79db0598b2eea700000000000854d0000c3500000c3110000000000000006d006fbfbb7a54fc65f2cc087f91d4a54e89c6a9189994fdf807cbace173f150527f4200000000000854d0000c3500000c3110000000000000006e00700940c82246dec163f6bcea0066dc1b8e42153a9a89cb87335bc874fef74954ae00000000000854d0000c3500000c3110000000000000006f007110fe330651d32c31fcab4753b4467cac95586300c264136f8f86aac3721fb16500000000000854d0000c3500000c311000000000000000700072a82c392b6da093a4bd54e33100ea7cd77b5bdcf688b1fb691b474a10e0bc363600000000000854d0000c3500000c3110000000000000007100732f743009fa6915ebadbfbfa27399c3a30f9b29b48e39e8e76a6bef920de3a57700000000000854d0000c3500000c311000000000000000720074815323725370ca6a62de58f815237acb314bc3c4ca763f5c260923f0dd2b4fcc00000000000854d0000c3500000c3110000000000000007300756727a4ae8c2ebb1d033eb969abd7476e90dc6b40b54f75b5f4d6d755679f6c0900000000000854d0000c3500000c3110000000000000007400767e07701c7cde480625ca923bebaa832bc17a6abed269b7b1d3601175c2ff6cfa00000000000854d0000c3500000c3110000000000000007500774173d50ab54b95deae539126064ca1a69a831ac4bfc2c9cc419c6f1da7572f5100000000000854d0000c3500000c311000000000000000760078c8bba7fb3d037affa75e8ab2b6784f404e9ef50f7a76b9382874b24086d5b5dd00000000000854d0000c3500000c3110000000000000007700791cae9402e611c095147a803f5b408d27ebaa9344cb951fb073666fd308fa557400000000000854d0000c3500000c31100000000000000078007afad15542780583e26b83691532044687dce16a82ea097c13d339da899a5ee02a00000000000854d0000c3500000c31100000000000000079007b245471ab91354bcc2c2bb09ab2a2b96840d5447ba9e3b9db177f63bf1894ab7500000000000854d0000c3500000c3110000000000000007a007c9a09a07adac2cf5ee978ef0898161ab40e666963976b0dcbbbaae8a0d7f8b28000000000000854d0000c3500000c3110000000000000007b007d14e2ab39cda053ec7cb75fdda214d03f85c30d9d137083645ec85d70119906c200000000000854d0000c3500000c3110000000000000007c007e768ff392d5d959b68e4eab28b75c719679c43ccc65060d8d9d1b55c56e410b1200000000000854d0000c3500000c3110000000000000007d007faed573b2efbc90729cc17c11f1947952c29ba79945746be3a3635cac70dcbef900000000000854d0000c3500000c3110000000000000007e0080b5f95315fb041ea28586d613d2a976d82d348d960989c7b9416fb3d56104da1600000000000854d0000c3500000c3110000000000000007f008140d95f0c5da8d32977131dc41a05a92f1a6876ca043a638240c2b1c8521f111400000000000854d0000c3500000c311000000000000000800082e58f57a865ff75144bdd08549c83c656431b680ab2e57f55501308f826216b8300000000000854d0000c3500000c311000000000000000810083d7f2320258354c20a9069262030e08e266332394343063bcea0cb1869f329ea200000000000854d0000c3500000c31100000000000000082008467c32697927595a930d79d6af1b4d0ef7eb48eca60a66b0b9295e281b055345200000000000854d0000c3500000c3110000000000000008300857954935a783c0293c7338c17e95ca0e84642aee0b1357adda4264d19d6dfc5a100000000000854d0000c3500000c311000000000000000840086649f6f69ab5b4062727281bfc562602b8d23866140ff820bae3629c28d2b68bb00000000000854d0000c3500000c3110000000000000008500878646a8a5d0c0a6d6abfc5db6d6f7f0675e2e502248d2c9868363efc858272cc600000000000854d0000c3500000c31100000000000000086008837ccf66214c602f0155d3350d8410b27f835f10d7ed7480d38697ee5928580aa00000000000854d0000c3500000c3110000000000000008700894dc5c25b4e7e181bacc5dc94f96cea194f4ad582ff9082e49f43af951f5f2c7200000000000854d0000c3500000c31100000000000000088008ab4a423a735be5b23a37b7e502b1d730cdf511b5f5dcd7e3dac491dbf42454bd700000000000854d0000c3500000c31100000000000000089008bd0300ea2db54c07c732c435c8b390eff7af09d1fec7d4183f26c11af6f06ae8400000000000854d0000c3500000c3110000000000000008a008c06eb3dd61b8993afd0aad49eaab19f9b363acc565df1eee863fd0cf189ae9e7900000000000854d0000c3500000c3110000000000000008b008de2f3402d5ec19a1b0c2df4f3bb8f8375692869e820a1e4071468c85bbfd561e100000000000854d0000c3500000c3110000000000000008c008ea4210d942e844c1b72a9418b1b4b3c4b0f6e514fe3b0f75e3d7df746e95326e400000000000854d0000c3500000c3110000000000000008d008f00608445dec5823810557eb56baddb365cdba4e465f2498abe69cd50b0333f3f00000000000854d0000c3500000c3110000000000000008e0090cd2a4d3bb7d1b6be07dd1f3c90d3cca5f5b3cedb97379673782f5afb23c3020000000000000854d0000c3500000c3110000000000000008f0091254a9a75cdf945525320029f7e8c5aba45dfd5b4a426f346603348a66b7dd45300000000000854d0000c3500000c31100000000000000090009225074aff4e140b03cc239486412dd0a130e3ec93b2648b95569ae55e5782af0500000000000854d0000c3500000c311000000000000000910093d8a8b5c8d165c407816b8eb5e89adb271a1078cdca4f29315161593ec86f769500000000000854d0000c3500000c311000000000000000920094aa968f2a69bb072521c766134d9c1521b5d49a368b0dd00ea3b4e21883f7c55600000000000854d0000c3500000c31100000000000000093009592d8e3576e981f7ef9fafcb12635311778fa6e9bd7544bb502e310113869bb2b00000000000854d0000c3500000c311000000000000000940096fce131a771b67463a1bf5b20192031a7780c14e21a74d4ac5a0c4468a45998ae00000000000854d0000c3500000c31100000000000000095009752267d49a6543212b263beea088dfe20227f97d1cc1a3b935bbfa0e11e47548400000000000854d0000c3500000c31100000000000000096009813496ffa2340f37f578504f289f12a5fc253e2e534f83ed2878e342bd0187c8a00000000000854d0000c3500000c311000000000000000970099c168e856e6948c8eb949d48e53f1c64266341a91c7995547f441d3f2bb7db66e00000000000854d0000c3500000c31100000000000000098009afd09c7a336cc28e97b5ff1e5c83d05c3e68257ddb2587b0c20338833512dcdce00000000000854d0000c3500000c31100000000000000099009b256bb41df47f9dc594b20b7aed566d3457cd3feab6a2ac54021ac61a65b74cd500000000000854d0000c3500000c3110000000000000009a009c1304a8cec2aaa0fb13195fc09a06d571fcc8ac066797be25754ce7da18783c4600000000000854d0000c3500000c3110000000000000009b009d82e93cabb272fbed2f0954226d7871dadbcc06fb772436fe4bd5e79a509dc33a00000000000854d0000c3500000c3110000000000000009c009e67f4fce731aef9e25e456361ebaa95b8dd448180b3780a57dd5e996e155fc12a00000000000854d0000c3500000c3110000000000000009d009fee0a1488e1db16efd197fe3307bb1ef3fecc70be961fb5d83f57293b2389742700000000000854d0000c3500000c3110000000000000009e00a0d8b7f44f79f3825b0898063c9be52e8430a4fc6556380971e45ba2bff87d41f300000000000854d0000c3500000c3110000000000000009f00a10cd85fbd33a658c370afb29f2c452a9c176e6b15b85cb999c849c3e5fcc3571600000000000854d0000c3500000c311000000000000000a000a272fb1ed9a9b17e17dc9252ab1b6c57842adef570518d2ddb9ca68fbd6cd6864f00000000000854d0000c3500000c311000000000000000a100a30e18954a79e2d97b37db47861208d7191933965b8a66a8fa2192c2f03706539500000000000854d0000c3500000c311000000000000000a200a4f2891d485e7fd8fb3acffbe35e13ee5fbc7ab4bd4b856819d6cff8e9b46054c100000000000854d0000c3500000c311000000000000000a300a5b17223b7dc16a717a0143c327abdf0ecec7a56ce9d4a42a187228d4764f3adaa00000000000854d0000c3500000c311000000000000000a400a6e664a80ba167ef9a42fe98f9e60997922edf02ab62b9515f3cc04cd742bbc0a000000000000854d0000c3500000c311000000000000000a500a71dc5c03a9ee981ff70177b153af6073d60b5dd62f266b099e2a9a4d4db5228b500000000000854d0000c3500000c311000000000000000a600a81cb37a70492764ccb4e974c7d238592775eccce4089735e69dddad210d68642d00000000000854d0000c3500000c311000000000000000a700a90821c9c1ef4a9e69d5f508ce86029fdc4601e2a1faeed4a8bbc0617640f5cea600000000000854d0000c3500000c311000000000000000a800aabe06ffb02722e4f7b8b02d33806acdf4fc6050e20ae54bb1955aafd407a8be9d00000000000854d0000c3500000c311000000000000000a900ab7518052cc5fc266960839ca65bbdcb933b36eaf9b2f61c37192e6ba5effca76600000000000854d0000c3500000c311000000000000000aa00ac9ea401167af230c9b049bd02d00e1636f21319cdab24c46937d04d8a4ddb41eb00000000000854d0000c3500000c311000000000000000ab00ad1f1b2f6031fd9c8380f15b2928f042e9645332d6e0bc2f55e06778108ece17ff00000000000854d0000c3500000c311000000000000000ac00aeb8410e7035d2c12ede9ebc2c6bc81af83b27d172db05b66b10c7c7f48c57222e00000000000854d0000c3500000c311000000000000000ad00af1b3fb423b0daed330e204543b2fab0fa9aafff31b387535ad322f5743c7b78d800000000000854d0000c3500000c311000000000000000ae00b02c9d6f58fd85711958fcfd2824474818828d5ecb641bdeed41c988b6eec5c22e00000000000854d0000c3500000c311000000000000000af00b1015bda7aab18ccb03f23a253bd3b94ec400a67d057cadeb092167e45001856df00000000000854d0000c3500000c311000000000000000b000b235eab6ff950e9ec80d4563255908a7ea09a5ae5dfbd0e3c2944295709ecd7e3500000000000854d0000c3500000c311000000000000000b100b3957cfa2365934f72f6559f95997c60304b0a98e3c9e0dc99ab7ea6554bb0a26100000000000854d0000c3500000c311000000000000000b200b41012ec8ec892231dbc8ce89d60f3bb9b375718e460e7b5bd1b8e84631e291a9b00000000000854d0000c3500000c311000000000000000b300b5ca5d940ca7b8fb92c4baecb5a6e464d3c3b5aa5c12e67d9e70650327b858ebcc00000000000854d0000c3500000c311000000000000000b400b69d5412f7b492ac0203d7a82aabe23f8aa7776d173df7da0afc42147476614d9100000000000854d0000c3500000c311000000000000000b500b711d3ab9e16a2c977acd6d131284d2fc8c0d1af94dd6191d6a84344bc4722135400000000000854d0000c3500000c311000000000000000b600b8eb9a2b0b9a696572dfc2adebd5f9177abda3769f294babd21480a6839801474b00000000000854d0000c3500000c311000000000000000b700b9078be2982828fed4bb25781dc9bccaa624f6058c722eac222b4a659e9489edbe00000000000854d0000c3500000c311000000000000000b800ba819151088a9e7aada910f638ca1a248bc94b4878b1031a41c101af8f35c9aef100000000000854d0000c3500000c311000000000000000b900bbf28de95ff9677f064bb695651008dd14631262188ce64c392f192d6efdf8d4f100000000000854d0000c3500000c311000000000000000ba00bc9ea9e5ffbeda1fd8846698475c057567d1f2c133c6d548f3e9722b3b8b9ec69600000000000854d0000c3500000c311000000000000000bb00bd0689a54e3457da2e4ec2a9b07cad6b256469eab859e2e8354b27bbb9f08c3ca700000000000854d0000c3500000c311000000000000000bc00be460f9ac1863a0d05133befb41a245dba3e136fe3e9de43e1000316991d135c4200000000000854d0000c3500000c311000000000000000bd00bf0006cb6b8ace8eb6f74e97e484bb283a123132eb4976f0c665de5394ed0b022100000000000854d0000c3500000c311000000000000000be00c07b38bdf8c89b9b0db4eb62ff10e98308e9ce8647200922d8d790b7dac81bef8e00000000000854d0000c3500000c311000000000000000bf00c181157bfa3420a29f5124ddf629161d0b986616e8aaac86278b838b7caadd850800000000000854d0000c3500000c311000000000000000c000c2b67b3796c8da1c71ac82a89c2fa7a8b40e2ee560c6df7ac42cc99e51645918c100000000000854d0000c3500000c311000000000000000c100c338b2fb62c7aa54b15597b8422142cd49a776092d55f92d5dad31e417844c63b300000000000854d0000c3500000c311000000000000000c200c41e7701adb1fc46ca8f1c24d0e24cc40fe7d9daff7deeb63211fbf5bc1c123c0200000000000854d0000c3500000c311000000000000000c300c5e2cb25607821cfb7e67028ef96056f6e3f81f72db1bf8851432472936bbc647600000000000854d0000c3500000c311000000000000000c400c6d9392204247dfded154f8863582e44f0acd0a30a3bc8a38ab35ab744b73ca18100000000000854d0000c3500000c311000000000000000c500c792e42b479904bc5abf82fbb467577afe7f53632f5a8e89be6791a36b49c326f500000000000854d0000c3500000c311000000000000000c600c84412ac310f1fbc90220101c29f748304f9496740c3eee14b64c8fc171558352700000000000854d0000c3500000c311000000000000000c700c95fdd380ed620c5abc8a4160f18375928c0ed6a926043eaf91365dfdefddef93f00000000000854d0000c3500000c311000000000000000c800ca0c8edc3204a6c47c4e9115755a24c31763efa78db17bfaa9645aabeb4972c89a00000000000854d0000c3500000c311000000000000000c900cb83e02383dcaff8714d79fc6df28563584f79be4435bb3632958eb98649d214e300000000000854d0000c3500000c311000000000000000ca00cce136d56b460bf32ae200ce0fe980817a2bbc78112c9969c70db679c2aa30d73b00000000000854d0000c3500000c311000000000000000cb00cd4cd753d127fca8a62042ce2dae9cf035561530b3e2e95f60fe74245ce293dfb600000000000854d0000c3500000c311000000000000000cc00ce2ee0ed4ba29122e130bfd9a32626fd46ac15ce11642d63e5c596b7d23d6b142300000000000854d0000c3500000c311000000000000000cd00cf92b59998dda902b1005f23746254e625f4a70103d02b0ee3cd7cf6f19425188d00000000000854d0000c3500000c311000000000000000ce00d0040c9ed7f1af1ab9d41814211ca770b40c250364d4a26bd0d57b4d6de3d406c800000000000854d0000c3500000c311000000000000000cf00d1cf54bb912bf745b8761c10c6ed566edc96e82ad8e7a9bc3fcd22fb0e3974cfb400000000000854d0000c3500000c311000000000000000d000d2a81886dd94579a2ebe3c14379490aec6fd8bc8bcd7981094f23011e1690bf6a800000000000854d0000c3500000c311000000000000000d100d353b474acbb9539a6631f51144498802e9eeb237d8ba7d11ac050c4a392129afc00000000000854d0000c3500000c311000000000000000d200d4c08ec36aaac6cb7b82a482059dd448f61a9b25249bc5d4e6be5517baf5f5e24200000000000854d0000c3500000c311000000000000000d300d5693de4032c7083eed7fb1803c6d5624a7e32d3107bd7a3939cc89a083388264b00000000000854d0000c3500000c311000000000000000d400d6f9756396a45f40c30da3a0a67d4dc1838b20a098e5351a1e4f344dbbe00dcb8600000000000854d0000c3500000c311000000000000000d500d7780b34031a24731ecf7e0e6844097a8e62b43267d565dd6d3a4886e55620678900000000000854d0000c3500000c311000000000000000d600d8bcc1624846430d2de342f54d7b8e61d264bfa39528a1fcc63532b291c994c2ab00000000000854d0000c3500000c311000000000000000d700d97a217a4e347a614aa9e651bd234eb0c3d49830a6a804e7ede79c77959ce14f7300000000000854d0000c3500000c311000000000000000d800da1d361f1504f6625c8289222276f9bceafa3281738e27e15f6547884c9ca7a5dc00000000000854d0000c3500000c311000000000000000d900dbfe2501594d57b4c25674b575507428defccdccfe69543a182575dd4817b9179d00000000000854d0000c3500000c311000000000000000da00dc45eebf5678f6a340be93a51d2e263e764839c2454a10bf8853fac76861923dc300000000000854d0000c3500000c311000000000000000db00dda5d81abfe6b8ac5e302b89b6057f782aad54a65adb51ed77e50d3a366db9d7a200000000000854d0000c3500000c311000000000000000dc00de0e6556de4b9b77a9d866d3bda9f4a87208766b0cc701414a4deaac8da2acfe5a00000000000854d0000c3500000c311000000000000000dd00dfbc0a8294b1cc8edafc68b3a95d9263f1499a02d63c93a0fe30633ad556a5b44600000000000854d0000c3500000c311000000000000000de00e0a1078aac0d34309b68c23dcc3d7da53e7f88c24e6ed76850021ab5ac808d6b1a00000000000854d0000c3500000c311000000000000000df00e1412173ca93c1a3db66b0695504c0151d21f1e65e417d5671dde59931256ac4fd00000000000854d0000c3500000c311000000000000000e000e2e601a904450d4f2e168e917bf4438d900cd17f571719d60bb900fb0e0740d65200000000000854d0000c3500000c311000000000000000e100e351a8a26ec67cb3ba4b95f2ac4be486745285e9434dcebd8f7df9f743dbd3653000000000000854d0000c3500000c311000000000000000e200e4d20396497388e26286a2a9bfef575a2269ea85b36b4005d294302aa05375862000000000000854d0000c3500000c311000000000000000e300e5abd37177996abc69e2ca24b121f45b32718a095250c0d533a89785fbc1692f8100000000000854d0000c3500000c311000000000000000e400e69491b3619b64e09337ff2b31ed0a98c07d47c910346fa6e43ff4da9c16a4317000000000000854d0000c3500000c311000000000000000e500e7fa38ff7b2f735c6d7ed6697968cc2792e9ed4c2cc9957e2152c773a23e10969000000000000854d0000c3500000c311000000000000000e600e8d82876021b20bad6dd3833506b1e2c7ddb85e8fc754b11f0099c558ad676db0d00000000000854d0000c3500000c311000000000000000e700e93f7a7c70b38e38652fe97203085488d82bc013586ad8a093aeceb4deaf2ee93b00000000000854d0000c3500000c311000000000000000e800eac0e5f3123292708627ace19965a0fb622f8eef55e33dd21febe02b2fb6dc202500000000000854d0000c3500000c311000000000000000e900ebe9c9a099ac9f0603aa645a573caf5b99f8eb03b28e881010b96d87e1ed55ce3400000000000854d0000c3500000c311000000000000000ea00ec1d875edf3e4221594917fca299d02b1c3f1990f6069f944330c3b2ad9edcba7b00000000000854d0000c3500000c311000000000000000eb00ed48640f071c6bcd7c1a94314bd8615a6f6493919fc216a555b9dabca0121775f500000000000854d0000c3500000c311000000000000000ec00ee457e830ff6e6eae43268bb92d5fecdbf8832a37b575285b1e113b29ea7e5f8c900000000000854d0000c3500000c311000000000000000ed00ef55b2b3b16fa963985019f4a6cac17b51fd164b85efd22180221311ba043e168800000000000854d0000c3500000c311000000000000000ee00f0eed5a265e23b6fee64a6800737f859959582dae01bf804141b7dc199139dc15a00000000000854d0000c3500000c311000000000000000ef00f17920b76551c9c0381e120a166e200e20e3b2e0d54267bb3c9f69a0a374af5f5300000000000854d0000c3500000c311000000000000000f000f2550cb6ae690524fbf6a6845f2df5da2d7e4aacc218263bc216db4be3c269ae5200000000000854d0000c3500000c311000000000000000f100f3b3ee1187678512ea1324b3e746df8c76d52008263fd5ed6ee3307608b02a7e6600000000000854d0000c3500000c311000000000000000f200f4c0c14618d3cc34a24260fcb5c7819db74e65d8eca279451104abcf31eb27a7fe00000000000854d0000c3500000c311000000000000000f300f564108c7261a446b472fa45a0c29af4b821210c8aabe6c113d292b6300224dc2c00000000000854d0000c3500000c311000000000000000f400f6bcf946cf2868a4749a156c578a4a6e40a2a7bc1716b35880eb9d87a4748ae09600000000000854d0000c3500000c311000000000000000f500f773465d6084beef0010d1ac1e7e2fcbc6783d854f3927e6d51f272c52bcc6bda100000000000854d0000c3500000c311000000000000000f600f86d175c4b0f2a9dbe8437a34789c0343991526bc83bdece3c71963f86cc51183a00000000000854d0000c3500000c311000000000000000f700f934b71da18c6f995f91aac176268d6ac0a3a2acf2717801ebf23b9d91926e914400000000000854d0000c3500000c311000000000000000f800faf380ea1d68711d9db0ab45e3397dbd39b5e728dcec8d55930f6f2affb0ca12c500000000000854d0000c3500000c311000000000000000f900fb834434e1f82adc0b7ac0bd9ec0d4179d36e2a611ede605a42977de8a9e466c4500000000000854d0000c3500000c311000000000000000fa00fc3479e0daf8b53f8c04ba0eddac3848dd88ae4cafeecc59bdec10b881101c883b00000000000854d0000c3500000c311000000000000000fb00fd61b82153bc8cf58c32d180d4f46da8f40ca6d8a3fff5ed4d5ba16a1fa73d4e6700000000000854d0000c3500000c311000000000000000fc00fed00cfde8a915c23cda075f4a88dc0ae74792bd8ece23dcfdba72f56b9813b0eb00000000000854d0000c3500000c311000000000000000fd00ffd559de901dbb27927b10cbb56d57e3b244a9f5c248ac8d85221547ba9dabfb3d00000000000854d0000c3500000c311000000000000000fe0100c68cdfd7846524e752cbb7ba1aa0e495352325399e03d91a7016e3671fd964d500000000000854d0000c3500000c311000000000000000ff010191141f42588c3501522fa211cdffd484c7889c2cc6716de25654169609657a4900000000000854d0000c3500000c311000000000000001000102e6ef1453006abcf1da0dfe1572ef43a2082f91f230b0f14ce55a6361e8650b9200000000000854d0000c3500000c31100000000000000101010312266945cef53747caa27b2ecb733afa5afa42bac3afee722206d2d0c5abb81100000000000854d0000c3500000c311000000000000001020104c7ca46af876947a45686fa1f11f8842a6ba14ef982a40f5138bf420c6f73176200000000000854d0000c3500000c3110000000000000010301053f1661f5435f16624c0e51a7a644ece8fe3d7f84af61a410787b30feeeb966d000000000000854d0000c3500000c311000000000000001040106cadaf4cd6e8d42db0682daba7a65e6a51434b843b5cf75213ea4427e0319f4ef00000000000854d0000c3500000c311000000000000001050107bc7d18e01e7f39891798b254ec5fbfd4066bfa49c634f8e3eac8fb5329aeb74300000000000854d0000c3500000c3110000000000000010601089cc3be9f123da1cbb918809e10bee97bdc9fa805b06c557bea2210471fa3307800000000000854d0000c3500000c311000000000000001070109f1a7aae9140ccfd98127b56044d9104e9de5fbcd682859598eef0d6f9ca92aab00000000000854d0000c3500000c31100000000000000108010afaa9ff70179a7b0eff8bef5d45379521f1110e8db1ee03b96d579764c8c344c600000000000854d0000c3500000c31100000000000000109010b07d1b09149d5c5275f85a10d8cd9202c9400ca2def4ad6791075291b4c3c5df300000000000854d0000c3500000c3110000000000000010a010cca26248920d3f7592d14d56435f07ccaba5650dfb6a88e4e39075376402718a300000000000854d0000c3500000c3110000000000000010b010ddee81eaa499f230eefdac7766ac46fda3d15ce50ba634d5dddaa37ed8b27706f00000000000854d0000c3500000c3110000000000000010c010ecddf241b4264b637a0ac3315b33d20ad2b4ee78abe9b109cd2ff12910527db0500000000000854d0000c3500000c3110000000000000010d010f2073acd612d2f8b24c44a0d791c8e458cb4209a578c5e54059a80bf078ea62f200000000000854d0000c3500000c3110000000000000010e0110f2bc07ca3c0917130c0549bb4528b3d30787e8a58e1a4e395f797ee1d5313c8600000000000854d0000c3500000c3110000000000000010f01111adb3bb2a1d5a92c063c04d8e4ccba90f31d32705ab9e5bf9937abcae9bd411300000000000854d0000c3500000c31100000000000000110011216d6fc2b34c08eb4c4047768a720a11c236f83a193eddbefb5f177811e1cb74800000000000854d0000c3500000c311000000000000001110113f7ef1e699a40d95dcd4c155bfe82b2458ffc85aad59e9a7b9a38b6f8e20129cb00000000000854d0000c3500000c311000000000000001120114ccd2d776df1d26ee861943082701bbb4b69249fadd8f143b597ac73e2af5c1c500000000000854d0000c3500000c31100000000000000113011556ceceb7b569011a449af0c305c8dbd28c6f9703aed425f8373c8497188b2b9e00000000000854d0000c3500000c311000000000000001140116b2f5712de794c52523a62778e83e0e59a1bbc6f06f97b7789670f0e5039ea9a100000000000854d0000c3500000c311000000000000001150117693cec364193e49f8f6aebf42955d1b49acda6c888a3a6e2ee274e372a45494b00000000000854d0000c3500000c3110000000000000011601184ebc0b53de00bce7dc1119c28cbf0648dc7ad74c976e4b49b1e5ab6d372fe7c800000000000854d0000c3500000c3110000000000000011701197dc46f6965e74bbaeb7789ee532264e843103a39d7a9d75132de7e9b5ef02a5300000000000854d0000c3500000c31100000000000000118011a00b1c69e8c2aee8a42f849b56150603e0541ba97656911b2bec92fb6b80e1c4700000000000854d0000c3500000c31100000000000000119011bbe8e25448c1ed1d4cf6851269c121e1c45e3cd58292714ca7a0afcd8c6813dc100000000000854d0000c3500000c3110000000000000011a011c42ef09220765bb717aad3c2d4dcfe180e8c0ebd8421df820386626d3a85e4d2900000000000854d0000c3500000c3110000000000000011b011d595e3312d192af31f4735e7352203a2b3e1ff5c8712247ea76b8d6521328a28100000000000854d0000c3500000c3110000000000000011c011ebb01550134b55966c3b8a175688d7fbd913988eb37701053abdebcd07b2c0ee200000000000854d0000c3500000c3110000000000000011d011fc8b44418ae4ad010eb4a16252265eb6e1ffbb8238ae74055e93170d5aed2959900000000000854d0000c3500000c3110000000000000011e0120996d1a0367ed61db81a131b5b163de5d6f2f693055b87a3cc7a0c6a15bb72ef800000000000854d0000c3500000c3110000000000000011f012126b6c2e89d781417985751c1fde5cd6069167fce4c56138370bcaa24d03ce24b00000000000854d0000c3500000c3110000000000000012001229e7af1b72e21a2785359677d3c101e9029b117a43dba617f8ea56d7dd022d8d700000000000854d0000c3500000c3110000000000000012101232d2e1c3b68cf4b0e8bd492eb5741cf78a3b36847fa0a97c3304a95dcc7a8a14400000000000854d0000c3500000c3110000000000000012201240e06ed3926bb95bf57bc7ee2c9e5f1c5bd661bacb1ce5a1a2deabe6736c8367800000000000854d0000c3500000c311000000000000001230125a6abd820de07f5cda1819cd9a9146f0adf4d8dcc60e3cab50bc5e1abc14891e200000000000854d0000c3500000c3110000000000000012401267ac610ff9d6969b53d28f85a92a1fad522b3bee82f76f010f45ef0a3e4c7c15d00000000000854d0000c3500000c3110000000000000012501277255670d53527fa3d330df360ac6c98d49a6a5ce3f6fdf1de4ca700844c8fa3e00000000000854d0000c3500000c3110000000000000012601281d6d6e357f1227e5e942653388260e8238b5612f4927fbfdce258bc4d0b7594300000000000854d0000c3500000c31100000000000000127012954e9dbc5cf7149868716b45dbdecdc449d0b39b5eafd69e3b49a7ed29d4459ae00000000000854d0000c3500000c31100000000000000128012ae285b863674b89d72cfb884f5cd5eb523ac0b493661c58f7851c5547a959ed8100000000000854d0000c3500000c31100000000000000129012b819532ccb84729849fbc4cce0ac5f8236350c3b67b316338e15cc62b954d30b400000000000854d0000c3500000c3110000000000000012a012cd23d76e2a5311b80e76c5229b01cb07f2bb85646fdcb4c62adc82f198b047beb00000000000854d0000c3500000c3110000000000000012b012df661b595023ecb62cbf93a64d234dbf889166b48fa8eb394bb9fbe841e1cd56600000000000854d0000c3500000c3110000000000000012c012e0513cfb510f00b170cf080b3ce2b850cec369d35c0550eff4b8fb2b6c91dfda800000000000854d0000c3500000c3110000000000000012d012fb9d1d054cf18aae245c31e11c0d122ed5aadd8a3b35ad39dfe30ac8798a46d2300000000000854d0000c3500000c3110000000000000012e0130e06b5899ba907b2923ca8677fa61c7f0943cdd7c1c40def5d83aad630bc00eda00000000000854d0000c3500000c3110000000000000012f0131293692db9029a74afbc68d94e574decdd9aa7f388e8aa344379018c4da18256e00000000000854d0000c3500000c3110000000000000013001326c2b3b309d75e679971f71daae4c69990da8707c359765d2c1f74a56ea45a25100000000000854d0000c3500000c31100000000000000131013391b688dbb1263e7d9c8898a7356dd313e14f066a6db7dbd8a05af623cfb1423000000000000854d0000c3500000c3110000000000000013201345d9daa987405ea5c478cc99679df047e1cca5d28b2ae550d88570c9a7b361b4600000000000854d0000c3500000c3110000000000000013301352d4bf0f568c72724e502c4ea615d9bdfe76205af4649db3381bb039a6c81d5e700000000000854d0000c3500000c311000000000000001340136387afecf394d4357604f540282bbd442f84f456c5ff810d7964941b31bc12f5f00000000000854d0000c3500000c311000000000000001350137b19741e64da87111ec3ab8a714db12a89a0e19cf667a0049a9dcd72aa5c30cfe00000000000854d0000c3500000c311000000000000001360138041c4bfedd2c41b363acd3fa746b0e65820abf0259851616c147a6253982860600000000000854d0000c3500000c311000000000000001370139bd4d8149d173108a9685d7305f1624dae2c8dda08e475d90494ff93e3b68d3f000000000000854d0000c3500000c31100000000000000138013a6807c6439794cc48e36826e845ad919d758bc0308611f2b2063f35e59badcc2800000000000854d0000c3500000c31100000000000000139013bb94b58b11fd0a16ba852395acedb03563c9992103fbb9c4541c0140ad25bba0d00000000000854d0000c3500000c3110000000000000013a013c98f301ca34257ff0bb60f8a29e8c2e75569f12c2e757cfcd86d441987754db9800000000000854d0000c3500000c3110000000000000013b013dd33a298ca334510f32a78b0d175ef81aa1283c5a43a0a36997337a188cf28df100000000000854d0000c3500000c3110000000000000013c013e6535f74d7b79af5ab10020827567deb1d7feb0d68c97b24375444fc91685c3d400000000000854d0000c3500000c3110000000000000013d013fcd31ecebd30f110cd6faeddf43207ca9ce128876a99d882d1eb2956ac66baeb600000000000854d0000c3500000c3110000000000000013e014080dc9f243390f1e5ff5f824b3558f3a71558ee2b76b6dc85b8767d8c3a762a9900000000000854d0000c3500000c3110000000000000013f014135e665b8a599899c0e0c8e20d270917ab23a1c5ec1392031cda458edb1c95f4700000000000854d0000c3500000c311000000000000001400142f390439757563240273ed69da2e0b49158ffac612227c538ab4df21e668f072400000000000854d0000c3500000c31100000000000000141014375199d7a5f1d5702a5e97030d9b85af5459e59ece07c34cf4eba44fc2fbe75f300000000000854d0000c3500000c31100000000000000142014488eb334984eb5a14d76478f8c7e277804cb813c573fe185e1269a0b1b3a7c4af00000000000854d0000c3500000c3110000000000000014301452934a43425a844e0cfcdce1f191ac83c216aabfe9cfae82ad8fb80e5ed32fd9500000000000854d0000c3500000c3110000000000000014401463e4182cabfe4bca69a2b2d26394b851e6f313fd7400e34f66b207fa7997336ee00000000000854d0000c3500000c311000000000000001450147beca3c2b2abf74149413af897d503c0940a04e65488cdca79adc80f3a1022fa700000000000854d0000c3500000c311000000000000001460148358cc7525792ec60cad166479beed863fbda52d75a2b3b21b44cb3948c74c3e500000000000854d0000c3500000c311000000000000001470149084911fc1e172537175b5cb1ed8e0ecae73beda0b4e5e13b826819e1dfeab3d900000000000854d0000c3500000c31100000000000000148014a415167f1a6ff33eb7eb3a208895a8bc11057d6fc0e3d3cde21eb23a9e8f0428f00000000000854d0000c3500000c31100000000000000149014b6bd1174aa697898fbf5fa97167d246f94cc930add143f6b5f3ba1c189160fe9900000000000854d0000c3500000c3110000000000000014a014c630f10f3c50d48328bf9fb5ecc61598d7fb7da587fb7821e4b42151afc4ee36d00000000000854d0000c3500000c3110000000000000014b014ddc396d879f620ece1af21eb2ae7af3ccb63520b7e5c2c86ff9704ecfeae2897300000000000854d0000c3500000c3110000000000000014c014e36074edd4eb00de98fd3f92fa28e76e19abdccc7183a427e0f80f2545099249000000000000854d0000c3500000c3110000000000000014d014f329bd6b4a05c40900c16d963d4cb2c32cd3b866fd2704809359d4e7be2b4d3c900000000000854d0000c3500000c3110000000000000014e0150e0d278e14a883573033cdcdbfe68747f69c02a77aee2631a53854877cd977f8c00000000000854d0000c3500000c3110000000000000014f0151cc619d317d853e8a8c9605c6b2016c5b77d1ed0ce2764865af5bb58da60d25fe00000000000854d0000c3500000c311000000000000001500152091da59c8e15c2d0b67a0c03241ed38bcaa08afe611e374ff56a04b1493496c000000000000854d0000c3500000c31100000000000000151015351d3aca70ad269740eb8143d40c40ccd9655137e1a797db99c19f0c74496eb0200000000000854d0000c3500000c311000000000000001520154e8b3f8186945c665a130ce8bb5022be66ee9a82d58bd04995dc2ed8817b9cf5300000000000854d0000c3500000c31100000000000000153015582bbf5437a38dff5538c0853418b67eed524fd6ec6730af6e83fd7e1819aa9a900000000000854d0000c3500000c311000000000000001540156126f1cdc86e71f3a885ba96d926e619de1dc600a5e917556b59349709a09202f00000000000854d0000c3500000c31100000000000000155015742f8ff596b8d24eb09eadb09653801d690e24c1fd00201534185e5d76616e75100000000000854d0000c3500000c31100000000000000156015838866d0eaecd45ba609331dc127de1cc6de73bedf6c8857b39d7bca574d5a95900000000000854d0000c3500000c311000000000000001570159fc5edceefdd5ba830ee696fa281bfaff5f19f03a517bcc14f2a723cb43db17bc00000000000854d0000c3500000c31100000000000000158015aabd47e25adb0dc27d1c5a063dd640ee22ac5716a1dcdbe152c27c2a4925deee300000000000854d0000c3500000c31100000000000000159015b44080f378bc7003840f1d01437c268fb20c7f9bf4b38cfaf1b18bbcdf7368fc500000000000854d0000c3500000c3110000000000000015a015c130157d7beaaf191672619dffdb99204c2ab01b35d299c29b4a4c9f1364570df00000000000854d0000c3500000c3110000000000000015b015d139c7812864191abffe378364b46c4929ce9f354e57994e56ef8690a9adbdc6b00000000000854d0000c3500000c3110000000000000015c015e76b343cf768eea6ea9430c144d1f833d2fd19f01a0b8d04e09ad0625c0c1ce4700000000000854d0000c3500000c3110000000000000015d015ff182f63a888b037893c449126093bb70f97b7360b903cc02705619b8232cac9700000000000854d0000c3500000c3110000000000000015e0160ddb46b199bd616225b980e5cf964c647898a44241ca45335f679143b0097d48600000000000854d0000c3500000c3110000000000000015f01619d4246b117a38112ba1bd600e78490ba170a1d1307043f3c83c2a1a2d50106c900000000000854d0000c3500000c31100000000000000160016205f692aacc895ec966a4c41045525845352c1993f7d5fad4f836f286565c62b900000000000854d0000c3500000c311000000000000001610163051eb76af4a34d66106771401412f01f4ac661594140355541f815f5e1e7490300000000000854d0000c3500000c311000000000000001620164b7e76d9101f61768d4ea6470dbf632941ad250c20d2ed9b3a9b62fc2ae398c0e00000000000854d0000c3500000c3110000000000000016301655cf571d20737997ec5f0f856dc59a1104d5b153b9377cab0a3107dcef4ee1c3400000000000854d0000c3500000c3110000000000000016401660bc27dbed94378bb13a6b1bc1a61789be9572d1d42a7611338d5a3e1351768d000000000000854d0000c3500000c3110000000000000016501670e63f7f71c19b7eda6368a15dc5dc2e7295541888e60738db15b3cb79292617000000000000854d0000c3500000c311000000000000001660168144788ed1842d11dfc84d06d656f2b8390e11778066f21d5f97626bd72b475fd00000000000854d0000c3500000c311000000000000001670169c4642f1fc647f5c8efe7acc255ba6bab55d280a095546aaca38921df1549375800000000000854d0000c3500000c31100000000000000168016a1e52b4f91a493d81e0ad67231f835c8f1ec2fbab50d1f4e5e324504000ee244500000000000854d0000c3500000c31100000000000000169016bb037cfbfe23395592f82f1ee8d5eab8a2277d8e3644908c775b16396774338f500000000000854d0000c3500000c3110000000000000016a016cd3781d0b608c534512544055182fe979fc8d439f5c49b64cc948bd6f7b6314df00000000000854d0000c3500000c3110000000000000016b016d800b2593788c198f7321ce6424338ef7e83123e241aa7fbe8dc38e7c1e3de86200000000000854d0000c3500000c3110000000000000016c016e97b0f44eae74f3a3bfba8c15b94663e93067a575289e7034c8ca6fb8fed4d1cd00000000000854d0000c3500000c3110000000000000016d016f87ab47f0d8959d60eb4147714356816f6fc95b0fb1f92abfb087f9fdd9721a6000000000000854d0000c3500000c3110000000000000016e017015c8f1a325c0740647aa9af8d6f41b9b364f2d6509cba4c318784ab5f98664fd00000000000854d0000c3500000c3110000000000000016f0171dd3765c9c21ac14cced443eed9254bb76cf595dd19763bdad8f3762118180c3300000000000854d0000c3500000c31100000000000000170017205dc8d8d03f1d1b79383291c9b91c6dbed7b5868793a94d86f8cd0b02ea9dffb00000000000854d0000c3500000c3110000000000000017101739285e8ae6dac04fec83f99de4cfd81186130119af91f3af4c2c2ed9556cd73b800000000000854d0000c3500000c3110000000000000017201745f9b9c5c46848b51684d15e7a018c16c7433251d4e05bf0a6fbb1c22e26624eb00000000000854d0000c3500000c311000000000000001730175356f2fcca2d099ddf402b0f28922c8be5276c406fb6a262a10e1ba2d37c1310d00000000000854d0000c3500000c3110000000000000017401762361231644dae69b1d58f83ac1795d89062f1d20f1e18bc58a38beeab8a143d500000000000854d0000c3500000c311000000000000001750177fc410231f56ded5634a2b827172d7ff54326b4d9ba55aff6f76dc0c1d8abe73400000000000854d0000c3500000c311000000000000001760178a3ce42369319689c90263bcf693655b2b5f73426e4299bb3edd27a61fcea84d600000000000854d0000c3500000c311000000000000001770179529f4cfceb408d8e4db8a93a81948458df679039b6b286ef8e2d6b392e584f7b00000000000854d0000c3500000c31100000000000000178017a652a19f302298f2ddedc5bdde42af0c1617f981d5942d33bd7bb436780ef950500000000000854d0000c3500000c31100000000000000179017b78799b9c840639e4d0d50fd3e6e4728c62c0bd0378096552acbbd4096572acb800000000000854d0000c3500000c3110000000000000017a017c8eb8760d4cf5b9077c2871390b10c3af43aa24de7873c145ee0efb6549c7d6ec00000000000854d0000c3500000c3110000000000000017b017d662d3d9dff9fc1daee14bac71957f258d155d3cf5351507f7f9c75f9fdf9750100000000000854d0000c3500000c3110000000000000017c017ee0dd1b9635b59c05c79911a8d80bff18b7575e6429c8d709a869e6a06e04c30b00000000000854d0000c3500000c3110000000000000017d017ffc1ae8163736881a06d397844694f4c1b611b2ec421e3e85830e6a0b06e7626400000000000854d0000c3500000c3110000000000000017e0180103f4ec21762e94c3a6176166b0112f1dec7ed9cec100950fed6956966a114a700000000000854d0000c3500000c3110000000000000017f018147369da11f17dbfdebe6329cef69ae7150b9003b2a03efda39443b9958f8154c00000000000854d0000c3500000c31100000000000000180018293127c2a56004f8d0a7a8fe5866174cea89697bf41df5e122ce7a23215e2d60400000000000854d0000c3500000c31100000000000000181018325e789b4b54bce1b239c8f012e156269e109a865bb46cf6d615412a6bd1e060a00000000000854d0000c3500000c31100000000000000182018415d04d386172988a10fb74a66f2edb3798cae4ae180cbf1b110e1ce447d1cded00000000000854d0000c3500000c311000000000000001830185d14b28e04b1008b7716368751b7281fd1c56efb14c4bda843b2e3e56a6dd167b00000000000854d0000c3500000c31100000000000000184018608135c514ba440467ae403e0a65e81f359ae4241789416377f7502ce739f6c3000000000000854d0000c3500000c3110000000000000018501874d1153cb1b5b24b1d021059395131e760ff239f5ac04487fabed4265acb7f43a00000000000854d0000c3500000c31100000000000000186018859c506c7f1424e8f6a02cbb4b0488a8253ed13971bd35d9857273fc03d9c2e3e00000000000854d0000c3500000c311000000000000001870189defb781b52260a9d6fd63130f1b8d6d603e4247eaae108d635e2e5bc04c5382e00000000000854d0000c3500000c31100000000000000188018a5335d54f7383e77089b8fdd6652f3fef12fb0f70fb2a84d2affb193bb1b7095f00000000000854d0000c3500000c31100000000000000189018bbf3e5a3c2868086d150869804d22d5da63dba8992833908943ce66f90dad171a00000000000854d0000c3500000c3110000000000000018a018cfed9687c86989821940a117f7fc601ff678677a12178775caca21a09249da22a00000000000854d0000c3500000c3110000000000000018b018df8ce826220eef839cd84d2b9df12a0f0ee5806b4aa19c173a92308a9f084543100000000000854d0000c3500000c3110000000000000018c018e1921efda69bc2cf34f1e47081b44b6400291eb927f4980a94f3134b111a2523200000000000854d0000c3500000c3110000000000000018d018f37c353aaaec137e0c9db2d33427135e6f8473ea35384b6a3eb206af450916ed100000000000854d0000c3500000c3110000000000000018e0190f3a2a2e0cebabac99e3d67147da573f39fcab1746d2a05d2e22a060af92efe9600000000000854d0000c3500000c3110000000000000018f01919375113bf780682741b62602e90cb9b3a50132ef3443924c0b93b7f3c0ea80a600000000000854d0000c3500000c311000000000000001900192b5085704b4b6b823059acda09858529e4e9fb542b52c668b392bc2900378b07900000000000854d0000c3500000c311000000000000001910193da08774a6ff7e6968eddad0fe921d39051caaf3c2bc2aa047102baa574852e7e00000000000854d0000c3500000c311000000000000001920194e6006b57c070a434ba17e69fc30dfdd88fc3faf7c00465df22f22fc575051dea00000000000854d0000c3500000c311000000000000001930195eb004427e0c3795eeb74ae52d8eb908d6395d93e90d93f113a941d728c7a186600000000000854d0000c3500000c31100000000000000194019625cce4babf6d48d362e642b15f5234e346d0ad56e01eff852be1d4bad5da4d9f00000000000854d0000c3500000c3110000000000000019501977de1734b52094a5045f4d3a962b0811c40148ab37a8cb8949ada98b9bb2ef98e00000000000854d0000c3500000c311000000000000001960198f6bb35d733dafb8788f89a35fc734c6fae0c79c7ef79d9ab79a619331d5991be00000000000854d0000c3500000c311000000000000001970199f608ebfa1bda8ea7044d7614459001856762aea6fa2a8da8a84f63b35b784aa900000000000854d0000c3500000c31100000000000000198019a5efd4cd712e8d819797cbdd2e5eb14ab8479581992c5cb4fefb71acd21259c9800000000000854d0000c3500000c31100000000000000199019be469abb620208408b16c28a5b22a408e86745059c187e533738786c8321e62d600000000000854d0000c3500000c3110000000000000019a019c55b7d42391295649268e33972b9b05e3863b06e927b1b6821f6be5c22cd5b32300000000000854d0000c3500000c3110000000000000019b019da71224eb2c47f79b4b7897332b4edf21831b271dd31265ea82a56ee423fbc51d00000000000854d0000c3500000c3110000000000000019c019e625238678138161a8a222110d7fae260b0d53dd718d8f0bbebc6dbf9f7c603d000000000000854d0000c3500000c3110000000000000019d019f998478e02c1ccb514f538e60d2d42bead26565df3239926c7ab2ba9e2556375c00000000000854d0000c3500000c3110000000000000019e01a0cadb273ea32290f6b387130461ebd022c6b559d4485f2dded4dc866794b1952e00000000000854d0000c3500000c3110000000000000019f01a1dcc463d2f12315dd333110aec4216d3ad68442ea011ebbf65cc03d6fc4def28d00000000000854d0000c3500000c311000000000000001a001a226af9c7b0ad6986d2014ee1c344720f7e5ecb5912d4533e3db9cec5abf3766fc00000000000854d0000c3500000c311000000000000001a101a337b0243c9147d7cdb24ab677cb997793184b4c6cbe078a2339c7050553debf1000000000000854d0000c3500000c311000000000000001a201a40b65bf566728aa8f0bd3b5f8b03efad99fe4d86570c3792a009bbd23aed8851600000000000854d0000c3500000c311000000000000001a301a57c0f33fb634f5b1cfad7cc130da55119a2fc282e4975d40204af4084a9df275300000000000854d0000c3500000c311000000000000001a401a610d4665d3a1c11e4bd872b0b47d822fefc75e8010f2766febba4206548da19e400000000000854d0000c3500000c311000000000000001a501a7e5998fdf3f51aedac8c428ebbe458f8039f8da6d996381998888846d7d7eac9300000000000854d0000c3500000c311000000000000001a601a84aa1e7b220a2ccb3d28ae1fc383aa4319dcb8df1a554f98e360ccb1b630674b000000000000854d0000c3500000c311000000000000001a701a90296a7c4204e0380ac993850b4041042fcff74542ba713574070c2982e5fe06d00000000000854d0000c3500000c311000000000000001a801aa1bcebf698d2ebc669f1e2e3ebbdb2bc9745616b0dddf8503965e9ce1bfbfbd4300000000000854d0000c3500000c311000000000000001a901ab17ca3153cfcf2c91ab678671c0193785a38fd13243f23bacf712297c1c3161df00000000000854d0000c3500000c311000000000000001aa01ac42aa6f3b6b844e832587ca38676960f99da8a7bf1cd0861f9476e8148056dc7b00000000000854d0000c3500000c311000000000000001ab01ad1677bf1f8138392b8a896be86688d8e2a852c94abed179d515ea75cbb0f0114400000000000854d0000c3500000c311000000000000001ac01ae5d07f5c149c59191cf2e949908b485c269c7237df230bcc66bff9d221b912af100000000000854d0000c3500000c311000000000000001ad01af4af01101f2fadf75503b90fa5519fabc113be039f7cac24d6a425ff6f4d46c9100000000000854d0000c3500000c311000000000000001ae01b0b75ae35825e628b1d386a81c380eac87e5063ea2fb581058a44e67647606faec00000000000854d0000c3500000c311000000000000001af01b1b3ace4f6e0b4d2504edf42d9fa09490ca53c59401c5ac8eae407a0f0347dc2b400000000000854d0000c3500000c311000000000000001b001b2da0319bb42c74cb71cc24608d303045b7c66b21c9cad1bcefa23190ea9008afd00000000000854d0000c3500000c311000000000000001b101b3e61dacb664d45941c172a551ede9e667bcf095113c15bac9217e1069c4549d3a00000000000854d0000c3500000c311000000000000001b201b46fea6aac4b9e2c07142170421a6f4f81ba464c8ddc49887d9f0f7826314b772e00000000000854d0000c3500000c311000000000000001b301b51df4dfc3f008e8b2348c48574f1c8e555a987162ec8347d793d0ceda160c952800000000000854d0000c3500000c311000000000000001b401b6e793bdc7a9b90cfd1a09081912fe24f588947146e99c46f58754cb16f81f6cd900000000000854d0000c3500000c311000000000000001b501b7ce19f510bda9ca936f1ce98af2c35aed4c87e98c38927a095b9b94f741305f9400000000000854d0000c3500000c311000000000000001b601b8c7930f425336449c2c3208d0e0b74d37e1eb5c1c0311e9a77bf00bf103daa2df00000000000854d0000c3500000c311000000000000001b701b9c7d9c3fecba42d41c4ceb332133da641fb6cdc531897be0caabbc3261cac5c5900000000000854d0000c3500000c311000000000000001b801ba871ca2ac3fcb9449b1cd3903798aa08e0e751d505d9aadec2d2b912094dd05c500000000000854d0000c3500000c311000000000000001b901bbd1dac0d405505faf085050662a9b20fe2e3398fd01a9b011dda41a55c740485c00000000000854d0000c3500000c311000000000000001ba01bc8aca06e52f2ee6f62f413b23f3bac9b4da3e93a2fb788f34c085be0128a293e600000000000854d0000c3500000c311000000000000001bb01bd50562444dd139b2bc45aa2064e6f945e1b4ff89ceef30842195d7e3766b5a25400000000000854d0000c3500000c311000000000000001bc01bea3112af2adcf6c56ac46a4cbf93312e2b6b7ea1848185a85273a7b11251c217900000000000854d0000c3500000c311000000000000001bd01bffa802b543ca6922c0656320b0761b883b9c1d75228ac9a7e9626ec76a7c8482000000000000854d0000c3500000c311000000000000001be01c0b2fcc13e833dd5bdd5d3c8ecb1745f50d831619c895ba3dcd24597bc52e0f67500000000000854d0000c3500000c311000000000000001bf01c11ddb4c5c55ed2746c69088f47da9cf8450cae2cba98e8a997381709e234c811500000000000854d0000c3500000c311000000000000001c001c2f88327f3873e70e5742dba0a8a41ab4555ef75ccbd49812534d625a38086465c00000000000854d0000c3500000c311000000000000001c101c3877f5cb86d90f3acfe194d244ff85604036251e73be7453d043470e8e87d1df100000000000854d0000c3500000c311000000000000001c201c41b6468631457a999e13d8556d28875f80d475e4080a822606e53ab4521e612fa00000000000854d0000c3500000c311000000000000001c301c5e27506829355cd7edd93156f5d3d78038a3cfa19d1b94502097914558d7a99d300000000000854d0000c3500000c311000000000000001c401c6e922eb6004f428a319dce0b00be94d9d65e8a891e0da8092cd17eb57f42719d700000000000854d0000c3500000c311000000000000001c501c72cf19e8b9cdbbce8788a37b61803d8c5ac9b705b451a16cf3fd4e44c98a6978400000000000854d0000c3500000c311000000000000001c601c86d4c389a327e75e07e26dde46d96ec5f9cbe652798c6e4c4f071e518ca431ae400000000000854d0000c3500000c311000000000000001c701c9ea874c9bfb2c727a572b1bb77a3911ddfe4890f0f4cc152b67e4490953fa741300000000000854d0000c3500000c311000000000000001c801ca3078da4bda050f5f723492d3f63a9f465a3f8352414b9b24747052268e8fd9f000000000000854d0000c3500000c311000000000000001c901cb0cbf24109afca71dc10ec95847ccd30693a340ac17a1c438de32c9d5ec779ce600000000000854d0000c3500000c311000000000000001ca01ccd1eebb68e4560bfc706060c2557d03e5fe55e5c76f1b0ef8e8b9102af101e1b600000000000854d0000c3500000c311000000000000001cb01cd60f5505127a1c513a7bf51a506bbd38413db23366690dde3b7a64ada8b38c3bf00000000000854d0000c3500000c311000000000000001cc01ce5f2a77c993b3c8c2860b9b6f661657e2ed9c3fb8a7f07b9f86e6908ff61e504000000000000854d0000c3500000c311000000000000001cd01cfbeeced096c0d29cb3117bd673c5a13fcc06788b4db4d7d24a2bb143cd675772200000000000854d0000c3500000c311000000000000001ce01d093861f3b9c8f22a3fc7f39be4019cbee78624119caa0ac94b509b691e516835c00000000000854d0000c3500000c311000000000000001cf01d13083a24fa948cd62be1d683b9cacaba9117c6a95ba3af20f8df2bedefbe3456300000000000854d0000c3500000c311000000000000001d001d2d12e24a4fc1c9116fc4eeead881d06ea1a7ecfbe5b50a60da676d03906784c3500000000000854d0000c3500000c311000000000000001d101d35c572a6639f35bec2e4ac4d0afad8f0b1a5b56f379ba18bb0f5a2239bed5684500000000000854d0000c3500000c311000000000000001d201d4fe0dd02a29705d5066285c49d0f62b8b7703247af1ca3f3ef80c72d2e73d4b0900000000000854d0000c3500000c311000000000000001d301d5270a3d63c0ee9b56b60d8422820e7d421d7cb5edd90bd36f171f3a248ca8ba2200000000000854d0000c3500000c311000000000000001d401d6a0ad7dd4c1094a8d2cad3b5f58c2f6521a82fcda6145f50a67031c99fac5ee3300000000000854d0000c3500000c311000000000000001d501d7bbc593f697857610ae607fcc8aaadfbfa9ccc8b666c1610fe58638c4b81b39d600000000000854d0000c3500000c311000000000000001d601d82ef8ef79e3f3125048e96e178148b987ad8ce75ddb86f226c9422f88311a1e5400000000000854d0000c3500000c311000000000000001d701d9584d870aff1afb86ba69ddc59fe8f846915e32928ec2b41ecfdcec2ddf6e034800000000000854d0000c3500000c311000000000000001d801da3f220ce62505abf5bd046d810d0a73121491fc5845984c6fc49d95fff620acf000000000000854d0000c3500000c311000000000000001d901db239fd782432147be7b01479d1bbf685ad71220496c5fbe49751f43661ad97ffa00000000000854d0000c3500000c311000000000000001da01dc452dbbf443cb66f47d94219064c1e11d02a35eee965b7f853b387676a12fc1bb00000000000854d0000c3500000c311000000000000001db01ddc92ca5e3fea2af39b5290131d48ae5ac6b8b3da7f8939f3416427aa7bf23c49500000000000854d0000c3500000c311000000000000001dc01de532077ccb94f30ba0e14ae25792e182fd3a9c89a136112a7875a582877d067c900000000000854d0000c3500000c311000000000000001dd01df994fd79bcbb666c872f3cf82331b4890fd26895337b42a7f957e3da716d9dede00000000000854d0000c3500000c311000000000000001de01e0b9563d83c0644780a2f7dc8441b391e23ad1a9d86a23b965931c13d66e41314e00000000000854d0000c3500000c311000000000000001df01e1a356c15452b8070d12716b40768018b3315c33b00a40c54fc825a77e16189b5800000000000854d0000c3500000c311000000000000001e001e250d2f5e73eeac5e1263a0df8a53ba2b1deb5b8e7815c61bd8d70015ed1912fa200000000000854d0000c3500000c311000000000000001e101e37498c3f78d908282db2e0ad1a117dad769c93569014fa2500d266959dc58f9d700000000000854d0000c3500000c311000000000000001e2cc184a69d3fc2fd2deb40ec4a13f19624540d06668f22572fae599d3ab8b9e48617e8bc904444217d8ec5b54a16df0bc55d63d76ed2cafe4b172e8c7e92773e6", + "provision": "d70f0505050505050505050505050505050501010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0fe012e565964e65d584adbc7138f1d115f6e00c27a90903e6c1b1657982fe5cff61034010d3510c0a44a53e0ef4a086226bfa039708f944ce4da9833f1ad5e5f3000c0df07d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a233365035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c3590006d9257d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da6040101e300017305b11a874bbd995b29b11d109eff432402136904a3bab1f7bd9f869109cfc100000000000854d0000c3500000c311000000000000000000002cbf141d9ad3b41938c25083328862acf711291bb47ab53b33dc37898ef9326b400000000000854d0000c3500000c31100000000000000001000362e42ded68912761a397087a1310af972f9022330fbbbf24c580b9e71479fd8700000000000854d0000c3500000c311000000000000000020004754bccfbb8e3ee44224e0290582ada7cb1dcb05ad968df63ac0aa01e81298f1b00000000000854d0000c3500000c3110000000000000000300057c23775d75a9e3e878e5e33ba9cf7da51c78a4d54431bc1981c8524eda36bb0d00000000000854d0000c3500000c311000000000000000040006bcd5fb4815309c1de8fbc2867eb93635d8764c565d952917457874fc6eceda1800000000000854d0000c3500000c311000000000000000050007e3ae54a63b8f9526f7955db3417b5a1f6743b99ad0ca7cc92c3fb199c149b76800000000000854d0000c3500000c311000000000000000060008bd471262b242310a4d1b298156a5fc3469395eb2fa3d28be02b88d8d0c97081100000000000854d0000c3500000c311000000000000000070009397b027bfa95da000fac9dac5d41825bb54a0cd0071a262be00d6d4445f3e2e900000000000854d0000c3500000c31100000000000000008000adb17a978e9e05effc0d7f4c33f7fb78898deec1a0b44212bf943818091fbc09e00000000000854d0000c3500000c31100000000000000009000b0fc65290b128e63d598e654d4a7bbe14cc3ef946f6fb6fe0d5b46cb9e0e2638800000000000854d0000c3500000c3110000000000000000a000c3f951c45254175c92549460b5a89d5d3c61a950e7e366f992dac9f7f7a7e6ce700000000000854d0000c3500000c3110000000000000000b000dfa9d083023788850024c7eb588fae1c7542604d23cc12f44ddd568c2320a754f00000000000854d0000c3500000c3110000000000000000c000e3aa43187fba488b5530dbc366257f82058801f6983509716a9e9bf00db680f1800000000000854d0000c3500000c3110000000000000000d000f7404ba142c5ad30fb6769e7f6a84843d4c437bd50254a72815c8b75c1deb0c4b00000000000854d0000c3500000c3110000000000000000e001028ced94a2fc573f8c251838edfc807be39d1b910d18ede82aa7a127c5888b3a300000000000854d0000c3500000c3110000000000000000f0011a6a8f52194b94c6d0b521756e5c2c20ab5b6da8a2483e44aeb48447112c0680400000000000854d0000c3500000c3110000000000000001000122014e1e38afe84f6da72dbab22108ec05f60555504bac35bbfc7c62d1ac3621100000000000854d0000c3500000c31100000000000000011001313667fb8817a03dd2f5fd97325ffaec561f597256d58742291c569ee251257fb00000000000854d0000c3500000c31100000000000000012001430ec738e4babdd6d0b18b82a7f5a7415074cd283260197c4efe5d439671c9abc00000000000854d0000c3500000c3110000000000000001300153e9e411c4bcb405bda68f31ef974cf91e5dc7a530fa8580048c4bc742805125a00000000000854d0000c3500000c3110000000000000001400167aacb62cd9c823ddf004ca04fb936c3093029ddceaef569190534039885b1b8d00000000000854d0000c3500000c311000000000000000150017a9e7ea66fc54f78b5a4441c5ca6ebaa742485894509dc379783c52d881fad9e400000000000854d0000c3500000c311000000000000000160018063c74693de53572427dd71747f7a47f95a72b83b95f62b60be7479516593c5300000000000854d0000c3500000c311000000000000000170019454a89ae1311664f5155c45544265e5b33911a120db3b683522ac590e64528b200000000000854d0000c3500000c31100000000000000018001adb1d9b5644e6d6b7befb0f222e567bef23ec977fd3d5a6f146b6b195f48384e400000000000854d0000c3500000c31100000000000000019001b54755b708745f22c50b1ded9d0f24bc9b87d93d95046accde5e0d0c1b27ee3a000000000000854d0000c3500000c3110000000000000001a001c1b29bd0c8af340c2ede7c9552fd9410b4c731a47156f54ae2fe2ef6ab170a3f900000000000854d0000c3500000c3110000000000000001b001d3b68781e8a122e02fdbade74d727eedafc80663a79c32726b7d555e730f6b00c00000000000854d0000c3500000c3110000000000000001c001e508a679821f8e0b736552664ffd03360eb8e08acc5ac5c653ce280885222ca8000000000000854d0000c3500000c3110000000000000001d001f599fd0cf5ce9487aae5390f0aa7e2f55bbf36c7fab020879c262c74f0659b80d00000000000854d0000c3500000c3110000000000000001e00201d35e8a88f39201d36bb5d8603822ee851e4f936cc9d941eda8f0c132be674ed00000000000854d0000c3500000c3110000000000000001f00218b0edea50f2efd820966577036831199400b94a6a5ddc1efde5caa77d770fd9300000000000854d0000c3500000c3110000000000000002000228267950cb8f56aee3a11f312d601c4e2f49e229db76eebe9df97b467478fe80400000000000854d0000c3500000c3110000000000000002100233e52b8a3ff4d5f870fa11ba4e1a7ffc5f35310840f33f01da137e5802ad25db100000000000854d0000c3500000c311000000000000000220024bb7507ed8a6155c9d7d58e9798f1822e143da862250702f237267f91bf1f6bc300000000000854d0000c3500000c3110000000000000002300253dbe9c22ae8f8733c3a60d21c30bf7e51a304f53bac7c4a2c6e3cbe61157679500000000000854d0000c3500000c31100000000000000024002698a89d04d4471120348d82a09c6588128fe927d38dfe26d98932961fc842764000000000000854d0000c3500000c31100000000000000025002736a20a0e1e99e0e8f0f9d389acb4ce07130e58276e847f52bf8d17c2bf36fc3300000000000854d0000c3500000c311000000000000000260028f5d5256f0c02a8e76d9de795b0a9a6624e694f2df4d106bfe32fa46b60400a3300000000000854d0000c3500000c31100000000000000027002908853d9812b63820a7c738e5fc15ab6181cd391a059c248b700657d4b21b4e5d00000000000854d0000c3500000c31100000000000000028002ab49ad03151b291227dc42af60dd203c373a9533afdc9875f4c02d08560c4533f00000000000854d0000c3500000c31100000000000000029002bdc43a956e852c354169466af263dd38c4f278c18fb54d209d9b65ef1828cbec200000000000854d0000c3500000c3110000000000000002a002c1c3da0878d813c406978c150f03595e9a26d582ad426d14ef279edd0e8f452fb00000000000854d0000c3500000c3110000000000000002b002ddccf935988fc435b5713186e04f48e53fe514466473a48ad5e0afad0d22fe46400000000000854d0000c3500000c3110000000000000002c002ee113b733aec9e1d33dfd95556e546977a6b907fda9164c86f41dff868d4a2cc300000000000854d0000c3500000c3110000000000000002d002ff6fd342210a6de76e74742ecb52b84f482aeed8d2e0b0a843cd8167b4554f5d600000000000854d0000c3500000c3110000000000000002e0030882620e0b4f1feee1c51202c3b9623135d41499777d1341f63730f98dad4702c00000000000854d0000c3500000c3110000000000000002f0031ba08838987d86618a0dc670de584e3cb9602dd2f60278ea817a31d7f311e634d00000000000854d0000c3500000c3110000000000000003000323f4fce46408992a463e8d153537aa72ed50c22cd6c76648191a9cb4b43de2b9b00000000000854d0000c3500000c3110000000000000003100332f030755b807abfb879b08abe7a8b15adcd9efd2f419214fa5d02d099fda961000000000000854d0000c3500000c3110000000000000003200346918f0ebfa087f6b72399aca062f7bde3577c44fd288090b9ff2718fc6f5cc8100000000000854d0000c3500000c311000000000000000330035f70d71f8c06e27b949b91c969119a7cb85a4779bef0397a4d2a6847b49d128dd00000000000854d0000c3500000c31100000000000000034003631f229a547af63a7d20a9603285eb3793ed667b25f4a22bd8cd94344665f31a800000000000854d0000c3500000c311000000000000000350037d872bb09a3b355b08b3bf759b436cbc36f6d645fc01dccf919454235b8cf509b00000000000854d0000c3500000c3110000000000000003600387bf97f96e2d82c0396dd60d8a32ca53a9034f4fafb590a6f8b3f02e3d16a26bd00000000000854d0000c3500000c3110000000000000003700391d44995dc923077293887ec5dab71deac2055a37fdf727d5a2c0d216bbb5a25f00000000000854d0000c3500000c31100000000000000038003a6fc5c81c3b0c8b25375f88a324a38ad92d72a73d2771886704e24ed7ab53595100000000000854d0000c3500000c31100000000000000039003b64a7f4d976600e0d3670d51fd1f1a08937a5247cd7314e78fc85a6c692cee5b100000000000854d0000c3500000c3110000000000000003a003c304c70efe0cdf3be30823623953ef905d37831a080ae3ae1d0c261f7921ab76700000000000854d0000c3500000c3110000000000000003b003d52f4c9863d3ae76c166247d9225b19a416ad39f7f46ff197bc95f695201dafe600000000000854d0000c3500000c3110000000000000003c003e76fbeb3ff3afa2700541928f23f11d59f8a56d40abd6b10086d70ff568c1b39d00000000000854d0000c3500000c3110000000000000003d003f83a811fe4aa924595c69a33b636f0410f8da6c59683048f6a63d011eecd0fc7400000000000854d0000c3500000c3110000000000000003e0040ab8a8b2d7759b68431f7539de4e7c40fcf22a15cbb3d472dd20a31d690bd696000000000000854d0000c3500000c3110000000000000003f0041c1305b0e61a9ea2da6d693234a815de4e712aa7a38fecf60291bb0e0af44276a00000000000854d0000c3500000c311000000000000000400042434b51045e65ddc0caf75b30495e146c0076fc28951d7396a23ecff1690e2f8a00000000000854d0000c3500000c3110000000000000004100439df059ca0da40a29d8afeebcfcd02cefa7cdef598a2673065f7d55b6617884c000000000000854d0000c3500000c311000000000000000420044ca418d14c97d01e4c0aad582a5174c09a21638342681bbcdf02ef3ff5f56cab200000000000854d0000c3500000c31100000000000000043004537594556a4f9feebc7b9efaf166d36bc7011be23886a2c89e590f9ad2a4665a500000000000854d0000c3500000c311000000000000000440046ad51420b09a07509e900aaa4fcc6cf6bace255fef4d89c31727806446edbba5100000000000854d0000c3500000c311000000000000000450047c733ad0dee32d9a52a4d62c9c96186e1afd55d35f54ddc21fa980f771e17fd7f00000000000854d0000c3500000c311000000000000000460048464b71d7e34e559d518b268791aa82b54cec4fc2aefa34cc66fe9f21eb90948a00000000000854d0000c3500000c311000000000000000470049cc2d776c5d23a365ffcb4604ee65a09cce259c871dab39b04939d4f3d2252a3d00000000000854d0000c3500000c31100000000000000048004a6469912e6512ff7a494d00d8ab8977f1378479cdd5919a687a6b4912af46667e00000000000854d0000c3500000c31100000000000000049004b378f6cfb89eef63eaf08aa5d3f5d83c90698ba63169104be2a28e46f9d1ccca700000000000854d0000c3500000c3110000000000000004a004c09e07c300817fd060a676575d6376ea8deaccbe2eec3714954f953826781831c00000000000854d0000c3500000c3110000000000000004b004daf5d250305bc69ba1cd8af664d439f6469f65361e3f5fe870b765f99bf9c840a00000000000854d0000c3500000c3110000000000000004c004eaf472cbaa57978e18a7beb1aab7b3231d10ca16d6900093ded5c8ac76aee2fdc00000000000854d0000c3500000c3110000000000000004d004fd208f7cc7c79c678fa3d3af63e43f8145e12c509786de6d484f0fa9eefd4df6500000000000854d0000c3500000c3110000000000000004e0050e28a412244c366997dc906ad005406191314722626a355dbbaa982075b3195b900000000000854d0000c3500000c3110000000000000004f0051164032bafa294351f3da469060dabc9ed49a700436b28e33ba98eb19575777b200000000000854d0000c3500000c311000000000000000500052f7c31d1c01ea89f750fd13cea775b223d7b33fb72787d312a5491919dbf1ccbe00000000000854d0000c3500000c31100000000000000051005325364a6509a5cad019874190a2d2575fed1814db0ec63349e0d45da1ddef18cc00000000000854d0000c3500000c311000000000000000520054d67bbaecdc0ceda6c4c73f713e4f2b0720eb3d477f514148f2dac77cc50ea51000000000000854d0000c3500000c311000000000000000530055f6bb6e89201ad4cf91179cb56daa171bd244803fd2d2552f81d9e8613b9a399100000000000854d0000c3500000c311000000000000000540056e8f85ca405596250eaa741554d424ed40359b7bc9a29df7116ecf508708d8a7000000000000854d0000c3500000c311000000000000000550057c87b12700480cfac14a3b6abeee0ce1529d0c8dcfe84a8ba767e97662476653600000000000854d0000c3500000c3110000000000000005600582caeafc4b9bf93574c0f8d4bc94f57a85d9f23efc58c769e716f0d7b18b38dd900000000000854d0000c3500000c31100000000000000057005969126f4e713f05ba71906ad30b5673030fe0d3f82d874741e2dc94bdcea49e1000000000000854d0000c3500000c31100000000000000058005a2bb80bd902fbb168b3121206e194f44db5ef0cbaaa3198ecb971466d128f175100000000000854d0000c3500000c31100000000000000059005b4082cad26a5c16ddc9f61d81b789669510a05281c338443b02ba356ea887c02f00000000000854d0000c3500000c3110000000000000005a005c265d9a20fafd71d0aa9696c1aaa52a8b570d3cd3aecff66d6a2ccbfa71e14d3900000000000854d0000c3500000c3110000000000000005b005dae6967ae7b6549b26551f919039be40948601cf75d580bd8a7273102944d505d00000000000854d0000c3500000c3110000000000000005c005e98476373841a91019fc58c72deef72db55794552bbe06b99a0cc70b545c257c700000000000854d0000c3500000c3110000000000000005d005f82011b8a630ed5a1d31aed2f71d4007347e50c49d0273abc634d77b7fd732c5700000000000854d0000c3500000c3110000000000000005e00600ad83b5a1329af2453f9a24a11729b565921757b15b24342cfa0cd68926626d900000000000854d0000c3500000c3110000000000000005f0061e98244eb7266bac47dfe83ad32ea0aa6c2a71c6c6efeeb6fc94dbec2888836a000000000000854d0000c3500000c311000000000000000600062c0bd14f5ac63c549c1df54d26ebd93b57f93f3610f87dc19b15d6178e97a3c8700000000000854d0000c3500000c311000000000000000610063c56dcdbbffe2ac6cd828e1885083843d6dff6883b91a547ed66ab4315d7e3bf700000000000854d0000c3500000c3110000000000000006200642e89e3b0df85fbe1eff77b8841b90b0fbc491812b75ac167e0df33b212447b5300000000000854d0000c3500000c3110000000000000006300659ed51626c3998d5bb14679fe39d2fc267375317eec386d12d9805e45bd9082d500000000000854d0000c3500000c311000000000000000640066071eda545451d9d4b94201fb5b5f6623d970d3839e46f870d7fa3bf2d2cf7ece00000000000854d0000c3500000c31100000000000000065006793865c1b6f4070c9bc30c2f29286d8eea729414d2828c3827d7add206c22b5d800000000000854d0000c3500000c311000000000000000660068fdf7662574899c10d0b4b4119824dc80a26595b4b44284ab1a6d95bf48da05e300000000000854d0000c3500000c311000000000000000670069f6e8deab009f70f621e12f36a033dc1bf7ce211100ec76ebbbdfbe1a8a296bbf00000000000854d0000c3500000c31100000000000000068006a3c96ed2d4e91b99750437ed50e90544e3ace2456c7fdef5e6d81d1c69797041200000000000854d0000c3500000c31100000000000000069006b64507da352e9f70ab879dbc4a2aa073630b6d61dd0e1eaf5bc215510ae551ed000000000000854d0000c3500000c3110000000000000006a006c523085244700d8bc42a08c99dab05caa6d4e00a0a3e974355e6a5cadd6a8604900000000000854d0000c3500000c3110000000000000006b006d843e8fb4fde1fb78042e83c43096cc68ab22b2d41b945781c674dff62520d3cf00000000000854d0000c3500000c3110000000000000006c006ed4c3b0a4e860f9d09f8e28525c44e2b32684d8ba68228a3f1f79db0598b2eea700000000000854d0000c3500000c3110000000000000006d006fbfbb7a54fc65f2cc087f91d4a54e89c6a9189994fdf807cbace173f150527f4200000000000854d0000c3500000c3110000000000000006e00700940c82246dec163f6bcea0066dc1b8e42153a9a89cb87335bc874fef74954ae00000000000854d0000c3500000c3110000000000000006f007110fe330651d32c31fcab4753b4467cac95586300c264136f8f86aac3721fb16500000000000854d0000c3500000c311000000000000000700072a82c392b6da093a4bd54e33100ea7cd77b5bdcf688b1fb691b474a10e0bc363600000000000854d0000c3500000c3110000000000000007100732f743009fa6915ebadbfbfa27399c3a30f9b29b48e39e8e76a6bef920de3a57700000000000854d0000c3500000c311000000000000000720074815323725370ca6a62de58f815237acb314bc3c4ca763f5c260923f0dd2b4fcc00000000000854d0000c3500000c3110000000000000007300756727a4ae8c2ebb1d033eb969abd7476e90dc6b40b54f75b5f4d6d755679f6c0900000000000854d0000c3500000c3110000000000000007400767e07701c7cde480625ca923bebaa832bc17a6abed269b7b1d3601175c2ff6cfa00000000000854d0000c3500000c3110000000000000007500774173d50ab54b95deae539126064ca1a69a831ac4bfc2c9cc419c6f1da7572f5100000000000854d0000c3500000c311000000000000000760078c8bba7fb3d037affa75e8ab2b6784f404e9ef50f7a76b9382874b24086d5b5dd00000000000854d0000c3500000c3110000000000000007700791cae9402e611c095147a803f5b408d27ebaa9344cb951fb073666fd308fa557400000000000854d0000c3500000c31100000000000000078007afad15542780583e26b83691532044687dce16a82ea097c13d339da899a5ee02a00000000000854d0000c3500000c31100000000000000079007b245471ab91354bcc2c2bb09ab2a2b96840d5447ba9e3b9db177f63bf1894ab7500000000000854d0000c3500000c3110000000000000007a007c9a09a07adac2cf5ee978ef0898161ab40e666963976b0dcbbbaae8a0d7f8b28000000000000854d0000c3500000c3110000000000000007b007d14e2ab39cda053ec7cb75fdda214d03f85c30d9d137083645ec85d70119906c200000000000854d0000c3500000c3110000000000000007c007e768ff392d5d959b68e4eab28b75c719679c43ccc65060d8d9d1b55c56e410b1200000000000854d0000c3500000c3110000000000000007d007faed573b2efbc90729cc17c11f1947952c29ba79945746be3a3635cac70dcbef900000000000854d0000c3500000c3110000000000000007e0080b5f95315fb041ea28586d613d2a976d82d348d960989c7b9416fb3d56104da1600000000000854d0000c3500000c3110000000000000007f008140d95f0c5da8d32977131dc41a05a92f1a6876ca043a638240c2b1c8521f111400000000000854d0000c3500000c311000000000000000800082e58f57a865ff75144bdd08549c83c656431b680ab2e57f55501308f826216b8300000000000854d0000c3500000c311000000000000000810083d7f2320258354c20a9069262030e08e266332394343063bcea0cb1869f329ea200000000000854d0000c3500000c31100000000000000082008467c32697927595a930d79d6af1b4d0ef7eb48eca60a66b0b9295e281b055345200000000000854d0000c3500000c3110000000000000008300857954935a783c0293c7338c17e95ca0e84642aee0b1357adda4264d19d6dfc5a100000000000854d0000c3500000c311000000000000000840086649f6f69ab5b4062727281bfc562602b8d23866140ff820bae3629c28d2b68bb00000000000854d0000c3500000c3110000000000000008500878646a8a5d0c0a6d6abfc5db6d6f7f0675e2e502248d2c9868363efc858272cc600000000000854d0000c3500000c31100000000000000086008837ccf66214c602f0155d3350d8410b27f835f10d7ed7480d38697ee5928580aa00000000000854d0000c3500000c3110000000000000008700894dc5c25b4e7e181bacc5dc94f96cea194f4ad582ff9082e49f43af951f5f2c7200000000000854d0000c3500000c31100000000000000088008ab4a423a735be5b23a37b7e502b1d730cdf511b5f5dcd7e3dac491dbf42454bd700000000000854d0000c3500000c31100000000000000089008bd0300ea2db54c07c732c435c8b390eff7af09d1fec7d4183f26c11af6f06ae8400000000000854d0000c3500000c3110000000000000008a008c06eb3dd61b8993afd0aad49eaab19f9b363acc565df1eee863fd0cf189ae9e7900000000000854d0000c3500000c3110000000000000008b008de2f3402d5ec19a1b0c2df4f3bb8f8375692869e820a1e4071468c85bbfd561e100000000000854d0000c3500000c3110000000000000008c008ea4210d942e844c1b72a9418b1b4b3c4b0f6e514fe3b0f75e3d7df746e95326e400000000000854d0000c3500000c3110000000000000008d008f00608445dec5823810557eb56baddb365cdba4e465f2498abe69cd50b0333f3f00000000000854d0000c3500000c3110000000000000008e0090cd2a4d3bb7d1b6be07dd1f3c90d3cca5f5b3cedb97379673782f5afb23c3020000000000000854d0000c3500000c3110000000000000008f0091254a9a75cdf945525320029f7e8c5aba45dfd5b4a426f346603348a66b7dd45300000000000854d0000c3500000c31100000000000000090009225074aff4e140b03cc239486412dd0a130e3ec93b2648b95569ae55e5782af0500000000000854d0000c3500000c311000000000000000910093d8a8b5c8d165c407816b8eb5e89adb271a1078cdca4f29315161593ec86f769500000000000854d0000c3500000c311000000000000000920094aa968f2a69bb072521c766134d9c1521b5d49a368b0dd00ea3b4e21883f7c55600000000000854d0000c3500000c31100000000000000093009592d8e3576e981f7ef9fafcb12635311778fa6e9bd7544bb502e310113869bb2b00000000000854d0000c3500000c311000000000000000940096fce131a771b67463a1bf5b20192031a7780c14e21a74d4ac5a0c4468a45998ae00000000000854d0000c3500000c31100000000000000095009752267d49a6543212b263beea088dfe20227f97d1cc1a3b935bbfa0e11e47548400000000000854d0000c3500000c31100000000000000096009813496ffa2340f37f578504f289f12a5fc253e2e534f83ed2878e342bd0187c8a00000000000854d0000c3500000c311000000000000000970099c168e856e6948c8eb949d48e53f1c64266341a91c7995547f441d3f2bb7db66e00000000000854d0000c3500000c31100000000000000098009afd09c7a336cc28e97b5ff1e5c83d05c3e68257ddb2587b0c20338833512dcdce00000000000854d0000c3500000c31100000000000000099009b256bb41df47f9dc594b20b7aed566d3457cd3feab6a2ac54021ac61a65b74cd500000000000854d0000c3500000c3110000000000000009a009c1304a8cec2aaa0fb13195fc09a06d571fcc8ac066797be25754ce7da18783c4600000000000854d0000c3500000c3110000000000000009b009d82e93cabb272fbed2f0954226d7871dadbcc06fb772436fe4bd5e79a509dc33a00000000000854d0000c3500000c3110000000000000009c009e67f4fce731aef9e25e456361ebaa95b8dd448180b3780a57dd5e996e155fc12a00000000000854d0000c3500000c3110000000000000009d009fee0a1488e1db16efd197fe3307bb1ef3fecc70be961fb5d83f57293b2389742700000000000854d0000c3500000c3110000000000000009e00a0d8b7f44f79f3825b0898063c9be52e8430a4fc6556380971e45ba2bff87d41f300000000000854d0000c3500000c3110000000000000009f00a10cd85fbd33a658c370afb29f2c452a9c176e6b15b85cb999c849c3e5fcc3571600000000000854d0000c3500000c311000000000000000a000a272fb1ed9a9b17e17dc9252ab1b6c57842adef570518d2ddb9ca68fbd6cd6864f00000000000854d0000c3500000c311000000000000000a100a30e18954a79e2d97b37db47861208d7191933965b8a66a8fa2192c2f03706539500000000000854d0000c3500000c311000000000000000a200a4f2891d485e7fd8fb3acffbe35e13ee5fbc7ab4bd4b856819d6cff8e9b46054c100000000000854d0000c3500000c311000000000000000a300a5b17223b7dc16a717a0143c327abdf0ecec7a56ce9d4a42a187228d4764f3adaa00000000000854d0000c3500000c311000000000000000a400a6e664a80ba167ef9a42fe98f9e60997922edf02ab62b9515f3cc04cd742bbc0a000000000000854d0000c3500000c311000000000000000a500a71dc5c03a9ee981ff70177b153af6073d60b5dd62f266b099e2a9a4d4db5228b500000000000854d0000c3500000c311000000000000000a600a81cb37a70492764ccb4e974c7d238592775eccce4089735e69dddad210d68642d00000000000854d0000c3500000c311000000000000000a700a90821c9c1ef4a9e69d5f508ce86029fdc4601e2a1faeed4a8bbc0617640f5cea600000000000854d0000c3500000c311000000000000000a800aabe06ffb02722e4f7b8b02d33806acdf4fc6050e20ae54bb1955aafd407a8be9d00000000000854d0000c3500000c311000000000000000a900ab7518052cc5fc266960839ca65bbdcb933b36eaf9b2f61c37192e6ba5effca76600000000000854d0000c3500000c311000000000000000aa00ac9ea401167af230c9b049bd02d00e1636f21319cdab24c46937d04d8a4ddb41eb00000000000854d0000c3500000c311000000000000000ab00ad1f1b2f6031fd9c8380f15b2928f042e9645332d6e0bc2f55e06778108ece17ff00000000000854d0000c3500000c311000000000000000ac00aeb8410e7035d2c12ede9ebc2c6bc81af83b27d172db05b66b10c7c7f48c57222e00000000000854d0000c3500000c311000000000000000ad00af1b3fb423b0daed330e204543b2fab0fa9aafff31b387535ad322f5743c7b78d800000000000854d0000c3500000c311000000000000000ae00b02c9d6f58fd85711958fcfd2824474818828d5ecb641bdeed41c988b6eec5c22e00000000000854d0000c3500000c311000000000000000af00b1015bda7aab18ccb03f23a253bd3b94ec400a67d057cadeb092167e45001856df00000000000854d0000c3500000c311000000000000000b000b235eab6ff950e9ec80d4563255908a7ea09a5ae5dfbd0e3c2944295709ecd7e3500000000000854d0000c3500000c311000000000000000b100b3957cfa2365934f72f6559f95997c60304b0a98e3c9e0dc99ab7ea6554bb0a26100000000000854d0000c3500000c311000000000000000b200b41012ec8ec892231dbc8ce89d60f3bb9b375718e460e7b5bd1b8e84631e291a9b00000000000854d0000c3500000c311000000000000000b300b5ca5d940ca7b8fb92c4baecb5a6e464d3c3b5aa5c12e67d9e70650327b858ebcc00000000000854d0000c3500000c311000000000000000b400b69d5412f7b492ac0203d7a82aabe23f8aa7776d173df7da0afc42147476614d9100000000000854d0000c3500000c311000000000000000b500b711d3ab9e16a2c977acd6d131284d2fc8c0d1af94dd6191d6a84344bc4722135400000000000854d0000c3500000c311000000000000000b600b8eb9a2b0b9a696572dfc2adebd5f9177abda3769f294babd21480a6839801474b00000000000854d0000c3500000c311000000000000000b700b9078be2982828fed4bb25781dc9bccaa624f6058c722eac222b4a659e9489edbe00000000000854d0000c3500000c311000000000000000b800ba819151088a9e7aada910f638ca1a248bc94b4878b1031a41c101af8f35c9aef100000000000854d0000c3500000c311000000000000000b900bbf28de95ff9677f064bb695651008dd14631262188ce64c392f192d6efdf8d4f100000000000854d0000c3500000c311000000000000000ba00bc9ea9e5ffbeda1fd8846698475c057567d1f2c133c6d548f3e9722b3b8b9ec69600000000000854d0000c3500000c311000000000000000bb00bd0689a54e3457da2e4ec2a9b07cad6b256469eab859e2e8354b27bbb9f08c3ca700000000000854d0000c3500000c311000000000000000bc00be460f9ac1863a0d05133befb41a245dba3e136fe3e9de43e1000316991d135c4200000000000854d0000c3500000c311000000000000000bd00bf0006cb6b8ace8eb6f74e97e484bb283a123132eb4976f0c665de5394ed0b022100000000000854d0000c3500000c311000000000000000be00c07b38bdf8c89b9b0db4eb62ff10e98308e9ce8647200922d8d790b7dac81bef8e00000000000854d0000c3500000c311000000000000000bf00c181157bfa3420a29f5124ddf629161d0b986616e8aaac86278b838b7caadd850800000000000854d0000c3500000c311000000000000000c000c2b67b3796c8da1c71ac82a89c2fa7a8b40e2ee560c6df7ac42cc99e51645918c100000000000854d0000c3500000c311000000000000000c100c338b2fb62c7aa54b15597b8422142cd49a776092d55f92d5dad31e417844c63b300000000000854d0000c3500000c311000000000000000c200c41e7701adb1fc46ca8f1c24d0e24cc40fe7d9daff7deeb63211fbf5bc1c123c0200000000000854d0000c3500000c311000000000000000c300c5e2cb25607821cfb7e67028ef96056f6e3f81f72db1bf8851432472936bbc647600000000000854d0000c3500000c311000000000000000c400c6d9392204247dfded154f8863582e44f0acd0a30a3bc8a38ab35ab744b73ca18100000000000854d0000c3500000c311000000000000000c500c792e42b479904bc5abf82fbb467577afe7f53632f5a8e89be6791a36b49c326f500000000000854d0000c3500000c311000000000000000c600c84412ac310f1fbc90220101c29f748304f9496740c3eee14b64c8fc171558352700000000000854d0000c3500000c311000000000000000c700c95fdd380ed620c5abc8a4160f18375928c0ed6a926043eaf91365dfdefddef93f00000000000854d0000c3500000c311000000000000000c800ca0c8edc3204a6c47c4e9115755a24c31763efa78db17bfaa9645aabeb4972c89a00000000000854d0000c3500000c311000000000000000c900cb83e02383dcaff8714d79fc6df28563584f79be4435bb3632958eb98649d214e300000000000854d0000c3500000c311000000000000000ca00cce136d56b460bf32ae200ce0fe980817a2bbc78112c9969c70db679c2aa30d73b00000000000854d0000c3500000c311000000000000000cb00cd4cd753d127fca8a62042ce2dae9cf035561530b3e2e95f60fe74245ce293dfb600000000000854d0000c3500000c311000000000000000cc00ce2ee0ed4ba29122e130bfd9a32626fd46ac15ce11642d63e5c596b7d23d6b142300000000000854d0000c3500000c311000000000000000cd00cf92b59998dda902b1005f23746254e625f4a70103d02b0ee3cd7cf6f19425188d00000000000854d0000c3500000c311000000000000000ce00d0040c9ed7f1af1ab9d41814211ca770b40c250364d4a26bd0d57b4d6de3d406c800000000000854d0000c3500000c311000000000000000cf00d1cf54bb912bf745b8761c10c6ed566edc96e82ad8e7a9bc3fcd22fb0e3974cfb400000000000854d0000c3500000c311000000000000000d000d2a81886dd94579a2ebe3c14379490aec6fd8bc8bcd7981094f23011e1690bf6a800000000000854d0000c3500000c311000000000000000d100d353b474acbb9539a6631f51144498802e9eeb237d8ba7d11ac050c4a392129afc00000000000854d0000c3500000c311000000000000000d200d4c08ec36aaac6cb7b82a482059dd448f61a9b25249bc5d4e6be5517baf5f5e24200000000000854d0000c3500000c311000000000000000d300d5693de4032c7083eed7fb1803c6d5624a7e32d3107bd7a3939cc89a083388264b00000000000854d0000c3500000c311000000000000000d400d6f9756396a45f40c30da3a0a67d4dc1838b20a098e5351a1e4f344dbbe00dcb8600000000000854d0000c3500000c311000000000000000d500d7780b34031a24731ecf7e0e6844097a8e62b43267d565dd6d3a4886e55620678900000000000854d0000c3500000c311000000000000000d600d8bcc1624846430d2de342f54d7b8e61d264bfa39528a1fcc63532b291c994c2ab00000000000854d0000c3500000c311000000000000000d700d97a217a4e347a614aa9e651bd234eb0c3d49830a6a804e7ede79c77959ce14f7300000000000854d0000c3500000c311000000000000000d800da1d361f1504f6625c8289222276f9bceafa3281738e27e15f6547884c9ca7a5dc00000000000854d0000c3500000c311000000000000000d900dbfe2501594d57b4c25674b575507428defccdccfe69543a182575dd4817b9179d00000000000854d0000c3500000c311000000000000000da00dc45eebf5678f6a340be93a51d2e263e764839c2454a10bf8853fac76861923dc300000000000854d0000c3500000c311000000000000000db00dda5d81abfe6b8ac5e302b89b6057f782aad54a65adb51ed77e50d3a366db9d7a200000000000854d0000c3500000c311000000000000000dc00de0e6556de4b9b77a9d866d3bda9f4a87208766b0cc701414a4deaac8da2acfe5a00000000000854d0000c3500000c311000000000000000dd00dfbc0a8294b1cc8edafc68b3a95d9263f1499a02d63c93a0fe30633ad556a5b44600000000000854d0000c3500000c311000000000000000de00e0a1078aac0d34309b68c23dcc3d7da53e7f88c24e6ed76850021ab5ac808d6b1a00000000000854d0000c3500000c311000000000000000df00e1412173ca93c1a3db66b0695504c0151d21f1e65e417d5671dde59931256ac4fd00000000000854d0000c3500000c311000000000000000e000e2e601a904450d4f2e168e917bf4438d900cd17f571719d60bb900fb0e0740d65200000000000854d0000c3500000c311000000000000000e100e351a8a26ec67cb3ba4b95f2ac4be486745285e9434dcebd8f7df9f743dbd3653000000000000854d0000c3500000c311000000000000000e200e4d20396497388e26286a2a9bfef575a2269ea85b36b4005d294302aa05375862000000000000854d0000c3500000c311000000000000000e300e5abd37177996abc69e2ca24b121f45b32718a095250c0d533a89785fbc1692f8100000000000854d0000c3500000c311000000000000000e400e69491b3619b64e09337ff2b31ed0a98c07d47c910346fa6e43ff4da9c16a4317000000000000854d0000c3500000c311000000000000000e500e7fa38ff7b2f735c6d7ed6697968cc2792e9ed4c2cc9957e2152c773a23e10969000000000000854d0000c3500000c311000000000000000e600e8d82876021b20bad6dd3833506b1e2c7ddb85e8fc754b11f0099c558ad676db0d00000000000854d0000c3500000c311000000000000000e700e93f7a7c70b38e38652fe97203085488d82bc013586ad8a093aeceb4deaf2ee93b00000000000854d0000c3500000c311000000000000000e800eac0e5f3123292708627ace19965a0fb622f8eef55e33dd21febe02b2fb6dc202500000000000854d0000c3500000c311000000000000000e900ebe9c9a099ac9f0603aa645a573caf5b99f8eb03b28e881010b96d87e1ed55ce3400000000000854d0000c3500000c311000000000000000ea00ec1d875edf3e4221594917fca299d02b1c3f1990f6069f944330c3b2ad9edcba7b00000000000854d0000c3500000c311000000000000000eb00ed48640f071c6bcd7c1a94314bd8615a6f6493919fc216a555b9dabca0121775f500000000000854d0000c3500000c311000000000000000ec00ee457e830ff6e6eae43268bb92d5fecdbf8832a37b575285b1e113b29ea7e5f8c900000000000854d0000c3500000c311000000000000000ed00ef55b2b3b16fa963985019f4a6cac17b51fd164b85efd22180221311ba043e168800000000000854d0000c3500000c311000000000000000ee00f0eed5a265e23b6fee64a6800737f859959582dae01bf804141b7dc199139dc15a00000000000854d0000c3500000c311000000000000000ef00f17920b76551c9c0381e120a166e200e20e3b2e0d54267bb3c9f69a0a374af5f5300000000000854d0000c3500000c311000000000000000f000f2550cb6ae690524fbf6a6845f2df5da2d7e4aacc218263bc216db4be3c269ae5200000000000854d0000c3500000c311000000000000000f100f3b3ee1187678512ea1324b3e746df8c76d52008263fd5ed6ee3307608b02a7e6600000000000854d0000c3500000c311000000000000000f200f4c0c14618d3cc34a24260fcb5c7819db74e65d8eca279451104abcf31eb27a7fe00000000000854d0000c3500000c311000000000000000f300f564108c7261a446b472fa45a0c29af4b821210c8aabe6c113d292b6300224dc2c00000000000854d0000c3500000c311000000000000000f400f6bcf946cf2868a4749a156c578a4a6e40a2a7bc1716b35880eb9d87a4748ae09600000000000854d0000c3500000c311000000000000000f500f773465d6084beef0010d1ac1e7e2fcbc6783d854f3927e6d51f272c52bcc6bda100000000000854d0000c3500000c311000000000000000f600f86d175c4b0f2a9dbe8437a34789c0343991526bc83bdece3c71963f86cc51183a00000000000854d0000c3500000c311000000000000000f700f934b71da18c6f995f91aac176268d6ac0a3a2acf2717801ebf23b9d91926e914400000000000854d0000c3500000c311000000000000000f800faf380ea1d68711d9db0ab45e3397dbd39b5e728dcec8d55930f6f2affb0ca12c500000000000854d0000c3500000c311000000000000000f900fb834434e1f82adc0b7ac0bd9ec0d4179d36e2a611ede605a42977de8a9e466c4500000000000854d0000c3500000c311000000000000000fa00fc3479e0daf8b53f8c04ba0eddac3848dd88ae4cafeecc59bdec10b881101c883b00000000000854d0000c3500000c311000000000000000fb00fd61b82153bc8cf58c32d180d4f46da8f40ca6d8a3fff5ed4d5ba16a1fa73d4e6700000000000854d0000c3500000c311000000000000000fc00fed00cfde8a915c23cda075f4a88dc0ae74792bd8ece23dcfdba72f56b9813b0eb00000000000854d0000c3500000c311000000000000000fd00ffd559de901dbb27927b10cbb56d57e3b244a9f5c248ac8d85221547ba9dabfb3d00000000000854d0000c3500000c311000000000000000fe0100c68cdfd7846524e752cbb7ba1aa0e495352325399e03d91a7016e3671fd964d500000000000854d0000c3500000c311000000000000000ff010191141f42588c3501522fa211cdffd484c7889c2cc6716de25654169609657a4900000000000854d0000c3500000c311000000000000001000102e6ef1453006abcf1da0dfe1572ef43a2082f91f230b0f14ce55a6361e8650b9200000000000854d0000c3500000c31100000000000000101010312266945cef53747caa27b2ecb733afa5afa42bac3afee722206d2d0c5abb81100000000000854d0000c3500000c311000000000000001020104c7ca46af876947a45686fa1f11f8842a6ba14ef982a40f5138bf420c6f73176200000000000854d0000c3500000c3110000000000000010301053f1661f5435f16624c0e51a7a644ece8fe3d7f84af61a410787b30feeeb966d000000000000854d0000c3500000c311000000000000001040106cadaf4cd6e8d42db0682daba7a65e6a51434b843b5cf75213ea4427e0319f4ef00000000000854d0000c3500000c311000000000000001050107bc7d18e01e7f39891798b254ec5fbfd4066bfa49c634f8e3eac8fb5329aeb74300000000000854d0000c3500000c3110000000000000010601089cc3be9f123da1cbb918809e10bee97bdc9fa805b06c557bea2210471fa3307800000000000854d0000c3500000c311000000000000001070109f1a7aae9140ccfd98127b56044d9104e9de5fbcd682859598eef0d6f9ca92aab00000000000854d0000c3500000c31100000000000000108010afaa9ff70179a7b0eff8bef5d45379521f1110e8db1ee03b96d579764c8c344c600000000000854d0000c3500000c31100000000000000109010b07d1b09149d5c5275f85a10d8cd9202c9400ca2def4ad6791075291b4c3c5df300000000000854d0000c3500000c3110000000000000010a010cca26248920d3f7592d14d56435f07ccaba5650dfb6a88e4e39075376402718a300000000000854d0000c3500000c3110000000000000010b010ddee81eaa499f230eefdac7766ac46fda3d15ce50ba634d5dddaa37ed8b27706f00000000000854d0000c3500000c3110000000000000010c010ecddf241b4264b637a0ac3315b33d20ad2b4ee78abe9b109cd2ff12910527db0500000000000854d0000c3500000c3110000000000000010d010f2073acd612d2f8b24c44a0d791c8e458cb4209a578c5e54059a80bf078ea62f200000000000854d0000c3500000c3110000000000000010e0110f2bc07ca3c0917130c0549bb4528b3d30787e8a58e1a4e395f797ee1d5313c8600000000000854d0000c3500000c3110000000000000010f01111adb3bb2a1d5a92c063c04d8e4ccba90f31d32705ab9e5bf9937abcae9bd411300000000000854d0000c3500000c31100000000000000110011216d6fc2b34c08eb4c4047768a720a11c236f83a193eddbefb5f177811e1cb74800000000000854d0000c3500000c311000000000000001110113f7ef1e699a40d95dcd4c155bfe82b2458ffc85aad59e9a7b9a38b6f8e20129cb00000000000854d0000c3500000c311000000000000001120114ccd2d776df1d26ee861943082701bbb4b69249fadd8f143b597ac73e2af5c1c500000000000854d0000c3500000c31100000000000000113011556ceceb7b569011a449af0c305c8dbd28c6f9703aed425f8373c8497188b2b9e00000000000854d0000c3500000c311000000000000001140116b2f5712de794c52523a62778e83e0e59a1bbc6f06f97b7789670f0e5039ea9a100000000000854d0000c3500000c311000000000000001150117693cec364193e49f8f6aebf42955d1b49acda6c888a3a6e2ee274e372a45494b00000000000854d0000c3500000c3110000000000000011601184ebc0b53de00bce7dc1119c28cbf0648dc7ad74c976e4b49b1e5ab6d372fe7c800000000000854d0000c3500000c3110000000000000011701197dc46f6965e74bbaeb7789ee532264e843103a39d7a9d75132de7e9b5ef02a5300000000000854d0000c3500000c31100000000000000118011a00b1c69e8c2aee8a42f849b56150603e0541ba97656911b2bec92fb6b80e1c4700000000000854d0000c3500000c31100000000000000119011bbe8e25448c1ed1d4cf6851269c121e1c45e3cd58292714ca7a0afcd8c6813dc100000000000854d0000c3500000c3110000000000000011a011c42ef09220765bb717aad3c2d4dcfe180e8c0ebd8421df820386626d3a85e4d2900000000000854d0000c3500000c3110000000000000011b011d595e3312d192af31f4735e7352203a2b3e1ff5c8712247ea76b8d6521328a28100000000000854d0000c3500000c3110000000000000011c011ebb01550134b55966c3b8a175688d7fbd913988eb37701053abdebcd07b2c0ee200000000000854d0000c3500000c3110000000000000011d011fc8b44418ae4ad010eb4a16252265eb6e1ffbb8238ae74055e93170d5aed2959900000000000854d0000c3500000c3110000000000000011e0120996d1a0367ed61db81a131b5b163de5d6f2f693055b87a3cc7a0c6a15bb72ef800000000000854d0000c3500000c3110000000000000011f012126b6c2e89d781417985751c1fde5cd6069167fce4c56138370bcaa24d03ce24b00000000000854d0000c3500000c3110000000000000012001229e7af1b72e21a2785359677d3c101e9029b117a43dba617f8ea56d7dd022d8d700000000000854d0000c3500000c3110000000000000012101232d2e1c3b68cf4b0e8bd492eb5741cf78a3b36847fa0a97c3304a95dcc7a8a14400000000000854d0000c3500000c3110000000000000012201240e06ed3926bb95bf57bc7ee2c9e5f1c5bd661bacb1ce5a1a2deabe6736c8367800000000000854d0000c3500000c311000000000000001230125a6abd820de07f5cda1819cd9a9146f0adf4d8dcc60e3cab50bc5e1abc14891e200000000000854d0000c3500000c3110000000000000012401267ac610ff9d6969b53d28f85a92a1fad522b3bee82f76f010f45ef0a3e4c7c15d00000000000854d0000c3500000c3110000000000000012501277255670d53527fa3d330df360ac6c98d49a6a5ce3f6fdf1de4ca700844c8fa3e00000000000854d0000c3500000c3110000000000000012601281d6d6e357f1227e5e942653388260e8238b5612f4927fbfdce258bc4d0b7594300000000000854d0000c3500000c31100000000000000127012954e9dbc5cf7149868716b45dbdecdc449d0b39b5eafd69e3b49a7ed29d4459ae00000000000854d0000c3500000c31100000000000000128012ae285b863674b89d72cfb884f5cd5eb523ac0b493661c58f7851c5547a959ed8100000000000854d0000c3500000c31100000000000000129012b819532ccb84729849fbc4cce0ac5f8236350c3b67b316338e15cc62b954d30b400000000000854d0000c3500000c3110000000000000012a012cd23d76e2a5311b80e76c5229b01cb07f2bb85646fdcb4c62adc82f198b047beb00000000000854d0000c3500000c3110000000000000012b012df661b595023ecb62cbf93a64d234dbf889166b48fa8eb394bb9fbe841e1cd56600000000000854d0000c3500000c3110000000000000012c012e0513cfb510f00b170cf080b3ce2b850cec369d35c0550eff4b8fb2b6c91dfda800000000000854d0000c3500000c3110000000000000012d012fb9d1d054cf18aae245c31e11c0d122ed5aadd8a3b35ad39dfe30ac8798a46d2300000000000854d0000c3500000c3110000000000000012e0130e06b5899ba907b2923ca8677fa61c7f0943cdd7c1c40def5d83aad630bc00eda00000000000854d0000c3500000c3110000000000000012f0131293692db9029a74afbc68d94e574decdd9aa7f388e8aa344379018c4da18256e00000000000854d0000c3500000c3110000000000000013001326c2b3b309d75e679971f71daae4c69990da8707c359765d2c1f74a56ea45a25100000000000854d0000c3500000c31100000000000000131013391b688dbb1263e7d9c8898a7356dd313e14f066a6db7dbd8a05af623cfb1423000000000000854d0000c3500000c3110000000000000013201345d9daa987405ea5c478cc99679df047e1cca5d28b2ae550d88570c9a7b361b4600000000000854d0000c3500000c3110000000000000013301352d4bf0f568c72724e502c4ea615d9bdfe76205af4649db3381bb039a6c81d5e700000000000854d0000c3500000c311000000000000001340136387afecf394d4357604f540282bbd442f84f456c5ff810d7964941b31bc12f5f00000000000854d0000c3500000c311000000000000001350137b19741e64da87111ec3ab8a714db12a89a0e19cf667a0049a9dcd72aa5c30cfe00000000000854d0000c3500000c311000000000000001360138041c4bfedd2c41b363acd3fa746b0e65820abf0259851616c147a6253982860600000000000854d0000c3500000c311000000000000001370139bd4d8149d173108a9685d7305f1624dae2c8dda08e475d90494ff93e3b68d3f000000000000854d0000c3500000c31100000000000000138013a6807c6439794cc48e36826e845ad919d758bc0308611f2b2063f35e59badcc2800000000000854d0000c3500000c31100000000000000139013bb94b58b11fd0a16ba852395acedb03563c9992103fbb9c4541c0140ad25bba0d00000000000854d0000c3500000c3110000000000000013a013c98f301ca34257ff0bb60f8a29e8c2e75569f12c2e757cfcd86d441987754db9800000000000854d0000c3500000c3110000000000000013b013dd33a298ca334510f32a78b0d175ef81aa1283c5a43a0a36997337a188cf28df100000000000854d0000c3500000c3110000000000000013c013e6535f74d7b79af5ab10020827567deb1d7feb0d68c97b24375444fc91685c3d400000000000854d0000c3500000c3110000000000000013d013fcd31ecebd30f110cd6faeddf43207ca9ce128876a99d882d1eb2956ac66baeb600000000000854d0000c3500000c3110000000000000013e014080dc9f243390f1e5ff5f824b3558f3a71558ee2b76b6dc85b8767d8c3a762a9900000000000854d0000c3500000c3110000000000000013f014135e665b8a599899c0e0c8e20d270917ab23a1c5ec1392031cda458edb1c95f4700000000000854d0000c3500000c311000000000000001400142f390439757563240273ed69da2e0b49158ffac612227c538ab4df21e668f072400000000000854d0000c3500000c31100000000000000141014375199d7a5f1d5702a5e97030d9b85af5459e59ece07c34cf4eba44fc2fbe75f300000000000854d0000c3500000c31100000000000000142014488eb334984eb5a14d76478f8c7e277804cb813c573fe185e1269a0b1b3a7c4af00000000000854d0000c3500000c3110000000000000014301452934a43425a844e0cfcdce1f191ac83c216aabfe9cfae82ad8fb80e5ed32fd9500000000000854d0000c3500000c3110000000000000014401463e4182cabfe4bca69a2b2d26394b851e6f313fd7400e34f66b207fa7997336ee00000000000854d0000c3500000c311000000000000001450147beca3c2b2abf74149413af897d503c0940a04e65488cdca79adc80f3a1022fa700000000000854d0000c3500000c311000000000000001460148358cc7525792ec60cad166479beed863fbda52d75a2b3b21b44cb3948c74c3e500000000000854d0000c3500000c311000000000000001470149084911fc1e172537175b5cb1ed8e0ecae73beda0b4e5e13b826819e1dfeab3d900000000000854d0000c3500000c31100000000000000148014a415167f1a6ff33eb7eb3a208895a8bc11057d6fc0e3d3cde21eb23a9e8f0428f00000000000854d0000c3500000c31100000000000000149014b6bd1174aa697898fbf5fa97167d246f94cc930add143f6b5f3ba1c189160fe9900000000000854d0000c3500000c3110000000000000014a014c630f10f3c50d48328bf9fb5ecc61598d7fb7da587fb7821e4b42151afc4ee36d00000000000854d0000c3500000c3110000000000000014b014ddc396d879f620ece1af21eb2ae7af3ccb63520b7e5c2c86ff9704ecfeae2897300000000000854d0000c3500000c3110000000000000014c014e36074edd4eb00de98fd3f92fa28e76e19abdccc7183a427e0f80f2545099249000000000000854d0000c3500000c3110000000000000014d014f329bd6b4a05c40900c16d963d4cb2c32cd3b866fd2704809359d4e7be2b4d3c900000000000854d0000c3500000c3110000000000000014e0150e0d278e14a883573033cdcdbfe68747f69c02a77aee2631a53854877cd977f8c00000000000854d0000c3500000c3110000000000000014f0151cc619d317d853e8a8c9605c6b2016c5b77d1ed0ce2764865af5bb58da60d25fe00000000000854d0000c3500000c311000000000000001500152091da59c8e15c2d0b67a0c03241ed38bcaa08afe611e374ff56a04b1493496c000000000000854d0000c3500000c31100000000000000151015351d3aca70ad269740eb8143d40c40ccd9655137e1a797db99c19f0c74496eb0200000000000854d0000c3500000c311000000000000001520154e8b3f8186945c665a130ce8bb5022be66ee9a82d58bd04995dc2ed8817b9cf5300000000000854d0000c3500000c31100000000000000153015582bbf5437a38dff5538c0853418b67eed524fd6ec6730af6e83fd7e1819aa9a900000000000854d0000c3500000c311000000000000001540156126f1cdc86e71f3a885ba96d926e619de1dc600a5e917556b59349709a09202f00000000000854d0000c3500000c31100000000000000155015742f8ff596b8d24eb09eadb09653801d690e24c1fd00201534185e5d76616e75100000000000854d0000c3500000c31100000000000000156015838866d0eaecd45ba609331dc127de1cc6de73bedf6c8857b39d7bca574d5a95900000000000854d0000c3500000c311000000000000001570159fc5edceefdd5ba830ee696fa281bfaff5f19f03a517bcc14f2a723cb43db17bc00000000000854d0000c3500000c31100000000000000158015aabd47e25adb0dc27d1c5a063dd640ee22ac5716a1dcdbe152c27c2a4925deee300000000000854d0000c3500000c31100000000000000159015b44080f378bc7003840f1d01437c268fb20c7f9bf4b38cfaf1b18bbcdf7368fc500000000000854d0000c3500000c3110000000000000015a015c130157d7beaaf191672619dffdb99204c2ab01b35d299c29b4a4c9f1364570df00000000000854d0000c3500000c3110000000000000015b015d139c7812864191abffe378364b46c4929ce9f354e57994e56ef8690a9adbdc6b00000000000854d0000c3500000c3110000000000000015c015e76b343cf768eea6ea9430c144d1f833d2fd19f01a0b8d04e09ad0625c0c1ce4700000000000854d0000c3500000c3110000000000000015d015ff182f63a888b037893c449126093bb70f97b7360b903cc02705619b8232cac9700000000000854d0000c3500000c3110000000000000015e0160ddb46b199bd616225b980e5cf964c647898a44241ca45335f679143b0097d48600000000000854d0000c3500000c3110000000000000015f01619d4246b117a38112ba1bd600e78490ba170a1d1307043f3c83c2a1a2d50106c900000000000854d0000c3500000c31100000000000000160016205f692aacc895ec966a4c41045525845352c1993f7d5fad4f836f286565c62b900000000000854d0000c3500000c311000000000000001610163051eb76af4a34d66106771401412f01f4ac661594140355541f815f5e1e7490300000000000854d0000c3500000c311000000000000001620164b7e76d9101f61768d4ea6470dbf632941ad250c20d2ed9b3a9b62fc2ae398c0e00000000000854d0000c3500000c3110000000000000016301655cf571d20737997ec5f0f856dc59a1104d5b153b9377cab0a3107dcef4ee1c3400000000000854d0000c3500000c3110000000000000016401660bc27dbed94378bb13a6b1bc1a61789be9572d1d42a7611338d5a3e1351768d000000000000854d0000c3500000c3110000000000000016501670e63f7f71c19b7eda6368a15dc5dc2e7295541888e60738db15b3cb79292617000000000000854d0000c3500000c311000000000000001660168144788ed1842d11dfc84d06d656f2b8390e11778066f21d5f97626bd72b475fd00000000000854d0000c3500000c311000000000000001670169c4642f1fc647f5c8efe7acc255ba6bab55d280a095546aaca38921df1549375800000000000854d0000c3500000c31100000000000000168016a1e52b4f91a493d81e0ad67231f835c8f1ec2fbab50d1f4e5e324504000ee244500000000000854d0000c3500000c31100000000000000169016bb037cfbfe23395592f82f1ee8d5eab8a2277d8e3644908c775b16396774338f500000000000854d0000c3500000c3110000000000000016a016cd3781d0b608c534512544055182fe979fc8d439f5c49b64cc948bd6f7b6314df00000000000854d0000c3500000c3110000000000000016b016d800b2593788c198f7321ce6424338ef7e83123e241aa7fbe8dc38e7c1e3de86200000000000854d0000c3500000c3110000000000000016c016e97b0f44eae74f3a3bfba8c15b94663e93067a575289e7034c8ca6fb8fed4d1cd00000000000854d0000c3500000c3110000000000000016d016f87ab47f0d8959d60eb4147714356816f6fc95b0fb1f92abfb087f9fdd9721a6000000000000854d0000c3500000c3110000000000000016e017015c8f1a325c0740647aa9af8d6f41b9b364f2d6509cba4c318784ab5f98664fd00000000000854d0000c3500000c3110000000000000016f0171dd3765c9c21ac14cced443eed9254bb76cf595dd19763bdad8f3762118180c3300000000000854d0000c3500000c31100000000000000170017205dc8d8d03f1d1b79383291c9b91c6dbed7b5868793a94d86f8cd0b02ea9dffb00000000000854d0000c3500000c3110000000000000017101739285e8ae6dac04fec83f99de4cfd81186130119af91f3af4c2c2ed9556cd73b800000000000854d0000c3500000c3110000000000000017201745f9b9c5c46848b51684d15e7a018c16c7433251d4e05bf0a6fbb1c22e26624eb00000000000854d0000c3500000c311000000000000001730175356f2fcca2d099ddf402b0f28922c8be5276c406fb6a262a10e1ba2d37c1310d00000000000854d0000c3500000c3110000000000000017401762361231644dae69b1d58f83ac1795d89062f1d20f1e18bc58a38beeab8a143d500000000000854d0000c3500000c311000000000000001750177fc410231f56ded5634a2b827172d7ff54326b4d9ba55aff6f76dc0c1d8abe73400000000000854d0000c3500000c311000000000000001760178a3ce42369319689c90263bcf693655b2b5f73426e4299bb3edd27a61fcea84d600000000000854d0000c3500000c311000000000000001770179529f4cfceb408d8e4db8a93a81948458df679039b6b286ef8e2d6b392e584f7b00000000000854d0000c3500000c31100000000000000178017a652a19f302298f2ddedc5bdde42af0c1617f981d5942d33bd7bb436780ef950500000000000854d0000c3500000c31100000000000000179017b78799b9c840639e4d0d50fd3e6e4728c62c0bd0378096552acbbd4096572acb800000000000854d0000c3500000c3110000000000000017a017c8eb8760d4cf5b9077c2871390b10c3af43aa24de7873c145ee0efb6549c7d6ec00000000000854d0000c3500000c3110000000000000017b017d662d3d9dff9fc1daee14bac71957f258d155d3cf5351507f7f9c75f9fdf9750100000000000854d0000c3500000c3110000000000000017c017ee0dd1b9635b59c05c79911a8d80bff18b7575e6429c8d709a869e6a06e04c30b00000000000854d0000c3500000c3110000000000000017d017ffc1ae8163736881a06d397844694f4c1b611b2ec421e3e85830e6a0b06e7626400000000000854d0000c3500000c3110000000000000017e0180103f4ec21762e94c3a6176166b0112f1dec7ed9cec100950fed6956966a114a700000000000854d0000c3500000c3110000000000000017f018147369da11f17dbfdebe6329cef69ae7150b9003b2a03efda39443b9958f8154c00000000000854d0000c3500000c31100000000000000180018293127c2a56004f8d0a7a8fe5866174cea89697bf41df5e122ce7a23215e2d60400000000000854d0000c3500000c31100000000000000181018325e789b4b54bce1b239c8f012e156269e109a865bb46cf6d615412a6bd1e060a00000000000854d0000c3500000c31100000000000000182018415d04d386172988a10fb74a66f2edb3798cae4ae180cbf1b110e1ce447d1cded00000000000854d0000c3500000c311000000000000001830185d14b28e04b1008b7716368751b7281fd1c56efb14c4bda843b2e3e56a6dd167b00000000000854d0000c3500000c31100000000000000184018608135c514ba440467ae403e0a65e81f359ae4241789416377f7502ce739f6c3000000000000854d0000c3500000c3110000000000000018501874d1153cb1b5b24b1d021059395131e760ff239f5ac04487fabed4265acb7f43a00000000000854d0000c3500000c31100000000000000186018859c506c7f1424e8f6a02cbb4b0488a8253ed13971bd35d9857273fc03d9c2e3e00000000000854d0000c3500000c311000000000000001870189defb781b52260a9d6fd63130f1b8d6d603e4247eaae108d635e2e5bc04c5382e00000000000854d0000c3500000c31100000000000000188018a5335d54f7383e77089b8fdd6652f3fef12fb0f70fb2a84d2affb193bb1b7095f00000000000854d0000c3500000c31100000000000000189018bbf3e5a3c2868086d150869804d22d5da63dba8992833908943ce66f90dad171a00000000000854d0000c3500000c3110000000000000018a018cfed9687c86989821940a117f7fc601ff678677a12178775caca21a09249da22a00000000000854d0000c3500000c3110000000000000018b018df8ce826220eef839cd84d2b9df12a0f0ee5806b4aa19c173a92308a9f084543100000000000854d0000c3500000c3110000000000000018c018e1921efda69bc2cf34f1e47081b44b6400291eb927f4980a94f3134b111a2523200000000000854d0000c3500000c3110000000000000018d018f37c353aaaec137e0c9db2d33427135e6f8473ea35384b6a3eb206af450916ed100000000000854d0000c3500000c3110000000000000018e0190f3a2a2e0cebabac99e3d67147da573f39fcab1746d2a05d2e22a060af92efe9600000000000854d0000c3500000c3110000000000000018f01919375113bf780682741b62602e90cb9b3a50132ef3443924c0b93b7f3c0ea80a600000000000854d0000c3500000c311000000000000001900192b5085704b4b6b823059acda09858529e4e9fb542b52c668b392bc2900378b07900000000000854d0000c3500000c311000000000000001910193da08774a6ff7e6968eddad0fe921d39051caaf3c2bc2aa047102baa574852e7e00000000000854d0000c3500000c311000000000000001920194e6006b57c070a434ba17e69fc30dfdd88fc3faf7c00465df22f22fc575051dea00000000000854d0000c3500000c311000000000000001930195eb004427e0c3795eeb74ae52d8eb908d6395d93e90d93f113a941d728c7a186600000000000854d0000c3500000c31100000000000000194019625cce4babf6d48d362e642b15f5234e346d0ad56e01eff852be1d4bad5da4d9f00000000000854d0000c3500000c3110000000000000019501977de1734b52094a5045f4d3a962b0811c40148ab37a8cb8949ada98b9bb2ef98e00000000000854d0000c3500000c311000000000000001960198f6bb35d733dafb8788f89a35fc734c6fae0c79c7ef79d9ab79a619331d5991be00000000000854d0000c3500000c311000000000000001970199f608ebfa1bda8ea7044d7614459001856762aea6fa2a8da8a84f63b35b784aa900000000000854d0000c3500000c31100000000000000198019a5efd4cd712e8d819797cbdd2e5eb14ab8479581992c5cb4fefb71acd21259c9800000000000854d0000c3500000c31100000000000000199019be469abb620208408b16c28a5b22a408e86745059c187e533738786c8321e62d600000000000854d0000c3500000c3110000000000000019a019c55b7d42391295649268e33972b9b05e3863b06e927b1b6821f6be5c22cd5b32300000000000854d0000c3500000c3110000000000000019b019da71224eb2c47f79b4b7897332b4edf21831b271dd31265ea82a56ee423fbc51d00000000000854d0000c3500000c3110000000000000019c019e625238678138161a8a222110d7fae260b0d53dd718d8f0bbebc6dbf9f7c603d000000000000854d0000c3500000c3110000000000000019d019f998478e02c1ccb514f538e60d2d42bead26565df3239926c7ab2ba9e2556375c00000000000854d0000c3500000c3110000000000000019e01a0cadb273ea32290f6b387130461ebd022c6b559d4485f2dded4dc866794b1952e00000000000854d0000c3500000c3110000000000000019f01a1dcc463d2f12315dd333110aec4216d3ad68442ea011ebbf65cc03d6fc4def28d00000000000854d0000c3500000c311000000000000001a001a226af9c7b0ad6986d2014ee1c344720f7e5ecb5912d4533e3db9cec5abf3766fc00000000000854d0000c3500000c311000000000000001a101a337b0243c9147d7cdb24ab677cb997793184b4c6cbe078a2339c7050553debf1000000000000854d0000c3500000c311000000000000001a201a40b65bf566728aa8f0bd3b5f8b03efad99fe4d86570c3792a009bbd23aed8851600000000000854d0000c3500000c311000000000000001a301a57c0f33fb634f5b1cfad7cc130da55119a2fc282e4975d40204af4084a9df275300000000000854d0000c3500000c311000000000000001a401a610d4665d3a1c11e4bd872b0b47d822fefc75e8010f2766febba4206548da19e400000000000854d0000c3500000c311000000000000001a501a7e5998fdf3f51aedac8c428ebbe458f8039f8da6d996381998888846d7d7eac9300000000000854d0000c3500000c311000000000000001a601a84aa1e7b220a2ccb3d28ae1fc383aa4319dcb8df1a554f98e360ccb1b630674b000000000000854d0000c3500000c311000000000000001a701a90296a7c4204e0380ac993850b4041042fcff74542ba713574070c2982e5fe06d00000000000854d0000c3500000c311000000000000001a801aa1bcebf698d2ebc669f1e2e3ebbdb2bc9745616b0dddf8503965e9ce1bfbfbd4300000000000854d0000c3500000c311000000000000001a901ab17ca3153cfcf2c91ab678671c0193785a38fd13243f23bacf712297c1c3161df00000000000854d0000c3500000c311000000000000001aa01ac42aa6f3b6b844e832587ca38676960f99da8a7bf1cd0861f9476e8148056dc7b00000000000854d0000c3500000c311000000000000001ab01ad1677bf1f8138392b8a896be86688d8e2a852c94abed179d515ea75cbb0f0114400000000000854d0000c3500000c311000000000000001ac01ae5d07f5c149c59191cf2e949908b485c269c7237df230bcc66bff9d221b912af100000000000854d0000c3500000c311000000000000001ad01af4af01101f2fadf75503b90fa5519fabc113be039f7cac24d6a425ff6f4d46c9100000000000854d0000c3500000c311000000000000001ae01b0b75ae35825e628b1d386a81c380eac87e5063ea2fb581058a44e67647606faec00000000000854d0000c3500000c311000000000000001af01b1b3ace4f6e0b4d2504edf42d9fa09490ca53c59401c5ac8eae407a0f0347dc2b400000000000854d0000c3500000c311000000000000001b001b2da0319bb42c74cb71cc24608d303045b7c66b21c9cad1bcefa23190ea9008afd00000000000854d0000c3500000c311000000000000001b101b3e61dacb664d45941c172a551ede9e667bcf095113c15bac9217e1069c4549d3a00000000000854d0000c3500000c311000000000000001b201b46fea6aac4b9e2c07142170421a6f4f81ba464c8ddc49887d9f0f7826314b772e00000000000854d0000c3500000c311000000000000001b301b51df4dfc3f008e8b2348c48574f1c8e555a987162ec8347d793d0ceda160c952800000000000854d0000c3500000c311000000000000001b401b6e793bdc7a9b90cfd1a09081912fe24f588947146e99c46f58754cb16f81f6cd900000000000854d0000c3500000c311000000000000001b501b7ce19f510bda9ca936f1ce98af2c35aed4c87e98c38927a095b9b94f741305f9400000000000854d0000c3500000c311000000000000001b601b8c7930f425336449c2c3208d0e0b74d37e1eb5c1c0311e9a77bf00bf103daa2df00000000000854d0000c3500000c311000000000000001b701b9c7d9c3fecba42d41c4ceb332133da641fb6cdc531897be0caabbc3261cac5c5900000000000854d0000c3500000c311000000000000001b801ba871ca2ac3fcb9449b1cd3903798aa08e0e751d505d9aadec2d2b912094dd05c500000000000854d0000c3500000c311000000000000001b901bbd1dac0d405505faf085050662a9b20fe2e3398fd01a9b011dda41a55c740485c00000000000854d0000c3500000c311000000000000001ba01bc8aca06e52f2ee6f62f413b23f3bac9b4da3e93a2fb788f34c085be0128a293e600000000000854d0000c3500000c311000000000000001bb01bd50562444dd139b2bc45aa2064e6f945e1b4ff89ceef30842195d7e3766b5a25400000000000854d0000c3500000c311000000000000001bc01bea3112af2adcf6c56ac46a4cbf93312e2b6b7ea1848185a85273a7b11251c217900000000000854d0000c3500000c311000000000000001bd01bffa802b543ca6922c0656320b0761b883b9c1d75228ac9a7e9626ec76a7c8482000000000000854d0000c3500000c311000000000000001be01c0b2fcc13e833dd5bdd5d3c8ecb1745f50d831619c895ba3dcd24597bc52e0f67500000000000854d0000c3500000c311000000000000001bf01c11ddb4c5c55ed2746c69088f47da9cf8450cae2cba98e8a997381709e234c811500000000000854d0000c3500000c311000000000000001c001c2f88327f3873e70e5742dba0a8a41ab4555ef75ccbd49812534d625a38086465c00000000000854d0000c3500000c311000000000000001c101c3877f5cb86d90f3acfe194d244ff85604036251e73be7453d043470e8e87d1df100000000000854d0000c3500000c311000000000000001c201c41b6468631457a999e13d8556d28875f80d475e4080a822606e53ab4521e612fa00000000000854d0000c3500000c311000000000000001c301c5e27506829355cd7edd93156f5d3d78038a3cfa19d1b94502097914558d7a99d300000000000854d0000c3500000c311000000000000001c401c6e922eb6004f428a319dce0b00be94d9d65e8a891e0da8092cd17eb57f42719d700000000000854d0000c3500000c311000000000000001c501c72cf19e8b9cdbbce8788a37b61803d8c5ac9b705b451a16cf3fd4e44c98a6978400000000000854d0000c3500000c311000000000000001c601c86d4c389a327e75e07e26dde46d96ec5f9cbe652798c6e4c4f071e518ca431ae400000000000854d0000c3500000c311000000000000001c701c9ea874c9bfb2c727a572b1bb77a3911ddfe4890f0f4cc152b67e4490953fa741300000000000854d0000c3500000c311000000000000001c801ca3078da4bda050f5f723492d3f63a9f465a3f8352414b9b24747052268e8fd9f000000000000854d0000c3500000c311000000000000001c901cb0cbf24109afca71dc10ec95847ccd30693a340ac17a1c438de32c9d5ec779ce600000000000854d0000c3500000c311000000000000001ca01ccd1eebb68e4560bfc706060c2557d03e5fe55e5c76f1b0ef8e8b9102af101e1b600000000000854d0000c3500000c311000000000000001cb01cd60f5505127a1c513a7bf51a506bbd38413db23366690dde3b7a64ada8b38c3bf00000000000854d0000c3500000c311000000000000001cc01ce5f2a77c993b3c8c2860b9b6f661657e2ed9c3fb8a7f07b9f86e6908ff61e504000000000000854d0000c3500000c311000000000000001cd01cfbeeced096c0d29cb3117bd673c5a13fcc06788b4db4d7d24a2bb143cd675772200000000000854d0000c3500000c311000000000000001ce01d093861f3b9c8f22a3fc7f39be4019cbee78624119caa0ac94b509b691e516835c00000000000854d0000c3500000c311000000000000001cf01d13083a24fa948cd62be1d683b9cacaba9117c6a95ba3af20f8df2bedefbe3456300000000000854d0000c3500000c311000000000000001d001d2d12e24a4fc1c9116fc4eeead881d06ea1a7ecfbe5b50a60da676d03906784c3500000000000854d0000c3500000c311000000000000001d101d35c572a6639f35bec2e4ac4d0afad8f0b1a5b56f379ba18bb0f5a2239bed5684500000000000854d0000c3500000c311000000000000001d201d4fe0dd02a29705d5066285c49d0f62b8b7703247af1ca3f3ef80c72d2e73d4b0900000000000854d0000c3500000c311000000000000001d301d5270a3d63c0ee9b56b60d8422820e7d421d7cb5edd90bd36f171f3a248ca8ba2200000000000854d0000c3500000c311000000000000001d401d6a0ad7dd4c1094a8d2cad3b5f58c2f6521a82fcda6145f50a67031c99fac5ee3300000000000854d0000c3500000c311000000000000001d501d7bbc593f697857610ae607fcc8aaadfbfa9ccc8b666c1610fe58638c4b81b39d600000000000854d0000c3500000c311000000000000001d601d82ef8ef79e3f3125048e96e178148b987ad8ce75ddb86f226c9422f88311a1e5400000000000854d0000c3500000c311000000000000001d701d9584d870aff1afb86ba69ddc59fe8f846915e32928ec2b41ecfdcec2ddf6e034800000000000854d0000c3500000c311000000000000001d801da3f220ce62505abf5bd046d810d0a73121491fc5845984c6fc49d95fff620acf000000000000854d0000c3500000c311000000000000001d901db239fd782432147be7b01479d1bbf685ad71220496c5fbe49751f43661ad97ffa00000000000854d0000c3500000c311000000000000001da01dc452dbbf443cb66f47d94219064c1e11d02a35eee965b7f853b387676a12fc1bb00000000000854d0000c3500000c311000000000000001db01ddc92ca5e3fea2af39b5290131d48ae5ac6b8b3da7f8939f3416427aa7bf23c49500000000000854d0000c3500000c311000000000000001dc01de532077ccb94f30ba0e14ae25792e182fd3a9c89a136112a7875a582877d067c900000000000854d0000c3500000c311000000000000001dd01df994fd79bcbb666c872f3cf82331b4890fd26895337b42a7f957e3da716d9dede00000000000854d0000c3500000c311000000000000001de01e0b9563d83c0644780a2f7dc8441b391e23ad1a9d86a23b965931c13d66e41314e00000000000854d0000c3500000c311000000000000001df01e1a356c15452b8070d12716b40768018b3315c33b00a40c54fc825a77e16189b5800000000000854d0000c3500000c311000000000000001e001e250d2f5e73eeac5e1263a0df8a53ba2b1deb5b8e7815c61bd8d70015ed1912fa200000000000854d0000c3500000c311000000000000001e101e37498c3f78d908282db2e0ad1a117dad769c93569014fa2500d266959dc58f9d700000000000854d0000c3500000c311000000000000001e2cc184a69d3fc2fd2deb40ec4a13f19624540d06668f22572fae599d3ab8b9e48617e8bc904444217d8ec5b54a16df0bc55d63d76ed2cafe4b172e8c7e92773e6", + "acknowledgement": "d711050505050505050505050505050505050102bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2000c3590", + "refusal": "d7110505050505050505050505050505050500000e63616e6e6f742072657365727665" + }, + { + "scenario": "D.6", + "unsigned": "01011010101010101010101010101010101010101010101010101010101010101010eb4e7e4a56d8612ce7dddd7ed5b8b31d22a9c394560909d5f0a8a7cfc6fb5a718f19b491169952d5c5852cb9c722ec7f9e52b4bf1627f2c80377f0e7bdbe9287000c0df065129deb07cd484e12e9badcbc11dace2e724e9a1ccba6b40ec92d57dbaaf028035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c359000005e75ac6c4567bb853d69517b2fe7660700e83262c129fb80c5f3e7b8705d2b2f9304010001000164c356aaa2f7789a1ddf82024f18b37dfc22b4fb3ec7583ab7b09f8a9ad1312a00000000000f4240000c3500000c31100000000000000000", + "digest": "6f7c03a79caa9a15c0bd7f2a2f1c31a403f3a2c97b6881f71ecc90d4ab490d6a", + "manifest": "01011010101010101010101010101010101010101010101010101010101010101010eb4e7e4a56d8612ce7dddd7ed5b8b31d22a9c394560909d5f0a8a7cfc6fb5a718f19b491169952d5c5852cb9c722ec7f9e52b4bf1627f2c80377f0e7bdbe9287000c0df065129deb07cd484e12e9badcbc11dace2e724e9a1ccba6b40ec92d57dbaaf028035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c359000005e75ac6c4567bb853d69517b2fe7660700e83262c129fb80c5f3e7b8705d2b2f9304010001000164c356aaa2f7789a1ddf82024f18b37dfc22b4fb3ec7583ab7b09f8a9ad1312a00000000000f4240000c3500000c31100000000000000000baa3672f24183845abcc54ee06eb829c3cd2c7d31062f06d8bb1bef82c9c01d4470b72cf0f566cc6cd54e25c22e2811fdd99fce44ad2578087b3875e6dd54fb1", + "provision": "d70f0606060606060606060606060606060601011010101010101010101010101010101010101010101010101010101010101010eb4e7e4a56d8612ce7dddd7ed5b8b31d22a9c394560909d5f0a8a7cfc6fb5a718f19b491169952d5c5852cb9c722ec7f9e52b4bf1627f2c80377f0e7bdbe9287000c0df065129deb07cd484e12e9badcbc11dace2e724e9a1ccba6b40ec92d57dbaaf028035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c359000005e75ac6c4567bb853d69517b2fe7660700e83262c129fb80c5f3e7b8705d2b2f9304010001000164c356aaa2f7789a1ddf82024f18b37dfc22b4fb3ec7583ab7b09f8a9ad1312a00000000000f4240000c3500000c31100000000000000000baa3672f24183845abcc54ee06eb829c3cd2c7d31062f06d8bb1bef82c9c01d4470b72cf0f566cc6cd54e25c22e2811fdd99fce44ad2578087b3875e6dd54fb1", + "acknowledgement": "d711060606060606060606060606060606060102bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2000c3590", + "refusal": "d7110606060606060606060606060606060600000e63616e6e6f742072657365727665" + } + ] +} diff --git a/lightning-ffor/tests/data/generate_beignet_witness.cjs b/lightning-ffor/tests/data/generate_beignet_witness.cjs new file mode 100644 index 00000000000..3461895d1a6 --- /dev/null +++ b/lightning-ffor/tests/data/generate_beignet_witness.cjs @@ -0,0 +1,71 @@ +// Deterministic Appendix F provisioning fixtures using the pinned Beignet codecs. +// Usage: TS_NODE_PROJECT=/path/to/beignet/tsconfig.json node \ +// -r /path/to/beignet/node_modules/ts-node/register generate_beignet_witness.cjs \ +// /path/to/beignet /path/to/appendix-d.json /path/to/output.json +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const { execFileSync } = require('node:child_process'); + +const reference = path.resolve(process.argv[2]); +const revision = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: reference, encoding: 'utf8' }).trim(); +if (revision !== '8aee31d18e596fe49a0d195b325a6e757d7a009b') { + throw new Error('Beignet revision differs from the fixture baseline'); +} +const codecs = require(path.join(reference, 'src/lightning/ffor/witness-messages.ts')); +const { decodeVoucherBook } = require(path.join(reference, 'src/lightning/ffor/transcript.ts')); +const { getPublicKey } = require(path.join(reference, 'src/lightning/crypto/ecdh.ts')); +const source = fs.readFileSync(process.argv[3]); +const inputs = JSON.parse(source.toString()); +// Public deterministic test material only. No wallet or channel signing keys are used. +const fetchSecret = Buffer.alloc(32, 42); +const fetchPublic = getPublicKey(fetchSecret); +const encryptionPublic = getPublicKey(Buffer.alloc(32, 44)); +const witness = getPublicKey(Buffer.alloc(32, 43)); +function wire(type, body) { + const prefix = Buffer.alloc(2); + prefix.writeUInt16BE(type); + return Buffer.concat([prefix, body]).toString('hex'); +} +const fixtures = inputs.map((input, i) => { + const book = Buffer.from(input.book, 'hex'); + const entries = decodeVoucherBook(book).entries; + const retentionUntil = entries[0].voucherExpiry + 144; + const requestId = Buffer.alloc(16, i + 1); + const fields = { + version: 1, profile: 1, mailboxId: Buffer.alloc(32, i + 11), + tSetup: Buffer.from(input.setup_hash, 'hex'), + hCommit: Buffer.from(input.commitment_hash, 'hex'), + epochStartHeight: 790000, hAct: Buffer.from(input.activation_hash, 'hex'), + fetchPubkey: fetchPublic, encPubkey: encryptionPublic, + retentionUntil, minReceipts: 0, book + }; + const unsigned = codecs.encodeManifestUnsigned(fields); + const manifest = codecs.signManifest(fields, fetchSecret); + if (!codecs.verifyManifest(manifest, codecs.decodeManifest(manifest))) { + throw new Error('Generated manifest did not authenticate'); + } + return { + scenario: input.scenario, + unsigned: unsigned.toString('hex'), + digest: codecs.manifestDigest(unsigned).toString('hex'), + manifest: manifest.toString('hex'), + provision: wire(55055, codecs.encodeWitnessProvision(requestId, manifest)), + acknowledgement: wire(55057, codecs.encodeWitnessAck({ + requestId, ok: true, witnessNodeId: witness, retentionUntil + })), + refusal: wire(55057, codecs.encodeWitnessAck({ + requestId, ok: false, error: 'cannot reserve' + })) + }; +}); +fs.writeFileSync(process.argv[4], JSON.stringify({ + source_revision: revision, + specification_revision: 'd719161f42d1eeb6bd6c3856d564222f03c2205e', + source_file: 'src/lightning/ffor/witness-messages.ts', + appendix_d_sha256: crypto.createHash('sha256').update(source).digest('hex'), + fetch_public_key: fetchPublic.toString('hex'), + encryption_public_key: encryptionPublic.toString('hex'), + witness_public_key: witness.toString('hex'), + fixtures +}, null, 2) + '\n'); diff --git a/lightning-ffor/tests/witness.rs b/lightning-ffor/tests/witness.rs new file mode 100644 index 00000000000..fa412b3d82a --- /dev/null +++ b/lightning-ffor/tests/witness.rs @@ -0,0 +1,411 @@ +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::{Message as SecpMessage, PublicKey, Secp256k1, SecretKey}; +use lightning_ffor::setup::AuthenticatedSetup; +use lightning_ffor::transcript; +use lightning_ffor::wire::{Accept, Header, Init, Message, Payload}; +use lightning_ffor::witness::{ + Acknowledgement, AcknowledgementResult, PendingProvision, Provision, SignedManifest, + UnsignedManifest, WitnessConnection, WitnessError, MAX_MESSAGE_LEN, RETENTION_MARGIN_BLOCKS, +}; +use proptest::prelude::*; +use serde::Deserialize; + +fn hex(value: &str) -> Vec { + (0..value.len()).step_by(2).map(|i| u8::from_str_radix(&value[i..i + 2], 16).unwrap()).collect() +} + +fn key(value: u8) -> PublicKey { + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[value; 32]).unwrap()) +} + +fn signature(digest: [u8; 32], signer: u8) -> [u8; 64] { + Secp256k1::new() + .sign_ecdsa( + &SecpMessage::from_digest(digest), + &SecretKey::from_slice(&[signer; 32]).unwrap(), + ) + .serialize_compact() +} + +#[derive(Deserialize)] +struct Appendix { + init_wire: String, + accept_wire: String, +} + +fn public_setup(index: usize) -> AuthenticatedSetup { + let fixtures: Vec = + serde_json::from_str(include_str!("data/appendix-d.json")).unwrap(); + let input = &fixtures[index]; + AuthenticatedSetup::new( + &Message::decode(&hex(&input.init_wire)).unwrap(), + &Message::decode(&hex(&input.accept_wire)).unwrap(), + PublicKey::from_slice(&hex( + "039fca7f8157aa768708894ffd92550fe970edd18526a5f936583ea3b54dab3228", + )) + .unwrap(), + PublicKey::from_slice(&hex( + "02087b7d1b4789170f6e374f0a0e58a1b7a899e34929795314ab6964e69609e9c0", + )) + .unwrap(), + ) + .unwrap() +} + +#[derive(Deserialize)] +struct Reference { + source_revision: String, + specification_revision: String, + source_file: String, + appendix_d_sha256: String, + fetch_public_key: String, + encryption_public_key: String, + witness_public_key: String, + fixtures: Vec, +} + +#[derive(Deserialize)] +struct Fixture { + scenario: String, + unsigned: String, + digest: String, + manifest: String, + provision: String, + acknowledgement: String, + refusal: String, +} + +fn reference() -> Reference { + serde_json::from_str(include_str!("data/beignet-witness.json")).unwrap() +} + +fn manifest() -> SignedManifest { + SignedManifest::decode(&hex(&reference().fixtures[0].manifest), &public_setup(0)).unwrap() +} + +fn synthetic_setup(slots: usize, expiry: u32) -> AuthenticatedSetup { + let mut init = Message { + header: Header { channel_id: [1; 32], epoch_id: [2; 32] }, + payload: Payload::Init(Init { + budget_msat: 1_000_000 * slots as u64, + min_payment_msat: 1_000_000, + settlement_deadline: 100, + voucher_expiry: expiry, + fee_base_msat: 0, + fee_proportional_millionths: 0, + amounts_msat: vec![1_000_000; slots], + witness_peers: None, + hash_chain: false, + }), + extensions: Vec::new(), + signature: [0; 64], + }; + init.signature = signature(init.signature_digest().unwrap(), 42); + let mut accept = Message { + header: init.header, + payload: Payload::Accept(Accept { + s_commitment_number: 3, + payment_hashes: (0..slots) + .map(|index| sha256::Hash::hash(&(index as u64).to_be_bytes()).to_byte_array()) + .collect(), + s_htlc_id_base: 0, + amounts_msat: vec![1_000_000; slots], + init_hash: transcript::init_hash(&init.encode().unwrap()), + }), + extensions: Vec::new(), + signature: [0; 64], + }; + accept.signature = signature(accept.signature_digest().unwrap(), 43); + AuthenticatedSetup::new(&init, &accept, key(42), key(43)).unwrap() +} + +#[test] +fn beignet_manifests_provisions_and_acknowledgements_preserve_exact_bytes() { + let reference = reference(); + assert_eq!(reference.source_revision, "8aee31d18e596fe49a0d195b325a6e757d7a009b"); + assert_eq!(reference.specification_revision, "d719161f42d1eeb6bd6c3856d564222f03c2205e"); + assert_eq!(reference.source_file, "src/lightning/ffor/witness-messages.ts"); + assert_eq!( + hex(&reference.appendix_d_sha256), + sha256::Hash::hash(include_bytes!("data/appendix-d.json")).to_byte_array() + ); + assert_eq!(hex(&reference.fetch_public_key), key(42).serialize()); + assert_eq!(hex(&reference.encryption_public_key), key(44).serialize()); + assert_eq!(hex(&reference.witness_public_key), key(43).serialize()); + assert_eq!(reference.fixtures.len(), 6); + for (index, fixture) in reference.fixtures.iter().enumerate() { + let setup = public_setup(index); + let manifest = SignedManifest::decode(&hex(&fixture.manifest), &setup).unwrap(); + assert_eq!(manifest.encode(), hex(&fixture.manifest), "{}", fixture.scenario); + assert_eq!(manifest.unsigned().unsigned_bytes(), hex(&fixture.unsigned)); + assert_eq!(manifest.unsigned().signing_digest().as_slice(), hex(&fixture.digest)); + assert_eq!(manifest.unsigned().canonical_book(), setup.canonical_book()); + assert_eq!(manifest.unsigned().setup_hash(), setup.setup_hash()); + let provision = Provision::decode(&hex(&fixture.provision), &setup).unwrap(); + assert_eq!(provision.encode(), hex(&fixture.provision)); + assert_eq!(provision.manifest(), &manifest); + for wire in [&fixture.acknowledgement, &fixture.refusal] { + assert_eq!(Acknowledgement::decode(&hex(wire)).unwrap().encode(), hex(wire)); + } + } +} + +#[test] +fn manifest_rejects_every_truncated_prefix_unknown_fields_and_bad_points() { + let fixture = &reference().fixtures[0]; + let setup = public_setup(0); + let wire = hex(&fixture.manifest); + for length in 0..wire.len() { + assert!(SignedManifest::decode(&wire[..length], &setup).is_err(), "prefix {length}"); + } + for (offset, value, expected) in [ + (0, 2, WitnessError::Version), + (1, 4, WitnessError::Profile), + (134, 0, WitnessError::PublicKey), + (167, 0, WitnessError::PublicKey), + ] { + let mut damaged = wire.clone(); + damaged[offset] = value; + assert_eq!(SignedManifest::decode(&damaged, &setup), Err(expected)); + } + let mut trailing = wire.clone(); + trailing.push(1); + assert_eq!(SignedManifest::decode(&trailing, &setup), Err(WitnessError::NonCanonical)); + let mut declared = wire.clone(); + declared[205..207].copy_from_slice(&u16::MAX.to_be_bytes()); + assert_eq!(SignedManifest::decode(&declared, &setup), Err(WitnessError::Truncated)); + assert_eq!( + SignedManifest::decode(&vec![0; MAX_MESSAGE_LEN], &setup), + Err(WitnessError::SizeLimit) + ); + let provision = hex(&fixture.provision); + for length in 0..provision.len() { + assert!(Provision::decode(&provision[..length], &setup).is_err()); + } + let mut wrong_type = provision; + wrong_type[1] ^= 1; + assert_eq!(Provision::decode(&wrong_type, &setup), Err(WitnessError::MessageType)); + assert_eq!( + Provision::decode(&vec![0; MAX_MESSAGE_LEN + 1], &setup), + Err(WitnessError::SizeLimit) + ); +} + +#[test] +fn manifest_binds_setup_book_activation_and_fetch_signature_domain() { + let setup = public_setup(0); + let valid = manifest(); + for (offset, expected) in [ + (34, WitnessError::Transcript), + (66, WitnessError::Transcript), + (102, WitnessError::Transcript), + (207, WitnessError::Book), + (243, WitnessError::Book), + ] { + let mut damaged = valid.encode(); + damaged[offset] ^= 1; + assert_eq!(SignedManifest::decode(&damaged, &setup), Err(expected)); + } + assert_eq!(SignedManifest::decode(&valid.encode(), &public_setup(1)), Err(WitnessError::Book)); + let unsigned = valid.unsigned().clone(); + assert!(unsigned.clone().authenticate(signature(unsigned.signing_digest(), 43)).is_err()); + let wrong_domain = transcript::message_digest(55055, &unsigned.unsigned_bytes()); + assert!(unsigned.clone().authenticate(signature(wrong_domain, 42)).is_err()); + let zero = [0; 64]; + assert!(unsigned.clone().authenticate(zero).is_err()); + let mut zero_r = *valid.signature(); + zero_r[..32].fill(0); + assert!(unsigned.clone().authenticate(zero_r).is_err()); + let mut zero_s = *valid.signature(); + zero_s[32..].fill(0); + assert!(unsigned.clone().authenticate(zero_s).is_err()); + let mut invalid = *valid.signature(); + invalid[..32].fill(255); + assert!(unsigned.clone().authenticate(invalid).is_err()); + let mut high = *valid.signature(); + let order = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + let mut borrow = 0_i16; + for index in (0..32).rev() { + let difference = order[index] as i16 - high[32 + index] as i16 - borrow; + high[32 + index] = difference as u8; + borrow = i16::from(difference < 0); + } + assert_eq!( + unsigned.authenticate(high), + Err(WitnessError::Signature(transcript::SignatureError::HighS)) + ); +} + +#[test] +fn retention_and_activation_height_boundaries_use_checked_arithmetic() { + let setup = public_setup(0); + let valid = manifest(); + let mut parameters = *valid.unsigned().parameters(); + assert_eq!(parameters.retention_until, setup.terms().voucher_expiry + RETENTION_MARGIN_BLOCKS); + parameters.retention_until -= 1; + assert_eq!(UnsignedManifest::new(&setup, parameters), Err(WitnessError::Retention)); + parameters.retention_until = u32::MAX; + assert!(UnsignedManifest::new(&setup, parameters).is_ok()); + parameters.epoch_start_height = setup.terms().settlement_deadline; + assert_eq!(UnsignedManifest::new(&setup, parameters), Err(WitnessError::Height)); + parameters.epoch_start_height -= 1; + assert!(UnsignedManifest::new(&setup, parameters).is_ok()); + parameters.epoch_start_height = 50; + assert_eq!( + UnsignedManifest::new(&synthetic_setup(1, u32::MAX), parameters), + Err(WitnessError::Height) + ); + let height_setup = synthetic_setup(1, 499_999_999); + let height_manifest = UnsignedManifest::new(&height_setup, parameters).unwrap(); + for expiry in [500_000_000, u32::MAX] { + let timestamp_setup = synthetic_setup(1, expiry); + assert_eq!(UnsignedManifest::new(×tamp_setup, parameters), Err(WitnessError::Height)); + // A previously signed manifest with matching timestamp-style setup must also fail restore. + let mut encoded = height_manifest.unsigned_bytes(); + encoded[34..66].copy_from_slice(×tamp_setup.setup_hash()); + encoded[102..134].copy_from_slice(&transcript::activation_hash( + ×tamp_setup.setup_hash(), + ×tamp_setup.book_hash(), + ¶meters.commitment_hash, + parameters.epoch_start_height, + )); + encoded[207..].copy_from_slice(timestamp_setup.canonical_book()); + let mut digest_input = b"ffor/witness/manifest".to_vec(); + digest_input.extend_from_slice(&encoded); + encoded + .extend_from_slice(&signature(sha256::Hash::hash(&digest_input).to_byte_array(), 42)); + assert_eq!(SignedManifest::decode(&encoded, ×tamp_setup), Err(WitnessError::Height)); + } + let mut bytes = valid.encode(); + bytes[200..204].copy_from_slice(&(setup.terms().voucher_expiry + 143).to_be_bytes()); + assert_eq!(SignedManifest::decode(&bytes, &setup), Err(WitnessError::Retention)); +} + +#[test] +fn maximum_book_and_receipt_count_remain_bounded_and_roundtrip() { + let setup = synthetic_setup(483, 1108); + let mut parameters = *manifest().unsigned().parameters(); + parameters.epoch_start_height = 50; + parameters.retention_until = 1252; + parameters.minimum_receipts = 255; + let unsigned = UnsignedManifest::new(&setup, parameters).unwrap(); + let signed = unsigned.clone().authenticate(signature(unsigned.signing_digest(), 42)).unwrap(); + let provision = Provision::new([5; 16], signed); + assert!(provision.encode().len() < MAX_MESSAGE_LEN); + assert_eq!(Provision::decode(&provision.encode(), &setup).unwrap(), provision); +} + +#[test] +fn acknowledgement_codec_rejects_invalid_flags_points_lengths_and_extensions() { + for encoded in [&reference().fixtures[0].acknowledgement, &reference().fixtures[0].refusal] { + let bytes = hex(encoded); + for length in 0..bytes.len() { + assert!(Acknowledgement::decode(&bytes[..length]).is_err()); + } + let mut trailing = bytes.clone(); + trailing.push(0); + assert_eq!(Acknowledgement::decode(&trailing), Err(WitnessError::NonCanonical)); + let mut flag = bytes.clone(); + flag[18] = 2; + assert_eq!(Acknowledgement::decode(&flag), Err(WitnessError::NonCanonical)); + let mut kind = bytes; + kind[0] = 0; + assert_eq!(Acknowledgement::decode(&kind), Err(WitnessError::MessageType)); + } + let mut bad_key = hex(&reference().fixtures[0].acknowledgement); + bad_key[19] = 0; + assert_eq!(Acknowledgement::decode(&bad_key), Err(WitnessError::PublicKey)); + let limit = MAX_MESSAGE_LEN - 21; + let max = + Acknowledgement::new([0; 16], AcknowledgementResult::Refused(vec![255; limit])).unwrap(); + assert_eq!(max.encode().len(), MAX_MESSAGE_LEN); + assert_eq!(Acknowledgement::decode(&max.encode()).unwrap(), max); + assert_eq!( + Acknowledgement::new([0; 16], AcknowledgementResult::Refused(vec![0; limit + 1])), + Err(WitnessError::SizeLimit) + ); + assert_eq!( + Acknowledgement::decode(&vec![0; MAX_MESSAGE_LEN + 1]), + Err(WitnessError::SizeLimit) + ); +} + +#[test] +fn checked_acknowledgement_requires_exact_request_witness_connection_and_promise() { + let fixture = &reference().fixtures[0]; + let provision = Provision::decode(&hex(&fixture.provision), &public_setup(0)).unwrap(); + let source = WitnessConnection { node_id: key(43), identity: 7_u64 }; + let pending = PendingProvision::new(provision.clone(), source.clone()); + let ack = Acknowledgement::decode(&hex(&fixture.acknowledgement)).unwrap(); + let checked = pending.check_acknowledgement(&ack, &source).unwrap(); + assert_eq!(checked.provision(), &provision); + assert_eq!(checked.connection(), pending.connection()); + assert_eq!( + checked.retention_until(), + provision.manifest().unsigned().parameters().retention_until + ); + let wrong_id = Acknowledgement::new([99; 16], ack.result().clone()).unwrap(); + assert_eq!(pending.check_acknowledgement(&wrong_id, &source), Err(WitnessError::Request)); + let mut reconnect = source.clone(); + reconnect.identity += 1; + assert_eq!(pending.check_acknowledgement(&ack, &reconnect), Err(WitnessError::Connection)); + let mut other_peer = source.clone(); + other_peer.node_id = key(44); + assert_eq!(pending.check_acknowledgement(&ack, &other_peer), Err(WitnessError::Witness)); + for (witness, retention_until, expected) in [ + (key(44), checked.retention_until(), WitnessError::Witness), + (key(43), checked.retention_until() - 1, WitnessError::Retention), + ] { + let changed = Acknowledgement::new( + ack.request_id(), + AcknowledgementResult::Accepted { witness, retention_until }, + ) + .unwrap(); + assert_eq!(pending.check_acknowledgement(&changed, &source), Err(expected)); + } + let refused = Acknowledgement::decode(&hex(&fixture.refusal)).unwrap(); + assert_eq!(pending.check_acknowledgement(&refused, &source), Err(WitnessError::Refused)); + // All failures leave the exact pending request available for an authenticated retry. + assert_eq!(pending.check_acknowledgement(&ack, &source).unwrap(), checked); + let another = + Provision::decode(&hex(&reference().fixtures[1].provision), &public_setup(1)).unwrap(); + assert_ne!(checked.provision().manifest(), another.manifest()); + // Sufficient retention means the exact requested promise, including any extra local margin. + let mut parameters = *provision.manifest().unsigned().parameters(); + parameters.retention_until += 100; + parameters.mailbox_id[0] ^= 1; + let unsigned = UnsignedManifest::new(&public_setup(0), parameters).unwrap(); + let signed = unsigned.clone().authenticate(signature(unsigned.signing_digest(), 42)).unwrap(); + let another_request = Provision::new([88; 16], signed); + let stronger = PendingProvision::new(another_request.clone(), source.clone()); + let too_short = + Acknowledgement::new(another_request.request_id(), ack.result().clone()).unwrap(); + assert_eq!(stronger.check_acknowledgement(&too_short, &source), Err(WitnessError::Retention)); + let sufficient = Acknowledgement::new( + another_request.request_id(), + AcknowledgementResult::Accepted { + witness: source.node_id, + retention_until: parameters.retention_until, + }, + ) + .unwrap(); + let second_checked = stronger.check_acknowledgement(&sufficient, &source).unwrap(); + assert_eq!(second_checked.provision(), &another_request); + assert_ne!(second_checked.provision().manifest(), checked.provision().manifest()); +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(64))] + #[test] + fn arbitrary_bounded_inputs_decode_canonically(bytes in prop::collection::vec(any::(), 0..=MAX_MESSAGE_LEN + 1)) { + let setup = public_setup(0); + if let Ok(value) = SignedManifest::decode(&bytes, &setup) { prop_assert_eq!(&value.encode(), &bytes); } + if let Ok(value) = Provision::decode(&bytes, &setup) { prop_assert_eq!(&value.encode(), &bytes); } + if let Ok(value) = Acknowledgement::decode(&bytes) { prop_assert_eq!(&value.encode(), &bytes); } + } + #[test] + fn arbitrary_refusal_bytes_roundtrip(bytes in prop::collection::vec(any::(), 0..2048), request_id in any::<[u8; 16]>()) { + let ack = Acknowledgement::new(request_id, AcknowledgementResult::Refused(bytes)).unwrap(); + prop_assert_eq!(Acknowledgement::decode(&ack.encode()).unwrap(), ack); + } +} From 11e6ffe06cf015d24c33911c421fe62ff322b63d Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 21:59:17 -0400 Subject: [PATCH 07/22] feat(ffor): retain activation evidence and fence channel mutations --- FFOR.md | 59 +- lightning/src/chain/channelmonitor.rs | 17 + lightning/src/ln/channel.rs | 148 ++++- lightning/src/ln/channel/ffor.rs | 16 +- lightning/src/ln/channel/ffor/fence.rs | 203 ++++++ lightning/src/ln/channel/ffor/fence/tests.rs | 603 ++++++++++++++++++ lightning/src/ln/channel/ffor/setup.rs | 1 + lightning/src/ln/channelmanager.rs | 19 +- .../ln/channelmanager/ffor_recovery_tests.rs | 225 ++++++- lightning/src/ln/ffor.rs | 15 + lightning/src/ln/ffor_recovery.rs | 194 +++++- lightning/src/ln/ffor_recovery/activation.rs | 182 ++++++ .../src/ln/ffor_recovery/activation/tests.rs | 497 +++++++++++++++ lightning/src/ln/ffor_recovery/tests.rs | 18 +- lightning/src/ln/ffor_tests.rs | 2 +- lightning/src/ln/ffor_tests/quiescence.rs | 10 +- 16 files changed, 2145 insertions(+), 64 deletions(-) create mode 100644 lightning/src/ln/channel/ffor/fence.rs create mode 100644 lightning/src/ln/channel/ffor/fence/tests.rs create mode 100644 lightning/src/ln/ffor_recovery/activation.rs create mode 100644 lightning/src/ln/ffor_recovery/activation/tests.rs diff --git a/FFOR.md b/FFOR.md index b72264b9cb3..906240bece6 100644 --- a/FFOR.md +++ b/FFOR.md @@ -1,4 +1,4 @@ -# Experimental FFOR receiver setup and parking +# Experimental FFOR receiver setup, parking and activation guards This fork targets rust-lightning v0.2.5. The current APIs provide receiver voucher parking and point-in-time verification of both commitment views for FFOR Variant D. @@ -79,11 +79,35 @@ Authenticated setup also enters a manager-owned recovery registry in required TL explicit closure, monitor-triggered closure and disposal of a stale manager channel. Restore reauthenticates records, checks the manager's identity and chain, and requires every retained channel setup to match its archive entry. The registry is bounded to -64 records, 8 MiB total and 192 KiB per record; capacity is reserved before channel -mutation. The setup-only archive version retains no activation or claim authority. +64 records and 8 MiB total, with a 192 KiB setup-only record limit and a 384 KiB +activation record limit. Capacity is reserved before channel mutation. An Activating +record also reserves the maximum acknowledgement size, including framing, so other +records cannot consume its eventual Active storage. This allowance is derived again +on restore and released when the acknowledgement is retained. The setup-only archive +version retains no activation or claim authority. Fully drained channel tombstones permit subsequent ordinary splicing while retaining the original funding context in their archive. +Private activation records now retain the exact signed activate and acknowledgement +messages, both commitment numbers and transaction IDs, monitor update identity, +preparation height and the monitor's original sweep destination. They reauthenticate +the historical transcript and permit only the first acknowledgement to be added. +A versioned registry prevents setup-only readers from accepting activation evidence. +Every retained channel fence must match its archive phase and activation hash, even +when the live channel will be discarded as stale. Restore also requires the original +monitor and checks both commitment identities, its destination and the saved update +floor. A preimage or force-close update may advance that floor without changing the +frozen commitments. The complete monitor remains necessary for signatures and claims. + +The private channel fence persists Activating or Active independently of stock STFU +flags. It blocks ordinary HTLC updates, commitment and revocation messages, fees, +splicing, cooperative close, signer retries and ordinary reconnect messages. Valid +owned preimages still enter stock monitor persistence, including delayed writes and +completion after channel removal. They cannot release an ordinary fulfill while the +fence is held. Force-close remains available. A required even field makes older +channel readers refuse fenced state. Installation currently exists only in tests; +there is no production transition into these phases or recovery driver to leave them. + These are consistency checks, not an authenticated storage envelope. Arbitrarily deleting a mismatching add's ownership record after abort can make its nonreserved hash indistinguishable from an ordinary post-abort payment. No valid writer creates @@ -92,8 +116,8 @@ checks do not claim to detect every arbitrary alteration of local storage. Similarly, an archive without its original live channel cannot independently prove the historical funding context against arbitrary local storage alteration. -No feature bit, wire activation, invoice readiness, commitment freeze, or automatic -setup deadline is implemented here. The caller must abort a setup that times out. +No feature bit, production wire activation, invoice readiness, authenticated +reconnect driver or automatic setup deadline is implemented here. The caller must abort a setup that times out. Ordinary channel updates can invalidate a previously returned `Parked` proof. `NodeSigner::sign_ffor_message` supplies the protocol's single-SHA256 `ffor/msg` @@ -139,13 +163,24 @@ partial rounds, delayed monitors, changed commitments, deadline equality, initia tie loss, pending abort, disconnect, restart and force-close. The ordinary quiescence and splice suites also pass with these channel changes. +The private fence tests cover both phases and funding directions, restored pending +state rejection, preimage retention, cleared STFU flags, signer and monitor callbacks, +reconnect refusal and force-close. Activation recovery tests use real committed +channels and signed transcripts, exercise retained evidence after closure, and reject +missing monitors, mismatched commitment identities, downgrade attempts and substituted +transcripts. Capacity tests reload a registry with competing admissions and retain a +maximum signed acknowledgement from its reserved allowance. + ## Next boundary Reusable epochs require durable retired epoch IDs and voucher hashes, with one -current signed transcript record under the same channel authority. Activation must -atomically verify commitment evidence, retain exact signed activation bytes beside -the existing setup archive, and freeze ordinary mutations. Acknowledgement release -and invoice eligibility must use the persistence-completion barrier and recheck the -channel phase. The archive must be extended with active commitment and claim evidence -for on-chain resolution, including witness and mailbox recovery. None of these later -guarantees can be inferred from setup retention or parking. +current signed transcript record under the same channel authority. A production +activation owner must derive and sign the exact transcript from owned quiescence, +atomically install its archive and fence, and release wire only after the manager +persistence barrier completes. Acknowledgement handling must authenticate the peer, +retain Active evidence and recheck the channel phase before reporting durability. +Reconnect needs the Variant D state comparison and acknowledgement-loss recovery; +ordinary channel traffic must remain frozen throughout. Witness/mailbox recovery, +preimage reconciliation, controlled voucher drain and invoice eligibility remain +separate required boundaries. None can be inferred from a retained setup or the +private activation fixtures. diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 022bc3a2495..12718b52525 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -2135,6 +2135,7 @@ impl ChannelMonitor { channel_id: inner.channel_id(), funding_txo: inner.get_funding_txo(), update_id: inner.latest_update_id, + destination_script: inner.destination_script.clone(), holder: inner.funding.current_holder_commitment_tx.clone(), holder_number: inner.current_holder_commitment_number, counterparty_txid, @@ -2144,6 +2145,22 @@ impl ChannelMonitor { }) } + /// The frozen archive must match the actual monitor, even when force-close prevents obtaining + /// a live activation snapshot. Preimage and close updates may advance only the monitor update ID. + pub(crate) fn ffor_recovery_identity(&self) -> crate::ln::ffor::FFORMonitorRecoveryIdentity { + let inner = self.inner.lock().unwrap(); + crate::ln::ffor::FFORMonitorRecoveryIdentity { + channel_id: inner.channel_id(), + funding_txo: inner.get_funding_txo(), + update_id: inner.latest_update_id, + holder_number: inner.current_holder_commitment_number, + holder_txid: inner.funding.current_holder_commitment_tx.trust().txid(), + counterparty_number: inner.current_counterparty_commitment_number, + counterparty_txid: inner.funding.current_counterparty_commitment_txid, + destination_script: inner.destination_script.clone(), + } + } + /// Gets the funding transaction outpoint of the channel this ChannelMonitor is monitoring for. pub fn get_funding_txo(&self) -> OutPoint { self.inner.lock().unwrap().get_funding_txo() diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 4d1259afa77..55ca9b9ad37 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -102,7 +102,7 @@ use super::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBase mod ffor; #[cfg(test)] pub(crate) use ffor::ffor_setup_test_messages; -pub(crate) use ffor::FFORReceiverSetup; +pub(crate) use ffor::{FFORReceiverFencePhase, FFORReceiverSetup}; #[cfg(any(test, feature = "_test_utils"))] #[allow(unused)] @@ -1171,6 +1171,7 @@ pub(super) struct ReestablishResponses { /// The first message we send to our peer after connection pub(super) enum ReconnectionMsg { + FFORFrozen(msgs::WarningMessage), Reestablish(msgs::ChannelReestablish), Open(OpenChannelMessage), None, @@ -1683,8 +1684,10 @@ where ) -> ReconnectionMsg where L::Target: Logger { match &mut self.phase { ChannelPhase::Undefined => unreachable!(), - ChannelPhase::Funded(chan) => - ReconnectionMsg::Reestablish(chan.get_channel_reestablish(logger)), + ChannelPhase::Funded(chan) => match chan.get_channel_reestablish(logger) { + Ok(message) => ReconnectionMsg::Reestablish(message), + Err(warning) => ReconnectionMsg::FFORFrozen(warning), + }, ChannelPhase::UnfundedOutboundV1(chan) => { chan.get_open_channel(chain_hash, logger) .map(|msg| ReconnectionMsg::Open(OpenChannelMessage::V1(msg))) @@ -1789,6 +1792,7 @@ where where L::Target: Logger, { + self.context().check_ffor_mutation().map_err(|error| (error, None))?; match self.interactive_tx_constructor_mut() { Some(interactive_tx_constructor) => interactive_tx_constructor .handle_tx_add_input(msg) @@ -1808,6 +1812,7 @@ where where L::Target: Logger, { + self.context().check_ffor_mutation().map_err(|error| (error, None))?; match self.interactive_tx_constructor_mut() { Some(interactive_tx_constructor) => interactive_tx_constructor .handle_tx_add_output(msg) @@ -1827,6 +1832,7 @@ where where L::Target: Logger, { + self.context().check_ffor_mutation().map_err(|error| (error, None))?; match self.interactive_tx_constructor_mut() { Some(interactive_tx_constructor) => interactive_tx_constructor .handle_tx_remove_input(msg) @@ -1846,6 +1852,7 @@ where where L::Target: Logger, { + self.context().check_ffor_mutation().map_err(|error| (error, None))?; match self.interactive_tx_constructor_mut() { Some(interactive_tx_constructor) => interactive_tx_constructor .handle_tx_remove_output(msg) @@ -1868,6 +1875,7 @@ where where L::Target: Logger, { + self.context().check_ffor_mutation().map_err(|error| (error, None))?; let tx_complete_action = match self.interactive_tx_constructor_mut() { Some(interactive_tx_constructor) => interactive_tx_constructor .handle_tx_complete(msg) @@ -1911,6 +1919,7 @@ where where L::Target: Logger, { + self.context().check_ffor_mutation()?; // If we have not sent a `tx_abort` message for this negotiation previously, we need to echo // back a tx_abort message according to the spec: // https://github.com/lightning/bolts/blob/247e83d/02-peer-protocol.md?plain=1#L560-L561 @@ -7331,6 +7340,11 @@ where where L::Target: Logger, { + if self.context.is_ffor_frozen() + && !self.context.ffor_owns_preimage(htlc_id_arg, &payment_preimage_arg) + { + return UpdateFulfillFetch::DuplicateClaim {}; + } // Either ChannelReady got set (which means it won't be unset) or there is no way any // caller thought we could have something claimed (cause we wouldn't have accepted in an // incoming HTLC anyway). If we got to ShutdownComplete, callers aren't allowed to call us, @@ -7398,7 +7412,9 @@ where channel_id: Some(self.context.channel_id()), }; - if !self.context.channel_state.can_generate_new_commitment() { + if self.context.is_ffor_frozen() + || !self.context.channel_state.can_generate_new_commitment() + { // Note that this condition is the same as the assertion in // `claim_htlc_while_disconnected_dropping_mon_update` and must match exactly - // `claim_htlc_while_disconnected_dropping_mon_update` would not work correctly if we @@ -7506,7 +7522,9 @@ where // already queued, we have to insert it into the pending queue and update the // update_ids of all the following monitors. if release_cs_monitor && !update_blocked { - let mut additional_update = self.build_commitment_no_status_check(logger); + let mut additional_update = self + .build_commitment_no_status_check(logger) + .expect("Commitment mutation was checked before entering this path"); // build_commitment_no_status_check may bump latest_monitor_id but we want them // to be strictly increasing by one, so decrement it here. self.context.latest_monitor_update_id = monitor_update.update_id; @@ -7522,7 +7540,9 @@ where } if !update_blocked { debug_assert!(false, "If there is a pending blocked monitor we should have MonitorUpdateInProgress set"); - let update = self.build_commitment_no_status_check(logger); + let update = self + .build_commitment_no_status_check(logger) + .expect("Commitment mutation was checked before entering this path"); self.context .blocked_monitor_updates .push(PendingChannelMonitorUpdate { update }); @@ -7576,6 +7596,7 @@ where &mut self, htlc_id_arg: u64, err_contents: E, mut force_holding_cell: bool, logger: &L ) -> Result, ChannelError> where L::Target: Logger { + if self.context.is_ffor_frozen() { return Err(ChannelError::Ignore(ffor::FFOR_FROZEN_MESSAGE.to_owned())); } if !matches!(self.context.channel_state, ChannelState::ChannelReady(_)) { panic!("Was asked to fail an HTLC when channel was not in an operational state"); } @@ -7690,6 +7711,7 @@ where NS::Target: NodeSigner, L::Target: Logger { + self.context.check_ffor_mutation()?; if self.context.channel_state.is_peer_disconnected() { self.context.workaround_lnd_bug_4006 = Some(msg.clone()); return Err(ChannelError::Ignore("Peer sent channel_ready when we needed a channel_reestablish. The peer is likely lnd, see https://github.com/lightningnetwork/lnd/issues/4006".to_owned())); @@ -7767,6 +7789,7 @@ where pub fn update_add_htlc( &mut self, msg: &msgs::UpdateAddHTLC, fee_estimator: &LowerBoundedFeeEstimator, ) -> Result<(), ChannelError> where F::Target: FeeEstimator { + self.context.check_ffor_mutation()?; if self.context.channel_state.is_remote_stfu_sent() || self.context.channel_state.is_quiescent() { return Err(ChannelError::WarnAndDisconnect("Got add HTLC message while quiescent".to_owned())); } @@ -7816,6 +7839,7 @@ where #[inline] #[rustfmt::skip] fn mark_outbound_htlc_removed(&mut self, htlc_id: u64, outcome: OutboundHTLCOutcome) -> Result<&OutboundHTLCOutput, ChannelError> { + self.context.check_ffor_mutation()?; for htlc in self.context.pending_outbound_htlcs.iter_mut() { if htlc.htlc_id == htlc_id { if let OutboundHTLCOutcome::Success(ref payment_preimage, ..) = outcome { @@ -7842,6 +7866,7 @@ where pub fn update_fulfill_htlc( &mut self, msg: &msgs::UpdateFulfillHTLC, ) -> Result<(HTLCSource, u64, Option, Option), ChannelError> { + self.context.check_ffor_mutation()?; if self.context.channel_state.is_remote_stfu_sent() || self.context.channel_state.is_quiescent() { @@ -7869,6 +7894,7 @@ where #[rustfmt::skip] pub fn update_fail_htlc(&mut self, msg: &msgs::UpdateFailHTLC, fail_reason: HTLCFailReason) -> Result<(), ChannelError> { + self.context.check_ffor_mutation()?; if self.context.channel_state.is_remote_stfu_sent() || self.context.channel_state.is_quiescent() { return Err(ChannelError::WarnAndDisconnect("Got fail HTLC message while quiescent".to_owned())); } @@ -7885,6 +7911,7 @@ where #[rustfmt::skip] pub fn update_fail_malformed_htlc(&mut self, msg: &msgs::UpdateFailMalformedHTLC, fail_reason: HTLCFailReason) -> Result<(), ChannelError> { + self.context.check_ffor_mutation()?; if self.context.channel_state.is_remote_stfu_sent() || self.context.channel_state.is_quiescent() { return Err(ChannelError::WarnAndDisconnect("Got fail malformed HTLC message while quiescent".to_owned())); } @@ -7905,6 +7932,7 @@ where ) -> Result::EcdsaSigner>, ChannelError> where L::Target: Logger { + self.context.check_ffor_mutation()?; if let Some(signing_session) = self.context.interactive_tx_signing_session.as_ref() { if signing_session.has_received_tx_signatures() { let msg = "Received initial commitment_signed after peer's tx_signatures received!"; @@ -7951,6 +7979,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { + self.context.check_ffor_mutation()?; debug_assert!(self .context .interactive_tx_signing_session @@ -8058,6 +8087,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { + self.context.check_ffor_mutation()?; self.commitment_signed_check_state()?; if !self.pending_funding().is_empty() { @@ -8102,6 +8132,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { + self.context.check_ffor_mutation()?; self.commitment_signed_check_state()?; let mut messages = BTreeMap::new(); @@ -8171,6 +8202,7 @@ where } fn commitment_signed_check_state(&self) -> Result<(), ChannelError> { + self.context.check_ffor_mutation()?; if self.context.channel_state.is_quiescent() { return Err(ChannelError::WarnAndDisconnect( "Got commitment_signed message while quiescent".to_owned(), @@ -8205,6 +8237,7 @@ where where L::Target: Logger, { + self.context.check_ffor_mutation()?; if self .holder_commitment_point .advance(&self.context.holder_signer, &self.context.secp_ctx, logger) @@ -8302,7 +8335,7 @@ where // the corresponding HTLC status updates so that // get_last_commitment_update_for_send includes the right HTLCs. self.context.monitor_pending_commitment_signed = true; - let mut additional_update = self.build_commitment_no_status_check(logger); + let mut additional_update = self.build_commitment_no_status_check(logger)?; // build_commitment_no_status_check may bump latest_monitor_id but we want them to be // strictly increasing by one, so decrement it here. self.context.latest_monitor_update_id = monitor_update.update_id; @@ -8318,7 +8351,7 @@ where // If we're AwaitingRemoteRevoke we can't send a new commitment here, but that's ok - // we'll send one right away when we get the revoke_and_ack when we // free_holding_cell_htlcs(). - let mut additional_update = self.build_commitment_no_status_check(logger); + let mut additional_update = self.build_commitment_no_status_check(logger)?; // build_commitment_no_status_check may bump latest_monitor_id but we want them to be // strictly increasing by one, so decrement it here. self.context.latest_monitor_update_id = monitor_update.update_id; @@ -8351,6 +8384,9 @@ where F::Target: FeeEstimator, L::Target: Logger, { + if self.context.is_ffor_frozen() { + return (None, Vec::new()); + } if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) && self.context.channel_state.can_generate_new_commitment() { @@ -8369,6 +8405,9 @@ where F::Target: FeeEstimator, L::Target: Logger, { + if self.context.is_ffor_frozen() { + return (None, Vec::new()); + } assert!(matches!(self.context.channel_state, ChannelState::ChannelReady(_))); assert!(!self.context.channel_state.is_monitor_update_in_progress()); assert!(!self.context.channel_state.is_quiescent()); @@ -8528,7 +8567,9 @@ where return (None, htlcs_to_fail); } - let mut additional_update = self.build_commitment_no_status_check(logger); + let mut additional_update = self + .build_commitment_no_status_check(logger) + .expect("Commitment mutation was checked before entering this path"); // build_commitment_no_status_check and get_update_fulfill_htlc may bump latest_monitor_id // but we want them to be strictly increasing by one, so reset it here. self.context.latest_monitor_update_id = monitor_update.update_id; @@ -8573,6 +8614,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { + self.context.check_ffor_mutation()?; if self.context.channel_state.is_quiescent() { return Err(ChannelError::WarnAndDisconnect( "Got revoke_and_ack message while quiescent".to_owned(), @@ -8920,7 +8962,7 @@ where // get_last_commitment_update_for_send(), which does not update state, but we're // definitely now awaiting a remote revoke before we can step forward any more, // so set it here. - let mut additional_update = self.build_commitment_no_status_check(logger); + let mut additional_update = self.build_commitment_no_status_check(logger)?; // build_commitment_no_status_check may bump latest_monitor_id but we want them to be // strictly increasing by one, so decrement it here. @@ -9046,6 +9088,7 @@ where where L::Target: Logger, { + self.context.check_ffor_local_mutation()?; let signing_session = if let Some(signing_session) = self.context.interactive_tx_signing_session.as_mut() { if let Some(pending_splice) = self.pending_splice.as_ref() { @@ -9146,6 +9189,7 @@ where where L::Target: Logger, { + self.context.check_ffor_mutation()?; let signing_session = if let Some(signing_session) = self.context.interactive_tx_signing_session.as_mut() { @@ -9239,6 +9283,7 @@ where ) -> Option where F::Target: FeeEstimator, L::Target: Logger { + if self.context.is_ffor_frozen() { return None; } if !self.funding.is_outbound() { panic!("Cannot send fee from inbound channel"); } @@ -9412,6 +9457,14 @@ where assert!(self.context.channel_state.is_monitor_update_in_progress()); self.context.channel_state.clear_monitor_update_in_progress(); assert_eq!(self.blocked_monitor_updates_pending(), 0); + if self.context.is_ffor_frozen() { + return MonitorRestoreUpdates { + raa: None, commitment_update: None, commitment_order: self.context.resend_order.clone(), + accepted_htlcs: Vec::new(), failed_htlcs: Vec::new(), finalized_claimed_htlcs: Vec::new(), + pending_update_adds: Vec::new(), funding_broadcastable: None, channel_ready: None, + announcement_sigs: None, tx_signatures: None, channel_ready_order: ChannelReadyOrder::ChannelReadyFirst, + }; + } // If we're past (or at) the AwaitingChannelReady stage on an outbound (or V2-established) channel, // try to (re-)broadcast the funding transaction as we may have declined to broadcast it when we @@ -9538,6 +9591,7 @@ where pub fn update_fee(&mut self, fee_estimator: &LowerBoundedFeeEstimator, msg: &msgs::UpdateFee, logger: &L) -> Result<(), ChannelError> where F::Target: FeeEstimator, L::Target: Logger { + self.context.check_ffor_mutation()?; if self.funding.is_outbound() { return Err(ChannelError::close("Non-funding remote tried to update channel fee".to_owned())); } @@ -9566,6 +9620,14 @@ where pub fn signer_maybe_unblocked( &mut self, logger: &L, path_for_release_htlc: CBP ) -> SignerResumeUpdates where L::Target: Logger, CBP: Fn(u64) -> BlindedMessagePath { + if self.context.is_ffor_frozen() { + return SignerResumeUpdates { + commitment_update: None, revoke_and_ack: None, open_channel: None, + accept_channel: None, funding_created: None, funding_signed: None, + channel_ready: None, order: self.context.resend_order.clone(), + closing_signed: None, signed_closing_tx: None, shutdown_result: None, + }; + } if !self.holder_commitment_point.can_advance() { log_trace!(logger, "Attempting to update holder per-commitment point..."); self.holder_commitment_point.try_resolve_pending(&self.context.holder_signer, &self.context.secp_ctx, logger); @@ -9678,6 +9740,9 @@ where L::Target: Logger, CBP: Fn(u64) -> BlindedMessagePath, { + if self.context.is_ffor_frozen() { + return None; + } debug_assert!( self.holder_commitment_point.next_transaction_number() <= INITIAL_COMMITMENT_NUMBER - 2 ); @@ -9736,6 +9801,7 @@ where where L::Target: Logger, { + self.context.check_ffor_mutation().map_err(|_| ())?; let mut update_add_htlcs = Vec::new(); let mut update_fulfill_htlcs = Vec::new(); let mut update_fail_htlcs = Vec::new(); @@ -9838,6 +9904,9 @@ where /// Gets the `Shutdown` message we should send our peer on reconnect, if any. pub fn get_outbound_shutdown(&self) -> Option { + if self.context.is_ffor_frozen() { + return None; + } if self.context.channel_state.is_local_shutdown_sent() { assert!(self.context.shutdown_scriptpubkey.is_some()); Some(msgs::Shutdown { @@ -9862,6 +9931,7 @@ where NS::Target: NodeSigner, CBP: Fn(u64) -> BlindedMessagePath { + self.context.check_ffor_mutation()?; if !self.context.channel_state.is_peer_disconnected() { // While BOLT 2 doesn't indicate explicitly we should error this channel here, it // almost certainly indicates we are going to end up out-of-sync in some way, so we @@ -10312,6 +10382,9 @@ where /// an Err if no progress is being made and the channel should be force-closed instead. /// Should be called on a one-minute timer. pub fn timer_check_closing_negotiation_progress(&mut self) -> Result<(), ChannelError> { + if self.context.is_ffor_frozen() { + return Ok(()); + } if self.closing_negotiation_ready() { if self.context.closing_signed_in_flight { return Err(ChannelError::close( @@ -10331,6 +10404,9 @@ where F::Target: FeeEstimator, L::Target: Logger, { + if self.context.is_ffor_frozen() { + return Ok((None, None)); + } // If we're waiting on a monitor persistence, that implies we're also waiting to send some // message to our counterparty (probably a `revoke_and_ack`). In such a case, we shouldn't // initiate `closing_signed` negotiation until we're clear of all pending messages. Note @@ -10415,6 +10491,7 @@ where (Option, Option, Vec<(HTLCSource, PaymentHash)>), ChannelError, > { + self.context.check_ffor_mutation()?; if self.context.channel_state.is_peer_disconnected() { return Err(ChannelError::close( "Peer sent shutdown when we needed a channel_reestablish".to_owned(), @@ -10585,6 +10662,9 @@ where where L::Target: Logger, { + if self.context.is_ffor_frozen() { + return None; + } let sig = match &self.context.holder_signer { ChannelSignerType::Ecdsa(ecdsa) => ecdsa .sign_closing_transaction( @@ -10647,6 +10727,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { + self.context.check_ffor_mutation()?; if self.is_shutdown_pending_signature() { return Err(ChannelError::Warn(String::from("Remote end sent us a closing_signed while fully shutdown and just waiting on the final closing signature"))); } @@ -10972,6 +11053,12 @@ where if self.context.blocked_monitor_updates.is_empty() { return None; } + if !self + .context + .ffor_monitor_update_allowed(&self.context.blocked_monitor_updates[0].update) + { + return None; + } Some(( self.context.blocked_monitor_updates.remove(0).update, !self.context.blocked_monitor_updates.is_empty(), @@ -11887,7 +11974,10 @@ where /// May panic if called on a channel that wasn't immediately-previously /// self.remove_uncommitted_htlcs_and_mark_paused()'d #[rustfmt::skip] - fn get_channel_reestablish(&mut self, logger: &L) -> msgs::ChannelReestablish where L::Target: Logger { + fn get_channel_reestablish(&mut self, logger: &L) -> Result where L::Target: Logger { + if self.context.is_ffor_frozen() { + return Err(msgs::WarningMessage { channel_id: self.context.channel_id(), data: ffor::FFOR_FROZEN_MESSAGE.to_owned() }); + } assert!(self.context.channel_state.is_peer_disconnected()); assert_ne!(self.context.counterparty_next_commitment_transaction_number, INITIAL_COMMITMENT_NUMBER); // This is generally the first function which gets called on any given channel once we're @@ -11912,7 +12002,7 @@ where log_info!(logger, "Sending a data_loss_protect with no previous remote per_commitment_secret for channel {}", &self.context.channel_id()); [0;32] }; - msgs::ChannelReestablish { + Ok(msgs::ChannelReestablish { channel_id: self.context.channel_id(), // The protocol has two different commitment number concepts - the "commitment // transaction number", which starts from 0 and counts up, and the "revocation key @@ -11936,7 +12026,7 @@ where my_current_per_commitment_point: dummy_pubkey, next_funding: self.maybe_get_next_funding(), my_current_funding_locked: self.maybe_get_my_current_funding_locked(), - } + }) } /// Initiate splicing. @@ -11951,6 +12041,7 @@ where where L::Target: Logger, { + self.context.check_ffor_local_mutation()?; if self.holder_commitment_point.current_point().is_none() { return Err(APIError::APIMisuseError { err: format!( @@ -12113,6 +12204,7 @@ where pub fn abandon_splice( &mut self, ) -> Result<(msgs::TxAbort, Option), APIError> { + self.context.check_ffor_local_mutation()?; if self.should_reset_pending_splice_state(false) { let tx_abort = msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() }; @@ -12139,6 +12231,7 @@ where pub fn validate_splice_init( &self, msg: &msgs::SpliceInit, our_funding_contribution: SignedAmount, ) -> Result { + self.context.check_ffor_mutation()?; if matches!(self.quiescent_action, Some(QuiescentAction::FFORReceiver(_))) { return Err(ChannelError::WarnAndDisconnect( "FFOR receiver owns this quiescence session".to_owned(), @@ -12328,6 +12421,7 @@ where ES::Target: EntropySource, L::Target: Logger, { + self.context.check_ffor_mutation()?; let our_funding_contribution = SignedAmount::from_sat(our_funding_contribution_satoshis); let splice_funding = self.validate_splice_init(msg, our_funding_contribution)?; @@ -12399,6 +12493,7 @@ where ES::Target: EntropySource, L::Target: Logger, { + self.context.check_ffor_mutation()?; let splice_funding = self.validate_splice_ack(msg)?; log_info!( @@ -12448,6 +12543,7 @@ where } fn validate_splice_ack(&self, msg: &msgs::SpliceAck) -> Result { + self.context.check_ffor_mutation()?; // TODO(splicing): Add check that we are the splice (quiescence) initiator let pending_splice = self @@ -12554,6 +12650,7 @@ where NS::Target: NodeSigner, L::Target: Logger, { + self.context.check_ffor_mutation()?; log_info!( logger, "Received splice_locked txid {} from our peer for channel {}", @@ -12657,6 +12754,12 @@ where F::Target: FeeEstimator, L::Target: Logger, { + if self.context.is_ffor_frozen() { + return Err(( + LocalHTLCFailureReason::ChannelNotReady, + ffor::FFOR_FROZEN_MESSAGE.to_owned(), + )); + } if !matches!(self.context.channel_state, ChannelState::ChannelReady(_)) || self.context.channel_state.is_local_shutdown_sent() || self.context.channel_state.is_remote_shutdown_sent() @@ -12783,7 +12886,8 @@ where } #[rustfmt::skip] - fn build_commitment_no_status_check(&mut self, logger: &L) -> ChannelMonitorUpdate where L::Target: Logger { + fn build_commitment_no_status_check(&mut self, logger: &L) -> Result where L::Target: Logger { + self.context.check_ffor_mutation()?; log_trace!(logger, "Updating HTLC state for a newly-sent commitment_signed..."); // We can upgrade the status of some HTLCs that are waiting on a commitment, even if we // fail to generate this, we still are at least at a position where upgrading their status @@ -12876,7 +12980,7 @@ where channel_id: Some(self.context.channel_id()), }; self.context.channel_state.set_awaiting_remote_revoke(); - monitor_update + Ok(monitor_update) } #[rustfmt::skip] @@ -12916,6 +13020,7 @@ where where L::Target: Logger, { + self.context.check_ffor_mutation()?; // Get the fee tests from `build_commitment_no_state_update` #[cfg(any(test, fuzzing))] self.build_commitment_no_state_update(funding, logger); @@ -13003,7 +13108,7 @@ where // All [`LocalHTLCFailureReason`] errors are temporary, so they are [`ChannelError::Ignore`]. let can_add_htlc = send_res.map_err(|(_, msg)| ChannelError::Ignore(msg))?; if can_add_htlc { - let monitor_update = self.build_commitment_no_status_check(logger); + let monitor_update = self.build_commitment_no_status_check(logger)?; self.monitor_updating_paused(false, true, false, Vec::new(), Vec::new(), Vec::new()); Ok(self.push_ret_blockable_mon_update(monitor_update)) } else { @@ -13037,6 +13142,7 @@ where (msgs::Shutdown, Option, Vec<(HTLCSource, PaymentHash)>), APIError, > { + self.context.check_ffor_local_mutation()?; if self.context.channel_state.is_local_stfu_sent() || self.context.channel_state.is_remote_stfu_sent() || self.context.channel_state.is_quiescent() @@ -13184,6 +13290,7 @@ where where L::Target: Logger, { + if self.context.is_ffor_frozen() { return Err(ffor::FFOR_FROZEN_MESSAGE); } log_debug!(logger, "Attempting to initiate quiescence"); if !self.context.is_usable() { @@ -13223,6 +13330,7 @@ where where L::Target: Logger, { + if self.context.is_ffor_frozen() { return Err(ffor::FFOR_FROZEN_MESSAGE); } debug_assert!(!self.context.channel_state.is_local_stfu_sent()); debug_assert!( self.context.channel_state.is_awaiting_quiescence() @@ -13260,6 +13368,7 @@ where pub fn stfu( &mut self, msg: &msgs::Stfu, logger: &L ) -> Result, ChannelError> where L::Target: Logger { + self.context.check_ffor_mutation()?; if self.context.channel_state.is_quiescent() { return Err(ChannelError::Warn("Channel is already quiescent".to_owned())); } @@ -13372,6 +13481,9 @@ where where L::Target: Logger, { + if self.context.is_ffor_frozen() { + return Ok(None); + } // We must never see both stfu flags set, we always set the quiescent flag instead. debug_assert!( !(self.context.channel_state.is_local_stfu_sent() @@ -16204,7 +16316,7 @@ mod tests { // Now disconnect the two nodes and check that the commitment point in // Node B's channel_reestablish message is sane. assert!(node_b_chan.remove_uncommitted_htlcs_and_mark_paused(&&logger).is_ok()); - let msg = node_b_chan.get_channel_reestablish(&&logger); + let msg = node_b_chan.get_channel_reestablish(&&logger).unwrap(); assert_eq!(msg.next_local_commitment_number, 1); // now called next_commitment_number assert_eq!(msg.next_remote_commitment_number, 0); // now called next_revocation_number assert_eq!(msg.your_last_per_commitment_secret, [0; 32]); @@ -16212,7 +16324,7 @@ mod tests { // Check that the commitment point in Node A's channel_reestablish message // is sane. assert!(node_a_chan.remove_uncommitted_htlcs_and_mark_paused(&&logger).is_ok()); - let msg = node_a_chan.get_channel_reestablish(&&logger); + let msg = node_a_chan.get_channel_reestablish(&&logger).unwrap(); assert_eq!(msg.next_local_commitment_number, 1); // now called next_commitment_number assert_eq!(msg.next_remote_commitment_number, 0); // now called next_revocation_number assert_eq!(msg.your_last_per_commitment_secret, [0; 32]); diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index 20b330ed5ab..46cd95909e4 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -1,5 +1,6 @@ use super::*; +mod fence; mod quiescence; mod setup; use crate::ln::ffor::{ @@ -7,6 +8,8 @@ use crate::ln::ffor::{ FFORReceiverError, FFORReceiverStatus, FFORSettlementParty, FFORVoucher, FFORVoucherCommitments, FFORVoucherFailure, }; +use fence::FFORReceiverFence; +pub(crate) use fence::{FFORReceiverFencePhase, FFOR_FROZEN_MESSAGE}; pub(crate) use quiescence::FFORReceiverQuiescence; #[cfg(test)] pub(crate) use setup::ffor_setup_test_messages; @@ -19,6 +22,7 @@ pub(super) struct FFORReceiverBook { received: Vec, abort_reason: Option, setup: Option, + fence: Option, } struct FFORReceivedVoucher { @@ -38,11 +42,15 @@ impl_writeable_tlv_based!(FFORReceiverBook, { (6, abort_reason, option), // Readers predating authenticated setup must refuse this record. (8, setup, option), + // A reader without the mutation fence must refuse this channel. + (10, fence, option), }); impl FFORReceiverBook { pub(super) fn abort(&mut self, reason: FFORReceiverAbortReason) { - self.abort_reason.get_or_insert(reason); + if self.fence.is_none() { + self.abort_reason.get_or_insert(reason); + } } pub(super) fn restored( @@ -130,6 +138,7 @@ where received: Vec::new(), abort_reason: None, setup: None, + fence: None, }); Ok(()) } @@ -183,6 +192,9 @@ where if book.epoch_id != epoch_id { return Err(FFORReceiverError::UnknownEpoch); } + if book.fence.is_some() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } book.abort(FFORReceiverAbortReason::Requested); Ok(()) } @@ -298,7 +310,7 @@ where { self.ffor_abort_revealed_secret_reuse(); let book = match self.context.ffor_receiver_book.as_ref() { - Some(book) if book.abort_reason.is_some() => book, + Some(book) if book.abort_reason.is_some() && book.fence.is_none() => book, _ => return, }; let failures: Vec<_> = book diff --git a/lightning/src/ln/channel/ffor/fence.rs b/lightning/src/ln/channel/ffor/fence.rs new file mode 100644 index 00000000000..4bc68e95c09 --- /dev/null +++ b/lightning/src/ln/channel/ffor/fence.rs @@ -0,0 +1,203 @@ +//! A private durable mutation fence, independent of connection-scoped STFU flags. +//! +//! There is deliberately no production installation or reconciliation entry point. A future +//! activation owner must first archive the exact signed evidence, then install this fence under +//! the same channel/manager persistence boundary. This module does not authenticate activation transcripts. + +use super::*; + +pub(crate) const FFOR_FROZEN_MESSAGE: &str = + "FFOR channel requires authenticated reconciliation before ordinary updates"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum FFORReceiverFencePhase { + Activating, + Active, +} + +impl_writeable_tlv_based_enum!(FFORReceiverFencePhase, + (0, Activating) => {}, + (2, Active) => {}, +); + +pub(super) struct FFORReceiverFence { + phase: FFORReceiverFencePhase, + activation_hash: [u8; 32], +} + +impl_writeable_tlv_based!(FFORReceiverFence, { + (0, phase, required), + (2, activation_hash, required), +}); + +impl ChannelContext +where + SP::Target: SignerProvider, +{ + pub(crate) fn is_ffor_frozen(&self) -> bool { + self.ffor_receiver_book.as_ref().map_or(false, |book| book.fence.is_some()) + } + + pub(in crate::ln::channel) fn check_ffor_mutation(&self) -> Result<(), ChannelError> { + if self.is_ffor_frozen() { + Err(ChannelError::WarnAndDisconnect(FFOR_FROZEN_MESSAGE.to_owned())) + } else { + Ok(()) + } + } + + pub(in crate::ln::channel) fn check_ffor_local_mutation(&self) -> Result<(), APIError> { + if self.is_ffor_frozen() { + Err(APIError::ChannelUnavailable { err: FFOR_FROZEN_MESSAGE.to_owned() }) + } else { + Ok(()) + } + } + + pub(in crate::ln::channel) fn ffor_monitor_update_allowed( + &self, update: &ChannelMonitorUpdate, + ) -> bool { + if !self.is_ffor_frozen() { + return true; + } + update.updates.iter().all(|step| match step { + ChannelMonitorUpdateStep::PaymentPreimage { payment_preimage, .. } => { + let hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); + self.ffor_receiver_book + .as_ref() + .unwrap() + .vouchers + .iter() + .any(|voucher| voucher.payment_hash == hash) + }, + _ => false, + }) + } + + pub(in crate::ln::channel) fn ffor_owns_preimage( + &self, id: u64, preimage: &PaymentPreimage, + ) -> bool { + let hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); + self.ffor_receiver_book.as_ref().map_or(false, |book| { + book.vouchers + .iter() + .any(|voucher| voucher.htlc_id == id && voucher.payment_hash == hash) + }) + } +} + +impl FundedChannel +where + SP::Target: SignerProvider, +{ + pub(crate) fn ffor_receiver_fence(&self) -> Option<(FFORReceiverFencePhase, [u8; 32])> { + self.context + .ffor_receiver_book + .as_ref() + .and_then(|book| book.fence.as_ref()) + .map(|fence| (fence.phase, fence.activation_hash)) + } + + pub(in crate::ln::channel) fn validate_ffor_fence(&self) -> Result<(), DecodeError> { + let book = match self.context.ffor_receiver_book.as_ref() { + Some(book) if book.fence.is_some() => book, + _ => return Ok(()), + }; + let context = &self.context; + if book.setup.is_none() + || book.abort_reason.is_some() + || !matches!(context.channel_state, ChannelState::ChannelReady(_)) + || context.channel_state.is_local_shutdown_sent() + || context.channel_state.is_remote_shutdown_sent() + || context.is_waiting_on_peer_pending_channel_update() + || context.pending_update_fee.is_some() + || context.holding_cell_update_fee.is_some() + || context.interactive_tx_signing_session.is_some() + || self.pending_splice.is_some() + || matches!(self.quiescent_action, Some(QuiescentAction::Splice(_))) + || !self.holder_commitment_point.can_advance() + || context.signer_pending_commitment_update + || context.signer_pending_revoke_and_ack + || context.monitor_pending_commitment_signed + || context.monitor_pending_revoke_and_ack + || context.signer_pending_closing + || context.signer_pending_funding + || context.signer_pending_channel_ready + || context.monitor_pending_channel_ready + || !context.monitor_pending_forwards.is_empty() + || !context.monitor_pending_update_adds.is_empty() + || !context.monitor_pending_failures.is_empty() + || !context.monitor_pending_finalized_fulfills.is_empty() + || !context.pending_outbound_htlcs.is_empty() + || context.pending_inbound_htlcs.len() != book.vouchers.len() + || book.received.len() != book.vouchers.len() + || book.received.iter().any(|received| received.failure.is_none()) + { + return Err(DecodeError::InvalidValue); + } + for htlc in &context.pending_inbound_htlcs { + if !matches!(htlc.state, InboundHTLCState::Committed) + || !book.vouchers.iter().any(|voucher| { + voucher.htlc_id == htlc.htlc_id + && voucher.payment_hash == htlc.payment_hash + && voucher.amount_msat == htlc.amount_msat + && voucher.cltv_expiry == htlc.cltv_expiry + }) { + return Err(DecodeError::InvalidValue); + } + } + for update in &context.holding_cell_htlc_updates { + match update { + HTLCUpdateAwaitingACK::ClaimHTLC { htlc_id, payment_preimage, .. } + if context.ffor_owns_preimage(*htlc_id, payment_preimage) => {}, + _ => return Err(DecodeError::InvalidValue), + } + } + for pending in &context.blocked_monitor_updates { + if !context.ffor_monitor_update_allowed(&pending.update) { + return Err(DecodeError::InvalidValue); + } + } + Ok(()) + } + + /// Test-only installation exercises gates using actual channel and monitor evidence. + /// The caller must also install matching authenticated recovery evidence before manager persistence. + /// This method checks the native channel proof, not the supplied activation hash. + #[cfg(test)] + pub(crate) fn install_ffor_fence_for_test( + &mut self, phase: FFORReceiverFencePhase, activation_hash: [u8; 32], + monitor: &FFORMonitorSnapshot, current_height: u32, logger: &L, + ) -> Result<(), FFORReceiverError> + where + L::Target: Logger, + { + let book = + self.context.ffor_receiver_book.as_ref().ok_or(FFORReceiverError::NotRegistered)?; + if book.fence.is_some() || book.setup.is_none() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let epoch_id = book.epoch_id; + if self.ffor_receiver_quiescence_status(epoch_id, current_height, logger)? + != crate::ln::ffor::FFORReceiverQuiescenceStatus::Quiescent + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + if !matches!( + self.ffor_receiver_book_status(monitor, logger)?, + FFORReceiverStatus::Parked { .. } + ) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + self.context.ffor_receiver_book.as_mut().unwrap().fence = + Some(FFORReceiverFence { phase, activation_hash }); + if self.validate_ffor_fence().is_err() { + self.context.ffor_receiver_book.as_mut().unwrap().fence = None; + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/channel/ffor/fence/tests.rs b/lightning/src/ln/channel/ffor/fence/tests.rs new file mode 100644 index 00000000000..22853b6c476 --- /dev/null +++ b/lightning/src/ln/channel/ffor/fence/tests.rs @@ -0,0 +1,603 @@ +use super::*; +use crate::ln::channelmanager::ffor_recovery_tests::install_ffor_activation_for_test; +use crate::ln::ffor_tests::quiescence::{complete_handshake, register_signed, request}; +use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; +use crate::ln::functional_test_utils::*; +use crate::ln::msgs::{BaseMessageHandler, MessageSendEvent}; +use crate::util::ser::Writeable; + +fn with_channel<'a, 'b, 'c, T>( + receiver: &Node<'a, 'b, 'c>, sender: &Node, id: ChannelId, + body: impl FnOnce(&mut FundedChannel<&'b crate::util::test_utils::TestKeysInterface>) -> T, +) -> T { + let peers = receiver.node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + body(peer.channel_by_id.get_mut(&id).unwrap().as_funded_mut().unwrap()) +} + +fn prepare( + sender: &Node, receiver: &Node, id: ChannelId, phase: FFORReceiverFencePhase, +) -> (msgs::CommitmentUpdate, FFORVoucher) { + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + register_signed(sender, receiver, id, voucher); + deliver_parked_voucher(sender, receiver, update.clone()); + request(sender, receiver, id).unwrap(); + complete_handshake(sender, receiver); + let activation_hash = install_ffor_activation_for_test(sender, receiver, id, phase); + with_channel(receiver, sender, id, |channel| { + // Deliberately remove stock STFU flags. The durable fence must stand independently. + assert!(channel.exit_quiescence()); + assert_eq!(channel.ffor_receiver_fence(), Some((phase, activation_hash))); + }); + (update, voucher) +} + +fn expect_fenced(result: Result) { + match result { + Err(ChannelError::WarnAndDisconnect(message)) => assert_eq!(message, FFOR_FROZEN_MESSAGE), + _ => panic!("Ordinary mutation did not hit the FFOR fence"), + } +} + +#[test] +fn ffor_fence_rejects_ordinary_mutations_without_stock_quiescence() { + for phase in [FFORReceiverFencePhase::Activating, FFORReceiverFencePhase::Active] { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let id = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = + if receiver_funds { (&nodes[1], &nodes[0]) } else { (&nodes[0], &nodes[1]) }; + let (update, voucher) = prepare(sender, receiver, id, phase); + let fee = LowerBoundedFeeEstimator::new(receiver.fee_estimator); + with_channel(receiver, sender, id, |channel| { + let before = channel.encode(); + expect_fenced(channel.update_add_htlc(&update.update_add_htlcs[0], &fee)); + expect_fenced(channel.update_fulfill_htlc(&msgs::UpdateFulfillHTLC { + channel_id: id, + htlc_id: voucher.htlc_id, + payment_preimage: PaymentPreimage([0; 32]), + attribution_data: None, + })); + let reason = || { + HTLCFailReason::from_failure_code(LocalHTLCFailureReason::TemporaryNodeFailure) + }; + expect_fenced(channel.update_fail_htlc( + &msgs::UpdateFailHTLC { + channel_id: id, + htlc_id: voucher.htlc_id, + reason: Vec::new(), + attribution_data: None, + }, + reason(), + )); + expect_fenced(channel.update_fail_malformed_htlc( + &msgs::UpdateFailMalformedHTLC { + channel_id: id, + htlc_id: voucher.htlc_id, + sha256_of_onion: [0; 32], + failure_code: 0x8004, + }, + reason(), + )); + expect_fenced(channel.update_fee( + &fee, + &msgs::UpdateFee { channel_id: id, feerate_per_kw: 500 }, + &receiver.logger, + )); + expect_fenced(channel.commitment_signed( + &update.commitment_signed[0], + &fee, + &receiver.logger, + )); + expect_fenced(channel.commitment_signed_batch( + update.commitment_signed.clone(), + &fee, + &receiver.logger, + )); + expect_fenced(channel.revoke_and_ack( + &msgs::RevokeAndACK { + channel_id: id, + per_commitment_secret: [0; 32], + next_per_commitment_point: sender.node.get_our_node_id(), + #[cfg(taproot)] + next_local_nonce: None, + release_htlc_message_paths: Vec::new(), + }, + &fee, + &receiver.logger, + false, + )); + assert!(channel + .send_htlc_and_commit( + 1_000_000, + PaymentHash([7; 32]), + voucher.cltv_expiry, + HTLCSource::dummy(), + update.update_add_htlcs[0].onion_routing_packet.clone(), + None, + false, + &fee, + &receiver.logger + ) + .is_err()); + assert!(channel + .queue_fail_htlc( + voucher.htlc_id, + msgs::OnionErrorPacket { data: Vec::new(), attribution_data: None }, + &receiver.logger + ) + .is_err()); + assert!(channel + .queue_fail_malformed_htlc(voucher.htlc_id, 0x8004, [0; 32], &receiver.logger) + .is_err()); + channel.queue_update_fee(500, &fee, &receiver.logger); + assert!(channel.maybe_free_holding_cell_htlcs(&fee, &receiver.logger).0.is_none()); + assert!(channel.free_holding_cell_htlcs(&fee, &receiver.logger).0.is_none()); + expect_fenced(channel.build_commitment_no_status_check(&receiver.logger)); + expect_fenced(channel.send_commitment_no_state_update_for_funding( + &channel.funding, + &receiver.logger, + )); + assert!(channel.get_last_commitment_update_for_send(&receiver.logger).is_err()); + assert!(channel + .get_last_revoke_and_ack(|_| unreachable!(), &receiver.logger) + .is_none()); + assert!(channel.get_channel_reestablish(&receiver.logger).is_err()); + expect_fenced( + channel.stfu(&msgs::Stfu { channel_id: id, initiator: true }, &receiver.logger), + ); + assert!(channel + .propose_quiescence(&receiver.logger, QuiescentAction::DoNothing) + .is_err()); + expect_fenced(channel.validate_splice_init( + &msgs::SpliceInit { + channel_id: id, + funding_contribution_satoshis: 1000, + funding_feerate_per_kw: 500, + locktime: 0, + funding_pubkey: sender.node.get_our_node_id(), + require_confirmed_inputs: None, + }, + SignedAmount::ZERO, + )); + expect_fenced(channel.shutdown( + &receiver.keys_manager, + &sender.node.init_features(), + &msgs::Shutdown { channel_id: id, scriptpubkey: ScriptBuf::new() }, + )); + expect_fenced(channel.closing_signed( + &fee, + &msgs::ClosingSigned { + channel_id: id, + fee_satoshis: 100, + signature: update.commitment_signed[0].signature, + fee_range: None, + }, + &receiver.logger, + )); + assert!(channel + .get_shutdown(&receiver.keys_manager, &sender.node.init_features(), None, None) + .is_err()); + assert!(channel.abort_ffor_receiver_book([81; 32]).is_err()); + channel.ffor_queue_aborted_vouchers(&receiver.logger); + assert_eq!(channel.encode(), before); + }); + check_added_monitors(receiver, 0); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + assert!(receiver.node.get_and_clear_pending_events().is_empty()); + if receiver_funds { + *receiver.fee_estimator.sat_per_kw.lock().unwrap() += 20; + receiver.node.timer_tick_occurred(); + check_added_monitors(receiver, 0); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + } + } + } +} + +fn expect_no_released_updates(updates: MonitorRestoreUpdates) { + assert!(updates.raa.is_none()); + assert!(updates.commitment_update.is_none()); + assert!(updates.accepted_htlcs.is_empty()); + assert!(updates.failed_htlcs.is_empty()); + assert!(updates.finalized_claimed_htlcs.is_empty()); + assert!(updates.pending_update_adds.is_empty()); + assert!(updates.funding_broadcastable.is_none()); + assert!(updates.channel_ready.is_none()); + assert!(updates.announcement_sigs.is_none()); + assert!(updates.tx_signatures.is_none()); +} + +#[test] +fn ffor_fence_preserves_preimage_and_blocks_restart_wire_until_reconciliation() { + use crate::chain::{ChannelMonitorUpdateStatus, Watch}; + for phase in [FFORReceiverFencePhase::Activating, FFORReceiverFencePhase::Active] { + for restart_before_completion in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let reloaded; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let preimage = PaymentPreimage([*nodes[1].network_payment_count.borrow(); 32]); + let (_, voucher) = prepare(&nodes[0], &nodes[1], id, phase); + assert_eq!( + PaymentHash(Sha256::hash(&preimage.0).to_byte_array()), + voucher.payment_hash + ); + let activation = with_channel(&nodes[1], &nodes[0], id, |channel| { + channel.ffor_receiver_fence().unwrap() + }); + let update = with_channel(&nodes[1], &nodes[0], id, |channel| { + let before = channel.encode(); + assert!(matches!( + channel.get_update_fulfill_htlc_and_commit( + voucher.htlc_id, + PaymentPreimage([99; 32]), + None, + None, + &nodes[1].logger + ), + UpdateFulfillCommitFetch::DuplicateClaim {} + )); + assert!(matches!( + channel.get_update_fulfill_htlc_and_commit( + voucher.htlc_id + 1, + preimage, + None, + None, + &nodes[1].logger + ), + UpdateFulfillCommitFetch::DuplicateClaim {} + )); + assert_eq!(channel.encode(), before); + let update = match channel.get_update_fulfill_htlc_and_commit( + voucher.htlc_id, + preimage, + None, + None, + &nodes[1].logger, + ) { + UpdateFulfillCommitFetch::NewClaim { monitor_update, htlc_value_msat } => { + assert_eq!(htlc_value_msat, voucher.amount_msat); + monitor_update + }, + _ => panic!("Valid owned preimage was not retained"), + }; + assert_eq!(update.updates.len(), 1); + assert!(channel.context.ffor_monitor_update_allowed(&update)); + assert!(channel.is_awaiting_monitor_update()); + assert_eq!(channel.context.holding_cell_htlc_updates.len(), 1); + assert!(matches!( + channel.context.pending_inbound_htlcs[0].state, + InboundHTLCState::Committed + )); + assert!(channel.validate_ffor_fence().is_ok()); + assert!(matches!( + channel.get_update_fulfill_htlc_and_commit( + voucher.htlc_id, + preimage, + None, + None, + &nodes[1].logger + ), + UpdateFulfillCommitFetch::DuplicateClaim {} + )); + update + }); + assert_eq!( + nodes[1].chain_monitor.update_channel(id, &update), + ChannelMonitorUpdateStatus::Completed + ); + check_added_monitors(&nodes[1], 1); + assert_eq!( + get_monitor!(nodes[1], id).get_stored_preimages()[&voucher.payment_hash].0, + preimage + ); + if !restart_before_completion { + with_channel(&nodes[1], &nodes[0], id, |channel| { + expect_no_released_updates(channel.monitor_updating_restored( + &nodes[1].logger, + &nodes[1].keys_manager, + ChainHash::using_genesis_block(bitcoin::Network::Testnet), + &config, + nodes[1].node.current_best_block().height, + |_| unreachable!(), + )); + }); + } + let manager = nodes[1].node.encode(); + let monitor = get_monitor!(nodes[1], id).encode(); + nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id()); + reload_node!( + nodes[1], + config, + &manager, + &[&monitor], + persister, + chain_monitor, + reloaded + ); + assert_eq!( + get_monitor!(nodes[1], id).get_stored_preimages()[&voucher.payment_hash].0, + preimage + ); + with_channel(&nodes[1], &nodes[0], id, |channel| { + assert_eq!(channel.ffor_receiver_fence(), Some(activation)); + assert!(channel + .context + .ffor_receiver_book + .as_ref() + .unwrap() + .abort_reason + .is_none()); + assert_eq!(channel.context.holding_cell_htlc_updates.len(), 1); + assert!(matches!( + channel.context.pending_inbound_htlcs[0].state, + InboundHTLCState::Committed + )); + let fee = LowerBoundedFeeEstimator::new(nodes[1].fee_estimator); + assert!(channel.maybe_free_holding_cell_htlcs(&fee, &nodes[1].logger).0.is_none()); + let signer = channel.signer_maybe_unblocked(&nodes[1].logger, |_| unreachable!()); + assert!(signer.commitment_update.is_none() && signer.revoke_and_ack.is_none()); + assert!(signer.channel_ready.is_none() && signer.closing_signed.is_none()); + assert!(channel.abort_ffor_receiver_book([81; 32]).is_err()); + }); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + for _ in 0..2 { + nodes[1] + .node + .peer_connected( + nodes[0].node.get_our_node_id(), + &msgs::Init { + features: nodes[0].node.init_features(), + networks: None, + remote_network_address: None, + }, + true, + ) + .unwrap(); + let events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1, "{events:?}"); + assert!(matches!( + &events[0], + MessageSendEvent::HandleError { + action: msgs::ErrorAction::DisconnectPeerWithWarning { .. }, + .. + } + )); + nodes[1].node.peer_disconnected(nodes[0].node.get_our_node_id()); + with_channel(&nodes[1], &nodes[0], id, |channel| { + assert_eq!(channel.ffor_receiver_fence(), Some(activation)) + }); + } + // The fence must preserve the existing force-close and monitor claim path. + nodes[1] + .node + .force_close_broadcasting_latest_txn( + &id, + &nodes[0].node.get_our_node_id(), + "fence recovery test".into(), + ) + .unwrap(); + assert!(nodes[1].node.list_channels().is_empty()); + handle_bump_close_event(&nodes[1]); + assert!(!nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().is_empty()); + assert_eq!( + get_monitor!(nodes[1], id).get_stored_preimages()[&voucher.payment_hash].0, + preimage + ); + assert!(nodes[1] + .node + .get_and_clear_pending_events() + .iter() + .any(|event| matches!(event, crate::events::Event::ChannelClosed { .. }))); + nodes[1].node.get_and_clear_pending_msg_events(); + nodes[1].chain_monitor.added_monitors.lock().unwrap().clear(); + } + } +} + +#[test] +fn ffor_fence_refuses_pending_state_on_restore_and_blocks_signer_monitor_replay() { + use crate::util::ser::ReadableArgs; + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let (_, voucher) = prepare(&nodes[0], &nodes[1], id, FFORReceiverFencePhase::Active); + let encoded = with_channel(&nodes[1], &nodes[0], id, |channel| channel.encode()); + let features = crate::ln::channelmanager::provided_channel_type_features(&config); + let read = |bytes: &[u8]| { + FundedChannel::read( + &mut &bytes[..], + (&nodes[1].keys_manager, &nodes[1].keys_manager, &features), + ) + }; + assert!(read(&encoded).unwrap().validate_ffor_fence().is_ok()); + for fault in 0..16 { + let mut channel = read(&encoded).unwrap(); + match fault { + 0 => { + channel.context.pending_update_fee = + Some((500, FeeUpdateState::AwaitingRemoteRevokeToAnnounce)) + }, + 1 => channel.context.holding_cell_update_fee = Some(500), + 2 => channel.context.signer_pending_commitment_update = true, + 3 => channel.context.signer_pending_revoke_and_ack = true, + 4 => channel.context.signer_pending_closing = true, + 5 => channel.context.signer_pending_funding = true, + 6 => channel.context.signer_pending_channel_ready = true, + 7 => channel.context.monitor_pending_commitment_signed = true, + 8 => channel.context.monitor_pending_revoke_and_ack = true, + 9 => channel.context.monitor_pending_channel_ready = true, + 10 => { + channel.context.ffor_receiver_book.as_mut().unwrap().abort_reason = + Some(FFORReceiverAbortReason::Requested) + }, + 11 => channel.context.ffor_receiver_book.as_mut().unwrap().setup = None, + 12 => { + channel.context.pending_inbound_htlcs[0].state = InboundHTLCState::LocalRemoved( + InboundHTLCRemovalReason::Fulfill(PaymentPreimage([99; 32]), None), + ) + }, + 13 => { + channel.context.holding_cell_htlc_updates.push(HTLCUpdateAwaitingACK::ClaimHTLC { + htlc_id: voucher.htlc_id, + payment_preimage: PaymentPreimage([99; 32]), + attribution_data: None, + }) + }, + 14 => channel.context.ffor_receiver_book.as_mut().unwrap().received[0].failure = None, + 15 => channel.context.blocked_monitor_updates.push(PendingChannelMonitorUpdate { + update: ChannelMonitorUpdate { + update_id: channel.context.latest_monitor_update_id + 1, + channel_id: Some(id), + updates: vec![ChannelMonitorUpdateStep::ChannelForceClosed { + should_broadcast: false, + }], + }, + }), + _ => unreachable!(), + } + assert!(channel.validate_ffor_fence().is_err(), "fault {fault}"); + // Even malformed restored bookkeeping must not bypass an installed fence before refusal. + let signer = channel.signer_maybe_unblocked(&nodes[1].logger, |_| unreachable!()); + assert!(signer.commitment_update.is_none() && signer.revoke_and_ack.is_none()); + assert!(signer.open_channel.is_none() && signer.accept_channel.is_none()); + assert!(signer.funding_created.is_none() && signer.funding_signed.is_none()); + assert!(signer.channel_ready.is_none() && signer.closing_signed.is_none()); + assert!(signer.signed_closing_tx.is_none() && signer.shutdown_result.is_none()); + if fault == 15 { + assert!(channel.unblock_next_blocked_monitor_update().is_none()); + assert_eq!(channel.context.blocked_monitor_updates.len(), 1); + } else { + channel.context.channel_state.set_monitor_update_in_progress(); + expect_no_released_updates(channel.monitor_updating_restored( + &nodes[1].logger, + &nodes[1].keys_manager, + ChainHash::using_genesis_block(bitcoin::Network::Testnet), + &config, + nodes[1].node.current_best_block().height, + |_| unreachable!(), + )); + } + if (2..=6).contains(&fault) { + // Stock signer retry flags are transient and are not serialized. + assert!(read(&channel.encode()).unwrap().validate_ffor_fence().is_ok()); + } else { + assert!(read(&channel.encode()).is_err(), "fault {fault}"); + } + } +} + +#[test] +fn ffor_fence_required_field_refuses_a_reader_without_fence_support() { + use crate::util::ser::Readable; + struct BeforeFenceBook { + epoch_id: [u8; 32], + vouchers: Vec, + received: Vec, + abort_reason: Option, + setup: Option, + } + impl_writeable_tlv_based!(BeforeFenceBook, { + (0, epoch_id, required), + (2, vouchers, required_vec), + (4, received, required_vec), + (6, abort_reason, option), + (8, setup, option), + }); + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + prepare(&nodes[0], &nodes[1], id, FFORReceiverFencePhase::Activating); + with_channel(&nodes[1], &nodes[0], id, |channel| { + let book = channel.context.ffor_receiver_book.as_mut().unwrap(); + assert!(matches!( + BeforeFenceBook::read(&mut &book.encode()[..]), + Err(DecodeError::UnknownRequiredFeature) + )); + let fence = book.fence.take(); + assert!(BeforeFenceBook::read(&mut &book.encode()[..]).is_ok()); + book.fence = fence; + }); +} + +#[test] +fn ffor_fence_force_close_completes_delayed_preimage_after_channel_removal() { + use crate::chain::ChannelMonitorUpdateStatus; + use crate::ln::channelmanager::ffor_recovery_tests::claim_ffor_preimage_for_test; + for phase in [FFORReceiverFencePhase::Activating, FFORReceiverFencePhase::Active] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let preimage = PaymentPreimage([*nodes[1].network_payment_count.borrow(); 32]); + let (_, voucher) = prepare(&nodes[0], &nodes[1], id, phase); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + let preimage_update = + claim_ffor_preimage_for_test(&nodes[0], &nodes[1], id, voucher.htlc_id, preimage); + check_added_monitors(&nodes[1], 1); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + with_channel(&nodes[1], &nodes[0], id, |channel| { + assert!(channel.is_awaiting_monitor_update()); + assert!(channel.validate_ffor_fence().is_ok()); + }); + nodes[1] + .node + .force_close_broadcasting_latest_txn( + &id, + &nodes[0].node.get_our_node_id(), + "delayed fenced preimage".into(), + ) + .unwrap(); + assert!(nodes[1].node.list_channels().is_empty()); + check_added_monitors(&nodes[1], 1); + let close_update = get_monitor!(nodes[1], id).get_latest_update_id(); + assert_eq!(close_update, preimage_update + 1); + assert_eq!( + get_monitor!(nodes[1], id).get_stored_preimages()[&voucher.payment_hash].0, + preimage + ); + assert!(nodes[1] + .node + .get_and_clear_pending_events() + .iter() + .any(|event| matches!(event, crate::events::Event::ChannelClosed { .. }))); + nodes[1].node.get_and_clear_pending_msg_events(); + // Both writes complete after the live channel has been removed. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + nodes[1].chain_monitor.chain_monitor.channel_monitor_updated(id, preimage_update).unwrap(); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[1].chain_monitor.chain_monitor.channel_monitor_updated(id, close_update).unwrap(); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + handle_bump_close_event(&nodes[1]); + assert!(!nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().is_empty()); + assert_eq!( + get_monitor!(nodes[1], id).get_stored_preimages()[&voucher.payment_hash].0, + preimage + ); + check_added_monitors(&nodes[1], 0); + } +} diff --git a/lightning/src/ln/channel/ffor/setup.rs b/lightning/src/ln/channel/ffor/setup.rs index 96e4b41f4e9..7f8ab63cf3d 100644 --- a/lightning/src/ln/channel/ffor/setup.rs +++ b/lightning/src/ln/channel/ffor/setup.rs @@ -352,6 +352,7 @@ where /// Validate before the reader converts a live setup into an aborted restart tombstone. pub(in crate::ln::channel) fn ffor_restored(&mut self) -> Result<(), DecodeError> { self.ffor_validate_receiver_setup()?; + self.validate_ffor_fence()?; if let Some(book) = self.context.ffor_receiver_book.as_mut() { book.abort(FFORReceiverAbortReason::Restarted); } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e6ced0d8787..9a070f969ea 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -14288,6 +14288,11 @@ where for (_, chan) in peer_state.channel_by_id.iter_mut() { let logger = WithChannelContext::from(&self.logger, &chan.context(), None); match chan.peer_connected_get_handshake(self.chain_hash, &&logger) { + ReconnectionMsg::FFORFrozen(msg) => + pending_msg_events.push(MessageSendEvent::HandleError { + node_id: chan.context().get_counterparty_node_id(), + action: msgs::ErrorAction::DisconnectPeerWithWarning { msg }, + }), ReconnectionMsg::Reestablish(msg) => pending_msg_events.push(MessageSendEvent::SendChannelReestablish { node_id: chan.context().get_counterparty_node_id(), @@ -17168,7 +17173,7 @@ where )?; channel.ffor_validate_receiver_identity(our_network_pubkey, chain_hash)?; if let Some(setup) = channel.ffor_receiver_setup_record()? { - ffor_channel_setups.push(setup); + ffor_channel_setups.push((setup, channel.ffor_receiver_fence())); } let logger = WithChannelContext::from(&args.logger, &channel.context, None); let channel_id = channel.context.channel_id(); @@ -17570,10 +17575,12 @@ where }); let ffor_recovery = ffor_recovery.unwrap_or_else(FFORRecoveryRegistry::new); ffor_recovery.validate_identity(our_network_pubkey, chain_hash)?; - for setup in &ffor_channel_setups { - if !ffor_recovery.contains_exact(setup) { - return Err(DecodeError::InvalidValue); - } + for (setup, fence) in &ffor_channel_setups { + ffor_recovery.validate_channel_fence(setup, *fence)?; + } + for channel_id in ffor_recovery.activation_channels() { + let monitor = args.channel_monitors.get(&channel_id).ok_or(DecodeError::InvalidValue)?; + ffor_recovery.validate_activation_monitor(channel_id, &monitor.ffor_recovery_identity())?; } let mut decode_update_add_htlcs = decode_update_add_htlcs.unwrap_or_else(|| new_hash_map()); for peer in per_peer_state.values() { @@ -18849,7 +18856,7 @@ where } #[cfg(test)] -mod ffor_recovery_tests; +pub(crate) mod ffor_recovery_tests; #[cfg(test)] mod tests { diff --git a/lightning/src/ln/channelmanager/ffor_recovery_tests.rs b/lightning/src/ln/channelmanager/ffor_recovery_tests.rs index e9a8de02c41..526ecdbb1ca 100644 --- a/lightning/src/ln/channelmanager/ffor_recovery_tests.rs +++ b/lightning/src/ln/channelmanager/ffor_recovery_tests.rs @@ -1,10 +1,121 @@ use super::*; use crate::chain::channelmonitor::ChannelMonitor; -use crate::ln::channel::{ffor_setup_test_messages, FFORReceiverSetup}; +use crate::ln::channel::{ffor_setup_test_messages, FFORReceiverFencePhase, FFORReceiverSetup}; use crate::ln::ffor_recovery::tests::full_registry; -use crate::ln::ffor_tests::anchor_config; +use crate::ln::ffor_recovery::{FFORReceiverActivation, FFORRecoveryKey}; +use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; use crate::ln::functional_test_utils::*; +use crate::sign::ffor::FFORSigningRequest; +use crate::sign::{KeysManager, NodeSigner, Recipient}; use crate::util::test_channel_signer::TestChannelSigner; +use lightning_ffor::transcript; +use lightning_ffor::wire::{Activate, Message as FFORMessage, Payload}; + +fn sign_activation(message: &mut FFORMessage, node: &Node) { + let keys = KeysManager::new(&node.node_seed, 0, 0, true); + assert_eq!(keys.get_node_id(Recipient::Node).unwrap(), node.node.get_our_node_id()); + let wire = message.unsigned_wire().unwrap(); + message.signature = keys + .sign_ffor_message(&FFORSigningRequest::new(&wire).unwrap()) + .unwrap() + .serialize_compact(); +} + +/// Test-only composition of genuine signed evidence and native fence installation. This helper +/// sends no activation wire and does not provide an operational activation or invoice API. +pub(crate) fn install_ffor_activation_for_test( + sender: &Node, receiver: &Node, channel_id: ChannelId, phase: FFORReceiverFencePhase, +) -> [u8; 32] { + let monitor = get_monitor!(receiver, channel_id).ffor_commitment_snapshot().unwrap(); + let current_height = receiver.node.current_best_block().height; + let sender_id = sender.node.get_our_node_id(); + let hash; + { + let peers = receiver.node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&sender_id).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get_mut(&channel_id).unwrap().as_funded_mut().unwrap(); + let setup = channel.ffor_receiver_setup_record().unwrap().unwrap(); + let authenticated = setup.validate_recovery().unwrap(); + let commitments = + match channel.ffor_receiver_book_status(&monitor, &receiver.logger).unwrap() { + FFORReceiverStatus::Parked { commitments } => commitments, + other => panic!("unexpected setup state: {:?}", other), + }; + let commit_hash = transcript::commitment_hash( + commitments.holder.number, + &commitments.holder.txid.to_byte_array(), + commitments.counterparty.number, + &commitments.counterparty.txid.to_byte_array(), + ); + let mut activate = FFORMessage { + header: authenticated.header(), + payload: Payload::Activate(Activate { + setup_hash: authenticated.setup_hash(), + book_hash: authenticated.book_hash(), + commit_hash, + epoch_start_height: current_height, + }), + extensions: vec![], + signature: [0; 64], + }; + sign_activation(&mut activate, receiver); + hash = authenticated.validate_activation(&activate, commit_hash, current_height).unwrap(); + let activating = FFORReceiverActivation::prepare( + &setup, + &activate.encode().unwrap(), + commitments, + &monitor, + current_height, + ) + .unwrap(); + let mut recovery = receiver.node.ffor_recovery.lock().unwrap(); + let _requirement = receiver.node.ffor_persistence.lock().unwrap().request().unwrap(); + let insertion = recovery.prepare_activation(&setup, &activating).unwrap(); + channel + .install_ffor_fence_for_test(phase, hash, &monitor, current_height, &receiver.logger) + .unwrap(); + insertion.commit(); + if phase == FFORReceiverFencePhase::Active { + let mut ack = FFORMessage { + header: authenticated.header(), + payload: Payload::ActivateAck(hash), + extensions: vec![], + signature: [0; 64], + }; + sign_activation(&mut ack, sender); + let active = activating.with_ack(&setup, &ack.encode().unwrap()).unwrap(); + recovery.prepare_activation(&setup, &active).unwrap().commit(); + } + } + let token = receiver.node.capture_ffor_persistence(); + let _persisted = receiver.node.encode(); + receiver.node.ffor_persistence_completed(token).unwrap(); + hash +} + +/// Exercises the stock manager claim and monitor persistence path for a privately fenced voucher. +pub(crate) fn claim_ffor_preimage_for_test( + sender: &Node, receiver: &Node, channel_id: ChannelId, htlc_id: u64, preimage: PaymentPreimage, +) -> u64 { + let source = { + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get(&channel_id).unwrap().as_funded().unwrap(); + assert!(channel.ffor_receiver_fence().is_some()); + HTLCClaimSource { + counterparty_node_id: sender.node.get_our_node_id(), + funding_txo: channel.funding_outpoint(), + channel_id, + htlc_id, + } + }; + receiver.node.claim_mpp_part(source, preimage, None, None, |amount, duplicate| { + assert!(amount.is_some()); + assert!(!duplicate); + (None, None) + }); + get_monitor!(receiver, channel_id).get_latest_update_id() +} fn setup_messages(sender: &Node, receiver: &Node, channel_id: ChannelId) -> (Vec, Vec) { let voucher = FFORVoucher { @@ -93,6 +204,116 @@ fn drain_close_events(node: &Node) { node.chain_monitor.added_monitors.lock().unwrap().clear(); } +#[test] +fn ffor_activation_archive_real_channel_requires_fence_and_retains_closed_evidence() { + for phase in [FFORReceiverFencePhase::Activating, FFORReceiverFencePhase::Active] { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let (funder, peer) = if receiver_funds { (1, 0) } else { (0, 1) }; + let channel_id = create_announced_chan_between_nodes_with_value( + &nodes, funder, peer, 100_000, 40_000_000, + ) + .2; + let sender = &nodes[0]; + let receiver = &nodes[1]; + let sender_id = sender.node.get_our_node_id(); + let receiver_id = receiver.node.get_our_node_id(); + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let (init, accept) = ffor_setup_test_messages(sender, receiver, channel_id, voucher); + receiver + .node + .register_ffor_receiver_setup( + &channel_id, + &sender_id, + &init.encode().unwrap(), + &accept.encode().unwrap(), + 20, + ) + .unwrap(); + deliver_parked_voucher(sender, receiver, update); + let monitor = get_monitor!(receiver, channel_id).ffor_commitment_snapshot().unwrap(); + receiver + .node + .request_ffor_receiver_quiescence( + &channel_id, + &sender_id, + init.header.epoch_id, + monitor, + ) + .unwrap(); + let proposed = get_event_msg!(receiver, MessageSendEvent::SendStfu, sender_id); + sender.node.handle_stfu(receiver_id, &proposed); + let response = get_event_msg!(sender, MessageSendEvent::SendStfu, receiver_id); + receiver.node.handle_stfu(sender_id, &response); + let hash = install_ffor_activation_for_test(sender, receiver, channel_id, phase); + let key = FFORRecoveryKey { channel_id, epoch_id: init.header.epoch_id }; + let archive = receiver.node.ffor_recovery.lock().unwrap().encode(); + let monitor = get_monitor!(receiver, channel_id).encode(); + let restored = restore(receiver, &receiver.node.encode(), &monitor).unwrap(); + assert_eq!(restored.ffor_recovery.lock().unwrap().encode(), archive); + { + let peers = restored.per_peer_state.read().unwrap(); + let peer = peers.get(&sender_id).unwrap().lock().unwrap(); + assert_eq!( + peer.channel_by_id + .get(&channel_id) + .unwrap() + .as_funded() + .unwrap() + .ffor_receiver_fence(), + Some((phase, hash)) + ); + } + let retained = core::mem::replace( + &mut *receiver.node.ffor_recovery.lock().unwrap(), + FFORRecoveryRegistry::new(), + ); + assert!(restore(receiver, &receiver.node.encode(), &monitor).is_err()); + *receiver.node.ffor_recovery.lock().unwrap() = retained; + receiver + .node + .force_close_broadcasting_latest_txn( + &channel_id, + &sender_id, + "activation archive test".into(), + ) + .unwrap(); + drain_close_events(receiver); + let monitor = get_monitor!(receiver, channel_id).encode(); + let encoded = receiver.node.encode(); + let restored = restore(receiver, &encoded, &monitor).unwrap(); + assert!(restored.list_channels().is_empty()); + assert_eq!(restored.ffor_recovery.lock().unwrap().encode(), archive); + assert_eq!( + restored.ffor_recovery.lock().unwrap().get_activation(&key).unwrap().is_active(), + phase == FFORReceiverFencePhase::Active + ); + let without_monitor = <(BlockHash, TestChannelManager)>::read( + &mut &encoded[..], + ChannelManagerReadArgs::new( + receiver.keys_manager, + receiver.keys_manager, + receiver.keys_manager, + receiver.fee_estimator, + receiver.chain_monitor, + receiver.tx_broadcaster, + receiver.router, + receiver.message_router, + receiver.logger, + anchor_config(), + vec![], + ), + ); + assert!(without_monitor.is_err()); + } + } +} + #[test] fn ffor_recovery_explicit_and_monitor_closure_retain_setup_after_reload() { for monitor_closes in [false, true] { diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index 78e046aba33..3aa8d7bd282 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -288,6 +288,7 @@ pub struct FFORMonitorSnapshot { pub(crate) channel_id: ChannelId, pub(crate) funding_txo: OutPoint, pub(crate) update_id: u64, + pub(crate) destination_script: bitcoin::ScriptBuf, pub(crate) holder: HolderCommitmentTransaction, pub(crate) holder_number: u64, pub(crate) counterparty_txid: Txid, @@ -296,6 +297,20 @@ pub struct FFORMonitorSnapshot { pub(crate) revoked_through: u64, } +/// Internal recovery identity captured under one monitor lock, including after force-close. +/// This is distinct from a live activation snapshot and carries no readiness authority. +#[derive(Clone)] +pub(crate) struct FFORMonitorRecoveryIdentity { + pub(crate) channel_id: ChannelId, + pub(crate) funding_txo: OutPoint, + pub(crate) update_id: u64, + pub(crate) holder_number: u64, + pub(crate) holder_txid: Txid, + pub(crate) counterparty_number: u64, + pub(crate) counterparty_txid: Option, + pub(crate) destination_script: bitcoin::ScriptBuf, +} + pub(crate) fn validate_vouchers(vouchers: &[FFORVoucher]) -> Result<(), FFORCommitmentError> { let first = vouchers.first().ok_or(FFORCommitmentError::InvalidVoucherBook)?; if vouchers.len() > 483 { diff --git a/lightning/src/ln/ffor_recovery.rs b/lightning/src/ln/ffor_recovery.rs index 7edaec65988..a0f1861dcf6 100644 --- a/lightning/src/ln/ffor_recovery.rs +++ b/lightning/src/ln/ffor_recovery.rs @@ -4,25 +4,34 @@ //! TLV 22, under the same consistency lock as channel admission and persistence. Admission uses //! a borrowed insertion permit so capacity failure cannot leave a channel without its evidence. +mod activation; +pub(crate) use activation::FFORReceiverActivation; + use alloc::vec::Vec; use bitcoin::constants::ChainHash; use bitcoin::secp256k1::PublicKey; use crate::io; -use crate::ln::channel::FFORReceiverSetup; +use crate::ln::channel::{FFORReceiverFencePhase, FFORReceiverSetup}; +use crate::ln::ffor::FFORMonitorRecoveryIdentity; use crate::ln::msgs::DecodeError; use crate::ln::types::ChannelId; use crate::util::ser::{FixedLengthReader, Readable, Writeable, Writer}; -// A later format containing activation evidence must use a different required version. A -// setup-only reader must never silently discard evidence for a channel that may be active. const SETUP_ONLY_VERSION: u8 = 0; +// Setup-only readers must never silently discard a possibly active epoch's evidence. +const ACTIVATION_VERSION: u8 = 1; const MAX_RECORDS: usize = 64; const MAX_ENCODED_BYTES: usize = 8 * 1024 * 1024; // Two maximum wire messages, a 483-slot canonical book, and fixed admission fields fit here. -const MAX_RECORD_BYTES: usize = 192 * 1024; +const MAX_SETUP_RECORD_BYTES: usize = 192 * 1024; +// Four maximum signed messages, the book, destination script and admission context fit here. +const MAX_RECORD_BYTES: usize = 384 * 1024; const HEADER_BYTES: usize = 3; const RECORD_LENGTH_BYTES: usize = 4; +// Includes the maximum acknowledgement plus its vector/TLV framing and growth of the enclosing +// activation and record length prefixes. The fixed outer record prefix does not grow. +const ACK_RESERVATION_BYTES: usize = lightning_ffor::wire::MAX_MESSAGE_LEN + 64; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct FFORRecoveryKey { @@ -40,11 +49,13 @@ pub(crate) enum FFORRecoveryError { struct StoredSetup { setup: FFORReceiverSetup, canonical_book: Vec, + activation: Option, } impl_writeable_tlv_based!(StoredSetup, { (0, setup, required), (2, canonical_book, required_vec), + (4, activation, option), }); struct Entry { @@ -60,9 +71,15 @@ impl Entry { if record.canonical_book != authenticated.canonical_book() { return Err(FFORRecoveryError::InvalidRecord); } + if let Some(activation) = record.activation.as_ref() { + activation.validate(&authenticated).map_err(|_| FFORRecoveryError::InvalidRecord)?; + } let header = authenticated.header(); let encoded_bytes = record.serialized_length(); - if encoded_bytes > MAX_RECORD_BYTES { + let reserved_ack_bytes = Self::ack_reservation(&record); + if encoded_bytes.saturating_add(reserved_ack_bytes) > MAX_RECORD_BYTES + || (record.activation.is_none() && encoded_bytes > MAX_SETUP_RECORD_BYTES) + { return Err(FFORRecoveryError::CapacityExceeded); } Ok(Self { @@ -80,11 +97,25 @@ impl Entry { || (self.record.setup.chain_hash() == other.record.setup.chain_hash() && self.record.setup.funding_txo() == other.record.setup.funding_txo()) } + + fn ack_reservation(record: &StoredSetup) -> usize { + if record.activation.as_ref().map_or(false, |activation| !activation.is_active()) { + ACK_RESERVATION_BYTES + } else { + 0 + } + } + + fn reserved_ack_bytes(&self) -> usize { + Self::ack_reservation(&self.record) + } } pub(crate) struct FFORRecoveryRegistry { entries: Vec, encoded_bytes: usize, + // Derived from authenticated phases, never serialized or supplied by a caller. + reserved_ack_bytes: usize, } /// Exclusive capacity reservation. Dropping it changes nothing; commit cannot fail. @@ -97,14 +128,32 @@ impl FFORRecoveryInsertion<'_> { pub(crate) fn commit(self) { if let Some(entry) = self.entry { self.registry.encoded_bytes += RECORD_LENGTH_BYTES + entry.encoded_bytes; + self.registry.reserved_ack_bytes += entry.reserved_ack_bytes(); self.registry.entries.push(entry); } } } +/// An exact monotonic replacement with byte capacity reserved before channel mutation. +pub(crate) struct FFORRecoveryUpgrade<'a> { + registry: &'a mut FFORRecoveryRegistry, + index: usize, + entry: Entry, + total_bytes: usize, + reserved_ack_bytes: usize, +} + +impl FFORRecoveryUpgrade<'_> { + pub(crate) fn commit(self) { + self.registry.entries[self.index] = self.entry; + self.registry.encoded_bytes = self.total_bytes; + self.registry.reserved_ack_bytes = self.reserved_ack_bytes; + } +} + impl FFORRecoveryRegistry { pub(crate) fn new() -> Self { - Self { entries: Vec::new(), encoded_bytes: HEADER_BYTES } + Self { entries: Vec::new(), encoded_bytes: HEADER_BYTES, reserved_ack_bytes: 0 } } pub(crate) fn is_empty(&self) -> bool { @@ -115,6 +164,13 @@ impl FFORRecoveryRegistry { self.entries.iter().find(|entry| entry.key == *key).map(|entry| &entry.record.setup) } + pub(crate) fn get_activation(&self, key: &FFORRecoveryKey) -> Option<&FFORReceiverActivation> { + self.entries + .iter() + .find(|entry| entry.key == *key) + .and_then(|entry| entry.record.activation.as_ref()) + } + pub(crate) fn contains_exact(&self, record: &FFORReceiverSetup) -> bool { let authenticated = match record.validate_recovery() { Ok(authenticated) => authenticated, @@ -126,6 +182,51 @@ impl FFORRecoveryRegistry { self.get(&key).map_or(false, |stored| stored.encode() == record.encode()) } + /// Check every serialized live channel before it can be removed as stale during restore. + /// Neither a missing fence nor a missing acknowledgement may downgrade archived evidence. + pub(crate) fn validate_channel_fence( + &self, setup: &FFORReceiverSetup, fence: Option<(FFORReceiverFencePhase, [u8; 32])>, + ) -> Result<(), DecodeError> { + if !self.contains_exact(setup) { + return Err(DecodeError::InvalidValue); + } + let authenticated = setup.validate_recovery()?; + let header = authenticated.header(); + let key = + FFORRecoveryKey { channel_id: ChannelId(header.channel_id), epoch_id: header.epoch_id }; + match (self.get_activation(&key), fence) { + (None, None) => Ok(()), + (Some(record), Some((phase, hash))) + if record.validate(&authenticated)? == hash + && record.is_active() == (phase == FFORReceiverFencePhase::Active) => + { + Ok(()) + }, + _ => Err(DecodeError::InvalidValue), + } + } + + /// A possibly active epoch requires its original persisted monitor even without a live channel. + pub(crate) fn activation_channels(&self) -> Vec { + self.entries + .iter() + .filter(|entry| entry.record.activation.is_some()) + .map(|entry| entry.key.channel_id) + .collect() + } + + pub(crate) fn validate_activation_monitor( + &self, channel_id: ChannelId, monitor: &FFORMonitorRecoveryIdentity, + ) -> Result<(), DecodeError> { + let entry = self + .entries + .iter() + .find(|entry| entry.key.channel_id == channel_id) + .ok_or(DecodeError::InvalidValue)?; + let activation = entry.record.activation.as_ref().ok_or(DecodeError::InvalidValue)?; + activation.validate_monitor(&entry.record.setup, monitor) + } + /// Must be checked against the actual restoring manager, including archive-only snapshots. pub(crate) fn validate_identity( &self, receiver: PublicKey, chain_hash: ChainHash, @@ -150,6 +251,7 @@ impl FFORRecoveryRegistry { let entry = Entry::new(StoredSetup { setup: setup.clone(), canonical_book: authenticated.canonical_book().to_vec(), + activation: None, })?; if let Some(existing) = self.entries.iter().find(|existing| existing.has_same_identity(&entry)) @@ -159,15 +261,72 @@ impl FFORRecoveryRegistry { } return Ok(FFORRecoveryInsertion { registry: self, entry: None }); } - self.check_capacity(entry.encoded_bytes)?; + self.check_capacity(entry.encoded_bytes, entry.reserved_ack_bytes())?; Ok(FFORRecoveryInsertion { registry: self, entry: Some(entry) }) } - fn check_capacity(&self, record_bytes: usize) -> Result<(), FFORRecoveryError> { + /// Upgrade an existing identical setup to ACTIVATING, then add only its exact signed ack. + /// The caller must install the corresponding native fence while retaining this permit. + /// ACTIVATING reserves capacity for a maximum acknowledgement until its exact bytes arrive. + pub(crate) fn prepare_activation( + &mut self, setup: &FFORReceiverSetup, activation: &FFORReceiverActivation, + ) -> Result, FFORRecoveryError> { + let authenticated = + setup.validate_recovery().map_err(|_| FFORRecoveryError::InvalidRecord)?; + let entry = Entry::new(StoredSetup { + setup: setup.clone(), + canonical_book: authenticated.canonical_book().to_vec(), + activation: Some(activation.clone()), + })?; + let index = self + .entries + .iter() + .position(|stored| stored.key == entry.key) + .ok_or(FFORRecoveryError::ConflictingRecord)?; + let existing = &self.entries[index]; + if existing.record.setup.encode() != setup.encode() + || match existing.record.activation.as_ref() { + Some(previous) => !previous.can_replace(activation), + None => activation.is_active(), + } { + return Err(FFORRecoveryError::ConflictingRecord); + } + let total_bytes = self + .encoded_bytes + .checked_sub(existing.encoded_bytes) + .and_then(|bytes| bytes.checked_add(entry.encoded_bytes)) + .ok_or(FFORRecoveryError::CapacityExceeded)?; + let reserved_ack_bytes = self + .reserved_ack_bytes + .checked_sub(existing.reserved_ack_bytes()) + .and_then(|bytes| bytes.checked_add(entry.reserved_ack_bytes())) + .ok_or(FFORRecoveryError::CapacityExceeded)?; + if total_bytes + .checked_add(reserved_ack_bytes) + .map_or(true, |bytes| bytes > MAX_ENCODED_BYTES) + { + return Err(FFORRecoveryError::CapacityExceeded); + } + Ok(FFORRecoveryUpgrade { registry: self, index, entry, total_bytes, reserved_ack_bytes }) + } + + fn version(&self) -> u8 { + if self.entries.iter().any(|entry| entry.record.activation.is_some()) { + ACTIVATION_VERSION + } else { + SETUP_ONLY_VERSION + } + } + + fn check_capacity( + &self, record_bytes: usize, reserved_ack_bytes: usize, + ) -> Result<(), FFORRecoveryError> { let total = self .encoded_bytes .checked_add(RECORD_LENGTH_BYTES) - .and_then(|total| total.checked_add(record_bytes)); + .and_then(|total| total.checked_add(record_bytes)) + .and_then(|total| total.checked_add(self.reserved_ack_bytes)) + .and_then(|total| total.checked_add(reserved_ack_bytes)); if self.entries.len() >= MAX_RECORDS || record_bytes > MAX_RECORD_BYTES || total.map_or(true, |total| total > MAX_ENCODED_BYTES) @@ -180,7 +339,7 @@ impl FFORRecoveryRegistry { impl Writeable for FFORRecoveryRegistry { fn write(&self, writer: &mut W) -> Result<(), io::Error> { - SETUP_ONLY_VERSION.write(writer)?; + self.version().write(writer)?; (self.entries.len() as u16).write(writer)?; for entry in &self.entries { (entry.encoded_bytes as u32).write(writer)?; @@ -192,7 +351,8 @@ impl Writeable for FFORRecoveryRegistry { impl Readable for FFORRecoveryRegistry { fn read(reader: &mut R) -> Result { - if u8::read(reader)? != SETUP_ONLY_VERSION { + let version = u8::read(reader)?; + if version != SETUP_ONLY_VERSION && version != ACTIVATION_VERSION { return Err(DecodeError::UnknownRequiredFeature); } let count = u16::read(reader)? as usize; @@ -202,7 +362,10 @@ impl Readable for FFORRecoveryRegistry { let mut registry = Self::new(); for _ in 0..count { let length = u32::read(reader)? as usize; - registry.check_capacity(length).map_err(|_| DecodeError::InvalidValue)?; + if version == SETUP_ONLY_VERSION && length > MAX_SETUP_RECORD_BYTES { + return Err(DecodeError::InvalidValue); + } + registry.check_capacity(length, 0).map_err(|_| DecodeError::InvalidValue)?; let mut bounded = FixedLengthReader::new(reader, length as u64); let record = StoredSetup::read(&mut bounded)?; if bounded.bytes_remain() { @@ -216,9 +379,16 @@ impl Readable for FFORRecoveryRegistry { { return Err(DecodeError::InvalidValue); } + registry + .check_capacity(length, entry.reserved_ack_bytes()) + .map_err(|_| DecodeError::InvalidValue)?; registry.encoded_bytes += RECORD_LENGTH_BYTES + length; + registry.reserved_ack_bytes += entry.reserved_ack_bytes(); registry.entries.push(entry); } + if registry.version() != version { + return Err(DecodeError::InvalidValue); + } Ok(registry) } } diff --git a/lightning/src/ln/ffor_recovery/activation.rs b/lightning/src/ln/ffor_recovery/activation.rs new file mode 100644 index 00000000000..8e036908653 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation.rs @@ -0,0 +1,182 @@ +//! Exact receiver activation evidence retained independently of a live channel. +//! +//! These records authenticate a historical transcript, not current channel authority. The +//! manager must install the matching channel fence and registry upgrade atomically, then await +//! persistence before releasing wire. No production activation entry point exists yet. + +use alloc::vec::Vec; +use bitcoin::hashes::Hash; +use bitcoin::{ScriptBuf, Txid}; +use lightning_ffor::setup::AuthenticatedSetup; +use lightning_ffor::transcript; +use lightning_ffor::wire::{Message, Payload, MAX_MESSAGE_LEN}; + +use crate::ln::channel::{FFORReceiverSetup, INITIAL_COMMITMENT_NUMBER}; +use crate::ln::ffor::{FFORMonitorRecoveryIdentity, FFORMonitorSnapshot, FFORVoucherCommitments}; +use crate::ln::msgs::DecodeError; +use crate::util::ser::Writeable; + +/// This copy binds the activation transcript to the monitor's identity and recovery destination. +/// The complete persisted ChannelMonitor remains required for signatures and on-chain claims. +#[derive(Clone)] +pub(crate) struct FFORReceiverActivation { + activate_wire: Vec, + ack_wire: Option>, + receiver_number: u64, + receiver_txid: Txid, + settlement_number: u64, + settlement_txid: Txid, + monitor_update_id: u64, + preparation_height: u32, + destination_script: ScriptBuf, +} + +impl_writeable_tlv_based!(FFORReceiverActivation, { + (0, activate_wire, required_vec), + (2, ack_wire, option), + (4, receiver_number, required), + (6, receiver_txid, required), + (8, settlement_number, required), + (10, settlement_txid, required), + (12, monitor_update_id, required), + (14, preparation_height, required), + (16, destination_script, required), +}); + +impl FFORReceiverActivation { + /// Called only after the channel rechecks both commitment views under owned quiescence. + /// The opaque monitor supplies its original destination; callers cannot substitute a script. + /// This does not prove that the manager write or monitor persistence has completed. + pub(crate) fn prepare( + setup: &FFORReceiverSetup, activate_wire: &[u8], commitments: FFORVoucherCommitments, + monitor: &FFORMonitorSnapshot, current_height: u32, + ) -> Result { + let authenticated = setup.validate_recovery()?; + if monitor.channel_id.0 != authenticated.header().channel_id + || monitor.funding_txo != setup.funding_txo() + || INITIAL_COMMITMENT_NUMBER.checked_sub(monitor.holder_number) + != Some(commitments.holder.number) + || INITIAL_COMMITMENT_NUMBER.checked_sub(monitor.counterparty_number) + != Some(commitments.counterparty.number) + || monitor.holder.trust().txid() != commitments.holder.txid + || monitor.counterparty_txid != commitments.counterparty.txid + || activate_wire.len() > MAX_MESSAGE_LEN + { + return Err(DecodeError::InvalidValue); + } + let record = Self { + activate_wire: activate_wire.to_vec(), + ack_wire: None, + receiver_number: commitments.holder.number, + receiver_txid: commitments.holder.txid, + settlement_number: commitments.counterparty.number, + settlement_txid: commitments.counterparty.txid, + monitor_update_id: monitor.update_id, + preparation_height: current_height, + destination_script: monitor.destination_script.clone(), + }; + record.validate(&authenticated)?; + Ok(record) + } + + /// Reauthenticate historical bytes against the archived setup. Live height checks belong to + /// the current transition; replaying persisted evidence must not expire its recovery record. + pub(super) fn validate(&self, setup: &AuthenticatedSetup) -> Result<[u8; 32], DecodeError> { + let starting_settlement_number = match &setup.accept().payload { + Payload::Accept(accept) => accept.s_commitment_number, + _ => return Err(DecodeError::InvalidValue), + }; + if self.activate_wire.len() > MAX_MESSAGE_LEN + || self.ack_wire.as_ref().map_or(false, |wire| wire.len() > MAX_MESSAGE_LEN) + || self.receiver_number == 0 + || self.receiver_number > INITIAL_COMMITMENT_NUMBER + || self.settlement_number <= starting_settlement_number + || self.settlement_number > INITIAL_COMMITMENT_NUMBER + || self.monitor_update_id == 0 + || self.monitor_update_id == u64::MAX + || self.destination_script.is_empty() + || self.destination_script.len() > 10_000 + { + return Err(DecodeError::InvalidValue); + } + let activate = + Message::decode(&self.activate_wire).map_err(|_| DecodeError::InvalidValue)?; + if !matches!(&activate.payload, Payload::Activate(message) + if message.epoch_start_height == self.preparation_height) + { + return Err(DecodeError::InvalidValue); + } + let hash = setup + .validate_activation(&activate, self.commitment_hash(), self.preparation_height) + .map_err(|_| DecodeError::InvalidValue)?; + if let Some(wire) = self.ack_wire.as_ref() { + let ack = Message::decode(wire).map_err(|_| DecodeError::InvalidValue)?; + setup.validate_activation_ack(&ack, hash).map_err(|_| DecodeError::InvalidValue)?; + } + Ok(hash) + } + + pub(crate) fn commitment_hash(&self) -> [u8; 32] { + transcript::commitment_hash( + self.receiver_number, + &self.receiver_txid.to_byte_array(), + self.settlement_number, + &self.settlement_txid.to_byte_array(), + ) + } + + /// Preserve the exact activation and accept only the first authentic acknowledgement. + /// Exact replay is idempotent; a different signed encoding cannot replace retained evidence. + pub(crate) fn with_ack( + &self, setup: &FFORReceiverSetup, ack_wire: &[u8], + ) -> Result { + if ack_wire.len() > MAX_MESSAGE_LEN + || self.ack_wire.as_ref().map_or(false, |existing| existing != ack_wire) + { + return Err(DecodeError::InvalidValue); + } + let mut record = self.clone(); + record.ack_wire = Some(ack_wire.to_vec()); + record.validate(&setup.validate_recovery()?)?; + Ok(record) + } + + pub(crate) fn is_active(&self) -> bool { + self.ack_wire.is_some() + } + + pub(super) fn validate_monitor( + &self, setup: &FFORReceiverSetup, monitor: &FFORMonitorRecoveryIdentity, + ) -> Result<(), DecodeError> { + if monitor.channel_id.0 != setup.validate_recovery()?.header().channel_id + || monitor.funding_txo != setup.funding_txo() + || monitor.update_id < self.monitor_update_id + || INITIAL_COMMITMENT_NUMBER.checked_sub(monitor.holder_number) + != Some(self.receiver_number) + || INITIAL_COMMITMENT_NUMBER.checked_sub(monitor.counterparty_number) + != Some(self.settlement_number) + || monitor.holder_txid != self.receiver_txid + || monitor.counterparty_txid != Some(self.settlement_txid) + || monitor.destination_script != self.destination_script + { + return Err(DecodeError::InvalidValue); + } + Ok(()) + } + + /// The only upgrade preserves every byte except adding the first acknowledgement. + pub(super) fn can_replace(&self, next: &Self) -> bool { + if self.encode() == next.encode() { + return true; + } + if self.ack_wire.is_some() || next.ack_wire.is_none() { + return false; + } + let mut without_ack = next.clone(); + without_ack.ack_wire = None; + self.encode() == without_ack.encode() + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/ffor_recovery/activation/tests.rs b/lightning/src/ln/ffor_recovery/activation/tests.rs new file mode 100644 index 00000000000..8c7d3a169e8 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/tests.rs @@ -0,0 +1,497 @@ +use super::*; +use crate::ln::ffor_recovery::tests::{fixture, sign}; +use crate::ln::ffor_recovery::{ + FFORRecoveryError, FFORRecoveryKey, FFORRecoveryRegistry, ACK_RESERVATION_BYTES, + ACTIVATION_VERSION, MAX_ENCODED_BYTES, MAX_RECORDS, SETUP_ONLY_VERSION, +}; +use crate::ln::types::ChannelId; +use crate::util::ser::Readable; +use lightning_ffor::wire::{Activate, Tlv}; + +// Synthetic transaction identities exercise archive authentication only. Real monitor and +// channel evidence are exercised by the manager tests; these values never activate a channel. +fn evidence(maximum_size: bool) -> (FFORReceiverSetup, FFORReceiverActivation) { + evidence_for_identity(71, maximum_size) +} + +fn evidence_for_identity( + identity: u8, maximum_size: bool, +) -> (FFORReceiverSetup, FFORReceiverActivation) { + let setup = fixture(identity, maximum_size).record(); + let authenticated = setup.validate_recovery().unwrap(); + let receiver_txid = Txid::from_byte_array(core::array::from_fn(|i| i as u8)); + let settlement_txid = Txid::from_byte_array(core::array::from_fn(|i| (i + 32) as u8)); + let mut message = Message { + header: authenticated.header(), + payload: Payload::Activate(Activate { + setup_hash: authenticated.setup_hash(), + book_hash: authenticated.book_hash(), + commit_hash: transcript::commitment_hash( + 4, + &receiver_txid.to_byte_array(), + 4, + &settlement_txid.to_byte_array(), + ), + epoch_start_height: 500, + }), + extensions: vec![Tlv { kind: 101, value: vec![9, 2, 3] }], + signature: [0; 64], + }; + sign(&mut message, 41); + if maximum_size { + message.extensions.clear(); + sign(&mut message, 41); + let padding = MAX_MESSAGE_LEN - message.encode().unwrap().len() - 4; + message.extensions.push(Tlv { kind: 101, value: vec![7; padding] }); + sign(&mut message, 41); + assert_eq!(message.encode().unwrap().len(), MAX_MESSAGE_LEN); + } + let record = FFORReceiverActivation { + activate_wire: message.encode().unwrap(), + ack_wire: None, + receiver_number: 4, + receiver_txid, + settlement_number: 4, + settlement_txid, + monitor_update_id: 5, + preparation_height: 500, + destination_script: ScriptBuf::from_bytes(vec![0, 20].into_iter().chain([3; 20]).collect()), + }; + assert!(record.validate(&authenticated).is_ok()); + (setup, record) +} + +fn acknowledgement( + setup: &FFORReceiverSetup, record: &FFORReceiverActivation, maximum: bool, +) -> Vec { + let authenticated = setup.validate_recovery().unwrap(); + let mut message = Message { + header: authenticated.header(), + payload: Payload::ActivateAck(record.validate(&authenticated).unwrap()), + extensions: vec![], + signature: [0; 64], + }; + sign(&mut message, 42); + if maximum { + let padding = MAX_MESSAGE_LEN - message.encode().unwrap().len() - 4; + message.extensions.push(Tlv { kind: 101, value: vec![8; padding] }); + sign(&mut message, 42); + assert_eq!(message.encode().unwrap().len(), MAX_MESSAGE_LEN); + } + message.encode().unwrap() +} + +fn registered(setup: &FFORReceiverSetup) -> FFORRecoveryRegistry { + let mut registry = FFORRecoveryRegistry::new(); + registry.prepare_insert(setup).unwrap().commit(); + registry +} + +fn key(setup: &FFORReceiverSetup) -> FFORRecoveryKey { + let header = setup.validate_recovery().unwrap().header(); + FFORRecoveryKey { channel_id: ChannelId(header.channel_id), epoch_id: header.epoch_id } +} + +#[test] +fn ffor_activation_archive_roundtrip_preserves_exact_maximum_transcripts() { + let (setup, activating) = evidence(true); + let ack = acknowledgement(&setup, &activating, true); + let active = activating.with_ack(&setup, &ack).unwrap(); + let mut registry = registered(&setup); + assert_eq!(registry.encode()[0], SETUP_ONLY_VERSION); + registry.prepare_activation(&setup, &activating).unwrap().commit(); + for expected in [&activating, &active] { + registry.prepare_activation(&setup, expected).unwrap().commit(); + let bytes = registry.encode(); + assert_eq!(bytes[0], ACTIVATION_VERSION); + assert_eq!(bytes.len(), registry.encoded_bytes); + let restored = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); + assert_eq!(restored.encode(), bytes); + let reservation = if expected.is_active() { 0 } else { ACK_RESERVATION_BYTES }; + assert_eq!(registry.reserved_ack_bytes, reservation); + assert_eq!(restored.reserved_ack_bytes, reservation); + assert_eq!(restored.get_activation(&key(&setup)).unwrap().encode(), expected.encode()); + assert!(restored.contains_exact(&setup)); + } +} + +#[test] +fn ffor_activation_archive_rejects_signature_role_identity_and_commitment_substitution() { + let (setup, original) = evidence(false); + let authenticated = setup.validate_recovery().unwrap(); + for change in 0..10 { + let mut record = original.clone(); + let mut message = Message::decode(&record.activate_wire).unwrap(); + match change { + 0 => message.header.epoch_id[0] ^= 1, + 1 => message.header.channel_id[0] ^= 1, + 2 => { + if let Payload::Activate(activate) = &mut message.payload { + activate.setup_hash[0] ^= 1 + } + }, + 3 => { + if let Payload::Activate(activate) = &mut message.payload { + activate.book_hash[0] ^= 1 + } + }, + 4 => { + if let Payload::Activate(activate) = &mut message.payload { + activate.commit_hash[0] ^= 1 + } + }, + 5 => record.receiver_number += 1, + 6 => record.settlement_number += 1, + 7 => record.receiver_txid = record.settlement_txid, + 8 => { + let mut displayed = record.receiver_txid.to_byte_array(); + displayed.reverse(); + record.receiver_txid = Txid::from_byte_array(displayed); + }, + _ => {}, + } + sign(&mut message, if change == 9 { 42 } else { 41 }); + record.activate_wire = message.encode().unwrap(); + assert!(record.validate(&authenticated).is_err(), "accepted change {}", change); + } +} + +#[test] +fn ffor_activation_archive_requires_historical_height_and_valid_recovery_context() { + let (setup, original) = evidence(false); + let authenticated = setup.validate_recovery().unwrap(); + for change in 0..8 { + let mut record = original.clone(); + match change { + 0 => record.preparation_height += 1, + 1 => record.receiver_number = 0, + 2 => record.settlement_number = 3, + 3 => record.settlement_number = INITIAL_COMMITMENT_NUMBER + 1, + 4 => record.monitor_update_id = 0, + 5 => record.monitor_update_id = u64::MAX, + 6 => record.destination_script = ScriptBuf::new(), + _ => record.destination_script = ScriptBuf::from_bytes(vec![1; 10_001]), + } + assert!(record.validate(&authenticated).is_err(), "accepted context change {}", change); + } + let mut expired = original.clone(); + let mut message = Message::decode(&expired.activate_wire).unwrap(); + if let Payload::Activate(activate) = &mut message.payload { + activate.epoch_start_height = 1000; + } + sign(&mut message, 41); + expired.activate_wire = message.encode().unwrap(); + expired.preparation_height = 1000; + assert!(expired.validate(&authenticated).is_err()); +} + +#[test] +fn ffor_activation_archive_acknowledgement_is_exact_authenticated_and_monotonic() { + let (setup, activating) = evidence(false); + let ack = acknowledgement(&setup, &activating, false); + let active = activating.with_ack(&setup, &ack).unwrap(); + assert!(!activating.is_active()); + assert!(active.is_active()); + assert!(activating.can_replace(&active)); + assert!(!active.can_replace(&activating)); + assert_eq!(active.with_ack(&setup, &ack).unwrap().encode(), active.encode()); + for change in 0..4 { + let mut changed = Message::decode(&ack).unwrap(); + match change { + 0 => changed.header.epoch_id[0] ^= 1, + 1 => { + if let Payload::ActivateAck(hash) = &mut changed.payload { + hash[0] ^= 1 + } + }, + 2 => {}, + _ => changed.extensions.push(Tlv { kind: 101, value: vec![1] }), + } + sign(&mut changed, if change == 2 { 41 } else { 42 }); + let bytes = changed.encode().unwrap(); + if change < 3 { + assert!(activating.with_ack(&setup, &bytes).is_err()); + } + assert!(active.with_ack(&setup, &bytes).is_err()); + } +} + +#[test] +fn ffor_activation_archive_upgrade_is_atomic_idempotent_and_capacity_bounded() { + let (setup, activating) = evidence(false); + let active = activating.with_ack(&setup, &acknowledgement(&setup, &activating, false)).unwrap(); + let mut registry = registered(&setup); + let original = registry.encode(); + assert!(matches!( + registry.prepare_activation(&setup, &active), + Err(FFORRecoveryError::ConflictingRecord) + )); + drop(registry.prepare_activation(&setup, &activating).unwrap()); + assert_eq!(registry.encode(), original); + assert_eq!(registry.reserved_ack_bytes, 0); + registry.reserved_ack_bytes = MAX_ENCODED_BYTES - registry.encoded_bytes; + assert!(matches!( + registry.prepare_activation(&setup, &activating), + Err(FFORRecoveryError::CapacityExceeded) + )); + assert_eq!(registry.encode(), original); + assert_eq!(registry.reserved_ack_bytes, MAX_ENCODED_BYTES - registry.encoded_bytes); + registry.reserved_ack_bytes = 0; + registry.prepare_activation(&setup, &activating).unwrap().commit(); + let pending = registry.encode(); + registry.prepare_activation(&setup, &activating).unwrap().commit(); + assert_eq!(registry.encode(), pending); + assert_eq!(registry.reserved_ack_bytes, ACK_RESERVATION_BYTES); + drop(registry.prepare_activation(&setup, &active).unwrap()); + assert_eq!(registry.encode(), pending); + assert_eq!(registry.reserved_ack_bytes, ACK_RESERVATION_BYTES); + registry.prepare_activation(&setup, &active).unwrap().commit(); + let completed = registry.encode(); + assert_eq!(registry.reserved_ack_bytes, 0); + assert!(completed.len() - pending.len() < ACK_RESERVATION_BYTES); + registry.prepare_activation(&setup, &active).unwrap().commit(); + assert_eq!(registry.encode(), completed); + assert!(matches!( + registry.prepare_activation(&setup, &activating), + Err(FFORRecoveryError::ConflictingRecord) + )); + assert_eq!(registry.encode(), completed); + assert_eq!(registry.reserved_ack_bytes, 0); +} + +// Fill with real authenticated records until a further activation cannot reserve its ack. Large +// records reach the byte quota well before the count quota; small ones consume the remainder. +fn fill_ack_capacity(registry: &mut FFORRecoveryRegistry) { + let mut maximum_size = true; + for identity in 1..MAX_RECORDS as u8 { + let (setup, activating) = evidence_for_identity(identity, maximum_size); + let inserted = match registry.prepare_insert(&setup) { + Ok(permit) => { + permit.commit(); + true + }, + Err(FFORRecoveryError::CapacityExceeded) => false, + Err(other) => panic!("unexpected insertion error: {:?}", other), + }; + let activated = inserted + && match registry.prepare_activation(&setup, &activating) { + Ok(permit) => { + permit.commit(); + true + }, + Err(FFORRecoveryError::CapacityExceeded) => false, + Err(other) => panic!("unexpected activation error: {:?}", other), + }; + if !activated { + if !maximum_size { + return; + } + maximum_size = false; + } + } + panic!("record count exhausted before acknowledgement capacity"); +} + +#[test] +fn ffor_activation_archive_reserves_maximum_ack_through_competing_admission_and_restart() { + let (setup, activating) = evidence(false); + let (competing_setup, competing_activation) = evidence_for_identity(72, false); + let mut registry = registered(&setup); + registry.prepare_insert(&competing_setup).unwrap().commit(); + registry.prepare_activation(&setup, &activating).unwrap().commit(); + fill_ack_capacity(&mut registry); + assert!(registry.entries.len() < MAX_RECORDS); + let before = registry.encode(); + let reserved = registry.reserved_ack_bytes; + assert!(matches!( + registry.prepare_activation(&competing_setup, &competing_activation), + Err(FFORRecoveryError::CapacityExceeded) + )); + let competing_record = fixture(90, true).record(); + // Its actual bytes would fit, but they must not consume the admitted acknowledgements. + assert!(registry.encoded_bytes + competing_record.encode().len() < MAX_ENCODED_BYTES); + assert!(matches!( + registry.prepare_insert(&competing_record), + Err(FFORRecoveryError::CapacityExceeded) + )); + assert_eq!(registry.encode(), before); + assert_eq!(registry.reserved_ack_bytes, reserved); + let mut restored = FFORRecoveryRegistry::read(&mut &before[..]).unwrap(); + assert_eq!(restored.reserved_ack_bytes, reserved); + assert_eq!(restored.encoded_bytes, before.len()); + assert_eq!(restored.encode(), before); + restored.prepare_activation(&setup, &activating).unwrap().commit(); + assert_eq!(restored.reserved_ack_bytes, reserved); + let maximum_ack = acknowledgement(&setup, &activating, true); + assert_eq!(maximum_ack.len(), MAX_MESSAGE_LEN); + let active = activating.with_ack(&setup, &maximum_ack).unwrap(); + drop(restored.prepare_activation(&setup, &active).unwrap()); + assert_eq!(restored.encode(), before); + assert_eq!(restored.reserved_ack_bytes, reserved); + restored.prepare_activation(&setup, &active).unwrap().commit(); + assert_eq!(restored.reserved_ack_bytes, reserved - ACK_RESERVATION_BYTES); + assert!(restored.encoded_bytes - before.len() < ACK_RESERVATION_BYTES); + assert!(restored.encoded_bytes + restored.reserved_ack_bytes <= MAX_ENCODED_BYTES); + assert_eq!( + restored.get_activation(&key(&setup)).unwrap().ack_wire.as_ref(), + Some(&maximum_ack) + ); + let completed = restored.encode(); + restored.prepare_activation(&setup, &active).unwrap().commit(); + assert_eq!(restored.encode(), completed); + let roundtrip = FFORRecoveryRegistry::read(&mut &completed[..]).unwrap(); + assert_eq!(roundtrip.reserved_ack_bytes, restored.reserved_ack_bytes); + + // A snapshot cannot bypass the reservation by declaring only its smaller current wire size. + let (extra_setup, extra_activation) = evidence_for_identity(91, true); + let extra = crate::ln::ffor_recovery::Entry::new(crate::ln::ffor_recovery::StoredSetup { + canonical_book: extra_setup.validate_recovery().unwrap().canonical_book().to_vec(), + setup: extra_setup, + activation: Some(extra_activation), + }) + .unwrap(); + assert!(restored.encoded_bytes + extra.encoded_bytes + 4 < MAX_ENCODED_BYTES); + assert!( + restored.encoded_bytes + + restored.reserved_ack_bytes + + extra.encoded_bytes + + extra.reserved_ack_bytes() + + 4 > MAX_ENCODED_BYTES + ); + restored.entries.push(extra); + let overcommitted = restored.encode(); + assert!(overcommitted.len() < MAX_ENCODED_BYTES); + assert!(matches!( + FFORRecoveryRegistry::read(&mut &overcommitted[..]), + Err(DecodeError::InvalidValue) + )); +} + +#[test] +fn ffor_activation_archive_requires_registered_setup_and_refuses_alternate_activation() { + let (setup, activating) = evidence(false); + assert!(matches!( + FFORRecoveryRegistry::new().prepare_activation(&setup, &activating), + Err(FFORRecoveryError::ConflictingRecord) + )); + let mut registry = registered(&setup); + registry.prepare_activation(&setup, &activating).unwrap().commit(); + let original = registry.encode(); + let mut alternative = activating.clone(); + let mut message = Message::decode(&alternative.activate_wire).unwrap(); + message.extensions[0].value[0] ^= 1; + sign(&mut message, 41); + alternative.activate_wire = message.encode().unwrap(); + assert!(alternative.validate(&setup.validate_recovery().unwrap()).is_ok()); + assert!(matches!( + registry.prepare_activation(&setup, &alternative), + Err(FFORRecoveryError::ConflictingRecord) + )); + assert_eq!(registry.encode(), original); +} + +#[test] +fn ffor_activation_archive_reader_rejects_downgrades_truncation_and_invalid_evidence() { + let (setup, activating) = evidence(false); + let mut registry = registered(&setup); + registry.prepare_activation(&setup, &activating).unwrap().commit(); + let bytes = registry.encode(); + for length in 0..bytes.len() { + assert!(FFORRecoveryRegistry::read(&mut &bytes[..length]).is_err()); + } + let mut downgraded = bytes.clone(); + downgraded[0] = SETUP_ONLY_VERSION; + assert!(FFORRecoveryRegistry::read(&mut &downgraded[..]).is_err()); + let mut missing = registered(&setup).encode(); + missing[0] = ACTIVATION_VERSION; + assert!(FFORRecoveryRegistry::read(&mut &missing[..]).is_err()); + // Keep all framing valid while corrupting only the retained signature. + let record = registry.entries[0].record.activation.as_mut().unwrap(); + let last = record.activate_wire.len() - 1; + record.activate_wire[last] ^= 1; + let invalid = registry.encode(); + assert!(FFORRecoveryRegistry::read(&mut &invalid[..]).is_err()); +} + +#[test] +fn ffor_activation_archive_requires_exact_live_phase_and_activation_hash() { + let (setup, activating) = evidence(false); + let authenticated = setup.validate_recovery().unwrap(); + let hash = activating.validate(&authenticated).unwrap(); + let active = activating.with_ack(&setup, &acknowledgement(&setup, &activating, false)).unwrap(); + let mut registry = registered(&setup); + assert!(registry.validate_channel_fence(&setup, None).is_ok()); + assert!(registry + .validate_channel_fence( + &setup, + Some((crate::ln::channel::FFORReceiverFencePhase::Activating, hash)) + ) + .is_err()); + for record in [&activating, &active] { + registry.prepare_activation(&setup, record).unwrap().commit(); + let phase = if record.is_active() { + crate::ln::channel::FFORReceiverFencePhase::Active + } else { + crate::ln::channel::FFORReceiverFencePhase::Activating + }; + let wrong_phase = if record.is_active() { + crate::ln::channel::FFORReceiverFencePhase::Activating + } else { + crate::ln::channel::FFORReceiverFencePhase::Active + }; + assert!(registry.validate_channel_fence(&setup, Some((phase, hash))).is_ok()); + assert!(registry.validate_channel_fence(&setup, None).is_err()); + assert!(registry.validate_channel_fence(&setup, Some((wrong_phase, hash))).is_err()); + let mut changed = hash; + changed[0] ^= 1; + assert!(registry.validate_channel_fence(&setup, Some((phase, changed))).is_err()); + } + assert_eq!(registry.activation_channels(), vec![ChannelId(authenticated.header().channel_id)]); +} + +#[test] +fn ffor_activation_archive_matches_monitor_destination_and_both_commitment_views() { + let (setup, activating) = evidence(false); + let identity = FFORMonitorRecoveryIdentity { + channel_id: key(&setup).channel_id, + funding_txo: setup.funding_txo(), + update_id: activating.monitor_update_id, + holder_number: INITIAL_COMMITMENT_NUMBER - activating.receiver_number, + holder_txid: activating.receiver_txid, + counterparty_number: INITIAL_COMMITMENT_NUMBER - activating.settlement_number, + counterparty_txid: Some(activating.settlement_txid), + destination_script: activating.destination_script.clone(), + }; + let mut registry = registered(&setup); + registry.prepare_activation(&setup, &activating).unwrap().commit(); + let channel_id = identity.channel_id; + assert!(registry.validate_activation_monitor(channel_id, &identity).is_ok()); + let mut advanced = identity.clone(); + advanced.update_id += 2; // Preimage and force-close writes retain the same commitment pair. + assert!(registry.validate_activation_monitor(channel_id, &advanced).is_ok()); + for change in 0..9 { + let mut changed = identity.clone(); + match change { + 0 => changed.channel_id.0[0] ^= 1, + 1 => changed.funding_txo.index += 1, + 2 => changed.update_id -= 1, + 3 => changed.holder_number -= 1, + 4 => changed.counterparty_number -= 1, + 5 => changed.holder_txid = activating.settlement_txid, + 6 => changed.counterparty_txid = Some(activating.receiver_txid), + 7 => changed.counterparty_txid = None, + _ => changed.destination_script = ScriptBuf::from_bytes(vec![81]), + } + assert!( + registry.validate_activation_monitor(channel_id, &changed).is_err(), + "accepted monitor change {}", + change + ); + } + // A well-formed but substituted destination in the archive must also be detected by the + // real monitor reference, even though scripts are not covered by the FFOR transcript hash. + let mut changed = activating.clone(); + changed.destination_script = ScriptBuf::from_bytes(vec![81]); + assert!(changed.validate(&setup.validate_recovery().unwrap()).is_ok()); + assert!(changed.validate_monitor(&setup, &identity).is_err()); +} diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs index 64db91a97fa..2e36f6e3fb3 100644 --- a/lightning/src/ln/ffor_recovery/tests.rs +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -7,7 +7,7 @@ use lightning_ffor::wire::{Accept, Header, Init, Message, Payload, Tlv, MAX_MESS use crate::chain::transaction::OutPoint; -struct Fixture { +pub(super) struct Fixture { init_wire: Vec, accept_wire: Vec, receiver: PublicKey, @@ -38,7 +38,7 @@ impl_writeable_tlv_based!(Fixture, { }); impl Fixture { - fn record(&self) -> FFORReceiverSetup { + pub(super) fn record(&self) -> FFORReceiverSetup { FFORReceiverSetup::read(&mut &self.encode()[..]).unwrap() } } @@ -47,7 +47,7 @@ fn key(value: u8) -> PublicKey { PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[value; 32]).unwrap()) } -fn sign(message: &mut Message, value: u8) { +pub(super) fn sign(message: &mut Message, value: u8) { message.signature = Secp256k1::new() .sign_ecdsa( &SecpMessage::from_digest(message.signature_digest().unwrap()), @@ -68,7 +68,7 @@ fn message(header: Header, payload: Payload, signer: u8, maximum_size: bool) -> message } -fn fixture(identity: u8, maximum_size: bool) -> Fixture { +pub(super) fn fixture(identity: u8, maximum_size: bool) -> Fixture { let header = Header { channel_id: [identity; 32], epoch_id: [identity.wrapping_add(1); 32] }; let count = if maximum_size { 483 } else { 2 }; let init = message( @@ -124,7 +124,7 @@ fn fixture(identity: u8, maximum_size: bool) -> Fixture { fn stored(record: FFORReceiverSetup) -> StoredSetup { let book = record.validate_recovery().unwrap().canonical_book().to_vec(); - StoredSetup { setup: record, canonical_book: book } + StoredSetup { setup: record, canonical_book: book, activation: None } } fn frame(records: &[Vec]) -> Vec { @@ -166,6 +166,7 @@ fn ffor_recovery_roundtrip_retains_exact_signed_setup_and_native_identity() { assert_eq!(restored.encode(), encoded); assert!(restored.contains_exact(&record)); assert_eq!(restored.encoded_bytes, encoded.len()); + assert_eq!(restored.reserved_ack_bytes, 0); assert!(restored.validate_identity(record.receiver(), record.chain_hash()).is_ok()); assert_eq!( restored.validate_identity(key(43), record.chain_hash()), @@ -292,7 +293,7 @@ fn ffor_recovery_reader_refuses_future_schema_missing_fields_and_truncation() { assert!(decode(&framed[..length]).is_err(), "accepted prefix {}", length); } let mut future = framed.clone(); - future[0] = 1; + future[0] = 2; assert!(matches!(decode(&future), Err(DecodeError::UnknownRequiredFeature))); // Empty TLV records omit both mandatory fields. assert!(decode(&frame(&[vec![0]])).is_err()); @@ -311,6 +312,7 @@ fn ffor_recovery_reader_bounds_declared_counts_and_lengths_before_reading_record } let mut registry = FFORRecoveryRegistry::new(); registry.encoded_bytes = MAX_ENCODED_BYTES; - assert_eq!(registry.check_capacity(0), Err(FFORRecoveryError::CapacityExceeded)); - assert_eq!(registry.check_capacity(usize::MAX), Err(FFORRecoveryError::CapacityExceeded)); + assert_eq!(registry.check_capacity(0, 0), Err(FFORRecoveryError::CapacityExceeded)); + assert_eq!(registry.check_capacity(usize::MAX, 0), Err(FFORRecoveryError::CapacityExceeded)); + assert_eq!(registry.check_capacity(0, usize::MAX), Err(FFORRecoveryError::CapacityExceeded)); } diff --git a/lightning/src/ln/ffor_tests.rs b/lightning/src/ln/ffor_tests.rs index bc9c1c04257..65cf9dc4d47 100644 --- a/lightning/src/ln/ffor_tests.rs +++ b/lightning/src/ln/ffor_tests.rs @@ -9,7 +9,7 @@ use crate::util::config::UserConfig; use crate::util::ser::Writeable; use core::sync::atomic::Ordering; -mod quiescence; +pub(crate) mod quiescence; pub(super) fn anchor_config() -> UserConfig { let mut config = test_default_channel_config(); diff --git a/lightning/src/ln/ffor_tests/quiescence.rs b/lightning/src/ln/ffor_tests/quiescence.rs index 91a7998acf6..808699c03d0 100644 --- a/lightning/src/ln/ffor_tests/quiescence.rs +++ b/lightning/src/ln/ffor_tests/quiescence.rs @@ -3,7 +3,9 @@ use crate::ln::channel::{ffor_setup_test_messages, DISCONNECT_PEER_AWAITING_RESP const EPOCH: [u8; 32] = [81; 32]; -fn register_signed(sender: &Node, receiver: &Node, channel_id: ChannelId, voucher: FFORVoucher) { +pub(crate) fn register_signed( + sender: &Node, receiver: &Node, channel_id: ChannelId, voucher: FFORVoucher, +) { let (init, accept) = ffor_setup_test_messages(sender, receiver, channel_id, voucher); let requirement = receiver .node @@ -21,7 +23,9 @@ fn register_signed(sender: &Node, receiver: &Node, channel_id: ChannelId, vouche assert!(receiver.node.is_ffor_state_persisted(&requirement)); } -fn request(sender: &Node, receiver: &Node, channel_id: ChannelId) -> Result<(), FFORReceiverError> { +pub(crate) fn request( + sender: &Node, receiver: &Node, channel_id: ChannelId, +) -> Result<(), FFORReceiverError> { receiver.node.request_ffor_receiver_quiescence( &channel_id, &sender.node.get_our_node_id(), @@ -40,7 +44,7 @@ fn status( ) } -fn complete_handshake(sender: &Node, receiver: &Node) { +pub(crate) fn complete_handshake(sender: &Node, receiver: &Node) { let receiver_id = receiver.node.get_our_node_id(); let sender_id = sender.node.get_our_node_id(); let proposed = get_event_msg!(receiver, MessageSendEvent::SendStfu, sender_id); From 8da701be295f392195de9b935c91f071c5a30919 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 22:47:17 -0400 Subject: [PATCH 08/22] feat(ffor): persist receiver activation and reconnect recovery --- FFOR.md | 97 +++-- lightning/src/ln/channel.rs | 72 +--- lightning/src/ln/channel/ffor.rs | 9 +- lightning/src/ln/channel/ffor/activation.rs | 154 +++++++ lightning/src/ln/channel/ffor/fence.rs | 20 +- lightning/src/ln/channel/ffor/fence/tests.rs | 11 +- lightning/src/ln/channel/ffor/quiescence.rs | 10 + lightning/src/ln/channel/ffor/reestablish.rs | 309 +++++++++++++ .../src/ln/channel/ffor/reestablish/tests.rs | 372 ++++++++++++++++ lightning/src/ln/channelmanager.rs | 39 +- .../src/ln/channelmanager/ffor_activation.rs | 405 ++++++++++++++++++ .../ffor_activation/drain_tests.rs | 344 +++++++++++++++ .../ffor_activation/reestablish.rs | 128 ++++++ .../channelmanager/ffor_activation/tests.rs | 316 ++++++++++++++ .../ffor_activation/tests/reestablish.rs | 193 +++++++++ .../ln/channelmanager/ffor_recovery_tests.rs | 9 +- lightning/src/ln/ffor.rs | 3 + lightning/src/ln/ffor_recovery.rs | 82 +++- lightning/src/ln/ffor_recovery/activation.rs | 129 +++++- .../src/ln/ffor_recovery/activation/tests.rs | 3 + .../ffor_recovery/activation/tests/aborted.rs | 235 ++++++++++ lightning/src/ln/ffor_recovery/tests.rs | 2 +- lightning/src/ln/msgs.rs | 56 ++- .../src/ln/msgs/ffor_reestablish_tests.rs | 85 ++++ lightning/src/util/dyn_signer.rs | 2 + lightning/src/util/test_utils.rs | 11 + 26 files changed, 2970 insertions(+), 126 deletions(-) create mode 100644 lightning/src/ln/channel/ffor/activation.rs create mode 100644 lightning/src/ln/channel/ffor/reestablish.rs create mode 100644 lightning/src/ln/channel/ffor/reestablish/tests.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/reestablish.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/tests.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/tests/reestablish.rs create mode 100644 lightning/src/ln/ffor_recovery/activation/tests/aborted.rs create mode 100644 lightning/src/ln/msgs/ffor_reestablish_tests.rs diff --git a/FFOR.md b/FFOR.md index 906240bece6..602f9985da1 100644 --- a/FFOR.md +++ b/FFOR.md @@ -1,8 +1,9 @@ -# Experimental FFOR receiver setup, parking and activation guards +# Experimental FFOR receiver setup, activation and recovery This fork targets rust-lightning v0.2.5. The current APIs provide receiver voucher -parking and point-in-time verification of both commitment views for FFOR Variant D. -They do not provide an active offline receiving epoch. +parking and verification of both commitment views for FFOR Variant D. Private manager +transitions compose activation, reconnect and pre-active abort with ordered storage. +No production transport or invoice API exposes those transitions yet. `register_ffor_receiver_setup` authenticates exact signed init and accept messages against the manager's actual node identity, chain and current height. It checks the @@ -81,9 +82,11 @@ Restore reauthenticates records, checks the manager's identity and chain, and re every retained channel setup to match its archive entry. The registry is bounded to 64 records and 8 MiB total, with a 192 KiB setup-only record limit and a 384 KiB activation record limit. Capacity is reserved before channel mutation. An Activating -record also reserves the maximum acknowledgement size, including framing, so other -records cannot consume its eventual Active storage. This allowance is derived again -on restore and released when the acknowledgement is retained. The setup-only archive +record also reserves the larger of a maximum acknowledgement and a terminal abort, +including framing, so other records cannot consume that transition storage. This +allowance is derived again on restore and released when either outcome is retained. +A production close path must also reserve its required evidence before activation. +The setup-only archive version retains no activation or claim authority. Fully drained channel tombstones permit subsequent ordinary splicing while retaining the original funding context in their archive. @@ -94,19 +97,51 @@ preparation height and the monitor's original sweep destination. They reauthenti the historical transcript and permit only the first acknowledgement to be added. A versioned registry prevents setup-only readers from accepting activation evidence. Every retained channel fence must match its archive phase and activation hash, even -when the live channel will be discarded as stale. Restore also requires the original +when the live channel will be discarded as stale. For unresolved activation, restore also requires the original monitor and checks both commitment identities, its destination and the saved update floor. A preimage or force-close update may advance that floor without changing the frozen commitments. The complete monitor remains necessary for signatures and claims. -The private channel fence persists Activating or Active independently of stock STFU -flags. It blocks ordinary HTLC updates, commitment and revocation messages, fees, -splicing, cooperative close, signer retries and ordinary reconnect messages. Valid +The private channel fence persists Activating, Active or a pending pre-active abort +independently of stock STFU flags. It blocks ordinary HTLC updates, commitment and +revocation messages, fees, splicing, cooperative close and signer retries. Frozen +channels are also excluded from usable-channel listings and ordinary routing. Valid owned preimages still enter stock monitor persistence, including delayed writes and completion after channel removal. They cannot release an ordinary fulfill while the fence is held. Force-close remains available. A required even field makes older -channel readers refuse fenced state. Installation currently exists only in tests; -there is no production transition into these phases or recovery driver to leave them. +channel readers refuse fenced state. + +A crate-private manager owner derives and signs activation from the actual completed +owned STFU handshake and monitor proof. It installs the archive and fence under the +same consistency boundary, then releases the exact signed bytes at most once after +that snapshot is durable. Release rechecks the current phase, height and original +handshake. Its transport callback must atomically validate the authenticated +connection token with queue insertion, without network I/O or manager callbacks. +A signed acknowledgement advances the channel and archive together and requests a +new persistence requirement before Active can be reported. No public runtime invokes +these private transitions yet. + +The native reconnect codec carries Variant D TLV 55001 while retaining the ordinary +BOLT commitment and revocation counter checks. Frozen reconnect never retransmits +ordinary commitment traffic. A receiver still Activating can recover a lost signed +acknowledgement only after the settlement peer reports Active with the same epoch and +activation hash. An unsigned report alone cannot activate the receiver. Activation +bytes are never replayed across a disconnect or restart. Active conflicts retain the +fence for future resolution or force-close. + +Other pre-active reconnect outcomes create a permanent abort record and keep the +channel frozen until that record is durable. Only then can the private manager owner +release ordinary voucher drain. Known preimages remain ahead of failures, including +preimages waiting for monitor persistence. Terminal abort evidence survives drain and +later ordinary payments without requiring the original frozen commitment pair. It +permanently refuses acknowledgement replay or replacement activation. + +Outgoing reconnect reports use the existing per-peer queue. A report and the queue +suffix behind it wait until the latest retained phase is durable. The FFOR report is +refreshed at release while the original ordinary reconnect counters are retained. +Restored managers request a fresh persistence requirement and notify the background +processor before releasing reports. Disconnect discards connection-specific queued +reports, and a subsequent connection builds fresh ones. These are consistency checks, not an authenticated storage envelope. Arbitrarily deleting a mismatching add's ownership record after abort can make its nonreserved @@ -116,8 +151,8 @@ checks do not claim to detect every arbitrary alteration of local storage. Similarly, an archive without its original live channel cannot independently prove the historical funding context against arbitrary local storage alteration. -No feature bit, production wire activation, invoice readiness, authenticated -reconnect driver or automatic setup deadline is implemented here. The caller must abort a setup that times out. +No feature bit, production custom-message transport, invoice readiness or automatic +setup deadline is implemented here. The caller must abort a setup that times out. Ordinary channel updates can invalidate a previously returned `Parked` proof. `NodeSigner::sign_ffor_message` supplies the protocol's single-SHA256 `ffor/msg` @@ -165,22 +200,32 @@ quiescence and splice suites also pass with these channel changes. The private fence tests cover both phases and funding directions, restored pending state rejection, preimage retention, cleared STFU flags, signer and monitor callbacks, -reconnect refusal and force-close. Activation recovery tests use real committed +frozen reconnect handling and force-close. Activation recovery tests use real committed channels and signed transcripts, exercise retained evidence after closure, and reject missing monitors, mismatched commitment identities, downgrade attempts and substituted transcripts. Capacity tests reload a registry with competing admissions and retain a -maximum signed acknowledgement from its reserved allowance. +maximum signed acknowledgement from its reserved allowance. Reconnect tests retain +stock counter and revocation-secret checks, classify absent or conflicting reports, +and exercise signed acknowledgement loss across reload. Manager tests cover signer +refusal, exact retry, phase-specific write barriers, height changes, staged reports, +disconnect, force-close and terminal abort replay. Eight drain scenarios use two +actual vouchers in both funding directions, with no known preimage, a known preimage, +a delayed preimage monitor write, or restart after abort release. They check actual +fulfill and fail messages, both empty commitment sets, tombstone reload and a later +ordinary payment. ## Next boundary Reusable epochs require durable retired epoch IDs and voucher hashes, with one -current signed transcript record under the same channel authority. A production -activation owner must derive and sign the exact transcript from owned quiescence, -atomically install its archive and fence, and release wire only after the manager -persistence barrier completes. Acknowledgement handling must authenticate the peer, -retain Active evidence and recheck the channel phase before reporting durability. -Reconnect needs the Variant D state comparison and acknowledgement-loss recovery; -ordinary channel traffic must remain frozen throughout. Witness/mailbox recovery, -preimage reconciliation, controlled voucher drain and invoice eligibility remain -separate required boundaries. None can be inferred from a retained setup or the -private activation fixtures. +current signed transcript record under the same channel authority. The next native +boundary is cooperative Active-to-Draining-to-Closed recovery: retain signed close +and close-ack evidence, persist every known preimage through the stock monitor, allow +only owned voucher removals and their commitment rounds, and retain that ownership +through reconnect and restart. Capacity for those terminal records must be reserved +before an epoch becomes operational. + +Production transport must bind the private manager transitions to actual authenticated +connections and deliver acknowledgement retries in the required order. Witness/mailbox +recovery, preimage reconciliation, deadline enforcement and invoice eligibility remain +separate required boundaries. None can be inferred from durable setup, activation or +a successful private protocol test. diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 55ca9b9ad37..ed35b64ea9a 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -102,7 +102,7 @@ use super::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBase mod ffor; #[cfg(test)] pub(crate) use ffor::ffor_setup_test_messages; -pub(crate) use ffor::{FFORReceiverFencePhase, FFORReceiverSetup}; +pub(crate) use ffor::{FFORReceiverFencePhase, FFORReceiverSetup, FFORReestablishOutcome}; #[cfg(any(test, feature = "_test_utils"))] #[allow(unused)] @@ -2168,6 +2168,7 @@ where holder_commitment_point, pending_splice: None, quiescent_action: None, + ffor_reconnect_outcome: None, }; let res = funded_channel.initial_commitment_signed_v2(msg, best_block, signer_provider, logger) .map(|monitor| (Some(monitor), None)) @@ -4079,7 +4080,7 @@ where /// is_usable() and considers things like the channel being temporarily disabled. /// Allowed in any state (including after shutdown) pub fn is_live(&self) -> bool { - self.is_usable() && !self.channel_state.is_peer_disconnected() + self.is_usable() && !self.channel_state.is_peer_disconnected() && !self.is_ffor_frozen() } /// Returns true if the peer for this channel is currently connected and we're not waiting on @@ -4091,6 +4092,7 @@ where /// Returns false if our last broadcasted channel_update message has the "channel disabled" bit set pub fn is_enabled(&self) -> bool { self.is_usable() + && !self.is_ffor_frozen() && match self.channel_update_status { ChannelUpdateStatus::Enabled | ChannelUpdateStatus::DisabledStaged(_) => true, ChannelUpdateStatus::Disabled | ChannelUpdateStatus::EnabledStaged(_) => false, @@ -6825,6 +6827,8 @@ where /// initiator we may be able to merge this action into what the counterparty wanted to do (e.g. /// in the case of splicing). quiescent_action: Option, + /// Unsigned peer observation valid only for the current authenticated connection. + ffor_reconnect_outcome: Option, } #[cfg(any(test, fuzzing))] @@ -9334,6 +9338,7 @@ where /// May return `Err(())`, which implies [`ChannelContext::force_shutdown`] should be called immediately. #[rustfmt::skip] fn remove_uncommitted_htlcs_and_mark_paused(&mut self, logger: &L) -> Result<(), ()> where L::Target: Logger { + self.ffor_reconnect_outcome = None; assert!(!matches!(self.context.channel_state, ChannelState::ShutdownComplete)); if !self.context.can_resume_on_reconnect() { return Err(()) @@ -9931,61 +9936,10 @@ where NS::Target: NodeSigner, CBP: Fn(u64) -> BlindedMessagePath { - self.context.check_ffor_mutation()?; - if !self.context.channel_state.is_peer_disconnected() { - // While BOLT 2 doesn't indicate explicitly we should error this channel here, it - // almost certainly indicates we are going to end up out-of-sync in some way, so we - // just close here instead of trying to recover. - return Err(ChannelError::close("Peer sent a loose channel_reestablish not after reconnect".to_owned())); - } - - // A node: - // - if `next_commitment_number` is zero: - // - MUST immediately fail the channel and broadcast any relevant latest commitment - // transaction. - if msg.next_local_commitment_number == 0 - || msg.next_local_commitment_number >= INITIAL_COMMITMENT_NUMBER - || msg.next_remote_commitment_number >= INITIAL_COMMITMENT_NUMBER - { - return Err(ChannelError::close("Peer sent an invalid channel_reestablish to force close in a non-standard way".to_owned())); - } - - let our_commitment_transaction = INITIAL_COMMITMENT_NUMBER - self.holder_commitment_point.current_transaction_number(); - if msg.next_remote_commitment_number > 0 { - let expected_point = self.context.holder_signer.as_ref() - .get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - msg.next_remote_commitment_number + 1, &self.context.secp_ctx) - .expect("TODO: async signing is not yet supported for per commitment points upon channel reestablishment"); - let given_secret = SecretKey::from_slice(&msg.your_last_per_commitment_secret) - .map_err(|_| ChannelError::close("Peer sent a garbage channel_reestablish with unparseable secret key".to_owned()))?; - if expected_point != PublicKey::from_secret_key(&self.context.secp_ctx, &given_secret) { - return Err(ChannelError::close("Peer sent a garbage channel_reestablish with secret key not matching the commitment height provided".to_owned())); - } - if msg.next_remote_commitment_number > our_commitment_transaction { - macro_rules! log_and_panic { - ($err_msg: expr) => { - log_error!(logger, $err_msg); - panic!($err_msg); - } - } - log_and_panic!("We have fallen behind - we have received proof that if we broadcast our counterparty is going to claim all our funds.\n\ - This implies you have restarted with lost ChannelMonitor and ChannelManager state, the first of which is a violation of the LDK chain::Watch requirements.\n\ - More specifically, this means you have a bug in your implementation that can cause loss of funds, or you are running with an old backup, which is unsafe.\n\ - If you have restored from an old backup and wish to claim any available funds, you should restart with\n\ - an empty ChannelManager and no ChannelMonitors, reconnect to peer(s), ensure they've force-closed all of your\n\ - previous channels and that the closure transaction(s) have confirmed on-chain,\n\ - then restart with an empty ChannelManager and the latest ChannelMonitors that you do have."); - } - } - - // Before we change the state of the channel, we check if the peer is sending a very old - // commitment transaction number, if yes we send a warning message. - if msg.next_remote_commitment_number + 1 < our_commitment_transaction { - return Err(ChannelError::Warn(format!( - "Peer attempted to reestablish channel with a very old local commitment transaction: {} (received) vs {} (expected)", - msg.next_remote_commitment_number, - our_commitment_transaction - ))); + if self.context.is_ffor_frozen() { + return self.ffor_handle_reestablish(msg, logger); } + let our_commitment_transaction = self.validate_reestablish_prefix(msg, logger)?; // Go ahead and unmark PeerDisconnected as various calls we may make check for it (and all // remaining cases either succeed or ErrorMessage-fail). @@ -11976,7 +11930,7 @@ where #[rustfmt::skip] fn get_channel_reestablish(&mut self, logger: &L) -> Result where L::Target: Logger { if self.context.is_ffor_frozen() { - return Err(msgs::WarningMessage { channel_id: self.context.channel_id(), data: ffor::FFOR_FROZEN_MESSAGE.to_owned() }); + return self.ffor_get_reestablish(logger); } assert!(self.context.channel_state.is_peer_disconnected()); assert_ne!(self.context.counterparty_next_commitment_transaction_number, INITIAL_COMMITMENT_NUMBER); @@ -12003,6 +11957,7 @@ where [0;32] }; Ok(msgs::ChannelReestablish { + ffor_reestablish: None, channel_id: self.context.channel_id(), // The protocol has two different commitment number concepts - the "commitment // transaction number", which starts from 0 and counts up, and the "revocation key @@ -13846,6 +13801,7 @@ where holder_commitment_point, pending_splice: None, quiescent_action: None, + ffor_reconnect_outcome: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some() @@ -14135,6 +14091,7 @@ where holder_commitment_point, pending_splice: None, quiescent_action: None, + ffor_reconnect_outcome: None, }; let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some() || channel.context.signer_pending_channel_ready; @@ -15885,6 +15842,7 @@ where holder_commitment_point, pending_splice, quiescent_action, + ffor_reconnect_outcome: None, }; channel.ffor_restored()?; channel.restore_ffor_receiver_quiescence()?; diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index 46cd95909e4..f59674308bb 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -1,7 +1,9 @@ use super::*; +mod activation; mod fence; mod quiescence; +mod reestablish; mod setup; use crate::ln::ffor::{ self as verification, FFORCommitmentError, FFORMonitorSnapshot, FFORReceiverAbortReason, @@ -11,6 +13,7 @@ use crate::ln::ffor::{ use fence::FFORReceiverFence; pub(crate) use fence::{FFORReceiverFencePhase, FFOR_FROZEN_MESSAGE}; pub(crate) use quiescence::FFORReceiverQuiescence; +pub(crate) use reestablish::FFORReestablishOutcome; #[cfg(test)] pub(crate) use setup::ffor_setup_test_messages; pub(crate) use setup::FFORReceiverSetup; @@ -317,11 +320,15 @@ where .received .iter() .filter_map(|received| { + let claimed = self.context.holding_cell_htlc_updates.iter().any(|update| { + matches!(update, HTLCUpdateAwaitingACK::ClaimHTLC { htlc_id, .. } + if *htlc_id == received.voucher.htlc_id) + }); let committed = self.context.pending_inbound_htlcs.iter().any(|htlc| { htlc.htlc_id == received.voucher.htlc_id && matches!(htlc.state, InboundHTLCState::Committed) }); - if committed { + if committed && !claimed { received.failure.clone().map(|failure| (received.voucher.htlc_id, failure)) } else { None diff --git a/lightning/src/ln/channel/ffor/activation.rs b/lightning/src/ln/channel/ffor/activation.rs new file mode 100644 index 00000000000..a5073b9f6fd --- /dev/null +++ b/lightning/src/ln/channel/ffor/activation.rs @@ -0,0 +1,154 @@ +//! Private phase transitions composed by the manager with its archive and persistence barrier. + +use super::*; +use crate::ln::ffor::FFORReceiverQuiescenceStatus; +use crate::ln::ffor_recovery::FFORReceiverActivation; + +impl FundedChannel +where + SP::Target: SignerProvider, +{ + pub(crate) fn ffor_receiver_abort_reason(&self) -> Option { + self.context.ffor_receiver_book.as_ref().and_then(|book| book.abort_reason) + } + + pub(crate) fn install_ffor_receiver_activation( + &mut self, activation: &FFORReceiverActivation, monitor: &FFORMonitorSnapshot, + current_height: u32, logger: &L, + ) -> Result<(), FFORReceiverError> + where + L::Target: Logger, + { + let setup = self + .ffor_receiver_setup_record() + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)? + .ok_or(FFORReceiverError::NotRegistered)?; + let authenticated = + setup.validate_recovery().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if activation.is_active() || activation.is_aborted() || self.ffor_receiver_fence().is_some() + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + if self.ffor_receiver_quiescence_status( + authenticated.header().epoch_id, + current_height, + logger, + )? != FFORReceiverQuiescenceStatus::Quiescent + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + match self.ffor_receiver_book_status(monitor, logger)? { + FFORReceiverStatus::Parked { commitments } + if commitments == activation.commitments() => {}, + _ => return Err(FFORCommitmentError::MonitorMismatch.into()), + } + let activation_hash = activation + .activation_hash(&setup) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + self.context.ffor_receiver_book.as_mut().unwrap().fence = + Some(FFORReceiverFence { phase: FFORReceiverFencePhase::Activating, activation_hash }); + if self.validate_ffor_fence().is_err() { + self.context.ffor_receiver_book.as_mut().unwrap().fence = None; + return Err(FFORCommitmentError::PendingUpdates.into()); + } + Ok(()) + } + + pub(crate) fn accept_ffor_receiver_activation( + &mut self, activation: &FFORReceiverActivation, + ) -> Result<(), FFORReceiverError> { + let setup = self + .ffor_receiver_setup_record() + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)? + .ok_or(FFORReceiverError::NotRegistered)?; + let authenticated = + setup.validate_recovery().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let epoch = authenticated.header().epoch_id; + let hash = activation + .activation_hash(&setup) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if !activation.is_active() || activation.is_aborted() || !self.context.is_connected() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + match self.ffor_receiver_fence() { + Some((FFORReceiverFencePhase::Active, existing)) if existing == hash => return Ok(()), + Some((FFORReceiverFencePhase::Activating, existing)) if existing == hash => {}, + _ => return Err(FFORCommitmentError::PendingUpdates.into()), + } + let recovered = matches!(self.ffor_receiver_reconnect_outcome(), + Some(FFORReestablishOutcome::MatchingActive { peer_report }) + if peer_report.epoch_id == epoch && peer_report.activation_hash == hash); + if !self.has_ffor_receiver_quiescence(epoch) && !recovered { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + self.validate_ffor_fence().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + self.context.ffor_receiver_book.as_mut().unwrap().fence.as_mut().unwrap().phase = + FFORReceiverFencePhase::Active; + self.finish_ffor_quiescence(); + Ok(()) + } + + /// Keep the fence while the manager persists the terminal abort observation. + pub(crate) fn begin_ffor_receiver_reconnect_abort( + &mut self, activation: &FFORReceiverActivation, + ) -> Result<(), FFORReceiverError> { + let setup = self + .ffor_receiver_setup_record() + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)? + .ok_or(FFORReceiverError::NotRegistered)?; + let hash = activation + .activation_hash(&setup) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if activation.aborted_reason() != Some(FFORReceiverAbortReason::Disconnected) { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + match self.ffor_receiver_fence() { + Some((FFORReceiverFencePhase::Aborting, existing)) if existing == hash => return Ok(()), + Some((FFORReceiverFencePhase::Activating, existing)) if existing == hash => {}, + _ => return Err(FFORCommitmentError::PendingUpdates.into()), + } + if !matches!( + self.ffor_receiver_reconnect_outcome(), + Some(FFORReestablishOutcome::AbortRequired { peer_report }) + if activation.matches_abort_report(*peer_report) + ) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + self.validate_ffor_fence().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let book = self.context.ffor_receiver_book.as_mut().unwrap(); + book.fence.as_mut().unwrap().phase = FFORReceiverFencePhase::Aborting; + book.abort_reason = Some(FFORReceiverAbortReason::Disconnected); + self.finish_ffor_quiescence(); + Ok(()) + } + + /// Called only after the manager's matching terminal archive write is durable. + pub(crate) fn release_ffor_receiver_reconnect_abort( + &mut self, epoch: [u8; 32], hash: [u8; 32], + ) -> Result<(), FFORReceiverError> { + let book = + self.context.ffor_receiver_book.as_mut().ok_or(FFORReceiverError::NotRegistered)?; + if book.epoch_id != epoch + || book.abort_reason != Some(FFORReceiverAbortReason::Disconnected) + { + return Err(FFORReceiverError::UnknownEpoch); + } + match book.fence.as_ref() { + Some(fence) + if fence.phase == FFORReceiverFencePhase::Aborting + && fence.activation_hash == hash => {}, + None => return Ok(()), + _ => return Err(FFORCommitmentError::PendingUpdates.into()), + } + book.fence = None; + Ok(()) + } + + fn finish_ffor_quiescence(&mut self) { + self.release_ffor_receiver_quiescence(); + self.context.channel_state.clear_local_stfu_sent(); + self.context.channel_state.clear_remote_stfu_sent(); + self.context.channel_state.clear_quiescent(); + self.mark_response_received(); + } +} diff --git a/lightning/src/ln/channel/ffor/fence.rs b/lightning/src/ln/channel/ffor/fence.rs index 4bc68e95c09..3c68150c60f 100644 --- a/lightning/src/ln/channel/ffor/fence.rs +++ b/lightning/src/ln/channel/ffor/fence.rs @@ -1,8 +1,8 @@ //! A private durable mutation fence, independent of connection-scoped STFU flags. //! -//! There is deliberately no production installation or reconciliation entry point. A future -//! activation owner must first archive the exact signed evidence, then install this fence under -//! the same channel/manager persistence boundary. This module does not authenticate activation transcripts. +//! The private manager activation owner archives exact signed evidence and installs the fence +//! under the same channel/manager persistence boundary. No public activation or reconciliation +//! entry point is exposed. This module does not authenticate activation transcripts. use super::*; @@ -13,16 +13,18 @@ pub(crate) const FFOR_FROZEN_MESSAGE: &str = pub(crate) enum FFORReceiverFencePhase { Activating, Active, + Aborting, } impl_writeable_tlv_based_enum!(FFORReceiverFencePhase, (0, Activating) => {}, (2, Active) => {}, + (4, Aborting) => {}, ); pub(super) struct FFORReceiverFence { - phase: FFORReceiverFencePhase, - activation_hash: [u8; 32], + pub(super) phase: FFORReceiverFencePhase, + pub(super) activation_hash: [u8; 32], } impl_writeable_tlv_based!(FFORReceiverFence, { @@ -104,8 +106,14 @@ where _ => return Ok(()), }; let context = &self.context; + let abort_matches_phase = match book.fence.as_ref().unwrap().phase { + FFORReceiverFencePhase::Aborting => { + book.abort_reason == Some(FFORReceiverAbortReason::Disconnected) + }, + _ => book.abort_reason.is_none(), + }; if book.setup.is_none() - || book.abort_reason.is_some() + || !abort_matches_phase || !matches!(context.channel_state, ChannelState::ChannelReady(_)) || context.channel_state.is_local_shutdown_sent() || context.channel_state.is_remote_shutdown_sent() diff --git a/lightning/src/ln/channel/ffor/fence/tests.rs b/lightning/src/ln/channel/ffor/fence/tests.rs index 22853b6c476..97bbf47418b 100644 --- a/lightning/src/ln/channel/ffor/fence/tests.rs +++ b/lightning/src/ln/channel/ffor/fence/tests.rs @@ -215,7 +215,7 @@ fn expect_no_released_updates(updates: MonitorRestoreUpdates) { } #[test] -fn ffor_fence_preserves_preimage_and_blocks_restart_wire_until_reconciliation() { +fn ffor_fence_preserves_preimage_and_limits_restart_wire_to_reconciliation() { use crate::chain::{ChannelMonitorUpdateStatus, Watch}; for phase in [FFORReceiverFencePhase::Activating, FFORReceiverFencePhase::Active] { for restart_before_completion in [false, true] { @@ -354,6 +354,9 @@ fn ffor_fence_preserves_preimage_and_blocks_restart_wire_until_reconciliation() }); assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + let restored_token = nodes[1].node.capture_ffor_persistence(); + let _restored_manager = nodes[1].node.encode(); + nodes[1].node.ffor_persistence_completed(restored_token).unwrap(); for _ in 0..2 { nodes[1] .node @@ -371,10 +374,8 @@ fn ffor_fence_preserves_preimage_and_blocks_restart_wire_until_reconciliation() assert_eq!(events.len(), 1, "{events:?}"); assert!(matches!( &events[0], - MessageSendEvent::HandleError { - action: msgs::ErrorAction::DisconnectPeerWithWarning { .. }, - .. - } + MessageSendEvent::SendChannelReestablish { msg, .. } + if msg.ffor_reestablish.is_some() )); nodes[1].node.peer_disconnected(nodes[0].node.get_our_node_id()); with_channel(&nodes[1], &nodes[0], id, |channel| { diff --git a/lightning/src/ln/channel/ffor/quiescence.rs b/lightning/src/ln/channel/ffor/quiescence.rs index 1baaf2a1f9b..1c03edb8e17 100644 --- a/lightning/src/ln/channel/ffor/quiescence.rs +++ b/lightning/src/ln/channel/ffor/quiescence.rs @@ -22,6 +22,16 @@ impl FundedChannel where SP::Target: SignerProvider, { + /// Whether the original completed STFU session still owns this connection. + /// A delayed signed acknowledgement is historical evidence and must not reapply the deadline. + pub(crate) fn has_ffor_receiver_quiescence(&self, epoch_id: [u8; 32]) -> bool { + self.context.is_connected() + && self.context.channel_state.is_quiescent() + && matches!(self.quiescent_action.as_ref(), + Some(QuiescentAction::FFORReceiver(request)) + if request.epoch_id == epoch_id && request.completed) + } + pub(crate) fn request_ffor_receiver_quiescence( &mut self, epoch_id: [u8; 32], monitor: FFORMonitorSnapshot, current_height: u32, logger: &L, diff --git a/lightning/src/ln/channel/ffor/reestablish.rs b/lightning/src/ln/channel/ffor/reestablish.rs new file mode 100644 index 00000000000..fdcea793eef --- /dev/null +++ b/lightning/src/ln/channel/ffor/reestablish.rs @@ -0,0 +1,309 @@ +//! Frozen Variant D reconnect observations and ordinary commitment proof checks. +//! +//! A peer report is unsigned. It cannot activate, abort, release vouchers or lift the fence. +//! The manager compares the rebuilt identities with its retained activation record before +//! consuming an outcome, then persists the corresponding transition before releasing any wire. + +use super::*; +use lightning_ffor::reestablish::{Reestablish, ReportedState}; + +/// An observation on the current authenticated peer connection, never transition authority. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum FFORReestablishOutcome { + /// The peer reports the exact active epoch. A matching signed acknowledgement is still needed. + MatchingActive { peer_report: Reestablish }, + /// Setup did not complete. Retain the fence until the manager durably records the abort. + AbortRequired { peer_report: Option }, + /// Possibly active evidence conflicts or needs a signed close response. Retain the fence. + ResolutionRequired { peer_report: Option }, +} + +impl FundedChannel +where + SP::Target: SignerProvider, +{ + pub(crate) fn ffor_receiver_reconnect_outcome(&self) -> Option<&FFORReestablishOutcome> { + self.ffor_reconnect_outcome.as_ref() + } + + /// Rebuild the frozen commitment pair without acquiring a monitor lock or advancing state. + /// The manager must compare the result to its authenticated archive under its registry lock. + /// Original monitor and claim-signature checks remain mandatory at installation and restore. + pub(crate) fn ffor_frozen_commitments( + &self, logger: &L, + ) -> Result + where + L::Target: Logger, + { + if !self.context.is_ffor_frozen() { + return Err(FFORCommitmentError::ChannelUnavailable); + } + self.validate_ffor_fence().map_err(|_| FFORCommitmentError::PendingUpdates)?; + let book = self.context.ffor_receiver_book.as_ref().unwrap(); + verification::validate_vouchers(&book.vouchers)?; + self.check_ffor_htlc_ids(FFORSettlementParty::Counterparty, &book.vouchers)?; + let holder_number = self.holder_commitment_point.current_transaction_number(); + let counterparty_number = self + .context + .counterparty_next_commitment_transaction_number + .checked_add(1) + .ok_or(FFORCommitmentError::PendingUpdates)?; + if holder_number > INITIAL_COMMITMENT_NUMBER + || counterparty_number > INITIAL_COMMITMENT_NUMBER + || counterparty_number.checked_add(1) + != Some(self.context.commitment_secrets.get_min_seen_secret()) + { + return Err(FFORCommitmentError::PendingUpdates); + } + let holder_point = self + .holder_commitment_point + .current_point() + .ok_or(FFORCommitmentError::PendingUpdates)?; + let counterparty_point = self + .context + .counterparty_current_commitment_point + .ok_or(FFORCommitmentError::PendingUpdates)?; + let holder = self + .context + .build_commitment_transaction( + &self.funding, + holder_number, + &holder_point, + true, + false, + logger, + ) + .tx; + let counterparty = self + .context + .build_commitment_transaction( + &self.funding, + counterparty_number, + &counterparty_point, + false, + true, + logger, + ) + .tx; + verification::verify_outputs(&holder, false, &book.vouchers)?; + verification::verify_outputs(&counterparty, true, &book.vouchers)?; + Ok(FFORVoucherCommitments { + holder: verification::commitment_identity(&holder), + counterparty: verification::commitment_identity(&counterparty), + }) + } + + pub(in crate::ln::channel) fn ffor_get_reestablish( + &mut self, logger: &L, + ) -> Result + where + L::Target: Logger, + { + self.ffor_reconnect_outcome = None; + let unavailable = || msgs::WarningMessage { + channel_id: self.context.channel_id(), + data: FFOR_FROZEN_MESSAGE.to_owned(), + }; + if !self.context.channel_state.is_peer_disconnected() { + return Err(unavailable()); + } + self.ffor_frozen_commitments(logger).map_err(|_| unavailable())?; + let book = self.context.ffor_receiver_book.as_ref().unwrap(); + let (phase, hash) = self.ffor_receiver_fence().unwrap(); + let (state, activation_hash) = match phase { + FFORReceiverFencePhase::Activating => (ReportedState::Activating, [0; 32]), + FFORReceiverFencePhase::Active => (ReportedState::Active, hash), + FFORReceiverFencePhase::Aborting => (ReportedState::Aborted, [0; 32]), + }; + let counterparty_next = self.context.counterparty_next_commitment_transaction_number; + let next_local_commitment_number = INITIAL_COMMITMENT_NUMBER + .checked_sub(self.holder_commitment_point.next_transaction_number()) + .ok_or_else(unavailable)?; + let next_remote_commitment_number = INITIAL_COMMITMENT_NUMBER + .checked_sub(counterparty_next) + .and_then(|number| number.checked_sub(1)) + .ok_or_else(unavailable)?; + let secret = if counterparty_next + .checked_add(1) + .map_or(false, |number| number < INITIAL_COMMITMENT_NUMBER) + { + self.context + .commitment_secrets + .get_secret(counterparty_next.checked_add(2).ok_or_else(unavailable)?) + .ok_or_else(unavailable)? + } else { + [0; 32] + }; + // static_remotekey uses the same valid dummy point as the stock reconnect path. + let mut point = [2; 33]; + point[1] = 0xff; + Ok(msgs::ChannelReestablish { + channel_id: self.context.channel_id(), + next_local_commitment_number, + next_remote_commitment_number, + your_last_per_commitment_secret: secret, + my_current_per_commitment_point: PublicKey::from_slice(&point).unwrap(), + next_funding: None, + my_current_funding_locked: self.maybe_get_my_current_funding_locked(), + ffor_reestablish: Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: book.epoch_id, + state, + activation_hash, + })), + }) + } + + pub(in crate::ln::channel) fn ffor_handle_reestablish( + &mut self, msg: &msgs::ChannelReestablish, logger: &L, + ) -> Result + where + L::Target: Logger, + { + self.ffor_reconnect_outcome = None; + self.ffor_frozen_commitments(logger) + .map_err(|_| ChannelError::WarnAndDisconnect(FFOR_FROZEN_MESSAGE.to_owned()))?; + let our_commitment = self.validate_reestablish_prefix(msg, logger)?; + let next_counterparty = INITIAL_COMMITMENT_NUMBER + - self.context.counterparty_next_commitment_transaction_number; + if msg.next_local_commitment_number + 1 < next_counterparty + || msg.next_local_commitment_number > next_counterparty + { + return Err(ChannelError::close(format!("Peer attempted to reestablish frozen channel with an inconsistent remote commitment transaction: {} (received) vs {} (expected)", msg.next_local_commitment_number, next_counterparty))); + } + if msg.next_remote_commitment_number != our_commitment + || msg.next_local_commitment_number != next_counterparty + { + return Err(ChannelError::WarnAndDisconnect("Frozen FFOR commitments require peer state recovery before ordinary retransmission".to_owned())); + } + if msg.next_funding.is_some() + || msg + .my_current_funding_locked + .as_ref() + .map_or(false, |funding| self.funding.get_funding_txid() != Some(funding.txid)) + { + return Err(ChannelError::WarnAndDisconnect( + "Frozen FFOR channel cannot reconcile a different funding transaction".to_owned(), + )); + } + let (phase, hash) = self.ffor_receiver_fence().unwrap(); + let epoch = self.context.ffor_receiver_book.as_ref().unwrap().epoch_id; + let peer_report = msg.ffor_reestablish.as_ref().map(|value| value.report()); + let outcome = if phase == FFORReceiverFencePhase::Aborting { + FFORReestablishOutcome::AbortRequired { peer_report } + } else if let Some(report) = peer_report.filter(|report| { + report.state == ReportedState::Active + && report.epoch_id == epoch + && report.activation_hash == hash + }) { + FFORReestablishOutcome::MatchingActive { peer_report: report } + } else if phase == FFORReceiverFencePhase::Activating + && peer_report.map_or(true, |report| { + matches!( + report.state, + ReportedState::Negotiating + | ReportedState::VouchersCommitted + | ReportedState::Activating + | ReportedState::Aborted + ) + }) { + FFORReestablishOutcome::AbortRequired { peer_report } + } else { + FFORReestablishOutcome::ResolutionRequired { peer_report } + }; + self.ffor_reconnect_outcome = Some(outcome); + self.context.channel_state.clear_peer_disconnected(); + self.mark_response_received(); + Ok(ReestablishResponses { + channel_ready: None, + channel_ready_order: ChannelReadyOrder::ChannelReadyFirst, + raa: None, + commitment_update: None, + commitment_order: self.context.resend_order.clone(), + announcement_sigs: None, + shutdown_msg: None, + tx_signatures: None, + tx_abort: None, + inferred_splice_locked: None, + }) + } + + /// Stock BOLT 2 checks performed before reconnect mutates channel state. Variant D uses + /// these same commitment counter, secret proof and data-loss checks without a carve-out. + pub(in crate::ln::channel) fn validate_reestablish_prefix( + &self, msg: &msgs::ChannelReestablish, logger: &L, + ) -> Result + where + L::Target: Logger, + { + if !self.context.channel_state.is_peer_disconnected() { + // While BOLT 2 doesn't indicate explicitly we should error this channel here, it + // almost certainly indicates we are going to end up out-of-sync in some way, so we + // just close here instead of trying to recover. + return Err(ChannelError::close( + "Peer sent a loose channel_reestablish not after reconnect".to_owned(), + )); + } + + // A node: + // - if `next_commitment_number` is zero: + // - MUST immediately fail the channel and broadcast any relevant latest commitment + // transaction. + if msg.next_local_commitment_number == 0 + || msg.next_local_commitment_number >= INITIAL_COMMITMENT_NUMBER + || msg.next_remote_commitment_number >= INITIAL_COMMITMENT_NUMBER + { + return Err(ChannelError::close( + "Peer sent an invalid channel_reestablish to force close in a non-standard way" + .to_owned(), + )); + } + + let our_commitment_transaction = + INITIAL_COMMITMENT_NUMBER - self.holder_commitment_point.current_transaction_number(); + if msg.next_remote_commitment_number > 0 { + let expected_point = self.context.holder_signer.as_ref() + .get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - msg.next_remote_commitment_number + 1, &self.context.secp_ctx) + .expect("TODO: async signing is not yet supported for per commitment points upon channel reestablishment"); + let given_secret = SecretKey::from_slice(&msg.your_last_per_commitment_secret) + .map_err(|_| { + ChannelError::close( + "Peer sent a garbage channel_reestablish with unparseable secret key" + .to_owned(), + ) + })?; + if expected_point != PublicKey::from_secret_key(&self.context.secp_ctx, &given_secret) { + return Err(ChannelError::close("Peer sent a garbage channel_reestablish with secret key not matching the commitment height provided".to_owned())); + } + if msg.next_remote_commitment_number > our_commitment_transaction { + macro_rules! log_and_panic { + ($err_msg: expr) => { + log_error!(logger, $err_msg); + panic!($err_msg); + }; + } + log_and_panic!("We have fallen behind - we have received proof that if we broadcast our counterparty is going to claim all our funds.\n\ + This implies you have restarted with lost ChannelMonitor and ChannelManager state, the first of which is a violation of the LDK chain::Watch requirements.\n\ + More specifically, this means you have a bug in your implementation that can cause loss of funds, or you are running with an old backup, which is unsafe.\n\ + If you have restored from an old backup and wish to claim any available funds, you should restart with\n\ + an empty ChannelManager and no ChannelMonitors, reconnect to peer(s), ensure they've force-closed all of your\n\ + previous channels and that the closure transaction(s) have confirmed on-chain,\n\ + then restart with an empty ChannelManager and the latest ChannelMonitors that you do have."); + } + } + + // Before we change the state of the channel, we check if the peer is sending a very old + // commitment transaction number, if yes we send a warning message. + if msg.next_remote_commitment_number + 1 < our_commitment_transaction { + return Err(ChannelError::Warn(format!( + "Peer attempted to reestablish channel with a very old local commitment transaction: {} (received) vs {} (expected)", + msg.next_remote_commitment_number, + our_commitment_transaction + ))); + } + + Ok(our_commitment_transaction) + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/channel/ffor/reestablish/tests.rs b/lightning/src/ln/channel/ffor/reestablish/tests.rs new file mode 100644 index 00000000000..8a5699356c0 --- /dev/null +++ b/lightning/src/ln/channel/ffor/reestablish/tests.rs @@ -0,0 +1,372 @@ +use super::*; +use crate::ln::channelmanager::ffor_recovery_tests::install_ffor_activation_for_test; +use crate::ln::ffor_tests::quiescence::{complete_handshake, register_signed, request}; +use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; +use crate::ln::functional_test_utils::*; +use crate::ln::msgs::{BaseMessageHandler, MessageSendEvent}; +use crate::util::ser::{ReadableArgs, Writeable}; + +fn with_channel<'a, 'b, 'c, T>( + receiver: &Node<'a, 'b, 'c>, sender: &Node, id: ChannelId, + body: impl FnOnce(&mut FundedChannel<&'b crate::util::test_utils::TestKeysInterface>) -> T, +) -> T { + let peers = receiver.node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + body(peer.channel_by_id.get_mut(&id).unwrap().as_funded_mut().unwrap()) +} + +fn prepare( + sender: &Node, receiver: &Node, id: ChannelId, phase: FFORReceiverFencePhase, +) -> [u8; 32] { + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + register_signed(sender, receiver, id, voucher); + deliver_parked_voucher(sender, receiver, update); + request(sender, receiver, id).unwrap(); + complete_handshake(sender, receiver); + install_ffor_activation_for_test(sender, receiver, id, phase) +} + +fn assert_no_wire(response: ReestablishResponses) { + assert!(response.channel_ready.is_none()); + assert!(response.raa.is_none()); + assert!(response.commitment_update.is_none()); + assert!(response.announcement_sigs.is_none()); + assert!(response.shutdown_msg.is_none()); + assert!(response.tx_signatures.is_none()); + assert!(response.tx_abort.is_none()); + assert!(response.inferred_splice_locked.is_none()); +} + +fn handle( + channel: &mut FundedChannel, receiver: &Node, message: &msgs::ChannelReestablish, +) -> Result +where + SP::Target: SignerProvider, +{ + channel.channel_reestablish( + message, + &receiver.logger, + &receiver.keys_manager, + ChainHash::using_genesis_block(bitcoin::Network::Testnet), + &anchor_config(), + &receiver.node.current_best_block(), + |_| unreachable!(), + ) +} + +#[test] +fn ffor_reestablish_reports_both_phases_and_funders_without_ordinary_retransmission() { + for phase in [FFORReceiverFencePhase::Activating, FFORReceiverFencePhase::Active] { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let id = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = + if receiver_funds { (&nodes[1], &nodes[0]) } else { (&nodes[0], &nodes[1]) }; + let hash = prepare(sender, receiver, id, phase); + let commitments = with_channel(receiver, sender, id, |channel| { + channel.ffor_frozen_commitments(&receiver.logger).unwrap() + }); + sender.node.peer_disconnected(receiver.node.get_our_node_id()); + receiver.node.peer_disconnected(sender.node.get_our_node_id()); + let mut peer_message = with_channel(sender, receiver, id, |channel| { + channel.get_channel_reestablish(&sender.logger).unwrap() + }); + let peer_report = Reestablish { + epoch_id: [81; 32], + state: ReportedState::Active, + activation_hash: hash, + }; + peer_message.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(peer_report)); + receiver + .node + .peer_connected( + sender.node.get_our_node_id(), + &msgs::Init { + features: sender.node.init_features(), + networks: None, + remote_network_address: None, + }, + true, + ) + .unwrap(); + let report = get_event_msg!( + receiver, + MessageSendEvent::SendChannelReestablish, + sender.node.get_our_node_id() + ); + with_channel(receiver, sender, id, |channel| { + assert_eq!( + report.next_local_commitment_number, + peer_message.next_remote_commitment_number + 1 + ); + assert_eq!( + report.next_remote_commitment_number + 1, + peer_message.next_local_commitment_number + ); + assert_eq!( + report.ffor_reestablish.unwrap().report(), + Reestablish { + epoch_id: [81; 32], + state: if phase == FFORReceiverFencePhase::Active { + ReportedState::Active + } else { + ReportedState::Activating + }, + activation_hash: if phase == FFORReceiverFencePhase::Active { + hash + } else { + [0; 32] + }, + } + ); + assert_no_wire(handle(channel, receiver, &peer_message).unwrap()); + assert_eq!( + channel.ffor_receiver_reconnect_outcome(), + Some(&FFORReestablishOutcome::MatchingActive { peer_report }) + ); + assert_eq!(channel.ffor_receiver_fence(), Some((phase, hash))); + assert_eq!(channel.ffor_frozen_commitments(&receiver.logger).unwrap(), commitments); + assert!(!channel.context.channel_state.is_peer_disconnected()); + let encoded = channel.encode(); + let features = crate::ln::channelmanager::provided_channel_type_features(&config); + let restored = FundedChannel::read( + &mut &encoded[..], + (&receiver.keys_manager, &receiver.keys_manager, &features), + ) + .unwrap(); + assert!(restored.ffor_receiver_reconnect_outcome().is_none()); + assert_eq!(restored.ffor_receiver_fence(), Some((phase, hash))); + }); + // A reconnect observation cannot survive connection loss or become a durable ACK. + receiver.node.peer_disconnected(sender.node.get_our_node_id()); + with_channel(receiver, sender, id, |channel| { + assert!(channel.ffor_receiver_reconnect_outcome().is_none()) + }); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + assert!(receiver.node.get_and_clear_pending_events().is_empty()); + check_added_monitors(receiver, 0); + } + } +} + +#[test] +fn ffor_reestablish_missing_conflicting_and_aborting_reports_retain_fence() { + for phase in [FFORReceiverFencePhase::Activating, FFORReceiverFencePhase::Active] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let (sender, receiver) = (&nodes[0], &nodes[1]); + let id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let hash = prepare(sender, receiver, id, phase); + sender.node.peer_disconnected(receiver.node.get_our_node_id()); + receiver.node.peer_disconnected(sender.node.get_our_node_id()); + let original = with_channel(sender, receiver, id, |channel| { + channel.get_channel_reestablish(&sender.logger).unwrap() + }); + let bytes = with_channel(receiver, sender, id, |channel| channel.encode()); + let features = crate::ln::channelmanager::provided_channel_type_features(&config); + let read = || { + FundedChannel::read( + &mut &bytes[..], + (&receiver.keys_manager, &receiver.keys_manager, &features), + ) + .unwrap() + }; + let mut reports = vec![None]; + for state in [ + ReportedState::Negotiating, + ReportedState::VouchersCommitted, + ReportedState::Activating, + ReportedState::Aborted, + ReportedState::Active, + ReportedState::Draining, + ReportedState::Closed, + ] { + for epoch_id in [[81; 32], [99; 32]] { + reports.push(Some(Reestablish { epoch_id, state, activation_hash: [99; 32] })); + } + } + for peer_report in reports { + let mut channel = read(); + let before = channel.ffor_frozen_commitments(&receiver.logger).unwrap(); + let mut message = original.clone(); + message.ffor_reestablish = peer_report.map(msgs::FFORChannelReestablish::new); + assert_no_wire(handle(&mut channel, receiver, &message).unwrap()); + let pre_active = peer_report.map_or(true, |r| { + matches!( + r.state, + ReportedState::Negotiating + | ReportedState::VouchersCommitted + | ReportedState::Activating + | ReportedState::Aborted + ) + }); + let expected = if phase == FFORReceiverFencePhase::Activating && pre_active { + FFORReestablishOutcome::AbortRequired { peer_report } + } else { + FFORReestablishOutcome::ResolutionRequired { peer_report } + }; + assert_eq!(channel.ffor_receiver_reconnect_outcome(), Some(&expected)); + assert_eq!(channel.ffor_receiver_fence(), Some((phase, hash))); + assert_eq!(channel.ffor_frozen_commitments(&receiver.logger).unwrap(), before); + assert!(channel.context.ffor_receiver_book.as_ref().unwrap().abort_reason.is_none()); + } + // A pending durable abort still keeps the exact commitment pair and emits zero H_act. + let mut channel = read(); + let book = channel.context.ffor_receiver_book.as_mut().unwrap(); + book.fence.as_mut().unwrap().phase = FFORReceiverFencePhase::Aborting; + book.abort_reason = Some(crate::ln::ffor::FFORReceiverAbortReason::Disconnected); + let message = channel.get_channel_reestablish(&receiver.logger).unwrap(); + assert_eq!( + message.ffor_reestablish.unwrap().report(), + Reestablish { + epoch_id: [81; 32], + state: ReportedState::Aborted, + activation_hash: [0; 32], + } + ); + assert_no_wire(handle(&mut channel, receiver, &original).unwrap()); + assert_eq!( + channel.ffor_receiver_reconnect_outcome(), + Some(&FFORReestablishOutcome::AbortRequired { peer_report: None }) + ); + assert_eq!(channel.ffor_receiver_fence(), Some((FFORReceiverFencePhase::Aborting, hash))); + } +} + +#[test] +fn ffor_reestablish_keeps_stock_counter_secret_and_data_loss_protection() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let (sender, receiver) = (&nodes[0], &nodes[1]); + let id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + // Advance history enough to exercise a valid but very old revocation counter. + send_payment(sender, &[receiver], 1_000_000); + prepare(sender, receiver, id, FFORReceiverFencePhase::Active); + sender.node.peer_disconnected(receiver.node.get_our_node_id()); + receiver.node.peer_disconnected(sender.node.get_our_node_id()); + let original = with_channel(sender, receiver, id, |channel| { + channel.get_channel_reestablish(&sender.logger).unwrap() + }); + let bytes = with_channel(receiver, sender, id, |channel| channel.encode()); + let features = crate::ln::channelmanager::provided_channel_type_features(&config); + let read = || { + FundedChannel::read( + &mut &bytes[..], + (&receiver.keys_manager, &receiver.keys_manager, &features), + ) + .unwrap() + }; + for fault in 0..11 { + let mut channel = read(); + let mut message = original.clone(); + match fault { + 0 => message.next_local_commitment_number = 0, + 1 => message.next_local_commitment_number = INITIAL_COMMITMENT_NUMBER, + 2 => message.next_remote_commitment_number = INITIAL_COMMITMENT_NUMBER, + 3 => message.your_last_per_commitment_secret = [0; 32], + 4 => message.your_last_per_commitment_secret = [42; 32], + 5 => message.next_local_commitment_number += 1, + 6 => message.next_local_commitment_number -= 1, + 7 => { + message.next_remote_commitment_number -= 1; + message.your_last_per_commitment_secret = channel + .context + .holder_signer + .as_ecdsa() + .unwrap() + .inner + .release_commitment_secret( + INITIAL_COMMITMENT_NUMBER - message.next_remote_commitment_number + 1, + ) + .unwrap(); + }, + 8 => { + message.next_remote_commitment_number = 0; + message.your_last_per_commitment_secret = [0; 32]; + }, + 9 => { + message.next_funding = Some(msgs::NextFunding { + txid: bitcoin::Txid::from_byte_array([42; 32]), + retransmit_flags: 0, + }) + }, + 10 => { + message.my_current_funding_locked = Some(msgs::FundingLocked { + txid: bitcoin::Txid::from_byte_array([42; 32]), + retransmit_flags: 0, + }) + }, + _ => unreachable!(), + } + let before = channel.encode(); + let result = handle(&mut channel, receiver, &message); + match (fault, result) { + (0..=5, Err(ChannelError::Close(_))) => {}, + (6..=7 | 9..=10, Err(ChannelError::WarnAndDisconnect(_))) => {}, + (8, Err(ChannelError::Warn(_))) => {}, + _ => panic!("unexpected rejection for fault {fault}"), + } + assert_eq!(channel.encode(), before); + assert!(channel.ffor_receiver_reconnect_outcome().is_none()); + } + for fault in 0..3 { + let mut channel = read(); + match fault { + 0 => channel.context.signer_pending_commitment_update = true, + 1 => channel.context.pending_inbound_htlcs[0].amount_msat += 1000, + 2 => { + channel.context.pending_inbound_htlcs[0].state = InboundHTLCState::LocalRemoved( + InboundHTLCRemovalReason::FailMalformed(([0; 32], 0x8004)), + ) + }, + _ => unreachable!(), + } + assert!(channel.get_channel_reestablish(&receiver.logger).is_err()); + assert!(matches!( + handle(&mut channel, receiver, &original), + Err(ChannelError::WarnAndDisconnect(_)) + )); + assert!(channel.ffor_receiver_reconnect_outcome().is_none()); + } + // A valid proof that our stored state was revoked preserves the stock fatal safety guard. + let mut channel = read(); + let mut message = original; + message.next_remote_commitment_number += 1; + message.your_last_per_commitment_secret = channel + .context + .holder_signer + .as_ecdsa() + .unwrap() + .inner + .release_commitment_secret( + INITIAL_COMMITMENT_NUMBER - message.next_remote_commitment_number + 1, + ) + .unwrap(); + let before = channel.encode(); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + handle(&mut channel, receiver, &message) + })); + let panic = result.err().expect("A proven revoked local state must not continue"); + let message = panic + .downcast_ref::<&str>() + .copied() + .or_else(|| panic.downcast_ref::().map(String::as_str)) + .unwrap(); + assert!(message.contains("We have fallen behind")); + assert_eq!(channel.encode(), before); + assert!(channel.ffor_receiver_reconnect_outcome().is_none()); +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9a070f969ea..c1ff9e5d501 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -72,6 +72,7 @@ use crate::ln::ffor::{ use crate::ln::ffor_persistence::FFORPersistenceBarrier; pub use crate::ln::ffor_persistence::{FFORPersistenceRequirement, FFORPersistenceToken}; use crate::ln::ffor_recovery::FFORRecoveryRegistry; +mod ffor_activation; use crate::ln::funding::SpliceContribution; use crate::ln::inbound_payment; use crate::ln::interactivetxs::InteractiveTxMessageSend; @@ -142,6 +143,7 @@ use crate::util::ser::{ WithoutLength, Writeable, Writer, }; use crate::util::wakers::{Future, Notifier}; +use ffor_activation::FFORReceiverRuntime; #[cfg(test)] use crate::blinded_path::payment::BlindedPaymentPath; @@ -2907,6 +2909,8 @@ pub struct ChannelManager< ffor_persistence: Mutex, // Evidence outlives channels, including force-close and stale-manager restoration. ffor_recovery: Mutex, + // Process-local durability and original-connection outbox bookkeeping. + ffor_activation: Mutex, /// Tracks the message events that are to be broadcasted when we are connected to some peer. pending_broadcast_messages: Mutex>, @@ -4031,6 +4035,7 @@ where needs_persist_flag: AtomicBool::new(false), ffor_persistence: Mutex::new(FFORPersistenceBarrier::new()), ffor_recovery: Mutex::new(FFORRecoveryRegistry::new()), + ffor_activation: Mutex::new(FFORReceiverRuntime::new()), funding_batch_states: Mutex::new(BTreeMap::new()), pending_broadcast_messages: Mutex::new(Vec::new()), @@ -12066,6 +12071,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // freed HTLCs to fail backwards. If in the future we no longer drop pending // add-HTLCs on disconnect, we may be handed HTLCs to fail backwards here. let outbound_scid_alias = chan.context.outbound_scid_alias(); + let proof = self.check_ffor_reconnect_archive(chan); + try_channel_entry!(self, peer_state, proof, chan_entry); let res = chan.channel_reestablish( msg, &&logger, @@ -12076,6 +12083,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ |htlc_id| self.path_for_release_held_htlc(htlc_id, outbound_scid_alias, &msg.channel_id, counterparty_node_id) ); let responses = try_channel_entry!(self, peer_state, res, chan_entry); + if chan.context.is_ffor_frozen() { + let transition = self.apply_ffor_reconnect_outcome(chan); + try_channel_entry!(self, peer_state, transition, chan_entry); + return Ok(()); + } let mut channel_update = None; if let Some(msg) = responses.shutdown_msg { peer_state.pending_msg_events.push(MessageSendEvent::SendShutdown { @@ -12131,6 +12143,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ peer_state.pending_msg_events.push(MessageSendEvent::SendChannelReestablish { node_id: *counterparty_node_id, msg: msgs::ChannelReestablish { + ffor_reestablish: None, channel_id: msg.channel_id, next_local_commitment_number: 0, next_remote_commitment_number: 0, @@ -14366,7 +14379,7 @@ where let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; if peer_state.pending_msg_events.len() > 0 { - pending_events.append(&mut peer_state.pending_msg_events); + self.drain_ffor_pending_messages(peer_state, &mut pending_events); } if peer_state.is_connected { is_any_peer_connected = true @@ -17173,7 +17186,11 @@ where )?; channel.ffor_validate_receiver_identity(our_network_pubkey, chain_hash)?; if let Some(setup) = channel.ffor_receiver_setup_record()? { - ffor_channel_setups.push((setup, channel.ffor_receiver_fence())); + ffor_channel_setups.push(( + setup, + channel.ffor_receiver_fence(), + channel.ffor_receiver_abort_reason(), + )); } let logger = WithChannelContext::from(&args.logger, &channel.context, None); let channel_id = channel.context.channel_id(); @@ -17574,13 +17591,17 @@ where (22, ffor_recovery, option), }); let ffor_recovery = ffor_recovery.unwrap_or_else(FFORRecoveryRegistry::new); + let mut ffor_persistence = FFORPersistenceBarrier::new(); + let ffor_activation = FFORReceiverRuntime::restored(&ffor_recovery, &mut ffor_persistence)?; ffor_recovery.validate_identity(our_network_pubkey, chain_hash)?; - for (setup, fence) in &ffor_channel_setups { - ffor_recovery.validate_channel_fence(setup, *fence)?; + for (setup, fence, abort_reason) in &ffor_channel_setups { + ffor_recovery.validate_channel_outcome(setup, *fence, *abort_reason)?; } for channel_id in ffor_recovery.activation_channels() { - let monitor = args.channel_monitors.get(&channel_id).ok_or(DecodeError::InvalidValue)?; - ffor_recovery.validate_activation_monitor(channel_id, &monitor.ffor_recovery_identity())?; + let monitor = + args.channel_monitors.get(&channel_id).ok_or(DecodeError::InvalidValue)?; + ffor_recovery + .validate_activation_monitor(channel_id, &monitor.ffor_recovery_identity())?; } let mut decode_update_add_htlcs = decode_update_add_htlcs.unwrap_or_else(|| new_hash_map()); for peer in per_peer_state.values() { @@ -18553,8 +18574,9 @@ where event_persist_notifier: Notifier::new(), needs_persist_flag: AtomicBool::new(false), - ffor_persistence: Mutex::new(FFORPersistenceBarrier::new()), + ffor_persistence: Mutex::new(ffor_persistence), ffor_recovery: Mutex::new(ffor_recovery), + ffor_activation: Mutex::new(ffor_activation), funding_batch_states: Mutex::new(BTreeMap::new()), @@ -18851,6 +18873,9 @@ where //TODO: Broadcast channel update for closed channels, but only after we've made a //connection or two. + if channel_manager.ffor_persistence.lock().unwrap().needs_persistence() { + channel_manager.event_persist_notifier.notify(); + } Ok((best_block_hash.clone(), channel_manager)) } } diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs new file mode 100644 index 00000000000..000e97af390 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -0,0 +1,405 @@ +//! Private composition of receiver activation, retained evidence and ordered persistence. +//! +//! No public activation or invoice API is exposed until reconnect and controlled drain are wired. + +use super::*; +use crate::ln::channel::{FFORReceiverFencePhase, FFORReestablishOutcome}; +use crate::ln::ffor_recovery::{FFORReceiverActivation, FFORRecoveryKey}; +use crate::sign::ffor::FFORSigningRequest; +use lightning_ffor::transcript; +use lightning_ffor::wire::{Activate, Message as FFORMessage, Payload}; + +mod reestablish; + +struct RuntimeEntry { + key: FFORRecoveryKey, + requirement: FFORPersistenceRequirement, + may_send_activate: bool, +} + +/// Bounded by the retained registry's keys. No bytes or phase authority are copied here. +pub(super) struct FFORReceiverRuntime { + entries: Vec, +} + +impl FFORReceiverRuntime { + pub(super) fn new() -> Self { + Self { entries: Vec::new() } + } + + pub(super) fn restored( + recovery: &FFORRecoveryRegistry, barrier: &mut FFORPersistenceBarrier, + ) -> Result { + let mut runtime = Self::new(); + let keys = recovery.activation_keys(); + if !keys.is_empty() { + let requirement = barrier.request().map_err(|_| DecodeError::InvalidValue)?; + for key in keys { + runtime.record(key, requirement.clone(), false); + } + } + Ok(runtime) + } + + fn get(&self, key: &FFORRecoveryKey) -> Result<&RuntimeEntry, FFORReceiverError> { + self.entries + .iter() + .find(|entry| entry.key == *key) + .ok_or(FFORReceiverError::RecoveryUnavailable) + } + + pub(super) fn record( + &mut self, key: FFORRecoveryKey, requirement: FFORPersistenceRequirement, + may_send_activate: bool, + ) { + let entry = RuntimeEntry { key, requirement, may_send_activate }; + if let Some(existing) = self.entries.iter_mut().find(|existing| existing.key == key) { + *existing = entry; + } else { + self.entries.push(entry); + } + } + + fn sent(&mut self, key: &FFORRecoveryKey) { + if let Some(entry) = self.entries.iter_mut().find(|entry| entry.key == *key) { + entry.may_send_activate = false; + } + } +} + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Derive and sign from the actual quiescent channel. This returns no outbound bytes. + pub(crate) fn prepare_ffor_receiver_activation( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + monitor: &FFORMonitorSnapshot, + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let current_height = self.best_block.read().unwrap().height; + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + channel + .ffor_validate_receiver_identity(self.our_network_pubkey, self.chain_hash) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let setup = channel + .ffor_receiver_setup_record() + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)? + .ok_or(FFORReceiverError::NotRegistered)?; + let authenticated = + setup.validate_recovery().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if authenticated.header().epoch_id != epoch_id { + return Err(FFORReceiverError::UnknownEpoch); + } + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let mut recovery = self.ffor_recovery.lock().unwrap(); + if let Some(activation) = recovery.get_activation(&key) { + let hash = activation + .activation_hash(&setup) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if activation.is_aborted() + || activation.is_active() + || channel.ffor_receiver_fence() != Some((FFORReceiverFencePhase::Activating, hash)) + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); + } + if channel.ffor_receiver_quiescence_status(epoch_id, current_height, &self.logger)? + != FFORReceiverQuiescenceStatus::Quiescent + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let commitments = match channel.ffor_receiver_book_status(monitor, &self.logger)? { + FFORReceiverStatus::Parked { commitments } => commitments, + _ => return Err(FFORCommitmentError::PendingUpdates.into()), + }; + let mut message = FFORMessage { + header: authenticated.header(), + payload: Payload::Activate(Activate { + setup_hash: authenticated.setup_hash(), + book_hash: authenticated.book_hash(), + commit_hash: transcript::commitment_hash( + commitments.holder.number, + &commitments.holder.txid.to_byte_array(), + commitments.counterparty.number, + &commitments.counterparty.txid.to_byte_array(), + ), + epoch_start_height: current_height, + }), + extensions: Vec::new(), + signature: [0; 64], + }; + let unsigned = + message.unsigned_wire().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let request = FFORSigningRequest::new(&unsigned) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + message.signature = self + .node_signer + .sign_ffor_message(&request) + .map_err(|_| FFORReceiverError::SignerUnavailable)? + .serialize_compact(); + let wire = message.encode().map_err(|_| FFORReceiverError::SignerUnavailable)?; + let activation = + FFORReceiverActivation::prepare(&setup, &wire, commitments, monitor, current_height) + .map_err(|_| FFORReceiverError::SignerUnavailable)?; + let live_height = self.best_block.read().unwrap().height; + authenticated + .validate_activation(&message, activation.commitment_hash(), live_height) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let upgrade = recovery + .prepare_activation(&setup, &activation) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.install_ffor_receiver_activation( + &activation, + monitor, + live_height, + &self.logger, + )?; + upgrade.commit(); + self.ffor_activation.lock().unwrap().record(key, requirement.clone(), true); + Ok(requirement) + } + + /// Enqueue at most once to the original connection after its exact snapshot is durable. + /// The callback must check its authenticated transport token atomically with queue insertion. + /// It must not call back into the manager, acquire a monitor, or perform network I/O. + pub(crate) fn release_ffor_receiver_activation( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + enqueue: C, + ) -> Result + where + C: FnOnce(&[u8]) -> Result<(), ()>, + { + let _guard = self.total_consistency_lock.read().unwrap(); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(channel_id) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let recovery = self.ffor_recovery.lock().unwrap(); + let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?; + let activation = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; + let hash = activation + .activation_hash(setup) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let mut runtime = self.ffor_activation.lock().unwrap(); + let entry = runtime.get(&key)?; + if !entry.may_send_activate + || !self.ffor_persistence.lock().unwrap().is_complete(&entry.requirement) + || channel.ffor_receiver_fence() != Some((FFORReceiverFencePhase::Activating, hash)) + || !channel.has_ffor_receiver_quiescence(epoch_id) + { + return Ok(false); + } + let current_height = self.best_block.read().unwrap().height; + channel.ffor_receiver_quiescence_status(epoch_id, current_height, &self.logger)?; + let message = FFORMessage::decode(activation.activate_wire()) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + setup + .validate_recovery() + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)? + .validate_activation(&message, activation.commitment_hash(), current_height) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if enqueue(activation.activate_wire()).is_err() { + return Ok(false); + } + runtime.sent(&key); + Ok(true) + } + + /// Retain an exact authenticated S acknowledgement under the same phase and archive authority. + /// The caller must supply the peer identity from the current authenticated transport callback. + pub(crate) fn accept_ffor_receiver_activation_ack( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ack_wire: &[u8], + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let mut recovery = self.ffor_recovery.lock().unwrap(); + let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?.clone(); + let previous = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; + let activation = previous + .with_ack(&setup, ack_wire) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if previous.is_active() { + channel.accept_ffor_receiver_activation(&activation)?; + return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); + } + let upgrade = recovery + .prepare_activation(&setup, &activation) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.accept_ffor_receiver_activation(&activation)?; + upgrade.commit(); + self.ffor_activation.lock().unwrap().record(key, requirement.clone(), false); + Ok(requirement) + } + + /// Called under the peer lock before ordinary reconnect can mutate connection state. + pub(super) fn check_ffor_reconnect_archive( + &self, channel: &FundedChannel, + ) -> Result<(), ChannelError> { + if !channel.context.is_ffor_frozen() { + return Ok(()); + } + let invalid = || { + ChannelError::WarnAndDisconnect( + "FFOR reconnect recovery evidence does not match the channel".to_owned(), + ) + }; + let setup = + channel.ffor_receiver_setup_record().map_err(|_| invalid())?.ok_or_else(invalid)?; + let authenticated = setup.validate_recovery().map_err(|_| invalid())?; + let key = FFORRecoveryKey { + channel_id: channel.context.channel_id(), + epoch_id: authenticated.header().epoch_id, + }; + let recovery = self.ffor_recovery.lock().unwrap(); + recovery + .validate_channel_outcome( + &setup, + channel.ffor_receiver_fence(), + channel.ffor_receiver_abort_reason(), + ) + .map_err(|_| invalid())?; + let activation = recovery.get_activation(&key).ok_or_else(invalid)?; + if channel.ffor_frozen_commitments(&self.logger).map_err(|_| invalid())? + != activation.commitments() + { + return Err(invalid()); + } + Ok(()) + } + + /// An unsigned report can require a durable abort, but cannot itself establish Active. + pub(super) fn apply_ffor_reconnect_outcome( + &self, channel: &mut FundedChannel, + ) -> Result<(), ChannelError> { + let report = match channel.ffor_receiver_reconnect_outcome() { + Some(FFORReestablishOutcome::AbortRequired { peer_report }) => *peer_report, + _ => return Ok(()), + }; + let invalid = || { + ChannelError::WarnAndDisconnect("FFOR reconnect abort could not be retained".to_owned()) + }; + let setup = + channel.ffor_receiver_setup_record().map_err(|_| invalid())?.ok_or_else(invalid)?; + let authenticated = setup.validate_recovery().map_err(|_| invalid())?; + let key = FFORRecoveryKey { + channel_id: channel.context.channel_id(), + epoch_id: authenticated.header().epoch_id, + }; + let mut recovery = self.ffor_recovery.lock().unwrap(); + let previous = recovery.get_activation(&key).ok_or_else(invalid)?; + if previous.is_aborted() { + return Ok(()); + } + let aborted = previous.abort_after_reestablish(&setup, report).map_err(|_| invalid())?; + let upgrade = recovery.prepare_activation(&setup, &aborted).map_err(|_| invalid())?; + let requirement = self.ffor_persistence.lock().unwrap().request().map_err(|_| invalid())?; + channel.begin_ffor_receiver_reconnect_abort(&aborted).map_err(|_| invalid())?; + upgrade.commit(); + self.ffor_activation.lock().unwrap().record(key, requirement, false); + Ok(()) + } + + /// Release only a durably recorded pre-active abort. Existing monitor claims remain ahead of failures. + pub(crate) fn release_ffor_receiver_reconnect_abort( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let recovery = self.ffor_recovery.lock().unwrap(); + let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?; + let activation = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; + if !activation.is_aborted() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let runtime = self.ffor_activation.lock().unwrap(); + let requirement = &runtime.get(&key)?.requirement; + if !self.ffor_persistence.lock().unwrap().is_complete(requirement) { + return Ok(false); + } + let hash = activation + .activation_hash(setup) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + channel.release_ffor_receiver_reconnect_abort(epoch_id, hash)?; + channel.ffor_queue_aborted_vouchers(&self.logger); + Ok(true) + } +} + +#[cfg(test)] +mod tests; + +#[cfg(test)] +mod drain_tests; diff --git a/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs b/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs new file mode 100644 index 00000000000..ae4cd8f85b5 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs @@ -0,0 +1,344 @@ +use super::*; +use crate::chain::ChannelMonitorUpdateStatus; +use crate::ln::channel::ffor_setup_test_messages; +use crate::ln::channelmanager::ffor_recovery_tests::claim_ffor_preimage_for_test; +use crate::ln::ffor::FFORReceiverAbortReason; +use crate::ln::ffor_tests::quiescence::{complete_handshake, request}; +use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; +use crate::ln::functional_test_utils::*; + +const EPOCH: [u8; 32] = [81; 32]; + +fn persist(node: &Node) -> Vec { + let token = node.node.capture_ffor_persistence(); + let encoded = node.node.encode(); + node.node.ffor_persistence_completed(token).unwrap(); + encoded +} + +fn sign(message: &mut FFORMessage, node: &Node) { + message.signature = node + .keys_manager + .sign_ffor_message(&FFORSigningRequest::new(&message.unsigned_wire().unwrap()).unwrap()) + .unwrap() + .serialize_compact(); +} + +/// Reserve two actual outgoing payment hashes before delivering either voucher to the receiver. +fn park_two(sender: &Node, receiver: &Node, id: ChannelId) -> ([FFORVoucher; 2], PaymentPreimage) { + let preimage = PaymentPreimage([*receiver.network_payment_count.as_ref().borrow(); 32]); + let (first, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let (mut route, hash, _, secret) = get_route_and_payment_hash!(sender, receiver, 2_000_000); + route.paths[0].hops[0].cltv_expiry_delta = 144; + let second_voucher = + FFORVoucher { htlc_id: voucher.htlc_id + 1, payment_hash: hash, ..voucher }; + let (mut init, mut accept) = ffor_setup_test_messages(sender, receiver, id, voucher); + if let Payload::Init(terms) = &mut init.payload { + terms.budget_msat += second_voucher.amount_msat; + terms.amounts_msat.push(second_voucher.amount_msat); + } else { + unreachable!(); + } + sign(&mut init, receiver); + if let Payload::Accept(accepted) = &mut accept.payload { + accepted.payment_hashes.push(second_voucher.payment_hash.0); + accepted.amounts_msat.push(second_voucher.amount_msat); + accepted.init_hash = transcript::init_hash(&init.encode().unwrap()); + } else { + unreachable!(); + } + sign(&mut accept, sender); + receiver + .node + .register_ffor_receiver_setup( + &id, + &sender.node.get_our_node_id(), + &init.encode().unwrap(), + &accept.encode().unwrap(), + 20, + ) + .unwrap(); + persist(receiver); + deliver_parked_voucher(sender, receiver, first); + sender + .node + .send_payment_with_route( + route, + hash, + RecipientOnionFields::secret_only(secret), + PaymentId(hash.0), + ) + .unwrap(); + check_added_monitors(sender, 1); + let second = get_htlc_update_msgs!(sender, receiver.node.get_our_node_id()); + assert_eq!(second.update_add_htlcs.len(), 1); + assert_eq!(second.update_add_htlcs[0].htlc_id, second_voucher.htlc_id); + assert_eq!(second.update_add_htlcs[0].cltv_expiry, second_voucher.cltv_expiry); + deliver_parked_voucher(sender, receiver, second); + request(sender, receiver, id).unwrap(); + complete_handshake(sender, receiver); + let snapshot = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); + receiver + .node + .prepare_ffor_receiver_activation(&id, &sender.node.get_our_node_id(), EPOCH, &snapshot) + .unwrap(); + persist(receiver); + ([voucher, second_voucher], preimage) +} + +fn phase(sender: &Node, receiver: &Node, id: ChannelId) -> Option { + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + peer.channel_by_id + .get(&id) + .unwrap() + .as_funded() + .unwrap() + .ffor_receiver_fence() + .map(|(phase, _)| phase) +} + +fn assert_no_update_wire(node: &Node) { + for event in node.node.get_and_clear_pending_msg_events() { + assert!( + matches!( + event, + MessageSendEvent::SendChannelUpdate { .. } + | MessageSendEvent::BroadcastChannelUpdate { .. } + ), + "premature wire: {event:?}" + ); + } +} + +/// Complete native commitment and revoke rounds, recording the actual terminal wire for every HTLC. +fn drain<'a, 'b, 'c>( + sender: &Node<'a, 'b, 'c>, receiver: &Node<'a, 'b, 'c>, +) -> (Vec, Vec) { + let mut fulfilled = Vec::new(); + let mut failed = Vec::new(); + for _ in 0..30 { + let mut progressed = false; + for (from, to) in [(sender, receiver), (receiver, sender)] { + for event in from.node.get_and_clear_pending_msg_events() { + progressed = true; + let from_id = from.node.get_our_node_id(); + match event { + MessageSendEvent::SendChannelReestablish { msg, .. } => { + to.node.handle_channel_reestablish(from_id, &msg) + }, + MessageSendEvent::SendChannelReady { msg, .. } => { + to.node.handle_channel_ready(from_id, &msg) + }, + MessageSendEvent::SendRevokeAndACK { msg, .. } => { + to.node.handle_revoke_and_ack(from_id, &msg) + }, + MessageSendEvent::SendAnnouncementSignatures { msg, .. } => { + to.node.handle_announcement_signatures(from_id, &msg) + }, + MessageSendEvent::UpdateHTLCs { updates, .. } => { + assert!(updates.update_add_htlcs.is_empty()); + assert!(updates.update_fee.is_none()); + for msg in updates.update_fulfill_htlcs { + assert_eq!(from_id, receiver.node.get_our_node_id()); + fulfilled.push(msg.htlc_id); + to.node.handle_update_fulfill_htlc(from_id, msg); + } + for msg in updates.update_fail_htlcs { + assert_eq!(from_id, receiver.node.get_our_node_id()); + failed.push(msg.htlc_id); + to.node.handle_update_fail_htlc(from_id, &msg); + } + for msg in updates.update_fail_malformed_htlcs { + assert_eq!(from_id, receiver.node.get_our_node_id()); + failed.push(msg.htlc_id); + to.node.handle_update_fail_malformed_htlc(from_id, &msg); + } + to.node.handle_commitment_signed_batch_test( + from_id, + &updates.commitment_signed, + ); + }, + MessageSendEvent::BroadcastChannelAnnouncement { .. } + | MessageSendEvent::BroadcastChannelUpdate { .. } + | MessageSendEvent::SendChannelUpdate { .. } => {}, + other => panic!("unexpected drain wire: {other:?}"), + } + } + if from.node.needs_pending_htlc_processing() { + from.node.process_pending_htlc_forwards(); + progressed = true; + } + from.chain_monitor.added_monitors.lock().unwrap().clear(); + } + assert!(receiver.node.get_and_clear_pending_events().is_empty()); + if !progressed { + return (fulfilled, failed); + } + } + panic!("ordinary voucher drain failed to converge"); +} + +#[test] +fn ffor_activation_manager_abort_drains_mixed_preimages_and_reloads_for_ordinary_payment() { + for receiver_funds in [false, true] { + for (learn_preimage, delay_monitor, restart_before_drain) in + [(false, false, false), (true, false, false), (true, true, false), (true, false, true)] + { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor); + let (abort_persister, abort_chain_monitor, abort_manager); + let config = anchor_config(); + let managers = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let reloaded; + let mut nodes = create_network(2, &node_cfgs, &managers); + let (funder, peer_index) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, peer_index, 100_000, 40_000_000, + ) + .2; + let (vouchers, preimage) = park_two(&nodes[0], &nodes[1], id); + let peer = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + let preimage_update = if learn_preimage { + if delay_monitor { + chanmon_cfgs[1] + .persister + .set_update_ret(ChannelMonitorUpdateStatus::InProgress); + } + let update = claim_ffor_preimage_for_test( + &nodes[0], + &nodes[1], + id, + vouchers[0].htlc_id, + preimage, + ); + check_added_monitors(&nodes[1], 1); + assert_eq!( + get_monitor!(nodes[1], id).get_stored_preimages()[&vouchers[0].payment_hash].0, + preimage + ); + Some(update) + } else { + None + }; + assert_no_update_wire(&nodes[1]); + nodes[0].node.peer_disconnected(receiver_id); + nodes[1].node.peer_disconnected(peer); + connect_nodes(&nodes[0], &nodes[1]); + let receiver_report = + get_event_msg!(&nodes[1], MessageSendEvent::SendChannelReestablish, peer); + let sender_report = + get_event_msg!(&nodes[0], MessageSendEvent::SendChannelReestablish, receiver_id); + assert!(sender_report.ffor_reestablish.is_none()); + nodes[0].node.handle_channel_reestablish(receiver_id, &receiver_report); + nodes[1].node.handle_channel_reestablish(peer, &sender_report); + assert_eq!(phase(&nodes[0], &nodes[1], id), Some(FFORReceiverFencePhase::Aborting)); + assert!(!nodes[1] + .node + .release_ffor_receiver_reconnect_abort(&id, &peer, EPOCH) + .unwrap()); + assert_no_update_wire(&nodes[1]); + check_added_monitors(&nodes[1], 0); + persist(&nodes[1]); + assert!(nodes[1] + .node + .release_ffor_receiver_reconnect_abort(&id, &peer, EPOCH) + .unwrap()); + assert_eq!(phase(&nodes[0], &nodes[1], id), None); + if delay_monitor { + assert_no_update_wire(&nodes[1]); + check_added_monitors(&nodes[1], 0); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + nodes[1] + .chain_monitor + .chain_monitor + .channel_monitor_updated(id, preimage_update.unwrap()) + .unwrap(); + } + if restart_before_drain { + // The claimed voucher remains in the holding cell when the abort fence is released. + // Restore that state before any fulfill or failure commitment has been generated. + let released = persist(&nodes[1]); + let monitor = get_monitor!(nodes[1], id).encode(); + nodes[0].node.peer_disconnected(receiver_id); + reload_node!( + nodes[1], + config.clone(), + &released, + &[&monitor], + abort_persister, + abort_chain_monitor, + abort_manager + ); + persist(&nodes[1]); + connect_nodes(&nodes[0], &nodes[1]); + } + let (fulfilled, failed) = drain(&nodes[0], &nodes[1]); + assert_eq!( + fulfilled, + if learn_preimage { vec![vouchers[0].htlc_id] } else { Vec::new() } + ); + let expected_failed: Vec<_> = vouchers + .iter() + .skip(if learn_preimage { 1 } else { 0 }) + .map(|v| v.htlc_id) + .collect(); + assert_eq!(failed, expected_failed); + let mut sent = Vec::new(); + let mut failed_hashes = Vec::new(); + for event in nodes[0].node.get_and_clear_pending_events() { + match event { + Event::PaymentSent { payment_hash, payment_preimage, .. } => { + assert_eq!(payment_preimage, preimage); + sent.push(payment_hash); + }, + Event::PaymentFailed { payment_hash, .. } => { + failed_hashes.push(payment_hash.unwrap()) + }, + Event::PaymentPathSuccessful { .. } | Event::PaymentPathFailed { .. } => {}, + other => panic!("unexpected sender event: {other:?}"), + } + } + assert_eq!( + sent, + if learn_preimage { vec![vouchers[0].payment_hash] } else { Vec::new() } + ); + assert_eq!(failed_hashes.len(), expected_failed.len()); + for voucher in vouchers.iter().skip(if learn_preimage { 1 } else { 0 }) { + assert!(failed_hashes.contains(&voucher.payment_hash)); + } + for node in &nodes { + let channels = node.node.list_channels(); + assert_eq!(channels.len(), 1); + assert!(channels[0].pending_inbound_htlcs.is_empty()); + assert!(channels[0].pending_outbound_htlcs.is_empty()); + node.chain_monitor.added_monitors.lock().unwrap().clear(); + } + let snapshot = get_monitor!(nodes[1], id).ffor_commitment_snapshot().unwrap(); + assert_eq!( + nodes[1].node.ffor_receiver_book_status(&id, &peer, &snapshot).unwrap(), + FFORReceiverStatus::Aborted { reason: FFORReceiverAbortReason::Disconnected } + ); + let manager = persist(&nodes[1]); + let monitor = get_monitor!(nodes[1], id).encode(); + nodes[0].node.peer_disconnected(receiver_id); + reload_node!( + nodes[1], + config, + &manager, + &[&monitor], + persister, + chain_monitor, + reloaded + ); + persist(&nodes[1]); + connect_nodes(&nodes[0], &nodes[1]); + assert_eq!(drain(&nodes[0], &nodes[1]), (Vec::new(), Vec::new())); + assert_eq!(phase(&nodes[0], &nodes[1], id), None); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + } + } +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs b/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs new file mode 100644 index 00000000000..c71eac3656d --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs @@ -0,0 +1,128 @@ +//! Release reconnect reports only after the currently retained phase is durable. + +use super::*; +use lightning_ffor::reestablish::{Reestablish, ReportedState}; + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Keep the existing per-peer ordering and queue. A blocked reconnect retains the entire + /// suffix, including stock updates queued after a durable abort releases the channel fence. + /// Disconnect already clears connection-scoped messages through the ordinary cleanup path. + pub(in crate::ln::channelmanager) fn drain_ffor_pending_messages( + &self, peer: &mut PeerState, output: &mut Vec, + ) { + let mut pending = core::mem::take(&mut peer.pending_msg_events).into_iter(); + while let Some(mut event) = pending.next() { + if let MessageSendEvent::SendChannelReestablish { node_id, msg } = &mut event { + if let Some(queued) = &msg.ffor_reestablish { + let channel = match peer + .channel_by_id + .get(&msg.channel_id) + .and_then(Channel::as_funded) + { + Some(channel) if peer.is_connected => channel, + // Force-close or disconnect invalidates the connection's unsent report. + _ => continue, + }; + match self.durable_ffor_reestablish(channel, *node_id, queued.report()) { + Ok(Some(report)) => { + // Retain the BOLT prefix captured before peer resumption. In particular, + // a received report may already have caused durable abort and stock drain. + msg.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(report)); + }, + Ok(None) => { + peer.pending_msg_events.push(event); + peer.pending_msg_events.extend(pending); + return; + }, + Err(()) => { + // Do not release other queued channel messages after failed recovery + // validation. The normal disconnect path will rebuild eligible work. + output.push(MessageSendEvent::HandleError { + node_id: *node_id, + action: msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id: msg.channel_id, + data: "FFOR reconnect recovery evidence does not match the channel".to_owned(), + }, + }, + }); + return; + }, + } + } + } + output.push(event); + } + } + + /// Called with the peer lock held. Archive and runtime are read together so completion of + /// an older phase cannot release a newer report. No channel or reconnect observation mutates. + fn durable_ffor_reestablish( + &self, channel: &FundedChannel, peer_id: PublicKey, queued: Reestablish, + ) -> Result, ()> { + if channel.context.get_counterparty_node_id() != peer_id { + return Err(()); + } + channel + .ffor_validate_receiver_identity(self.our_network_pubkey, self.chain_hash) + .map_err(|_| ())?; + let setup = channel.ffor_receiver_setup_record().map_err(|_| ())?.ok_or(())?; + let authenticated = setup.validate_recovery().map_err(|_| ())?; + let key = FFORRecoveryKey { + channel_id: channel.context.channel_id(), + epoch_id: authenticated.header().epoch_id, + }; + if key.epoch_id != queued.epoch_id { + return Err(()); + } + let recovery = self.ffor_recovery.lock().unwrap(); + recovery + .validate_channel_outcome( + &setup, + channel.ffor_receiver_fence(), + channel.ffor_receiver_abort_reason(), + ) + .map_err(|_| ())?; + let activation = recovery.get_activation(&key).ok_or(())?; + if channel.context.is_ffor_frozen() + && channel.ffor_frozen_commitments(&self.logger).map_err(|_| ())? + != activation.commitments() + { + return Err(()); + } + let runtime = self.ffor_activation.lock().unwrap(); + let entry = runtime.get(&key).map_err(|_| ())?; + if !self.ffor_persistence.lock().unwrap().is_complete(&entry.requirement) { + return Ok(None); + } + let (state, activation_hash) = if activation.is_aborted() { + (ReportedState::Aborted, [0; 32]) + } else if activation.is_active() { + (ReportedState::Active, activation.activation_hash(&setup).map_err(|_| ())?) + } else { + (ReportedState::Activating, [0; 32]) + }; + Ok(Some(Reestablish { epoch_id: key.epoch_id, state, activation_hash })) + } +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/tests.rs new file mode 100644 index 00000000000..3985bc533fd --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/tests.rs @@ -0,0 +1,316 @@ +use super::*; +use crate::ln::channelmanager::ffor_recovery_tests::restore; +use crate::ln::ffor_tests::quiescence::{complete_handshake, register_signed, request}; +use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; +use crate::ln::functional_test_utils::*; +use lightning_ffor::reestablish::{Reestablish, ReportedState}; + +const EPOCH: [u8; 32] = [81; 32]; + +macro_rules! channel_fixture { + ($sender:ident, $receiver:ident, $id:ident, $receiver_funds:expr) => { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let $id = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let ($sender, $receiver) = + if $receiver_funds { (&nodes[1], &nodes[0]) } else { (&nodes[0], &nodes[1]) }; + }; +} + +fn park(sender: &Node, receiver: &Node, id: ChannelId) { + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + register_signed(sender, receiver, id, voucher); + deliver_parked_voucher(sender, receiver, update); + request(sender, receiver, id).unwrap(); + complete_handshake(sender, receiver); +} + +fn prepare(sender: &Node, receiver: &Node, id: ChannelId) -> FFORPersistenceRequirement { + let snapshot = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); + receiver + .node + .prepare_ffor_receiver_activation(&id, &sender.node.get_our_node_id(), EPOCH, &snapshot) + .unwrap() +} + +fn persist(receiver: &Node) -> Vec { + let token = receiver.node.capture_ffor_persistence(); + let bytes = receiver.node.encode(); + receiver.node.ffor_persistence_completed(token).unwrap(); + bytes +} + +fn ack(sender: &Node, receiver: &Node, id: ChannelId) -> (Vec, [u8; 32]) { + let key = FFORRecoveryKey { channel_id: id, epoch_id: EPOCH }; + let recovery = receiver.node.ffor_recovery.lock().unwrap(); + let setup = recovery.get(&key).unwrap(); + let hash = recovery.get_activation(&key).unwrap().activation_hash(setup).unwrap(); + let mut message = FFORMessage { + header: setup.validate_recovery().unwrap().header(), + payload: Payload::ActivateAck(hash), + extensions: Vec::new(), + signature: [0; 64], + }; + let unsigned = message.unsigned_wire().unwrap(); + message.signature = sender + .keys_manager + .sign_ffor_message(&FFORSigningRequest::new(&unsigned).unwrap()) + .unwrap() + .serialize_compact(); + (message.encode().unwrap(), hash) +} + +fn fence(sender: &Node, receiver: &Node, id: ChannelId) -> Option { + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + peer.channel_by_id + .get(&id) + .unwrap() + .as_funded() + .unwrap() + .ffor_receiver_fence() + .map(|(phase, _)| phase) +} + +#[test] +fn ffor_activation_manager_orders_signing_wire_and_ack_persistence() { + for receiver_funds in [false, true] { + channel_fixture!(sender, receiver, id, receiver_funds); + park(sender, receiver, id); + let peer = sender.node.get_our_node_id(); + receiver.keys_manager.unavailable_ffor_signer.store(true, Ordering::Release); + let snapshot = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); + assert_eq!( + receiver.node.prepare_ffor_receiver_activation(&id, &peer, EPOCH, &snapshot), + Err(FFORReceiverError::SignerUnavailable) + ); + assert_eq!(fence(sender, receiver, id), None); + assert!(receiver + .node + .ffor_recovery + .lock() + .unwrap() + .get_activation(&FFORRecoveryKey { channel_id: id, epoch_id: EPOCH }) + .is_none()); + receiver.keys_manager.unavailable_ffor_signer.store(false, Ordering::Release); + let older = receiver.node.capture_ffor_persistence(); + let requirement = prepare(sender, receiver, id); + assert!(receiver.node.list_usable_channels().is_empty()); + assert!(!receiver.node.list_channels()[0].is_usable); + assert_eq!(prepare(sender, receiver, id), requirement); + receiver.node.ffor_persistence_completed(older).unwrap(); + assert!(!receiver.node.is_ffor_state_persisted(&requirement)); + assert!(!receiver + .node + .release_ffor_receiver_activation(&id, &peer, EPOCH, |_| panic!("unpersisted wire")) + .unwrap()); + drop(receiver.node.capture_ffor_persistence()); + assert!(receiver.node.get_and_clear_needs_persistence()); + persist(receiver); + assert!(receiver.node.is_ffor_state_persisted(&requirement)); + assert!(!receiver + .node + .release_ffor_receiver_activation(&id, &peer, EPOCH, |_| Err(())) + .unwrap()); + let mut activation_wire = Vec::new(); + assert!(receiver + .node + .release_ffor_receiver_activation(&id, &peer, EPOCH, |wire| { + activation_wire = wire.to_vec(); + Ok(()) + }) + .unwrap()); + assert!(matches!( + FFORMessage::decode(&activation_wire).unwrap().payload, + Payload::Activate(_) + )); + assert!(!receiver + .node + .release_ffor_receiver_activation(&id, &peer, EPOCH, |_| panic!("duplicate wire")) + .unwrap()); + let (ack_wire, _) = ack(sender, receiver, id); + let mut corrupt = ack_wire.clone(); + *corrupt.last_mut().unwrap() ^= 1; + assert!(receiver + .node + .accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &corrupt) + .is_err()); + assert_eq!(fence(sender, receiver, id), Some(FFORReceiverFencePhase::Activating)); + let preceding = receiver.node.capture_ffor_persistence(); + let active = receiver + .node + .accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &ack_wire) + .unwrap(); + assert_eq!(fence(sender, receiver, id), Some(FFORReceiverFencePhase::Active)); + assert!(!receiver.node.is_ffor_state_persisted(&active)); + receiver.node.ffor_persistence_completed(preceding).unwrap(); + assert!(!receiver.node.is_ffor_state_persisted(&active)); + assert_eq!( + receiver + .node + .accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &ack_wire) + .unwrap(), + active + ); + persist(receiver); + assert!(receiver.node.is_ffor_state_persisted(&active)); + assert!(!receiver + .node + .release_ffor_receiver_activation(&id, &peer, EPOCH, |_| panic!("active replay")) + .unwrap()); + assert!(receiver.node.get_and_clear_pending_events().is_empty()); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + } +} + +#[test] +fn ffor_activation_manager_refuses_stale_initial_wire_without_expiring_history() { + for height_delta in [6, 7] { + channel_fixture!(sender, receiver, id, false); + park(sender, receiver, id); + prepare(sender, receiver, id); + persist(receiver); + receiver.node.best_block.write().unwrap().height += height_delta; + let sent = receiver.node.release_ffor_receiver_activation( + &id, + &sender.node.get_our_node_id(), + EPOCH, + |_| Ok(()), + ); + if height_delta == 6 { + assert_eq!(sent, Ok(true)); + } else { + assert!(sent.is_err()); + } + let encoded = receiver.node.encode(); + let monitor = get_monitor!(receiver, id).encode(); + let restored = restore(receiver, &encoded, &monitor).unwrap(); + assert!(restored.get_and_clear_needs_persistence()); + assert!(!restored + .release_ffor_receiver_activation( + &id, + &sender.node.get_our_node_id(), + EPOCH, + |_| panic!("restart replay") + ) + .unwrap()); + } +} + +#[test] +fn ffor_activation_manager_recovers_ack_loss_after_restart_and_deadline() { + channel_fixture!(sender, receiver, id, false); + park(sender, receiver, id); + let old_requirement = prepare(sender, receiver, id); + let bytes = persist(receiver); + let monitor = get_monitor!(receiver, id).encode(); + let (ack_wire, hash) = ack(sender, receiver, id); + let restored = restore(receiver, &bytes, &monitor).unwrap(); + assert!(restored.get_event_or_persist_condvar_value()); + assert!(!restored.is_ffor_state_persisted(&old_requirement)); + assert!(restored + .accept_ffor_receiver_activation_ack(&id, &sender.node.get_our_node_id(), EPOCH, &ack_wire) + .is_err()); + let token = restored.capture_ffor_persistence(); + let _saved = restored.encode(); + restored.ffor_persistence_completed(token).unwrap(); + let peer = sender.node.get_our_node_id(); + let init = msgs::Init { + features: sender.init_features(receiver.node.get_our_node_id()), + networks: None, + remote_network_address: None, + }; + restored.peer_connected(peer, &init, false).unwrap(); + sender.node.peer_disconnected(receiver.node.get_our_node_id()); + let receiver_init = msgs::Init { + features: receiver.init_features(peer), + networks: None, + remote_network_address: None, + }; + sender.node.peer_connected(receiver.node.get_our_node_id(), &receiver_init, true).unwrap(); + let mut report = get_event_msg!( + sender, + MessageSendEvent::SendChannelReestablish, + receiver.node.get_our_node_id() + ); + report.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: EPOCH, + state: ReportedState::Active, + activation_hash: hash, + })); + restored.handle_channel_reestablish(peer, &report); + assert!(!restored + .release_ffor_receiver_activation(&id, &peer, EPOCH, |_| panic!("reconnect replay")) + .unwrap()); + restored.best_block.write().unwrap().height = 400; + let active = + restored.accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &ack_wire).unwrap(); + assert!(!restored.is_ffor_state_persisted(&active)); + let token = restored.capture_ffor_persistence(); + let active_bytes = restored.encode(); + restored.ffor_persistence_completed(token).unwrap(); + assert!(restored.is_ffor_state_persisted(&active)); + let recovered_active = restore(receiver, &active_bytes, &monitor).unwrap(); + assert!(!recovered_active.is_ffor_state_persisted(&active)); + let peers = recovered_active.per_peer_state.read().unwrap(); + let peer_state = peers.get(&peer).unwrap().lock().unwrap(); + assert_eq!( + peer_state.channel_by_id.get(&id).unwrap().as_funded().unwrap().ffor_receiver_fence(), + Some((FFORReceiverFencePhase::Active, hash)) + ); +} + +#[test] +fn ffor_activation_manager_persists_reconnect_abort_before_releasing_vouchers() { + for receiver_funds in [false, true] { + channel_fixture!(sender, receiver, id, receiver_funds); + park(sender, receiver, id); + prepare(sender, receiver, id); + persist(receiver); + let (ack_wire, _) = ack(sender, receiver, id); + let peer = sender.node.get_our_node_id(); + sender.node.peer_disconnected(receiver.node.get_our_node_id()); + receiver.node.peer_disconnected(peer); + connect_nodes(sender, receiver); + let report = get_event_msg!( + sender, + MessageSendEvent::SendChannelReestablish, + receiver.node.get_our_node_id() + ); + receiver.node.handle_channel_reestablish(peer, &report); + assert_eq!(fence(sender, receiver, id), Some(FFORReceiverFencePhase::Aborting)); + assert!(!receiver.node.release_ffor_receiver_reconnect_abort(&id, &peer, EPOCH).unwrap()); + assert!(receiver + .node + .accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &ack_wire) + .is_err()); + let aborting_bytes = persist(receiver); + let monitor = get_monitor!(receiver, id).encode(); + let restored = restore(receiver, &aborting_bytes, &monitor).unwrap(); + assert!(!restored.release_ffor_receiver_reconnect_abort(&id, &peer, EPOCH).unwrap()); + let token = restored.capture_ffor_persistence(); + let _saved = restored.encode(); + restored.ffor_persistence_completed(token).unwrap(); + assert!(restored.release_ffor_receiver_reconnect_abort(&id, &peer, EPOCH).unwrap()); + let released = restored.encode(); + assert!(restore(receiver, &released, &monitor).is_ok()); + assert!(receiver.node.release_ffor_receiver_reconnect_abort(&id, &peer, EPOCH).unwrap()); + assert_eq!(fence(sender, receiver, id), None); + assert!(!receiver + .node + .release_ffor_receiver_activation(&id, &peer, EPOCH, |_| panic!("aborted replay")) + .unwrap()); + let messages = receiver.node.get_and_clear_pending_msg_events(); + assert!(messages.iter().all(|event| matches!( + event, + MessageSendEvent::SendChannelReestablish { .. } | MessageSendEvent::UpdateHTLCs { .. } + ))); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + } +} + +mod reestablish; diff --git a/lightning/src/ln/channelmanager/ffor_activation/tests/reestablish.rs b/lightning/src/ln/channelmanager/ffor_activation/tests/reestablish.rs new file mode 100644 index 00000000000..4dc4c14292e --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/tests/reestablish.rs @@ -0,0 +1,193 @@ +use super::*; + +fn reconnect<'a, 'b: 'a, 'c: 'b>( + sender: &Node<'a, 'b, 'c>, receiver: &Node<'a, 'b, 'c>, +) -> msgs::ChannelReestablish { + sender.node.peer_disconnected(receiver.node.get_our_node_id()); + receiver.node.peer_disconnected(sender.node.get_our_node_id()); + connect_nodes(sender, receiver); + get_event_msg!( + sender, + MessageSendEvent::SendChannelReestablish, + receiver.node.get_our_node_id() + ) +} + +fn queued_reestablish(sender: &Node, receiver: &Node) -> msgs::ChannelReestablish { + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + match &peer.pending_msg_events[0] { + MessageSendEvent::SendChannelReestablish { msg, .. } => msg.clone(), + other => panic!("unexpected queued event: {:?}", other), + } +} + +fn queue_suffix(sender: &Node, receiver: &Node) { + let peers = receiver.node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + peer.pending_msg_events.push(MessageSendEvent::SendPeerStorage { + node_id: sender.node.get_our_node_id(), + msg: msgs::PeerStorage { data: vec![42] }, + }); +} + +fn take_report(events: &mut Vec) -> msgs::ChannelReestablish { + match events.remove(0) { + MessageSendEvent::SendChannelReestablish { msg, .. } => msg, + other => panic!("unexpected first event: {:?}", other), + } +} + +#[test] +fn ffor_activation_reestablish_waits_for_latest_phase_and_preserves_peer_order() { + channel_fixture!(sender, receiver, id, false); + park(sender, receiver, id); + prepare(sender, receiver, id); + let activating_write = receiver.node.capture_ffor_persistence(); + let _activating_bytes = receiver.node.encode(); + let (ack_wire, hash) = ack(sender, receiver, id); + let mut remote_report = reconnect(sender, receiver); + let mut expected = queued_reestablish(sender, receiver); + queue_suffix(sender, receiver); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + remote_report.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: EPOCH, + state: ReportedState::Active, + activation_hash: hash, + })); + receiver.node.handle_channel_reestablish(sender.node.get_our_node_id(), &remote_report); + let active = receiver + .node + .accept_ffor_receiver_activation_ack(&id, &sender.node.get_our_node_id(), EPOCH, &ack_wire) + .unwrap(); + receiver.node.ffor_persistence_completed(activating_write).unwrap(); + assert!(!receiver.node.is_ffor_state_persisted(&active)); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + assert_eq!( + queued_reestablish(sender, receiver).ffor_reestablish.unwrap().report().state, + ReportedState::Activating + ); + persist(receiver); + let mut events = receiver.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 2); + expected.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: EPOCH, + state: ReportedState::Active, + activation_hash: hash, + })); + assert_eq!(take_report(&mut events), expected); + assert!( + matches!(&events[0], MessageSendEvent::SendPeerStorage { msg, .. } if msg.data == [42]) + ); +} + +#[test] +fn ffor_activation_reestablish_failed_or_canceled_write_retains_exact_queue() { + channel_fixture!(sender, receiver, id, false); + park(sender, receiver, id); + prepare(sender, receiver, id); + let _remote = reconnect(sender, receiver); + let expected = queued_reestablish(sender, receiver); + queue_suffix(sender, receiver); + for encode_before_failure in [false, true] { + let failed = receiver.node.capture_ffor_persistence(); + if encode_before_failure { + let _not_stored = receiver.node.encode(); + } + drop(failed); + assert!(receiver.node.get_and_clear_needs_persistence()); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + assert_eq!(queued_reestablish(sender, receiver), expected); + } + persist(receiver); + let mut events = receiver.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 2); + assert_eq!(take_report(&mut events), expected); + assert!( + matches!(&events[0], MessageSendEvent::SendPeerStorage { msg, .. } if msg.data == [42]) + ); +} + +#[test] +fn ffor_activation_reestablish_force_close_discards_unsent_report() { + channel_fixture!(sender, receiver, id, false); + park(sender, receiver, id); + prepare(sender, receiver, id); + let _remote = reconnect(sender, receiver); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + receiver + .node + .force_close_broadcasting_latest_txn( + &id, + &sender.node.get_our_node_id(), + "reconnect queue test".into(), + ) + .unwrap(); + let events = receiver.node.get_and_clear_pending_msg_events(); + assert!(!events + .iter() + .any(|event| matches!(event, MessageSendEvent::SendChannelReestablish { .. }))); + assert!(events.iter().any(|event| matches!(event, MessageSendEvent::HandleError { .. }))); + assert!(receiver.node.list_channels().is_empty()); + let closed = receiver.node.get_and_clear_pending_events(); + assert!(closed.iter().any( + |event| matches!(event, events::Event::ChannelClosed { channel_id, .. } if *channel_id == id) + )); + check_added_monitors!(receiver, 1); + persist(receiver); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); +} + +#[test] +fn ffor_activation_reestablish_disconnect_rebind_clears_connection_work() { + channel_fixture!(sender, receiver, id, false); + park(sender, receiver, id); + prepare(sender, receiver, id); + let _remote = reconnect(sender, receiver); + queue_suffix(sender, receiver); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + receiver.node.peer_disconnected(sender.node.get_our_node_id()); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + let init = msgs::Init { + features: sender.init_features(receiver.node.get_our_node_id()), + networks: None, + remote_network_address: None, + }; + receiver.node.peer_connected(sender.node.get_our_node_id(), &init, false).unwrap(); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + persist(receiver); + let mut events = receiver.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + assert_eq!( + take_report(&mut events).ffor_reestablish.unwrap().report(), + Reestablish { epoch_id: EPOCH, state: ReportedState::Activating, activation_hash: [0; 32] } + ); +} + +#[test] +fn ffor_activation_reestablish_terminal_abort_precedes_stock_drain() { + channel_fixture!(sender, receiver, id, false); + park(sender, receiver, id); + prepare(sender, receiver, id); + persist(receiver); + let remote = reconnect(sender, receiver); + let mut expected = queued_reestablish(sender, receiver); + let peer = sender.node.get_our_node_id(); + receiver.node.handle_channel_reestablish(peer, &remote); + assert_eq!(fence(sender, receiver, id), Some(FFORReceiverFencePhase::Aborting)); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + persist(receiver); + assert!(receiver.node.release_ffor_receiver_reconnect_abort(&id, &peer, EPOCH).unwrap()); + assert_eq!(fence(sender, receiver, id), None); + let mut events = receiver.node.get_and_clear_pending_msg_events(); + expected.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: EPOCH, + state: ReportedState::Aborted, + activation_hash: [0; 32], + })); + assert_eq!(take_report(&mut events), expected); + assert_eq!(events.len(), 1); + assert!(matches!(&events[0], MessageSendEvent::UpdateHTLCs { updates, .. } + if updates.update_fail_htlcs.len() == 1 && updates.update_fulfill_htlcs.is_empty())); + check_added_monitors!(receiver, 1); +} diff --git a/lightning/src/ln/channelmanager/ffor_recovery_tests.rs b/lightning/src/ln/channelmanager/ffor_recovery_tests.rs index 526ecdbb1ca..30b83682e67 100644 --- a/lightning/src/ln/channelmanager/ffor_recovery_tests.rs +++ b/lightning/src/ln/channelmanager/ffor_recovery_tests.rs @@ -69,7 +69,7 @@ pub(crate) fn install_ffor_activation_for_test( ) .unwrap(); let mut recovery = receiver.node.ffor_recovery.lock().unwrap(); - let _requirement = receiver.node.ffor_persistence.lock().unwrap().request().unwrap(); + let requirement = receiver.node.ffor_persistence.lock().unwrap().request().unwrap(); let insertion = recovery.prepare_activation(&setup, &activating).unwrap(); channel .install_ffor_fence_for_test(phase, hash, &monitor, current_height, &receiver.logger) @@ -86,6 +86,11 @@ pub(crate) fn install_ffor_activation_for_test( let active = activating.with_ack(&setup, &ack.encode().unwrap()).unwrap(); recovery.prepare_activation(&setup, &active).unwrap().commit(); } + receiver.node.ffor_activation.lock().unwrap().record( + FFORRecoveryKey { channel_id, epoch_id: authenticated.header().epoch_id }, + requirement, + false, + ); } let token = receiver.node.capture_ffor_persistence(); let _persisted = receiver.node.encode(); @@ -171,7 +176,7 @@ fn register(sender: &Node, receiver: &Node, channel_id: ChannelId) -> FFORReceiv .unwrap() } -fn restore<'a, 'b, 'c>( +pub(crate) fn restore<'a, 'b, 'c>( node: &Node<'a, 'b, 'c>, manager: &[u8], monitor: &[u8], ) -> Result, DecodeError> { let (_, monitor) = <(BlockHash, ChannelMonitor)>::read( diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index 3aa8d7bd282..7dedd3fecfb 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -138,6 +138,8 @@ pub enum FFORReceiverError { PersistenceUnavailable, /// Retained recovery storage is full or conflicts with an existing setup. RecoveryUnavailable, + /// The node signer could not supply an authentic signature for the prepared transition. + SignerUnavailable, } impl From for FFORReceiverError { @@ -155,6 +157,7 @@ impl fmt::Display for FFORReceiverError { Self::UnknownEpoch => f.write_str("unknown FFOR receiver epoch"), Self::PersistenceUnavailable => f.write_str("FFOR persistence revision exhausted"), Self::RecoveryUnavailable => f.write_str("FFOR recovery record cannot be retained"), + Self::SignerUnavailable => f.write_str("FFOR node signature unavailable"), } } } diff --git a/lightning/src/ln/ffor_recovery.rs b/lightning/src/ln/ffor_recovery.rs index a0f1861dcf6..ff610a4b938 100644 --- a/lightning/src/ln/ffor_recovery.rs +++ b/lightning/src/ln/ffor_recovery.rs @@ -13,7 +13,7 @@ use bitcoin::secp256k1::PublicKey; use crate::io; use crate::ln::channel::{FFORReceiverFencePhase, FFORReceiverSetup}; -use crate::ln::ffor::FFORMonitorRecoveryIdentity; +use crate::ln::ffor::{FFORMonitorRecoveryIdentity, FFORReceiverAbortReason}; use crate::ln::msgs::DecodeError; use crate::ln::types::ChannelId; use crate::util::ser::{FixedLengthReader, Readable, Writeable, Writer}; @@ -21,6 +21,8 @@ use crate::util::ser::{FixedLengthReader, Readable, Writeable, Writer}; const SETUP_ONLY_VERSION: u8 = 0; // Setup-only readers must never silently discard a possibly active epoch's evidence. const ACTIVATION_VERSION: u8 = 1; +// Earlier activation readers do not understand a retained abort or its relaxed monitor binding. +const ABORTED_ACTIVATION_VERSION: u8 = 2; const MAX_RECORDS: usize = 64; const MAX_ENCODED_BYTES: usize = 8 * 1024 * 1024; // Two maximum wire messages, a 483-slot canonical book, and fixed admission fields fit here. @@ -32,6 +34,7 @@ const RECORD_LENGTH_BYTES: usize = 4; // Includes the maximum acknowledgement plus its vector/TLV framing and growth of the enclosing // activation and record length prefixes. The fixed outer record prefix does not grow. const ACK_RESERVATION_BYTES: usize = lightning_ffor::wire::MAX_MESSAGE_LEN + 64; +const ABORT_RESERVATION_BYTES: usize = lightning_ffor::reestablish::VALUE_LEN + 64; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct FFORRecoveryKey { @@ -99,8 +102,10 @@ impl Entry { } fn ack_reservation(record: &StoredSetup) -> usize { - if record.activation.as_ref().map_or(false, |activation| !activation.is_active()) { - ACK_RESERVATION_BYTES + if record.activation.as_ref().map_or(false, |activation| { + !activation.is_active() && activation.aborted_reason().is_none() + }) { + core::cmp::max(ACK_RESERVATION_BYTES, ABORT_RESERVATION_BYTES) } else { 0 } @@ -186,6 +191,15 @@ impl FFORRecoveryRegistry { /// Neither a missing fence nor a missing acknowledgement may downgrade archived evidence. pub(crate) fn validate_channel_fence( &self, setup: &FFORReceiverSetup, fence: Option<(FFORReceiverFencePhase, [u8; 32])>, + ) -> Result<(), DecodeError> { + self.validate_channel_outcome(setup, fence, None) + } + + /// A terminal abort must match the channel's retained reason. Its Aborting fence remains + /// until the manager persists that outcome; only then may stock voucher drain release it. + pub(crate) fn validate_channel_outcome( + &self, setup: &FFORReceiverSetup, fence: Option<(FFORReceiverFencePhase, [u8; 32])>, + abort_reason: Option, ) -> Result<(), DecodeError> { if !self.contains_exact(setup) { return Err(DecodeError::InvalidValue); @@ -196,9 +210,28 @@ impl FFORRecoveryRegistry { FFORRecoveryKey { channel_id: ChannelId(header.channel_id), epoch_id: header.epoch_id }; match (self.get_activation(&key), fence) { (None, None) => Ok(()), + (Some(record), fence) if record.aborted_reason().is_some() => { + if record.aborted_reason() != abort_reason { + return Err(DecodeError::InvalidValue); + } + match fence { + None => Ok(()), + Some((FFORReceiverFencePhase::Aborting, hash)) + if record.validate(&authenticated)? == hash => + { + Ok(()) + }, + _ => Err(DecodeError::InvalidValue), + } + }, (Some(record), Some((phase, hash))) if record.validate(&authenticated)? == hash - && record.is_active() == (phase == FFORReceiverFencePhase::Active) => + && abort_reason.is_none() + && matches!( + (record.is_active(), phase), + (false, FFORReceiverFencePhase::Activating) + | (true, FFORReceiverFencePhase::Active) + ) => { Ok(()) }, @@ -206,11 +239,27 @@ impl FFORRecoveryRegistry { } } - /// A possibly active epoch requires its original persisted monitor even without a live channel. - pub(crate) fn activation_channels(&self) -> Vec { + /// Every retained activation identity, including terminal outcomes. + pub(crate) fn activation_keys(&self) -> Vec { self.entries .iter() .filter(|entry| entry.record.activation.is_some()) + .map(|entry| entry.key) + .collect() + } + + /// Only unresolved activation evidence requires its original frozen monitor pair. After a + /// retained pre-active abort, live channel recovery follows ordinary monitor and splice rules. + pub(crate) fn activation_channels(&self) -> Vec { + self.entries + .iter() + .filter(|entry| { + entry + .record + .activation + .as_ref() + .map_or(false, |activation| activation.aborted_reason().is_none()) + }) .map(|entry| entry.key.channel_id) .collect() } @@ -265,9 +314,9 @@ impl FFORRecoveryRegistry { Ok(FFORRecoveryInsertion { registry: self, entry: Some(entry) }) } - /// Upgrade an existing identical setup to ACTIVATING, then add only its exact signed ack. + /// Upgrade an existing identical setup to ACTIVATING, then retain its exact ack or abort. /// The caller must install the corresponding native fence while retaining this permit. - /// ACTIVATING reserves capacity for a maximum acknowledgement until its exact bytes arrive. + /// ACTIVATING reserves capacity for either terminal outcome until its exact bytes arrive. pub(crate) fn prepare_activation( &mut self, setup: &FFORReceiverSetup, activation: &FFORReceiverActivation, ) -> Result, FFORRecoveryError> { @@ -287,7 +336,7 @@ impl FFORRecoveryRegistry { if existing.record.setup.encode() != setup.encode() || match existing.record.activation.as_ref() { Some(previous) => !previous.can_replace(activation), - None => activation.is_active(), + None => activation.is_active() || activation.aborted_reason().is_some(), } { return Err(FFORRecoveryError::ConflictingRecord); } @@ -311,7 +360,15 @@ impl FFORRecoveryRegistry { } fn version(&self) -> u8 { - if self.entries.iter().any(|entry| entry.record.activation.is_some()) { + if self.entries.iter().any(|entry| { + entry + .record + .activation + .as_ref() + .map_or(false, |activation| activation.aborted_reason().is_some()) + }) { + ABORTED_ACTIVATION_VERSION + } else if self.entries.iter().any(|entry| entry.record.activation.is_some()) { ACTIVATION_VERSION } else { SETUP_ONLY_VERSION @@ -352,7 +409,10 @@ impl Writeable for FFORRecoveryRegistry { impl Readable for FFORRecoveryRegistry { fn read(reader: &mut R) -> Result { let version = u8::read(reader)?; - if version != SETUP_ONLY_VERSION && version != ACTIVATION_VERSION { + if version != SETUP_ONLY_VERSION + && version != ACTIVATION_VERSION + && version != ABORTED_ACTIVATION_VERSION + { return Err(DecodeError::UnknownRequiredFeature); } let count = u16::read(reader)? as usize; diff --git a/lightning/src/ln/ffor_recovery/activation.rs b/lightning/src/ln/ffor_recovery/activation.rs index 8e036908653..ddfb637e7ee 100644 --- a/lightning/src/ln/ffor_recovery/activation.rs +++ b/lightning/src/ln/ffor_recovery/activation.rs @@ -7,12 +7,16 @@ use alloc::vec::Vec; use bitcoin::hashes::Hash; use bitcoin::{ScriptBuf, Txid}; +use lightning_ffor::reestablish::{Reestablish, ReportedState}; use lightning_ffor::setup::AuthenticatedSetup; use lightning_ffor::transcript; use lightning_ffor::wire::{Message, Payload, MAX_MESSAGE_LEN}; use crate::ln::channel::{FFORReceiverSetup, INITIAL_COMMITMENT_NUMBER}; -use crate::ln::ffor::{FFORMonitorRecoveryIdentity, FFORMonitorSnapshot, FFORVoucherCommitments}; +use crate::ln::ffor::{ + FFORCommitment, FFORMonitorRecoveryIdentity, FFORMonitorSnapshot, FFORReceiverAbortReason, + FFORVoucherCommitments, +}; use crate::ln::msgs::DecodeError; use crate::util::ser::Writeable; @@ -22,6 +26,7 @@ use crate::util::ser::Writeable; pub(crate) struct FFORReceiverActivation { activate_wire: Vec, ack_wire: Option>, + abort: Option, receiver_number: u64, receiver_txid: Txid, settlement_number: u64, @@ -41,8 +46,40 @@ impl_writeable_tlv_based!(FFORReceiverActivation, { (12, monitor_update_id, required), (14, preparation_height, required), (16, destination_script, required), + (18, abort, option), }); +/// A manager-observed reconnect outcome, not a signed statement from the peer. The transition +/// owner must authenticate the connection before constructing this terminal consistency record. +#[derive(Clone)] +struct AbortedActivation { + reason: FFORReceiverAbortReason, + peer_report: Option>, +} + +impl_writeable_tlv_based!(AbortedActivation, { + (0, reason, required), + (2, peer_report, option), +}); + +impl AbortedActivation { + fn validate(&self) -> Result<(), DecodeError> { + if self.reason != FFORReceiverAbortReason::Disconnected { + return Err(DecodeError::InvalidValue); + } + if let Some(bytes) = self.peer_report.as_ref() { + let report = Reestablish::decode(bytes).map_err(|_| DecodeError::InvalidValue)?; + if matches!( + report.state, + ReportedState::Active | ReportedState::Draining | ReportedState::Closed + ) { + return Err(DecodeError::InvalidValue); + } + } + Ok(()) + } +} + impl FFORReceiverActivation { /// Called only after the channel rechecks both commitment views under owned quiescence. /// The opaque monitor supplies its original destination; callers cannot substitute a script. @@ -67,6 +104,7 @@ impl FFORReceiverActivation { let record = Self { activate_wire: activate_wire.to_vec(), ack_wire: None, + abort: None, receiver_number: commitments.holder.number, receiver_txid: commitments.holder.txid, settlement_number: commitments.counterparty.number, @@ -109,6 +147,12 @@ impl FFORReceiverActivation { let hash = setup .validate_activation(&activate, self.commitment_hash(), self.preparation_height) .map_err(|_| DecodeError::InvalidValue)?; + if let Some(abort) = self.abort.as_ref() { + if self.ack_wire.is_some() { + return Err(DecodeError::InvalidValue); + } + abort.validate()?; + } if let Some(wire) = self.ack_wire.as_ref() { let ack = Message::decode(wire).map_err(|_| DecodeError::InvalidValue)?; setup.validate_activation_ack(&ack, hash).map_err(|_| DecodeError::InvalidValue)?; @@ -130,7 +174,8 @@ impl FFORReceiverActivation { pub(crate) fn with_ack( &self, setup: &FFORReceiverSetup, ack_wire: &[u8], ) -> Result { - if ack_wire.len() > MAX_MESSAGE_LEN + if self.abort.is_some() + || ack_wire.len() > MAX_MESSAGE_LEN || self.ack_wire.as_ref().map_or(false, |existing| existing != ack_wire) { return Err(DecodeError::InvalidValue); @@ -145,10 +190,73 @@ impl FFORReceiverActivation { self.ack_wire.is_some() } + /// Permanently retain a pre-active abort observed on an authenticated reconnect. Absence of + /// an epoch or a pre-active/aborted peer report permits this outcome; an active or later report + /// leaves the obligation unresolved even if its epoch or hash differs. The report itself is + /// unsigned and cannot establish that the caller actually observed that connection. + pub(crate) fn abort_after_reestablish( + &self, setup: &FFORReceiverSetup, peer_report: Option, + ) -> Result { + if self.ack_wire.is_some() { + return Err(DecodeError::InvalidValue); + } + let abort = AbortedActivation { + reason: FFORReceiverAbortReason::Disconnected, + peer_report: peer_report.map(|report| report.encode().to_vec()), + }; + abort.validate()?; + if self.abort.as_ref().map_or(false, |previous| previous.encode() != abort.encode()) { + return Err(DecodeError::InvalidValue); + } + let mut record = self.clone(); + record.abort = Some(abort); + record.validate(&setup.validate_recovery()?)?; + Ok(record) + } + + pub(crate) fn aborted_reason(&self) -> Option { + self.abort.as_ref().map(|abort| abort.reason) + } + + pub(crate) fn is_aborted(&self) -> bool { + self.abort.is_some() + } + + pub(crate) fn matches_abort_report(&self, peer_report: Option) -> bool { + self.abort.as_ref().map_or(false, |abort| { + abort.peer_report == peer_report.map(|report| report.encode().to_vec()) + }) + } + + pub(crate) fn activation_hash( + &self, setup: &FFORReceiverSetup, + ) -> Result<[u8; 32], DecodeError> { + self.validate(&setup.validate_recovery()?) + } + + pub(crate) fn activate_wire(&self) -> &[u8] { + &self.activate_wire + } + + pub(crate) fn ack_wire(&self) -> Option<&[u8]> { + self.ack_wire.as_deref() + } + + pub(crate) fn commitments(&self) -> FFORVoucherCommitments { + FFORVoucherCommitments { + holder: FFORCommitment { number: self.receiver_number, txid: self.receiver_txid }, + counterparty: FFORCommitment { + number: self.settlement_number, + txid: self.settlement_txid, + }, + } + } + pub(super) fn validate_monitor( &self, setup: &FFORReceiverSetup, monitor: &FFORMonitorRecoveryIdentity, ) -> Result<(), DecodeError> { - if monitor.channel_id.0 != setup.validate_recovery()?.header().channel_id + if self.abort.is_some() + || monitor.channel_id.0 != setup.validate_recovery()?.header().channel_id || monitor.funding_txo != setup.funding_txo() || monitor.update_id < self.monitor_update_id || INITIAL_COMMITMENT_NUMBER.checked_sub(monitor.holder_number) @@ -164,17 +272,22 @@ impl FFORReceiverActivation { Ok(()) } - /// The only upgrade preserves every byte except adding the first acknowledgement. + /// The only upgrade adds the first acknowledgement or terminal abort, preserving every byte + /// of the activation. Neither terminal outcome can be replaced by another transition. pub(super) fn can_replace(&self, next: &Self) -> bool { if self.encode() == next.encode() { return true; } - if self.ack_wire.is_some() || next.ack_wire.is_none() { + if self.ack_wire.is_some() + || self.abort.is_some() + || (next.ack_wire.is_none() && next.abort.is_none()) + { return false; } - let mut without_ack = next.clone(); - without_ack.ack_wire = None; - self.encode() == without_ack.encode() + let mut without_outcome = next.clone(); + without_outcome.ack_wire = None; + without_outcome.abort = None; + self.encode() == without_outcome.encode() } } diff --git a/lightning/src/ln/ffor_recovery/activation/tests.rs b/lightning/src/ln/ffor_recovery/activation/tests.rs index 8c7d3a169e8..91659a82d6c 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests.rs @@ -8,6 +8,8 @@ use crate::ln::types::ChannelId; use crate::util::ser::Readable; use lightning_ffor::wire::{Activate, Tlv}; +mod aborted; + // Synthetic transaction identities exercise archive authentication only. Real monitor and // channel evidence are exercised by the manager tests; these values never activate a channel. fn evidence(maximum_size: bool) -> (FFORReceiverSetup, FFORReceiverActivation) { @@ -49,6 +51,7 @@ fn evidence_for_identity( let record = FFORReceiverActivation { activate_wire: message.encode().unwrap(), ack_wire: None, + abort: None, receiver_number: 4, receiver_txid, settlement_number: 4, diff --git a/lightning/src/ln/ffor_recovery/activation/tests/aborted.rs b/lightning/src/ln/ffor_recovery/activation/tests/aborted.rs new file mode 100644 index 00000000000..cd787e08452 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/tests/aborted.rs @@ -0,0 +1,235 @@ +use super::*; +use crate::ln::channel::FFORReceiverFencePhase; +use crate::ln::ffor_recovery::{ABORTED_ACTIVATION_VERSION, ABORT_RESERVATION_BYTES}; + +fn report(setup: &FFORReceiverSetup, state: ReportedState) -> Reestablish { + Reestablish { epoch_id: key(setup).epoch_id, state, activation_hash: [0; 32] } +} + +fn abort_registry( + setup: &FFORReceiverSetup, activating: &FFORReceiverActivation, + peer_report: Option, +) -> FFORRecoveryRegistry { + let mut registry = registered(setup); + registry.prepare_activation(setup, activating).unwrap().commit(); + let aborted = activating.abort_after_reestablish(setup, peer_report).unwrap(); + registry.prepare_activation(setup, &aborted).unwrap().commit(); + registry +} + +#[test] +fn ffor_activation_abort_retains_exact_history_and_unsigned_reconnect_observation() { + let (setup, activating) = evidence(false); + let mut reports = vec![None]; + for state in [ + ReportedState::Negotiating, + ReportedState::VouchersCommitted, + ReportedState::Activating, + ReportedState::Aborted, + ] { + let expected = report(&setup, state); + reports.push(Some(expected)); + // A pre-active report may name a different epoch or retain Beignet's pre-active hash. + let mut other = expected; + other.epoch_id[0] ^= 1; + other.activation_hash = [7; 32]; + reports.push(Some(other)); + } + for peer_report in reports { + let aborted = activating.abort_after_reestablish(&setup, peer_report).unwrap(); + assert!(aborted.is_aborted()); + assert!(!aborted.is_active()); + assert_eq!(aborted.aborted_reason(), Some(FFORReceiverAbortReason::Disconnected)); + assert!(aborted.matches_abort_report(peer_report)); + assert!(!activating.matches_abort_report(peer_report)); + assert_eq!(aborted.activate_wire(), activating.activate_wire()); + assert_eq!(aborted.commitments(), activating.commitments()); + assert_eq!( + aborted.activation_hash(&setup).unwrap(), + activating.activation_hash(&setup).unwrap() + ); + assert!(aborted.ack_wire().is_none()); + assert!(activating.can_replace(&aborted)); + assert!(!aborted.can_replace(&activating)); + assert_eq!( + aborted.abort_after_reestablish(&setup, peer_report).unwrap().encode(), + aborted.encode() + ); + assert!(aborted.with_ack(&setup, &acknowledgement(&setup, &activating, false)).is_err()); + let alternate = Some(report(&setup, ReportedState::Negotiating)); + if alternate != peer_report { + assert!(!aborted.matches_abort_report(alternate)); + assert!(aborted.abort_after_reestablish(&setup, alternate).is_err()); + } + } +} + +#[test] +fn ffor_activation_abort_never_discards_active_or_later_obligations() { + let (setup, activating) = evidence(false); + let ack = acknowledgement(&setup, &activating, false); + let active = activating.with_ack(&setup, &ack).unwrap(); + assert_eq!(active.ack_wire(), Some(&ack[..])); + assert!(active.abort_after_reestablish(&setup, None).is_err()); + for state in [ReportedState::Active, ReportedState::Draining, ReportedState::Closed] { + for changed_identity in [false, true] { + let mut peer_report = report(&setup, state); + peer_report.activation_hash = activating.activation_hash(&setup).unwrap(); + if changed_identity { + peer_report.epoch_id[0] ^= 1; + peer_report.activation_hash[0] ^= 1; + } + assert!(activating.abort_after_reestablish(&setup, Some(peer_report)).is_err()); + } + } +} + +#[test] +fn ffor_activation_abort_is_monotonic_atomic_and_permanent() { + let (setup, activating) = evidence(false); + let aborted = activating.abort_after_reestablish(&setup, None).unwrap(); + let active = activating.with_ack(&setup, &acknowledgement(&setup, &activating, false)).unwrap(); + let mut registry = registered(&setup); + assert!(matches!( + registry.prepare_activation(&setup, &aborted), + Err(FFORRecoveryError::ConflictingRecord) + )); + registry.prepare_activation(&setup, &activating).unwrap().commit(); + let before = registry.encode(); + drop(registry.prepare_activation(&setup, &aborted).unwrap()); + assert_eq!(registry.encode(), before); + assert_eq!(registry.reserved_ack_bytes, ACK_RESERVATION_BYTES); + registry.prepare_activation(&setup, &aborted).unwrap().commit(); + let terminal = registry.encode(); + let different = activating + .abort_after_reestablish(&setup, Some(report(&setup, ReportedState::Aborted))) + .unwrap(); + for refused in [&activating, &active, &different] { + assert!(matches!( + registry.prepare_activation(&setup, refused), + Err(FFORRecoveryError::ConflictingRecord) + )); + assert_eq!(registry.encode(), terminal); + } + registry.prepare_activation(&setup, &aborted).unwrap().commit(); + assert_eq!(registry.encode(), terminal); + assert_eq!(registry.reserved_ack_bytes, 0); + let mut active_registry = registered(&setup); + active_registry.prepare_activation(&setup, &activating).unwrap().commit(); + active_registry.prepare_activation(&setup, &active).unwrap().commit(); + assert!(matches!( + active_registry.prepare_activation(&setup, &aborted), + Err(FFORRecoveryError::ConflictingRecord) + )); +} + +#[test] +fn ffor_activation_abort_restore_requires_terminal_schema_and_matching_channel_reason() { + let (setup, activating) = evidence(false); + let peer_report = Some(report(&setup, ReportedState::Aborted)); + let registry = abort_registry(&setup, &activating, peer_report); + let bytes = registry.encode(); + assert_eq!(bytes[0], ABORTED_ACTIVATION_VERSION); + let restored = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); + assert_eq!(restored.encode(), bytes); + assert_eq!(restored.encoded_bytes, bytes.len()); + assert_eq!(restored.reserved_ack_bytes, 0); + assert_eq!(restored.activation_keys(), vec![key(&setup)]); + assert!(restored.activation_channels().is_empty()); + assert!(restored.get_activation(&key(&setup)).unwrap().matches_abort_report(peer_report)); + let hash = activating.activation_hash(&setup).unwrap(); + for fence in [None, Some((FFORReceiverFencePhase::Aborting, hash))] { + assert!(restored + .validate_channel_outcome(&setup, fence, Some(FFORReceiverAbortReason::Disconnected)) + .is_ok()); + for reason in [ + None, + Some(FFORReceiverAbortReason::Requested), + Some(FFORReceiverAbortReason::Restarted), + ] { + assert!(restored.validate_channel_outcome(&setup, fence, reason).is_err()); + } + } + assert!(restored.validate_channel_fence(&setup, None).is_err()); + for phase in [FFORReceiverFencePhase::Activating, FFORReceiverFencePhase::Active] { + assert!(restored + .validate_channel_outcome( + &setup, + Some((phase, hash)), + Some(FFORReceiverAbortReason::Disconnected) + ) + .is_err()); + } + let mut wrong_hash = hash; + wrong_hash[0] ^= 1; + assert!(restored + .validate_channel_outcome( + &setup, + Some((FFORReceiverFencePhase::Aborting, wrong_hash)), + Some(FFORReceiverAbortReason::Disconnected) + ) + .is_err()); + for version in [SETUP_ONLY_VERSION, ACTIVATION_VERSION] { + let mut downgrade = bytes.clone(); + downgrade[0] = version; + assert!(FFORRecoveryRegistry::read(&mut &downgrade[..]).is_err()); + } + let mut missing = registered(&setup); + missing.prepare_activation(&setup, &activating).unwrap().commit(); + let mut missing = missing.encode(); + missing[0] = ABORTED_ACTIVATION_VERSION; + assert!(FFORRecoveryRegistry::read(&mut &missing[..]).is_err()); +} + +#[test] +fn ffor_activation_abort_rejects_corrupt_reason_report_and_simultaneous_ack() { + let (setup, activating) = evidence(false); + let aborted = activating.abort_after_reestablish(&setup, None).unwrap(); + let authenticated = setup.validate_recovery().unwrap(); + let mut corrupt = aborted.clone(); + corrupt.abort.as_mut().unwrap().reason = FFORReceiverAbortReason::Requested; + assert!(corrupt.validate(&authenticated).is_err()); + let mut corrupt = aborted.clone(); + corrupt.ack_wire = Some(acknowledgement(&setup, &activating, false)); + assert!(corrupt.validate(&authenticated).is_err()); + let valid_report = report(&setup, ReportedState::Aborted).encode(); + let mut malformed = vec![vec![], valid_report[..66].to_vec(), vec![0; 68]]; + let mut bad_state = valid_report.to_vec(); + bad_state[32] = 7; + malformed.push(bad_state); + let mut bad_sequence = valid_report.to_vec(); + bad_sequence[34] = 1; + malformed.push(bad_sequence); + malformed.push(report(&setup, ReportedState::Active).encode().to_vec()); + for bytes in malformed { + let mut corrupt = aborted.clone(); + corrupt.abort.as_mut().unwrap().peer_report = Some(bytes); + assert!(corrupt.validate(&authenticated).is_err()); + let mut registry = abort_registry(&setup, &activating, None); + registry.entries[0].record.activation = Some(corrupt); + registry.entries[0].encoded_bytes = registry.entries[0].record.serialized_length(); + assert!(FFORRecoveryRegistry::read(&mut ®istry.encode()[..]).is_err()); + } +} + +#[test] +fn ffor_activation_abort_has_reserved_capacity_after_restart_and_competing_admissions() { + let (setup, activating) = evidence(false); + let mut registry = registered(&setup); + registry.prepare_activation(&setup, &activating).unwrap().commit(); + fill_ack_capacity(&mut registry); + let before = registry.encode(); + let mut restored = FFORRecoveryRegistry::read(&mut &before[..]).unwrap(); + let reserved = restored.reserved_ack_bytes; + let aborted = activating + .abort_after_reestablish(&setup, Some(report(&setup, ReportedState::Aborted))) + .unwrap(); + restored.prepare_activation(&setup, &aborted).unwrap().commit(); + assert_eq!(restored.reserved_ack_bytes, reserved - ACK_RESERVATION_BYTES); + assert!(restored.encoded_bytes - before.len() < ABORT_RESERVATION_BYTES); + assert!(restored.encoded_bytes + restored.reserved_ack_bytes <= MAX_ENCODED_BYTES); + let terminal = restored.encode(); + let restored = FFORRecoveryRegistry::read(&mut &terminal[..]).unwrap(); + assert_eq!(restored.reserved_ack_bytes, reserved - ACK_RESERVATION_BYTES); + assert_eq!(restored.encode(), terminal); +} diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs index 2e36f6e3fb3..7ecb43ca3f6 100644 --- a/lightning/src/ln/ffor_recovery/tests.rs +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -293,7 +293,7 @@ fn ffor_recovery_reader_refuses_future_schema_missing_fields_and_truncation() { assert!(decode(&framed[..length]).is_err(), "accepted prefix {}", length); } let mut future = framed.clone(); - future[0] = 2; + future[0] = ABORTED_ACTIVATION_VERSION + 1; assert!(matches!(decode(&future), Err(DecodeError::UnknownRequiredFeature))); // Empty TLV records omit both mandatory fields. assert!(decode(&frame(&[vec![0]])).is_err()); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 81fa2fc41fa..6e23cc36595 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -909,6 +909,47 @@ pub struct UpdateFee { pub feerate_per_kw: u32, } +/// The fixed Variant D reconnect report carried by TLV 55001. +/// +/// This unsigned peer statement grants no activation or payment authority. Locally emitted +/// reports must come from the channel engine's retained epoch. The ordinary BOLT 2 counters +/// in [`ChannelReestablish`] still require their normal validation. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub struct FFORChannelReestablish([u8; lightning_ffor::reestablish::VALUE_LEN]); + +impl FFORChannelReestablish { + /// Wrap a structurally valid Variant D report, preserving its reported hash exactly. + /// This constructor does not authenticate the report or authorize a channel transition. + pub fn new(report: lightning_ffor::reestablish::Reestablish) -> Self { + Self(report.encode()) + } + + /// Read the reported epoch and state. This is untrusted peer data until the engine checks it. + pub fn report(&self) -> lightning_ffor::reestablish::Reestablish { + lightning_ffor::reestablish::Reestablish::decode(&self.0) + .expect("FFOR reconnect wrapper contains a validated fixed value") + } +} + +impl Writeable for FFORChannelReestablish { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + writer.write_all(&self.0) + } + fn serialized_length(&self) -> usize { + lightning_ffor::reestablish::VALUE_LEN + } +} + +impl Readable for FFORChannelReestablish { + fn read(reader: &mut R) -> Result { + let mut value = [0; lightning_ffor::reestablish::VALUE_LEN]; + reader.read_exact(&mut value)?; + lightning_ffor::reestablish::Reestablish::decode(&value) + .map_err(|_| DecodeError::InvalidValue)?; + Ok(Self(value)) + } +} + /// A [`channel_reestablish`] message to be sent to or received from a peer. /// /// [`channel_reestablish`]: https://github.com/lightning/bolts/blob/master/02-peer-protocol.md#message-retransmission @@ -946,6 +987,8 @@ pub struct ChannelReestablish { /// /// Also contains a bitfield indicating which messages should be retransmitted. pub my_current_funding_locked: Option, + /// The optional unsigned FFOR Variant D epoch report, carried in TLV 55001. + pub ffor_reestablish: Option, } /// Information exchanged during channel reestablishment about the next funding from interactive @@ -2937,6 +2980,7 @@ impl_writeable_msg!(ChannelReestablish, { }, { (1, next_funding, option), (5, my_current_funding_locked, option), + (55001, ffor_reestablish, option), }); impl_writeable!(NextFunding, { @@ -3671,7 +3715,8 @@ where }), } => { if amt.is_some() - || cltv_value.is_some() || total_msat.is_some() + || cltv_value.is_some() + || total_msat.is_some() || keysend_preimage.is_some() || invoice_request.is_some() { @@ -3821,7 +3866,8 @@ where }), } => { if amt.is_some() - || cltv_value.is_some() || total_msat.is_some() + || cltv_value.is_some() + || total_msat.is_some() || keysend_preimage.is_some() || invoice_request.is_some() { @@ -4411,6 +4457,7 @@ mod tests { }; let cr = msgs::ChannelReestablish { + ffor_reestablish: None, channel_id: ChannelId::from_bytes([ 4, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, @@ -4457,6 +4504,7 @@ mod tests { }; let cr = msgs::ChannelReestablish { + ffor_reestablish: None, channel_id: ChannelId::from_bytes([ 4, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, @@ -4516,6 +4564,7 @@ mod tests { }; let cr = msgs::ChannelReestablish { + ffor_reestablish: None, channel_id: ChannelId::from_bytes([ 4, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, @@ -6753,3 +6802,6 @@ mod tests { .is_err()); } } + +#[cfg(test)] +mod ffor_reestablish_tests; diff --git a/lightning/src/ln/msgs/ffor_reestablish_tests.rs b/lightning/src/ln/msgs/ffor_reestablish_tests.rs new file mode 100644 index 00000000000..a47f5cb36f9 --- /dev/null +++ b/lightning/src/ln/msgs/ffor_reestablish_tests.rs @@ -0,0 +1,85 @@ +use super::*; +use lightning_ffor::reestablish::{Reestablish, ReportedState}; + +fn base() -> ChannelReestablish { + ChannelReestablish { + channel_id: ChannelId::from_bytes([3; 32]), + next_local_commitment_number: 2, + next_remote_commitment_number: 1, + your_last_per_commitment_secret: [4; 32], + my_current_per_commitment_point: PublicKey::from_slice(&[2; 33]).unwrap(), + next_funding: None, + my_current_funding_locked: None, + ffor_reestablish: None, + } +} + +#[test] +fn ffor_reestablish_tlv_has_exact_bolt_framing_and_shared_value() { + let ordinary = base(); + let original = ordinary.encode(); + assert_eq!(original.len(), 113); + assert_eq!( + ChannelReestablish::read_from_fixed_length_buffer(&mut &original[..]).unwrap(), + ordinary + ); + for state in [ + ReportedState::Negotiating, + ReportedState::VouchersCommitted, + ReportedState::Activating, + ReportedState::Active, + ReportedState::Draining, + ReportedState::Closed, + ReportedState::Aborted, + ] { + // Preserve an incoming pre-active hash, including the pinned reference's encoding. + let report = Reestablish { epoch_id: [5; 32], state, activation_hash: [6; 32] }; + let mut message = ordinary.clone(); + message.ffor_reestablish = Some(FFORChannelReestablish::new(report)); + let bytes = message.encode(); + let mut expected = original.clone(); + expected.extend_from_slice(&[0xfd, 0xd6, 0xd9, 67]); + expected.extend_from_slice(&report.encode()); + assert_eq!(bytes, expected); + let restored = ChannelReestablish::read_from_fixed_length_buffer(&mut &bytes[..]).unwrap(); + assert_eq!(restored, message); + assert_eq!(restored.ffor_reestablish.unwrap().report(), report); + } +} + +#[test] +fn ffor_reestablish_tlv_rejects_length_state_sequence_and_duplicate_fields() { + let prefix = base().encode(); + let report = + Reestablish { epoch_id: [5; 32], state: ReportedState::Active, activation_hash: [6; 32] }; + for fault in 0..6 { + let mut value = report.encode().to_vec(); + match fault { + 0 => { + value.pop(); + }, + 1 => value.push(0), + 2 => value[32] = 7, + 3 => value[33] = 1, + 4 => value[34] = 1, + _ => {}, + } + let mut bytes = prefix.clone(); + bytes.extend_from_slice(&[0xfd, 0xd6, 0xd9, value.len() as u8]); + bytes.extend_from_slice(&value); + if fault == 5 { + bytes.extend_from_slice(&[0xfd, 0xd6, 0xd9, 67]); + bytes.extend_from_slice(&report.encode()); + } + assert!( + ChannelReestablish::read_from_fixed_length_buffer(&mut &bytes[..]).is_err(), + "fault {fault}" + ); + } + let mut valid = prefix; + valid.extend_from_slice(&[0xfd, 0xd6, 0xd9, 67]); + valid.extend_from_slice(&report.encode()); + for length in 114..valid.len() { + assert!(ChannelReestablish::read_from_fixed_length_buffer(&mut &valid[..length]).is_err()); + } +} diff --git a/lightning/src/util/dyn_signer.rs b/lightning/src/util/dyn_signer.rs index cf1cac37903..1359441a8b4 100644 --- a/lightning/src/util/dyn_signer.rs +++ b/lightning/src/util/dyn_signer.rs @@ -214,6 +214,7 @@ inner, fn get_node_id(, recipient: Recipient) -> Result, fn sign_gossip_message(, msg: UnsignedGossipMessage) -> Result, fn sign_message(, msg: &[u8]) -> Result, + fn sign_ffor_message(, request: &crate::sign::ffor::FFORSigningRequest<'_>) -> Result, fn ecdh(, recipient: Recipient, other_key: &PublicKey, tweak: Option<&Scalar>) -> Result, fn sign_invoice(, invoice: &RawBolt11Invoice, recipient: Recipient) -> Result, fn sign_bolt12_invoice(, @@ -282,6 +283,7 @@ delegate!(DynPhantomKeysInterface, NodeSigner, fn get_node_id(, recipient: Recipient) -> Result, fn sign_gossip_message(, msg: UnsignedGossipMessage) -> Result, fn sign_message(, msg: &[u8]) -> Result, + fn sign_ffor_message(, request: &crate::sign::ffor::FFORSigningRequest<'_>) -> Result, fn ecdh(, recipient: Recipient, other_key: &PublicKey, tweak: Option<&Scalar>) -> Result, fn sign_invoice(, invoice: &RawBolt11Invoice, recipient: Recipient) -> Result, fn sign_bolt12_invoice(, invoice: &crate::offers::invoice::UnsignedBolt12Invoice diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 5f1728b80b9..7b0db4fbbe5 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -1801,6 +1801,7 @@ impl NodeSigner for TestNodeSigner { } pub struct TestKeysInterface { + pub unavailable_ffor_signer: AtomicBool, pub unavailable_node_ecdh: AtomicBool, pub backing: DynKeysInterface, pub override_random_bytes: Mutex>, @@ -1824,6 +1825,15 @@ impl EntropySource for TestKeysInterface { } impl NodeSigner for TestKeysInterface { + fn sign_ffor_message( + &self, request: &crate::sign::ffor::FFORSigningRequest<'_>, + ) -> Result { + if self.unavailable_ffor_signer.load(Ordering::Acquire) { + return Err(()); + } + self.backing.sign_ffor_message(request) + } + fn get_node_id(&self, recipient: Recipient) -> Result { self.backing.get_node_id(recipient) } @@ -1955,6 +1965,7 @@ impl TestKeysInterface { fn build(backing: Box>) -> Self { Self { backing: DynKeysInterface::new(backing), + unavailable_ffor_signer: AtomicBool::new(false), unavailable_node_ecdh: AtomicBool::new(false), override_random_bytes: Mutex::new(None), disable_revocation_policy_check: false, From 1d6ed49ca9dae373f9b46bd0f41eca968c323456 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 23:03:32 -0400 Subject: [PATCH 09/22] feat(ffor): authenticate witness fetch pages and encrypted records --- lightning-ffor/README.md | 56 ++- lightning-ffor/fuzz/README.md | 8 +- lightning-ffor/fuzz/fuzz_targets/witness.rs | 19 +- lightning-ffor/fuzz/seed_corpus.py | 9 + lightning-ffor/src/setup.rs | 15 + lightning-ffor/src/wire/mod.rs | 2 +- lightning-ffor/src/wire/tlv.rs | 42 +- lightning-ffor/src/witness/fetch.rs | 255 ++++++++++ .../src/witness/fetch_correlation.rs | 148 ++++++ lightning-ffor/src/witness/mod.rs | 43 +- lightning-ffor/src/witness/record.rs | 220 ++++++++ .../tests/data/beignet-witness-fetch.json | 216 ++++++++ .../data/generate_beignet_witness_fetch.cjs | 86 ++++ lightning-ffor/tests/setup.rs | 21 + lightning-ffor/tests/witness.rs | 3 + lightning-ffor/tests/witness/fetch.rs | 476 ++++++++++++++++++ 16 files changed, 1585 insertions(+), 34 deletions(-) create mode 100644 lightning-ffor/src/witness/fetch.rs create mode 100644 lightning-ffor/src/witness/fetch_correlation.rs create mode 100644 lightning-ffor/src/witness/record.rs create mode 100644 lightning-ffor/tests/data/beignet-witness-fetch.json create mode 100644 lightning-ffor/tests/data/generate_beignet_witness_fetch.cjs create mode 100644 lightning-ffor/tests/witness/fetch.rs diff --git a/lightning-ffor/README.md b/lightning-ffor/README.md index 71c2766bfa3..f169020d219 100644 --- a/lightning-ffor/README.md +++ b/lightning-ffor/README.md @@ -90,7 +90,7 @@ The standalone [fuzz target](fuzz/README.md) exercises parsing, canonical round signatures and authenticated setup with real signed fixture seeds. A bounded local run completed 6,359,059 inputs in 121 seconds without a crash; a run including the reconnect parser completed 2,118,228 inputs in 31 seconds without a crash. The current -suite has 66 tests and four doctests. Nightly LLVM instrumentation measured 865/892 +suite at that checkpoint had 66 tests and four doctests. Nightly LLVM instrumentation measured 865/892 source lines (96.97%) and 198/200 branches (99.00%) covered across this crate. Uncovered code includes diagnostic formatting and defensive paths whose preconditions are excluded by prior validated bounds. These are measured results, not exhaustive @@ -170,7 +170,7 @@ fee funding. No production window or witness deployment is chosen by this crate. ## Receiver witness provisioning primitives -The `witness` module implements only version 1/D-R manifest construction, receiver-side +The `witness` module implements version 1/D-R manifest construction, receiver-side decoding against `AuthenticatedSetup`, fetch-key signature authentication, and Appendix F.1 `ff_witness_provision`/`ff_witness_ack`. It reuses the authenticated canonical book and transcript hashes. Timestamp-style voucher expiries, short retention, mismatched setup/book/activation @@ -191,7 +191,7 @@ supply transport identity from its actual handshake, persist registration keys a manifest before sending, persist the correlated acknowledgement, and bind it to the engine's current ACTIVE epoch before any invoice use. The protocol object cannot establish those facts, or that a witness will honor its promise. Missing/failed/short acknowledgements remain incomplete. -Fetch/receipt decryption, witness selection and invoice paths, durable receiver recovery, and a +Receipt decryption, witness selection and invoice paths, durable receiver recovery, and a witness service remain outside this slice. `tests/data/beignet-witness.json` is generated from Beignet `8aee31d1` using its actual manifest @@ -201,6 +201,56 @@ Appendix D input digest. The fixtures are new cross-implementation examples, not Appendix F vectors. Focused tests cover exact bytes, all truncated prefixes, signature domains, retention/height bounds, maximum books, connection changes, retries and bounded arbitrary input. +## Witness fetch and opaque encrypted records + +Appendix F.1 types 55059 and 55061 have bounded canonical codecs. `UnsignedFetch` supplies +the digest for an external fetch-key signer; `SignedFetch` verifies a compact low-S signature. +The exact signature domain is a single SHA256 of `ffor/witness/fetch`, mailbox ID, nonce and +the trailing TLV stream. Neither request ID nor wire type is signed. TLV 1 is the two-byte +pagination slot; unknown odd fields are preserved, while unknown even fields, duplicates, +nonminimal BigSize values and malformed known lengths fail. A first page with no TLVs +retains the older unpaged digest exactly. Absence and an explicit zero cursor stay distinct. + +`PendingFetch` retains the exact request, signed manifest and actual expected connection. +It checks the echoed request ID and authenticated transport source before checking each +record against the provisioned witness, mailbox, H_act, encryption key and canonical book +entry. `CheckedFetchPage` exposes only authenticated opaque encrypted records. Its next +request must use the returned cursor and a new request ID and nonce. Slots strictly increase, +continuation must equal the final returned slot and remain below K, and traversal is capped +at K pages. Already returned pages remain available if a later page fails. The caller must +ensure mailbox-wide nonce freshness across traversals and restarts; the witness must +durably refuse reused nonces. Noise identity routes replies but never authorizes mailbox access. + +Version 1 records have a 235-byte header, a compact low-S witness signature over the single +SHA256 domain `ffor/witness/record`, a 191-byte ciphertext and bounded opaque guardian +attachments. Record parsing rejects unsupported version/profile/flags, malformed points, +incorrect ciphertext hashes, invalid signatures, truncated fields and extra bytes. Guardian +attachments are outside the signature and provide no proof of payment or storage. Record +retrieval remains possible after voucher expiry and therefore has no current-height gate. + +There is no Rust ECIES decryptor in this checkpoint. A witness can sign ciphertext with an +invalid AEAD tag or false plaintext; successful metadata authentication does not prove that +a payment occurred. A future decryptor must authenticate ChaCha20-Poly1305 and validate +the epoch, slot, payment hash, amount, expiry, deadline and actual preimage before crediting +or claiming a voucher. The reference uses SHA256 of the **compressed** ECDH shared point +once, then HKDF-SHA256 extract/expand with empty salt and `ffor/witness/body` as info. +The nonce is 12 zero bytes, the Poly1305 tag is appended, and AAD is the exact header with +only its final ciphertext-hash field zeroed. Applying another SHA256 to the reference +ECDH helper's output would derive the wrong key. + +`generate_beignet_witness_fetch.cjs` regenerates `beignet-witness-fetch.json` from pinned +Beignet and the six public Appendix D setups. Deterministic fixture keys and ephemeral +keys are public test inputs only. The generator invokes the actual reference codecs and +encryption helper, verifies witness signatures, decrypts every generated record and checks +the published preimages. Rust tests independently compare bytes, domains, AAD and the +secp256k1 ECDH hash, but do not perform decryption. The existing witness fuzz target also +checks these new codecs and is seeded with requests, pages and signed encrypted records. + +The fetch checkpoint passes 88 tests and five doctests, no_std, all-target Clippy with +warnings denied, and actual Rust 1.63 checks with and without std. The seeded witness fuzz +target completed 1,431,038 inputs in 31 seconds without a crash. These bounded checks do +not establish successful decryption, runtime storage safety or offline invoice readiness. + The requested settlement baseline is LND v0.21.3-beta. LND implementation work is kept local. This unpublished workspace crate does not change a binding version or a release. diff --git a/lightning-ffor/fuzz/README.md b/lightning-ffor/fuzz/README.md index f7416c061a5..efeaa4c5a83 100644 --- a/lightning-ffor/fuzz/README.md +++ b/lightning-ffor/fuzz/README.md @@ -21,9 +21,11 @@ crate's property tests exercise bounded arbitrary bytes, extension lengths, and bitmaps independently of this target. Live network partitions, persistence and channel transitions are outside this pure codec and require later engine integration tests. -The `witness` target checks strict provision/ack encodings and authenticated receiver manifest -decoding. It accepts raw acknowledgement bytes or `[u16 init_length][u16 accept_length][init] +The `witness` target checks strict provision/ack encodings, authenticated receiver manifests, +signed fetch requests, paginated replies and signed encrypted record framing. It accepts raw +acknowledgement/fetch/response/record bytes or `[u16 init_length][u16 accept_length][init] [accept][candidate]`, with candidate being a manifest or complete provision. Its seeds use the -six public Appendix D setups and manifests generated by the pinned Beignet implementation. +six public Appendix D setups and manifests, fetch pages and encrypted records generated by the +pinned Beignet implementation. The target never decrypts ciphertext or treats it as a preimage. Run `cargo +nightly fuzz run witness -- -max_len=196609 -max_total_time=60` after seeding. Neither fuzz target signs messages or accesses private keys. diff --git a/lightning-ffor/fuzz/fuzz_targets/witness.rs b/lightning-ffor/fuzz/fuzz_targets/witness.rs index efcf04bf929..95c9fe38d3c 100644 --- a/lightning-ffor/fuzz/fuzz_targets/witness.rs +++ b/lightning-ffor/fuzz/fuzz_targets/witness.rs @@ -4,12 +4,29 @@ use bitcoin::secp256k1::PublicKey; use libfuzzer_sys::fuzz_target; use lightning_ffor::setup::AuthenticatedSetup; use lightning_ffor::wire::Message; -use lightning_ffor::witness::{Acknowledgement, Provision, SignedManifest, MAX_MESSAGE_LEN}; +use lightning_ffor::witness::{ + Acknowledgement, EncryptedRecord, FetchResponse, Provision, SignedFetch, SignedManifest, + MAX_MESSAGE_LEN, +}; fuzz_target!(|data: &[u8]| { if let Ok(ack) = Acknowledgement::decode(data) { assert_eq!(ack.encode(), data); } + if let Ok(record) = EncryptedRecord::decode(data) { + assert_eq!(record.encode(), data); + } + if let Ok(response) = FetchResponse::decode(data) { + assert_eq!(response.encode(), data); + } + let fetch_key = PublicKey::from_slice(&[ + 3, 91, 229, 233, 71, 130, 9, 103, 74, 150, 230, 15, 31, 3, 127, 97, 118, 84, 15, 208, 1, + 250, 29, 100, 105, 71, 112, 197, 106, 119, 9, 196, 44, + ]) + .unwrap(); + if let Ok(request) = SignedFetch::decode(data, fetch_key) { + assert_eq!(request.encode(), data); + } // Public signed setup plus candidate, so the receiver-specific manifest decoder is exercised // without a private key or a second independent canonical-book implementation. if data.len() < 4 || data.len() > 3 * MAX_MESSAGE_LEN + 4 { diff --git a/lightning-ffor/fuzz/seed_corpus.py b/lightning-ffor/fuzz/seed_corpus.py index bf07c87f02b..956279ad592 100644 --- a/lightning-ffor/fuzz/seed_corpus.py +++ b/lightning-ffor/fuzz/seed_corpus.py @@ -34,3 +34,12 @@ for field in ("acknowledgement", "refusal"): (witness_corpus / f"witness-{index}-{field}").write_bytes(bytes.fromhex(witness[field])) print("Seeded 24 witness manifest/provision/acknowledgement cases") + +fetches = json.loads((crate / "tests/data/beignet-witness-fetch.json").read_text()) +for index, fixture in enumerate(fetches["fixtures"]): + for field in ("first_request", "continuation_request", "odd_request", "first_response", + "continuation_response", "empty_response", "refusal"): + (witness_corpus / f"fetch-{index}-{field}").write_bytes(bytes.fromhex(fixture[field])) + for slot, record in enumerate(fixture["records"]): + (witness_corpus / f"record-{index}-{slot}").write_bytes(bytes.fromhex(record["record"])) +print("Seeded fetch pages and signed encrypted records from all six reference scenarios") diff --git a/lightning-ffor/src/setup.rs b/lightning-ffor/src/setup.rs index c5c9cea6d4b..1164e6b812f 100644 --- a/lightning-ffor/src/setup.rs +++ b/lightning-ffor/src/setup.rs @@ -252,6 +252,21 @@ impl AuthenticatedSetup { } } + /// Authenticate the receiver's close intent against this setup and its accepted activation. + /// + /// This only verifies signed intent. The engine must retain it durably before sending, + /// and wait for the settlement peer's signed acknowledgement before removing vouchers. + pub fn validate_close( + &self, message: &Message, activation_hash: Digest, + ) -> Result<(), SetupError> { + self.authenticate(message, &self.receiver)?; + match message.payload { + Payload::Close(hash) if hash == activation_hash => Ok(()), + Payload::Close(_) => Err(SetupError::Transcript), + _ => Err(SetupError::MessageType), + } + } + /// Verify close accounting and every disclosed preimage against this signed book. /// /// An unset bit never permits discarding a preimage learned from another source. Retain diff --git a/lightning-ffor/src/wire/mod.rs b/lightning-ffor/src/wire/mod.rs index 91504fb9aae..04345c00295 100644 --- a/lightning-ffor/src/wire/mod.rs +++ b/lightning-ffor/src/wire/mod.rs @@ -14,7 +14,7 @@ mod codec; mod lifecycle; mod setup; -mod tlv; +pub(crate) mod tlv; mod types; use alloc::vec::Vec; diff --git a/lightning-ffor/src/wire/tlv.rs b/lightning-ffor/src/wire/tlv.rs index e74add02eab..bf4d8e02d12 100644 --- a/lightning-ffor/src/wire/tlv.rs +++ b/lightning-ffor/src/wire/tlv.rs @@ -2,38 +2,38 @@ use alloc::vec::Vec; use super::{Tlv, WireError, MAX_MESSAGE_LEN, MAX_TLV_COUNT}; -pub(super) struct Reader<'a> { +pub(crate) struct Reader<'a> { remaining: &'a [u8], } impl<'a> Reader<'a> { - pub(super) fn new(bytes: &'a [u8]) -> Self { + pub(crate) fn new(bytes: &'a [u8]) -> Self { Self { remaining: bytes } } - pub(super) fn take(&mut self, size: usize) -> Result<&'a [u8], WireError> { + pub(crate) fn take(&mut self, size: usize) -> Result<&'a [u8], WireError> { let value = self.remaining.get(..size).ok_or(WireError::Truncated)?; self.remaining = &self.remaining[size..]; Ok(value) } - pub(super) fn array(&mut self) -> Result<[u8; N], WireError> { + pub(crate) fn array(&mut self) -> Result<[u8; N], WireError> { self.take(N)?.try_into().map_err(|_| WireError::Truncated) } - pub(super) fn u8(&mut self) -> Result { + pub(crate) fn u8(&mut self) -> Result { Ok(self.array::<1>()?[0]) } - pub(super) fn u16(&mut self) -> Result { + pub(crate) fn u16(&mut self) -> Result { Ok(u16::from_be_bytes(self.array()?)) } - pub(super) fn u32(&mut self) -> Result { + pub(crate) fn u32(&mut self) -> Result { Ok(u32::from_be_bytes(self.array()?)) } - pub(super) fn u64(&mut self) -> Result { + pub(crate) fn u64(&mut self) -> Result { Ok(u64::from_be_bytes(self.array()?)) } - pub(super) fn is_empty(&self) -> bool { + pub(crate) fn is_empty(&self) -> bool { self.remaining.is_empty() } @@ -51,7 +51,7 @@ impl<'a> Reader<'a> { } } -pub(super) fn read_tlvs(reader: &mut Reader<'_>) -> Result, WireError> { +pub(crate) fn read_tlvs(reader: &mut Reader<'_>) -> Result, WireError> { let mut entries = Vec::new(); let mut last = None; while !reader.is_empty() { @@ -70,16 +70,16 @@ pub(super) fn read_tlvs(reader: &mut Reader<'_>) -> Result, WireError> Ok(entries) } -pub(super) fn required(entries: &mut Vec, kind: u64) -> Result, WireError> { +pub(crate) fn required(entries: &mut Vec, kind: u64) -> Result, WireError> { optional(entries, kind).ok_or(WireError::MissingTlv(kind)) } -pub(super) fn optional(entries: &mut Vec, kind: u64) -> Option> { +pub(crate) fn optional(entries: &mut Vec, kind: u64) -> Option> { let position = entries.iter().position(|entry| entry.kind == kind)?; Some(entries.remove(position).value) } -pub(super) fn check_extensions(entries: &[Tlv]) -> Result<(), WireError> { +pub(crate) fn check_extensions(entries: &[Tlv]) -> Result<(), WireError> { if entries.len() > MAX_TLV_COUNT { return Err(WireError::SizeLimit); } @@ -104,26 +104,26 @@ pub(super) fn check_extensions(entries: &[Tlv]) -> Result<(), WireError> { Ok(()) } -pub(super) struct Writer(pub(super) Vec); +pub(crate) struct Writer(pub(crate) Vec); impl Writer { - pub(super) fn new() -> Self { + pub(crate) fn new() -> Self { Self(Vec::new()) } - pub(super) fn put(&mut self, bytes: &[u8]) -> Result<(), WireError> { + pub(crate) fn put(&mut self, bytes: &[u8]) -> Result<(), WireError> { if bytes.len() > MAX_MESSAGE_LEN - self.0.len() { return Err(WireError::SizeLimit); } self.0.extend_from_slice(bytes); Ok(()) } - pub(super) fn u16(&mut self, value: u16) -> Result<(), WireError> { + pub(crate) fn u16(&mut self, value: u16) -> Result<(), WireError> { self.put(&value.to_be_bytes()) } - pub(super) fn u32(&mut self, value: u32) -> Result<(), WireError> { + pub(crate) fn u32(&mut self, value: u32) -> Result<(), WireError> { self.put(&value.to_be_bytes()) } - pub(super) fn u64(&mut self, value: u64) -> Result<(), WireError> { + pub(crate) fn u64(&mut self, value: u64) -> Result<(), WireError> { self.put(&value.to_be_bytes()) } @@ -146,7 +146,7 @@ impl Writer { } } -pub(super) fn write_tlvs( +pub(crate) fn write_tlvs( writer: &mut Writer, known: Vec, extensions: &[Tlv], ) -> Result<(), WireError> { check_extensions(extensions)?; @@ -165,6 +165,6 @@ pub(super) fn write_tlvs( Ok(()) } -pub(super) fn fixed(value: Vec, kind: u64) -> Result<[u8; N], WireError> { +pub(crate) fn fixed(value: Vec, kind: u64) -> Result<[u8; N], WireError> { value.try_into().map_err(|_| WireError::InvalidTlv(kind)) } diff --git a/lightning-ffor/src/witness/fetch.rs b/lightning-ffor/src/witness/fetch.rs new file mode 100644 index 00000000000..2362b5847f7 --- /dev/null +++ b/lightning-ffor/src/witness/fetch.rs @@ -0,0 +1,255 @@ +use alloc::vec::Vec; +use bitcoin::secp256k1::PublicKey; + +use super::{ + EncryptedRecord, Reader, WitnessError, FETCH_MESSAGE_TYPE, FETCH_RESPONSE_MESSAGE_TYPE, + MAX_MESSAGE_LEN, +}; +use crate::transcript; +use crate::wire::{tlv, Tlv}; + +/// Public fetch fields. Fresh nonces authorize each page independently; request IDs only correlate. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FetchParameters { + /// Transport correlation identity, intentionally outside the fetch signature domain. + pub request_id: [u8; 16], + /// Mailbox selected in the retained signed manifest. + pub mailbox_id: [u8; 32], + /// Fresh nonce that the witness must never accept twice for this mailbox. + pub nonce: [u8; 32], + /// Exclusive lower slot bound. Absence means zero and has a distinct signed encoding. + pub after_slot: Option, + /// Canonical unknown odd TLVs, retained exactly and covered by the request signature. + pub extensions: Vec, +} + +/// Canonical fetch request awaiting an external fetch-key signature. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UnsignedFetch { + parameters: FetchParameters, + tlvs: Vec, +} + +impl UnsignedFetch { + /// Validate all TLVs and the completed signed message size without accessing a key. + pub fn new(parameters: FetchParameters) -> Result { + let tlvs = encode_paging(parameters.after_slot, ¶meters.extensions)?; + if 2 + 16 + 32 + 32 + 64 + tlvs.len() > MAX_MESSAGE_LEN { + return Err(WitnessError::SizeLimit); + } + Ok(Self { parameters, tlvs }) + } + + /// Exact immutable fields whose mailbox, nonce and TLVs will be signed. + pub fn parameters(&self) -> &FetchParameters { + &self.parameters + } + + /// Single SHA256 of `ffor/witness/fetch || mailbox_id || nonce || trailing_tlvs`. + /// Neither the two-byte wire type nor request_id is included by Appendix F.1. + pub fn signing_digest(&self) -> [u8; 32] { + transcript::hash_parts( + b"ffor/witness/fetch", + &[&self.parameters.mailbox_id, &self.parameters.nonce, &self.tlvs], + ) + } + + /// Validate an external compact low-S signature using the mailbox's expected fetch key. + pub fn authenticate( + self, signature: [u8; 64], expected_fetch_key: PublicKey, + ) -> Result { + transcript::verify_digest_signature( + self.signing_digest(), + &signature, + &expected_fetch_key, + )?; + Ok(SignedFetch { request: self, signature, fetch_key: expected_fetch_key }) + } +} + +/// Canonical fetch authorized by a supplied fetch key, not by the request's Noise peer. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignedFetch { + request: UnsignedFetch, + signature: [u8; 64], + fetch_key: PublicKey, +} + +impl SignedFetch { + /// Decode the complete wire message and authenticate against a trusted mailbox fetch key. + pub fn decode(bytes: &[u8], expected_fetch_key: PublicKey) -> Result { + if bytes.len() > MAX_MESSAGE_LEN { + return Err(WitnessError::SizeLimit); + } + let mut reader = Reader(bytes); + if reader.u16()? != FETCH_MESSAGE_TYPE { + return Err(WitnessError::MessageType); + } + let request_id = reader.array()?; + let mailbox_id = reader.array()?; + let nonce = reader.array()?; + let signature = reader.array()?; + let (after_slot, extensions) = decode_paging(reader.0)?; + UnsignedFetch::new(FetchParameters { + request_id, + mailbox_id, + nonce, + after_slot, + extensions, + })? + .authenticate(signature, expected_fetch_key) + } + + /// Exact original canonical request, including its correlation ID and compact signature. + pub fn encode(&self) -> Vec { + let p = self.request.parameters(); + let mut bytes = Vec::new(); + bytes.extend_from_slice(&FETCH_MESSAGE_TYPE.to_be_bytes()); + bytes.extend_from_slice(&p.request_id); + bytes.extend_from_slice(&p.mailbox_id); + bytes.extend_from_slice(&p.nonce); + bytes.extend_from_slice(&self.signature); + bytes.extend_from_slice(&self.request.tlvs); + bytes + } + + /// Immutable canonical request fields and signing domain. + pub fn unsigned(&self) -> &UnsignedFetch { + &self.request + } + /// The trusted public key under which this request was checked. + pub fn fetch_key(&self) -> PublicKey { + self.fetch_key + } +} + +/// Unsigned response fields. Every returned record still needs expected-manifest correlation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FetchResult { + /// One ascending page, with a cursor only when records remain above the last returned slot. + Page { + /// Bounded individually signed encrypted records. + records: Vec, + /// The last returned slot if another page is available. + next_after_slot: Option, + /// Canonical unknown odd response TLVs, preserved without granting semantics. + extensions: Vec, + }, + /// Bounded opaque diagnostic bytes, with no trailing TLV stream. + Refused(Vec), +} + +/// Canonical `ff_witness_fetch_resp`. Noise connection and request correlation remain mandatory. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FetchResponse { + request_id: [u8; 16], + result: FetchResult, +} + +impl FetchResponse { + /// Construct a response bounded by one complete BOLT 8 plaintext frame. + pub fn new(request_id: [u8; 16], result: FetchResult) -> Result { + let response = Self { request_id, result }; + response.encode_checked()?; + Ok(response) + } + + /// Parse strict 0/1 status, record counts, record framing and canonical pagination TLVs. + pub fn decode(bytes: &[u8]) -> Result { + if bytes.len() > MAX_MESSAGE_LEN { + return Err(WitnessError::SizeLimit); + } + let mut reader = Reader(bytes); + if reader.u16()? != FETCH_RESPONSE_MESSAGE_TYPE { + return Err(WitnessError::MessageType); + } + let request_id = reader.array()?; + let result = match reader.byte()? { + 0 => { + let length = usize::from(reader.u16()?); + let error = reader.take(length)?.to_vec(); + reader.finish()?; + FetchResult::Refused(error) + }, + 1 => { + let count = usize::from(reader.u16()?); + if count > crate::book::MAX_VOUCHERS { + return Err(WitnessError::SizeLimit); + } + let mut records = Vec::new(); + for _ in 0..count { + let length = usize::from(reader.u16()?); + records.push(EncryptedRecord::decode(reader.take(length)?)?); + } + let (next_after_slot, extensions) = decode_paging(reader.0)?; + FetchResult::Page { records, next_after_slot, extensions } + }, + _ => return Err(WitnessError::NonCanonical), + }; + Self::new(request_id, result) + } + + /// Exact canonical response bytes. Construction already bounded every field. + pub fn encode(&self) -> Vec { + self.encode_checked().expect("invariant: immutable response was bounded on construction") + } + /// Echo of the pending request's unsigned correlation identity. + pub fn request_id(&self) -> [u8; 16] { + self.request_id + } + /// Uncorrelated response contents; no plaintext or payment is established. + pub fn result(&self) -> &FetchResult { + &self.result + } + + fn encode_checked(&self) -> Result, WitnessError> { + let mut writer = tlv::Writer::new(); + writer.u16(FETCH_RESPONSE_MESSAGE_TYPE)?; + writer.put(&self.request_id)?; + match &self.result { + FetchResult::Refused(error) => { + if error.len() > MAX_MESSAGE_LEN - 21 { + return Err(WitnessError::SizeLimit); + } + writer.put(&[0])?; + writer.u16(error.len() as u16)?; + writer.put(error)?; + }, + FetchResult::Page { records, next_after_slot, extensions } => { + if records.len() > crate::book::MAX_VOUCHERS { + return Err(WitnessError::SizeLimit); + } + writer.put(&[1])?; + writer.u16(records.len() as u16)?; + for record in records { + let bytes = record.encode(); + writer.u16(bytes.len() as u16)?; + writer.put(&bytes)?; + } + writer.put(&encode_paging(*next_after_slot, extensions)?)?; + }, + } + Ok(writer.0) + } +} + +fn encode_paging(slot: Option, extensions: &[Tlv]) -> Result, WitnessError> { + let known = + slot.map(|slot| Tlv { kind: 1, value: slot.to_be_bytes().to_vec() }).into_iter().collect(); + let mut writer = tlv::Writer::new(); + // TLV 1 is reserved even when omitted, never an unknown extension. + if extensions.iter().any(|entry| entry.kind == 1) { + return Err(WitnessError::NonCanonical); + } + tlv::write_tlvs(&mut writer, known, extensions)?; + Ok(writer.0) +} + +fn decode_paging(bytes: &[u8]) -> Result<(Option, Vec), WitnessError> { + let mut entries = tlv::read_tlvs(&mut tlv::Reader::new(bytes))?; + let slot = tlv::optional(&mut entries, 1) + .map(|value| tlv::fixed::<2>(value, 1).map(u16::from_be_bytes)) + .transpose()?; + tlv::check_extensions(&entries)?; + Ok((slot, entries)) +} diff --git a/lightning-ffor/src/witness/fetch_correlation.rs b/lightning-ffor/src/witness/fetch_correlation.rs new file mode 100644 index 00000000000..06d6fef8ffe --- /dev/null +++ b/lightning-ffor/src/witness/fetch_correlation.rs @@ -0,0 +1,148 @@ +use alloc::vec::Vec; + +use super::{ + AuthenticatedEncryptedRecord, FetchResponse, FetchResult, SignedFetch, SignedManifest, + WitnessConnection, WitnessError, +}; + +/// An exact signed fetch and its expected manifest, witness and connection instance. +/// +/// Progress is bounded to K pages and retains no accumulated record payloads. Fresh request IDs +/// and nonces are required within this traversal. The caller must also guarantee nonce freshness +/// across restarts/traversals; the witness durably enforces mailbox-wide nonce replay protection. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PendingFetch { + request: SignedFetch, + manifest: SignedManifest, + connection: WitnessConnection, + used: Vec<([u8; 16], [u8; 32])>, +} + +impl PendingFetch { + /// Begin a bounded traversal from slot zero with an already authenticated fetch request. + /// Noise identity routes the response; only the fetch-key signature authorizes the mailbox. + pub fn first( + request: SignedFetch, manifest: SignedManifest, connection: WitnessConnection, + ) -> Result { + if request.unsigned().parameters().after_slot.unwrap_or(0) != 0 { + return Err(WitnessError::Pagination); + } + Self::bind(request, manifest, connection, Vec::new()) + } + + /// Exact signed request to send. Request ID uniqueness is a caller-owned transport invariant. + pub fn request(&self) -> &SignedFetch { + &self.request + } + /// Retained signed manifest that every record must match. + pub fn manifest(&self) -> &SignedManifest { + &self.manifest + } + /// Expected authenticated response connection; never inferred from record/header bytes. + pub fn connection(&self) -> &WitnessConnection { + &self.connection + } + + fn bind( + request: SignedFetch, manifest: SignedManifest, connection: WitnessConnection, + mut used: Vec<([u8; 16], [u8; 32])>, + ) -> Result { + let p = request.unsigned().parameters(); + let m = manifest.unsigned().parameters(); + if p.mailbox_id != m.mailbox_id || request.fetch_key() != m.fetch_public_key { + return Err(WitnessError::Mailbox); + } + if used.iter().any(|(id, nonce)| *id == p.request_id || *nonce == p.nonce) { + return Err(WitnessError::Replay); + } + let count = (manifest.unsigned().canonical_book().len() - 36) / 58; + if used.len() >= count { + return Err(WitnessError::Pagination); + } + used.push((p.request_id, p.nonce)); + Ok(Self { request, manifest, connection, used }) + } +} + +impl PendingFetch { + /// Check request, actual source, ascending slots, manifest binding and continuation progress. + /// A failure never mutates this request or a previously returned page. Successful encrypted + /// records remain opaque and cannot be credited or submitted as preimages. + pub fn check_response( + &self, response: &FetchResponse, source: &WitnessConnection, + ) -> Result, WitnessError> { + if response.request_id() != self.request.unsigned().parameters().request_id { + return Err(WitnessError::Request); + } + if source.identity != self.connection.identity { + return Err(WitnessError::Connection); + } + if source.node_id != self.connection.node_id { + return Err(WitnessError::Witness); + } + let (records, next_after_slot) = match response.result() { + FetchResult::Refused(_) => return Err(WitnessError::Refused), + FetchResult::Page { records, next_after_slot, .. } => (records, *next_after_slot), + }; + let count = (self.manifest.unsigned().canonical_book().len() - 36) / 58; + let mut last = self.request.unsigned().parameters().after_slot.unwrap_or(0); + let mut checked = Vec::new(); + for record in records { + if record.header().slot <= last { + return Err(WitnessError::Pagination); + } + last = record.header().slot; + checked.push(record.clone().authenticate(&self.manifest, self.connection.node_id)?); + } + if let Some(next) = next_after_slot { + if records.is_empty() + || next != last + || usize::from(next) >= count + || self.used.len() >= count + { + return Err(WitnessError::Pagination); + } + } + Ok(CheckedFetchPage { pending: self.clone(), records: checked, next_after_slot }) + } +} + +/// One exactly correlated page of authenticated opaque records, with bounded continuation. +/// +/// The caller retains earlier returned pages even if any subsequent request fails. Guardian +/// attachments and a witness's omission of records are never evidence of an unpaid voucher. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CheckedFetchPage { + pending: PendingFetch, + records: Vec, + next_after_slot: Option, +} + +impl CheckedFetchPage { + /// Signed encrypted statements requiring authenticated decryption before payment use. + pub fn records(&self) -> &[AuthenticatedEncryptedRecord] { + &self.records + } + /// Required exclusive lower slot bound for the next request, or completion if absent. + pub fn next_after_slot(&self) -> Option { + self.next_after_slot + } +} + +impl CheckedFetchPage { + /// Correlate a freshly signed continuation without discarding this page's records. + /// This retains the same authenticated connection. After reconnect, start a fresh traversal + /// with globally fresh nonces and preserve previously authenticated records independently. + pub fn next(&self, request: SignedFetch) -> Result, WitnessError> { + let after = self.next_after_slot.ok_or(WitnessError::Pagination)?; + if request.unsigned().parameters().after_slot != Some(after) { + return Err(WitnessError::Pagination); + } + PendingFetch::bind( + request, + self.pending.manifest.clone(), + self.pending.connection.clone(), + self.pending.used.clone(), + ) + } +} diff --git a/lightning-ffor/src/witness/mod.rs b/lightning-ffor/src/witness/mod.rs index 9f2e2714058..5a7cc2d99cf 100644 --- a/lightning-ffor/src/witness/mod.rs +++ b/lightning-ffor/src/witness/mod.rs @@ -1,4 +1,4 @@ -//! Receiver-side D-R manifest and provisioning primitives from section 9.6 and Appendix F.1. +//! Receiver-side D-R provisioning, fetch and opaque record primitives from section 9.6/Appendix F. //! //! A manifest proves fetch-key control and agreement with a supplied authenticated setup. A //! checked acknowledgement only correlates a response with one immutable provisioning request. @@ -8,6 +8,9 @@ //! Version 1/profile 1 manifests and the two provisioning messages define no extension stream. //! Decoders reject every trailing byte, unknown version/profile, and noncanonical success flag. //! Refusal text is bounded opaque bytes and need not be UTF-8. +//! Fetch messages preserve canonical unknown odd TLVs and reject unknown even fields. Records +//! define no extension stream and reject reserved flags. Authenticated encrypted records prove +//! their witness signature and manifest binding only: no AEAD or plaintext verification exists. //! //! ``` //! use lightning_ffor::witness::{Acknowledgement, AcknowledgementResult}; @@ -24,17 +27,29 @@ use bitcoin::secp256k1::PublicKey; use crate::transcript::SignatureError; mod correlation; +mod fetch; +mod fetch_correlation; mod manifest; mod messages; +mod record; pub use correlation::{CheckedAcknowledgement, PendingProvision, WitnessConnection}; +pub use fetch::{FetchParameters, FetchResponse, FetchResult, SignedFetch, UnsignedFetch}; +pub use fetch_correlation::{CheckedFetchPage, PendingFetch}; pub use manifest::{ManifestParameters, SignedManifest, UnsignedManifest}; pub use messages::{Acknowledgement, AcknowledgementResult, Provision}; +pub use record::{ + AuthenticatedEncryptedRecord, EncryptedRecord, RecordHeader, CIPHERTEXT_LEN, RECORD_HEADER_LEN, +}; /// Appendix F.1 receiver-to-witness provisioning message, including its two-byte type. pub const PROVISION_MESSAGE_TYPE: u16 = 55055; /// Appendix F.1 witness-to-receiver provisioning acknowledgement. pub const ACK_MESSAGE_TYPE: u16 = 55057; +/// Appendix F.1 signed mailbox fetch request. +pub const FETCH_MESSAGE_TYPE: u16 = 55059; +/// Appendix F.1 paginated encrypted-record response. +pub const FETCH_RESPONSE_MESSAGE_TYPE: u16 = 55061; /// Minimum promised record retention beyond voucher expiry, in blocks. pub const RETENTION_MARGIN_BLOCKS: u32 = 144; /// BOLT 8's plaintext limit, including the two-byte message type. @@ -47,7 +62,7 @@ pub enum WitnessError { Truncated, /// A message exceeds the bounded BOLT 8 envelope. SizeLimit, - /// The wire type is outside the two supported provisioning messages. + /// The wire type is outside the supported witness request/response codecs. MessageType, /// Only manifest version 1 is supported. Version, @@ -57,7 +72,7 @@ pub enum WitnessError { NonCanonical, /// A compressed secp256k1 public key is invalid. PublicKey, - /// The supplied fetch-key signature does not authenticate the exact manifest. + /// A compact low-S fetch-key or witness signature is invalid for the exact signed bytes. Signature(SignatureError), /// The manifest differs from the authenticated setup's canonical book. Book, @@ -73,13 +88,25 @@ pub enum WitnessError { Connection, /// The actual response peer or acknowledgement names another witness. Witness, - /// The witness explicitly refused the provisioning request. + /// The witness explicitly refused the pending request. Refused, + /// A canonical trailing TLV stream is invalid. + Tlv(crate::wire::WireError), + /// The encrypted record's declared ciphertext hash does not match its bytes. + Ciphertext, + /// A signed record names another mailbox or encryption key. + Mailbox, + /// The slot or canonical book entry differs from this manifest. + Terms, + /// A response is unordered, repeats a slot, or has an invalid pagination cursor. + Pagination, + /// A request identifier or nonce was already used in this bounded fetch traversal. + Replay, } impl fmt::Display for WitnessError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "invalid FFOR witness provisioning: {self:?}") + write!(f, "invalid FFOR witness data: {self:?}") } } @@ -92,6 +119,12 @@ impl From for WitnessError { } } +impl From for WitnessError { + fn from(error: crate::wire::WireError) -> Self { + Self::Tlv(error) + } +} + struct Reader<'a>(&'a [u8]); impl<'a> Reader<'a> { diff --git a/lightning-ffor/src/witness/record.rs b/lightning-ffor/src/witness/record.rs new file mode 100644 index 00000000000..aedd1ac8e9b --- /dev/null +++ b/lightning-ffor/src/witness/record.rs @@ -0,0 +1,220 @@ +use alloc::vec::Vec; +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::PublicKey; + +use super::{Reader, SignedManifest, WitnessError, MAX_MESSAGE_LEN}; +use crate::transcript; + +/// Appendix F.2 fixed version 1 record header length. +pub const RECORD_HEADER_LEN: usize = 235; +/// Compressed ephemeral key, 142-byte version 1 body, and 16-byte Poly1305 tag. +pub const CIPHERTEXT_LEN: usize = 33 + 142 + 16; +const MAX_RECORD_LEN: usize = MAX_MESSAGE_LEN - 2 - 16 - 1 - 2 - 2; + +/// Public signed record metadata. These claims do not prove that its body decrypts. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RecordHeader { + /// Receiver-selected mailbox from the signed manifest. + pub mailbox_id: [u8; 32], + /// Witness-selected random record identity. + pub record_id: [u8; 32], + /// One-based canonical voucher slot. + pub slot: u16, + /// H_act of the recorded epoch. + pub activation_hash: [u8; 32], + /// SHA256 of `ffor/terms` and the exact 58-byte canonical book entry. + pub terms_hash: [u8; 32], + /// Claimed signing witness, requiring comparison with the provisioned identity. + pub witness: PublicKey, + /// Receiver's epoch encryption public key. + pub encryption_public_key: PublicKey, + /// Witness-reported observation height, not an authenticated chain observation. + pub recorded_height: u32, + /// True when the witness reports propagating without completing its storage barrier. + pub unbarriered: bool, + /// SHA256 of the entire ciphertext, including ephemeral key and authentication tag. + pub ciphertext_hash: [u8; 32], +} + +impl RecordHeader { + /// Canonical version 1/profile 1 signed header. + pub fn encode(&self) -> Vec { + let mut bytes = Vec::with_capacity(RECORD_HEADER_LEN); + bytes.extend_from_slice(&[1, 1]); + bytes.extend_from_slice(&self.mailbox_id); + bytes.extend_from_slice(&self.record_id); + bytes.extend_from_slice(&self.slot.to_be_bytes()); + bytes.extend_from_slice(&self.activation_hash); + bytes.extend_from_slice(&self.terms_hash); + bytes.extend_from_slice(&self.witness.serialize()); + bytes.extend_from_slice(&self.encryption_public_key.serialize()); + bytes.extend_from_slice(&self.recorded_height.to_be_bytes()); + bytes.push(u8::from(self.unbarriered)); + bytes.extend_from_slice(&self.ciphertext_hash); + bytes + } + + /// The F.3 AEAD associated data, with only ciphertext_hash replaced by zero bytes. + /// Providing AAD does not decrypt or authenticate an encrypted body. + pub fn associated_data(&self) -> Vec { + let mut bytes = self.encode(); + bytes[RECORD_HEADER_LEN - 32..].fill(0); + bytes + } + + /// Single SHA256 record domain over the exact canonical header. + pub fn signing_digest(&self) -> [u8; 32] { + transcript::hash_parts(b"ffor/witness/record", &[&self.encode()]) + } + + fn read(reader: &mut Reader<'_>) -> Result { + if reader.byte()? != 1 { + return Err(WitnessError::Version); + } + if reader.byte()? != 1 { + return Err(WitnessError::Profile); + } + let mailbox_id = reader.array()?; + let record_id = reader.array()?; + let slot = reader.u16()?; + if slot == 0 || usize::from(slot) > crate::book::MAX_VOUCHERS { + return Err(WitnessError::Terms); + } + let activation_hash = reader.array()?; + let terms_hash = reader.array()?; + let witness = reader.public_key()?; + let encryption_public_key = reader.public_key()?; + let recorded_height = reader.u32()?; + let unbarriered = match reader.byte()? { + 0 => false, + 1 => true, + _ => return Err(WitnessError::NonCanonical), + }; + let ciphertext_hash = reader.array()?; + Ok(Self { + mailbox_id, + record_id, + slot, + activation_hash, + terms_hash, + witness, + encryption_public_key, + recorded_height, + unbarriered, + ciphertext_hash, + }) + } +} + +/// Canonical encrypted record with a valid signature under its claimed witness key. +/// +/// This does not authenticate the claimed witness against a provision. Call `authenticate` +/// before use. No method decrypts, verifies a Poly1305 tag, or establishes a payment preimage. +/// Guardian receipt bytes are unsigned opaque attachments, not proof of storage or payment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EncryptedRecord { + header: RecordHeader, + signature: [u8; 64], + ciphertext: Vec, + receipts: Vec>, +} + +impl EncryptedRecord { + /// Parse bounded framing, strict version/profile/flags, compressed points, low-S signature + /// and ciphertext hash. No trailing fields are defined by version 1. + pub fn decode(bytes: &[u8]) -> Result { + if bytes.len() > MAX_RECORD_LEN { + return Err(WitnessError::SizeLimit); + } + let mut reader = Reader(bytes); + let header = RecordHeader::read(&mut reader)?; + let signature = reader.array()?; + let length = usize::from(reader.u16()?); + if length != CIPHERTEXT_LEN { + return Err(WitnessError::Ciphertext); + } + let ciphertext = reader.take(length)?; + PublicKey::from_slice(&ciphertext[..33]).map_err(|_| WitnessError::PublicKey)?; + if sha256::Hash::hash(ciphertext).to_byte_array() != header.ciphertext_hash { + return Err(WitnessError::Ciphertext); + } + let count = usize::from(reader.byte()?); + let mut receipts = Vec::new(); + for _ in 0..count { + let length = usize::from(reader.u16()?); + receipts.push(reader.take(length)?.to_vec()); + } + reader.finish()?; + transcript::verify_digest_signature(header.signing_digest(), &signature, &header.witness)?; + Ok(Self { header, signature, ciphertext: ciphertext.to_vec(), receipts }) + } + + /// Preserve the exact signed record and opaque receipt framing. + pub fn encode(&self) -> Vec { + let mut bytes = self.header.encode(); + bytes.extend_from_slice(&self.signature); + bytes.extend_from_slice(&(self.ciphertext.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&self.ciphertext); + bytes.push(self.receipts.len() as u8); + for receipt in &self.receipts { + bytes.extend_from_slice(&(receipt.len() as u16).to_be_bytes()); + bytes.extend_from_slice(receipt); + } + bytes + } + + /// The signed public claims. They remain insufficient for plaintext verification. + pub fn header(&self) -> &RecordHeader { + &self.header + } + /// Exact compressed ephemeral key followed by encrypted body and its AEAD tag. + pub fn ciphertext(&self) -> &[u8] { + &self.ciphertext + } + /// Unsigned guardian attachments. Their count does not establish a storage promise. + pub fn receipts(&self) -> &[Vec] { + &self.receipts + } + + /// Bind the signed metadata to the retained manifest and provisioned witness identity. + /// Records can be fetched after voucher expiry; this method has no live-height admission rule. + pub fn authenticate( + self, manifest: &SignedManifest, expected_witness: PublicKey, + ) -> Result { + let m = manifest.unsigned(); + let p = m.parameters(); + if self.header.witness != expected_witness { + return Err(WitnessError::Witness); + } + if self.header.mailbox_id != p.mailbox_id + || self.header.encryption_public_key != p.encryption_public_key + { + return Err(WitnessError::Mailbox); + } + if self.header.activation_hash != m.activation_hash() { + return Err(WitnessError::Transcript); + } + let start = 36 + 58 * (usize::from(self.header.slot) - 1); + let entry = m.canonical_book().get(start..start + 58).ok_or(WitnessError::Terms)?; + if transcript::hash_parts(b"ffor/terms", &[entry]) != self.header.terms_hash { + return Err(WitnessError::Terms); + } + Ok(AuthenticatedEncryptedRecord { record: self }) + } +} + +/// Authenticated opaque ciphertext bound to one provisioned witness, manifest and book entry. +/// +/// This is deliberately not a receipt or preimage authority. A later decryption boundary must +/// verify AEAD, epoch and all plaintext terms, then SHA256(t) == H_k before using a preimage. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AuthenticatedEncryptedRecord { + record: EncryptedRecord, +} + +impl AuthenticatedEncryptedRecord { + /// The exact checked encrypted record, still requiring authenticated decryption. + pub fn record(&self) -> &EncryptedRecord { + &self.record + } +} diff --git a/lightning-ffor/tests/data/beignet-witness-fetch.json b/lightning-ffor/tests/data/beignet-witness-fetch.json new file mode 100644 index 00000000000..e992a8046c8 --- /dev/null +++ b/lightning-ffor/tests/data/beignet-witness-fetch.json @@ -0,0 +1,216 @@ +{ + "source_revision": "8aee31d18e596fe49a0d195b325a6e757d7a009b", + "specification_revision": "d719161f42d1eeb6bd6c3856d564222f03c2205e", + "source_files": [ + "src/lightning/ffor/witness-messages.ts", + "src/lightning/ffor/witness-crypto.ts", + "src/lightning/crypto/ecdh.ts" + ], + "appendix_d_sha256": "beaf507dcde419fb10b3611babb3344616712b467de4b6decbfb9f8b7b3c9be9", + "fixtures": [ + { + "scenario": "D.1", + "records": [ + { + "record": "01010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0a3db013b83ab85aa5bb26de4e4043d567569b9c370faed861aba258c2e8883c0001580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e716a599edf07d16acfacd1128c82f1741bba098d480b830f04f352e822c5183da8b02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa0145fa5d649d54851f62b46e76fa8f1eef468d60403bdb470936026f5c266b4d0bde6fc54e92010c9f45d01996c45c594f6979a3dcbf26c3ee2910828726358fa965d429fd860c6658263840e4f21a18dc0171d843ab61a4f1518ad8ffb5295d8600bf034679787536ac4c5d9be764f215cbc4dcf4268533d164ea96890e0b23769268aba93176d0f31c15fab5403c4719be5750906967cce9e3d9519859a9e300e915b0543993f30d1149542bf81df6745e8575f9f853f3d1c65915c9f9096b6adce1fbcd1991567dd0247d2ff86a6b6b41847e514213187cbc99157b2f52b0d17d5f07c2253aa9426e4e6b819c85ef7a14d5dec0469d4cb34d7f13ce1f442c90677c3216bec593e948e1f798ed9edadc419e027c0b004f129d7b4edfd5640fe1ed00", + "header": "01010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0a3db013b83ab85aa5bb26de4e4043d567569b9c370faed861aba258c2e8883c0001580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e716a599edf07d16acfacd1128c82f1741bba098d480b830f04f352e822c5183da8b02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa0145fa5d649d54851f62b46e76fa8f1eef468d60403bdb470936026f5c266b4d0b", + "aad": "01010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0a3db013b83ab85aa5bb26de4e4043d567569b9c370faed861aba258c2e8883c0001580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e716a599edf07d16acfacd1128c82f1741bba098d480b830f04f352e822c5183da8b02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010000000000000000000000000000000000000000000000000000000000000000", + "digest": "46004c09150b222d6cad5725bcaba92e0f8a29be5f024251381da1b1fbcaf7fa", + "ciphertext": "034679787536ac4c5d9be764f215cbc4dcf4268533d164ea96890e0b23769268aba93176d0f31c15fab5403c4719be5750906967cce9e3d9519859a9e300e915b0543993f30d1149542bf81df6745e8575f9f853f3d1c65915c9f9096b6adce1fbcd1991567dd0247d2ff86a6b6b41847e514213187cbc99157b2f52b0d17d5f07c2253aa9426e4e6b819c85ef7a14d5dec0469d4cb34d7f13ce1f442c90677c3216bec593e948e1f798ed9edadc419e027c0b004f129d7b4edfd5640fe1ed", + "body": "fc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff1500018ae4a313bd01bcfcce12b797ed20603ed56b0f0b9194e7c000e5cbd6fe376517a0a62adc5092cd3c202aaea0900b5826c242194c5f9984070e833e58b273eda500000000000f4240000c3500000c311000000000000f59b000000000000f4240000c11d8000000006553f100", + "shared_hash": "c01a2672b5d05782b78ebbb28b7f3065ff8cef83861e684568baf7e2ef2b26a6", + "body_key": "cdca258c9f825a6e8d8d53272b3b6c36fa6a456690273a69bee39e23690b3c99" + } + ], + "first_request": "d7131f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d738ccdf16b95552981c11778e42185693fca15bc33c713597835595416c29df47478354e10d21bdbd9ab3f19c3d4f58d369674b6c7f08287e8be40497eb3e3a3", + "first_digest": "a6db36519280c52b410c05a81e5d8e69b85f4037de09ae8b94766a7f875b48d9", + "continuation_request": "d713292929292929292929292929292929290b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b4747474747474747474747474747474747474747474747474747474747474747db788b27462e9d3feb6851431bf4df515e7fa30ecbfabac0ac88690bbb2cc4465e76209bd9c27c68623a40b7ade460d8b7e63f95d9f131bd52846f5638d2285101020001", + "continuation_digest": "3bfd9934e952efc1e58e23bd0dcd8e57ff8fa4eb6e8733c65bca1a088ed929ec", + "odd_request": "d7131f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3db3ec3d984d49b4f071e2f3ba5265915ff08529dab4aa61f347cc9d9439683f6e3cef7f8b4483445adabfa4633635ee8a9c324a60cff27a3226c67c98bb2be36c030107", + "first_response": "d7151f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f01000101ed01010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0a3db013b83ab85aa5bb26de4e4043d567569b9c370faed861aba258c2e8883c0001580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e716a599edf07d16acfacd1128c82f1741bba098d480b830f04f352e822c5183da8b02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa0145fa5d649d54851f62b46e76fa8f1eef468d60403bdb470936026f5c266b4d0bde6fc54e92010c9f45d01996c45c594f6979a3dcbf26c3ee2910828726358fa965d429fd860c6658263840e4f21a18dc0171d843ab61a4f1518ad8ffb5295d8600bf034679787536ac4c5d9be764f215cbc4dcf4268533d164ea96890e0b23769268aba93176d0f31c15fab5403c4719be5750906967cce9e3d9519859a9e300e915b0543993f30d1149542bf81df6745e8575f9f853f3d1c65915c9f9096b6adce1fbcd1991567dd0247d2ff86a6b6b41847e514213187cbc99157b2f52b0d17d5f07c2253aa9426e4e6b819c85ef7a14d5dec0469d4cb34d7f13ce1f442c90677c3216bec593e948e1f798ed9edadc419e027c0b004f129d7b4edfd5640fe1ed00", + "continuation_response": "d71529292929292929292929292929292929010000", + "empty_response": "d7151f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f010000", + "refusal": "d7151f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f00000b756e617661696c61626c65" + }, + { + "scenario": "D.2", + "records": [ + { + "record": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cbcd699ce25eda581b77eec605b501c6935c5cc5b80941e235708371c6d1d663a00011d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4de619aec79901ee0ab8d6ccded96ec853812587f785556a69cc0e4bcffef14a302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010cc903269e7b9bd12266f09f0e4afbeb4bff0005ba1b8cdb6bd66ab10edaeb4a8e7800231692024b2409a747d54e7317cb6152431e89a0925599b22e60979fe77c745510f735ad04426e8bf364d9c37fde7fcb16a964163bbd69b0915603137b00bf02d7b6a5bda48673403a0fffd45ceb5bf35d5ceb92271aecf5d4fcef2c644552d7d01331daa63ebf4c6af455538301daed14c25424e9bb57c1eace0e2448e00ab055fef3f2cd9541117bbd57494d9b86efaf87af81aedd461fed72c0087ede06ff8ac7b8237c0d767bb18f53bfd80f26d4f9e2cca6bd1f3d509da2a0676233ac0f31b0a008d9552951ac103a42f0baddc292ad52460c2898dbf71ecc4ef516df35228aec3b34655168122aa3c3bc5f1aae5abaea77541f2b66cfd6e63c5a3a00", + "header": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cbcd699ce25eda581b77eec605b501c6935c5cc5b80941e235708371c6d1d663a00011d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4de619aec79901ee0ab8d6ccded96ec853812587f785556a69cc0e4bcffef14a302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010cc903269e7b9bd12266f09f0e4afbeb4bff0005ba1b8cdb6bd66ab10edaeb4a", + "aad": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cbcd699ce25eda581b77eec605b501c6935c5cc5b80941e235708371c6d1d663a00011d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4de619aec79901ee0ab8d6ccded96ec853812587f785556a69cc0e4bcffef14a302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010000000000000000000000000000000000000000000000000000000000000000", + "digest": "9f3efaf05d3b38a5bfaa19d15ebbf562b6e35e84e3fb8a5c5117ea90b46a7769", + "ciphertext": "02d7b6a5bda48673403a0fffd45ceb5bf35d5ceb92271aecf5d4fcef2c644552d7d01331daa63ebf4c6af455538301daed14c25424e9bb57c1eace0e2448e00ab055fef3f2cd9541117bbd57494d9b86efaf87af81aedd461fed72c0087ede06ff8ac7b8237c0d767bb18f53bfd80f26d4f9e2cca6bd1f3d509da2a0676233ac0f31b0a008d9552951ac103a42f0baddc292ad52460c2898dbf71ecc4ef516df35228aec3b34655168122aa3c3bc5f1aae5abaea77541f2b66cfd6e63c5a3a", + "body": "2436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d600019ac2111ef6621b1e5a5e2045b1811e3fbed11db86124fe4b7d6f0589fe69765e24e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd99300000000000f2ad0000c3500000c311000000000000f424000000000000f2ad0000c11d8000000006553f100", + "shared_hash": "a6a818f27abd6fb7e02b9420dad3419ef83b89e2bb1f28b67794cc0932527de5", + "body_key": "791123509e4e327389c97796110b0c417c0275d8bb2d4f66264e10299567e307" + }, + { + "record": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c1666deb356225ce9ad5e9b64676970a7d2a2dafb2b2cd9f669df993c081603de00021d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4b2d9c11c5bcd065716e0d8664b3f869b96dac515c6be66fc9bbe4861fe32a9a902bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa00404a86662e5a6d1c7735e1250c28dc5e0103176c3a9274ac25aa0dc1a784d773ed984c97a7cca886059ca31bdd007210d2ba81525a2f9554a249f2d3d8b2e0dc306c2274601fea41a86226119b2d356d0fe06f0667a975ae86250c94210dff1a00bf021ad1aadcca777393747962322a6300c1c4269fd1032fa164f205050e387cb3d96b5799ef4fbd038072e3b84343b946da61e13ef776026d0b3c89c576f28580fd81958edf8218a61c58929e22e232ec8aa8f093efa33243ce746900ad76a23a2244699fb419c5ef6ff8b44a5a04b1282f77d4ea54ec4570734bf53fc52205aebc45fee93b4a3207dee9fcdd5894e225fea414a3a1306a0bd12775c60b678c7765893797c463eaa8ecb73cef571732a239535f2157137c4d0ed4ebd05c150802001a6f706171756520677561726469616e206174746163686d656e74000200ff", + "header": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c1666deb356225ce9ad5e9b64676970a7d2a2dafb2b2cd9f669df993c081603de00021d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4b2d9c11c5bcd065716e0d8664b3f869b96dac515c6be66fc9bbe4861fe32a9a902bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa00404a86662e5a6d1c7735e1250c28dc5e0103176c3a9274ac25aa0dc1a784d773", + "aad": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c1666deb356225ce9ad5e9b64676970a7d2a2dafb2b2cd9f669df993c081603de00021d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4b2d9c11c5bcd065716e0d8664b3f869b96dac515c6be66fc9bbe4861fe32a9a902bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa000000000000000000000000000000000000000000000000000000000000000000", + "digest": "8ec35099db8eef5165fbf9153e25e42a363fe649fc0ceb59b03c1c3341c88ad8", + "ciphertext": "021ad1aadcca777393747962322a6300c1c4269fd1032fa164f205050e387cb3d96b5799ef4fbd038072e3b84343b946da61e13ef776026d0b3c89c576f28580fd81958edf8218a61c58929e22e232ec8aa8f093efa33243ce746900ad76a23a2244699fb419c5ef6ff8b44a5a04b1282f77d4ea54ec4570734bf53fc52205aebc45fee93b4a3207dee9fcdd5894e225fea414a3a1306a0bd12775c60b678c7765893797c463eaa8ecb73cef571732a239535f2157137c4d0ed4ebd05c1508", + "body": "2436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d600024190c6e6e079c8575ea0a17d450565ec47ea99662b118c34b6608a7105d3b35ea7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf70713259600000000000855ca000c3500000c31100000000000086d3a00000000000855ca000c11d8000000006553f100", + "shared_hash": "fc095b142760f6cd830191095f1c28d47cce6815903067f5db090044cfde282a", + "body_key": "c505e6eca19ce6cc9a6c2f05b003e139a26ff27f49181a900d44b271c0aef7ab" + }, + { + "record": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cfc64314060ae5338b793a7dda00d9fd7e1c154b60fa52e324778fe63014c16fc00031d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d48c946fbad478cc946802555991c23ce67a4e1356639e5d6a12212eca7abd898902bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa012319822880a9ae3fee7a22c25a0107daa3d8bdd42251bddb4d4008be10a7589b289517b7742990a161b378bd1281783f9df6ae6def0d2a3c17ad42b3c835c80552470e92c37ecd8b0cb3ccdb2bbcfa67d9efb6f699d25776691c8d8518477f2400bf02ed0ef5b80e404a0bba3207635e45a99ab93ef3f43a96aca2e3837ca888274751b07a7e079f429c9ee9d98488a4f389bb9825b29b9abd871819290e5a3f33a6469b57abcccd14df422ca0172255f138c4130ca3087c21685c563be9af21b2e2633988c6e3e87e78366c001e48ab741db29abb54dd9c4eb3ad886b8467e7576dfa47b414f898908b7c9e028cc145e13f344ad7e3200a758d338fcf79d6a1e4b77cdb837bbc8f599789fd9f98e485f07a1c242269e95d7a187b05b02aa18cab02001a6f706171756520677561726469616e206174746163686d656e74000200ff", + "header": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cfc64314060ae5338b793a7dda00d9fd7e1c154b60fa52e324778fe63014c16fc00031d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d48c946fbad478cc946802555991c23ce67a4e1356639e5d6a12212eca7abd898902bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa012319822880a9ae3fee7a22c25a0107daa3d8bdd42251bddb4d4008be10a7589b", + "aad": "01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cfc64314060ae5338b793a7dda00d9fd7e1c154b60fa52e324778fe63014c16fc00031d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d48c946fbad478cc946802555991c23ce67a4e1356639e5d6a12212eca7abd898902bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010000000000000000000000000000000000000000000000000000000000000000", + "digest": "fcd09d346fdca51eca26f035fcefc215af2428e156492c5b8517b051dad8c7f0", + "ciphertext": "02ed0ef5b80e404a0bba3207635e45a99ab93ef3f43a96aca2e3837ca888274751b07a7e079f429c9ee9d98488a4f389bb9825b29b9abd871819290e5a3f33a6469b57abcccd14df422ca0172255f138c4130ca3087c21685c563be9af21b2e2633988c6e3e87e78366c001e48ab741db29abb54dd9c4eb3ad886b8467e7576dfa47b414f898908b7c9e028cc145e13f344ad7e3200a758d338fcf79d6a1e4b77cdb837bbc8f599789fd9f98e485f07a1c242269e95d7a187b05b02aa18cab", + "body": "2436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d600038f3b529ca4bb18dc2b26ba4b0902f272f6aa9cf95b0e8c1464e54b14054b5f40934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f60000000002f71c08000c3500000c31100000000002f733780000000002f71c08000c11d8000000006553f100", + "shared_hash": "9b229b95522d9411cbf5606640e61477d5b029426f6115d99f9e4349567f52c0", + "body_key": "7bda592ec756215eef6afed794fd4baec5850f88e5e6f91978141b79f37f696d" + } + ], + "first_request": "d713202020202020202020202020202020200c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3ed7f03307d4b6cf49005bf8bf5d1dff91b546277bc6240b13d8b692adb30e3e42608e678d0d987d1a3d137b3d8a6603f95d258314ca5ac19e824150d2603c7028", + "first_digest": "8fe78344aa06e37349ebfd510de21631706917c95d55b3503bd6aaa602305fe6", + "continuation_request": "d7132a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c4848484848484848484848484848484848484848484848484848484848484848cab3135c1740416b1a0e2933f26b9194e92797a3c71fa7dfde0fc80dbc9a6f160d5d27d63ea398fc86c15d3743f5f1d10e99fbe1004388617fc13f2ae481420c01020001", + "continuation_digest": "a842fc6e5ac0c946a720443606c24d0797de4b6cb9352ac34a50c9e7eb8eed54", + "odd_request": "d713202020202020202020202020202020200c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3ebefa5f65e7f11854a09810ec91d1898b7f09d95a2134bd7281d7dcd66dd031a47c1cdbe37238d9c247632b4f39529e895354e0481cf39394853de222e55d9f6e030107", + "first_response": "d7152020202020202020202020202020202001000101ed01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cbcd699ce25eda581b77eec605b501c6935c5cc5b80941e235708371c6d1d663a00011d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4de619aec79901ee0ab8d6ccded96ec853812587f785556a69cc0e4bcffef14a302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010cc903269e7b9bd12266f09f0e4afbeb4bff0005ba1b8cdb6bd66ab10edaeb4a8e7800231692024b2409a747d54e7317cb6152431e89a0925599b22e60979fe77c745510f735ad04426e8bf364d9c37fde7fcb16a964163bbd69b0915603137b00bf02d7b6a5bda48673403a0fffd45ceb5bf35d5ceb92271aecf5d4fcef2c644552d7d01331daa63ebf4c6af455538301daed14c25424e9bb57c1eace0e2448e00ab055fef3f2cd9541117bbd57494d9b86efaf87af81aedd461fed72c0087ede06ff8ac7b8237c0d767bb18f53bfd80f26d4f9e2cca6bd1f3d509da2a0676233ac0f31b0a008d9552951ac103a42f0baddc292ad52460c2898dbf71ecc4ef516df35228aec3b34655168122aa3c3bc5f1aae5abaea77541f2b66cfd6e63c5a3a0001020001", + "continuation_response": "d7152a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a010002020d01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c1666deb356225ce9ad5e9b64676970a7d2a2dafb2b2cd9f669df993c081603de00021d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4b2d9c11c5bcd065716e0d8664b3f869b96dac515c6be66fc9bbe4861fe32a9a902bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa00404a86662e5a6d1c7735e1250c28dc5e0103176c3a9274ac25aa0dc1a784d773ed984c97a7cca886059ca31bdd007210d2ba81525a2f9554a249f2d3d8b2e0dc306c2274601fea41a86226119b2d356d0fe06f0667a975ae86250c94210dff1a00bf021ad1aadcca777393747962322a6300c1c4269fd1032fa164f205050e387cb3d96b5799ef4fbd038072e3b84343b946da61e13ef776026d0b3c89c576f28580fd81958edf8218a61c58929e22e232ec8aa8f093efa33243ce746900ad76a23a2244699fb419c5ef6ff8b44a5a04b1282f77d4ea54ec4570734bf53fc52205aebc45fee93b4a3207dee9fcdd5894e225fea414a3a1306a0bd12775c60b678c7765893797c463eaa8ecb73cef571732a239535f2157137c4d0ed4ebd05c150802001a6f706171756520677561726469616e206174746163686d656e74000200ff020d01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cfc64314060ae5338b793a7dda00d9fd7e1c154b60fa52e324778fe63014c16fc00031d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d48c946fbad478cc946802555991c23ce67a4e1356639e5d6a12212eca7abd898902bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa012319822880a9ae3fee7a22c25a0107daa3d8bdd42251bddb4d4008be10a7589b289517b7742990a161b378bd1281783f9df6ae6def0d2a3c17ad42b3c835c80552470e92c37ecd8b0cb3ccdb2bbcfa67d9efb6f699d25776691c8d8518477f2400bf02ed0ef5b80e404a0bba3207635e45a99ab93ef3f43a96aca2e3837ca888274751b07a7e079f429c9ee9d98488a4f389bb9825b29b9abd871819290e5a3f33a6469b57abcccd14df422ca0172255f138c4130ca3087c21685c563be9af21b2e2633988c6e3e87e78366c001e48ab741db29abb54dd9c4eb3ad886b8467e7576dfa47b414f898908b7c9e028cc145e13f344ad7e3200a758d338fcf79d6a1e4b77cdb837bbc8f599789fd9f98e485f07a1c242269e95d7a187b05b02aa18cab02001a6f706171756520677561726469616e206174746163686d656e74000200ff", + "empty_response": "d71520202020202020202020202020202020010000", + "refusal": "d7152020202020202020202020202020202000000b756e617661696c61626c65" + }, + { + "scenario": "D.3", + "records": [ + { + "record": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0dff93b538dbbecc0656c3b8c891f34f58c6be173df6eb28f403bbe1567b3f94990001bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03b4a74a3fe9d634b97cb7d04e9dbf735c98686281041432bb62779cfcb44dc45a02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa0149dc1b04bcef10227bec6317c66ad4c23e7a49877e5929a33472ddb7004de1561577966b8eb138ab27007372194ffb316bff2b29d453522a2eba0f7b5d739b7f612b4d9c163d2ba1dee5b386ff10038e713ac806c5d60ea159da409f2608230000bf022d2595881cfd763dc2cf1a10beb77ff2d80f905318f010293cae31f25fcb6305a033926da31618c3eed370a4c3f7ef4041d9c8f3e2523d4a819bfe0b1c73d75c988815ddd0157ebe16422029b89ba7ef234894b0e31e2573cb9dbc67c7e82488e2e110d78c1bc46f12f7be52ee137bd27d6ba4db9b82bca7755de551d72c60ded7c2ae2dac5061ee91594ddb645cf7ae69ddb55c368bf0fad162346290b04cfefda1cc31154e788cca34d9db6fc86e481d879a1ccdc428151c3c110d7af100", + "header": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0dff93b538dbbecc0656c3b8c891f34f58c6be173df6eb28f403bbe1567b3f94990001bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03b4a74a3fe9d634b97cb7d04e9dbf735c98686281041432bb62779cfcb44dc45a02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa0149dc1b04bcef10227bec6317c66ad4c23e7a49877e5929a33472ddb7004de156", + "aad": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0dff93b538dbbecc0656c3b8c891f34f58c6be173df6eb28f403bbe1567b3f94990001bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03b4a74a3fe9d634b97cb7d04e9dbf735c98686281041432bb62779cfcb44dc45a02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010000000000000000000000000000000000000000000000000000000000000000", + "digest": "dd51fef439d32d6275f0d59408ecf438c9b98221fc13861dfd884b4250b8195c", + "ciphertext": "022d2595881cfd763dc2cf1a10beb77ff2d80f905318f010293cae31f25fcb6305a033926da31618c3eed370a4c3f7ef4041d9c8f3e2523d4a819bfe0b1c73d75c988815ddd0157ebe16422029b89ba7ef234894b0e31e2573cb9dbc67c7e82488e2e110d78c1bc46f12f7be52ee137bd27d6ba4db9b82bca7755de551d72c60ded7c2ae2dac5061ee91594ddb645cf7ae69ddb55c368bf0fad162346290b04cfefda1cc31154e788cca34d9db6fc86e481d879a1ccdc428151c3c110d7af1", + "body": "8685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecb0001ad7d3f3daebc0ed34454033f88de67df1e2281c3d3d66edaa8aa379725d5457d21f2705152e9753f5d63b5a0bdbb1d3cd0da4d56d83e3d3853cd44c12dfe6f8400000000000f2ad0000c3500000c311000000000000f424000000000000f2ad0000c11d8000000006553f100", + "shared_hash": "c21ad369773cff2f2c13c2e522847e691dc42604f638043eb685e9371062a298", + "body_key": "ad74af5ee575cf0dd66654b7a86a5a3d4f65e7f8af57e81b4ea8fcbfd61f89bf" + }, + { + "record": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0ddac78ba1c4ba6cfcd9a6baa5b3859e50630c2682646f1c8bce67b21189b083290002bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03b3940aa781aa6e7392c6b1c6671ee9e83be49d7cd7c9fc4a28be190d3f542b4002bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa0050f7d12201abb3a481b274dbe8dd65b2bc2d645c3f7eee16a78aed86376ef496ec6fe92a6bd51c0b429e31d2c4596bbc07f7565cdecf39bf74659ca137de779c06881feb082155ac0aa25e279b36c036b05189a13325dc768ddfb5f5086a2ee900bf028d05e52ba585a3177ff053e0258e6fbf75d8fb87383b328d35fd3f22b04cbe84e7ada09cfaae01e70ab02ca64983e542b53c2b5526e0f1e9c2b9ce34c3d6447c6fd13aa64f8cbbfe906dc5138fdb391cc6dad0020b0d61121be2d835b8550d7b0115d9ce04e96acbcc7b930627dad8e0062831e9ec2274d32c28cc5f45d977b9a1ad84ffefd8b49a7aabc9680a5abf87450b68d9a7aa9b14954e9adf39a09fbac1981473f18ed3a1d84b63a6a41fa989bebd99b89fe9c871cca7d2e4b96402001a6f706171756520677561726469616e206174746163686d656e74000200ff", + "header": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0ddac78ba1c4ba6cfcd9a6baa5b3859e50630c2682646f1c8bce67b21189b083290002bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03b3940aa781aa6e7392c6b1c6671ee9e83be49d7cd7c9fc4a28be190d3f542b4002bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa0050f7d12201abb3a481b274dbe8dd65b2bc2d645c3f7eee16a78aed86376ef496", + "aad": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0ddac78ba1c4ba6cfcd9a6baa5b3859e50630c2682646f1c8bce67b21189b083290002bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03b3940aa781aa6e7392c6b1c6671ee9e83be49d7cd7c9fc4a28be190d3f542b4002bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa000000000000000000000000000000000000000000000000000000000000000000", + "digest": "45900b8975e275bdf98bcacfb3f47c67d0f95e8808fd73a4528eb0b9e7f4fd53", + "ciphertext": "028d05e52ba585a3177ff053e0258e6fbf75d8fb87383b328d35fd3f22b04cbe84e7ada09cfaae01e70ab02ca64983e542b53c2b5526e0f1e9c2b9ce34c3d6447c6fd13aa64f8cbbfe906dc5138fdb391cc6dad0020b0d61121be2d835b8550d7b0115d9ce04e96acbcc7b930627dad8e0062831e9ec2274d32c28cc5f45d977b9a1ad84ffefd8b49a7aabc9680a5abf87450b68d9a7aa9b14954e9adf39a09fbac1981473f18ed3a1d84b63a6a41fa989bebd99b89fe9c871cca7d2e4b964", + "body": "8685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecb00022436f5dbf9f9b2cf2681dd99f89640d158c3aa94ea880c97a95bc4d10315544258bdb1620d8f37d85a846d3f80a4e43ca084cbe1743d043d429de8dcfb0843c000000000000855ca000c3500000c31100000000000086d3a00000000000855ca000c11d8000000006553f100", + "shared_hash": "a9d4be578020c280ff059cc19edb76d80b6277ad7dd4a8b43000aec4746cdc7a", + "body_key": "e274d00e6b3ab9613b752d7ee4eed4748153d767387c05ab6087cbb80ec75e58" + }, + { + "record": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d84b8c7985979e0a66d6adba1cebed2fb98fd7ceece04a4fa25ff692d175e1f040003bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03ae63fd986d0118207fd97e242d366d1646d10e25d0ba193d7f33a4c0da1aed3302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010bfd3b0d0c30d5efe5e5e5ca02b7c9dc76420814615d8586d601494f7735f5d5fa51ec73bc2879c0f694e2900931fb0f7c63572687fd2c02a2d426c3954eafd121dec33ffdf1fe76cf519744dd458f4242142812b2502121d28f167718c96c2700bf0221d3eb37dfe70b42825e406a369c52c0b4a288a052d5c97195f0a192fc189fe16cd0c1b0846fddfcc8bf1fb0942f3c06f725517ca88c9509c4e0d7f34677fa66c5cf1085071414810cf9eb885f4a352646880257c050c28bc63bacfb25e279abf831f04d198763f63a348b5ef946a720434b006dbc28c41adb612a8f9777f57298dbff15b7882e73b87e28116f6c1a375aa3c70f8e94b2a34547041dd509d2f6d3fbdec4ce74d4ab004c2f052eaa1287b2f804c119db3e941e3ec7f9c91602001a6f706171756520677561726469616e206174746163686d656e74000200ff", + "header": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d84b8c7985979e0a66d6adba1cebed2fb98fd7ceece04a4fa25ff692d175e1f040003bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03ae63fd986d0118207fd97e242d366d1646d10e25d0ba193d7f33a4c0da1aed3302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010bfd3b0d0c30d5efe5e5e5ca02b7c9dc76420814615d8586d601494f7735f5d5", + "aad": "01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d84b8c7985979e0a66d6adba1cebed2fb98fd7ceece04a4fa25ff692d175e1f040003bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03ae63fd986d0118207fd97e242d366d1646d10e25d0ba193d7f33a4c0da1aed3302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010000000000000000000000000000000000000000000000000000000000000000", + "digest": "8d113fdf6c6cc2889addb2d1775ea75ad6d989c52850dde46a7e0af979ad2fcd", + "ciphertext": "0221d3eb37dfe70b42825e406a369c52c0b4a288a052d5c97195f0a192fc189fe16cd0c1b0846fddfcc8bf1fb0942f3c06f725517ca88c9509c4e0d7f34677fa66c5cf1085071414810cf9eb885f4a352646880257c050c28bc63bacfb25e279abf831f04d198763f63a348b5ef946a720434b006dbc28c41adb612a8f9777f57298dbff15b7882e73b87e28116f6c1a375aa3c70f8e94b2a34547041dd509d2f6d3fbdec4ce74d4ab004c2f052eaa1287b2f804c119db3e941e3ec7f9c916", + "body": "8685960c118c83535c1bcc47e33666a775e26e9d1ca6cbca79aade33f9fe3ecb000309bda50251c61692cc3a1c32155c03bc415ed08d6ca22aca8e837986931b5acfdfe77b118b59852036e38b7409770e502899c35e8aced4a5d405ebecc167d0ea0000000002f71c08000c3500000c31100000000002f733780000000002f71c08000c11d8000000006553f100", + "shared_hash": "18b20a1f2be4f544cdc344e87f167cfe99b7bb64def71b381945d3a0473cc7ec", + "body_key": "d694c5ae765f6fe476b3860fe7a4d87ddb83190109beec72e1da3c84946f21de" + } + ], + "first_request": "d713212121212121212121212121212121210d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f8efc656ab10950320a10d02a8a4f4d4e2ed548a6b04f18cb5c28e5d3fd9e7f2b139066260b1ef42489e61bdb003300919d33eee8a9714f1f51ec0e7460363290", + "first_digest": "ce9d89ae500270b1ea1ca0ab1a98b60e507ac74b26956c1ccd5be729c775b8fb", + "continuation_request": "d7132b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d494949494949494949494949494949494949494949494949494949494949494921ab68d77ac4a564b7d3af642e9cdbb51c4b6fe300b4ff31344aadc124c6688c45e0d323f7d532fcbbed960770479a97f37a77fde666ca45ab387ccf2b72533b01020001", + "continuation_digest": "00899bc8247f659f18bcef41f5ce6237b68053d53096e0850902ab8713621f15", + "odd_request": "d713212121212121212121212121212121210d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3fdcb1cd542cd588b8bc82dc66cc311e8a80217db17269fd12d8887f6053ca1b6a689ff906638141d0549bc91c6cc88a2248e2d459119b370ce5d69f8109384b56030107", + "first_response": "d7152121212121212121212121212121212101000101ed01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0dff93b538dbbecc0656c3b8c891f34f58c6be173df6eb28f403bbe1567b3f94990001bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03b4a74a3fe9d634b97cb7d04e9dbf735c98686281041432bb62779cfcb44dc45a02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa0149dc1b04bcef10227bec6317c66ad4c23e7a49877e5929a33472ddb7004de1561577966b8eb138ab27007372194ffb316bff2b29d453522a2eba0f7b5d739b7f612b4d9c163d2ba1dee5b386ff10038e713ac806c5d60ea159da409f2608230000bf022d2595881cfd763dc2cf1a10beb77ff2d80f905318f010293cae31f25fcb6305a033926da31618c3eed370a4c3f7ef4041d9c8f3e2523d4a819bfe0b1c73d75c988815ddd0157ebe16422029b89ba7ef234894b0e31e2573cb9dbc67c7e82488e2e110d78c1bc46f12f7be52ee137bd27d6ba4db9b82bca7755de551d72c60ded7c2ae2dac5061ee91594ddb645cf7ae69ddb55c368bf0fad162346290b04cfefda1cc31154e788cca34d9db6fc86e481d879a1ccdc428151c3c110d7af10001020001", + "continuation_response": "d7152b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b010002020d01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0ddac78ba1c4ba6cfcd9a6baa5b3859e50630c2682646f1c8bce67b21189b083290002bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03b3940aa781aa6e7392c6b1c6671ee9e83be49d7cd7c9fc4a28be190d3f542b4002bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa0050f7d12201abb3a481b274dbe8dd65b2bc2d645c3f7eee16a78aed86376ef496ec6fe92a6bd51c0b429e31d2c4596bbc07f7565cdecf39bf74659ca137de779c06881feb082155ac0aa25e279b36c036b05189a13325dc768ddfb5f5086a2ee900bf028d05e52ba585a3177ff053e0258e6fbf75d8fb87383b328d35fd3f22b04cbe84e7ada09cfaae01e70ab02ca64983e542b53c2b5526e0f1e9c2b9ce34c3d6447c6fd13aa64f8cbbfe906dc5138fdb391cc6dad0020b0d61121be2d835b8550d7b0115d9ce04e96acbcc7b930627dad8e0062831e9ec2274d32c28cc5f45d977b9a1ad84ffefd8b49a7aabc9680a5abf87450b68d9a7aa9b14954e9adf39a09fbac1981473f18ed3a1d84b63a6a41fa989bebd99b89fe9c871cca7d2e4b96402001a6f706171756520677561726469616e206174746163686d656e74000200ff020d01010d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d84b8c7985979e0a66d6adba1cebed2fb98fd7ceece04a4fa25ff692d175e1f040003bcb6c726241e7b35d19c5e0f952271531cf2e61b4979c61cf8c817ebcd22da03ae63fd986d0118207fd97e242d366d1646d10e25d0ba193d7f33a4c0da1aed3302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010bfd3b0d0c30d5efe5e5e5ca02b7c9dc76420814615d8586d601494f7735f5d5fa51ec73bc2879c0f694e2900931fb0f7c63572687fd2c02a2d426c3954eafd121dec33ffdf1fe76cf519744dd458f4242142812b2502121d28f167718c96c2700bf0221d3eb37dfe70b42825e406a369c52c0b4a288a052d5c97195f0a192fc189fe16cd0c1b0846fddfcc8bf1fb0942f3c06f725517ca88c9509c4e0d7f34677fa66c5cf1085071414810cf9eb885f4a352646880257c050c28bc63bacfb25e279abf831f04d198763f63a348b5ef946a720434b006dbc28c41adb612a8f9777f57298dbff15b7882e73b87e28116f6c1a375aa3c70f8e94b2a34547041dd509d2f6d3fbdec4ce74d4ab004c2f052eaa1287b2f804c119db3e941e3ec7f9c91602001a6f706171756520677561726469616e206174746163686d656e74000200ff", + "empty_response": "d71521212121212121212121212121212121010000", + "refusal": "d7152121212121212121212121212121212100000b756e617661696c61626c65" + }, + { + "scenario": "D.4", + "records": [ + { + "record": "01010e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0efdd7ce22551f4a7d434f23eeb167aed3461b642a2e8b10397b0f36cd8a9dc75d0001982ee14fdd19b21558b89adda15b53f54122fc1c5a72bae5ef4261ee154cc776c435aa98298d84d17aa43075d85f909359f3e01d8b3da500e51c452c2e777e9f02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa01994977822c27e61850bdaac4469989c44cec97259e48996dbf2a3c9336c67a0907c8a41633473904bea1fd853d3fac668c5638545a546af80cd950a2d223332022671a2a189b918279666f492177737bb437a2a2c9d5eb0872ce88db561dbcb000bf0319a7062e8e763501a51b2eeab2597131b291da9f7028a330bf42e69e73d49d6861f686e712ce32803f0ed314ed3fb74f488d1c17fb6983be85221853ca8d2b0136194ef326542071ca70ff36a46abc8ead01945f7a4bc44c700f2ed0073ec4250ba4937d5cbcd8e0bada4b3ce54da6c9ce62088f9c2427485ec05a862d652d94271cac47d133c27c42f49b34cdb7aed55f2b02b3127ce38b005c6b5ba291c9bf5ccd24c965b4c14026072f78a2635facd4c786436375ca03fd4830ddaaa900", + "header": "01010e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0efdd7ce22551f4a7d434f23eeb167aed3461b642a2e8b10397b0f36cd8a9dc75d0001982ee14fdd19b21558b89adda15b53f54122fc1c5a72bae5ef4261ee154cc776c435aa98298d84d17aa43075d85f909359f3e01d8b3da500e51c452c2e777e9f02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa01994977822c27e61850bdaac4469989c44cec97259e48996dbf2a3c9336c67a09", + "aad": "01010e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0efdd7ce22551f4a7d434f23eeb167aed3461b642a2e8b10397b0f36cd8a9dc75d0001982ee14fdd19b21558b89adda15b53f54122fc1c5a72bae5ef4261ee154cc776c435aa98298d84d17aa43075d85f909359f3e01d8b3da500e51c452c2e777e9f02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010000000000000000000000000000000000000000000000000000000000000000", + "digest": "1d3d78b938a3b9f211c22db258b89dec1b69e2dbe5ebaaf45958bfdbc90c6c5d", + "ciphertext": "0319a7062e8e763501a51b2eeab2597131b291da9f7028a330bf42e69e73d49d6861f686e712ce32803f0ed314ed3fb74f488d1c17fb6983be85221853ca8d2b0136194ef326542071ca70ff36a46abc8ead01945f7a4bc44c700f2ed0073ec4250ba4937d5cbcd8e0bada4b3ce54da6c9ce62088f9c2427485ec05a862d652d94271cac47d133c27c42f49b34cdb7aed55f2b02b3127ce38b005c6b5ba291c9bf5ccd24c965b4c14026072f78a2635facd4c786436375ca03fd4830ddaaa9", + "body": "87cb2b4c1f4e6b9246bc30f7a7cb145dff0ad8b75153061fe08a31344ec15a0d000170dc8fdc7219f79d7e9fd225046385b269d08c4cd539a06d3de8c88cc1bd0898ec5abb4d6d027af6f9bc6451f6cc98d95ad28fb9c20d67d3e51cb30614c15a6e00000000000854d0000c3500000c31100000000000086c4000000000000854d0000c11d8000000006553f100", + "shared_hash": "0ed1bdfb5e0591029dbabfed1779a3b242012bf9c4e385ef2542044ebdff539a", + "body_key": "2fbce0433d7429536b23c90332da086b95b1372cae6327b0853316ec487db55e" + } + ], + "first_request": "d713222222222222222222222222222222220e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e40404040404040404040404040404040404040404040404040404040404040402341c2612bf00705893db23df9f3b08287ffa3adf56d30cb59ba7c331a137cbf76f7d058045d87faefb025c7c7e577171735a83464d903e4768db0f59e7d2f69", + "first_digest": "29fc06de375fcd80ae7f5015f920addf86283713cf305dcc84e0738f171ff358", + "continuation_request": "d7132c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4abf9c81289d3939c931a756d0b1d477fdd0434634b611e87c0a69b287abcb40b40e58c298e1a95840f1bfc6d9b9013f7d30bc1a68918a4c39800d242fedf08b5c01020001", + "continuation_digest": "62b1d925708aff396677e098eecffe60e2df23df634d04065a26db13b7190f6d", + "odd_request": "d713222222222222222222222222222222220e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e40404040404040404040404040404040404040404040404040404040404040406dbda9bd8cb4da1891db25d607ed26b3a9f61fccba5b8077b95740b2001aa8230a6a26748a8c11c76c400be3afeab068ac26fb6b5f4daccc97cf7456999c78a2030107", + "first_response": "d7152222222222222222222222222222222201000101ed01010e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0efdd7ce22551f4a7d434f23eeb167aed3461b642a2e8b10397b0f36cd8a9dc75d0001982ee14fdd19b21558b89adda15b53f54122fc1c5a72bae5ef4261ee154cc776c435aa98298d84d17aa43075d85f909359f3e01d8b3da500e51c452c2e777e9f02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa01994977822c27e61850bdaac4469989c44cec97259e48996dbf2a3c9336c67a0907c8a41633473904bea1fd853d3fac668c5638545a546af80cd950a2d223332022671a2a189b918279666f492177737bb437a2a2c9d5eb0872ce88db561dbcb000bf0319a7062e8e763501a51b2eeab2597131b291da9f7028a330bf42e69e73d49d6861f686e712ce32803f0ed314ed3fb74f488d1c17fb6983be85221853ca8d2b0136194ef326542071ca70ff36a46abc8ead01945f7a4bc44c700f2ed0073ec4250ba4937d5cbcd8e0bada4b3ce54da6c9ce62088f9c2427485ec05a862d652d94271cac47d133c27c42f49b34cdb7aed55f2b02b3127ce38b005c6b5ba291c9bf5ccd24c965b4c14026072f78a2635facd4c786436375ca03fd4830ddaaa900", + "continuation_response": "d7152c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c010000", + "empty_response": "d71522222222222222222222222222222222010000", + "refusal": "d7152222222222222222222222222222222200000b756e617661696c61626c65" + }, + { + "scenario": "D.5", + "records": [ + { + "record": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0fd1f77ef851befcab3ff6b15941d48f6f243cf0d6deab40cf61a708a015013fdc00017d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a233365f77b786f47d9ead580167ed9f3fb2dae3be84e54b36113f055a82d7592cf7bb102bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa01f3ef2f006ff2ac64c3b9d5169a6f7e85d90630d43e0dac95cce5bb7cf84a2b199591812aa3d085bb01b5c59efaadee7edaaccfe4aa01a62ec20d0c22ffc2b95f7cfdbb025f7fd99cc1f1a22c529945ac9ab363ca6d2fc3c3ee9f4c2a4634693e00bf022b930ce14148bedbe51b004ca3925818837072a6fc96e766a0948581b4f0fed6c4becda5566366e711c4af7b40c23cb1044de100b2c6280526d2c83f1dee9253d23e8b92ce581397a14effffa61246124c94b2861e10adfab8706e6dad00154daef8d40df04bfc13cac8e241e37f42e9ea49bf22fffdba48d3d1f30bfb646a0803c378538cc2f31a788b4fcffeae97908d04ba485564919aa58bb06a21d6f6b194a4b51c7d297fb47f8ae932cb862bf9013f29fe391f56c09a1f87748b4e00", + "header": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0fd1f77ef851befcab3ff6b15941d48f6f243cf0d6deab40cf61a708a015013fdc00017d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a233365f77b786f47d9ead580167ed9f3fb2dae3be84e54b36113f055a82d7592cf7bb102bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa01f3ef2f006ff2ac64c3b9d5169a6f7e85d90630d43e0dac95cce5bb7cf84a2b19", + "aad": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0fd1f77ef851befcab3ff6b15941d48f6f243cf0d6deab40cf61a708a015013fdc00017d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a233365f77b786f47d9ead580167ed9f3fb2dae3be84e54b36113f055a82d7592cf7bb102bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010000000000000000000000000000000000000000000000000000000000000000", + "digest": "40dff01fe0699132e30e7acb5218b95b34bd98ca6b03891ad5f9bd7b3424707b", + "ciphertext": "022b930ce14148bedbe51b004ca3925818837072a6fc96e766a0948581b4f0fed6c4becda5566366e711c4af7b40c23cb1044de100b2c6280526d2c83f1dee9253d23e8b92ce581397a14effffa61246124c94b2861e10adfab8706e6dad00154daef8d40df04bfc13cac8e241e37f42e9ea49bf22fffdba48d3d1f30bfb646a0803c378538cc2f31a788b4fcffeae97908d04ba485564919aa58bb06a21d6f6b194a4b51c7d297fb47f8ae932cb862bf9013f29fe391f56c09a1f87748b4e", + "body": "57d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da600014a248bc87020dceb535314eb837aa562a9d765d7333bae404d7d84f3dfec16f57305b11a874bbd995b29b11d109eff432402136904a3bab1f7bd9f869109cfc100000000000854d0000c3500000c31100000000000086c4000000000000854d0000c11d8000000006553f100", + "shared_hash": "aedd39b6d40ae53c037ca694f2e67c1006da1c749f84d6b649c69b23b358d06f", + "body_key": "5fffcde08d1bd10e221f6a1edc4efa183bb624cdeb0cf20b05bed052c770223b" + }, + { + "record": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f19924db827d72e1e7fa6e43a6e5c02cd37e8bf8ff3acd46f422c98c5c6f42f8100027d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a2333652849c35e9b8408eec0bbc2b7f2a1824c427d7c14ab1eee51102ecc8ed1fcfebf02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa00e7ec659da527b9788f8b5b56b08a2d6bfde7a893b403664d9630655ea5ba6297094d65d319f8bdb5ec0362cbb8d7fc9caef4755375d34054179916073ff38d404f4ddd3ea9f073628f16766dcd100fd60415586cf136294aa0fafebd100b92c900bf027cb2bda4b1dbaa6c8e4348ef03372e2d75580b74e18d930e2a3ad376ae13464b367dc66af494bb110e740f2003c40ea305ed3be6724e4d093717f6ce1785ea2cf907db09a6a0659b2bb17ccbdeea700657e7e21f0c95fd77e05dc9b7c48b434effea0dc91f7ac4cf59755b2ea45782782cd80d6ae5e4c9e7d4031cec4915c73efcc84e98afa492c94a2f63442683a37b0cdeb62d20f5fe80d3d5aa0c5ee1f8afa234bc6c9e0128ee1a65671f9fa7e7053a487b9e4716cbb682e22c69c9e902001a6f706171756520677561726469616e206174746163686d656e74000200ff", + "header": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f19924db827d72e1e7fa6e43a6e5c02cd37e8bf8ff3acd46f422c98c5c6f42f8100027d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a2333652849c35e9b8408eec0bbc2b7f2a1824c427d7c14ab1eee51102ecc8ed1fcfebf02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa00e7ec659da527b9788f8b5b56b08a2d6bfde7a893b403664d9630655ea5ba6297", + "aad": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f19924db827d72e1e7fa6e43a6e5c02cd37e8bf8ff3acd46f422c98c5c6f42f8100027d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a2333652849c35e9b8408eec0bbc2b7f2a1824c427d7c14ab1eee51102ecc8ed1fcfebf02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa000000000000000000000000000000000000000000000000000000000000000000", + "digest": "063a222d05bc040eeb93f4a49dc31819bc88bf5ce52adc60e0bd2558e1ed5780", + "ciphertext": "027cb2bda4b1dbaa6c8e4348ef03372e2d75580b74e18d930e2a3ad376ae13464b367dc66af494bb110e740f2003c40ea305ed3be6724e4d093717f6ce1785ea2cf907db09a6a0659b2bb17ccbdeea700657e7e21f0c95fd77e05dc9b7c48b434effea0dc91f7ac4cf59755b2ea45782782cd80d6ae5e4c9e7d4031cec4915c73efcc84e98afa492c94a2f63442683a37b0cdeb62d20f5fe80d3d5aa0c5ee1f8afa234bc6c9e0128ee1a65671f9fa7e7053a487b9e4716cbb682e22c69c9e9", + "body": "57d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da60002d2b82a0e68a98f632058bffb2e87af0dd759c7129279512b5aabf27b23007e4ccbf141d9ad3b41938c25083328862acf711291bb47ab53b33dc37898ef9326b400000000000854d0000c3500000c31100000000000086c4000000000000854d0000c11d8000000006553f100", + "shared_hash": "e250073effcb403c96ea4b426f0e1c5c8ea0d1296957fb4701acd07a5c7b4cda", + "body_key": "fa9dd3f99e3d0573b7f5eceb2f342bd62adc272c072f6899a9b30c496d47e05c" + }, + { + "record": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f03dc2fb7b5ea3d236b4eed4ee526445383bd9eb630e97d5877c8ec429441963500037d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a2333655d390fdc47a3b65b53f35f69a2672c7a558a163234610dfac3361a824a6b4a6302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa016c62c7ff6f8446dbd5fdeb9e4983cf0a55ff9ee066983debb57a6c55d0f64f5894fefa6ba80df936d0966686185665c3c42e0180d482dae6eb66bee8711e44e20c12f7de867fc12a8e75d1e8053220b4cf075e8b8a51d5b00543c0b675ad0fdc00bf02195d50fa2f6bbdbbf032f241754f606e9be51ea20d7aa898535b95badb718ff685894956e177bfb4082dc0d983bc63375abb774b1fcdbf20cfe828a4e54149771d182dd7d37d09bb920fae9778df3a0c6b70db2099ab9520894598435268cf51b200d5b898187109aa4d205ceb7f09340d030238fce23c314469ebd7995bd123acdee4ef33c9a6940df20955f98153f8f780fcb832d095aebd9d1599fd4b087f163083d1e8ed650194eea66febc2f0515a445937e338b1922d346fe1375f02001a6f706171756520677561726469616e206174746163686d656e74000200ff", + "header": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f03dc2fb7b5ea3d236b4eed4ee526445383bd9eb630e97d5877c8ec429441963500037d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a2333655d390fdc47a3b65b53f35f69a2672c7a558a163234610dfac3361a824a6b4a6302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa016c62c7ff6f8446dbd5fdeb9e4983cf0a55ff9ee066983debb57a6c55d0f64f58", + "aad": "01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f03dc2fb7b5ea3d236b4eed4ee526445383bd9eb630e97d5877c8ec429441963500037d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a2333655d390fdc47a3b65b53f35f69a2672c7a558a163234610dfac3361a824a6b4a6302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010000000000000000000000000000000000000000000000000000000000000000", + "digest": "84687ba0c061a1403df20d733822deba7c63f4204b593394d13e55ad332f0c9e", + "ciphertext": "02195d50fa2f6bbdbbf032f241754f606e9be51ea20d7aa898535b95badb718ff685894956e177bfb4082dc0d983bc63375abb774b1fcdbf20cfe828a4e54149771d182dd7d37d09bb920fae9778df3a0c6b70db2099ab9520894598435268cf51b200d5b898187109aa4d205ceb7f09340d030238fce23c314469ebd7995bd123acdee4ef33c9a6940df20955f98153f8f780fcb832d095aebd9d1599fd4b087f163083d1e8ed650194eea66febc2f0515a445937e338b1922d346fe1375f", + "body": "57d51e11b3a8feac73e8ea58aa6824b7bba69e045416f6ba4a586348adc40da60003dd5a139a0dc0e8c4ff8edb7852eb139eede27b3cb97ebaada4c012f5c48b26a562e42ded68912761a397087a1310af972f9022330fbbbf24c580b9e71479fd8700000000000854d0000c3500000c31100000000000086c4000000000000854d0000c11d8000000006553f100", + "shared_hash": "4156166d5203c323a918b55d28c2c3440f44e0ea65631f08e161a14d1885593d", + "body_key": "a1b1ef9a04a02c7d5935e63e241566e22b8ace1f3358a3da02eb9e8f2b45c552" + } + ], + "first_request": "d713232323232323232323232323232323230f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f4141414141414141414141414141414141414141414141414141414141414141b91c67b7b9eff74f27e9adb437298a6c51ab83b78356e6d52f135d1843a6f5d92c602d7753e4f20d9fcac7ed2fd0ce638839b45f524d6e2b6cf03462af315f6b", + "first_digest": "6652d4e3734dca335384c4738ff144b3e7332da66e12603bc898267010812322", + "continuation_request": "d7132d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4be7f5e5cae4c2fa74f14c0e145d33120f743c5c7577994a97c68cc1b65feba6d76ea7715eeda021ca23777419c52e2e862113a0e27369b975c519816d23308dd701020001", + "continuation_digest": "37097321cdd46e93b5e0fee840a6c7c2c4832624b6e7168d004ea5cbbe849085", + "odd_request": "d713232323232323232323232323232323230f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f41414141414141414141414141414141414141414141414141414141414141419c8ecdcfca5921bcd7f1ef8b3cc1d5e0e619c1b4f564faae92eb1a5e61ebf3ac7d3bcb6056ef8059afc5e2f8cd6492eb50bbb3fef4fb96eea65d6d9f0e93cc2d030107", + "first_response": "d7152323232323232323232323232323232301000101ed01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0fd1f77ef851befcab3ff6b15941d48f6f243cf0d6deab40cf61a708a015013fdc00017d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a233365f77b786f47d9ead580167ed9f3fb2dae3be84e54b36113f055a82d7592cf7bb102bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa01f3ef2f006ff2ac64c3b9d5169a6f7e85d90630d43e0dac95cce5bb7cf84a2b199591812aa3d085bb01b5c59efaadee7edaaccfe4aa01a62ec20d0c22ffc2b95f7cfdbb025f7fd99cc1f1a22c529945ac9ab363ca6d2fc3c3ee9f4c2a4634693e00bf022b930ce14148bedbe51b004ca3925818837072a6fc96e766a0948581b4f0fed6c4becda5566366e711c4af7b40c23cb1044de100b2c6280526d2c83f1dee9253d23e8b92ce581397a14effffa61246124c94b2861e10adfab8706e6dad00154daef8d40df04bfc13cac8e241e37f42e9ea49bf22fffdba48d3d1f30bfb646a0803c378538cc2f31a788b4fcffeae97908d04ba485564919aa58bb06a21d6f6b194a4b51c7d297fb47f8ae932cb862bf9013f29fe391f56c09a1f87748b4e0001020001", + "continuation_response": "d7152d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d010002020d01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f19924db827d72e1e7fa6e43a6e5c02cd37e8bf8ff3acd46f422c98c5c6f42f8100027d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a2333652849c35e9b8408eec0bbc2b7f2a1824c427d7c14ab1eee51102ecc8ed1fcfebf02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa00e7ec659da527b9788f8b5b56b08a2d6bfde7a893b403664d9630655ea5ba6297094d65d319f8bdb5ec0362cbb8d7fc9caef4755375d34054179916073ff38d404f4ddd3ea9f073628f16766dcd100fd60415586cf136294aa0fafebd100b92c900bf027cb2bda4b1dbaa6c8e4348ef03372e2d75580b74e18d930e2a3ad376ae13464b367dc66af494bb110e740f2003c40ea305ed3be6724e4d093717f6ce1785ea2cf907db09a6a0659b2bb17ccbdeea700657e7e21f0c95fd77e05dc9b7c48b434effea0dc91f7ac4cf59755b2ea45782782cd80d6ae5e4c9e7d4031cec4915c73efcc84e98afa492c94a2f63442683a37b0cdeb62d20f5fe80d3d5aa0c5ee1f8afa234bc6c9e0128ee1a65671f9fa7e7053a487b9e4716cbb682e22c69c9e902001a6f706171756520677561726469616e206174746163686d656e74000200ff020d01010f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f03dc2fb7b5ea3d236b4eed4ee526445383bd9eb630e97d5877c8ec429441963500037d36f8f1728a75368594fed88f57f4b24113c1a76d673264b1a2b89c4a2333655d390fdc47a3b65b53f35f69a2672c7a558a163234610dfac3361a824a6b4a6302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa016c62c7ff6f8446dbd5fdeb9e4983cf0a55ff9ee066983debb57a6c55d0f64f5894fefa6ba80df936d0966686185665c3c42e0180d482dae6eb66bee8711e44e20c12f7de867fc12a8e75d1e8053220b4cf075e8b8a51d5b00543c0b675ad0fdc00bf02195d50fa2f6bbdbbf032f241754f606e9be51ea20d7aa898535b95badb718ff685894956e177bfb4082dc0d983bc63375abb774b1fcdbf20cfe828a4e54149771d182dd7d37d09bb920fae9778df3a0c6b70db2099ab9520894598435268cf51b200d5b898187109aa4d205ceb7f09340d030238fce23c314469ebd7995bd123acdee4ef33c9a6940df20955f98153f8f780fcb832d095aebd9d1599fd4b087f163083d1e8ed650194eea66febc2f0515a445937e338b1922d346fe1375f02001a6f706171756520677561726469616e206174746163686d656e74000200ff", + "empty_response": "d71523232323232323232323232323232323010000", + "refusal": "d7152323232323232323232323232323232300000b756e617661696c61626c65" + }, + { + "scenario": "D.6", + "records": [ + { + "record": "0101101010101010101010101010101010101010101010101010101010101010101060e998a7596be1d6214a4dedab40b1ef6240eb80e9af2d13a17a580bc2e27835000165129deb07cd484e12e9badcbc11dace2e724e9a1ccba6b40ec92d57dbaaf028ce7a76caca891ecdf0e4ab28850e7144fcb172485814913dd61f76a12a14191702bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa019ab098922a90109660f460325aa1c6a07fb576421f502cb4e1ef6f0c19752e71dbb56d1d25935d8746ad440c1dff07a7ed8edc97ad8866ecffdaac441214451c708a399a00eed8d533e1b2edcc4d70c1c3b81958c015951f9a0cef4c7e35a15500bf029b35e642afeb65b12bec1bafd85e422048683ca68f538416e1b5311942b35d8dd7cd87177447dacbd9093061d9adc75de83073b09d6a0b36d06ab47ba828e3fea28d269957f63ee251193de1ed29c0a237a0b69ebf7dd4c6241411482d023529bd526756e9345fb6592d288d6adc1212bae924ebd26c0efd1bd7b1768ce6689668710dbe4744f448ef4d608ff884323bd75a68432bfb4d41d61d53bf0f1adeb2ede49e54eafa05b9a5d846cd1eed10095347d7d569d65059a90bd32098ba00", + "header": "0101101010101010101010101010101010101010101010101010101010101010101060e998a7596be1d6214a4dedab40b1ef6240eb80e9af2d13a17a580bc2e27835000165129deb07cd484e12e9badcbc11dace2e724e9a1ccba6b40ec92d57dbaaf028ce7a76caca891ecdf0e4ab28850e7144fcb172485814913dd61f76a12a14191702bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa019ab098922a90109660f460325aa1c6a07fb576421f502cb4e1ef6f0c19752e71", + "aad": "0101101010101010101010101010101010101010101010101010101010101010101060e998a7596be1d6214a4dedab40b1ef6240eb80e9af2d13a17a580bc2e27835000165129deb07cd484e12e9badcbc11dace2e724e9a1ccba6b40ec92d57dbaaf028ce7a76caca891ecdf0e4ab28850e7144fcb172485814913dd61f76a12a14191702bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010000000000000000000000000000000000000000000000000000000000000000", + "digest": "1add212ba7e2d6b77eda74f989dbd80a8664e20f4f7803b6c8c4c839a74a17e4", + "ciphertext": "029b35e642afeb65b12bec1bafd85e422048683ca68f538416e1b5311942b35d8dd7cd87177447dacbd9093061d9adc75de83073b09d6a0b36d06ab47ba828e3fea28d269957f63ee251193de1ed29c0a237a0b69ebf7dd4c6241411482d023529bd526756e9345fb6592d288d6adc1212bae924ebd26c0efd1bd7b1768ce6689668710dbe4744f448ef4d608ff884323bd75a68432bfb4d41d61d53bf0f1adeb2ede49e54eafa05b9a5d846cd1eed10095347d7d569d65059a90bd32098ba", + "body": "75ac6c4567bb853d69517b2fe7660700e83262c129fb80c5f3e7b8705d2b2f9300012844e487856ac207d67b8c1c60ed43adf76cd50df7262fba665046fc4a6762db64c356aaa2f7789a1ddf82024f18b37dfc22b4fb3ec7583ab7b09f8a9ad1312a00000000000f4240000c3500000c311000000000000f59b000000000000f4240000c11d8000000006553f100", + "shared_hash": "19d3ff84236ada8caedfd713963b29876664010a39e5c7e7fcad080a50ae4dac", + "body_key": "a332fae27d2b1f037901701dbea4087c4fe4d73d56c1dabe70ffea6743f3a170" + } + ], + "first_request": "d7132424242424242424242424242424242410101010101010101010101010101010101010101010101010101010101010104242424242424242424242424242424242424242424242424242424242424242e6b02eacf844b7e95ab19362093a596b5a42ea51510606e0f6ef16b5e235f4e64451ff0da26ca0eb0c392e6f96b336aada2f4b83f00a95abfdfc78face600017", + "first_digest": "a759663e7277991a975309d30d995137be51dabeff3b9361045e384d1e6995c8", + "continuation_request": "d7132e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e10101010101010101010101010101010101010101010101010101010101010104c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c336827ad81678140f80dd502bfd55b4f4751870cbd17b3ccc2faf011dd08c54f47f608616d13fc63def4f38f326b8eaf79b3a3f21578db4e9041b984e5d022aa01020001", + "continuation_digest": "67c08681c72ca3d11196f72575722a82767ab1d4d6c93c7812151c451706e0b5", + "odd_request": "d71324242424242424242424242424242424101010101010101010101010101010101010101010101010101010101010101042424242424242424242424242424242424242424242424242424242424242420abdfa8754279f8893ecde1f2454754a0d3ac305b64bdeb01ba055540c9b53991a074b0ee308b8457a90217c417f1f9bdc89ed0bea3a28cd687cea7f5f101d88030107", + "first_response": "d7152424242424242424242424242424242401000101ed0101101010101010101010101010101010101010101010101010101010101010101060e998a7596be1d6214a4dedab40b1ef6240eb80e9af2d13a17a580bc2e27835000165129deb07cd484e12e9badcbc11dace2e724e9a1ccba6b40ec92d57dbaaf028ce7a76caca891ecdf0e4ab28850e7144fcb172485814913dd61f76a12a14191702bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa019ab098922a90109660f460325aa1c6a07fb576421f502cb4e1ef6f0c19752e71dbb56d1d25935d8746ad440c1dff07a7ed8edc97ad8866ecffdaac441214451c708a399a00eed8d533e1b2edcc4d70c1c3b81958c015951f9a0cef4c7e35a15500bf029b35e642afeb65b12bec1bafd85e422048683ca68f538416e1b5311942b35d8dd7cd87177447dacbd9093061d9adc75de83073b09d6a0b36d06ab47ba828e3fea28d269957f63ee251193de1ed29c0a237a0b69ebf7dd4c6241411482d023529bd526756e9345fb6592d288d6adc1212bae924ebd26c0efd1bd7b1768ce6689668710dbe4744f448ef4d608ff884323bd75a68432bfb4d41d61d53bf0f1adeb2ede49e54eafa05b9a5d846cd1eed10095347d7d569d65059a90bd32098ba00", + "continuation_response": "d7152e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e010000", + "empty_response": "d71524242424242424242424242424242424010000", + "refusal": "d7152424242424242424242424242424242400000b756e617661696c61626c65" + } + ] +} diff --git a/lightning-ffor/tests/data/generate_beignet_witness_fetch.cjs b/lightning-ffor/tests/data/generate_beignet_witness_fetch.cjs new file mode 100644 index 00000000000..fbaf63de589 --- /dev/null +++ b/lightning-ffor/tests/data/generate_beignet_witness_fetch.cjs @@ -0,0 +1,86 @@ +// Reproduce Appendix F fetch and encrypted-record fixtures with pinned Beignet codecs. +// Usage: TS_NODE_PROJECT=/path/to/beignet/tsconfig.json node \ +// -r /path/to/beignet/node_modules/ts-node/register generate_beignet_witness_fetch.cjs \ +// /path/to/beignet /path/to/appendix-d.json /path/to/beignet-witness.json /path/to/output.json +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const { execFileSync } = require('node:child_process'); +const reference = path.resolve(process.argv[2]); +const revision = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: reference, encoding: 'utf8' }).trim(); +if (revision !== '8aee31d18e596fe49a0d195b325a6e757d7a009b') throw new Error('Reference revision mismatch'); +const codecs = require(path.join(reference, 'src/lightning/ffor/witness-messages.ts')); +const encryption = require(path.join(reference, 'src/lightning/ffor/witness-crypto.ts')); +const curve = require(path.join(reference, 'src/lightning/crypto/ecdh.ts')); +const { hkdf } = require(path.join(reference, 'src/lightning/crypto/hkdf.ts')); +const { decodeVoucherBook } = require(path.join(reference, 'src/lightning/ffor/transcript.ts')); +const source = fs.readFileSync(process.argv[3]); +const setups = JSON.parse(source); +const provisions = JSON.parse(fs.readFileSync(process.argv[4])); +const sha = (...parts) => crypto.createHash('sha256').update(Buffer.concat(parts.map(x => typeof x === 'string' ? Buffer.from(x) : x))).digest(); +function wire(type, bytes) { const b = Buffer.alloc(2); b.writeUInt16BE(type); return Buffer.concat([b, bytes]).toString('hex'); } +// Public deterministic fixture keys, matching the provisioning fixture generator. +const fetchSecret = Buffer.alloc(32, 42), witnessSecret = Buffer.alloc(32, 43), encSecret = Buffer.alloc(32, 44); +const fixtures = setups.map((setup, i) => { + const manifest = codecs.decodeManifest(Buffer.from(provisions.fixtures[i].manifest, 'hex')); + const book = decodeVoucherBook(manifest.book); + const records = book.entries.slice(0, 3).map(entry => { + const slot = Buffer.alloc(2); slot.writeUInt16BE(entry.k); + const preimage = sha(`ffor/vector/${setup.scenario}/preimage`, slot); + if (!sha(preimage).equals(entry.paymentHash)) throw new Error('Public vector preimage mismatch'); + const header = { + version: 1, profile: 1, mailboxId: manifest.mailboxId, + recordId: sha(`ffor/witness-fetch-fixture/${setup.scenario}/record`, slot), + k: entry.k, hAct: manifest.hAct, termsHash: codecs.termsHash(entry), + witnessNodeId: curve.getPublicKey(witnessSecret), encPubkey: manifest.encPubkey, + recordedHeight: 790010, flags: entry.k % 2, ciphertextHash: Buffer.alloc(32) + }; + const body = codecs.encodeRecordBody({ + epochId: book.epochId, k: entry.k, t: preimage, hK: entry.paymentHash, + dK: entry.amountMsat, tExp: entry.voucherExpiry, d: entry.settlementDeadline, + amountInMsat: entry.amountMsat + 6000n, amountOutMsat: entry.amountMsat, + outgoingCltv: 791000, observedUnixTime: 1700000000n + }); + const ephemeral = sha(`ffor/witness-fetch-fixture/${setup.scenario}/ephemeral`, slot); + const savedRandom = crypto.randomBytes; + let ciphertext; + try { + // Only this isolated deterministic fixture replaces randomness, never production code. + crypto.randomBytes = length => { if (length !== 32) throw new Error('Unexpected draw'); return ephemeral; }; + ciphertext = encryption.sealRecordBody(manifest.encPubkey, codecs.recordAad(header), body); + } finally { crypto.randomBytes = savedRandom; } + header.ciphertextHash = sha(ciphertext); + const record = { header, witnessSig: curve.sign(codecs.recordDigest(codecs.encodeRecordHeader(header)), witnessSecret), ciphertext, + receipts: entry.k === 1 ? [] : [Buffer.from('opaque guardian attachment'), Buffer.from([0, 255])] }; + const encoded = codecs.encodeRecord(record); + if (!codecs.verifyRecordSignature(codecs.decodeRecord(encoded))) throw new Error('Record signature mismatch'); + if (!encryption.openRecordBody(encSecret, codecs.recordAad(header), ciphertext).equals(body)) throw new Error('Reference AEAD mismatch'); + const verified = codecs.verifyWitnessRecord(record, { + witnessNodeId: header.witnessNodeId, mailboxId: manifest.mailboxId, encPrivkey: encSecret + }, book.entries, book.epochId, manifest.hAct); + if (!verified.ok) throw new Error(verified.reason); + const shared = curve.ecdh(ephemeral, manifest.encPubkey); + return { record: encoded.toString('hex'), header: codecs.encodeRecordHeader(header).toString('hex'), aad: codecs.recordAad(header).toString('hex'), + digest: codecs.recordDigest(codecs.encodeRecordHeader(header)).toString('hex'), ciphertext: ciphertext.toString('hex'), body: body.toString('hex'), + shared_hash: shared.toString('hex'), body_key: hkdf(Buffer.alloc(0), shared, Buffer.from('ffor/witness/body'), 32).toString('hex') }; + }); + const id = Buffer.alloc(16, i + 31), nonce = Buffer.alloc(32, i + 61); + const first = codecs.encodeWitnessFetch(id, manifest.mailboxId, nonce, fetchSecret); + const after = codecs.encodeWitnessFetch(Buffer.alloc(16, i + 41), manifest.mailboxId, Buffer.alloc(32, i + 71), fetchSecret, 1); + const tlv = Buffer.from([3, 1, 7]); + const odd = Buffer.concat([id, manifest.mailboxId, nonce, curve.sign(codecs.fetchDigest(manifest.mailboxId, nonce, tlv), fetchSecret), tlv]); + return { scenario: setup.scenario, records, + first_request: wire(55059, first), first_digest: codecs.fetchDigest(manifest.mailboxId, nonce).toString('hex'), + continuation_request: wire(55059, after), continuation_digest: codecs.fetchDigest(manifest.mailboxId, Buffer.alloc(32, i + 71), codecs.fetchTlv(1)).toString('hex'), + odd_request: wire(55059, odd), + first_response: wire(55061, codecs.encodeWitnessFetchResp({ requestId: id, ok: true, + records: [codecs.decodeRecord(Buffer.from(records[0].record, 'hex'))], ...(records.length > 1 ? { nextAfterK: 1 } : {}) })), + continuation_response: wire(55061, codecs.encodeWitnessFetchResp({ requestId: Buffer.alloc(16, i + 41), ok: true, + records: records.slice(1).map(r => codecs.decodeRecord(Buffer.from(r.record, 'hex'))) })), + empty_response: wire(55061, codecs.encodeWitnessFetchResp({ requestId: id, ok: true, records: [] })), + refusal: wire(55061, codecs.encodeWitnessFetchResp({ requestId: id, ok: false, error: 'unavailable' })) }; +}); +fs.writeFileSync(process.argv[5], JSON.stringify({ source_revision: revision, + specification_revision: 'd719161f42d1eeb6bd6c3856d564222f03c2205e', + source_files: ['src/lightning/ffor/witness-messages.ts', 'src/lightning/ffor/witness-crypto.ts', 'src/lightning/crypto/ecdh.ts'], + appendix_d_sha256: sha(source).toString('hex'), fixtures }, null, 2) + '\n'); diff --git a/lightning-ffor/tests/setup.rs b/lightning-ffor/tests/setup.rs index de5b0f32d41..30b3ef59f1c 100644 --- a/lightning-ffor/tests/setup.rs +++ b/lightning-ffor/tests/setup.rs @@ -371,3 +371,24 @@ proptest! { prop_assert_eq!(setup.validate_activation(&activation, [9; 32], 990), Err(SetupError::Transcript)); } } + +#[test] +fn close_intent_requires_receiver_signature_exact_epoch_and_activation() { + let setup = setup(); + let hash = [21; 32]; + let close = message(Payload::Close(hash), 42); + assert_eq!(setup.validate_close(&close, hash), Ok(())); + assert_eq!(setup.validate_close(&close, [22; 32]), Err(SetupError::Transcript)); + assert!(setup.validate_close(&message(Payload::Close(hash), 43), hash).is_err()); + let mut wrong_epoch = close.clone(); + wrong_epoch.header.epoch_id[0] ^= 1; + sign(&mut wrong_epoch, 42); + assert_eq!(setup.validate_close(&wrong_epoch, hash), Err(SetupError::Identity)); + assert_eq!( + setup.validate_close(&message(Payload::ActivateAck(hash), 42), hash), + Err(SetupError::MessageType) + ); + let mut modified = close; + modified.extensions.push(Tlv { kind: 103, value: vec![1] }); + assert!(setup.validate_close(&modified, hash).is_err()); +} diff --git a/lightning-ffor/tests/witness.rs b/lightning-ffor/tests/witness.rs index fa412b3d82a..aff40573cce 100644 --- a/lightning-ffor/tests/witness.rs +++ b/lightning-ffor/tests/witness.rs @@ -10,6 +10,9 @@ use lightning_ffor::witness::{ use proptest::prelude::*; use serde::Deserialize; +#[path = "witness/fetch.rs"] +mod fetch; + fn hex(value: &str) -> Vec { (0..value.len()).step_by(2).map(|i| u8::from_str_radix(&value[i..i + 2], 16).unwrap()).collect() } diff --git a/lightning-ffor/tests/witness/fetch.rs b/lightning-ffor/tests/witness/fetch.rs new file mode 100644 index 00000000000..b24434923ec --- /dev/null +++ b/lightning-ffor/tests/witness/fetch.rs @@ -0,0 +1,476 @@ +use super::*; +use lightning_ffor::wire::{Tlv, WireError}; +use lightning_ffor::witness::{ + EncryptedRecord, FetchParameters, FetchResponse, FetchResult, PendingFetch, SignedFetch, + UnsignedFetch, CIPHERTEXT_LEN, RECORD_HEADER_LEN, +}; + +#[derive(Deserialize)] +struct FetchReference { + source_revision: String, + specification_revision: String, + appendix_d_sha256: String, + source_files: Vec, + fixtures: Vec, +} +#[derive(Deserialize)] +struct FetchFixture { + scenario: String, + records: Vec, + first_request: String, + first_digest: String, + continuation_request: String, + continuation_digest: String, + odd_request: String, + first_response: String, + continuation_response: String, + empty_response: String, + refusal: String, +} +#[derive(Deserialize)] +struct RecordFixture { + record: String, + header: String, + aad: String, + digest: String, + ciphertext: String, + body: String, + shared_hash: String, + body_key: String, +} + +fn fixtures() -> FetchReference { + serde_json::from_str(include_str!("../data/beignet-witness-fetch.json")).unwrap() +} +fn retained(index: usize) -> SignedManifest { + SignedManifest::decode(&hex(&reference().fixtures[index].manifest), &public_setup(index)) + .unwrap() +} +fn connection() -> WitnessConnection { + WitnessConnection { node_id: key(43), identity: 7 } +} +fn first(index: usize) -> PendingFetch { + let request = + SignedFetch::decode(&hex(&fixtures().fixtures[index].first_request), key(42)).unwrap(); + PendingFetch::first(request, retained(index), connection()).unwrap() +} +fn resign_record(mut bytes: Vec, signer: u8) -> Vec { + let digest = sha256::Hash::hash(&[b"ffor/witness/record".as_slice(), &bytes[..235]].concat()) + .to_byte_array(); + bytes[235..299].copy_from_slice(&signature(digest, signer)); + bytes +} +fn resign_fetch(mut bytes: Vec) -> Vec { + let digest = sha256::Hash::hash( + &[b"ffor/witness/fetch".as_slice(), &bytes[18..82], &bytes[146..]].concat(), + ) + .to_byte_array(); + bytes[82..146].copy_from_slice(&signature(digest, 42)); + bytes +} +fn page(request: [u8; 16], records: Vec, next: Option) -> FetchResponse { + FetchResponse::new( + request, + FetchResult::Page { records, next_after_slot: next, extensions: Vec::new() }, + ) + .unwrap() +} + +#[test] +fn witness_fetch_pinned_reference_roundtrips_and_domains_match() { + let inputs = fixtures(); + assert_eq!(inputs.source_revision, "8aee31d18e596fe49a0d195b325a6e757d7a009b"); + assert_eq!(inputs.specification_revision, "d719161f42d1eeb6bd6c3856d564222f03c2205e"); + assert_eq!(inputs.source_files.len(), 3); + assert_eq!( + hex(&inputs.appendix_d_sha256), + sha256::Hash::hash(include_bytes!("../data/appendix-d.json")).to_byte_array() + ); + for (index, fixture) in inputs.fixtures.iter().enumerate() { + assert_eq!(fixture.scenario, reference().fixtures[index].scenario); + for (wire, digest) in [ + (&fixture.first_request, &fixture.first_digest), + (&fixture.continuation_request, &fixture.continuation_digest), + ] { + let request = SignedFetch::decode(&hex(wire), key(42)).unwrap(); + assert_eq!(request.encode(), hex(wire)); + assert_eq!(request.unsigned().signing_digest().to_vec(), hex(digest)); + } + let odd = SignedFetch::decode(&hex(&fixture.odd_request), key(42)).unwrap(); + assert_eq!(odd.encode(), hex(&fixture.odd_request)); + assert_eq!(odd.unsigned().parameters().extensions, vec![Tlv { kind: 3, value: vec![7] }]); + for wire in [ + &fixture.first_response, + &fixture.continuation_response, + &fixture.empty_response, + &fixture.refusal, + ] { + assert_eq!(FetchResponse::decode(&hex(wire)).unwrap().encode(), hex(wire)); + } + for record in &fixture.records { + let parsed = EncryptedRecord::decode(&hex(&record.record)).unwrap(); + assert_eq!(parsed.encode(), hex(&record.record)); + assert_eq!(parsed.header().encode(), hex(&record.header)); + assert_eq!(parsed.header().associated_data(), hex(&record.aad)); + assert_eq!(parsed.header().signing_digest().to_vec(), hex(&record.digest)); + assert_eq!(parsed.ciphertext(), hex(&record.ciphertext)); + assert_eq!(parsed.ciphertext().len(), CIPHERTEXT_LEN); + assert_eq!(hex(&record.body).len(), 142); + assert_eq!(hex(&record.body_key).len(), 32); + let ephemeral = sha256::Hash::hash( + &[ + format!("ffor/witness-fetch-fixture/{}/ephemeral", fixture.scenario).as_bytes(), + &parsed.header().slot.to_be_bytes(), + ] + .concat(), + ) + .to_byte_array(); + let shared = bitcoin::secp256k1::ecdh::SharedSecret::new( + &key(44), + &SecretKey::from_slice(&ephemeral).unwrap(), + ); + assert_eq!(shared.secret_bytes().to_vec(), hex(&record.shared_hash)); + assert!(parsed.authenticate(&retained(index), key(43)).is_ok()); + } + } +} + +#[test] +fn witness_fetch_authentication_excludes_request_id_but_binds_mailbox_nonce_and_tlvs() { + let wire = hex(&fixtures().fixtures[1].continuation_request); + let mut renamed = wire.clone(); + renamed[2] ^= 1; + assert!(SignedFetch::decode(&renamed, key(42)).is_ok()); + for offset in [18, 50, 149] { + let mut damaged = wire.clone(); + damaged[offset] ^= 1; + assert!(SignedFetch::decode(&damaged, key(42)).is_err()); + } + assert!(SignedFetch::decode(&wire, key(43)).is_err()); + let parsed = SignedFetch::decode(&wire, key(42)).unwrap(); + assert!(parsed + .unsigned() + .clone() + .authenticate(signature(transcript::message_digest(55059, &wire[2..82]), 42), key(42)) + .is_err()); + let p = parsed.unsigned().parameters().clone(); + let mut collision = p.clone(); + collision.extensions.push(Tlv { kind: 1, value: vec![0, 1] }); + assert_eq!(UnsignedFetch::new(collision), Err(WitnessError::NonCanonical)); + let mut even = p.clone(); + even.extensions.push(Tlv { kind: 2, value: vec![] }); + assert_eq!(UnsignedFetch::new(even), Err(WitnessError::Tlv(WireError::UnknownEvenTlv(2)))); + let mut oversized = p; + oversized.extensions.push(Tlv { kind: 3, value: vec![0; MAX_MESSAGE_LEN] }); + assert!(UnsignedFetch::new(oversized).is_err()); +} + +#[test] +fn witness_fetch_rejects_noncanonical_tlvs_status_lengths_and_counts() { + let fixture = &fixtures().fixtures[0]; + let request = hex(&fixture.first_request); + for suffix in [ + vec![1, 1, 0], + vec![1, 2, 0, 1, 1, 2, 0, 2], + vec![3, 0, 1, 2, 0, 1], + vec![253, 0, 1, 2, 0, 1], + vec![2, 0], + vec![3, 253, 0, 1, 7], + ] { + let mut damaged = request.clone(); + damaged.extend_from_slice(&suffix); + assert!(SignedFetch::decode(&resign_fetch(damaged), key(42)).is_err()); + } + for length in 0..request.len() { + assert!(SignedFetch::decode(&request[..length], key(42)).is_err()); + } + let response = hex(&fixture.first_response); + for length in 0..response.len() { + assert!(FetchResponse::decode(&response[..length]).is_err()); + } + for (offset, value) in [(0, 0), (18, 2), (19, 255), (21, 255)] { + let mut damaged = response.clone(); + damaged[offset] = value; + assert!(FetchResponse::decode(&damaged).is_err()); + } + let mut refusal = hex(&fixture.refusal); + refusal.push(0); + assert_eq!(FetchResponse::decode(&refusal), Err(WitnessError::NonCanonical)); + assert_eq!(FetchResponse::decode(&vec![0; MAX_MESSAGE_LEN + 1]), Err(WitnessError::SizeLimit)); + assert_eq!( + SignedFetch::decode(&vec![0; MAX_MESSAGE_LEN + 1], key(42)), + Err(WitnessError::SizeLimit) + ); +} + +#[test] +fn witness_record_rejects_header_points_flags_signature_and_ciphertext_damage() { + let wire = hex(&fixtures().fixtures[0].records[0].record); + for length in 0..wire.len() { + assert!(EncryptedRecord::decode(&wire[..length]).is_err()); + } + for offset in [0, 1, 66, 132, 165, 202, 203, 235, 299, 301, 334] { + let mut damaged = wire.clone(); + damaged[offset] ^= 255; + assert!(EncryptedRecord::decode(&damaged).is_err(), "offset {offset}"); + } + let mut trailing = wire.clone(); + trailing.push(0); + assert_eq!(EncryptedRecord::decode(&trailing), Err(WitnessError::NonCanonical)); + let mut wrong_len = wire.clone(); + wrong_len[299..301].copy_from_slice(&190u16.to_be_bytes()); + assert_eq!(EncryptedRecord::decode(&wrong_len), Err(WitnessError::Ciphertext)); + for range in [235..267, 267..299] { + let mut zero = wire.clone(); + zero[range].fill(0); + assert!(EncryptedRecord::decode(&zero).is_err()); + } + let mut high = wire.clone(); + let order = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + let mut borrow = 0_i16; + for i in (0..32).rev() { + let n = i16::from(order[i]) - i16::from(high[267 + i]) - borrow; + high[267 + i] = n as u8; + borrow = i16::from(n < 0); + } + assert_eq!( + EncryptedRecord::decode(&high), + Err(WitnessError::Signature(transcript::SignatureError::HighS)) + ); + let mut fetch = hex(&fixtures().fixtures[0].first_request); + fetch[114..146].copy_from_slice(&high[267..299]); + assert_eq!( + SignedFetch::decode(&fetch, key(42)), + Err(WitnessError::Signature(transcript::SignatureError::HighS)) + ); +} + +#[test] +fn witness_record_binds_expected_witness_manifest_encryption_and_canonical_terms() { + let wire = hex(&fixtures().fixtures[0].records[0].record); + let manifest = retained(0); + for (offset, expected) in + [(2, WitnessError::Mailbox), (68, WitnessError::Transcript), (100, WitnessError::Terms)] + { + let mut wrong = wire.clone(); + wrong[offset] ^= 1; + assert_eq!( + EncryptedRecord::decode(&resign_record(wrong, 43)) + .unwrap() + .authenticate(&manifest, key(43)), + Err(expected) + ); + } + let mut wrong_key = wire.clone(); + wrong_key[165..198].copy_from_slice(&key(45).serialize()); + assert_eq!( + EncryptedRecord::decode(&resign_record(wrong_key, 43)) + .unwrap() + .authenticate(&manifest, key(43)), + Err(WitnessError::Mailbox) + ); + assert_eq!( + EncryptedRecord::decode(&wire).unwrap().authenticate(&manifest, key(44)), + Err(WitnessError::Witness) + ); + let mut wrong_slot = wire; + wrong_slot[66..68].copy_from_slice(&2u16.to_be_bytes()); + assert_eq!( + EncryptedRecord::decode(&resign_record(wrong_slot, 43)) + .unwrap() + .authenticate(&manifest, key(43)), + Err(WitnessError::Terms) + ); +} + +#[test] +fn witness_record_remains_opaque_even_if_witness_signs_an_invalid_aead_body() { + let mut wire = hex(&fixtures().fixtures[0].records[0].record); + wire[334] ^= 1; + let hash = sha256::Hash::hash(&wire[301..492]).to_byte_array(); + wire[203..235].copy_from_slice(&hash); + let opaque = EncryptedRecord::decode(&resign_record(wire, 43)) + .unwrap() + .authenticate(&retained(0), key(43)) + .unwrap(); + assert_eq!(opaque.record().ciphertext().len(), CIPHERTEXT_LEN); + // Metadata authentication intentionally does not claim that the altered AEAD tag/body opens. + assert_ne!(opaque.record().ciphertext(), hex(&fixtures().fixtures[0].records[0].ciphertext)); +} + +#[test] +fn witness_fetch_correlates_connection_request_mailbox_and_preserves_earlier_pages() { + let pending = first(1); + let f = &fixtures().fixtures[1]; + let response = FetchResponse::decode(&hex(&f.first_response)).unwrap(); + for (source, expected) in [ + (WitnessConnection { identity: 8, ..connection() }, WitnessError::Connection), + (WitnessConnection { node_id: key(44), ..connection() }, WitnessError::Witness), + ] { + assert_eq!(pending.check_response(&response, &source), Err(expected)); + } + let wrong_id = FetchResponse::new([99; 16], response.result().clone()).unwrap(); + assert_eq!(pending.check_response(&wrong_id, &connection()), Err(WitnessError::Request)); + let checked = pending.check_response(&response, &connection()).unwrap(); + assert_eq!(checked.records().len(), 1); + let next = + checked.next(SignedFetch::decode(&hex(&f.continuation_request), key(42)).unwrap()).unwrap(); + let wrong_mailbox = + SignedFetch::decode(&hex(&fixtures().fixtures[0].first_request), key(42)).unwrap(); + assert_eq!( + PendingFetch::first(wrong_mailbox, retained(1), connection()), + Err(WitnessError::Mailbox) + ); + assert_eq!(next.check_response(&response, &connection()), Err(WitnessError::Request)); + assert_eq!(checked.records().len(), 1); + let terminal = next + .check_response( + &FetchResponse::decode(&hex(&f.continuation_response)).unwrap(), + &connection(), + ) + .unwrap(); + assert!(terminal.next_after_slot().is_none()); + assert_eq!(terminal.records().len(), f.records.len() - 1); + assert!(terminal.next(next.request().clone()).is_err()); +} + +#[test] +fn witness_fetch_rejects_backward_duplicate_skipped_and_impossible_cursors() { + let pending = first(1); + let records: Vec<_> = fixtures().fixtures[1] + .records + .iter() + .map(|r| EncryptedRecord::decode(&hex(&r.record)).unwrap()) + .collect(); + let id = pending.request().unsigned().parameters().request_id; + for (entries, next) in [ + (vec![], Some(1)), + (vec![records[0].clone()], Some(0)), + (vec![records[0].clone()], Some(2)), + (vec![records[0].clone(), records[0].clone()], None), + (vec![records[1].clone(), records[0].clone()], None), + ] { + assert_eq!( + pending.check_response(&page(id, entries, next), &connection()), + Err(WitnessError::Pagination) + ); + } + let only = first(0); + let record = EncryptedRecord::decode(&hex(&fixtures().fixtures[0].records[0].record)).unwrap(); + assert_eq!( + only.check_response( + &page(only.request().unsigned().parameters().request_id, vec![record], Some(1)), + &connection() + ), + Err(WitnessError::Pagination) + ); + let checked = pending + .check_response(&page(id, vec![records[0].clone()], Some(1)), &connection()) + .unwrap(); + let mut params = pending.request().unsigned().parameters().clone(); + params.after_slot = Some(1); + for reuse_nonce in [false, true] { + let mut p = params.clone(); + if reuse_nonce { + p.request_id = [89; 16]; + } else { + p.nonce = [89; 32]; + } + let unsigned = UnsignedFetch::new(p).unwrap(); + let signed = unsigned + .clone() + .authenticate(signature(unsigned.signing_digest(), 42), key(42)) + .unwrap(); + assert_eq!(checked.next(signed), Err(WitnessError::Replay)); + } +} + +#[test] +fn witness_record_receipt_and_frame_boundaries_are_bounded_opaque_bytes() { + let mut wire = hex(&fixtures().fixtures[0].records[0].record); + assert_eq!(wire.len(), RECORD_HEADER_LEN + 64 + 2 + CIPHERTEXT_LEN + 1); + wire[492] = 255; + for _ in 0..255 { + wire.extend_from_slice(&[0, 0]); + } + assert_eq!(EncryptedRecord::decode(&wire).unwrap().receipts().len(), 255); + let mut maximal = wire[..493].to_vec(); + maximal[492] = 1; + let remaining = MAX_MESSAGE_LEN - 23 - 493 - 2; + maximal.extend_from_slice(&(remaining as u16).to_be_bytes()); + maximal.resize(MAX_MESSAGE_LEN - 23, 7); + let record = EncryptedRecord::decode(&maximal).unwrap(); + let response = page([0; 16], vec![record.clone()], None); + assert_eq!(response.encode().len(), MAX_MESSAGE_LEN); + assert!(FetchResponse::new( + [0; 16], + FetchResult::Page { + records: vec![record.clone()], + next_after_slot: Some(1), + extensions: Vec::new() + } + ) + .is_err()); + assert!(FetchResponse::new( + [0; 16], + FetchResult::Page { + records: vec![record; 484], + next_after_slot: None, + extensions: Vec::new() + } + ) + .is_err()); + maximal.push(0); + assert_eq!(EncryptedRecord::decode(&maximal), Err(WitnessError::SizeLimit)); + let refusal = + FetchResponse::new([0; 16], FetchResult::Refused(vec![255; MAX_MESSAGE_LEN - 21])).unwrap(); + assert_eq!(refusal.encode().len(), MAX_MESSAGE_LEN); + assert_eq!(FetchResponse::decode(&refusal.encode()).unwrap(), refusal); +} + +#[test] +fn witness_fetch_exact_envelope_and_optional_zero_cursor_are_preserved() { + let mut parameters = FetchParameters { + request_id: [1; 16], + mailbox_id: [2; 32], + nonce: [3; 32], + after_slot: None, + extensions: vec![Tlv { kind: 3, value: vec![7; MAX_MESSAGE_LEN - 146 - 4] }], + }; + let maximum = UnsignedFetch::new(parameters.clone()).unwrap(); + let signature = signature(maximum.signing_digest(), 42); + let wire = maximum.authenticate(signature, key(42)).unwrap().encode(); + assert_eq!(wire.len(), MAX_MESSAGE_LEN); + assert_eq!(SignedFetch::decode(&wire, key(42)).unwrap().encode(), wire); + parameters.extensions[0].value.push(7); + assert_eq!(UnsignedFetch::new(parameters.clone()), Err(WitnessError::SizeLimit)); + parameters.extensions.clear(); + let absent = UnsignedFetch::new(parameters.clone()).unwrap(); + parameters.after_slot = Some(0); + let present = UnsignedFetch::new(parameters).unwrap(); + assert_ne!(absent.signing_digest(), present.signing_digest()); + let signed = present + .clone() + .authenticate(super::signature(present.signing_digest(), 42), key(42)) + .unwrap(); + assert_eq!(SignedFetch::decode(&signed.encode(), key(42)).unwrap(), signed); + let response = FetchResponse::new( + [1; 16], + FetchResult::Page { + records: vec![], + next_after_slot: None, + extensions: vec![Tlv { kind: 3, value: vec![5] }], + }, + ) + .unwrap(); + assert_eq!(FetchResponse::decode(&response.encode()).unwrap(), response); +} + +proptest! { + #[test] + fn witness_fetch_bounded_arbitrary_inputs_never_panic(bytes in prop::collection::vec(any::(), 0..70_000)) { + if let Ok(request) = SignedFetch::decode(&bytes, key(42)) { prop_assert_eq!(request.encode(), bytes.clone()); } + if let Ok(response) = FetchResponse::decode(&bytes) { prop_assert_eq!(response.encode(), bytes.clone()); } + if let Ok(record) = EncryptedRecord::decode(&bytes) { prop_assert_eq!(record.encode(), bytes); } + } +} From f33c5f7ca0777fc9e45305005b3232ac72ef0c74 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 23:32:20 -0400 Subject: [PATCH 10/22] feat(ffor): verify witness plaintext terms and preimages --- lightning-ffor/README.md | 31 ++-- lightning-ffor/fuzz/README.md | 7 +- lightning-ffor/fuzz/fuzz_targets/witness.rs | 35 ++++ lightning-ffor/fuzz/seed_corpus.py | 7 + lightning-ffor/src/witness/mod.rs | 7 +- lightning-ffor/src/witness/record.rs | 21 ++- lightning-ffor/src/witness/record_body.rs | 174 ++++++++++++++++++++ lightning-ffor/tests/witness/body.rs | 172 +++++++++++++++++++ lightning-ffor/tests/witness/fetch.rs | 3 + 9 files changed, 435 insertions(+), 22 deletions(-) create mode 100644 lightning-ffor/src/witness/record_body.rs create mode 100644 lightning-ffor/tests/witness/body.rs diff --git a/lightning-ffor/README.md b/lightning-ffor/README.md index f169020d219..1d203108085 100644 --- a/lightning-ffor/README.md +++ b/lightning-ffor/README.md @@ -228,11 +228,16 @@ incorrect ciphertext hashes, invalid signatures, truncated fields and extra byte attachments are outside the signature and provide no proof of payment or storage. Record retrieval remains possible after voucher expiry and therefore has no current-height gate. -There is no Rust ECIES decryptor in this checkpoint. A witness can sign ciphertext with an -invalid AEAD tag or false plaintext; successful metadata authentication does not prove that -a payment occurred. A future decryptor must authenticate ChaCha20-Poly1305 and validate -the epoch, slot, payment hash, amount, expiry, deadline and actual preimage before crediting -or claiming a voucher. The reference uses SHA256 of the **compressed** ECDH shared point +`AuthenticatedEncryptedRecord::verify_body` verifies exactly 142 plaintext bytes against +the retained manifest, including epoch, slot, payment hash, amount, expiry, deadline and +SHA256(preimage). Its immutable result redacts the preimage in Debug. Observation amounts +and times remain informational. This shared method does not establish that supplied bytes +came from authenticated decryption. The native `lightning::ln::ffor` decryptor checks the +AEAD tag before invoking it and exposes a separate receipt type. Neither helper mutates +channel state or proves payment settlement. A witness can sign ciphertext with an invalid +AEAD tag or false plaintext, so signature checking alone is insufficient. + +The reference uses SHA256 of the **compressed** ECDH shared point once, then HKDF-SHA256 extract/expand with empty salt and `ffor/witness/body` as info. The nonce is 12 zero bytes, the Poly1305 tag is appended, and AAD is the exact header with only its final ciphertext-hash field zeroed. Applying another SHA256 to the reference @@ -243,13 +248,15 @@ Beignet and the six public Appendix D setups. Deterministic fixture keys and eph keys are public test inputs only. The generator invokes the actual reference codecs and encryption helper, verifies witness signatures, decrypts every generated record and checks the published preimages. Rust tests independently compare bytes, domains, AAD and the -secp256k1 ECDH hash, but do not perform decryption. The existing witness fuzz target also -checks these new codecs and is seeded with requests, pages and signed encrypted records. - -The fetch checkpoint passes 88 tests and five doctests, no_std, all-target Clippy with -warnings denied, and actual Rust 1.63 checks with and without std. The seeded witness fuzz -target completed 1,431,038 inputs in 31 seconds without a crash. These bounded checks do -not establish successful decryption, runtime storage safety or offline invoice readiness. +secp256k1 ECDH hash. Shared tests check plaintext against all six scenarios; native tests +add actual decryption of four records from the first two scenarios, exported by +`generate_native_witness_fixtures.py`. The witness fuzz target also checks candidate bodies +against authenticated manifests and record terms. + +The body-verification checkpoint passes 95 tests and five doctests, no_std, all-target +Clippy with warnings denied, and actual Rust 1.63 checks with and without std. The extended +seeded witness fuzz target completed 987,022 inputs in 31 seconds without a crash. These +bounded checks do not establish runtime storage safety or offline invoice readiness. The requested settlement baseline is LND v0.21.3-beta. LND implementation work is kept local. This unpublished workspace crate does not change a binding version or diff --git a/lightning-ffor/fuzz/README.md b/lightning-ffor/fuzz/README.md index efeaa4c5a83..00fdb9b02ff 100644 --- a/lightning-ffor/fuzz/README.md +++ b/lightning-ffor/fuzz/README.md @@ -24,8 +24,11 @@ transitions are outside this pure codec and require later engine integration tes The `witness` target checks strict provision/ack encodings, authenticated receiver manifests, signed fetch requests, paginated replies and signed encrypted record framing. It accepts raw acknowledgement/fetch/response/record bytes or `[u16 init_length][u16 accept_length][init] -[accept][candidate]`, with candidate being a manifest or complete provision. Its seeds use the +[accept][candidate]`, with candidate being a manifest, complete provision, or +`[u16 manifest_length][u16 record_length][manifest][record][body]` for body verification. +Its seeds use the six public Appendix D setups and manifests, fetch pages and encrypted records generated by the -pinned Beignet implementation. The target never decrypts ciphertext or treats it as a preimage. +pinned Beignet implementation. It checks candidate plaintext terms and preimage hashes, +but never decrypts ciphertext or treats those candidates as authenticated witness evidence. Run `cargo +nightly fuzz run witness -- -max_len=196609 -max_total_time=60` after seeding. Neither fuzz target signs messages or accesses private keys. diff --git a/lightning-ffor/fuzz/fuzz_targets/witness.rs b/lightning-ffor/fuzz/fuzz_targets/witness.rs index 95c9fe38d3c..3f87834ecb0 100644 --- a/lightning-ffor/fuzz/fuzz_targets/witness.rs +++ b/lightning-ffor/fuzz/fuzz_targets/witness.rs @@ -1,5 +1,6 @@ #![no_main] +use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::PublicKey; use libfuzzer_sys::fuzz_target; use lightning_ffor::setup::AuthenticatedSetup; @@ -65,4 +66,38 @@ fuzz_target!(|data: &[u8]| { if let Ok(provision) = Provision::decode(candidate, &setup) { assert_eq!(provision.encode(), candidate); } + // Length-prefixed signed manifest and record leave arbitrary plaintext bytes for the + // consistency checker. This does not attempt decryption or claim authenticated plaintext. + if candidate.len() < 4 { + return; + } + let manifest_length = usize::from(u16::from_be_bytes([candidate[0], candidate[1]])); + let record_length = usize::from(u16::from_be_bytes([candidate[2], candidate[3]])); + let Some(manifest) = candidate.get(4..4 + manifest_length) else { + return; + }; + let Some(record) = candidate.get(4 + manifest_length..4 + manifest_length + record_length) + else { + return; + }; + let Some(body) = candidate.get(4 + manifest_length + record_length..) else { + return; + }; + let (Ok(manifest), Ok(record)) = + (SignedManifest::decode(manifest, &setup), EncryptedRecord::decode(record)) + else { + return; + }; + let witness = record.header().witness; + let Ok(record) = record.authenticate(&manifest, witness) else { + return; + }; + if let Ok(verified) = record.verify_body(&manifest, body) { + assert_eq!(body.len(), lightning_ffor::witness::RECORD_BODY_LEN); + assert_eq!(verified.slot(), record.record().header().slot); + assert_eq!( + sha256::Hash::hash(&verified.preimage()).to_byte_array(), + verified.payment_hash() + ); + } }); diff --git a/lightning-ffor/fuzz/seed_corpus.py b/lightning-ffor/fuzz/seed_corpus.py index 956279ad592..ff816dbe130 100644 --- a/lightning-ffor/fuzz/seed_corpus.py +++ b/lightning-ffor/fuzz/seed_corpus.py @@ -37,9 +37,16 @@ fetches = json.loads((crate / "tests/data/beignet-witness-fetch.json").read_text()) for index, fixture in enumerate(fetches["fixtures"]): + init = bytes.fromhex(fixtures[index]["init_wire"]) + accept = bytes.fromhex(fixtures[index]["accept_wire"]) + manifest = bytes.fromhex(witnesses["fixtures"][index]["manifest"]) + prefix = len(init).to_bytes(2, "big") + len(accept).to_bytes(2, "big") + init + accept for field in ("first_request", "continuation_request", "odd_request", "first_response", "continuation_response", "empty_response", "refusal"): (witness_corpus / f"fetch-{index}-{field}").write_bytes(bytes.fromhex(fixture[field])) for slot, record in enumerate(fixture["records"]): (witness_corpus / f"record-{index}-{slot}").write_bytes(bytes.fromhex(record["record"])) + wire = bytes.fromhex(record["record"]) + candidate = len(manifest).to_bytes(2, "big") + len(wire).to_bytes(2, "big") + manifest + wire + (witness_corpus / f"body-{index}-{slot}").write_bytes(prefix + candidate + bytes.fromhex(record["body"])) print("Seeded fetch pages and signed encrypted records from all six reference scenarios") diff --git a/lightning-ffor/src/witness/mod.rs b/lightning-ffor/src/witness/mod.rs index 5a7cc2d99cf..29485dcb33b 100644 --- a/lightning-ffor/src/witness/mod.rs +++ b/lightning-ffor/src/witness/mod.rs @@ -10,7 +10,8 @@ //! Refusal text is bounded opaque bytes and need not be UTF-8. //! Fetch messages preserve canonical unknown odd TLVs and reject unknown even fields. Records //! define no extension stream and reject reserved flags. Authenticated encrypted records prove -//! their witness signature and manifest binding only: no AEAD or plaintext verification exists. +//! their witness signature and manifest binding only. Plaintext body verification is separate +//! from AEAD authentication, which must be performed by the caller's decryption boundary. //! //! ``` //! use lightning_ffor::witness::{Acknowledgement, AcknowledgementResult}; @@ -32,6 +33,7 @@ mod fetch_correlation; mod manifest; mod messages; mod record; +mod record_body; pub use correlation::{CheckedAcknowledgement, PendingProvision, WitnessConnection}; pub use fetch::{FetchParameters, FetchResponse, FetchResult, SignedFetch, UnsignedFetch}; @@ -41,6 +43,7 @@ pub use messages::{Acknowledgement, AcknowledgementResult, Provision}; pub use record::{ AuthenticatedEncryptedRecord, EncryptedRecord, RecordHeader, CIPHERTEXT_LEN, RECORD_HEADER_LEN, }; +pub use record_body::{RecordBodyContext, VerifiedRecordBody, RECORD_BODY_LEN}; /// Appendix F.1 receiver-to-witness provisioning message, including its two-byte type. pub const PROVISION_MESSAGE_TYPE: u16 = 55055; @@ -98,6 +101,8 @@ pub enum WitnessError { Mailbox, /// The slot or canonical book entry differs from this manifest. Terms, + /// The supplied body preimage does not hash to the canonical payment hash. + Preimage, /// A response is unordered, repeats a slot, or has an invalid pagination cursor. Pagination, /// A request identifier or nonce was already used in this bounded fetch traversal. diff --git a/lightning-ffor/src/witness/record.rs b/lightning-ffor/src/witness/record.rs index aedd1ac8e9b..e72bc24d5f2 100644 --- a/lightning-ffor/src/witness/record.rs +++ b/lightning-ffor/src/witness/record.rs @@ -2,13 +2,13 @@ use alloc::vec::Vec; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::PublicKey; -use super::{Reader, SignedManifest, WitnessError, MAX_MESSAGE_LEN}; +use super::{Reader, SignedManifest, WitnessError, MAX_MESSAGE_LEN, RECORD_BODY_LEN}; use crate::transcript; /// Appendix F.2 fixed version 1 record header length. pub const RECORD_HEADER_LEN: usize = 235; /// Compressed ephemeral key, 142-byte version 1 body, and 16-byte Poly1305 tag. -pub const CIPHERTEXT_LEN: usize = 33 + 142 + 16; +pub const CIPHERTEXT_LEN: usize = 33 + RECORD_BODY_LEN + 16; const MAX_RECORD_LEN: usize = MAX_MESSAGE_LEN - 2 - 16 - 1 - 2 - 2; /// Public signed record metadata. These claims do not prove that its body decrypts. @@ -181,11 +181,18 @@ impl EncryptedRecord { pub fn authenticate( self, manifest: &SignedManifest, expected_witness: PublicKey, ) -> Result { - let m = manifest.unsigned(); - let p = m.parameters(); if self.header.witness != expected_witness { return Err(WitnessError::Witness); } + self.checked_entry(manifest)?; + Ok(AuthenticatedEncryptedRecord { record: self }) + } + + pub(super) fn checked_entry<'a>( + &self, manifest: &'a SignedManifest, + ) -> Result<&'a [u8], WitnessError> { + let m = manifest.unsigned(); + let p = m.parameters(); if self.header.mailbox_id != p.mailbox_id || self.header.encryption_public_key != p.encryption_public_key { @@ -199,14 +206,14 @@ impl EncryptedRecord { if transcript::hash_parts(b"ffor/terms", &[entry]) != self.header.terms_hash { return Err(WitnessError::Terms); } - Ok(AuthenticatedEncryptedRecord { record: self }) + Ok(entry) } } /// Authenticated opaque ciphertext bound to one provisioned witness, manifest and book entry. /// -/// This is deliberately not a receipt or preimage authority. A later decryption boundary must -/// verify AEAD, epoch and all plaintext terms, then SHA256(t) == H_k before using a preimage. +/// This is deliberately not a receipt or preimage authority. A decryption boundary must verify +/// AEAD before passing the resulting plaintext to [`Self::verify_body`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AuthenticatedEncryptedRecord { record: EncryptedRecord, diff --git a/lightning-ffor/src/witness/record_body.rs b/lightning-ffor/src/witness/record_body.rs new file mode 100644 index 00000000000..69ed9d2faae --- /dev/null +++ b/lightning-ffor/src/witness/record_body.rs @@ -0,0 +1,174 @@ +use core::fmt; + +use bitcoin::hashes::{sha256, Hash}; + +use super::{AuthenticatedEncryptedRecord, Reader, SignedManifest, WitnessError}; + +/// Appendix F.2 version 1 plaintext body length, without encryption framing. +pub const RECORD_BODY_LEN: usize = 142; + +/// Witness-reported bookkeeping context with no protocol authority. +/// +/// These fields are preserved exactly, without interpreting them as proof of payment, fees, +/// current height, or current time. Body verification does not authenticate their origin. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RecordBodyContext { + amount_in_msat: u64, + amount_out_msat: u64, + outgoing_cltv: u32, + observed_unix_time: u64, +} + +impl RecordBodyContext { + /// Informational inbound amount reported by the witness. + pub fn amount_in_msat(&self) -> u64 { + self.amount_in_msat + } + + /// Informational outbound amount reported by the witness. + pub fn amount_out_msat(&self) -> u64 { + self.amount_out_msat + } + + /// Informational outgoing CLTV reported by the witness. + pub fn outgoing_cltv(&self) -> u32 { + self.outgoing_cltv + } + + /// Informational observation timestamp reported by the witness. + pub fn observed_unix_time(&self) -> u64 { + self.observed_unix_time + } +} + +/// Exact plaintext terms and preimage checked against a signed manifest and record header. +/// +/// This value proves body consistency only. It does not establish that the bytes came from +/// the encrypted record, passed AEAD authentication, or were observed by the witness. Only a +/// separate authenticated decryption boundary can establish that provenance. +#[derive(Clone, PartialEq, Eq)] +pub struct VerifiedRecordBody { + epoch_id: [u8; 32], + slot: u16, + preimage: [u8; 32], + payment_hash: [u8; 32], + amount_msat: u64, + voucher_expiry: u32, + settlement_deadline: u32, + context: RecordBodyContext, +} + +impl VerifiedRecordBody { + /// Epoch identity from the signed canonical book. + pub fn epoch_id(&self) -> [u8; 32] { + self.epoch_id + } + + /// One-based slot matching the signed record and canonical entry. + pub fn slot(&self) -> u16 { + self.slot + } + + /// Preimage whose SHA256 equals the canonical payment hash. + /// This alone does not prove authenticated decryption or witness observation. + pub fn preimage(&self) -> [u8; 32] { + self.preimage + } + + /// Payment hash from the signed canonical entry. + pub fn payment_hash(&self) -> [u8; 32] { + self.payment_hash + } + + /// Voucher amount from the signed canonical entry. + pub fn amount_msat(&self) -> u64 { + self.amount_msat + } + + /// Voucher expiry height from the signed canonical entry. + pub fn voucher_expiry(&self) -> u32 { + self.voucher_expiry + } + + /// Settlement admission deadline from the signed canonical entry. + pub fn settlement_deadline(&self) -> u32 { + self.settlement_deadline + } + + /// Uninterpreted context, distinct from the verified payment terms. + pub fn context(&self) -> &RecordBodyContext { + &self.context + } + + fn decode(bytes: &[u8]) -> Result { + let mut reader = Reader(bytes); + let body = Self { + epoch_id: reader.array()?, + slot: reader.u16()?, + preimage: reader.array()?, + payment_hash: reader.array()?, + amount_msat: u64::from_be_bytes(reader.array()?), + voucher_expiry: reader.u32()?, + settlement_deadline: reader.u32()?, + context: RecordBodyContext { + amount_in_msat: u64::from_be_bytes(reader.array()?), + amount_out_msat: u64::from_be_bytes(reader.array()?), + outgoing_cltv: reader.u32()?, + observed_unix_time: u64::from_be_bytes(reader.array()?), + }, + }; + reader.finish()?; + Ok(body) + } + + fn verify_entry(&self, entry: &[u8]) -> Result<(), WitnessError> { + let mut reader = Reader(entry); + if self.slot != reader.u16()? + || self.payment_hash != reader.array::<32>()? + || self.amount_msat != u64::from_be_bytes(reader.array()?) + || self.voucher_expiry != reader.u32()? + || self.settlement_deadline != reader.u32()? + { + return Err(WitnessError::Terms); + } + if sha256::Hash::hash(&self.preimage).to_byte_array() != self.payment_hash { + return Err(WitnessError::Preimage); + } + Ok(()) + } +} + +impl fmt::Debug for VerifiedRecordBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("VerifiedRecordBody") + .field("epoch_id", &self.epoch_id) + .field("slot", &self.slot) + .field("preimage", &"[redacted]") + .field("payment_hash", &self.payment_hash) + .field("amount_msat", &self.amount_msat) + .field("voucher_expiry", &self.voucher_expiry) + .field("settlement_deadline", &self.settlement_deadline) + .field("context", &self.context) + .finish() + } +} + +impl AuthenticatedEncryptedRecord { + /// Verify exactly 142 supplied plaintext bytes against this record and a retained manifest. + /// + /// Rechecks the record's mailbox, encryption key, H_act and full canonical entry terms hash, + /// then verifies the body epoch, slot, payment terms and SHA256(preimage). Informational + /// context is preserved without semantic restrictions. No decryption or AEAD check occurs: + /// callers must authenticate the encrypted body before interpreting this as witness evidence. + pub fn verify_body( + &self, manifest: &SignedManifest, bytes: &[u8], + ) -> Result { + let entry = self.record().checked_entry(manifest)?; + let body = VerifiedRecordBody::decode(bytes)?; + if manifest.unsigned().canonical_book().get(..32) != Some(body.epoch_id.as_slice()) { + return Err(WitnessError::Transcript); + } + body.verify_entry(entry)?; + Ok(body) + } +} diff --git a/lightning-ffor/tests/witness/body.rs b/lightning-ffor/tests/witness/body.rs new file mode 100644 index 00000000000..261c3c793e1 --- /dev/null +++ b/lightning-ffor/tests/witness/body.rs @@ -0,0 +1,172 @@ +use super::*; +use lightning_ffor::witness::{AuthenticatedEncryptedRecord, RECORD_BODY_LEN}; + +fn authenticated(index: usize, record: usize) -> AuthenticatedEncryptedRecord { + EncryptedRecord::decode(&hex(&fixtures().fixtures[index].records[record].record)) + .unwrap() + .authenticate(&retained(index), key(43)) + .unwrap() +} + +#[test] +fn witness_body_matches_all_pinned_beignet_scenarios() { + let inputs = fixtures(); + assert_eq!(inputs.fixtures.len(), 6); + for (index, fixture) in inputs.fixtures.iter().enumerate() { + let setup = public_setup(index); + let manifest = retained(index); + for (record_index, record) in fixture.records.iter().enumerate() { + let body = authenticated(index, record_index) + .verify_body(&manifest, &hex(&record.body)) + .unwrap(); + let slot = body.slot(); + let voucher = &setup.vouchers()[usize::from(slot) - 1]; + let expected_preimage = sha256::Hash::hash( + &[ + format!("ffor/vector/{}/preimage", fixture.scenario).as_bytes(), + &slot.to_be_bytes(), + ] + .concat(), + ) + .to_byte_array(); + assert_eq!(hex(&record.body).len(), RECORD_BODY_LEN); + assert_eq!(body.epoch_id(), setup.header().epoch_id); + assert_eq!(body.preimage(), expected_preimage); + assert_eq!(body.payment_hash(), voucher.payment_hash); + assert_eq!(body.amount_msat(), voucher.amount_msat); + assert_eq!(body.voucher_expiry(), voucher.expiry); + assert_eq!(body.settlement_deadline(), voucher.deadline); + assert_eq!(body.context().amount_in_msat(), voucher.amount_msat + 6000); + assert_eq!(body.context().amount_out_msat(), voucher.amount_msat); + assert_eq!(body.context().outgoing_cltv(), 791_000); + assert_eq!(body.context().observed_unix_time(), 1_700_000_000); + assert_eq!(body.clone(), body); + } + } +} + +#[test] +fn witness_body_rejects_truncated_trailing_and_mismatched_terms() { + let record = authenticated(0, 0); + let manifest = retained(0); + let bytes = hex(&fixtures().fixtures[0].records[0].body); + for length in 0..RECORD_BODY_LEN { + assert_eq!(record.verify_body(&manifest, &bytes[..length]), Err(WitnessError::Truncated)); + } + for extra in [1, 2, 142, MAX_MESSAGE_LEN] { + let mut trailing = bytes.clone(); + trailing.resize(RECORD_BODY_LEN + extra, 0); + assert_eq!(record.verify_body(&manifest, &trailing), Err(WitnessError::NonCanonical)); + } + for (offset, error) in [ + (0, WitnessError::Transcript), + (32, WitnessError::Terms), + (34, WitnessError::Preimage), + (66, WitnessError::Terms), + (98, WitnessError::Terms), + (106, WitnessError::Terms), + (110, WitnessError::Terms), + ] { + let mut wrong = bytes.clone(); + wrong[offset] ^= 1; + assert_eq!(record.verify_body(&manifest, &wrong), Err(error)); + } + for slot in [0u16, 2, 483, 484, u16::MAX] { + let mut wrong = bytes.clone(); + wrong[32..34].copy_from_slice(&slot.to_be_bytes()); + assert_eq!(record.verify_body(&manifest, &wrong), Err(WitnessError::Terms)); + } + // A valid plaintext for another slot is still not the body named by this signed header. + assert_eq!( + authenticated(1, 0) + .verify_body(&retained(1), &hex(&fixtures().fixtures[1].records[1].body)), + Err(WitnessError::Terms) + ); +} + +#[test] +fn witness_body_rechecks_supplied_manifest_record_binding() { + let record = authenticated(0, 0); + let manifest = retained(0); + let bytes = hex(&fixtures().fixtures[0].records[0].body); + for (change, expected) in [ + (0, WitnessError::Mailbox), + (1, WitnessError::Mailbox), + (2, WitnessError::Transcript), + (3, WitnessError::Transcript), + ] { + let mut params = *manifest.unsigned().parameters(); + match change { + 0 => params.mailbox_id[0] ^= 1, + 1 => params.encryption_public_key = key(45), + 2 => params.commitment_hash[0] ^= 1, + 3 => params.epoch_start_height -= 1, + _ => unreachable!(), + } + let unsigned = UnsignedManifest::new(&public_setup(0), params).unwrap(); + let signed = + unsigned.clone().authenticate(signature(unsigned.signing_digest(), 42)).unwrap(); + assert_eq!(record.verify_body(&signed, &bytes), Err(expected)); + } + assert!(record.verify_body(&retained(1), &bytes).is_err()); + assert!(record.verify_body(&manifest, &bytes).is_ok()); +} + +#[test] +fn witness_body_context_is_informational_and_preimage_debug_is_redacted() { + let record = authenticated(0, 0); + let manifest = retained(0); + let mut bytes = hex(&fixtures().fixtures[0].records[0].body); + for value in [0, 255] { + bytes[114..].fill(value); + let body = record.verify_body(&manifest, &bytes).unwrap(); + let expected_u64 = if value == 0 { 0 } else { u64::MAX }; + let expected_u32 = if value == 0 { 0 } else { u32::MAX }; + assert_eq!(body.context().amount_in_msat(), expected_u64); + assert_eq!(body.context().amount_out_msat(), expected_u64); + assert_eq!(body.context().outgoing_cltv(), expected_u32); + assert_eq!(body.context().observed_unix_time(), expected_u64); + for debug in [format!("{body:?}"), format!("{body:#?}")] { + assert!(debug.contains("[redacted]")); + assert!(!debug.contains(&format!("{:?}", body.preimage()))); + assert!(!debug.contains(&format!("{:#?}", body.preimage()))); + assert!(!debug.contains(&fixtures().fixtures[0].records[0].body[68..132])); + } + } +} + +#[test] +fn witness_body_consistency_does_not_claim_authenticated_decryption() { + let fixture = &fixtures().fixtures[0].records[0]; + let mut wire = hex(&fixture.record); + wire[334] ^= 1; + let hash = sha256::Hash::hash(&wire[301..492]).to_byte_array(); + wire[203..235].copy_from_slice(&hash); + let record = EncryptedRecord::decode(&resign_record(wire, 43)) + .unwrap() + .authenticate(&retained(0), key(43)) + .unwrap(); + // Supplied valid plaintext remains consistent even though this signed ciphertext cannot + // decrypt to it. A native authenticated receipt must additionally verify the AEAD tag. + assert!(record.verify_body(&retained(0), &hex(&fixture.body)).is_ok()); +} + +proptest! { + #[test] + fn witness_body_any_changed_authoritative_byte_fails(offset in 0usize..114, bit in 0u8..8) { + let mut bytes = hex(&fixtures().fixtures[0].records[0].body); + bytes[offset] ^= 1 << bit; + prop_assert!(authenticated(0, 0).verify_body(&retained(0), &bytes).is_err()); + } + + #[test] + fn witness_body_arbitrary_informational_context_is_preserved(context in any::<[u8; 28]>()) { + let mut bytes = hex(&fixtures().fixtures[0].records[0].body); + bytes[114..].copy_from_slice(&context); + let body = authenticated(0, 0).verify_body(&retained(0), &bytes).unwrap(); + prop_assert_eq!(body.context().amount_in_msat(), u64::from_be_bytes(context[..8].try_into().unwrap())); + prop_assert_eq!(body.context().amount_out_msat(), u64::from_be_bytes(context[8..16].try_into().unwrap())); + prop_assert_eq!(body.context().outgoing_cltv(), u32::from_be_bytes(context[16..20].try_into().unwrap())); + prop_assert_eq!(body.context().observed_unix_time(), u64::from_be_bytes(context[20..].try_into().unwrap())); + } +} diff --git a/lightning-ffor/tests/witness/fetch.rs b/lightning-ffor/tests/witness/fetch.rs index b24434923ec..49ec5a34850 100644 --- a/lightning-ffor/tests/witness/fetch.rs +++ b/lightning-ffor/tests/witness/fetch.rs @@ -5,6 +5,9 @@ use lightning_ffor::witness::{ UnsignedFetch, CIPHERTEXT_LEN, RECORD_HEADER_LEN, }; +#[path = "body.rs"] +mod body; + #[derive(Deserialize)] struct FetchReference { source_revision: String, From f9e879bf60d41f6b2d0ee6041870d4283d903166 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 23:40:01 -0400 Subject: [PATCH 11/22] feat(ffor): persist cooperative voucher drain and close recovery --- lightning/src/ln/channel.rs | 50 +- lightning/src/ln/channel/ffor.rs | 13 +- lightning/src/ln/channel/ffor/drain.rs | 699 ++++++++++++++++++ lightning/src/ln/channel/ffor/drain/tests.rs | 228 ++++++ lightning/src/ln/channel/ffor/fence.rs | 18 + lightning/src/ln/channel/ffor/reestablish.rs | 5 + lightning/src/ln/channel/ffor/setup.rs | 5 +- lightning/src/ln/channelmanager.rs | 16 +- .../src/ln/channelmanager/ffor_activation.rs | 27 +- .../channelmanager/ffor_activation/close.rs | 376 ++++++++++ .../ffor_activation/close/tests.rs | 498 +++++++++++++ .../ffor_activation/drain_tests.rs | 4 +- .../ffor_activation/reestablish.rs | 36 +- lightning/src/ln/ffor_recovery.rs | 143 +++- lightning/src/ln/ffor_recovery/activation.rs | 70 +- .../src/ln/ffor_recovery/activation/close.rs | 283 +++++++ .../src/ln/ffor_recovery/activation/tests.rs | 48 +- .../ffor_recovery/activation/tests/aborted.rs | 20 +- .../ffor_recovery/activation/tests/close.rs | 513 +++++++++++++ lightning/src/ln/ffor_recovery/tests.rs | 4 +- 20 files changed, 2944 insertions(+), 112 deletions(-) create mode 100644 lightning/src/ln/channel/ffor/drain.rs create mode 100644 lightning/src/ln/channel/ffor/drain/tests.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/close.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/close/tests.rs create mode 100644 lightning/src/ln/ffor_recovery/activation/close.rs create mode 100644 lightning/src/ln/ffor_recovery/activation/tests/close.rs diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index ed35b64ea9a..b974bcfa2f1 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -102,7 +102,10 @@ use super::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBase mod ffor; #[cfg(test)] pub(crate) use ffor::ffor_setup_test_messages; -pub(crate) use ffor::{FFORReceiverFencePhase, FFORReceiverSetup, FFORReestablishOutcome}; +pub(crate) use ffor::{ + ffor_drain_completion_digest, FFORReceiverDrainCompletion, FFORReceiverFencePhase, + FFORReceiverSetup, FFORReestablishOutcome, +}; #[cfg(any(test, feature = "_test_utils"))] #[allow(unused)] @@ -7349,6 +7352,11 @@ where { return UpdateFulfillFetch::DuplicateClaim {}; } + if let Some(result) = + self.ffor_prepare_drain_claim(htlc_id_arg, payment_preimage_arg, payment_info.clone()) + { + return result; + } // Either ChannelReady got set (which means it won't be unset) or there is no way any // caller thought we could have something claimed (cause we wouldn't have accepted in an // incoming HTLC anyway). If we got to ShutdownComplete, callers aren't allowed to call us, @@ -7416,7 +7424,7 @@ where channel_id: Some(self.context.channel_id()), }; - if self.context.is_ffor_frozen() + if self.context.ffor_blocks_commitment_round() || !self.context.channel_state.can_generate_new_commitment() { // Note that this condition is the same as the assertion in @@ -7600,7 +7608,7 @@ where &mut self, htlc_id_arg: u64, err_contents: E, mut force_holding_cell: bool, logger: &L ) -> Result, ChannelError> where L::Target: Logger { - if self.context.is_ffor_frozen() { return Err(ChannelError::Ignore(ffor::FFOR_FROZEN_MESSAGE.to_owned())); } + if !self.context.ffor_failure_permitted(htlc_id_arg) { return Err(ChannelError::Ignore(ffor::FFOR_FROZEN_MESSAGE.to_owned())); } if !matches!(self.context.channel_state, ChannelState::ChannelReady(_)) { panic!("Was asked to fail an HTLC when channel was not in an operational state"); } @@ -8091,7 +8099,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { - self.context.check_ffor_mutation()?; + self.context.check_ffor_commitment_round()?; self.commitment_signed_check_state()?; if !self.pending_funding().is_empty() { @@ -8136,7 +8144,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { - self.context.check_ffor_mutation()?; + self.context.check_ffor_commitment_round()?; self.commitment_signed_check_state()?; let mut messages = BTreeMap::new(); @@ -8206,7 +8214,7 @@ where } fn commitment_signed_check_state(&self) -> Result<(), ChannelError> { - self.context.check_ffor_mutation()?; + self.context.check_ffor_commitment_round()?; if self.context.channel_state.is_quiescent() { return Err(ChannelError::WarnAndDisconnect( "Got commitment_signed message while quiescent".to_owned(), @@ -8241,7 +8249,7 @@ where where L::Target: Logger, { - self.context.check_ffor_mutation()?; + self.context.check_ffor_commitment_round()?; if self .holder_commitment_point .advance(&self.context.holder_signer, &self.context.secp_ctx, logger) @@ -8388,7 +8396,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { - if self.context.is_ffor_frozen() { + if self.context.ffor_blocks_commitment_round() { return (None, Vec::new()); } if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) @@ -8409,7 +8417,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { - if self.context.is_ffor_frozen() { + if self.context.ffor_blocks_commitment_round() { return (None, Vec::new()); } assert!(matches!(self.context.channel_state, ChannelState::ChannelReady(_))); @@ -8618,7 +8626,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { - self.context.check_ffor_mutation()?; + self.context.check_ffor_commitment_round()?; if self.context.channel_state.is_quiescent() { return Err(ChannelError::WarnAndDisconnect( "Got revoke_and_ack message while quiescent".to_owned(), @@ -9462,7 +9470,7 @@ where assert!(self.context.channel_state.is_monitor_update_in_progress()); self.context.channel_state.clear_monitor_update_in_progress(); assert_eq!(self.blocked_monitor_updates_pending(), 0); - if self.context.is_ffor_frozen() { + if self.context.ffor_blocks_commitment_round() { return MonitorRestoreUpdates { raa: None, commitment_update: None, commitment_order: self.context.resend_order.clone(), accepted_htlcs: Vec::new(), failed_htlcs: Vec::new(), finalized_claimed_htlcs: Vec::new(), @@ -9625,7 +9633,7 @@ where pub fn signer_maybe_unblocked( &mut self, logger: &L, path_for_release_htlc: CBP ) -> SignerResumeUpdates where L::Target: Logger, CBP: Fn(u64) -> BlindedMessagePath { - if self.context.is_ffor_frozen() { + if self.context.ffor_blocks_commitment_round() { return SignerResumeUpdates { commitment_update: None, revoke_and_ack: None, open_channel: None, accept_channel: None, funding_created: None, funding_signed: None, @@ -9745,7 +9753,7 @@ where L::Target: Logger, CBP: Fn(u64) -> BlindedMessagePath, { - if self.context.is_ffor_frozen() { + if self.context.ffor_blocks_commitment_round() { return None; } debug_assert!( @@ -9806,7 +9814,7 @@ where where L::Target: Logger, { - self.context.check_ffor_mutation().map_err(|_| ())?; + self.context.check_ffor_commitment_round().map_err(|_| ())?; let mut update_add_htlcs = Vec::new(); let mut update_fulfill_htlcs = Vec::new(); let mut update_fail_htlcs = Vec::new(); @@ -9937,7 +9945,11 @@ where CBP: Fn(u64) -> BlindedMessagePath { if self.context.is_ffor_frozen() { - return self.ffor_handle_reestablish(msg, logger); + if self.ffor_receiver_drain_binding().is_some() { + if let Some(response) = self.ffor_check_drain_reestablish(msg, logger)? { return Ok(response); } + } else { + return self.ffor_handle_reestablish(msg, logger); + } } let our_commitment_transaction = self.validate_reestablish_prefix(msg, logger)?; @@ -11929,7 +11941,7 @@ where /// self.remove_uncommitted_htlcs_and_mark_paused()'d #[rustfmt::skip] fn get_channel_reestablish(&mut self, logger: &L) -> Result where L::Target: Logger { - if self.context.is_ffor_frozen() { + if self.context.is_ffor_frozen() && self.ffor_receiver_drain_binding().is_none() { return self.ffor_get_reestablish(logger); } assert!(self.context.channel_state.is_peer_disconnected()); @@ -11957,7 +11969,7 @@ where [0;32] }; Ok(msgs::ChannelReestablish { - ffor_reestablish: None, + ffor_reestablish: self.ffor_drain_reestablish_report(), channel_id: self.context.channel_id(), // The protocol has two different commitment number concepts - the "commitment // transaction number", which starts from 0 and counts up, and the "revocation key @@ -12842,7 +12854,7 @@ where #[rustfmt::skip] fn build_commitment_no_status_check(&mut self, logger: &L) -> Result where L::Target: Logger { - self.context.check_ffor_mutation()?; + self.context.check_ffor_commitment_round()?; log_trace!(logger, "Updating HTLC state for a newly-sent commitment_signed..."); // We can upgrade the status of some HTLCs that are waiting on a commitment, even if we // fail to generate this, we still are at least at a position where upgrading their status @@ -12975,7 +12987,7 @@ where where L::Target: Logger, { - self.context.check_ffor_mutation()?; + self.context.check_ffor_commitment_round()?; // Get the fee tests from `build_commitment_no_state_update` #[cfg(any(test, fuzzing))] self.build_commitment_no_state_update(funding, logger); diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index f59674308bb..cd8f21845c3 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -1,6 +1,7 @@ use super::*; mod activation; +mod drain; mod fence; mod quiescence; mod reestablish; @@ -10,6 +11,8 @@ use crate::ln::ffor::{ FFORReceiverError, FFORReceiverStatus, FFORSettlementParty, FFORVoucher, FFORVoucherCommitments, FFORVoucherFailure, }; +use drain::FFORReceiverDrain; +pub(crate) use drain::{ffor_drain_completion_digest, FFORReceiverDrainCompletion}; use fence::FFORReceiverFence; pub(crate) use fence::{FFORReceiverFencePhase, FFOR_FROZEN_MESSAGE}; pub(crate) use quiescence::FFORReceiverQuiescence; @@ -26,6 +29,7 @@ pub(super) struct FFORReceiverBook { abort_reason: Option, setup: Option, fence: Option, + drain: Option, } struct FFORReceivedVoucher { @@ -47,11 +51,13 @@ impl_writeable_tlv_based!(FFORReceiverBook, { (8, setup, option), // A reader without the mutation fence must refuse this channel. (10, fence, option), + // Older readers must not discard drain ownership or a completed epoch tombstone. + (12, drain, option), }); impl FFORReceiverBook { pub(super) fn abort(&mut self, reason: FFORReceiverAbortReason) { - if self.fence.is_none() { + if self.fence.is_none() && !self.is_closed() { self.abort_reason.get_or_insert(reason); } } @@ -96,7 +102,7 @@ impl FFORReceiverBook { { return Err(DecodeError::InvalidValue); } - } else if self.abort_reason.is_none() + } else if (self.abort_reason.is_none() && !self.is_closed()) || self.vouchers.iter().any(|voucher| voucher.payment_hash == htlc.payment_hash) { // Never restore a live voucher whose channel-owned interception record was lost. @@ -142,6 +148,7 @@ where abort_reason: None, setup: None, fence: None, + drain: None, }); Ok(()) } @@ -226,7 +233,7 @@ where }); let reserved_hash = book.vouchers.iter().any(|voucher| voucher.payment_hash == msg.payment_hash); - if book.abort_reason.is_some() && !reserved_hash && !owned_pending { + if (book.abort_reason.is_some() || book.is_closed()) && !reserved_hash && !owned_pending { return; } let voucher = FFORVoucher { diff --git a/lightning/src/ln/channel/ffor/drain.rs b/lightning/src/ln/channel/ffor/drain.rs new file mode 100644 index 00000000000..7a4206a6fcd --- /dev/null +++ b/lightning/src/ln/channel/ffor/drain.rs @@ -0,0 +1,699 @@ +//! Voucher-only commitment rounds after an authenticated, durably retained close acknowledgement. +//! The manager owns signed evidence and persistence. This module never treats a peer report as +//! permission to drain, and never releases the ordinary mutation fence before final persistence. + +use super::*; +use crate::ln::ffor_recovery::FFORReceiverCloseRecord; + +pub(super) struct FFORReceiverDrain { + acknowledgement_hash: [u8; 32], + activation_hash: [u8; 32], + feerate_per_kw: u32, + settled: Vec, + known_preimages: Vec<(u64, PaymentPreimage)>, + completion_hash: Option<[u8; 32]>, + closed: bool, + // Instance-local permission follows a successful manager write. Never restored from disk. + enabled: bool, +} + +impl_writeable_tlv_based!(FFORReceiverDrain, { + (0, acknowledgement_hash, required), + (2, settled, required_vec), + (4, known_preimages, required_vec), + (6, completion_hash, option), + (8, closed, required), + (10, activation_hash, required), + (12, feerate_per_kw, required), + (11, enabled, (static_value, false)), +}); + +/// Point-in-time proof that both current commitments are empty and all stock rounds completed. +/// Only the channel can construct this value. The manager must retain it and complete its final +/// write before calling `finish_ffor_receiver_closed` under the same channel authority. +#[derive(Clone)] +pub(crate) struct FFORReceiverDrainCompletion { + epoch_id: [u8; 32], + activation_hash: [u8; 32], + acknowledgement_hash: [u8; 32], + commitments: FFORVoucherCommitments, + channel_id: ChannelId, + funding_txo: OutPoint, + monitor_update_id: u64, +} + +impl FFORReceiverDrainCompletion { + pub(crate) fn epoch_id(&self) -> [u8; 32] { + self.epoch_id + } + pub(crate) fn activation_hash(&self) -> [u8; 32] { + self.activation_hash + } + pub(crate) fn acknowledgement_hash(&self) -> [u8; 32] { + self.acknowledgement_hash + } + pub(crate) fn commitments(&self) -> FFORVoucherCommitments { + self.commitments + } + pub(crate) fn monitor_identity(&self) -> (ChannelId, OutPoint, u64) { + (self.channel_id, self.funding_txo, self.monitor_update_id) + } + pub(crate) fn completion_hash(&self) -> [u8; 32] { + ffor_drain_completion_digest( + self.epoch_id, + self.activation_hash, + self.acknowledgement_hash, + self.channel_id, + self.funding_txo, + self.commitments, + self.monitor_update_id, + ) + } +} + +/// Recompute a retained completion digest. This does not construct or authenticate a channel proof. +pub(crate) fn ffor_drain_completion_digest( + epoch_id: [u8; 32], activation_hash: [u8; 32], acknowledgement_hash: [u8; 32], + channel_id: ChannelId, funding_txo: OutPoint, commitments: FFORVoucherCommitments, + monitor_update_id: u64, +) -> [u8; 32] { + let mut bytes = b"ffor/native-drain-complete/v1".to_vec(); + bytes.extend_from_slice(&epoch_id); + bytes.extend_from_slice(&activation_hash); + bytes.extend_from_slice(&acknowledgement_hash); + bytes.extend_from_slice(&channel_id.0); + bytes.extend_from_slice(&funding_txo.encode()); + bytes.extend_from_slice(&monitor_update_id.to_be_bytes()); + for identity in [commitments.holder, commitments.counterparty] { + bytes.extend_from_slice(&identity.number.to_be_bytes()); + bytes.extend_from_slice(identity.txid.as_byte_array()); + } + Sha256::hash(&bytes).to_byte_array() +} + +impl FFORReceiverBook { + pub(super) fn is_closed(&self) -> bool { + self.drain.as_ref().map_or(false, |drain| drain.closed) + } +} + +impl ChannelContext +where + SP::Target: SignerProvider, +{ + pub(in crate::ln::channel) fn ffor_drain_enabled(&self) -> bool { + self.ffor_receiver_book.as_ref().map_or(false, |book| { + matches!(book.fence.as_ref().map(|f| f.phase), Some(FFORReceiverFencePhase::Draining)) + && book.drain.as_ref().map_or(false, |drain| drain.enabled && !drain.closed) + }) + } + + pub(in crate::ln::channel) fn ffor_blocks_commitment_round(&self) -> bool { + self.is_ffor_frozen() + && (!self.ffor_drain_enabled() || self.validate_ffor_drain_context().is_err()) + } + + pub(in crate::ln::channel) fn check_ffor_commitment_round(&self) -> Result<(), ChannelError> { + if self.ffor_blocks_commitment_round() { + Err(ChannelError::WarnAndDisconnect(FFOR_FROZEN_MESSAGE.to_owned())) + } else { + Ok(()) + } + } + + pub(in crate::ln::channel) fn ffor_failure_permitted(&self, id: u64) -> bool { + if !self.is_ffor_frozen() { + return true; + } + if !self.ffor_drain_enabled() { + return false; + } + let book = self.ffor_receiver_book.as_ref().unwrap(); + let drain = book.drain.as_ref().unwrap(); + book.vouchers.iter().position(|v| v.htlc_id == id).map_or(false, |slot| { + drain.settled[slot / 8] & (1 << (slot % 8)) == 0 + && !drain.known_preimages.iter().any(|(known, _)| *known == id) + && !self.holding_cell_htlc_updates.iter().any( + |update| matches!(update, HTLCUpdateAwaitingACK::ClaimHTLC { htlc_id, .. } if *htlc_id == id), + ) + }) + } + + fn validate_ffor_drain_context(&self) -> Result<(), DecodeError> { + let book = self.ffor_receiver_book.as_ref().ok_or(DecodeError::InvalidValue)?; + let drain = book.drain.as_ref().ok_or(DecodeError::InvalidValue)?; + if book.setup.is_none() + || book.abort_reason.is_some() + || drain.settled.len() != (book.vouchers.len() + 7) / 8 + || drain.known_preimages.len() > book.vouchers.len() + || self.feerate_per_kw != drain.feerate_per_kw + || (book.vouchers.len() % 8 != 0 + && drain + .settled + .last() + .map_or(true, |last| *last >> (book.vouchers.len() % 8) != 0)) + || !matches!(self.channel_state, ChannelState::ChannelReady(_)) + || self.channel_state.is_local_shutdown_sent() + || self.channel_state.is_remote_shutdown_sent() + || self.pending_update_fee.is_some() + || self.holding_cell_update_fee.is_some() + || self.interactive_tx_signing_session.is_some() + || self.signer_pending_closing + || self.signer_pending_funding + || self.signer_pending_channel_ready + || self.monitor_pending_channel_ready + || !self.monitor_pending_forwards.is_empty() + || !self.monitor_pending_update_adds.is_empty() + || !self.monitor_pending_failures.is_empty() + || !self.pending_outbound_htlcs.is_empty() + { + return Err(DecodeError::InvalidValue); + } + let mut ids = alloc::collections::BTreeSet::new(); + for (id, preimage) in &drain.known_preimages { + if !ids.insert(*id) || !self.ffor_owns_preimage(*id, preimage) { + return Err(DecodeError::InvalidValue); + } + } + for htlc in &self.pending_inbound_htlcs { + let slot = book + .vouchers + .iter() + .position(|v| { + v.htlc_id == htlc.htlc_id + && v.payment_hash == htlc.payment_hash + && v.amount_msat == htlc.amount_msat + && v.cltv_expiry == htlc.cltv_expiry + }) + .ok_or(DecodeError::InvalidValue)?; + match &htlc.state { + InboundHTLCState::Committed => {}, + InboundHTLCState::LocalRemoved(InboundHTLCRemovalReason::Fulfill(preimage, _)) + if self.ffor_owns_preimage(htlc.htlc_id, preimage) => {}, + InboundHTLCState::LocalRemoved(_) + if drain.settled[slot / 8] & (1 << (slot % 8)) == 0 => {}, + _ => return Err(DecodeError::InvalidValue), + } + } + for update in &self.holding_cell_htlc_updates { + match update { + HTLCUpdateAwaitingACK::ClaimHTLC { htlc_id, payment_preimage, .. } + if self.ffor_owns_preimage(*htlc_id, payment_preimage) => {}, + HTLCUpdateAwaitingACK::FailHTLC { htlc_id, .. } + | HTLCUpdateAwaitingACK::FailMalformedHTLC { htlc_id, .. } => { + let slot = book + .vouchers + .iter() + .position(|v| v.htlc_id == *htlc_id) + .ok_or(DecodeError::InvalidValue)?; + if drain.settled[slot / 8] & (1 << (slot % 8)) != 0 + || drain.known_preimages.iter().any(|(id, _)| id == htlc_id) + { + return Err(DecodeError::InvalidValue); + } + }, + _ => return Err(DecodeError::InvalidValue), + } + } + Ok(()) + } +} + +impl FundedChannel +where + SP::Target: SignerProvider, +{ + pub(crate) fn install_ffor_receiver_drain( + &mut self, record: &FFORReceiverCloseRecord, + ) -> Result<(), FFORReceiverError> { + let book = + self.context.ffor_receiver_book.as_ref().ok_or(FFORReceiverError::NotRegistered)?; + let fence = book.fence.as_ref().ok_or(FFORCommitmentError::PendingUpdates)?; + record + .validate( + book.setup.as_ref().ok_or(FFORCommitmentError::InvalidVoucherBook)?, + fence.activation_hash, + ) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let acknowledgement_hash = + record.acknowledgement_hash().ok_or(FFORCommitmentError::InvalidVoucherBook)?; + let settled = record.settled().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if let Some(drain) = &book.drain { + return if drain.acknowledgement_hash == acknowledgement_hash && drain.settled == settled + { + Ok(()) + } else { + Err(FFORCommitmentError::InvalidVoucherBook.into()) + }; + } + if fence.phase != FFORReceiverFencePhase::Active { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + self.validate_ffor_fence().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let known_preimages = self + .context + .holding_cell_htlc_updates + .iter() + .filter_map(|update| { + if let HTLCUpdateAwaitingACK::ClaimHTLC { htlc_id, payment_preimage, .. } = update { + Some((*htlc_id, *payment_preimage)) + } else { + None + } + }) + .collect(); + let book = self.context.ffor_receiver_book.as_mut().unwrap(); + book.drain = Some(FFORReceiverDrain { + acknowledgement_hash, + activation_hash: book.fence.as_ref().unwrap().activation_hash, + feerate_per_kw: self.context.feerate_per_kw, + settled, + known_preimages, + completion_hash: None, + closed: false, + enabled: false, + }); + book.fence.as_mut().unwrap().phase = FFORReceiverFencePhase::Draining; + Ok(()) + } + + /// Call only after the exact close acknowledgement is durably retained in this instance. + pub(crate) fn enable_ffor_receiver_drain( + &mut self, epoch: [u8; 32], ack_hash: [u8; 32], + ) -> Result<(), FFORReceiverError> { + if matches!( + self.ffor_reconnect_outcome, + Some(FFORReestablishOutcome::CloseReplayRequired { .. }) + ) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + self.validate_ffor_drain().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let book = + self.context.ffor_receiver_book.as_mut().ok_or(FFORReceiverError::NotRegistered)?; + if book.epoch_id != epoch { + return Err(FFORReceiverError::UnknownEpoch); + } + if !matches!(book.fence.as_ref().map(|f| f.phase), Some(FFORReceiverFencePhase::Draining)) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let drain = book.drain.as_mut().ok_or(FFORCommitmentError::InvalidVoucherBook)?; + if drain.acknowledgement_hash != ack_hash { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + drain.enabled = true; + Ok(()) + } + + /// Restore messages whose monitor write completed while the manager barrier still blocked drain. + /// If true, the manager must immediately call the stock `monitor_updating_restored` handler. + pub(crate) fn prepare_ffor_receiver_drain_monitor_resume(&mut self) -> bool { + if self.context.ffor_blocks_commitment_round() + || !self.context.ffor_drain_enabled() + || self.context.channel_state.is_monitor_update_in_progress() + || !self.context.blocked_monitor_updates.is_empty() + { + return false; + } + if self.context.monitor_pending_commitment_signed + || self.context.monitor_pending_revoke_and_ack + || !self.context.monitor_pending_finalized_fulfills.is_empty() + { + self.context.channel_state.set_monitor_update_in_progress(); + true + } else { + false + } + } + + pub(crate) fn ffor_receiver_drain_enabled(&self) -> bool { + self.context.ffor_drain_enabled() + } + + pub(crate) fn ffor_receiver_drain_binding(&self) -> Option<([u8; 32], Vec, bool)> { + self.context + .ffor_receiver_book + .as_ref()? + .drain + .as_ref() + .map(|drain| (drain.acknowledgement_hash, drain.settled.clone(), drain.closed)) + } + + pub(crate) fn ffor_receiver_drain_activation_hash(&self) -> Option<[u8; 32]> { + self.context.ffor_receiver_book.as_ref()?.drain.as_ref().map(|drain| drain.activation_hash) + } + + pub(crate) fn ffor_receiver_closed_completion_hash(&self) -> Option<[u8; 32]> { + self.context.ffor_receiver_book.as_ref()?.drain.as_ref()?.completion_hash + } + + pub(crate) fn validate_ffor_drain(&self) -> Result<(), DecodeError> { + let book = self.context.ffor_receiver_book.as_ref().ok_or(DecodeError::InvalidValue)?; + if book.is_closed() { + return if book.fence.is_none() + && book.abort_reason.is_none() + && book.drain.as_ref().unwrap().completion_hash.is_some() + { + Ok(()) + } else { + Err(DecodeError::InvalidValue) + }; + } + self.context.validate_ffor_drain_context()?; + if self.pending_splice.is_some() || self.quiescent_action.is_some() { + return Err(DecodeError::InvalidValue); + } + let book = self.context.ffor_receiver_book.as_ref().unwrap(); + let drain = book.drain.as_ref().unwrap(); + if book.fence.as_ref().map(|f| f.activation_hash) != Some(drain.activation_hash) { + return Err(DecodeError::InvalidValue); + } + match book.fence.as_ref().map(|f| f.phase) { + Some(FFORReceiverFencePhase::Draining) + if !drain.closed && drain.completion_hash.is_none() => {}, + Some(FFORReceiverFencePhase::ClosedPendingPersistence) + if !drain.closed && drain.completion_hash.is_some() => {}, + None if drain.closed && drain.completion_hash.is_some() => return Ok(()), + _ => return Err(DecodeError::InvalidValue), + } + Ok(()) + } + + /// Queue failures only after retained claims have priority. Signed settled slots are never failed. + pub(crate) fn ffor_queue_draining_vouchers(&mut self, logger: &L) + where + L::Target: Logger, + { + if self.context.ffor_blocks_commitment_round() || !self.context.ffor_drain_enabled() { + return; + } + let book = self.context.ffor_receiver_book.as_ref().unwrap(); + let failures: Vec<_> = + book.received + .iter() + .filter_map(|received| { + let id = received.voucher.htlc_id; + if self.context.ffor_failure_permitted(id) + && self.context.pending_inbound_htlcs.iter().any(|h| { + h.htlc_id == id && matches!(h.state, InboundHTLCState::Committed) + }) { + received.failure.clone().map(|failure| (id, failure)) + } else { + None + } + }) + .collect(); + for (id, failure) in failures { + match failure { + FFORVoucherFailure::Relay { packet } => { + let _ = self.queue_fail_htlc( + id, + msgs::OnionErrorPacket { data: packet, attribution_data: None }, + logger, + ); + }, + FFORVoucherFailure::Malformed { sha256_of_onion, failure_code } => { + let _ = + self.queue_fail_malformed_htlc(id, failure_code, sha256_of_onion, logger); + }, + } + } + } + + /// Remember owned knowledge and replace only failures which have not entered a commitment. + /// A late preimage for an already signed failure still reaches the monitor, never rewrites it. + pub(in crate::ln::channel) fn ffor_prepare_drain_claim( + &mut self, id: u64, preimage: PaymentPreimage, payment_info: Option, + ) -> Option { + if !self.context.ffor_owns_preimage(id, &preimage) + || self.context.ffor_receiver_book.as_ref().and_then(|b| b.drain.as_ref()).is_none() + { + return None; + } + let state = + self.context.pending_inbound_htlcs.iter().find(|h| h.htlc_id == id).map(|h| &h.state); + let late = !matches!( + state, + Some(InboundHTLCState::Committed) + | Some(InboundHTLCState::LocalRemoved(InboundHTLCRemovalReason::Fulfill(_, _))) + ); + let value = self + .context + .ffor_receiver_book + .as_ref() + .unwrap() + .vouchers + .iter() + .find(|v| v.htlc_id == id) + .unwrap() + .amount_msat; + let drain = self.context.ffor_receiver_book.as_mut().unwrap().drain.as_mut().unwrap(); + let known = drain.known_preimages.iter().any(|(known, _)| *known == id); + if !known { + drain.known_preimages.push((id, preimage)); + } + self.context.holding_cell_htlc_updates.retain(|update| !matches!(update, + HTLCUpdateAwaitingACK::FailHTLC { htlc_id, .. } | HTLCUpdateAwaitingACK::FailMalformedHTLC { htlc_id, .. } if *htlc_id == id)); + if !late { + return None; + } + if known { + return Some(UpdateFulfillFetch::DuplicateClaim {}); + } + self.context.latest_monitor_update_id += 1; + Some(UpdateFulfillFetch::NewClaim { + monitor_update: ChannelMonitorUpdate { + update_id: self.context.latest_monitor_update_id, + updates: vec![ChannelMonitorUpdateStep::PaymentPreimage { + payment_preimage: preimage, + payment_info, + }], + channel_id: Some(self.context.channel_id()), + }, + htlc_value_msat: value, + update_blocked: true, + }) + } + + pub(crate) fn ffor_receiver_drain_completion( + &self, monitor: &FFORMonitorSnapshot, logger: &L, + ) -> Result + where + L::Target: Logger, + { + self.validate_ffor_drain().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + self.check_ffor_synchronized()?; + let book = self.context.ffor_receiver_book.as_ref().unwrap(); + let drain = book.drain.as_ref().unwrap(); + let fence = book.fence.as_ref().ok_or(FFORCommitmentError::PendingUpdates)?; + if !matches!( + fence.phase, + FFORReceiverFencePhase::Draining | FFORReceiverFencePhase::ClosedPendingPersistence + ) || !self.context.pending_inbound_htlcs.is_empty() + || !self.holder_commitment_point.can_advance() + || !self.context.monitor_pending_finalized_fulfills.is_empty() + || monitor.channel_id != self.context.channel_id() + || Some(monitor.funding_txo) != self.funding.get_funding_txo() + || monitor.update_id != self.context.latest_monitor_update_id + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let holder_point = self + .holder_commitment_point + .current_point() + .ok_or(FFORCommitmentError::PendingUpdates)?; + let remote_point = self + .context + .counterparty_current_commitment_point + .ok_or(FFORCommitmentError::PendingUpdates)?; + let holder = self + .context + .build_commitment_transaction( + &self.funding, + self.holder_commitment_point.current_transaction_number(), + &holder_point, + true, + false, + logger, + ) + .tx; + let counterparty = self + .context + .build_commitment_transaction( + &self.funding, + self.context.counterparty_next_commitment_transaction_number + 1, + &remote_point, + false, + true, + logger, + ) + .tx; + if !holder.nondust_htlcs().is_empty() + || !counterparty.nondust_htlcs().is_empty() + || !monitor.holder.nondust_htlcs().is_empty() + || !monitor.counterparty_htlcs.is_empty() + || monitor.holder_number != holder.commitment_number() + || monitor.holder.commitment_number() != holder.commitment_number() + || monitor.holder.trust().built_transaction().transaction + != holder.trust().built_transaction().transaction + || monitor.counterparty_number != counterparty.commitment_number() + || monitor.counterparty_txid != counterparty.trust().txid() + || monitor.revoked_through != counterparty.commitment_number() + 1 + || self.context.commitment_secrets.get_min_seen_secret() != monitor.revoked_through + { + return Err(FFORCommitmentError::MonitorMismatch.into()); + } + verification::verify_claim_signatures( + &holder, + &monitor.holder, + &self.funding.channel_transaction_parameters, + )?; + Ok(FFORReceiverDrainCompletion { + epoch_id: book.epoch_id, + activation_hash: fence.activation_hash, + acknowledgement_hash: drain.acknowledgement_hash, + channel_id: monitor.channel_id, + funding_txo: monitor.funding_txo, + monitor_update_id: monitor.update_id, + commitments: FFORVoucherCommitments { + holder: verification::commitment_identity(&holder), + counterparty: verification::commitment_identity(&counterparty), + }, + }) + } + + pub(crate) fn prepare_ffor_receiver_closed( + &mut self, completion: &FFORReceiverDrainCompletion, + ) -> Result<(), FFORReceiverError> { + self.check_ffor_synchronized()?; + let book = + self.context.ffor_receiver_book.as_mut().ok_or(FFORReceiverError::NotRegistered)?; + let fence = book.fence.as_mut().ok_or(FFORCommitmentError::PendingUpdates)?; + let drain = book.drain.as_mut().ok_or(FFORCommitmentError::PendingUpdates)?; + if book.epoch_id != completion.epoch_id + || fence.activation_hash != completion.activation_hash + || drain.acknowledgement_hash != completion.acknowledgement_hash + || self.context.latest_monitor_update_id != completion.monitor_update_id + || !self.context.pending_inbound_htlcs.is_empty() + || !matches!( + fence.phase, + FFORReceiverFencePhase::Draining | FFORReceiverFencePhase::ClosedPendingPersistence + ) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + fence.phase = FFORReceiverFencePhase::ClosedPendingPersistence; + drain.enabled = false; + drain.completion_hash = Some(completion.completion_hash()); + Ok(()) + } + + pub(crate) fn finish_ffor_receiver_closed( + &mut self, epoch: [u8; 32], completion_hash: [u8; 32], + ) -> Result<(), FFORReceiverError> { + let book = + self.context.ffor_receiver_book.as_mut().ok_or(FFORReceiverError::NotRegistered)?; + let drain = book.drain.as_mut().ok_or(FFORCommitmentError::PendingUpdates)?; + if book.epoch_id != epoch + || drain.completion_hash != Some(completion_hash) + || !matches!( + book.fence.as_ref().map(|f| f.phase), + Some(FFORReceiverFencePhase::ClosedPendingPersistence) + ) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + drain.closed = true; + book.fence = None; + Ok(()) + } +} + +impl FundedChannel +where + SP::Target: SignerProvider, +{ + /// Drain reconnect uses unchanged BOLT counters and resend rules, after this report check. + pub(in crate::ln::channel) fn ffor_check_drain_reestablish( + &mut self, msg: &msgs::ChannelReestablish, logger: &L, + ) -> Result, ChannelError> + where + L::Target: Logger, + { + use lightning_ffor::reestablish::ReportedState; + self.context.check_ffor_commitment_round()?; + self.validate_ffor_drain() + .map_err(|_| ChannelError::WarnAndDisconnect(FFOR_FROZEN_MESSAGE.to_owned()))?; + let book = self.context.ffor_receiver_book.as_ref().unwrap(); + let hash = book.drain.as_ref().unwrap().activation_hash; + let peer_report = msg.ffor_reestablish.as_ref().map(|wire| wire.report()); + if let Some(report) = peer_report.filter(|report| { + report.epoch_id == book.epoch_id + && report.activation_hash == hash + && report.state == ReportedState::Active + }) { + self.validate_reestablish_prefix(msg, logger)?; + let next_counterparty = INITIAL_COMMITMENT_NUMBER + - self.context.counterparty_next_commitment_transaction_number; + if msg.next_local_commitment_number + 1 < next_counterparty + || msg.next_local_commitment_number > next_counterparty + || msg.next_funding.is_some() + || msg + .my_current_funding_locked + .as_ref() + .map_or(false, |funding| self.funding.get_funding_txid() != Some(funding.txid)) + { + return Err(ChannelError::WarnAndDisconnect(FFOR_FROZEN_MESSAGE.to_owned())); + } + self.context.ffor_receiver_book.as_mut().unwrap().drain.as_mut().unwrap().enabled = + false; + self.ffor_reconnect_outcome = + Some(FFORReestablishOutcome::CloseReplayRequired { peer_report: report }); + self.context.channel_state.clear_peer_disconnected(); + self.mark_response_received(); + return Ok(Some(ReestablishResponses { + channel_ready: None, + channel_ready_order: ChannelReadyOrder::ChannelReadyFirst, + raa: None, + commitment_update: None, + commitment_order: self.context.resend_order.clone(), + announcement_sigs: None, + shutdown_msg: None, + tx_signatures: None, + tx_abort: None, + inferred_splice_locked: None, + })); + } + if peer_report.map_or(false, |report| { + report.epoch_id == book.epoch_id + && report.activation_hash == hash + && matches!(report.state, ReportedState::Draining | ReportedState::Closed) + }) && msg.next_funding.is_none() + && msg + .my_current_funding_locked + .as_ref() + .map_or(true, |funding| self.funding.get_funding_txid() == Some(funding.txid)) + { + return Ok(None); + } + self.ffor_reconnect_outcome = + Some(FFORReestablishOutcome::ResolutionRequired { peer_report }); + Err(ChannelError::WarnAndDisconnect( + "FFOR drain reconnect requires the same retained epoch and close acknowledgement" + .to_owned(), + )) + } + + pub(in crate::ln::channel) fn ffor_drain_reestablish_report( + &self, + ) -> Option { + use lightning_ffor::reestablish::{Reestablish, ReportedState}; + let book = self.context.ffor_receiver_book.as_ref()?; + let drain = book.drain.as_ref()?; + Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: book.epoch_id, + activation_hash: drain.activation_hash, + state: if drain.closed { ReportedState::Closed } else { ReportedState::Draining }, + })) + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/channel/ffor/drain/tests.rs b/lightning/src/ln/channel/ffor/drain/tests.rs new file mode 100644 index 00000000000..c09cbf34b1f --- /dev/null +++ b/lightning/src/ln/channel/ffor/drain/tests.rs @@ -0,0 +1,228 @@ +use super::*; +use crate::ln::channelmanager::ffor_recovery_tests::install_ffor_activation_for_test; +use crate::ln::ffor_recovery::FFORReceiverActivation; +use crate::ln::ffor_tests::quiescence::{complete_handshake, register_signed, request}; +use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; +use crate::ln::functional_test_utils::*; +use crate::sign::ffor::FFORSigningRequest; +use lightning_ffor::transcript; +use lightning_ffor::wire::{Activate, CloseAck, Message, Payload}; + +fn sign(message: &mut Message, node: &Node) { + message.signature = node + .keys_manager + .sign_ffor_message(&FFORSigningRequest::new(&message.unsigned_wire().unwrap()).unwrap()) + .unwrap() + .serialize_compact(); +} + +#[test] +fn ffor_drain_restored_permission_and_duplicate_hash_fail_closed() { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = + if receiver_funds { (&nodes[1], &nodes[0]) } else { (&nodes[0], &nodes[1]) }; + let preimage = PaymentPreimage([*receiver.network_payment_count.as_ref().borrow(); 32]); + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + register_signed(sender, receiver, id, voucher); + deliver_parked_voucher(sender, receiver, update.clone()); + request(sender, receiver, id).unwrap(); + complete_handshake(sender, receiver); + let hash = + install_ffor_activation_for_test(sender, receiver, id, FFORReceiverFencePhase::Active); + let encoded = { + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + peer.channel_by_id.get(&id).unwrap().as_funded().unwrap().encode() + }; + let features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + let args = (&receiver.keys_manager, &receiver.keys_manager, &features); + let mut channel = FundedChannel::read(&mut &encoded[..], args).unwrap(); + channel.quiescent_action = None; + channel.context.channel_state.clear_quiescent(); + channel.context.channel_state.clear_remote_stfu_sent(); + channel.context.channel_state.clear_local_stfu_sent(); + channel.context.channel_state.clear_peer_disconnected(); + let setup = channel.ffor_receiver_setup_record().unwrap().unwrap(); + let authenticated = setup.validate_recovery().unwrap(); + let monitor = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); + let commitments = channel + .ffor_voucher_commitments( + FFORSettlementParty::Counterparty, + &[voucher], + &monitor, + &receiver.logger, + ) + .unwrap(); + let mut message = Message { + header: authenticated.header(), + payload: Payload::Activate(Activate { + setup_hash: authenticated.setup_hash(), + book_hash: authenticated.book_hash(), + commit_hash: transcript::commitment_hash( + commitments.holder.number, + &commitments.holder.txid.to_byte_array(), + commitments.counterparty.number, + &commitments.counterparty.txid.to_byte_array(), + ), + epoch_start_height: receiver.node.current_best_block().height, + }), + extensions: Vec::new(), + signature: [0; 64], + }; + sign(&mut message, receiver); + let activation = FFORReceiverActivation::prepare( + &setup, + &message.encode().unwrap(), + commitments, + &monitor, + receiver.node.current_best_block().height, + ) + .unwrap(); + assert_eq!(activation.activation_hash(&setup).unwrap(), hash); + message.payload = Payload::ActivateAck(hash); + sign(&mut message, sender); + let activation = activation.with_ack(&setup, &message.encode().unwrap()).unwrap(); + message.payload = Payload::Close(hash); + sign(&mut message, receiver); + let close = activation.with_close(&setup, &message.encode().unwrap()).unwrap(); + message.payload = Payload::CloseAck(CloseAck { + activation_hash: hash, + num_slots: 1, + settled: vec![0], + preimages: Vec::new(), + preimages_tlv_present: true, + }); + sign(&mut message, sender); + let close = close.with_close_ack(&setup, &message.encode().unwrap()).unwrap(); + channel.install_ffor_receiver_drain(close.close_record().unwrap()).unwrap(); + assert!(channel.context.ffor_blocks_commitment_round()); + assert!(channel + .queue_fail_htlc( + voucher.htlc_id, + msgs::OnionErrorPacket { data: vec![], attribution_data: None }, + &receiver.logger + ) + .is_err()); + channel + .enable_ffor_receiver_drain( + [81; 32], + close.close_record().unwrap().acknowledgement_hash().unwrap(), + ) + .unwrap(); + assert!(!channel.context.ffor_blocks_commitment_round()); + let fee = LowerBoundedFeeEstimator::new(receiver.fee_estimator); + assert!(channel.update_add_htlc(&update.update_add_htlcs[0], &fee).is_err()); + assert!(channel + .update_fee( + &fee, + &msgs::UpdateFee { channel_id: id, feerate_per_kw: 500 }, + &receiver.logger + ) + .is_err()); + assert!(channel.propose_quiescence(&receiver.logger, QuiescentAction::DoNothing).is_err()); + let persisted = channel.encode(); + let mut restored = FundedChannel::read(&mut &persisted[..], args).unwrap(); + assert!(restored.context.ffor_blocks_commitment_round()); + assert_eq!(restored.ffor_receiver_abort_reason(), None); + let message = restored.get_channel_reestablish(&receiver.logger).unwrap(); + let replay = FFORReestablishOutcome::CloseReplayRequired { + peer_report: lightning_ffor::reestablish::Reestablish { + epoch_id: [81; 32], + activation_hash: hash, + state: lightning_ffor::reestablish::ReportedState::Active, + }, + }; + restored.ffor_reconnect_outcome = Some(replay); + assert!(restored.ffor_check_drain_reestablish(&message, &receiver.logger).is_err()); + assert_eq!(restored.ffor_receiver_reconnect_outcome(), Some(&replay)); + assert!(restored + .enable_ffor_receiver_drain( + [81; 32], + close.close_record().unwrap().acknowledgement_hash().unwrap() + ) + .is_err()); + + for failure_signed in [false, true] { + let mut claiming = FundedChannel::read(&mut &persisted[..], args).unwrap(); + claiming.context.channel_state.clear_peer_disconnected(); + claiming + .enable_ffor_receiver_drain( + [81; 32], + close.close_record().unwrap().acknowledgement_hash().unwrap(), + ) + .unwrap(); + let unchanged = claiming.encode(); + assert!(matches!( + claiming.get_update_fulfill_htlc_and_commit( + voucher.htlc_id, + PaymentPreimage([255; 32]), + None, + None, + &receiver.logger + ), + UpdateFulfillCommitFetch::DuplicateClaim {} + )); + assert_eq!(claiming.encode(), unchanged); + claiming.ffor_queue_draining_vouchers(&receiver.logger); + assert!(matches!( + claiming.context.holding_cell_htlc_updates[0], + HTLCUpdateAwaitingACK::FailHTLC { .. } + )); + if failure_signed { + assert!(claiming.maybe_free_holding_cell_htlcs(&fee, &receiver.logger).0.is_some()); + } + let monitor = match claiming.get_update_fulfill_htlc_and_commit( + voucher.htlc_id, + preimage, + None, + None, + &receiver.logger, + ) { + UpdateFulfillCommitFetch::NewClaim { monitor_update, .. } => monitor_update, + _ => panic!("owned preimage must be persisted"), + }; + assert!(monitor.updates.iter().any(|step| matches!(step, ChannelMonitorUpdateStep::PaymentPreimage { payment_preimage, .. } if *payment_preimage == preimage))); + assert!(!claiming + .context + .holding_cell_htlc_updates + .iter() + .any(|update| matches!(update, HTLCUpdateAwaitingACK::FailHTLC { .. }))); + if failure_signed { + assert_eq!(monitor.updates.len(), 1); + assert!(matches!( + claiming.context.pending_inbound_htlcs[0].state, + InboundHTLCState::LocalRemoved(InboundHTLCRemovalReason::FailRelay(_)) + )); + } else { + assert!(matches!( + claiming.context.pending_inbound_htlcs[0].state, + InboundHTLCState::LocalRemoved(InboundHTLCRemovalReason::Fulfill(_, _)) + )); + } + let claimed_bytes = claiming.encode(); + assert!(FundedChannel::read(&mut &claimed_bytes[..], args).is_ok()); + } + + channel + .context + .ffor_receiver_book + .as_mut() + .unwrap() + .drain + .as_mut() + .unwrap() + .activation_hash[0] ^= 1; + let corrupt = channel.encode(); + assert!(matches!( + FundedChannel::read(&mut &corrupt[..], args), + Err(DecodeError::InvalidValue) + )); + } +} diff --git a/lightning/src/ln/channel/ffor/fence.rs b/lightning/src/ln/channel/ffor/fence.rs index 3c68150c60f..bb5e89f3bf8 100644 --- a/lightning/src/ln/channel/ffor/fence.rs +++ b/lightning/src/ln/channel/ffor/fence.rs @@ -14,12 +14,16 @@ pub(crate) enum FFORReceiverFencePhase { Activating, Active, Aborting, + Draining, + ClosedPendingPersistence, } impl_writeable_tlv_based_enum!(FFORReceiverFencePhase, (0, Activating) => {}, (2, Active) => {}, (4, Aborting) => {}, + (6, Draining) => {}, + (8, ClosedPendingPersistence) => {}, ); pub(super) struct FFORReceiverFence { @@ -63,6 +67,11 @@ where return true; } update.updates.iter().all(|step| match step { + ChannelMonitorUpdateStep::LatestHolderCommitmentTXInfo { .. } + | ChannelMonitorUpdateStep::LatestHolderCommitment { .. } + | ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTXInfo { .. } + | ChannelMonitorUpdateStep::LatestCounterpartyCommitment { .. } + | ChannelMonitorUpdateStep::CommitmentSecret { .. } => self.ffor_drain_enabled(), ChannelMonitorUpdateStep::PaymentPreimage { payment_preimage, .. } => { let hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); self.ffor_receiver_book @@ -105,6 +114,15 @@ where Some(book) if book.fence.is_some() => book, _ => return Ok(()), }; + if book.drain.is_some() { + return self.validate_ffor_drain(); + } + if matches!( + book.fence.as_ref().unwrap().phase, + FFORReceiverFencePhase::Draining | FFORReceiverFencePhase::ClosedPendingPersistence + ) { + return Err(DecodeError::InvalidValue); + } let context = &self.context; let abort_matches_phase = match book.fence.as_ref().unwrap().phase { FFORReceiverFencePhase::Aborting => { diff --git a/lightning/src/ln/channel/ffor/reestablish.rs b/lightning/src/ln/channel/ffor/reestablish.rs index fdcea793eef..b26d66e7db8 100644 --- a/lightning/src/ln/channel/ffor/reestablish.rs +++ b/lightning/src/ln/channel/ffor/reestablish.rs @@ -12,6 +12,8 @@ use lightning_ffor::reestablish::{Reestablish, ReportedState}; pub(crate) enum FFORReestablishOutcome { /// The peer reports the exact active epoch. A matching signed acknowledgement is still needed. MatchingActive { peer_report: Reestablish }, + /// The peer needs the retained close transcript before a fresh reconnect can replay stock rounds. + CloseReplayRequired { peer_report: Reestablish }, /// Setup did not complete. Retain the fence until the manager durably records the abort. AbortRequired { peer_report: Option }, /// Possibly active evidence conflicts or needs a signed close response. Retain the fence. @@ -114,6 +116,9 @@ where FFORReceiverFencePhase::Activating => (ReportedState::Activating, [0; 32]), FFORReceiverFencePhase::Active => (ReportedState::Active, hash), FFORReceiverFencePhase::Aborting => (ReportedState::Aborted, [0; 32]), + FFORReceiverFencePhase::Draining | FFORReceiverFencePhase::ClosedPendingPersistence => { + return Err(unavailable()) + }, }; let counterparty_next = self.context.counterparty_next_commitment_transaction_number; let next_local_commitment_number = INITIAL_COMMITMENT_NUMBER diff --git a/lightning/src/ln/channel/ffor/setup.rs b/lightning/src/ln/channel/ffor/setup.rs index 7f8ab63cf3d..19d03da2cc9 100644 --- a/lightning/src/ln/channel/ffor/setup.rs +++ b/lightning/src/ln/channel/ffor/setup.rs @@ -353,6 +353,9 @@ where pub(in crate::ln::channel) fn ffor_restored(&mut self) -> Result<(), DecodeError> { self.ffor_validate_receiver_setup()?; self.validate_ffor_fence()?; + if self.ffor_receiver_drain_binding().is_some() { + self.validate_ffor_drain()?; + } if let Some(book) = self.context.ffor_receiver_book.as_mut() { book.abort(FFORReceiverAbortReason::Restarted); } @@ -375,7 +378,7 @@ where return Err(DecodeError::InvalidValue); } let pending = self.context.pending_inbound_htlcs.iter().any(|htlc| book.owns(htlc.htlc_id)); - if book.abort_reason.is_none() || pending { + if (book.abort_reason.is_none() && !book.is_closed()) || pending { let channel_type = self.funding.get_channel_type(); if record.funding_txo != self.funding.get_funding_txo().ok_or(DecodeError::InvalidValue)? diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c1ff9e5d501..15eb2fa8bec 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12083,7 +12083,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ |htlc_id| self.path_for_release_held_htlc(htlc_id, outbound_scid_alias, &msg.channel_id, counterparty_node_id) ); let responses = try_channel_entry!(self, peer_state, res, chan_entry); - if chan.context.is_ffor_frozen() { + if chan.context.is_ffor_frozen() && chan.ffor_receiver_drain_binding().is_none() { let transition = self.apply_ffor_reconnect_outcome(chan); try_channel_entry!(self, peer_state, transition, chan_entry); return Ok(()); @@ -17190,6 +17190,9 @@ where setup, channel.ffor_receiver_fence(), channel.ffor_receiver_abort_reason(), + channel.ffor_receiver_drain_binding(), + channel.ffor_receiver_closed_completion_hash(), + channel.ffor_receiver_drain_activation_hash(), )); } let logger = WithChannelContext::from(&args.logger, &channel.context, None); @@ -17594,8 +17597,15 @@ where let mut ffor_persistence = FFORPersistenceBarrier::new(); let ffor_activation = FFORReceiverRuntime::restored(&ffor_recovery, &mut ffor_persistence)?; ffor_recovery.validate_identity(our_network_pubkey, chain_hash)?; - for (setup, fence, abort_reason) in &ffor_channel_setups { - ffor_recovery.validate_channel_outcome(setup, *fence, *abort_reason)?; + for (setup, fence, abort_reason, drain, completion, drain_hash) in &ffor_channel_setups { + ffor_recovery.validate_channel_lifecycle( + setup, + *fence, + *abort_reason, + drain.clone(), + *completion, + *drain_hash, + )?; } for channel_id in ffor_recovery.activation_channels() { let monitor = diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs index 000e97af390..a4b879de763 100644 --- a/lightning/src/ln/channelmanager/ffor_activation.rs +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -9,6 +9,7 @@ use crate::sign::ffor::FFORSigningRequest; use lightning_ffor::transcript; use lightning_ffor::wire::{Activate, Message as FFORMessage, Payload}; +mod close; mod reestablish; struct RuntimeEntry { @@ -275,7 +276,18 @@ where .with_ack(&setup, ack_wire) .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; if previous.is_active() { - channel.accept_ffor_receiver_activation(&activation)?; + // A byte-identical acknowledgement remains idempotent after close or drain. + // Check the current lifecycle instead of repeating the earlier phase transition. + recovery + .validate_channel_lifecycle( + &setup, + channel.ffor_receiver_fence(), + channel.ffor_receiver_abort_reason(), + channel.ffor_receiver_drain_binding(), + channel.ffor_receiver_closed_completion_hash(), + channel.ffor_receiver_drain_activation_hash(), + ) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); } let upgrade = recovery @@ -297,7 +309,7 @@ where pub(super) fn check_ffor_reconnect_archive( &self, channel: &FundedChannel, ) -> Result<(), ChannelError> { - if !channel.context.is_ffor_frozen() { + if !channel.context.is_ffor_frozen() && channel.ffor_receiver_drain_binding().is_none() { return Ok(()); } let invalid = || { @@ -314,13 +326,22 @@ where }; let recovery = self.ffor_recovery.lock().unwrap(); recovery - .validate_channel_outcome( + .validate_channel_lifecycle( &setup, channel.ffor_receiver_fence(), channel.ffor_receiver_abort_reason(), + channel.ffor_receiver_drain_binding(), + channel.ffor_receiver_closed_completion_hash(), + channel.ffor_receiver_drain_activation_hash(), ) .map_err(|_| invalid())?; let activation = recovery.get_activation(&key).ok_or_else(invalid)?; + if activation.is_draining() { + if !activation.is_closed() { + channel.validate_ffor_drain().map_err(|_| invalid())?; + } + return Ok(()); + } if channel.ffor_frozen_commitments(&self.logger).map_err(|_| invalid())? != activation.commitments() { diff --git a/lightning/src/ln/channelmanager/ffor_activation/close.rs b/lightning/src/ln/channelmanager/ffor_activation/close.rs new file mode 100644 index 00000000000..ac6cb3d30ba --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/close.rs @@ -0,0 +1,376 @@ +//! Private signed close and voucher drain orchestration. Each wire and channel release follows +//! the exact retained transition's persistence requirement; stock monitor writes still gate HTLCs. + +use super::*; + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Sign and retain close intent. Exact retries reuse the existing bytes and barrier. + pub(crate) fn prepare_ffor_receiver_close( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(channel_id) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let mut recovery = self.ffor_recovery.lock().unwrap(); + let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?.clone(); + let previous = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; + let hash = previous + .activation_hash(&setup) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if previous.is_aborted() || !previous.is_active() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + if previous.close_record().is_some() { + return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); + } + if channel.ffor_receiver_fence() != Some((FFORReceiverFencePhase::Active, hash)) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let mut message = FFORMessage { + header: setup + .validate_recovery() + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)? + .header(), + payload: Payload::Close(hash), + extensions: Vec::new(), + signature: [0; 64], + }; + let unsigned = + message.unsigned_wire().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let request = FFORSigningRequest::new(&unsigned) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + message.signature = self + .node_signer + .sign_ffor_message(&request) + .map_err(|_| FFORReceiverError::SignerUnavailable)? + .serialize_compact(); + let next = previous + .with_close( + &setup, + &message.encode().map_err(|_| FFORReceiverError::SignerUnavailable)?, + ) + .map_err(|_| FFORReceiverError::SignerUnavailable)?; + let upgrade = recovery + .prepare_activation(&setup, &next) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + upgrade.commit(); + self.ffor_activation.lock().unwrap().record(key, requirement.clone(), false); + Ok(requirement) + } + + /// Exact close retransmission is allowed after reconnect. The callback atomically verifies the + /// current authenticated connection and enqueues a bounded message without manager reentry or I/O. + pub(crate) fn release_ffor_receiver_close( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + enqueue: C, + ) -> Result + where + C: FnOnce(&[u8]) -> Result<(), ()>, + { + let _guard = self.total_consistency_lock.read().unwrap(); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(channel_id) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let recovery = self.ffor_recovery.lock().unwrap(); + let setup = channel + .ffor_receiver_setup_record() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + .ok_or(FFORReceiverError::NotRegistered)?; + recovery + .validate_channel_lifecycle( + &setup, + channel.ffor_receiver_fence(), + channel.ffor_receiver_abort_reason(), + channel.ffor_receiver_drain_binding(), + channel.ffor_receiver_closed_completion_hash(), + channel.ffor_receiver_drain_activation_hash(), + ) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if matches!( + channel.ffor_receiver_reconnect_outcome(), + Some(FFORReestablishOutcome::ResolutionRequired { .. }) + | Some(FFORReestablishOutcome::AbortRequired { .. }) + ) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let activation = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; + let close = activation.close_record().ok_or(FFORCommitmentError::PendingUpdates)?; + if let Some(FFORReestablishOutcome::CloseReplayRequired { peer_report }) = + channel.ffor_receiver_reconnect_outcome() + { + let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?; + let hash = activation + .activation_hash(setup) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if peer_report.epoch_id != epoch_id + || peer_report.activation_hash != hash + || peer_report.state != lightning_ffor::reestablish::ReportedState::Active + { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + } + + let runtime = self.ffor_activation.lock().unwrap(); + if !channel.context.is_connected() + || !self.ffor_persistence.lock().unwrap().is_complete(&runtime.get(&key)?.requirement) + { + return Ok(false); + } + Ok(enqueue(close.close_wire()).is_ok()) + } + + /// Authenticate an exact settlement response and install the still-disabled channel drain. + pub(crate) fn accept_ffor_receiver_close_ack( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ack_wire: &[u8], + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let mut recovery = self.ffor_recovery.lock().unwrap(); + let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?.clone(); + let previous = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; + let next = previous + .with_close_ack(&setup, ack_wire) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if previous.is_draining() { + return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); + } + let upgrade = recovery + .prepare_activation(&setup, &next) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.install_ffor_receiver_drain(next.close_record().unwrap())?; + upgrade.commit(); + self.ffor_activation.lock().unwrap().record(key, requirement.clone(), false); + Ok(requirement) + } + + /// Import every signed preimage before enabling any failure. Repeated calls and restart use + /// the stock idempotent claim path, whose own monitor persistence gates fulfill wire. + pub(crate) fn release_ffor_receiver_drain( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let (ack_hash, claims) = { + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(channel_id) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let recovery = self.ffor_recovery.lock().unwrap(); + let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?; + let activation = + recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; + if activation.is_closed() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let close = activation.close_record().ok_or(FFORCommitmentError::PendingUpdates)?; + let ack_hash = + close.acknowledgement_hash().ok_or(FFORCommitmentError::PendingUpdates)?; + let runtime = self.ffor_activation.lock().unwrap(); + if !self.ffor_persistence.lock().unwrap().is_complete(&runtime.get(&key)?.requirement) { + return Ok(false); + } + let authenticated = + setup.validate_recovery().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let claims = close + .preimages() + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)? + .into_iter() + .map(|(slot, preimage)| { + let voucher = &authenticated.vouchers()[usize::from(slot) - 1]; + ( + HTLCClaimSource { + counterparty_node_id: *counterparty_node_id, + funding_txo: channel.funding_outpoint(), + channel_id: *channel_id, + htlc_id: voucher.htlc_id, + }, + preimage, + ) + }) + .collect::>(); + (ack_hash, claims) + }; + for (source, preimage) in claims { + self.claim_mpp_part(source, preimage, None, None, |_, _| (None, None)); + } + { + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let recovery = self.ffor_recovery.lock().unwrap(); + let activation = + recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; + if activation.is_closed() + || activation.close_record().and_then(|close| close.acknowledgement_hash()) + != Some(ack_hash) + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let runtime = self.ffor_activation.lock().unwrap(); + if !self.ffor_persistence.lock().unwrap().is_complete(&runtime.get(&key)?.requirement) { + return Ok(false); + } + channel.enable_ffor_receiver_drain(epoch_id, ack_hash)?; + channel.ffor_queue_draining_vouchers(&self.logger); + channel.prepare_ffor_receiver_drain_monitor_resume(); + }; + // The stock completion path checks in-flight writes and unblocks only their ordered successors. + self.channel_monitor_updated(channel_id, None, counterparty_node_id); + Ok(true) + } + + /// Capture both empty commitment views and retain Closed before releasing ordinary updates. + pub(crate) fn prepare_ffor_receiver_closed( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + monitor: &FFORMonitorSnapshot, + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let mut recovery = self.ffor_recovery.lock().unwrap(); + let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?.clone(); + let previous = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; + if previous.is_closed() { + return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); + } + let completion = channel.ffor_receiver_drain_completion(monitor, &self.logger)?; + let next = previous + .with_closed(&setup, &completion) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + let upgrade = recovery + .prepare_activation(&setup, &next) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.prepare_ffor_receiver_closed(&completion)?; + upgrade.commit(); + self.ffor_activation.lock().unwrap().record(key, requirement.clone(), false); + Ok(requirement) + } + + pub(crate) fn release_ffor_receiver_closed( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let recovery = self.ffor_recovery.lock().unwrap(); + let activation = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; + let close = activation.close_record().ok_or(FFORCommitmentError::PendingUpdates)?; + let digest = close.completion_hash().ok_or(FFORCommitmentError::PendingUpdates)?; + let runtime = self.ffor_activation.lock().unwrap(); + if !self.ffor_persistence.lock().unwrap().is_complete(&runtime.get(&key)?.requirement) { + return Ok(false); + } + if channel.ffor_receiver_drain_binding().map(|(_, _, closed)| closed) != Some(true) { + channel.finish_ffor_receiver_closed(epoch_id, digest)?; + } + Ok(true) + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs new file mode 100644 index 00000000000..0030f944439 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs @@ -0,0 +1,498 @@ +use super::*; +use crate::chain::ChannelMonitorUpdateStatus; +use crate::ln::channelmanager::ffor_activation::drain_tests::{drain, park_two}; +use crate::ln::channelmanager::ffor_recovery_tests::claim_ffor_preimage_for_test; +use crate::ln::ffor_tests::anchor_config; +use crate::ln::functional_test_utils::*; +use lightning_ffor::reestablish::{Reestablish, ReportedState}; +use lightning_ffor::wire::{CloseAck, Preimage}; + +const EPOCH: [u8; 32] = [81; 32]; + +fn persist(node: &Node) -> Vec { + let token = node.node.capture_ffor_persistence(); + let bytes = node.node.encode(); + node.node.ffor_persistence_completed(token).unwrap(); + bytes +} + +fn signed(node: &Node, receiver: &Node, id: ChannelId, payload: Payload) -> Vec { + let registry = receiver.node.ffor_recovery.lock().unwrap(); + let header = registry + .get(&FFORRecoveryKey { channel_id: id, epoch_id: EPOCH }) + .unwrap() + .validate_recovery() + .unwrap() + .header(); + let mut message = FFORMessage { header, payload, extensions: Vec::new(), signature: [0; 64] }; + message.signature = node + .keys_manager + .sign_ffor_message(&FFORSigningRequest::new(&message.unsigned_wire().unwrap()).unwrap()) + .unwrap() + .serialize_compact(); + message.encode().unwrap() +} + +fn activate(sender: &Node, receiver: &Node, id: ChannelId) -> ([u8; 32], Vec) { + let key = FFORRecoveryKey { channel_id: id, epoch_id: EPOCH }; + let hash = { + let registry = receiver.node.ffor_recovery.lock().unwrap(); + registry.get_activation(&key).unwrap().activation_hash(registry.get(&key).unwrap()).unwrap() + }; + assert!(receiver + .node + .release_ffor_receiver_activation(&id, &sender.node.get_our_node_id(), EPOCH, |_| Ok(())) + .unwrap()); + let ack = signed(sender, receiver, id, Payload::ActivateAck(hash)); + receiver + .node + .accept_ffor_receiver_activation_ack(&id, &sender.node.get_our_node_id(), EPOCH, &ack) + .unwrap(); + persist(receiver); + // The settlement test node models its signed Active response and exits its stock STFU state. + let peers = sender.node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&receiver.node.get_our_node_id()).unwrap().lock().unwrap(); + peer.channel_by_id.get_mut(&id).unwrap().as_funded_mut().unwrap().exit_quiescence(); + (hash, ack) +} + +fn assert_no_htlc_wire(node: &Node) { + for event in node.node.get_and_clear_pending_msg_events() { + assert!( + matches!( + event, + MessageSendEvent::SendChannelUpdate { .. } + | MessageSendEvent::BroadcastChannelUpdate { .. } + ), + "unexpected wire {event:?}" + ); + } +} + +#[test] +fn ffor_close_manager_drains_both_views_and_persists_closed_before_ordinary_payment() { + for receiver_funds in [false, true] { + for (settled, learned, delay_monitor, restart_mid_round) in [ + (false, false, false, false), + (false, true, false, false), + (true, false, false, false), + (true, false, true, false), + (true, false, false, true), + ] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor); + let (mid_persister, mid_monitor, mid_manager); + let reloaded; + let config = anchor_config(); + let managers = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let mut nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let peer = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + let (vouchers, preimage) = park_two(&nodes[0], &nodes[1], id); + let (hash, activation_ack) = activate(&nodes[0], &nodes[1], id); + let old_monitor = get_monitor!(nodes[1], id).ffor_commitment_snapshot().unwrap(); + if learned { + claim_ffor_preimage_for_test( + &nodes[0], + &nodes[1], + id, + vouchers[0].htlc_id, + preimage, + ); + check_added_monitors(&nodes[1], 1); + } + let old_token = nodes[1].node.capture_ffor_persistence(); + let close_requirement = + nodes[1].node.prepare_ffor_receiver_close(&id, &peer, EPOCH).unwrap(); + assert_eq!( + nodes[1].node.prepare_ffor_receiver_close(&id, &peer, EPOCH).unwrap(), + close_requirement + ); + nodes[1].node.ffor_persistence_completed(old_token).unwrap(); + assert!(!nodes[1] + .node + .release_ffor_receiver_close(&id, &peer, EPOCH, |_| panic!("unpersisted close")) + .unwrap()); + persist(&nodes[1]); + let mut close_wire = Vec::new(); + assert!(nodes[1] + .node + .release_ffor_receiver_close(&id, &peer, EPOCH, |wire| { + close_wire = wire.to_vec(); + Ok(()) + }) + .unwrap()); + assert!(nodes[1] + .node + .release_ffor_receiver_close(&id, &peer, EPOCH, |wire| { + assert_eq!(wire, close_wire); + Ok(()) + }) + .unwrap()); + let ack = signed( + &nodes[0], + &nodes[1], + id, + Payload::CloseAck(CloseAck { + activation_hash: hash, + num_slots: 2, + settled: vec![u8::from(settled)], + preimages: if settled { + vec![Preimage { slot: 1, value: preimage.0 }] + } else { + Vec::new() + }, + preimages_tlv_present: true, + }), + ); + let requirement = + nodes[1].node.accept_ffor_receiver_close_ack(&id, &peer, EPOCH, &ack).unwrap(); + assert_eq!( + nodes[1].node.accept_ffor_receiver_close_ack(&id, &peer, EPOCH, &ack).unwrap(), + requirement + ); + assert_eq!( + nodes[1] + .node + .accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &activation_ack) + .unwrap(), + requirement + ); + assert!(!nodes[1].node.release_ffor_receiver_drain(&id, &peer, EPOCH).unwrap()); + assert_no_htlc_wire(&nodes[1]); + assert!(nodes[1] + .node + .prepare_ffor_receiver_closed(&id, &peer, EPOCH, &old_monitor) + .is_err()); + persist(&nodes[1]); + if delay_monitor { + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + } + assert!(nodes[1].node.release_ffor_receiver_drain(&id, &peer, EPOCH).unwrap()); + if delay_monitor { + assert_no_htlc_wire(&nodes[1]); + let update = get_monitor!(nodes[1], id).get_latest_update_id(); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + nodes[1].chain_monitor.chain_monitor.channel_monitor_updated(id, update).unwrap(); + } + if restart_mid_round { + // Persist after signing removals, then lose that entire outbound commitment flight. + let flight = nodes[1].node.get_and_clear_pending_msg_events(); + assert!(flight + .iter() + .any(|event| matches!(event, MessageSendEvent::UpdateHTLCs { .. }))); + let incomplete = get_monitor!(nodes[1], id).ffor_commitment_snapshot().unwrap(); + assert!(nodes[1] + .node + .prepare_ffor_receiver_closed(&id, &peer, EPOCH, &incomplete) + .is_err()); + let manager = persist(&nodes[1]); + let monitor = get_monitor!(nodes[1], id).encode(); + nodes[1].chain_monitor.added_monitors.lock().unwrap().clear(); + nodes[0].node.peer_disconnected(receiver_id); + reload_node!( + nodes[1], + config.clone(), + &manager, + &[&monitor], + mid_persister, + mid_monitor, + mid_manager + ); + assert!(!nodes[1].node.release_ffor_receiver_drain(&id, &peer, EPOCH).unwrap()); + persist(&nodes[1]); + connect_nodes(&nodes[0], &nodes[1]); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.release_ffor_receiver_drain(&id, &peer, EPOCH).unwrap()); + // The settlement test node supplies its authenticated connection's retained epoch report. + for event in nodes[0].node.get_and_clear_pending_msg_events() { + match event { + MessageSendEvent::SendChannelReestablish { mut msg, .. } => { + msg.ffor_reestablish = + Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: EPOCH, + activation_hash: hash, + state: ReportedState::Active, + })); + nodes[1].node.handle_channel_reestablish(peer, &msg); + }, + MessageSendEvent::SendChannelUpdate { .. } + | MessageSendEvent::BroadcastChannelUpdate { .. } => {}, + other => panic!("unexpected reconnect wire {other:?}"), + } + } + assert!(nodes[1] + .node + .release_ffor_receiver_close(&id, &peer, EPOCH, |wire| { + assert_eq!(wire, close_wire); + Ok(()) + }) + .unwrap()); + nodes[1].node.accept_ffor_receiver_close_ack(&id, &peer, EPOCH, &ack).unwrap(); + assert!(nodes[1].node.release_ffor_receiver_drain(&id, &peer, EPOCH).is_err()); + for event in nodes[1].node.get_and_clear_pending_msg_events() { + match event { + MessageSendEvent::SendChannelReestablish { msg, .. } => { + nodes[0].node.handle_channel_reestablish(receiver_id, &msg) + }, + MessageSendEvent::SendChannelUpdate { .. } + | MessageSendEvent::BroadcastChannelUpdate { .. } => {}, + other => panic!("stock replay before close reconciliation {other:?}"), + } + } + // Exact ACK authorizes no forgotten stock replay on this connection. Reconnect first. + nodes[0].node.peer_disconnected(receiver_id); + nodes[1].node.peer_disconnected(peer); + assert!(nodes[1].node.release_ffor_receiver_drain(&id, &peer, EPOCH).unwrap()); + connect_nodes(&nodes[0], &nodes[1]); + for event in nodes[0].node.get_and_clear_pending_msg_events() { + match event { + MessageSendEvent::SendChannelReestablish { mut msg, .. } => { + msg.ffor_reestablish = + Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: EPOCH, + activation_hash: hash, + state: ReportedState::Draining, + })); + nodes[1].node.handle_channel_reestablish(peer, &msg); + }, + MessageSendEvent::SendChannelUpdate { .. } + | MessageSendEvent::BroadcastChannelUpdate { .. } => {}, + other => panic!("unexpected fresh reconnect wire {other:?}"), + } + } + } + let (fulfilled, failed) = drain(&nodes[0], &nodes[1]); + assert_eq!( + fulfilled, + if settled || learned { vec![vouchers[0].htlc_id] } else { Vec::new() } + ); + assert_eq!( + failed, + vouchers + .iter() + .skip(usize::from(settled || learned)) + .map(|v| v.htlc_id) + .collect::>() + ); + nodes[0].node.get_and_clear_pending_events(); + assert!(nodes[1].node.list_usable_channels().is_empty()); + assert!(nodes[1] + .node + .prepare_ffor_receiver_closed(&id, &peer, EPOCH, &old_monitor) + .is_err()); + let snapshot = get_monitor!(nodes[1], id).ffor_commitment_snapshot().unwrap(); + let closed = + nodes[1].node.prepare_ffor_receiver_closed(&id, &peer, EPOCH, &snapshot).unwrap(); + assert_eq!( + nodes[1] + .node + .accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &activation_ack) + .unwrap(), + closed + ); + assert!(!nodes[1].node.is_ffor_state_persisted(&closed)); + assert!(!nodes[1].node.release_ffor_receiver_closed(&id, &peer, EPOCH).unwrap()); + persist(&nodes[1]); + nodes[0].node.peer_disconnected(receiver_id); + nodes[1].node.peer_disconnected(peer); + connect_nodes(&nodes[0], &nodes[1]); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.release_ffor_receiver_closed(&id, &peer, EPOCH).unwrap()); + assert_eq!( + nodes[1] + .node + .accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &activation_ack) + .unwrap(), + closed + ); + assert_eq!(drain(&nodes[0], &nodes[1]), (Vec::new(), Vec::new())); + assert_eq!(nodes[1].node.list_usable_channels().len(), 1); + let manager = persist(&nodes[1]); + let monitor = get_monitor!(nodes[1], id).encode(); + nodes[0].node.peer_disconnected(receiver_id); + reload_node!( + nodes[1], + config, + &manager, + &[&monitor], + persister, + chain_monitor, + reloaded + ); + persist(&nodes[1]); + nodes[1] + .node + .accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &activation_ack) + .unwrap(); + connect_nodes(&nodes[0], &nodes[1]); + assert_eq!(drain(&nodes[0], &nodes[1]), (Vec::new(), Vec::new())); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + } + } +} + +#[test] +fn ffor_close_manager_force_close_retains_draining_archive_and_monitor_preimage() { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor, reloaded); + let config = anchor_config(); + let managers = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let mut nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let peer = nodes[0].node.get_our_node_id(); + let (vouchers, preimage) = park_two(&nodes[0], &nodes[1], id); + let (hash, _) = activate(&nodes[0], &nodes[1], id); + let activation_update = get_monitor!(nodes[1], id).get_latest_update_id(); + nodes[1].node.prepare_ffor_receiver_close(&id, &peer, EPOCH).unwrap(); + persist(&nodes[1]); + assert!(nodes[1].node.release_ffor_receiver_close(&id, &peer, EPOCH, |_| Ok(())).unwrap()); + let ack = signed( + &nodes[0], + &nodes[1], + id, + Payload::CloseAck(CloseAck { + activation_hash: hash, + num_slots: 2, + settled: vec![1], + preimages: vec![Preimage { slot: 1, value: preimage.0 }], + preimages_tlv_present: true, + }), + ); + nodes[1].node.accept_ffor_receiver_close_ack(&id, &peer, EPOCH, &ack).unwrap(); + persist(&nodes[1]); + assert!(nodes[1].node.release_ffor_receiver_drain(&id, &peer, EPOCH).unwrap()); + // Lose the removal flight. Both stock monitors still own their unresolved commitments. + let flight = nodes[1].node.get_and_clear_pending_msg_events(); + assert!(flight.iter().any(|event| matches!(event, MessageSendEvent::UpdateHTLCs { .. }))); + assert!(get_monitor!(nodes[1], id).get_latest_update_id() > activation_update); + assert_eq!( + get_monitor!(nodes[1], id).get_stored_preimages()[&vouchers[0].payment_hash].0, + preimage + ); + let archive = nodes[1].node.ffor_recovery.lock().unwrap().encode(); + nodes[1] + .node + .force_close_broadcasting_latest_txn(&id, &peer, "draining recovery test".into()) + .unwrap(); + assert!(nodes[1].node.list_channels().is_empty()); + assert!(nodes[1] + .node + .get_and_clear_pending_events() + .iter() + .any(|event| matches!(event, Event::ChannelClosed { .. }))); + nodes[1].node.get_and_clear_pending_msg_events(); + nodes[1].chain_monitor.added_monitors.lock().unwrap().clear(); + let manager = persist(&nodes[1]); + let monitor = get_monitor!(nodes[1], id).encode(); + let without_monitor = <(BlockHash, TestChannelManager)>::read( + &mut &manager[..], + ChannelManagerReadArgs::new( + nodes[1].keys_manager, + nodes[1].keys_manager, + nodes[1].keys_manager, + nodes[1].fee_estimator, + nodes[1].chain_monitor, + nodes[1].tx_broadcaster, + nodes[1].router, + nodes[1].message_router, + nodes[1].logger, + config.clone(), + vec![], + ), + ); + assert!(without_monitor.is_err()); + reload_node!(nodes[1], config, &manager, &[&monitor], persister, chain_monitor, reloaded); + assert!(nodes[1].node.list_channels().is_empty()); + assert_eq!(nodes[1].node.ffor_recovery.lock().unwrap().encode(), archive); + assert_eq!( + get_monitor!(nodes[1], id).get_stored_preimages()[&vouchers[0].payment_hash].0, + preimage + ); + let key = FFORRecoveryKey { channel_id: id, epoch_id: EPOCH }; + let recovery = nodes[1].node.ffor_recovery.lock().unwrap(); + let close = recovery.get_activation(&key).unwrap().close_record().unwrap(); + assert_eq!(close.acknowledgement_wire().unwrap(), ack.as_slice()); + assert!(!close.is_closed()); + } +} + +#[test] +fn ffor_close_manager_refuses_replay_during_conflicting_reconnect() { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let peer = nodes[0].node.get_our_node_id(); + let receiver = nodes[1].node.get_our_node_id(); + park_two(&nodes[0], &nodes[1], id); + let (hash, _) = activate(&nodes[0], &nodes[1], id); + nodes[1].node.prepare_ffor_receiver_close(&id, &peer, EPOCH).unwrap(); + persist(&nodes[1]); + for conflicting in [true, false] { + nodes[0].node.peer_disconnected(receiver); + nodes[1].node.peer_disconnected(peer); + connect_nodes(&nodes[0], &nodes[1]); + let local = get_event_msg!(&nodes[1], MessageSendEvent::SendChannelReestablish, peer); + let mut remote = + get_event_msg!(&nodes[0], MessageSendEvent::SendChannelReestablish, receiver); + let mut reported_hash = hash; + if conflicting { + reported_hash[0] ^= 1; + } + remote.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: EPOCH, + activation_hash: reported_hash, + state: ReportedState::Active, + })); + nodes[0].node.handle_channel_reestablish(receiver, &local); + nodes[1].node.handle_channel_reestablish(peer, &remote); + if conflicting { + { + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let peer_state = peers.get(&peer).unwrap().lock().unwrap(); + let channel = peer_state.channel_by_id.get(&id).unwrap().as_funded().unwrap(); + assert!(channel.context.is_connected()); + assert!(matches!( + channel.ffor_receiver_reconnect_outcome(), + Some(FFORReestablishOutcome::ResolutionRequired { .. }) + )); + } + assert!(nodes[1] + .node + .release_ffor_receiver_close(&id, &peer, EPOCH, |_| panic!( + "conflicting replay" + )) + .is_err()); + } else { + assert!(nodes[1] + .node + .release_ffor_receiver_close(&id, &peer, EPOCH, |_| Ok(())) + .unwrap()); + } + assert_no_htlc_wire(&nodes[0]); + assert_no_htlc_wire(&nodes[1]); + } + } +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs b/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs index ae4cd8f85b5..5770982a184 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs @@ -25,7 +25,7 @@ fn sign(message: &mut FFORMessage, node: &Node) { } /// Reserve two actual outgoing payment hashes before delivering either voucher to the receiver. -fn park_two(sender: &Node, receiver: &Node, id: ChannelId) -> ([FFORVoucher; 2], PaymentPreimage) { +pub(super) fn park_two(sender: &Node, receiver: &Node, id: ChannelId) -> ([FFORVoucher; 2], PaymentPreimage) { let preimage = PaymentPreimage([*receiver.network_payment_count.as_ref().borrow(); 32]); let (first, voucher, _) = offer_voucher(sender, receiver, 2_000_000); let (mut route, hash, _, secret) = get_route_and_payment_hash!(sender, receiver, 2_000_000); @@ -112,7 +112,7 @@ fn assert_no_update_wire(node: &Node) { } /// Complete native commitment and revoke rounds, recording the actual terminal wire for every HTLC. -fn drain<'a, 'b, 'c>( +pub(super) fn drain<'a, 'b, 'c>( sender: &Node<'a, 'b, 'c>, receiver: &Node<'a, 'b, 'c>, ) -> (Vec, Vec) { let mut fulfilled = Vec::new(); diff --git a/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs b/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs index c71eac3656d..bb2ad7ce6ec 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs @@ -98,14 +98,21 @@ where } let recovery = self.ffor_recovery.lock().unwrap(); recovery - .validate_channel_outcome( + .validate_channel_lifecycle( &setup, channel.ffor_receiver_fence(), channel.ffor_receiver_abort_reason(), + channel.ffor_receiver_drain_binding(), + channel.ffor_receiver_closed_completion_hash(), + channel.ffor_receiver_drain_activation_hash(), ) .map_err(|_| ())?; let activation = recovery.get_activation(&key).ok_or(())?; + if activation.is_draining() && !activation.is_closed() { + channel.validate_ffor_drain().map_err(|_| ())?; + } if channel.context.is_ffor_frozen() + && !activation.is_draining() && channel.ffor_frozen_commitments(&self.logger).map_err(|_| ())? != activation.commitments() { @@ -116,7 +123,32 @@ where if !self.ffor_persistence.lock().unwrap().is_complete(&entry.requirement) { return Ok(None); } - let (state, activation_hash) = if activation.is_aborted() { + let (state, activation_hash) = if activation.is_closed() { + // The peer may resume ordinary updates as soon as it sees Closed. Persistence + // alone does not grant that permission to this manager instance after restore. + if channel.ffor_receiver_fence().is_some() + || channel.ffor_receiver_drain_binding().map(|(_, _, closed)| closed) != Some(true) + { + return Ok(None); + } + (ReportedState::Closed, activation.activation_hash(&setup).map_err(|_| ())?) + } else if activation.is_draining() { + // A saved peer commitment may follow this report immediately. Retain the + // entire queue until the durable release path enables stock removal rounds. + let hash = activation.activation_hash(&setup).map_err(|_| ())?; + // An exact Active report instead enters control-message-only recovery. The + // channel permits retained close replay and requires a fresh reconnect before + // any stock round can resume, even after receiving the signed close reply. + let close_replay = matches!(channel.ffor_receiver_reconnect_outcome(), + Some(FFORReestablishOutcome::CloseReplayRequired { peer_report }) + if peer_report.epoch_id == key.epoch_id + && peer_report.activation_hash == hash + && peer_report.state == ReportedState::Active); + if !channel.ffor_receiver_drain_enabled() && !close_replay { + return Ok(None); + } + (ReportedState::Draining, hash) + } else if activation.is_aborted() { (ReportedState::Aborted, [0; 32]) } else if activation.is_active() { (ReportedState::Active, activation.activation_hash(&setup).map_err(|_| ())?) diff --git a/lightning/src/ln/ffor_recovery.rs b/lightning/src/ln/ffor_recovery.rs index ff610a4b938..01d4570d7aa 100644 --- a/lightning/src/ln/ffor_recovery.rs +++ b/lightning/src/ln/ffor_recovery.rs @@ -5,7 +5,7 @@ //! a borrowed insertion permit so capacity failure cannot leave a channel without its evidence. mod activation; -pub(crate) use activation::FFORReceiverActivation; +pub(crate) use activation::{FFORReceiverActivation, FFORReceiverCloseRecord}; use alloc::vec::Vec; use bitcoin::constants::ChainHash; @@ -23,18 +23,21 @@ const SETUP_ONLY_VERSION: u8 = 0; const ACTIVATION_VERSION: u8 = 1; // Earlier activation readers do not understand a retained abort or its relaxed monitor binding. const ABORTED_ACTIVATION_VERSION: u8 = 2; +const CLOSE_VERSION: u8 = 3; const MAX_RECORDS: usize = 64; const MAX_ENCODED_BYTES: usize = 8 * 1024 * 1024; // Two maximum wire messages, a 483-slot canonical book, and fixed admission fields fit here. const MAX_SETUP_RECORD_BYTES: usize = 192 * 1024; -// Four maximum signed messages, the book, destination script and admission context fit here. -const MAX_RECORD_BYTES: usize = 384 * 1024; +// Six maximum signed messages, the book, destination and completed drain proof fit here. +const MAX_RECORD_BYTES: usize = 512 * 1024; const HEADER_BYTES: usize = 3; const RECORD_LENGTH_BYTES: usize = 4; // Includes the maximum acknowledgement plus its vector/TLV framing and growth of the enclosing // activation and record length prefixes. The fixed outer record prefix does not grow. const ACK_RESERVATION_BYTES: usize = lightning_ffor::wire::MAX_MESSAGE_LEN + 64; const ABORT_RESERVATION_BYTES: usize = lightning_ffor::reestablish::VALUE_LEN + 64; +const CLOSED_RESERVATION_BYTES: usize = 512; +const CLOSE_RESERVATION_BYTES: usize = 2 * ACK_RESERVATION_BYTES + CLOSED_RESERVATION_BYTES; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct FFORRecoveryKey { @@ -75,12 +78,19 @@ impl Entry { return Err(FFORRecoveryError::InvalidRecord); } if let Some(activation) = record.activation.as_ref() { - activation.validate(&authenticated).map_err(|_| FFORRecoveryError::InvalidRecord)?; + let hash = activation + .validate(&authenticated) + .map_err(|_| FFORRecoveryError::InvalidRecord)?; + if let Some(close) = activation.close_record() { + close + .validate(&record.setup, hash) + .map_err(|_| FFORRecoveryError::InvalidRecord)?; + } } let header = authenticated.header(); let encoded_bytes = record.serialized_length(); - let reserved_ack_bytes = Self::ack_reservation(&record); - if encoded_bytes.saturating_add(reserved_ack_bytes) > MAX_RECORD_BYTES + let reserved_transition_bytes = Self::transition_reservation(&record); + if encoded_bytes.saturating_add(reserved_transition_bytes) > MAX_RECORD_BYTES || (record.activation.is_none() && encoded_bytes > MAX_SETUP_RECORD_BYTES) { return Err(FFORRecoveryError::CapacityExceeded); @@ -101,18 +111,28 @@ impl Entry { && self.record.setup.funding_txo() == other.record.setup.funding_txo()) } - fn ack_reservation(record: &StoredSetup) -> usize { - if record.activation.as_ref().map_or(false, |activation| { - !activation.is_active() && activation.aborted_reason().is_none() - }) { - core::cmp::max(ACK_RESERVATION_BYTES, ABORT_RESERVATION_BYTES) - } else { - 0 - } + fn transition_reservation(record: &StoredSetup) -> usize { + let activation = match &record.activation { + Some(activation) if !activation.is_aborted() => activation, + _ => return 0, + }; + let activation_ack = if activation.is_active() { 0 } else { ACK_RESERVATION_BYTES }; + let close = match activation.close_record() { + None => CLOSE_RESERVATION_BYTES, + Some(close) if close.acknowledgement_wire().is_none() => { + ACK_RESERVATION_BYTES + CLOSED_RESERVATION_BYTES + }, + Some(close) if !close.is_closed() => CLOSED_RESERVATION_BYTES, + Some(_) => 0, + }; + core::cmp::max( + activation_ack + close, + if activation.is_active() { 0 } else { ABORT_RESERVATION_BYTES }, + ) } - fn reserved_ack_bytes(&self) -> usize { - Self::ack_reservation(&self.record) + fn reserved_transition_bytes(&self) -> usize { + Self::transition_reservation(&self.record) } } @@ -120,7 +140,7 @@ pub(crate) struct FFORRecoveryRegistry { entries: Vec, encoded_bytes: usize, // Derived from authenticated phases, never serialized or supplied by a caller. - reserved_ack_bytes: usize, + reserved_transition_bytes: usize, } /// Exclusive capacity reservation. Dropping it changes nothing; commit cannot fail. @@ -133,7 +153,7 @@ impl FFORRecoveryInsertion<'_> { pub(crate) fn commit(self) { if let Some(entry) = self.entry { self.registry.encoded_bytes += RECORD_LENGTH_BYTES + entry.encoded_bytes; - self.registry.reserved_ack_bytes += entry.reserved_ack_bytes(); + self.registry.reserved_transition_bytes += entry.reserved_transition_bytes(); self.registry.entries.push(entry); } } @@ -145,20 +165,20 @@ pub(crate) struct FFORRecoveryUpgrade<'a> { index: usize, entry: Entry, total_bytes: usize, - reserved_ack_bytes: usize, + reserved_transition_bytes: usize, } impl FFORRecoveryUpgrade<'_> { pub(crate) fn commit(self) { self.registry.entries[self.index] = self.entry; self.registry.encoded_bytes = self.total_bytes; - self.registry.reserved_ack_bytes = self.reserved_ack_bytes; + self.registry.reserved_transition_bytes = self.reserved_transition_bytes; } } impl FFORRecoveryRegistry { pub(crate) fn new() -> Self { - Self { entries: Vec::new(), encoded_bytes: HEADER_BYTES, reserved_ack_bytes: 0 } + Self { entries: Vec::new(), encoded_bytes: HEADER_BYTES, reserved_transition_bytes: 0 } } pub(crate) fn is_empty(&self) -> bool { @@ -200,6 +220,15 @@ impl FFORRecoveryRegistry { pub(crate) fn validate_channel_outcome( &self, setup: &FFORReceiverSetup, fence: Option<(FFORReceiverFencePhase, [u8; 32])>, abort_reason: Option, + ) -> Result<(), DecodeError> { + self.validate_channel_lifecycle(setup, fence, abort_reason, None, None, None) + } + + /// Check the channel-owned close binding against exact authenticated archived evidence. + pub(crate) fn validate_channel_lifecycle( + &self, setup: &FFORReceiverSetup, fence: Option<(FFORReceiverFencePhase, [u8; 32])>, + abort_reason: Option, drain: Option<([u8; 32], Vec, bool)>, + completion_hash: Option<[u8; 32]>, drain_activation_hash: Option<[u8; 32]>, ) -> Result<(), DecodeError> { if !self.contains_exact(setup) { return Err(DecodeError::InvalidValue); @@ -208,6 +237,31 @@ impl FFORRecoveryRegistry { let header = authenticated.header(); let key = FFORRecoveryKey { channel_id: ChannelId(header.channel_id), epoch_id: header.epoch_id }; + if let Some(record) = self.get_activation(&key) { + if record.is_draining() { + let close = record.close_record().ok_or(DecodeError::InvalidValue)?; + let (ack_hash, settled, closed) = drain.ok_or(DecodeError::InvalidValue)?; + let hash = record.validate(&authenticated)?; + if abort_reason.is_some() + || Some(ack_hash) != close.acknowledgement_hash() + || settled != close.settled()? + || completion_hash != close.completion_hash() + || drain_activation_hash != Some(hash) + { + return Err(DecodeError::InvalidValue); + } + let valid = if record.is_closed() { + (fence == Some((FFORReceiverFencePhase::ClosedPendingPersistence, hash)) + && !closed) || (fence.is_none() && closed) + } else { + fence == Some((FFORReceiverFencePhase::Draining, hash)) && !closed + }; + return if valid { Ok(()) } else { Err(DecodeError::InvalidValue) }; + } + } + if drain.is_some() || completion_hash.is_some() || drain_activation_hash.is_some() { + return Err(DecodeError::InvalidValue); + } match (self.get_activation(&key), fence) { (None, None) => Ok(()), (Some(record), fence) if record.aborted_reason().is_some() => { @@ -254,11 +308,9 @@ impl FFORRecoveryRegistry { self.entries .iter() .filter(|entry| { - entry - .record - .activation - .as_ref() - .map_or(false, |activation| activation.aborted_reason().is_none()) + entry.record.activation.as_ref().map_or(false, |activation| { + activation.aborted_reason().is_none() && !activation.is_closed() + }) }) .map(|entry| entry.key.channel_id) .collect() @@ -310,7 +362,7 @@ impl FFORRecoveryRegistry { } return Ok(FFORRecoveryInsertion { registry: self, entry: None }); } - self.check_capacity(entry.encoded_bytes, entry.reserved_ack_bytes())?; + self.check_capacity(entry.encoded_bytes, entry.reserved_transition_bytes())?; Ok(FFORRecoveryInsertion { registry: self, entry: Some(entry) }) } @@ -345,22 +397,36 @@ impl FFORRecoveryRegistry { .checked_sub(existing.encoded_bytes) .and_then(|bytes| bytes.checked_add(entry.encoded_bytes)) .ok_or(FFORRecoveryError::CapacityExceeded)?; - let reserved_ack_bytes = self - .reserved_ack_bytes - .checked_sub(existing.reserved_ack_bytes()) - .and_then(|bytes| bytes.checked_add(entry.reserved_ack_bytes())) + let reserved_transition_bytes = self + .reserved_transition_bytes + .checked_sub(existing.reserved_transition_bytes()) + .and_then(|bytes| bytes.checked_add(entry.reserved_transition_bytes())) .ok_or(FFORRecoveryError::CapacityExceeded)?; if total_bytes - .checked_add(reserved_ack_bytes) + .checked_add(reserved_transition_bytes) .map_or(true, |bytes| bytes > MAX_ENCODED_BYTES) { return Err(FFORRecoveryError::CapacityExceeded); } - Ok(FFORRecoveryUpgrade { registry: self, index, entry, total_bytes, reserved_ack_bytes }) + Ok(FFORRecoveryUpgrade { + registry: self, + index, + entry, + total_bytes, + reserved_transition_bytes, + }) } fn version(&self) -> u8 { if self.entries.iter().any(|entry| { + entry + .record + .activation + .as_ref() + .map_or(false, |activation| activation.close_record().is_some()) + }) { + CLOSE_VERSION + } else if self.entries.iter().any(|entry| { entry .record .activation @@ -376,14 +442,14 @@ impl FFORRecoveryRegistry { } fn check_capacity( - &self, record_bytes: usize, reserved_ack_bytes: usize, + &self, record_bytes: usize, reserved_transition_bytes: usize, ) -> Result<(), FFORRecoveryError> { let total = self .encoded_bytes .checked_add(RECORD_LENGTH_BYTES) .and_then(|total| total.checked_add(record_bytes)) - .and_then(|total| total.checked_add(self.reserved_ack_bytes)) - .and_then(|total| total.checked_add(reserved_ack_bytes)); + .and_then(|total| total.checked_add(self.reserved_transition_bytes)) + .and_then(|total| total.checked_add(reserved_transition_bytes)); if self.entries.len() >= MAX_RECORDS || record_bytes > MAX_RECORD_BYTES || total.map_or(true, |total| total > MAX_ENCODED_BYTES) @@ -412,6 +478,7 @@ impl Readable for FFORRecoveryRegistry { if version != SETUP_ONLY_VERSION && version != ACTIVATION_VERSION && version != ABORTED_ACTIVATION_VERSION + && version != CLOSE_VERSION { return Err(DecodeError::UnknownRequiredFeature); } @@ -440,10 +507,10 @@ impl Readable for FFORRecoveryRegistry { return Err(DecodeError::InvalidValue); } registry - .check_capacity(length, entry.reserved_ack_bytes()) + .check_capacity(length, entry.reserved_transition_bytes()) .map_err(|_| DecodeError::InvalidValue)?; registry.encoded_bytes += RECORD_LENGTH_BYTES + length; - registry.reserved_ack_bytes += entry.reserved_ack_bytes(); + registry.reserved_transition_bytes += entry.reserved_transition_bytes(); registry.entries.push(entry); } if registry.version() != version { diff --git a/lightning/src/ln/ffor_recovery/activation.rs b/lightning/src/ln/ffor_recovery/activation.rs index ddfb637e7ee..f688e2e532a 100644 --- a/lightning/src/ln/ffor_recovery/activation.rs +++ b/lightning/src/ln/ffor_recovery/activation.rs @@ -4,6 +4,9 @@ //! manager must install the matching channel fence and registry upgrade atomically, then await //! persistence before releasing wire. No production activation entry point exists yet. +mod close; +pub(crate) use close::FFORReceiverCloseRecord; + use alloc::vec::Vec; use bitcoin::hashes::Hash; use bitcoin::{ScriptBuf, Txid}; @@ -27,6 +30,7 @@ pub(crate) struct FFORReceiverActivation { activate_wire: Vec, ack_wire: Option>, abort: Option, + close: Option, receiver_number: u64, receiver_txid: Txid, settlement_number: u64, @@ -47,6 +51,7 @@ impl_writeable_tlv_based!(FFORReceiverActivation, { (14, preparation_height, required), (16, destination_script, required), (18, abort, option), + (20, close, option), }); /// A manager-observed reconnect outcome, not a signed statement from the peer. The transition @@ -105,6 +110,7 @@ impl FFORReceiverActivation { activate_wire: activate_wire.to_vec(), ack_wire: None, abort: None, + close: None, receiver_number: commitments.holder.number, receiver_txid: commitments.holder.txid, settlement_number: commitments.counterparty.number, @@ -147,6 +153,9 @@ impl FFORReceiverActivation { let hash = setup .validate_activation(&activate, self.commitment_hash(), self.preparation_height) .map_err(|_| DecodeError::InvalidValue)?; + if self.close.is_some() && (self.ack_wire.is_none() || self.abort.is_some()) { + return Err(DecodeError::InvalidValue); + } if let Some(abort) = self.abort.as_ref() { if self.ack_wire.is_some() { return Err(DecodeError::InvalidValue); @@ -157,6 +166,14 @@ impl FFORReceiverActivation { let ack = Message::decode(wire).map_err(|_| DecodeError::InvalidValue)?; setup.validate_activation_ack(&ack, hash).map_err(|_| DecodeError::InvalidValue)?; } + if let Some(close) = &self.close { + close.validate_authenticated(setup, hash)?; + close.validate_closed_after( + self.receiver_number, + self.settlement_number, + self.monitor_update_id, + )?; + } Ok(hash) } @@ -255,7 +272,25 @@ impl FFORReceiverActivation { pub(super) fn validate_monitor( &self, setup: &FFORReceiverSetup, monitor: &FFORMonitorRecoveryIdentity, ) -> Result<(), DecodeError> { + if self.is_draining() && !self.is_closed() { + if monitor.channel_id.0 != setup.validate_recovery()?.header().channel_id + || monitor.funding_txo != setup.funding_txo() + || monitor.update_id < self.monitor_update_id + || monitor.destination_script != self.destination_script + || monitor.counterparty_txid.is_none() + || INITIAL_COMMITMENT_NUMBER + .checked_sub(monitor.holder_number) + .map_or(true, |number| number < self.receiver_number) + || INITIAL_COMMITMENT_NUMBER + .checked_sub(monitor.counterparty_number) + .map_or(true, |number| number < self.settlement_number) + { + return Err(DecodeError::InvalidValue); + } + return Ok(()); + } if self.abort.is_some() + || self.is_closed() || monitor.channel_id.0 != setup.validate_recovery()?.header().channel_id || monitor.funding_txo != setup.funding_txo() || monitor.update_id < self.monitor_update_id @@ -272,22 +307,37 @@ impl FFORReceiverActivation { Ok(()) } - /// The only upgrade adds the first acknowledgement or terminal abort, preserving every byte - /// of the activation. Neither terminal outcome can be replaced by another transition. + /// Every retained transcript byte is immutable. Only the next lifecycle evidence may be added. pub(super) fn can_replace(&self, next: &Self) -> bool { if self.encode() == next.encode() { return true; } - if self.ack_wire.is_some() - || self.abort.is_some() - || (next.ack_wire.is_none() && next.abort.is_none()) - { + if self.abort.is_some() { return false; } - let mut without_outcome = next.clone(); - without_outcome.ack_wire = None; - without_outcome.abort = None; - self.encode() == without_outcome.encode() + if self.ack_wire.is_none() && next.close.is_some() { + return false; + } + if self.close.is_none() && next.is_draining() { + return false; + } + if self.ack_wire.as_ref().map_or(false, |old| next.ack_wire.as_ref() != Some(old)) { + return false; + } + if let Some(close) = &self.close { + if !next.close.as_ref().map_or(false, |new| close.can_replace(new)) { + return false; + } + } + let mut before = self.clone(); + let mut after = next.clone(); + before.ack_wire = None; + before.abort = None; + before.close = None; + after.ack_wire = None; + after.abort = None; + after.close = None; + before.encode() == after.encode() } } diff --git a/lightning/src/ln/ffor_recovery/activation/close.rs b/lightning/src/ln/ffor_recovery/activation/close.rs new file mode 100644 index 00000000000..61391a1e505 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/close.rs @@ -0,0 +1,283 @@ +//! Exact cooperative close evidence. Channel authority and durability remain with the manager. + +use super::*; +use crate::chain::transaction::OutPoint; +use crate::ln::channel::{ffor_drain_completion_digest, FFORReceiverDrainCompletion}; +use crate::ln::types::ChannelId; +use crate::types::payment::PaymentPreimage; +use bitcoin::hashes::sha256; + +/// Immutable signed close intent and its first exact authenticated settlement response. +/// A record alone never permits a voucher removal or establishes that a monitor is durable. +#[derive(Clone)] +pub(crate) struct FFORReceiverCloseRecord { + close_wire: Vec, + ack_wire: Option>, + closed: Option, +} + +impl_writeable_tlv_based!(FFORReceiverCloseRecord, { + (0, close_wire, required_vec), + (2, ack_wire, option), + (4, closed, option), +}); + +/// Retained historical result of the engine's opaque, fully synchronized drain proof. +#[derive(Clone)] +struct ClosedDrain { + completion_hash: [u8; 32], + receiver_number: u64, + receiver_txid: Txid, + settlement_number: u64, + settlement_txid: Txid, + monitor_update_id: u64, + funding_txo: OutPoint, +} + +impl_writeable_tlv_based!(ClosedDrain, { + (0, completion_hash, required), + (2, receiver_number, required), + (4, receiver_txid, required), + (6, settlement_number, required), + (8, settlement_txid, required), + (10, monitor_update_id, required), + (12, funding_txo, required), +}); + +impl FFORReceiverCloseRecord { + fn new(setup: &FFORReceiverSetup, hash: [u8; 32], wire: &[u8]) -> Result { + if wire.len() > MAX_MESSAGE_LEN { + return Err(DecodeError::InvalidValue); + } + let record = Self { close_wire: wire.to_vec(), ack_wire: None, closed: None }; + record.validate(setup, hash)?; + Ok(record) + } + + /// Authenticate both retained messages against the actual setup and activation digest. + pub(crate) fn validate( + &self, setup: &FFORReceiverSetup, hash: [u8; 32], + ) -> Result<(), DecodeError> { + if self.close_wire.len() > MAX_MESSAGE_LEN + || self.ack_wire.as_ref().map_or(false, |wire| wire.len() > MAX_MESSAGE_LEN) + { + return Err(DecodeError::InvalidValue); + } + if self.closed.as_ref().map_or(false, |closed| closed.funding_txo != setup.funding_txo()) { + return Err(DecodeError::InvalidValue); + } + self.validate_authenticated(&setup.validate_recovery()?, hash) + } + + pub(super) fn validate_authenticated( + &self, setup: &AuthenticatedSetup, hash: [u8; 32], + ) -> Result<(), DecodeError> { + if self.close_wire.len() > MAX_MESSAGE_LEN + || self.ack_wire.as_ref().map_or(false, |wire| wire.len() > MAX_MESSAGE_LEN) + || (self.closed.is_some() && self.ack_wire.is_none()) + { + return Err(DecodeError::InvalidValue); + } + let close = Message::decode(&self.close_wire).map_err(|_| DecodeError::InvalidValue)?; + setup.validate_close(&close, hash).map_err(|_| DecodeError::InvalidValue)?; + if let Some(bytes) = &self.ack_wire { + let ack = Message::decode(bytes).map_err(|_| DecodeError::InvalidValue)?; + setup.validate_close_ack(&ack, hash).map_err(|_| DecodeError::InvalidValue)?; + } + if let Some(closed) = &self.closed { + let header = setup.header(); + let commitments = FFORVoucherCommitments { + holder: FFORCommitment { + number: closed.receiver_number, + txid: closed.receiver_txid, + }, + counterparty: FFORCommitment { + number: closed.settlement_number, + txid: closed.settlement_txid, + }, + }; + let digest = ffor_drain_completion_digest( + header.epoch_id, + hash, + self.acknowledgement_hash().ok_or(DecodeError::InvalidValue)?, + ChannelId(header.channel_id), + closed.funding_txo, + commitments, + closed.monitor_update_id, + ); + if digest != closed.completion_hash { + return Err(DecodeError::InvalidValue); + } + if closed.receiver_number == 0 + || closed.receiver_number > INITIAL_COMMITMENT_NUMBER + || closed.settlement_number == 0 + || closed.settlement_number > INITIAL_COMMITMENT_NUMBER + || closed.monitor_update_id == 0 + || closed.monitor_update_id == u64::MAX + { + return Err(DecodeError::InvalidValue); + } + } + Ok(()) + } + + fn with_ack( + &self, setup: &FFORReceiverSetup, hash: [u8; 32], wire: &[u8], + ) -> Result { + if wire.len() > MAX_MESSAGE_LEN || self.ack_wire.as_ref().map_or(false, |old| old != wire) { + return Err(DecodeError::InvalidValue); + } + let mut next = self.clone(); + next.ack_wire = Some(wire.to_vec()); + next.validate(setup, hash)?; + Ok(next) + } + + pub(crate) fn close_wire(&self) -> &[u8] { + &self.close_wire + } + + pub(crate) fn acknowledgement_wire(&self) -> Option<&[u8]> { + self.ack_wire.as_deref() + } + + pub(crate) fn acknowledgement_hash(&self) -> Option<[u8; 32]> { + self.ack_wire.as_ref().map(|wire| sha256::Hash::hash(wire).to_byte_array()) + } + + pub(crate) fn is_closed(&self) -> bool { + self.closed.is_some() + } + + pub(crate) fn completion_hash(&self) -> Option<[u8; 32]> { + self.closed.as_ref().map(|closed| closed.completion_hash) + } + + /// Returns the signed bitmap only after the caller has authenticated this immutable record. + pub(crate) fn settled(&self) -> Result, DecodeError> { + match self.ack_payload()? { + Payload::CloseAck(ack) => Ok(ack.settled), + _ => Err(DecodeError::InvalidValue), + } + } + + /// Every returned preimage still requires stock monitor persistence before claim wire. + pub(crate) fn preimages(&self) -> Result, DecodeError> { + match self.ack_payload()? { + Payload::CloseAck(ack) => { + Ok(ack.preimages.into_iter().map(|p| (p.slot, PaymentPreimage(p.value))).collect()) + }, + _ => Err(DecodeError::InvalidValue), + } + } + + fn ack_payload(&self) -> Result { + let wire = self.ack_wire.as_ref().ok_or(DecodeError::InvalidValue)?; + Ok(Message::decode(wire).map_err(|_| DecodeError::InvalidValue)?.payload) + } + + pub(super) fn validate_closed_after( + &self, receiver: u64, settlement: u64, update: u64, + ) -> Result<(), DecodeError> { + if self.closed.as_ref().map_or(false, |closed| { + closed.receiver_number <= receiver + || closed.settlement_number <= settlement + || closed.monitor_update_id <= update + }) { + return Err(DecodeError::InvalidValue); + } + Ok(()) + } + + pub(super) fn can_replace(&self, next: &Self) -> bool { + self.close_wire == next.close_wire + && self.ack_wire.as_ref().map_or(true, |old| next.ack_wire.as_ref() == Some(old)) + && !(self.ack_wire.is_none() && next.closed.is_some()) + && self.closed.as_ref().map_or(true, |old| { + next.closed.as_ref().map(|new| new.encode()) == Some(old.encode()) + }) + } +} + +impl FFORReceiverActivation { + /// Retain close intent only after signed activation. An exact retry never replaces bytes. + pub(crate) fn with_close( + &self, setup: &FFORReceiverSetup, wire: &[u8], + ) -> Result { + if !self.is_active() || self.is_aborted() { + return Err(DecodeError::InvalidValue); + } + let hash = self.activation_hash(setup)?; + if let Some(existing) = &self.close { + return if existing.close_wire == wire { + Ok(self.clone()) + } else { + Err(DecodeError::InvalidValue) + }; + } + let mut next = self.clone(); + next.close = Some(FFORReceiverCloseRecord::new(setup, hash, wire)?); + Ok(next) + } + + pub(crate) fn with_close_ack( + &self, setup: &FFORReceiverSetup, wire: &[u8], + ) -> Result { + let close = self.close.as_ref().ok_or(DecodeError::InvalidValue)?; + let hash = self.activation_hash(setup)?; + let mut next = self.clone(); + next.close = Some(close.with_ack(setup, hash, wire)?); + Ok(next) + } + + pub(crate) fn close_record(&self) -> Option<&FFORReceiverCloseRecord> { + self.close.as_ref() + } + + pub(crate) fn is_draining(&self) -> bool { + self.close.as_ref().map_or(false, |close| close.ack_wire.is_some()) + } + + pub(crate) fn is_closed(&self) -> bool { + self.close.as_ref().map_or(false, |close| close.is_closed()) + } + + /// Only the channel can construct completion proof, after comparing its empty commitment + /// pair with a fresh persisted monitor snapshot. The manager must retain both mutations. + pub(crate) fn with_closed( + &self, setup: &FFORReceiverSetup, completion: &FFORReceiverDrainCompletion, + ) -> Result { + let hash = self.activation_hash(setup)?; + let close = self.close.as_ref().ok_or(DecodeError::InvalidValue)?; + let (channel_id, funding_txo, monitor_update_id) = completion.monitor_identity(); + let commitments = completion.commitments(); + let header = setup.validate_recovery()?.header(); + if completion.epoch_id() != header.epoch_id + || completion.activation_hash() != hash + || Some(completion.acknowledgement_hash()) != close.acknowledgement_hash() + || channel_id.0 != header.channel_id + || funding_txo != setup.funding_txo() + || commitments.holder.number <= self.receiver_number + || commitments.counterparty.number <= self.settlement_number + || monitor_update_id <= self.monitor_update_id + { + return Err(DecodeError::InvalidValue); + } + let mut next = self.clone(); + let closed = ClosedDrain { + completion_hash: completion.completion_hash(), + receiver_number: commitments.holder.number, + receiver_txid: commitments.holder.txid, + settlement_number: commitments.counterparty.number, + settlement_txid: commitments.counterparty.txid, + monitor_update_id, + funding_txo, + }; + if close.closed.as_ref().map_or(false, |old| old.encode() != closed.encode()) { + return Err(DecodeError::InvalidValue); + } + next.close.as_mut().unwrap().closed = Some(closed); + next.validate(&setup.validate_recovery()?)?; + Ok(next) + } +} diff --git a/lightning/src/ln/ffor_recovery/activation/tests.rs b/lightning/src/ln/ffor_recovery/activation/tests.rs index 91659a82d6c..439dacb4ddc 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests.rs @@ -2,13 +2,15 @@ use super::*; use crate::ln::ffor_recovery::tests::{fixture, sign}; use crate::ln::ffor_recovery::{ FFORRecoveryError, FFORRecoveryKey, FFORRecoveryRegistry, ACK_RESERVATION_BYTES, - ACTIVATION_VERSION, MAX_ENCODED_BYTES, MAX_RECORDS, SETUP_ONLY_VERSION, + ACTIVATION_VERSION, CLOSE_RESERVATION_BYTES, MAX_ENCODED_BYTES, MAX_RECORDS, + SETUP_ONLY_VERSION, }; use crate::ln::types::ChannelId; use crate::util::ser::Readable; use lightning_ffor::wire::{Activate, Tlv}; mod aborted; +mod close; // Synthetic transaction identities exercise archive authentication only. Real monitor and // channel evidence are exercised by the manager tests; these values never activate a channel. @@ -52,6 +54,7 @@ fn evidence_for_identity( activate_wire: message.encode().unwrap(), ack_wire: None, abort: None, + close: None, receiver_number: 4, receiver_txid, settlement_number: 4, @@ -110,9 +113,10 @@ fn ffor_activation_archive_roundtrip_preserves_exact_maximum_transcripts() { assert_eq!(bytes.len(), registry.encoded_bytes); let restored = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); assert_eq!(restored.encode(), bytes); - let reservation = if expected.is_active() { 0 } else { ACK_RESERVATION_BYTES }; - assert_eq!(registry.reserved_ack_bytes, reservation); - assert_eq!(restored.reserved_ack_bytes, reservation); + let reservation = + CLOSE_RESERVATION_BYTES + if expected.is_active() { 0 } else { ACK_RESERVATION_BYTES }; + assert_eq!(registry.reserved_transition_bytes, reservation); + assert_eq!(restored.reserved_transition_bytes, reservation); assert_eq!(restored.get_activation(&key(&setup)).unwrap().encode(), expected.encode()); assert!(restored.contains_exact(&setup)); } @@ -231,26 +235,26 @@ fn ffor_activation_archive_upgrade_is_atomic_idempotent_and_capacity_bounded() { )); drop(registry.prepare_activation(&setup, &activating).unwrap()); assert_eq!(registry.encode(), original); - assert_eq!(registry.reserved_ack_bytes, 0); - registry.reserved_ack_bytes = MAX_ENCODED_BYTES - registry.encoded_bytes; + assert_eq!(registry.reserved_transition_bytes, 0); + registry.reserved_transition_bytes = MAX_ENCODED_BYTES - registry.encoded_bytes; assert!(matches!( registry.prepare_activation(&setup, &activating), Err(FFORRecoveryError::CapacityExceeded) )); assert_eq!(registry.encode(), original); - assert_eq!(registry.reserved_ack_bytes, MAX_ENCODED_BYTES - registry.encoded_bytes); - registry.reserved_ack_bytes = 0; + assert_eq!(registry.reserved_transition_bytes, MAX_ENCODED_BYTES - registry.encoded_bytes); + registry.reserved_transition_bytes = 0; registry.prepare_activation(&setup, &activating).unwrap().commit(); let pending = registry.encode(); registry.prepare_activation(&setup, &activating).unwrap().commit(); assert_eq!(registry.encode(), pending); - assert_eq!(registry.reserved_ack_bytes, ACK_RESERVATION_BYTES); + assert_eq!(registry.reserved_transition_bytes, ACK_RESERVATION_BYTES + CLOSE_RESERVATION_BYTES); drop(registry.prepare_activation(&setup, &active).unwrap()); assert_eq!(registry.encode(), pending); - assert_eq!(registry.reserved_ack_bytes, ACK_RESERVATION_BYTES); + assert_eq!(registry.reserved_transition_bytes, ACK_RESERVATION_BYTES + CLOSE_RESERVATION_BYTES); registry.prepare_activation(&setup, &active).unwrap().commit(); let completed = registry.encode(); - assert_eq!(registry.reserved_ack_bytes, 0); + assert_eq!(registry.reserved_transition_bytes, CLOSE_RESERVATION_BYTES); assert!(completed.len() - pending.len() < ACK_RESERVATION_BYTES); registry.prepare_activation(&setup, &active).unwrap().commit(); assert_eq!(registry.encode(), completed); @@ -259,7 +263,7 @@ fn ffor_activation_archive_upgrade_is_atomic_idempotent_and_capacity_bounded() { Err(FFORRecoveryError::ConflictingRecord) )); assert_eq!(registry.encode(), completed); - assert_eq!(registry.reserved_ack_bytes, 0); + assert_eq!(registry.reserved_transition_bytes, CLOSE_RESERVATION_BYTES); } // Fill with real authenticated records until a further activation cannot reserve its ack. Large @@ -305,7 +309,7 @@ fn ffor_activation_archive_reserves_maximum_ack_through_competing_admission_and_ fill_ack_capacity(&mut registry); assert!(registry.entries.len() < MAX_RECORDS); let before = registry.encode(); - let reserved = registry.reserved_ack_bytes; + let reserved = registry.reserved_transition_bytes; assert!(matches!( registry.prepare_activation(&competing_setup, &competing_activation), Err(FFORRecoveryError::CapacityExceeded) @@ -318,23 +322,23 @@ fn ffor_activation_archive_reserves_maximum_ack_through_competing_admission_and_ Err(FFORRecoveryError::CapacityExceeded) )); assert_eq!(registry.encode(), before); - assert_eq!(registry.reserved_ack_bytes, reserved); + assert_eq!(registry.reserved_transition_bytes, reserved); let mut restored = FFORRecoveryRegistry::read(&mut &before[..]).unwrap(); - assert_eq!(restored.reserved_ack_bytes, reserved); + assert_eq!(restored.reserved_transition_bytes, reserved); assert_eq!(restored.encoded_bytes, before.len()); assert_eq!(restored.encode(), before); restored.prepare_activation(&setup, &activating).unwrap().commit(); - assert_eq!(restored.reserved_ack_bytes, reserved); + assert_eq!(restored.reserved_transition_bytes, reserved); let maximum_ack = acknowledgement(&setup, &activating, true); assert_eq!(maximum_ack.len(), MAX_MESSAGE_LEN); let active = activating.with_ack(&setup, &maximum_ack).unwrap(); drop(restored.prepare_activation(&setup, &active).unwrap()); assert_eq!(restored.encode(), before); - assert_eq!(restored.reserved_ack_bytes, reserved); + assert_eq!(restored.reserved_transition_bytes, reserved); restored.prepare_activation(&setup, &active).unwrap().commit(); - assert_eq!(restored.reserved_ack_bytes, reserved - ACK_RESERVATION_BYTES); + assert_eq!(restored.reserved_transition_bytes, reserved - ACK_RESERVATION_BYTES); assert!(restored.encoded_bytes - before.len() < ACK_RESERVATION_BYTES); - assert!(restored.encoded_bytes + restored.reserved_ack_bytes <= MAX_ENCODED_BYTES); + assert!(restored.encoded_bytes + restored.reserved_transition_bytes <= MAX_ENCODED_BYTES); assert_eq!( restored.get_activation(&key(&setup)).unwrap().ack_wire.as_ref(), Some(&maximum_ack) @@ -343,7 +347,7 @@ fn ffor_activation_archive_reserves_maximum_ack_through_competing_admission_and_ restored.prepare_activation(&setup, &active).unwrap().commit(); assert_eq!(restored.encode(), completed); let roundtrip = FFORRecoveryRegistry::read(&mut &completed[..]).unwrap(); - assert_eq!(roundtrip.reserved_ack_bytes, restored.reserved_ack_bytes); + assert_eq!(roundtrip.reserved_transition_bytes, restored.reserved_transition_bytes); // A snapshot cannot bypass the reservation by declaring only its smaller current wire size. let (extra_setup, extra_activation) = evidence_for_identity(91, true); @@ -356,9 +360,9 @@ fn ffor_activation_archive_reserves_maximum_ack_through_competing_admission_and_ assert!(restored.encoded_bytes + extra.encoded_bytes + 4 < MAX_ENCODED_BYTES); assert!( restored.encoded_bytes - + restored.reserved_ack_bytes + + restored.reserved_transition_bytes + extra.encoded_bytes - + extra.reserved_ack_bytes() + + extra.reserved_transition_bytes() + 4 > MAX_ENCODED_BYTES ); restored.entries.push(extra); diff --git a/lightning/src/ln/ffor_recovery/activation/tests/aborted.rs b/lightning/src/ln/ffor_recovery/activation/tests/aborted.rs index cd787e08452..99f4d0a87cc 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests/aborted.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests/aborted.rs @@ -98,7 +98,7 @@ fn ffor_activation_abort_is_monotonic_atomic_and_permanent() { let before = registry.encode(); drop(registry.prepare_activation(&setup, &aborted).unwrap()); assert_eq!(registry.encode(), before); - assert_eq!(registry.reserved_ack_bytes, ACK_RESERVATION_BYTES); + assert_eq!(registry.reserved_transition_bytes, ACK_RESERVATION_BYTES + CLOSE_RESERVATION_BYTES); registry.prepare_activation(&setup, &aborted).unwrap().commit(); let terminal = registry.encode(); let different = activating @@ -113,7 +113,7 @@ fn ffor_activation_abort_is_monotonic_atomic_and_permanent() { } registry.prepare_activation(&setup, &aborted).unwrap().commit(); assert_eq!(registry.encode(), terminal); - assert_eq!(registry.reserved_ack_bytes, 0); + assert_eq!(registry.reserved_transition_bytes, 0); let mut active_registry = registered(&setup); active_registry.prepare_activation(&setup, &activating).unwrap().commit(); active_registry.prepare_activation(&setup, &active).unwrap().commit(); @@ -133,7 +133,7 @@ fn ffor_activation_abort_restore_requires_terminal_schema_and_matching_channel_r let restored = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); assert_eq!(restored.encode(), bytes); assert_eq!(restored.encoded_bytes, bytes.len()); - assert_eq!(restored.reserved_ack_bytes, 0); + assert_eq!(restored.reserved_transition_bytes, 0); assert_eq!(restored.activation_keys(), vec![key(&setup)]); assert!(restored.activation_channels().is_empty()); assert!(restored.get_activation(&key(&setup)).unwrap().matches_abort_report(peer_report)); @@ -220,16 +220,22 @@ fn ffor_activation_abort_has_reserved_capacity_after_restart_and_competing_admis fill_ack_capacity(&mut registry); let before = registry.encode(); let mut restored = FFORRecoveryRegistry::read(&mut &before[..]).unwrap(); - let reserved = restored.reserved_ack_bytes; + let reserved = restored.reserved_transition_bytes; let aborted = activating .abort_after_reestablish(&setup, Some(report(&setup, ReportedState::Aborted))) .unwrap(); restored.prepare_activation(&setup, &aborted).unwrap().commit(); - assert_eq!(restored.reserved_ack_bytes, reserved - ACK_RESERVATION_BYTES); + assert_eq!( + restored.reserved_transition_bytes, + reserved - ACK_RESERVATION_BYTES - CLOSE_RESERVATION_BYTES + ); assert!(restored.encoded_bytes - before.len() < ABORT_RESERVATION_BYTES); - assert!(restored.encoded_bytes + restored.reserved_ack_bytes <= MAX_ENCODED_BYTES); + assert!(restored.encoded_bytes + restored.reserved_transition_bytes <= MAX_ENCODED_BYTES); let terminal = restored.encode(); let restored = FFORRecoveryRegistry::read(&mut &terminal[..]).unwrap(); - assert_eq!(restored.reserved_ack_bytes, reserved - ACK_RESERVATION_BYTES); + assert_eq!( + restored.reserved_transition_bytes, + reserved - ACK_RESERVATION_BYTES - CLOSE_RESERVATION_BYTES + ); assert_eq!(restored.encode(), terminal); } diff --git a/lightning/src/ln/ffor_recovery/activation/tests/close.rs b/lightning/src/ln/ffor_recovery/activation/tests/close.rs new file mode 100644 index 00000000000..0213640b102 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/tests/close.rs @@ -0,0 +1,513 @@ +use super::*; +use crate::chain::transaction::OutPoint; +use crate::io::Read; +use crate::ln::ffor_recovery::{ + CLOSED_RESERVATION_BYTES, CLOSE_RESERVATION_BYTES, CLOSE_VERSION, MAX_RECORD_BYTES, +}; +use crate::util::ser::BigSize; +use bitcoin::hashes::sha256; +use lightning_ffor::wire::{CloseAck, Preimage}; + +fn signed(mut message: Message, signer: u8, maximum: bool) -> Vec { + sign(&mut message, signer); + if maximum { + message.extensions.clear(); + let padding = MAX_MESSAGE_LEN - message.encode().unwrap().len() - 4; + message.extensions.push(Tlv { kind: 101, value: vec![19; padding] }); + sign(&mut message, signer); + assert_eq!(message.encode().unwrap().len(), MAX_MESSAGE_LEN); + } + message.encode().unwrap() +} + +fn close_wire( + setup: &FFORReceiverSetup, record: &FFORReceiverActivation, maximum: bool, +) -> Vec { + signed( + Message { + header: setup.validate_recovery().unwrap().header(), + payload: Payload::Close(record.activation_hash(setup).unwrap()), + extensions: vec![], + signature: [0; 64], + }, + 41, + maximum, + ) +} + +fn close_ack( + setup: &FFORReceiverSetup, record: &FFORReceiverActivation, maximum: bool, + preimages: Vec, +) -> Vec { + let authenticated = setup.validate_recovery().unwrap(); + let count = authenticated.vouchers().len(); + let mut settled = vec![0; (count + 7) / 8]; + for preimage in &preimages { + let bit = usize::from(preimage.slot - 1); + settled[bit / 8] |= 1 << (bit % 8); + } + signed( + Message { + header: authenticated.header(), + payload: Payload::CloseAck(CloseAck { + activation_hash: record.activation_hash(setup).unwrap(), + num_slots: count as u16, + settled, + preimages, + preimages_tlv_present: true, + }), + extensions: vec![], + signature: [0; 64], + }, + 42, + maximum, + ) +} + +fn phases(maximum: bool) -> (FFORReceiverSetup, Vec) { + let (setup, activating) = evidence(maximum); + let active = + activating.with_ack(&setup, &acknowledgement(&setup, &activating, maximum)).unwrap(); + let closing = active.with_close(&setup, &close_wire(&setup, &active, maximum)).unwrap(); + let draining = + closing.with_close_ack(&setup, &close_ack(&setup, &closing, maximum, vec![])).unwrap(); + let closed = historical_closed(&setup, &draining, &completion(&setup, &draining)); + (setup, vec![activating, active, closing, draining, closed]) +} + +// This storage fixture exercises historical framing only. It cannot construct the channel's +// opaque completion proof or authorize any runtime transition. Real channel tests cover that. +#[derive(Clone)] +struct HistoricalCompletion { + completion_hash: [u8; 32], + receiver_number: u64, + receiver_txid: Txid, + settlement_number: u64, + settlement_txid: Txid, + monitor_update_id: u64, + funding_txo: OutPoint, +} +impl_writeable_tlv_based!(HistoricalCompletion, { + (0, completion_hash, required), (2, receiver_number, required), + (4, receiver_txid, required), (6, settlement_number, required), + (8, settlement_txid, required), (10, monitor_update_id, required), (12, funding_txo, required), +}); + +struct HistoricalClose { + close_wire: Vec, + ack_wire: Option>, + closed: Option, +} +impl_writeable_tlv_based!(HistoricalClose, { + (0, close_wire, required_vec), (2, ack_wire, option), (4, closed, option), +}); + +fn completion( + setup: &FFORReceiverSetup, draining: &FFORReceiverActivation, +) -> HistoricalCompletion { + let mut completed = HistoricalCompletion { + completion_hash: [0; 32], + receiver_number: 5, + receiver_txid: Txid::from_byte_array([7; 32]), + settlement_number: 5, + settlement_txid: Txid::from_byte_array([8; 32]), + monitor_update_id: 6, + funding_txo: setup.funding_txo(), + }; + let mut bytes = b"ffor/native-drain-complete/v1".to_vec(); + bytes.extend_from_slice(&key(setup).epoch_id); + bytes.extend_from_slice(&draining.activation_hash(setup).unwrap()); + bytes.extend_from_slice(&draining.close_record().unwrap().acknowledgement_hash().unwrap()); + bytes.extend_from_slice(&key(setup).channel_id.0); + bytes.extend_from_slice(&setup.funding_txo().encode()); + bytes.extend_from_slice(&completed.monitor_update_id.to_be_bytes()); + for (number, txid) in [ + (completed.receiver_number, completed.receiver_txid), + (completed.settlement_number, completed.settlement_txid), + ] { + bytes.extend_from_slice(&number.to_be_bytes()); + bytes.extend_from_slice(txid.as_byte_array()); + } + completed.completion_hash = sha256::Hash::hash(&bytes).to_byte_array(); + completed +} + +fn historical_closed( + _setup: &FFORReceiverSetup, record: &FFORReceiverActivation, completed: &HistoricalCompletion, +) -> FFORReceiverActivation { + let close = record.close_record().unwrap(); + let stored = HistoricalClose { + close_wire: close.close_wire().to_vec(), + ack_wire: close.acknowledgement_wire().map(|bytes| bytes.to_vec()), + closed: Some(completed.clone()), + }; + let mut next = record.clone(); + next.close = Some(FFORReceiverCloseRecord::read(&mut &stored.encode()[..]).unwrap()); + next +} + +fn roundtrip(registry: &FFORRecoveryRegistry) -> FFORRecoveryRegistry { + let bytes = registry.encode(); + assert_eq!(bytes.len(), registry.encoded_bytes); + let restored = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); + assert_eq!(restored.encode(), bytes); + assert_eq!(restored.reserved_transition_bytes, registry.reserved_transition_bytes); + restored +} + +#[test] +fn ffor_activation_close_maximum_transcripts_preserve_history_and_reservations() { + let (setup, phases) = phases(true); + let mut registry = registered(&setup); + let reservations = [ + ACK_RESERVATION_BYTES + CLOSE_RESERVATION_BYTES, + CLOSE_RESERVATION_BYTES, + ACK_RESERVATION_BYTES + CLOSED_RESERVATION_BYTES, + CLOSED_RESERVATION_BYTES, + 0, + ]; + for (index, phase) in phases.iter().enumerate() { + registry.prepare_activation(&setup, phase).unwrap().commit(); + assert_eq!(registry.reserved_transition_bytes, reservations[index]); + assert!(registry.entries[0].encoded_bytes + reservations[index] <= MAX_RECORD_BYTES); + assert!(registry.contains_exact(&setup)); + let retained = registry.get_activation(&key(&setup)).unwrap(); + assert_eq!(retained.activate_wire(), phases[0].activate_wire()); + assert_eq!(retained.commitments(), phases[0].commitments()); + if index >= 1 { + assert_eq!(retained.ack_wire(), phases[1].ack_wire()); + } + if index >= 2 { + assert_eq!(registry.encode()[0], CLOSE_VERSION); + let close = retained.close_record().unwrap(); + assert_eq!(close.close_wire().len(), MAX_MESSAGE_LEN); + assert_eq!(close.close_wire(), phases[2].close_record().unwrap().close_wire()); + } + if index >= 3 { + let close = retained.close_record().unwrap(); + assert_eq!(close.acknowledgement_wire().unwrap().len(), MAX_MESSAGE_LEN); + assert_eq!(close.settled().unwrap(), vec![0; 61]); + assert!(close.preimages().unwrap().is_empty()); + } + registry = roundtrip(®istry); + } + assert!(registry.get_activation(&key(&setup)).unwrap().is_closed()); +} + +#[test] +fn ffor_activation_close_registry_refuses_skipped_steps_reversal_and_mutating_retries() { + let (setup, phases) = phases(false); + for current in 0..phases.len() { + let mut registry = registered(&setup); + for phase in &phases[..=current] { + registry.prepare_activation(&setup, phase).unwrap().commit(); + } + let before = registry.encode(); + let reserved = registry.reserved_transition_bytes; + for next in 0..phases.len() { + let result = registry.prepare_activation(&setup, &phases[next]); + if next == current || next == current + 1 { + drop(result.unwrap()); + } else { + assert!( + matches!(result, Err(FFORRecoveryError::ConflictingRecord)), + "{} -> {}", + current, + next + ); + } + assert_eq!(registry.encode(), before); + assert_eq!(registry.reserved_transition_bytes, reserved); + } + registry.prepare_activation(&setup, &phases[current]).unwrap().commit(); + assert_eq!(registry.encode(), before); + } + let mut registry = registered(&setup); + for phase in &phases[1..] { + assert!(matches!( + registry.prepare_activation(&setup, phase), + Err(FFORRecoveryError::ConflictingRecord) + )); + } + let close = phases[2].close_record().unwrap().close_wire(); + assert!(phases[0].with_close(&setup, close).is_err()); + let aborted = phases[0].abort_after_reestablish(&setup, None).unwrap(); + assert!(aborted.with_close(&setup, close).is_err()); + assert!(phases[1] + .with_close_ack(&setup, phases[3].close_record().unwrap().acknowledgement_wire().unwrap()) + .is_err()); + for phase in &phases[2..] { + assert!(phase.abort_after_reestablish(&setup, None).is_err()); + assert_eq!(phase.with_close(&setup, close).unwrap().encode(), phase.encode()); + } +} + +#[test] +fn ffor_activation_close_authenticates_receiver_header_hash_and_exact_bytes() { + let (setup, phases) = phases(false); + let original = phases[2].close_record().unwrap().close_wire(); + for change in 0..6 { + let mut message = Message::decode(original).unwrap(); + match change { + 0 => message.header.channel_id[0] ^= 1, + 1 => message.header.epoch_id[0] ^= 1, + 2 => { + if let Payload::Close(hash) = &mut message.payload { + hash[0] ^= 1; + } + }, + 3 => message.payload = Payload::ActivateAck(phases[1].activation_hash(&setup).unwrap()), + _ => {}, + } + let mut bytes = signed(message, if change == 4 { 42 } else { 41 }, false); + if change == 5 { + let last = bytes.len() - 1; + bytes[last] ^= 1; + } + assert!(phases[1].with_close(&setup, &bytes).is_err(), "change {}", change); + } + let mut alternate = Message::decode(original).unwrap(); + alternate.extensions.push(Tlv { kind: 101, value: vec![3] }); + let alternate = signed(alternate, 41, false); + assert!(phases[1].with_close(&setup, &alternate).is_ok()); + assert!(phases[2].with_close(&setup, &alternate).is_err()); + assert!(phases[1].with_close(&setup, &vec![0; MAX_MESSAGE_LEN + 1]).is_err()); +} + +fn replace_setup_wire( + setup: &FFORReceiverSetup, init: Vec, accept: Vec, +) -> FFORReceiverSetup { + let encoded = setup.encode(); + let mut reader = &encoded[..]; + let length = BigSize::read(&mut reader).unwrap().0 as usize; + assert_eq!(reader.len(), length); + let mut body = Vec::new(); + while !reader.is_empty() { + let kind = BigSize::read(&mut reader).unwrap(); + let len = BigSize::read(&mut reader).unwrap().0 as usize; + let mut value = vec![0; len]; + reader.read_exact(&mut value).unwrap(); + let replacement = match kind.0 { + 0 => &init, + 2 => &accept, + _ => &value, + }; + kind.write(&mut body).unwrap(); + BigSize(replacement.len() as u64).write(&mut body).unwrap(); + body.extend_from_slice(replacement); + } + let mut bytes = Vec::new(); + BigSize(body.len() as u64).write(&mut bytes).unwrap(); + bytes.extend_from_slice(&body); + FFORReceiverSetup::read(&mut &bytes[..]).unwrap() +} + +fn payable(chain: bool) -> (FFORReceiverSetup, FFORReceiverActivation, Vec) { + let (setup, mut record) = evidence(false); + let authenticated = setup.validate_recovery().unwrap(); + let values = if chain { + [sha256::Hash::hash(&[18; 32]).to_byte_array(), [18; 32]] + } else { + [[17; 32], [18; 32]] + }; + let mut init = authenticated.init().clone(); + if let Payload::Init(terms) = &mut init.payload { + terms.hash_chain = chain; + } + let init_wire = signed(init, 41, false); + let mut accept = authenticated.accept().clone(); + if let Payload::Accept(terms) = &mut accept.payload { + terms.init_hash = transcript::init_hash(&init_wire); + terms.payment_hashes = + values.iter().map(|p| sha256::Hash::hash(p).to_byte_array()).collect(); + } + let setup = replace_setup_wire(&setup, init_wire, signed(accept, 42, false)); + let authenticated = setup.validate_recovery().unwrap(); + let mut activate = Message::decode(record.activate_wire()).unwrap(); + if let Payload::Activate(message) = &mut activate.payload { + message.setup_hash = authenticated.setup_hash(); + message.book_hash = authenticated.book_hash(); + } + record.activate_wire = signed(activate, 41, false); + let active = record.with_ack(&setup, &acknowledgement(&setup, &record, false)).unwrap(); + let closing = active.with_close(&setup, &close_wire(&setup, &active, false)).unwrap(); + ( + setup, + closing, + values + .iter() + .enumerate() + .map(|(i, value)| Preimage { slot: i as u16 + 1, value: *value }) + .collect(), + ) +} + +#[test] +fn ffor_activation_close_ack_checks_settlement_role_book_preimages_and_chain_prefix() { + for chain in [false, true] { + let (setup, closing, preimages) = payable(chain); + let wire = close_ack(&setup, &closing, false, preimages.clone()); + let draining = closing.with_close_ack(&setup, &wire).unwrap(); + assert_eq!(draining.close_record().unwrap().settled().unwrap(), vec![3]); + let retained = draining.close_record().unwrap().preimages().unwrap(); + assert_eq!(retained.len(), 2); + for (index, (slot, preimage)) in retained.iter().enumerate() { + assert_eq!(*slot, index as u16 + 1); + assert_eq!(preimage.0, preimages[index].value); + } + for change in 0..7 { + let mut message = Message::decode(&wire).unwrap(); + match change { + 0 => message.header.channel_id[0] ^= 1, + 1 => message.header.epoch_id[0] ^= 1, + 2 => { + if let Payload::CloseAck(ack) = &mut message.payload { + ack.activation_hash[0] ^= 1; + } + }, + 3 => { + if let Payload::CloseAck(ack) = &mut message.payload { + ack.num_slots = 3; + } + }, + 4 => { + if let Payload::CloseAck(ack) = &mut message.payload { + ack.preimages[0].value[0] ^= 1; + } + }, + 5 => message.payload = Payload::Close(closing.activation_hash(&setup).unwrap()), + _ => {}, + } + assert!( + closing + .with_close_ack( + &setup, + &signed(message, if change == 6 { 41 } else { 42 }, false) + ) + .is_err(), + "change {}", + change + ); + } + let later_only = close_ack(&setup, &closing, false, vec![preimages[1].clone()]); + assert_eq!(closing.with_close_ack(&setup, &later_only).is_err(), chain); + assert!(closing + .with_close_ack(&setup, &close_ack(&setup, &closing, false, vec![preimages[0].clone()])) + .is_ok()); + assert_eq!(draining.with_close_ack(&setup, &wire).unwrap().encode(), draining.encode()); + let mut alternate = Message::decode(&wire).unwrap(); + alternate.extensions.push(Tlv { kind: 101, value: vec![8] }); + let alternate = signed(alternate, 42, false); + assert!(closing.with_close_ack(&setup, &alternate).is_ok()); + assert!(draining.with_close_ack(&setup, &alternate).is_err()); + assert!(closing.with_close_ack(&setup, &vec![0; MAX_MESSAGE_LEN + 1]).is_err()); + } +} + +#[test] +fn ffor_activation_close_preserves_distinct_empty_ack_tlv_and_terminal_history() { + let (setup, phases) = phases(false); + let wire = phases[3].close_record().unwrap().acknowledgement_wire().unwrap(); + let mut absent = Message::decode(wire).unwrap(); + if let Payload::CloseAck(ack) = &mut absent.payload { + ack.preimages_tlv_present = false; + } + let absent = signed(absent, 42, false); + assert_ne!(&absent[..], wire); + let alternate = phases[2].with_close_ack(&setup, &absent).unwrap(); + assert!(alternate.validate(&setup.validate_recovery().unwrap()).is_ok()); + assert!(phases[3].with_close_ack(&setup, &absent).is_err()); + assert!(phases[4].with_close_ack(&setup, &absent).is_err()); + assert_eq!(phases[4].with_close_ack(&setup, wire).unwrap().encode(), phases[4].encode()); + let mut changed = completion(&setup, &phases[3]); + changed.receiver_txid = Txid::from_byte_array([9; 32]); + assert!(!phases[4].can_replace(&historical_closed(&setup, &phases[3], &changed))); +} + +#[test] +fn ffor_activation_close_restore_rejects_downgrades_missing_steps_and_altered_completion() { + let (setup, phases) = phases(false); + let mut registry = registered(&setup); + for phase in &phases { + registry.prepare_activation(&setup, phase).unwrap().commit(); + } + let original = registry.encode(); + for version in 0..CLOSE_VERSION { + let mut bytes = original.clone(); + bytes[0] = version; + assert!(FFORRecoveryRegistry::read(&mut &bytes[..]).is_err()); + } + for change in 0..11 { + let mut closed = phases[4].clone(); + if change == 0 { + closed.ack_wire = None; + } else if change == 1 { + let stored = HistoricalClose { + close_wire: vec![], + ack_wire: None, + closed: Some(completion(&setup, &phases[3])), + }; + closed.close = Some(FFORReceiverCloseRecord::read(&mut &stored.encode()[..]).unwrap()); + } else { + let mut completed = completion(&setup, &phases[3]); + match change { + 2 => completed.receiver_number = 0, + 3 => completed.receiver_number = 4, + 4 => completed.settlement_number = INITIAL_COMMITMENT_NUMBER + 1, + 5 => completed.monitor_update_id = u64::MAX, + 6 => completed.monitor_update_id = 5, + 7 => completed.receiver_txid = Txid::from_byte_array([23; 32]), + 8 => completed.settlement_txid = Txid::from_byte_array([23; 32]), + 9 => completed.monitor_update_id = 7, + _ => completed.completion_hash[0] ^= 1, + } + closed = historical_closed(&setup, &phases[3], &completed); + } + assert!(closed.validate(&setup.validate_recovery().unwrap()).is_err(), "change {}", change); + registry.entries[0].record.activation = Some(closed); + registry.entries[0].encoded_bytes = registry.entries[0].record.serialized_length(); + assert!( + FFORRecoveryRegistry::read(&mut ®istry.encode()[..]).is_err(), + "stored change {}", + change + ); + } +} + +#[test] +fn ffor_activation_close_reserved_capacity_survives_competing_admission_and_each_restart() { + let (setup, phases) = phases(true); + let mut registry = registered(&setup); + registry.prepare_activation(&setup, &phases[0]).unwrap().commit(); + fill_ack_capacity(&mut registry); + let original = registry.encode(); + let reserved = registry.reserved_transition_bytes; + assert!(matches!( + registry.prepare_insert(&fixture(90, true).record()), + Err(FFORRecoveryError::CapacityExceeded) + )); + assert_eq!(registry.encode(), original); + assert_eq!(registry.reserved_transition_bytes, reserved); + let reservations = [ + CLOSE_RESERVATION_BYTES, + ACK_RESERVATION_BYTES + CLOSED_RESERVATION_BYTES, + CLOSED_RESERVATION_BYTES, + 0, + ]; + let mut previous_charge = registry.encoded_bytes + registry.reserved_transition_bytes; + for (index, phase) in phases[1..].iter().enumerate() { + registry = roundtrip(®istry); + let before = registry.encode(); + let before_reserved = registry.reserved_transition_bytes; + drop(registry.prepare_activation(&setup, phase).unwrap()); + assert_eq!(registry.encode(), before); + assert_eq!(registry.reserved_transition_bytes, before_reserved); + registry.prepare_activation(&setup, phase).unwrap().commit(); + assert_eq!(registry.entries[0].reserved_transition_bytes(), reservations[index]); + let charged = registry.encoded_bytes + registry.reserved_transition_bytes; + assert!(charged <= previous_charge); + assert!(charged <= MAX_ENCODED_BYTES); + previous_charge = charged; + } + assert!(roundtrip(®istry).get_activation(&key(&setup)).unwrap().is_closed()); +} diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs index 7ecb43ca3f6..43775a1bb16 100644 --- a/lightning/src/ln/ffor_recovery/tests.rs +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -166,7 +166,7 @@ fn ffor_recovery_roundtrip_retains_exact_signed_setup_and_native_identity() { assert_eq!(restored.encode(), encoded); assert!(restored.contains_exact(&record)); assert_eq!(restored.encoded_bytes, encoded.len()); - assert_eq!(restored.reserved_ack_bytes, 0); + assert_eq!(restored.reserved_transition_bytes, 0); assert!(restored.validate_identity(record.receiver(), record.chain_hash()).is_ok()); assert_eq!( restored.validate_identity(key(43), record.chain_hash()), @@ -293,7 +293,7 @@ fn ffor_recovery_reader_refuses_future_schema_missing_fields_and_truncation() { assert!(decode(&framed[..length]).is_err(), "accepted prefix {}", length); } let mut future = framed.clone(); - future[0] = ABORTED_ACTIVATION_VERSION + 1; + future[0] = CLOSE_VERSION + 1; assert!(matches!(decode(&future), Err(DecodeError::UnknownRequiredFeature))); // Empty TLV records omit both mandatory fields. assert!(decode(&frame(&[vec![0]])).is_err()); From 49e4f367c7385338524448716250f5dbc1a69c1d Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 23:40:26 -0400 Subject: [PATCH 12/22] feat(ffor): decrypt authenticated witness records --- FFOR.md | 97 ++++++++-- .../data/generate_native_witness_fixtures.py | 39 ++++ lightning/src/ln/ffor.rs | 3 + lightning/src/ln/ffor/witness.rs | 118 +++++++++++ lightning/src/ln/ffor/witness/fixtures.txt | 35 ++++ lightning/src/ln/ffor/witness/tests.rs | 183 ++++++++++++++++++ 6 files changed, 454 insertions(+), 21 deletions(-) create mode 100644 lightning-ffor/tests/data/generate_native_witness_fixtures.py create mode 100644 lightning/src/ln/ffor/witness.rs create mode 100644 lightning/src/ln/ffor/witness/fixtures.txt create mode 100644 lightning/src/ln/ffor/witness/tests.rs diff --git a/FFOR.md b/FFOR.md index 602f9985da1..d616906ef23 100644 --- a/FFOR.md +++ b/FFOR.md @@ -2,7 +2,8 @@ This fork targets rust-lightning v0.2.5. The current APIs provide receiver voucher parking and verification of both commitment views for FFOR Variant D. Private manager -transitions compose activation, reconnect and pre-active abort with ordered storage. +transitions compose activation, reconnect, pre-active abort and cooperative voucher drain +with ordered storage. No production transport or invoice API exposes those transitions yet. `register_ffor_receiver_setup` authenticates exact signed init and accept messages @@ -80,14 +81,13 @@ Authenticated setup also enters a manager-owned recovery registry in required TL explicit closure, monitor-triggered closure and disposal of a stale manager channel. Restore reauthenticates records, checks the manager's identity and chain, and requires every retained channel setup to match its archive entry. The registry is bounded to -64 records and 8 MiB total, with a 192 KiB setup-only record limit and a 384 KiB +64 records and 8 MiB total, with a 192 KiB setup-only record limit and a 512 KiB activation record limit. Capacity is reserved before channel mutation. An Activating -record also reserves the larger of a maximum acknowledgement and a terminal abort, -including framing, so other records cannot consume that transition storage. This -allowance is derived again on restore and released when either outcome is retained. -A production close path must also reserve its required evidence before activation. -The setup-only archive -version retains no activation or claim authority. +record reserves its maximum acknowledgement, both maximum close messages and final +completion evidence, including framing, so competing admissions cannot consume its +remaining transition storage. Each later phase retains the allowance for its remaining +transitions, recomputed on restore. A terminal abort releases that reservation. +The setup-only archive version retains no activation or claim authority. Fully drained channel tombstones permit subsequent ordinary splicing while retaining the original funding context in their archive. @@ -102,9 +102,10 @@ monitor and checks both commitment identities, its destination and the saved upd floor. A preimage or force-close update may advance that floor without changing the frozen commitments. The complete monitor remains necessary for signatures and claims. -The private channel fence persists Activating, Active or a pending pre-active abort -independently of stock STFU flags. It blocks ordinary HTLC updates, commitment and -revocation messages, fees, splicing, cooperative close and signer retries. Frozen +The private channel fence persists Activating, Active, Draining, pending Closed or +a pending pre-active abort independently of stock STFU flags. Activating and Active block +ordinary HTLC updates, commitment and revocation messages, fees, splicing, cooperative +close and signer retries. Frozen channels are also excluded from usable-channel listings and ordinary routing. Valid owned preimages still enter stock monitor persistence, including delayed writes and completion after channel removal. They cannot release an ordinary fulfill while the @@ -122,8 +123,9 @@ new persistence requirement before Active can be reported. No public runtime inv these private transitions yet. The native reconnect codec carries Variant D TLV 55001 while retaining the ordinary -BOLT commitment and revocation counter checks. Frozen reconnect never retransmits -ordinary commitment traffic. A receiver still Activating can recover a lost signed +BOLT commitment and revocation counter checks. Activating and Active reconnect never +retransmit ordinary commitment traffic. Draining can replay only its owned removals +through the normal commitment protocol after its durable release grants permission. A receiver still Activating can recover a lost signed acknowledgement only after the settlement peer reports Active with the same epoch and activation hash. An unsigned report alone cannot activate the receiver. Activation bytes are never replayed across a disconnect or restart. Active conflicts retain the @@ -143,6 +145,44 @@ Restored managers request a fresh persistence requirement and notify the backgro processor before releasing reports. Disconnect discards connection-specific queued reports, and a subsequent connection builds fresh ones. +Cooperative close retains exact signed receiver intent and settlement acknowledgement, +including the settled bitmap and all acknowledged preimages. These records use archive +version 3 and a required even activation field. Exact control-message replay remains +idempotent through later phases; different bytes cannot replace retained evidence. +The private manager persists the close acknowledgement before enabling Draining. +Its native permission is instance-local and resets to false on restore. Known preimages +enter the stock monitor before any eligible voucher failures are queued. Settled slots +and slots with known preimages cannot receive new failure decisions. An unsent failure +can become a fulfill; a preimage learned after a signed failure still reaches the monitor +without rewriting an already committed removal. + +Draining permits only owned voucher removals and their commitment, revocation, monitor +and signer work. Adds, fees, STFU, splicing and ordinary cooperative close remain blocked. +A matching peer Draining or Closed report uses normal BOLT replay once permission is +released. A matching Active report enters control-message-only recovery for exact +retained close retransmission. After its signed reply the private caller must reconnect +to rebuild ordinary replay obligations; same-connection drain stays disabled. Conflicting +reports retain the fence for resolution. Outgoing Draining reports otherwise remain +queued until native drain permission is enabled, even when the archive write is complete. + +Closed requires a fresh proof that both actual commitment transactions have no HTLCs, +all removal rounds and monitor writes completed, and funding and claim signatures match. +The archived completion binds the funding outpoint, both transaction IDs and commitment +numbers, acknowledgement, activation and monitor update identity. The final write must +complete before the manager clears the fence or advertises Closed. Ordinary payments +and later splices then work while the permanent epoch and voucher tombstone remains. + +Witness fetch codecs authenticate encrypted records against the provisioned identity, +mailbox, activation digest, canonical book entry, ciphertext hash and low-S signature. +The shared body verifier checks exact framing, epoch, slot, payment terms and the +preimage hash. It accepts already decrypted bytes and does not itself establish AEAD +provenance. The native `decrypt_ffor_witness_record` adapter uses the existing secp256k1, +HKDF-SHA256 and ChaCha20-Poly1305 implementations to verify the key, associated data and +authentication tag before exposing a receipt. Its Debug output redacts the preimage. +Witness observation amounts and timestamps carry no payment authority. This helper +borrows the caller's epoch key; protected key storage, durable receipt retention and +monitor reconciliation remain runtime responsibilities. + These are consistency checks, not an authenticated storage envelope. Arbitrarily deleting a mismatching add's ownership record after abort can make its nonreserved hash indistinguishable from an ordinary post-abort payment. No valid writer creates @@ -214,18 +254,33 @@ a delayed preimage monitor write, or restart after abort release. They check act fulfill and fail messages, both empty commitment sets, tombstone reload and a later ordinary payment. +Cooperative close tests cover ten two-voucher scenarios across both funders, including +signed settled preimages, an already known unset-bit preimage, delayed monitor writes, +and restart after a signed removal flight is lost. They verify normal BOLT retransmission, +matching Active close replay, same-connection drain refusal, final monitor proof, retained +ACK replay and Closed release before ordinary payments. Native claim tests cover preimage +priority over unsent failures and monitor persistence after an already signed failure. +Force-close during unresolved drain retains exact archive evidence and monitor preimages +after reload, while missing-monitor recovery is rejected. Conflicting reconnect state +cannot release a close message. +Archive tests reject phase skips, changed signatures, wrong bitmaps, preimages and completion +metadata while preserving maximum-message transition capacity through competing admissions. + +Native witness tests use four pinned Beignet records to compare ECDH, HKDF, plaintext and +preimages; they reject signed ciphertext, ephemeral-key, AAD, manifest and plaintext-term +substitution. The shared crate separately covers all six reference scenarios and arbitrary +body bytes. These tests do not exercise production witness transport or key storage. + ## Next boundary Reusable epochs require durable retired epoch IDs and voucher hashes, with one -current signed transcript record under the same channel authority. The next native -boundary is cooperative Active-to-Draining-to-Closed recovery: retain signed close -and close-ack evidence, persist every known preimage through the stock monitor, allow -only owned voucher removals and their commitment rounds, and retain that ownership -through reconnect and restart. Capacity for those terminal records must be reserved -before an epoch becomes operational. +current signed transcript record under the same channel authority. Cooperative close +now has private archive and channel transitions, but no production driver invokes them. +That driver must retain witness keys and receipts, reconcile every recovered preimage +through the stock monitor, and enforce the configured deadline before claim safety ends. Production transport must bind the private manager transitions to actual authenticated -connections and deliver acknowledgement retries in the required order. Witness/mailbox -recovery, preimage reconciliation, deadline enforcement and invoice eligibility remain +connections and deliver acknowledgement retries in the required order. Durable witness +mailbox recovery, preimage reconciliation, deadline enforcement and invoice eligibility remain separate required boundaries. None can be inferred from durable setup, activation or a successful private protocol test. diff --git a/lightning-ffor/tests/data/generate_native_witness_fixtures.py b/lightning-ffor/tests/data/generate_native_witness_fixtures.py new file mode 100644 index 00000000000..0690158ff11 --- /dev/null +++ b/lightning-ffor/tests/data/generate_native_witness_fixtures.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Export two pinned Beignet scenarios for native tests without a JSON dependency.""" + +import hashlib +import json +from pathlib import Path + + +def main(): + source = Path(__file__).resolve().parent + setup_bytes = (source / "appendix-d.json").read_bytes() + setups = json.loads(setup_bytes) + manifests = json.loads((source / "beignet-witness.json").read_text()) + records = json.loads((source / "beignet-witness-fetch.json").read_text()) + assert records["source_revision"] == "8aee31d18e596fe49a0d195b325a6e757d7a009b" + assert records["appendix_d_sha256"] == hashlib.sha256(setup_bytes).hexdigest() + assert manifests["appendix_d_sha256"] == records["appendix_d_sha256"] + groups = [] + for setup, manifest, scenario in zip(setups[:2], manifests["fixtures"], records["fixtures"]): + assert setup["scenario"] == manifest["scenario"] == scenario["scenario"] + for record in scenario["records"]: + values = { + "scenario": setup["scenario"], + "init": setup["init_wire"], + "accept": setup["accept_wire"], + "manifest": manifest["manifest"], + "record": record["record"], + "body": record["body"], + "shared_hash": record["shared_hash"], + "body_key": record["body_key"], + } + groups.append("\n".join(f"{key}={value}" for key, value in values.items())) + destination = source.parents[2] / "lightning/src/ln/ffor/witness/fixtures.txt" + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text("\n\n".join(groups) + "\n") + + +if __name__ == "__main__": + main() diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index 7dedd3fecfb..175b264f4a3 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -25,6 +25,9 @@ use crate::prelude::*; use crate::sign::{NodeSigner, Recipient}; use crate::types::payment::PaymentHash; +mod witness; +pub use witness::{decrypt_ffor_witness_record, FFORWitnessDecryptionError, FFORWitnessReceipt}; + /// Which channel participant offers every voucher HTLC in the book. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FFORSettlementParty { diff --git a/lightning/src/ln/ffor/witness.rs b/lightning/src/ln/ffor/witness.rs new file mode 100644 index 00000000000..5a864ae2040 --- /dev/null +++ b/lightning/src/ln/ffor/witness.rs @@ -0,0 +1,118 @@ +//! Authenticated witness decryption using the native channel library's existing crypto. + +use bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use bitcoin::hashes::{sha256, Hash, HashEngine}; +use bitcoin::secp256k1::{ecdh::SharedSecret, PublicKey, Secp256k1, SecretKey}; +use core::fmt; +use lightning_ffor::witness::{ + AuthenticatedEncryptedRecord, RecordHeader, SignedManifest, VerifiedRecordBody, WitnessError, + CIPHERTEXT_LEN, RECORD_BODY_LEN as BODY_LEN, +}; + +use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC; + +/// A witness record whose signature, manifest, AEAD tag and plaintext terms were checked. +/// +/// This proves knowledge of a voucher preimage. It does not prove that the witness stored it +/// durably or that the channel has claimed it. The owner must retain the evidence and pass the +/// preimage through the normal channel monitor before reporting payment or releasing claims. +/// Debug output redacts the preimage, including through the body accessor. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FFORWitnessReceipt { + header: RecordHeader, + body: VerifiedRecordBody, +} + +impl FFORWitnessReceipt { + /// Exact authenticated public metadata, including the witness's storage-barrier claim. + pub fn header(&self) -> &RecordHeader { + &self.header + } + + /// Verified voucher terms and preimage. Observation amounts and times are informational. + pub fn body(&self) -> &VerifiedRecordBody { + &self.body + } +} + +/// Why an authenticated encrypted record could not be opened for this retained epoch. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FFORWitnessDecryptionError { + /// The supplied epoch encryption key does not match the signed record. + EncryptionKey, + /// The ciphertext or Poly1305 authentication tag is invalid. + Ciphertext, + /// The decrypted body or supplied manifest differs from the authenticated record. + Record(WitnessError), +} + +impl fmt::Display for FFORWitnessDecryptionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EncryptionKey => f.write_str("FFOR witness encryption key mismatch"), + Self::Ciphertext => f.write_str("FFOR witness ciphertext authentication failed"), + Self::Record(error) => error.fmt(f), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for FFORWitnessDecryptionError {} + +/// Decrypt an Appendix F.3 record using a borrowed, caller-owned epoch encryption key. +/// +/// The record must first be authenticated against its provisioned witness. This function checks +/// that the key matches the record, authenticates the complete header as associated data, and +/// verifies the decrypted epoch, slot, terms and preimage against the supplied signed manifest. +/// The preimage is available only after all checks succeed. No channel state or storage changes. +/// +/// The caller owns protected key storage and must use a fresh encryption key for each epoch. +/// Fetching and decrypting historical evidence remains valid after the voucher deadline. +pub fn decrypt_ffor_witness_record( + record: &AuthenticatedEncryptedRecord, manifest: &SignedManifest, encryption_key: &SecretKey, +) -> Result { + let header = record.record().header(); + let public_key = PublicKey::from_secret_key(&Secp256k1::signing_only(), encryption_key); + if public_key != header.encryption_public_key { + return Err(FFORWitnessDecryptionError::EncryptionKey); + } + let mut plaintext = decrypt_body(record, encryption_key)?; + let result = record.verify_body(manifest, &plaintext); + plaintext.fill(0); + let body = result.map_err(FFORWitnessDecryptionError::Record)?; + Ok(FFORWitnessReceipt { header: header.clone(), body }) +} + +fn decrypt_body( + record: &AuthenticatedEncryptedRecord, encryption_key: &SecretKey, +) -> Result<[u8; BODY_LEN], FFORWitnessDecryptionError> { + let bytes = record.record().ciphertext(); + if bytes.len() != CIPHERTEXT_LEN { + return Err(FFORWitnessDecryptionError::Ciphertext); + } + let ephemeral = + PublicKey::from_slice(&bytes[..33]).map_err(|_| FFORWitnessDecryptionError::Ciphertext)?; + // libsecp256k1 already hashes the compressed ECDH point. Do not hash it a second time. + let shared = SharedSecret::new(&ephemeral, encryption_key); + let key = body_key(&shared.secret_bytes()); + let aad = record.record().header().associated_data(); + let mut cipher = ChaCha20Poly1305RFC::new(&key, &[0; 12], &aad); + let mut plaintext = [0; BODY_LEN]; + cipher + .variable_time_decrypt(&bytes[33..33 + BODY_LEN], &mut plaintext, &bytes[33 + BODY_LEN..]) + .map_err(|_| FFORWitnessDecryptionError::Ciphertext)?; + Ok(plaintext) +} + +fn body_key(shared: &[u8; 32]) -> [u8; 32] { + let mut extract = HmacEngine::::new(&[]); + extract.input(shared); + let prk = Hmac::from_engine(extract).to_byte_array(); + let mut expand = HmacEngine::::new(&prk); + expand.input(b"ffor/witness/body"); + expand.input(&[1]); + Hmac::from_engine(expand).to_byte_array() +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/ffor/witness/fixtures.txt b/lightning/src/ln/ffor/witness/fixtures.txt new file mode 100644 index 00000000000..e6842848050 --- /dev/null +++ b/lightning/src/ln/ffor/witness/fixtures.txt @@ -0,0 +1,35 @@ +scenario=D.1 +init=d6d9bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489fc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff150400000000000f4240000100000000000854d0000c3110000c3500000003e80000138800000000000000000000090800000000000f4240b650a5af1658bafe768dc0d5b07af4556ee5776b010bd4da19cd64aefd40f1a0184923774f95b2d3d9e024283db263075e8ed6f793ba4553b9960e638fd90933 +accept=d6dbbef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489fc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff15000000000000002a0120a0a62adc5092cd3c202aaea0900b5826c242194c5f9984070e833e58b273eda507080000000000000000090800000000000f42400b20343331edc28e500ea03cbce26983583e66fff33dc9f096a3d6924b8239f68eb898fbd933af0a181f69b015f155a48608d1295327607d85bdb03a9598b54dc58f6ac01b0421309cebbbaa596e62113de7e6fe2aafaccc281f535eda535525b3ba +manifest=01010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0bba34be8afe89e4457543a6e7279f86054f1d6e9038234b786b740e9c905c001f723024696ad20b4758b228284620d18fd30750b7051cc64efca23a1417cf1321000c0df0580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e716035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c359000005efc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff15040100010001a0a62adc5092cd3c202aaea0900b5826c242194c5f9984070e833e58b273eda500000000000f4240000c3500000c3110000000000000000068c460b699c82f1694ff36a6e872b82c566d529e13c49f63dd6db9217279044f6e5b46f06c9abf252aec4701b251aa871de7c4dfe48d0eb3b2c84e96a2fb7753 +record=01010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0a3db013b83ab85aa5bb26de4e4043d567569b9c370faed861aba258c2e8883c0001580a4a958e8131b32452741e51d724e0f231cb518896c743fa59de236ac6e716a599edf07d16acfacd1128c82f1741bba098d480b830f04f352e822c5183da8b02bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa0145fa5d649d54851f62b46e76fa8f1eef468d60403bdb470936026f5c266b4d0bde6fc54e92010c9f45d01996c45c594f6979a3dcbf26c3ee2910828726358fa965d429fd860c6658263840e4f21a18dc0171d843ab61a4f1518ad8ffb5295d8600bf034679787536ac4c5d9be764f215cbc4dcf4268533d164ea96890e0b23769268aba93176d0f31c15fab5403c4719be5750906967cce9e3d9519859a9e300e915b0543993f30d1149542bf81df6745e8575f9f853f3d1c65915c9f9096b6adce1fbcd1991567dd0247d2ff86a6b6b41847e514213187cbc99157b2f52b0d17d5f07c2253aa9426e4e6b819c85ef7a14d5dec0469d4cb34d7f13ce1f442c90677c3216bec593e948e1f798ed9edadc419e027c0b004f129d7b4edfd5640fe1ed00 +body=fc4bc36f402d396fe452db96030f1563fd554e9c9f9313ef4cdd6299d21dff1500018ae4a313bd01bcfcce12b797ed20603ed56b0f0b9194e7c000e5cbd6fe376517a0a62adc5092cd3c202aaea0900b5826c242194c5f9984070e833e58b273eda500000000000f4240000c3500000c311000000000000f59b000000000000f4240000c11d8000000006553f100 +shared_hash=c01a2672b5d05782b78ebbb28b7f3065ff8cef83861e684568baf7e2ef2b26a6 +body_key=cdca258c9f825a6e8d8d53272b3b6c36fa6a456690273a69bee39e23690b3c99 + +scenario=D.2 +init=d6d9bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884892436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d60400000000030e9ca2000300000000000854d0000c3110000c3500000003e80000138800000000000000000000091800000000000f2ad000000000000855ca0000000002f71c081950e84703f6ab745d5e93ed17fadd4cb4750b1b4bc04b01b18dd4051b0fc58114f5b24aa52b2a1c5c391d98621ce6a37512049c6dac1cbf048f61cb52868547 +accept=d6dbbef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884892436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d6000000000000002a016024e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd993a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf707132596934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f607080000000000000007091800000000000f2ad000000000000855ca0000000002f71c080b2019644c62830070c3d7ca623b582f274e6e94669d4938e54262a1c6e6e6ed179105ec065a191d90cbb8a6adbeb0d1fd2838be33fa511588e2a3ccb83ce7313a0a1946d43ead676cd1be4a6780b5dd6feb03dc71f0c8d98056ce61304ab243d91b +manifest=01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c8465b03bd1ab3001129cd91fffb6e5129460cf82fb396cad2ebbb0ede8916396e7e996cb809a52736589d78d22b1a7c2a76b2ba07a8ac1f957aabc2651b0b9c5000c0df01d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c35900000d22436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d604010003000124e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd99300000000000f2ad0000c3500000c311000000000000000070002a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf70713259600000000000855ca000c3500000c311000000000000000080003934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f60000000002f71c08000c3500000c31100000000000000009ab2800d9b36f24db717e37c55b739581b3697a6b298520240117cbc6f71130dd07dbdd77a581b1804cd3db0c1f656aa75ea6326f08fafeef1a4460f70f0a84a0 +record=01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cbcd699ce25eda581b77eec605b501c6935c5cc5b80941e235708371c6d1d663a00011d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4de619aec79901ee0ab8d6ccded96ec853812587f785556a69cc0e4bcffef14a302bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa010cc903269e7b9bd12266f09f0e4afbeb4bff0005ba1b8cdb6bd66ab10edaeb4a8e7800231692024b2409a747d54e7317cb6152431e89a0925599b22e60979fe77c745510f735ad04426e8bf364d9c37fde7fcb16a964163bbd69b0915603137b00bf02d7b6a5bda48673403a0fffd45ceb5bf35d5ceb92271aecf5d4fcef2c644552d7d01331daa63ebf4c6af455538301daed14c25424e9bb57c1eace0e2448e00ab055fef3f2cd9541117bbd57494d9b86efaf87af81aedd461fed72c0087ede06ff8ac7b8237c0d767bb18f53bfd80f26d4f9e2cca6bd1f3d509da2a0676233ac0f31b0a008d9552951ac103a42f0baddc292ad52460c2898dbf71ecc4ef516df35228aec3b34655168122aa3c3bc5f1aae5abaea77541f2b66cfd6e63c5a3a00 +body=2436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d600019ac2111ef6621b1e5a5e2045b1811e3fbed11db86124fe4b7d6f0589fe69765e24e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd99300000000000f2ad0000c3500000c311000000000000f424000000000000f2ad0000c11d8000000006553f100 +shared_hash=a6a818f27abd6fb7e02b9420dad3419ef83b89e2bb1f28b67794cc0932527de5 +body_key=791123509e4e327389c97796110b0c417c0275d8bb2d4f66264e10299567e307 + +scenario=D.2 +init=d6d9bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884892436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d60400000000030e9ca2000300000000000854d0000c3110000c3500000003e80000138800000000000000000000091800000000000f2ad000000000000855ca0000000002f71c081950e84703f6ab745d5e93ed17fadd4cb4750b1b4bc04b01b18dd4051b0fc58114f5b24aa52b2a1c5c391d98621ce6a37512049c6dac1cbf048f61cb52868547 +accept=d6dbbef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884892436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d6000000000000002a016024e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd993a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf707132596934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f607080000000000000007091800000000000f2ad000000000000855ca0000000002f71c080b2019644c62830070c3d7ca623b582f274e6e94669d4938e54262a1c6e6e6ed179105ec065a191d90cbb8a6adbeb0d1fd2838be33fa511588e2a3ccb83ce7313a0a1946d43ead676cd1be4a6780b5dd6feb03dc71f0c8d98056ce61304ab243d91b +manifest=01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c8465b03bd1ab3001129cd91fffb6e5129460cf82fb396cad2ebbb0ede8916396e7e996cb809a52736589d78d22b1a7c2a76b2ba07a8ac1f957aabc2651b0b9c5000c0df01d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c35900000d22436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d604010003000124e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd99300000000000f2ad0000c3500000c311000000000000000070002a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf70713259600000000000855ca000c3500000c311000000000000000080003934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f60000000002f71c08000c3500000c31100000000000000009ab2800d9b36f24db717e37c55b739581b3697a6b298520240117cbc6f71130dd07dbdd77a581b1804cd3db0c1f656aa75ea6326f08fafeef1a4460f70f0a84a0 +record=01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c1666deb356225ce9ad5e9b64676970a7d2a2dafb2b2cd9f669df993c081603de00021d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4b2d9c11c5bcd065716e0d8664b3f869b96dac515c6be66fc9bbe4861fe32a9a902bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa00404a86662e5a6d1c7735e1250c28dc5e0103176c3a9274ac25aa0dc1a784d773ed984c97a7cca886059ca31bdd007210d2ba81525a2f9554a249f2d3d8b2e0dc306c2274601fea41a86226119b2d356d0fe06f0667a975ae86250c94210dff1a00bf021ad1aadcca777393747962322a6300c1c4269fd1032fa164f205050e387cb3d96b5799ef4fbd038072e3b84343b946da61e13ef776026d0b3c89c576f28580fd81958edf8218a61c58929e22e232ec8aa8f093efa33243ce746900ad76a23a2244699fb419c5ef6ff8b44a5a04b1282f77d4ea54ec4570734bf53fc52205aebc45fee93b4a3207dee9fcdd5894e225fea414a3a1306a0bd12775c60b678c7765893797c463eaa8ecb73cef571732a239535f2157137c4d0ed4ebd05c150802001a6f706171756520677561726469616e206174746163686d656e74000200ff +body=2436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d600024190c6e6e079c8575ea0a17d450565ec47ea99662b118c34b6608a7105d3b35ea7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf70713259600000000000855ca000c3500000c31100000000000086d3a00000000000855ca000c11d8000000006553f100 +shared_hash=fc095b142760f6cd830191095f1c28d47cce6815903067f5db090044cfde282a +body_key=c505e6eca19ce6cc9a6c2f05b003e139a26ff27f49181a900d44b271c0aef7ab + +scenario=D.2 +init=d6d9bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884892436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d60400000000030e9ca2000300000000000854d0000c3110000c3500000003e80000138800000000000000000000091800000000000f2ad000000000000855ca0000000002f71c081950e84703f6ab745d5e93ed17fadd4cb4750b1b4bc04b01b18dd4051b0fc58114f5b24aa52b2a1c5c391d98621ce6a37512049c6dac1cbf048f61cb52868547 +accept=d6dbbef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a4884892436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d6000000000000002a016024e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd993a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf707132596934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f607080000000000000007091800000000000f2ad000000000000855ca0000000002f71c080b2019644c62830070c3d7ca623b582f274e6e94669d4938e54262a1c6e6e6ed179105ec065a191d90cbb8a6adbeb0d1fd2838be33fa511588e2a3ccb83ce7313a0a1946d43ead676cd1be4a6780b5dd6feb03dc71f0c8d98056ce61304ab243d91b +manifest=01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c8465b03bd1ab3001129cd91fffb6e5129460cf82fb396cad2ebbb0ede8916396e7e996cb809a52736589d78d22b1a7c2a76b2ba07a8ac1f957aabc2651b0b9c5000c0df01d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d4035be5e9478209674a96e60f1f037f6176540fd001fa1d64694770c56a7709c42c028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c35900000d22436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d604010003000124e4e3ab2a0cbbe957d5dc41c688721d956cf7babe8f304ad24b4589787bd99300000000000f2ad0000c3500000c311000000000000000070002a7334c46d83bba22364ed8b863d3052fe24f62b973072776b537aaf70713259600000000000855ca000c3500000c311000000000000000080003934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f60000000002f71c08000c3500000c31100000000000000009ab2800d9b36f24db717e37c55b739581b3697a6b298520240117cbc6f71130dd07dbdd77a581b1804cd3db0c1f656aa75ea6326f08fafeef1a4460f70f0a84a0 +record=01010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0cfc64314060ae5338b793a7dda00d9fd7e1c154b60fa52e324778fe63014c16fc00031d561fb397d1ac599cf6876a4f390d6bd81de4cd425b30383b14a5de4b4b61d48c946fbad478cc946802555991c23ce67a4e1356639e5d6a12212eca7abd898902bb58b5feca505c74edc000d8282fc556e51a1024fc8e7d7e56c6f887c5c8d5f2028f5304e2373e56ee7d774cb89e9f1afecf0ee7e3e3757f189908f069daa36c60000c0dfa012319822880a9ae3fee7a22c25a0107daa3d8bdd42251bddb4d4008be10a7589b289517b7742990a161b378bd1281783f9df6ae6def0d2a3c17ad42b3c835c80552470e92c37ecd8b0cb3ccdb2bbcfa67d9efb6f699d25776691c8d8518477f2400bf02ed0ef5b80e404a0bba3207635e45a99ab93ef3f43a96aca2e3837ca888274751b07a7e079f429c9ee9d98488a4f389bb9825b29b9abd871819290e5a3f33a6469b57abcccd14df422ca0172255f138c4130ca3087c21685c563be9af21b2e2633988c6e3e87e78366c001e48ab741db29abb54dd9c4eb3ad886b8467e7576dfa47b414f898908b7c9e028cc145e13f344ad7e3200a758d338fcf79d6a1e4b77cdb837bbc8f599789fd9f98e485f07a1c242269e95d7a187b05b02aa18cab02001a6f706171756520677561726469616e206174746163686d656e74000200ff +body=2436d73f6fdf469b68bedbbd9c90aa51871d491fadf862d815853b27f13f62d600038f3b529ca4bb18dc2b26ba4b0902f272f6aa9cf95b0e8c1464e54b14054b5f40934cbfbc3fdfea5fc984e1270f06ebb219beb18ee02e8441acafa05ec7d928f60000000002f71c08000c3500000c31100000000002f733780000000002f71c08000c11d8000000006553f100 +shared_hash=9b229b95522d9411cbf5606640e61477d5b029426f6115d99f9e4349567f52c0 +body_key=7bda592ec756215eef6afed794fd4baec5850f88e5e6f91978141b79f37f696d diff --git a/lightning/src/ln/ffor/witness/tests.rs b/lightning/src/ln/ffor/witness/tests.rs new file mode 100644 index 00000000000..5290bcf0066 --- /dev/null +++ b/lightning/src/ln/ffor/witness/tests.rs @@ -0,0 +1,183 @@ +use super::*; +use crate::prelude::*; +use bitcoin::secp256k1::Message; +use lightning_ffor::setup::AuthenticatedSetup; +use lightning_ffor::wire::Message as WireMessage; +use lightning_ffor::witness::{EncryptedRecord, RECORD_HEADER_LEN}; + +// Public deterministic Beignet vectors exported by generate_native_witness_fixtures.py. +// The shared tests cover all six scenarios; these pin native ECDH, HKDF and AEAD together. +struct Fixture<'a>(&'a str); + +impl Fixture<'_> { + fn value(&self, name: &str) -> &str { + self.0 + .lines() + .find_map(|line| { + let (key, value) = line.split_once('=')?; + if key == name { + Some(value) + } else { + None + } + }) + .unwrap() + } + fn bytes(&self, name: &str) -> Vec { + hex(self.value(name)) + } + fn manifest(&self) -> SignedManifest { + let receiver = PublicKey::from_slice(&hex( + "039fca7f8157aa768708894ffd92550fe970edd18526a5f936583ea3b54dab3228", + )) + .unwrap(); + let settlement = PublicKey::from_slice(&hex( + "02087b7d1b4789170f6e374f0a0e58a1b7a899e34929795314ab6964e69609e9c0", + )) + .unwrap(); + let setup = AuthenticatedSetup::new( + &WireMessage::decode(&self.bytes("init")).unwrap(), + &WireMessage::decode(&self.bytes("accept")).unwrap(), + receiver, + settlement, + ) + .unwrap(); + SignedManifest::decode(&self.bytes("manifest"), &setup).unwrap() + } + fn record(&self) -> AuthenticatedEncryptedRecord { + authenticate(&self.bytes("record"), &self.manifest()) + } +} + +fn fixtures() -> impl Iterator> { + include_str!("fixtures.txt").trim().split("\n\n").map(Fixture) +} + +fn hex(value: &str) -> Vec { + (0..value.len()).step_by(2).map(|i| u8::from_str_radix(&value[i..i + 2], 16).unwrap()).collect() +} + +fn secret(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() +} +fn public(byte: u8) -> PublicKey { + PublicKey::from_secret_key(&Secp256k1::new(), &secret(byte)) +} + +fn authenticate(bytes: &[u8], manifest: &SignedManifest) -> AuthenticatedEncryptedRecord { + EncryptedRecord::decode(bytes).unwrap().authenticate(manifest, public(43)).unwrap() +} + +fn sign_record(bytes: &mut [u8]) { + let digest = sha256::Hash::hash(&[b"ffor/witness/record".as_slice(), &bytes[..235]].concat()); + let signature = + Secp256k1::new().sign_ecdsa(&Message::from_digest(digest.to_byte_array()), &secret(43)); + bytes[235..299].copy_from_slice(&signature.serialize_compact()); +} + +fn ciphertext_hash(bytes: &mut [u8]) { + let hash = sha256::Hash::hash(&bytes[301..301 + CIPHERTEXT_LEN]); + bytes[203..235].copy_from_slice(hash.as_byte_array()); +} + +#[test] +fn ffor_witness_decryption_matches_beignet_ecdh_hkdf_aead_and_body() { + for fixture in fixtures() { + let manifest = fixture.manifest(); + let record = fixture.record(); + let ephemeral = PublicKey::from_slice(&record.record().ciphertext()[..33]).unwrap(); + let shared = SharedSecret::new(&ephemeral, &secret(44)); + assert_eq!(shared.secret_bytes().as_slice(), fixture.bytes("shared_hash")); + assert_eq!(body_key(&shared.secret_bytes()).as_slice(), fixture.bytes("body_key")); + assert_eq!(decrypt_body(&record, &secret(44)).unwrap().as_slice(), fixture.bytes("body")); + let receipt = decrypt_ffor_witness_record(&record, &manifest, &secret(44)).unwrap(); + assert_eq!(receipt.header(), record.record().header()); + assert_eq!(receipt.body().preimage().as_slice(), &fixture.bytes("body")[34..66]); + let debug = format!("{:?}", receipt); + assert!(!debug.contains(&format!("{:?}", &fixture.bytes("body")[34..66]))); + assert_eq!( + decrypt_ffor_witness_record(&record, &manifest, &secret(45)), + Err(FFORWitnessDecryptionError::EncryptionKey) + ); + } +} + +#[test] +fn ffor_witness_decryption_rejects_signed_ciphertext_and_aad_substitution() { + let fixture = fixtures().next().unwrap(); + let manifest = fixture.manifest(); + for offset in [301 + 33, 301 + CIPHERTEXT_LEN - 1, 34, 198, 202] { + let mut wire = fixture.bytes("record"); + wire[offset] ^= 1; + ciphertext_hash(&mut wire); + sign_record(&mut wire); + let record = authenticate(&wire, &manifest); + assert_eq!( + decrypt_ffor_witness_record(&record, &manifest, &secret(44)), + Err(FFORWitnessDecryptionError::Ciphertext), + "offset {offset}" + ); + } + let mut wire = fixture.bytes("record"); + wire[301..334].copy_from_slice(&public(46).serialize()); + ciphertext_hash(&mut wire); + sign_record(&mut wire); + assert_eq!( + decrypt_ffor_witness_record(&authenticate(&wire, &manifest), &manifest, &secret(44)), + Err(FFORWitnessDecryptionError::Ciphertext) + ); +} + +#[test] +fn ffor_witness_decryption_rejects_a_different_retained_manifest() { + let fixture = fixtures().next().unwrap(); + let other = fixtures().nth(1).unwrap(); + assert_eq!( + decrypt_ffor_witness_record(&fixture.record(), &other.manifest(), &secret(44)), + Err(FFORWitnessDecryptionError::Record(WitnessError::Mailbox)) + ); +} + +fn reseal(fixture: &Fixture, plaintext: &[u8]) -> AuthenticatedEncryptedRecord { + let record = fixture.record(); + let mut header = record.record().header().clone(); + let key = + body_key(&SharedSecret::new(&header.encryption_public_key, &secret(47)).secret_bytes()); + let mut cipher = ChaCha20Poly1305RFC::new(&key, &[0; 12], &header.associated_data()); + let mut encrypted = [0; BODY_LEN]; + let mut tag = [0; 16]; + cipher.encrypt(plaintext, &mut encrypted, &mut tag); + let bytes = [&public(47).serialize()[..], &encrypted, &tag].concat(); + header.ciphertext_hash = sha256::Hash::hash(&bytes).to_byte_array(); + let mut wire = header.encode(); + wire.resize(RECORD_HEADER_LEN + 64, 0); + wire.extend_from_slice(&(bytes.len() as u16).to_be_bytes()); + wire.extend_from_slice(&bytes); + wire.push(0); + sign_record(&mut wire); + authenticate(&wire, &fixture.manifest()) +} + +#[test] +fn ffor_witness_decryption_rejects_authenticated_bodies_with_wrong_voucher_terms() { + let fixture = fixtures().next().unwrap(); + let manifest = fixture.manifest(); + for offset in [0, 33, 34, 66, 105, 109, 113] { + let mut plaintext = fixture.bytes("body"); + plaintext[offset] ^= 1; + let record = reseal(&fixture, &plaintext); + assert!( + matches!( + decrypt_ffor_witness_record(&record, &manifest, &secret(44)), + Err(FFORWitnessDecryptionError::Record(_)) + ), + "offset {offset}" + ); + } + // Observation context is authenticated but does not change a voucher's payment amount. + let mut plaintext = fixture.bytes("body"); + plaintext[114..].fill(255); + let record = reseal(&fixture, &plaintext); + let receipt = decrypt_ffor_witness_record(&record, &manifest, &secret(44)).unwrap(); + assert_eq!(receipt.body().preimage().as_slice(), &plaintext[34..66]); +} From 8478895388b656b4fa674d1ede4c48f33a9688b4 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Sun, 20 Sep 2026 23:49:03 -0400 Subject: [PATCH 13/22] feat(ffor): expose read-only receiver recovery contexts --- FFOR.md | 16 ++ lightning/src/ln/channel/ffor/setup.rs | 3 + .../src/ln/channelmanager/ffor_activation.rs | 1 + .../channelmanager/ffor_activation/context.rs | 178 ++++++++++++++++++ .../ffor_activation/context/tests.rs | 150 +++++++++++++++ lightning/src/ln/ffor.rs | 3 + lightning/src/ln/ffor/context.rs | 159 ++++++++++++++++ lightning/src/ln/ffor_recovery/activation.rs | 1 + .../ln/ffor_recovery/activation/context.rs | 30 +++ 9 files changed, 541 insertions(+) create mode 100644 lightning/src/ln/channelmanager/ffor_activation/context.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/context/tests.rs create mode 100644 lightning/src/ln/ffor/context.rs create mode 100644 lightning/src/ln/ffor_recovery/activation/context.rs diff --git a/FFOR.md b/FFOR.md index d616906ef23..b82262a448a 100644 --- a/FFOR.md +++ b/FFOR.md @@ -183,6 +183,18 @@ Witness observation amounts and timestamps carry no payment authority. This help borrows the caller's epoch key; protected key storage, durable receipt retention and monitor reconciliation remain runtime responsibilities. +Public read-only receiver contexts let an application join protected records to native +history. Historical contexts expose authenticated setup, exact activation messages, +original funding and commitment identities, and a stable digest which survives ACK, +close, channel removal and restart. They do not assert a current phase. Active contexts +are opaque observations tied to this manager instance and the exact completed persistence +requirement. Capture checks the live Active fence, original commitment pair, signed ACK, +absence of close intent and absence of conflicting reconnect state. Close, removal, +conflict or restore invalidates an old observation. A point-in-time validation call does +not authorize later work; the future driver must recheck under its transition locks. +A retained Active phase can outlive the settlement deadline, so neither context grants +provisioning permission or invoice readiness. + These are consistency checks, not an authenticated storage envelope. Arbitrarily deleting a mismatching add's ownership record after abort can make its nonreserved hash indistinguishable from an ordinary post-abort payment. No valid writer creates @@ -266,6 +278,10 @@ cannot release a close message. Archive tests reject phase skips, changed signatures, wrong bitmaps, preimages and completion metadata while preserving maximum-message transition capacity through competing admissions. +Read-only context tests cover both funders, absent ACK, pending and current persistence, +foreign/restored manager rejection, offline Active observation, conflicting reconnect, +and stable historical binding through close and archive-only force-close recovery. + Native witness tests use four pinned Beignet records to compare ECDH, HKDF, plaintext and preimages; they reject signed ciphertext, ephemeral-key, AAD, manifest and plaintext-term substitution. The shared crate separately covers all six reference scenarios and arbitrary diff --git a/lightning/src/ln/channel/ffor/setup.rs b/lightning/src/ln/channel/ffor/setup.rs index 19d03da2cc9..0195a658b47 100644 --- a/lightning/src/ln/channel/ffor/setup.rs +++ b/lightning/src/ln/channel/ffor/setup.rs @@ -115,6 +115,9 @@ impl FFORReceiverSetup { pub(crate) fn receiver(&self) -> PublicKey { self.receiver } + pub(crate) fn settlement(&self) -> PublicKey { + self.settlement + } pub(crate) fn chain_hash(&self) -> ChainHash { self.chain_hash } diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs index a4b879de763..198e7105432 100644 --- a/lightning/src/ln/channelmanager/ffor_activation.rs +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -10,6 +10,7 @@ use lightning_ffor::transcript; use lightning_ffor::wire::{Activate, Message as FFORMessage, Payload}; mod close; +mod context; mod reestablish; struct RuntimeEntry { diff --git a/lightning/src/ln/channelmanager/ffor_activation/context.rs b/lightning/src/ln/channelmanager/ffor_activation/context.rs new file mode 100644 index 00000000000..14a75d4b941 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/context.rs @@ -0,0 +1,178 @@ +//! Read-only receiver contexts. No observation releases a message or changes a native phase. + +use super::*; +use crate::ln::ffor::context::{FFORReceiverActiveContext, FFORReceiverRecoveryContext}; + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Return authenticated historical activation evidence, including an archive-only epoch. + /// This neither requires nor establishes current Active authority or durable readiness. + pub fn ffor_receiver_recovery_context( + &self, channel_id: &ChannelId, epoch_id: [u8; 32], + ) -> Result { + let _guard = self.total_consistency_lock.read().unwrap(); + let recovery = self.ffor_recovery.lock().unwrap(); + self.ffor_recovery_context_from_registry( + &recovery, + &FFORRecoveryKey { channel_id: *channel_id, epoch_id }, + ) + } + + /// List at most 64 retained activation histories for joining protected application records. + /// Includes terminal histories and removed channels. Missing application keys must never be + /// interpreted as permission to downgrade or discard the corresponding native epoch. + pub fn list_ffor_receiver_recovery_contexts( + &self, + ) -> Result, FFORReceiverError> { + let _guard = self.total_consistency_lock.read().unwrap(); + let recovery = self.ffor_recovery.lock().unwrap(); + recovery + .activation_keys() + .iter() + .map(|key| self.ffor_recovery_context_from_registry(&recovery, key)) + .collect() + } + + fn ffor_recovery_context_from_registry( + &self, recovery: &FFORRecoveryRegistry, key: &FFORRecoveryKey, + ) -> Result { + recovery + .validate_identity(self.our_network_pubkey, self.chain_hash) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let setup = recovery.get(key).ok_or(FFORReceiverError::UnknownEpoch)?; + let activation = recovery.get_activation(key).ok_or(FFORReceiverError::NotRegistered)?; + activation.receiver_context(setup).map_err(|_| FFORReceiverError::RecoveryUnavailable) + } + + /// Capture a current durable Active observation without releasing any external work. + /// + /// The epoch must retain its exact signed acknowledgement and original frozen commitments. + /// Any retained close intent, pending persistence, or unresolved peer report prevents capture. + /// Mere disconnection does not revoke a retained Active epoch. The observation may remain valid + /// after its admission deadline and authorizes neither witness provisioning nor invoices. + pub fn capture_ffor_receiver_active_context( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result { + let _guard = self.total_consistency_lock.read().unwrap(); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(channel_id) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let recovery = self.ffor_recovery.lock().unwrap(); + self.ffor_active_context_locked(channel, &recovery, &key) + } + + /// Check whether this manager still has the exact Active state captured in `context`. + /// + /// This checks manager identity, the latest exact persistence requirement and current native + /// state. It does not grant authority for a later side effect: another thread may close the + /// epoch immediately after this call. A future driver must recheck while releasing work. + /// Every context from an earlier manager instance is invalid after restore, even if the + /// serialized channel bytes match. Newly captured contexts require new persistence completion. + pub fn validate_ffor_receiver_active_context( + &self, context: &FFORReceiverActiveContext, + ) -> Result<(), FFORReceiverError> { + let _guard = self.total_consistency_lock.read().unwrap(); + let historical = context.recovery_context(); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(&historical.settlement_node_id()) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(&historical.channel_id()) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { + channel_id: historical.channel_id(), + epoch_id: historical.epoch_id(), + }; + let recovery = self.ffor_recovery.lock().unwrap(); + let current = self.ffor_active_context_locked(channel, &recovery, &key)?; + if current.requirement != context.requirement + || current.recovery.context_digest() != historical.context_digest() + || current.recovery.activation_ack_wire() != historical.activation_ack_wire() + { + return Err(FFORReceiverError::RecoveryUnavailable); + } + Ok(()) + } + + fn ffor_active_context_locked( + &self, channel: &FundedChannel, recovery: &FFORRecoveryRegistry, key: &FFORRecoveryKey, + ) -> Result { + channel + .ffor_validate_receiver_identity(self.our_network_pubkey, self.chain_hash) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let setup = channel + .ffor_receiver_setup_record() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + .ok_or(FFORReceiverError::NotRegistered)?; + recovery + .validate_channel_lifecycle( + &setup, + channel.ffor_receiver_fence(), + channel.ffor_receiver_abort_reason(), + channel.ffor_receiver_drain_binding(), + channel.ffor_receiver_closed_completion_hash(), + channel.ffor_receiver_drain_activation_hash(), + ) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let activation = recovery.get_activation(key).ok_or(FFORReceiverError::NotRegistered)?; + let context = self.ffor_recovery_context_from_registry(recovery, key)?; + if !activation.is_active() + || activation.is_aborted() + || activation.close_record().is_some() + || channel.ffor_receiver_fence() + != Some((FFORReceiverFencePhase::Active, context.activation_hash())) + || channel.context.get_counterparty_node_id() != context.settlement_node_id() + || channel.ffor_frozen_commitments(&self.logger)? != activation.commitments() + || matches!( + channel.ffor_receiver_reconnect_outcome(), + Some(FFORReestablishOutcome::ResolutionRequired { .. }) + | Some(FFORReestablishOutcome::AbortRequired { .. }) + | Some(FFORReestablishOutcome::CloseReplayRequired { .. }) + ) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let runtime = self.ffor_activation.lock().unwrap(); + let requirement = &runtime.get(key)?.requirement; + if !self.ffor_persistence.lock().unwrap().is_complete(requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + Ok(FFORReceiverActiveContext { recovery: context, requirement: requirement.clone() }) + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/channelmanager/ffor_activation/context/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/context/tests.rs new file mode 100644 index 00000000000..e07ab62be1f --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/context/tests.rs @@ -0,0 +1,150 @@ +use super::*; +use crate::ln::channelmanager::ffor_activation::drain_tests::park_two; +use crate::ln::channelmanager::ffor_recovery_tests::restore; +use crate::ln::ffor_tests::anchor_config; +use crate::ln::functional_test_utils::*; +use lightning_ffor::reestablish::{Reestablish, ReportedState}; + +const EPOCH: [u8; 32] = [81; 32]; + +fn persist(node: &TestChannelManager) -> Vec { + let token = node.capture_ffor_persistence(); + let bytes = node.encode(); + node.ffor_persistence_completed(token).unwrap(); + bytes +} + +fn signed_ack(sender: &Node, context: &FFORReceiverRecoveryContext) -> Vec { + let mut ack = FFORMessage { + header: context.setup().header(), + payload: Payload::ActivateAck(context.activation_hash()), + extensions: Vec::new(), + signature: [0; 64], + }; + ack.signature = sender + .keys_manager + .sign_ffor_message(&FFORSigningRequest::new(&ack.unsigned_wire().unwrap()).unwrap()) + .unwrap() + .serialize_compact(); + ack.encode().unwrap() +} + +#[test] +fn ffor_context_requires_current_durable_active_and_invalidates_after_restore_or_close() { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let sender = &nodes[0]; + let receiver = &nodes[1]; + let peer = sender.node.get_our_node_id(); + assert!(receiver.node.list_ffor_receiver_recovery_contexts().unwrap().is_empty()); + park_two(sender, receiver, id); + let before_ack = receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + assert_eq!(before_ack.receiver_node_id(), receiver.node.get_our_node_id()); + assert_eq!(before_ack.settlement_node_id(), peer); + assert_eq!(before_ack.channel_id(), id); + assert_eq!(before_ack.epoch_id(), EPOCH); + assert_eq!(before_ack.setup().vouchers().len(), 2); + assert!(before_ack.activation_ack_wire().is_none()); + assert!(receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).is_err()); + assert_eq!(receiver.node.list_ffor_receiver_recovery_contexts().unwrap().len(), 1); + let ack = signed_ack(sender, &before_ack); + receiver.node.accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &ack).unwrap(); + assert!(receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).is_err()); + let manager = persist(receiver.node); + let active = receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).unwrap(); + assert_eq!(active.recovery_context().context_digest(), before_ack.context_digest()); + assert_eq!(active.recovery_context().activation_ack_wire(), Some(ack.as_slice())); + receiver.node.validate_ffor_receiver_active_context(&active).unwrap(); + assert!(sender.node.validate_ffor_receiver_active_context(&active).is_err()); + let monitor = get_monitor!(receiver, id).encode(); + let restored = restore(receiver, &manager, &monitor).unwrap(); + assert!(restored.validate_ffor_receiver_active_context(&active).is_err()); + assert!(restored.capture_ffor_receiver_active_context(&id, &peer, EPOCH).is_err()); + persist(&restored); + assert!(restored.validate_ffor_receiver_active_context(&active).is_err()); + let fresh = restored.capture_ffor_receiver_active_context(&id, &peer, EPOCH).unwrap(); + restored.validate_ffor_receiver_active_context(&fresh).unwrap(); + assert_eq!(fresh.recovery_context().context_digest(), before_ack.context_digest()); + assert!(receiver.node.validate_ffor_receiver_active_context(&fresh).is_err()); + // Even a completed older requirement cannot survive a retained close intent. + receiver.node.prepare_ffor_receiver_close(&id, &peer, EPOCH).unwrap(); + assert!(receiver.node.validate_ffor_receiver_active_context(&active).is_err()); + persist(receiver.node); + assert!(receiver.node.validate_ffor_receiver_active_context(&active).is_err()); + let historical = receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + assert_eq!(historical.context_digest(), before_ack.context_digest()); + receiver + .node + .force_close_broadcasting_latest_txn(&id, &peer, "context recovery test".into()) + .unwrap(); + receiver.node.get_and_clear_pending_msg_events(); + assert!(receiver + .node + .get_and_clear_pending_events() + .iter() + .any(|event| matches!(event, Event::ChannelClosed { .. }))); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + assert!(receiver.node.validate_ffor_receiver_active_context(&active).is_err()); + let monitor = get_monitor!(receiver, id).encode(); + let restored = restore(receiver, &persist(receiver.node), &monitor).unwrap(); + assert!(restored.list_channels().is_empty()); + let contexts = restored.list_ffor_receiver_recovery_contexts().unwrap(); + assert_eq!(contexts.len(), 1); + assert_eq!(contexts[0].context_digest(), before_ack.context_digest()); + assert_eq!(contexts[0].activation_wire(), before_ack.activation_wire()); + assert_eq!(contexts[0].activation_ack_wire(), Some(ack.as_slice())); + } +} + +#[test] +fn ffor_context_rejects_conflicting_connection_observation() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let sender = &nodes[0]; + let receiver = &nodes[1]; + let peer = sender.node.get_our_node_id(); + let receiver_id = receiver.node.get_our_node_id(); + park_two(sender, receiver, id); + let context = receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + let ack = signed_ack(sender, &context); + receiver.node.accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &ack).unwrap(); + persist(receiver.node); + let active = receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).unwrap(); + // Local retained Active authority remains observable while the peer is offline. + sender.node.peer_disconnected(receiver_id); + receiver.node.peer_disconnected(peer); + receiver.node.validate_ffor_receiver_active_context(&active).unwrap(); + connect_nodes(sender, receiver); + let local = get_event_msg!(receiver, MessageSendEvent::SendChannelReestablish, peer); + let mut remote = get_event_msg!(sender, MessageSendEvent::SendChannelReestablish, receiver_id); + let mut conflict = context.activation_hash(); + conflict[0] ^= 1; + remote.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: EPOCH, + activation_hash: conflict, + state: ReportedState::Active, + })); + sender.node.handle_channel_reestablish(receiver_id, &local); + receiver.node.handle_channel_reestablish(peer, &remote); + assert!(receiver.node.validate_ffor_receiver_active_context(&active).is_err()); + assert!(receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).is_err()); + assert_eq!( + receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap().context_digest(), + context.context_digest() + ); + sender.node.get_and_clear_pending_msg_events(); + receiver.node.get_and_clear_pending_msg_events(); +} diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index 175b264f4a3..f39bf2e32e7 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -25,6 +25,9 @@ use crate::prelude::*; use crate::sign::{NodeSigner, Recipient}; use crate::types::payment::PaymentHash; +pub(crate) mod context; +pub use context::{FFORReceiverActiveContext, FFORReceiverRecoveryContext}; + mod witness; pub use witness::{decrypt_ffor_witness_record, FFORWitnessDecryptionError, FFORWitnessReceipt}; diff --git a/lightning/src/ln/ffor/context.rs b/lightning/src/ln/ffor/context.rs new file mode 100644 index 00000000000..31d7ef710c7 --- /dev/null +++ b/lightning/src/ln/ffor/context.rs @@ -0,0 +1,159 @@ +//! Experimental observations of retained receiver evidence and current durable activation. +//! +//! Neither context grants invoice readiness or authorizes a later side effect. A driver must +//! recheck live authority while holding the manager's transition locks before releasing work. + +use alloc::vec::Vec; +use core::fmt; + +use bitcoin::constants::ChainHash; +use bitcoin::hashes::{sha256, Hash, HashEngine}; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{Script, ScriptBuf}; +use lightning_ffor::setup::AuthenticatedSetup; + +use crate::chain::transaction::OutPoint; +use crate::ln::ffor::FFORVoucherCommitments; +use crate::ln::ffor_persistence::FFORPersistenceRequirement; +use crate::ln::types::ChannelId; + +/// Immutable historical evidence retained even when its live channel has been removed. +/// +/// This value authenticates the recorded setup and activation. It does not assert that the +/// channel is still active, that its latest state is durable, or that receiving is ready. +/// Its stable digest lets a protected application record identify the same native epoch after +/// restart. Possession of that record or digest does not confer native transition authority. +#[derive(Clone)] +pub struct FFORReceiverRecoveryContext { + pub(crate) data: FFORReceiverRecoveryContextData, + context_digest: [u8; 32], +} + +#[derive(Clone)] +pub(crate) struct FFORReceiverRecoveryContextData { + pub(crate) setup: AuthenticatedSetup, + pub(crate) chain_hash: ChainHash, + pub(crate) receiver: PublicKey, + pub(crate) settlement: PublicKey, + pub(crate) funding_txo: OutPoint, + pub(crate) activate_wire: Vec, + pub(crate) activation_hash: [u8; 32], + pub(crate) ack_wire: Option>, + pub(crate) commitments: FFORVoucherCommitments, + pub(crate) monitor_update_id: u64, + pub(crate) preparation_height: u32, + pub(crate) destination_script: ScriptBuf, +} + +impl FFORReceiverRecoveryContext { + pub(crate) fn from_authenticated( + data: FFORReceiverRecoveryContextData, exact_setup: &[u8], + ) -> Self { + let mut engine = sha256::Hash::engine(); + engine.input(b"ffor/native-receiver-context/v1"); + // Every component has a fixed u64 byte length prefix, including fixed-size fields. + // Exact setup serialization includes its authenticated identities and admission facts. + for bytes in [ + exact_setup, + data.setup.canonical_book(), + &data.activate_wire, + &data.activation_hash, + &data.commitments.holder.number.to_be_bytes(), + data.commitments.holder.txid.as_byte_array(), + &data.commitments.counterparty.number.to_be_bytes(), + data.commitments.counterparty.txid.as_byte_array(), + &data.monitor_update_id.to_be_bytes(), + &data.preparation_height.to_be_bytes(), + data.destination_script.as_bytes(), + ] { + engine.input(&(bytes.len() as u64).to_be_bytes()); + engine.input(bytes); + } + Self { data, context_digest: sha256::Hash::from_engine(engine).to_byte_array() } + } + + /// Original chain authenticated by the native setup record. + pub fn chain_hash(&self) -> ChainHash { + self.data.chain_hash + } + /// Receiver identity authenticated by the native setup record. + pub fn receiver_node_id(&self) -> PublicKey { + self.data.receiver + } + /// Settlement identity authenticated by the native setup record. + pub fn settlement_node_id(&self) -> PublicKey { + self.data.settlement + } + /// Original channel identifier retained by the signed setup. + pub fn channel_id(&self) -> ChannelId { + ChannelId(self.data.setup.header().channel_id) + } + /// Epoch identifier retained by the signed setup. + pub fn epoch_id(&self) -> [u8; 32] { + self.data.setup.header().epoch_id + } + /// Original funding output, even after a completed epoch allows later channel splices. + pub fn funding_txo(&self) -> OutPoint { + self.data.funding_txo + } + /// Immutable authenticated setup and its canonical book. + pub fn setup(&self) -> &AuthenticatedSetup { + &self.data.setup + } + /// Exact retained receiver activation message, including its signature. + pub fn activation_wire(&self) -> &[u8] { + &self.data.activate_wire + } + /// Authenticated activation digest. This does not assert a current Active channel. + pub fn activation_hash(&self) -> [u8; 32] { + self.data.activation_hash + } + /// Exact settlement activation acknowledgement, if one was retained. + pub fn activation_ack_wire(&self) -> Option<&[u8]> { + self.data.ack_wire.as_deref() + } + /// Original commitment identities checked during native activation preparation. + pub fn commitments(&self) -> FFORVoucherCommitments { + self.data.commitments + } + /// Original monitor-owned recovery destination retained during activation preparation. + pub fn recovery_destination(&self) -> &Script { + &self.data.destination_script + } + /// Stable local context binding, excluding acknowledgement presence and later lifecycle state. + /// + /// The digest is an application record key, not a message signature or authorization token. + pub fn context_digest(&self) -> [u8; 32] { + self.context_digest + } +} + +impl fmt::Debug for FFORReceiverRecoveryContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FFORReceiverRecoveryContext") + .field("channel_id", &self.channel_id()) + .field("epoch_id", &self.epoch_id()) + .finish_non_exhaustive() + } +} + +/// An opaque, manager-instance-bound observation of a currently durable Active epoch. +/// +/// Capture requires an exact signed settlement acknowledgement, current native fence and frozen +/// commitment pair, and completion of the epoch's latest persistence requirement. It cannot be +/// restored or constructed by an application. A later native transition or conflicting reconnect +/// can invalidate it immediately. Validate it again when observing current state, and let the +/// manager perform another locked check before any future action. This describes the retained native +/// phase even after its admission deadline; it authorizes neither witness provisioning nor invoices. +#[derive(Clone, Debug)] +pub struct FFORReceiverActiveContext { + pub(crate) recovery: FFORReceiverRecoveryContext, + pub(crate) requirement: FFORPersistenceRequirement, +} + +impl FFORReceiverActiveContext { + /// Historical metadata only. Reading it does not refresh this context's live validity. + pub fn recovery_context(&self) -> &FFORReceiverRecoveryContext { + &self.recovery + } +} diff --git a/lightning/src/ln/ffor_recovery/activation.rs b/lightning/src/ln/ffor_recovery/activation.rs index f688e2e532a..994a3713531 100644 --- a/lightning/src/ln/ffor_recovery/activation.rs +++ b/lightning/src/ln/ffor_recovery/activation.rs @@ -5,6 +5,7 @@ //! persistence before releasing wire. No production activation entry point exists yet. mod close; +mod context; pub(crate) use close::FFORReceiverCloseRecord; use alloc::vec::Vec; diff --git a/lightning/src/ln/ffor_recovery/activation/context.rs b/lightning/src/ln/ffor_recovery/activation/context.rs new file mode 100644 index 00000000000..4c923939ec8 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/context.rs @@ -0,0 +1,30 @@ +//! Authenticated projection of retained activation evidence into public historical observations. + +use super::*; +use crate::ln::ffor::context::{FFORReceiverRecoveryContext, FFORReceiverRecoveryContextData}; + +impl FFORReceiverActivation { + pub(crate) fn receiver_context( + &self, setup: &FFORReceiverSetup, + ) -> Result { + let authenticated = setup.validate_recovery()?; + let hash = self.validate(&authenticated)?; + Ok(FFORReceiverRecoveryContext::from_authenticated( + FFORReceiverRecoveryContextData { + setup: authenticated, + chain_hash: setup.chain_hash(), + receiver: setup.receiver(), + settlement: setup.settlement(), + funding_txo: setup.funding_txo(), + activate_wire: self.activate_wire.clone(), + activation_hash: hash, + ack_wire: self.ack_wire.clone(), + commitments: self.commitments(), + monitor_update_id: self.monitor_update_id, + preparation_height: self.preparation_height, + destination_script: self.destination_script.clone(), + }, + &setup.encode(), + )) + } +} From dec0df9b5ff7e04255e48d7db506fd12a3216a89 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Mon, 21 Sep 2026 00:39:32 -0400 Subject: [PATCH 14/22] Add durable FFOR pre-init admission and synchronous setup --- FFOR.md | 64 +- lightning/src/ln/channel.rs | 2 +- lightning/src/ln/channel/ffor.rs | 44 +- lightning/src/ln/channel/ffor/setup.rs | 27 +- .../src/ln/channel/ffor/setup/request.rs | 375 +++++++++++ .../ln/channel/ffor/setup/request/tests.rs | 96 +++ lightning/src/ln/channelmanager.rs | 26 +- .../src/ln/channelmanager/ffor_activation.rs | 18 +- .../channelmanager/ffor_activation/context.rs | 26 + .../channelmanager/ffor_activation/driver.rs | 509 +++++++++++++++ .../ffor_activation/driver/tests.rs | 600 ++++++++++++++++++ lightning/src/ln/ffor.rs | 9 + lightning/src/ln/ffor/connection.rs | 36 ++ lightning/src/ln/ffor/driver.rs | 103 +++ lightning/src/ln/ffor_recovery.rs | 100 ++- .../src/ln/ffor_recovery/activation/tests.rs | 1 + lightning/src/ln/ffor_recovery/request.rs | 160 +++++ lightning/src/ln/ffor_recovery/tests.rs | 4 +- 18 files changed, 2172 insertions(+), 28 deletions(-) create mode 100644 lightning/src/ln/channel/ffor/setup/request.rs create mode 100644 lightning/src/ln/channel/ffor/setup/request/tests.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/driver.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs create mode 100644 lightning/src/ln/ffor/connection.rs create mode 100644 lightning/src/ln/ffor/driver.rs create mode 100644 lightning/src/ln/ffor_recovery/request.rs diff --git a/FFOR.md b/FFOR.md index b82262a448a..81cc67090aa 100644 --- a/FFOR.md +++ b/FFOR.md @@ -4,9 +4,41 @@ This fork targets rust-lightning v0.2.5. The current APIs provide receiver vouch parking and verification of both commitment views for FFOR Variant D. Private manager transitions compose activation, reconnect, pre-active abort and cooperative voucher drain with ordered storage. -No production transport or invoice API exposes those transitions yet. - -`register_ffor_receiver_setup` authenticates exact signed init and accept messages +An experimental public setup facade now owns pre-init admission and synchronous +Accept processing. No production transport or invoice API exposes the complete +activation and recovery path yet. + +`prepare_ffor_receiver` checks the current authenticated native peer generation, +derives a fresh protocol epoch and signs the exact Init. Before any bytes leave, +it installs a channel-owned interception gate and reserves bounded recovery storage. +`advance_ffor_receiver` releases those bytes only after the ordered manager write +completes, the original connection remains current and the settlement deadline has +not passed. Backpressure preserves the exact retry; successful queue insertion +consumes this one-shot send. Disconnect and restart never replay Init. + +The application's stable `local_request_id` is local correlation, not a protocol +epoch or channel authority. An exact retry returns the retained native selector; +changed channel, peer or policy is refused. A bounded historical lookup recovers +that selector after restart or live-channel removal without granting permission +to send or expose an invoice. + +The peer may send ordinary voucher adds immediately after Accept. +`handle_ffor_receiver_message` therefore processes Accept synchronously under the +native peer lock before custom message handling returns. It installs exact voucher +ownership before those adds arrive. A pre-Accept add is intercepted and aborts +negotiation; it can never be relabelled by a later Accept. If a crash leaves newer +monitor state beside the durable pre-init manager, stock stale-manager recovery +can force-close without exposing an ordinary claimable or forwarded payment. + +The custom handler captures `FFORPeerConnection` only after the native successful +peer-connected callback and pairs it with its own transport generation. It must not +hold a transport mutex across a manager call. The release callback runs under native +authority and may only check its paired token and insert into a bounded queue. +Malformed or contradictory setup, cancellation, disconnect and restart retain the +interception gate. Controlled release requires durable abort state and a fresh +connection, preserving the permanent request and any accepted voucher tombstones. + +The lower-level `register_ffor_receiver_setup` authenticates exact signed init and accept messages against the manager's actual node identity, chain and current height. It checks the empty, connected, synchronized anchor channel, funding terms, commitment number, exact next incoming HTLC ID, fees, dust and reserves before installing the book. @@ -21,9 +53,10 @@ watcher-free eligibility. The setup remains ineligible for offline invoices. `register_ffor_receiver_book` is the lower-level experimental parking API. It checks the public book's structural invariants and exact first incoming HTLC ID, but does -not retain signed setup evidence and cannot authorize later activation. Both APIs -request manager persistence without waiting for a durable write. The application -must persist registration before allowing the settlement peer to send vouchers. +not retain signed setup evidence and cannot authorize later activation. Both +low-level registration APIs require an external barrier preventing peer adds until +registration is durable. The wire protocol provides no post-Accept barrier, so an +operational receiver must use the pre-init facade instead. Registration returns an opaque persistence requirement. The background processor captures a token before encoding the manager and completes it only after its @@ -86,7 +119,11 @@ activation record limit. Capacity is reserved before channel mutation. An Activa record reserves its maximum acknowledgement, both maximum close messages and final completion evidence, including framing, so competing admissions cannot consume its remaining transition storage. Each later phase retains the allowance for its remaining -transitions, recomputed on restore. A terminal abort releases that reservation. +transitions, recomputed on restore. Legacy registrations release that reservation +on terminal abort. Facade requests reserve the full 512 KiB record allowance before +Init and retain it after promotion into accepted setup. Pending requests and their +local retry identity use archive version 4 and required even channel fields; they +share the same global count and byte limits with accepted records. The setup-only archive version retains no activation or claim authority. Fully drained channel tombstones permit subsequent ordinary splicing while retaining the original funding context in their archive. @@ -203,8 +240,9 @@ checks do not claim to detect every arbitrary alteration of local storage. Similarly, an archive without its original live channel cannot independently prove the historical funding context against arbitrary local storage alteration. -No feature bit, production custom-message transport, invoice readiness or automatic -setup deadline is implemented here. The caller must abort a setup that times out. +No feature bit, production custom-message transport, invoice readiness or background +deadline service is implemented here. The facade rechecks the deadline before Init +release and Accept processing; the caller must also cancel a stalled setup. Ordinary channel updates can invalidate a previously returned `Parked` proof. `NodeSigner::sign_ffor_message` supplies the protocol's single-SHA256 `ffor/msg` @@ -282,6 +320,14 @@ Read-only context tests cover both funders, absent ACK, pending and current pers foreign/restored manager rejection, offline Active observation, conflicting reconnect, and stable historical binding through close and archive-only force-close recovery. +The setup facade tests use real two-node channels in both funding directions. They +cover pending or failed storage, exact backpressure retries, synchronous Accept followed +immediately by stock voucher frames, and a crash after monitor persistence but before +accepted-manager persistence. Other cases cover pre-Accept adds, contradictory signed +replies, deadline crossing, retry identity after restore, missing archive evidence, +capacity refusal, witness policy, cancellation and normal payment after controlled +gate release on a fresh connection. + Native witness tests use four pinned Beignet records to compare ECDH, HKDF, plaintext and preimages; they reject signed ciphertext, ephemeral-key, AAD, manifest and plaintext-term substitution. The shared crate separately covers all six reference scenarios and arbitrary diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index b974bcfa2f1..faf59a71564 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -104,7 +104,7 @@ mod ffor; pub(crate) use ffor::ffor_setup_test_messages; pub(crate) use ffor::{ ffor_drain_completion_digest, FFORReceiverDrainCompletion, FFORReceiverFencePhase, - FFORReceiverSetup, FFORReestablishOutcome, + FFORReceiverRequest, FFORReceiverSetup, FFORReestablishOutcome, }; #[cfg(any(test, feature = "_test_utils"))] diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index cd8f21845c3..5ff91dc7eff 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -19,7 +19,7 @@ pub(crate) use quiescence::FFORReceiverQuiescence; pub(crate) use reestablish::FFORReestablishOutcome; #[cfg(test)] pub(crate) use setup::ffor_setup_test_messages; -pub(crate) use setup::FFORReceiverSetup; +pub(crate) use setup::{FFORReceiverRequest, FFORReceiverSetup}; /// Channel-owned identity survives the stock inbound HTLC's transition to `Committed`. pub(super) struct FFORReceiverBook { @@ -30,6 +30,8 @@ pub(super) struct FFORReceiverBook { setup: Option, fence: Option, drain: Option, + request: Option, + request_gate_released: Option, } struct FFORReceivedVoucher { @@ -53,6 +55,9 @@ impl_writeable_tlv_based!(FFORReceiverBook, { (10, fence, option), // Older readers must not discard drain ownership or a completed epoch tombstone. (12, drain, option), + // Pre-init interception must never be discarded by older readers. + (14, request, option), + (16, request_gate_released, option), }); impl FFORReceiverBook { @@ -65,7 +70,26 @@ impl FFORReceiverBook { pub(super) fn restored( &mut self, inbound_htlcs: &[InboundHTLCOutput], ) -> Result<(), DecodeError> { - verification::validate_vouchers(&self.vouchers).map_err(|_| DecodeError::InvalidValue)?; + if self.request_gate_released == Some(false) + || (self.request_gate_released.is_some() && self.request.is_none()) + { + return Err(DecodeError::InvalidValue); + } + if self.vouchers.is_empty() { + let request = self.request.as_ref().ok_or(DecodeError::InvalidValue)?; + let message = request.validate_recovery()?; + if self.setup.is_some() + || self.fence.is_some() + || self.drain.is_some() + || message.header.epoch_id != self.epoch_id + || (!self.received.is_empty() && self.abort_reason.is_none()) + { + return Err(DecodeError::InvalidValue); + } + } else { + verification::validate_vouchers(&self.vouchers) + .map_err(|_| DecodeError::InvalidValue)?; + } let mut ids = alloc::collections::BTreeSet::new(); if self.received.len() > 483 || self.received.iter().any(|received| !ids.insert(received.voucher.htlc_id)) @@ -102,7 +126,8 @@ impl FFORReceiverBook { { return Err(DecodeError::InvalidValue); } - } else if (self.abort_reason.is_none() && !self.is_closed()) + } else if (self.request.is_some() && !self.request_gate_released.unwrap_or(false)) + || (self.abort_reason.is_none() && !self.is_closed()) || self.vouchers.iter().any(|voucher| voucher.payment_hash == htlc.payment_hash) { // Never restore a live voucher whose channel-owned interception record was lost. @@ -149,6 +174,8 @@ where setup: None, fence: None, drain: None, + request: None, + request_gate_released: None, }); Ok(()) } @@ -175,6 +202,9 @@ where }, ); } + if book.setup.is_none() && book.request.is_some() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } let parked = book.received.iter().filter(|received| received.failure.is_some()).count(); if parked != book.vouchers.len() { return Ok(FFORReceiverStatus::Registered { @@ -202,7 +232,7 @@ where if book.epoch_id != epoch_id { return Err(FFORReceiverError::UnknownEpoch); } - if book.fence.is_some() { + if book.fence.is_some() || book.request.is_some() { return Err(FFORCommitmentError::PendingUpdates.into()); } book.abort(FFORReceiverAbortReason::Requested); @@ -233,7 +263,11 @@ where }); let reserved_hash = book.vouchers.iter().any(|voucher| voucher.payment_hash == msg.payment_hash); - if (book.abort_reason.is_some() || book.is_closed()) && !reserved_hash && !owned_pending { + if (book.abort_reason.is_some() || book.is_closed()) + && !reserved_hash + && !owned_pending + && !(book.request.is_some() && !book.request_gate_released.unwrap_or(false)) + { return; } let voucher = FFORVoucher { diff --git a/lightning/src/ln/channel/ffor/setup.rs b/lightning/src/ln/channel/ffor/setup.rs index 0195a658b47..ed12147a163 100644 --- a/lightning/src/ln/channel/ffor/setup.rs +++ b/lightning/src/ln/channel/ffor/setup.rs @@ -10,6 +10,9 @@ use lightning_ffor::book::{validate_anchor_book, AnchorChannelLimits}; use lightning_ffor::setup::AuthenticatedSetup; use lightning_ffor::wire::{Message, Payload, MAX_MESSAGE_LEN}; +mod request; +pub(crate) use request::FFORReceiverRequest; + // This experimental admission path scans shachain under the channel lock. Older histories // require a future durable digest index; refuse them before doing unbounded work. const MAX_SETUP_COMMITMENT_HISTORY: u64 = 4096; @@ -241,7 +244,23 @@ where cltv_expiry: voucher.expiry, }) .collect(); - self.register_ffor_receiver_book(setup.header().epoch_id, &vouchers)?; + if let Some(book) = self.context.ffor_receiver_book.as_ref() { + let request = book.request.as_ref().ok_or(FFORReceiverError::AlreadyRegistered)?; + if !request.validates_setup(&record) + || book.abort_reason.is_some() + || book.setup.is_some() + || !book.received.is_empty() + || book.request_gate_released.unwrap_or(false) + { + return Err(invalid_setup()); + } + self.check_ffor_synchronized()?; + self.ffor_validate_request(true).map_err(|_| invalid_setup())?; + verification::validate_vouchers(&vouchers)?; + self.context.ffor_receiver_book.as_mut().unwrap().vouchers = vouchers; + } else { + self.register_ffor_receiver_book(setup.header().epoch_id, &vouchers)?; + } self.context.ffor_receiver_book.as_mut().unwrap().setup = Some(record); Ok(()) } @@ -342,6 +361,11 @@ where pub(crate) fn ffor_validate_receiver_identity( &self, our_node_id: PublicKey, chain_hash: ChainHash, ) -> Result<(), DecodeError> { + if let Some(request) = self.ffor_receiver_request() { + if request.receiver() != our_node_id || request.chain_hash() != chain_hash { + return Err(DecodeError::InvalidValue); + } + } if let Some(record) = self.context.ffor_receiver_book.as_ref().and_then(|book| book.setup.as_ref()) { @@ -366,6 +390,7 @@ where } pub(crate) fn ffor_validate_receiver_setup(&self) -> Result<(), DecodeError> { + self.ffor_validate_request(false)?; let book = match self.context.ffor_receiver_book.as_ref() { Some(book) => book, None => return Ok(()), diff --git a/lightning/src/ln/channel/ffor/setup/request.rs b/lightning/src/ln/channel/ffor/setup/request.rs new file mode 100644 index 00000000000..323f0e1b420 --- /dev/null +++ b/lightning/src/ln/channel/ffor/setup/request.rs @@ -0,0 +1,375 @@ +//! Immutable pre-init admission evidence. This gate must be durable before Init can leave. + +use super::*; +use lightning_ffor::amounts::FeePolicy; +use lightning_ffor::book::BookTerms; + +#[derive(Clone)] +pub(crate) struct FFORReceiverRequest { + init_wire: Vec, + local_request_id: [u8; 32], + receiver: PublicKey, + settlement: PublicKey, + chain_hash: ChainHash, + funding_txo: OutPoint, + channel_value_sat: u64, + receiver_balance_msat: u64, + feerate_sat_per_kw: u32, + settlement_is_funder: bool, + admission_height: u32, + claim_margin_blocks: u32, + counterparty_commitment_number: u64, + incoming_htlc_id: u64, +} + +impl_writeable_tlv_based!(FFORReceiverRequest, { + (0, init_wire, required_vec), (2, receiver, required), (4, settlement, required), + (6, chain_hash, required), (8, funding_txo, required), (10, channel_value_sat, required), + (12, receiver_balance_msat, required), (14, feerate_sat_per_kw, required), + (16, settlement_is_funder, required), (18, admission_height, required), + (20, claim_margin_blocks, required), (22, counterparty_commitment_number, required), + (24, incoming_htlc_id, required), + (26, local_request_id, required), +}); + +impl FFORReceiverRequest { + pub(crate) fn validate_recovery(&self) -> Result { + let message = Message::decode(&self.init_wire).map_err(|_| DecodeError::InvalidValue)?; + message.verify_signature(&self.receiver).map_err(|_| DecodeError::InvalidValue)?; + let terms = match &message.payload { + Payload::Init(init) => Self::terms(init), + _ => return Err(DecodeError::InvalidValue), + }; + if self.receiver == self.settlement + || self.claim_margin_blocks == 0 + || self.counterparty_commitment_number > INITIAL_COMMITMENT_NUMBER + || self.incoming_htlc_id.checked_add(terms.amounts_msat.len() as u64).is_none() + || self + .channel_value_sat + .checked_mul(1000) + .map_or(true, |total| self.receiver_balance_msat > total) + || self.admission_height >= terms.settlement_deadline + || terms.voucher_expiry >= LOCK_TIME_THRESHOLD + || terms + .settlement_deadline + .checked_add(self.claim_margin_blocks) + .map_or(true, |height| height > terms.voucher_expiry) + { + return Err(DecodeError::InvalidValue); + } + Ok(message) + } + + fn terms(init: &lightning_ffor::wire::Init) -> BookTerms { + BookTerms { + amounts_msat: init.amounts_msat.clone(), + budget_msat: init.budget_msat, + minimum_payment_msat: init.min_payment_msat, + fees: FeePolicy { + base_msat: init.fee_base_msat, + proportional_millionths: init.fee_proportional_millionths, + }, + settlement_deadline: init.settlement_deadline, + voucher_expiry: init.voucher_expiry, + } + } + + fn setup(&self, accept_wire: &[u8]) -> FFORReceiverSetup { + FFORReceiverSetup { + init_wire: self.init_wire.clone(), + accept_wire: accept_wire.to_vec(), + receiver: self.receiver, + settlement: self.settlement, + chain_hash: self.chain_hash, + funding_txo: self.funding_txo, + channel_value_sat: self.channel_value_sat, + receiver_balance_msat: self.receiver_balance_msat, + feerate_sat_per_kw: self.feerate_sat_per_kw, + settlement_is_funder: self.settlement_is_funder, + admission_height: self.admission_height, + claim_margin_blocks: self.claim_margin_blocks, + } + } + + pub(crate) fn validates_setup(&self, setup: &FFORReceiverSetup) -> bool { + self.validate_recovery().is_ok() + && self.setup(&setup.accept_wire).encode() == setup.encode() + && setup.validate_recovery().map_or(false, |authenticated| { + match &authenticated.accept().payload { + Payload::Accept(accept) => { + accept.s_commitment_number == self.counterparty_commitment_number + && accept.s_htlc_id_base == self.incoming_htlc_id + }, + _ => false, + } + }) + } + pub(crate) fn local_request_id(&self) -> [u8; 32] { + self.local_request_id + } + pub(crate) fn matches_intent( + &self, channel_id: ChannelId, peer: PublicKey, + parameters: &crate::ln::ffor::FFORReceiverParameters, + ) -> bool { + let message = match self.validate_recovery() { + Ok(message) => message, + Err(_) => return false, + }; + self.settlement == peer + && message.header.channel_id == channel_id.0 + && self.local_request_id == parameters.local_request_id + && self.claim_margin_blocks == parameters.claim_margin_blocks + && parameters.init().map_or(false, |init| message.payload == Payload::Init(init)) + } + pub(crate) fn init_wire(&self) -> &[u8] { + &self.init_wire + } + pub(crate) fn receiver(&self) -> PublicKey { + self.receiver + } + pub(crate) fn chain_hash(&self) -> ChainHash { + self.chain_hash + } + pub(crate) fn funding_txo(&self) -> OutPoint { + self.funding_txo + } +} + +impl FundedChannel +where + SP::Target: SignerProvider, +{ + pub(crate) fn prepare_ffor_receiver_request( + &self, init_wire: &[u8], receiver: PublicKey, chain_hash: ChainHash, current_height: u32, + claim_margin_blocks: u32, local_request_id: [u8; 32], + ) -> Result { + if self.context.ffor_receiver_book.is_some() { + return Err(FFORReceiverError::AlreadyRegistered); + } + self.check_ffor_synchronized()?; + if !self.context.is_connected() + || !self.context.pending_inbound_htlcs.is_empty() + || !self.context.pending_outbound_htlcs.is_empty() + { + return Err(invalid_setup()); + } + let request = FFORReceiverRequest { + init_wire: init_wire.to_vec(), + local_request_id, + receiver, + settlement: self.context.counterparty_node_id, + chain_hash, + funding_txo: self.funding.get_funding_txo().ok_or_else(invalid_setup)?, + channel_value_sat: self.funding.get_value_satoshis(), + receiver_balance_msat: self.funding.value_to_self_msat, + feerate_sat_per_kw: self.context.feerate_per_kw, + settlement_is_funder: !self.funding.is_outbound(), + admission_height: current_height, + claim_margin_blocks, + counterparty_commitment_number: self.ffor_counterparty_commitment_number()?, + incoming_htlc_id: self.context.next_counterparty_htlc_id, + }; + let message = request.validate_recovery().map_err(|_| invalid_setup())?; + if message.header.channel_id != self.context.channel_id().0 { + return Err(invalid_setup()); + } + let terms = match &message.payload { + Payload::Init(init) => FFORReceiverRequest::terms(init), + _ => return Err(invalid_setup()), + }; + validate_anchor_book( + &terms, + &self.ffor_setup_limits(&request.setup(&[]))?, + current_height, + claim_margin_blocks, + ) + .map_err(|_| invalid_setup())?; + checked_history_start(self.context.commitment_secrets.get_min_seen_secret())?; + Ok(request) + } + + pub(crate) fn install_ffor_receiver_request( + &mut self, request: FFORReceiverRequest, + ) -> Result<(), FFORReceiverError> { + let current = self.prepare_ffor_receiver_request( + &request.init_wire, + request.receiver, + request.chain_hash, + request.admission_height, + request.claim_margin_blocks, + request.local_request_id, + )?; + if current.encode() != request.encode() { + return Err(invalid_setup()); + } + let epoch_id = request.validate_recovery().map_err(|_| invalid_setup())?.header.epoch_id; + self.context.ffor_receiver_book = Some(FFORReceiverBook { + epoch_id, + vouchers: Vec::new(), + received: Vec::new(), + abort_reason: None, + setup: None, + fence: None, + drain: None, + request: Some(request), + request_gate_released: None, + }); + Ok(()) + } + + pub(crate) fn ffor_validate_pending_request(&self) -> Result<(), DecodeError> { + self.check_ffor_synchronized().map_err(|_| DecodeError::InvalidValue)?; + self.ffor_validate_request(true) + } + + pub(crate) fn abort_ffor_receiver_request( + &mut self, reason: FFORReceiverAbortReason, + ) -> Result<(), FFORReceiverError> { + let book = + self.context.ffor_receiver_book.as_mut().ok_or(FFORReceiverError::NotRegistered)?; + if book.request.is_none() || book.fence.is_some() || book.is_closed() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + book.abort(reason); + Ok(()) + } + + pub(crate) fn release_ffor_receiver_request_gate(&mut self) -> Result { + let book = + self.context.ffor_receiver_book.as_mut().ok_or(FFORReceiverError::NotRegistered)?; + if book.request.is_none() || book.abort_reason.is_none() || book.fence.is_some() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let changed = !book.request_gate_released.unwrap_or(false); + book.request_gate_released = Some(true); + Ok(changed) + } + + pub(crate) fn ffor_receiver_request_gate_released(&self) -> bool { + self.context + .ffor_receiver_book + .as_ref() + .map_or(false, |book| book.request_gate_released.unwrap_or(false)) + } + + pub(crate) fn ffor_receiver_request(&self) -> Option<&FFORReceiverRequest> { + self.context.ffor_receiver_book.as_ref().and_then(|book| book.request.as_ref()) + } + + pub(crate) fn prepare_ffor_receiver_accept( + &self, accept_wire: &[u8], height: u32, + ) -> Result { + let book = + self.context.ffor_receiver_book.as_ref().ok_or(FFORReceiverError::NotRegistered)?; + let request = book.request.as_ref().ok_or(FFORReceiverError::NotRegistered)?; + if book.abort_reason.is_some() + || book.setup.is_some() + || !book.received.is_empty() + || book.request_gate_released.unwrap_or(false) + { + return Err(invalid_setup()); + } + self.check_ffor_synchronized()?; + let setup = request.setup(accept_wire); + let authenticated = setup.validate_record()?; + if height >= authenticated.terms().settlement_deadline { + return Err(invalid_setup()); + } + let accept = match &authenticated.accept().payload { + Payload::Accept(accept) => accept, + _ => return Err(invalid_setup()), + }; + if accept.s_commitment_number != request.counterparty_commitment_number + || accept.s_htlc_id_base != request.incoming_htlc_id + { + return Err(invalid_setup()); + } + self.ffor_validate_request(true).map_err(|_| invalid_setup())?; + Ok(setup) + } + + pub(super) fn ffor_validate_request(&self, current: bool) -> Result<(), DecodeError> { + let book = match self.context.ffor_receiver_book.as_ref() { + Some(book) => book, + None => return Ok(()), + }; + let request = match book.request.as_ref() { + Some(request) => request, + None => return Ok(()), + }; + let message = request.validate_recovery()?; + if message.header.channel_id != self.context.channel_id().0 + || message.header.epoch_id != book.epoch_id + || request.settlement != self.context.counterparty_node_id + { + return Err(DecodeError::InvalidValue); + } + if let Some(setup) = book.setup.as_ref() { + if !request.validates_setup(setup) + || (book.request_gate_released.unwrap_or(false) + && (book.abort_reason.is_none() || book.fence.is_some())) + { + return Err(DecodeError::InvalidValue); + } + return Ok(()); + } + if !book.vouchers.is_empty() + || book.fence.is_some() + || book.drain.is_some() + || (book.request_gate_released.unwrap_or(false) && book.abort_reason.is_none()) + { + return Err(DecodeError::InvalidValue); + } + let owned = self.context.pending_inbound_htlcs.iter().any(|htlc| book.owns(htlc.htlc_id)); + if current || !book.request_gate_released.unwrap_or(false) || owned { + if request.funding_txo + != self.funding.get_funding_txo().ok_or(DecodeError::InvalidValue)? + || request.channel_value_sat != self.funding.get_value_satoshis() + || request.settlement_is_funder == self.funding.is_outbound() + { + return Err(DecodeError::InvalidValue); + } + } + if current || !book.request_gate_released.unwrap_or(false) || owned { + let channel_type = self.funding.get_channel_type(); + if !channel_type.supports_static_remote_key() + || !channel_type.supports_anchors_zero_fee_htlc_tx() + || channel_type.supports_taproot() + || channel_type.supports_anchor_zero_fee_commitments() + { + return Err(DecodeError::InvalidValue); + } + let terms = match &message.payload { + Payload::Init(init) => FFORReceiverRequest::terms(init), + _ => return Err(DecodeError::InvalidValue), + }; + let limits = self + .ffor_setup_limits(&request.setup(&[])) + .map_err(|_| DecodeError::InvalidValue)?; + validate_anchor_book( + &terms, + &limits, + request.admission_height, + request.claim_margin_blocks, + ) + .map_err(|_| DecodeError::InvalidValue)?; + } + if current + && (request.receiver_balance_msat != self.funding.value_to_self_msat + || request.feerate_sat_per_kw != self.context.feerate_per_kw + || request.counterparty_commitment_number + != self + .ffor_counterparty_commitment_number() + .map_err(|_| DecodeError::InvalidValue)? + || request.incoming_htlc_id != self.context.next_counterparty_htlc_id + || !self.context.pending_inbound_htlcs.is_empty() + || !self.context.pending_outbound_htlcs.is_empty()) + { + return Err(DecodeError::InvalidValue); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/channel/ffor/setup/request/tests.rs b/lightning/src/ln/channel/ffor/setup/request/tests.rs new file mode 100644 index 00000000000..a1c199030b4 --- /dev/null +++ b/lightning/src/ln/channel/ffor/setup/request/tests.rs @@ -0,0 +1,96 @@ +use super::*; +use crate::ln::channel::ffor::{FFORReceivedVoucher, FFORReceiverDrain, FFORReceiverFence}; +use crate::ln::ffor::FFORReceiverParameters; +use crate::ln::ffor_tests::anchor_config; +use crate::ln::functional_test_utils::*; + +// Exact previous book schema: an older reader understands parking, setup, fence and drain, +// but must refuse the new required request gate rather than accepting an empty ordinary book. +struct PreviousBook { + epoch_id: [u8; 32], + vouchers: Vec, + received: Vec, + abort_reason: Option, + setup: Option, + fence: Option, + drain: Option, +} +impl_writeable_tlv_based!(PreviousBook, { + (0, epoch_id, required), (2, vouchers, required_vec), (4, received, required_vec), + (6, abort_reason, option), (8, setup, option), (10, fence, option), (12, drain, option), +}); + +#[test] +fn ffor_driver_request_gate_restore_and_previous_reader_refusal() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let channel_id = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let connection = nodes[1].node.ffor_peer_connection(&nodes[0].node.get_our_node_id()).unwrap(); + nodes[1] + .node + .prepare_ffor_receiver( + &channel_id, + &connection, + FFORReceiverParameters { + local_request_id: [34; 32], + amounts_msat: vec![2_000_000], + minimum_payment_msat: 2_000_000, + settlement_deadline: 180, + voucher_expiry: 200, + fee_base_msat: 0, + fee_proportional_millionths: 0, + claim_margin_blocks: 20, + witness_peers: None, + hash_chain: false, + }, + ) + .unwrap(); + let peers = nodes[1].node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get_mut(&channel_id).unwrap().as_funded_mut().unwrap(); + let original = channel.context.ffor_receiver_book.as_ref().unwrap().encode(); + assert!(matches!( + PreviousBook::read(&mut &original[..]), + Err(DecodeError::UnknownRequiredFeature) + )); + let features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + for mutation in 0..3 { + let book = channel.context.ffor_receiver_book.as_mut().unwrap(); + match mutation { + 0 => book.request = None, + 1 => book.request_gate_released = Some(true), + _ => book.request_gate_released = Some(false), + } + let damaged = channel.encode(); + channel.context.ffor_receiver_book = + Some(FFORReceiverBook::read(&mut &original[..]).unwrap()); + assert!(matches!( + FundedChannel::read( + &mut &damaged[..], + (&nodes[1].keys_manager, &nodes[1].keys_manager, &features) + ), + Err(DecodeError::InvalidValue) + )); + } + let ordinary = PreviousBook { + epoch_id: [2; 32], + vouchers: vec![FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([3; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }], + received: vec![], + abort_reason: None, + setup: None, + fence: None, + drain: None, + } + .encode(); + // Absence of request metadata retains byte compatibility with the preceding book format. + assert_eq!(FFORReceiverBook::read(&mut &ordinary[..]).unwrap().encode(), ordinary); +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 15eb2fa8bec..944d3d633f7 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1646,6 +1646,8 @@ where /// [`BaseMessageHandler::peer_connected`] and no corresponding /// [`BaseMessageHandler::peer_disconnected`]. pub is_connected: bool, + /// Transient authenticated generation. Never persisted or numerically reused. + ffor_connection: Option>, /// Holds the peer storage data for the channel partner on a per-peer basis. peer_storage: Vec, } @@ -4362,8 +4364,11 @@ where /// `claim_margin_blocks` is local deployment policy and must not come from the peer. Invalid /// signatures, incompatible terms, or unsupported channel history leave the book unregistered. /// - /// Await the returned persistence requirement before telling the peer to send vouchers. This - /// retains setup evidence but does not activate or freeze the channel, or authorize an invoice. + /// This low-level method requires an external barrier preventing peer HTLCs before registration. + /// The wire protocol has no post-Accept barrier; operational callers must instead stage native + /// pre-init admission with `prepare_ffor_receiver` and process Accept synchronously with + /// `handle_ffor_receiver_message`. This retains setup evidence but does not activate or freeze + /// the channel, or authorize an invoice. /// The same abort, restart and one-registration limits as [`Self::register_ffor_receiver_book`] /// apply. Raw-book registration does not supply the authenticated evidence retained here. pub fn register_ffor_receiver_setup( @@ -14090,6 +14095,7 @@ where fn peer_disconnected(&self, counterparty_node_id: PublicKey) { let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { let mut splice_failed_events = Vec::new(); + let mut ffor_persist = false; let mut failed_channels: Vec<(Result, _)> = Vec::new(); let mut per_peer_state = self.per_peer_state.write().unwrap(); let remove_peer = { @@ -14101,6 +14107,7 @@ where if let Some(peer_state_mutex) = per_peer_state.get(&counterparty_node_id) { let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; + ffor_persist = self.ffor_receiver_setup_disconnected(peer_state); let pending_msg_events = &mut peer_state.pending_msg_events; peer_state.channel_by_id.retain(|_, chan| { let logger = WithChannelContext::from(&self.logger, &chan.context(), None); @@ -14194,6 +14201,7 @@ where }); debug_assert!(peer_state.is_connected, "A disconnected peer cannot disconnect"); peer_state.is_connected = false; + peer_state.ffor_connection = None; peer_state.ok_to_remove(true) } else { debug_assert!(false, "Unconnected peer disconnected"); true } }; @@ -14202,7 +14210,7 @@ where } mem::drop(per_peer_state); - let persist = if splice_failed_events.is_empty() { + let persist = if splice_failed_events.is_empty() && !ffor_persist { NotifyOption::SkipPersistHandleEvents } else { let mut pending_events = self.pending_events.lock().unwrap(); @@ -14256,6 +14264,7 @@ where actions_blocking_raa_monitor_updates: BTreeMap::new(), closed_channel_monitor_update_ids: BTreeMap::new(), is_connected: true, + ffor_connection: Some(Arc::new(())), peer_storage: Vec::new(), })); }, @@ -14277,6 +14286,7 @@ where debug_assert!(!peer_state.is_connected, "A peer shouldn't be connected twice"); peer_state.is_connected = true; + peer_state.ffor_connection = Some(Arc::new(())); }, } } @@ -17162,10 +17172,12 @@ where closed_channel_monitor_update_ids: BTreeMap::new(), peer_storage: Vec::new(), is_connected: false, + ffor_connection: None, }; let mut failed_htlcs = Vec::new(); let mut ffor_channel_setups = Vec::new(); + let mut ffor_channel_requests = Vec::new(); let channel_count: u64 = Readable::read(reader)?; let mut channel_id_set = hash_set_with_capacity(cmp::min(channel_count as usize, 128)); let mut per_peer_state = hash_map_with_capacity(cmp::min( @@ -17185,6 +17197,9 @@ where ), )?; channel.ffor_validate_receiver_identity(our_network_pubkey, chain_hash)?; + if let Some(request) = channel.ffor_receiver_request() { + ffor_channel_requests.push(request.clone()); + } if let Some(setup) = channel.ffor_receiver_setup_record()? { ffor_channel_setups.push(( setup, @@ -17597,6 +17612,11 @@ where let mut ffor_persistence = FFORPersistenceBarrier::new(); let ffor_activation = FFORReceiverRuntime::restored(&ffor_recovery, &mut ffor_persistence)?; ffor_recovery.validate_identity(our_network_pubkey, chain_hash)?; + for request in &ffor_channel_requests { + if !ffor_recovery.contains_request(request) { + return Err(DecodeError::InvalidValue); + } + } for (setup, fence, abort_reason, drain, completion, drain_hash) in &ffor_channel_setups { ffor_recovery.validate_channel_lifecycle( setup, diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs index 198e7105432..82f7afbc2cb 100644 --- a/lightning/src/ln/channelmanager/ffor_activation.rs +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -11,12 +11,15 @@ use lightning_ffor::wire::{Activate, Message as FFORMessage, Payload}; mod close; mod context; +mod driver; mod reestablish; struct RuntimeEntry { key: FFORRecoveryKey, requirement: FFORPersistenceRequirement, may_send_activate: bool, + request_connection: Option, + may_send_init: bool, } /// Bounded by the retained registry's keys. No bytes or phase authority are copied here. @@ -33,7 +36,12 @@ impl FFORReceiverRuntime { recovery: &FFORRecoveryRegistry, barrier: &mut FFORPersistenceBarrier, ) -> Result { let mut runtime = Self::new(); - let keys = recovery.activation_keys(); + let mut keys = recovery.activation_keys(); + for key in recovery.request_keys() { + if !keys.contains(&key) { + keys.push(key); + } + } if !keys.is_empty() { let requirement = barrier.request().map_err(|_| DecodeError::InvalidValue)?; for key in keys { @@ -54,7 +62,13 @@ impl FFORReceiverRuntime { &mut self, key: FFORRecoveryKey, requirement: FFORPersistenceRequirement, may_send_activate: bool, ) { - let entry = RuntimeEntry { key, requirement, may_send_activate }; + let entry = RuntimeEntry { + key, + requirement, + may_send_activate, + request_connection: None, + may_send_init: false, + }; if let Some(existing) = self.entries.iter_mut().find(|existing| existing.key == key) { *existing = entry; } else { diff --git a/lightning/src/ln/channelmanager/ffor_activation/context.rs b/lightning/src/ln/channelmanager/ffor_activation/context.rs index 14a75d4b941..beed9a37b5d 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/context.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/context.rs @@ -25,6 +25,32 @@ where MR::Target: MessageRouter, L::Target: Logger, { + /// Capture the generation of an already-authenticated native peer connection. + /// + /// The custom message handler calls this after its native `peer_connected` callback succeeds. + /// Supply the result with exact incoming bytes to synchronous receiver handling. A transport + /// queue must retain its own connection token as well; this observation alone cannot authorize + /// a future queue insertion. Disconnected, rejected and restored peers have no generation. + pub fn ffor_peer_connection( + &self, counterparty_node_id: &PublicKey, + ) -> Result { + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(counterparty_node_id) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + if !peer.is_connected { + return Err(FFORCommitmentError::ChannelUnavailable.into()); + } + let generation = + peer.ffor_connection.as_ref().ok_or(FFORCommitmentError::ChannelUnavailable)?; + Ok(crate::ln::ffor::FFORPeerConnection { + peer: *counterparty_node_id, + generation: Arc::clone(generation), + }) + } + /// Return authenticated historical activation evidence, including an archive-only epoch. /// This neither requires nor establishes current Active authority or durable readiness. pub fn ffor_receiver_recovery_context( diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver.rs b/lightning/src/ln/channelmanager/ffor_activation/driver.rs new file mode 100644 index 00000000000..d47001edaf5 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/driver.rs @@ -0,0 +1,509 @@ +//! Operational pre-init admission and synchronous Accept. Native state owns every wire decision. + +use super::*; +use crate::ln::ffor::{ + FFORPeerConnection, FFORReceiverAbortReason, FFORReceiverId, FFORReceiverParameters, + FFORReceiverProgress, +}; +use lightning_ffor::wire::Header; + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Stage receiver-owned interception and exact signed Init before any peer traffic is released. + /// + /// The supplied connection must be the current authenticated native generation. The channel + /// must be supported, empty and synchronized. The engine generates the epoch, checks native + /// limits and reserves bounded storage for the eventual accepted transcript and terminal state. + /// No bytes are returned. Call `advance_ffor_receiver` after ordered manager persistence. + /// This experimental setup facade does not activate offline receiving or authorize invoices. + pub fn prepare_ffor_receiver( + &self, channel_id: &ChannelId, connection: &FFORPeerConnection, + parameters: FFORReceiverParameters, + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(&connection.peer) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + if !peer.is_connected || !connection.matches(connection.peer, peer.ffor_connection.as_ref()) + { + return Err(FFORCommitmentError::ChannelUnavailable.into()); + } + { + let recovery = self.ffor_recovery.lock().unwrap(); + if let Some(request) = recovery.find_request(parameters.local_request_id) { + if !request.matches_intent(*channel_id, connection.peer, ¶meters) { + return Err(FFORReceiverError::AlreadyRegistered); + } + let header = request + .validate_recovery() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + .header; + return Ok(FFORReceiverId { + channel_id: ChannelId(header.channel_id), + epoch_id: header.epoch_id, + }); + } + } + let channel = peer + .channel_by_id + .get_mut(channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let epoch_id = self.entropy_source.get_secure_random_bytes(); + let mut message = FFORMessage { + header: Header { channel_id: channel_id.0, epoch_id }, + payload: Payload::Init(parameters.init()?), + extensions: Vec::new(), + signature: [0; 64], + }; + let unsigned = + message.unsigned_wire().map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + message.signature = self + .node_signer + .sign_ffor_message( + &FFORSigningRequest::new(&unsigned) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?, + ) + .map_err(|_| FFORReceiverError::SignerUnavailable)? + .serialize_compact(); + let wire = message.encode().map_err(|_| FFORReceiverError::SignerUnavailable)?; + let height = self.best_block.read().unwrap().height; + let request = channel.prepare_ffor_receiver_request( + &wire, + self.our_network_pubkey, + self.chain_hash, + height, + parameters.claim_margin_blocks, + parameters.local_request_id, + )?; + let mut recovery = self.ffor_recovery.lock().unwrap(); + let permit = recovery + .prepare_request(&request) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.install_ffor_receiver_request(request)?; + permit.commit(); + let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; + let mut runtime = self.ffor_activation.lock().unwrap(); + runtime.record(key, requirement, false); + let entry = runtime.entries.iter_mut().find(|entry| entry.key == key).unwrap(); + entry.request_connection = Some(connection.clone()); + entry.may_send_init = true; + Ok(FFORReceiverId { channel_id: *channel_id, epoch_id }) + } + + /// Recover an existing native selector after an ambiguous application retry or restart. + /// Searches at most 64 retained requests, including removed channels. The result is historical + /// correlation only; it neither changes a phase nor permits retransmission or invoice exposure. + pub fn find_ffor_receiver_request( + &self, local_request_id: [u8; 32], + ) -> Result, FFORReceiverError> { + let _guard = self.total_consistency_lock.read().unwrap(); + let recovery = self.ffor_recovery.lock().unwrap(); + recovery + .validate_identity(self.our_network_pubkey, self.chain_hash) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + recovery + .find_request(local_request_id) + .map(|request| { + let header = request + .validate_recovery() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + .header; + Ok(FFORReceiverId { + channel_id: ChannelId(header.channel_id), + epoch_id: header.epoch_id, + }) + }) + .transpose() + } + + /// Release at most one exact Init after its pre-init gate is durable on the original connection. + /// + /// `enqueue` must atomically verify its paired authenticated transport generation and insert + /// these bytes into a bounded queue. It must not perform I/O, acquire a monitor or reenter this + /// manager. Returning `Err(())` preserves the exact retry; `Ok(())` consumes this one-shot send. + /// A disconnected or restored negotiation never sends Init again. Accepted setup reports only + /// voucher-round progress here; activation and invoice readiness are not exposed by this slice. + pub fn advance_ffor_receiver( + &self, id: &FFORReceiverId, connection: &FFORPeerConnection, enqueue: C, + ) -> Result + where + C: FnOnce(&[u8]) -> Result<(), ()>, + { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(&connection.peer) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + if !peer.is_connected || !connection.matches(connection.peer, peer.ffor_connection.as_ref()) + { + return Err(FFORCommitmentError::ChannelUnavailable.into()); + } + let channel = peer + .channel_by_id + .get_mut(&id.channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let request = channel.ffor_receiver_request().ok_or(FFORReceiverError::NotRegistered)?; + let init = + request.validate_recovery().map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if init.header.epoch_id != id.epoch_id { + return Err(FFORReceiverError::UnknownEpoch); + } + let key = FFORRecoveryKey { channel_id: id.channel_id, epoch_id: id.epoch_id }; + let recovery = self.ffor_recovery.lock().unwrap(); + if !recovery.contains_request(request) { + return Err(FFORReceiverError::RecoveryUnavailable); + } + let mut runtime = self.ffor_activation.lock().unwrap(); + let entry = runtime + .entries + .iter_mut() + .find(|entry| entry.key == key) + .ok_or(FFORReceiverError::RecoveryUnavailable)?; + if !self.ffor_persistence.lock().unwrap().is_complete(&entry.requirement) { + return Ok(FFORReceiverProgress::AwaitingPersistence); + } + if let Some(reason) = channel.ffor_receiver_abort_reason() { + if !channel.ffor_receiver_request_gate_released() + && entry.request_connection.as_ref() != Some(connection) + { + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.release_ffor_receiver_request_gate()?; + entry.requirement = requirement; + channel.ffor_queue_aborted_vouchers(&self.logger); + return Ok(FFORReceiverProgress::AwaitingPersistence); + } + return Ok(FFORReceiverProgress::Aborted { reason }); + } + if channel + .ffor_receiver_setup_record() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + .is_some() + { + return Ok(FFORReceiverProgress::AwaitingVoucherCommitments); + } + if entry.request_connection.as_ref() != Some(connection) { + return Err(FFORCommitmentError::ChannelUnavailable.into()); + } + if !entry.may_send_init { + return Ok(FFORReceiverProgress::AwaitingPeer); + } + channel.ffor_validate_pending_request().map_err(|_| FFORCommitmentError::PendingUpdates)?; + let deadline = match &init.payload { + Payload::Init(init) => init.settlement_deadline, + _ => return Err(FFORReceiverError::RecoveryUnavailable), + }; + if self.best_block.read().unwrap().height >= deadline { + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.abort_ffor_receiver_request(FFORReceiverAbortReason::SetupRejected)?; + entry.requirement = requirement; + entry.may_send_init = false; + return Ok(FFORReceiverProgress::AwaitingPersistence); + } + if enqueue(channel.ffor_receiver_request().unwrap().init_wire()).is_err() { + return Ok(FFORReceiverProgress::Backpressured); + } + entry.may_send_init = false; + Ok(FFORReceiverProgress::AwaitingPeer) + } + + /// Process exact signed peer input synchronously before returning from custom message handling. + /// + /// Do not hold the transport mutex while calling. PeerManager may next deliver ordinary voucher + /// add frames on this same connection. A valid Accept installs their exact native identities + /// before this returns; the accepted manager revision may persist alongside stock monitor work. + /// A durable pre-init gate already prevents a stale-manager crash from exposing these as normal + /// payments. A malformed or incompatible setup irreversibly aborts pending requests for this peer. + /// This bounded facade currently handles Accept and signed pre-accept Abort only. + pub fn handle_ffor_receiver_message( + &self, connection: &FFORPeerConnection, wire: &[u8], + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(&connection.peer) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + if !peer.is_connected || !connection.matches(connection.peer, peer.ffor_connection.as_ref()) + { + return Err(FFORCommitmentError::ChannelUnavailable.into()); + } + let message = match FFORMessage::decode(wire) { + Ok(message) => message, + Err(_) => { + self.ffor_reject_pending_requests(&mut peer)?; + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + }, + }; + let channel_id = ChannelId(message.header.channel_id); + let key = FFORRecoveryKey { channel_id, epoch_id: message.header.epoch_id }; + let result = (|| { + let channel = peer + .channel_by_id + .get_mut(&channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let request = + channel.ffor_receiver_request().ok_or(FFORReceiverError::NotRegistered)?; + let init = + request.validate_recovery().map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if init.header != message.header { + return Err(FFORReceiverError::UnknownEpoch); + } + let mut recovery = self.ffor_recovery.lock().unwrap(); + if !recovery.contains_request(request) { + return Err(FFORReceiverError::RecoveryUnavailable); + } + let mut runtime = self.ffor_activation.lock().unwrap(); + let entry = runtime.get(&key)?; + if entry.request_connection.as_ref() != Some(connection) || entry.may_send_init { + return Err(FFORCommitmentError::ChannelUnavailable.into()); + } + if let Some(reason) = channel.ffor_receiver_abort_reason() { + return Ok(FFORReceiverProgress::Aborted { reason }); + } + if let Some(existing) = channel + .ffor_receiver_setup_record() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + { + let setup = existing + .validate_recovery() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if matches!(&message.payload, Payload::Accept(_)) + && setup.accept().encode().ok().as_deref() == Some(wire) + { + return Ok( + if self.ffor_persistence.lock().unwrap().is_complete(&entry.requirement) { + FFORReceiverProgress::AwaitingVoucherCommitments + } else { + FFORReceiverProgress::AwaitingPersistence + }, + ); + } + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + if let Payload::Abort(abort) = &message.payload { + message + .verify_signature(&connection.peer) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if abort.transcript_hash != transcript::init_hash(request.init_wire()) { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + if !matches!(message.payload, Payload::Accept(_)) { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + let height = self.best_block.read().unwrap().height; + let setup = channel.prepare_ffor_receiver_accept(wire, height)?; + let permit = recovery + .prepare_accept(&setup) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.install_prepared_ffor_receiver_setup(setup)?; + permit.commit(); + runtime.record(key, requirement, false); + let entry = runtime.entries.iter_mut().find(|entry| entry.key == key).unwrap(); + entry.request_connection = Some(connection.clone()); + Ok(FFORReceiverProgress::AwaitingPersistence) + })(); + if result.is_err() { + self.ffor_reject_pending_requests(&mut peer)?; + if let Some(channel) = + peer.channel_by_id.get_mut(&channel_id).and_then(Channel::as_funded_mut) + { + if channel.ffor_receiver_request().is_some() + && channel.ffor_receiver_fence().is_none() + && channel.ffor_receiver_abort_reason().is_none() + { + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.abort_ffor_receiver_request(FFORReceiverAbortReason::SetupRejected)?; + let mut runtime = self.ffor_activation.lock().unwrap(); + if let Some(entry) = + runtime.entries.iter_mut().find(|entry| entry.key.channel_id == channel_id) + { + entry.requirement = requirement; + entry.may_send_init = false; + } + } + } + } + result + } + + /// Irreversibly cancel a setup before activation, retaining its interception gate. + /// After persistence, reconnect and advance again to release the gate for ordinary traffic. + /// This cannot cancel an activation that may already have reached the peer. + pub fn cancel_ffor_receiver_setup( + &self, id: &FFORReceiverId, connection: &FFORPeerConnection, + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(&connection.peer) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + if !peer.is_connected || !connection.matches(connection.peer, peer.ffor_connection.as_ref()) + { + return Err(FFORCommitmentError::ChannelUnavailable.into()); + } + let channel = peer + .channel_by_id + .get_mut(&id.channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let request = channel.ffor_receiver_request().ok_or(FFORReceiverError::NotRegistered)?; + if request + .validate_recovery() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + .header + .epoch_id != id.epoch_id + { + return Err(FFORReceiverError::UnknownEpoch); + } + let key = FFORRecoveryKey { channel_id: id.channel_id, epoch_id: id.epoch_id }; + let recovery = self.ffor_recovery.lock().unwrap(); + if recovery.get_activation(&key).is_some() || !recovery.contains_request(request) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.abort_ffor_receiver_request(FFORReceiverAbortReason::Requested)?; + let mut runtime = self.ffor_activation.lock().unwrap(); + let entry = runtime + .entries + .iter_mut() + .find(|entry| entry.key == key) + .ok_or(FFORReceiverError::RecoveryUnavailable)?; + entry.requirement = requirement; + entry.may_send_init = false; + channel.ffor_queue_aborted_vouchers(&self.logger); + Ok(FFORReceiverProgress::AwaitingPersistence) + } + + /// Called under the native peer lock before stock disconnect aborts setup and drops adds. + pub(in crate::ln::channelmanager) fn ffor_receiver_setup_disconnected( + &self, peer: &PeerState, + ) -> bool { + let mut changed = false; + let mut runtime = self.ffor_activation.lock().unwrap(); + for channel in peer.channel_by_id.values().filter_map(Channel::as_funded) { + if channel.ffor_receiver_request().is_none() + || channel.ffor_receiver_fence().is_some() + || channel.ffor_receiver_request_gate_released() + { + continue; + } + if let Some(entry) = runtime + .entries + .iter_mut() + .find(|entry| entry.key.channel_id == channel.context.channel_id()) + { + if let Ok(requirement) = self.ffor_persistence.lock().unwrap().request() { + entry.requirement = requirement; + entry.may_send_init = false; + changed = true; + } + } + } + changed + } + + fn ffor_reject_pending_requests( + &self, peer: &mut PeerState, + ) -> Result<(), FFORReceiverError> { + for channel in peer.channel_by_id.values_mut().filter_map(Channel::as_funded_mut) { + let request = match channel.ffor_receiver_request() { + Some(request) => request, + None => continue, + }; + if channel + .ffor_receiver_setup_record() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + .is_some() + { + continue; + } + let init = + request.validate_recovery().map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let key = FFORRecoveryKey { + channel_id: ChannelId(init.header.channel_id), + epoch_id: init.header.epoch_id, + }; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.abort_ffor_receiver_request(FFORReceiverAbortReason::SetupRejected)?; + let mut runtime = self.ffor_activation.lock().unwrap(); + if let Some(entry) = runtime.entries.iter_mut().find(|entry| entry.key == key) { + entry.requirement = requirement; + entry.may_send_init = false; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs new file mode 100644 index 00000000000..86b913a5454 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs @@ -0,0 +1,600 @@ +use super::*; +use crate::ln::channel::ffor_setup_test_messages; +use crate::ln::channelmanager::ffor_recovery_tests::restore; +use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; +use crate::ln::functional_test_utils::*; +use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler}; +use crate::sign::{KeysManager, NodeSigner}; + +macro_rules! fixture { + ($nodes:ident, $sender:ident, $receiver:ident, $id:ident, $receiver_funds:expr) => { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let $nodes = create_network(2, &node_cfgs, &managers); + let $id = + create_announced_chan_between_nodes_with_value(&$nodes, 0, 1, 100_000, 40_000_000).2; + let ($sender, $receiver) = + if $receiver_funds { (&$nodes[1], &$nodes[0]) } else { (&$nodes[0], &$nodes[1]) }; + }; +} + +fn parameters(voucher: &FFORVoucher) -> FFORReceiverParameters { + FFORReceiverParameters { + local_request_id: [91; 32], + amounts_msat: vec![voucher.amount_msat], + minimum_payment_msat: voucher.amount_msat, + settlement_deadline: voucher.cltv_expiry - 20, + voucher_expiry: voucher.cltv_expiry, + fee_base_msat: 0, + fee_proportional_millionths: 0, + claim_margin_blocks: 20, + witness_peers: None, + hash_chain: false, + } +} +fn persist(receiver: &Node) -> Vec { + let token = receiver.node.capture_ffor_persistence(); + let bytes = receiver.node.encode(); + receiver.node.ffor_persistence_completed(token).unwrap(); + bytes +} +fn sign(message: &mut FFORMessage, node: &Node) { + let keys = KeysManager::new(&node.node_seed, 0, 0, true); + message.signature = keys + .sign_ffor_message(&FFORSigningRequest::new(&message.unsigned_wire().unwrap()).unwrap()) + .unwrap() + .serialize_compact(); +} +fn accept( + sender: &Node, receiver: &Node, channel: ChannelId, voucher: FFORVoucher, init_wire: &[u8], +) -> FFORMessage { + let init = FFORMessage::decode(init_wire).unwrap(); + let (_, mut accept) = ffor_setup_test_messages(sender, receiver, channel, voucher); + accept.header = init.header; + if let Payload::Accept(terms) = &mut accept.payload { + terms.init_hash = transcript::init_hash(init_wire); + } + sign(&mut accept, sender); + accept +} +fn emit(receiver: &Node, id: &FFORReceiverId, connection: &FFORPeerConnection) -> Vec { + let mut wire = Vec::new(); + assert_eq!( + receiver + .node + .advance_ffor_receiver(id, connection, |bytes| { + wire = bytes.to_vec(); + Ok(()) + }) + .unwrap(), + FFORReceiverProgress::AwaitingPeer + ); + assert!(!wire.is_empty()); + wire +} + +#[test] +fn ffor_driver_durable_init_exact_backpressure_and_synchronous_accept() { + for receiver_funds in [false, true] { + fixture!(nodes, sender, receiver, channel, receiver_funds); + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let connection = + receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let id = receiver + .node + .prepare_ffor_receiver(&channel, &connection, parameters(&voucher)) + .unwrap(); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("not durable")) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + // A failed or cancelled store has no completion, even after encoding the same bytes. + let failed = receiver.node.capture_ffor_persistence(); + let _ = receiver.node.encode(); + drop(failed); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("failed store")) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + let mut refused = Vec::new(); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |wire| { + refused = wire.to_vec(); + Err(()) + }) + .unwrap(), + FFORReceiverProgress::Backpressured + ); + let wire = emit(receiver, &id, &connection); + assert_eq!(wire, refused); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("duplicate Init")) + .unwrap(), + FFORReceiverProgress::AwaitingPeer + ); + let accepted = accept(sender, receiver, channel, voucher, &wire).encode().unwrap(); + assert_eq!( + receiver.node.handle_ffor_receiver_message(&connection, &accepted).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + assert_eq!( + receiver.node.handle_ffor_receiver_message(&connection, &accepted).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + // The peer is allowed to send these stock frames immediately after Accept. + deliver_parked_voucher(sender, receiver, update); + assert!(receiver.node.get_and_clear_pending_events().is_empty()); + persist(receiver); + assert_eq!( + receiver.node.handle_ffor_receiver_message(&connection, &accepted).unwrap(), + FFORReceiverProgress::AwaitingVoucherCommitments + ); + let snapshot = get_monitor!(receiver, channel).ffor_commitment_snapshot().unwrap(); + assert!(matches!( + receiver + .node + .ffor_receiver_book_status(&channel, &sender.node.get_our_node_id(), &snapshot) + .unwrap(), + FFORReceiverStatus::Parked { .. } + )); + let bytes = persist(receiver); + let monitor = get_monitor!(receiver, channel).encode(); + let restored = restore(receiver, &bytes, &monitor).unwrap(); + assert!(restored.get_and_clear_pending_events().is_empty()); + } +} + +#[test] +fn ffor_driver_crash_after_monitor_before_accept_manager_cannot_claim_payment() { + for receiver_funds in [false, true] { + fixture!(nodes, sender, receiver, channel, receiver_funds); + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let connection = + receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let id = receiver + .node + .prepare_ffor_receiver(&channel, &connection, parameters(&voucher)) + .unwrap(); + let before_accept = persist(receiver); + let wire = emit(receiver, &id, &connection); + let accepted = accept(sender, receiver, channel, voucher, &wire).encode().unwrap(); + receiver.node.handle_ffor_receiver_message(&connection, &accepted).unwrap(); + receiver + .node + .handle_update_add_htlc(sender.node.get_our_node_id(), &update.update_add_htlcs[0]); + receiver + .node + .handle_commitment_signed(sender.node.get_our_node_id(), &update.commitment_signed[0]); + check_added_monitors(receiver, 1); + let newer_monitor = get_monitor!(receiver, channel).encode(); + let restored = restore(receiver, &before_accept, &newer_monitor).unwrap(); + assert!(restored.list_channels().is_empty()); + let events = restored.get_and_clear_pending_events(); + assert!(events.iter().any(|event| matches!(event, Event::ChannelClosed { .. }))); + assert!(events.iter().all(|event| !matches!( + event, + Event::PaymentClaimable { .. } | Event::PaymentForwarded { .. } + ))); + assert!(restored + .ffor_recovery + .lock() + .unwrap() + .request_keys() + .contains(&FFORRecoveryKey { channel_id: channel, epoch_id: id.epoch_id() })); + receiver.node.get_and_clear_pending_msg_events(); + receiver.node.get_and_clear_pending_events(); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + } +} + +#[test] +fn ffor_driver_preaccept_add_cannot_be_relabelled_and_stale_connection_cannot_mutate() { + fixture!(nodes, sender, receiver, channel, false); + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let id = + receiver.node.prepare_ffor_receiver(&channel, &connection, parameters(&voucher)).unwrap(); + persist(receiver); + let wire = emit(receiver, &id, &connection); + receiver + .node + .handle_update_add_htlc(sender.node.get_our_node_id(), &update.update_add_htlcs[0]); + let accepted = accept(sender, receiver, channel, voucher, &wire).encode().unwrap(); + assert!(matches!( + receiver.node.handle_ffor_receiver_message(&connection, &accepted).unwrap(), + FFORReceiverProgress::Aborted { reason: FFORReceiverAbortReason::VoucherMismatch } + )); + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + assert!(peer.channel_by_id[&channel] + .as_funded() + .unwrap() + .ffor_receiver_setup_record() + .unwrap() + .is_none()); + drop(peer); + drop(peers); + // Disconnect discards uncommitted adds, but the request tombstone still refuses promotion. + receiver.node.peer_disconnected(sender.node.get_our_node_id()); + sender.node.peer_disconnected(receiver.node.get_our_node_id()); + let init = msgs::Init { + features: sender.node.init_features(), + networks: None, + remote_network_address: None, + }; + receiver.node.peer_connected(sender.node.get_our_node_id(), &init, false).unwrap(); + let replacement = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + assert_ne!(replacement, connection); + let before = receiver.node.encode(); + assert!(receiver.node.handle_ffor_receiver_message(&connection, &accepted).is_err()); + assert_eq!(before, receiver.node.encode()); + assert!(receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("stale queue")) + .is_err()); + receiver.node.get_and_clear_pending_msg_events(); +} + +#[test] +fn ffor_driver_deadline_crossing_prevents_delayed_init_release() { + for backpressure in [false, true] { + fixture!(nodes, sender, receiver, channel, false); + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([7; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let connection = + receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let id = receiver + .node + .prepare_ffor_receiver(&channel, &connection, parameters(&voucher)) + .unwrap(); + if backpressure { + persist(receiver); + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &connection, |_| Err(())).unwrap(), + FFORReceiverProgress::Backpressured + ); + } + receiver.node.best_block.write().unwrap().height = 180; + persist(receiver); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("expired Init")) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("aborted Init")) + .unwrap(), + FFORReceiverProgress::Aborted { reason: FFORReceiverAbortReason::SetupRejected } + ); + } +} + +#[test] +fn ffor_driver_pending_restore_required_fence_and_malformed_refusal() { + fixture!(nodes, sender, receiver, channel, false); + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([8; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let id = + receiver.node.prepare_ffor_receiver(&channel, &connection, parameters(&voucher)).unwrap(); + let pending = persist(receiver); + let monitor = get_monitor!(receiver, channel).encode(); + let restored = restore(receiver, &pending, &monitor).unwrap(); + assert!(restored.ffor_peer_connection(&sender.node.get_our_node_id()).is_err()); + assert!(restored.handle_ffor_receiver_message(&connection, &[0]).is_err()); + let registry = receiver.node.ffor_recovery.lock().unwrap().encode(); + assert_eq!(registry[0], 4); + let mut unsupported = registry.clone(); + unsupported[0] = 5; + assert!(::read(&mut &unsupported[..]).is_err()); + let _ = emit(receiver, &id, &connection); + assert!(receiver.node.handle_ffor_receiver_message(&connection, &[0]).is_err()); + persist(receiver); + assert!(matches!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("malformed retry")) + .unwrap(), + FFORReceiverProgress::Aborted { .. } + )); +} + +#[test] +fn ffor_driver_local_retry_correlation_survives_restore_and_conflicts() { + fixture!(nodes, sender, receiver, channel, false); + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([9; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let params = parameters(&voucher); + let id = receiver.node.prepare_ffor_receiver(&channel, &connection, params.clone()).unwrap(); + assert_ne!(id.epoch_id(), params.local_request_id); + let before = receiver.node.encode(); + assert_eq!( + receiver.node.prepare_ffor_receiver(&channel, &connection, params.clone()).unwrap(), + id + ); + assert_eq!(before, receiver.node.encode()); + let mut conflict = params.clone(); + conflict.amounts_msat[0] += 1; + assert_eq!( + receiver.node.prepare_ffor_receiver(&channel, &connection, conflict), + Err(FFORReceiverError::AlreadyRegistered) + ); + let bytes = persist(receiver); + let monitor = get_monitor!(receiver, channel).encode(); + let restored = restore(receiver, &bytes, &monitor).unwrap(); + assert_eq!(restored.find_ffor_receiver_request(params.local_request_id).unwrap(), Some(id)); + assert_eq!(restored.find_ffor_receiver_request([0; 32]).unwrap(), None); + let mut absent = core::mem::replace( + &mut *receiver.node.ffor_recovery.lock().unwrap(), + FFORRecoveryRegistry::new(), + ); + let damaged = receiver.node.encode(); + core::mem::swap(&mut absent, &mut *receiver.node.ffor_recovery.lock().unwrap()); + assert!(restore(receiver, &damaged, &monitor).is_err()); +} + +#[test] +fn ffor_driver_cancel_requires_durable_fresh_connection_before_ordinary_payment() { + fixture!(nodes, sender, receiver, channel, false); + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([10; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let sender_id = sender.node.get_our_node_id(); + let connection = receiver.node.ffor_peer_connection(&sender_id).unwrap(); + let id = + receiver.node.prepare_ffor_receiver(&channel, &connection, parameters(&voucher)).unwrap(); + persist(receiver); + emit(receiver, &id, &connection); + assert_eq!( + receiver.node.cancel_ffor_receiver_setup(&id, &connection).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + assert!(matches!( + receiver.node.advance_ffor_receiver(&id, &connection, |_| panic!("cancel send")).unwrap(), + FFORReceiverProgress::Aborted { .. } + )); + receiver.node.peer_disconnected(sender_id); + sender.node.peer_disconnected(receiver.node.get_our_node_id()); + let mut reconnect = ReconnectArgs::new(receiver, sender); + reconnect.send_channel_ready = (true, true); + reconnect.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect); + let next = receiver.node.ffor_peer_connection(&sender_id).unwrap(); + assert_ne!(connection, next); + // Disconnect itself creates a protected abort revision, so an old completed token cannot + // release this gate on a new connection. + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &next, |_| panic!("reconnected Init")).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &next, |_| panic!("gate release send")).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + assert!(matches!( + receiver.node.advance_ffor_receiver(&id, &next, |_| panic!("terminal send")).unwrap(), + FFORReceiverProgress::Aborted { .. } + )); + let released = persist(receiver); + let released_monitor = get_monitor!(receiver, channel).encode(); + assert!(restore(receiver, &released, &released_monitor).is_ok()); + let (update, later, secret) = offer_voucher(sender, receiver, 2_000_000); + receiver.node.handle_update_add_htlc(sender_id, &update.update_add_htlcs[0]); + commitment_signed_dance!(receiver, sender, update.commitment_signed, false); + expect_and_process_pending_htlcs(receiver, false); + expect_payment_claimable!(receiver, later.payment_hash, secret, later.amount_msat); + assert_eq!( + receiver.node.prepare_ffor_receiver(&channel, &next, parameters(&voucher)).unwrap(), + id + ); +} + +#[test] +fn ffor_driver_contradictory_accept_retains_interception_and_never_promotes_again() { + fixture!(nodes, sender, receiver, channel, false); + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let id = + receiver.node.prepare_ffor_receiver(&channel, &connection, parameters(&voucher)).unwrap(); + persist(receiver); + let init = emit(receiver, &id, &connection); + let accepted = accept(sender, receiver, channel, voucher, &init); + receiver.node.handle_ffor_receiver_message(&connection, &accepted.encode().unwrap()).unwrap(); + let mut conflicting = accepted.clone(); + if let Payload::Accept(terms) = &mut conflicting.payload { + terms.payment_hashes[0] = [99; 32]; + } + sign(&mut conflicting, sender); + assert!(receiver + .node + .handle_ffor_receiver_message(&connection, &conflicting.encode().unwrap()) + .is_err()); + assert!(matches!( + receiver + .node + .handle_ffor_receiver_message(&connection, &accepted.encode().unwrap()) + .unwrap(), + FFORReceiverProgress::Aborted { reason: FFORReceiverAbortReason::SetupRejected } + )); + // No received slot exists yet. Even a different hash cannot pass the rejected connection's + // gate and reach ordinary onion processing. + let mut wrong_add = update.update_add_htlcs[0].clone(); + wrong_add.payment_hash = PaymentHash([99; 32]); + receiver.node.handle_update_add_htlc(sender.node.get_our_node_id(), &wrong_add); + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + assert!(peer.channel_by_id[&channel] + .as_funded() + .unwrap() + .ffor_owns_received_htlc(wrong_add.htlc_id)); + drop(peer); + drop(peers); + receiver.node.peer_disconnected(sender.node.get_our_node_id()); + sender.node.peer_disconnected(receiver.node.get_our_node_id()); +} + +#[test] +fn ffor_driver_refuses_capacity_and_unsupported_channel_before_registration() { + { + fixture!(nodes, sender, receiver, channel, false); + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([11; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let connection = + receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let saved = core::mem::replace( + &mut *receiver.node.ffor_recovery.lock().unwrap(), + crate::ln::ffor_recovery::tests::full_registry(), + ); + assert_eq!( + receiver.node.prepare_ffor_receiver(&channel, &connection, parameters(&voucher)), + Err(FFORReceiverError::RecoveryUnavailable) + ); + *receiver.node.ffor_recovery.lock().unwrap() = saved; + assert_eq!(receiver.node.find_ffor_receiver_request([91; 32]).unwrap(), None); + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + assert!(peer.channel_by_id[&channel] + .as_funded() + .unwrap() + .ffor_receiver_request() + .is_none()); + } + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let managers = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &managers); + let channel = create_announced_chan_between_nodes(&nodes, 0, 1).2; + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([12; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let connection = nodes[1].node.ffor_peer_connection(&nodes[0].node.get_our_node_id()).unwrap(); + assert_eq!( + nodes[1].node.prepare_ffor_receiver(&channel, &connection, parameters(&voucher)), + Err(FFORCommitmentError::UnsupportedChannelType.into()) + ); + assert_eq!(nodes[1].node.find_ffor_receiver_request([91; 32]).unwrap(), None); +} + +#[test] +fn ffor_driver_witness_policy_and_signed_preaccept_refusal() { + fixture!(nodes, sender, receiver, channel, false); + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([13; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let mut params = parameters(&voucher); + params.witness_peers = Some(vec![]); + assert!(params.init().is_err()); + params.witness_peers = Some(vec![sender.node.get_our_node_id(); 2]); + assert!(params.init().is_err()); + params.witness_peers = Some(vec![sender.node.get_our_node_id()]); + assert!(params.init().is_ok()); + let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let id = receiver.node.prepare_ffor_receiver(&channel, &connection, params).unwrap(); + persist(receiver); + let init = emit(receiver, &id, &connection); + let mut abort = FFORMessage { + header: FFORMessage::decode(&init).unwrap().header, + payload: Payload::Abort(lightning_ffor::wire::Abort { + transcript_hash: transcript::init_hash(&init), + reason: 1, + data: vec![], + }), + extensions: Vec::new(), + signature: [0; 64], + }; + sign(&mut abort, sender); + assert!(receiver + .node + .handle_ffor_receiver_message(&connection, &abort.encode().unwrap()) + .is_err()); + persist(receiver); + assert!(matches!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("refused setup send")) + .unwrap(), + FFORReceiverProgress::Aborted { reason: FFORReceiverAbortReason::SetupRejected } + )); +} + +#[test] +fn ffor_driver_accept_rechecks_current_deadline_before_promotion() { + fixture!(nodes, sender, receiver, channel, false); + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([14; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let id = + receiver.node.prepare_ffor_receiver(&channel, &connection, parameters(&voucher)).unwrap(); + persist(receiver); + let init = emit(receiver, &id, &connection); + let accepted = accept(sender, receiver, channel, voucher, &init).encode().unwrap(); + receiver.node.best_block.write().unwrap().height = 180; + assert!(receiver.node.handle_ffor_receiver_message(&connection, &accepted).is_err()); + persist(receiver); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("expired Accept send")) + .unwrap(), + FFORReceiverProgress::Aborted { reason: FFORReceiverAbortReason::SetupRejected } + ); + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + assert!(peer.channel_by_id[&channel] + .as_funded() + .unwrap() + .ffor_receiver_setup_record() + .unwrap() + .is_none()); +} diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index f39bf2e32e7..2a210d9c221 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -25,6 +25,12 @@ use crate::prelude::*; use crate::sign::{NodeSigner, Recipient}; use crate::types::payment::PaymentHash; +mod driver; +pub use driver::{FFORReceiverId, FFORReceiverParameters, FFORReceiverProgress}; + +mod connection; +pub use connection::FFORPeerConnection; + pub(crate) mod context; pub use context::{FFORReceiverActiveContext, FFORReceiverRecoveryContext}; @@ -76,6 +82,8 @@ pub enum FFORReceiverAbortReason { CommitmentSecretReused, /// The owned quiescence handshake no longer matched its voucher evidence or deadline. QuiescenceFailed, + /// A pending setup received incompatible, malformed, or refused protocol input. + SetupRejected, } impl_writeable_tlv_based_enum!(FFORReceiverAbortReason, @@ -85,6 +93,7 @@ impl_writeable_tlv_based_enum!(FFORReceiverAbortReason, (6, Restarted) => {}, (8, CommitmentSecretReused) => {}, (10, QuiescenceFailed) => {}, + (12, SetupRejected) => {}, ); /// State of the receiver's experimental, connection-scoped STFU handshake. diff --git a/lightning/src/ln/ffor/connection.rs b/lightning/src/ln/ffor/connection.rs new file mode 100644 index 00000000000..3e62aad5cb5 --- /dev/null +++ b/lightning/src/ln/ffor/connection.rs @@ -0,0 +1,36 @@ +//! Opaque authenticated peer generations for synchronous receiver message handling. + +use crate::sync::Arc; +use bitcoin::secp256k1::PublicKey; + +/// One authenticated peer connection observed by this channel manager instance. +/// +/// Capture this in the custom handler's successful `peer_connected` callback, after the channel +/// manager has processed that same callback. Keep it with the transport's own generation token. +/// The native receiver handler checks this identity under its peer lock before changing state. +/// Cloning preserves a generation; reconnect and manager restore never reuse it. This value has +/// no public constructor or serialized representation and does not attest any FFOR message. +#[derive(Clone, Debug)] +pub struct FFORPeerConnection { + pub(crate) peer: PublicKey, + pub(crate) generation: Arc<()>, +} + +impl FFORPeerConnection { + /// The peer identity supplied by the authenticated native connection callback. + pub fn peer_node_id(&self) -> PublicKey { + self.peer + } + + pub(crate) fn matches(&self, peer: PublicKey, generation: Option<&Arc<()>>) -> bool { + self.peer == peer + && generation.map_or(false, |current| Arc::ptr_eq(current, &self.generation)) + } +} + +impl PartialEq for FFORPeerConnection { + fn eq(&self, other: &Self) -> bool { + self.matches(other.peer, Some(&other.generation)) + } +} +impl Eq for FFORPeerConnection {} diff --git a/lightning/src/ln/ffor/driver.rs b/lightning/src/ln/ffor/driver.rs new file mode 100644 index 00000000000..0425f6f1867 --- /dev/null +++ b/lightning/src/ln/ffor/driver.rs @@ -0,0 +1,103 @@ +//! Experimental receiver setup intents. These values never authorize offline invoices. + +use super::{FFORReceiverAbortReason, FFORReceiverError}; +use crate::ln::types::ChannelId; +use crate::prelude::*; +use bitcoin::secp256k1::PublicKey; +use lightning_ffor::wire::Init; + +/// Local policy for a fixed-amount Variant D request. All deadlines are block heights. +/// The engine derives the budget, epoch, channel identities and live funding limits itself. +#[derive(Clone, Debug)] +pub struct FFORReceiverParameters { + /// Stable application correlation for exact retries. It never chooses the protocol epoch. + pub local_request_id: [u8; 32], + /// Exact voucher amounts in millisatoshis, with one through 483 entries. + pub amounts_msat: Vec, + /// Minimum accepted voucher amount in millisatoshis. + pub minimum_payment_msat: u64, + /// Last permitted settlement admission height. + pub settlement_deadline: u32, + /// Common absolute voucher expiry height. + pub voucher_expiry: u32, + /// Proposed base forwarding fee in millisatoshis. + pub fee_base_msat: u32, + /// Proposed proportional forwarding fee in millionths. + pub fee_proportional_millionths: u32, + /// Local on-chain claim policy, never a value supplied by the peer. + pub claim_margin_blocks: u32, + /// Optional witness peer restriction, with one through four distinct keys when present. + pub witness_peers: Option>, + /// Request uniform hash-chained vouchers. + pub hash_chain: bool, +} + +impl FFORReceiverParameters { + pub(crate) fn init(&self) -> Result { + if self.amounts_msat.is_empty() + || self.amounts_msat.len() > 483 + || self.witness_peers.as_ref().map_or(false, |peers| peers.len() > 4) + { + return Err(super::FFORCommitmentError::InvalidVoucherBook.into()); + } + if let Some(peers) = self.witness_peers.as_ref() { + if peers.is_empty() + || peers.iter().enumerate().any(|(index, peer)| peers[..index].contains(peer)) + { + return Err(super::FFORCommitmentError::InvalidVoucherBook.into()); + } + } + let budget_msat = self + .amounts_msat + .iter() + .try_fold(0u64, |sum, amount| sum.checked_add(*amount)) + .ok_or(super::FFORCommitmentError::InvalidVoucherBook)?; + Ok(Init { + budget_msat, + min_payment_msat: self.minimum_payment_msat, + settlement_deadline: self.settlement_deadline, + voucher_expiry: self.voucher_expiry, + fee_base_msat: self.fee_base_msat, + fee_proportional_millionths: self.fee_proportional_millionths, + amounts_msat: self.amounts_msat.clone(), + witness_peers: self.witness_peers.clone(), + hash_chain: self.hash_chain, + }) + } +} + +/// An epoch selector returned by native admission. Possessing it grants no phase authority. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FFORReceiverId { + pub(crate) channel_id: ChannelId, + pub(crate) epoch_id: [u8; 32], +} + +impl FFORReceiverId { + /// The channel that owns this request. + pub fn channel_id(&self) -> ChannelId { + self.channel_id + } + /// The fresh epoch generated by the manager. + pub fn epoch_id(&self) -> [u8; 32] { + self.epoch_id + } +} + +/// Current progress of experimental setup, without activation or invoice readiness. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FFORReceiverProgress { + /// The ordered manager persister has not yet completed the protected revision. + AwaitingPersistence, + /// Exact outgoing bytes remain owned by the engine because the transport queue refused them. + Backpressured, + /// Init has been enqueued on its original connection; no retry is emitted on another one. + AwaitingPeer, + /// Exact accepted vouchers are owned natively and await their stock commitment rounds. + AwaitingVoucherCommitments, + /// The pending request was irreversibly aborted. The gate remains until controlled release. + Aborted { + /// First retained native reason. + reason: FFORReceiverAbortReason, + }, +} diff --git a/lightning/src/ln/ffor_recovery.rs b/lightning/src/ln/ffor_recovery.rs index 01d4570d7aa..106e78bb2af 100644 --- a/lightning/src/ln/ffor_recovery.rs +++ b/lightning/src/ln/ffor_recovery.rs @@ -4,6 +4,9 @@ //! TLV 22, under the same consistency lock as channel admission and persistence. Admission uses //! a borrowed insertion permit so capacity failure cannot leave a channel without its evidence. +mod request; +use request::PendingRequest; + mod activation; pub(crate) use activation::{FFORReceiverActivation, FFORReceiverCloseRecord}; @@ -24,6 +27,7 @@ const ACTIVATION_VERSION: u8 = 1; // Earlier activation readers do not understand a retained abort or its relaxed monitor binding. const ABORTED_ACTIVATION_VERSION: u8 = 2; const CLOSE_VERSION: u8 = 3; +const REQUEST_VERSION: u8 = 4; const MAX_RECORDS: usize = 64; const MAX_ENCODED_BYTES: usize = 8 * 1024 * 1024; // Two maximum wire messages, a 483-slot canonical book, and fixed admission fields fit here. @@ -56,12 +60,14 @@ struct StoredSetup { setup: FFORReceiverSetup, canonical_book: Vec, activation: Option, + request: Option, } impl_writeable_tlv_based!(StoredSetup, { (0, setup, required), (2, canonical_book, required_vec), (4, activation, option), + (6, request, option), }); struct Entry { @@ -74,6 +80,10 @@ impl Entry { fn new(record: StoredSetup) -> Result { let authenticated = record.setup.validate_recovery().map_err(|_| FFORRecoveryError::InvalidRecord)?; + if record.request.as_ref().map_or(false, |request| !request.validates_setup(&record.setup)) + { + return Err(FFORRecoveryError::InvalidRecord); + } if record.canonical_book != authenticated.canonical_book() { return Err(FFORRecoveryError::InvalidRecord); } @@ -112,6 +122,9 @@ impl Entry { } fn transition_reservation(record: &StoredSetup) -> usize { + if record.request.is_some() { + return MAX_RECORD_BYTES.saturating_sub(record.serialized_length()); + } let activation = match &record.activation { Some(activation) if !activation.is_aborted() => activation, _ => return 0, @@ -138,6 +151,7 @@ impl Entry { pub(crate) struct FFORRecoveryRegistry { entries: Vec, + pending_requests: Vec, encoded_bytes: usize, // Derived from authenticated phases, never serialized or supplied by a caller. reserved_transition_bytes: usize, @@ -178,11 +192,16 @@ impl FFORRecoveryUpgrade<'_> { impl FFORRecoveryRegistry { pub(crate) fn new() -> Self { - Self { entries: Vec::new(), encoded_bytes: HEADER_BYTES, reserved_transition_bytes: 0 } + Self { + entries: Vec::new(), + pending_requests: Vec::new(), + encoded_bytes: HEADER_BYTES, + reserved_transition_bytes: 0, + } } pub(crate) fn is_empty(&self) -> bool { - self.entries.is_empty() + self.entries.is_empty() && self.pending_requests.is_empty() } pub(crate) fn get(&self, key: &FFORRecoveryKey) -> Option<&FFORReceiverSetup> { @@ -332,7 +351,9 @@ impl FFORRecoveryRegistry { pub(crate) fn validate_identity( &self, receiver: PublicKey, chain_hash: ChainHash, ) -> Result<(), DecodeError> { - if self.entries.iter().any(|entry| { + if self.pending_requests.iter().any(|entry| { + entry.record.receiver() != receiver || entry.record.chain_hash() != chain_hash + }) || self.entries.iter().any(|entry| { entry.record.setup.receiver() != receiver || entry.record.setup.chain_hash() != chain_hash }) { @@ -353,7 +374,15 @@ impl FFORRecoveryRegistry { setup: setup.clone(), canonical_book: authenticated.canonical_book().to_vec(), activation: None, + request: None, })?; + if self.pending_requests.iter().any(|pending| { + pending.key.channel_id == entry.key.channel_id + || (pending.record.chain_hash() == setup.chain_hash() + && pending.record.funding_txo() == setup.funding_txo()) + }) { + return Err(FFORRecoveryError::ConflictingRecord); + } if let Some(existing) = self.entries.iter().find(|existing| existing.has_same_identity(&entry)) { @@ -374,10 +403,16 @@ impl FFORRecoveryRegistry { ) -> Result, FFORRecoveryError> { let authenticated = setup.validate_recovery().map_err(|_| FFORRecoveryError::InvalidRecord)?; + let key = FFORRecoveryKey { + channel_id: ChannelId(authenticated.header().channel_id), + epoch_id: authenticated.header().epoch_id, + }; + let request = self.get_request(&key).cloned(); let entry = Entry::new(StoredSetup { setup: setup.clone(), canonical_book: authenticated.canonical_book().to_vec(), activation: Some(activation.clone()), + request, })?; let index = self .entries @@ -418,6 +453,11 @@ impl FFORRecoveryRegistry { } fn version(&self) -> u8 { + if !self.pending_requests.is_empty() + || self.entries.iter().any(|entry| entry.record.request.is_some()) + { + return REQUEST_VERSION; + } if self.entries.iter().any(|entry| { entry .record @@ -450,7 +490,7 @@ impl FFORRecoveryRegistry { .and_then(|total| total.checked_add(record_bytes)) .and_then(|total| total.checked_add(self.reserved_transition_bytes)) .and_then(|total| total.checked_add(reserved_transition_bytes)); - if self.entries.len() >= MAX_RECORDS + if self.entries.len() + self.pending_requests.len() >= MAX_RECORDS || record_bytes > MAX_RECORD_BYTES || total.map_or(true, |total| total > MAX_ENCODED_BYTES) { @@ -468,6 +508,13 @@ impl Writeable for FFORRecoveryRegistry { (entry.encoded_bytes as u32).write(writer)?; entry.record.write(writer)?; } + if self.version() == REQUEST_VERSION { + (self.pending_requests.len() as u16).write(writer)?; + for entry in &self.pending_requests { + (entry.encoded_bytes as u32).write(writer)?; + entry.record.write(writer)?; + } + } Ok(()) } } @@ -479,6 +526,7 @@ impl Readable for FFORRecoveryRegistry { && version != ACTIVATION_VERSION && version != ABORTED_ACTIVATION_VERSION && version != CLOSE_VERSION + && version != REQUEST_VERSION { return Err(DecodeError::UnknownRequiredFeature); } @@ -487,6 +535,9 @@ impl Readable for FFORRecoveryRegistry { return Err(DecodeError::InvalidValue); } let mut registry = Self::new(); + if version == REQUEST_VERSION { + registry.encoded_bytes += 2; + } for _ in 0..count { let length = u32::read(reader)? as usize; if version == SETUP_ONLY_VERSION && length > MAX_SETUP_RECORD_BYTES { @@ -501,7 +552,9 @@ impl Readable for FFORRecoveryRegistry { let entry = Entry::new(record).map_err(|_| DecodeError::InvalidValue)?; // Reject ignored storage extensions and alternate framing as well as duplicate keys. // Exact signed optional wire extensions remain inside the retained message bytes. - if entry.encoded_bytes != length + if entry.record.request.as_ref().map_or(false, |request| { + registry.find_request(request.local_request_id()).is_some() + }) || entry.encoded_bytes != length || registry.entries.iter().any(|existing| existing.has_same_identity(&entry)) { return Err(DecodeError::InvalidValue); @@ -513,6 +566,43 @@ impl Readable for FFORRecoveryRegistry { registry.reserved_transition_bytes += entry.reserved_transition_bytes(); registry.entries.push(entry); } + if version == REQUEST_VERSION { + let count = u16::read(reader)? as usize; + if registry.entries.len() + count > MAX_RECORDS { + return Err(DecodeError::InvalidValue); + } + for _ in 0..count { + let length = u32::read(reader)? as usize; + if length > MAX_SETUP_RECORD_BYTES { + return Err(DecodeError::InvalidValue); + } + let mut bounded = FixedLengthReader::new(reader, length as u64); + let request = crate::ln::channel::FFORReceiverRequest::read(&mut bounded)?; + if bounded.bytes_remain() { + return Err(DecodeError::InvalidValue); + } + let entry = PendingRequest::new(request).map_err(|_| DecodeError::InvalidValue)?; + if registry.find_request(entry.record.local_request_id()).is_some() + || entry.encoded_bytes != length + || registry.pending_requests.iter().any(|saved| { + saved.key.channel_id == entry.key.channel_id + || (saved.record.chain_hash() == entry.record.chain_hash() + && saved.record.funding_txo() == entry.record.funding_txo()) + }) || registry.entries.iter().any(|saved| { + saved.key.channel_id == entry.key.channel_id + || (saved.record.setup.chain_hash() == entry.record.chain_hash() + && saved.record.setup.funding_txo() == entry.record.funding_txo()) + }) { + return Err(DecodeError::InvalidValue); + } + registry + .check_capacity(length, entry.reserved_bytes()) + .map_err(|_| DecodeError::InvalidValue)?; + registry.encoded_bytes += RECORD_LENGTH_BYTES + length; + registry.reserved_transition_bytes += entry.reserved_bytes(); + registry.pending_requests.push(entry); + } + } if registry.version() != version { return Err(DecodeError::InvalidValue); } diff --git a/lightning/src/ln/ffor_recovery/activation/tests.rs b/lightning/src/ln/ffor_recovery/activation/tests.rs index 439dacb4ddc..8fcb81908ee 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests.rs @@ -352,6 +352,7 @@ fn ffor_activation_archive_reserves_maximum_ack_through_competing_admission_and_ // A snapshot cannot bypass the reservation by declaring only its smaller current wire size. let (extra_setup, extra_activation) = evidence_for_identity(91, true); let extra = crate::ln::ffor_recovery::Entry::new(crate::ln::ffor_recovery::StoredSetup { + request: None, canonical_book: extra_setup.validate_recovery().unwrap().canonical_book().to_vec(), setup: extra_setup, activation: Some(extra_activation), diff --git a/lightning/src/ln/ffor_recovery/request.rs b/lightning/src/ln/ffor_recovery/request.rs new file mode 100644 index 00000000000..08ec265d034 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/request.rs @@ -0,0 +1,160 @@ +//! Bounded pre-init reservations and atomic promotion into accepted setup evidence. + +use super::*; +use crate::ln::channel::FFORReceiverRequest; + +pub(super) struct PendingRequest { + pub(super) key: FFORRecoveryKey, + pub(super) record: FFORReceiverRequest, + pub(super) encoded_bytes: usize, +} + +impl PendingRequest { + pub(super) fn new(record: FFORReceiverRequest) -> Result { + let message = record.validate_recovery().map_err(|_| FFORRecoveryError::InvalidRecord)?; + let encoded_bytes = record.serialized_length(); + if encoded_bytes > MAX_SETUP_RECORD_BYTES { + return Err(FFORRecoveryError::CapacityExceeded); + } + Ok(Self { + key: FFORRecoveryKey { + channel_id: ChannelId(message.header.channel_id), + epoch_id: message.header.epoch_id, + }, + record, + encoded_bytes, + }) + } + pub(super) fn reserved_bytes(&self) -> usize { + MAX_RECORD_BYTES - self.encoded_bytes + } +} + +pub(crate) struct FFORRequestInsertion<'a> { + registry: &'a mut FFORRecoveryRegistry, + entry: PendingRequest, + header_bytes: usize, +} +impl FFORRequestInsertion<'_> { + pub(crate) fn commit(self) { + self.registry.encoded_bytes += + self.header_bytes + RECORD_LENGTH_BYTES + self.entry.encoded_bytes; + self.registry.reserved_transition_bytes += self.entry.reserved_bytes(); + self.registry.pending_requests.push(self.entry); + } +} + +pub(crate) struct FFORRequestPromotion<'a> { + registry: &'a mut FFORRecoveryRegistry, + index: usize, + entry: Entry, +} +impl FFORRequestPromotion<'_> { + pub(crate) fn commit(self) { + let pending = self.registry.pending_requests.remove(self.index); + self.registry.encoded_bytes = + self.registry.encoded_bytes - pending.encoded_bytes + self.entry.encoded_bytes; + self.registry.reserved_transition_bytes = self.registry.reserved_transition_bytes + - pending.reserved_bytes() + + self.entry.reserved_transition_bytes(); + self.registry.entries.push(self.entry); + } +} + +impl FFORRecoveryRegistry { + pub(crate) fn find_request(&self, local_request_id: [u8; 32]) -> Option<&FFORReceiverRequest> { + self.pending_requests + .iter() + .map(|entry| &entry.record) + .chain(self.entries.iter().filter_map(|entry| entry.record.request.as_ref())) + .find(|request| request.local_request_id() == local_request_id) + } + + pub(crate) fn request_keys(&self) -> Vec { + self.pending_requests + .iter() + .map(|pending| pending.key) + .chain( + self.entries + .iter() + .filter(|entry| entry.record.request.is_some()) + .map(|entry| entry.key), + ) + .collect() + } + pub(crate) fn get_request(&self, key: &FFORRecoveryKey) -> Option<&FFORReceiverRequest> { + self.pending_requests + .iter() + .find(|entry| entry.key == *key) + .map(|entry| &entry.record) + .or_else(|| { + self.entries + .iter() + .find(|entry| entry.key == *key) + .and_then(|entry| entry.record.request.as_ref()) + }) + } + pub(crate) fn contains_request(&self, request: &FFORReceiverRequest) -> bool { + let message = match request.validate_recovery() { + Ok(message) => message, + Err(_) => return false, + }; + let key = FFORRecoveryKey { + channel_id: ChannelId(message.header.channel_id), + epoch_id: message.header.epoch_id, + }; + self.get_request(&key).map_or(false, |saved| saved.encode() == request.encode()) + } + + pub(crate) fn prepare_request( + &mut self, request: &FFORReceiverRequest, + ) -> Result, FFORRecoveryError> { + let pending = PendingRequest::new(request.clone())?; + if self.find_request(request.local_request_id()).is_some() { + return Err(FFORRecoveryError::ConflictingRecord); + } + if self.pending_requests.iter().any(|entry| { + entry.key.channel_id == pending.key.channel_id + || (entry.record.chain_hash() == request.chain_hash() + && entry.record.funding_txo() == request.funding_txo()) + }) || self.entries.iter().any(|entry| { + entry.key.channel_id == pending.key.channel_id + || (entry.record.setup.chain_hash() == request.chain_hash() + && entry.record.setup.funding_txo() == request.funding_txo()) + }) { + return Err(FFORRecoveryError::ConflictingRecord); + } + let header_bytes = if self.version() == REQUEST_VERSION { 0 } else { 2 }; + self.check_capacity(pending.encoded_bytes + header_bytes, pending.reserved_bytes())?; + Ok(FFORRequestInsertion { registry: self, entry: pending, header_bytes }) + } + + pub(crate) fn prepare_accept( + &mut self, setup: &FFORReceiverSetup, + ) -> Result, FFORRecoveryError> { + let authenticated = + setup.validate_recovery().map_err(|_| FFORRecoveryError::InvalidRecord)?; + let key = FFORRecoveryKey { + channel_id: ChannelId(authenticated.header().channel_id), + epoch_id: authenticated.header().epoch_id, + }; + let index = self + .pending_requests + .iter() + .position(|entry| entry.key == key) + .ok_or(FFORRecoveryError::ConflictingRecord)?; + let request = &self.pending_requests[index].record; + if !request.validates_setup(setup) { + return Err(FFORRecoveryError::ConflictingRecord); + } + let entry = Entry::new(StoredSetup { + setup: setup.clone(), + canonical_book: authenticated.canonical_book().to_vec(), + activation: None, + request: Some(request.clone()), + })?; + // Both forms were charged the full record allowance before Init. Promotion cannot increase + // total used plus reserved bytes, and the borrowed permit prevents a conflicting insertion. + Ok(FFORRequestPromotion { registry: self, index, entry }) + } +} diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs index 43775a1bb16..189efbd824c 100644 --- a/lightning/src/ln/ffor_recovery/tests.rs +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -124,7 +124,7 @@ pub(super) fn fixture(identity: u8, maximum_size: bool) -> Fixture { fn stored(record: FFORReceiverSetup) -> StoredSetup { let book = record.validate_recovery().unwrap().canonical_book().to_vec(); - StoredSetup { setup: record, canonical_book: book, activation: None } + StoredSetup { setup: record, canonical_book: book, activation: None, request: None } } fn frame(records: &[Vec]) -> Vec { @@ -293,7 +293,7 @@ fn ffor_recovery_reader_refuses_future_schema_missing_fields_and_truncation() { assert!(decode(&framed[..length]).is_err(), "accepted prefix {}", length); } let mut future = framed.clone(); - future[0] = CLOSE_VERSION + 1; + future[0] = REQUEST_VERSION + 1; assert!(matches!(decode(&future), Err(DecodeError::UnknownRequiredFeature))); // Empty TLV records omit both mandatory fields. assert!(decode(&frame(&[vec![0]])).is_err()); From a20b34f9822e7131d229a526249c917971c25d14 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Mon, 21 Sep 2026 01:11:54 -0400 Subject: [PATCH 15/22] feat(ffor): drive receiver activation and cooperative close --- FFOR.md | 63 +- lightning/src/ln/channel/ffor.rs | 8 +- lightning/src/ln/channel/ffor/drain.rs | 10 + .../src/ln/channel/ffor/setup/request.rs | 2 +- .../src/ln/channelmanager/ffor_activation.rs | 66 +- .../channelmanager/ffor_activation/close.rs | 127 ++++ .../channelmanager/ffor_activation/context.rs | 2 +- .../channelmanager/ffor_activation/driver.rs | 39 +- .../ffor_activation/driver/advance.rs | 593 +++++++++++++++ .../ffor_activation/driver/advance_tests.rs | 712 ++++++++++++++++++ .../ffor_activation/driver/tests.rs | 3 + lightning/src/ln/ffor/driver.rs | 17 +- 12 files changed, 1618 insertions(+), 24 deletions(-) create mode 100644 lightning/src/ln/channelmanager/ffor_activation/driver/advance.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/driver/advance_tests.rs diff --git a/FFOR.md b/FFOR.md index 81cc67090aa..e3bd5eb1ea2 100644 --- a/FFOR.md +++ b/FFOR.md @@ -1,12 +1,12 @@ # Experimental FFOR receiver setup, activation and recovery This fork targets rust-lightning v0.2.5. The current APIs provide receiver voucher -parking and verification of both commitment views for FFOR Variant D. Private manager +parking and verification of both commitment views for FFOR Variant D. Native manager transitions compose activation, reconnect, pre-active abort and cooperative voucher drain with ordered storage. -An experimental public setup facade now owns pre-init admission and synchronous -Accept processing. No production transport or invoice API exposes the complete -activation and recovery path yet. +An experimental public receiver facade owns pre-init admission, synchronous peer input, +activation and cooperative close advancement. Production transport orchestration and +invoice readiness remain unfinished. `prepare_ffor_receiver` checks the current authenticated native peer generation, derives a fresh protocol epoch and signs the exact Init. Before any bytes leave, @@ -16,6 +16,29 @@ completes, the original connection remains current and the settlement deadline h not passed. Backpressure preserves the exact retry; successful queue insertion consumes this one-shot send. Disconnect and restart never replay Init. +After Accept, the same advancement API chooses the next action from native lifecycle +state. It requests an opaque monitor snapshot when proof is needed; callers must drop +the monitor guard before `advance_ffor_receiver_with_monitor`. Complete voucher proof +starts the owned STFU exchange, then a fresh matching snapshot permits activation +preparation. Every transition rechecks the authenticated connection generation under +its own native peer lock. Persisted exact Activate is released at most once, and an +authentic ActivateAck enters the existing persistence-gated Active transition. + +`request_ffor_receiver_close` retains an exact signed close intent. Subsequent advances +release it after persistence, import acknowledged preimages before enabling failures, +drive stock removal rounds and verify the final empty commitment pair before Closed. +Restored Draining and Closed may complete their local persistence gates before stock +reestablish finishes; their outgoing reports depend on that completion. Conflicting +reports retain the fence. An exact CloseAck after a required close replay requests a +fresh reconnect so stock commitment replay can reconcile the new report. + +New setup, STFU and activation releases check the current settlement deadline. +Historical signed acknowledgements and close recovery remain usable after that +deadline. An ambiguous activation is reconciled through a fresh connection, never +replayed. Progress values such as Active, Draining or Closed do not authorize invoices +or witness provisioning. Callers must continue stock peer events, commitment rounds +and monitor/manager persistence between advances. + The application's stable `local_request_id` is local correlation, not a protocol epoch or channel authority. An exact retry returns the retained native selector; changed channel, peer or policy is refused. A bounded historical lookup recovers @@ -149,15 +172,15 @@ completion after channel removal. They cannot release an ordinary fulfill while fence is held. Force-close remains available. A required even field makes older channel readers refuse fenced state. -A crate-private manager owner derives and signs activation from the actual completed +The native manager derives and signs activation from the actual completed owned STFU handshake and monitor proof. It installs the archive and fence under the same consistency boundary, then releases the exact signed bytes at most once after that snapshot is durable. Release rechecks the current phase, height and original handshake. Its transport callback must atomically validate the authenticated connection token with queue insertion, without network I/O or manager callbacks. A signed acknowledgement advances the channel and archive together and requests a -new persistence requirement before Active can be reported. No public runtime invokes -these private transitions yet. +new persistence requirement before Active can be reported. The public receiver facade +selects and invokes these transitions without granting application readiness. The native reconnect codec carries Variant D TLV 55001 while retaining the ordinary BOLT commitment and revocation counter checks. Activating and Active reconnect never @@ -197,7 +220,7 @@ Draining permits only owned voucher removals and their commitment, revocation, m and signer work. Adds, fees, STFU, splicing and ordinary cooperative close remain blocked. A matching peer Draining or Closed report uses normal BOLT replay once permission is released. A matching Active report enters control-message-only recovery for exact -retained close retransmission. After its signed reply the private caller must reconnect +retained close retransmission. After its signed reply the facade requests a reconnect to rebuild ordinary replay obligations; same-connection drain stays disabled. Conflicting reports retain the fence for resolution. Outgoing Draining reports otherwise remain queued until native drain permission is enabled, even when the archive write is complete. @@ -241,8 +264,11 @@ Similarly, an archive without its original live channel cannot independently pro the historical funding context against arbitrary local storage alteration. No feature bit, production custom-message transport, invoice readiness or background -deadline service is implemented here. The facade rechecks the deadline before Init -release and Accept processing; the caller must also cancel a stalled setup. +deadline service is implemented here. The facade rechecks deadlines before new Init, +Accept, STFU and activation work; its caller must continue advancing and may cancel +a stalled pre-activation setup. Cancellation of an owned STFU handshake requests a +native disconnect. Automatic voucher failures wait for controlled release after the +abort revision is durable. Ordinary channel updates can invalidate a previously returned `Parked` proof. `NodeSigner::sign_ffor_message` supplies the protocol's single-SHA256 `ffor/msg` @@ -328,6 +354,15 @@ replies, deadline crossing, retry identity after restore, missing archive eviden capacity refusal, witness policy, cancellation and normal payment after controlled gate release on a fresh connection. +The lifecycle facade tests advance through real voucher and STFU rounds, durable +activation, close, both settled and failed drain, and final Closed in both funding +directions. They cover delayed monitor writes, exact retries, sent or unsent activation +expiry, signed Abort while activation is ambiguous, lost acknowledgements and late +recovery, stale generations, conflicting reports, and close replay followed by a fresh +connection. Both Closed serialization boundaries reload, and ordinary payment works +after the final completion releases the request gate. Owned-STFU cancellation before +abort persistence cannot send voucher failures after an early reconnect. + Native witness tests use four pinned Beignet records to compare ECDH, HKDF, plaintext and preimages; they reject signed ciphertext, ephemeral-key, AAD, manifest and plaintext-term substitution. The shared crate separately covers all six reference scenarios and arbitrary @@ -336,12 +371,12 @@ body bytes. These tests do not exercise production witness transport or key stor ## Next boundary Reusable epochs require durable retired epoch IDs and voucher hashes, with one -current signed transcript record under the same channel authority. Cooperative close -now has private archive and channel transitions, but no production driver invokes them. -That driver must retain witness keys and receipts, reconcile every recovered preimage +current signed transcript record under the same channel authority. The public facade +now composes the native activation and cooperative-close transitions. Production +orchestration must retain witness keys and receipts, reconcile every recovered preimage through the stock monitor, and enforce the configured deadline before claim safety ends. -Production transport must bind the private manager transitions to actual authenticated +Production transport must bind the receiver facade to actual authenticated connections and deliver acknowledgement retries in the required order. Durable witness mailbox recovery, preimage reconciliation, deadline enforcement and invoice eligibility remain separate required boundaries. None can be inferred from durable setup, activation or diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index 5ff91dc7eff..d48bbe95e0f 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -354,7 +354,13 @@ where { self.ffor_abort_revealed_secret_reuse(); let book = match self.context.ffor_receiver_book.as_ref() { - Some(book) if book.abort_reason.is_some() && book.fence.is_none() => book, + Some(book) + if book.abort_reason.is_some() + && book.fence.is_none() + && (book.request.is_none() || book.request_gate_released.unwrap_or(false)) => + { + book + }, _ => return, }; let failures: Vec<_> = book diff --git a/lightning/src/ln/channel/ffor/drain.rs b/lightning/src/ln/channel/ffor/drain.rs index 7a4206a6fcd..793dc48676e 100644 --- a/lightning/src/ln/channel/ffor/drain.rs +++ b/lightning/src/ln/channel/ffor/drain.rs @@ -325,6 +325,11 @@ where } } + /// Scheduling observation only. The opaque final proof still checks both views and monitor. + pub(crate) fn ffor_receiver_drain_pending(&self) -> bool { + !self.context.pending_inbound_htlcs.is_empty() || self.check_ffor_synchronized().is_err() + } + pub(crate) fn ffor_receiver_drain_enabled(&self) -> bool { self.context.ffor_drain_enabled() } @@ -602,6 +607,11 @@ where } drain.closed = true; book.fence = None; + // The same completed Closed proof releases pre-init interception, if this epoch used it. + // No separate gate flag may authorize ordinary traffic before the final barrier. + if book.request.is_some() { + book.request_gate_released = Some(true); + } Ok(()) } } diff --git a/lightning/src/ln/channel/ffor/setup/request.rs b/lightning/src/ln/channel/ffor/setup/request.rs index 323f0e1b420..52f1aeea7d9 100644 --- a/lightning/src/ln/channel/ffor/setup/request.rs +++ b/lightning/src/ln/channel/ffor/setup/request.rs @@ -307,7 +307,7 @@ where if let Some(setup) = book.setup.as_ref() { if !request.validates_setup(setup) || (book.request_gate_released.unwrap_or(false) - && (book.abort_reason.is_none() || book.fence.is_some())) + && ((book.abort_reason.is_none() && !book.is_closed()) || book.fence.is_some())) { return Err(DecodeError::InvalidValue); } diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs index 82f7afbc2cb..54a998abf2e 100644 --- a/lightning/src/ln/channelmanager/ffor_activation.rs +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -1,6 +1,6 @@ //! Private composition of receiver activation, retained evidence and ordered persistence. //! -//! No public activation or invoice API is exposed until reconnect and controlled drain are wired. +//! The experimental driver selects transitions; no observation authorizes invoice exposure. use super::*; use crate::ln::channel::{FFORReceiverFencePhase, FFORReestablishOutcome}; @@ -106,18 +106,33 @@ where L::Target: Logger, { /// Derive and sign from the actual quiescent channel. This returns no outbound bytes. + #[cfg(test)] pub(crate) fn prepare_ffor_receiver_activation( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], monitor: &FFORMonitorSnapshot, + ) -> Result { + self.prepare_ffor_receiver_activation_on_connection( + channel_id, + counterparty_node_id, + epoch_id, + monitor, + None, + ) + } + + pub(crate) fn prepare_ffor_receiver_activation_on_connection( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + monitor: &FFORMonitorSnapshot, connection: Option<&crate::ln::ffor::FFORPeerConnection>, ) -> Result { let _guard = PersistenceNotifierGuard::notify_on_drop(self); - let current_height = self.best_block.read().unwrap().height; let peers = self.per_peer_state.read().unwrap(); let mut peer = peers .get(counterparty_node_id) .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; + let current_height = self.best_block.read().unwrap().height; let channel = peer .channel_by_id .get_mut(channel_id) @@ -214,10 +229,27 @@ where /// Enqueue at most once to the original connection after its exact snapshot is durable. /// The callback must check its authenticated transport token atomically with queue insertion. /// It must not call back into the manager, acquire a monitor, or perform network I/O. + #[cfg(test)] pub(crate) fn release_ffor_receiver_activation( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], enqueue: C, ) -> Result + where + C: FnOnce(&[u8]) -> Result<(), ()>, + { + self.release_ffor_receiver_activation_on_connection( + channel_id, + counterparty_node_id, + epoch_id, + enqueue, + None, + ) + } + + pub(crate) fn release_ffor_receiver_activation_on_connection( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + enqueue: C, connection: Option<&crate::ln::ffor::FFORPeerConnection>, + ) -> Result where C: FnOnce(&[u8]) -> Result<(), ()>, { @@ -228,6 +260,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; let channel = peer .channel_by_id .get(channel_id) @@ -267,9 +300,23 @@ where /// Retain an exact authenticated S acknowledgement under the same phase and archive authority. /// The caller must supply the peer identity from the current authenticated transport callback. + #[cfg(test)] pub(crate) fn accept_ffor_receiver_activation_ack( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], ack_wire: &[u8], + ) -> Result { + self.accept_ffor_receiver_activation_ack_on_connection( + channel_id, + counterparty_node_id, + epoch_id, + ack_wire, + None, + ) + } + + pub(crate) fn accept_ffor_receiver_activation_ack_on_connection( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ack_wire: &[u8], connection: Option<&crate::ln::ffor::FFORPeerConnection>, ) -> Result { let _guard = PersistenceNotifierGuard::notify_on_drop(self); let peers = self.per_peer_state.read().unwrap(); @@ -278,6 +325,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; let channel = peer .channel_by_id .get_mut(channel_id) @@ -398,8 +446,21 @@ where } /// Release only a durably recorded pre-active abort. Existing monitor claims remain ahead of failures. + #[cfg(test)] pub(crate) fn release_ffor_receiver_reconnect_abort( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result { + self.release_ffor_receiver_reconnect_abort_on_connection( + channel_id, + counterparty_node_id, + epoch_id, + None, + ) + } + + pub(crate) fn release_ffor_receiver_reconnect_abort_on_connection( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + connection: Option<&crate::ln::ffor::FFORPeerConnection>, ) -> Result { let _guard = PersistenceNotifierGuard::notify_on_drop(self); let peers = self.per_peer_state.read().unwrap(); @@ -408,6 +469,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; let channel = peer .channel_by_id .get_mut(channel_id) diff --git a/lightning/src/ln/channelmanager/ffor_activation/close.rs b/lightning/src/ln/channelmanager/ffor_activation/close.rs index ac6cb3d30ba..23acca0cc20 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/close.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/close.rs @@ -25,9 +25,52 @@ where MR::Target: MessageRouter, L::Target: Logger, { + fn validate_ffor_close_transition( + &self, channel: &FundedChannel, recovery: &FFORRecoveryRegistry, + allow_close_replay: bool, + ) -> Result<(), FFORReceiverError> { + let setup = channel + .ffor_receiver_setup_record() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + .ok_or(FFORReceiverError::NotRegistered)?; + recovery + .validate_channel_lifecycle( + &setup, + channel.ffor_receiver_fence(), + channel.ffor_receiver_abort_reason(), + channel.ffor_receiver_drain_binding(), + channel.ffor_receiver_closed_completion_hash(), + channel.ffor_receiver_drain_activation_hash(), + ) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + match channel.ffor_receiver_reconnect_outcome() { + Some(FFORReestablishOutcome::ResolutionRequired { .. }) + | Some(FFORReestablishOutcome::AbortRequired { .. }) => { + Err(FFORCommitmentError::PendingUpdates.into()) + }, + Some(FFORReestablishOutcome::CloseReplayRequired { .. }) if !allow_close_replay => { + Err(FFORCommitmentError::PendingUpdates.into()) + }, + _ => Ok(()), + } + } + /// Sign and retain close intent. Exact retries reuse the existing bytes and barrier. + #[cfg(test)] pub(crate) fn prepare_ffor_receiver_close( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result { + self.prepare_ffor_receiver_close_on_connection( + channel_id, + counterparty_node_id, + epoch_id, + None, + ) + } + + pub(crate) fn prepare_ffor_receiver_close_on_connection( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + connection: Option<&crate::ln::ffor::FFORPeerConnection>, ) -> Result { let _guard = PersistenceNotifierGuard::notify_on_drop(self); let peers = self.per_peer_state.read().unwrap(); @@ -36,6 +79,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; let channel = peer .channel_by_id .get(channel_id) @@ -43,6 +87,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)?; let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; let mut recovery = self.ffor_recovery.lock().unwrap(); + self.validate_ffor_close_transition(channel, &recovery, true)?; let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?.clone(); let previous = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; let hash = previous @@ -97,10 +142,27 @@ where /// Exact close retransmission is allowed after reconnect. The callback atomically verifies the /// current authenticated connection and enqueues a bounded message without manager reentry or I/O. + #[cfg(test)] pub(crate) fn release_ffor_receiver_close( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], enqueue: C, ) -> Result + where + C: FnOnce(&[u8]) -> Result<(), ()>, + { + self.release_ffor_receiver_close_on_connection( + channel_id, + counterparty_node_id, + epoch_id, + enqueue, + None, + ) + } + + pub(crate) fn release_ffor_receiver_close_on_connection( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + enqueue: C, connection: Option<&crate::ln::ffor::FFORPeerConnection>, + ) -> Result where C: FnOnce(&[u8]) -> Result<(), ()>, { @@ -111,6 +173,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; let channel = peer .channel_by_id .get(channel_id) @@ -166,9 +229,23 @@ where } /// Authenticate an exact settlement response and install the still-disabled channel drain. + #[cfg(test)] pub(crate) fn accept_ffor_receiver_close_ack( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], ack_wire: &[u8], + ) -> Result { + self.accept_ffor_receiver_close_ack_on_connection( + channel_id, + counterparty_node_id, + epoch_id, + ack_wire, + None, + ) + } + + pub(crate) fn accept_ffor_receiver_close_ack_on_connection( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ack_wire: &[u8], connection: Option<&crate::ln::ffor::FFORPeerConnection>, ) -> Result { let _guard = PersistenceNotifierGuard::notify_on_drop(self); let peers = self.per_peer_state.read().unwrap(); @@ -177,6 +254,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; let channel = peer .channel_by_id .get_mut(channel_id) @@ -184,6 +262,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)?; let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; let mut recovery = self.ffor_recovery.lock().unwrap(); + self.validate_ffor_close_transition(channel, &recovery, true)?; let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?.clone(); let previous = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; let next = previous @@ -209,8 +288,21 @@ where /// Import every signed preimage before enabling any failure. Repeated calls and restart use /// the stock idempotent claim path, whose own monitor persistence gates fulfill wire. + #[cfg(test)] pub(crate) fn release_ffor_receiver_drain( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result { + self.release_ffor_receiver_drain_on_connection( + channel_id, + counterparty_node_id, + epoch_id, + None, + ) + } + + pub(crate) fn release_ffor_receiver_drain_on_connection( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + connection: Option<&crate::ln::ffor::FFORPeerConnection>, ) -> Result { let _guard = PersistenceNotifierGuard::notify_on_drop(self); let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; @@ -221,12 +313,14 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; let channel = peer .channel_by_id .get(channel_id) .and_then(Channel::as_funded) .ok_or(FFORCommitmentError::ChannelUnavailable)?; let recovery = self.ffor_recovery.lock().unwrap(); + self.validate_ffor_close_transition(channel, &recovery, false)?; let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?; let activation = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; @@ -271,12 +365,14 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; let channel = peer .channel_by_id .get_mut(channel_id) .and_then(Channel::as_funded_mut) .ok_or(FFORCommitmentError::ChannelUnavailable)?; let recovery = self.ffor_recovery.lock().unwrap(); + self.validate_ffor_close_transition(channel, &recovery, false)?; let activation = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; if activation.is_closed() @@ -299,9 +395,23 @@ where } /// Capture both empty commitment views and retain Closed before releasing ordinary updates. + #[cfg(test)] pub(crate) fn prepare_ffor_receiver_closed( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], monitor: &FFORMonitorSnapshot, + ) -> Result { + self.prepare_ffor_receiver_closed_on_connection( + channel_id, + counterparty_node_id, + epoch_id, + monitor, + None, + ) + } + + pub(crate) fn prepare_ffor_receiver_closed_on_connection( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + monitor: &FFORMonitorSnapshot, connection: Option<&crate::ln::ffor::FFORPeerConnection>, ) -> Result { let _guard = PersistenceNotifierGuard::notify_on_drop(self); let peers = self.per_peer_state.read().unwrap(); @@ -310,6 +420,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; let channel = peer .channel_by_id .get_mut(channel_id) @@ -317,6 +428,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)?; let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; let mut recovery = self.ffor_recovery.lock().unwrap(); + self.validate_ffor_close_transition(channel, &recovery, false)?; let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?.clone(); let previous = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; if previous.is_closed() { @@ -341,8 +453,21 @@ where Ok(requirement) } + #[cfg(test)] pub(crate) fn release_ffor_receiver_closed( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + ) -> Result { + self.release_ffor_receiver_closed_on_connection( + channel_id, + counterparty_node_id, + epoch_id, + None, + ) + } + + pub(crate) fn release_ffor_receiver_closed_on_connection( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], + connection: Option<&crate::ln::ffor::FFORPeerConnection>, ) -> Result { let _guard = PersistenceNotifierGuard::notify_on_drop(self); let peers = self.per_peer_state.read().unwrap(); @@ -351,6 +476,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); + self.require_current_ffor_connection(&peer, counterparty_node_id, connection)?; let channel = peer .channel_by_id .get_mut(channel_id) @@ -358,6 +484,7 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)?; let key = FFORRecoveryKey { channel_id: *channel_id, epoch_id }; let recovery = self.ffor_recovery.lock().unwrap(); + self.validate_ffor_close_transition(channel, &recovery, false)?; let activation = recovery.get_activation(&key).ok_or(FFORReceiverError::NotRegistered)?; let close = activation.close_record().ok_or(FFORCommitmentError::PendingUpdates)?; let digest = close.completion_hash().ok_or(FFORCommitmentError::PendingUpdates)?; diff --git a/lightning/src/ln/channelmanager/ffor_activation/context.rs b/lightning/src/ln/channelmanager/ffor_activation/context.rs index beed9a37b5d..e5fdcba124a 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/context.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/context.rs @@ -154,7 +154,7 @@ where Ok(()) } - fn ffor_active_context_locked( + pub(in crate::ln::channelmanager) fn ffor_active_context_locked( &self, channel: &FundedChannel, recovery: &FFORRecoveryRegistry, key: &FFORRecoveryKey, ) -> Result { channel diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver.rs b/lightning/src/ln/channelmanager/ffor_activation/driver.rs index d47001edaf5..ca8f512e081 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/driver.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/driver.rs @@ -7,6 +7,8 @@ use crate::ln::ffor::{ }; use lightning_ffor::wire::Header; +mod advance; + impl< M: Deref, T: Deref, @@ -29,13 +31,30 @@ where MR::Target: MessageRouter, L::Target: Logger, { + pub(in crate::ln::channelmanager) fn require_current_ffor_connection( + &self, peer: &PeerState, node_id: &PublicKey, connection: Option<&FFORPeerConnection>, + ) -> Result<(), FFORReceiverError> { + // Legacy in-module channel tests exercise private transitions without a transport. Every + // non-test call must carry actual native connection authority, including crate-internal use. + #[cfg(not(test))] + if connection.is_none() { + return Err(FFORCommitmentError::ChannelUnavailable.into()); + } + if let Some(connection) = connection { + if !peer.is_connected || !connection.matches(*node_id, peer.ffor_connection.as_ref()) { + return Err(FFORCommitmentError::ChannelUnavailable.into()); + } + } + Ok(()) + } + /// Stage receiver-owned interception and exact signed Init before any peer traffic is released. /// /// The supplied connection must be the current authenticated native generation. The channel /// must be supported, empty and synchronized. The engine generates the epoch, checks native /// limits and reserves bounded storage for the eventual accepted transcript and terminal state. /// No bytes are returned. Call `advance_ffor_receiver` after ordered manager persistence. - /// This experimental setup facade does not activate offline receiving or authorize invoices. + /// This experimental facade does not authorize invoices or advertise offline readiness. pub fn prepare_ffor_receiver( &self, channel_id: &ChannelId, connection: &FFORPeerConnection, parameters: FFORReceiverParameters, @@ -153,7 +172,7 @@ where /// manager. Returning `Err(())` preserves the exact retry; `Ok(())` consumes this one-shot send. /// A disconnected or restored negotiation never sends Init again. Accepted setup reports only /// voucher-round progress here; activation and invoice readiness are not exposed by this slice. - pub fn advance_ffor_receiver( + fn advance_ffor_receiver_setup( &self, id: &FFORReceiverId, connection: &FFORPeerConnection, enqueue: C, ) -> Result where @@ -257,7 +276,7 @@ where /// A durable pre-init gate already prevents a stale-manager crash from exposing these as normal /// payments. A malformed or incompatible setup irreversibly aborts pending requests for this peer. /// This bounded facade currently handles Accept and signed pre-accept Abort only. - pub fn handle_ffor_receiver_message( + fn handle_ffor_receiver_setup_message( &self, connection: &FFORPeerConnection, wire: &[u8], ) -> Result { let _guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -386,6 +405,7 @@ where /// Irreversibly cancel a setup before activation, retaining its interception gate. /// After persistence, reconnect and advance again to release the gate for ordinary traffic. + /// If FFOR already owns an STFU handshake, this queues the required native disconnect warning. /// This cannot cancel an activation that may already have reached the peer. pub fn cancel_ffor_receiver_setup( &self, id: &FFORReceiverId, connection: &FFORPeerConnection, @@ -435,7 +455,20 @@ where .ok_or(FFORReceiverError::RecoveryUnavailable)?; entry.requirement = requirement; entry.may_send_init = false; + let reconnect = channel.release_ffor_receiver_quiescence(); channel.ffor_queue_aborted_vouchers(&self.logger); + if reconnect { + peer.pending_msg_events.push(MessageSendEvent::HandleError { + node_id: connection.peer, + action: msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id: id.channel_id, + data: "FFOR setup cancellation requires reconnect before voucher drain" + .to_owned(), + }, + }, + }); + } Ok(FFORReceiverProgress::AwaitingPersistence) } diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver/advance.rs b/lightning/src/ln/channelmanager/ffor_activation/driver/advance.rs new file mode 100644 index 00000000000..6b8de802810 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/advance.rs @@ -0,0 +1,593 @@ +//! Thin receiver orchestration over the channel's existing lifecycle and persistence authority. + +use super::*; + +// An ephemeral choice, rechecked by each transition under its own current-generation peer lock. +// It is never serialized, exposed as authority, or used to authorize a later transport enqueue. +enum ReceiverStep { + Observe(FFORReceiverProgress), + Setup, + Quiesce, + PrepareActivation, + ReleaseActivation, + ReleaseAbort, + ReleaseClose, + ReleaseDrain, + PrepareClosed, + ReleaseClosed, + Reconnect, +} + +enum ReconnectCause<'a> { + Deadline, + Abort(&'a FFORMessage), + CloseAcknowledged(&'a [u8]), +} + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Advance one receiver operation chosen exclusively from current native state. + /// + /// The current native connection generation is checked under every transition lock. Exact Init + /// and Activate are released at most once on their original connection after persistence. Close + /// may be replayed when native reconciliation permits it. `enqueue` must atomically check its + /// paired authenticated transport token and insert into a bounded queue, with no I/O or manager + /// reentry. `Err(())` preserves exact pending bytes. No progress value authorizes an invoice. + /// + /// When progress is `NeedsMonitorSnapshot`, capture a fresh monitor snapshot outside manager + /// locks and call [`Self::advance_ffor_receiver_with_monitor`]. Continue stock events and peer + /// processing for STFU, commitment rounds, monitor persistence and requested disconnects. + pub fn advance_ffor_receiver( + &self, id: &FFORReceiverId, connection: &FFORPeerConnection, enqueue: C, + ) -> Result + where + C: FnOnce(&[u8]) -> Result<(), ()>, + { + self.advance_ffor_receiver_inner(id, connection, None, enqueue) + } + + /// Advance using a fresh opaque monitor snapshot captured before entering native peer locks. + /// + /// Drop the monitor guard before calling. The channel verifies exact identities and completed + /// updates again; a stale snapshot cannot start activation or prove Closed. This consumes the + /// snapshot because the first complete book proof becomes the owned STFU handshake's evidence. + pub fn advance_ffor_receiver_with_monitor( + &self, id: &FFORReceiverId, connection: &FFORPeerConnection, monitor: FFORMonitorSnapshot, + enqueue: C, + ) -> Result + where + C: FnOnce(&[u8]) -> Result<(), ()>, + { + self.advance_ffor_receiver_inner(id, connection, Some(monitor), enqueue) + } + + fn advance_ffor_receiver_inner( + &self, id: &FFORReceiverId, connection: &FFORPeerConnection, + monitor: Option, enqueue: C, + ) -> Result + where + C: FnOnce(&[u8]) -> Result<(), ()>, + { + let step = self.ffor_receiver_next_step(id, connection, monitor.as_ref())?; + let peer = &connection.peer; + match step { + ReceiverStep::Observe(progress) => Ok(progress), + ReceiverStep::Setup => self.advance_ffor_receiver_setup(id, connection, enqueue), + ReceiverStep::Quiesce => { + self.ffor_driver_request_quiescence( + id, + connection, + monitor.ok_or(FFORCommitmentError::MonitorMismatch)?, + )?; + Ok(FFORReceiverProgress::AwaitingPeer) + }, + ReceiverStep::PrepareActivation => { + self.prepare_ffor_receiver_activation_on_connection( + &id.channel_id, + peer, + id.epoch_id, + monitor.as_ref().ok_or(FFORCommitmentError::MonitorMismatch)?, + Some(connection), + )?; + Ok(FFORReceiverProgress::AwaitingPersistence) + }, + ReceiverStep::ReleaseActivation => { + let mut refused = false; + let sent = self.release_ffor_receiver_activation_on_connection( + &id.channel_id, + peer, + id.epoch_id, + |wire| { + let result = enqueue(wire); + refused = result.is_err(); + result + }, + Some(connection), + )?; + Ok(if refused { + FFORReceiverProgress::Backpressured + } else if sent { + FFORReceiverProgress::AwaitingPeer + } else { + FFORReceiverProgress::AwaitingPersistence + }) + }, + ReceiverStep::ReleaseAbort => { + if self.release_ffor_receiver_reconnect_abort_on_connection( + &id.channel_id, + peer, + id.epoch_id, + Some(connection), + )? { + // The retained request gate still requires its normal fresh-connection release. + self.advance_ffor_receiver_setup(id, connection, enqueue) + } else { + Ok(FFORReceiverProgress::AwaitingPersistence) + } + }, + ReceiverStep::ReleaseClose => { + let mut refused = false; + let sent = self.release_ffor_receiver_close_on_connection( + &id.channel_id, + peer, + id.epoch_id, + |wire| { + let result = enqueue(wire); + refused = result.is_err(); + result + }, + Some(connection), + )?; + Ok(if refused { + FFORReceiverProgress::Backpressured + } else if sent { + FFORReceiverProgress::AwaitingPeer + } else { + FFORReceiverProgress::AwaitingPersistence + }) + }, + ReceiverStep::ReleaseDrain => { + if self.release_ffor_receiver_drain_on_connection( + &id.channel_id, + peer, + id.epoch_id, + Some(connection), + )? { + Ok(FFORReceiverProgress::Draining) + } else { + Ok(FFORReceiverProgress::AwaitingPersistence) + } + }, + ReceiverStep::PrepareClosed => { + self.prepare_ffor_receiver_closed_on_connection( + &id.channel_id, + peer, + id.epoch_id, + monitor.as_ref().ok_or(FFORCommitmentError::MonitorMismatch)?, + Some(connection), + )?; + Ok(FFORReceiverProgress::AwaitingPersistence) + }, + ReceiverStep::ReleaseClosed => { + if self.release_ffor_receiver_closed_on_connection( + &id.channel_id, + peer, + id.epoch_id, + Some(connection), + )? { + Ok(FFORReceiverProgress::Closed) + } else { + Ok(FFORReceiverProgress::AwaitingPersistence) + } + }, + ReceiverStep::Reconnect => self + .ffor_driver_request_reconnect(id, connection, ReconnectCause::Deadline) + .map(|progress| progress.unwrap_or(FFORReceiverProgress::AwaitingPeer)), + } + } + + fn ffor_receiver_next_step( + &self, id: &FFORReceiverId, connection: &FFORPeerConnection, + monitor: Option<&FFORMonitorSnapshot>, + ) -> Result { + let _guard = self.total_consistency_lock.read().unwrap(); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(&connection.peer) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + self.require_current_ffor_connection(&peer, &connection.peer, Some(connection))?; + let channel = peer + .channel_by_id + .get(&id.channel_id) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let request = channel.ffor_receiver_request().ok_or(FFORReceiverError::NotRegistered)?; + let message = + request.validate_recovery().map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if message.header.epoch_id != id.epoch_id { + return Err(FFORReceiverError::UnknownEpoch); + } + let key = FFORRecoveryKey { channel_id: id.channel_id, epoch_id: id.epoch_id }; + let recovery = self.ffor_recovery.lock().unwrap(); + if !recovery.contains_request(request) { + return Err(FFORReceiverError::RecoveryUnavailable); + } + let runtime = self.ffor_activation.lock().unwrap(); + let entry = runtime.get(&key)?; + if !self.ffor_persistence.lock().unwrap().is_complete(&entry.requirement) { + return Ok(ReceiverStep::Observe(FFORReceiverProgress::AwaitingPersistence)); + } + let setup = match recovery.get(&key) { + Some(setup) => setup, + None => return Ok(ReceiverStep::Setup), + }; + recovery + .validate_channel_lifecycle( + setup, + channel.ffor_receiver_fence(), + channel.ffor_receiver_abort_reason(), + channel.ffor_receiver_drain_binding(), + channel.ffor_receiver_closed_completion_hash(), + channel.ffor_receiver_drain_activation_hash(), + ) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if matches!( + channel.ffor_receiver_reconnect_outcome(), + Some(FFORReestablishOutcome::ResolutionRequired { .. }) + ) { + return Ok(ReceiverStep::Observe(FFORReceiverProgress::ResolutionRequired)); + } + if let Some(activation) = recovery.get_activation(&key) { + if activation.is_aborted() { + return Ok(ReceiverStep::ReleaseAbort); + } + if matches!( + channel.ffor_receiver_reconnect_outcome(), + Some(FFORReestablishOutcome::AbortRequired { .. }) + ) { + return Ok(ReceiverStep::Observe(FFORReceiverProgress::ResolutionRequired)); + } + if matches!( + channel.ffor_receiver_reconnect_outcome(), + Some(FFORReestablishOutcome::CloseReplayRequired { .. }) + ) { + return Ok(ReceiverStep::ReleaseClose); + } + if activation.is_closed() { + return Ok(ReceiverStep::ReleaseClosed); + } + if activation.is_draining() { + if !channel.ffor_receiver_drain_enabled() { + return Ok(ReceiverStep::ReleaseDrain); + } + if channel.ffor_receiver_drain_pending() { + return Ok(ReceiverStep::Observe(FFORReceiverProgress::Draining)); + } + return Ok(if monitor.is_some() { + ReceiverStep::PrepareClosed + } else { + ReceiverStep::Observe(FFORReceiverProgress::NeedsMonitorSnapshot) + }); + } + if activation.close_record().is_some() { + return Ok(ReceiverStep::ReleaseClose); + } + if activation.is_active() { + // The context helper takes the runtime lock itself. Preserve the peer/archive lock + // while dropping only the process-local entry guard before its exact proof check. + drop(runtime); + self.ffor_active_context_locked(channel, &recovery, &key)?; + return Ok(ReceiverStep::Observe(FFORReceiverProgress::Active)); + } + if !entry.may_send_activate && !channel.has_ffor_receiver_quiescence(id.epoch_id) { + // Reconnect must finish observing the peer, and matching Active must wait for its + // exact signed ACK even after D. The original STFU connection is bounded below. + return Ok(ReceiverStep::Observe(FFORReceiverProgress::AwaitingPeer)); + } + let height = self.best_block.read().unwrap().height; + let authenticated = + setup.validate_recovery().map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let activate = FFORMessage::decode(activation.activate_wire()) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if authenticated + .validate_activation(&activate, activation.commitment_hash(), height) + .is_err() + { + return Ok(ReceiverStep::Reconnect); + } + return Ok(if entry.may_send_activate { + ReceiverStep::ReleaseActivation + } else { + ReceiverStep::Observe(FFORReceiverProgress::AwaitingPeer) + }); + } + if channel.ffor_receiver_abort_reason().is_some() { + return Ok(ReceiverStep::Setup); + } + let authenticated = + setup.validate_recovery().map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let height = self.best_block.read().unwrap().height; + if height >= authenticated.terms().settlement_deadline { + return Ok(ReceiverStep::Reconnect); + } + match channel.ffor_receiver_quiescence_status(id.epoch_id, height, &self.logger) { + Ok(FFORReceiverQuiescenceStatus::Negotiating) => { + return Ok(ReceiverStep::Observe(FFORReceiverProgress::AwaitingPeer)) + }, + Ok(FFORReceiverQuiescenceStatus::Quiescent) => { + return Ok(if monitor.is_some() { + ReceiverStep::PrepareActivation + } else { + ReceiverStep::Observe(FFORReceiverProgress::NeedsMonitorSnapshot) + }) + }, + Err(FFORReceiverError::ChannelState(FFORCommitmentError::ChannelUnavailable)) => {}, + Err(error) => return Err(error), + } + let monitor = match monitor { + Some(monitor) => monitor, + None => return Ok(ReceiverStep::Observe(FFORReceiverProgress::NeedsMonitorSnapshot)), + }; + match channel.ffor_receiver_book_status(monitor, &self.logger)? { + FFORReceiverStatus::Parked { .. } => Ok(ReceiverStep::Quiesce), + FFORReceiverStatus::Registered { .. } => { + Ok(ReceiverStep::Observe(FFORReceiverProgress::AwaitingVoucherCommitments)) + }, + FFORReceiverStatus::Aborting { reason } | FFORReceiverStatus::Aborted { reason } => { + Ok(ReceiverStep::Observe(FFORReceiverProgress::Aborted { reason })) + }, + } + } + + fn ffor_driver_request_quiescence( + &self, id: &FFORReceiverId, connection: &FFORPeerConnection, monitor: FFORMonitorSnapshot, + ) -> Result<(), FFORReceiverError> { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(&connection.peer) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + self.require_current_ffor_connection(&peer, &connection.peer, Some(connection))?; + if !self.init_features().supports_quiescence() + || !peer.latest_features.supports_quiescence() + { + return Err(FFORCommitmentError::UnsupportedChannelType.into()); + } + let channel = peer + .channel_by_id + .get_mut(&id.channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: id.channel_id, epoch_id: id.epoch_id }; + let recovery = self.ffor_recovery.lock().unwrap(); + if recovery.get_activation(&key).is_some() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let runtime = self.ffor_activation.lock().unwrap(); + if !self.ffor_persistence.lock().unwrap().is_complete(&runtime.get(&key)?.requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let height = self.best_block.read().unwrap().height; + if let Some(msg) = + channel.request_ffor_receiver_quiescence(id.epoch_id, monitor, height, &self.logger)? + { + peer.pending_msg_events + .push(MessageSendEvent::SendStfu { node_id: connection.peer, msg }); + } + Ok(()) + } + + /// Retain a signed close intent for a natively Active epoch. Repeated calls reuse exact bytes. + /// The close is released only by advance after its current persistence barrier completes. + pub fn request_ffor_receiver_close( + &self, id: &FFORReceiverId, connection: &FFORPeerConnection, + ) -> Result { + self.prepare_ffor_receiver_close_on_connection( + &id.channel_id, + &connection.peer, + id.epoch_id, + Some(connection), + )?; + Ok(FFORReceiverProgress::AwaitingPersistence) + } + + /// Synchronously authenticate exact receiver protocol input on its native peer generation. + /// Accept installs ownership before subsequent stock add frames can be delivered. Activation + /// and close acknowledgements enter their native persisted transitions, never a transport-owned + /// phase. Delayed exact acknowledgements remain historical even after the settlement deadline. + pub fn handle_ffor_receiver_message( + &self, connection: &FFORPeerConnection, wire: &[u8], + ) -> Result { + let message = match FFORMessage::decode(wire) { + Ok(message) => message, + Err(_) => return self.handle_ffor_receiver_setup_message(connection, wire), + }; + let id = FFORReceiverId { + channel_id: ChannelId(message.header.channel_id), + epoch_id: message.header.epoch_id, + }; + let requirement = match message.payload { + Payload::ActivateAck(_) => self.accept_ffor_receiver_activation_ack_on_connection( + &id.channel_id, + &connection.peer, + id.epoch_id, + wire, + Some(connection), + )?, + Payload::CloseAck(_) => self.accept_ffor_receiver_close_ack_on_connection( + &id.channel_id, + &connection.peer, + id.epoch_id, + wire, + Some(connection), + )?, + Payload::Abort(_) => { + return self.ffor_driver_handle_abort(&id, connection, &message, wire) + }, + _ => return self.handle_ffor_receiver_setup_message(connection, wire), + }; + if matches!(message.payload, Payload::CloseAck(_)) { + if let Some(progress) = self.ffor_driver_request_reconnect( + &id, + connection, + ReconnectCause::CloseAcknowledged(wire), + )? { + return Ok(progress); + } + } + Ok(if self.is_ffor_state_persisted(&requirement) { + FFORReceiverProgress::AwaitingPeer + } else { + FFORReceiverProgress::AwaitingPersistence + }) + } + + fn ffor_driver_handle_abort( + &self, id: &FFORReceiverId, connection: &FFORPeerConnection, message: &FFORMessage, + wire: &[u8], + ) -> Result { + match self.ffor_driver_request_reconnect(id, connection, ReconnectCause::Abort(message))? { + Some(progress) => Ok(progress), + None => self.handle_ffor_receiver_setup_message(connection, wire), + } + } + + fn ffor_driver_request_reconnect( + &self, id: &FFORReceiverId, connection: &FFORPeerConnection, cause: ReconnectCause<'_>, + ) -> Result, FFORReceiverError> { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let mut peer = peers + .get(&connection.peer) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + self.require_current_ffor_connection(&peer, &connection.peer, Some(connection))?; + let channel = peer + .channel_by_id + .get_mut(&id.channel_id) + .and_then(Channel::as_funded_mut) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let key = FFORRecoveryKey { channel_id: id.channel_id, epoch_id: id.epoch_id }; + let recovery = self.ffor_recovery.lock().unwrap(); + let setup = match recovery.get(&key) { + Some(setup) => setup, + None if matches!(cause, ReconnectCause::Abort(_)) => return Ok(None), + None => return Err(FFORReceiverError::UnknownEpoch), + }; + let authenticated = + setup.validate_recovery().map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if authenticated.header().epoch_id != id.epoch_id { + return Err(FFORReceiverError::UnknownEpoch); + } + recovery + .validate_channel_lifecycle( + setup, + channel.ffor_receiver_fence(), + channel.ffor_receiver_abort_reason(), + channel.ffor_receiver_drain_binding(), + channel.ffor_receiver_closed_completion_hash(), + channel.ffor_receiver_drain_activation_hash(), + ) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let activation = recovery.get_activation(&key); + match cause { + ReconnectCause::Abort(message) => { + message + .verify_signature(&connection.peer) + .map_err(|_| FFORCommitmentError::InvalidVoucherBook)?; + if !matches!(&message.payload, Payload::Abort(abort) if abort.transcript_hash == authenticated.setup_hash()) + { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + if activation.map_or(false, |record| record.is_active()) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + }, + ReconnectCause::Deadline => { + let height = self.best_block.read().unwrap().height; + if let Some(activation) = activation { + if activation.is_active() || activation.is_aborted() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let message = FFORMessage::decode(activation.activate_wire()) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if authenticated + .validate_activation(&message, activation.commitment_hash(), height) + .is_ok() + { + return Ok(None); + } + } else if height < authenticated.terms().settlement_deadline { + return Ok(None); + } + }, + ReconnectCause::CloseAcknowledged(wire) => { + let activation = activation.ok_or(FFORReceiverError::NotRegistered)?; + if activation.close_record().and_then(|close| close.acknowledgement_wire()) + != Some(wire) + { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + match channel.ffor_receiver_reconnect_outcome() { + Some(FFORReestablishOutcome::CloseReplayRequired { peer_report }) + if peer_report.epoch_id == id.epoch_id + && peer_report.activation_hash + == activation + .activation_hash(setup) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? => {}, + _ => return Ok(None), + } + }, + } + if recovery.get_activation(&key).is_none() && channel.ffor_receiver_abort_reason().is_none() + { + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + channel.abort_ffor_receiver_request(FFORReceiverAbortReason::SetupRejected)?; + let mut runtime = self.ffor_activation.lock().unwrap(); + let entry = runtime + .entries + .iter_mut() + .find(|entry| entry.key == key) + .ok_or(FFORReceiverError::RecoveryUnavailable)?; + entry.requirement = requirement; + entry.may_send_init = false; + } + // A retained activation is never cleared here. The authenticated reestablish report must + // select the existing native abort or ACK-loss path on the next connection. + if !peer.pending_msg_events.iter().any(|event| matches!(event, MessageSendEvent::HandleError { node_id, action: msgs::ErrorAction::DisconnectPeerWithWarning { msg } } if *node_id == connection.peer && msg.channel_id == id.channel_id)) { + peer.pending_msg_events.push(MessageSendEvent::HandleError { node_id: connection.peer, + action: msgs::ErrorAction::DisconnectPeerWithWarning { msg: msgs::WarningMessage { channel_id: id.channel_id, data: "FFOR receiver requires connection reconciliation".to_owned() } } }); + } + Ok(Some(FFORReceiverProgress::ReconnectRequired)) + } +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver/advance_tests.rs b/lightning/src/ln/channelmanager/ffor_activation/driver/advance_tests.rs new file mode 100644 index 00000000000..205533c68d9 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/advance_tests.rs @@ -0,0 +1,712 @@ +use super::*; +use crate::chain::ChannelMonitorUpdateStatus; +use crate::ln::channelmanager::ffor_activation::drain_tests::drain; +use crate::ln::ffor_tests::quiescence::complete_handshake; +use lightning_ffor::reestablish::{Reestablish, ReportedState}; +use lightning_ffor::wire::{Abort, CloseAck, Preimage}; + +fn advance_monitor( + receiver: &Node, id: &FFORReceiverId, connection: &FFORPeerConnection, +) -> Result { + let snapshot = get_monitor!(receiver, id.channel_id()).ffor_commitment_snapshot().unwrap(); + receiver.node.advance_ffor_receiver_with_monitor(id, connection, snapshot, |_| { + panic!("proof emits no custom wire") + }) +} + +fn park( + sender: &Node, receiver: &Node, channel: ChannelId, +) -> (FFORReceiverId, FFORPeerConnection, FFORVoucher, PaymentPreimage) { + let preimage = PaymentPreimage([*receiver.network_payment_count.as_ref().borrow(); 32]); + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let id = + receiver.node.prepare_ffor_receiver(&channel, &connection, parameters(&voucher)).unwrap(); + persist(receiver); + let init = emit(receiver, &id, &connection); + let accepted = accept(sender, receiver, channel, voucher, &init).encode().unwrap(); + receiver.node.handle_ffor_receiver_message(&connection, &accepted).unwrap(); + persist(receiver); + assert_eq!( + advance_monitor(receiver, &id, &connection).unwrap(), + FFORReceiverProgress::AwaitingVoucherCommitments + ); + deliver_parked_voucher(sender, receiver, update); + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &connection, |_| panic!("no proof")).unwrap(), + FFORReceiverProgress::NeedsMonitorSnapshot + ); + assert_eq!( + advance_monitor(receiver, &id, &connection).unwrap(), + FFORReceiverProgress::AwaitingPeer + ); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("negotiating STFU")) + .unwrap(), + FFORReceiverProgress::AwaitingPeer + ); + complete_handshake(sender, receiver); + assert_eq!( + advance_monitor(receiver, &id, &connection).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + (id, connection, voucher, preimage) +} + +fn signed(sender: &Node, id: &FFORReceiverId, payload: Payload) -> Vec { + let mut message = FFORMessage { + header: Header { channel_id: id.channel_id().0, epoch_id: id.epoch_id() }, + payload, + extensions: Vec::new(), + signature: [0; 64], + }; + sign(&mut message, sender); + message.encode().unwrap() +} + +fn activation_ack(sender: &Node, id: &FFORReceiverId, activate: &[u8]) -> (Vec, [u8; 32]) { + let message = FFORMessage::decode(activate).unwrap(); + let activation = match message.payload { + Payload::Activate(activation) => activation, + _ => panic!("expected Activate"), + }; + let hash = transcript::activation_hash( + &activation.setup_hash, + &activation.book_hash, + &activation.commit_hash, + activation.epoch_start_height, + ); + (signed(sender, id, Payload::ActivateAck(hash)), hash) +} + +fn settlement_exits_stfu(sender: &Node, receiver: &Node, channel: ChannelId) { + // The stock settlement test node has no sender FFOR engine. Model its signed Active response. + let peers = sender.node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&receiver.node.get_our_node_id()).unwrap().lock().unwrap(); + peer.channel_by_id.get_mut(&channel).unwrap().as_funded_mut().unwrap().exit_quiescence(); +} + +#[test] +fn ffor_driver_lifecycle_orders_real_activation_close_and_drain() { + for receiver_funds in [false, true] { + for settled in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let channel = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = + if receiver_funds { (&nodes[1], &nodes[0]) } else { (&nodes[0], &nodes[1]) }; + let persister = &chanmon_cfgs[usize::from(!receiver_funds)].persister; + let (id, connection, voucher, preimage) = park(sender, receiver, channel); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("unpersisted Activate")) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + let mut refused = Vec::new(); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |bytes| { + refused = bytes.to_vec(); + Err(()) + }) + .unwrap(), + FFORReceiverProgress::Backpressured + ); + let activate = emit(receiver, &id, &connection); + assert_eq!(activate, refused); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("duplicate Activate")) + .unwrap(), + FFORReceiverProgress::AwaitingPeer + ); + let (ack, hash) = activation_ack(sender, &id, &activate); + assert_eq!( + receiver.node.handle_ffor_receiver_message(&connection, &ack).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("unpersisted ACK")) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("Active wire")) + .unwrap(), + FFORReceiverProgress::Active + ); + assert!(receiver.node.list_usable_channels().is_empty()); + settlement_exits_stfu(sender, receiver, channel); + assert_eq!( + receiver.node.request_ffor_receiver_close(&id, &connection).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("unpersisted Close")) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + let mut close_retry = Vec::new(); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |bytes| { + close_retry = bytes.to_vec(); + Err(()) + }) + .unwrap(), + FFORReceiverProgress::Backpressured + ); + assert_eq!(emit(receiver, &id, &connection), close_retry); + assert_eq!(FFORMessage::decode(&close_retry).unwrap().payload, Payload::Close(hash)); + let close_ack = signed( + sender, + &id, + Payload::CloseAck(CloseAck { + activation_hash: hash, + num_slots: 1, + settled: vec![u8::from(settled)], + preimages: if settled { + vec![Preimage { slot: 1, value: preimage.0 }] + } else { + Vec::new() + }, + preimages_tlv_present: true, + }), + ); + assert_eq!( + receiver.node.handle_ffor_receiver_message(&connection, &close_ack).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("unpersisted drain")) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + if settled { + persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + } + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("drain custom wire")) + .unwrap(), + FFORReceiverProgress::Draining + ); + if settled { + assert!(receiver.node.get_and_clear_pending_msg_events().iter().all( + |event| matches!( + event, + MessageSendEvent::SendChannelUpdate { .. } + | MessageSendEvent::BroadcastChannelUpdate { .. } + ) + )); + let update = get_monitor!(receiver, channel).get_latest_update_id(); + persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + receiver + .chain_monitor + .chain_monitor + .channel_monitor_updated(channel, update) + .unwrap(); + } + assert_eq!( + drain(sender, receiver), + if settled { + (vec![voucher.htlc_id], Vec::new()) + } else { + (Vec::new(), vec![voucher.htlc_id]) + } + ); + sender.node.get_and_clear_pending_events(); + assert_eq!(drain(sender, receiver), (Vec::new(), Vec::new())); + assert_eq!( + advance_monitor(receiver, &id, &connection).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("unpersisted Closed")) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + let before_release = persist(receiver); + let monitor = get_monitor!(receiver, channel).encode(); + let restored = restore(receiver, &before_release, &monitor).unwrap(); + let sender_id = sender.node.get_our_node_id(); + restored + .peer_connected( + sender_id, + &msgs::Init { + features: sender.init_features(receiver.node.get_our_node_id()), + networks: None, + remote_network_address: None, + }, + false, + ) + .unwrap(); + let next = restored.ffor_peer_connection(&sender_id).unwrap(); + assert_eq!( + restored + .advance_ffor_receiver(&id, &next, |_| panic!( + "restored Closed before persistence" + )) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + assert!(restored.get_and_clear_pending_msg_events().is_empty()); + let token = restored.capture_ffor_persistence(); + let _ = restored.encode(); + restored.ffor_persistence_completed(token).unwrap(); + assert!(restored.get_and_clear_pending_msg_events().is_empty()); + assert_eq!( + restored + .advance_ffor_receiver(&id, &next, |_| panic!("restored Closed release")) + .unwrap(), + FFORReceiverProgress::Closed + ); + assert!(restore(receiver, &restored.encode(), &monitor).is_ok()); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("Closed wire")) + .unwrap(), + FFORReceiverProgress::Closed + ); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("Closed retry wire")) + .unwrap(), + FFORReceiverProgress::Closed + ); + receiver.node.handle_ffor_receiver_message(&connection, &ack).unwrap(); + receiver.node.handle_ffor_receiver_message(&connection, &close_ack).unwrap(); + let bytes = persist(receiver); + assert!(restore(receiver, &bytes, &get_monitor!(receiver, channel).encode()).is_ok()); + send_payment(sender, &[receiver], 1_000_000); + } + } +} + +#[test] +fn ffor_driver_expired_unsent_activation_keeps_fence_and_requires_reconnect() { + for (backpressure, sent, cross_deadline) in + [(false, false, false), (true, false, false), (false, true, true)] + { + fixture!(nodes, sender, receiver, channel, false); + let (id, connection, voucher, _) = park(sender, receiver, channel); + if sent { + persist(receiver); + emit(receiver, &id, &connection); + } + if backpressure { + persist(receiver); + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &connection, |_| Err(())).unwrap(), + FFORReceiverProgress::Backpressured + ); + } + if cross_deadline { + receiver.node.best_block.write().unwrap().height = voucher.cltv_expiry - 20; + } else { + receiver.node.best_block.write().unwrap().height += 7; + } + persist(receiver); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("stale Activate")) + .unwrap(), + FFORReceiverProgress::ReconnectRequired + ); + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + assert_eq!( + peer.channel_by_id + .get(&channel) + .unwrap() + .as_funded() + .unwrap() + .ffor_receiver_fence() + .unwrap() + .0, + FFORReceiverFencePhase::Activating + ); + drop(peer); + drop(peers); + assert!(receiver.node.get_and_clear_pending_msg_events().iter().any(|event| matches!( + event, + MessageSendEvent::HandleError { + action: msgs::ErrorAction::DisconnectPeerWithWarning { .. }, + .. + } + ))); + } +} + +#[test] +fn ffor_driver_signed_abort_during_activation_never_drops_fence() { + fixture!(nodes, sender, receiver, channel, false); + let (id, connection, _, _) = park(sender, receiver, channel); + persist(receiver); + emit(receiver, &id, &connection); + let setup_hash = { + let registry = receiver.node.ffor_recovery.lock().unwrap(); + registry + .get(&FFORRecoveryKey { channel_id: channel, epoch_id: id.epoch_id() }) + .unwrap() + .validate_recovery() + .unwrap() + .setup_hash() + }; + let wrong = signed( + sender, + &id, + Payload::Abort(Abort { transcript_hash: [0; 32], reason: 7, data: Vec::new() }), + ); + assert!(receiver.node.handle_ffor_receiver_message(&connection, &wrong).is_err()); + let abort = signed( + sender, + &id, + Payload::Abort(Abort { transcript_hash: setup_hash, reason: 7, data: Vec::new() }), + ); + assert_eq!( + receiver.node.handle_ffor_receiver_message(&connection, &abort).unwrap(), + FFORReceiverProgress::ReconnectRequired + ); + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + assert_eq!( + peer.channel_by_id + .get(&channel) + .unwrap() + .as_funded() + .unwrap() + .ffor_receiver_fence() + .unwrap() + .0, + FFORReceiverFencePhase::Activating + ); + drop(peer); + drop(peers); + receiver.node.get_and_clear_pending_msg_events(); +} + +#[test] +fn ffor_driver_restored_ack_loss_uses_current_generation_and_accepts_late_exact_ack() { + fixture!(nodes, sender, receiver, channel, false); + let (id, connection, _, _) = park(sender, receiver, channel); + persist(receiver); + let activate = emit(receiver, &id, &connection); + let (ack, hash) = activation_ack(sender, &id, &activate); + let bytes = persist(receiver); + let monitor = get_monitor!(receiver, channel).encode(); + let restored = restore(receiver, &bytes, &monitor).unwrap(); + assert!(restored.handle_ffor_receiver_message(&connection, &ack).is_err()); + let token = restored.capture_ffor_persistence(); + let _ = restored.encode(); + restored.ffor_persistence_completed(token).unwrap(); + let sender_id = sender.node.get_our_node_id(); + let receiver_id = receiver.node.get_our_node_id(); + restored + .peer_connected( + sender_id, + &msgs::Init { + features: sender.init_features(receiver_id), + networks: None, + remote_network_address: None, + }, + false, + ) + .unwrap(); + let replacement = restored.ffor_peer_connection(&sender_id).unwrap(); + assert_ne!(replacement, connection); + assert!(restored + .advance_ffor_receiver(&id, &connection, |_| panic!("old generation")) + .is_err()); + assert!(restored.request_ffor_receiver_close(&id, &connection).is_err()); + assert!(restored.cancel_ffor_receiver_setup(&id, &connection).is_err()); + assert_eq!( + restored.advance_ffor_receiver(&id, &replacement, |_| panic!("restored Activate")).unwrap(), + FFORReceiverProgress::AwaitingPeer + ); + sender.node.peer_disconnected(receiver_id); + sender + .node + .peer_connected( + receiver_id, + &msgs::Init { + features: receiver.init_features(sender_id), + networks: None, + remote_network_address: None, + }, + true, + ) + .unwrap(); + let mut report = get_event_msg!(sender, MessageSendEvent::SendChannelReestablish, receiver_id); + report.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: id.epoch_id(), + activation_hash: hash, + state: ReportedState::Active, + })); + restored.handle_channel_reestablish(sender_id, &report); + restored.best_block.write().unwrap().height = 400; + assert_eq!( + restored.advance_ffor_receiver(&id, &replacement, |_| panic!("ACK-loss Activate")).unwrap(), + FFORReceiverProgress::AwaitingPeer + ); + assert_eq!( + restored.handle_ffor_receiver_message(&replacement, &ack).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + let token = restored.capture_ffor_persistence(); + let saved = restored.encode(); + restored.ffor_persistence_completed(token).unwrap(); + assert_eq!( + restored.advance_ffor_receiver(&id, &replacement, |_| panic!("late Active wire")).unwrap(), + FFORReceiverProgress::Active + ); + assert!(restore(receiver, &saved, &monitor).is_ok()); + sender.node.get_and_clear_pending_msg_events(); +} + +#[test] +fn ffor_driver_conflicting_reconnect_cannot_release_close_or_restore_active_progress() { + fixture!(nodes, sender, receiver, channel, false); + let (id, connection, _, _) = park(sender, receiver, channel); + persist(receiver); + let activate = emit(receiver, &id, &connection); + let (ack, _) = activation_ack(sender, &id, &activate); + receiver.node.handle_ffor_receiver_message(&connection, &ack).unwrap(); + persist(receiver); + settlement_exits_stfu(sender, receiver, channel); + receiver.node.request_ffor_receiver_close(&id, &connection).unwrap(); + persist(receiver); + let sender_id = sender.node.get_our_node_id(); + let receiver_id = receiver.node.get_our_node_id(); + receiver.node.peer_disconnected(sender_id); + sender.node.peer_disconnected(receiver_id); + connect_nodes(sender, receiver); + let replacement = receiver.node.ffor_peer_connection(&sender_id).unwrap(); + let mut report = get_event_msg!(sender, MessageSendEvent::SendChannelReestablish, receiver_id); + report.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: id.epoch_id(), + activation_hash: [42; 32], + state: ReportedState::Active, + })); + receiver.node.handle_channel_reestablish(sender_id, &report); + assert!(receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("old generation Close")) + .is_err()); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &replacement, |_| panic!("conflicting Close")) + .unwrap(), + FFORReceiverProgress::ResolutionRequired + ); + assert!(receiver.node.request_ffor_receiver_close(&id, &replacement).is_err()); + // Historical exact ACK remains idempotent, but cannot erase the conflicting observation. + receiver.node.handle_ffor_receiver_message(&replacement, &ack).unwrap(); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &replacement, |_| panic!("conflicting ACK replay")) + .unwrap(), + FFORReceiverProgress::ResolutionRequired + ); + receiver.node.get_and_clear_pending_msg_events(); + sender.node.get_and_clear_pending_msg_events(); +} + +#[test] +fn ffor_driver_retained_close_replay_requires_fresh_connection_before_stock_drain() { + fixture!(nodes, sender, receiver, channel, false); + let (id, connection, voucher, _) = park(sender, receiver, channel); + persist(receiver); + let activate = emit(receiver, &id, &connection); + let (ack, hash) = activation_ack(sender, &id, &activate); + receiver.node.handle_ffor_receiver_message(&connection, &ack).unwrap(); + persist(receiver); + settlement_exits_stfu(sender, receiver, channel); + receiver.node.request_ffor_receiver_close(&id, &connection).unwrap(); + persist(receiver); + let close = emit(receiver, &id, &connection); + let close_ack = signed( + sender, + &id, + Payload::CloseAck(CloseAck { + activation_hash: hash, + num_slots: 1, + settled: vec![0], + preimages: Vec::new(), + preimages_tlv_present: true, + }), + ); + receiver.node.handle_ffor_receiver_message(&connection, &close_ack).unwrap(); + persist(receiver); + let sender_id = sender.node.get_our_node_id(); + let receiver_id = receiver.node.get_our_node_id(); + receiver.node.peer_disconnected(sender_id); + sender.node.peer_disconnected(receiver_id); + connect_nodes(sender, receiver); + let replay_connection = receiver.node.ffor_peer_connection(&sender_id).unwrap(); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + // The local durable release must run before accepting any saved peer stock flight. + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &replay_connection, |_| panic!( + "reconnect drain custom wire" + )) + .unwrap(), + FFORReceiverProgress::Draining + ); + let mut report = get_event_msg!(sender, MessageSendEvent::SendChannelReestablish, receiver_id); + report.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: id.epoch_id(), + activation_hash: hash, + state: ReportedState::Active, + })); + receiver.node.handle_channel_reestablish(sender_id, &report); + assert_eq!(emit(receiver, &id, &replay_connection), close); + assert_eq!( + receiver.node.handle_ffor_receiver_message(&replay_connection, &close_ack).unwrap(), + FFORReceiverProgress::ReconnectRequired + ); + assert!(receiver + .node + .release_ffor_receiver_drain_on_connection( + &channel, + &sender_id, + id.epoch_id(), + Some(&replay_connection) + ) + .is_err()); + let events = receiver.node.get_and_clear_pending_msg_events(); + assert!(events.iter().any(|event| matches!( + event, + MessageSendEvent::HandleError { + action: msgs::ErrorAction::DisconnectPeerWithWarning { .. }, + .. + } + ))); + assert!(!events.iter().any(|event| matches!( + event, + MessageSendEvent::UpdateHTLCs { .. } | MessageSendEvent::SendRevokeAndACK { .. } + ))); + receiver.node.peer_disconnected(sender_id); + sender.node.peer_disconnected(receiver_id); + connect_nodes(sender, receiver); + let fresh = receiver.node.ffor_peer_connection(&sender_id).unwrap(); + assert!(receiver.node.handle_ffor_receiver_message(&replay_connection, &close_ack).is_err()); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &fresh, |_| panic!("fresh drain custom wire")) + .unwrap(), + FFORReceiverProgress::Draining + ); + let mut report = get_event_msg!(sender, MessageSendEvent::SendChannelReestablish, receiver_id); + report.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: id.epoch_id(), + activation_hash: hash, + state: ReportedState::Draining, + })); + receiver.node.handle_channel_reestablish(sender_id, &report); + assert_eq!(drain(sender, receiver), (Vec::new(), vec![voucher.htlc_id])); + sender.node.get_and_clear_pending_events(); + assert_eq!( + advance_monitor(receiver, &id, &fresh).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &fresh, |_| panic!("Closed wire")).unwrap(), + FFORReceiverProgress::Closed + ); +} + +#[test] +fn ffor_driver_cancel_owned_stfu_drives_reconnect_and_gates_failures_on_durable_abort() { + fixture!(nodes, sender, receiver, channel, false); + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let sender_id = sender.node.get_our_node_id(); + let receiver_id = receiver.node.get_our_node_id(); + let connection = receiver.node.ffor_peer_connection(&sender_id).unwrap(); + let id = + receiver.node.prepare_ffor_receiver(&channel, &connection, parameters(&voucher)).unwrap(); + persist(receiver); + let init = emit(receiver, &id, &connection); + let accepted = accept(sender, receiver, channel, voucher, &init).encode().unwrap(); + receiver.node.handle_ffor_receiver_message(&connection, &accepted).unwrap(); + deliver_parked_voucher(sender, receiver, update); + persist(receiver); + assert_eq!( + advance_monitor(receiver, &id, &connection).unwrap(), + FFORReceiverProgress::AwaitingPeer + ); + complete_handshake(sender, receiver); + assert_eq!( + receiver.node.cancel_ffor_receiver_setup(&id, &connection).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + let events = receiver.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + assert!(matches!( + events[0], + MessageSendEvent::HandleError { + action: msgs::ErrorAction::DisconnectPeerWithWarning { .. }, + .. + } + )); + // Process the required disconnect before either the cancel or disconnect revision is stored. + receiver.node.peer_disconnected(sender_id); + sender.node.peer_disconnected(receiver_id); + connect_nodes(sender, receiver); + let fresh = receiver.node.ffor_peer_connection(&sender_id).unwrap(); + assert_eq!(drain(sender, receiver), (Vec::new(), Vec::new())); + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &fresh, |_| panic!("cancel Init")).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &fresh, |_| panic!("gate release wire")).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + assert_eq!(drain(sender, receiver), (Vec::new(), vec![voucher.htlc_id])); + sender.node.get_and_clear_pending_events(); + persist(receiver); + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &fresh, |_| panic!("aborted retry")).unwrap(), + FFORReceiverProgress::Aborted { reason: FFORReceiverAbortReason::Requested } + ); + send_payment(sender, &[receiver], 1_000_000); +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs index 86b913a5454..1104e389321 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs @@ -598,3 +598,6 @@ fn ffor_driver_accept_rechecks_current_deadline_before_promotion() { .unwrap() .is_none()); } + +#[path = "advance_tests.rs"] +mod lifecycle; diff --git a/lightning/src/ln/ffor/driver.rs b/lightning/src/ln/ffor/driver.rs index 0425f6f1867..8e7201ab59a 100644 --- a/lightning/src/ln/ffor/driver.rs +++ b/lightning/src/ln/ffor/driver.rs @@ -84,17 +84,30 @@ impl FFORReceiverId { } } -/// Current progress of experimental setup, without activation or invoice readiness. +/// Current native receiver progress. No variant authorizes invoice exposure or witness work. #[derive(Clone, Debug, PartialEq, Eq)] pub enum FFORReceiverProgress { /// The ordered manager persister has not yet completed the protected revision. AwaitingPersistence, /// Exact outgoing bytes remain owned by the engine because the transport queue refused them. Backpressured, - /// Init has been enqueued on its original connection; no retry is emitted on another one. + /// Native state awaits an authenticated peer message or the existing stock handshake. AwaitingPeer, /// Exact accepted vouchers are owned natively and await their stock commitment rounds. AwaitingVoucherCommitments, + /// Obtain an opaque snapshot from the monitor, drop its guard, and use the with-monitor entry. + NeedsMonitorSnapshot, + /// The exact signed activation acknowledgement is durable and native frozen state is intact. + /// This may be observed after the settlement deadline and is not invoice readiness. + Active, + /// A durable close acknowledgement owns the stock voucher removal rounds. + Draining, + /// Both voucher views are empty and the final Closed manager barrier has completed. + Closed, + /// A fresh connection and native reestablish reconciliation are required before more work. + ReconnectRequired, + /// Conflicting retained peer evidence requires explicit recovery; the fence is retained. + ResolutionRequired, /// The pending request was irreversibly aborted. The gate remains until controlled release. Aborted { /// First retained native reason. From 7ed8161b7e3ac375b639de5e4d55542cf936af17 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Mon, 21 Sep 2026 01:48:00 -0400 Subject: [PATCH 16/22] feat(ffor): register immutable witnesses before provisioning --- FFOR.md | 30 ++ .../src/ln/channelmanager/ffor_activation.rs | 1 + .../channelmanager/ffor_activation/context.rs | 19 +- .../ffor_activation/drain_tests.rs | 11 +- .../channelmanager/ffor_activation/witness.rs | 198 +++++++++ .../ffor_activation/witness/tests.rs | 411 ++++++++++++++++++ lightning/src/ln/ffor.rs | 6 + lightning/src/ln/ffor/provision.rs | 71 +++ lightning/src/ln/ffor_recovery.rs | 42 +- lightning/src/ln/ffor_recovery/activation.rs | 2 +- .../src/ln/ffor_recovery/activation/tests.rs | 2 + .../ffor_recovery/activation/tests/close.rs | 2 +- .../ffor_recovery/activation/tests/witness.rs | 193 ++++++++ lightning/src/ln/ffor_recovery/request.rs | 3 +- lightning/src/ln/ffor_recovery/tests.rs | 10 +- lightning/src/ln/ffor_recovery/witness.rs | 188 ++++++++ 16 files changed, 1173 insertions(+), 16 deletions(-) create mode 100644 lightning/src/ln/channelmanager/ffor_activation/witness.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs create mode 100644 lightning/src/ln/ffor/provision.rs create mode 100644 lightning/src/ln/ffor_recovery/activation/tests/witness.rs create mode 100644 lightning/src/ln/ffor_recovery/witness.rs diff --git a/FFOR.md b/FFOR.md index e3bd5eb1ea2..71feba8d436 100644 --- a/FFOR.md +++ b/FFOR.md @@ -255,6 +255,28 @@ not authorize later work; the future driver must recheck under its transition lo A retained Active phase can outlive the settlement deadline, so neither context grants provisioning permission or invoice readiness. +Native witness registration now retains one immutable selection of one through four +witnesses before provisioning. The protected application store must first reserve +its recovery keys, exact signed manifests and receipt capacity. Registration compares +those manifests to the actual Active epoch, acknowledgement, frozen pair and current +settlement deadline. It stores compact public parameters, signatures and exact manifest +digests, reconstructing the canonical book from native history during validation. +Witness restrictions, separate mailbox/fetch identities and distinct fetch/encryption +keys are enforced. Exact retries retain the same selection and persistence requirement; +changed manifests or keys are refused. Required archive field 8 and schema 5 prevent +older readers from forgetting this ownership, including after channel removal. + +Provision release requires a newly captured Active context after registration +persistence. The manager holds the actual settlement channel, archive, current height +and persistence locks while validating the exact typed Provision and invoking its +bounded enqueue callback. The callback must atomically check the authenticated witness +transport token and capacity, perform no I/O and acquire no native or store locks. +Settlement-peer connectivity is unnecessary. Backpressure permits exact request retry; +queue acceptance proves neither delivery nor witness acknowledgement. Restore requires +a fresh manager write and rejects an earlier instance's context. Historical registration +metadata remains inspectable after close or force-close, so missing sidecar keys cannot +be treated as permission to create a replacement selection. No invoice authority follows. + These are consistency checks, not an authenticated storage envelope. Arbitrarily deleting a mismatching add's ownership record after abort can make its nonreserved hash indistinguishable from an ordinary post-abort payment. No valid writer creates @@ -368,6 +390,14 @@ preimages; they reject signed ciphertext, ephemeral-key, AAD, manifest and plain substitution. The shared crate separately covers all six reference scenarios and arbitrary body bytes. These tests do not exercise production witness transport or key storage. +Eight witness-registration tests cover both funders, missing acknowledgements, pending +manager and monitor writes, exact/reordered retries, stale manager contexts, disconnected +settlement peers, deadline crossings, conflicting reconnects and close refusal. Archive +checks cover compact four-witness ownership for a 483-slot book, remaining terminal +capacity, corrupted metadata, key reuse, missing required evidence and version downgrade. +An opt-in test exporter produces a real Active manager and stock monitor using a public +Node wallet seed for downstream integration tests; normal test runs do not write fixtures. + ## Next boundary Reusable epochs require durable retired epoch IDs and voucher hashes, with one diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs index 54a998abf2e..8b07baa9ed5 100644 --- a/lightning/src/ln/channelmanager/ffor_activation.rs +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -13,6 +13,7 @@ mod close; mod context; mod driver; mod reestablish; +mod witness; struct RuntimeEntry { key: FFORRecoveryKey, diff --git a/lightning/src/ln/channelmanager/ffor_activation/context.rs b/lightning/src/ln/channelmanager/ffor_activation/context.rs index e5fdcba124a..1023e62618f 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/context.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/context.rs @@ -157,6 +157,18 @@ where pub(in crate::ln::channelmanager) fn ffor_active_context_locked( &self, channel: &FundedChannel, recovery: &FFORRecoveryRegistry, key: &FFORRecoveryKey, ) -> Result { + let context = self.ffor_active_state_locked(channel, recovery, key)?; + let runtime = self.ffor_activation.lock().unwrap(); + let requirement = &runtime.get(key)?.requirement; + if !self.ffor_persistence.lock().unwrap().is_complete(requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + Ok(FFORReceiverActiveContext { recovery: context, requirement: requirement.clone() }) + } + + pub(super) fn ffor_active_state_locked( + &self, channel: &FundedChannel, recovery: &FFORRecoveryRegistry, key: &FFORRecoveryKey, + ) -> Result { channel .ffor_validate_receiver_identity(self.our_network_pubkey, self.chain_hash) .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; @@ -191,12 +203,7 @@ where ) { return Err(FFORCommitmentError::PendingUpdates.into()); } - let runtime = self.ffor_activation.lock().unwrap(); - let requirement = &runtime.get(key)?.requirement; - if !self.ffor_persistence.lock().unwrap().is_complete(requirement) { - return Err(FFORCommitmentError::PendingUpdates.into()); - } - Ok(FFORReceiverActiveContext { recovery: context, requirement: requirement.clone() }) + Ok(context) } } diff --git a/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs b/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs index 5770982a184..8227b93af1a 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs @@ -25,7 +25,15 @@ fn sign(message: &mut FFORMessage, node: &Node) { } /// Reserve two actual outgoing payment hashes before delivering either voucher to the receiver. -pub(super) fn park_two(sender: &Node, receiver: &Node, id: ChannelId) -> ([FFORVoucher; 2], PaymentPreimage) { +pub(super) fn park_two( + sender: &Node, receiver: &Node, id: ChannelId, +) -> ([FFORVoucher; 2], PaymentPreimage) { + park_two_with_witnesses(sender, receiver, id, None) +} + +pub(super) fn park_two_with_witnesses( + sender: &Node, receiver: &Node, id: ChannelId, witnesses: Option>, +) -> ([FFORVoucher; 2], PaymentPreimage) { let preimage = PaymentPreimage([*receiver.network_payment_count.as_ref().borrow(); 32]); let (first, voucher, _) = offer_voucher(sender, receiver, 2_000_000); let (mut route, hash, _, secret) = get_route_and_payment_hash!(sender, receiver, 2_000_000); @@ -34,6 +42,7 @@ pub(super) fn park_two(sender: &Node, receiver: &Node, id: ChannelId) -> ([FFORV FFORVoucher { htlc_id: voucher.htlc_id + 1, payment_hash: hash, ..voucher }; let (mut init, mut accept) = ffor_setup_test_messages(sender, receiver, id, voucher); if let Payload::Init(terms) = &mut init.payload { + terms.witness_peers = witnesses; terms.budget_msat += second_voucher.amount_msat; terms.amounts_msat.push(second_voucher.amount_msat); } else { diff --git a/lightning/src/ln/channelmanager/ffor_activation/witness.rs b/lightning/src/ln/channelmanager/ffor_activation/witness.rs new file mode 100644 index 00000000000..1ffa59590ba --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/witness.rs @@ -0,0 +1,198 @@ +//! Native ownership of compact witness registrations and persistence-gated provisioning. + +use super::*; +use crate::ln::ffor::{ + FFORReceiverActiveContext, FFORReceiverRecoveryContext, FFORReceiverWitnessRegistration, +}; +use lightning_ffor::witness::{Provision, SignedManifest}; + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Retain the immutable selection of one through four signed witness manifests. + /// + /// First reserve and durably store the exact manifests and recovery keys in protected + /// application storage. This method retains compact public evidence only. The supplied + /// historical context identifies an epoch; actual current Active state, exact commitments, + /// signed acknowledgement and the live settlement deadline are checked under native locks. + /// No settlement-peer connection is required. A missing sidecar for an existing registration + /// must be recovered, never replaced with freshly generated keys. + /// + /// Exact retries return the current persistence requirement, including while the registration + /// write is pending. They still require the same current Active state and an unexpired deadline. + /// A different witness, key or signed manifest cannot replace this registration. Provisioning + /// remains blocked until persistence completes and a fresh Active context is captured. + pub fn register_ffor_receiver_witnesses( + &self, context: &FFORReceiverRecoveryContext, manifests: &[(PublicKey, SignedManifest)], + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(&context.settlement_node_id()) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(&context.channel_id()) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + if channel.context.is_monitor_or_signer_pending_channel_update() + || peer + .in_flight_monitor_updates + .get(&context.channel_id()) + .map_or(false, |(_, updates)| !updates.is_empty()) + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let key = + FFORRecoveryKey { channel_id: context.channel_id(), epoch_id: context.epoch_id() }; + let mut recovery = self.ffor_recovery.lock().unwrap(); + let current = self.ffor_active_state_locked(channel, &recovery, &key)?; + let height = self.best_block.read().unwrap(); + Self::validate_ffor_witness_context(context, ¤t, height.height)?; + let registration = FFORReceiverWitnessRegistration::from_manifests(¤t, manifests) + .map_err(|_| FFORReceiverError::InvalidWitnessRegistration)?; + if let Some(existing) = recovery.get_witnesses(&key) { + if existing != ®istration { + return Err(FFORReceiverError::InvalidWitnessRegistration); + } + return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); + } + // Initial registration cannot convert a merely observed or unpersisted Active phase into + // authority. Its exact compact evidence and all later close reservations must fit first. + self.ffor_active_context_locked(channel, &recovery, &key)?; + let upgrade = recovery + .prepare_witnesses(&key, registration) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + upgrade.commit(); + self.ffor_activation.lock().unwrap().record(key, requirement.clone(), false); + Ok(requirement) + } + + /// Inspect immutable historical witness ownership, including after close or channel removal. + /// + /// The result is not a current-state capability. Applications must compare every selected + /// witness and exact manifest digest to their confirmed protected records before continuing. + /// An existing registration with missing application secrets never permits key regeneration. + pub fn ffor_receiver_witness_registration( + &self, context: &FFORReceiverRecoveryContext, + ) -> Result, FFORReceiverError> { + let _guard = self.total_consistency_lock.read().unwrap(); + let recovery = self.ffor_recovery.lock().unwrap(); + let key = + FFORRecoveryKey { channel_id: context.channel_id(), epoch_id: context.epoch_id() }; + let setup = recovery.get(&key).ok_or(FFORReceiverError::UnknownEpoch)?; + let current = recovery + .get_activation(&key) + .ok_or(FFORReceiverError::NotRegistered)? + .receiver_context(setup) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if current.context_digest() != context.context_digest() { + return Err(FFORReceiverError::RecoveryUnavailable); + } + Ok(recovery.get_witnesses(&key).cloned()) + } + + /// Release one exact registered Provision while current native authority remains locked. + /// + /// Capture a fresh Active context after registration persistence. This method checks its + /// manager instance and latest epoch requirement, the current Active fence and commitment pair, + /// exact signed manifest, and live settlement deadline. Neither witness acknowledgement nor + /// invoice readiness follows from success. The settlement peer may be disconnected. + /// + /// `enqueue` must atomically check its actual authenticated witness connection token and + /// reserve bounded transport capacity for this exact typed request. It must perform no I/O, + /// acquire no manager/peer/store locks, and never reenter the manager. No witness peer mutex + /// is acquired here. `Ok(false)` means callback backpressure; retry the same protected request. + /// A stale context, changed phase or pending requirement fails before the callback is called. + pub fn release_ffor_receiver_witness_provision( + &self, context: &FFORReceiverActiveContext, witness: &PublicKey, provision: &Provision, + enqueue: C, + ) -> Result + where + C: FnOnce(&Provision) -> Result<(), ()>, + { + let _guard = self.total_consistency_lock.read().unwrap(); + let historical = context.recovery_context(); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(&historical.settlement_node_id()) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(&historical.channel_id()) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + if channel.context.is_monitor_or_signer_pending_channel_update() + || peer + .in_flight_monitor_updates + .get(&historical.channel_id()) + .map_or(false, |(_, updates)| !updates.is_empty()) + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let key = FFORRecoveryKey { + channel_id: historical.channel_id(), + epoch_id: historical.epoch_id(), + }; + let recovery = self.ffor_recovery.lock().unwrap(); + let current = self.ffor_active_state_locked(channel, &recovery, &key)?; + let height = self.best_block.read().unwrap(); + Self::validate_ffor_witness_context(historical, ¤t, height.height)?; + let registration = + recovery.get_witnesses(&key).ok_or(FFORReceiverError::InvalidWitnessRegistration)?; + if !registration.matches_manifest(witness, provision.manifest()) { + return Err(FFORReceiverError::InvalidWitnessRegistration); + } + let runtime = self.ffor_activation.lock().unwrap(); + let requirement = &runtime.get(&key)?.requirement; + let barrier = self.ffor_persistence.lock().unwrap(); + if requirement != &context.requirement || !barrier.is_complete(requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + Ok(enqueue(provision).is_ok()) + } + + fn validate_ffor_witness_context( + provided: &FFORReceiverRecoveryContext, current: &FFORReceiverRecoveryContext, height: u32, + ) -> Result<(), FFORReceiverError> { + if provided.context_digest() != current.context_digest() + || provided.activation_ack_wire() != current.activation_ack_wire() + || height >= current.setup().terms().settlement_deadline + { + return Err(FFORReceiverError::InvalidWitnessRegistration); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs new file mode 100644 index 00000000000..7ba29429d88 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs @@ -0,0 +1,411 @@ +use super::*; +use crate::chain::ChannelMonitorUpdateStatus; +use crate::ln::channelmanager::ffor_activation::drain_tests::{park_two, park_two_with_witnesses}; +use crate::ln::channelmanager::ffor_recovery_tests::{claim_ffor_preimage_for_test, restore}; +use crate::ln::ffor_recovery::ffor_test_witness_manifests as manifests; +use crate::ln::ffor_tests::anchor_config; +use crate::ln::functional_test_utils::*; +use bitcoin::secp256k1::{Message as SecpMessage, Secp256k1, SecretKey}; +use lightning_ffor::witness::{ManifestParameters, UnsignedManifest}; + +const EPOCH: [u8; 32] = [81; 32]; + +fn persist(node: &TestChannelManager) -> Vec { + let token = node.capture_ffor_persistence(); + let bytes = node.encode(); + node.ffor_persistence_completed(token).unwrap(); + bytes +} + +fn activate(sender: &Node, receiver: &Node, id: ChannelId) -> FFORReceiverRecoveryContext { + let context = receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + let mut ack = FFORMessage { + header: context.setup().header(), + payload: Payload::ActivateAck(context.activation_hash()), + extensions: Vec::new(), + signature: [0; 64], + }; + ack.signature = sender + .keys_manager + .sign_ffor_message(&FFORSigningRequest::new(&ack.unsigned_wire().unwrap()).unwrap()) + .unwrap() + .serialize_compact(); + receiver + .node + .accept_ffor_receiver_activation_ack( + &id, + &sender.node.get_our_node_id(), + EPOCH, + &ack.encode().unwrap(), + ) + .unwrap(); + receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap() +} + +fn modified_manifest( + context: &FFORReceiverRecoveryContext, parameters: ManifestParameters, +) -> SignedManifest { + let unsigned = UnsignedManifest::new(context.setup(), parameters).unwrap(); + let signature = Secp256k1::new() + .sign_ecdsa( + &SecpMessage::from_digest(unsigned.signing_digest()), + &SecretKey::from_slice(&[100; 32]).unwrap(), + ) + .serialize_compact(); + unsigned.authenticate(signature).unwrap() +} + +fn export_node_fixture(receiver: &Node, context: &FFORReceiverRecoveryContext) { + let directory = match std::env::var_os("FFOR_NODE_WITNESS_FIXTURE_DIR") { + Some(directory) => std::path::PathBuf::from(directory), + None => return, + }; + std::fs::create_dir_all(&directory).unwrap(); + std::fs::write(directory.join("active-manager.bin"), persist(receiver.node)).unwrap(); + std::fs::write( + directory.join("active-monitor.bin"), + get_monitor!(receiver, context.channel_id()).encode(), + ) + .unwrap(); + let hex = |bytes: &[u8]| bytes.iter().map(|byte| format!("{byte:02x}")).collect::(); + std::fs::write(directory.join("fixture.txt"), format!( + "network=testnet\nwallet_seed={}\nchannel_id={}\nepoch_id={}\nreceiver={}\nsettlement={}\nfunding_txid={}\nfunding_vout={}\ngenerator=ffor_witness_registration_release_requires_current_durability_and_survives_archive_only_restore\nbase_revision=a20b34f98\n", + hex(&[91; 64]), hex(&context.channel_id().0), hex(&context.epoch_id()), + context.receiver_node_id(), context.settlement_node_id(), context.funding_txo().txid, context.funding_txo().index, + )).unwrap(); +} + +#[test] +fn ffor_witness_registration_release_requires_current_durability_and_survives_archive_only_restore() +{ + for receiver_funds in [false, true] { + let mut chanmon_cfgs = create_chanmon_cfgs(2); + let wallet_root = + bitcoin::bip32::Xpriv::new_master(bitcoin::Network::Testnet, &[91; 64]).unwrap(); + chanmon_cfgs[1].keys_manager = crate::util::test_utils::TestKeysInterface::new( + &wallet_root.private_key.secret_bytes(), + bitcoin::Network::Testnet, + ); + let mut node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + node_cfgs[1].node_seed = wallet_root.private_key.secret_bytes(); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let peer = sender.node.get_our_node_id(); + park_two(sender, receiver, id); + let no_ack = receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + assert!(receiver + .node + .register_ffor_receiver_witnesses(&no_ack, &manifests(&no_ack, 1)) + .is_err()); + let context = activate(sender, receiver, id); + let selected = manifests(&context, 4); + assert!(receiver.node.register_ffor_receiver_witnesses(&context, &selected).is_err()); + persist(receiver.node); + if !receiver_funds { + export_node_fixture(receiver, &context); + } + assert!(receiver.node.register_ffor_receiver_witnesses(&no_ack, &selected).is_err()); + let old_active = + receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).unwrap(); + let older_write = receiver.node.capture_ffor_persistence(); + let requirement = + receiver.node.register_ffor_receiver_witnesses(&context, &selected).unwrap(); + receiver.node.ffor_persistence_completed(older_write).unwrap(); + assert!(!receiver.node.is_ffor_state_persisted(&requirement)); + let mut reordered = selected.clone(); + reordered.reverse(); + assert_eq!( + receiver.node.register_ffor_receiver_witnesses(&context, &reordered).unwrap(), + requirement + ); + let metadata = receiver.node.ffor_receiver_witness_registration(&context).unwrap().unwrap(); + assert_eq!(metadata.witnesses().len(), 4); + assert_eq!(metadata.context_digest(), context.context_digest()); + let provision = Provision::new([44; 16], selected[0].1.clone()); + assert!(receiver + .node + .release_ffor_receiver_witness_provision( + &old_active, + &selected[0].0, + &provision, + |_| panic!("unpersisted registration released") + ) + .is_err()); + assert!(receiver.node.register_ffor_receiver_witnesses(&context, &selected[..1]).is_err()); + let saved = persist(receiver.node); + assert!(receiver.node.is_ffor_state_persisted(&requirement)); + assert!(receiver + .node + .release_ffor_receiver_witness_provision( + &old_active, + &selected[0].0, + &provision, + |_| panic!("stale context released") + ) + .is_err()); + let active = receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).unwrap(); + assert!(!receiver + .node + .release_ffor_receiver_witness_provision(&active, &selected[0].0, &provision, |_| Err( + () + )) + .unwrap()); + sender.node.peer_disconnected(receiver.node.get_our_node_id()); + receiver.node.peer_disconnected(peer); + assert!(receiver + .node + .release_ffor_receiver_witness_provision(&active, &selected[0].0, &provision, |exact| { + assert_eq!(exact, &provision); + Ok(()) + }) + .unwrap()); + let monitor = get_monitor!(receiver, id).encode(); + let restored = restore(receiver, &saved, &monitor).unwrap(); + assert_eq!( + restored.ffor_receiver_witness_registration(&context).unwrap(), + Some(metadata.clone()) + ); + assert!(restored + .release_ffor_receiver_witness_provision( + &active, + &selected[0].0, + &provision, + |_| panic!("old instance released") + ) + .is_err()); + assert!(restored.capture_ffor_receiver_active_context(&id, &peer, EPOCH).is_err()); + let restored_requirement = + restored.register_ffor_receiver_witnesses(&context, &selected).unwrap(); + assert!(!restored.is_ffor_state_persisted(&restored_requirement)); + persist(&restored); + let fresh = restored.capture_ffor_receiver_active_context(&id, &peer, EPOCH).unwrap(); + assert!(restored + .release_ffor_receiver_witness_provision(&fresh, &selected[0].0, &provision, |_| Ok(())) + .unwrap()); + assert!(receiver + .node + .release_ffor_receiver_witness_provision( + &fresh, + &selected[0].0, + &provision, + |_| panic!("other manager released") + ) + .is_err()); + receiver + .node + .force_close_broadcasting_latest_txn(&id, &peer, "witness archive test".into()) + .unwrap(); + receiver.node.get_and_clear_pending_msg_events(); + assert!(receiver + .node + .get_and_clear_pending_events() + .iter() + .any(|event| matches!(event, Event::ChannelClosed { .. }))); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + let closed_monitor = get_monitor!(receiver, id).encode(); + let closed = restore(receiver, &persist(receiver.node), &closed_monitor).unwrap(); + assert!(closed.list_channels().is_empty()); + assert_eq!(closed.ffor_receiver_witness_registration(&context).unwrap(), Some(metadata)); + assert!(closed + .release_ffor_receiver_witness_provision( + &active, + &selected[0].0, + &provision, + |_| panic!("removed channel released") + ) + .is_err()); + } +} + +#[test] +fn ffor_witness_release_refuses_conflicting_reconnect_without_callback() { + use lightning_ffor::reestablish::{Reestablish, ReportedState}; + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let peer = sender.node.get_our_node_id(); + let receiver_id = receiver.node.get_our_node_id(); + park_two(sender, receiver, id); + let context = activate(sender, receiver, id); + persist(receiver.node); + let selected = manifests(&context, 1); + receiver.node.register_ffor_receiver_witnesses(&context, &selected).unwrap(); + persist(receiver.node); + let active = receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).unwrap(); + let provision = Provision::new([1; 16], selected[0].1.clone()); + sender.node.peer_disconnected(receiver_id); + receiver.node.peer_disconnected(peer); + connect_nodes(sender, receiver); + let local = get_event_msg!(receiver, MessageSendEvent::SendChannelReestablish, peer); + let mut remote = get_event_msg!(sender, MessageSendEvent::SendChannelReestablish, receiver_id); + let mut conflicting = context.activation_hash(); + conflicting[0] ^= 1; + remote.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: EPOCH, + activation_hash: conflicting, + state: ReportedState::Active, + })); + sender.node.handle_channel_reestablish(receiver_id, &local); + receiver.node.handle_channel_reestablish(peer, &remote); + assert!(receiver + .node + .release_ffor_receiver_witness_provision(&active, &selected[0].0, &provision, |_| panic!( + "conflicting reconnect released" + )) + .is_err()); + assert!(receiver.node.register_ffor_receiver_witnesses(&context, &selected).is_err()); + assert!(receiver.node.ffor_receiver_witness_registration(&context).unwrap().is_some()); + sender.node.get_and_clear_pending_msg_events(); + receiver.node.get_and_clear_pending_msg_events(); +} + +#[test] +fn ffor_witness_registration_refuses_wrong_terms_selection_and_deadline() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let peer = sender.node.get_our_node_id(); + let selected_peer = + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[90; 32]).unwrap()); + park_two_with_witnesses(sender, receiver, id, Some(vec![selected_peer])); + let context = activate(sender, receiver, id); + persist(receiver.node); + let selected = manifests(&context, 1); + assert_eq!(selected[0].0, selected_peer); + assert!(receiver.node.register_ffor_receiver_witnesses(&context, &[]).is_err()); + assert!(receiver + .node + .register_ffor_receiver_witnesses(&context, &manifests(&context, 5)) + .is_err()); + assert!(receiver + .node + .register_ffor_receiver_witnesses(&context, &manifests(&context, 2)) + .is_err()); + let mut duplicated = selected.clone(); + duplicated.push(selected[0].clone()); + assert!(receiver.node.register_ffor_receiver_witnesses(&context, &duplicated).is_err()); + let mut parameters = *selected[0].1.unsigned().parameters(); + parameters.commitment_hash[0] ^= 1; + let wrong = modified_manifest(&context, parameters); + assert!(receiver + .node + .register_ffor_receiver_witnesses(&context, &[(selected_peer, wrong)]) + .is_err()); + let mut parameters = *selected[0].1.unsigned().parameters(); + parameters.encryption_public_key = parameters.fetch_public_key; + let reused_key = modified_manifest(&context, parameters); + assert!(receiver + .node + .register_ffor_receiver_witnesses(&context, &[(selected_peer, reused_key)]) + .is_err()); + assert!(receiver.node.ffor_receiver_witness_registration(&context).unwrap().is_none()); + receiver.node.register_ffor_receiver_witnesses(&context, &selected).unwrap(); + persist(receiver.node); + let active = receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).unwrap(); + let provision = Provision::new([1; 16], selected[0].1.clone()); + let mut parameters = *selected[0].1.unsigned().parameters(); + parameters.mailbox_id[0] ^= 1; + let changed = modified_manifest(&context, parameters); + assert!(receiver + .node + .register_ffor_receiver_witnesses(&context, &[(selected_peer, changed.clone())]) + .is_err()); + assert!(receiver + .node + .release_ffor_receiver_witness_provision( + &active, + &selected_peer, + &Provision::new([1; 16], changed), + |_| panic!("changed manifest released") + ) + .is_err()); + assert!(receiver + .node + .release_ffor_receiver_witness_provision(&active, &peer, &provision, |_| panic!( + "unselected peer released" + )) + .is_err()); + let deadline = context.setup().terms().settlement_deadline; + receiver.node.best_block.write().unwrap().height = deadline - 1; + assert!(receiver + .node + .release_ffor_receiver_witness_provision(&active, &selected_peer, &provision, |_| Ok(())) + .unwrap()); + receiver.node.best_block.write().unwrap().height = deadline; + assert!(receiver.node.register_ffor_receiver_witnesses(&context, &selected).is_err()); + assert!(receiver + .node + .release_ffor_receiver_witness_provision(&active, &selected_peer, &provision, |_| panic!( + "expired epoch released" + )) + .is_err()); + // The immutable historical ownership is still visible after expiration. + assert!(receiver.node.ffor_receiver_witness_registration(&context).unwrap().is_some()); +} + +#[test] +fn ffor_witness_release_waits_for_owned_preimage_monitor_and_refuses_close() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 1, 0, 100_000, 40_000_000).2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let peer = sender.node.get_our_node_id(); + let (vouchers, preimage) = park_two(sender, receiver, id); + let context = activate(sender, receiver, id); + persist(receiver.node); + let selected = manifests(&context, 1); + receiver.node.register_ffor_receiver_witnesses(&context, &selected).unwrap(); + persist(receiver.node); + let active = receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).unwrap(); + let provision = Provision::new([1; 16], selected[0].1.clone()); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + let update = claim_ffor_preimage_for_test(sender, receiver, id, vouchers[0].htlc_id, preimage); + check_added_monitors(receiver, 1); + assert!(receiver + .node + .release_ffor_receiver_witness_provision(&active, &selected[0].0, &provision, |_| panic!( + "unpersisted monitor released" + )) + .is_err()); + assert!(receiver.node.register_ffor_receiver_witnesses(&context, &selected).is_err()); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + receiver.chain_monitor.chain_monitor.channel_monitor_updated(id, update).unwrap(); + receiver.node.get_and_clear_pending_msg_events(); + assert!(receiver + .node + .release_ffor_receiver_witness_provision(&active, &selected[0].0, &provision, |_| Ok(())) + .unwrap()); + receiver.node.prepare_ffor_receiver_close(&id, &peer, EPOCH).unwrap(); + assert!(receiver + .node + .release_ffor_receiver_witness_provision(&active, &selected[0].0, &provision, |_| panic!( + "closing epoch released" + )) + .is_err()); + assert!(receiver.node.register_ffor_receiver_witnesses(&context, &selected).is_err()); + persist(receiver.node); + assert!(receiver + .node + .release_ffor_receiver_witness_provision(&active, &selected[0].0, &provision, |_| panic!( + "durable close released" + )) + .is_err()); +} diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index 2a210d9c221..e7793a32ba1 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -37,6 +37,9 @@ pub use context::{FFORReceiverActiveContext, FFORReceiverRecoveryContext}; mod witness; pub use witness::{decrypt_ffor_witness_record, FFORWitnessDecryptionError, FFORWitnessReceipt}; +mod provision; +pub use provision::{FFORReceiverWitnessRegistration, FFORRegisteredWitness}; + /// Which channel participant offers every voucher HTLC in the book. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FFORSettlementParty { @@ -155,6 +158,8 @@ pub enum FFORReceiverError { RecoveryUnavailable, /// The node signer could not supply an authentic signature for the prepared transition. SignerUnavailable, + /// Witness selection or exact manifest bytes differ from the native epoch or registration. + InvalidWitnessRegistration, } impl From for FFORReceiverError { @@ -173,6 +178,7 @@ impl fmt::Display for FFORReceiverError { Self::PersistenceUnavailable => f.write_str("FFOR persistence revision exhausted"), Self::RecoveryUnavailable => f.write_str("FFOR recovery record cannot be retained"), Self::SignerUnavailable => f.write_str("FFOR node signature unavailable"), + Self::InvalidWitnessRegistration => f.write_str("invalid FFOR witness registration"), } } } diff --git a/lightning/src/ln/ffor/provision.rs b/lightning/src/ln/ffor/provision.rs new file mode 100644 index 00000000000..bee148a8f82 --- /dev/null +++ b/lightning/src/ln/ffor/provision.rs @@ -0,0 +1,71 @@ +//! Immutable public witness registration metadata. These observations contain no secrets or readiness. + +use alloc::vec::Vec; +use bitcoin::secp256k1::PublicKey; + +/// Retained native selection of witnesses for one authenticated receiver epoch. +/// +/// This survives channel removal and later lifecycle changes. It lets protected application +/// storage detect missing or substituted keys and manifests. It proves neither current Active +/// authority, witness acknowledgement, nor permission to expose an invoice. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FFORReceiverWitnessRegistration { + pub(crate) context_digest: [u8; 32], + pub(crate) witnesses: Vec, +} + +impl FFORReceiverWitnessRegistration { + /// Stable digest of the native historical context to which every manifest is bound. + pub fn context_digest(&self) -> [u8; 32] { + self.context_digest + } + + /// The complete immutable selection, in authenticated compressed-public-key order. + pub fn witnesses(&self) -> &[FFORRegisteredWitness] { + &self.witnesses + } +} + +/// Public metadata for one exact signed manifest, without the repeated canonical voucher book. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FFORRegisteredWitness { + pub(crate) witness: PublicKey, + pub(crate) manifest_digest: [u8; 32], + pub(crate) mailbox_id: [u8; 32], + pub(crate) fetch_public_key: PublicKey, + pub(crate) encryption_public_key: PublicKey, + pub(crate) retention_until: u32, + pub(crate) minimum_receipts: u8, + pub(crate) signature: [u8; 64], +} + +impl FFORRegisteredWitness { + /// Selected witness identity, checked against the signed setup restriction when present. + pub fn witness_node_id(&self) -> PublicKey { + self.witness + } + /// SHA256 of the entire exact signed manifest, including its signature. + pub fn manifest_digest(&self) -> [u8; 32] { + self.manifest_digest + } + /// Per-witness mailbox retained in the exact signed manifest. + pub fn mailbox_id(&self) -> [u8; 32] { + self.mailbox_id + } + /// Public fetch identity. Its private key belongs in protected application storage. + pub fn fetch_public_key(&self) -> PublicKey { + self.fetch_public_key + } + /// Shared public encryption identity for this epoch. + pub fn encryption_public_key(&self) -> PublicKey { + self.encryption_public_key + } + /// Minimum record retention requested by this immutable manifest. + pub fn retention_until(&self) -> u32 { + self.retention_until + } + /// Requested guardian receipt count, without asserting any witness acknowledgement. + pub fn minimum_receipts(&self) -> u8 { + self.minimum_receipts + } +} diff --git a/lightning/src/ln/ffor_recovery.rs b/lightning/src/ln/ffor_recovery.rs index 106e78bb2af..07ca36d535f 100644 --- a/lightning/src/ln/ffor_recovery.rs +++ b/lightning/src/ln/ffor_recovery.rs @@ -6,6 +6,7 @@ mod request; use request::PendingRequest; +mod witness; mod activation; pub(crate) use activation::{FFORReceiverActivation, FFORReceiverCloseRecord}; @@ -16,7 +17,9 @@ use bitcoin::secp256k1::PublicKey; use crate::io; use crate::ln::channel::{FFORReceiverFencePhase, FFORReceiverSetup}; -use crate::ln::ffor::{FFORMonitorRecoveryIdentity, FFORReceiverAbortReason}; +use crate::ln::ffor::{ + FFORMonitorRecoveryIdentity, FFORReceiverAbortReason, FFORReceiverWitnessRegistration, +}; use crate::ln::msgs::DecodeError; use crate::ln::types::ChannelId; use crate::util::ser::{FixedLengthReader, Readable, Writeable, Writer}; @@ -28,6 +31,8 @@ const ACTIVATION_VERSION: u8 = 1; const ABORTED_ACTIVATION_VERSION: u8 = 2; const CLOSE_VERSION: u8 = 3; const REQUEST_VERSION: u8 = 4; +// Earlier readers must not forget protected application key and manifest ownership. +const WITNESS_VERSION: u8 = 5; const MAX_RECORDS: usize = 64; const MAX_ENCODED_BYTES: usize = 8 * 1024 * 1024; // Two maximum wire messages, a 483-slot canonical book, and fixed admission fields fit here. @@ -61,6 +66,7 @@ struct StoredSetup { canonical_book: Vec, activation: Option, request: Option, + witnesses: Option, } impl_writeable_tlv_based!(StoredSetup, { @@ -68,6 +74,7 @@ impl_writeable_tlv_based!(StoredSetup, { (2, canonical_book, required_vec), (4, activation, option), (6, request, option), + (8, witnesses, option), }); struct Entry { @@ -97,6 +104,15 @@ impl Entry { .map_err(|_| FFORRecoveryError::InvalidRecord)?; } } + if let Some(witnesses) = record.witnesses.as_ref() { + let context = record + .activation + .as_ref() + .ok_or(FFORRecoveryError::InvalidRecord)? + .receiver_context(&record.setup) + .map_err(|_| FFORRecoveryError::InvalidRecord)?; + witnesses.validate(&context).map_err(|_| FFORRecoveryError::InvalidRecord)?; + } let header = authenticated.header(); let encoded_bytes = record.serialized_length(); let reserved_transition_bytes = Self::transition_reservation(&record); @@ -375,6 +391,7 @@ impl FFORRecoveryRegistry { canonical_book: authenticated.canonical_book().to_vec(), activation: None, request: None, + witnesses: None, })?; if self.pending_requests.iter().any(|pending| { pending.key.channel_id == entry.key.channel_id @@ -408,11 +425,13 @@ impl FFORRecoveryRegistry { epoch_id: authenticated.header().epoch_id, }; let request = self.get_request(&key).cloned(); + let witnesses = self.get_witnesses(&key).cloned(); let entry = Entry::new(StoredSetup { setup: setup.clone(), canonical_book: authenticated.canonical_book().to_vec(), activation: Some(activation.clone()), request, + witnesses, })?; let index = self .entries @@ -427,10 +446,18 @@ impl FFORRecoveryRegistry { } { return Err(FFORRecoveryError::ConflictingRecord); } + self.prepare_replacement(index, entry, 0) + } + + fn prepare_replacement( + &mut self, index: usize, entry: Entry, header_growth: usize, + ) -> Result, FFORRecoveryError> { + let existing = &self.entries[index]; let total_bytes = self .encoded_bytes .checked_sub(existing.encoded_bytes) .and_then(|bytes| bytes.checked_add(entry.encoded_bytes)) + .and_then(|bytes| bytes.checked_add(header_growth)) .ok_or(FFORRecoveryError::CapacityExceeded)?; let reserved_transition_bytes = self .reserved_transition_bytes @@ -453,6 +480,9 @@ impl FFORRecoveryRegistry { } fn version(&self) -> u8 { + if self.entries.iter().any(|entry| entry.record.witnesses.is_some()) { + return WITNESS_VERSION; + } if !self.pending_requests.is_empty() || self.entries.iter().any(|entry| entry.record.request.is_some()) { @@ -508,7 +538,7 @@ impl Writeable for FFORRecoveryRegistry { (entry.encoded_bytes as u32).write(writer)?; entry.record.write(writer)?; } - if self.version() == REQUEST_VERSION { + if self.version() >= REQUEST_VERSION { (self.pending_requests.len() as u16).write(writer)?; for entry in &self.pending_requests { (entry.encoded_bytes as u32).write(writer)?; @@ -527,6 +557,7 @@ impl Readable for FFORRecoveryRegistry { && version != ABORTED_ACTIVATION_VERSION && version != CLOSE_VERSION && version != REQUEST_VERSION + && version != WITNESS_VERSION { return Err(DecodeError::UnknownRequiredFeature); } @@ -535,7 +566,7 @@ impl Readable for FFORRecoveryRegistry { return Err(DecodeError::InvalidValue); } let mut registry = Self::new(); - if version == REQUEST_VERSION { + if version >= REQUEST_VERSION { registry.encoded_bytes += 2; } for _ in 0..count { @@ -566,7 +597,7 @@ impl Readable for FFORRecoveryRegistry { registry.reserved_transition_bytes += entry.reserved_transition_bytes(); registry.entries.push(entry); } - if version == REQUEST_VERSION { + if version >= REQUEST_VERSION { let count = u16::read(reader)? as usize; if registry.entries.len() + count > MAX_RECORDS { return Err(DecodeError::InvalidValue); @@ -612,3 +643,6 @@ impl Readable for FFORRecoveryRegistry { #[cfg(test)] pub(crate) mod tests; + +#[cfg(test)] +pub(crate) use activation::tests::witness::manifests as ffor_test_witness_manifests; diff --git a/lightning/src/ln/ffor_recovery/activation.rs b/lightning/src/ln/ffor_recovery/activation.rs index 994a3713531..63722bb500b 100644 --- a/lightning/src/ln/ffor_recovery/activation.rs +++ b/lightning/src/ln/ffor_recovery/activation.rs @@ -343,4 +343,4 @@ impl FFORReceiverActivation { } #[cfg(test)] -mod tests; +pub(super) mod tests; diff --git a/lightning/src/ln/ffor_recovery/activation/tests.rs b/lightning/src/ln/ffor_recovery/activation/tests.rs index 8fcb81908ee..dc910ddfdaf 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests.rs @@ -11,6 +11,7 @@ use lightning_ffor::wire::{Activate, Tlv}; mod aborted; mod close; +pub(in crate::ln::ffor_recovery) mod witness; // Synthetic transaction identities exercise archive authentication only. Real monitor and // channel evidence are exercised by the manager tests; these values never activate a channel. @@ -353,6 +354,7 @@ fn ffor_activation_archive_reserves_maximum_ack_through_competing_admission_and_ let (extra_setup, extra_activation) = evidence_for_identity(91, true); let extra = crate::ln::ffor_recovery::Entry::new(crate::ln::ffor_recovery::StoredSetup { request: None, + witnesses: None, canonical_book: extra_setup.validate_recovery().unwrap().canonical_book().to_vec(), setup: extra_setup, activation: Some(extra_activation), diff --git a/lightning/src/ln/ffor_recovery/activation/tests/close.rs b/lightning/src/ln/ffor_recovery/activation/tests/close.rs index 0213640b102..5862ec6dfb0 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests/close.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests/close.rs @@ -64,7 +64,7 @@ fn close_ack( ) } -fn phases(maximum: bool) -> (FFORReceiverSetup, Vec) { +pub(super) fn phases(maximum: bool) -> (FFORReceiverSetup, Vec) { let (setup, activating) = evidence(maximum); let active = activating.with_ack(&setup, &acknowledgement(&setup, &activating, maximum)).unwrap(); diff --git a/lightning/src/ln/ffor_recovery/activation/tests/witness.rs b/lightning/src/ln/ffor_recovery/activation/tests/witness.rs new file mode 100644 index 00000000000..6581da9f3d3 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/tests/witness.rs @@ -0,0 +1,193 @@ +use super::*; +use crate::ln::ffor::{FFORReceiverRecoveryContext, FFORReceiverWitnessRegistration}; +use crate::ln::ffor_recovery::{Entry, StoredSetup, WITNESS_VERSION}; +use bitcoin::secp256k1::{Message as SecpMessage, PublicKey, Secp256k1, SecretKey}; +use lightning_ffor::witness::{ManifestParameters, SignedManifest, UnsignedManifest}; + +pub(crate) fn manifests( + context: &FFORReceiverRecoveryContext, count: u8, +) -> Vec<(PublicKey, SignedManifest)> { + let secp = Secp256k1::new(); + let key = + |value| PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&[value; 32]).unwrap()); + let activation = match Message::decode(context.activation_wire()).unwrap().payload { + Payload::Activate(value) => value, + _ => unreachable!(), + }; + (0..count) + .map(|index| { + let secret = SecretKey::from_slice(&[index + 100; 32]).unwrap(); + let unsigned = UnsignedManifest::new( + context.setup(), + ManifestParameters { + mailbox_id: [index + 80; 32], + commitment_hash: activation.commit_hash, + epoch_start_height: activation.epoch_start_height, + fetch_public_key: PublicKey::from_secret_key(&secp, &secret), + encryption_public_key: key(120), + retention_until: context.setup().terms().voucher_expiry + 144, + minimum_receipts: 0, + }, + ) + .unwrap(); + let signature = secp + .sign_ecdsa(&SecpMessage::from_digest(unsigned.signing_digest()), &secret) + .serialize_compact(); + (key(index + 90), unsigned.authenticate(signature).unwrap()) + }) + .collect() +} + +#[test] +fn ffor_witness_archive_bounds_maximum_book_and_reserves_terminal_transitions() { + let (setup, phases) = close::phases(true); + let context = phases[1].receiver_context(&setup).unwrap(); + assert_eq!(context.setup().vouchers().len(), 483); + let selected = manifests(&context, 4); + let metadata = FFORReceiverWitnessRegistration::from_manifests(&context, &selected).unwrap(); + assert!(metadata.serialized_length() < 1_300); + assert!(selected.iter().map(|(_, manifest)| manifest.encode().len()).sum::() > 100_000); + let mut registry = registered(&setup); + for phase in &phases[..2] { + registry.prepare_activation(&setup, phase).unwrap().commit(); + } + registry.prepare_witnesses(&key(&setup), metadata.clone()).unwrap().commit(); + let reserved = registry.encoded_bytes + registry.reserved_transition_bytes; + for phase in phases.iter().skip(2) { + registry.prepare_activation(&setup, phase).unwrap().commit(); + assert!(registry.encoded_bytes + registry.reserved_transition_bytes <= reserved); + assert_eq!(registry.get_witnesses(&key(&setup)), Some(&metadata)); + let bytes = registry.encode(); + assert_eq!(bytes[0], WITNESS_VERSION); + assert_eq!(bytes.len(), registry.encoded_bytes); + let restored = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); + assert_eq!(restored.get_witnesses(&key(&setup)), Some(&metadata)); + assert_eq!(restored.encode(), bytes); + } +} + +#[test] +fn ffor_witness_archive_reauthenticates_compact_metadata_and_required_fence() { + let (setup, phases) = close::phases(false); + let context = phases[1].receiver_context(&setup).unwrap(); + let metadata = + FFORReceiverWitnessRegistration::from_manifests(&context, &manifests(&context, 2)).unwrap(); + let record = || StoredSetup { + setup: setup.clone(), + canonical_book: context.setup().canonical_book().to_vec(), + activation: Some(phases[1].clone()), + request: None, + witnesses: Some(metadata.clone()), + }; + for mutation in 0..9 { + let mut damaged = record(); + let registration = damaged.witnesses.as_mut().unwrap(); + match mutation { + 0 => registration.context_digest[0] ^= 1, + 1 => registration.witnesses[0].manifest_digest[0] ^= 1, + 2 => registration.witnesses[0].signature[0] ^= 1, + 3 => registration.witnesses[0].mailbox_id[0] ^= 1, + 4 => registration.witnesses[0].retention_until += 1, + 5 => registration.witnesses.swap(0, 1), + 6 => registration.witnesses.clear(), + 7 => damaged.activation = Some(phases[0].clone()), + _ => registration.witnesses.push(registration.witnesses[0].clone()), + } + assert!(Entry::new(damaged).is_err(), "accepted compact metadata corruption {mutation}"); + } + let mut registry = registered(&setup); + for phase in &phases[..2] { + registry.prepare_activation(&setup, phase).unwrap().commit(); + } + registry.prepare_witnesses(&key(&setup), metadata.clone()).unwrap().commit(); + let mut old_version = registry.encode(); + old_version[0] = crate::ln::ffor_recovery::REQUEST_VERSION; + assert!(FFORRecoveryRegistry::read(&mut &old_version[..]).is_err()); + let mut missing = record(); + missing.witnesses = None; + let mut bytes = vec![WITNESS_VERSION]; + 1u16.write(&mut bytes).unwrap(); + (missing.serialized_length() as u32).write(&mut bytes).unwrap(); + missing.write(&mut bytes).unwrap(); + 0u16.write(&mut bytes).unwrap(); + assert!(FFORRecoveryRegistry::read(&mut &bytes[..]).is_err()); + // The pre-registration record reader has no field 8 and must reject it as required. + #[derive(Clone)] + struct PreviousRecord { + setup: FFORReceiverSetup, + canonical_book: Vec, + activation: Option, + request: Option, + } + impl_writeable_tlv_based!(PreviousRecord, { + (0, setup, required), (2, canonical_book, required_vec), + (4, activation, option), (6, request, option), + }); + assert!(matches!( + PreviousRecord::read(&mut &record().encode()[..]), + Err(DecodeError::UnknownRequiredFeature) + )); +} + +#[test] +fn ffor_witness_archive_capacity_failure_cannot_install_registration() { + let (setup, phases) = close::phases(false); + let context = phases[1].receiver_context(&setup).unwrap(); + let metadata = + FFORReceiverWitnessRegistration::from_manifests(&context, &manifests(&context, 4)).unwrap(); + let mut registry = registered(&setup); + for phase in &phases[..2] { + registry.prepare_activation(&setup, phase).unwrap().commit(); + } + // Exercise the exact checked quota arithmetic at the limit without allocating an 8 MiB fixture. + registry.encoded_bytes = MAX_ENCODED_BYTES - registry.reserved_transition_bytes; + let before = registry.encode(); + assert!(matches!( + registry.prepare_witnesses(&key(&setup), metadata), + Err(FFORRecoveryError::CapacityExceeded) + )); + assert_eq!(registry.encode(), before); + assert!(registry.get_witnesses(&key(&setup)).is_none()); +} + +#[test] +fn ffor_witness_archive_rejects_signed_key_and_mailbox_reuse() { + let (setup, phases) = close::phases(false); + let context = phases[1].receiver_context(&setup).unwrap(); + let selected = manifests(&context, 2); + let sign_manifest = |parameters, key: u8| { + let unsigned = UnsignedManifest::new(context.setup(), parameters).unwrap(); + let signature = Secp256k1::new() + .sign_ecdsa( + &SecpMessage::from_digest(unsigned.signing_digest()), + &SecretKey::from_slice(&[key; 32]).unwrap(), + ) + .serialize_compact(); + unsigned.authenticate(signature).unwrap() + }; + for duplicate_fetch in [false, true] { + let mut repeated = selected.clone(); + let mut parameters = *selected[1].1.unsigned().parameters(); + let signer = if duplicate_fetch { + parameters.fetch_public_key = selected[0].1.unsigned().parameters().fetch_public_key; + 100 + } else { + parameters.mailbox_id = selected[0].1.unsigned().parameters().mailbox_id; + 101 + }; + repeated[1].1 = sign_manifest(parameters, signer); + assert!(FFORReceiverWitnessRegistration::from_manifests(&context, &repeated).is_err()); + } + let mut parameters = *selected[0].1.unsigned().parameters(); + parameters.encryption_public_key = parameters.fetch_public_key; + let signed_reuse = sign_manifest(parameters, 100); + let mut metadata = + FFORReceiverWitnessRegistration::from_manifests(&context, &selected[..1]).unwrap(); + metadata.witnesses[0].encryption_public_key = parameters.encryption_public_key; + metadata.witnesses[0].signature = *signed_reuse.signature(); + metadata.witnesses[0].manifest_digest = + bitcoin::hashes::sha256::Hash::hash(&signed_reuse.encode()).to_byte_array(); + let compact = metadata.encode(); + let restored = FFORReceiverWitnessRegistration::read(&mut &compact[..]).unwrap(); + assert!(restored.validate(&context).is_err()); +} diff --git a/lightning/src/ln/ffor_recovery/request.rs b/lightning/src/ln/ffor_recovery/request.rs index 08ec265d034..96feb4b3e1d 100644 --- a/lightning/src/ln/ffor_recovery/request.rs +++ b/lightning/src/ln/ffor_recovery/request.rs @@ -124,7 +124,7 @@ impl FFORRecoveryRegistry { }) { return Err(FFORRecoveryError::ConflictingRecord); } - let header_bytes = if self.version() == REQUEST_VERSION { 0 } else { 2 }; + let header_bytes = if self.version() >= REQUEST_VERSION { 0 } else { 2 }; self.check_capacity(pending.encoded_bytes + header_bytes, pending.reserved_bytes())?; Ok(FFORRequestInsertion { registry: self, entry: pending, header_bytes }) } @@ -152,6 +152,7 @@ impl FFORRecoveryRegistry { canonical_book: authenticated.canonical_book().to_vec(), activation: None, request: Some(request.clone()), + witnesses: None, })?; // Both forms were charged the full record allowance before Init. Promotion cannot increase // total used plus reserved bytes, and the borrowed permit prevents a conflicting insertion. diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs index 189efbd824c..429f5fbe187 100644 --- a/lightning/src/ln/ffor_recovery/tests.rs +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -124,7 +124,13 @@ pub(super) fn fixture(identity: u8, maximum_size: bool) -> Fixture { fn stored(record: FFORReceiverSetup) -> StoredSetup { let book = record.validate_recovery().unwrap().canonical_book().to_vec(); - StoredSetup { setup: record, canonical_book: book, activation: None, request: None } + StoredSetup { + setup: record, + canonical_book: book, + activation: None, + request: None, + witnesses: None, + } } fn frame(records: &[Vec]) -> Vec { @@ -293,7 +299,7 @@ fn ffor_recovery_reader_refuses_future_schema_missing_fields_and_truncation() { assert!(decode(&framed[..length]).is_err(), "accepted prefix {}", length); } let mut future = framed.clone(); - future[0] = REQUEST_VERSION + 1; + future[0] = WITNESS_VERSION + 1; assert!(matches!(decode(&future), Err(DecodeError::UnknownRequiredFeature))); // Empty TLV records omit both mandatory fields. assert!(decode(&frame(&[vec![0]])).is_err()); diff --git a/lightning/src/ln/ffor_recovery/witness.rs b/lightning/src/ln/ffor_recovery/witness.rs new file mode 100644 index 00000000000..0b6cde17c32 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/witness.rs @@ -0,0 +1,188 @@ +//! Compact immutable manifest evidence, reauthenticated against the retained native context. + +use super::*; +use bitcoin::hashes::{sha256, Hash}; +use lightning_ffor::wire::Payload; +use lightning_ffor::witness::{ManifestParameters, SignedManifest, UnsignedManifest}; + +use crate::ln::ffor::{ + FFORReceiverRecoveryContext, FFORReceiverWitnessRegistration, FFORRegisteredWitness, +}; + +const MAX_WITNESSES: usize = 4; + +impl_writeable_tlv_based!(FFORRegisteredWitness, { + (0, witness, required), + (2, manifest_digest, required), + (4, mailbox_id, required), + (6, fetch_public_key, required), + (8, encryption_public_key, required), + (10, retention_until, required), + (12, minimum_receipts, required), + (14, signature, required), +}); + +impl Writeable for FFORReceiverWitnessRegistration { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + self.context_digest.write(writer)?; + (self.witnesses.len() as u8).write(writer)?; + for witness in &self.witnesses { + witness.write(writer)?; + } + Ok(()) + } +} + +impl Readable for FFORReceiverWitnessRegistration { + fn read(reader: &mut R) -> Result { + let context_digest = <[u8; 32]>::read(reader)?; + let count = u8::read(reader)? as usize; + if count == 0 || count > MAX_WITNESSES { + return Err(DecodeError::InvalidValue); + } + let mut witnesses = Vec::with_capacity(count); + for _ in 0..count { + witnesses.push(FFORRegisteredWitness::read(reader)?); + } + Ok(Self { context_digest, witnesses }) + } +} + +impl FFORReceiverWitnessRegistration { + pub(crate) fn from_manifests( + context: &FFORReceiverRecoveryContext, manifests: &[(PublicKey, SignedManifest)], + ) -> Result { + if manifests.is_empty() || manifests.len() > MAX_WITNESSES { + return Err(DecodeError::InvalidValue); + } + let mut witnesses = Vec::with_capacity(manifests.len()); + for (witness, manifest) in manifests { + let p = manifest.unsigned().parameters(); + let entry = FFORRegisteredWitness { + witness: *witness, + manifest_digest: sha256::Hash::hash(&manifest.encode()).to_byte_array(), + mailbox_id: p.mailbox_id, + fetch_public_key: p.fetch_public_key, + encryption_public_key: p.encryption_public_key, + retention_until: p.retention_until, + minimum_receipts: p.minimum_receipts, + signature: *manifest.signature(), + }; + // Reconstruct from native setup, commitment identity and activation height. This also + // refuses a valid caller-constructed manifest for a different authenticated context. + if entry.manifest(context)?.encode() != manifest.encode() { + return Err(DecodeError::InvalidValue); + } + witnesses.push(entry); + } + witnesses.sort_unstable_by_key(|entry| entry.witness.serialize()); + let registration = Self { context_digest: context.context_digest(), witnesses }; + registration.validate(context)?; + Ok(registration) + } + + pub(crate) fn validate( + &self, context: &FFORReceiverRecoveryContext, + ) -> Result<(), DecodeError> { + if self.context_digest != context.context_digest() + || context.activation_ack_wire().is_none() + || self.witnesses.is_empty() + || self.witnesses.len() > MAX_WITNESSES + { + return Err(DecodeError::InvalidValue); + } + let restriction = match &context.setup().init().payload { + Payload::Init(init) => init.witness_peers.as_ref(), + _ => return Err(DecodeError::InvalidValue), + }; + for (index, witness) in self.witnesses.iter().enumerate() { + if restriction.map_or(false, |peers| !peers.contains(&witness.witness)) + || witness.fetch_public_key == witness.encryption_public_key + || witness.encryption_public_key != self.witnesses[0].encryption_public_key + || (index > 0 + && self.witnesses[index - 1].witness.serialize() >= witness.witness.serialize()) + || self.witnesses[..index].iter().any(|previous| { + previous.mailbox_id == witness.mailbox_id + || previous.fetch_public_key == witness.fetch_public_key + }) { + return Err(DecodeError::InvalidValue); + } + witness.manifest(context)?; + } + Ok(()) + } + + pub(crate) fn matches_manifest(&self, witness: &PublicKey, manifest: &SignedManifest) -> bool { + self.witnesses.iter().any(|entry| { + entry.witness == *witness + && entry.manifest_digest == sha256::Hash::hash(&manifest.encode()).to_byte_array() + }) + } +} + +impl FFORRegisteredWitness { + fn manifest( + &self, context: &FFORReceiverRecoveryContext, + ) -> Result { + let activation = lightning_ffor::wire::Message::decode(context.activation_wire()) + .map_err(|_| DecodeError::InvalidValue)?; + let activation = match activation.payload { + Payload::Activate(activation) => activation, + _ => return Err(DecodeError::InvalidValue), + }; + let manifest = UnsignedManifest::new( + context.setup(), + ManifestParameters { + mailbox_id: self.mailbox_id, + commitment_hash: activation.commit_hash, + epoch_start_height: activation.epoch_start_height, + fetch_public_key: self.fetch_public_key, + encryption_public_key: self.encryption_public_key, + retention_until: self.retention_until, + minimum_receipts: self.minimum_receipts, + }, + ) + .and_then(|unsigned| unsigned.authenticate(self.signature)) + .map_err(|_| DecodeError::InvalidValue)?; + if manifest.unsigned().activation_hash() != context.activation_hash() + || sha256::Hash::hash(&manifest.encode()).to_byte_array() != self.manifest_digest + { + return Err(DecodeError::InvalidValue); + } + Ok(manifest) + } +} + +impl FFORRecoveryRegistry { + pub(crate) fn get_witnesses( + &self, key: &FFORRecoveryKey, + ) -> Option<&FFORReceiverWitnessRegistration> { + self.entries + .iter() + .find(|entry| entry.key == *key) + .and_then(|entry| entry.record.witnesses.as_ref()) + } + + pub(crate) fn prepare_witnesses( + &mut self, key: &FFORRecoveryKey, witnesses: FFORReceiverWitnessRegistration, + ) -> Result, FFORRecoveryError> { + let index = self + .entries + .iter() + .position(|entry| entry.key == *key) + .ok_or(FFORRecoveryError::ConflictingRecord)?; + let previous = &self.entries[index]; + if previous.record.witnesses.as_ref().map_or(false, |existing| existing != &witnesses) { + return Err(FFORRecoveryError::ConflictingRecord); + } + let entry = Entry::new(StoredSetup { + setup: previous.record.setup.clone(), + canonical_book: previous.record.canonical_book.clone(), + activation: previous.record.activation.clone(), + request: previous.record.request.clone(), + witnesses: Some(witnesses), + })?; + let header_growth = if self.version() < REQUEST_VERSION { 2 } else { 0 }; + self.prepare_replacement(index, entry, header_growth) + } +} From 0def88713b26264ab50c2451ba97a17839f331ac Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Mon, 21 Sep 2026 02:34:37 -0400 Subject: [PATCH 17/22] feat(ffor): protect recovered receipts with the original monitor --- FFOR.md | 42 +- lightning/src/chain/channelmonitor.rs | 29 + lightning/src/ln/channel/ffor.rs | 1 + lightning/src/ln/channel/ffor/receipt.rs | 87 +++ lightning/src/ln/channelmanager.rs | 21 +- .../src/ln/channelmanager/ffor_activation.rs | 1 + .../channelmanager/ffor_activation/context.rs | 2 +- .../channelmanager/ffor_activation/receipt.rs | 223 ++++++ .../ffor_activation/receipt/tests.rs | 725 ++++++++++++++++++ .../ffor_activation/witness/tests.rs | 68 ++ lightning/src/ln/ffor.rs | 5 + lightning/src/ln/ffor/receipt.rs | 42 + 12 files changed, 1241 insertions(+), 5 deletions(-) create mode 100644 lightning/src/ln/channel/ffor/receipt.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/receipt.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs create mode 100644 lightning/src/ln/ffor/receipt.rs diff --git a/FFOR.md b/FFOR.md index 71feba8d436..398f438e9ee 100644 --- a/FFOR.md +++ b/FFOR.md @@ -241,7 +241,7 @@ HKDF-SHA256 and ChaCha20-Poly1305 implementations to verify the key, associated authentication tag before exposing a receipt. Its Debug output redacts the preimage. Witness observation amounts and timestamps carry no payment authority. This helper borrows the caller's epoch key; protected key storage, durable receipt retention and -monitor reconciliation remain runtime responsibilities. +invoking native monitor reconciliation remain runtime responsibilities. Public read-only receiver contexts let an application join protected records to native history. Historical contexts expose authenticated setup, exact activation messages, @@ -277,6 +277,28 @@ a fresh manager write and rejects an earlier instance's context. Historical regi metadata remains inspectable after close or force-close, so missing sidecar keys cannot be treated as permission to create a replacement selection. No invoice authority follows. +`ChannelMonitor::ffor_witness_receipt_snapshot` captures opaque evidence for one verified +receipt. The caller must drop its monitor guard before passing that snapshot, the receipt +and its historical context to `import_ffor_receiver_witness_receipt`. The manager checks +the original funding output, channel, settlement identity, canonical slot/hash/amount/D/E +and registered witness/mailbox/encryption key under the native peer and archive locks. +Receipt recovery remains valid after deadlines, disconnection, conflicting reports and +channel removal. Missing monitor ownership, stale counters and changed or pending funding +are refused. A closed channel's funding identity comes from its actual channel at removal +or its supplied durable monitor at restore, never from the receipt. + +Live vouchers use the existing stock claim and monitor path without ordinary payment +metadata. Known preimages replace only failures that have not entered a commitment. +A late preimage for an already failed or removed voucher protects the original monitor +without reversing that outcome. The native holding cell, in-flight updates and monitor +own persistence and idempotence; the archive does not duplicate preimage storage. +`PendingMonitor` requires normal monitor completion processing and a fresh snapshot retry. +`MonitorPersisted` observes preimage protection under the application's Watch contract; +it is neither payment credit, successful settlement nor invoice readiness, and no ordinary +`PaymentClaimed` event is synthesized. As with all stock recovery, manager ReadArgs must +contain actual durable monitors. Encoding an in-memory monitor does not complete a failed +or outstanding write. + These are consistency checks, not an authenticated storage envelope. Arbitrarily deleting a mismatching add's ownership record after abort can make its nonreserved hash indistinguishable from an ordinary post-abort payment. No valid writer creates @@ -302,6 +324,9 @@ required before requesting a signature. ## Validation +The current focused native suite passes 145 tests. Native no-default-features and +documentation builds with broken intra-doc links denied also pass. + The FFOR tests exercise real two-node commitment rounds, both funding directions, asymmetric dust limits and contest delays, signature corruption, stale monitors, monitor persistence delays, exact and partial books, mismatching and extra adds, @@ -398,16 +423,27 @@ capacity, corrupted metadata, key reuse, missing required evidence and version d An opt-in test exporter produces a real Active manager and stock monitor using a public Node wallet seed for downstream integration tests; normal test runs do not write fixtures. +Seven receipt-import integration tests exercise both funders, delayed monitor persistence, +idempotent retries, crash recovery from a failed write using the prior durable monitor, +missing peer/monitor refusal, unregistered witnesses, changed identities, expired epochs, +conflicting reconnects and force-close. They drive queued failures into fulfills only when +the preimage arrives before commitment, preserve already signed failures, and test actual +Closed to splice to removal recovery without applying old-funding receipts to the new scope. +Receiver payment events remain absent. Seven ordinary splice tests and the stock force-close +failure test also pass. A separate opt-in request exporter writes genuine empty and pending +manager/monitor fixtures only when `FFOR_NODE_REQUEST_FIXTURE_DIR` is set. + ## Next boundary Reusable epochs require durable retired epoch IDs and voucher hashes, with one current signed transcript record under the same channel authority. The public facade now composes the native activation and cooperative-close transitions. Production orchestration must retain witness keys and receipts, reconcile every recovered preimage -through the stock monitor, and enforce the configured deadline before claim safety ends. +through the native import API to completed monitor protection, and enforce the configured +deadline before claim safety ends. Production transport must bind the receiver facade to actual authenticated connections and deliver acknowledgement retries in the required order. Durable witness -mailbox recovery, preimage reconciliation, deadline enforcement and invoice eligibility remain +mailbox recovery, receipt-import orchestration, deadline enforcement and invoice eligibility remain separate required boundaries. None can be inferred from durable setup, activation or a successful private protocol test. diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 12718b52525..7c1de4fafcb 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -2145,6 +2145,35 @@ impl ChannelMonitor { }) } + /// Observe one authenticated witness preimage in this monitor, including after force-close. + /// + /// Drop every monitor guard before passing the result to + /// [`ChannelManager::import_ffor_receiver_witness_receipt`]. This captures in-memory state; + /// only that manager call can check matching native ownership and outstanding persistence. + /// A pending splice is conservatively refused so original funding cannot be substituted. + /// + /// [`ChannelManager::import_ffor_receiver_witness_receipt`]: crate::ln::channelmanager::ChannelManager::import_ffor_receiver_witness_receipt + pub fn ffor_witness_receipt_snapshot( + &self, receipt: &crate::ln::ffor::FFORWitnessReceipt, + ) -> Result { + let inner = self.inner.lock().unwrap(); + if !inner.pending_funding.is_empty() { + return Err(FFORCommitmentError::PendingUpdates); + } + let payment_hash = PaymentHash(receipt.body().payment_hash()); + Ok(crate::ln::ffor::FFORWitnessMonitorSnapshot { + channel_id: inner.channel_id(), + funding_txo: inner.get_funding_txo(), + counterparty: inner.counterparty_node_id, + update_id: inner.latest_update_id, + payment_hash, + known_preimage: inner + .payment_preimages + .get(&payment_hash) + .map_or(false, |(preimage, _)| preimage.0 == receipt.body().preimage()), + }) + } + /// The frozen archive must match the actual monitor, even when force-close prevents obtaining /// a live activation snapshot. Preimage and close updates may advance only the monitor update ID. pub(crate) fn ffor_recovery_identity(&self) -> crate::ln::ffor::FFORMonitorRecoveryIdentity { diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index d48bbe95e0f..d80cdffa4a2 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -4,6 +4,7 @@ mod activation; mod drain; mod fence; mod quiescence; +mod receipt; mod reestablish; mod setup; use crate::ln::ffor::{ diff --git a/lightning/src/ln/channel/ffor/receipt.rs b/lightning/src/ln/channel/ffor/receipt.rs new file mode 100644 index 00000000000..39d5ec102f6 --- /dev/null +++ b/lightning/src/ln/channel/ffor/receipt.rs @@ -0,0 +1,87 @@ +//! Preserve a historical voucher preimage through the stock monitor and claim machinery. + +use super::*; + +impl FundedChannel +where + SP::Target: SignerProvider, +{ + pub(crate) fn ffor_import_receipt_preimage( + &mut self, voucher: &FFORVoucher, preimage: PaymentPreimage, monitor_knows_preimage: bool, + logger: &L, + ) -> Result, FFORReceiverError> + where + L::Target: Logger, + { + let book = + self.context.ffor_receiver_book.as_ref().ok_or(FFORReceiverError::NotRegistered)?; + if !book.vouchers.contains(voucher) + || !self.context.ffor_owns_preimage(voucher.htlc_id, &preimage) + { + return Err(FFORReceiverError::InvalidWitnessReceipt); + } + if self.pending_splice.is_some() || self.context.interactive_tx_signing_session.is_some() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let has_drain = book.drain.is_some(); + // A historical receipt can arrive after a failure was already committed. The stock FFOR + // drain claim hook preserves that outcome while retaining preimage knowledge. A historical + // tombstone without a drain needs the monitor-only fallback below. + let htlc = + self.context.pending_inbound_htlcs.iter().find(|htlc| htlc.htlc_id == voucher.htlc_id); + if let Some(htlc) = htlc { + if htlc.payment_hash != voucher.payment_hash + || htlc.amount_msat != voucher.amount_msat + || htlc.cltv_expiry != voucher.cltv_expiry + { + return Err(FFORReceiverError::InvalidWitnessReceipt); + } + } + let committed = + htlc.map_or(false, |htlc| matches!(htlc.state, InboundHTLCState::Committed)); + if (committed && matches!(self.context.channel_state, ChannelState::ChannelReady(_))) + || (has_drain && !committed) + { + // A queued failure has not entered a commitment yet. A newly learned preimage wins + // before its admission, including when a monitor write currently blocks that round. + self.context.holding_cell_htlc_updates.retain(|update| { + !matches!(update, + HTLCUpdateAwaitingACK::FailHTLC { htlc_id, .. } + | HTLCUpdateAwaitingACK::FailMalformedHTLC { htlc_id, .. } + if *htlc_id == voucher.htlc_id) + }); + if let UpdateFulfillCommitFetch::NewClaim { monitor_update, .. } = self + .get_update_fulfill_htlc_and_commit(voucher.htlc_id, preimage, None, None, logger) + { + return Ok(Some(monitor_update)); + } + } + if monitor_knows_preimage { + return Ok(None); + } + // Match the stock claim helper's priority insertion: preimage protection must not wait + // behind a commitment update blocked by another channel or a signer. + self.context.latest_monitor_update_id = self + .context + .latest_monitor_update_id + .checked_add(1) + .ok_or(FFORCommitmentError::PendingUpdates)?; + let update_id = self + .context + .blocked_monitor_updates + .first() + .map_or(self.context.latest_monitor_update_id, |pending| pending.update.update_id); + for pending in &mut self.context.blocked_monitor_updates { + pending.update.update_id += 1; + } + self.monitor_updating_paused(false, false, false, Vec::new(), Vec::new(), Vec::new()); + Ok(Some(ChannelMonitorUpdate { + update_id, + updates: vec![ChannelMonitorUpdateStep::PaymentPreimage { + payment_preimage: preimage, + payment_info: None, + }], + channel_id: Some(self.context.channel_id()), + })) + } +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 944d3d633f7..19e67deb301 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -72,7 +72,6 @@ use crate::ln::ffor::{ use crate::ln::ffor_persistence::FFORPersistenceBarrier; pub use crate::ln::ffor_persistence::{FFORPersistenceRequirement, FFORPersistenceToken}; use crate::ln::ffor_recovery::FFORRecoveryRegistry; -mod ffor_activation; use crate::ln::funding::SpliceContribution; use crate::ln::inbound_payment; use crate::ln::interactivetxs::InteractiveTxMessageSend; @@ -1642,6 +1641,9 @@ where /// /// Note that channels which were closed prior to LDK 0.1 may have a value here of `u64::MAX`. closed_channel_monitor_update_ids: BTreeMap, + // Original monitor ownership for historical receipt imports. Rebuilt from supplied monitors + // at restart, and captured from the actual channel when it is removed. + closed_channel_monitor_funding: BTreeMap, /// The peer is currently connected (i.e. we've seen a /// [`BaseMessageHandler::peer_connected`] and no corresponding /// [`BaseMessageHandler::peer_disconnected`]. @@ -3337,6 +3339,7 @@ macro_rules! locked_close_channel { if $funded_chan.funding.get_funding_tx_confirmation_height().is_some() || $funded_chan.context.minimum_depth(&$funded_chan.funding) == Some(0) || update_id > 1 { let chan_id = $funded_chan.context.channel_id(); $peer_state.closed_channel_monitor_update_ids.insert(chan_id, update_id); + $peer_state.closed_channel_monitor_funding.insert(chan_id, $funded_chan.funding_outpoint()); } let mut short_to_chan_info = $self.short_to_chan_info.write().unwrap(); if let Some(short_id) = $funded_chan.funding.get_short_channel_id() { @@ -3847,6 +3850,9 @@ macro_rules! handle_new_monitor_update { } }; } +// Receipt imports use the stock monitor update and completion macros above. +mod ffor_activation; + #[rustfmt::skip] macro_rules! process_events_body { ($self: expr, $event_to_handle: expr, $handle_event: expr) => { @@ -14263,6 +14269,7 @@ where monitor_update_blocked_actions: BTreeMap::new(), actions_blocking_raa_monitor_updates: BTreeMap::new(), closed_channel_monitor_update_ids: BTreeMap::new(), + closed_channel_monitor_funding: BTreeMap::new(), is_connected: true, ffor_connection: Some(Arc::new(())), peer_storage: Vec::new(), @@ -17170,6 +17177,7 @@ where monitor_update_blocked_actions: BTreeMap::new(), actions_blocking_raa_monitor_updates: BTreeMap::new(), closed_channel_monitor_update_ids: BTreeMap::new(), + closed_channel_monitor_funding: BTreeMap::new(), peer_storage: Vec::new(), is_connected: false, ffor_connection: None, @@ -18558,6 +18566,17 @@ where ) .with_async_payments_offers_cache(async_receive_offer_cache); + // A historical receipt must not bind an old funding output to a post-splice counter. + // Only the actual supplied monitor establishes this identity after restart. + for (channel_id, monitor) in args.channel_monitors.iter() { + if let Some(peer) = per_peer_state.get(&monitor.get_counterparty_node_id()) { + let mut peer = peer.lock().unwrap(); + if peer.closed_channel_monitor_update_ids.contains_key(channel_id) { + peer.closed_channel_monitor_funding.insert(*channel_id, monitor.get_funding_txo()); + } + } + } + let channel_manager = ChannelManager { chain_hash, fee_estimator: bounded_fee_estimator, diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs index 8b07baa9ed5..c61106786ce 100644 --- a/lightning/src/ln/channelmanager/ffor_activation.rs +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -12,6 +12,7 @@ use lightning_ffor::wire::{Activate, Message as FFORMessage, Payload}; mod close; mod context; mod driver; +mod receipt; mod reestablish; mod witness; diff --git a/lightning/src/ln/channelmanager/ffor_activation/context.rs b/lightning/src/ln/channelmanager/ffor_activation/context.rs index 1023e62618f..f42b8127a20 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/context.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/context.rs @@ -79,7 +79,7 @@ where .collect() } - fn ffor_recovery_context_from_registry( + pub(super) fn ffor_recovery_context_from_registry( &self, recovery: &FFORRecoveryRegistry, key: &FFORRecoveryKey, ) -> Result { recovery diff --git a/lightning/src/ln/channelmanager/ffor_activation/receipt.rs b/lightning/src/ln/channelmanager/ffor_activation/receipt.rs new file mode 100644 index 00000000000..38535770f9a --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/receipt.rs @@ -0,0 +1,223 @@ +//! Historical witness preimages enter only the original native channel monitor. + +use super::*; +use crate::ln::ffor::{ + FFORReceiverRecoveryContext, FFORWitnessMonitorSnapshot, FFORWitnessReceipt, + FFORWitnessReceiptProgress, +}; + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Import an authenticated witness preimage into its original stock channel monitor. + /// + /// Historical recovery remains available after deadlines, disconnection, conflicting peer + /// reports and channel removal. The retained native setup, activation and immutable witness + /// selection must match every receipt term. Neither a witness timestamp nor an application + /// payment record supplies authority. Missing original monitor state, a changed funding output, + /// a pending splice or a stale snapshot is refused without changing the channel. + /// + /// Capture `monitor` with [`ChannelMonitor::ffor_witness_receipt_snapshot`] and release all + /// monitor guards first. Ownership checks and the update share the native peer lock, including + /// after force-close. Live unresolved vouchers use the stock claim path; an already failed or + /// removed voucher receives only preimage protection for possible older on-chain commitments. + /// No ordinary `PaymentClaimed` event, payment credit or invoice readiness is created here. + /// + /// A submitted write returns `PendingMonitor` even if persistence completed synchronously. + /// Process normal monitor events, capture a fresh snapshot and retry. `MonitorPersisted` means + /// only that the monitor knows this preimage and its native writes have completed according to + /// the application's [`Watch`] contract. The monitor owns durable idempotence across restart; + /// callers must retain the authenticated receipt for retries until that observation succeeds. + /// As with ordinary recovery, [`ChannelManagerReadArgs`] must contain the actual durable + /// monitors. Encoding an in-memory monitor does not complete an outstanding storage write. + /// + /// [`ChannelMonitor::ffor_witness_receipt_snapshot`]: crate::chain::channelmonitor::ChannelMonitor::ffor_witness_receipt_snapshot + /// [`Watch`]: crate::chain::Watch + pub fn import_ffor_receiver_witness_receipt( + &self, context: &FFORReceiverRecoveryContext, receipt: &FFORWitnessReceipt, + monitor: &FFORWitnessMonitorSnapshot, + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_mutex = per_peer_state + .get(&context.settlement_node_id()) + .ok_or(FFORCommitmentError::MonitorMismatch)?; + let mut peer_state_lock = peer_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + let channel_id = context.channel_id(); + let counterparty = context.settlement_node_id(); + let funding_txo = context.funding_txo(); + let key = FFORRecoveryKey { channel_id, epoch_id: context.epoch_id() }; + let recovery = self.ffor_recovery.lock().unwrap(); + let current = self.ffor_recovery_context_from_registry(&recovery, &key)?; + if current.context_digest() != context.context_digest() + || current.activation_ack_wire().is_none() + { + return Err(FFORReceiverError::InvalidWitnessReceipt); + } + let registration = + recovery.get_witnesses(&key).ok_or(FFORReceiverError::InvalidWitnessRegistration)?; + let header = receipt.header(); + let body = receipt.body(); + let witness = registration + .witnesses() + .iter() + .find(|witness| witness.witness_node_id() == header.witness) + .ok_or(FFORReceiverError::InvalidWitnessReceipt)?; + let index = usize::from(body.slot()) + .checked_sub(1) + .ok_or(FFORReceiverError::InvalidWitnessReceipt)?; + let signed = current + .setup() + .vouchers() + .get(index) + .ok_or(FFORReceiverError::InvalidWitnessReceipt)?; + let entry = current + .setup() + .canonical_book() + .get(36 + index * 58..36 + (index + 1) * 58) + .ok_or(FFORReceiverError::InvalidWitnessReceipt)?; + if header.slot != body.slot() + || header.activation_hash != current.activation_hash() + || header.mailbox_id != witness.mailbox_id() + || header.encryption_public_key != witness.encryption_public_key() + || header.terms_hash + != Sha256::hash(&[b"ffor/terms".as_slice(), entry].concat()).to_byte_array() + || body.epoch_id() != current.epoch_id() + || body.payment_hash() != signed.payment_hash + || body.amount_msat() != signed.amount_msat + || body.voucher_expiry() != signed.expiry + || body.settlement_deadline() != current.setup().terms().settlement_deadline + { + return Err(FFORReceiverError::InvalidWitnessReceipt); + } + let voucher = crate::ln::ffor::FFORVoucher { + htlc_id: signed.htlc_id, + payment_hash: PaymentHash(signed.payment_hash), + amount_msat: signed.amount_msat, + cltv_expiry: signed.expiry, + }; + if monitor.channel_id != channel_id + || monitor.funding_txo != funding_txo + || monitor.counterparty != counterparty + || monitor.payment_hash != voucher.payment_hash + { + return Err(FFORCommitmentError::MonitorMismatch.into()); + } + // Registry data is immutable for this historical identity. Drop its lock before the + // stock completion macro can release the peer lock and run unrelated completion actions. + drop(recovery); + let pending = if let Some((funding, updates)) = + peer_state.in_flight_monitor_updates.get(&channel_id) + { + if *funding != funding_txo { + return Err(FFORCommitmentError::MonitorMismatch.into()); + } + !updates.is_empty() + } else { + false + }; + let preimage = PaymentPreimage(body.preimage()); + if let Some(channel) = peer_state.channel_by_id.get_mut(&channel_id) { + let channel = channel.as_funded_mut().ok_or(FFORCommitmentError::MonitorMismatch)?; + if channel.funding_outpoint() != funding_txo + || channel.get_latest_unblocked_monitor_update_id() != monitor.update_id + { + return Err(FFORCommitmentError::MonitorMismatch.into()); + } + let logger = WithChannelContext::from(&self.logger, &channel.context, None); + let update = channel.ffor_import_receipt_preimage( + &voucher, + preimage, + monitor.known_preimage, + &&logger, + )?; + if let Some(update) = update { + let update_id = update.update_id; + handle_new_monitor_update!( + self, + funding_txo, + update, + peer_state_lock, + peer_state, + per_peer_state, + channel + ); + return Ok(FFORWitnessReceiptProgress::PendingMonitor { + monitor_update_id: update_id, + }); + } + let pending = pending + || channel.is_awaiting_monitor_update() + || channel.blocked_monitor_updates_pending() != 0; + return Ok(receipt_progress(monitor, pending)); + } + if peer_state.closed_channel_monitor_funding.get(&channel_id) != Some(&funding_txo) { + return Err(FFORCommitmentError::MonitorMismatch.into()); + } + let latest = peer_state + .closed_channel_monitor_update_ids + .get_mut(&channel_id) + .ok_or(FFORCommitmentError::MonitorMismatch)?; + if *latest != monitor.update_id { + return Err(FFORCommitmentError::MonitorMismatch.into()); + } + if monitor.known_preimage { + return Ok(receipt_progress(monitor, pending)); + } + *latest = latest.checked_add(1).ok_or(FFORCommitmentError::PendingUpdates)?; + let update_id = *latest; + let update = ChannelMonitorUpdate { + update_id, + updates: vec![ChannelMonitorUpdateStep::PaymentPreimage { + payment_preimage: preimage, + payment_info: None, + }], + channel_id: Some(channel_id), + }; + handle_new_monitor_update!( + self, + funding_txo, + update, + peer_state_lock, + peer_state, + per_peer_state, + counterparty, + channel_id, + POST_CHANNEL_CLOSE + ); + Ok(FFORWitnessReceiptProgress::PendingMonitor { monitor_update_id: update_id }) + } +} + +fn receipt_progress( + monitor: &FFORWitnessMonitorSnapshot, pending: bool, +) -> FFORWitnessReceiptProgress { + if pending { + FFORWitnessReceiptProgress::PendingMonitor { monitor_update_id: monitor.update_id } + } else { + FFORWitnessReceiptProgress::MonitorPersisted { monitor_update_id: monitor.update_id } + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs new file mode 100644 index 00000000000..de45297c308 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs @@ -0,0 +1,725 @@ +use super::*; +use crate::chain::ChannelMonitorUpdateStatus; +use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC; +use crate::ln::channelmanager::ffor_activation::drain_tests::{drain, park_two}; +use crate::ln::channelmanager::ffor_recovery_tests::restore; +use crate::ln::ffor::decrypt_ffor_witness_record; +use crate::ln::ffor_recovery::ffor_test_witness_manifests as manifests; +use crate::ln::ffor_tests::anchor_config; +use crate::ln::functional_test_utils::*; +use bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use bitcoin::hashes::HashEngine; +use bitcoin::secp256k1::{ecdh::SharedSecret, Message as SecpMessage, Secp256k1, SecretKey}; +use lightning_ffor::wire::CloseAck; +use lightning_ffor::witness::{EncryptedRecord, RecordHeader, SignedManifest}; + +const EPOCH: [u8; 32] = [81; 32]; + +fn persist(manager: &TestChannelManager) -> Vec { + let token = manager.capture_ffor_persistence(); + let bytes = manager.encode(); + manager.ffor_persistence_completed(token).unwrap(); + bytes +} + +fn signed(sender: &Node, context: &FFORReceiverRecoveryContext, payload: Payload) -> Vec { + let mut message = FFORMessage { + header: context.setup().header(), + payload, + extensions: Vec::new(), + signature: [0; 64], + }; + message.signature = sender + .keys_manager + .sign_ffor_message(&FFORSigningRequest::new(&message.unsigned_wire().unwrap()).unwrap()) + .unwrap() + .serialize_compact(); + message.encode().unwrap() +} + +fn activate(sender: &Node, receiver: &Node, id: ChannelId) -> FFORReceiverRecoveryContext { + let context = receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + let ack = signed(sender, &context, Payload::ActivateAck(context.activation_hash())); + receiver + .node + .accept_ffor_receiver_activation_ack(&id, &sender.node.get_our_node_id(), EPOCH, &ack) + .unwrap(); + persist(receiver.node); + let context = receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + receiver.node.register_ffor_receiver_witnesses(&context, &manifests(&context, 1)).unwrap(); + persist(receiver.node); + context +} + +/// Build a genuine signed, encrypted record and pass it through production authentication/AEAD. +fn receipt( + context: &FFORReceiverRecoveryContext, manifest: &SignedManifest, preimage: PaymentPreimage, + witness_byte: u8, +) -> FFORWitnessReceipt { + receipt_slot(context, manifest, preimage, witness_byte, 1) +} + +fn receipt_slot( + context: &FFORReceiverRecoveryContext, manifest: &SignedManifest, preimage: PaymentPreimage, + witness_byte: u8, slot: u16, +) -> FFORWitnessReceipt { + let secp = Secp256k1::new(); + let encryption = SecretKey::from_slice(&[120; 32]).unwrap(); + let ephemeral = SecretKey::from_slice(&[121; 32]).unwrap(); + let witness = SecretKey::from_slice(&[witness_byte; 32]).unwrap(); + let voucher = &context.setup().vouchers()[usize::from(slot) - 1]; + let mut body = Vec::new(); + body.extend_from_slice(&context.epoch_id()); + body.extend_from_slice(&slot.to_be_bytes()); + body.extend_from_slice(&preimage.0); + body.extend_from_slice(&voucher.payment_hash); + body.extend_from_slice(&voucher.amount_msat.to_be_bytes()); + body.extend_from_slice(&voucher.expiry.to_be_bytes()); + body.extend_from_slice(&context.setup().terms().settlement_deadline.to_be_bytes()); + body.extend_from_slice(&[0; 28]); + assert_eq!(body.len(), 142); + let parameters = manifest.unsigned().parameters(); + let start = 36 + (usize::from(slot) - 1) * 58; + let entry = &context.setup().canonical_book()[start..start + 58]; + let mut header = RecordHeader { + mailbox_id: parameters.mailbox_id, + record_id: [9; 32], + slot, + activation_hash: context.activation_hash(), + terms_hash: Sha256::hash(&[b"ffor/terms".as_slice(), entry].concat()).to_byte_array(), + witness: PublicKey::from_secret_key(&secp, &witness), + encryption_public_key: parameters.encryption_public_key, + recorded_height: 0, + unbarriered: true, + ciphertext_hash: [0; 32], + }; + let shared = SharedSecret::new(¶meters.encryption_public_key, &ephemeral); + let mut extract = HmacEngine::::new(&[]); + extract.input(&shared.secret_bytes()); + let prk = Hmac::from_engine(extract).to_byte_array(); + let mut expand = HmacEngine::::new(&prk); + expand.input(b"ffor/witness/body"); + expand.input(&[1]); + let key = Hmac::from_engine(expand).to_byte_array(); + let mut ciphertext = vec![0; 142]; + let mut tag = [0; 16]; + ChaCha20Poly1305RFC::new(&key, &[0; 12], &header.associated_data()).encrypt( + &body, + &mut ciphertext, + &mut tag, + ); + let mut encrypted = PublicKey::from_secret_key(&secp, &ephemeral).serialize().to_vec(); + encrypted.extend_from_slice(&ciphertext); + encrypted.extend_from_slice(&tag); + header.ciphertext_hash = Sha256::hash(&encrypted).to_byte_array(); + let signature = secp.sign_ecdsa(&SecpMessage::from_digest(header.signing_digest()), &witness); + let mut wire = header.encode(); + wire.extend_from_slice(&signature.serialize_compact()); + wire.extend_from_slice(&(encrypted.len() as u16).to_be_bytes()); + wire.extend_from_slice(&encrypted); + wire.push(0); + let record = + EncryptedRecord::decode(&wire).unwrap().authenticate(manifest, header.witness).unwrap(); + decrypt_ffor_witness_record(&record, manifest, &encryption).unwrap() +} + +fn snapshot( + node: &Node, context: &FFORReceiverRecoveryContext, receipt: &FFORWitnessReceipt, +) -> FFORWitnessMonitorSnapshot { + get_monitor!(node, context.channel_id()).ffor_witness_receipt_snapshot(receipt).unwrap() +} + +fn no_payment_events(manager: &TestChannelManager) { + for event in manager.get_and_clear_pending_events() { + assert!( + !matches!(event, Event::PaymentClaimed { .. } | Event::PaymentClaimable { .. }), + "unexpected {event:?}" + ); + } +} + +#[test] +fn ffor_receipt_import_fenced_preimage_waits_for_monitor_and_is_idempotent_after_restart() { + for receiver_funds in [false, true] { + let configs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &configs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let (vouchers, preimage) = park_two(sender, receiver, id); + let context = activate(sender, receiver, id); + let receipt = receipt(&context, &manifests(&context, 1)[0].1, preimage, 90); + let stale = snapshot(receiver, &context, &receipt); + configs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + let progress = + receiver.node.import_ffor_receiver_witness_receipt(&context, &receipt, &stale).unwrap(); + let update = match progress { + FFORWitnessReceiptProgress::PendingMonitor { monitor_update_id } => monitor_update_id, + _ => panic!(), + }; + check_added_monitors(receiver, 1); + assert_eq!( + get_monitor!(receiver, id).get_stored_preimages()[&vouchers[0].payment_hash].0, + preimage + ); + assert!(receiver + .node + .import_ffor_receiver_witness_receipt(&context, &receipt, &stale) + .is_err()); + let fresh = snapshot(receiver, &context, &receipt); + assert_eq!( + receiver.node.import_ffor_receiver_witness_receipt(&context, &receipt, &fresh).unwrap(), + progress + ); + check_added_monitors(receiver, 0); + no_payment_events(receiver.node); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + // Serializing an in-memory monitor must not complete the manager's outstanding write. + let _saved_pending = persist(receiver.node); + let monitor_pending = get_monitor!(receiver, id).encode(); + assert!(!monitor_pending.is_empty()); + assert_eq!( + receiver.node.import_ffor_receiver_witness_receipt(&context, &receipt, &fresh).unwrap(), + progress + ); + configs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + receiver.chain_monitor.chain_monitor.channel_monitor_updated(id, update).unwrap(); + no_payment_events(receiver.node); + let protected = FFORWitnessReceiptProgress::MonitorPersisted { monitor_update_id: update }; + assert_eq!( + receiver.node.import_ffor_receiver_witness_receipt(&context, &receipt, &fresh).unwrap(), + protected + ); + check_added_monitors(receiver, 0); + let saved = persist(receiver.node); + let restored = restore(receiver, &saved, &get_monitor!(receiver, id).encode()).unwrap(); + assert_eq!( + restored.import_ffor_receiver_witness_receipt(&context, &receipt, &fresh).unwrap(), + protected + ); + no_payment_events(&restored); + assert!(restored.get_and_clear_pending_msg_events().is_empty()); + } +} + +#[test] +fn ffor_receipt_import_refuses_unregistered_witness_and_changed_monitor_identity() { + let configs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &configs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let (_, preimage) = park_two(sender, receiver, id); + let context = activate(sender, receiver, id); + let selected = manifests(&context, 2); + let unknown = receipt(&context, &selected[1].1, preimage, 91); + assert_eq!( + receiver.node.import_ffor_receiver_witness_receipt( + &context, + &unknown, + &snapshot(receiver, &context, &unknown) + ), + Err(FFORReceiverError::InvalidWitnessReceipt) + ); + let receipt = receipt(&context, &selected[0].1, preimage, 90); + for mutation in 0..5 { + let mut evidence = snapshot(receiver, &context, &receipt); + match mutation { + 0 => evidence.funding_txo.index += 1, + 1 => evidence.channel_id.0[0] ^= 1, + 2 => evidence.counterparty = receiver.node.get_our_node_id(), + 3 => evidence.update_id += 1, + _ => evidence.payment_hash.0[0] ^= 1, + } + assert_eq!( + receiver.node.import_ffor_receiver_witness_receipt(&context, &receipt, &evidence), + Err(FFORCommitmentError::MonitorMismatch.into()) + ); + } + check_added_monitors(receiver, 0); + no_payment_events(receiver.node); +} + +#[test] +fn ffor_receipt_import_after_deadline_disconnect_and_force_close_preserves_original_monitor() { + for receiver_funds in [false, true] { + let configs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &configs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let (vouchers, preimage) = park_two(sender, receiver, id); + let context = activate(sender, receiver, id); + let receipt = receipt(&context, &manifests(&context, 1)[0].1, preimage, 90); + sender.node.peer_disconnected(receiver.node.get_our_node_id()); + receiver.node.peer_disconnected(sender.node.get_our_node_id()); + receiver.node.best_block.write().unwrap().height = + context.setup().terms().settlement_deadline + 1; + receiver + .node + .force_close_broadcasting_latest_txn( + &id, + &sender.node.get_our_node_id(), + "receipt recovery".to_owned(), + ) + .unwrap(); + no_payment_events(receiver.node); + receiver.node.get_and_clear_pending_msg_events(); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + let progress = receiver + .node + .import_ffor_receiver_witness_receipt( + &context, + &receipt, + &snapshot(receiver, &context, &receipt), + ) + .unwrap(); + assert!(matches!(progress, FFORWitnessReceiptProgress::PendingMonitor { .. })); + check_added_monitors(receiver, 1); + let fresh = snapshot(receiver, &context, &receipt); + assert!(matches!( + receiver.node.import_ffor_receiver_witness_receipt(&context, &receipt, &fresh).unwrap(), + FFORWitnessReceiptProgress::MonitorPersisted { .. } + )); + let known = get_monitor!(receiver, id).get_stored_preimages(); + assert_eq!(known[&vouchers[0].payment_hash].0, preimage); + assert!(known[&vouchers[0].payment_hash].1.is_empty()); + let restored = + restore(receiver, &persist(receiver.node), &get_monitor!(receiver, id).encode()) + .unwrap(); + assert!(matches!( + restored.import_ffor_receiver_witness_receipt(&context, &receipt, &fresh).unwrap(), + FFORWitnessReceiptProgress::MonitorPersisted { .. } + )); + no_payment_events(&restored); + // Archive-only history without native peer or monitor bookkeeping refuses rather than panics. + let removed = receiver + .node + .per_peer_state + .write() + .unwrap() + .remove(&sender.node.get_our_node_id()) + .unwrap(); + assert!(receiver + .node + .import_ffor_receiver_witness_receipt(&context, &receipt, &fresh) + .is_err()); + receiver + .node + .per_peer_state + .write() + .unwrap() + .insert(sender.node.get_our_node_id(), removed); + let latest = receiver + .node + .per_peer_state + .read() + .unwrap() + .get(&sender.node.get_our_node_id()) + .unwrap() + .lock() + .unwrap() + .closed_channel_monitor_update_ids + .remove(&id) + .unwrap(); + assert!(receiver + .node + .import_ffor_receiver_witness_receipt(&context, &receipt, &fresh) + .is_err()); + receiver + .node + .per_peer_state + .read() + .unwrap() + .get(&sender.node.get_our_node_id()) + .unwrap() + .lock() + .unwrap() + .closed_channel_monitor_update_ids + .insert(id, latest); + no_payment_events(receiver.node); + receiver.node.get_and_clear_pending_msg_events(); + } +} + +#[test] +fn ffor_receipt_import_late_failed_voucher_only_protects_monitor_and_keeps_stock_drain() { + for receive_after_drain in [false, true] { + let configs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &configs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let (vouchers, preimage) = park_two(sender, receiver, id); + let context = activate(sender, receiver, id); + let peer_id = sender.node.get_our_node_id(); + let receipt = receipt(&context, &manifests(&context, 1)[0].1, preimage, 90); + { + let peers = sender.node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&receiver.node.get_our_node_id()).unwrap().lock().unwrap(); + peer.channel_by_id.get_mut(&id).unwrap().as_funded_mut().unwrap().exit_quiescence(); + } + receiver.node.prepare_ffor_receiver_close(&id, &peer_id, EPOCH).unwrap(); + persist(receiver.node); + assert!(receiver + .node + .release_ffor_receiver_close(&id, &peer_id, EPOCH, |_| Ok(())) + .unwrap()); + let ack = signed( + sender, + &context, + Payload::CloseAck(CloseAck { + activation_hash: context.activation_hash(), + num_slots: 2, + settled: vec![0], + preimages: Vec::new(), + preimages_tlv_present: true, + }), + ); + receiver.node.accept_ffor_receiver_close_ack(&id, &peer_id, EPOCH, &ack).unwrap(); + persist(receiver.node); + assert!(receiver.node.release_ffor_receiver_drain(&id, &peer_id, EPOCH).unwrap()); + if receive_after_drain { + let (fulfilled, failed) = drain(sender, receiver); + assert!(fulfilled.is_empty()); + assert_eq!(failed, vouchers.iter().map(|voucher| voucher.htlc_id).collect::>()); + } else { + // The failure was admitted to a signed commitment. A late receipt cannot rewind it. + let flight = receiver.node.get_and_clear_pending_msg_events(); + assert!(flight.iter().any(|event| matches!(event, + MessageSendEvent::UpdateHTLCs { updates, .. } if !updates.update_fail_htlcs.is_empty()))); + let peers = receiver.node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&peer_id).unwrap().lock().unwrap(); + peer.pending_msg_events.extend(flight); + } + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + assert!(matches!( + receiver + .node + .import_ffor_receiver_witness_receipt( + &context, + &receipt, + &snapshot(receiver, &context, &receipt) + ) + .unwrap(), + FFORWitnessReceiptProgress::PendingMonitor { .. } + )); + check_added_monitors(receiver, 1); + let known = get_monitor!(receiver, id).get_stored_preimages(); + assert_eq!(known[&vouchers[0].payment_hash].0, preimage); + assert!(known[&vouchers[0].payment_hash].1.is_empty()); + if !receive_after_drain { + let (fulfilled, failed) = drain(sender, receiver); + assert!(fulfilled.is_empty()); + assert_eq!(failed.len(), 2); + } + assert!(matches!( + receiver + .node + .import_ffor_receiver_witness_receipt( + &context, + &receipt, + &snapshot(receiver, &context, &receipt) + ) + .unwrap(), + FFORWitnessReceiptProgress::MonitorPersisted { .. } + )); + let monitor = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); + receiver.node.prepare_ffor_receiver_closed(&id, &peer_id, EPOCH, &monitor).unwrap(); + persist(receiver.node); + receiver.node.release_ffor_receiver_closed(&id, &peer_id, EPOCH).unwrap(); + let restored = + restore(receiver, &persist(receiver.node), &get_monitor!(receiver, id).encode()) + .unwrap(); + assert!(matches!( + restored + .import_ffor_receiver_witness_receipt( + &context, + &receipt, + &snapshot(receiver, &context, &receipt) + ) + .unwrap(), + FFORWitnessReceiptProgress::MonitorPersisted { .. } + )); + no_payment_events(receiver.node); + no_payment_events(&restored); + receiver.node.get_and_clear_pending_msg_events(); + sender.node.get_and_clear_pending_events(); + if receive_after_drain { + let mut old_scope = snapshot(receiver, &context, &receipt); + let contribution = crate::ln::funding::SpliceContribution::SpliceOut { + outputs: vec![bitcoin::TxOut { + value: bitcoin::Amount::from_sat(10_000), + script_pubkey: bitcoin::ScriptBuf::from_bytes( + [vec![0, 20], vec![42; 20]].concat(), + ), + }], + }; + let tx = crate::ln::splicing_tests::splice_channel(sender, receiver, id, contribution); + assert!(get_monitor!(receiver, id).ffor_witness_receipt_snapshot(&receipt).is_err()); + assert!(receiver + .node + .import_ffor_receiver_witness_receipt(&context, &receipt, &old_scope) + .is_err()); + mine_transaction(sender, &tx); + mine_transaction(receiver, &tx); + crate::ln::splicing_tests::lock_splice_after_blocks( + sender, + receiver, + crate::chain::channelmonitor::ANTI_REORG_DELAY - 1, + ); + assert_ne!(get_monitor!(receiver, id).get_funding_txo(), context.funding_txo()); + receiver + .node + .force_close_broadcasting_latest_txn( + &id, + &peer_id, + "post-splice receipt".to_owned(), + ) + .unwrap(); + // Even a matching numeric counter cannot bind an old scope to the closed new funding. + old_scope.update_id = get_monitor!(receiver, id).get_latest_update_id(); + assert_eq!( + receiver.node.import_ffor_receiver_witness_receipt(&context, &receipt, &old_scope), + Err(FFORCommitmentError::MonitorMismatch.into()) + ); + let restored = + restore(receiver, &persist(receiver.node), &get_monitor!(receiver, id).encode()) + .unwrap(); + assert_eq!( + restored.import_ffor_receiver_witness_receipt(&context, &receipt, &old_scope), + Err(FFORCommitmentError::MonitorMismatch.into()) + ); + no_payment_events(receiver.node); + no_payment_events(&restored); + receiver.node.get_and_clear_pending_msg_events(); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + } + } +} + +#[test] +fn ffor_receipt_import_crash_replays_unpersisted_monitor_preimage_without_payment_events() { + let configs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &configs); + let config = anchor_config(); + let (persister, chain_monitor, restored); + let managers = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let mut nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (vouchers, preimage) = park_two(&nodes[0], &nodes[1], id); + let context = activate(&nodes[0], &nodes[1], id); + let receipt = receipt(&context, &manifests(&context, 1)[0].1, preimage, 90); + let old_monitor = get_monitor!(nodes[1], id).encode(); + configs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + let first = nodes[1] + .node + .import_ffor_receiver_witness_receipt( + &context, + &receipt, + &snapshot(&nodes[1], &context, &receipt), + ) + .unwrap(); + assert!(matches!(first, FFORWitnessReceiptProgress::PendingMonitor { .. })); + check_added_monitors(&nodes[1], 1); + let saved = persist(nodes[1].node); + // The failed write did not replace the durable monitor. The saved manager retains its update. + reload_node!(nodes[1], config, &saved, &[&old_monitor], persister, chain_monitor, restored); + persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + no_payment_events(nodes[1].node); + check_added_monitors(&nodes[1], 1); + let pending = snapshot(&nodes[1], &context, &receipt); + assert_eq!( + nodes[1].node.import_ffor_receiver_witness_receipt(&context, &receipt, &pending).unwrap(), + first + ); + assert_eq!( + get_monitor!(nodes[1], id).get_stored_preimages()[&vouchers[0].payment_hash].0, + preimage + ); + persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + nodes[1].chain_monitor.chain_monitor.channel_monitor_updated(id, pending.update_id).unwrap(); + no_payment_events(nodes[1].node); + assert!(matches!( + nodes[1].node.import_ffor_receiver_witness_receipt(&context, &receipt, &pending).unwrap(), + FFORWitnessReceiptProgress::MonitorPersisted { .. } + )); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + check_added_monitors(&nodes[1], 0); +} + +#[test] +fn ffor_receipt_import_preimage_wins_over_queued_failure_before_monitor_release() { + let configs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &configs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let (vouchers, first_preimage) = park_two(sender, receiver, id); + let context = activate(sender, receiver, id); + let manifest = &manifests(&context, 1)[0].1; + let first = receipt(&context, manifest, first_preimage, 90); + let second_preimage = PaymentPreimage([first_preimage.0[0] + 1; 32]); + let second = receipt_slot(&context, manifest, second_preimage, 90, 2); + configs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + receiver + .node + .import_ffor_receiver_witness_receipt( + &context, + &second, + &snapshot(receiver, &context, &second), + ) + .unwrap(); + check_added_monitors(receiver, 1); + let second_update = get_monitor!(receiver, id).get_latest_update_id(); + { + let peers = sender.node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&receiver.node.get_our_node_id()).unwrap().lock().unwrap(); + peer.channel_by_id.get_mut(&id).unwrap().as_funded_mut().unwrap().exit_quiescence(); + } + let peer_id = sender.node.get_our_node_id(); + receiver.node.prepare_ffor_receiver_close(&id, &peer_id, EPOCH).unwrap(); + persist(receiver.node); + let ack = signed( + sender, + &context, + Payload::CloseAck(CloseAck { + activation_hash: context.activation_hash(), + num_slots: 2, + settled: vec![0], + preimages: Vec::new(), + preimages_tlv_present: true, + }), + ); + receiver.node.accept_ffor_receiver_close_ack(&id, &peer_id, EPOCH, &ack).unwrap(); + persist(receiver.node); + assert!(receiver.node.release_ffor_receiver_drain(&id, &peer_id, EPOCH).unwrap()); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + let progress = receiver + .node + .import_ffor_receiver_witness_receipt( + &context, + &first, + &snapshot(receiver, &context, &first), + ) + .unwrap(); + let update_id = match progress { + FFORWitnessReceiptProgress::PendingMonitor { monitor_update_id } => monitor_update_id, + _ => panic!(), + }; + check_added_monitors(receiver, 1); + assert!(receiver.node.get_and_clear_pending_msg_events().is_empty()); + let pending = snapshot(receiver, &context, &first); + assert_eq!( + receiver.node.import_ffor_receiver_witness_receipt(&context, &first, &pending).unwrap(), + progress + ); + configs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + receiver.chain_monitor.chain_monitor.channel_monitor_updated(id, second_update).unwrap(); + receiver.chain_monitor.chain_monitor.channel_monitor_updated(id, update_id).unwrap(); + no_payment_events(receiver.node); + let (fulfilled, failed) = drain(sender, receiver); + assert!(failed.is_empty()); + assert_eq!(fulfilled.len(), 2); + for voucher in vouchers { + assert!(fulfilled.contains(&voucher.htlc_id)); + } + sender.node.get_and_clear_pending_events(); + sender.chain_monitor.added_monitors.lock().unwrap().clear(); + assert!(matches!( + receiver + .node + .import_ffor_receiver_witness_receipt( + &context, + &first, + &snapshot(receiver, &context, &first) + ) + .unwrap(), + FFORWitnessReceiptProgress::MonitorPersisted { .. } + )); + no_payment_events(receiver.node); +} + +#[test] +fn ffor_receipt_import_retains_protection_during_conflicting_reconnect() { + use lightning_ffor::reestablish::{Reestablish, ReportedState}; + let configs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &configs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let (_, preimage) = park_two(sender, receiver, id); + let context = activate(sender, receiver, id); + let selected = manifests(&context, 1); + let receipt = receipt(&context, &selected[0].1, preimage, 90); + let peer_id = sender.node.get_our_node_id(); + let receiver_id = receiver.node.get_our_node_id(); + sender.node.peer_disconnected(receiver_id); + receiver.node.peer_disconnected(peer_id); + connect_nodes(sender, receiver); + let local = get_event_msg!(receiver, MessageSendEvent::SendChannelReestablish, peer_id); + let mut remote = get_event_msg!(sender, MessageSendEvent::SendChannelReestablish, receiver_id); + let mut conflicting = context.activation_hash(); + conflicting[0] ^= 1; + remote.ffor_reestablish = Some(msgs::FFORChannelReestablish::new(Reestablish { + epoch_id: EPOCH, + activation_hash: conflicting, + state: ReportedState::Active, + })); + sender.node.handle_channel_reestablish(receiver_id, &local); + receiver.node.handle_channel_reestablish(peer_id, &remote); + assert!(receiver.node.capture_ffor_receiver_active_context(&id, &peer_id, EPOCH).is_err()); + receiver.node.best_block.write().unwrap().height = + context.setup().terms().settlement_deadline + 1; + assert!(matches!( + receiver + .node + .import_ffor_receiver_witness_receipt( + &context, + &receipt, + &snapshot(receiver, &context, &receipt) + ) + .unwrap(), + FFORWitnessReceiptProgress::PendingMonitor { .. } + )); + check_added_monitors(receiver, 1); + assert!(matches!( + receiver + .node + .import_ffor_receiver_witness_receipt( + &context, + &receipt, + &snapshot(receiver, &context, &receipt) + ) + .unwrap(), + FFORWitnessReceiptProgress::MonitorPersisted { .. } + )); + assert!(receiver.node.capture_ffor_receiver_active_context(&id, &peer_id, EPOCH).is_err()); + no_payment_events(receiver.node); + for event in receiver.node.get_and_clear_pending_msg_events() { + assert!(!matches!(event, MessageSendEvent::UpdateHTLCs { .. })); + } + sender.node.get_and_clear_pending_msg_events(); +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs index 7ba29429d88..5aa4bd9d14e 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs @@ -409,3 +409,71 @@ fn ffor_witness_release_waits_for_owned_preimage_monitor_and_refuses_close() { )) .is_err()); } + +/// Opt-in public fixture for the Node request store, before vouchers or activation exist. +#[test] +fn ffor_export_node_request_fixture() { + let directory = match std::env::var_os("FFOR_NODE_REQUEST_FIXTURE_DIR") { + Some(path) => std::path::PathBuf::from(path), + None => return, + }; + let mut configs = create_chanmon_cfgs(2); + let wallet_root = + bitcoin::bip32::Xpriv::new_master(bitcoin::Network::Testnet, &[91; 64]).unwrap(); + configs[1].keys_manager = crate::util::test_utils::TestKeysInterface::new( + &wallet_root.private_key.secret_bytes(), + bitcoin::Network::Testnet, + ); + let mut node_cfgs = create_node_cfgs(2, &configs); + node_cfgs[1].node_seed = wallet_root.private_key.secret_bytes(); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + std::fs::create_dir_all(&directory).unwrap(); + std::fs::write(directory.join("empty-manager.bin"), persist(receiver.node)).unwrap(); + let monitor = get_monitor!(receiver, id).encode(); + std::fs::write(directory.join("empty-monitor.bin"), &monitor).unwrap(); + let local_id = Sha256::hash( + &[ + b"ldk-node/ffor/request-id/v1".as_slice(), + &receiver.node.chain_hash.to_bytes(), + &receiver.node.get_our_node_id().serialize(), + &15u16.to_be_bytes(), + b"request-fixture", + ] + .concat(), + ) + .to_byte_array(); + let height = receiver.node.current_best_block().height; + let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let request = receiver + .node + .prepare_ffor_receiver( + &id, + &connection, + crate::ln::ffor::FFORReceiverParameters { + local_request_id: local_id, + amounts_msat: vec![2_000_000], + minimum_payment_msat: 2_000_000, + settlement_deadline: height + 100, + voucher_expiry: height + 144, + fee_base_msat: 0, + fee_proportional_millionths: 0, + claim_margin_blocks: 20, + witness_peers: None, + hash_chain: false, + }, + ) + .unwrap(); + std::fs::write(directory.join("pending-manager.bin"), persist(receiver.node)).unwrap(); + std::fs::write(directory.join("pending-monitor.bin"), monitor).unwrap(); + let hex = |bytes: &[u8]| bytes.iter().map(|byte| format!("{byte:02x}")).collect::(); + let funding = get_monitor!(receiver, id).get_funding_txo(); + std::fs::write(directory.join("fixture.txt"), format!( + "network=testnet\nwallet_seed={}\nchannel_id={}\nepoch_id={}\nreceiver={}\nsettlement={}\nfunding_txid={}\nfunding_vout={}\nheight={}\nclient_request_id=request-fixture\nlocal_request_id={}\namounts_msat=2000000\nminimum_payment_msat=2000000\nsettlement_deadline={}\nvoucher_expiry={}\nfee_base_msat=0\nfee_proportional_millionths=0\nclaim_margin_blocks=20\nwitness_peers=none\nhash_chain=false\ngenerator=ffor_export_node_request_fixture\nbase_revision=7ed8161b7\n", + hex(&[91; 64]), hex(&id.0), hex(&request.epoch_id()), receiver.node.get_our_node_id(), + sender.node.get_our_node_id(), funding.txid, funding.index, height, hex(&local_id), height + 100, height + 144, + )).unwrap(); +} diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index e7793a32ba1..d8ac24c40dd 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -36,6 +36,8 @@ pub use context::{FFORReceiverActiveContext, FFORReceiverRecoveryContext}; mod witness; pub use witness::{decrypt_ffor_witness_record, FFORWitnessDecryptionError, FFORWitnessReceipt}; +mod receipt; +pub use receipt::{FFORWitnessMonitorSnapshot, FFORWitnessReceiptProgress}; mod provision; pub use provision::{FFORReceiverWitnessRegistration, FFORRegisteredWitness}; @@ -160,6 +162,8 @@ pub enum FFORReceiverError { SignerUnavailable, /// Witness selection or exact manifest bytes differ from the native epoch or registration. InvalidWitnessRegistration, + /// The authenticated receipt does not belong to the retained native epoch and witness selection. + InvalidWitnessReceipt, } impl From for FFORReceiverError { @@ -179,6 +183,7 @@ impl fmt::Display for FFORReceiverError { Self::RecoveryUnavailable => f.write_str("FFOR recovery record cannot be retained"), Self::SignerUnavailable => f.write_str("FFOR node signature unavailable"), Self::InvalidWitnessRegistration => f.write_str("invalid FFOR witness registration"), + Self::InvalidWitnessReceipt => f.write_str("invalid FFOR witness receipt"), } } } diff --git a/lightning/src/ln/ffor/receipt.rs b/lightning/src/ln/ffor/receipt.rs new file mode 100644 index 00000000000..c5cd51922f1 --- /dev/null +++ b/lightning/src/ln/ffor/receipt.rs @@ -0,0 +1,42 @@ +//! Monitor observations for importing authenticated historical witness preimages. + +use crate::chain::transaction::OutPoint; +use crate::ln::types::ChannelId; +use crate::types::payment::PaymentHash; +use bitcoin::secp256k1::PublicKey; + +/// Opaque evidence about one receipt's original monitor, captured before locking the manager. +/// +/// Obtain this from [`ChannelMonitor::ffor_witness_receipt_snapshot`] and drop every monitor +/// guard before importing. A known preimage in memory is not proof of completed persistence. +/// The manager also checks its exact current counter and outstanding monitor writes. +/// +/// [`ChannelMonitor::ffor_witness_receipt_snapshot`]: crate::chain::channelmonitor::ChannelMonitor::ffor_witness_receipt_snapshot +pub struct FFORWitnessMonitorSnapshot { + pub(crate) channel_id: ChannelId, + pub(crate) funding_txo: OutPoint, + pub(crate) counterparty: PublicKey, + pub(crate) update_id: u64, + pub(crate) payment_hash: PaymentHash, + pub(crate) known_preimage: bool, +} + +/// Point-in-time protection of a witness preimage, not payment settlement or receive credit. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FFORWitnessReceiptProgress { + /// A stock monitor update was submitted, or its persistence is still outstanding. + /// Process normal monitor completion events, capture a fresh snapshot and retry the import. + PendingMonitor { + /// Exact monitor counter at the observation or submitted preimage update. + monitor_update_id: u64, + }, + /// The original monitor knows the preimage and the manager has no outstanding writes for it. + /// This relies on the application's normal [`Watch`] persistence completion contract. It + /// neither promises an on-chain claim will succeed nor authorizes an invoice or payment event. + /// + /// [`Watch`]: crate::chain::Watch + MonitorPersisted { + /// Exact monitor counter checked under the native peer lock. + monitor_update_id: u64, + }, +} From 40d105c175e5580a605bfbb5334a8995ade9ae16 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Mon, 21 Sep 2026 02:51:53 -0400 Subject: [PATCH 18/22] feat(ffor): validate exact retained receiver request intent --- FFOR.md | 10 + .../channelmanager/ffor_activation/driver.rs | 42 ++++ .../ffor_activation/driver/tests.rs | 2 + .../ffor_activation/driver/tests/intent.rs | 222 ++++++++++++++++++ 4 files changed, 276 insertions(+) create mode 100644 lightning/src/ln/channelmanager/ffor_activation/driver/tests/intent.rs diff --git a/FFOR.md b/FFOR.md index 398f438e9ee..c16d965c23d 100644 --- a/FFOR.md +++ b/FFOR.md @@ -44,6 +44,13 @@ epoch or channel authority. An exact retry returns the retained native selector; changed channel, peer or policy is refused. A bounded historical lookup recovers that selector after restart or live-channel removal without granting permission to send or expose an invoice. +`validate_ffor_receiver_request_intent` additionally compares every original +preparation parameter, including ordered amounts and witness restriction, against +pending or accepted native history without requiring a peer connection. Exact +historical matches remain available after abort, expiry and channel removal. A +changed local ID, channel, peer or parameter is refused. `None` means no retained +history for that ID; a protected application record that was already bound must +refuse that missing history rather than treat absence as permission to prepare again. The peer may send ordinary voucher adds immediately after Accept. `handle_ffor_receiver_message` therefore processes Accept synchronously under the @@ -400,6 +407,9 @@ accepted-manager persistence. Other cases cover pre-Accept adds, contradictory s replies, deadline crossing, retry identity after restore, missing archive evidence, capacity refusal, witness policy, cancellation and normal payment after controlled gate release on a fresh connection. +Read-only intent tests check every preparation field, preserve pending write barriers, +and recover exact pending, promoted and archive-only histories after restart in both +funding directions where applicable. The lifecycle facade tests advance through real voucher and STFU rounds, durable activation, close, both settled and failed drain, and final Closed in both funding diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver.rs b/lightning/src/ln/channelmanager/ffor_activation/driver.rs index ca8f512e081..ab2b0af3135 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/driver.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/driver.rs @@ -165,6 +165,48 @@ where .transpose() } + /// Compare an application's retained preparation intent to the exact native request history. + /// + /// Checks every original parameter, including local retry identity, channel, settlement peer, + /// ordered voucher amounts and witness restriction, fees, deadlines, claim margin and hash-chain + /// policy. A known request with different parameters returns [`FFORReceiverError::AlreadyRegistered`]. + /// The explicit `local_request_id` must also equal the one in `parameters`. + /// + /// This bounded read-only check works for pending, accepted and terminal histories, including + /// after restart or channel removal. It requires neither a connection nor a current deadline, + /// phase or completed persistence requirement. Success grants no permission to resend, replace + /// a request, advance an epoch or expose an invoice. + /// + /// `Ok(None)` means this manager has no retained history for that local request ID. Applications + /// whose protected record was already bound must treat this as missing native history and refuse + /// to continue that record. Absence is not proof that a new request is safe to prepare. + pub fn validate_ffor_receiver_request_intent( + &self, local_request_id: [u8; 32], channel_id: &ChannelId, + counterparty_node_id: &PublicKey, parameters: &FFORReceiverParameters, + ) -> Result, FFORReceiverError> { + let _guard = self.total_consistency_lock.read().unwrap(); + let recovery = self.ffor_recovery.lock().unwrap(); + recovery + .validate_identity(self.our_network_pubkey, self.chain_hash) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if parameters.local_request_id != local_request_id { + return Err(FFORReceiverError::AlreadyRegistered); + } + let request = match recovery.find_request(local_request_id) { + Some(request) => request, + None => return Ok(None), + }; + let header = + request.validate_recovery().map_err(|_| FFORReceiverError::RecoveryUnavailable)?.header; + if !request.matches_intent(*channel_id, *counterparty_node_id, parameters) { + return Err(FFORReceiverError::AlreadyRegistered); + } + Ok(Some(FFORReceiverId { + channel_id: ChannelId(header.channel_id), + epoch_id: header.epoch_id, + })) + } + /// Release at most one exact Init after its pre-init gate is durable on the original connection. /// /// `enqueue` must atomically verify its paired authenticated transport generation and insert diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs index 1104e389321..ca1b085b9f3 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs @@ -75,6 +75,8 @@ fn emit(receiver: &Node, id: &FFORReceiverId, connection: &FFORPeerConnection) - wire } +mod intent; + #[test] fn ffor_driver_durable_init_exact_backpressure_and_synchronous_accept() { for receiver_funds in [false, true] { diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver/tests/intent.rs b/lightning/src/ln/channelmanager/ffor_activation/driver/tests/intent.rs new file mode 100644 index 00000000000..10ec2253bdc --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/tests/intent.rs @@ -0,0 +1,222 @@ +use super::*; +use bitcoin::secp256k1::{Secp256k1, SecretKey}; + +#[test] +fn ffor_driver_read_only_intent_matches_every_original_parameter() { + fixture!(nodes, sender, receiver, channel, false); + let voucher = FFORVoucher { + htlc_id: 0, + payment_hash: PaymentHash([9; 32]), + amount_msat: 2_000_000, + cltv_expiry: 200, + }; + let peer = sender.node.get_our_node_id(); + let connection = receiver.node.ffor_peer_connection(&peer).unwrap(); + let mut params = parameters(&voucher); + params.amounts_msat.push(3_000_000); + params.witness_peers = Some( + [101, 102] + .iter() + .map(|byte| { + PublicKey::from_secret_key( + &Secp256k1::new(), + &SecretKey::from_slice(&[*byte; 32]).unwrap(), + ) + }) + .collect(), + ); + assert_eq!( + receiver.node.validate_ffor_receiver_request_intent( + params.local_request_id, + &channel, + &peer, + ¶ms, + ), + Ok(None) + ); + let id = receiver.node.prepare_ffor_receiver(&channel, &connection, params.clone()).unwrap(); + let before = receiver.node.encode(); + assert_eq!( + receiver.node.validate_ffor_receiver_request_intent( + params.local_request_id, + &channel, + &peer, + ¶ms, + ), + Ok(Some(id)) + ); + let changes: &[fn(&mut FFORReceiverParameters)] = &[ + |params| params.local_request_id[0] ^= 1, + |params| params.amounts_msat[0] += 1, + |params| params.amounts_msat.reverse(), + |params| params.amounts_msat.clear(), + |params| params.minimum_payment_msat += 1, + |params| params.settlement_deadline -= 1, + |params| params.voucher_expiry += 1, + |params| params.fee_base_msat += 1, + |params| params.fee_proportional_millionths += 1, + |params| params.claim_margin_blocks += 1, + |params| params.witness_peers.as_mut().unwrap().reverse(), + |params| { + params.witness_peers.as_mut().unwrap().pop(); + }, + |params| params.witness_peers = None, + |params| params.hash_chain = true, + ]; + for (index, change) in changes.iter().enumerate() { + let mut changed = params.clone(); + change(&mut changed); + assert_eq!( + receiver.node.validate_ffor_receiver_request_intent( + params.local_request_id, + &channel, + &peer, + &changed, + ), + Err(FFORReceiverError::AlreadyRegistered), + "parameter change {} was accepted", + index + ); + } + for (other_channel, other_peer) in + [(ChannelId([0; 32]), peer), (channel, receiver.node.get_our_node_id())] + { + assert_eq!( + receiver.node.validate_ffor_receiver_request_intent( + params.local_request_id, + &other_channel, + &other_peer, + ¶ms, + ), + Err(FFORReceiverError::AlreadyRegistered) + ); + } + let mut absent = params.clone(); + absent.local_request_id = [0; 32]; + assert_eq!( + receiver.node.validate_ffor_receiver_request_intent( + absent.local_request_id, + &channel, + &peer, + &absent, + ), + Ok(None) + ); + assert_eq!(before, receiver.node.encode()); + assert_eq!( + receiver + .node + .advance_ffor_receiver(&id, &connection, |_| panic!("read released Init")) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + // Historical correlation does not require a connection, unexpired deadline or live request. + receiver.node.peer_disconnected(peer); + receiver.node.best_block.write().unwrap().height = params.settlement_deadline; + let bytes = persist(receiver); + let monitor = get_monitor!(receiver, channel).encode(); + let restored = restore(receiver, &bytes, &monitor).unwrap(); + assert!(restored.ffor_peer_connection(&peer).is_err()); + assert_eq!( + restored.validate_ffor_receiver_request_intent( + params.local_request_id, + &channel, + &peer, + ¶ms + ), + Ok(Some(id)) + ); + assert!(restored.get_and_clear_pending_msg_events().is_empty()); + assert!(restored.get_and_clear_pending_events().is_empty()); +} + +#[test] +fn ffor_driver_read_only_intent_preserves_promoted_and_removed_history() { + for receiver_funds in [false, true] { + fixture!(nodes, sender, receiver, channel, receiver_funds); + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let peer = sender.node.get_our_node_id(); + let params = parameters(&voucher); + let connection = receiver.node.ffor_peer_connection(&peer).unwrap(); + let id = + receiver.node.prepare_ffor_receiver(&channel, &connection, params.clone()).unwrap(); + persist(receiver); + let init = emit(receiver, &id, &connection); + let accepted = accept(sender, receiver, channel, voucher, &init).encode().unwrap(); + receiver.node.handle_ffor_receiver_message(&connection, &accepted).unwrap(); + assert_eq!( + receiver.node.validate_ffor_receiver_request_intent( + params.local_request_id, + &channel, + &peer, + ¶ms + ), + Ok(Some(id)) + ); + deliver_parked_voucher(sender, receiver, update); + let bytes = persist(receiver); + let monitor = get_monitor!(receiver, channel).encode(); + let restored = restore(receiver, &bytes, &monitor).unwrap(); + assert_eq!( + restored.validate_ffor_receiver_request_intent( + params.local_request_id, + &channel, + &peer, + ¶ms + ), + Ok(Some(id)) + ); + let mut conflict = params.clone(); + conflict.claim_margin_blocks += 1; + assert_eq!( + restored.validate_ffor_receiver_request_intent( + params.local_request_id, + &channel, + &peer, + &conflict + ), + Err(FFORReceiverError::AlreadyRegistered) + ); + assert!(restored.get_and_clear_pending_events().is_empty()); + receiver + .node + .force_close_broadcasting_latest_txn(&channel, &peer, "request intent history".into()) + .unwrap(); + receiver.node.get_and_clear_pending_msg_events(); + receiver.node.get_and_clear_pending_events(); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + assert!(receiver.node.list_channels().is_empty()); + assert_eq!( + receiver.node.validate_ffor_receiver_request_intent( + params.local_request_id, + &channel, + &peer, + ¶ms + ), + Ok(Some(id)) + ); + let closed_bytes = persist(receiver); + let closed_monitor = get_monitor!(receiver, channel).encode(); + let closed = restore(receiver, &closed_bytes, &closed_monitor).unwrap(); + assert!(closed.list_channels().is_empty()); + assert_eq!( + closed.validate_ffor_receiver_request_intent( + params.local_request_id, + &channel, + &peer, + ¶ms + ), + Ok(Some(id)) + ); + assert_eq!( + closed.validate_ffor_receiver_request_intent( + params.local_request_id, + &channel, + &peer, + &conflict + ), + Err(FFORReceiverError::AlreadyRegistered) + ); + assert!(closed.get_and_clear_pending_events().is_empty()); + } +} From 016778d264edf6c3f7201199cfdc9162fdc29562 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Mon, 21 Sep 2026 03:20:50 -0400 Subject: [PATCH 19/22] feat(ffor): persist native witness acknowledgement authority --- FFOR.md | 34 +- lightning/src/ln/channelmanager.rs | 2 + .../src/ln/channelmanager/ffor_activation.rs | 4 +- .../channelmanager/ffor_activation/witness.rs | 8 +- .../ffor_activation/witness/tests.rs | 19 + .../ffor_activation/witness/tests/ack.rs | 406 ++++++++++++++++++ .../ffor_activation/witness_ack.rs | 321 ++++++++++++++ lightning/src/ln/ffor.rs | 4 + lightning/src/ln/ffor/witness_ack.rs | 76 ++++ lightning/src/ln/ffor_recovery.rs | 16 +- .../src/ln/ffor_recovery/activation/tests.rs | 1 + .../ffor_recovery/activation/tests/witness.rs | 5 +- .../activation/tests/witness/ack.rs | 150 +++++++ lightning/src/ln/ffor_recovery/request.rs | 1 + lightning/src/ln/ffor_recovery/tests.rs | 3 +- lightning/src/ln/ffor_recovery/witness.rs | 7 + lightning/src/ln/ffor_recovery/witness_ack.rs | 122 ++++++ 17 files changed, 1172 insertions(+), 7 deletions(-) create mode 100644 lightning/src/ln/channelmanager/ffor_activation/witness/tests/ack.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/witness_ack.rs create mode 100644 lightning/src/ln/ffor/witness_ack.rs create mode 100644 lightning/src/ln/ffor_recovery/activation/tests/witness/ack.rs create mode 100644 lightning/src/ln/ffor_recovery/witness_ack.rs diff --git a/FFOR.md b/FFOR.md index c16d965c23d..2eb1d8565fd 100644 --- a/FFOR.md +++ b/FFOR.md @@ -284,6 +284,30 @@ a fresh manager write and rejects an earlier instance's context. Historical regi metadata remains inspectable after close or force-close, so missing sidecar keys cannot be treated as permission to create a replacement selection. No invoice authority follows. +The stronger provisioning path stages an opaque attempt against the actual native +witness connection, exact registered manifest and request ID. Release rechecks the +current Active state and completed persistence, then marks the attempt sent only after +its paired transport callback accepts the bounded enqueue. Already sent retries do not +invoke the callback again. Disconnect and connection replacement invalidate attempts; +restore never recovers transient send authority. The manager retains at most 64 attempts +and request-ID tombstones without eviction until the corresponding witness disconnects. + +Only a positive ACK correlated to a native sent attempt can enter native history. +The application first confirms its own protected ACK write, then passes the response +from the same authenticated connection to native retention. Native preserves its first +valid promise for each selected witness and returns a new persistence requirement. +Native and application history may retain different first request IDs after a crash +and fresh retry; both must bind the same epoch, manifest and witness with adequate +retention. Neither promise may be silently replaced to make their transport IDs match. + +Required archive field 10 and schema 6 reserve a fixed 373 bytes for up to four native +promises before the stronger provisioning path is available. Filling a promise cannot +grow storage or enclosing length prefixes. Legacy witness registration needs an explicit +capacity-checked upgrade and successful write. Restore validates the complete record, +rejects noncanonical padding and imposes a fresh persistence barrier. Historical ACK +metadata survives close and removal but supplies no invoice or payment readiness. +The earlier observational Provision release cannot establish native sent correlation. + `ChannelMonitor::ffor_witness_receipt_snapshot` captures opaque evidence for one verified receipt. The caller must drop its monitor guard before passing that snapshot, the receipt and its historical context to `import_ffor_receiver_witness_receipt`. The manager checks @@ -331,7 +355,7 @@ required before requesting a signature. ## Validation -The current focused native suite passes 145 tests. Native no-default-features and +The current focused native suite passes 156 tests. Native no-default-features and documentation builds with broken intra-doc links denied also pass. The FFOR tests exercise real two-node commitment rounds, both funding directions, @@ -433,6 +457,14 @@ capacity, corrupted metadata, key reuse, missing required evidence and version d An opt-in test exporter produces a real Active manager and stock monitor using a public Node wallet seed for downstream integration tests; normal test runs do not write fixtures. +Nine native ACK tests cover both funders, unsent requests, backpressure, wrong witness, +request and retention, refusal tombstones, connection replacement, failed persistence, +exact retries, independent first promises, restoration and archive-only history. They +also check offline settlement-peer operation, current deadlines, fixed storage through +all terminal phases with a maximum book, legacy quota refusal and malformed archives. +A previous field-layout reader rejects the new required ACK field; schema downgrade is +also refused. These checks do not exercise a production witness or invoice issuance. + Seven receipt-import integration tests exercise both funders, delayed monitor persistence, idempotent retries, crash recovery from a failed write using the prior durable monitor, missing peer/monitor refusal, unregistered witnesses, changed identities, expired epochs, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 19e67deb301..1a4ee08105f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -14207,6 +14207,7 @@ where }); debug_assert!(peer_state.is_connected, "A disconnected peer cannot disconnect"); peer_state.is_connected = false; + self.ffor_activation.lock().unwrap().invalidate_witness_attempts(counterparty_node_id); peer_state.ffor_connection = None; peer_state.ok_to_remove(true) } else { debug_assert!(false, "Unconnected peer disconnected"); true } @@ -14293,6 +14294,7 @@ where debug_assert!(!peer_state.is_connected, "A peer shouldn't be connected twice"); peer_state.is_connected = true; + self.ffor_activation.lock().unwrap().invalidate_witness_attempts(counterparty_node_id); peer_state.ffor_connection = Some(Arc::new(())); }, } diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs index c61106786ce..a7f80dceb75 100644 --- a/lightning/src/ln/channelmanager/ffor_activation.rs +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -15,6 +15,7 @@ mod driver; mod receipt; mod reestablish; mod witness; +mod witness_ack; struct RuntimeEntry { key: FFORRecoveryKey, @@ -27,11 +28,12 @@ struct RuntimeEntry { /// Bounded by the retained registry's keys. No bytes or phase authority are copied here. pub(super) struct FFORReceiverRuntime { entries: Vec, + witness_attempts: Vec, } impl FFORReceiverRuntime { pub(super) fn new() -> Self { - Self { entries: Vec::new() } + Self { entries: Vec::new(), witness_attempts: Vec::new() } } pub(super) fn restored( diff --git a/lightning/src/ln/channelmanager/ffor_activation/witness.rs b/lightning/src/ln/channelmanager/ffor_activation/witness.rs index 1ffa59590ba..a7692d44780 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/witness.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/witness.rs @@ -76,7 +76,11 @@ where if existing != ®istration { return Err(FFORReceiverError::InvalidWitnessRegistration); } - return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); + if recovery.get_witness_acks(&key).is_some() { + return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); + } + // A legacy registration must reserve compact ACK capacity before the new authenticated + // attempt path sends anything. Exact retries cannot skip this checked upgrade. } // Initial registration cannot convert a merely observed or unpersisted Active phase into // authority. Its exact compact evidence and all later close reservations must fit first. @@ -181,7 +185,7 @@ where Ok(enqueue(provision).is_ok()) } - fn validate_ffor_witness_context( + pub(super) fn validate_ffor_witness_context( provided: &FFORReceiverRecoveryContext, current: &FFORReceiverRecoveryContext, height: u32, ) -> Result<(), FFORReceiverError> { if provided.context_digest() != current.context_digest() diff --git a/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs index 5aa4bd9d14e..1a483bc9968 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs @@ -10,6 +10,8 @@ use lightning_ffor::witness::{ManifestParameters, UnsignedManifest}; const EPOCH: [u8; 32] = [81; 32]; +mod ack; + fn persist(node: &TestChannelManager) -> Vec { let token = node.capture_ffor_persistence(); let bytes = node.encode(); @@ -376,6 +378,11 @@ fn ffor_witness_release_waits_for_owned_preimage_monitor_and_refuses_close() { persist(receiver.node); let active = receiver.node.capture_ffor_receiver_active_context(&id, &peer, EPOCH).unwrap(); let provision = Provision::new([1; 16], selected[0].1.clone()); + let witness_connection = ack::connect(receiver, selected[0].0); + let attempt = receiver + .node + .stage_ffor_receiver_witness_provision(&context, &witness_connection, &provision) + .unwrap(); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); let update = claim_ffor_preimage_for_test(sender, receiver, id, vouchers[0].htlc_id, preimage); check_added_monitors(receiver, 1); @@ -386,6 +393,12 @@ fn ffor_witness_release_waits_for_owned_preimage_monitor_and_refuses_close() { )) .is_err()); assert!(receiver.node.register_ffor_receiver_witnesses(&context, &selected).is_err()); + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| panic!( + "pending monitor sent attempt" + )) + .is_err()); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); receiver.chain_monitor.chain_monitor.channel_monitor_updated(id, update).unwrap(); receiver.node.get_and_clear_pending_msg_events(); @@ -394,6 +407,12 @@ fn ffor_witness_release_waits_for_owned_preimage_monitor_and_refuses_close() { .release_ffor_receiver_witness_provision(&active, &selected[0].0, &provision, |_| Ok(())) .unwrap()); receiver.node.prepare_ffor_receiver_close(&id, &peer, EPOCH).unwrap(); + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| panic!( + "closing sent attempt" + )) + .is_err()); assert!(receiver .node .release_ffor_receiver_witness_provision(&active, &selected[0].0, &provision, |_| panic!( diff --git a/lightning/src/ln/channelmanager/ffor_activation/witness/tests/ack.rs b/lightning/src/ln/channelmanager/ffor_activation/witness/tests/ack.rs new file mode 100644 index 00000000000..c7c386b4bd5 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/witness/tests/ack.rs @@ -0,0 +1,406 @@ +use super::*; +use crate::ln::ffor::{FFORPeerConnection, FFORWitnessProvisionAttempt}; +use lightning_ffor::witness::{Acknowledgement, AcknowledgementResult}; + +macro_rules! fixture { + ($sender:ident, $receiver:ident, $id:ident, $context:ident, $selected:ident, $receiver_funds:expr) => { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if $receiver_funds { (1, 0) } else { (0, 1) }; + let $id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let ($sender, $receiver) = (&nodes[0], &nodes[1]); + park_two($sender, $receiver, $id); + let $context = activate($sender, $receiver, $id); + persist($receiver.node); + let $selected = manifests(&$context, 2); + $receiver.node.register_ffor_receiver_witnesses(&$context, &$selected).unwrap(); + }; +} + +pub(super) fn connect(receiver: &Node, witness: PublicKey) -> FFORPeerConnection { + let init = msgs::Init { + features: receiver.node.init_features(), + networks: None, + remote_network_address: None, + }; + receiver.node.peer_connected(witness, &init, false).unwrap(); + receiver.node.ffor_peer_connection(&witness).unwrap() +} + +fn ack(provision: &Provision, witness: PublicKey, retention_until: u32) -> Acknowledgement { + Acknowledgement::new( + provision.request_id(), + AcknowledgementResult::Accepted { witness, retention_until }, + ) + .unwrap() +} + +fn staged( + receiver: &Node, context: &FFORReceiverRecoveryContext, selected: &(PublicKey, SignedManifest), + id: u8, +) -> (FFORPeerConnection, Provision, FFORWitnessProvisionAttempt) { + let connection = connect(receiver, selected.0); + let provision = Provision::new([id; 16], selected.1.clone()); + let attempt = receiver + .node + .stage_ffor_receiver_witness_provision(context, &connection, &provision) + .unwrap(); + (connection, provision, attempt) +} + +#[test] +fn ffor_witness_ack_requires_native_sent_request_and_manager_durability() { + for receiver_funds in [false, true] { + fixture!(sender, receiver, id, context, selected, receiver_funds); + let (connection, provision, attempt) = staged(receiver, &context, &selected[0], 1); + let retention = selected[0].1.unsigned().parameters().retention_until; + let response = ack(&provision, selected[0].0, retention); + assert!(receiver.node.retain_ffor_receiver_witness_ack(&connection, &response).is_err()); + assert!(receiver + .node + .capture_ffor_receiver_active_context(&id, &context.settlement_node_id(), EPOCH) + .is_err()); + persist(receiver.node); + let active = receiver + .node + .capture_ffor_receiver_active_context(&id, &context.settlement_node_id(), EPOCH) + .unwrap(); + assert!(!receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| Err(())) + .unwrap()); + assert!(receiver.node.retain_ffor_receiver_witness_ack(&connection, &response).is_err()); + // The legacy release does not mark a staged native attempt sent. + assert!(receiver + .node + .release_ffor_receiver_witness_provision( + &active, + &selected[0].0, + &provision, + |_| Ok(()) + ) + .unwrap()); + assert!(receiver.node.retain_ffor_receiver_witness_ack(&connection, &response).is_err()); + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |wire| { + assert_eq!(wire.encode(), provision.encode()); + Ok(()) + }) + .unwrap()); + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| panic!( + "duplicate enqueue" + )) + .unwrap()); + let requirement = + receiver.node.retain_ffor_receiver_witness_ack(&connection, &response).unwrap(); + assert!(!receiver.node.is_ffor_state_persisted(&requirement)); + assert_eq!( + receiver.node.retain_ffor_receiver_witness_ack(&connection, &response).unwrap(), + requirement + ); + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| panic!( + "stale requirement" + )) + .is_err()); + let token = receiver.node.capture_ffor_persistence(); + let _ = receiver.node.encode(); + drop(token); + assert!(!receiver.node.is_ffor_state_persisted(&requirement)); + let original = + receiver.node.ffor_receiver_witness_acknowledgements(&context).unwrap().unwrap(); + assert_eq!(original.acknowledgements().len(), 1); + assert_eq!(original.acknowledgements()[0].request_id(), provision.request_id()); + persist(receiver.node); + assert!(receiver.node.is_ffor_state_persisted(&requirement)); + let active = receiver + .node + .capture_ffor_receiver_active_context(&id, &context.settlement_node_id(), EPOCH) + .unwrap(); + // A sidecar may have retained an earlier ACK before a disconnect. Each store preserves its + // own first valid promise; a later successful attempt never replaces native evidence. + let later = Provision::new([2; 16], selected[0].1.clone()); + let later_attempt = receiver + .node + .stage_ffor_receiver_witness_provision(&context, &connection, &later) + .unwrap(); + receiver + .node + .release_ffor_receiver_witness_attempt(&active, &later_attempt, &later, |_| Ok(())) + .unwrap(); + let before = receiver.node.encode(); + assert_eq!( + receiver + .node + .retain_ffor_receiver_witness_ack( + &connection, + &ack(&later, selected[0].0, retention + 1) + ) + .unwrap(), + requirement + ); + assert_eq!(before, receiver.node.encode()); + assert_eq!( + receiver.node.ffor_receiver_witness_acknowledgements(&context).unwrap().unwrap(), + original + ); + assert!(receiver.node.get_and_clear_pending_events().is_empty()); + } +} + +#[test] +fn ffor_witness_ack_rejects_wrong_correlation_and_retains_refusal_tombstone() { + fixture!(sender, receiver, id, context, selected, false); + persist(receiver.node); + let (connection, provision, attempt) = staged(receiver, &context, &selected[0], 1); + let other_connection = connect(receiver, selected[1].0); + let active = receiver + .node + .capture_ffor_receiver_active_context(&id, &sender.node.get_our_node_id(), EPOCH) + .unwrap(); + let substituted = Provision::new(provision.request_id(), selected[1].1.clone()); + assert!(receiver + .node + .stage_ffor_receiver_witness_provision(&context, &connection, &substituted) + .is_err()); + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &substituted, |_| panic!( + "substitution" + )) + .is_err()); + receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| Ok(())) + .unwrap(); + let retention = selected[0].1.unsigned().parameters().retention_until; + assert!(receiver + .node + .retain_ffor_receiver_witness_ack( + &other_connection, + &ack(&provision, selected[0].0, retention) + ) + .is_err()); + assert!(receiver + .node + .retain_ffor_receiver_witness_ack(&connection, &ack(&provision, selected[1].0, retention)) + .is_err()); + assert!(receiver + .node + .retain_ffor_receiver_witness_ack( + &connection, + &ack(&provision, selected[0].0, retention - 1) + ) + .is_err()); + let unknown = Provision::new([9; 16], selected[0].1.clone()); + assert!(receiver + .node + .retain_ffor_receiver_witness_ack(&connection, &ack(&unknown, selected[0].0, retention)) + .is_err()); + let refused = + Acknowledgement::new(provision.request_id(), AcknowledgementResult::Refused(vec![1])) + .unwrap(); + assert!(receiver.node.retain_ffor_receiver_witness_ack(&connection, &refused).is_err()); + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| panic!( + "refused retry" + )) + .is_err()); + assert!(receiver + .node + .stage_ffor_receiver_witness_provision(&context, &connection, &provision) + .is_err()); + assert!(receiver + .node + .retain_ffor_receiver_witness_ack(&connection, &ack(&provision, selected[0].0, retention)) + .is_err()); + assert!(receiver + .node + .ffor_receiver_witness_acknowledgements(&context) + .unwrap() + .unwrap() + .acknowledgements() + .is_empty()); +} + +#[test] +fn ffor_witness_ack_connection_loss_requires_fresh_native_attempt() { + fixture!(sender, receiver, id, context, selected, false); + persist(receiver.node); + let (connection, provision, attempt) = staged(receiver, &context, &selected[0], 1); + let active = receiver + .node + .capture_ffor_receiver_active_context(&id, &sender.node.get_our_node_id(), EPOCH) + .unwrap(); + receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| Ok(())) + .unwrap(); + let retention = selected[0].1.unsigned().parameters().retention_until; + // The application may already have persisted this ACK. Native lost the connection first. + let observed = ack(&provision, selected[0].0, retention); + receiver.node.peer_disconnected(selected[0].0); + assert!(receiver.node.retain_ffor_receiver_witness_ack(&connection, &observed).is_err()); + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| panic!( + "disconnected enqueue" + )) + .is_err()); + let replacement = connect(receiver, selected[0].0); + assert_ne!(connection, replacement); + assert!(receiver.node.retain_ffor_receiver_witness_ack(&replacement, &observed).is_err()); + let fresh = Provision::new([2; 16], selected[0].1.clone()); + let fresh_attempt = receiver + .node + .stage_ffor_receiver_witness_provision(&context, &replacement, &fresh) + .unwrap(); + receiver + .node + .release_ffor_receiver_witness_attempt(&active, &fresh_attempt, &fresh, |_| Ok(())) + .unwrap(); + receiver + .node + .retain_ffor_receiver_witness_ack(&replacement, &ack(&fresh, selected[0].0, retention + 1)) + .unwrap(); + let saved = receiver.node.ffor_receiver_witness_acknowledgements(&context).unwrap().unwrap(); + assert_eq!(saved.acknowledgements()[0].request_id(), fresh.request_id()); + assert_eq!(saved.acknowledgements()[0].retention_until(), retention + 1); +} + +#[test] +fn ffor_witness_ack_restore_and_archive_only_history_never_restore_attempts() { + fixture!(sender, receiver, id, context, selected, false); + persist(receiver.node); + let (connection, provision, attempt) = staged(receiver, &context, &selected[0], 1); + let active = receiver + .node + .capture_ffor_receiver_active_context(&id, &sender.node.get_our_node_id(), EPOCH) + .unwrap(); + receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| Ok(())) + .unwrap(); + let response = + ack(&provision, selected[0].0, selected[0].1.unsigned().parameters().retention_until); + receiver.node.retain_ffor_receiver_witness_ack(&connection, &response).unwrap(); + let metadata = receiver.node.ffor_receiver_witness_acknowledgements(&context).unwrap().unwrap(); + let bytes = persist(receiver.node); + let monitor = get_monitor!(receiver, id).encode(); + let restored = restore(receiver, &bytes, &monitor).unwrap(); + assert_eq!( + restored.ffor_receiver_witness_acknowledgements(&context).unwrap(), + Some(metadata.clone()) + ); + assert!(restored.retain_ffor_receiver_witness_ack(&connection, &response).is_err()); + assert!(restored + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| panic!( + "restored attempt" + )) + .is_err()); + let requirement = restored.register_ffor_receiver_witnesses(&context, &selected).unwrap(); + assert!(!restored.is_ffor_state_persisted(&requirement)); + persist(&restored); + assert!(restored.is_ffor_state_persisted(&requirement)); + receiver + .node + .force_close_broadcasting_latest_txn( + &id, + &sender.node.get_our_node_id(), + "witness ACK archive".into(), + ) + .unwrap(); + receiver.node.get_and_clear_pending_msg_events(); + receiver.node.get_and_clear_pending_events(); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + let closed = + restore(receiver, &persist(receiver.node), &get_monitor!(receiver, id).encode()).unwrap(); + assert!(closed.list_channels().is_empty()); + assert_eq!(closed.ffor_receiver_witness_acknowledgements(&context).unwrap(), Some(metadata)); +} + +#[test] +fn ffor_witness_ack_attempt_budget_preserves_exact_retry_until_disconnect() { + fixture!(sender, receiver, id, context, selected, false); + persist(receiver.node); + let (connection, first, first_attempt) = staged(receiver, &context, &selected[0], 0); + for index in 1..64 { + let provision = Provision::new([index; 16], selected[0].1.clone()); + receiver + .node + .stage_ffor_receiver_witness_provision(&context, &connection, &provision) + .unwrap(); + } + let repeated = + receiver.node.stage_ffor_receiver_witness_provision(&context, &connection, &first).unwrap(); + assert!(Arc::ptr_eq(&repeated.identity, &first_attempt.identity)); + assert!(matches!( + receiver.node.stage_ffor_receiver_witness_provision( + &context, + &connection, + &Provision::new([64; 16], selected[0].1.clone()) + ), + Err(FFORReceiverError::RecoveryUnavailable) + )); + receiver.node.peer_disconnected(selected[0].0); + let replacement = connect(receiver, selected[0].0); + assert!(receiver + .node + .stage_ffor_receiver_witness_provision( + &context, + &replacement, + &Provision::new([65; 16], selected[0].1.clone()), + ) + .is_ok()); +} + +#[test] +fn ffor_witness_ack_release_is_offline_capable_but_rechecks_current_deadline() { + fixture!(sender, receiver, id, context, selected, false); + persist(receiver.node); + let (connection, provision, attempt) = staged(receiver, &context, &selected[0], 1); + receiver.node.peer_disconnected(sender.node.get_our_node_id()); + persist(receiver.node); + let active = receiver + .node + .capture_ffor_receiver_active_context(&id, &context.settlement_node_id(), EPOCH) + .unwrap(); + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| { + // White-box evidence that the native generation replacement guard spans the callback. + // Production callbacks must only check their paired transport token and bounded queue. + assert!(receiver.node.per_peer_state.try_write().is_err()); + Ok(()) + }) + .unwrap()); + let later = Provision::new([2; 16], selected[0].1.clone()); + let later_attempt = + receiver.node.stage_ffor_receiver_witness_provision(&context, &connection, &later).unwrap(); + receiver.node.best_block.write().unwrap().height = context.setup().terms().settlement_deadline; + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &later_attempt, &later, |_| panic!( + "expired enqueue" + )) + .is_err()); + // An already observed storage promise remains retainable as historical evidence after D. + receiver + .node + .retain_ffor_receiver_witness_ack( + &connection, + &ack(&provision, selected[0].0, selected[0].1.unsigned().parameters().retention_until), + ) + .unwrap(); +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/witness_ack.rs b/lightning/src/ln/channelmanager/ffor_activation/witness_ack.rs new file mode 100644 index 00000000000..543d9199f4b --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/witness_ack.rs @@ -0,0 +1,321 @@ +//! Native sent-request correlation and compact durable witness promises. + +use super::*; +use crate::ln::ffor::{ + FFORPeerConnection, FFORReceiverActiveContext, FFORReceiverRecoveryContext, + FFORReceiverWitnessAcknowledgements, FFORReceiverWitnessRegistration, + FFORWitnessAcknowledgement, FFORWitnessProvisionAttempt, +}; +use bitcoin::hashes::{sha256, Hash}; +use lightning_ffor::witness::{Acknowledgement, AcknowledgementResult, Provision}; + +const MAX_WITNESS_ATTEMPTS: usize = 64; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum AttemptState { + Staged, + Sent, + Refused, +} + +pub(super) struct WitnessAttempt { + handle: FFORWitnessProvisionAttempt, + state: AttemptState, +} + +impl FFORReceiverRuntime { + pub(in crate::ln::channelmanager) fn invalidate_witness_attempts(&mut self, peer: PublicKey) { + self.witness_attempts.retain(|attempt| attempt.handle.connection.peer != peer); + } +} + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Bind an exact registered Provision to this manager's authenticated witness connection. + /// + /// Staging grants no send, ACK or invoice authority. At most 64 attempts and request-ID + /// tombstones are retained across the manager. Exact retries return the same handle. Reusing + /// one request ID for different content on the same connection is refused. A witness disconnect + /// invalidates all its attempts; use a fresh request ID for the same manifest on reconnect. + /// Registration must have reserved native ACK storage before this path can be used. + pub fn stage_ffor_receiver_witness_provision( + &self, context: &FFORReceiverRecoveryContext, connection: &FFORPeerConnection, + provision: &Provision, + ) -> Result { + let _guard = self.total_consistency_lock.read().unwrap(); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(&connection.peer) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + self.require_current_ffor_connection(&peer, &connection.peer, Some(connection))?; + let recovery = self.ffor_recovery.lock().unwrap(); + let (key, registration, _) = self.ffor_ack_registration_locked(&recovery, context)?; + if !registration.matches_manifest(&connection.peer, provision.manifest()) { + return Err(FFORReceiverError::InvalidWitnessRegistration); + } + let digest = sha256::Hash::hash(&provision.manifest().encode()).to_byte_array(); + let mut runtime = self.ffor_activation.lock().unwrap(); + if let Some(existing) = runtime.witness_attempts.iter().find(|attempt| { + attempt.handle.connection == *connection + && attempt.handle.request_id == provision.request_id() + }) { + if existing.handle.key != key + || existing.handle.context_digest != context.context_digest() + || existing.handle.manifest_digest != digest + || existing.state == AttemptState::Refused + { + return Err(FFORReceiverError::InvalidWitnessRegistration); + } + return Ok(existing.handle.clone()); + } + if runtime.witness_attempts.len() >= MAX_WITNESS_ATTEMPTS { + return Err(FFORReceiverError::RecoveryUnavailable); + } + let handle = FFORWitnessProvisionAttempt { + identity: Arc::new(()), + key, + context_digest: context.context_digest(), + connection: connection.clone(), + request_id: provision.request_id(), + manifest_digest: digest, + }; + runtime + .witness_attempts + .push(WitnessAttempt { handle: handle.clone(), state: AttemptState::Staged }); + Ok(handle) + } + + /// Release a staged request while native Active state and the witness generation remain valid. + /// + /// The callback must atomically check its paired actual witness transport token and enqueue + /// this exact Provision with bounded capacity. It must perform no I/O and acquire no native, + /// owner or storage locks. A successful enqueue alone marks this attempt sent. Backpressure + /// leaves it staged. An already sent exact retry returns true without calling the callback. + /// The settlement peer need not be connected. This does not authorize invoice exposure. + pub fn release_ffor_receiver_witness_attempt( + &self, context: &FFORReceiverActiveContext, attempt: &FFORWitnessProvisionAttempt, + provision: &Provision, enqueue: C, + ) -> Result + where + C: FnOnce(&Provision) -> Result<(), ()>, + { + let _guard = self.total_consistency_lock.read().unwrap(); + let historical = context.recovery_context(); + // Native generation changes require this map's write lock. Retain the read guard through + // enqueue, without locking the witness peer while holding the settlement channel. + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(&historical.settlement_node_id()) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(&historical.channel_id()) + .and_then(Channel::as_funded) + .ok_or(FFORCommitmentError::ChannelUnavailable)?; + if channel.context.is_monitor_or_signer_pending_channel_update() + || peer + .in_flight_monitor_updates + .get(&historical.channel_id()) + .map_or(false, |(_, updates)| !updates.is_empty()) + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let recovery = self.ffor_recovery.lock().unwrap(); + let (key, registration, _) = self.ffor_ack_registration_locked(&recovery, historical)?; + let current = self.ffor_active_state_locked(channel, &recovery, &key)?; + let height = self.best_block.read().unwrap(); + Self::validate_ffor_witness_context(historical, ¤t, height.height)?; + if attempt.key != key + || attempt.context_digest != historical.context_digest() + || attempt.request_id != provision.request_id() + || attempt.manifest_digest + != sha256::Hash::hash(&provision.manifest().encode()).to_byte_array() + || !registration.matches_manifest(&attempt.connection.peer, provision.manifest()) + { + return Err(FFORReceiverError::InvalidWitnessRegistration); + } + let mut runtime = self.ffor_activation.lock().unwrap(); + let requirement = &runtime.get(&key)?.requirement; + let barrier = self.ffor_persistence.lock().unwrap(); + if requirement != &context.requirement || !barrier.is_complete(requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let saved = runtime + .witness_attempts + .iter_mut() + .find(|saved| Arc::ptr_eq(&saved.handle.identity, &attempt.identity)) + .ok_or(FFORReceiverError::InvalidWitnessRegistration)?; + match saved.state { + AttemptState::Refused => Err(FFORReceiverError::InvalidWitnessRegistration), + AttemptState::Sent => Ok(true), + AttemptState::Staged => { + let sent = enqueue(provision).is_ok(); + if sent { + saved.state = AttemptState::Sent; + } + Ok(sent) + }, + } + } + + /// Retain a positive ACK from the current native witness connection and an exact sent attempt. + /// + /// Call only from authenticated custom-message handling, after the application has durably + /// retained its own correlated promise. This unsigned wire message is not portable evidence: + /// caller-created correlation objects cannot replace the actual native session and sent record. + /// The first native promise for each immutable manifest is retained permanently. Later valid + /// responses never replace it, even if their request ID or sufficient retention differs. + /// The returned manager requirement must complete before any later use. This is not readiness. + pub fn retain_ffor_receiver_witness_ack( + &self, connection: &FFORPeerConnection, acknowledgement: &Acknowledgement, + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(&connection.peer) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + self.require_current_ffor_connection(&peer, &connection.peer, Some(connection))?; + let mut recovery = self.ffor_recovery.lock().unwrap(); + let mut runtime = self.ffor_activation.lock().unwrap(); + let saved = runtime + .witness_attempts + .iter_mut() + .find(|saved| { + saved.handle.connection == *connection + && saved.handle.request_id == acknowledgement.request_id() + }) + .ok_or(FFORReceiverError::InvalidWitnessRegistration)?; + if saved.state != AttemptState::Sent { + return Err(FFORReceiverError::InvalidWitnessRegistration); + } + let retention_until = match acknowledgement.result() { + AcknowledgementResult::Accepted { witness, retention_until } + if *witness == connection.peer => + { + *retention_until + }, + AcknowledgementResult::Refused(_) => { + saved.state = AttemptState::Refused; + return Err(FFORReceiverError::InvalidWitnessRegistration); + }, + _ => return Err(FFORReceiverError::InvalidWitnessRegistration), + }; + let key = saved.handle.key; + let registration = + recovery.get_witnesses(&key).ok_or(FFORReceiverError::InvalidWitnessRegistration)?; + let witness = registration + .witnesses() + .iter() + .find(|w| w.witness_node_id() == connection.peer) + .ok_or(FFORReceiverError::InvalidWitnessRegistration)?; + if registration.context_digest() != saved.handle.context_digest + || witness.manifest_digest() != saved.handle.manifest_digest + || retention_until < witness.retention_until() + { + return Err(FFORReceiverError::InvalidWitnessRegistration); + } + let existing = + recovery.get_witness_acks(&key).ok_or(FFORReceiverError::InvalidWitnessRegistration)?; + if existing.acknowledgements().iter().any(|ack| ack.witness_node_id() == connection.peer) { + return Ok(runtime.get(&key)?.requirement.clone()); + } + let promise = FFORWitnessAcknowledgement { + witness: connection.peer, + manifest_digest: saved.handle.manifest_digest, + request_id: acknowledgement.request_id(), + retention_until, + }; + let permit = recovery + .prepare_witness_ack(&key, promise) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let requirement = self + .ffor_persistence + .lock() + .unwrap() + .request() + .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + permit.commit(); + runtime.record(key, requirement.clone(), false); + Ok(requirement) + } + + /// Inspect compact historical native ACK evidence, without current durability or readiness. + /// `None` means no native tracking was reserved; an empty set means no ACK was retained. + pub fn ffor_receiver_witness_acknowledgements( + &self, context: &FFORReceiverRecoveryContext, + ) -> Result, FFORReceiverError> { + let _guard = self.total_consistency_lock.read().unwrap(); + let recovery = self.ffor_recovery.lock().unwrap(); + let key = + FFORRecoveryKey { channel_id: context.channel_id(), epoch_id: context.epoch_id() }; + self.ffor_validate_ack_context_locked(&recovery, context, &key)?; + Ok(recovery.get_witness_acks(&key).cloned()) + } + + fn ffor_ack_registration_locked<'a>( + &self, recovery: &'a FFORRecoveryRegistry, context: &FFORReceiverRecoveryContext, + ) -> Result< + ( + FFORRecoveryKey, + &'a FFORReceiverWitnessRegistration, + &'a FFORReceiverWitnessAcknowledgements, + ), + FFORReceiverError, + > { + let key = + FFORRecoveryKey { channel_id: context.channel_id(), epoch_id: context.epoch_id() }; + self.ffor_validate_ack_context_locked(recovery, context, &key)?; + Ok(( + key, + recovery.get_witnesses(&key).ok_or(FFORReceiverError::InvalidWitnessRegistration)?, + recovery.get_witness_acks(&key).ok_or(FFORReceiverError::InvalidWitnessRegistration)?, + )) + } + + fn ffor_validate_ack_context_locked( + &self, recovery: &FFORRecoveryRegistry, context: &FFORReceiverRecoveryContext, + key: &FFORRecoveryKey, + ) -> Result<(), FFORReceiverError> { + recovery + .validate_identity(self.our_network_pubkey, self.chain_hash) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let setup = recovery.get(key).ok_or(FFORReceiverError::UnknownEpoch)?; + let current = recovery + .get_activation(key) + .ok_or(FFORReceiverError::NotRegistered)? + .receiver_context(setup) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + if context.context_digest() != current.context_digest() + || context.activation_ack_wire() != current.activation_ack_wire() + { + return Err(FFORReceiverError::InvalidWitnessRegistration); + } + Ok(()) + } +} diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index d8ac24c40dd..57039bbd63b 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -41,6 +41,10 @@ pub use receipt::{FFORWitnessMonitorSnapshot, FFORWitnessReceiptProgress}; mod provision; pub use provision::{FFORReceiverWitnessRegistration, FFORRegisteredWitness}; +mod witness_ack; +pub use witness_ack::{ + FFORReceiverWitnessAcknowledgements, FFORWitnessAcknowledgement, FFORWitnessProvisionAttempt, +}; /// Which channel participant offers every voucher HTLC in the book. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/lightning/src/ln/ffor/witness_ack.rs b/lightning/src/ln/ffor/witness_ack.rs new file mode 100644 index 00000000000..bf01c0c9331 --- /dev/null +++ b/lightning/src/ln/ffor/witness_ack.rs @@ -0,0 +1,76 @@ +//! Observations of native witness promises and opaque connection-scoped provisioning attempts. + +use alloc::vec::Vec; +use bitcoin::secp256k1::PublicKey; + +use super::FFORPeerConnection; +use crate::ln::ffor_recovery::FFORRecoveryKey; +use crate::sync::Arc; + +/// One native-correlated provisioning attempt. This is not an acknowledgement or readiness token. +/// +/// Only native staging constructs it, using the actual authenticated witness generation. Cloning +/// preserves the attempt. Disconnect, replacement and manager restore invalidate it. No request, +/// manifest or phase can be substituted by the application when releasing this attempt. +#[derive(Clone, Debug)] +pub struct FFORWitnessProvisionAttempt { + pub(crate) identity: Arc<()>, + pub(crate) key: FFORRecoveryKey, + pub(crate) context_digest: [u8; 32], + pub(crate) connection: FFORPeerConnection, + pub(crate) request_id: [u8; 16], + pub(crate) manifest_digest: [u8; 32], +} + +/// Retained unsigned witness promises, observed on authenticated native connections. +/// +/// An empty list means that native ACK tracking is reserved but no witness promise was retained. +/// This historical observation survives channel removal. It does not assert current durability, +/// an Active channel, invoice readiness or that a witness will honor its storage promise. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FFORReceiverWitnessAcknowledgements { + pub(crate) context_digest: [u8; 32], + pub(crate) acknowledgements: Vec, +} + +impl FFORReceiverWitnessAcknowledgements { + /// Immutable native context to which these promises belong. + pub fn context_digest(&self) -> [u8; 32] { + self.context_digest + } + /// First retained promise for each witness, in compressed-public-key order. + pub fn acknowledgements(&self) -> &[FFORWitnessAcknowledgement] { + &self.acknowledgements + } +} + +/// Compact evidence of one accepted ACK and its exact native manifest correlation. +/// +/// The wire ACK is unsigned. These fields describe an observation made by the authenticated +/// native handler, not a portable witness signature or permission to expose an invoice. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FFORWitnessAcknowledgement { + pub(crate) witness: PublicKey, + pub(crate) manifest_digest: [u8; 32], + pub(crate) request_id: [u8; 16], + pub(crate) retention_until: u32, +} + +impl FFORWitnessAcknowledgement { + /// Witness authenticated on the original native connection. + pub fn witness_node_id(&self) -> PublicKey { + self.witness + } + /// SHA256 of the exact registered signed manifest acknowledged on that connection. + pub fn manifest_digest(&self) -> [u8; 32] { + self.manifest_digest + } + /// Exact Provision request ID to which the original ACK replied. + pub fn request_id(&self) -> [u8; 16] { + self.request_id + } + /// Original accepted retention promise, at least the immutable manifest requirement. + pub fn retention_until(&self) -> u32 { + self.retention_until + } +} diff --git a/lightning/src/ln/ffor_recovery.rs b/lightning/src/ln/ffor_recovery.rs index 07ca36d535f..9f330d0e915 100644 --- a/lightning/src/ln/ffor_recovery.rs +++ b/lightning/src/ln/ffor_recovery.rs @@ -7,6 +7,7 @@ mod request; use request::PendingRequest; mod witness; +mod witness_ack; mod activation; pub(crate) use activation::{FFORReceiverActivation, FFORReceiverCloseRecord}; @@ -18,7 +19,8 @@ use bitcoin::secp256k1::PublicKey; use crate::io; use crate::ln::channel::{FFORReceiverFencePhase, FFORReceiverSetup}; use crate::ln::ffor::{ - FFORMonitorRecoveryIdentity, FFORReceiverAbortReason, FFORReceiverWitnessRegistration, + FFORMonitorRecoveryIdentity, FFORReceiverAbortReason, FFORReceiverWitnessAcknowledgements, + FFORReceiverWitnessRegistration, }; use crate::ln::msgs::DecodeError; use crate::ln::types::ChannelId; @@ -33,6 +35,7 @@ const CLOSE_VERSION: u8 = 3; const REQUEST_VERSION: u8 = 4; // Earlier readers must not forget protected application key and manifest ownership. const WITNESS_VERSION: u8 = 5; +const WITNESS_ACK_VERSION: u8 = 6; const MAX_RECORDS: usize = 64; const MAX_ENCODED_BYTES: usize = 8 * 1024 * 1024; // Two maximum wire messages, a 483-slot canonical book, and fixed admission fields fit here. @@ -67,6 +70,7 @@ struct StoredSetup { activation: Option, request: Option, witnesses: Option, + witness_acks: Option, } impl_writeable_tlv_based!(StoredSetup, { @@ -75,6 +79,7 @@ impl_writeable_tlv_based!(StoredSetup, { (4, activation, option), (6, request, option), (8, witnesses, option), + (10, witness_acks, option), }); struct Entry { @@ -113,6 +118,9 @@ impl Entry { .map_err(|_| FFORRecoveryError::InvalidRecord)?; witnesses.validate(&context).map_err(|_| FFORRecoveryError::InvalidRecord)?; } + if let Some(acks) = record.witness_acks.as_ref() { + acks.validate(record.witnesses.as_ref().ok_or(FFORRecoveryError::InvalidRecord)?)?; + } let header = authenticated.header(); let encoded_bytes = record.serialized_length(); let reserved_transition_bytes = Self::transition_reservation(&record); @@ -392,6 +400,7 @@ impl FFORRecoveryRegistry { activation: None, request: None, witnesses: None, + witness_acks: None, })?; if self.pending_requests.iter().any(|pending| { pending.key.channel_id == entry.key.channel_id @@ -432,6 +441,7 @@ impl FFORRecoveryRegistry { activation: Some(activation.clone()), request, witnesses, + witness_acks: self.get_witness_acks(&key).cloned(), })?; let index = self .entries @@ -480,6 +490,9 @@ impl FFORRecoveryRegistry { } fn version(&self) -> u8 { + if self.entries.iter().any(|entry| entry.record.witness_acks.is_some()) { + return WITNESS_ACK_VERSION; + } if self.entries.iter().any(|entry| entry.record.witnesses.is_some()) { return WITNESS_VERSION; } @@ -558,6 +571,7 @@ impl Readable for FFORRecoveryRegistry { && version != CLOSE_VERSION && version != REQUEST_VERSION && version != WITNESS_VERSION + && version != WITNESS_ACK_VERSION { return Err(DecodeError::UnknownRequiredFeature); } diff --git a/lightning/src/ln/ffor_recovery/activation/tests.rs b/lightning/src/ln/ffor_recovery/activation/tests.rs index dc910ddfdaf..0a762f4b982 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests.rs @@ -355,6 +355,7 @@ fn ffor_activation_archive_reserves_maximum_ack_through_competing_admission_and_ let extra = crate::ln::ffor_recovery::Entry::new(crate::ln::ffor_recovery::StoredSetup { request: None, witnesses: None, + witness_acks: None, canonical_book: extra_setup.validate_recovery().unwrap().canonical_book().to_vec(), setup: extra_setup, activation: Some(extra_activation), diff --git a/lightning/src/ln/ffor_recovery/activation/tests/witness.rs b/lightning/src/ln/ffor_recovery/activation/tests/witness.rs index 6581da9f3d3..fb6c3e6d716 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests/witness.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests/witness.rs @@ -1,6 +1,8 @@ use super::*; use crate::ln::ffor::{FFORReceiverRecoveryContext, FFORReceiverWitnessRegistration}; use crate::ln::ffor_recovery::{Entry, StoredSetup, WITNESS_VERSION}; + +mod ack; use bitcoin::secp256k1::{Message as SecpMessage, PublicKey, Secp256k1, SecretKey}; use lightning_ffor::witness::{ManifestParameters, SignedManifest, UnsignedManifest}; @@ -58,7 +60,7 @@ fn ffor_witness_archive_bounds_maximum_book_and_reserves_terminal_transitions() assert!(registry.encoded_bytes + registry.reserved_transition_bytes <= reserved); assert_eq!(registry.get_witnesses(&key(&setup)), Some(&metadata)); let bytes = registry.encode(); - assert_eq!(bytes[0], WITNESS_VERSION); + assert_eq!(bytes[0], crate::ln::ffor_recovery::WITNESS_ACK_VERSION); assert_eq!(bytes.len(), registry.encoded_bytes); let restored = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); assert_eq!(restored.get_witnesses(&key(&setup)), Some(&metadata)); @@ -78,6 +80,7 @@ fn ffor_witness_archive_reauthenticates_compact_metadata_and_required_fence() { activation: Some(phases[1].clone()), request: None, witnesses: Some(metadata.clone()), + witness_acks: None, }; for mutation in 0..9 { let mut damaged = record(); diff --git a/lightning/src/ln/ffor_recovery/activation/tests/witness/ack.rs b/lightning/src/ln/ffor_recovery/activation/tests/witness/ack.rs new file mode 100644 index 00000000000..31079ec8e62 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/tests/witness/ack.rs @@ -0,0 +1,150 @@ +use super::*; +use crate::ln::ffor::{FFORReceiverWitnessAcknowledgements, FFORWitnessAcknowledgement}; +use crate::ln::ffor_recovery::WITNESS_ACK_VERSION; + +fn promise(witness: &crate::ln::ffor::FFORRegisteredWitness, id: u8) -> FFORWitnessAcknowledgement { + FFORWitnessAcknowledgement { + witness: witness.witness_node_id(), + manifest_digest: witness.manifest_digest(), + request_id: [id; 16], + retention_until: witness.retention_until(), + } +} + +#[test] +fn ffor_witness_ack_archive_fixed_capacity_survives_maximum_book_and_terminal_growth() { + let (setup, phases) = close::phases(true); + let context = phases[1].receiver_context(&setup).unwrap(); + let metadata = + FFORReceiverWitnessRegistration::from_manifests(&context, &manifests(&context, 4)).unwrap(); + let mut registry = registered(&setup); + for phase in &phases[..2] { + registry.prepare_activation(&setup, phase).unwrap().commit(); + } + let key = key(&setup); + registry.prepare_witnesses(&key, metadata.clone()).unwrap().commit(); + let before = registry.encoded_bytes; + let reserved = registry.reserved_transition_bytes; + assert_eq!(registry.get_witness_acks(&key).unwrap().serialized_length(), 373); + for (index, witness) in metadata.witnesses().iter().enumerate() { + registry.prepare_witness_ack(&key, promise(witness, index as u8)).unwrap().commit(); + assert_eq!(registry.encoded_bytes, before); + assert_eq!(registry.reserved_transition_bytes, reserved); + let bytes = registry.encode(); + assert_eq!(bytes[0], WITNESS_ACK_VERSION); + registry = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); + } + let promises = registry.get_witness_acks(&key).unwrap().clone(); + for phase in &phases[2..] { + registry.prepare_activation(&setup, phase).unwrap().commit(); + assert_eq!(registry.get_witness_acks(&key), Some(&promises)); + } + assert_eq!(registry.reserved_transition_bytes, 0); + let encoded = registry.encode(); + assert_eq!(FFORRecoveryRegistry::read(&mut &encoded[..]).unwrap().encode(), encoded); +} + +#[test] +fn ffor_witness_ack_archive_legacy_upgrade_checks_capacity_before_tracking() { + let (setup, phases) = close::phases(false); + let context = phases[1].receiver_context(&setup).unwrap(); + let metadata = + FFORReceiverWitnessRegistration::from_manifests(&context, &manifests(&context, 4)).unwrap(); + let legacy = StoredSetup { + setup: setup.clone(), + canonical_book: context.setup().canonical_book().to_vec(), + activation: Some(phases[1].clone()), + request: None, + witnesses: Some(metadata.clone()), + witness_acks: None, + }; + let mut bytes = vec![WITNESS_VERSION]; + 1u16.write(&mut bytes).unwrap(); + (legacy.serialized_length() as u32).write(&mut bytes).unwrap(); + legacy.write(&mut bytes).unwrap(); + 0u16.write(&mut bytes).unwrap(); + let mut registry = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); + let key = key(&setup); + assert!(registry.get_witness_acks(&key).is_none()); + let actual_bytes = registry.encoded_bytes; + registry.encoded_bytes = MAX_ENCODED_BYTES - registry.reserved_transition_bytes; + assert!(matches!( + registry.prepare_witnesses(&key, metadata.clone()), + Err(FFORRecoveryError::CapacityExceeded) + )); + assert!(registry.get_witness_acks(&key).is_none()); + assert_eq!(registry.encode(), bytes); + registry.encoded_bytes = actual_bytes; + registry.prepare_witnesses(&key, metadata.clone()).unwrap().commit(); + assert_eq!(registry.encode()[0], WITNESS_ACK_VERSION); + // All future ACK bytes are already charged, including all TLV prefix changes. + registry.encoded_bytes = MAX_ENCODED_BYTES - registry.reserved_transition_bytes; + for witness in metadata.witnesses() { + registry.prepare_witness_ack(&key, promise(witness, 1)).unwrap().commit(); + assert_eq!(registry.encoded_bytes + registry.reserved_transition_bytes, MAX_ENCODED_BYTES); + } +} + +#[test] +fn ffor_witness_ack_archive_rejects_corruption_and_old_reader_downgrade() { + let (setup, phases) = close::phases(false); + let context = phases[1].receiver_context(&setup).unwrap(); + let metadata = + FFORReceiverWitnessRegistration::from_manifests(&context, &manifests(&context, 2)).unwrap(); + let acks = FFORReceiverWitnessAcknowledgements { + context_digest: context.context_digest(), + acknowledgements: metadata.witnesses().iter().map(|witness| promise(witness, 1)).collect(), + }; + let record = || StoredSetup { + setup: setup.clone(), + canonical_book: context.setup().canonical_book().to_vec(), + activation: Some(phases[1].clone()), + request: None, + witnesses: Some(metadata.clone()), + witness_acks: Some(acks.clone()), + }; + for mutation in 0..7 { + let mut damaged = record(); + let promises = damaged.witness_acks.as_mut().unwrap(); + match mutation { + 0 => promises.context_digest[0] ^= 1, + 1 => promises.acknowledgements[0].manifest_digest[0] ^= 1, + 2 => promises.acknowledgements[0].retention_until -= 1, + 3 => promises.acknowledgements.swap(0, 1), + 4 => promises.acknowledgements[1] = promises.acknowledgements[0].clone(), + 5 => damaged.witnesses = None, + _ => damaged.activation = Some(phases[0].clone()), + } + assert!(Entry::new(damaged).is_err(), "accepted ACK corruption {}", mutation); + } + let encoded = acks.encode(); + assert_eq!(encoded.len(), 373); + let mut padding = encoded.clone(); + *padding.last_mut().unwrap() = 1; + assert!(FFORReceiverWitnessAcknowledgements::read(&mut &padding[..]).is_err()); + let mut oversized = encoded.clone(); + oversized[32] = 5; + assert!(FFORReceiverWitnessAcknowledgements::read(&mut &oversized[..]).is_err()); + assert!(FFORReceiverWitnessAcknowledgements::read(&mut &encoded[..372]).is_err()); + struct PreviousRecord { + setup: FFORReceiverSetup, + canonical_book: Vec, + activation: Option, + request: Option, + witnesses: Option, + } + impl_writeable_tlv_based!(PreviousRecord, { + (0, setup, required), (2, canonical_book, required_vec), (4, activation, option), + (6, request, option), (8, witnesses, option), + }); + assert!(matches!( + PreviousRecord::read(&mut &record().encode()[..]), + Err(DecodeError::UnknownRequiredFeature) + )); + let mut framed = vec![WITNESS_VERSION]; + 1u16.write(&mut framed).unwrap(); + (record().serialized_length() as u32).write(&mut framed).unwrap(); + record().write(&mut framed).unwrap(); + 0u16.write(&mut framed).unwrap(); + assert!(FFORRecoveryRegistry::read(&mut &framed[..]).is_err()); +} diff --git a/lightning/src/ln/ffor_recovery/request.rs b/lightning/src/ln/ffor_recovery/request.rs index 96feb4b3e1d..2d662071659 100644 --- a/lightning/src/ln/ffor_recovery/request.rs +++ b/lightning/src/ln/ffor_recovery/request.rs @@ -153,6 +153,7 @@ impl FFORRecoveryRegistry { activation: None, request: Some(request.clone()), witnesses: None, + witness_acks: None, })?; // Both forms were charged the full record allowance before Init. Promotion cannot increase // total used plus reserved bytes, and the borrowed permit prevents a conflicting insertion. diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs index 429f5fbe187..788f789a162 100644 --- a/lightning/src/ln/ffor_recovery/tests.rs +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -130,6 +130,7 @@ fn stored(record: FFORReceiverSetup) -> StoredSetup { activation: None, request: None, witnesses: None, + witness_acks: None, } } @@ -299,7 +300,7 @@ fn ffor_recovery_reader_refuses_future_schema_missing_fields_and_truncation() { assert!(decode(&framed[..length]).is_err(), "accepted prefix {}", length); } let mut future = framed.clone(); - future[0] = WITNESS_VERSION + 1; + future[0] = WITNESS_ACK_VERSION + 1; assert!(matches!(decode(&future), Err(DecodeError::UnknownRequiredFeature))); // Empty TLV records omit both mandatory fields. assert!(decode(&frame(&[vec![0]])).is_err()); diff --git a/lightning/src/ln/ffor_recovery/witness.rs b/lightning/src/ln/ffor_recovery/witness.rs index 0b6cde17c32..236d2458332 100644 --- a/lightning/src/ln/ffor_recovery/witness.rs +++ b/lightning/src/ln/ffor_recovery/witness.rs @@ -180,6 +180,13 @@ impl FFORRecoveryRegistry { canonical_book: previous.record.canonical_book.clone(), activation: previous.record.activation.clone(), request: previous.record.request.clone(), + witness_acks: Some( + previous + .record + .witness_acks + .clone() + .unwrap_or_else(|| FFORReceiverWitnessAcknowledgements::empty(&witnesses)), + ), witnesses: Some(witnesses), })?; let header_growth = if self.version() < REQUEST_VERSION { 2 } else { 0 }; diff --git a/lightning/src/ln/ffor_recovery/witness_ack.rs b/lightning/src/ln/ffor_recovery/witness_ack.rs new file mode 100644 index 00000000000..8c6b29723c3 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/witness_ack.rs @@ -0,0 +1,122 @@ +//! Compact persisted witness promises and reserved capacity for all selected witnesses. + +use super::*; +use crate::ln::ffor::{FFORReceiverWitnessAcknowledgements, FFORWitnessAcknowledgement}; + +// Fixed-width slots charge the full maximum at registration. Filling an ACK cannot grow an +// enclosing TLV prefix or consume storage which another channel has since reserved. +const ACK_SLOT_BYTES: usize = 33 + 32 + 16 + 4; + +impl Writeable for FFORReceiverWitnessAcknowledgements { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + self.context_digest.write(writer)?; + (self.acknowledgements.len() as u8).write(writer)?; + for ack in &self.acknowledgements { + ack.witness.write(writer)?; + ack.manifest_digest.write(writer)?; + ack.request_id.write(writer)?; + ack.retention_until.write(writer)?; + } + for _ in self.acknowledgements.len()..4 { + writer.write_all(&[0; ACK_SLOT_BYTES])?; + } + Ok(()) + } +} + +impl Readable for FFORReceiverWitnessAcknowledgements { + fn read(reader: &mut R) -> Result { + let context_digest = <[u8; 32]>::read(reader)?; + let count = u8::read(reader)? as usize; + if count > 4 { + return Err(DecodeError::InvalidValue); + } + let mut acknowledgements = Vec::with_capacity(count); + for _ in 0..count { + acknowledgements.push(FFORWitnessAcknowledgement { + witness: PublicKey::read(reader)?, + manifest_digest: <[u8; 32]>::read(reader)?, + request_id: <[u8; 16]>::read(reader)?, + retention_until: u32::read(reader)?, + }); + } + for _ in count..4 { + let mut unused = [0; ACK_SLOT_BYTES]; + reader.read_exact(&mut unused)?; + if unused != [0; ACK_SLOT_BYTES] { + return Err(DecodeError::InvalidValue); + } + } + Ok(Self { context_digest, acknowledgements }) + } +} + +impl FFORReceiverWitnessAcknowledgements { + pub(super) fn empty(registration: &FFORReceiverWitnessRegistration) -> Self { + Self { context_digest: registration.context_digest(), acknowledgements: Vec::new() } + } + + pub(super) fn validate( + &self, registration: &FFORReceiverWitnessRegistration, + ) -> Result<(), FFORRecoveryError> { + if self.context_digest != registration.context_digest() + || self.acknowledgements.len() > registration.witnesses().len() + { + return Err(FFORRecoveryError::InvalidRecord); + } + for (index, ack) in self.acknowledgements.iter().enumerate() { + let witness = registration + .witnesses() + .iter() + .find(|w| w.witness_node_id() == ack.witness) + .ok_or(FFORRecoveryError::InvalidRecord)?; + if witness.manifest_digest() != ack.manifest_digest + || ack.retention_until < witness.retention_until() + || (index > 0 + && self.acknowledgements[index - 1].witness.serialize() + >= ack.witness.serialize()) + { + return Err(FFORRecoveryError::InvalidRecord); + } + } + Ok(()) + } +} + +impl FFORRecoveryRegistry { + pub(crate) fn get_witness_acks( + &self, key: &FFORRecoveryKey, + ) -> Option<&FFORReceiverWitnessAcknowledgements> { + self.entries + .iter() + .find(|entry| entry.key == *key) + .and_then(|entry| entry.record.witness_acks.as_ref()) + } + + pub(crate) fn prepare_witness_ack( + &mut self, key: &FFORRecoveryKey, ack: FFORWitnessAcknowledgement, + ) -> Result, FFORRecoveryError> { + let index = self + .entries + .iter() + .position(|entry| entry.key == *key) + .ok_or(FFORRecoveryError::ConflictingRecord)?; + let previous = &self.entries[index]; + let mut acks = + previous.record.witness_acks.clone().ok_or(FFORRecoveryError::ConflictingRecord)?; + if acks.acknowledgements.iter().any(|existing| existing.witness == ack.witness) { + return Err(FFORRecoveryError::ConflictingRecord); + } + acks.acknowledgements.push(ack); + acks.acknowledgements.sort_unstable_by_key(|ack| ack.witness.serialize()); + let entry = Entry::new(StoredSetup { + setup: previous.record.setup.clone(), + canonical_book: previous.record.canonical_book.clone(), + activation: previous.record.activation.clone(), + request: previous.record.request.clone(), + witnesses: previous.record.witnesses.clone(), + witness_acks: Some(acks), + })?; + self.prepare_replacement(index, entry, 0) + } +} From a00512b79f33e2db9b1bea691ab3637122c41564 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Mon, 21 Sep 2026 09:09:54 -0400 Subject: [PATCH 20/22] feat(ffor): retain and gate exact receiver invoices --- FFOR.md | 70 +- lightning/src/chain/chainmonitor.rs | 17 + lightning/src/chain/channelmonitor.rs | 43 ++ lightning/src/chain/mod.rs | 15 + .../src/ln/channelmanager/ffor_activation.rs | 1 + .../channelmanager/ffor_activation/invoice.rs | 368 +++++++++++ .../ffor_activation/invoice/fixture_tests.rs | 174 +++++ .../ffor_activation/invoice/tests.rs | 615 ++++++++++++++++++ .../channelmanager/ffor_activation/witness.rs | 20 +- .../ffor_activation/witness_ack.rs | 2 +- lightning/src/ln/ffor.rs | 8 + lightning/src/ln/ffor/invoice.rs | 159 +++++ lightning/src/ln/ffor_recovery.rs | 26 + .../src/ln/ffor_recovery/activation/tests.rs | 1 + .../ffor_recovery/activation/tests/witness.rs | 1 + .../activation/tests/witness/ack.rs | 2 + lightning/src/ln/ffor_recovery/invoice.rs | 329 ++++++++++ .../src/ln/ffor_recovery/invoice/tests.rs | 79 +++ lightning/src/ln/ffor_recovery/request.rs | 1 + lightning/src/ln/ffor_recovery/tests.rs | 3 +- lightning/src/ln/ffor_recovery/witness.rs | 1 + lightning/src/ln/ffor_recovery/witness_ack.rs | 1 + lightning/src/util/test_utils.rs | 15 + 23 files changed, 1929 insertions(+), 22 deletions(-) create mode 100644 lightning/src/ln/channelmanager/ffor_activation/invoice.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/invoice/fixture_tests.rs create mode 100644 lightning/src/ln/channelmanager/ffor_activation/invoice/tests.rs create mode 100644 lightning/src/ln/ffor/invoice.rs create mode 100644 lightning/src/ln/ffor_recovery/invoice.rs create mode 100644 lightning/src/ln/ffor_recovery/invoice/tests.rs diff --git a/FFOR.md b/FFOR.md index 2eb1d8565fd..da3b4a8e5c3 100644 --- a/FFOR.md +++ b/FFOR.md @@ -5,8 +5,10 @@ parking and verification of both commitment views for FFOR Variant D. Native man transitions compose activation, reconnect, pre-active abort and cooperative voucher drain with ordered storage. An experimental public receiver facade owns pre-init admission, synchronous peer input, -activation and cooperative close advancement. Production transport orchestration and -invoice readiness remain unfinished. +activation and cooperative close advancement. One immutable signed receiver invoice can +be assigned per supported epoch and released only through a guarded application +publication contract. Production transport orchestration, the application publication +runtime and payment outcome credit remain unfinished. `prepare_ffor_receiver` checks the current authenticated native peer generation, derives a fresh protocol epoch and signs the exact Init. Before any bytes leave, @@ -338,9 +340,9 @@ checks do not claim to detect every arbitrary alteration of local storage. Similarly, an archive without its original live channel cannot independently prove the historical funding context against arbitrary local storage alteration. -No feature bit, production custom-message transport, invoice readiness or background -deadline service is implemented here. The facade rechecks deadlines before new Init, -Accept, STFU and activation work; its caller must continue advancing and may cancel +No feature bit, production custom-message transport, application invoice publication +runtime or background deadline service is implemented here. The facade rechecks deadlines +before new Init, Accept, STFU and activation work; its caller must continue advancing and may cancel a stalled pre-activation setup. Cancellation of an owned STFU handshake requests a native disconnect. Automatic voucher failures wait for controlled release after the abort revision is durable. @@ -353,10 +355,47 @@ identity. External signers refuse this operation by default. Signing requests ch only the allowed envelope type and size; protocol and transition validation remain required before requesting a signature. +## Invoice assignment and publication + +The supported issuer uses a one-slot, non-hash-chained Variant D book. It derives amount, +payment hash, receiver identity, settlement fees, inbound alias and CLTV from native +ownership. The signed Init witness restriction must exactly match the registered and +durably acknowledged witness set. + +The invoice has one W -> S -> R route hint and no MPP support. Native verifies the +public W-S announcement and directional update, signatures, distinct identities, canonical +endpoint ordering, enabled state, amount bounds, fees and freshness. These signatures +identify route terms; they do not prove current channel funding or liquidity. Private W-S +channels without authenticated route evidence are not supported. BOLT11 hints remain +advisory, with the signed witness restriction providing the honest settlement peer's +admission guard. + +`prepare_ffor_receiver_invoice` signs outside locks. Native then rechecks the current +phase, frozen pair, deadline, route binding and completed persistence before retaining the +exact signed bytes. Requested expiry is capped using eight minutes per remaining block +before admission closes, less the explicit safety margin. The actual watched monitor is +locked through retention or final bounded publication, excluding pending off-chain +persistence, known preimages, funding spends, pending funding changes and local commitment +signing. The higher of manager and monitor height is used so delayed notifications cannot +extend admission. New issuance requires the std clock; no_std supports historical +decoding only. + +`ffor_receiver_invoice_for_storage` returns exact bytes only after the latest native write +completes. It supports historical recovery after expiry or close and does not grant +display permission. The application must first durably confirm these bytes, the matching +Pending payment and its protected confirmation marker. `release_ffor_receiver_invoice` +then rechecks native and monitor state through `Watch::validate_and_publish_ffor_invoice` +and a callback that performs only bounded in-memory publication. Retry, failed publication +and restart cannot replace the assigned invoice. Custom Watch implementations default to +refusing this operation until they implement the actual-monitor boundary. Invoice +assignment adds required field 12 and archive schema 7, bounding the signed string to +4096 bytes and its record to 8192 bytes. The assignment is permanent after failed +publication, expiry or restart, and it is not a payment success signal. + ## Validation -The current focused native suite passes 156 tests. Native no-default-features and -documentation builds with broken intra-doc links denied also pass. +The current focused native suite passes 169 tests, including 13 invoice tests. Native +no-default-features and documentation builds with broken intra-doc links denied also pass. The FFOR tests exercise real two-node commitment rounds, both funding directions, asymmetric dust limits and contest delays, signature corruption, stale monitors, @@ -463,7 +502,17 @@ exact retries, independent first promises, restoration and archive-only history. also check offline settlement-peer operation, current deadlines, fixed storage through all terminal phases with a maximum book, legacy quota refusal and malformed archives. A previous field-layout reader rejects the new required ACK field; schema downgrade is -also refused. These checks do not exercise a production witness or invoice issuance. +also refused. These checks do not exercise a production witness. + +Thirteen invoice tests cover every durable witness acknowledgement, exact retries, failed +and stale persistence, restored handles, wrong signers, signed route loops, missing or +additional witness permissions, archive corruption and capacity, monitor height ahead of +manager, known preimages without a counter change, funding spend before manager +notification, and actual monitor exclusion through publication. A genuine public-driver +fixture covers setup through witness registration and invoice assignment; it exposed a +runtime/height lock inversion in witness registration and release, which now use a +consistent registry, runtime, height and persistence order. An opt-in exporter writes that +fixture only when `FFOR_NODE_INVOICE_FIXTURE_DIR` is set. Seven receipt-import integration tests exercise both funders, delayed monitor persistence, idempotent retries, crash recovery from a failed write using the prior durable monitor, @@ -486,6 +535,7 @@ deadline before claim safety ends. Production transport must bind the receiver facade to actual authenticated connections and deliver acknowledgement retries in the required order. Durable witness -mailbox recovery, receipt-import orchestration, deadline enforcement and invoice eligibility remain -separate required boundaries. None can be inferred from durable setup, activation or +mailbox recovery, receipt-import orchestration, deadline enforcement, the application +invoice publication runtime and authoritative payment outcomes remain separate required +boundaries. None can be inferred from durable setup, activation or a successful private protocol test. diff --git a/lightning/src/chain/chainmonitor.rs b/lightning/src/chain/chainmonitor.rs index 7c96d918e34..9ffcdfee93a 100644 --- a/lightning/src/chain/chainmonitor.rs +++ b/lightning/src/chain/chainmonitor.rs @@ -1402,6 +1402,23 @@ where Ok(persist_res) } + fn validate_and_publish_ffor_invoice( + &self, check: &crate::ln::ffor::FFORInvoiceMonitorCheck, + publish: &mut dyn FnMut() -> Result<(), ()>, + ) -> Result { + let monitors = self.monitors.read().unwrap(); + let monitor = monitors + .get(&check.channel_id()) + .ok_or(crate::ln::ffor::FFORReceiverError::InvalidInvoice)?; + let pending = monitor.pending_monitor_updates.lock().unwrap(); + if !pending.is_empty() { + return Err(crate::ln::ffor::FFORReceiverError::ChannelState( + crate::ln::ffor::FFORCommitmentError::PendingUpdates, + )); + } + monitor.monitor.validate_and_publish_ffor_invoice(check, publish) + } + fn update_channel( &self, channel_id: ChannelId, update: &ChannelMonitorUpdate, ) -> ChannelMonitorUpdateStatus { diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 7c1de4fafcb..aca92803ae6 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -2145,6 +2145,49 @@ impl ChannelMonitor { }) } + /// Validate publication while holding this monitor's state lock. + /// + /// Only the actual owning watcher may use this boundary. It must also exclude pending monitor + /// persistence and retain ownership of this exact monitor throughout the bounded callback. + /// The callback must perform no I/O or native reentry. Snapshot comparisons alone cannot replace + /// this lock: chain spends and locally learned preimages need not change the update counter. + pub fn validate_and_publish_ffor_invoice( + &self, check: &crate::ln::ffor::FFORInvoiceMonitorCheck, + publish: &mut dyn FnMut() -> Result<(), ()>, + ) -> Result { + use crate::ln::channel::INITIAL_COMMITMENT_NUMBER; + use crate::ln::ffor::FFORReceiverError; + let inner = self.inner.lock().unwrap(); + let holder = inner.funding.current_holder_commitment_tx.trust(); + if inner.channel_id() != check.channel_id + || inner.get_funding_txo() != check.funding_txo + || inner.counterparty_node_id != check.settlement + || inner.latest_update_id != check.update_id + || inner.funding_spend_seen + || inner.lockdown_from_offchain + || inner.holder_tx_signed + || !inner.pending_funding.is_empty() + || inner.payment_preimages.contains_key(&check.payment_hash) + || holder.txid() != check.commitments.holder.txid + || inner.funding.current_counterparty_commitment_txid + != Some(check.commitments.counterparty.txid) + || INITIAL_COMMITMENT_NUMBER.checked_sub(inner.current_holder_commitment_number) + != Some(check.commitments.holder.number) + || INITIAL_COMMITMENT_NUMBER.checked_sub(inner.current_counterparty_commitment_number) + != Some(check.commitments.counterparty.number) + || !holder.nondust_htlcs().iter().any(|htlc| { + !htlc.offered + && htlc.payment_hash == check.payment_hash + && htlc.amount_msat == check.amount_msat + && htlc.cltv_expiry == check.voucher_expiry + && htlc.transaction_output_index.is_some() + }) { + return Err(FFORReceiverError::InvalidInvoice); + } + check.validate_time(inner.best_block.height)?; + Ok(publish().is_ok()) + } + /// Observe one authenticated witness preimage in this monitor, including after force-close. /// /// Drop every monitor guard before passing the result to diff --git a/lightning/src/chain/mod.rs b/lightning/src/chain/mod.rs index b4cc6a302ae..101eba01949 100644 --- a/lightning/src/chain/mod.rs +++ b/lightning/src/chain/mod.rs @@ -332,6 +332,21 @@ pub trait Watch { &self, channel_id: ChannelId, update: &ChannelMonitorUpdate, ) -> ChannelMonitorUpdateStatus; + /// Atomically validate the actual watched monitor for an FFOR invoice action. + /// + /// Implementations must exclude monitor mutation and outstanding off-chain writes throughout + /// validation and the callback, using the original watched monitor, never a clone. The callback + /// is invoked at most once and performs only a bounded native assignment or publication of an + /// already durable invoice: no I/O, monitor, manager or storage reentry. The default refuses, + /// preserving compatibility for custom watchers which have not implemented this stronger + /// boundary. + fn validate_and_publish_ffor_invoice( + &self, _check: &crate::ln::ffor::FFORInvoiceMonitorCheck, + _publish: &mut dyn FnMut() -> Result<(), ()>, + ) -> Result { + Err(crate::ln::ffor::FFORReceiverError::InvalidInvoice) + } + /// Returns any monitor events since the last call. Subsequent calls must only return new /// events. /// diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs index a7f80dceb75..a3e9138f4f0 100644 --- a/lightning/src/ln/channelmanager/ffor_activation.rs +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -12,6 +12,7 @@ use lightning_ffor::wire::{Activate, Message as FFORMessage, Payload}; mod close; mod context; mod driver; +mod invoice; mod receipt; mod reestablish; mod witness; diff --git a/lightning/src/ln/channelmanager/ffor_activation/invoice.rs b/lightning/src/ln/channelmanager/ffor_activation/invoice.rs new file mode 100644 index 00000000000..a09ca8b1501 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/invoice.rs @@ -0,0 +1,368 @@ +//! One immutable signed invoice per single-slot epoch, with actual-monitor publication exclusion. + +use super::*; +use crate::ln::ffor::invoice::invoice_time; +use crate::ln::ffor::{ + FFORInvoiceIntent, FFORInvoiceMonitorCheck, FFORInvoicePreparation, + FFORReceiverRecoveryContext, FFORStoredInvoice, FFORWitnessRouteEvidence, +}; +use crate::ln::ffor_recovery::invoice::{ + acknowledgements_digest, terms, validate_route, FFORInvoiceRecord, +}; +use lightning_invoice::Bolt11Invoice; + +struct InvoiceCapture { + record: FFORInvoiceRecord, + requirement: FFORPersistenceRequirement, +} + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Retain one exact invoice for a one-slot, non-hash-chained Variant D epoch. + /// + /// The signed Init witness restriction must exactly name the registered witnesses, and each + /// must already have a native durable acknowledgement. The public + /// W-to-S announcement and update are verified, and the invoice contains exactly W -> S -> R. + /// Private witness channels without authenticated route evidence are not supported. Amount, + /// payment hash, settlement fees and the final channel alias are derived from native ownership. + /// Signing occurs outside native locks, followed by complete revalidation. No invoice bytes are + /// returned here. A successful reservation is permanent and can never produce a replacement. + /// New issuance requires the std clock; no_std can restore and inspect historical records. + pub fn prepare_ffor_receiver_invoice( + &self, context: &FFORReceiverRecoveryContext, intent: &FFORInvoiceIntent, + route: &FFORWitnessRouteEvidence, + ) -> Result { + let capture = match self.capture_ffor_invoice(context, intent, route)? { + Ok(existing) => return Ok(existing), + Err(capture) => capture, + }; + let mut record = capture.record; + let registration = self + .ffor_receiver_witness_registration(context)? + .ok_or(FFORReceiverError::InvalidInvoice)?; + let raw = record + .unsigned( + context, + ®istration, + invoice_time()?, + PaymentSecret(self.entropy_source.get_secure_random_bytes()), + ) + .map_err(|_| FFORReceiverError::InvalidInvoice)?; + let signature = self + .node_signer + .sign_invoice(&raw, Recipient::Node) + .map_err(|_| FFORReceiverError::SignerUnavailable)?; + let signed = raw + .sign::<_, ()>(|_| Ok(signature)) + .map_err(|_| FFORReceiverError::SignerUnavailable)?; + record.invoice = Bolt11Invoice::from_signed(signed) + .map_err(|_| FFORReceiverError::InvalidInvoice)? + .to_string(); + self.commit_ffor_invoice(context, record, capture.requirement) + } + + fn capture_ffor_invoice( + &self, context: &FFORReceiverRecoveryContext, intent: &FFORInvoiceIntent, + route: &FFORWitnessRouteEvidence, + ) -> Result, FFORReceiverError> { + let _guard = self.total_consistency_lock.read().unwrap(); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(&context.settlement_node_id()) + .ok_or(FFORReceiverError::InvalidInvoice)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(&context.channel_id()) + .and_then(Channel::as_funded) + .ok_or(FFORReceiverError::InvalidInvoice)?; + let recovery = self.ffor_recovery.lock().unwrap(); + let key = + FFORRecoveryKey { channel_id: context.channel_id(), epoch_id: context.epoch_id() }; + let current = self.ffor_active_state_locked(channel, &recovery, &key)?; + let runtime = self.ffor_activation.lock().unwrap(); + let height = self.best_block.read().unwrap(); + Self::validate_ffor_witness_context(context, ¤t, height.height)?; + let registration = recovery.get_witnesses(&key).ok_or(FFORReceiverError::InvalidInvoice)?; + let acks = recovery.get_witness_acks(&key).ok_or(FFORReceiverError::InvalidInvoice)?; + let acknowledgement_digest = acknowledgements_digest(registration, acks) + .map_err(|_| FFORReceiverError::InvalidInvoice)?; + validate_route(context, registration, route, invoice_time()?) + .map_err(|_| FFORReceiverError::InvalidInvoice)?; + let requirement = &runtime.get(&key)?.requirement; + if let Some(existing) = recovery.get_invoice(&key) { + if !existing.matches_intent(intent, route) { + return Err(FFORReceiverError::AlreadyRegistered); + } + return Ok(Ok(FFORInvoicePreparation { + invoice_digest: existing.digest(), + requirement: requirement.clone(), + })); + } + if !self.ffor_persistence.lock().unwrap().is_complete(requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let (settlement_scid, settlement_cltv) = Self::ffor_invoice_channel_binding(channel)?; + Ok(Err(InvoiceCapture { + record: FFORInvoiceRecord { + context_digest: context.context_digest(), + acknowledgement_digest, + created_height: height.height, + settlement_scid, + settlement_cltv, + intent: intent.clone(), + route: route.clone(), + invoice: String::new(), + }, + requirement: requirement.clone(), + })) + } + + fn ffor_invoice_channel_binding( + channel: &FundedChannel, + ) -> Result<(u64, u16), FFORReceiverError> { + let scid = channel.get_inbound_scid().ok_or(FFORReceiverError::InvalidInvoice)?; + let forwarding = channel + .context + .counterparty_forwarding_info() + .ok_or(FFORReceiverError::InvalidInvoice)?; + if scid == 0 || forwarding.cltv_expiry_delta == 0 { + return Err(FFORReceiverError::InvalidInvoice); + } + Ok((scid, forwarding.cltv_expiry_delta)) + } + + fn ffor_invoice_monitor_check( + context: &FFORReceiverRecoveryContext, channel: &FundedChannel, + record: &FFORInvoiceRecord, height: u32, + ) -> Result { + let init = terms(context).map_err(|_| FFORReceiverError::InvalidInvoice)?; + let voucher = &context.setup().vouchers()[0]; + Ok(FFORInvoiceMonitorCheck { + channel_id: context.channel_id(), + funding_txo: context.funding_txo(), + settlement: context.settlement_node_id(), + update_id: channel.context.get_latest_monitor_update_id(), + commitments: context.commitments(), + payment_hash: PaymentHash(voucher.payment_hash), + amount_msat: voucher.amount_msat, + voucher_expiry: voucher.expiry, + deadline: init.settlement_deadline, + manager_height: height, + invoice_expires_at: record + .expires_at() + .map_err(|_| FFORReceiverError::InvalidInvoice)?, + safety_margin_seconds: record.intent.safety_margin_seconds, + }) + } + + fn commit_ffor_invoice( + &self, context: &FFORReceiverRecoveryContext, record: FFORInvoiceRecord, + captured_requirement: FFORPersistenceRequirement, + ) -> Result { + let _guard = PersistenceNotifierGuard::notify_on_drop(self); + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(&context.settlement_node_id()) + .ok_or(FFORReceiverError::InvalidInvoice)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(&context.channel_id()) + .and_then(Channel::as_funded) + .ok_or(FFORReceiverError::InvalidInvoice)?; + if channel.context.is_monitor_or_signer_pending_channel_update() + || peer + .in_flight_monitor_updates + .get(&context.channel_id()) + .map_or(false, |(_, updates)| !updates.is_empty()) + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let key = + FFORRecoveryKey { channel_id: context.channel_id(), epoch_id: context.epoch_id() }; + let mut recovery = self.ffor_recovery.lock().unwrap(); + let current = self.ffor_active_state_locked(channel, &recovery, &key)?; + let mut runtime = self.ffor_activation.lock().unwrap(); + let height = self.best_block.read().unwrap(); + Self::validate_ffor_witness_context(context, ¤t, height.height)?; + if Self::ffor_invoice_channel_binding(channel)? + != (record.settlement_scid, record.settlement_cltv) + { + return Err(FFORReceiverError::InvalidInvoice); + } + let registration = recovery.get_witnesses(&key).ok_or(FFORReceiverError::InvalidInvoice)?; + let acks = recovery.get_witness_acks(&key).ok_or(FFORReceiverError::InvalidInvoice)?; + record + .validate(¤t, registration, acks) + .map_err(|_| FFORReceiverError::InvalidInvoice)?; + validate_route(¤t, registration, &record.route, invoice_time()?) + .map_err(|_| FFORReceiverError::InvalidInvoice)?; + if let Some(existing) = recovery.get_invoice(&key) { + if !existing.matches_intent(&record.intent, &record.route) { + return Err(FFORReceiverError::AlreadyRegistered); + } + return Ok(FFORInvoicePreparation { + invoice_digest: existing.digest(), + requirement: runtime.get(&key)?.requirement.clone(), + }); + } + let mut barrier = self.ffor_persistence.lock().unwrap(); + if runtime.get(&key)?.requirement != captured_requirement + || !barrier.is_complete(&captured_requirement) + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let check = Self::ffor_invoice_monitor_check(¤t, channel, &record, height.height)?; + let digest = record.digest(); + let mut upgrade = Some( + recovery + .prepare_invoice(&key, record) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?, + ); + let mut result = None; + self.chain_monitor.validate_and_publish_ffor_invoice(&check, &mut || { + let permit = upgrade.take().ok_or(())?; + let requirement = match barrier.request() { + Ok(requirement) => requirement, + Err(_) => { + result = Some(Err(FFORReceiverError::PersistenceUnavailable)); + return Err(()); + }, + }; + permit.commit(); + runtime.record(key, requirement.clone(), false); + result = Some(Ok(FFORInvoicePreparation { invoice_digest: digest, requirement })); + Ok(()) + })?; + result.unwrap_or(Err(FFORReceiverError::InvalidInvoice)) + } + + /// Retrieve the exact persisted invoice only for confirmed application storage or historical + /// recovery. + /// + /// The epoch's current manager requirement must be complete, including a fresh barrier after + /// restore. This historical getter can succeed after close or expiry; it never permits exposure. + pub fn ffor_receiver_invoice_for_storage( + &self, context: &FFORReceiverRecoveryContext, + ) -> Result, FFORReceiverError> { + let _guard = self.total_consistency_lock.read().unwrap(); + let recovery = self.ffor_recovery.lock().unwrap(); + let key = + FFORRecoveryKey { channel_id: context.channel_id(), epoch_id: context.epoch_id() }; + let current = self.ffor_recovery_context_from_registry(&recovery, &key)?; + if current.context_digest() != context.context_digest() { + return Err(FFORReceiverError::InvalidInvoice); + } + let record = match recovery.get_invoice(&key) { + Some(record) => record, + None => return Ok(None), + }; + let runtime = self.ffor_activation.lock().unwrap(); + let requirement = &runtime.get(&key)?.requirement; + if !self.ffor_persistence.lock().unwrap().is_complete(requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + Ok(Some(FFORStoredInvoice { + context: current, + invoice: record.invoice.clone(), + digest: record.digest(), + intent: record.intent.clone(), + requirement: requirement.clone(), + })) + } + + /// Publish the already confirmed application invoice under current native and monitor locks. + /// + /// Before calling, a private application owner must successfully persist these exact bytes and + /// the matching Pending payment, retaining uncertainty across failed writes and restore. The + /// callback only publishes that preconfirmed response in memory, at most once. It performs no + /// I/O and acquires no native, monitor, owner or storage locks. Returning Err retains the exact + /// assignment for retry. Publication never fabricates PaymentClaimed or completed receive credit. + pub fn release_ffor_receiver_invoice( + &self, stored: &FFORStoredInvoice, publish: C, + ) -> Result + where + C: FnOnce(&str) -> Result<(), ()>, + { + let _guard = self.total_consistency_lock.read().unwrap(); + let context = &stored.context; + let peers = self.per_peer_state.read().unwrap(); + let peer = peers + .get(&context.settlement_node_id()) + .ok_or(FFORReceiverError::InvalidInvoice)? + .lock() + .unwrap(); + let channel = peer + .channel_by_id + .get(&context.channel_id()) + .and_then(Channel::as_funded) + .ok_or(FFORReceiverError::InvalidInvoice)?; + if channel.context.is_monitor_or_signer_pending_channel_update() + || peer + .in_flight_monitor_updates + .get(&context.channel_id()) + .map_or(false, |(_, updates)| !updates.is_empty()) + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let key = + FFORRecoveryKey { channel_id: context.channel_id(), epoch_id: context.epoch_id() }; + let recovery = self.ffor_recovery.lock().unwrap(); + let current = self.ffor_active_state_locked(channel, &recovery, &key)?; + let runtime = self.ffor_activation.lock().unwrap(); + let height = self.best_block.read().unwrap(); + Self::validate_ffor_witness_context(context, ¤t, height.height)?; + let record = recovery.get_invoice(&key).ok_or(FFORReceiverError::InvalidInvoice)?; + let registration = recovery.get_witnesses(&key).ok_or(FFORReceiverError::InvalidInvoice)?; + let acks = recovery.get_witness_acks(&key).ok_or(FFORReceiverError::InvalidInvoice)?; + record + .validate(¤t, registration, acks) + .map_err(|_| FFORReceiverError::InvalidInvoice)?; + validate_route(¤t, registration, &record.route, invoice_time()?) + .map_err(|_| FFORReceiverError::InvalidInvoice)?; + if record.digest() != stored.digest + || record.invoice != stored.invoice + || Self::ffor_invoice_channel_binding(channel)? + != (record.settlement_scid, record.settlement_cltv) + { + return Err(FFORReceiverError::InvalidInvoice); + } + let requirement = &runtime.get(&key)?.requirement; + let barrier = self.ffor_persistence.lock().unwrap(); + if requirement != &stored.requirement || !barrier.is_complete(requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let check = Self::ffor_invoice_monitor_check(¤t, channel, record, height.height)?; + let mut callback = Some(publish); + let released = self.chain_monitor.validate_and_publish_ffor_invoice(&check, &mut || { + callback.take().ok_or(())?(&record.invoice) + })?; + Ok(released && callback.is_none()) + } +} + +#[cfg(test)] +mod tests; diff --git a/lightning/src/ln/channelmanager/ffor_activation/invoice/fixture_tests.rs b/lightning/src/ln/channelmanager/ffor_activation/invoice/fixture_tests.rs new file mode 100644 index 00000000000..06cb7b46767 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/invoice/fixture_tests.rs @@ -0,0 +1,174 @@ +//! Optional public-seed fixture for the Node storage join, generated through the real driver. +use super::*; +use crate::ln::ffor::{FFORReceiverParameters, FFORReceiverProgress}; +use bitcoin::constants::ChainHash; + +fn emit( + receiver: &Node, id: &crate::ln::ffor::FFORReceiverId, + connection: &crate::ln::ffor::FFORPeerConnection, +) -> Vec { + let mut wire = Vec::new(); + receiver + .node + .advance_ffor_receiver(id, connection, |message| { + wire = message.to_vec(); + Ok(()) + }) + .unwrap(); + assert!(!wire.is_empty()); + wire +} + +#[test] +fn ffor_invoice_public_driver_one_slot_fixture_preserves_native_request() { + let mut chanmon_cfgs = create_chanmon_cfgs(3); + let wallet_root = + bitcoin::bip32::Xpriv::new_master(bitcoin::Network::Testnet, &[91; 64]).unwrap(); + chanmon_cfgs[1].keys_manager = crate::util::test_utils::TestKeysInterface::new( + &wallet_root.private_key.secret_bytes(), + bitcoin::Network::Testnet, + ); + let mut node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + node_cfgs[1].node_seed = wallet_root.private_key.secret_bytes(); + let config = anchor_config(); + let managers = create_node_chanmgrs( + 3, + &node_cfgs, + &[Some(config.clone()), Some(config.clone()), Some(config)], + ); + let nodes = create_network(3, &node_cfgs, &managers); + let channel = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let public = create_announced_chan_between_nodes_with_value(&nodes, 2, 0, 100_000, 40_000_000); + let (sender, receiver, witness) = (&nodes[0], &nodes[1], &nodes[2]); + let announcement = nodes[0] + .network_graph + .read_only() + .channels() + .get(&public.0.contents.short_channel_id) + .unwrap() + .announcement_message + .clone() + .unwrap(); + let mut update = public.0; + update.contents.timestamp = invoice_time().unwrap() as u32; + update.signature = witness + .keys_manager + .sign_gossip_message(msgs::UnsignedGossipMessage::ChannelUpdate(&update.contents)) + .unwrap(); + let route = FFORWitnessRouteEvidence { announcement, update }; + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let mut local = b"ldk-node/ffor/request-id/v1".to_vec(); + local.extend_from_slice(&ChainHash::TESTNET3.to_bytes()); + local.extend_from_slice(&receiver.node.get_our_node_id().serialize()); + local.extend_from_slice(&(b"request-fixture".len() as u16).to_be_bytes()); + local.extend_from_slice(b"request-fixture"); + let local_request_id = sha256::Hash::hash(&local).to_byte_array(); + let parameters = FFORReceiverParameters { + local_request_id, + amounts_msat: vec![voucher.amount_msat], + minimum_payment_msat: voucher.amount_msat, + settlement_deadline: voucher.cltv_expiry - 20, + voucher_expiry: voucher.cltv_expiry, + fee_base_msat: 0, + fee_proportional_millionths: 0, + claim_margin_blocks: 20, + witness_peers: Some(vec![witness.node.get_our_node_id()]), + hash_chain: false, + }; + let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let id = + receiver.node.prepare_ffor_receiver(&channel, &connection, parameters.clone()).unwrap(); + persist(receiver.node); + let init_wire = emit(receiver, &id, &connection); + let init = FFORMessage::decode(&init_wire).unwrap(); + let (_, mut accept) = ffor_setup_test_messages(sender, receiver, channel, voucher); + accept.header = init.header; + if let Payload::Accept(terms) = &mut accept.payload { + terms.init_hash = transcript::init_hash(&init_wire); + } + sign(&mut accept, sender); + receiver.node.handle_ffor_receiver_message(&connection, &accept.encode().unwrap()).unwrap(); + persist(receiver.node); + deliver_parked_voucher(sender, receiver, update); + let snapshot = get_monitor!(receiver, channel).ffor_commitment_snapshot().unwrap(); + assert_eq!( + receiver + .node + .advance_ffor_receiver_with_monitor(&id, &connection, snapshot, |_| panic!( + "no custom wire during STFU" + )) + .unwrap(), + FFORReceiverProgress::AwaitingPeer + ); + complete_handshake(sender, receiver); + let snapshot = get_monitor!(receiver, channel).ffor_commitment_snapshot().unwrap(); + assert_eq!( + receiver + .node + .advance_ffor_receiver_with_monitor(&id, &connection, snapshot, |_| panic!( + "not durable" + )) + .unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver.node); + let activate = FFORMessage::decode(&emit(receiver, &id, &connection)).unwrap(); + let context = receiver.node.ffor_receiver_recovery_context(&channel, id.epoch_id()).unwrap(); + let mut ack = FFORMessage { + header: activate.header, + payload: Payload::ActivateAck(context.activation_hash()), + extensions: Vec::new(), + signature: [0; 64], + }; + sign(&mut ack, sender); + receiver.node.handle_ffor_receiver_message(&connection, &ack.encode().unwrap()).unwrap(); + persist(receiver.node); + let context = receiver.node.ffor_receiver_recovery_context(&channel, id.epoch_id()).unwrap(); + let mut selected = manifests(&context, 1); + selected[0].0 = witness.node.get_our_node_id(); + receiver.node.register_ffor_receiver_witnesses(&context, &selected).unwrap(); + persist(receiver.node); + all_acknowledged(receiver, &context, &selected); + assert_eq!( + receiver + .node + .validate_ffor_receiver_request_intent( + local_request_id, + &channel, + &sender.node.get_our_node_id(), + ¶meters + ) + .unwrap(), + Some(id) + ); + let active_manager = persist(receiver.node); + let monitor = get_monitor!(receiver, channel).encode(); + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).unwrap(); + let issued_manager = persist(receiver.node); + let stored = receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().unwrap(); + assert!(receiver.node.release_ffor_receiver_invoice(&stored, |_| Ok(())).unwrap()); + let directory = match std::env::var_os("FFOR_NODE_INVOICE_FIXTURE_DIR") { + Some(directory) => std::path::PathBuf::from(directory), + None => return, + }; + std::fs::create_dir_all(&directory).unwrap(); + for (name, bytes) in [ + ("active-manager.bin", active_manager), + ("issued-manager.bin", issued_manager), + ("monitor.bin", monitor), + ("route-announcement.bin", route.announcement.encode()), + ("route-update.bin", route.update.encode()), + ("witness-manifest.bin", selected[0].1.encode()), + ] { + std::fs::write(directory.join(name), bytes).unwrap(); + } + let hex = |bytes: &[u8]| bytes.iter().map(|byte| format!("{byte:02x}")).collect::(); + std::fs::write(directory.join("fixture.txt"), format!( + "network=testnet\nwallet_seed={}\nclient_id=request-fixture\nlocal_request_id={}\nchannel_id={}\nepoch_id={}\nreceiver={}\nsettlement={}\nwitness={}\nwitness_node_seed={}\nfunding_txid={}\nfunding_vout={}\nheight={}\namount_msat={}\nminimum_payment_msat={}\nsettlement_deadline={}\nvoucher_expiry={}\nfee_base_msat=0\nfee_proportional_millionths=0\nclaim_margin_blocks=20\nhash_chain=false\ndescription={}\nexpiry_seconds={}\nsafety_margin_seconds={}\nfetch_secret={}\nencryption_secret={}\ngenerator=ffor_invoice_public_driver_one_slot_fixture_preserves_native_request\nbase_revision=016778d\n", + hex(&[91;64]), hex(&local_request_id), hex(&channel.0), hex(&id.epoch_id()), context.receiver_node_id(), context.settlement_node_id(), witness.node.get_our_node_id(), hex(&witness.node_seed), + context.funding_txo().txid, context.funding_txo().index, receiver.node.current_best_block().height, + voucher.amount_msat, voucher.amount_msat, parameters.settlement_deadline, parameters.voucher_expiry, + intent().description, intent().expiry_seconds, intent().safety_margin_seconds, hex(&[100;32]), hex(&[120;32]), + )).unwrap(); +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/invoice/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/invoice/tests.rs new file mode 100644 index 00000000000..3b34a184059 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/invoice/tests.rs @@ -0,0 +1,615 @@ +use super::*; +use crate::chain::ChannelMonitorUpdateStatus; +use crate::ln::channel::ffor_setup_test_messages; +use crate::ln::channelmanager::ffor_recovery_tests::{claim_ffor_preimage_for_test, restore}; +use crate::ln::ffor_recovery::ffor_test_witness_manifests as manifests; +use crate::ln::ffor_tests::quiescence::{complete_handshake, request}; +use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; +use crate::ln::functional_test_utils::*; +use bitcoin::hashes::{sha256, Hash}; +use lightning_ffor::witness::{Acknowledgement, AcknowledgementResult, Provision, SignedManifest}; + +const EPOCH: [u8; 32] = [81; 32]; + +#[path = "fixture_tests.rs"] +mod fixture_tests; + +fn persist(node: &TestChannelManager) -> Vec { + let token = node.capture_ffor_persistence(); + let bytes = node.encode(); + node.ffor_persistence_completed(token).unwrap(); + bytes +} +fn sign(message: &mut FFORMessage, node: &Node) { + message.signature = node + .keys_manager + .sign_ffor_message(&FFORSigningRequest::new(&message.unsigned_wire().unwrap()).unwrap()) + .unwrap() + .serialize_compact(); +} +fn park_single( + sender: &Node, receiver: &Node, id: ChannelId, witnesses: Option>, +) -> (FFORReceiverRecoveryContext, FFORVoucher, PaymentPreimage) { + let preimage = PaymentPreimage([*receiver.network_payment_count.as_ref().borrow(); 32]); + let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); + let (mut init, mut accept) = ffor_setup_test_messages(sender, receiver, id, voucher); + if let Payload::Init(terms) = &mut init.payload { + terms.witness_peers = witnesses; + } + sign(&mut init, receiver); + if let Payload::Accept(terms) = &mut accept.payload { + terms.init_hash = lightning_ffor::transcript::init_hash(&init.encode().unwrap()); + } + sign(&mut accept, sender); + receiver + .node + .register_ffor_receiver_setup( + &id, + &sender.node.get_our_node_id(), + &init.encode().unwrap(), + &accept.encode().unwrap(), + 20, + ) + .unwrap(); + persist(receiver.node); + deliver_parked_voucher(sender, receiver, update); + request(sender, receiver, id).unwrap(); + complete_handshake(sender, receiver); + let snapshot = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); + receiver + .node + .prepare_ffor_receiver_activation(&id, &sender.node.get_our_node_id(), EPOCH, &snapshot) + .unwrap(); + persist(receiver.node); + let context = receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + let mut ack = FFORMessage { + header: context.setup().header(), + payload: Payload::ActivateAck(context.activation_hash()), + extensions: Vec::new(), + signature: [0; 64], + }; + sign(&mut ack, sender); + receiver + .node + .accept_ffor_receiver_activation_ack( + &id, + &sender.node.get_our_node_id(), + EPOCH, + &ack.encode().unwrap(), + ) + .unwrap(); + persist(receiver.node); + (receiver.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(), voucher, preimage) +} + +macro_rules! fixture { + ($sender:ident, $receiver:ident, $witness:ident, $id:ident, $context:ident, $voucher:ident, $preimage:ident, $route:ident, $selected:ident, $funds:expr $(, $persister:ident)? $(; $policy:expr)?) => { + let chanmon_cfgs = create_chanmon_cfgs(3); + $(let $persister = &chanmon_cfgs[1].persister;)? + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(3, &node_cfgs, &[Some(config.clone()), Some(config.clone()), Some(config)]); + let nodes = create_network(3, &node_cfgs, &managers); + let (funder, other) = if $funds { (1, 0) } else { (0, 1) }; + let $id = create_announced_chan_between_nodes_with_value(&nodes, funder, other, 100_000, 40_000_000).2; + let public = create_announced_chan_between_nodes_with_value(&nodes, 2, 0, 100_000, 40_000_000); + let ($sender, $receiver, $witness) = (&nodes[0], &nodes[1], &nodes[2]); + let announcement = nodes[0].network_graph.read_only().channels().get(&public.0.contents.short_channel_id).unwrap().announcement_message.clone().unwrap(); + let mut update = public.0; + update.contents.timestamp = invoice_time().unwrap() as u32; + update.signature = $witness.keys_manager.sign_gossip_message(msgs::UnsignedGossipMessage::ChannelUpdate(&update.contents)).unwrap(); + let $route = FFORWitnessRouteEvidence { announcement, update }; + let second_witness = PublicKey::from_secret_key(&bitcoin::secp256k1::Secp256k1::new(), &bitcoin::secp256k1::SecretKey::from_slice(&[91; 32]).unwrap()); + let invoice_witnesses = Some(vec![$witness.node.get_our_node_id(), second_witness]); + $(let invoice_witnesses = ($policy)(invoice_witnesses);)? + let ($context, $voucher, $preimage) = park_single($sender, $receiver, $id, invoice_witnesses); + let mut $selected = manifests(&$context, 2); + $selected[0].0 = $witness.node.get_our_node_id(); + $receiver.node.register_ffor_receiver_witnesses(&$context, &$selected).unwrap(); + persist($receiver.node); + }; +} +fn intent() -> FFORInvoiceIntent { + FFORInvoiceIntent { + description: "Offline receipt".into(), + expiry_seconds: 3600, + safety_margin_seconds: 120, + } +} +fn acknowledge( + receiver: &Node, context: &FFORReceiverRecoveryContext, selected: &(PublicKey, SignedManifest), + request: u8, +) { + let init = msgs::Init { + features: receiver.node.init_features(), + networks: None, + remote_network_address: None, + }; + if receiver.node.ffor_peer_connection(&selected.0).is_err() { + receiver.node.peer_connected(selected.0, &init, false).unwrap(); + } + let connection = receiver.node.ffor_peer_connection(&selected.0).unwrap(); + let provision = Provision::new([request; 16], selected.1.clone()); + let attempt = receiver + .node + .stage_ffor_receiver_witness_provision(context, &connection, &provision) + .unwrap(); + let active = receiver + .node + .capture_ffor_receiver_active_context( + &context.channel_id(), + &context.settlement_node_id(), + context.epoch_id(), + ) + .unwrap(); + assert!(receiver + .node + .release_ffor_receiver_witness_attempt(&active, &attempt, &provision, |_| Ok(())) + .unwrap()); + let ack = Acknowledgement::new( + provision.request_id(), + AcknowledgementResult::Accepted { + witness: selected.0, + retention_until: selected.1.unsigned().parameters().retention_until, + }, + ) + .unwrap(); + receiver.node.retain_ffor_receiver_witness_ack(&connection, &ack).unwrap(); +} +fn all_acknowledged( + receiver: &Node, context: &FFORReceiverRecoveryContext, + selected: &[(PublicKey, SignedManifest)], +) { + for (index, witness) in selected.iter().enumerate() { + acknowledge(receiver, context, witness, index as u8); + persist(receiver.node); + } +} + +#[test] +fn ffor_invoice_requires_every_durable_ack_and_retains_one_exact_no_mpp_route() { + for receiver_funds in [false, true] { + fixture!( + sender, + receiver, + witness, + id, + context, + voucher, + _preimage, + route, + selected, + receiver_funds + ); + assert!(receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).is_err()); + acknowledge(receiver, &context, &selected[0], 0); + persist(receiver.node); + assert!(receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).is_err()); + acknowledge(receiver, &context, &selected[1], 1); + assert!(receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).is_err()); + persist(receiver.node); + let older = receiver.node.capture_ffor_persistence(); + let prepared = + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).unwrap(); + assert!(!receiver.node.is_ffor_state_persisted(prepared.persistence_requirement())); + assert!(receiver.node.ffor_receiver_invoice_for_storage(&context).is_err()); + receiver.node.ffor_persistence_completed(older).unwrap(); + assert!(!receiver.node.is_ffor_state_persisted(prepared.persistence_requirement())); + assert_eq!( + receiver + .node + .prepare_ffor_receiver_invoice(&context, &intent(), &route) + .unwrap() + .invoice_digest(), + prepared.invoice_digest() + ); + let mut changed = intent(); + changed.description.push('!'); + assert_eq!( + receiver.node.prepare_ffor_receiver_invoice(&context, &changed, &route).unwrap_err(), + FFORReceiverError::AlreadyRegistered + ); + let manager = persist(receiver.node); + let stored = receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().unwrap(); + assert_eq!(stored.intent(), &intent()); + assert_eq!(stored.invoice_digest(), prepared.invoice_digest()); + let invoice: Bolt11Invoice = stored.invoice_for_storage().parse().unwrap(); + assert_eq!(invoice.amount_milli_satoshis(), Some(voucher.amount_msat)); + assert_eq!(invoice.payment_hash().to_byte_array(), voucher.payment_hash.0); + assert_eq!(invoice.get_payee_pub_key(), receiver.node.get_our_node_id()); + assert!(!invoice.features().unwrap().supports_basic_mpp()); + let routes = invoice.private_routes(); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].0.len(), 2); + assert_eq!(routes[0].0[0].src_node_id, witness.node.get_our_node_id()); + assert_eq!(routes[0].0[1].src_node_id, sender.node.get_our_node_id()); + assert_eq!(routes[0].0[1].fees.base_msat, context.setup().terms().fees.base_msat); + assert!(!receiver.node.release_ffor_receiver_invoice(&stored, |_| Err(())).unwrap()); + assert!(receiver + .node + .release_ffor_receiver_invoice(&stored, |wire| { + assert_eq!(wire, stored.invoice_for_storage()); + Ok(()) + }) + .unwrap()); + let monitor = get_monitor!(receiver, id).encode(); + let restored = restore(receiver, &manager, &monitor).unwrap(); + assert!(restored.ffor_receiver_invoice_for_storage(&context).is_err()); + assert!(restored + .release_ffor_receiver_invoice(&stored, |_| panic!("old manager handle")) + .is_err()); + persist(&restored); + let fresh = restored.ffor_receiver_invoice_for_storage(&context).unwrap().unwrap(); + assert_eq!(fresh.invoice_for_storage(), stored.invoice_for_storage()); + assert!(restored.release_ffor_receiver_invoice(&fresh, |_| Ok(())).unwrap()); + assert!(receiver.node.get_and_clear_pending_events().is_empty()); + } +} + +#[test] +fn ffor_invoice_rejects_signed_wrong_route_policy_and_stale_updates() { + fixture!(sender, receiver, witness, id, context, _voucher, _preimage, route, selected, false); + all_acknowledged(receiver, &context, &selected); + for fault in 0..7 { + let mut bad = route.clone(); + match fault { + 0 => bad.update.contents.channel_flags ^= 1, + 1 => bad.update.contents.channel_flags |= 2, + 2 => bad.update.contents.htlc_maximum_msat = 1, + 3 => bad.update.contents.short_channel_id += 1, + 4 => bad.update.contents.timestamp = 1, + 5 => bad.update.contents.chain_hash = bitcoin::constants::ChainHash::BITCOIN, + 6 => bad.announcement.node_signature_1 = bad.announcement.bitcoin_signature_1, + _ => unreachable!(), + } + bad.update.signature = witness + .keys_manager + .sign_gossip_message(msgs::UnsignedGossipMessage::ChannelUpdate(&bad.update.contents)) + .unwrap(); + assert!( + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &bad).is_err(), + "fault {fault}" + ); + assert!(receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().is_none()); + } + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).unwrap(); + persist(receiver.node); + let stored = receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().unwrap(); + receiver.node.prepare_ffor_receiver_close(&id, &sender.node.get_our_node_id(), EPOCH).unwrap(); + assert!(receiver + .node + .release_ffor_receiver_invoice(&stored, |_| panic!("closed intent")) + .is_err()); + persist(receiver.node); + assert_eq!( + receiver + .node + .ffor_receiver_invoice_for_storage(&context) + .unwrap() + .unwrap() + .invoice_for_storage(), + stored.invoice_for_storage() + ); +} + +#[test] +fn ffor_invoice_signing_capture_cannot_commit_after_deadline_or_native_route_change() { + fixture!(_sender, receiver, _witness, id, context, _voucher, _preimage, route, selected, false); + all_acknowledged(receiver, &context, &selected); + let capture = + receiver.node.capture_ffor_invoice(&context, &intent(), &route).unwrap().err().unwrap(); + let mut record = capture.record; + let registration = receiver.node.ffor_receiver_witness_registration(&context).unwrap().unwrap(); + let raw = record + .unsigned(&context, ®istration, invoice_time().unwrap(), PaymentSecret([1; 32])) + .unwrap(); + let signature = receiver.keys_manager.sign_invoice(&raw, Recipient::Node).unwrap(); + record.invoice = Bolt11Invoice::from_signed(raw.sign::<_, ()>(|_| Ok(signature)).unwrap()) + .unwrap() + .to_string(); + let original = receiver.node.best_block.read().unwrap().height; + receiver.node.best_block.write().unwrap().height = context.setup().terms().settlement_deadline; + assert!(receiver + .node + .commit_ffor_invoice(&context, record.clone(), capture.requirement.clone()) + .is_err()); + receiver.node.best_block.write().unwrap().height = original; + record.settlement_scid ^= 1; + assert!(receiver.node.commit_ffor_invoice(&context, record, capture.requirement).is_err()); + assert!(receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().is_none()); +} + +#[test] +fn ffor_invoice_actual_monitor_rejects_known_preimage_without_counter_change() { + fixture!(_sender, receiver, _witness, id, context, voucher, preimage, route, selected, false); + all_acknowledged(receiver, &context, &selected); + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).unwrap(); + persist(receiver.node); + let stored = receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().unwrap(); + let monitor = get_monitor!(receiver, id); + let update_id = monitor.get_latest_update_id(); + assert_eq!(sha256::Hash::hash(&preimage.0).to_byte_array(), voucher.payment_hash.0); + monitor.provide_payment_preimage_unsafe_legacy( + &voucher.payment_hash, + &preimage, + &receiver.tx_broadcaster, + &crate::chain::chaininterface::LowerBoundedFeeEstimator::new(receiver.fee_estimator), + &receiver.logger, + ); + assert_eq!(monitor.get_latest_update_id(), update_id); + drop(monitor); + assert!(receiver + .node + .release_ffor_receiver_invoice(&stored, |_| panic!("known preimage")) + .is_err()); + assert!(receiver.node.get_and_clear_pending_events().is_empty()); +} + +#[test] +fn ffor_invoice_monitor_persistence_and_funding_spend_exclude_publication() { + for direct_close in [false, true] { + fixture!( + sender, receiver, _witness, id, context, voucher, preimage, route, selected, false, + persister + ); + all_acknowledged(receiver, &context, &selected); + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).unwrap(); + persist(receiver.node); + let stored = receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().unwrap(); + if direct_close { + let monitor = get_monitor!(receiver, id); + let before = monitor.get_latest_update_id(); + monitor.broadcast_latest_holder_commitment_txn( + &receiver.tx_broadcaster, + &receiver.fee_estimator, + &receiver.logger, + ); + assert_eq!(monitor.get_latest_update_id(), before); + drop(monitor); + } else { + persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + let update = + claim_ffor_preimage_for_test(sender, receiver, id, voucher.htlc_id, preimage); + assert!(receiver + .node + .release_ffor_receiver_invoice(&stored, |_| panic!("pending preimage")) + .is_err()); + persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + receiver.chain_monitor.chain_monitor.channel_monitor_updated(id, update).unwrap(); + receiver.node.process_pending_events(&|_: Event| Ok(())); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + } + assert!(receiver + .node + .release_ffor_receiver_invoice(&stored, |_| panic!("monitor state changed")) + .is_err()); + receiver.node.get_and_clear_pending_msg_events(); + receiver.node.get_and_clear_pending_events(); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + } +} + +#[test] +fn ffor_invoice_archive_rejects_corruption_and_reserves_terminal_capacity() { + fixture!(sender, receiver, _witness, id, context, _voucher, _preimage, route, selected, false); + all_acknowledged(receiver, &context, &selected); + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).unwrap(); + persist(receiver.node); + let key = FFORRecoveryKey { channel_id: id, epoch_id: EPOCH }; + crate::ln::ffor_recovery::invoice::tests::check_archive( + &receiver.node.ffor_recovery.lock().unwrap(), + key, + ); + receiver.node.prepare_ffor_receiver_close(&id, &sender.node.get_our_node_id(), EPOCH).unwrap(); + persist(receiver.node); + crate::ln::ffor_recovery::invoice::tests::check_archive( + &receiver.node.ffor_recovery.lock().unwrap(), + key, + ); +} + +#[test] +fn ffor_invoice_monitor_tip_ahead_of_manager_blocks_conservative_expiry() { + fixture!(_sender, receiver, _witness, id, context, _voucher, _preimage, route, selected, false); + all_acknowledged(receiver, &context, &selected); + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).unwrap(); + persist(receiver.node); + let stored = receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().unwrap(); + let manager_height = receiver.node.current_best_block().height; + let block = create_dummy_header( + receiver.node.current_best_block().block_hash, + invoice_time().unwrap() as u32, + ); + get_monitor!(receiver, id).best_block_updated( + &block, + context.setup().terms().settlement_deadline - 1, + receiver.tx_broadcaster, + receiver.fee_estimator, + &receiver.logger, + ); + assert_eq!(receiver.node.current_best_block().height, manager_height); + assert!(receiver + .node + .release_ffor_receiver_invoice(&stored, |_| panic!("monitor height advanced")) + .is_err()); +} + +#[test] +fn ffor_invoice_confirmed_funding_spend_blocks_before_manager_observes_close() { + use crate::chain::Confirm; + fixture!(sender, receiver, _witness, id, context, _voucher, _preimage, route, selected, false); + all_acknowledged(receiver, &context, &selected); + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).unwrap(); + persist(receiver.node); + let stored = receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().unwrap(); + let before = get_monitor!(receiver, id).get_latest_update_id(); + let commitment = get_local_commitment_txn!(sender, id).remove(0); + let header = create_dummy_header( + receiver.node.current_best_block().block_hash, + invoice_time().unwrap() as u32, + ); + receiver.chain_monitor.chain_monitor.transactions_confirmed( + &header, + &[(0, &commitment)], + receiver.node.current_best_block().height + 1, + ); + assert_eq!(get_monitor!(receiver, id).get_latest_update_id(), before); + assert!(receiver + .node + .capture_ffor_receiver_active_context(&id, &context.settlement_node_id(), EPOCH) + .is_ok()); + assert!(receiver + .node + .release_ffor_receiver_invoice(&stored, |_| panic!("funding already spent")) + .is_err()); + receiver.node.process_pending_events(&|_: Event| Ok(())); + receiver.node.get_and_clear_pending_msg_events(); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); +} + +#[test] +fn ffor_invoice_wrong_signer_cannot_reserve_or_expose_a_slot() { + fixture!( + _sender, receiver, _witness, _id, context, _voucher, _preimage, route, selected, false + ); + all_acknowledged(receiver, &context, &selected); + receiver.keys_manager.wrong_invoice_signer.store(true, core::sync::atomic::Ordering::Release); + assert!(receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).is_err()); + assert!(receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().is_none()); + receiver.keys_manager.wrong_invoice_signer.store(false, core::sync::atomic::Ordering::Release); + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).unwrap(); +} + +#[test] +fn ffor_invoice_publication_holds_actual_monitor_through_callback() { + fixture!(_sender, receiver, _witness, id, context, voucher, preimage, route, selected, false); + all_acknowledged(receiver, &context, &selected); + receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).unwrap(); + persist(receiver.node); + let stored = receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().unwrap(); + let chain_monitor = &receiver.chain_monitor.chain_monitor; + let broadcaster = receiver.tx_broadcaster; + let fee_estimator = receiver.fee_estimator; + let logger = &receiver.logger; + let start = std::sync::Barrier::new(2); + let (blocked_send, blocked_receive) = std::sync::mpsc::channel(); + let (finished_send, finished_receive) = std::sync::mpsc::channel(); + std::thread::scope(|scope| { + let start = &start; + let worker = scope.spawn(move || { + start.wait(); + let monitor = chain_monitor.get_monitor(id).unwrap(); + blocked_send.send(monitor.inner.try_lock().is_err()).unwrap(); + monitor.provide_payment_preimage_unsafe_legacy( + &voucher.payment_hash, + &preimage, + &broadcaster, + &crate::chain::chaininterface::LowerBoundedFeeEstimator::new(fee_estimator), + logger, + ); + finished_send.send(()).unwrap(); + }); + // The waits are a deterministic test probe only. Production callbacks must remain bounded + // in-memory publication and never wait for monitor work or another native operation. + assert!(receiver + .node + .release_ffor_receiver_invoice(&stored, |_| { + start.wait(); + assert!(blocked_receive.recv().unwrap()); + assert!(finished_receive.try_recv().is_err()); + Ok(()) + }) + .unwrap()); + worker.join().unwrap(); + }); + assert!(receiver + .node + .release_ffor_receiver_invoice(&stored, |_| panic!("preimage now known")) + .is_err()); +} + +#[test] +fn ffor_invoice_rejects_correctly_signed_identity_loops_and_reversed_endpoints() { + use crate::routing::gossip::{verify_channel_announcement, NodeId}; + use bitcoin::secp256k1::Secp256k1; + fixture!(sender, receiver, witness, _id, context, _voucher, _preimage, route, selected, false); + for (first, second, selected_witness, reversed) in [ + (sender, sender, sender, false), + (sender, receiver, receiver, false), + (witness, sender, witness, true), + ] { + let mut evidence = route.clone(); + let (first, second) = if (first.node.get_our_node_id().serialize() + > second.node.get_our_node_id().serialize()) + == reversed + { + (first, second) + } else { + (second, first) + }; + evidence.announcement.contents.node_id_1 = + NodeId::from_pubkey(&first.node.get_our_node_id()); + evidence.announcement.contents.node_id_2 = + NodeId::from_pubkey(&second.node.get_our_node_id()); + evidence.announcement.contents.bitcoin_key_1 = evidence.announcement.contents.node_id_1; + evidence.announcement.contents.bitcoin_key_2 = evidence.announcement.contents.node_id_2; + let signature1 = first + .keys_manager + .sign_gossip_message(msgs::UnsignedGossipMessage::ChannelAnnouncement( + &evidence.announcement.contents, + )) + .unwrap(); + let signature2 = second + .keys_manager + .sign_gossip_message(msgs::UnsignedGossipMessage::ChannelAnnouncement( + &evidence.announcement.contents, + )) + .unwrap(); + evidence.announcement.node_signature_1 = signature1; + evidence.announcement.bitcoin_signature_1 = signature1; + evidence.announcement.node_signature_2 = signature2; + evidence.announcement.bitcoin_signature_2 = signature2; + verify_channel_announcement(&evidence.announcement, &Secp256k1::verification_only()) + .unwrap(); + evidence.update.contents.channel_flags = + if first.node.get_our_node_id() == selected_witness.node.get_our_node_id() { + 0 + } else { + 1 + }; + evidence.update.signature = selected_witness + .keys_manager + .sign_gossip_message(msgs::UnsignedGossipMessage::ChannelUpdate( + &evidence.update.contents, + )) + .unwrap(); + let metadata = + crate::ln::ffor::FFORReceiverWitnessRegistration::from_manifests(&context, &selected) + .unwrap(); + assert!(validate_route(&context, &metadata, &evidence, invoice_time().unwrap()).is_err()); + } +} + +#[test] +fn ffor_invoice_requires_exact_signed_witness_restriction() { + for unrestricted in [true, false] { + fixture!( + _sender, receiver, _witness, _id, context, _voucher, _preimage, route, selected, false; + |restriction: Option>| { + if unrestricted { + None + } else { + let mut allowed = restriction.unwrap(); + allowed.push(PublicKey::from_secret_key( + &bitcoin::secp256k1::Secp256k1::new(), + &bitcoin::secp256k1::SecretKey::from_slice(&[92; 32]).unwrap(), + )); + Some(allowed) + } + } + ); + all_acknowledged(receiver, &context, &selected); + assert!(receiver.node.prepare_ffor_receiver_invoice(&context, &intent(), &route).is_err()); + assert!(receiver.node.ffor_receiver_invoice_for_storage(&context).unwrap().is_none()); + } +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/witness.rs b/lightning/src/ln/channelmanager/ffor_activation/witness.rs index a7692d44780..48046f5e86f 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/witness.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/witness.rs @@ -68,6 +68,7 @@ where FFORRecoveryKey { channel_id: context.channel_id(), epoch_id: context.epoch_id() }; let mut recovery = self.ffor_recovery.lock().unwrap(); let current = self.ffor_active_state_locked(channel, &recovery, &key)?; + let mut runtime = self.ffor_activation.lock().unwrap(); let height = self.best_block.read().unwrap(); Self::validate_ffor_witness_context(context, ¤t, height.height)?; let registration = FFORReceiverWitnessRegistration::from_manifests(¤t, manifests) @@ -77,25 +78,24 @@ where return Err(FFORReceiverError::InvalidWitnessRegistration); } if recovery.get_witness_acks(&key).is_some() { - return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); + return Ok(runtime.get(&key)?.requirement.clone()); } // A legacy registration must reserve compact ACK capacity before the new authenticated // attempt path sends anything. Exact retries cannot skip this checked upgrade. } // Initial registration cannot convert a merely observed or unpersisted Active phase into // authority. Its exact compact evidence and all later close reservations must fit first. - self.ffor_active_context_locked(channel, &recovery, &key)?; + let mut barrier = self.ffor_persistence.lock().unwrap(); + if !barrier.is_complete(&runtime.get(&key)?.requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } let upgrade = recovery .prepare_witnesses(&key, registration) .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; - let requirement = self - .ffor_persistence - .lock() - .unwrap() - .request() - .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + let requirement = + barrier.request().map_err(|_| FFORReceiverError::PersistenceUnavailable)?; upgrade.commit(); - self.ffor_activation.lock().unwrap().record(key, requirement.clone(), false); + runtime.record(key, requirement.clone(), false); Ok(requirement) } @@ -169,6 +169,7 @@ where }; let recovery = self.ffor_recovery.lock().unwrap(); let current = self.ffor_active_state_locked(channel, &recovery, &key)?; + let runtime = self.ffor_activation.lock().unwrap(); let height = self.best_block.read().unwrap(); Self::validate_ffor_witness_context(historical, ¤t, height.height)?; let registration = @@ -176,7 +177,6 @@ where if !registration.matches_manifest(witness, provision.manifest()) { return Err(FFORReceiverError::InvalidWitnessRegistration); } - let runtime = self.ffor_activation.lock().unwrap(); let requirement = &runtime.get(&key)?.requirement; let barrier = self.ffor_persistence.lock().unwrap(); if requirement != &context.requirement || !barrier.is_complete(requirement) { diff --git a/lightning/src/ln/channelmanager/ffor_activation/witness_ack.rs b/lightning/src/ln/channelmanager/ffor_activation/witness_ack.rs index 543d9199f4b..89a20be1420 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/witness_ack.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/witness_ack.rs @@ -147,6 +147,7 @@ where let recovery = self.ffor_recovery.lock().unwrap(); let (key, registration, _) = self.ffor_ack_registration_locked(&recovery, historical)?; let current = self.ffor_active_state_locked(channel, &recovery, &key)?; + let mut runtime = self.ffor_activation.lock().unwrap(); let height = self.best_block.read().unwrap(); Self::validate_ffor_witness_context(historical, ¤t, height.height)?; if attempt.key != key @@ -158,7 +159,6 @@ where { return Err(FFORReceiverError::InvalidWitnessRegistration); } - let mut runtime = self.ffor_activation.lock().unwrap(); let requirement = &runtime.get(&key)?.requirement; let barrier = self.ffor_persistence.lock().unwrap(); if requirement != &context.requirement || !barrier.is_complete(requirement) { diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index 57039bbd63b..b7d5cdac252 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -45,6 +45,11 @@ mod witness_ack; pub use witness_ack::{ FFORReceiverWitnessAcknowledgements, FFORWitnessAcknowledgement, FFORWitnessProvisionAttempt, }; +pub(crate) mod invoice; +pub use invoice::{ + FFORInvoiceIntent, FFORInvoiceMonitorCheck, FFORInvoicePreparation, FFORStoredInvoice, + FFORWitnessRouteEvidence, +}; /// Which channel participant offers every voucher HTLC in the book. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -168,6 +173,8 @@ pub enum FFORReceiverError { InvalidWitnessRegistration, /// The authenticated receipt does not belong to the retained native epoch and witness selection. InvalidWitnessReceipt, + /// Invoice intent, signed route evidence, retained bytes or current publication conditions differ. + InvalidInvoice, } impl From for FFORReceiverError { @@ -188,6 +195,7 @@ impl fmt::Display for FFORReceiverError { Self::SignerUnavailable => f.write_str("FFOR node signature unavailable"), Self::InvalidWitnessRegistration => f.write_str("invalid FFOR witness registration"), Self::InvalidWitnessReceipt => f.write_str("invalid FFOR witness receipt"), + Self::InvalidInvoice => f.write_str("invalid or unavailable FFOR invoice"), } } } diff --git a/lightning/src/ln/ffor/invoice.rs b/lightning/src/ln/ffor/invoice.rs new file mode 100644 index 00000000000..b2e3f0a5fed --- /dev/null +++ b/lightning/src/ln/ffor/invoice.rs @@ -0,0 +1,159 @@ +//! Immutable one-slot invoice intent and protected publication inputs. + +use super::{FFORReceiverError, FFORReceiverRecoveryContext, FFORVoucherCommitments}; +use crate::chain::transaction::OutPoint; +use crate::ln::channelmanager::FFORPersistenceRequirement; +use crate::ln::msgs::{ChannelAnnouncement, ChannelUpdate}; +use crate::ln::types::ChannelId; +use crate::prelude::*; +use crate::types::payment::PaymentHash; +use bitcoin::secp256k1::PublicKey; + +/// Application presentation and conservative expiry policy for one fixed native voucher. +/// Amount, payment hash, routes and node identities cannot be supplied by the application. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FFORInvoiceIntent { + /// Direct BOLT 11 description, subject to the standard invoice description bound. + pub description: String, + /// Requested lifetime, capped at eight minutes per remaining block before admission closes. + pub expiry_seconds: u32, + /// Additional seconds subtracted from the conservative remaining-block estimate. + pub safety_margin_seconds: u32, +} + +/// Signed public-channel evidence for a registered witness immediately before settlement. +/// This is untrusted input until the manager validates both identities, signatures and direction. +/// Issuance also requires the signed Init witness restriction to exactly match every registered +/// witness, with all native acknowledgements durable. Public gossip signatures do not establish +/// on-chain funding or current forwarding liquidity; a BOLT 11 route hint remains advisory. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FFORWitnessRouteEvidence { + /// Full announcement signed by both channel nodes and their funding keys, without excess data. + pub announcement: ChannelAnnouncement, + /// Enabled witness-to-settlement update for the same chain and public short channel ID. + pub update: ChannelUpdate, +} + +/// A retained immutable invoice assignment. Wait for its requirement before storage retrieval. +#[derive(Clone, Debug)] +pub struct FFORInvoicePreparation { + pub(crate) invoice_digest: [u8; 32], + pub(crate) requirement: FFORPersistenceRequirement, +} + +impl FFORInvoicePreparation { + /// Digest of the exact retained signed BOLT 11 string. + pub fn invoice_digest(&self) -> [u8; 32] { + self.invoice_digest + } + /// Native manager write which must complete before retrieving the invoice for application + /// storage. + pub fn persistence_requirement(&self) -> &FFORPersistenceRequirement { + &self.requirement + } +} + +/// Exact invoice bytes retrieved only for protected application storage and recovery. +/// +/// This is not permission to display, return or otherwise expose an invoice to a payer. Persist +/// these bytes and the matching Pending payment first, then use the manager's publication method. +/// Restored application bytes alone do not recreate this manager-instance-bound handle. +#[derive(Clone)] +pub struct FFORStoredInvoice { + pub(crate) context: FFORReceiverRecoveryContext, + pub(crate) invoice: String, + pub(crate) digest: [u8; 32], + pub(crate) intent: FFORInvoiceIntent, + pub(crate) requirement: FFORPersistenceRequirement, +} + +impl FFORStoredInvoice { + /// Exact signed invoice for storage only, never a replacement constructed on retry. + pub fn invoice_for_storage(&self) -> &str { + &self.invoice + } + /// Digest to join the native assignment to confirmed application writes. + pub fn invoice_digest(&self) -> [u8; 32] { + self.digest + } + /// Exact original presentation and expiry intent, for joining confirmed application storage. + pub fn intent(&self) -> &FFORInvoiceIntent { + &self.intent + } + /// Immutable epoch context for the application record. + pub fn recovery_context(&self) -> &FFORReceiverRecoveryContext { + &self.context + } +} + +/// Engine-created conditions for an atomic check against the actual watched monitor. +/// +/// A custom [`crate::chain::Watch`] implementation can pass this to its actual monitor's +/// `validate_and_publish_ffor_invoice` method. A clone or detached monitor is insufficient. +/// No application constructor exists. The check itself grants no publication authority. +#[derive(Clone, Debug)] +pub struct FFORInvoiceMonitorCheck { + pub(crate) channel_id: ChannelId, + pub(crate) funding_txo: OutPoint, + pub(crate) settlement: PublicKey, + pub(crate) update_id: u64, + pub(crate) commitments: FFORVoucherCommitments, + pub(crate) payment_hash: PaymentHash, + pub(crate) amount_msat: u64, + pub(crate) voucher_expiry: u32, + pub(crate) deadline: u32, + pub(crate) manager_height: u32, + pub(crate) invoice_expires_at: u64, + pub(crate) safety_margin_seconds: u32, +} + +impl FFORInvoiceMonitorCheck { + /// Actual channel whose watched monitor must be locked during publication. + pub fn channel_id(&self) -> ChannelId { + self.channel_id + } + + pub(crate) fn validate_time(&self, monitor_height: u32) -> Result<(), FFORReceiverError> { + let now = invoice_time()?; + let height = core::cmp::max(monitor_height, self.manager_height); + let remaining = self + .deadline + .checked_sub(height) + .and_then(|blocks| u64::from(blocks).checked_mul(480)) + .and_then(|seconds| seconds.checked_sub(u64::from(self.safety_margin_seconds))) + .ok_or(FFORReceiverError::InvalidInvoice)?; + if remaining == 0 + || self.invoice_expires_at <= now + || self.invoice_expires_at + > now.checked_add(remaining).ok_or(FFORReceiverError::InvalidInvoice)? + { + return Err(FFORReceiverError::InvalidInvoice); + } + Ok(()) + } +} + +pub(crate) fn invoice_time() -> Result { + #[cfg(feature = "std")] + { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|time| time.as_secs()) + .map_err(|_| FFORReceiverError::InvalidInvoice) + } + #[cfg(not(feature = "std"))] + { + Err(FFORReceiverError::InvalidInvoice) + } +} + +impl core::fmt::Debug for FFORStoredInvoice { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("FFORStoredInvoice") + .field("context", &self.context) + .field("invoice_digest", &self.digest) + .field("invoice_bytes", &self.invoice.len()) + .finish_non_exhaustive() + } +} diff --git a/lightning/src/ln/ffor_recovery.rs b/lightning/src/ln/ffor_recovery.rs index 9f330d0e915..ef660a891d2 100644 --- a/lightning/src/ln/ffor_recovery.rs +++ b/lightning/src/ln/ffor_recovery.rs @@ -6,8 +6,10 @@ mod request; use request::PendingRequest; +pub(crate) mod invoice; mod witness; mod witness_ack; +use invoice::FFORInvoiceRecord; mod activation; pub(crate) use activation::{FFORReceiverActivation, FFORReceiverCloseRecord}; @@ -36,6 +38,7 @@ const REQUEST_VERSION: u8 = 4; // Earlier readers must not forget protected application key and manifest ownership. const WITNESS_VERSION: u8 = 5; const WITNESS_ACK_VERSION: u8 = 6; +const INVOICE_VERSION: u8 = 7; const MAX_RECORDS: usize = 64; const MAX_ENCODED_BYTES: usize = 8 * 1024 * 1024; // Two maximum wire messages, a 483-slot canonical book, and fixed admission fields fit here. @@ -71,6 +74,7 @@ struct StoredSetup { request: Option, witnesses: Option, witness_acks: Option, + invoice: Option, } impl_writeable_tlv_based!(StoredSetup, { @@ -80,6 +84,7 @@ impl_writeable_tlv_based!(StoredSetup, { (6, request, option), (8, witnesses, option), (10, witness_acks, option), + (12, invoice, option), }); struct Entry { @@ -121,6 +126,21 @@ impl Entry { if let Some(acks) = record.witness_acks.as_ref() { acks.validate(record.witnesses.as_ref().ok_or(FFORRecoveryError::InvalidRecord)?)?; } + if let Some(invoice) = record.invoice.as_ref() { + let context = record + .activation + .as_ref() + .ok_or(FFORRecoveryError::InvalidRecord)? + .receiver_context(&record.setup) + .map_err(|_| FFORRecoveryError::InvalidRecord)?; + invoice + .validate( + &context, + record.witnesses.as_ref().ok_or(FFORRecoveryError::InvalidRecord)?, + record.witness_acks.as_ref().ok_or(FFORRecoveryError::InvalidRecord)?, + ) + .map_err(|_| FFORRecoveryError::InvalidRecord)?; + } let header = authenticated.header(); let encoded_bytes = record.serialized_length(); let reserved_transition_bytes = Self::transition_reservation(&record); @@ -401,6 +421,7 @@ impl FFORRecoveryRegistry { request: None, witnesses: None, witness_acks: None, + invoice: None, })?; if self.pending_requests.iter().any(|pending| { pending.key.channel_id == entry.key.channel_id @@ -442,6 +463,7 @@ impl FFORRecoveryRegistry { request, witnesses, witness_acks: self.get_witness_acks(&key).cloned(), + invoice: self.get_invoice(&key).cloned(), })?; let index = self .entries @@ -490,6 +512,9 @@ impl FFORRecoveryRegistry { } fn version(&self) -> u8 { + if self.entries.iter().any(|entry| entry.record.invoice.is_some()) { + return INVOICE_VERSION; + } if self.entries.iter().any(|entry| entry.record.witness_acks.is_some()) { return WITNESS_ACK_VERSION; } @@ -572,6 +597,7 @@ impl Readable for FFORRecoveryRegistry { && version != REQUEST_VERSION && version != WITNESS_VERSION && version != WITNESS_ACK_VERSION + && version != INVOICE_VERSION { return Err(DecodeError::UnknownRequiredFeature); } diff --git a/lightning/src/ln/ffor_recovery/activation/tests.rs b/lightning/src/ln/ffor_recovery/activation/tests.rs index 0a762f4b982..0f619b75fb5 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests.rs @@ -356,6 +356,7 @@ fn ffor_activation_archive_reserves_maximum_ack_through_competing_admission_and_ request: None, witnesses: None, witness_acks: None, + invoice: None, canonical_book: extra_setup.validate_recovery().unwrap().canonical_book().to_vec(), setup: extra_setup, activation: Some(extra_activation), diff --git a/lightning/src/ln/ffor_recovery/activation/tests/witness.rs b/lightning/src/ln/ffor_recovery/activation/tests/witness.rs index fb6c3e6d716..936af6c49e6 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests/witness.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests/witness.rs @@ -81,6 +81,7 @@ fn ffor_witness_archive_reauthenticates_compact_metadata_and_required_fence() { request: None, witnesses: Some(metadata.clone()), witness_acks: None, + invoice: None, }; for mutation in 0..9 { let mut damaged = record(); diff --git a/lightning/src/ln/ffor_recovery/activation/tests/witness/ack.rs b/lightning/src/ln/ffor_recovery/activation/tests/witness/ack.rs index 31079ec8e62..39ab1c9f288 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests/witness/ack.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests/witness/ack.rs @@ -57,6 +57,7 @@ fn ffor_witness_ack_archive_legacy_upgrade_checks_capacity_before_tracking() { request: None, witnesses: Some(metadata.clone()), witness_acks: None, + invoice: None, }; let mut bytes = vec![WITNESS_VERSION]; 1u16.write(&mut bytes).unwrap(); @@ -102,6 +103,7 @@ fn ffor_witness_ack_archive_rejects_corruption_and_old_reader_downgrade() { request: None, witnesses: Some(metadata.clone()), witness_acks: Some(acks.clone()), + invoice: None, }; for mutation in 0..7 { let mut damaged = record(); diff --git a/lightning/src/ln/ffor_recovery/invoice.rs b/lightning/src/ln/ffor_recovery/invoice.rs new file mode 100644 index 00000000000..dd0faf297a1 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/invoice.rs @@ -0,0 +1,329 @@ +//! Bounded immutable exact invoice assignment and signed public route evidence. + +use super::*; +use crate::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; +use crate::ln::ffor::{FFORInvoiceIntent, FFORReceiverRecoveryContext, FFORWitnessRouteEvidence}; +use crate::ln::msgs::{ChannelAnnouncement, ChannelUpdate}; +use crate::prelude::*; +use crate::routing::gossip::verify_channel_announcement; +use crate::types::payment::PaymentSecret; +use crate::util::ser::LengthReadable; +use bitcoin::hashes::{sha256, sha256d, Hash}; +use bitcoin::secp256k1::{Message, Secp256k1}; +use bitcoin::Network; +use core::time::Duration; +use lightning_ffor::wire::{Init, Payload}; +use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RawBolt11Invoice}; +use lightning_types::routing::{RouteHint, RouteHintHop, RoutingFees}; + +const MAX_INVOICE_BYTES: usize = 4096; +const MAX_INVOICE_RECORD_BYTES: usize = 8192; +const MAX_DESCRIPTION_BYTES: usize = 639; +const MAX_ANNOUNCEMENT_BYTES: usize = 512; +const MAX_UPDATE_BYTES: usize = 256; + +#[derive(Clone)] +pub(crate) struct FFORInvoiceRecord { + pub(crate) context_digest: [u8; 32], + pub(crate) acknowledgement_digest: [u8; 32], + pub(crate) created_height: u32, + pub(crate) settlement_scid: u64, + pub(crate) settlement_cltv: u16, + pub(crate) intent: FFORInvoiceIntent, + pub(crate) route: FFORWitnessRouteEvidence, + pub(crate) invoice: String, +} + +fn write_bytes(bytes: &[u8], writer: &mut W) -> Result<(), io::Error> { + (bytes.len() as u16).write(writer)?; + writer.write_all(bytes) +} +fn read_bytes(reader: &mut R, maximum: usize) -> Result, DecodeError> { + let length = u16::read(reader)? as usize; + if length > maximum { + return Err(DecodeError::InvalidValue); + } + let mut bytes = vec![0; length]; + reader.read_exact(&mut bytes)?; + Ok(bytes) +} +impl Writeable for FFORInvoiceRecord { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + self.context_digest.write(writer)?; + self.acknowledgement_digest.write(writer)?; + self.created_height.write(writer)?; + self.settlement_scid.write(writer)?; + self.settlement_cltv.write(writer)?; + self.intent.expiry_seconds.write(writer)?; + self.intent.safety_margin_seconds.write(writer)?; + write_bytes(self.intent.description.as_bytes(), writer)?; + write_bytes(self.invoice.as_bytes(), writer)?; + write_bytes(&self.route.announcement.encode(), writer)?; + write_bytes(&self.route.update.encode(), writer) + } +} +impl Readable for FFORInvoiceRecord { + fn read(reader: &mut R) -> Result { + let context_digest = Readable::read(reader)?; + let acknowledgement_digest = Readable::read(reader)?; + let created_height = Readable::read(reader)?; + let settlement_scid = Readable::read(reader)?; + let settlement_cltv = Readable::read(reader)?; + let expiry_seconds = Readable::read(reader)?; + let safety_margin_seconds = Readable::read(reader)?; + let description = String::from_utf8(read_bytes(reader, MAX_DESCRIPTION_BYTES)?) + .map_err(|_| DecodeError::InvalidValue)?; + let invoice = String::from_utf8(read_bytes(reader, MAX_INVOICE_BYTES)?) + .map_err(|_| DecodeError::InvalidValue)?; + let announcement = ChannelAnnouncement::read_from_fixed_length_buffer( + &mut &read_bytes(reader, MAX_ANNOUNCEMENT_BYTES)?[..], + )?; + let update = ChannelUpdate::read_from_fixed_length_buffer( + &mut &read_bytes(reader, MAX_UPDATE_BYTES)?[..], + )?; + Ok(Self { + context_digest, + acknowledgement_digest, + created_height, + settlement_scid, + settlement_cltv, + intent: FFORInvoiceIntent { description, expiry_seconds, safety_margin_seconds }, + route: FFORWitnessRouteEvidence { announcement, update }, + invoice, + }) + } +} + +pub(crate) fn terms(context: &FFORReceiverRecoveryContext) -> Result<&Init, DecodeError> { + match &context.setup().init().payload { + Payload::Init(init) if !init.hash_chain && context.setup().vouchers().len() == 1 => { + Ok(init) + }, + _ => Err(DecodeError::InvalidValue), + } +} + +pub(crate) fn acknowledgements_digest( + registration: &FFORReceiverWitnessRegistration, acks: &FFORReceiverWitnessAcknowledgements, +) -> Result<[u8; 32], DecodeError> { + acks.validate(registration).map_err(|_| DecodeError::InvalidValue)?; + if acks.acknowledgements().len() != registration.witnesses().len() { + return Err(DecodeError::InvalidValue); + } + let mut bytes = b"ffor/receiver/invoice-ack-set/v1".to_vec(); + bytes.extend_from_slice(&acks.encode()); + Ok(sha256::Hash::hash(&bytes).to_byte_array()) +} + +pub(crate) fn validate_route( + context: &FFORReceiverRecoveryContext, registration: &FFORReceiverWitnessRegistration, + route: &FFORWitnessRouteEvidence, now: u64, +) -> Result { + let init = terms(context)?; + // The signed Init restriction is the honest settlement peer's guard against a payer + // bypassing the advisory BOLT 11 hint. Every allowed witness must be provisioned and ACKed. + let allowed = init.witness_peers.as_ref().ok_or(DecodeError::InvalidValue)?; + if allowed.is_empty() + || allowed.len() != registration.witnesses().len() + || !registration + .witnesses() + .iter() + .all(|entry| allowed.iter().any(|key| key == &entry.witness_node_id())) + { + return Err(DecodeError::InvalidValue); + } + let ann = &route.announcement.contents; + let update = &route.update.contents; + let witness = if ann.node_id_1.as_slice() == context.settlement_node_id().serialize() { + PublicKey::from_slice(ann.node_id_2.as_slice()).map_err(|_| DecodeError::InvalidValue)? + } else if ann.node_id_2.as_slice() == context.settlement_node_id().serialize() { + PublicKey::from_slice(ann.node_id_1.as_slice()).map_err(|_| DecodeError::InvalidValue)? + } else { + return Err(DecodeError::InvalidValue); + }; + let source = if update.channel_flags & 1 == 0 { ann.node_id_1 } else { ann.node_id_2 }; + let fee = lightning_ffor::amounts::FeePolicy { + base_msat: init.fee_base_msat, + proportional_millionths: init.fee_proportional_millionths, + }; + let gross = fee + .gross_msat(context.setup().vouchers()[0].amount_msat) + .map_err(|_| DecodeError::InvalidValue)?; + if witness == context.receiver_node_id() + || witness == context.settlement_node_id() + || ann.node_id_1.as_slice() >= ann.node_id_2.as_slice() + || !registration.witnesses().iter().any(|entry| entry.witness_node_id() == witness) + || source.as_slice() != witness.serialize() + || ann.chain_hash != context.chain_hash() + || update.chain_hash != context.chain_hash() + || ann.short_channel_id == 0 + || ann.short_channel_id != update.short_channel_id + || !ann.excess_data.is_empty() + || !update.excess_data.is_empty() + || ann.features.requires_unknown_bits() + || route.announcement.serialized_length() > MAX_ANNOUNCEMENT_BYTES + || route.update.serialized_length() > MAX_UPDATE_BYTES + || update.channel_flags & !1 != 0 + || update.message_flags & !1 != 0 + || update.cltv_expiry_delta == 0 + || update.htlc_minimum_msat > gross + || update.htlc_maximum_msat < gross + || update.htlc_maximum_msat > crate::ln::msgs::MAX_VALUE_MSAT + || u64::from(update.timestamp) < now.saturating_sub(14 * 24 * 60 * 60) + || u64::from(update.timestamp) > now.saturating_add(24 * 60 * 60) + { + return Err(DecodeError::InvalidValue); + } + lightning_ffor::amounts::FeePolicy { + base_msat: update.fee_base_msat, + proportional_millionths: update.fee_proportional_millionths, + } + .gross_msat(gross) + .map_err(|_| DecodeError::InvalidValue)?; + let secp = Secp256k1::verification_only(); + verify_channel_announcement(&route.announcement, &secp) + .map_err(|_| DecodeError::InvalidValue)?; + let digest = sha256d::Hash::hash(&update.encode()).to_byte_array(); + secp.verify_ecdsa(&Message::from_digest(digest), &route.update.signature, &witness) + .map_err(|_| DecodeError::InvalidValue)?; + Ok(witness) +} + +impl FFORInvoiceRecord { + pub(crate) fn digest(&self) -> [u8; 32] { + sha256::Hash::hash(self.invoice.as_bytes()).to_byte_array() + } + pub(crate) fn expires_at(&self) -> Result { + let invoice: Bolt11Invoice = self.invoice.parse().map_err(|_| DecodeError::InvalidValue)?; + invoice + .duration_since_epoch() + .as_secs() + .checked_add(invoice.expiry_time().as_secs()) + .ok_or(DecodeError::InvalidValue) + } + pub(crate) fn matches_intent( + &self, intent: &FFORInvoiceIntent, route: &FFORWitnessRouteEvidence, + ) -> bool { + self.intent == *intent && self.route == *route + } + pub(crate) fn unsigned( + &self, context: &FFORReceiverRecoveryContext, + registration: &FFORReceiverWitnessRegistration, timestamp: u64, secret: PaymentSecret, + ) -> Result { + let init = terms(context)?; + let voucher = &context.setup().vouchers()[0]; + let witness = validate_route(context, registration, &self.route, timestamp)?; + let remaining = init + .settlement_deadline + .checked_sub(self.created_height) + .and_then(|blocks| u64::from(blocks).checked_mul(480)) + .and_then(|seconds| seconds.checked_sub(u64::from(self.intent.safety_margin_seconds))) + .ok_or(DecodeError::InvalidValue)?; + let expiry = core::cmp::min(u64::from(self.intent.expiry_seconds), remaining); + if expiry == 0 + || self.intent.description.len() > MAX_DESCRIPTION_BYTES + || self.settlement_scid == 0 + || self.settlement_cltv == 0 + { + return Err(DecodeError::InvalidValue); + } + let currency = + Network::from_chain_hash(context.chain_hash()).ok_or(DecodeError::InvalidValue)?.into(); + let update = &self.route.update.contents; + let hint = RouteHint(vec![ + RouteHintHop { + src_node_id: witness, + short_channel_id: update.short_channel_id, + fees: RoutingFees { + base_msat: update.fee_base_msat, + proportional_millionths: update.fee_proportional_millionths, + }, + cltv_expiry_delta: update.cltv_expiry_delta, + htlc_minimum_msat: None, + htlc_maximum_msat: None, + }, + RouteHintHop { + src_node_id: context.settlement_node_id(), + short_channel_id: self.settlement_scid, + fees: RoutingFees { + base_msat: init.fee_base_msat, + proportional_millionths: init.fee_proportional_millionths, + }, + cltv_expiry_delta: self.settlement_cltv, + htlc_minimum_msat: None, + htlc_maximum_msat: None, + }, + ]); + InvoiceBuilder::new(currency) + .description(self.intent.description.clone()) + .duration_since_epoch(Duration::from_secs(timestamp)) + .payee_pub_key(context.receiver_node_id()) + .payment_hash(sha256::Hash::from_byte_array(voucher.payment_hash)) + .payment_secret(secret) + .amount_milli_satoshis(voucher.amount_msat) + .min_final_cltv_expiry_delta(u64::from(MIN_FINAL_CLTV_EXPIRY_DELTA)) + .expiry_time(Duration::from_secs(expiry)) + .private_route(hint) + .build_raw() + .map_err(|_| DecodeError::InvalidValue) + } + pub(crate) fn validate( + &self, context: &FFORReceiverRecoveryContext, + registration: &FFORReceiverWitnessRegistration, acks: &FFORReceiverWitnessAcknowledgements, + ) -> Result<(), DecodeError> { + if self.context_digest != context.context_digest() + || self.acknowledgement_digest != acknowledgements_digest(registration, acks)? + || self.invoice.is_empty() + || self.invoice.len() > MAX_INVOICE_BYTES + || self.serialized_length() > MAX_INVOICE_RECORD_BYTES + { + return Err(DecodeError::InvalidValue); + } + let invoice: Bolt11Invoice = self.invoice.parse().map_err(|_| DecodeError::InvalidValue)?; + let raw = self.unsigned( + context, + registration, + invoice.duration_since_epoch().as_secs(), + *invoice.payment_secret(), + )?; + if *invoice.into_signed_raw().raw_invoice() != raw { + return Err(DecodeError::InvalidValue); + } + Ok(()) + } +} + +impl FFORRecoveryRegistry { + pub(crate) fn get_invoice(&self, key: &FFORRecoveryKey) -> Option<&FFORInvoiceRecord> { + self.entries + .iter() + .find(|entry| entry.key == *key) + .and_then(|entry| entry.record.invoice.as_ref()) + } + pub(crate) fn prepare_invoice( + &mut self, key: &FFORRecoveryKey, invoice: FFORInvoiceRecord, + ) -> Result, FFORRecoveryError> { + let index = self + .entries + .iter() + .position(|entry| entry.key == *key) + .ok_or(FFORRecoveryError::ConflictingRecord)?; + let previous = &self.entries[index]; + if previous.record.invoice.is_some() { + return Err(FFORRecoveryError::ConflictingRecord); + } + let entry = Entry::new(StoredSetup { + setup: previous.record.setup.clone(), + canonical_book: previous.record.canonical_book.clone(), + activation: previous.record.activation.clone(), + request: previous.record.request.clone(), + witnesses: previous.record.witnesses.clone(), + witness_acks: previous.record.witness_acks.clone(), + invoice: Some(invoice), + })?; + self.prepare_replacement(index, entry, 0) + } +} + +#[cfg(test)] +pub(crate) mod tests; diff --git a/lightning/src/ln/ffor_recovery/invoice/tests.rs b/lightning/src/ln/ffor_recovery/invoice/tests.rs new file mode 100644 index 00000000000..1178dc244c2 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/invoice/tests.rs @@ -0,0 +1,79 @@ +use super::*; + +// Called by the real-channel integration fixture, so these archive mutations cannot accidentally +// invent invoice admission authority from synthetic commitment identities. +pub(crate) fn check_archive(registry: &FFORRecoveryRegistry, key: FFORRecoveryKey) { + let bytes = registry.encode(); + assert_eq!(bytes[0], INVOICE_VERSION); + let restored = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); + assert_eq!(restored.encode(), bytes); + let record = restored.get_invoice(&key).unwrap().clone(); + assert!(record.serialized_length() < MAX_INVOICE_RECORD_BYTES); + let entry = restored.entries.iter().find(|entry| entry.key == key).unwrap(); + let context = + entry.record.activation.as_ref().unwrap().receiver_context(&entry.record.setup).unwrap(); + let registration = entry.record.witnesses.as_ref().unwrap(); + let acks = entry.record.witness_acks.as_ref().unwrap(); + for fault in 0..9 { + let mut bad = record.clone(); + match fault { + 0 => bad.context_digest[0] ^= 1, + 1 => bad.acknowledgement_digest[0] ^= 1, + 2 => bad.intent.description.push('!'), + 3 => bad.intent.expiry_seconds += 1, + 4 => bad.intent.safety_margin_seconds = u32::MAX, + 5 => bad.settlement_scid ^= 1, + 6 => bad.settlement_cltv += 1, + 7 => bad.invoice.push('x'), + 8 => bad.route.update.contents.fee_base_msat += 1, + _ => unreachable!(), + } + assert!(bad.validate(&context, registration, acks).is_err(), "fault {fault}"); + } + let mut old_reader = bytes.clone(); + old_reader[0] = WITNESS_ACK_VERSION; + assert!(FFORRecoveryRegistry::read(&mut &old_reader[..]).is_err()); + let mut future = bytes.clone(); + future[0] = INVOICE_VERSION + 1; + assert!(FFORRecoveryRegistry::read(&mut &future[..]).is_err()); + let encoded = record.encode(); + for end in [0, 31, 63, 87, encoded.len() - 1] { + assert!(FFORInvoiceRecord::read(&mut &encoded[..end]).is_err()); + } + let mut oversized_description = encoded.clone(); + oversized_description[86..88].copy_from_slice(&640u16.to_be_bytes()); + assert!(FFORInvoiceRecord::read(&mut &oversized_description[..]).is_err()); + + let mut base = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); + let index = base.entries.iter().position(|entry| entry.key == key).unwrap(); + let old = base.entries.remove(index); + let old_bytes = old.encoded_bytes; + let old_reservation = old.reserved_transition_bytes(); + let mut before_record = old.record; + before_record.invoice = None; + let before = Entry::new(before_record).unwrap(); + base.encoded_bytes = base.encoded_bytes - old_bytes + before.encoded_bytes; + base.reserved_transition_bytes = + base.reserved_transition_bytes - old_reservation + before.reserved_transition_bytes(); + base.entries.insert(index, before); + let before_bytes = base.encode(); + let real_count = base.encoded_bytes; + base.encoded_bytes = MAX_ENCODED_BYTES - base.reserved_transition_bytes; + assert!(matches!( + base.prepare_invoice(&key, record.clone()), + Err(FFORRecoveryError::CapacityExceeded) + )); + assert!(base.get_invoice(&key).is_none()); + base.encoded_bytes = real_count; + assert_eq!(base.encode(), before_bytes); + { + let _cancelled = base.prepare_invoice(&key, record.clone()).unwrap(); + } + assert!(base.get_invoice(&key).is_none()); + base.prepare_invoice(&key, record.clone()).unwrap().commit(); + assert_eq!(base.encode(), bytes); + assert!(matches!( + base.prepare_invoice(&key, record), + Err(FFORRecoveryError::ConflictingRecord) + )); +} diff --git a/lightning/src/ln/ffor_recovery/request.rs b/lightning/src/ln/ffor_recovery/request.rs index 2d662071659..1fec4c625f9 100644 --- a/lightning/src/ln/ffor_recovery/request.rs +++ b/lightning/src/ln/ffor_recovery/request.rs @@ -154,6 +154,7 @@ impl FFORRecoveryRegistry { request: Some(request.clone()), witnesses: None, witness_acks: None, + invoice: None, })?; // Both forms were charged the full record allowance before Init. Promotion cannot increase // total used plus reserved bytes, and the borrowed permit prevents a conflicting insertion. diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs index 788f789a162..53501a3f502 100644 --- a/lightning/src/ln/ffor_recovery/tests.rs +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -131,6 +131,7 @@ fn stored(record: FFORReceiverSetup) -> StoredSetup { request: None, witnesses: None, witness_acks: None, + invoice: None, } } @@ -300,7 +301,7 @@ fn ffor_recovery_reader_refuses_future_schema_missing_fields_and_truncation() { assert!(decode(&framed[..length]).is_err(), "accepted prefix {}", length); } let mut future = framed.clone(); - future[0] = WITNESS_ACK_VERSION + 1; + future[0] = INVOICE_VERSION + 1; assert!(matches!(decode(&future), Err(DecodeError::UnknownRequiredFeature))); // Empty TLV records omit both mandatory fields. assert!(decode(&frame(&[vec![0]])).is_err()); diff --git a/lightning/src/ln/ffor_recovery/witness.rs b/lightning/src/ln/ffor_recovery/witness.rs index 236d2458332..439fdba6f93 100644 --- a/lightning/src/ln/ffor_recovery/witness.rs +++ b/lightning/src/ln/ffor_recovery/witness.rs @@ -188,6 +188,7 @@ impl FFORRecoveryRegistry { .unwrap_or_else(|| FFORReceiverWitnessAcknowledgements::empty(&witnesses)), ), witnesses: Some(witnesses), + invoice: previous.record.invoice.clone(), })?; let header_growth = if self.version() < REQUEST_VERSION { 2 } else { 0 }; self.prepare_replacement(index, entry, header_growth) diff --git a/lightning/src/ln/ffor_recovery/witness_ack.rs b/lightning/src/ln/ffor_recovery/witness_ack.rs index 8c6b29723c3..7c111e180bd 100644 --- a/lightning/src/ln/ffor_recovery/witness_ack.rs +++ b/lightning/src/ln/ffor_recovery/witness_ack.rs @@ -116,6 +116,7 @@ impl FFORRecoveryRegistry { request: previous.record.request.clone(), witnesses: previous.record.witnesses.clone(), witness_acks: Some(acks), + invoice: previous.record.invoice.clone(), })?; self.prepare_replacement(index, entry, 0) } diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 7b0db4fbbe5..78742ff9f25 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -557,6 +557,13 @@ impl<'a> TestChainMonitor<'a> { } } impl<'a> chain::Watch for TestChainMonitor<'a> { + fn validate_and_publish_ffor_invoice( + &self, check: &crate::ln::ffor::FFORInvoiceMonitorCheck, + publish: &mut dyn FnMut() -> Result<(), ()>, + ) -> Result { + self.chain_monitor.validate_and_publish_ffor_invoice(check, publish) + } + fn watch_channel( &self, channel_id: ChannelId, monitor: ChannelMonitor, ) -> Result { @@ -1802,6 +1809,7 @@ impl NodeSigner for TestNodeSigner { pub struct TestKeysInterface { pub unavailable_ffor_signer: AtomicBool, + pub wrong_invoice_signer: AtomicBool, pub unavailable_node_ecdh: AtomicBool, pub backing: DynKeysInterface, pub override_random_bytes: Mutex>, @@ -1854,6 +1862,12 @@ impl NodeSigner for TestKeysInterface { fn sign_invoice( &self, invoice: &RawBolt11Invoice, recipient: Recipient, ) -> Result { + if self.wrong_invoice_signer.load(Ordering::Acquire) { + return Ok(Secp256k1::new().sign_ecdsa_recoverable( + &secp256k1::Message::from_digest(invoice.signable_hash()), + &SecretKey::from_slice(&[3; 32]).unwrap(), + )); + } self.backing.sign_invoice(invoice, recipient) } @@ -1966,6 +1980,7 @@ impl TestKeysInterface { Self { backing: DynKeysInterface::new(backing), unavailable_ffor_signer: AtomicBool::new(false), + wrong_invoice_signer: AtomicBool::new(false), unavailable_node_ecdh: AtomicBool::new(false), override_random_bytes: Mutex::new(None), disable_revocation_policy_check: false, From 6f91649374195ab7022e480eff0981fd1104e6fb Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Mon, 21 Sep 2026 09:57:38 -0400 Subject: [PATCH 21/22] feat(ffor): journal cooperative voucher outcomes --- FFOR.md | 55 ++++- lightning/src/ln/channel.rs | 14 +- lightning/src/ln/channel/ffor.rs | 18 ++ lightning/src/ln/channel/ffor/drain.rs | 87 ++++++- lightning/src/ln/channel/ffor/drain/tests.rs | 90 +++++++ .../src/ln/channelmanager/ffor_activation.rs | 1 + .../channelmanager/ffor_activation/close.rs | 2 +- .../ffor_activation/close/tests.rs | 77 ++++++ .../channelmanager/ffor_activation/outcome.rs | 78 +++++++ .../ffor_activation/receipt/tests.rs | 17 +- .../ffor_activation/reestablish.rs | 3 +- lightning/src/ln/ffor.rs | 2 + lightning/src/ln/ffor/journal.rs | 193 +++++++++++++++ lightning/src/ln/ffor_recovery.rs | 24 +- .../src/ln/ffor_recovery/activation/close.rs | 17 ++ .../ffor_recovery/activation/tests/close.rs | 221 +++++++++++++++++- .../ffor_recovery/activation/tests/witness.rs | 9 +- lightning/src/ln/ffor_recovery/tests.rs | 2 +- 18 files changed, 883 insertions(+), 27 deletions(-) create mode 100644 lightning/src/ln/channelmanager/ffor_activation/outcome.rs create mode 100644 lightning/src/ln/ffor/journal.rs diff --git a/FFOR.md b/FFOR.md index da3b4a8e5c3..7f574c398a3 100644 --- a/FFOR.md +++ b/FFOR.md @@ -392,10 +392,44 @@ assignment adds required field 12 and archive schema 7, bounding the signed stri 4096 bytes and its record to 8192 bytes. The assignment is permanent after failed publication, expiry or restart, and it is not a payment success signal. +## Cooperative outcome journal + +A new drain initializes a versioned journal with one resolved bit and one fulfilled bit per +book slot, at most 483 slots and two 61-byte bitmaps. The stock revoke-and-ack removal of an +inbound LocalRemoved HTLC records the exact owned voucher's slot at the same point it adds a +fulfilled amount to the channel balance, without a new fallible step after the existing +commitment-round preflight. The journal is native accounting only. It is never derived from +the signed settled bitmap or from known preimages, and fulfilled is always a subset of +resolved. A preimage learned after a signed failure protects the monitor but leaves that slot +failed. + +Restore validation requires exact bitmap lengths, zero padding, the subset rule and, for each +slot, either a resolved bit or the voucher still pending as a stock HTLC, never both; a +fulfilled slot must have a known preimage. The drain completion proof requires every slot +resolved and every signed settled slot fulfilled, binds the whole journal into a distinct +`ffor/native-drain-complete/v2` digest domain, and carries it into the retained ClosedDrain +under required-even field 14 with archive version 8. Legacy drains and archives without a +journal keep the original digest domain, remain readable, and never acquire invented outcomes. +Older readers reject journaled records with `UnknownRequiredFeature` instead of dropping them. +A journaled Closed proof also raises a close-only archive past the request version, and that +framing growth is charged inside the existing 512-byte terminal reservation. + +`ffor_receiver_voucher_outcome` is a historical, manager-instance-bound getter keyed by the +current epoch context, the one-based slot, the exact payment hash and amount. It reports +Fulfilled or Failed only from a fully finalized journal after the latest native manager write +completed, including a fresh barrier after restore. Pending drains, force-closed epochs and +legacy absence return no outcome. It changes no balances and emits no events. On-chain +outcomes remain a separate unimplemented boundary: a future proof must join the stock +monitor's irrevocably resolved HTLC evidence to the actual confirmed funding spend, an owned +inbound voucher output, the original commitment identity and completed monitor persistence, +with explicit partially drained commitment and reorg analysis. A preimage or a generic +spendable output does not identify a payment. + ## Validation -The current focused native suite passes 169 tests, including 13 invoice tests. Native -no-default-features and documentation builds with broken intra-doc links denied also pass. +The current focused native suite passes 172 tests, including 13 invoice tests and the +cooperative outcome journal checks. Native no-default-features and documentation builds with +broken intra-doc links denied also pass. The FFOR tests exercise real two-node commitment rounds, both funding directions, asymmetric dust limits and contest delays, signature corruption, stale monitors, @@ -514,6 +548,19 @@ runtime/height lock inversion in witness registration and release, which now use consistent registry, runtime, height and persistence order. An opt-in exporter writes that fixture only when `FFOR_NODE_INVOICE_FIXTURE_DIR` is set. +Journal coverage extends the cooperative drain matrix across both funders, signed settled, +learned and failed slots, delayed monitors and restart mid-round: outcomes are absent before +the retained Closed proof, exact afterwards, refused for wrong slots, hashes and amounts, +absent again on a restored manager until its fresh write completes, and preserved as +archive-only history after the channel is force-closed away. A late receipt after a signed +failure reports Failed. A force-closed drain reports nothing. Archive tests cover the legacy +digest without a journal, journaled round trips at the maximum 483-slot book inside the +terminal reservation, replacement refusal, old readers, downgraded and future version bytes, +and malformed journals with the wrong domain, incomplete coverage, padding bits, fulfilled +bits outside resolved, the wrong slot count and a signed settled slot that stock accounting +never fulfilled. Drain tests cover legacy records restoring without a journal, never +acquiring one, and closed drains freezing theirs. + Seven receipt-import integration tests exercise both funders, delayed monitor persistence, idempotent retries, crash recovery from a failed write using the prior durable monitor, missing peer/monitor refusal, unregistered witnesses, changed identities, expired epochs, @@ -536,6 +583,6 @@ deadline before claim safety ends. Production transport must bind the receiver facade to actual authenticated connections and deliver acknowledgement retries in the required order. Durable witness mailbox recovery, receipt-import orchestration, deadline enforcement, the application -invoice publication runtime and authoritative payment outcomes remain separate required -boundaries. None can be inferred from durable setup, activation or +invoice publication runtime, joining journaled outcomes to the application payment ledger and +on-chain outcomes remain separate required boundaries. None can be inferred from durable setup, activation or a successful private protocol test. diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index faf59a71564..08f46f0e9db 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -8747,14 +8747,26 @@ where let pending_outbound_htlcs: &mut Vec<_> = &mut self.context.pending_outbound_htlcs; let expecting_peer_commitment_signed = &mut self.context.expecting_peer_commitment_signed; + let ffor_receiver_book = &mut self.context.ffor_receiver_book; // We really shouldnt have two passes here, but retain gives a non-mutable ref (Rust bug) pending_inbound_htlcs.retain(|htlc| { if let &InboundHTLCState::LocalRemoved(ref reason) = &htlc.state { log_trace!(logger, " ...removing inbound LocalRemoved {}", &htlc.payment_hash); - if let &InboundHTLCRemovalReason::Fulfill(_, _) = reason { + let fulfilled = matches!(reason, InboundHTLCRemovalReason::Fulfill(_, _)); + if fulfilled { value_to_self_msat_diff += htlc.amount_msat as i64; } + // An owned voucher's outcome is journaled with the same stock accounting. + if let Some(book) = ffor_receiver_book.as_mut() { + book.record_drain_removal( + htlc.htlc_id, + htlc.payment_hash, + htlc.amount_msat, + htlc.cltv_expiry, + fulfilled, + ); + } *expecting_peer_commitment_signed = true; false } else { diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index d80cdffa4a2..e95453ac302 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -141,6 +141,24 @@ impl FFORReceiverBook { Ok(()) } + /// Record one irrevocable inbound removal in the drain journal at the stock revoke-and-ack + /// point. Ownership requires the exact voucher identity. Restore validation guarantees the + /// slot is still unresolved whenever a journal exists, so this never fails after preflight. + pub(super) fn record_drain_removal( + &mut self, htlc_id: u64, payment_hash: PaymentHash, amount_msat: u64, cltv_expiry: u32, + fulfilled: bool, + ) { + let slot = self.vouchers.iter().position(|v| { + v.htlc_id == htlc_id + && v.payment_hash == payment_hash + && v.amount_msat == amount_msat + && v.cltv_expiry == cltv_expiry + }); + if let (Some(slot), Some(drain)) = (slot, self.drain.as_mut()) { + drain.record_removal(slot, fulfilled); + } + } + fn owns(&self, htlc_id: u64) -> bool { self.received.iter().any(|received| received.voucher.htlc_id == htlc_id) } diff --git a/lightning/src/ln/channel/ffor/drain.rs b/lightning/src/ln/channel/ffor/drain.rs index 793dc48676e..adabe54706a 100644 --- a/lightning/src/ln/channel/ffor/drain.rs +++ b/lightning/src/ln/channel/ffor/drain.rs @@ -3,6 +3,7 @@ //! permission to drain, and never releases the ordinary mutation fence before final persistence. use super::*; +use crate::ln::ffor::journal::FFORCooperativeJournal; use crate::ln::ffor_recovery::FFORReceiverCloseRecord; pub(super) struct FFORReceiverDrain { @@ -15,6 +16,8 @@ pub(super) struct FFORReceiverDrain { closed: bool, // Instance-local permission follows a successful manager write. Never restored from disk. enabled: bool, + // Native per-slot outcomes. Legacy drains restored without one never acquire outcomes. + journal: Option, } impl_writeable_tlv_based!(FFORReceiverDrain, { @@ -26,6 +29,7 @@ impl_writeable_tlv_based!(FFORReceiverDrain, { (10, activation_hash, required), (12, feerate_per_kw, required), (11, enabled, (static_value, false)), + (14, journal, option), }); /// Point-in-time proof that both current commitments are empty and all stock rounds completed. @@ -40,9 +44,14 @@ pub(crate) struct FFORReceiverDrainCompletion { channel_id: ChannelId, funding_txo: OutPoint, monitor_update_id: u64, + journal: Option, } impl FFORReceiverDrainCompletion { + /// The complete native outcome journal, absent only for a legacy drain without one. + pub(crate) fn journal(&self) -> Option<&FFORCooperativeJournal> { + self.journal.as_ref() + } pub(crate) fn epoch_id(&self) -> [u8; 32] { self.epoch_id } @@ -67,17 +76,24 @@ impl FFORReceiverDrainCompletion { self.funding_txo, self.commitments, self.monitor_update_id, + self.journal.as_ref(), ) } } /// Recompute a retained completion digest. This does not construct or authenticate a channel proof. +/// A journal binds every resolved and fulfilled slot under a distinct domain; legacy records +/// without one keep the original domain. pub(crate) fn ffor_drain_completion_digest( epoch_id: [u8; 32], activation_hash: [u8; 32], acknowledgement_hash: [u8; 32], channel_id: ChannelId, funding_txo: OutPoint, commitments: FFORVoucherCommitments, - monitor_update_id: u64, + monitor_update_id: u64, journal: Option<&FFORCooperativeJournal>, ) -> [u8; 32] { - let mut bytes = b"ffor/native-drain-complete/v1".to_vec(); + let mut bytes = if journal.is_some() { + b"ffor/native-drain-complete/v2".to_vec() + } else { + b"ffor/native-drain-complete/v1".to_vec() + }; bytes.extend_from_slice(&epoch_id); bytes.extend_from_slice(&activation_hash); bytes.extend_from_slice(&acknowledgement_hash); @@ -88,9 +104,23 @@ pub(crate) fn ffor_drain_completion_digest( bytes.extend_from_slice(&identity.number.to_be_bytes()); bytes.extend_from_slice(identity.txid.as_byte_array()); } + if let Some(journal) = journal { + bytes.extend_from_slice(&journal.digest_bytes()); + } Sha256::hash(&bytes).to_byte_array() } +impl FFORReceiverDrain { + pub(super) fn record_removal(&mut self, slot: usize, fulfilled: bool) { + if self.closed { + return; + } + if let Some(journal) = &mut self.journal { + journal.record(slot, fulfilled); + } + } +} + impl FFORReceiverBook { pub(super) fn is_closed(&self) -> bool { self.drain.as_ref().map_or(false, |drain| drain.closed) @@ -195,6 +225,21 @@ where _ => return Err(DecodeError::InvalidValue), } } + if let Some(journal) = &drain.journal { + journal.validate(book.vouchers.len())?; + for (slot, voucher) in book.vouchers.iter().enumerate() { + let pending = + self.pending_inbound_htlcs.iter().any(|h| h.htlc_id == voucher.htlc_id); + if journal.is_resolved(slot) == pending { + return Err(DecodeError::InvalidValue); + } + if journal.is_fulfilled(slot) + && !drain.known_preimages.iter().any(|(id, _)| *id == voucher.htlc_id) + { + return Err(DecodeError::InvalidValue); + } + } + } for update in &self.holding_cell_htlc_updates { match update { HTLCUpdateAwaitingACK::ClaimHTLC { htlc_id, payment_preimage, .. } @@ -272,6 +317,7 @@ where completion_hash: None, closed: false, enabled: false, + journal: FFORCooperativeJournal::new(book.vouchers.len()), }); book.fence.as_mut().unwrap().phase = FFORReceiverFencePhase::Draining; Ok(()) @@ -334,13 +380,12 @@ where self.context.ffor_drain_enabled() } - pub(crate) fn ffor_receiver_drain_binding(&self) -> Option<([u8; 32], Vec, bool)> { - self.context - .ffor_receiver_book - .as_ref()? - .drain - .as_ref() - .map(|drain| (drain.acknowledgement_hash, drain.settled.clone(), drain.closed)) + pub(crate) fn ffor_receiver_drain_binding( + &self, + ) -> Option<([u8; 32], Vec, bool, Option)> { + self.context.ffor_receiver_book.as_ref()?.drain.as_ref().map(|drain| { + (drain.acknowledgement_hash, drain.settled.clone(), drain.closed, drain.journal.clone()) + }) } pub(crate) fn ffor_receiver_drain_activation_hash(&self) -> Option<[u8; 32]> { @@ -351,12 +396,24 @@ where self.context.ffor_receiver_book.as_ref()?.drain.as_ref()?.completion_hash } + /// The native outcome journal of the current drain, if this drain has one. + pub(crate) fn ffor_receiver_drain_journal(&self) -> Option<&FFORCooperativeJournal> { + self.context.ffor_receiver_book.as_ref()?.drain.as_ref()?.journal.as_ref() + } + pub(crate) fn validate_ffor_drain(&self) -> Result<(), DecodeError> { let book = self.context.ffor_receiver_book.as_ref().ok_or(DecodeError::InvalidValue)?; if book.is_closed() { + let drain = book.drain.as_ref().unwrap(); + if let Some(journal) = &drain.journal { + journal.validate(book.vouchers.len())?; + if !journal.all_resolved() || !journal.covers_settled(&drain.settled) { + return Err(DecodeError::InvalidValue); + } + } return if book.fence.is_none() && book.abort_reason.is_none() - && book.drain.as_ref().unwrap().completion_hash.is_some() + && drain.completion_hash.is_some() { Ok(()) } else { @@ -552,6 +609,14 @@ where &monitor.holder, &self.funding.channel_transaction_parameters, )?; + if let Some(journal) = &drain.journal { + if !journal.all_resolved() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + if !journal.covers_settled(&drain.settled) { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + } Ok(FFORReceiverDrainCompletion { epoch_id: book.epoch_id, activation_hash: fence.activation_hash, @@ -563,6 +628,7 @@ where holder: verification::commitment_identity(&holder), counterparty: verification::commitment_identity(&counterparty), }, + journal: drain.journal.clone(), }) } @@ -577,6 +643,7 @@ where if book.epoch_id != completion.epoch_id || fence.activation_hash != completion.activation_hash || drain.acknowledgement_hash != completion.acknowledgement_hash + || drain.journal != completion.journal || self.context.latest_monitor_update_id != completion.monitor_update_id || !self.context.pending_inbound_htlcs.is_empty() || !matches!( diff --git a/lightning/src/ln/channel/ffor/drain/tests.rs b/lightning/src/ln/channel/ffor/drain/tests.rs index c09cbf34b1f..43e37734871 100644 --- a/lightning/src/ln/channel/ffor/drain/tests.rs +++ b/lightning/src/ln/channel/ffor/drain/tests.rs @@ -1,5 +1,6 @@ use super::*; use crate::ln::channelmanager::ffor_recovery_tests::install_ffor_activation_for_test; +use crate::ln::ffor::FFORCommitment; use crate::ln::ffor_recovery::FFORReceiverActivation; use crate::ln::ffor_tests::quiescence::{complete_handshake, register_signed, request}; use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; @@ -226,3 +227,92 @@ fn ffor_drain_restored_permission_and_duplicate_hash_fail_closed() { )); } } + +/// The drain layout before the native outcome journal existed. +struct LegacyDrain { + acknowledgement_hash: [u8; 32], + activation_hash: [u8; 32], + feerate_per_kw: u32, + settled: Vec, + known_preimages: Vec<(u64, PaymentPreimage)>, + completion_hash: Option<[u8; 32]>, + closed: bool, +} +impl_writeable_tlv_based!(LegacyDrain, { + (0, acknowledgement_hash, required), + (2, settled, required_vec), + (4, known_preimages, required_vec), + (6, completion_hash, option), + (8, closed, required), + (10, activation_hash, required), + (12, feerate_per_kw, required), +}); + +#[test] +fn ffor_drain_legacy_records_restore_without_a_journal_and_never_acquire_one() { + let legacy = LegacyDrain { + acknowledgement_hash: [1; 32], + activation_hash: [2; 32], + feerate_per_kw: 253, + settled: vec![0], + known_preimages: vec![], + completion_hash: None, + closed: false, + }; + let mut restored = FFORReceiverDrain::read(&mut &legacy.encode()[..]).unwrap(); + assert!(restored.journal.is_none()); + assert!(!restored.enabled); + // A restored legacy drain records nothing at the stock removal point. + restored.record_removal(0, true); + assert!(restored.journal.is_none()); + assert_eq!(restored.encode(), legacy.encode()); + assert_eq!( + ffor_drain_completion_digest( + [3; 32], + [2; 32], + [1; 32], + ChannelId([4; 32]), + OutPoint { txid: Txid::from_byte_array([5; 32]), index: 0 }, + FFORVoucherCommitments { + holder: FFORCommitment { number: 5, txid: Txid::from_byte_array([6; 32]) }, + counterparty: FFORCommitment { number: 5, txid: Txid::from_byte_array([7; 32]) }, + }, + 6, + None, + ), + { + let mut bytes = b"ffor/native-drain-complete/v1".to_vec(); + bytes.extend_from_slice(&[3; 32]); + bytes.extend_from_slice(&[2; 32]); + bytes.extend_from_slice(&[1; 32]); + bytes.extend_from_slice(&[4; 32]); + bytes.extend_from_slice( + &OutPoint { txid: Txid::from_byte_array([5; 32]), index: 0 }.encode(), + ); + bytes.extend_from_slice(&6u64.to_be_bytes()); + for (number, txid) in [(5u64, [6u8; 32]), (5, [7; 32])] { + bytes.extend_from_slice(&number.to_be_bytes()); + bytes.extend_from_slice(&txid); + } + Sha256::hash(&bytes).to_byte_array() + } + ); + // New drains carry a journal that old readers must refuse rather than drop. + let mut journaled = FFORReceiverDrain::read(&mut &legacy.encode()[..]).unwrap(); + journaled.journal = FFORCooperativeJournal::new(8); + journaled.record_removal(0, true); + journaled.record_removal(1, false); + assert!(matches!( + LegacyDrain::read(&mut &journaled.encode()[..]), + Err(DecodeError::UnknownRequiredFeature) + )); + let again = FFORReceiverDrain::read(&mut &journaled.encode()[..]).unwrap(); + assert_eq!(again.journal, journaled.journal); + assert!(again.journal.as_ref().unwrap().is_fulfilled(0)); + assert!(again.journal.as_ref().unwrap().is_resolved(1)); + assert!(!again.journal.as_ref().unwrap().is_fulfilled(1)); + // A closed drain freezes its journal. + journaled.closed = true; + journaled.record_removal(2, true); + assert!(!journaled.journal.as_ref().unwrap().is_resolved(2)); +} diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs index a3e9138f4f0..539d7f15996 100644 --- a/lightning/src/ln/channelmanager/ffor_activation.rs +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -13,6 +13,7 @@ mod close; mod context; mod driver; mod invoice; +mod outcome; mod receipt; mod reestablish; mod witness; diff --git a/lightning/src/ln/channelmanager/ffor_activation/close.rs b/lightning/src/ln/channelmanager/ffor_activation/close.rs index 23acca0cc20..34fa8fec704 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/close.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/close.rs @@ -492,7 +492,7 @@ where if !self.ffor_persistence.lock().unwrap().is_complete(&runtime.get(&key)?.requirement) { return Ok(false); } - if channel.ffor_receiver_drain_binding().map(|(_, _, closed)| closed) != Some(true) { + if channel.ffor_receiver_drain_binding().map(|(_, _, closed, _)| closed) != Some(true) { channel.finish_ffor_receiver_closed(epoch_id, digest)?; } Ok(true) diff --git a/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs index 0030f944439..9bb0ad31e84 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::chain::ChannelMonitorUpdateStatus; use crate::ln::channelmanager::ffor_activation::drain_tests::{drain, park_two}; use crate::ln::channelmanager::ffor_recovery_tests::claim_ffor_preimage_for_test; +use crate::ln::ffor::FFORVoucherOutcome; use crate::ln::ffor_tests::anchor_config; use crate::ln::functional_test_utils::*; use lightning_ffor::reestablish::{Reestablish, ReportedState}; @@ -98,6 +99,21 @@ fn ffor_close_manager_drains_both_views_and_persists_closed_before_ordinary_paym let (vouchers, preimage) = park_two(&nodes[0], &nodes[1], id); let (hash, activation_ack) = activate(&nodes[0], &nodes[1], id); let old_monitor = get_monitor!(nodes[1], id).ffor_commitment_snapshot().unwrap(); + let outcome = |node: &Node, slot: u16| { + let context = node.node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + let voucher = vouchers[usize::from(slot.clamp(1, 2)) - 1]; + node.node.ffor_receiver_voucher_outcome( + &context, + slot, + voucher.payment_hash, + voucher.amount_msat, + ) + }; + let expected_first = if settled || learned { + FFORVoucherOutcome::Fulfilled + } else { + FFORVoucherOutcome::Failed + }; if learned { claim_ffor_preimage_for_test( &nodes[0], @@ -284,6 +300,9 @@ fn ffor_close_manager_drains_both_views_and_persists_closed_before_ordinary_paym ); nodes[0].node.get_and_clear_pending_events(); assert!(nodes[1].node.list_usable_channels().is_empty()); + // Every slot is journaled, but nothing is reported before the retained Closed proof. + assert_eq!(outcome(&nodes[1], 1), Ok(None)); + assert_eq!(outcome(&nodes[1], 2), Ok(None)); assert!(nodes[1] .node .prepare_ffor_receiver_closed(&id, &peer, EPOCH, &old_monitor) @@ -313,6 +332,31 @@ fn ffor_close_manager_drains_both_views_and_persists_closed_before_ordinary_paym .unwrap(), closed ); + assert_eq!(outcome(&nodes[1], 1), Ok(Some(expected_first))); + assert_eq!(outcome(&nodes[1], 2), Ok(Some(FFORVoucherOutcome::Failed))); + assert!(outcome(&nodes[1], 0).is_err()); + assert!(outcome(&nodes[1], 3).is_err()); + { + let context = nodes[1].node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + assert!(nodes[1] + .node + .ffor_receiver_voucher_outcome( + &context, + 1, + vouchers[1].payment_hash, + vouchers[0].amount_msat + ) + .is_err()); + assert!(nodes[1] + .node + .ffor_receiver_voucher_outcome( + &context, + 1, + vouchers[0].payment_hash, + vouchers[0].amount_msat + 1 + ) + .is_err()); + } assert_eq!(drain(&nodes[0], &nodes[1]), (Vec::new(), Vec::new())); assert_eq!(nodes[1].node.list_usable_channels().len(), 1); let manager = persist(&nodes[1]); @@ -327,7 +371,11 @@ fn ffor_close_manager_drains_both_views_and_persists_closed_before_ordinary_paym chain_monitor, reloaded ); + // A restored manager reports nothing until its own fresh write completes. + assert!(outcome(&nodes[1], 1).is_err()); persist(&nodes[1]); + assert_eq!(outcome(&nodes[1], 1), Ok(Some(expected_first))); + assert_eq!(outcome(&nodes[1], 2), Ok(Some(FFORVoucherOutcome::Failed))); nodes[1] .node .accept_ffor_receiver_activation_ack(&id, &peer, EPOCH, &activation_ack) @@ -335,6 +383,21 @@ fn ffor_close_manager_drains_both_views_and_persists_closed_before_ordinary_paym connect_nodes(&nodes[0], &nodes[1]); assert_eq!(drain(&nodes[0], &nodes[1]), (Vec::new(), Vec::new())); send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + if settled && !delay_monitor && !restart_mid_round { + // Archive-only history after the channel is gone still reports the same outcomes. + nodes[1] + .node + .force_close_broadcasting_latest_txn(&id, &peer, "archive only".into()) + .unwrap(); + nodes[1].node.get_and_clear_pending_events(); + nodes[1].node.get_and_clear_pending_msg_events(); + nodes[1].chain_monitor.added_monitors.lock().unwrap().clear(); + assert!(nodes[1].node.list_channels().is_empty()); + assert_eq!(outcome(&nodes[1], 1), Ok(Some(expected_first))); + persist(&nodes[1]); + assert_eq!(outcome(&nodes[1], 1), Ok(Some(expected_first))); + assert_eq!(outcome(&nodes[1], 2), Ok(Some(FFORVoucherOutcome::Failed))); + } } } } @@ -419,6 +482,20 @@ fn ffor_close_manager_force_close_retains_draining_archive_and_monitor_preimage( reload_node!(nodes[1], config, &manager, &[&monitor], persister, chain_monitor, reloaded); assert!(nodes[1].node.list_channels().is_empty()); assert_eq!(nodes[1].node.ffor_recovery.lock().unwrap().encode(), archive); + persist(&nodes[1]); + let context = nodes[1].node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + assert_eq!( + nodes[1] + .node + .ffor_receiver_voucher_outcome( + &context, + 1, + vouchers[0].payment_hash, + vouchers[0].amount_msat + ) + .unwrap(), + None + ); assert_eq!( get_monitor!(nodes[1], id).get_stored_preimages()[&vouchers[0].payment_hash].0, preimage diff --git a/lightning/src/ln/channelmanager/ffor_activation/outcome.rs b/lightning/src/ln/channelmanager/ffor_activation/outcome.rs new file mode 100644 index 00000000000..76c94d82e95 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/outcome.rs @@ -0,0 +1,78 @@ +//! Historical per-voucher cooperative outcomes from a fully completed native drain journal. +//! This getter changes no balances and emits no events. It reports only what stock accounting +//! recorded at irrevocable removal, retained with the Closed proof and the latest completed +//! native write. + +use super::*; +use crate::ln::ffor::{FFORReceiverRecoveryContext, FFORVoucherOutcome}; +use crate::types::payment::PaymentHash; + +impl< + M: Deref, + T: Deref, + ES: Deref, + NS: Deref, + SP: Deref, + F: Deref, + R: Deref, + MR: Deref, + L: Deref, + > ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + /// Report the cooperative outcome of one owned voucher slot after the epoch reached Closed. + /// + /// The slot is the one-based position in the signed book. The payment hash and amount must + /// match that exact voucher, and the context must be the current native history for the + /// epoch. The result is available only after the latest native manager write completed, + /// including a fresh barrier after restore. Pending drains, force-closed epochs and legacy + /// records without a journal return None. This is historical accounting, not receive credit + /// and not permission to notify a payer twice. + pub fn ffor_receiver_voucher_outcome( + &self, context: &FFORReceiverRecoveryContext, slot: u16, payment_hash: PaymentHash, + amount_msat: u64, + ) -> Result, FFORReceiverError> { + let _guard = self.total_consistency_lock.read().unwrap(); + let recovery = self.ffor_recovery.lock().unwrap(); + let key = + FFORRecoveryKey { channel_id: context.channel_id(), epoch_id: context.epoch_id() }; + let current = self.ffor_recovery_context_from_registry(&recovery, &key)?; + if current.context_digest() != context.context_digest() { + return Err(FFORReceiverError::UnknownEpoch); + } + let vouchers = current.setup().vouchers(); + let voucher = usize::from(slot) + .checked_sub(1) + .and_then(|index| vouchers.get(index)) + .ok_or(FFORCommitmentError::InvalidVoucherBook)?; + if voucher.payment_hash != payment_hash.0 + || voucher.amount_msat != amount_msat + || current.funding_txo() != context.funding_txo() + { + return Err(FFORCommitmentError::InvalidVoucherBook.into()); + } + let journal = match recovery + .get_activation(&key) + .and_then(|activation| activation.close_record()) + .and_then(|close| close.journal()) + { + Some(journal) => journal, + None => return Ok(None), + }; + let runtime = self.ffor_activation.lock().unwrap(); + let requirement = &runtime.get(&key)?.requirement; + if !self.ffor_persistence.lock().unwrap().is_complete(requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + Ok(journal.outcome(usize::from(slot) - 1)) + } +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs index de45297c308..b585879b542 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs @@ -444,7 +444,22 @@ fn ffor_receipt_import_late_failed_voucher_only_protects_monitor_and_keeps_stock let monitor = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); receiver.node.prepare_ffor_receiver_closed(&id, &peer_id, EPOCH, &monitor).unwrap(); persist(receiver.node); - receiver.node.release_ffor_receiver_closed(&id, &peer_id, EPOCH).unwrap(); + assert!(receiver.node.release_ffor_receiver_closed(&id, &peer_id, EPOCH).unwrap()); + // The late preimage protected the monitor but the signed failure is the recorded outcome. + for (index, voucher) in vouchers.iter().enumerate() { + assert_eq!( + receiver + .node + .ffor_receiver_voucher_outcome( + &context, + index as u16 + 1, + voucher.payment_hash, + voucher.amount_msat + ) + .unwrap(), + Some(crate::ln::ffor::FFORVoucherOutcome::Failed) + ); + } let restored = restore(receiver, &persist(receiver.node), &get_monitor!(receiver, id).encode()) .unwrap(); diff --git a/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs b/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs index bb2ad7ce6ec..b6dbade52c9 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs @@ -127,7 +127,8 @@ where // The peer may resume ordinary updates as soon as it sees Closed. Persistence // alone does not grant that permission to this manager instance after restore. if channel.ffor_receiver_fence().is_some() - || channel.ffor_receiver_drain_binding().map(|(_, _, closed)| closed) != Some(true) + || channel.ffor_receiver_drain_binding().map(|(_, _, closed, _)| closed) + != Some(true) { return Ok(None); } diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index b7d5cdac252..11b1ce3cb09 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -50,6 +50,8 @@ pub use invoice::{ FFORInvoiceIntent, FFORInvoiceMonitorCheck, FFORInvoicePreparation, FFORStoredInvoice, FFORWitnessRouteEvidence, }; +pub(crate) mod journal; +pub use journal::FFORVoucherOutcome; /// Which channel participant offers every voucher HTLC in the book. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/lightning/src/ln/ffor/journal.rs b/lightning/src/ln/ffor/journal.rs new file mode 100644 index 00000000000..6a553083656 --- /dev/null +++ b/lightning/src/ln/ffor/journal.rs @@ -0,0 +1,193 @@ +//! Per-voucher cooperative drain outcomes recorded at the stock irrevocable removal point. +//! +//! The journal is native accounting, never a peer statement. It is never derived from the signed +//! settled bitmap or from known preimages. A slot becomes resolved exactly once, when the stock +//! revoke-and-ack removal drops its owned inbound HTLC, and is fulfilled only when that removal +//! carried the preimage. Fulfilled is always a subset of resolved. + +use crate::ln::msgs::DecodeError; +use crate::prelude::*; +use crate::util::ser::Writeable; + +/// The historical result of one owned voucher slot in a fully completed cooperative drain. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FFORVoucherOutcome { + /// The owned voucher was irrevocably removed with its preimage under stock accounting. + Fulfilled, + /// The owned voucher was irrevocably removed as a failure under stock accounting. + Failed, +} + +/// Resolved and fulfilled bitmaps for one drain, bounded by the native book of at most 483 slots. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct FFORCooperativeJournal { + slots: u16, + resolved: Vec, + fulfilled: Vec, +} + +impl_writeable_tlv_based!(FFORCooperativeJournal, { + (0, slots, required), + (2, resolved, required_vec), + (4, fulfilled, required_vec), +}); + +/// Encoded upper bound: two 61-byte bitmaps, the slot count and TLV framing. +pub(crate) const MAX_JOURNAL_BYTES: usize = 140; + +impl FFORCooperativeJournal { + /// A fresh journal with every slot unresolved. Only valid book sizes are accepted. + pub(crate) fn new(slots: usize) -> Option { + if slots == 0 || slots > lightning_ffor::book::MAX_VOUCHERS { + return None; + } + let bytes = (slots + 7) / 8; + Some(Self { slots: slots as u16, resolved: vec![0; bytes], fulfilled: vec![0; bytes] }) + } + + pub(crate) fn slots(&self) -> u16 { + self.slots + } + + /// Test-only raw constructor for malformed journals; production code only uses `new`. + #[cfg(test)] + pub(crate) fn from_parts(slots: u16, resolved: Vec, fulfilled: Vec) -> Self { + Self { slots, resolved, fulfilled } + } + + /// Structural validity against the actual book size: exact bitmap lengths, zero padding and + /// fulfilled being a subset of resolved. + pub(crate) fn validate(&self, slots: usize) -> Result<(), DecodeError> { + let count = usize::from(self.slots); + if count != slots || count == 0 || count > lightning_ffor::book::MAX_VOUCHERS { + return Err(DecodeError::InvalidValue); + } + let bytes = (count + 7) / 8; + if self.resolved.len() != bytes || self.fulfilled.len() != bytes { + return Err(DecodeError::InvalidValue); + } + let used = count % 8; + if used != 0 { + let mask = 0xffu8 << used; + if self.resolved[bytes - 1] & mask != 0 || self.fulfilled[bytes - 1] & mask != 0 { + return Err(DecodeError::InvalidValue); + } + } + if self.resolved.iter().zip(&self.fulfilled).any(|(r, f)| f & !r != 0) { + return Err(DecodeError::InvalidValue); + } + Ok(()) + } + + fn bit(bytes: &[u8], slot: usize) -> bool { + bytes.get(slot / 8).map_or(false, |byte| byte & (1 << (slot % 8)) != 0) + } + + pub(crate) fn is_resolved(&self, slot: usize) -> bool { + slot < usize::from(self.slots) && Self::bit(&self.resolved, slot) + } + + pub(crate) fn is_fulfilled(&self, slot: usize) -> bool { + slot < usize::from(self.slots) && Self::bit(&self.fulfilled, slot) + } + + /// Record one irrevocable removal. Returns false, changing nothing, for an out-of-range or + /// already resolved slot; restore validation guarantees neither happens after preflight. + pub(crate) fn record(&mut self, slot: usize, fulfilled: bool) -> bool { + if slot >= usize::from(self.slots) || Self::bit(&self.resolved, slot) { + return false; + } + self.resolved[slot / 8] |= 1 << (slot % 8); + if fulfilled { + self.fulfilled[slot / 8] |= 1 << (slot % 8); + } + true + } + + pub(crate) fn all_resolved(&self) -> bool { + (0..usize::from(self.slots)).all(|slot| Self::bit(&self.resolved, slot)) + } + + /// Every signed settled slot must have been fulfilled under stock accounting. Additional + /// locally proven fulfillments are allowed. + pub(crate) fn covers_settled(&self, settled: &[u8]) -> bool { + settled.len() == self.fulfilled.len() + && settled.iter().zip(&self.fulfilled).all(|(s, f)| s & !f == 0) + } + + /// Canonical bytes bound into the completion digest. + pub(crate) fn digest_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(2 + self.resolved.len() + self.fulfilled.len()); + bytes.extend_from_slice(&self.slots.to_be_bytes()); + bytes.extend_from_slice(&self.resolved); + bytes.extend_from_slice(&self.fulfilled); + bytes + } + + /// Zero-based slot outcome. None for unresolved or out-of-range slots. + pub(crate) fn outcome(&self, slot: usize) -> Option { + if !self.is_resolved(slot) { + return None; + } + Some(if self.is_fulfilled(slot) { + FFORVoucherOutcome::Fulfilled + } else { + FFORVoucherOutcome::Failed + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::ser::Readable; + + #[test] + fn ffor_journal_bounds_padding_subset_and_encoding() { + assert!(FFORCooperativeJournal::new(0).is_none()); + assert!(FFORCooperativeJournal::new(484).is_none()); + let mut journal = FFORCooperativeJournal::new(483).unwrap(); + assert_eq!(journal.resolved.len(), 61); + journal.validate(483).unwrap(); + assert!(journal.validate(482).is_err()); + assert!(!journal.record(483, true)); + assert!(journal.record(482, true)); + assert!(!journal.record(482, false)); + assert!(journal.is_resolved(482) && journal.is_fulfilled(482)); + assert!(!journal.all_resolved()); + for slot in 0..482 { + assert!(journal.record(slot, slot % 3 == 0)); + } + assert!(journal.all_resolved()); + assert_eq!(journal.outcome(1), Some(FFORVoucherOutcome::Failed)); + assert_eq!(journal.outcome(3), Some(FFORVoucherOutcome::Fulfilled)); + assert_eq!(journal.outcome(483), None); + let encoded = journal.encode(); + assert_eq!(journal.serialized_length(), encoded.len()); + assert!(encoded.len() <= MAX_JOURNAL_BYTES, "{}", encoded.len()); + let restored = FFORCooperativeJournal::read(&mut &encoded[..]).unwrap(); + assert_eq!(restored, journal); + let mut settled = vec![0u8; 61]; + settled[0] = 0b1001; + assert!(journal.covers_settled(&settled)); + settled[0] = 0b1011; + assert!(!journal.covers_settled(&settled)); + assert!(!journal.covers_settled(&settled[..60])); + + let mut short = FFORCooperativeJournal::new(9).unwrap(); + short.validate(9).unwrap(); + assert_eq!(short.resolved.len(), 2); + short.resolved[1] = 0b10; + assert!(short.validate(9).is_err(), "padding bit"); + short.resolved[1] = 0; + short.fulfilled[0] = 1; + assert!(short.validate(9).is_err(), "fulfilled outside resolved"); + short.resolved[0] = 1; + short.validate(9).unwrap(); + assert_eq!(short.outcome(0), Some(FFORVoucherOutcome::Fulfilled)); + assert_eq!(short.outcome(8), None); + let mut wrong = short.clone(); + wrong.resolved.push(0); + assert!(wrong.validate(9).is_err()); + } +} diff --git a/lightning/src/ln/ffor_recovery.rs b/lightning/src/ln/ffor_recovery.rs index ef660a891d2..9a73f5d1c55 100644 --- a/lightning/src/ln/ffor_recovery.rs +++ b/lightning/src/ln/ffor_recovery.rs @@ -20,6 +20,7 @@ use bitcoin::secp256k1::PublicKey; use crate::io; use crate::ln::channel::{FFORReceiverFencePhase, FFORReceiverSetup}; +use crate::ln::ffor::journal::FFORCooperativeJournal; use crate::ln::ffor::{ FFORMonitorRecoveryIdentity, FFORReceiverAbortReason, FFORReceiverWitnessAcknowledgements, FFORReceiverWitnessRegistration, @@ -39,6 +40,8 @@ const REQUEST_VERSION: u8 = 4; const WITNESS_VERSION: u8 = 5; const WITNESS_ACK_VERSION: u8 = 6; const INVOICE_VERSION: u8 = 7; +// Earlier readers must reject the native outcome journal rather than drop it. +const JOURNAL_VERSION: u8 = 8; const MAX_RECORDS: usize = 64; const MAX_ENCODED_BYTES: usize = 8 * 1024 * 1024; // Two maximum wire messages, a 483-slot canonical book, and fixed admission fields fit here. @@ -290,7 +293,8 @@ impl FFORRecoveryRegistry { /// Check the channel-owned close binding against exact authenticated archived evidence. pub(crate) fn validate_channel_lifecycle( &self, setup: &FFORReceiverSetup, fence: Option<(FFORReceiverFencePhase, [u8; 32])>, - abort_reason: Option, drain: Option<([u8; 32], Vec, bool)>, + abort_reason: Option, + drain: Option<([u8; 32], Vec, bool, Option)>, completion_hash: Option<[u8; 32]>, drain_activation_hash: Option<[u8; 32]>, ) -> Result<(), DecodeError> { if !self.contains_exact(setup) { @@ -303,13 +307,15 @@ impl FFORRecoveryRegistry { if let Some(record) = self.get_activation(&key) { if record.is_draining() { let close = record.close_record().ok_or(DecodeError::InvalidValue)?; - let (ack_hash, settled, closed) = drain.ok_or(DecodeError::InvalidValue)?; + let (ack_hash, settled, closed, journal) = + drain.ok_or(DecodeError::InvalidValue)?; let hash = record.validate(&authenticated)?; if abort_reason.is_some() || Some(ack_hash) != close.acknowledgement_hash() || settled != close.settled()? || completion_hash != close.completion_hash() || drain_activation_hash != Some(hash) + || (record.is_closed() && close.journal() != journal.as_ref()) { return Err(DecodeError::InvalidValue); } @@ -478,7 +484,11 @@ impl FFORRecoveryRegistry { } { return Err(FFORRecoveryError::ConflictingRecord); } - self.prepare_replacement(index, entry, 0) + // A journaled Closed proof raises the archive past the request version, which adds the + // pending-request count to the framing. The terminal reservation covers this growth. + let journaled = activation.close_record().map_or(false, |close| close.journal().is_some()); + let header_growth = if journaled && self.version() < REQUEST_VERSION { 2 } else { 0 }; + self.prepare_replacement(index, entry, header_growth) } fn prepare_replacement( @@ -512,6 +522,13 @@ impl FFORRecoveryRegistry { } fn version(&self) -> u8 { + if self.entries.iter().any(|entry| { + entry.record.activation.as_ref().map_or(false, |activation| { + activation.close_record().map_or(false, |close| close.journal().is_some()) + }) + }) { + return JOURNAL_VERSION; + } if self.entries.iter().any(|entry| entry.record.invoice.is_some()) { return INVOICE_VERSION; } @@ -598,6 +615,7 @@ impl Readable for FFORRecoveryRegistry { && version != WITNESS_VERSION && version != WITNESS_ACK_VERSION && version != INVOICE_VERSION + && version != JOURNAL_VERSION { return Err(DecodeError::UnknownRequiredFeature); } diff --git a/lightning/src/ln/ffor_recovery/activation/close.rs b/lightning/src/ln/ffor_recovery/activation/close.rs index 61391a1e505..696747d3eab 100644 --- a/lightning/src/ln/ffor_recovery/activation/close.rs +++ b/lightning/src/ln/ffor_recovery/activation/close.rs @@ -3,6 +3,7 @@ use super::*; use crate::chain::transaction::OutPoint; use crate::ln::channel::{ffor_drain_completion_digest, FFORReceiverDrainCompletion}; +use crate::ln::ffor::journal::FFORCooperativeJournal; use crate::ln::types::ChannelId; use crate::types::payment::PaymentPreimage; use bitcoin::hashes::sha256; @@ -32,6 +33,8 @@ struct ClosedDrain { settlement_txid: Txid, monitor_update_id: u64, funding_txo: OutPoint, + // Complete native outcome journal. Legacy records without one report no outcomes. + journal: Option, } impl_writeable_tlv_based!(ClosedDrain, { @@ -42,6 +45,7 @@ impl_writeable_tlv_based!(ClosedDrain, { (8, settlement_txid, required), (10, monitor_update_id, required), (12, funding_txo, required), + (14, journal, option), }); impl FFORReceiverCloseRecord { @@ -104,10 +108,17 @@ impl FFORReceiverCloseRecord { closed.funding_txo, commitments, closed.monitor_update_id, + closed.journal.as_ref(), ); if digest != closed.completion_hash { return Err(DecodeError::InvalidValue); } + if let Some(journal) = &closed.journal { + journal.validate(setup.vouchers().len())?; + if !journal.all_resolved() || !journal.covers_settled(&self.settled()?) { + return Err(DecodeError::InvalidValue); + } + } if closed.receiver_number == 0 || closed.receiver_number > INITIAL_COMMITMENT_NUMBER || closed.settlement_number == 0 @@ -153,6 +164,11 @@ impl FFORReceiverCloseRecord { self.closed.as_ref().map(|closed| closed.completion_hash) } + /// The complete native outcome journal retained with the Closed proof, if any. + pub(crate) fn journal(&self) -> Option<&FFORCooperativeJournal> { + self.closed.as_ref()?.journal.as_ref() + } + /// Returns the signed bitmap only after the caller has authenticated this immutable record. pub(crate) fn settled(&self) -> Result, DecodeError> { match self.ack_payload()? { @@ -272,6 +288,7 @@ impl FFORReceiverActivation { settlement_txid: commitments.counterparty.txid, monitor_update_id, funding_txo, + journal: completion.journal().cloned(), }; if close.closed.as_ref().map_or(false, |old| old.encode() != closed.encode()) { return Err(DecodeError::InvalidValue); diff --git a/lightning/src/ln/ffor_recovery/activation/tests/close.rs b/lightning/src/ln/ffor_recovery/activation/tests/close.rs index 5862ec6dfb0..943598c9038 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests/close.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests/close.rs @@ -1,8 +1,10 @@ use super::*; use crate::chain::transaction::OutPoint; use crate::io::Read; +use crate::ln::ffor::journal::FFORCooperativeJournal; use crate::ln::ffor_recovery::{ - CLOSED_RESERVATION_BYTES, CLOSE_RESERVATION_BYTES, CLOSE_VERSION, MAX_RECORD_BYTES, + CLOSED_RESERVATION_BYTES, CLOSE_RESERVATION_BYTES, CLOSE_VERSION, INVOICE_VERSION, + JOURNAL_VERSION, MAX_RECORD_BYTES, }; use crate::util::ser::BigSize; use bitcoin::hashes::sha256; @@ -71,10 +73,22 @@ pub(super) fn phases(maximum: bool) -> (FFORReceiverSetup, Vec FFORCooperativeJournal { + let count = setup.validate_recovery().unwrap().vouchers().len(); + let mut journal = FFORCooperativeJournal::new(count).unwrap(); + for slot in 0..count { + assert!(journal.record(slot, slot % every == 0)); + } + journal +} + // This storage fixture exercises historical framing only. It cannot construct the channel's // opaque completion proof or authorize any runtime transition. Real channel tests cover that. #[derive(Clone)] @@ -86,11 +100,30 @@ struct HistoricalCompletion { settlement_txid: Txid, monitor_update_id: u64, funding_txo: OutPoint, + journal: Option, } impl_writeable_tlv_based!(HistoricalCompletion, { (0, completion_hash, required), (2, receiver_number, required), (4, receiver_txid, required), (6, settlement_number, required), (8, settlement_txid, required), (10, monitor_update_id, required), (12, funding_txo, required), + (14, journal, option), +}); + +/// The completion record layout before the native outcome journal existed. +#[derive(Clone)] +struct PreviousCompletion { + completion_hash: [u8; 32], + receiver_number: u64, + receiver_txid: Txid, + settlement_number: u64, + settlement_txid: Txid, + monitor_update_id: u64, + funding_txo: OutPoint, +} +impl_writeable_tlv_based!(PreviousCompletion, { + (0, completion_hash, required), (2, receiver_number, required), + (4, receiver_txid, required), (6, settlement_number, required), + (8, settlement_txid, required), (10, monitor_update_id, required), (12, funding_txo, required), }); struct HistoricalClose { @@ -104,6 +137,13 @@ impl_writeable_tlv_based!(HistoricalClose, { fn completion( setup: &FFORReceiverSetup, draining: &FFORReceiverActivation, +) -> HistoricalCompletion { + completion_with(setup, draining, None) +} + +fn completion_with( + setup: &FFORReceiverSetup, draining: &FFORReceiverActivation, + journal: Option, ) -> HistoricalCompletion { let mut completed = HistoricalCompletion { completion_hash: [0; 32], @@ -113,8 +153,13 @@ fn completion( settlement_txid: Txid::from_byte_array([8; 32]), monitor_update_id: 6, funding_txo: setup.funding_txo(), + journal, + }; + let mut bytes = if completed.journal.is_some() { + b"ffor/native-drain-complete/v2".to_vec() + } else { + b"ffor/native-drain-complete/v1".to_vec() }; - let mut bytes = b"ffor/native-drain-complete/v1".to_vec(); bytes.extend_from_slice(&key(setup).epoch_id); bytes.extend_from_slice(&draining.activation_hash(setup).unwrap()); bytes.extend_from_slice(&draining.close_record().unwrap().acknowledgement_hash().unwrap()); @@ -128,6 +173,9 @@ fn completion( bytes.extend_from_slice(&number.to_be_bytes()); bytes.extend_from_slice(txid.as_byte_array()); } + if let Some(journal) = &completed.journal { + bytes.extend_from_slice(&journal.digest_bytes()); + } completed.completion_hash = sha256::Hash::hash(&bytes).to_byte_array(); completed } @@ -178,7 +226,10 @@ fn ffor_activation_close_maximum_transcripts_preserve_history_and_reservations() assert_eq!(retained.ack_wire(), phases[1].ack_wire()); } if index >= 2 { - assert_eq!(registry.encode()[0], CLOSE_VERSION); + assert_eq!( + registry.encode()[0], + if index == 4 { JOURNAL_VERSION } else { CLOSE_VERSION } + ); let close = retained.close_record().unwrap(); assert_eq!(close.close_wire().len(), MAX_MESSAGE_LEN); assert_eq!(close.close_wire(), phases[2].close_record().unwrap().close_wire()); @@ -189,9 +240,171 @@ fn ffor_activation_close_maximum_transcripts_preserve_history_and_reservations() assert_eq!(close.settled().unwrap(), vec![0; 61]); assert!(close.preimages().unwrap().is_empty()); } + if index == 4 { + assert_eq!(registry.encode()[0], JOURNAL_VERSION); + let close = retained.close_record().unwrap(); + assert_eq!(close.journal(), Some(&full_journal(&setup, 3))); + assert_eq!(close.journal().unwrap().slots(), 483); + } registry = roundtrip(®istry); } assert!(registry.get_activation(&key(&setup)).unwrap().is_closed()); + // The maximum journaled Closed record plus its framing growth stays inside the reservation. + let maximum = completion_with(&setup, &phases[3], Some(full_journal(&setup, 1))); + assert!( + maximum.serialized_length() + 64 <= CLOSED_RESERVATION_BYTES, + "{}", + maximum.serialized_length() + ); +} + +#[test] +fn ffor_activation_close_journal_binds_completion_and_refuses_legacy_readers_and_corruption() { + let (setup, phases) = phases(false); + let authenticated = setup.validate_recovery().unwrap(); + let count = authenticated.vouchers().len(); + let draining = &phases[3]; + // A legacy completion without a journal still validates under the original digest domain + // and reports no outcomes. + let legacy = historical_closed(&setup, draining, &completion(&setup, draining)); + legacy.validate(&authenticated).unwrap(); + assert!(legacy.close_record().unwrap().journal().is_none()); + let mut registry = registered(&setup); + for phase in &phases[..4] { + registry.prepare_activation(&setup, phase).unwrap().commit(); + } + let before_closed = registry.encode(); + registry.prepare_activation(&setup, &legacy).unwrap().commit(); + assert_eq!(registry.encode()[0], CLOSE_VERSION); + registry = FFORRecoveryRegistry::read(&mut &before_closed[..]).unwrap(); + + let journal = full_journal(&setup, 2); + let journaled = historical_closed( + &setup, + draining, + &completion_with(&setup, draining, Some(journal.clone())), + ); + journaled.validate(&authenticated).unwrap(); + registry.prepare_activation(&setup, &journaled).unwrap().commit(); + let bytes = registry.encode(); + assert_eq!(bytes[0], JOURNAL_VERSION); + let restored = roundtrip(®istry); + assert_eq!( + restored.get_activation(&key(&setup)).unwrap().close_record().unwrap().journal(), + Some(&journal) + ); + // The journal cannot be replaced or dropped once the Closed proof is retained. + assert!(registry.prepare_activation(&setup, &legacy).is_err()); + let other = historical_closed( + &setup, + draining, + &completion_with(&setup, draining, Some(full_journal(&setup, 1))), + ); + assert!(registry.prepare_activation(&setup, &other).is_err()); + // Old readers reject the journal field and the new version byte instead of dropping them. + let old_reader = PreviousCompletion::read( + &mut &completion_with(&setup, draining, Some(journal.clone())).encode()[..], + ); + assert!(matches!(old_reader, Err(DecodeError::UnknownRequiredFeature))); + let mut downgraded = bytes.clone(); + downgraded[0] = INVOICE_VERSION; + assert!(FFORRecoveryRegistry::read(&mut &downgraded[..]).is_err()); + let mut future = bytes.clone(); + future[0] = JOURNAL_VERSION + 1; + assert!(matches!( + FFORRecoveryRegistry::read(&mut &future[..]), + Err(DecodeError::UnknownRequiredFeature) + )); + // Malformed journals: wrong domain, incomplete coverage, padding, subset, size and a signed + // settled slot that stock accounting never fulfilled. + let bytes_len = (count + 7) / 8; + let (payable_setup, payable_closing, preimages) = payable(false); + let payable_authenticated = payable_setup.validate_recovery().unwrap(); + let settled_draining = payable_closing + .with_close_ack( + &payable_setup, + &close_ack(&payable_setup, &payable_closing, false, vec![preimages[0].clone()]), + ) + .unwrap(); + let unfulfilled = { + let mut journal = FFORCooperativeJournal::new(2).unwrap(); + assert!(journal.record(0, false)); + assert!(journal.record(1, true)); + journal + }; + let cases: Vec<(&FFORReceiverSetup, &FFORReceiverActivation, HistoricalCompletion)> = vec![ + (&setup, draining, { + let mut completed = completion(&setup, draining); + completed.journal = Some(journal.clone()); + completed + }), + ( + &setup, + draining, + completion_with(&setup, draining, Some(FFORCooperativeJournal::new(count).unwrap())), + ), + (&setup, draining, { + let mut resolved = vec![0xff; bytes_len]; + if count % 8 == 0 { + resolved.push(0); + } + completion_with( + &setup, + draining, + Some(FFORCooperativeJournal::from_parts( + count as u16, + resolved, + vec![0; bytes_len], + )), + ) + }), + ( + &setup, + draining, + completion_with( + &setup, + draining, + Some(FFORCooperativeJournal::from_parts( + count as u16, + vec![0; bytes_len], + vec![1; bytes_len], + )), + ), + ), + (&setup, draining, completion_with(&setup, draining, Some(full_journal_of(count + 1, 2)))), + ( + &payable_setup, + &settled_draining, + completion_with(&payable_setup, &settled_draining, Some(unfulfilled)), + ), + ]; + for (index, (case_setup, base, completed)) in cases.iter().enumerate() { + let closed = historical_closed(case_setup, base, completed); + let authenticated = if index == 5 { &payable_authenticated } else { &authenticated }; + assert!(closed.validate(authenticated).is_err(), "change {index}"); + } + // The same signed settled slot validates once stock accounting fulfilled it. + let fulfilled = { + let mut journal = FFORCooperativeJournal::new(2).unwrap(); + assert!(journal.record(0, true)); + assert!(journal.record(1, false)); + journal + }; + historical_closed( + &payable_setup, + &settled_draining, + &completion_with(&payable_setup, &settled_draining, Some(fulfilled)), + ) + .validate(&payable_authenticated) + .unwrap(); +} + +fn full_journal_of(count: usize, every: usize) -> FFORCooperativeJournal { + let mut journal = FFORCooperativeJournal::new(count).unwrap(); + for slot in 0..count { + assert!(journal.record(slot, slot % every == 0)); + } + journal } #[test] diff --git a/lightning/src/ln/ffor_recovery/activation/tests/witness.rs b/lightning/src/ln/ffor_recovery/activation/tests/witness.rs index 936af6c49e6..287feebdce3 100644 --- a/lightning/src/ln/ffor_recovery/activation/tests/witness.rs +++ b/lightning/src/ln/ffor_recovery/activation/tests/witness.rs @@ -60,7 +60,14 @@ fn ffor_witness_archive_bounds_maximum_book_and_reserves_terminal_transitions() assert!(registry.encoded_bytes + registry.reserved_transition_bytes <= reserved); assert_eq!(registry.get_witnesses(&key(&setup)), Some(&metadata)); let bytes = registry.encode(); - assert_eq!(bytes[0], crate::ln::ffor_recovery::WITNESS_ACK_VERSION); + assert_eq!( + bytes[0], + if phase.is_closed() { + crate::ln::ffor_recovery::JOURNAL_VERSION + } else { + crate::ln::ffor_recovery::WITNESS_ACK_VERSION + } + ); assert_eq!(bytes.len(), registry.encoded_bytes); let restored = FFORRecoveryRegistry::read(&mut &bytes[..]).unwrap(); assert_eq!(restored.get_witnesses(&key(&setup)), Some(&metadata)); diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs index 53501a3f502..f92dffa1e81 100644 --- a/lightning/src/ln/ffor_recovery/tests.rs +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -301,7 +301,7 @@ fn ffor_recovery_reader_refuses_future_schema_missing_fields_and_truncation() { assert!(decode(&framed[..length]).is_err(), "accepted prefix {}", length); } let mut future = framed.clone(); - future[0] = INVOICE_VERSION + 1; + future[0] = JOURNAL_VERSION + 1; assert!(matches!(decode(&future), Err(DecodeError::UnknownRequiredFeature))); // Empty TLV records omit both mandatory fields. assert!(decode(&frame(&[vec![0]])).is_err()); From 996f5b000d575c8d54e627acc8b70810293011e8 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Mon, 21 Sep 2026 10:40:16 -0400 Subject: [PATCH 22/22] feat(ffor): reuse a channel after a terminal receiver epoch --- FFOR.md | 82 ++- lightning/src/ln/channel/ffor.rs | 146 +++++- lightning/src/ln/channel/ffor/drain.rs | 5 + lightning/src/ln/channel/ffor/receipt.rs | 42 +- lightning/src/ln/channel/ffor/setup.rs | 39 +- .../src/ln/channel/ffor/setup/request.rs | 24 +- lightning/src/ln/channelmanager.rs | 56 +- .../src/ln/channelmanager/ffor_activation.rs | 2 + .../channelmanager/ffor_activation/close.rs | 2 + .../ffor_activation/close/tests.rs | 479 +++++++++++++++++- .../channelmanager/ffor_activation/context.rs | 1 + .../ffor_activation/drain_tests.rs | 26 +- .../channelmanager/ffor_activation/driver.rs | 121 ++++- .../ffor_activation/driver/advance.rs | 2 + .../ffor_activation/driver/advance_tests.rs | 155 +++++- .../ffor_activation/driver/tests.rs | 5 +- .../channelmanager/ffor_activation/receipt.rs | 28 +- .../ffor_activation/receipt/tests.rs | 115 ++++- .../ffor_activation/reestablish.rs | 1 + lightning/src/ln/ffor.rs | 3 +- lightning/src/ln/ffor_recovery.rs | 86 +++- lightning/src/ln/ffor_recovery/request.rs | 8 +- lightning/src/ln/ffor_recovery/tests.rs | 9 + 23 files changed, 1302 insertions(+), 135 deletions(-) diff --git a/FFOR.md b/FFOR.md index 7f574c398a3..d1062e70333 100644 --- a/FFOR.md +++ b/FFOR.md @@ -134,12 +134,49 @@ ordinary HTLC failure rounds. Partial rounds finish before their HTLCs are faile all owned HTLCs drained. Ordinary payments work after drain. Registered voucher hashes remain excluded from ordinary receiving. -There is deliberately only one registration per channel. Its epoch and public book -remain as a permanent tombstone, and attempts to register either the same or another -epoch are refused. The required even channel TLV 65534 is retained after abort; -older readers must reject the channel. Restore validates live voucher ownership -against stock inbound HTLCs before accepting the channel, and checks that committed -vouchers retain either failure material or their deferred add. +A channel holds one registration at a time. Its epoch and public book remain after +abort or Closed, and the same epoch is never registered twice. A later epoch may replace +that book only through the manager's archive-checked admission described below; any other +existing registration refuses a new epoch with `AlreadyRegistered`. The required even +channel TLV 65534 is retained after abort; older readers must reject the channel. Restore +validates live voucher ownership against stock inbound HTLCs before accepting the channel, +and checks that committed vouchers retain either failure material or their deferred add. + +## Repeated epochs on one channel + +`prepare_ffor_receiver` and `register_ffor_receiver_setup` admit a new epoch on a channel +with an existing registration only when the previous epoch is terminal without ambiguity. +The channel-local shape is checked first: the previous book has an authenticated setup, +no fence and no pending pre-init gate, and either its drain is Closed with the retained +completion hash and no abort reason, or it is aborted without a drain. No owned inbound +HTLC may be pending, no owned preimage may be waiting in the holding cell, no monitor +update may be in progress, blocked or in flight for the channel, and no splice, STFU +handshake or unresolved reconnect observation may exist. Under the same peer and archive +locks the manager then requires that the archive holds that epoch's activation record with +its Closed proof matching the channel's completion hash, or its reconnect abort matching +the channel's abort reason, and that the runtime persistence requirement of that terminal +transition has completed. A setup aborted before activation, or a request the peer never +accepted, leaves no durable abort evidence in the archive and therefore never permits +reuse. Any missing condition refuses with the existing error variants; the new epoch also +needs a new `local_request_id`, since an exact retry of the old one still returns the old +selector. + +Admission does not delete history. The archive keeps the previous epoch's setup, +activation, close acknowledgement, outcome journal and Closed proof under its own +(channel, epoch) key; the new epoch is a further record subject to the same 64-record and +byte limits, and a capacity refusal leaves the terminal book untouched. The channel book is +replaced under the transition locks with the same ordered persistence barrier as +activation, and the replacement names the epoch it replaced in required even field 18. +Restore fails closed when a book names a predecessor whose archive record is missing or +not terminal, when a book without a predecessor shares its channel with another archived +epoch, when two unresolved archive records share a channel or funding identity, or when a +pending request names a different epoch of the channel. Historical getters keyed by epoch, +including recovery contexts, voucher outcomes and stored invoices, keep answering for +replaced epochs after reuse and after restart. A witness receipt of a replaced epoch still +imports into the original monitor as preimage protection only; the current book owns none +of its vouchers, and a live HTLC of the current epoch with the same payment hash defers it. +Previous-epoch payment hashes are retained in the archive, not in the channel book, so +after replacement they follow ordinary add handling on the channel. Authenticated setup also enters a manager-owned recovery registry in required TLV 22. Its exact signed messages, canonical book and original admission context survive @@ -157,7 +194,7 @@ Init and retain it after promotion into accepted setup. Pending requests and the local retry identity use archive version 4 and required even channel fields; they share the same global count and byte limits with accepted records. The setup-only archive version retains no activation or claim authority. -Fully drained channel tombstones permit subsequent ordinary splicing while retaining +Fully drained terminal books permit subsequent ordinary splicing while retaining the original funding context in their archive. Private activation records now retain the exact signed activate and acknowledgement @@ -239,7 +276,8 @@ all removal rounds and monitor writes completed, and funding and claim signature The archived completion binds the funding outpoint, both transaction IDs and commitment numbers, acknowledgement, activation and monitor update identity. The final write must complete before the manager clears the fence or advertises Closed. Ordinary payments -and later splices then work while the permanent epoch and voucher tombstone remains. +and later splices then work while the completed epoch's book remains the channel's +registration until a later epoch replaces it. Witness fetch codecs authenticate encrypted records against the provisioned identity, mailbox, activation digest, canonical book entry, ciphertext hash and low-S signature. @@ -427,16 +465,16 @@ spendable output does not identify a payment. ## Validation -The current focused native suite passes 172 tests, including 13 invoice tests and the -cooperative outcome journal checks. Native no-default-features and documentation builds with -broken intra-doc links denied also pass. +The current focused native suite passes 179 tests, including 13 invoice tests, the +cooperative outcome journal checks and the repeated-epoch checks. Native +no-default-features and documentation builds with broken intra-doc links denied also pass. The FFOR tests exercise real two-node commitment rounds, both funding directions, asymmetric dust limits and contest delays, signature corruption, stale monitors, monitor persistence delays, exact and partial books, mismatching and extra adds, and ordinary payments after abort. The parking crash matrix reloads at registration, uncommitted add, first commitment, both commitments before interception, parked, -explicit abort, failure commitment sent, and fully drained tombstone. +explicit abort, failure commitment sent, and fully drained terminal book. Run the focused suite with: @@ -478,7 +516,7 @@ refusal, exact retry, phase-specific write barriers, height changes, staged repo disconnect, force-close and terminal abort replay. Eight drain scenarios use two actual vouchers in both funding directions, with no known preimage, a known preimage, a delayed preimage monitor write, or restart after abort release. They check actual -fulfill and fail messages, both empty commitment sets, tombstone reload and a later +fulfill and fail messages, both empty commitment sets, terminal book reload and a later ordinary payment. Cooperative close tests cover ten two-voucher scenarios across both funders, including @@ -561,6 +599,17 @@ bits outside resolved, the wrong slot count and a signed settled slot that stock never fulfilled. Drain tests cover legacy records restoring without a journal, never acquiring one, and closed drains freezing theirs. +Repeated-epoch tests run two complete cooperative epochs on one channel for both funders, +with an ordinary payment between them, and check that both journals stay readable by +epoch before and after restart. Refusal tests attempt a new epoch while the first is +Activating, Active, Draining, ClosedPendingPersistence before and after its final write, +Aborting, durably aborted with vouchers still pending, and after force close; the same +attempt succeeds once the first epoch is Closed or its abort has drained. Further tests +cover a capacity refusal that leaves the terminal book untouched, a restore whose archive +lacks the replaced epoch or holds an unresolved record beside the new one, a channel book +naming itself as predecessor, a two-epoch facade lifecycle through the public driver, and +a first-epoch witness receipt imported after the channel moved to its second epoch. + Seven receipt-import integration tests exercise both funders, delayed monitor persistence, idempotent retries, crash recovery from a failed write using the prior durable monitor, missing peer/monitor refusal, unregistered witnesses, changed identities, expired epochs, @@ -573,9 +622,10 @@ manager/monitor fixtures only when `FFOR_NODE_REQUEST_FIXTURE_DIR` is set. ## Next boundary -Reusable epochs require durable retired epoch IDs and voucher hashes, with one -current signed transcript record under the same channel authority. The public facade -now composes the native activation and cooperative-close transitions. Production +Repeated epochs after Closed or a reconnect abort are admitted as described above. +Reuse after a setup aborted before activation, or after a request the peer never accepted, +still needs durable abort evidence in the archive and remains refused. The public facade +composes the native activation and cooperative-close transitions. Production orchestration must retain witness keys and receipts, reconcile every recovered preimage through the native import API to completed monitor protection, and enforce the configured deadline before claim safety ends. diff --git a/lightning/src/ln/channel/ffor.rs b/lightning/src/ln/channel/ffor.rs index e95453ac302..f80b4dec2b6 100644 --- a/lightning/src/ln/channel/ffor.rs +++ b/lightning/src/ln/channel/ffor.rs @@ -33,6 +33,9 @@ pub(super) struct FFORReceiverBook { drain: Option, request: Option, request_gate_released: Option, + // The terminal epoch this book replaced on the same channel. Restore requires its archived + // terminal record; the archive never drops the replaced epoch's history. + predecessor_epoch_id: Option<[u8; 32]>, } struct FFORReceivedVoucher { @@ -54,14 +57,53 @@ impl_writeable_tlv_based!(FFORReceiverBook, { (8, setup, option), // A reader without the mutation fence must refuse this channel. (10, fence, option), - // Older readers must not discard drain ownership or a completed epoch tombstone. + // Older readers must not discard drain ownership or a completed epoch's terminal record. (12, drain, option), // Pre-init interception must never be discarded by older readers. (14, request, option), (16, request_gate_released, option), + // A reader that ignores the predecessor could restore a later epoch without its history. + (18, predecessor_epoch_id, option), }); impl FFORReceiverBook { + fn new( + epoch_id: [u8; 32], vouchers: Vec, request: Option, + predecessor_epoch_id: Option<[u8; 32]>, + ) -> Self { + Self { + epoch_id, + vouchers, + received: Vec::new(), + abort_reason: None, + setup: None, + fence: None, + drain: None, + request, + request_gate_released: None, + predecessor_epoch_id, + } + } + + /// The channel-local terminal shape that permits a later epoch on this channel: an + /// authenticated setup whose drain reached Closed with its retained completion hash, or an + /// aborted setup without a drain, in either case without a fence or a pending pre-init gate. + /// The manager must still match this shape against the archive's terminal record and its + /// completed write before replacing the book. Raw books without a setup are never reusable. + fn is_terminal_for_reuse(&self) -> bool { + if self.fence.is_some() || self.setup.is_none() || self.vouchers.is_empty() { + return false; + } + if self.request.is_some() && self.request_gate_released != Some(true) { + return false; + } + match (self.abort_reason, self.drain.is_some()) { + (None, true) => self.is_closed_with_completion(), + (Some(_), false) => true, + _ => false, + } + } + pub(super) fn abort(&mut self, reason: FFORReceiverAbortReason) { if self.fence.is_none() && !self.is_closed() { self.abort_reason.get_or_insert(reason); @@ -73,6 +115,7 @@ impl FFORReceiverBook { ) -> Result<(), DecodeError> { if self.request_gate_released == Some(false) || (self.request_gate_released.is_some() && self.request.is_none()) + || self.predecessor_epoch_id == Some(self.epoch_id) { return Err(DecodeError::InvalidValue); } @@ -174,6 +217,13 @@ where if self.context.ffor_receiver_book.is_some() { return Err(FFORReceiverError::AlreadyRegistered); } + self.ffor_check_book_admission(vouchers)?; + self.context.ffor_receiver_book = + Some(FFORReceiverBook::new(epoch_id, vouchers.to_vec(), None, None)); + Ok(()) + } + + fn ffor_check_book_admission(&self, vouchers: &[FFORVoucher]) -> Result<(), FFORReceiverError> { self.check_ffor_synchronized()?; if !self.context.is_connected() { return Err(FFORCommitmentError::ChannelUnavailable.into()); @@ -185,20 +235,81 @@ where { return Err(FFORCommitmentError::InvalidVoucherBook.into()); } - self.context.ffor_receiver_book = Some(FFORReceiverBook { - epoch_id, - vouchers: vouchers.to_vec(), - received: Vec::new(), - abort_reason: None, - setup: None, - fence: None, - drain: None, - request: None, - request_gate_released: None, - }); Ok(()) } + /// Replace a terminal book with a later authenticated epoch. The manager calls this only + /// under its archive, runtime and persistence locks after verifying the archived terminal + /// record of the epoch returned by [`Self::ffor_receiver_reusable_epoch`]. + pub(super) fn ffor_replace_terminal_book( + &mut self, epoch_id: [u8; 32], vouchers: Vec, + request: Option, + ) -> Result<(), FFORReceiverError> { + let predecessor = + self.ffor_receiver_reusable_epoch()?.ok_or(FFORReceiverError::NotRegistered)?; + if predecessor == epoch_id { + return Err(FFORReceiverError::AlreadyRegistered); + } + if !vouchers.is_empty() { + self.ffor_check_book_admission(&vouchers)?; + } + self.context.ffor_receiver_book = + Some(FFORReceiverBook::new(epoch_id, vouchers, request, Some(predecessor))); + Ok(()) + } + + /// The channel-local reuse precondition. `Ok(None)` means this channel has no registration. + /// `Ok(Some(epoch))` names a terminal previous epoch: its book is Closed with a retained + /// completion hash or aborted without a drain, the fence is gone, no owned inbound HTLC is + /// pending, no owned preimage is waiting for monitor persistence and the channel has no + /// splice or quiescence in progress. The archive record, the terminal transition's completed + /// manager write and the remaining admission checks belong to the caller. Any other + /// registration refuses with [`FFORReceiverError::AlreadyRegistered`]. + pub(crate) fn ffor_receiver_reusable_epoch( + &self, + ) -> Result, FFORReceiverError> { + let book = match self.context.ffor_receiver_book.as_ref() { + Some(book) => book, + None => return Ok(None), + }; + let context = &self.context; + let owned_pending = + context.pending_inbound_htlcs.iter().any(|htlc| book.owns(htlc.htlc_id)); + let owned_claim = context.holding_cell_htlc_updates.iter().any(|update| { + matches!(update, HTLCUpdateAwaitingACK::ClaimHTLC { htlc_id, .. } if book.owns(*htlc_id)) + }); + if !book.is_terminal_for_reuse() + || owned_pending + || owned_claim + || !context.monitor_pending_finalized_fulfills.is_empty() + || !context.blocked_monitor_updates.is_empty() + || context.channel_state.is_monitor_update_in_progress() + || context.channel_state.is_awaiting_quiescence() + || context.channel_state.is_local_stfu_sent() + || context.channel_state.is_remote_stfu_sent() + || context.channel_state.is_quiescent() + || self.quiescent_action.is_some() + || self.pending_splice.is_some() + || context.interactive_tx_signing_session.is_some() + || matches!( + self.ffor_reconnect_outcome, + Some(FFORReestablishOutcome::ResolutionRequired { .. }) + | Some(FFORReestablishOutcome::CloseReplayRequired { .. }) + ) { + return Err(FFORReceiverError::AlreadyRegistered); + } + Ok(Some(book.epoch_id)) + } + + pub(crate) fn ffor_receiver_epoch_id(&self) -> Option<[u8; 32]> { + self.context.ffor_receiver_book.as_ref().map(|book| book.epoch_id) + } + + /// The terminal epoch this channel's current book replaced, if any. + pub(crate) fn ffor_receiver_predecessor_epoch(&self) -> Option<[u8; 32]> { + self.context.ffor_receiver_book.as_ref().and_then(|book| book.predecessor_epoch_id) + } + pub(crate) fn ffor_receiver_book_status( &self, monitor: &FFORMonitorSnapshot, logger: &L, ) -> Result @@ -272,8 +383,9 @@ where Some(book) => book, None => return, }; - // Bound retained received records to the live HTLC set. The expected book itself is the - // permanent tombstone, including hashes that must never reenter ordinary payment handling. + // Bound retained received records to the live HTLC set. The expected book of the current + // epoch retains its hashes, which must never reenter ordinary payment handling while it + // remains this channel's registration. book.received.retain(|received| { self.context .pending_inbound_htlcs @@ -567,7 +679,7 @@ mod tests { #[test] fn ffor_parking_rejects_inconsistent_stored_ownership_and_unwind_material() { - for mutation in 0..7 { + for mutation in 0..8 { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let config = anchor_config(); @@ -598,10 +710,12 @@ mod tests { }) }, 5 => book.received[0].failure = None, - _ => { + 6 => { book.received[0].failure = Some(FFORVoucherFailure::Relay { packet: Vec::new() }) }, + // A book can never name itself as the epoch it replaced. + _ => book.predecessor_epoch_id = Some(book.epoch_id), } let damaged = channel.encode(); channel.context.ffor_receiver_book = diff --git a/lightning/src/ln/channel/ffor/drain.rs b/lightning/src/ln/channel/ffor/drain.rs index adabe54706a..275c2ff533a 100644 --- a/lightning/src/ln/channel/ffor/drain.rs +++ b/lightning/src/ln/channel/ffor/drain.rs @@ -125,6 +125,11 @@ impl FFORReceiverBook { pub(super) fn is_closed(&self) -> bool { self.drain.as_ref().map_or(false, |drain| drain.closed) } + + /// Closed with the retained completion hash of the final proof. + pub(super) fn is_closed_with_completion(&self) -> bool { + self.drain.as_ref().map_or(false, |drain| drain.closed && drain.completion_hash.is_some()) + } } impl ChannelContext diff --git a/lightning/src/ln/channel/ffor/receipt.rs b/lightning/src/ln/channel/ffor/receipt.rs index 39d5ec102f6..f0fb7a173a4 100644 --- a/lightning/src/ln/channel/ffor/receipt.rs +++ b/lightning/src/ln/channel/ffor/receipt.rs @@ -26,7 +26,7 @@ where let has_drain = book.drain.is_some(); // A historical receipt can arrive after a failure was already committed. The stock FFOR // drain claim hook preserves that outcome while retaining preimage knowledge. A historical - // tombstone without a drain needs the monitor-only fallback below. + // terminal book without a drain needs the monitor-only fallback below. let htlc = self.context.pending_inbound_htlcs.iter().find(|htlc| htlc.htlc_id == voucher.htlc_id); if let Some(htlc) = htlc { @@ -59,6 +59,42 @@ where if monitor_knows_preimage { return Ok(None); } + self.ffor_monitor_only_preimage_update(preimage).map(Some) + } + + /// Protect a preimage of an archived terminal epoch that this channel's current book no longer + /// owns. Every voucher of that epoch was already resolved, so no stock claim is possible; only + /// the original monitor gains the preimage. The manager has matched the epoch, funding output + /// and witness selection against the archive before calling. A live HTLC of the current epoch + /// with the same payment hash keeps stock ownership and refuses this path. + pub(crate) fn ffor_import_historical_receipt_preimage( + &mut self, voucher: &FFORVoucher, preimage: PaymentPreimage, monitor_knows_preimage: bool, + ) -> Result, FFORReceiverError> { + let hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); + if hash != voucher.payment_hash + || self + .context + .ffor_receiver_book + .as_ref() + .map_or(true, |book| book.vouchers.contains(voucher)) + { + return Err(FFORReceiverError::InvalidWitnessReceipt); + } + if self.pending_splice.is_some() || self.context.interactive_tx_signing_session.is_some() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + if self.context.pending_inbound_htlcs.iter().any(|htlc| htlc.payment_hash == hash) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + if monitor_knows_preimage { + return Ok(None); + } + self.ffor_monitor_only_preimage_update(preimage).map(Some) + } + + fn ffor_monitor_only_preimage_update( + &mut self, preimage: PaymentPreimage, + ) -> Result { // Match the stock claim helper's priority insertion: preimage protection must not wait // behind a commitment update blocked by another channel or a signer. self.context.latest_monitor_update_id = self @@ -75,13 +111,13 @@ where pending.update.update_id += 1; } self.monitor_updating_paused(false, false, false, Vec::new(), Vec::new(), Vec::new()); - Ok(Some(ChannelMonitorUpdate { + Ok(ChannelMonitorUpdate { update_id, updates: vec![ChannelMonitorUpdateStep::PaymentPreimage { payment_preimage: preimage, payment_info: None, }], channel_id: Some(self.context.channel_id()), - })) + }) } } diff --git a/lightning/src/ln/channel/ffor/setup.rs b/lightning/src/ln/channel/ffor/setup.rs index ed12147a163..bed7b9a36d7 100644 --- a/lightning/src/ln/channel/ffor/setup.rs +++ b/lightning/src/ln/channel/ffor/setup.rs @@ -1,8 +1,9 @@ //! Signed setup retained by the channel's experimental receiver parking record. //! //! Admission currently supports at most 4096 revealed peer commitments. Longer histories are -//! refused until a bounded native history index exists. A setup remains preactivation evidence, -//! with the parking record's one-registration limit and conservative abort on restart. +//! refused until a bounded native history index exists. A setup remains preactivation evidence +//! with conservative abort on restart. A channel holds one registration at a time; a terminal +//! epoch may be replaced by a later one only through the manager's archive-checked admission. use super::*; use bitcoin::locktime::absolute::LOCK_TIME_THRESHOLD; @@ -92,7 +93,7 @@ impl FFORReceiverSetup { } /// Reauthenticate historical signed evidence and its expiry height without claiming current - /// channel eligibility. Timestamp locktimes are invalid even for fully drained tombstones. + /// channel eligibility. Timestamp locktimes are invalid even for fully drained terminal books. pub(crate) fn validate_recovery(&self) -> Result { self.validate_record().map_err(|_| DecodeError::InvalidValue) } @@ -156,8 +157,12 @@ where &self, init_wire: &[u8], accept_wire: &[u8], our_node_id: PublicKey, chain_hash: ChainHash, current_height: u32, claim_margin_blocks: u32, ) -> Result { - if self.context.ffor_receiver_book.is_some() { - return Err(FFORReceiverError::AlreadyRegistered); + // A terminal previous epoch may be replaced by a different epoch only; the manager + // verifies its archive record. The same epoch is never registered twice. + if let Some(previous) = self.ffor_receiver_reusable_epoch()? { + if Message::decode(init_wire).map_or(true, |init| init.header.epoch_id == previous) { + return Err(FFORReceiverError::AlreadyRegistered); + } } self.check_ffor_synchronized()?; if init_wire.len() > MAX_MESSAGE_LEN || accept_wire.len() > MAX_MESSAGE_LEN { @@ -244,20 +249,26 @@ where cltv_expiry: voucher.expiry, }) .collect(); - if let Some(book) = self.context.ffor_receiver_book.as_ref() { - let request = book.request.as_ref().ok_or(FFORReceiverError::AlreadyRegistered)?; - if !request.validates_setup(&record) - || book.abort_reason.is_some() - || book.setup.is_some() - || !book.received.is_empty() - || book.request_gate_released.unwrap_or(false) - { + let promoting = self.context.ffor_receiver_book.as_ref().map_or(false, |book| { + book.request.is_some() + && book.setup.is_none() + && book.abort_reason.is_none() + && !book.request_gate_released.unwrap_or(false) + }); + if promoting { + let book = self.context.ffor_receiver_book.as_ref().unwrap(); + let request = book.request.as_ref().unwrap(); + if !request.validates_setup(&record) || !book.received.is_empty() { return Err(invalid_setup()); } self.check_ffor_synchronized()?; self.ffor_validate_request(true).map_err(|_| invalid_setup())?; verification::validate_vouchers(&vouchers)?; self.context.ffor_receiver_book.as_mut().unwrap().vouchers = vouchers; + } else if self.context.ffor_receiver_book.is_some() { + // Only a terminal epoch can be replaced. The manager has already matched it against + // the archive under the same locks; the channel rechecks its own shape here. + self.ffor_replace_terminal_book(setup.header().epoch_id, vouchers, None)?; } else { self.register_ffor_receiver_book(setup.header().epoch_id, &vouchers)?; } @@ -376,7 +387,7 @@ where Ok(()) } - /// Validate before the reader converts a live setup into an aborted restart tombstone. + /// Validate before the reader converts a live setup into an aborted restart record. pub(in crate::ln::channel) fn ffor_restored(&mut self) -> Result<(), DecodeError> { self.ffor_validate_receiver_setup()?; self.validate_ffor_fence()?; diff --git a/lightning/src/ln/channel/ffor/setup/request.rs b/lightning/src/ln/channel/ffor/setup/request.rs index 52f1aeea7d9..d983e2a5b87 100644 --- a/lightning/src/ln/channel/ffor/setup/request.rs +++ b/lightning/src/ln/channel/ffor/setup/request.rs @@ -143,8 +143,12 @@ where &self, init_wire: &[u8], receiver: PublicKey, chain_hash: ChainHash, current_height: u32, claim_margin_blocks: u32, local_request_id: [u8; 32], ) -> Result { - if self.context.ffor_receiver_book.is_some() { - return Err(FFORReceiverError::AlreadyRegistered); + // A terminal previous epoch may be replaced by a different epoch only; the manager + // verifies its archive record. The same epoch is never registered twice. + if let Some(previous) = self.ffor_receiver_reusable_epoch()? { + if Message::decode(init_wire).map_or(true, |init| init.header.epoch_id == previous) { + return Err(FFORReceiverError::AlreadyRegistered); + } } self.check_ffor_synchronized()?; if !self.context.is_connected() @@ -203,17 +207,11 @@ where return Err(invalid_setup()); } let epoch_id = request.validate_recovery().map_err(|_| invalid_setup())?.header.epoch_id; - self.context.ffor_receiver_book = Some(FFORReceiverBook { - epoch_id, - vouchers: Vec::new(), - received: Vec::new(), - abort_reason: None, - setup: None, - fence: None, - drain: None, - request: Some(request), - request_gate_released: None, - }); + if self.context.ffor_receiver_book.is_some() { + return self.ffor_replace_terminal_book(epoch_id, Vec::new(), Some(request)); + } + self.context.ffor_receiver_book = + Some(FFORReceiverBook::new(epoch_id, Vec::new(), Some(request), None)); Ok(()) } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 1a4ee08105f..a490005a639 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4334,8 +4334,8 @@ where /// This is a private experimental protocol boundary, with no feature advertisement or activation. /// It does not freeze commitment updates or authorize invoices. Disconnects and restarts abort /// the registration and fail its vouchers using the ordinary channel commitment protocol. - /// Only one registration is supported for the lifetime of a channel, even after abort. Its - /// permanent tombstone prevents epoch reuse until a future durable epoch-history design exists. + /// A raw book is never archived, so it can neither replace an earlier epoch nor be replaced + /// by a later one: the channel keeps this registration for its lifetime, even after abort. /// Serialized channels using this API require a reader that understands voucher parking. pub fn register_ffor_receiver_book( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, epoch_id: [u8; 32], @@ -4375,8 +4375,12 @@ where /// pre-init admission with `prepare_ffor_receiver` and process Accept synchronously with /// `handle_ffor_receiver_message`. This retains setup evidence but does not activate or freeze /// the channel, or authorize an invoice. - /// The same abort, restart and one-registration limits as [`Self::register_ffor_receiver_book`] - /// apply. Raw-book registration does not supply the authenticated evidence retained here. + /// The same abort and restart behaviour as [`Self::register_ffor_receiver_book`] applies. A + /// channel whose previous authenticated epoch is terminal in both the channel and the archive, + /// with that terminal write completed, may register a later epoch; the earlier epoch's + /// archive record is retained. Any other existing registration is refused with + /// [`FFORReceiverError::AlreadyRegistered`]. Raw-book registration does not supply the + /// authenticated evidence retained here. pub fn register_ffor_receiver_setup( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, init_wire: &[u8], accept_wire: &[u8], claim_margin_blocks: u32, @@ -4389,11 +4393,18 @@ where .ok_or(FFORCommitmentError::ChannelUnavailable)? .lock() .unwrap(); - let channel = peer + let peer_state = &mut *peer; + let in_flight = peer_state + .in_flight_monitor_updates + .get(channel_id) + .map_or(false, |(_, updates)| !updates.is_empty()); + let channel = peer_state .channel_by_id .get_mut(channel_id) .and_then(Channel::as_funded_mut) .ok_or(FFORCommitmentError::ChannelUnavailable)?; + let mut recovery = self.ffor_recovery.lock().unwrap(); + self.check_ffor_epoch_reuse(channel, &recovery, in_flight)?; let setup = channel.prepare_ffor_receiver_setup( init_wire, accept_wire, @@ -4402,7 +4413,6 @@ where current_height, claim_margin_blocks, )?; - let mut recovery = self.ffor_recovery.lock().unwrap(); let insertion = recovery.prepare_insert(&setup).map_err(|_| FFORReceiverError::RecoveryUnavailable)?; let requirement = self @@ -4514,8 +4524,9 @@ where /// /// Continue normal event and peer-message processing until status is `Aborted`. HTLCs in partial /// commitment rounds are failed only after those rounds finish. A disconnect or restart retains - /// the abort and resumes the unwind on reconnection. Ordinary payments can proceed after drain, - /// but further FFOR registrations on this channel remain refused, including a different epoch. + /// the abort and resumes the unwind on reconnection. Ordinary payments can proceed after drain. + /// A further registration on this channel is refused unless the archive retains a terminal + /// record for this epoch; an abort before activation never creates one. /// If STFU was sent, abort requests a peer disconnect to end both sides' quiescence. Reconnect /// and continue message processing to drain; failures are not sent while the peer is quiescent. pub fn abort_ffor_receiver_book( @@ -9231,7 +9242,8 @@ where ComplFunc: FnOnce( Option, bool, - ) -> (Option, Option), + ) + -> (Option, Option), >( &self, prev_hop: HTLCPreviousHopData, payment_preimage: PaymentPreimage, payment_info: Option, attribution_data: Option, @@ -9269,7 +9281,8 @@ where ComplFunc: FnOnce( Option, bool, - ) -> (Option, Option), + ) + -> (Option, Option), >( &self, prev_hop: HTLCClaimSource, payment_preimage: PaymentPreimage, payment_info: Option, attribution_data: Option, @@ -17208,7 +17221,8 @@ where )?; channel.ffor_validate_receiver_identity(our_network_pubkey, chain_hash)?; if let Some(request) = channel.ffor_receiver_request() { - ffor_channel_requests.push(request.clone()); + ffor_channel_requests + .push((request.clone(), channel.ffor_receiver_predecessor_epoch())); } if let Some(setup) = channel.ffor_receiver_setup_record()? { ffor_channel_setups.push(( @@ -17218,6 +17232,7 @@ where channel.ffor_receiver_drain_binding(), channel.ffor_receiver_closed_completion_hash(), channel.ffor_receiver_drain_activation_hash(), + channel.ffor_receiver_predecessor_epoch(), )); } let logger = WithChannelContext::from(&args.logger, &channel.context, None); @@ -17622,12 +17637,23 @@ where let mut ffor_persistence = FFORPersistenceBarrier::new(); let ffor_activation = FFORReceiverRuntime::restored(&ffor_recovery, &mut ffor_persistence)?; ffor_recovery.validate_identity(our_network_pubkey, chain_hash)?; - for request in &ffor_channel_requests { + for (request, predecessor) in &ffor_channel_requests { if !ffor_recovery.contains_request(request) { return Err(DecodeError::InvalidValue); } + // A later epoch's pre-init gate must also retain its replaced epoch's terminal record. + let header = request.validate_recovery()?.header; + ffor_recovery.validate_channel_predecessor( + crate::ln::ffor_recovery::FFORRecoveryKey { + channel_id: ChannelId(header.channel_id), + epoch_id: header.epoch_id, + }, + *predecessor, + )?; } - for (setup, fence, abort_reason, drain, completion, drain_hash) in &ffor_channel_setups { + for (setup, fence, abort_reason, drain, completion, drain_hash, predecessor) in + &ffor_channel_setups + { ffor_recovery.validate_channel_lifecycle( setup, *fence, @@ -17635,6 +17661,7 @@ where drain.clone(), *completion, *drain_hash, + *predecessor, )?; } for channel_id in ffor_recovery.activation_channels() { @@ -18574,7 +18601,8 @@ where if let Some(peer) = per_peer_state.get(&monitor.get_counterparty_node_id()) { let mut peer = peer.lock().unwrap(); if peer.closed_channel_monitor_update_ids.contains_key(channel_id) { - peer.closed_channel_monitor_funding.insert(*channel_id, monitor.get_funding_txo()); + peer.closed_channel_monitor_funding + .insert(*channel_id, monitor.get_funding_txo()); } } } diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs index 539d7f15996..192e5336532 100644 --- a/lightning/src/ln/channelmanager/ffor_activation.rs +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -355,6 +355,7 @@ where channel.ffor_receiver_drain_binding(), channel.ffor_receiver_closed_completion_hash(), channel.ffor_receiver_drain_activation_hash(), + channel.ffor_receiver_predecessor_epoch(), ) .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; return Ok(self.ffor_activation.lock().unwrap().get(&key)?.requirement.clone()); @@ -402,6 +403,7 @@ where channel.ffor_receiver_drain_binding(), channel.ffor_receiver_closed_completion_hash(), channel.ffor_receiver_drain_activation_hash(), + channel.ffor_receiver_predecessor_epoch(), ) .map_err(|_| invalid())?; let activation = recovery.get_activation(&key).ok_or_else(invalid)?; diff --git a/lightning/src/ln/channelmanager/ffor_activation/close.rs b/lightning/src/ln/channelmanager/ffor_activation/close.rs index 34fa8fec704..3610c9d16ea 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/close.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/close.rs @@ -41,6 +41,7 @@ where channel.ffor_receiver_drain_binding(), channel.ffor_receiver_closed_completion_hash(), channel.ffor_receiver_drain_activation_hash(), + channel.ffor_receiver_predecessor_epoch(), ) .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; match channel.ffor_receiver_reconnect_outcome() { @@ -193,6 +194,7 @@ where channel.ffor_receiver_drain_binding(), channel.ffor_receiver_closed_completion_hash(), channel.ffor_receiver_drain_activation_hash(), + channel.ffor_receiver_predecessor_epoch(), ) .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; if matches!( diff --git a/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs index 9bb0ad31e84..751bfb30088 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs @@ -1,14 +1,18 @@ use super::*; use crate::chain::ChannelMonitorUpdateStatus; -use crate::ln::channelmanager::ffor_activation::drain_tests::{drain, park_two}; -use crate::ln::channelmanager::ffor_recovery_tests::claim_ffor_preimage_for_test; -use crate::ln::ffor::FFORVoucherOutcome; +use crate::ln::channelmanager::ffor_activation::drain_tests::{drain, park_two, park_two_epoch}; +use crate::ln::channelmanager::ffor_recovery_tests::{claim_ffor_preimage_for_test, restore}; +use crate::ln::ffor::{ + FFORReceiverAbortReason, FFORReceiverId, FFORReceiverParameters, FFORVoucherOutcome, +}; +use crate::ln::ffor_recovery::tests::fill_registry; use crate::ln::ffor_tests::anchor_config; use crate::ln::functional_test_utils::*; use lightning_ffor::reestablish::{Reestablish, ReportedState}; use lightning_ffor::wire::{CloseAck, Preimage}; const EPOCH: [u8; 32] = [81; 32]; +const EPOCH2: [u8; 32] = [82; 32]; fn persist(node: &Node) -> Vec { let token = node.node.capture_ffor_persistence(); @@ -18,9 +22,15 @@ fn persist(node: &Node) -> Vec { } fn signed(node: &Node, receiver: &Node, id: ChannelId, payload: Payload) -> Vec { + signed_epoch(node, receiver, id, EPOCH, payload) +} + +fn signed_epoch( + node: &Node, receiver: &Node, id: ChannelId, epoch: [u8; 32], payload: Payload, +) -> Vec { let registry = receiver.node.ffor_recovery.lock().unwrap(); let header = registry - .get(&FFORRecoveryKey { channel_id: id, epoch_id: EPOCH }) + .get(&FFORRecoveryKey { channel_id: id, epoch_id: epoch }) .unwrap() .validate_recovery() .unwrap() @@ -35,19 +45,25 @@ fn signed(node: &Node, receiver: &Node, id: ChannelId, payload: Payload) -> Vec< } fn activate(sender: &Node, receiver: &Node, id: ChannelId) -> ([u8; 32], Vec) { - let key = FFORRecoveryKey { channel_id: id, epoch_id: EPOCH }; + activate_epoch(sender, receiver, id, EPOCH) +} + +fn activate_epoch( + sender: &Node, receiver: &Node, id: ChannelId, epoch: [u8; 32], +) -> ([u8; 32], Vec) { + let key = FFORRecoveryKey { channel_id: id, epoch_id: epoch }; let hash = { let registry = receiver.node.ffor_recovery.lock().unwrap(); registry.get_activation(&key).unwrap().activation_hash(registry.get(&key).unwrap()).unwrap() }; assert!(receiver .node - .release_ffor_receiver_activation(&id, &sender.node.get_our_node_id(), EPOCH, |_| Ok(())) + .release_ffor_receiver_activation(&id, &sender.node.get_our_node_id(), epoch, |_| Ok(())) .unwrap()); - let ack = signed(sender, receiver, id, Payload::ActivateAck(hash)); + let ack = signed_epoch(sender, receiver, id, epoch, Payload::ActivateAck(hash)); receiver .node - .accept_ffor_receiver_activation_ack(&id, &sender.node.get_our_node_id(), EPOCH, &ack) + .accept_ffor_receiver_activation_ack(&id, &sender.node.get_our_node_id(), epoch, &ack) .unwrap(); persist(receiver); // The settlement test node models its signed Active response and exits its stock STFU state. @@ -573,3 +589,450 @@ fn ffor_close_manager_refuses_replay_during_conflicting_reconnect() { } } } + +/// Drive an Active epoch through signed close, drain and the retained Closed proof until the +/// fence is released. Returns the exact close acknowledgement and the drained HTLC IDs. +fn close_epoch<'a, 'b, 'c>( + sender: &Node<'a, 'b, 'c>, receiver: &Node<'a, 'b, 'c>, id: ChannelId, epoch: [u8; 32], + hash: [u8; 32], settled_preimage: Option, +) -> (Vec, Vec, Vec) { + let peer = sender.node.get_our_node_id(); + receiver.node.prepare_ffor_receiver_close(&id, &peer, epoch).unwrap(); + persist(receiver); + assert!(receiver.node.release_ffor_receiver_close(&id, &peer, epoch, |_| Ok(())).unwrap()); + let ack = signed_epoch( + sender, + receiver, + id, + epoch, + Payload::CloseAck(CloseAck { + activation_hash: hash, + num_slots: 2, + settled: vec![u8::from(settled_preimage.is_some())], + preimages: settled_preimage + .map(|preimage| vec![Preimage { slot: 1, value: preimage.0 }]) + .unwrap_or_default(), + preimages_tlv_present: true, + }), + ); + receiver.node.accept_ffor_receiver_close_ack(&id, &peer, epoch, &ack).unwrap(); + persist(receiver); + assert!(receiver.node.release_ffor_receiver_drain(&id, &peer, epoch).unwrap()); + let (fulfilled, failed) = drain(sender, receiver); + // PaymentSent releases the sender's held monitor update once its event is processed. + sender.node.get_and_clear_pending_events(); + sender.chain_monitor.added_monitors.lock().unwrap().clear(); + let snapshot = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); + receiver.node.prepare_ffor_receiver_closed(&id, &peer, epoch, &snapshot).unwrap(); + persist(receiver); + assert!(receiver.node.release_ffor_receiver_closed(&id, &peer, epoch).unwrap()); + (ack, fulfilled, failed) +} + +fn outcome( + node: &Node, id: ChannelId, epoch: [u8; 32], voucher: &FFORVoucher, slot: u16, +) -> Result, FFORReceiverError> { + let context = node.node.ffor_receiver_recovery_context(&id, epoch)?; + node.node.ffor_receiver_voucher_outcome( + &context, + slot, + voucher.payment_hash, + voucher.amount_msat, + ) +} + +/// The channel's current epoch and the epoch that book replaced. +fn book_epochs( + sender: &Node, receiver: &Node, id: ChannelId, +) -> (Option<[u8; 32]>, Option<[u8; 32]>) { + let peers = receiver.node.per_peer_state.read().unwrap(); + let peer = peers.get(&sender.node.get_our_node_id()).unwrap().lock().unwrap(); + let channel = peer.channel_by_id.get(&id).unwrap().as_funded().unwrap(); + (channel.ffor_receiver_epoch_id(), channel.ffor_receiver_predecessor_epoch()) +} + +/// Stage a new epoch through the public pre-init facade on the current connection. +fn attempt_reuse( + sender: &Node, receiver: &Node, id: ChannelId, voucher: &FFORVoucher, + local_request_id: [u8; 32], +) -> Result { + let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); + let parameters = FFORReceiverParameters { + local_request_id, + amounts_msat: vec![voucher.amount_msat], + minimum_payment_msat: voucher.amount_msat, + settlement_deadline: voucher.cltv_expiry - 20, + voucher_expiry: voucher.cltv_expiry, + fee_base_msat: 0, + fee_proportional_millionths: 0, + claim_margin_blocks: 20, + witness_peers: None, + hash_chain: false, + }; + receiver.node.prepare_ffor_receiver(&id, &connection, parameters) +} + +#[test] +fn ffor_close_manager_reuses_channel_after_closed_epoch_and_retains_history() { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_monitor, reloaded); + let config = anchor_config(); + let managers = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let mut nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let peer = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + let (first, first_preimage) = park_two(&nodes[0], &nodes[1], id); + let (first_hash, _) = activate(&nodes[0], &nodes[1], id); + let (_, fulfilled, failed) = + close_epoch(&nodes[0], &nodes[1], id, EPOCH, first_hash, Some(first_preimage)); + assert_eq!(fulfilled, vec![first[0].htlc_id]); + assert_eq!(failed, vec![first[1].htlc_id]); + assert_eq!( + outcome(&nodes[1], id, EPOCH, &first[0], 1), + Ok(Some(FFORVoucherOutcome::Fulfilled)) + ); + assert_eq!( + outcome(&nodes[1], id, EPOCH, &first[1], 2), + Ok(Some(FFORVoucherOutcome::Failed)) + ); + assert_eq!(book_epochs(&nodes[0], &nodes[1], id), (Some(EPOCH), None)); + assert_eq!(nodes[1].node.list_ffor_receiver_recovery_contexts().unwrap().len(), 1); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + // The second epoch replaces the channel book while the first epoch's archive record, + // journal and Closed proof stay readable by their own epoch ID. + let (second, _) = park_two_epoch(&nodes[0], &nodes[1], id, EPOCH2); + assert_ne!(second[0].payment_hash, first[0].payment_hash); + assert_eq!(book_epochs(&nodes[0], &nodes[1], id), (Some(EPOCH2), Some(EPOCH))); + assert_eq!(nodes[1].node.list_ffor_receiver_recovery_contexts().unwrap().len(), 2); + assert_eq!( + outcome(&nodes[1], id, EPOCH, &first[0], 1), + Ok(Some(FFORVoucherOutcome::Fulfilled)) + ); + let (second_hash, _) = activate_epoch(&nodes[0], &nodes[1], id, EPOCH2); + assert_eq!(outcome(&nodes[1], id, EPOCH2, &second[0], 1), Ok(None)); + assert!(nodes[1].node.release_ffor_receiver_closed(&id, &peer, EPOCH).is_err()); + let (_, fulfilled, failed) = + close_epoch(&nodes[0], &nodes[1], id, EPOCH2, second_hash, None); + assert!(fulfilled.is_empty()); + assert_eq!(failed, vec![second[0].htlc_id, second[1].htlc_id]); + assert_eq!( + outcome(&nodes[1], id, EPOCH2, &second[0], 1), + Ok(Some(FFORVoucherOutcome::Failed)) + ); + assert_eq!( + outcome(&nodes[1], id, EPOCH2, &second[1], 2), + Ok(Some(FFORVoucherOutcome::Failed)) + ); + assert_eq!( + outcome(&nodes[1], id, EPOCH, &first[0], 1), + Ok(Some(FFORVoucherOutcome::Fulfilled)) + ); + assert_eq!( + outcome(&nodes[1], id, EPOCH, &first[1], 2), + Ok(Some(FFORVoucherOutcome::Failed)) + ); + // Slots and hashes are bound to their own epoch. + assert!(outcome(&nodes[1], id, EPOCH, &second[0], 1).is_err()); + assert!(outcome(&nodes[1], id, EPOCH2, &first[0], 1).is_err()); + let history = nodes[1].node.ffor_receiver_recovery_context(&id, EPOCH).unwrap(); + assert_eq!(history.epoch_id(), EPOCH); + assert_eq!(history.activation_hash(), first_hash); + assert!(nodes[1].node.ffor_receiver_invoice_for_storage(&history).unwrap().is_none()); + assert_eq!(nodes[1].node.list_usable_channels().len(), 1); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + let manager = persist(&nodes[1]); + let monitor = get_monitor!(nodes[1], id).encode(); + nodes[0].node.peer_disconnected(receiver_id); + reload_node!(nodes[1], config, &manager, &[&monitor], persister, chain_monitor, reloaded); + // Stock restore replays the ordinary claim; no voucher event is ever synthesized. + for event in nodes[1].node.get_and_clear_pending_events() { + assert!(matches!(event, Event::PaymentClaimed { .. }), "unexpected {event:?}"); + } + assert!(outcome(&nodes[1], id, EPOCH, &first[0], 1).is_err()); + persist(&nodes[1]); + assert_eq!( + outcome(&nodes[1], id, EPOCH, &first[0], 1), + Ok(Some(FFORVoucherOutcome::Fulfilled)) + ); + assert_eq!( + outcome(&nodes[1], id, EPOCH2, &second[0], 1), + Ok(Some(FFORVoucherOutcome::Failed)) + ); + assert_eq!(book_epochs(&nodes[0], &nodes[1], id), (Some(EPOCH2), Some(EPOCH))); + assert_eq!(nodes[1].node.list_ffor_receiver_recovery_contexts().unwrap().len(), 2); + connect_nodes(&nodes[0], &nodes[1]); + assert_eq!(drain(&nodes[0], &nodes[1]), (Vec::new(), Vec::new())); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + } +} + +#[test] +fn ffor_close_manager_refuses_reuse_until_previous_epoch_is_terminal() { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let peer = nodes[0].node.get_our_node_id(); + let (vouchers, _) = park_two(&nodes[0], &nodes[1], id); + let attempt = || attempt_reuse(&nodes[0], &nodes[1], id, &vouchers[0], [93; 32]); + let refused = Err(FFORReceiverError::AlreadyRegistered); + assert_eq!(attempt(), refused); + let (hash, _) = activate(&nodes[0], &nodes[1], id); + assert_eq!(attempt(), refused); + nodes[1].node.prepare_ffor_receiver_close(&id, &peer, EPOCH).unwrap(); + persist(&nodes[1]); + assert!(nodes[1].node.release_ffor_receiver_close(&id, &peer, EPOCH, |_| Ok(())).unwrap()); + let ack = signed( + &nodes[0], + &nodes[1], + id, + Payload::CloseAck(CloseAck { + activation_hash: hash, + num_slots: 2, + settled: vec![0], + preimages: Vec::new(), + preimages_tlv_present: true, + }), + ); + nodes[1].node.accept_ffor_receiver_close_ack(&id, &peer, EPOCH, &ack).unwrap(); + assert_eq!(attempt(), refused); + persist(&nodes[1]); + assert!(nodes[1].node.release_ffor_receiver_drain(&id, &peer, EPOCH).unwrap()); + assert_eq!(attempt(), refused); + assert_eq!( + drain(&nodes[0], &nodes[1]), + (Vec::new(), vec![vouchers[0].htlc_id, vouchers[1].htlc_id]) + ); + nodes[0].node.get_and_clear_pending_events(); + // Every voucher is removed, but Draining without the retained Closed proof is not terminal. + assert_eq!(attempt(), refused); + let snapshot = get_monitor!(nodes[1], id).ffor_commitment_snapshot().unwrap(); + nodes[1].node.prepare_ffor_receiver_closed(&id, &peer, EPOCH, &snapshot).unwrap(); + // ClosedPendingPersistence before the final write, and still fenced after it. + assert_eq!(attempt(), refused); + persist(&nodes[1]); + assert_eq!(attempt(), refused); + assert!(nodes[1].node.release_ffor_receiver_closed(&id, &peer, EPOCH).unwrap()); + assert_eq!(book_epochs(&nodes[0], &nodes[1], id), (Some(EPOCH), None)); + let next = attempt().unwrap(); + assert_eq!(next.channel_id(), id); + assert_ne!(next.epoch_id(), EPOCH); + assert_eq!(book_epochs(&nodes[0], &nodes[1], id), (Some(next.epoch_id()), Some(EPOCH))); + // An exact retry of the new request is idempotent; the replaced epoch keeps its history. + assert_eq!(attempt(), Ok(next)); + assert!(nodes[1].node.ffor_receiver_recovery_context(&id, EPOCH).is_ok()); + assert_eq!( + outcome(&nodes[1], id, EPOCH, &vouchers[0], 1), + Ok(Some(FFORVoucherOutcome::Failed)) + ); + let manager = persist(&nodes[1]); + let monitor = get_monitor!(nodes[1], id).encode(); + assert!(restore(&nodes[1], &manager, &monitor).is_ok()); + // A closed channel cannot host another epoch, while its history stays readable. + nodes[1] + .node + .force_close_broadcasting_latest_txn(&id, &peer, "reuse after close".into()) + .unwrap(); + nodes[1].node.get_and_clear_pending_events(); + nodes[1].node.get_and_clear_pending_msg_events(); + nodes[1].chain_monitor.added_monitors.lock().unwrap().clear(); + assert_eq!( + attempt_reuse(&nodes[0], &nodes[1], id, &vouchers[0], [94; 32]), + Err(FFORCommitmentError::ChannelUnavailable.into()) + ); + assert!(nodes[1].node.ffor_receiver_recovery_context(&id, EPOCH).is_ok()); + assert_eq!(nodes[1].node.list_ffor_receiver_recovery_contexts().unwrap().len(), 1); + } +} + +#[test] +fn ffor_close_manager_refuses_reuse_while_aborting_and_permits_it_after_aborted_drain() { + for receiver_funds in [false, true] { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let peer = nodes[0].node.get_our_node_id(); + let receiver_id = nodes[1].node.get_our_node_id(); + let (vouchers, _) = park_two(&nodes[0], &nodes[1], id); + let attempt = || attempt_reuse(&nodes[0], &nodes[1], id, &vouchers[0], [95; 32]); + let refused = Err(FFORReceiverError::AlreadyRegistered); + nodes[0].node.peer_disconnected(receiver_id); + nodes[1].node.peer_disconnected(peer); + connect_nodes(&nodes[0], &nodes[1]); + let receiver_report = + get_event_msg!(&nodes[1], MessageSendEvent::SendChannelReestablish, peer); + let sender_report = + get_event_msg!(&nodes[0], MessageSendEvent::SendChannelReestablish, receiver_id); + nodes[0].node.handle_channel_reestablish(receiver_id, &receiver_report); + nodes[1].node.handle_channel_reestablish(peer, &sender_report); + // Aborting: the terminal abort is retained but not yet durable. + assert_eq!(attempt(), refused); + assert!(!nodes[1].node.release_ffor_receiver_reconnect_abort(&id, &peer, EPOCH).unwrap()); + persist(&nodes[1]); + assert!(nodes[1].node.release_ffor_receiver_reconnect_abort(&id, &peer, EPOCH).unwrap()); + // Durable abort, but the owned vouchers are still pending in the channel. + assert_eq!(attempt(), refused); + assert_eq!( + drain(&nodes[0], &nodes[1]), + (Vec::new(), vec![vouchers[0].htlc_id, vouchers[1].htlc_id]) + ); + nodes[0].node.get_and_clear_pending_events(); + let snapshot = get_monitor!(nodes[1], id).ffor_commitment_snapshot().unwrap(); + assert_eq!( + nodes[1].node.ffor_receiver_book_status(&id, &peer, &snapshot).unwrap(), + FFORReceiverStatus::Aborted { reason: FFORReceiverAbortReason::Disconnected } + ); + let next = attempt().unwrap(); + assert_ne!(next.epoch_id(), EPOCH); + assert_eq!(book_epochs(&nodes[0], &nodes[1], id), (Some(next.epoch_id()), Some(EPOCH))); + assert_eq!(nodes[1].node.list_ffor_receiver_recovery_contexts().unwrap().len(), 1); + assert!(nodes[1].node.ffor_receiver_recovery_context(&id, EPOCH).is_ok()); + let manager = persist(&nodes[1]); + let monitor = get_monitor!(nodes[1], id).encode(); + assert!(restore(&nodes[1], &manager, &monitor).is_ok()); + nodes[1].chain_monitor.added_monitors.lock().unwrap().clear(); + } +} + +#[test] +fn ffor_close_manager_refuses_reuse_when_archive_capacity_is_exhausted() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let (vouchers, _) = park_two(&nodes[0], &nodes[1], id); + let (hash, _) = activate(&nodes[0], &nodes[1], id); + close_epoch(&nodes[0], &nodes[1], id, EPOCH, hash, None); + let saved = nodes[1].node.ffor_recovery.lock().unwrap().encode(); + fill_registry(&mut *nodes[1].node.ffor_recovery.lock().unwrap()); + assert_eq!( + attempt_reuse(&nodes[0], &nodes[1], id, &vouchers[0], [96; 32]), + Err(FFORReceiverError::RecoveryUnavailable) + ); + // Capacity refusal leaves the terminal book and its history untouched. + assert_eq!(book_epochs(&nodes[0], &nodes[1], id), (Some(EPOCH), None)); + *nodes[1].node.ffor_recovery.lock().unwrap() = + ::read(&mut &saved[..]).unwrap(); + assert_eq!(nodes[1].node.find_ffor_receiver_request([96; 32]).unwrap(), None); + let next = attempt_reuse(&nodes[0], &nodes[1], id, &vouchers[0], [96; 32]).unwrap(); + assert_eq!(book_epochs(&nodes[0], &nodes[1], id), (Some(next.epoch_id()), Some(EPOCH))); + assert_eq!( + outcome(&nodes[1], id, EPOCH, &vouchers[0], 1), + Ok(Some(FFORVoucherOutcome::Failed)) + ); +} + +/// Split a serialized registry into its version byte and framed activation records. +fn archive_records(bytes: &[u8]) -> (u8, Vec>) { + let count = u16::from_be_bytes([bytes[1], bytes[2]]) as usize; + let mut offset = 3; + let mut records = Vec::new(); + for _ in 0..count { + let length = u32::from_be_bytes([ + bytes[offset], + bytes[offset + 1], + bytes[offset + 2], + bytes[offset + 3], + ]) as usize; + records.push(bytes[offset + 4..offset + 4 + length].to_vec()); + offset += 4 + length; + } + (bytes[0], records) +} + +#[test] +fn ffor_close_manager_restore_rejects_later_epoch_without_terminal_predecessor_record() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let id = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 40_000_000).2; + let peer = nodes[0].node.get_our_node_id(); + park_two(&nodes[0], &nodes[1], id); + let (hash, _) = activate(&nodes[0], &nodes[1], id); + nodes[1].node.prepare_ffor_receiver_close(&id, &peer, EPOCH).unwrap(); + persist(&nodes[1]); + assert!(nodes[1].node.release_ffor_receiver_close(&id, &peer, EPOCH, |_| Ok(())).unwrap()); + let ack = signed( + &nodes[0], + &nodes[1], + id, + Payload::CloseAck(CloseAck { + activation_hash: hash, + num_slots: 2, + settled: vec![0], + preimages: Vec::new(), + preimages_tlv_present: true, + }), + ); + nodes[1].node.accept_ffor_receiver_close_ack(&id, &peer, EPOCH, &ack).unwrap(); + persist(&nodes[1]); + assert!(nodes[1].node.release_ffor_receiver_drain(&id, &peer, EPOCH).unwrap()); + let draining_archive = nodes[1].node.ffor_recovery.lock().unwrap().encode(); + drain(&nodes[0], &nodes[1]); + nodes[0].node.get_and_clear_pending_events(); + let snapshot = get_monitor!(nodes[1], id).ffor_commitment_snapshot().unwrap(); + nodes[1].node.prepare_ffor_receiver_closed(&id, &peer, EPOCH, &snapshot).unwrap(); + persist(&nodes[1]); + assert!(nodes[1].node.release_ffor_receiver_closed(&id, &peer, EPOCH).unwrap()); + park_two_epoch(&nodes[0], &nodes[1], id, EPOCH2); + assert_eq!(book_epochs(&nodes[0], &nodes[1], id), (Some(EPOCH2), Some(EPOCH))); + let manager = persist(&nodes[1]); + let monitor = get_monitor!(nodes[1], id).encode(); + assert!(restore(&nodes[1], &manager, &monitor).is_ok()); + // The same channel bytes with an archive that lacks the replaced epoch fail closed. + let key = FFORRecoveryKey { channel_id: id, epoch_id: EPOCH2 }; + let mut without_first = FFORRecoveryRegistry::new(); + { + let recovery = nodes[1].node.ffor_recovery.lock().unwrap(); + let setup = recovery.get(&key).unwrap().clone(); + let activation = recovery.get_activation(&key).unwrap().clone(); + without_first.prepare_insert(&setup).unwrap().commit(); + without_first.prepare_activation(&setup, &activation).unwrap().commit(); + } + let complete = + core::mem::replace(&mut *nodes[1].node.ffor_recovery.lock().unwrap(), without_first); + let damaged = nodes[1].node.encode(); + *nodes[1].node.ffor_recovery.lock().unwrap() = complete; + assert!(matches!(restore(&nodes[1], &damaged, &monitor), Err(DecodeError::InvalidValue))); + // An archive holding an unresolved first epoch beside the second is refused by the reader. + let (version, draining) = archive_records(&draining_archive); + let (_, current) = archive_records(&nodes[1].node.ffor_recovery.lock().unwrap().encode()); + assert_eq!(draining.len(), 1); + assert_eq!(current.len(), 2); + let mut spliced = vec![version, 0, 2]; + for record in [&draining[0], ¤t[1]] { + spliced.extend_from_slice(&(record.len() as u32).to_be_bytes()); + spliced.extend_from_slice(record); + } + assert!(matches!( + ::read(&mut &spliced[..]), + Err(DecodeError::InvalidValue) + )); + assert!(restore(&nodes[1], &manager, &monitor).is_ok()); +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/context.rs b/lightning/src/ln/channelmanager/ffor_activation/context.rs index f42b8127a20..d62a8a95dd8 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/context.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/context.rs @@ -184,6 +184,7 @@ where channel.ffor_receiver_drain_binding(), channel.ffor_receiver_closed_completion_hash(), channel.ffor_receiver_drain_activation_hash(), + channel.ffor_receiver_predecessor_epoch(), ) .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; let activation = recovery.get_activation(key).ok_or(FFORReceiverError::NotRegistered)?; diff --git a/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs b/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs index 8227b93af1a..ccaa8c08019 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs @@ -3,7 +3,7 @@ use crate::chain::ChannelMonitorUpdateStatus; use crate::ln::channel::ffor_setup_test_messages; use crate::ln::channelmanager::ffor_recovery_tests::claim_ffor_preimage_for_test; use crate::ln::ffor::FFORReceiverAbortReason; -use crate::ln::ffor_tests::quiescence::{complete_handshake, request}; +use crate::ln::ffor_tests::quiescence::complete_handshake; use crate::ln::ffor_tests::{anchor_config, deliver_parked_voucher, offer_voucher}; use crate::ln::functional_test_utils::*; @@ -33,6 +33,20 @@ pub(super) fn park_two( pub(super) fn park_two_with_witnesses( sender: &Node, receiver: &Node, id: ChannelId, witnesses: Option>, +) -> ([FFORVoucher; 2], PaymentPreimage) { + park_two_epoch_with_witnesses(sender, receiver, id, witnesses, EPOCH) +} + +/// Park two vouchers under an explicit epoch, for a later epoch on an already used channel. +pub(super) fn park_two_epoch( + sender: &Node, receiver: &Node, id: ChannelId, epoch: [u8; 32], +) -> ([FFORVoucher; 2], PaymentPreimage) { + park_two_epoch_with_witnesses(sender, receiver, id, None, epoch) +} + +fn park_two_epoch_with_witnesses( + sender: &Node, receiver: &Node, id: ChannelId, witnesses: Option>, + epoch: [u8; 32], ) -> ([FFORVoucher; 2], PaymentPreimage) { let preimage = PaymentPreimage([*receiver.network_payment_count.as_ref().borrow(); 32]); let (first, voucher, _) = offer_voucher(sender, receiver, 2_000_000); @@ -41,6 +55,8 @@ pub(super) fn park_two_with_witnesses( let second_voucher = FFORVoucher { htlc_id: voucher.htlc_id + 1, payment_hash: hash, ..voucher }; let (mut init, mut accept) = ffor_setup_test_messages(sender, receiver, id, voucher); + init.header.epoch_id = epoch; + accept.header = init.header; if let Payload::Init(terms) = &mut init.payload { terms.witness_peers = witnesses; terms.budget_msat += second_voucher.amount_msat; @@ -84,12 +100,16 @@ pub(super) fn park_two_with_witnesses( assert_eq!(second.update_add_htlcs[0].htlc_id, second_voucher.htlc_id); assert_eq!(second.update_add_htlcs[0].cltv_expiry, second_voucher.cltv_expiry); deliver_parked_voucher(sender, receiver, second); - request(sender, receiver, id).unwrap(); + let proof = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); + receiver + .node + .request_ffor_receiver_quiescence(&id, &sender.node.get_our_node_id(), epoch, proof) + .unwrap(); complete_handshake(sender, receiver); let snapshot = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); receiver .node - .prepare_ffor_receiver_activation(&id, &sender.node.get_our_node_id(), EPOCH, &snapshot) + .prepare_ffor_receiver_activation(&id, &sender.node.get_our_node_id(), epoch, &snapshot) .unwrap(); persist(receiver); ([voucher, second_voucher], preimage) diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver.rs b/lightning/src/ln/channelmanager/ffor_activation/driver.rs index ab2b0af3135..0243455df48 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/driver.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/driver.rs @@ -48,6 +48,63 @@ where Ok(()) } + /// The manager half of the epoch reuse precondition, evaluated under the peer and archive + /// locks that will replace the book. The channel must report a terminal previous epoch, the + /// archive must hold that epoch's Closed proof or abort evidence matching the channel, the + /// runtime requirement of that terminal transition must be complete and no monitor update for + /// the channel may be in flight. A channel without a registration passes. + pub(in crate::ln::channelmanager) fn check_ffor_epoch_reuse( + &self, channel: &FundedChannel, recovery: &FFORRecoveryRegistry, + in_flight_monitor_updates: bool, + ) -> Result<(), FFORReceiverError> { + let previous = match channel.ffor_receiver_reusable_epoch()? { + Some(previous) => previous, + None => return Ok(()), + }; + let key = FFORRecoveryKey { channel_id: channel.context.channel_id(), epoch_id: previous }; + let setup = channel + .ffor_receiver_setup_record() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + .ok_or(FFORReceiverError::AlreadyRegistered)?; + recovery + .validate_channel_lifecycle( + &setup, + channel.ffor_receiver_fence(), + channel.ffor_receiver_abort_reason(), + channel.ffor_receiver_drain_binding(), + channel.ffor_receiver_closed_completion_hash(), + channel.ffor_receiver_drain_activation_hash(), + channel.ffor_receiver_predecessor_epoch(), + ) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + let activation = + recovery.get_activation(&key).ok_or(FFORReceiverError::AlreadyRegistered)?; + let closed = activation.is_closed() + && channel.ffor_receiver_abort_reason().is_none() + && channel.ffor_receiver_drain_binding().map(|(_, _, closed, _)| closed) == Some(true) + && channel.ffor_receiver_closed_completion_hash() + == activation.close_record().and_then(|close| close.completion_hash()); + let aborted = activation.is_aborted() + && activation.aborted_reason().is_some() + && channel.ffor_receiver_abort_reason() == activation.aborted_reason() + && channel.ffor_receiver_drain_binding().is_none(); + if !closed && !aborted { + return Err(FFORReceiverError::AlreadyRegistered); + } + if in_flight_monitor_updates + || channel.is_awaiting_monitor_update() + || channel.blocked_monitor_updates_pending() != 0 + { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + let runtime = self.ffor_activation.lock().unwrap(); + let requirement = &runtime.get(&key)?.requirement; + if !self.ffor_persistence.lock().unwrap().is_complete(requirement) { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + Ok(()) + } + /// Stage receiver-owned interception and exact signed Init before any peer traffic is released. /// /// The supplied connection must be the current authenticated native generation. The channel @@ -55,6 +112,12 @@ where /// limits and reserves bounded storage for the eventual accepted transcript and terminal state. /// No bytes are returned. Call `advance_ffor_receiver` after ordered manager persistence. /// This experimental facade does not authorize invoices or advertise offline readiness. + /// + /// A channel whose previous epoch reached Closed, or whose activation was aborted on + /// reconnect, may stage a new epoch under a new `local_request_id` once that terminal outcome + /// is retained in the archive and its manager write has completed. The previous epoch's + /// history stays readable by its own epoch ID. A previous epoch in any other state, including + /// a setup aborted before activation, refuses with [`FFORReceiverError::AlreadyRegistered`]. pub fn prepare_ffor_receiver( &self, channel_id: &ChannelId, connection: &FFORPeerConnection, parameters: FFORReceiverParameters, @@ -70,27 +133,33 @@ where { return Err(FFORCommitmentError::ChannelUnavailable.into()); } - { - let recovery = self.ffor_recovery.lock().unwrap(); - if let Some(request) = recovery.find_request(parameters.local_request_id) { - if !request.matches_intent(*channel_id, connection.peer, ¶meters) { - return Err(FFORReceiverError::AlreadyRegistered); - } - let header = request - .validate_recovery() - .map_err(|_| FFORReceiverError::RecoveryUnavailable)? - .header; - return Ok(FFORReceiverId { - channel_id: ChannelId(header.channel_id), - epoch_id: header.epoch_id, - }); + let mut recovery = self.ffor_recovery.lock().unwrap(); + if let Some(request) = recovery.find_request(parameters.local_request_id) { + if !request.matches_intent(*channel_id, connection.peer, ¶meters) { + return Err(FFORReceiverError::AlreadyRegistered); } + let header = request + .validate_recovery() + .map_err(|_| FFORReceiverError::RecoveryUnavailable)? + .header; + return Ok(FFORReceiverId { + channel_id: ChannelId(header.channel_id), + epoch_id: header.epoch_id, + }); } - let channel = peer + let peer_state = &mut *peer; + let in_flight = peer_state + .in_flight_monitor_updates + .get(channel_id) + .map_or(false, |(_, updates)| !updates.is_empty()); + let channel = peer_state .channel_by_id .get_mut(channel_id) .and_then(Channel::as_funded_mut) .ok_or(FFORCommitmentError::ChannelUnavailable)?; + // A previous epoch on this channel must be terminal in the channel, in the archive and in + // its completed write before a new epoch may replace its book. Its history is retained. + self.check_ffor_epoch_reuse(channel, &recovery, in_flight)?; let epoch_id = self.entropy_source.get_secure_random_bytes(); let mut message = FFORMessage { header: Header { channel_id: channel_id.0, epoch_id }, @@ -118,7 +187,6 @@ where parameters.claim_margin_blocks, parameters.local_request_id, )?; - let mut recovery = self.ffor_recovery.lock().unwrap(); let permit = recovery .prepare_request(&request) .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; @@ -432,9 +500,15 @@ where .request() .map_err(|_| FFORReceiverError::PersistenceUnavailable)?; channel.abort_ffor_receiver_request(FFORReceiverAbortReason::SetupRejected)?; + // The channel's current request may belong to a different epoch than the + // rejected message named; its own runtime entry carries the new barrier. + let current = FFORRecoveryKey { + channel_id, + epoch_id: channel.ffor_receiver_epoch_id().unwrap_or(key.epoch_id), + }; let mut runtime = self.ffor_activation.lock().unwrap(); if let Some(entry) = - runtime.entries.iter_mut().find(|entry| entry.key.channel_id == channel_id) + runtime.entries.iter_mut().find(|entry| entry.key == current) { entry.requirement = requirement; entry.may_send_init = false; @@ -527,11 +601,14 @@ where { continue; } - if let Some(entry) = runtime - .entries - .iter_mut() - .find(|entry| entry.key.channel_id == channel.context.channel_id()) - { + let key = FFORRecoveryKey { + channel_id: channel.context.channel_id(), + epoch_id: match channel.ffor_receiver_epoch_id() { + Some(epoch_id) => epoch_id, + None => continue, + }, + }; + if let Some(entry) = runtime.entries.iter_mut().find(|entry| entry.key == key) { if let Ok(requirement) = self.ffor_persistence.lock().unwrap().request() { entry.requirement = requirement; entry.may_send_init = false; diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver/advance.rs b/lightning/src/ln/channelmanager/ffor_activation/driver/advance.rs index 6b8de802810..55603778d26 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/driver/advance.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/advance.rs @@ -251,6 +251,7 @@ where channel.ffor_receiver_drain_binding(), channel.ffor_receiver_closed_completion_hash(), channel.ffor_receiver_drain_activation_hash(), + channel.ffor_receiver_predecessor_epoch(), ) .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; if matches!( @@ -512,6 +513,7 @@ where channel.ffor_receiver_drain_binding(), channel.ffor_receiver_closed_completion_hash(), channel.ffor_receiver_drain_activation_hash(), + channel.ffor_receiver_predecessor_epoch(), ) .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; let activation = recovery.get_activation(&key); diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver/advance_tests.rs b/lightning/src/ln/channelmanager/ffor_activation/driver/advance_tests.rs index 205533c68d9..bb9288873b6 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/driver/advance_tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/advance_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::chain::ChannelMonitorUpdateStatus; use crate::ln::channelmanager::ffor_activation::drain_tests::drain; +use crate::ln::ffor::FFORVoucherOutcome; use crate::ln::ffor_tests::quiescence::complete_handshake; use lightning_ffor::reestablish::{Reestablish, ReportedState}; use lightning_ffor::wire::{Abort, CloseAck, Preimage}; @@ -16,12 +17,24 @@ fn advance_monitor( fn park( sender: &Node, receiver: &Node, channel: ChannelId, +) -> (FFORReceiverId, FFORPeerConnection, FFORVoucher, PaymentPreimage) { + park_with_request_id(sender, receiver, channel, [91; 32]) +} + +fn park_with_request_id( + sender: &Node, receiver: &Node, channel: ChannelId, local_request_id: [u8; 32], ) -> (FFORReceiverId, FFORPeerConnection, FFORVoucher, PaymentPreimage) { let preimage = PaymentPreimage([*receiver.network_payment_count.as_ref().borrow(); 32]); let (update, voucher, _) = offer_voucher(sender, receiver, 2_000_000); let connection = receiver.node.ffor_peer_connection(&sender.node.get_our_node_id()).unwrap(); - let id = - receiver.node.prepare_ffor_receiver(&channel, &connection, parameters(&voucher)).unwrap(); + let id = receiver + .node + .prepare_ffor_receiver( + &channel, + &connection, + parameters_with_id(&voucher, local_request_id), + ) + .unwrap(); persist(receiver); let init = emit(receiver, &id, &connection); let accepted = accept(sender, receiver, channel, voucher, &init).encode().unwrap(); @@ -710,3 +723,141 @@ fn ffor_driver_cancel_owned_stfu_drives_reconnect_and_gates_failures_on_durable_ ); send_payment(sender, &[receiver], 1_000_000); } + +/// Drive a fresh epoch through the public facade to durable Draining with a released drain. +fn drive_to_draining( + sender: &Node, receiver: &Node, channel: ChannelId, local_request_id: [u8; 32], settled: bool, +) -> (FFORReceiverId, FFORPeerConnection, FFORVoucher, PaymentPreimage) { + let (id, connection, voucher, preimage) = + park_with_request_id(sender, receiver, channel, local_request_id); + persist(receiver); + let activate = emit(receiver, &id, &connection); + let (ack, hash) = activation_ack(sender, &id, &activate); + receiver.node.handle_ffor_receiver_message(&connection, &ack).unwrap(); + persist(receiver); + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &connection, |_| panic!("Active wire")).unwrap(), + FFORReceiverProgress::Active + ); + settlement_exits_stfu(sender, receiver, channel); + receiver.node.request_ffor_receiver_close(&id, &connection).unwrap(); + persist(receiver); + emit(receiver, &id, &connection); + let close_ack = signed( + sender, + &id, + Payload::CloseAck(CloseAck { + activation_hash: hash, + num_slots: 1, + settled: vec![u8::from(settled)], + preimages: if settled { + vec![Preimage { slot: 1, value: preimage.0 }] + } else { + Vec::new() + }, + preimages_tlv_present: true, + }), + ); + receiver.node.handle_ffor_receiver_message(&connection, &close_ack).unwrap(); + persist(receiver); + assert_eq!( + receiver.node.advance_ffor_receiver(&id, &connection, |_| panic!("drain wire")).unwrap(), + FFORReceiverProgress::Draining + ); + (id, connection, voucher, preimage) +} + +fn finish_closed<'a, 'b, 'c>( + sender: &Node<'a, 'b, 'c>, receiver: &Node<'a, 'b, 'c>, id: &FFORReceiverId, + connection: &FFORPeerConnection, voucher: &FFORVoucher, settled: bool, +) { + assert_eq!( + drain(sender, receiver), + if settled { + (vec![voucher.htlc_id], Vec::new()) + } else { + (Vec::new(), vec![voucher.htlc_id]) + } + ); + // PaymentSent releases the sender's held monitor update once its event is processed. + sender.node.get_and_clear_pending_events(); + sender.chain_monitor.added_monitors.lock().unwrap().clear(); + assert_eq!( + advance_monitor(receiver, id, connection).unwrap(), + FFORReceiverProgress::AwaitingPersistence + ); + persist(receiver); + assert_eq!( + receiver.node.advance_ffor_receiver(id, connection, |_| panic!("Closed wire")).unwrap(), + FFORReceiverProgress::Closed + ); +} + +fn facade_outcome( + receiver: &Node, id: &FFORReceiverId, voucher: &FFORVoucher, +) -> Result, FFORReceiverError> { + let context = receiver.node.ffor_receiver_recovery_context(&id.channel_id(), id.epoch_id())?; + receiver.node.ffor_receiver_voucher_outcome( + &context, + 1, + voucher.payment_hash, + voucher.amount_msat, + ) +} + +#[test] +fn ffor_driver_second_epoch_after_closed_completes_with_its_own_journal() { + for receiver_funds in [false, true] { + fixture!(nodes, sender, receiver, channel, receiver_funds); + let (first, connection, first_voucher, _) = + drive_to_draining(sender, receiver, channel, [91; 32], true); + // A new epoch is refused while the first one is still Draining. + assert_eq!( + receiver.node.prepare_ffor_receiver( + &channel, + &connection, + parameters_with_id(&first_voucher, [92; 32]) + ), + Err(FFORReceiverError::AlreadyRegistered) + ); + finish_closed(sender, receiver, &first, &connection, &first_voucher, true); + assert_eq!( + facade_outcome(receiver, &first, &first_voucher), + Ok(Some(FFORVoucherOutcome::Fulfilled)) + ); + send_payment(sender, &[receiver], 1_000_000); + // The original local request still resolves to the first epoch; a new one starts another. + assert_eq!( + receiver.node.prepare_ffor_receiver(&channel, &connection, parameters(&first_voucher)), + Ok(first) + ); + let (second, connection, second_voucher, _) = + drive_to_draining(sender, receiver, channel, [92; 32], false); + assert_ne!(second.epoch_id(), first.epoch_id()); + assert_eq!(second.channel_id(), channel); + assert_eq!( + receiver.node.advance_ffor_receiver(&first, &connection, |_| panic!("old epoch wire")), + Err(FFORReceiverError::UnknownEpoch) + ); + assert_eq!( + facade_outcome(receiver, &first, &first_voucher), + Ok(Some(FFORVoucherOutcome::Fulfilled)) + ); + finish_closed(sender, receiver, &second, &connection, &second_voucher, false); + assert_eq!( + facade_outcome(receiver, &second, &second_voucher), + Ok(Some(FFORVoucherOutcome::Failed)) + ); + assert_eq!( + facade_outcome(receiver, &first, &first_voucher), + Ok(Some(FFORVoucherOutcome::Fulfilled)) + ); + assert_eq!(receiver.node.list_ffor_receiver_recovery_contexts().unwrap().len(), 2); + let bytes = persist(receiver); + let monitor = get_monitor!(receiver, channel).encode(); + let restored = restore(receiver, &bytes, &monitor).unwrap(); + assert!(restored.ffor_receiver_recovery_context(&channel, first.epoch_id()).is_ok()); + assert!(restored.ffor_receiver_recovery_context(&channel, second.epoch_id()).is_ok()); + send_payment(sender, &[receiver], 1_000_000); + } +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs index ca1b085b9f3..338d0bcb050 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs @@ -21,8 +21,11 @@ macro_rules! fixture { } fn parameters(voucher: &FFORVoucher) -> FFORReceiverParameters { + parameters_with_id(voucher, [91; 32]) +} +fn parameters_with_id(voucher: &FFORVoucher, local_request_id: [u8; 32]) -> FFORReceiverParameters { FFORReceiverParameters { - local_request_id: [91; 32], + local_request_id, amounts_msat: vec![voucher.amount_msat], minimum_payment_msat: voucher.amount_msat, settlement_deadline: voucher.cltv_expiry - 20, diff --git a/lightning/src/ln/channelmanager/ffor_activation/receipt.rs b/lightning/src/ln/channelmanager/ffor_activation/receipt.rs index 38535770f9a..59df3bf22c3 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/receipt.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/receipt.rs @@ -31,7 +31,8 @@ where /// Import an authenticated witness preimage into its original stock channel monitor. /// /// Historical recovery remains available after deadlines, disconnection, conflicting peer - /// reports and channel removal. The retained native setup, activation and immutable witness + /// reports, channel removal and replacement of the epoch by a later one on the same channel. + /// The retained native setup, activation and immutable witness /// selection must match every receipt term. Neither a witness timestamp nor an application /// payment record supplies authority. Missing original monitor state, a changed funding output, /// a pending splice or a stale snapshot is refused without changing the channel. @@ -123,6 +124,9 @@ where { return Err(FFORCommitmentError::MonitorMismatch.into()); } + // An epoch this channel already replaced is terminal in the archive. Its preimages still + // protect the original monitor, but the current book owns no voucher of that epoch. + let archived_terminal = recovery.is_terminal(&key); // Registry data is immutable for this historical identity. Drop its lock before the // stock completion macro can release the peer lock and run unrelated completion actions. drop(recovery); @@ -145,12 +149,22 @@ where return Err(FFORCommitmentError::MonitorMismatch.into()); } let logger = WithChannelContext::from(&self.logger, &channel.context, None); - let update = channel.ffor_import_receipt_preimage( - &voucher, - preimage, - monitor.known_preimage, - &&logger, - )?; + let update = if channel.ffor_receiver_epoch_id() == Some(context.epoch_id()) { + channel.ffor_import_receipt_preimage( + &voucher, + preimage, + monitor.known_preimage, + &&logger, + )? + } else if archived_terminal { + channel.ffor_import_historical_receipt_preimage( + &voucher, + preimage, + monitor.known_preimage, + )? + } else { + return Err(FFORReceiverError::InvalidWitnessReceipt); + }; if let Some(update) = update { let update_id = update.update_id; handle_new_monitor_update!( diff --git a/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs b/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs index b585879b542..6e81664e78b 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs @@ -1,7 +1,7 @@ use super::*; use crate::chain::ChannelMonitorUpdateStatus; use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC; -use crate::ln::channelmanager::ffor_activation::drain_tests::{drain, park_two}; +use crate::ln::channelmanager::ffor_activation::drain_tests::{drain, park_two, park_two_epoch}; use crate::ln::channelmanager::ffor_recovery_tests::restore; use crate::ln::ffor::decrypt_ffor_witness_record; use crate::ln::ffor_recovery::ffor_test_witness_manifests as manifests; @@ -738,3 +738,116 @@ fn ffor_receipt_import_retains_protection_during_conflicting_reconnect() { } sender.node.get_and_clear_pending_msg_events(); } + +#[test] +fn ffor_receipt_import_previous_epoch_after_channel_reuse_protects_original_monitor() { + for receiver_funds in [false, true] { + let configs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &configs); + let config = anchor_config(); + let managers = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &managers); + let (funder, other) = if receiver_funds { (1, 0) } else { (0, 1) }; + let id = create_announced_chan_between_nodes_with_value( + &nodes, funder, other, 100_000, 40_000_000, + ) + .2; + let (sender, receiver) = (&nodes[0], &nodes[1]); + let peer_id = sender.node.get_our_node_id(); + let (vouchers, preimage) = park_two(sender, receiver, id); + let context = activate(sender, receiver, id); + let receipt = receipt(&context, &manifests(&context, 1)[0].1, preimage, 90); + { + let peers = sender.node.per_peer_state.read().unwrap(); + let mut peer = peers.get(&receiver.node.get_our_node_id()).unwrap().lock().unwrap(); + peer.channel_by_id.get_mut(&id).unwrap().as_funded_mut().unwrap().exit_quiescence(); + } + receiver.node.prepare_ffor_receiver_close(&id, &peer_id, EPOCH).unwrap(); + persist(receiver.node); + assert!(receiver + .node + .release_ffor_receiver_close(&id, &peer_id, EPOCH, |_| Ok(())) + .unwrap()); + let ack = signed( + sender, + &context, + Payload::CloseAck(CloseAck { + activation_hash: context.activation_hash(), + num_slots: 2, + settled: vec![0], + preimages: Vec::new(), + preimages_tlv_present: true, + }), + ); + receiver.node.accept_ffor_receiver_close_ack(&id, &peer_id, EPOCH, &ack).unwrap(); + persist(receiver.node); + assert!(receiver.node.release_ffor_receiver_drain(&id, &peer_id, EPOCH).unwrap()); + let (fulfilled, failed) = drain(sender, receiver); + assert!(fulfilled.is_empty()); + assert_eq!(failed, vouchers.iter().map(|voucher| voucher.htlc_id).collect::>()); + sender.node.get_and_clear_pending_events(); + let monitor = get_monitor!(receiver, id).ffor_commitment_snapshot().unwrap(); + receiver.node.prepare_ffor_receiver_closed(&id, &peer_id, EPOCH, &monitor).unwrap(); + persist(receiver.node); + assert!(receiver.node.release_ffor_receiver_closed(&id, &peer_id, EPOCH).unwrap()); + // A later epoch now owns the channel book. The first epoch's receipt still belongs to + // the original monitor and never touches the new book or an ordinary payment. + let (later, _) = park_two_epoch(sender, receiver, id, [82; 32]); + assert_ne!(later[0].payment_hash, vouchers[0].payment_hash); + receiver.chain_monitor.added_monitors.lock().unwrap().clear(); + assert!(matches!( + receiver + .node + .import_ffor_receiver_witness_receipt( + &context, + &receipt, + &snapshot(receiver, &context, &receipt) + ) + .unwrap(), + FFORWitnessReceiptProgress::PendingMonitor { .. } + )); + check_added_monitors(receiver, 1); + let known = get_monitor!(receiver, id).get_stored_preimages(); + assert_eq!(known[&vouchers[0].payment_hash].0, preimage); + assert!(!known.contains_key(&later[0].payment_hash)); + assert!(matches!( + receiver + .node + .import_ffor_receiver_witness_receipt( + &context, + &receipt, + &snapshot(receiver, &context, &receipt) + ) + .unwrap(), + FFORWitnessReceiptProgress::MonitorPersisted { .. } + )); + assert_eq!( + receiver + .node + .ffor_receiver_voucher_outcome( + &context, + 1, + vouchers[0].payment_hash, + vouchers[0].amount_msat + ) + .unwrap(), + Some(crate::ln::ffor::FFORVoucherOutcome::Failed) + ); + let restored = + restore(receiver, &persist(receiver.node), &get_monitor!(receiver, id).encode()) + .unwrap(); + assert!(matches!( + restored + .import_ffor_receiver_witness_receipt( + &context, + &receipt, + &snapshot(receiver, &context, &receipt) + ) + .unwrap(), + FFORWitnessReceiptProgress::MonitorPersisted { .. } + )); + no_payment_events(receiver.node); + no_payment_events(&restored); + receiver.node.get_and_clear_pending_msg_events(); + } +} diff --git a/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs b/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs index b6dbade52c9..185ab1f190d 100644 --- a/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs +++ b/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs @@ -105,6 +105,7 @@ where channel.ffor_receiver_drain_binding(), channel.ffor_receiver_closed_completion_hash(), channel.ffor_receiver_drain_activation_hash(), + channel.ffor_receiver_predecessor_epoch(), ) .map_err(|_| ())?; let activation = recovery.get_activation(&key).ok_or(())?; diff --git a/lightning/src/ln/ffor.rs b/lightning/src/ln/ffor.rs index 11b1ce3cb09..486fba4f7db 100644 --- a/lightning/src/ln/ffor.rs +++ b/lightning/src/ln/ffor.rs @@ -159,7 +159,8 @@ pub enum FFORReceiverStatus { pub enum FFORReceiverError { /// The channel or supplied commitment evidence does not meet the required conditions. ChannelState(FFORCommitmentError), - /// This channel already has a registration, including a permanently retained aborted one. + /// This channel already has a registration that is not terminal in both the channel and the + /// archive, or the request would register the same epoch again. AlreadyRegistered, /// This channel has no receiver registration. NotRegistered, diff --git a/lightning/src/ln/ffor_recovery.rs b/lightning/src/ln/ffor_recovery.rs index 9a73f5d1c55..08ee9a750a5 100644 --- a/lightning/src/ln/ffor_recovery.rs +++ b/lightning/src/ln/ffor_recovery.rs @@ -168,6 +168,22 @@ impl Entry { && self.record.setup.funding_txo() == other.record.setup.funding_txo()) } + /// Only an archived terminal outcome lets a later epoch share this record's channel. A + /// setup-only record carries no durable abort evidence and therefore never permits reuse. + fn is_terminal(&self) -> bool { + self.record + .activation + .as_ref() + .map_or(false, |activation| activation.is_closed() || activation.is_aborted()) + } + + /// Two records may share a channel or funding identity only when at most one of them is + /// still unresolved. Exact duplicate keys are always refused. + fn conflicts_with(&self, other: &Self) -> bool { + self.has_same_identity(other) + && (self.key == other.key || (!self.is_terminal() && !other.is_terminal())) + } + fn transition_reservation(record: &StoredSetup) -> usize { if record.request.is_some() { return MAX_RECORD_BYTES.saturating_sub(record.serialized_length()); @@ -287,15 +303,54 @@ impl FFORRecoveryRegistry { &self, setup: &FFORReceiverSetup, fence: Option<(FFORReceiverFencePhase, [u8; 32])>, abort_reason: Option, ) -> Result<(), DecodeError> { - self.validate_channel_lifecycle(setup, fence, abort_reason, None, None, None) + self.validate_channel_lifecycle(setup, fence, abort_reason, None, None, None, None) + } + + /// A channel book for one epoch may coexist with earlier epochs of the same channel only + /// when every earlier record is terminal and the book names the one it replaced. A book + /// without a predecessor must be the channel's only epoch. Fails closed on any other shape, + /// including a pending request for a different epoch of the same channel. + pub(crate) fn validate_channel_predecessor( + &self, key: FFORRecoveryKey, predecessor: Option<[u8; 32]>, + ) -> Result<(), DecodeError> { + if self.pending_requests.iter().any(|pending| { + pending.key.channel_id == key.channel_id && pending.key.epoch_id != key.epoch_id + }) { + return Err(DecodeError::InvalidValue); + } + let others: Vec<&Entry> = self + .entries + .iter() + .filter(|entry| entry.key.channel_id == key.channel_id && entry.key != key) + .collect(); + match predecessor { + None if others.is_empty() => Ok(()), + Some(previous) + if previous != key.epoch_id + && others.iter().all(|entry| entry.is_terminal()) + && others.iter().any(|entry| entry.key.epoch_id == previous) => + { + Ok(()) + }, + _ => Err(DecodeError::InvalidValue), + } + } + + /// Whether the archive retains a Closed or aborted activation record for this exact epoch. + pub(crate) fn is_terminal(&self, key: &FFORRecoveryKey) -> bool { + self.entries.iter().any(|entry| entry.key == *key && entry.is_terminal()) } /// Check the channel-owned close binding against exact authenticated archived evidence. + /// `predecessor` is the terminal epoch the channel's current book replaced, if any; the + /// archive must retain that epoch's Closed or aborted record, and no other unresolved epoch + /// may share the channel. pub(crate) fn validate_channel_lifecycle( &self, setup: &FFORReceiverSetup, fence: Option<(FFORReceiverFencePhase, [u8; 32])>, abort_reason: Option, drain: Option<([u8; 32], Vec, bool, Option)>, completion_hash: Option<[u8; 32]>, drain_activation_hash: Option<[u8; 32]>, + predecessor: Option<[u8; 32]>, ) -> Result<(), DecodeError> { if !self.contains_exact(setup) { return Err(DecodeError::InvalidValue); @@ -304,6 +359,7 @@ impl FFORRecoveryRegistry { let header = authenticated.header(); let key = FFORRecoveryKey { channel_id: ChannelId(header.channel_id), epoch_id: header.epoch_id }; + self.validate_channel_predecessor(key, predecessor)?; if let Some(record) = self.get_activation(&key) { if record.is_draining() { let close = record.close_record().ok_or(DecodeError::InvalidValue)?; @@ -388,10 +444,15 @@ impl FFORRecoveryRegistry { pub(crate) fn validate_activation_monitor( &self, channel_id: ChannelId, monitor: &FFORMonitorRecoveryIdentity, ) -> Result<(), DecodeError> { + // Earlier epochs of the same channel are terminal; only the unresolved one binds the monitor. let entry = self .entries .iter() - .find(|entry| entry.key.channel_id == channel_id) + .find(|entry| { + entry.key.channel_id == channel_id + && entry.record.activation.is_some() + && !entry.is_terminal() + }) .ok_or(DecodeError::InvalidValue)?; let activation = entry.record.activation.as_ref().ok_or(DecodeError::InvalidValue)?; activation.validate_monitor(&entry.record.setup, monitor) @@ -414,7 +475,8 @@ impl FFORRecoveryRegistry { /// Reauthenticate and reserve storage before changing the channel. Retain this permit while /// installing the identical prepared setup under the peer lock, then commit it on success. - /// Exact retries are idempotent; the current format permits only one setup per channel/funding. + /// Exact retries are idempotent. A channel or funding identity may hold several epochs only + /// when every earlier record is Closed or aborted; those records are never replaced. pub(crate) fn prepare_insert( &mut self, setup: &FFORReceiverSetup, ) -> Result, FFORRecoveryError> { @@ -436,14 +498,15 @@ impl FFORRecoveryRegistry { }) { return Err(FFORRecoveryError::ConflictingRecord); } - if let Some(existing) = - self.entries.iter().find(|existing| existing.has_same_identity(&entry)) - { - if existing.key != entry.key || existing.record.encode() != entry.record.encode() { + if let Some(existing) = self.entries.iter().find(|existing| existing.key == entry.key) { + if existing.record.encode() != entry.record.encode() { return Err(FFORRecoveryError::ConflictingRecord); } return Ok(FFORRecoveryInsertion { registry: self, entry: None }); } + if self.entries.iter().any(|existing| existing.conflicts_with(&entry)) { + return Err(FFORRecoveryError::ConflictingRecord); + } self.check_capacity(entry.encoded_bytes, entry.reserved_transition_bytes())?; Ok(FFORRecoveryInsertion { registry: self, entry: Some(entry) }) } @@ -644,7 +707,7 @@ impl Readable for FFORRecoveryRegistry { if entry.record.request.as_ref().map_or(false, |request| { registry.find_request(request.local_request_id()).is_some() }) || entry.encoded_bytes != length - || registry.entries.iter().any(|existing| existing.has_same_identity(&entry)) + || registry.entries.iter().any(|existing| existing.conflicts_with(&entry)) { return Err(DecodeError::InvalidValue); } @@ -678,9 +741,10 @@ impl Readable for FFORRecoveryRegistry { || (saved.record.chain_hash() == entry.record.chain_hash() && saved.record.funding_txo() == entry.record.funding_txo()) }) || registry.entries.iter().any(|saved| { - saved.key.channel_id == entry.key.channel_id - || (saved.record.setup.chain_hash() == entry.record.chain_hash() - && saved.record.setup.funding_txo() == entry.record.funding_txo()) + !saved.is_terminal() + && (saved.key.channel_id == entry.key.channel_id + || (saved.record.setup.chain_hash() == entry.record.chain_hash() + && saved.record.setup.funding_txo() == entry.record.funding_txo())) }) { return Err(DecodeError::InvalidValue); } diff --git a/lightning/src/ln/ffor_recovery/request.rs b/lightning/src/ln/ffor_recovery/request.rs index 1fec4c625f9..0bec5055425 100644 --- a/lightning/src/ln/ffor_recovery/request.rs +++ b/lightning/src/ln/ffor_recovery/request.rs @@ -118,9 +118,11 @@ impl FFORRecoveryRegistry { || (entry.record.chain_hash() == request.chain_hash() && entry.record.funding_txo() == request.funding_txo()) }) || self.entries.iter().any(|entry| { - entry.key.channel_id == pending.key.channel_id - || (entry.record.setup.chain_hash() == request.chain_hash() - && entry.record.setup.funding_txo() == request.funding_txo()) + // Earlier epochs of this channel must be Closed or aborted in the archive. + !entry.is_terminal() + && (entry.key.channel_id == pending.key.channel_id + || (entry.record.setup.chain_hash() == request.chain_hash() + && entry.record.setup.funding_txo() == request.funding_txo())) }) { return Err(FFORRecoveryError::ConflictingRecord); } diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs index f92dffa1e81..22f392ed8d7 100644 --- a/lightning/src/ln/ffor_recovery/tests.rs +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -150,6 +150,15 @@ fn decode(bytes: &[u8]) -> Result { FFORRecoveryRegistry::read(&mut &bytes[..]) } +/// Insert unrelated fixture setups until the record count limit is reached. +pub(crate) fn fill_registry(registry: &mut FFORRecoveryRegistry) { + let mut identity = 1u8; + while registry.entries.len() + registry.pending_requests.len() < MAX_RECORDS { + registry.prepare_insert(&fixture(identity, false).record()).unwrap().commit(); + identity += 1; + } +} + pub(crate) fn full_registry() -> FFORRecoveryRegistry { let mut registry = FFORRecoveryRegistry::new(); for identity in 1..=MAX_RECORDS {