diff --git a/.github/workflows/ffor-protocol.yml b/.github/workflows/ffor-protocol.yml new file mode 100644 index 00000000000..8dee1f48dad --- /dev/null +++ b/.github/workflows/ffor-protocol.yml @@ -0,0 +1,48 @@ +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 + - run: cargo fuzz run witness -- -max_len=196609 -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/FFOR.md b/FFOR.md new file mode 100644 index 00000000000..d1062e70333 --- /dev/null +++ b/FFOR.md @@ -0,0 +1,638 @@ +# 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. Native manager +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. 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, +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. + +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 +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 +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. +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 +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 +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 +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. + +`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 +all owned HTLCs drained. Ordinary payments work after drain. Registered voucher +hashes remain excluded from ordinary receiving. + +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 +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 512 KiB +activation record limit. Capacity is reserved before channel mutation. An Activating +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. 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 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 +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. 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, 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 +fence is held. Force-close remains available. A required even field makes older +channel readers refuse fenced state. + +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. 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 +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 +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. + +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 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. + +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 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. +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 +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, +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. + +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. + +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 +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 +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, 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. +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. + +## 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. + +## 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 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 terminal book. + +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`. +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. +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. + +The private fence tests cover both phases and funding directions, restored pending +state rejection, preimage retention, cleared STFU flags, signer and monitor callbacks, +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. 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, terminal book 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. + +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. + +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. +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 +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 +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. + +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. + +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. + +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. + +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, +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 + +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. + +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, 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-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/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..1d203108085 --- /dev/null +++ b/lightning-ffor/README.md @@ -0,0 +1,263 @@ +# 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 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 +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. + +## Receiver witness provisioning primitives + +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 +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. +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. + +## 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. + +`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 +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. 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 +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..83b7bb9f2e4 --- /dev/null +++ b/lightning-ffor/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[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 + +[[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 new file mode 100644 index 00000000000..00fdb9b02ff --- /dev/null +++ b/lightning-ffor/fuzz/README.md @@ -0,0 +1,34 @@ +# 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. + +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, 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. 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/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/fuzz_targets/witness.rs b/lightning-ffor/fuzz/fuzz_targets/witness.rs new file mode 100644 index 00000000000..3f87834ecb0 --- /dev/null +++ b/lightning-ffor/fuzz/fuzz_targets/witness.rs @@ -0,0 +1,103 @@ +#![no_main] + +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::PublicKey; +use libfuzzer_sys::fuzz_target; +use lightning_ffor::setup::AuthenticatedSetup; +use lightning_ffor::wire::Message; +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 { + 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); + } + // 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 new file mode 100644 index 00000000000..ff816dbe130 --- /dev/null +++ b/lightning-ffor/fuzz/seed_corpus.py @@ -0,0 +1,52 @@ +"""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") + +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") + +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/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..c5fb7d4d1a3 --- /dev/null +++ b/lightning-ffor/src/book.rs @@ -0,0 +1,172 @@ +//! 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 bitcoin::locktime::absolute::LOCK_TIME_THRESHOLD; + +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 height, strictly below Bitcoin's locktime threshold. + 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 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, + /// 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.voucher_expiry >= LOCK_TIME_THRESHOLD + || 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..e46b6c0bc43 --- /dev/null +++ b/lightning-ffor/src/lib.rs @@ -0,0 +1,21 @@ +//! 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; +pub mod witness; 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..1164e6b812f --- /dev/null +++ b/lightning-ffor/src/setup.rs @@ -0,0 +1,336 @@ +//! 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), + } + } + + /// 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 + /// 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..39be618a950 --- /dev/null +++ b/lightning-ffor/src/transcript.rs @@ -0,0 +1,142 @@ +//! 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 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 public key did not sign the exact domain-bound digest. + 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> { + 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)?; + let mut normalized = signature; + normalized.normalize_s(); + if normalized != signature { + return Err(SignatureError::HighS); + } + let message = Message::from_digest(digest); + Secp256k1::verification_only() + .verify_ecdsa(&message, &signature, expected_signer) + .map_err(|_| SignatureError::Invalid) +} + +pub(crate) 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..04345c00295 --- /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; +pub(crate) 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..bf4d8e02d12 --- /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(crate) struct Reader<'a> { + remaining: &'a [u8], +} + +impl<'a> Reader<'a> { + pub(crate) fn new(bytes: &'a [u8]) -> Self { + Self { remaining: bytes } + } + + 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(crate) fn array(&mut self) -> Result<[u8; N], WireError> { + self.take(N)?.try_into().map_err(|_| WireError::Truncated) + } + + pub(crate) fn u8(&mut self) -> Result { + Ok(self.array::<1>()?[0]) + } + pub(crate) fn u16(&mut self) -> Result { + Ok(u16::from_be_bytes(self.array()?)) + } + pub(crate) fn u32(&mut self) -> Result { + Ok(u32::from_be_bytes(self.array()?)) + } + pub(crate) fn u64(&mut self) -> Result { + Ok(u64::from_be_bytes(self.array()?)) + } + pub(crate) 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(crate) 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(crate) fn required(entries: &mut Vec, kind: u64) -> Result, WireError> { + optional(entries, kind).ok_or(WireError::MissingTlv(kind)) +} + +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(crate) 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(crate) struct Writer(pub(crate) Vec); + +impl Writer { + pub(crate) fn new() -> Self { + Self(Vec::new()) + } + 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(crate) fn u16(&mut self, value: u16) -> Result<(), WireError> { + self.put(&value.to_be_bytes()) + } + pub(crate) fn u32(&mut self, value: u32) -> Result<(), WireError> { + self.put(&value.to_be_bytes()) + } + pub(crate) 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(crate) 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(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/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/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/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/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..29485dcb33b --- /dev/null +++ b/lightning-ffor/src/witness/mod.rs @@ -0,0 +1,169 @@ +//! 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. +//! 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. +//! 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. Plaintext body verification is separate +//! from AEAD authentication, which must be performed by the caller's decryption boundary. +//! +//! ``` +//! 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 fetch; +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}; +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, +}; +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; +/// 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. +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 supported witness request/response codecs. + 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, + /// 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, + /// 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 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, + /// 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. + Replay, +} + +impl fmt::Display for WitnessError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid FFOR witness data: {self:?}") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for WitnessError {} + +impl From for WitnessError { + fn from(error: SignatureError) -> Self { + Self::Signature(error) + } +} + +impl From for WitnessError { + fn from(error: crate::wire::WireError) -> Self { + Self::Tlv(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/src/witness/record.rs b/lightning-ffor/src/witness/record.rs new file mode 100644 index 00000000000..e72bc24d5f2 --- /dev/null +++ b/lightning-ffor/src/witness/record.rs @@ -0,0 +1,227 @@ +use alloc::vec::Vec; +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::PublicKey; + +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 + 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. +#[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 { + 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 + { + 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(entry) + } +} + +/// Authenticated opaque ciphertext bound to one provisioned witness, manifest and book entry. +/// +/// 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, +} + +impl AuthenticatedEncryptedRecord { + /// The exact checked encrypted record, still requiring authenticated decryption. + pub fn record(&self) -> &EncryptedRecord { + &self.record + } +} 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/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..31888ea97e7 --- /dev/null +++ b/lightning-ffor/tests/book.rs @@ -0,0 +1,164 @@ +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 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]); + 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/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/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/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/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/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/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-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..30b3ef59f1c --- /dev/null +++ b/lightning-ffor/tests/setup.rs @@ -0,0 +1,394 @@ +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)); + } +} + +#[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/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-ffor/tests/witness.rs b/lightning-ffor/tests/witness.rs new file mode 100644 index 00000000000..aff40573cce --- /dev/null +++ b/lightning-ffor/tests/witness.rs @@ -0,0 +1,414 @@ +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; + +#[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() +} + +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); + } +} 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 new file mode 100644 index 00000000000..49ec5a34850 --- /dev/null +++ b/lightning-ffor/tests/witness/fetch.rs @@ -0,0 +1,479 @@ +use super::*; +use lightning_ffor::wire::{Tlv, WireError}; +use lightning_ffor::witness::{ + EncryptedRecord, FetchParameters, FetchResponse, FetchResult, PendingFetch, SignedFetch, + UnsignedFetch, CIPHERTEXT_LEN, RECORD_HEADER_LEN, +}; + +#[path = "body.rs"] +mod body; + +#[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); } + } +} 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" } 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 7727366c2ad..aca92803ae6 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,134 @@ 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, + destination_script: inner.destination_script.clone(), + 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(), + }) + } + + /// 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 + /// [`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 { + 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/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/channel.rs b/lightning/src/ln/channel.rs index a89e240edc1..08f46f0e9db 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -99,6 +99,14 @@ 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::{ + ffor_drain_completion_digest, FFORReceiverDrainCompletion, FFORReceiverFencePhase, + FFORReceiverRequest, FFORReceiverSetup, FFORReestablishOutcome, +}; + #[cfg(any(test, feature = "_test_utils"))] #[allow(unused)] pub struct ChannelValueStat { @@ -1166,6 +1174,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, @@ -1678,8 +1687,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))) @@ -1784,6 +1795,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) @@ -1803,6 +1815,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) @@ -1822,6 +1835,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) @@ -1841,6 +1855,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) @@ -1863,6 +1878,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) @@ -1906,6 +1922,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 @@ -2154,6 +2171,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)) @@ -2815,6 +2833,7 @@ impl_writeable_tlv_based!(SpliceInstructions, { pub(crate) enum QuiescentAction { Splice(SpliceInstructions), + FFORReceiver(ffor::FFORReceiverQuiescence), #[cfg(any(test, fuzzing))] DoNothing, } @@ -2822,16 +2841,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`]. @@ -2972,6 +2994,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 @@ -3647,6 +3671,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, @@ -3885,6 +3910,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, @@ -4057,7 +4083,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 @@ -4069,6 +4095,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, @@ -6803,6 +6830,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))] @@ -7003,7 +7032,6 @@ where contributed_outputs: outputs, }) }, - #[cfg(any(test, fuzzing))] Some(quiescent_action) => { self.quiescent_action = Some(quiescent_action); None @@ -7319,6 +7347,16 @@ 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 {}; + } + 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, @@ -7386,7 +7424,9 @@ where channel_id: Some(self.context.channel_id()), }; - if !self.context.channel_state.can_generate_new_commitment() { + 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 // `claim_htlc_while_disconnected_dropping_mon_update` and must match exactly - // `claim_htlc_while_disconnected_dropping_mon_update` would not work correctly if we @@ -7494,7 +7534,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; @@ -7510,7 +7552,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 }); @@ -7564,6 +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.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"); } @@ -7678,6 +7723,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())); @@ -7755,6 +7801,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())); } @@ -7786,6 +7833,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, @@ -7803,6 +7851,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 { @@ -7829,6 +7878,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() { @@ -7856,6 +7906,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())); } @@ -7872,6 +7923,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())); } @@ -7892,6 +7944,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!"; @@ -7938,6 +7991,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { + self.context.check_ffor_mutation()?; debug_assert!(self .context .interactive_tx_signing_session @@ -8045,6 +8099,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { + self.context.check_ffor_commitment_round()?; self.commitment_signed_check_state()?; if !self.pending_funding().is_empty() { @@ -8089,6 +8144,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { + self.context.check_ffor_commitment_round()?; self.commitment_signed_check_state()?; let mut messages = BTreeMap::new(); @@ -8158,6 +8214,7 @@ where } fn commitment_signed_check_state(&self) -> Result<(), ChannelError> { + 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(), @@ -8192,6 +8249,7 @@ where where L::Target: Logger, { + self.context.check_ffor_commitment_round()?; if self .holder_commitment_point .advance(&self.context.holder_signer, &self.context.secp_ctx, logger) @@ -8289,7 +8347,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; @@ -8305,7 +8363,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; @@ -8338,6 +8396,9 @@ where F::Target: FeeEstimator, L::Target: Logger, { + if self.context.ffor_blocks_commitment_round() { + return (None, Vec::new()); + } if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) && self.context.channel_state.can_generate_new_commitment() { @@ -8356,6 +8417,9 @@ where F::Target: FeeEstimator, L::Target: Logger, { + if self.context.ffor_blocks_commitment_round() { + 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()); @@ -8515,7 +8579,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; @@ -8560,6 +8626,7 @@ where F::Target: FeeEstimator, L::Target: Logger, { + 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(), @@ -8680,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 { @@ -8907,7 +8986,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. @@ -9033,6 +9112,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() { @@ -9133,6 +9213,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() { @@ -9226,6 +9307,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"); } @@ -9276,11 +9358,17 @@ 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(()) } + 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. self.context.sent_message_awaiting_response = None; @@ -9394,6 +9482,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.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(), + 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 @@ -9520,6 +9616,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())); } @@ -9548,6 +9645,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.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, + 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); @@ -9660,6 +9765,9 @@ where L::Target: Logger, CBP: Fn(u64) -> BlindedMessagePath, { + if self.context.ffor_blocks_commitment_round() { + return None; + } debug_assert!( self.holder_commitment_point.next_transaction_number() <= INITIAL_COMMITMENT_NUMBER - 2 ); @@ -9718,6 +9826,7 @@ where where L::Target: Logger, { + 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(); @@ -9820,6 +9929,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 { @@ -9844,60 +9956,14 @@ where NS::Target: NodeSigner, CBP: Fn(u64) -> BlindedMessagePath { - 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."); + if self.context.is_ffor_frozen() { + 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); } } - - // 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 - ))); - } + 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). @@ -10294,6 +10360,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( @@ -10313,6 +10382,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 @@ -10397,6 +10469,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(), @@ -10567,6 +10640,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( @@ -10629,6 +10705,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"))); } @@ -10954,6 +11031,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(), @@ -11869,7 +11952,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() && self.ffor_receiver_drain_binding().is_none() { + 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); // This is generally the first function which gets called on any given channel once we're @@ -11894,7 +11980,8 @@ 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 { + 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 @@ -11918,7 +12005,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. @@ -11933,6 +12020,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!( @@ -12095,6 +12183,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() }; @@ -12121,6 +12210,12 @@ 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(), + )); + } if self.holder_commitment_point.current_point().is_none() { return Err(ChannelError::WarnAndDisconnect(format!( "Channel {} commitment point needs to be advanced once before spliced", @@ -12305,6 +12400,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)?; @@ -12376,6 +12472,7 @@ where ES::Target: EntropySource, L::Target: Logger, { + self.context.check_ffor_mutation()?; let splice_funding = self.validate_splice_ack(msg)?; log_info!( @@ -12425,6 +12522,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 @@ -12531,6 +12629,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 {}", @@ -12634,6 +12733,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() @@ -12760,7 +12865,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_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 @@ -12853,7 +12959,7 @@ where channel_id: Some(self.context.channel_id()), }; self.context.channel_state.set_awaiting_remote_revoke(); - monitor_update + Ok(monitor_update) } #[rustfmt::skip] @@ -12893,6 +12999,7 @@ where where L::Target: Logger, { + 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); @@ -12980,7 +13087,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 { @@ -13014,6 +13121,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() @@ -13161,6 +13269,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() { @@ -13200,6 +13309,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() @@ -13237,6 +13347,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())); } @@ -13302,6 +13413,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() { @@ -13326,6 +13442,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 @@ -13343,6 +13460,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() @@ -13705,6 +13825,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() @@ -13994,6 +14115,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; @@ -14928,6 +15050,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(()) @@ -15295,6 +15419,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), @@ -15342,8 +15467,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(); @@ -15597,7 +15727,7 @@ where } } - Ok(FundedChannel { + let mut channel = FundedChannel { funding: FundingScope { value_to_self_msat, counterparty_selected_channel_reserve_satoshis, @@ -15671,6 +15801,7 @@ where holding_cell_update_fee, next_holder_htlc_id, next_counterparty_htlc_id, + ffor_receiver_book, update_time_counter, feerate_per_kw, @@ -15735,7 +15866,11 @@ where holder_commitment_point, pending_splice, quiescent_action, - }) + ffor_reconnect_outcome: None, + }; + channel.ffor_restored()?; + channel.restore_ffor_receiver_quiescence()?; + Ok(channel) } } @@ -16163,7 +16298,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]); @@ -16171,7 +16306,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 new file mode 100644 index 00000000000..f80b4dec2b6 --- /dev/null +++ b/lightning/src/ln/channel/ffor.rs @@ -0,0 +1,747 @@ +use super::*; + +mod activation; +mod drain; +mod fence; +mod quiescence; +mod receipt; +mod reestablish; +mod setup; +use crate::ln::ffor::{ + self as verification, FFORCommitmentError, FFORMonitorSnapshot, FFORReceiverAbortReason, + 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; +pub(crate) use reestablish::FFORReestablishOutcome; +#[cfg(test)] +pub(crate) use setup::ffor_setup_test_messages; +pub(crate) use setup::{FFORReceiverRequest, FFORReceiverSetup}; + +/// 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, + setup: Option, + fence: Option, + 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 { + 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), + // Readers predating authenticated setup must refuse this record. + (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'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); + } + } + + pub(super) fn restored( + &mut self, inbound_htlcs: &[InboundHTLCOutput], + ) -> 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); + } + 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)) + { + 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.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. + return Err(DecodeError::InvalidValue); + } + } + if let Some(setup) = self.setup.as_ref() { + setup.validate_book(self).map_err(|_| DecodeError::InvalidValue)?; + } + 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) + } +} + +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.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()); + } + 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()); + } + 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 + 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 } + }, + ); + } + 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 { + parked_vouchers: parked as u16, + 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, + 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); + } + if book.fence.is_some() || book.request.is_some() { + return Err(FFORCommitmentError::PendingUpdates.into()); + } + 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 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 + .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() || book.is_closed()) + && !reserved_hash + && !owned_pending + && !(book.request.is_some() && !book.request_gate_released.unwrap_or(false)) + { + 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> { + self.ffor_validate_receiver_setup()?; + 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) { + 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) + { + 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, + { + 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.request.is_none() || book.request_gate_released.unwrap_or(false)) => + { + book + }, + _ => return, + }; + let failures: Vec<_> = book + .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 && !claimed { + 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, + ) -> 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(()) + } +} + +#[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..8 { + 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, + 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 = + 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/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/drain.rs b/lightning/src/ln/channel/ffor/drain.rs new file mode 100644 index 00000000000..275c2ff533a --- /dev/null +++ b/lightning/src/ln/channel/ffor/drain.rs @@ -0,0 +1,781 @@ +//! 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::journal::FFORCooperativeJournal; +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, + // Native per-slot outcomes. Legacy drains restored without one never acquire outcomes. + journal: Option, +} + +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)), + (14, journal, option), +}); + +/// 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, + 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 + } + 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, + 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, journal: Option<&FFORCooperativeJournal>, +) -> [u8; 32] { + 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); + 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()); + } + 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) + } + + /// 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 +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), + } + } + 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, .. } + 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, + journal: FFORCooperativeJournal::new(book.vouchers.len()), + }); + 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 + } + } + + /// 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() + } + + 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]> { + 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 + } + + /// 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() + && drain.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, + )?; + 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, + 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), + }, + journal: drain.journal.clone(), + }) + } + + 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 + || drain.journal != completion.journal + || 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; + // 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(()) + } +} + +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..43e37734871 --- /dev/null +++ b/lightning/src/ln/channel/ffor/drain/tests.rs @@ -0,0 +1,318 @@ +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}; +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) + )); + } +} + +/// 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/channel/ffor/fence.rs b/lightning/src/ln/channel/ffor/fence.rs new file mode 100644 index 00000000000..bb5e89f3bf8 --- /dev/null +++ b/lightning/src/ln/channel/ffor/fence.rs @@ -0,0 +1,229 @@ +//! A private durable mutation fence, independent of connection-scoped STFU flags. +//! +//! 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::*; + +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, + Aborting, + Draining, + ClosedPendingPersistence, +} + +impl_writeable_tlv_based_enum!(FFORReceiverFencePhase, + (0, Activating) => {}, + (2, Active) => {}, + (4, Aborting) => {}, + (6, Draining) => {}, + (8, ClosedPendingPersistence) => {}, +); + +pub(super) struct FFORReceiverFence { + pub(super) phase: FFORReceiverFencePhase, + pub(super) 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::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 + .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(()), + }; + 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 => { + book.abort_reason == Some(FFORReceiverAbortReason::Disconnected) + }, + _ => book.abort_reason.is_none(), + }; + if book.setup.is_none() + || !abort_matches_phase + || !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..97bbf47418b --- /dev/null +++ b/lightning/src/ln/channel/ffor/fence/tests.rs @@ -0,0 +1,604 @@ +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_limits_restart_wire_to_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()); + 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 + .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::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| { + 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/quiescence.rs b/lightning/src/ln/channel/ffor/quiescence.rs new file mode 100644 index 00000000000..1c03edb8e17 --- /dev/null +++ b/lightning/src/ln/channel/ffor/quiescence.rs @@ -0,0 +1,207 @@ +//! 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, +{ + /// 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, + ) -> 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/channel/ffor/receipt.rs b/lightning/src/ln/channel/ffor/receipt.rs new file mode 100644 index 00000000000..f0fb7a173a4 --- /dev/null +++ b/lightning/src/ln/channel/ffor/receipt.rs @@ -0,0 +1,123 @@ +//! 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 + // 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 { + 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); + } + 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 + .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(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/reestablish.rs b/lightning/src/ln/channel/ffor/reestablish.rs new file mode 100644 index 00000000000..b26d66e7db8 --- /dev/null +++ b/lightning/src/ln/channel/ffor/reestablish.rs @@ -0,0 +1,314 @@ +//! 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 }, + /// 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. + 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]), + 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 + .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/channel/ffor/setup.rs b/lightning/src/ln/channel/ffor/setup.rs new file mode 100644 index 00000000000..bed7b9a36d7 --- /dev/null +++ b/lightning/src/ln/channel/ffor/setup.rs @@ -0,0 +1,462 @@ +//! 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 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; +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; + +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 terminal books. + 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 settlement(&self) -> PublicKey { + self.settlement + } + 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 { + // 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 { + 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(); + 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)?; + } + 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(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()) + { + 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 record. + 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); + } + Ok(()) + } + + 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(()), + }; + 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() && !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)? + || 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/request.rs b/lightning/src/ln/channel/ffor/setup/request.rs new file mode 100644 index 00000000000..d983e2a5b87 --- /dev/null +++ b/lightning/src/ln/channel/ffor/setup/request.rs @@ -0,0 +1,373 @@ +//! 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 { + // 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() + || !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; + 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(()) + } + + 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.is_closed()) || 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/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 dbfe949dd25..a490005a639 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -65,6 +65,13 @@ use crate::ln::channel::{ WithChannelContext, }; use crate::ln::channel_state::ChannelDetails; +use crate::ln::ffor::{ + FFORCommitmentError, FFORMonitorSnapshot, FFORReceiverError, FFORReceiverQuiescenceStatus, + 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; @@ -135,6 +142,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; @@ -1633,10 +1641,15 @@ 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`]. 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, } @@ -2896,6 +2909,12 @@ 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, + // 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>, @@ -3320,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() { @@ -3830,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) => { @@ -4018,6 +4041,9 @@ 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()), + ffor_activation: Mutex::new(FFORReceiverRuntime::new()), funding_batch_states: Mutex::new(BTreeMap::new()), pending_broadcast_messages: Mutex::new(Vec::new()), @@ -4262,6 +4288,280 @@ 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) + } + + /// 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 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 + /// the registration and fail its vouchers using the ordinary channel commitment protocol. + /// 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], + vouchers: &[FFORVoucher], + ) -> 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. + /// + /// 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 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, + ) -> 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) + .ok_or(FFORCommitmentError::ChannelUnavailable)? + .lock() + .unwrap(); + 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, + self.our_network_pubkey, + self.chain_hash, + current_height, + claim_margin_blocks, + )?; + 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) + } + + /// 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. + /// + /// 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. + /// 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( + &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)?; + 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(()) + } + /// 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, @@ -6936,6 +7236,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, @@ -8917,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, @@ -8955,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, @@ -11583,7 +11910,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); @@ -11612,6 +11939,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, @@ -11760,6 +12095,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, @@ -11770,6 +12107,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() && 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(()); + } let mut channel_update = None; if let Some(msg) = responses.shutdown_msg { peer_state.pending_msg_events.push(MessageSendEvent::SendShutdown { @@ -11825,6 +12167,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, @@ -12195,6 +12538,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, @@ -13770,6 +14114,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 = { @@ -13781,6 +14126,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); @@ -13874,6 +14220,8 @@ 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 } }; @@ -13882,7 +14230,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(); @@ -13935,7 +14283,9 @@ 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(), })); }, @@ -13957,6 +14307,8 @@ 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(())); }, } } @@ -13981,6 +14333,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(), @@ -14054,7 +14411,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 @@ -14619,7 +14976,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"))] @@ -14807,17 +15213,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 @@ -16458,6 +16869,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), @@ -16476,6 +16889,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. @@ -16766,6 +17180,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(), @@ -16776,11 +17192,15 @@ 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, }; 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( @@ -16799,6 +17219,22 @@ where &provided_channel_type_features(&args.config), ), )?; + channel.ffor_validate_receiver_identity(our_network_pubkey, chain_hash)?; + if let Some(request) = channel.ffor_receiver_request() { + ffor_channel_requests + .push((request.clone(), channel.ffor_receiver_predecessor_epoch())); + } + if let Some(setup) = channel.ffor_receiver_setup_record()? { + ffor_channel_setups.push(( + 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(), + )); + } let logger = WithChannelContext::from(&args.logger, &channel.context, None); let channel_id = channel.context.channel_id(); channel_id_set.insert(channel_id); @@ -17176,6 +17612,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), @@ -17194,8 +17631,57 @@ 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); + 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, 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, predecessor) in + &ffor_channel_setups + { + ffor_recovery.validate_channel_lifecycle( + setup, + *fence, + *abort_reason, + drain.clone(), + *completion, + *drain_hash, + *predecessor, + )?; + } + 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() { + 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()); @@ -17950,10 +18436,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."); @@ -18113,6 +18595,18 @@ 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, @@ -18159,6 +18653,9 @@ where event_persist_notifier: Notifier::new(), needs_persist_flag: AtomicBool::new(false), + 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()), @@ -18455,10 +18952,16 @@ 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)) } } +#[cfg(test)] +pub(crate) mod ffor_recovery_tests; + #[cfg(test)] mod tests { use crate::events::{ClosureReason, Event, HTLCHandlingFailureType}; diff --git a/lightning/src/ln/channelmanager/ffor_activation.rs b/lightning/src/ln/channelmanager/ffor_activation.rs new file mode 100644 index 00000000000..192e5336532 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation.rs @@ -0,0 +1,511 @@ +//! Private composition of receiver activation, retained evidence and ordered persistence. +//! +//! The experimental driver selects transitions; no observation authorizes invoice exposure. + +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 close; +mod context; +mod driver; +mod invoice; +mod outcome; +mod receipt; +mod reestablish; +mod witness; +mod witness_ack; + +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. +pub(super) struct FFORReceiverRuntime { + entries: Vec, + witness_attempts: Vec, +} + +impl FFORReceiverRuntime { + pub(super) fn new() -> Self { + Self { entries: Vec::new(), witness_attempts: Vec::new() } + } + + pub(super) fn restored( + recovery: &FFORRecoveryRegistry, barrier: &mut FFORPersistenceBarrier, + ) -> Result { + let mut runtime = Self::new(); + 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 { + 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, + request_connection: None, + may_send_init: false, + }; + 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. + #[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 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) + .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. + #[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<(), ()>, + { + 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(); + 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 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. + #[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(); + 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 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() { + // 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(), + channel.ffor_receiver_predecessor_epoch(), + ) + .map_err(|_| FFORReceiverError::RecoveryUnavailable)?; + 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() && channel.ffor_receiver_drain_binding().is_none() { + 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_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(|_| 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() + { + 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. + #[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(); + 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 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/close.rs b/lightning/src/ln/channelmanager/ffor_activation/close.rs new file mode 100644 index 00000000000..3610c9d16ea --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/close.rs @@ -0,0 +1,505 @@ +//! 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, +{ + 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(), + channel.ffor_receiver_predecessor_epoch(), + ) + .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(); + let peer = peers + .get(counterparty_node_id) + .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 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 + .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. + #[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<(), ()>, + { + 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(); + 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 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(), + channel.ffor_receiver_predecessor_epoch(), + ) + .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. + #[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(); + 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 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(); + 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 + .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. + #[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 }; + 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(); + 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)?; + 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(); + 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() + || 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. + #[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(); + 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 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(); + 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() { + 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) + } + + #[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(); + 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 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(); + 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)?; + 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..751bfb30088 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/close/tests.rs @@ -0,0 +1,1038 @@ +use super::*; +use crate::chain::ChannelMonitorUpdateStatus; +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(); + let bytes = node.node.encode(); + node.node.ffor_persistence_completed(token).unwrap(); + bytes +} + +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 }) + .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) { + 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(())) + .unwrap()); + 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) + .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(); + 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], + &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()); + // 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) + .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!(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]); + 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 + ); + // 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) + .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); + 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))); + } + } + } +} + +#[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); + 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 + ); + 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]); + } + } +} + +/// 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 new file mode 100644 index 00000000000..d62a8a95dd8 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/context.rs @@ -0,0 +1,212 @@ +//! 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, +{ + /// 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( + &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() + } + + pub(super) 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(()) + } + + 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)?; + 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(), + channel.ffor_receiver_predecessor_epoch(), + ) + .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()); + } + Ok(context) + } +} + +#[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/channelmanager/ffor_activation/drain_tests.rs b/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs new file mode 100644 index 00000000000..ccaa8c08019 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/drain_tests.rs @@ -0,0 +1,373 @@ +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; +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. +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) { + 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); + 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); + 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; + 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); + 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) + .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. +pub(super) 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/driver.rs b/lightning/src/ln/channelmanager/ffor_activation/driver.rs new file mode 100644 index 00000000000..0243455df48 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/driver.rs @@ -0,0 +1,661 @@ +//! 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; + +mod advance; + +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, +{ + 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(()) + } + + /// 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 + /// 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 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, + ) -> 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 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 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 }, + 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 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() + } + + /// 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 + /// 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. + fn advance_ffor_receiver_setup( + &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. + fn handle_ffor_receiver_setup_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)?; + // 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 == current) + { + 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. + /// 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, + ) -> 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; + 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) + } + + /// 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; + } + 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; + 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/advance.rs b/lightning/src/ln/channelmanager/ffor_activation/driver/advance.rs new file mode 100644 index 00000000000..55603778d26 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/advance.rs @@ -0,0 +1,595 @@ +//! 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(), + channel.ffor_receiver_predecessor_epoch(), + ) + .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(), + channel.ffor_receiver_predecessor_epoch(), + ) + .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..bb9288873b6 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/advance_tests.rs @@ -0,0 +1,863 @@ +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}; + +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) { + 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_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(); + 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); +} + +/// 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 new file mode 100644 index 00000000000..338d0bcb050 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/driver/tests.rs @@ -0,0 +1,608 @@ +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 { + parameters_with_id(voucher, [91; 32]) +} +fn parameters_with_id(voucher: &FFORVoucher, local_request_id: [u8; 32]) -> FFORReceiverParameters { + 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, + } +} +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 +} + +mod intent; + +#[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()); +} + +#[path = "advance_tests.rs"] +mod lifecycle; 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()); + } +} 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/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.rs b/lightning/src/ln/channelmanager/ffor_activation/receipt.rs new file mode 100644 index 00000000000..59df3bf22c3 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/receipt.rs @@ -0,0 +1,237 @@ +//! 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, 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. + /// + /// 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()); + } + // 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); + 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 = 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!( + 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..6e81664e78b --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/receipt/tests.rs @@ -0,0 +1,853 @@ +use super::*; +use crate::chain::ChannelMonitorUpdateStatus; +use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC; +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; +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); + 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(); + 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(); +} + +#[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 new file mode 100644 index 00000000000..185ab1f190d --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/reestablish.rs @@ -0,0 +1,162 @@ +//! 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_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(|_| ())?; + 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() + { + 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_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(|_| ())?) + } 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_activation/witness.rs b/lightning/src/ln/channelmanager/ffor_activation/witness.rs new file mode 100644 index 00000000000..48046f5e86f --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/witness.rs @@ -0,0 +1,202 @@ +//! 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 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) + .map_err(|_| FFORReceiverError::InvalidWitnessRegistration)?; + if let Some(existing) = recovery.get_witnesses(&key) { + if existing != ®istration { + return Err(FFORReceiverError::InvalidWitnessRegistration); + } + if recovery.get_witness_acks(&key).is_some() { + 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. + 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 = + barrier.request().map_err(|_| FFORReceiverError::PersistenceUnavailable)?; + upgrade.commit(); + runtime.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 runtime = self.ffor_activation.lock().unwrap(); + 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 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()) + } + + pub(super) 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..1a483bc9968 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_activation/witness/tests.rs @@ -0,0 +1,498 @@ +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]; + +mod ack; + +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()); + 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); + 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()); + 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(); + 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_attempt(&active, &attempt, &provision, |_| panic!( + "closing sent attempt" + )) + .is_err()); + 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()); +} + +/// 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/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..89a20be1420 --- /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 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 + || 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 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/channelmanager/ffor_recovery_tests.rs b/lightning/src/ln/channelmanager/ffor_recovery_tests.rs new file mode 100644 index 00000000000..30b83682e67 --- /dev/null +++ b/lightning/src/ln/channelmanager/ffor_recovery_tests.rs @@ -0,0 +1,500 @@ +use super::*; +use crate::chain::channelmonitor::ChannelMonitor; +use crate::ln::channel::{ffor_setup_test_messages, FFORReceiverFencePhase, FFORReceiverSetup}; +use crate::ln::ffor_recovery::tests::full_registry; +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(); + } + 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(); + 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 { + 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() +} + +pub(crate) 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_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] { + 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 new file mode 100644 index 00000000000..486fba4f7db --- /dev/null +++ b/lightning/src/ln/ffor.rs @@ -0,0 +1,467 @@ +//! Experimental receiver parking and commitment verification for FFOR Variant D. +//! +//! 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; +use bitcoin::locktime::absolute::LOCK_TIME_THRESHOLD; +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::ln::{msgs, onion_utils}; +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}; + +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}; +mod witness_ack; +pub use witness_ack::{ + FFORReceiverWitnessAcknowledgements, FFORWitnessAcknowledgement, FFORWitnessProvisionAttempt, +}; +pub(crate) mod invoice; +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)] +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, strictly below Bitcoin's + /// locktime threshold. Timestamp locktimes are not valid voucher expiries. + 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, + /// The first voucher hash reused a revealed counterparty commitment secret. + 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, + (0, Requested) => {}, + (2, VoucherMismatch) => {}, + (4, Disconnected) => {}, + (6, Restarted) => {}, + (8, CommitmentSecretReused) => {}, + (10, QuiescenceFailed) => {}, + (12, SetupRejected) => {}, +); + +/// 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 +/// 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 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, + /// 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, + /// 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, + /// 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 { + 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"), + 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"), + Self::InvalidWitnessReceipt => f.write_str("invalid FFOR witness receipt"), + Self::InvalidInvoice => f.write_str("invalid or unavailable FFOR invoice"), + } + } +} + +#[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 { + /// 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) destination_script: bitcoin::ScriptBuf, + 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, +} + +/// 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 { + 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 >= LOCK_TIME_THRESHOLD + || 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/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/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/driver.rs b/lightning/src/ln/ffor/driver.rs new file mode 100644 index 00000000000..8e7201ab59a --- /dev/null +++ b/lightning/src/ln/ffor/driver.rs @@ -0,0 +1,116 @@ +//! 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 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, + /// 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. + reason: FFORReceiverAbortReason, + }, +} 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/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/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/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, + }, +} 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]); +} 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_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..08ee9a750a5 --- /dev/null +++ b/lightning/src/ln/ffor_recovery.rs @@ -0,0 +1,770 @@ +//! 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. + +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}; + +use alloc::vec::Vec; +use bitcoin::constants::ChainHash; +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, +}; +use crate::ln::msgs::DecodeError; +use crate::ln::types::ChannelId; +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 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 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. +const MAX_SETUP_RECORD_BYTES: usize = 192 * 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 { + 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, + activation: Option, + request: Option, + witnesses: Option, + witness_acks: Option, + invoice: Option, +} + +impl_writeable_tlv_based!(StoredSetup, { + (0, setup, required), + (2, canonical_book, required_vec), + (4, activation, option), + (6, request, option), + (8, witnesses, option), + (10, witness_acks, option), + (12, invoice, option), +}); + +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.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); + } + if let Some(activation) = record.activation.as_ref() { + 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)?; + } + } + 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)?; + } + 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); + 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); + } + 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()) + } + + /// 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()); + } + 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_transition_bytes(&self) -> usize { + Self::transition_reservation(&self.record) + } +} + +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, +} + +/// 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.reserved_transition_bytes += entry.reserved_transition_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_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_transition_bytes = self.reserved_transition_bytes; + } +} + +impl FFORRecoveryRegistry { + pub(crate) fn new() -> Self { + 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.pending_requests.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 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, + 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()) + } + + /// 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> { + 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> { + 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); + } + let authenticated = setup.validate_recovery()?; + 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)?; + 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); + } + 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() => { + 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 + && abort_reason.is_none() + && matches!( + (record.is_active(), phase), + (false, FFORReceiverFencePhase::Activating) + | (true, FFORReceiverFencePhase::Active) + ) => + { + Ok(()) + }, + _ => Err(DecodeError::InvalidValue), + } + } + + /// 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() && !activation.is_closed() + }) + }) + .map(|entry| entry.key.channel_id) + .collect() + } + + 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 + && 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) + } + + /// 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.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 + }) { + 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. 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> { + 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: None, + request: None, + witnesses: None, + witness_acks: None, + invoice: 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.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) }) + } + + /// 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 either terminal outcome 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 key = FFORRecoveryKey { + channel_id: ChannelId(authenticated.header().channel_id), + 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, + witness_acks: self.get_witness_acks(&key).cloned(), + invoice: self.get_invoice(&key).cloned(), + })?; + 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() || activation.aborted_reason().is_some(), + } { + return Err(FFORRecoveryError::ConflictingRecord); + } + // 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( + &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 + .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_transition_bytes) + .map_or(true, |bytes| bytes > MAX_ENCODED_BYTES) + { + return Err(FFORRecoveryError::CapacityExceeded); + } + 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().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; + } + 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; + } + 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 + .activation + .as_ref() + .map_or(false, |activation| activation.close_record().is_some()) + }) { + CLOSE_VERSION + } else 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 + } + } + + fn check_capacity( + &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_transition_bytes)) + .and_then(|total| total.checked_add(reserved_transition_bytes)); + if self.entries.len() + self.pending_requests.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> { + self.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)?; + } + 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(()) + } +} + +impl Readable for FFORRecoveryRegistry { + fn read(reader: &mut R) -> Result { + let version = u8::read(reader)?; + if version != SETUP_ONLY_VERSION + && version != ACTIVATION_VERSION + && version != ABORTED_ACTIVATION_VERSION + && version != CLOSE_VERSION + && version != REQUEST_VERSION + && version != WITNESS_VERSION + && version != WITNESS_ACK_VERSION + && version != INVOICE_VERSION + && version != JOURNAL_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(); + 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 { + 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() { + 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.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.conflicts_with(&entry)) + { + return Err(DecodeError::InvalidValue); + } + registry + .check_capacity(length, entry.reserved_transition_bytes()) + .map_err(|_| DecodeError::InvalidValue)?; + registry.encoded_bytes += RECORD_LENGTH_BYTES + length; + 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.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); + } + 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); + } + Ok(registry) + } +} + +#[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 new file mode 100644 index 00000000000..63722bb500b --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation.rs @@ -0,0 +1,346 @@ +//! 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. + +mod close; +mod context; +pub(crate) use close::FFORReceiverCloseRecord; + +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::{ + FFORCommitment, FFORMonitorRecoveryIdentity, FFORMonitorSnapshot, FFORReceiverAbortReason, + 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>, + abort: Option, + close: 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), + (18, abort, option), + (20, close, 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. + /// 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, + abort: None, + close: 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 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); + } + 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)?; + } + 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) + } + + 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 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); + } + 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() + } + + /// 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 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 + || 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(()) + } + + /// 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.abort.is_some() { + return false; + } + 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() + } +} + +#[cfg(test)] +pub(super) mod tests; 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..696747d3eab --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/close.rs @@ -0,0 +1,300 @@ +//! 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::ffor::journal::FFORCooperativeJournal; +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, + // Complete native outcome journal. Legacy records without one report no outcomes. + journal: Option, +} + +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), + (14, journal, option), +}); + +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, + 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 + || 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) + } + + /// 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()? { + 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, + journal: completion.journal().cloned(), + }; + 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/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(), + )) + } +} 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..0f619b75fb5 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/tests.rs @@ -0,0 +1,509 @@ +use super::*; +use crate::ln::ffor_recovery::tests::{fixture, sign}; +use crate::ln::ffor_recovery::{ + FFORRecoveryError, FFORRecoveryKey, FFORRecoveryRegistry, ACK_RESERVATION_BYTES, + 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; +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. +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, + abort: None, + close: 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 = + 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)); + } +} + +#[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_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_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_transition_bytes, ACK_RESERVATION_BYTES + CLOSE_RESERVATION_BYTES); + drop(registry.prepare_activation(&setup, &active).unwrap()); + assert_eq!(registry.encode(), pending); + 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_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); + assert!(matches!( + registry.prepare_activation(&setup, &activating), + Err(FFORRecoveryError::ConflictingRecord) + )); + assert_eq!(registry.encode(), completed); + assert_eq!(registry.reserved_transition_bytes, CLOSE_RESERVATION_BYTES); +} + +// 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_transition_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_transition_bytes, reserved); + let mut restored = FFORRecoveryRegistry::read(&mut &before[..]).unwrap(); + 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_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_transition_bytes, reserved); + restored.prepare_activation(&setup, &active).unwrap().commit(); + 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_transition_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_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); + let extra = crate::ln::ffor_recovery::Entry::new(crate::ln::ffor_recovery::StoredSetup { + 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), + }) + .unwrap(); + assert!(restored.encoded_bytes + extra.encoded_bytes + 4 < MAX_ENCODED_BYTES); + assert!( + restored.encoded_bytes + + restored.reserved_transition_bytes + + extra.encoded_bytes + + extra.reserved_transition_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/activation/tests/aborted.rs b/lightning/src/ln/ffor_recovery/activation/tests/aborted.rs new file mode 100644 index 00000000000..99f4d0a87cc --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/tests/aborted.rs @@ -0,0 +1,241 @@ +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_transition_bytes, ACK_RESERVATION_BYTES + CLOSE_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_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(); + 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_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)); + 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_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_transition_bytes, + reserved - ACK_RESERVATION_BYTES - CLOSE_RESERVATION_BYTES + ); + assert!(restored.encoded_bytes - before.len() < ABORT_RESERVATION_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_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..943598c9038 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/tests/close.rs @@ -0,0 +1,726 @@ +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, INVOICE_VERSION, + JOURNAL_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, + ) +} + +pub(super) 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 journal = full_journal(&setup, 3); + let closed = + historical_closed(&setup, &draining, &completion_with(&setup, &draining, Some(journal))); + (setup, vec![activating, active, closing, draining, closed]) +} + +/// A complete journal for the whole book, fulfilling every `every`-th slot from the first. +fn full_journal(setup: &FFORReceiverSetup, every: usize) -> 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)] +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, + 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 { + 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 { + completion_with(setup, draining, None) +} + +fn completion_with( + setup: &FFORReceiverSetup, draining: &FFORReceiverActivation, + journal: Option, +) -> 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(), + 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() + }; + 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()); + } + if let Some(journal) = &completed.journal { + bytes.extend_from_slice(&journal.digest_bytes()); + } + 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], + 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()); + } + 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()); + } + 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] +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/activation/tests/witness.rs b/lightning/src/ln/ffor_recovery/activation/tests/witness.rs new file mode 100644 index 00000000000..287feebdce3 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/tests/witness.rs @@ -0,0 +1,204 @@ +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}; + +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], + 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)); + 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()), + witness_acks: None, + invoice: None, + }; + 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/activation/tests/witness/ack.rs b/lightning/src/ln/ffor_recovery/activation/tests/witness/ack.rs new file mode 100644 index 00000000000..39ab1c9f288 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/activation/tests/witness/ack.rs @@ -0,0 +1,152 @@ +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, + invoice: 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()), + invoice: None, + }; + 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/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 new file mode 100644 index 00000000000..0bec5055425 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/request.rs @@ -0,0 +1,165 @@ +//! 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| { + // 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); + } + 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()), + 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. + Ok(FFORRequestPromotion { registry: self, index, entry }) + } +} diff --git a/lightning/src/ln/ffor_recovery/tests.rs b/lightning/src/ln/ffor_recovery/tests.rs new file mode 100644 index 00000000000..22f392ed8d7 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/tests.rs @@ -0,0 +1,335 @@ +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; + +pub(super) 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 { + pub(super) 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()) +} + +pub(super) 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 +} + +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( + 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, + activation: None, + request: None, + witnesses: None, + witness_acks: None, + invoice: None, + } +} + +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[..]) +} + +/// 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 { + 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_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()), + 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] = JOURNAL_VERSION + 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, 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_recovery/witness.rs b/lightning/src/ln/ffor_recovery/witness.rs new file mode 100644 index 00000000000..439fdba6f93 --- /dev/null +++ b/lightning/src/ln/ffor_recovery/witness.rs @@ -0,0 +1,196 @@ +//! 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(), + witness_acks: Some( + previous + .record + .witness_acks + .clone() + .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 new file mode 100644 index 00000000000..7c111e180bd --- /dev/null +++ b/lightning/src/ln/ffor_recovery/witness_ack.rs @@ -0,0 +1,123 @@ +//! 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), + invoice: previous.record.invoice.clone(), + })?; + self.prepare_replacement(index, entry, 0) + } +} diff --git a/lightning/src/ln/ffor_tests.rs b/lightning/src/ln/ffor_tests.rs new file mode 100644 index 00000000000..65cf9dc4d47 --- /dev/null +++ b/lightning/src/ln/ffor_tests.rs @@ -0,0 +1,1177 @@ +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; +use core::sync::atomic::Ordering; + +pub(crate) 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; + 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, + ) +} + +pub(super) 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 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)); + 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() + ); +} + +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); +} + +#[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/ffor_tests/quiescence.rs b/lightning/src/ln/ffor_tests/quiescence.rs new file mode 100644 index 00000000000..808699c03d0 --- /dev/null +++ b/lightning/src/ln/ffor_tests/quiescence.rs @@ -0,0 +1,482 @@ +use super::*; +use crate::ln::channel::{ffor_setup_test_messages, DISCONNECT_PEER_AWAITING_RESPONSE_TICKS}; + +const EPOCH: [u8; 32] = [81; 32]; + +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 + .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)); +} + +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(), + 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, + ) +} + +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); + 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); +} diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 2e6b279965f..b84cccd9814 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -18,6 +18,11 @@ pub mod channel_keys; 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; pub mod inbound_payment; pub mod msgs; 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/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 { 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 db0b0503118..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 { @@ -1801,6 +1808,9 @@ 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>, pub disable_revocation_policy_check: bool, @@ -1823,6 +1833,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) } @@ -1830,6 +1849,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) } @@ -1840,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) } @@ -1951,6 +1979,9 @@ impl TestKeysInterface { fn build(backing: Box>) -> Self { 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, disable_all_state_policy_checks: false,