Skip to content

feat: CCH multi-asset swap#1257

Open
doitian wants to merge 17 commits intonervosnetwork:developfrom
doitian:feat/cch-multi-asset-swap
Open

feat: CCH multi-asset swap#1257
doitian wants to merge 17 commits intonervosnetwork:developfrom
doitian:feat/cch-multi-asset-swap

Conversation

@doitian
Copy link
Copy Markdown
Member

@doitian doitian commented Apr 8, 2026

Summary

This PR tracks CCH multi-asset swap work: a protocol specification plus the implementation that follows it.

Current branch state: the spec only (docs/specs/cch-multi-asset-swap.md). Implementation commits will be added here before merge.

Specification (done on branch)

  • Scope: HTLC flow unchanged; Fiber leg covers native CKB (shannons) and UDTs with type script; Lightning remains BTC-denominated.
  • Economics: swap rate from invoice pair + hub fees (BTC-side fee basis); no hard-coded 1:1 wrapped BTC mapping.
  • Hub policy: asset allowlist and per-UDT decimals/metadata.
  • Swap acceptor: WebSocket JSON-RPC subscription (subscribe_swap_proposals / respond_swap_proposal) with binary accept/reject only, configurable timeout, and recommended explicit RPC errors on reject/timeout.
  • Cross-links to docs/specs/payment-invoice.md and docs/specs/cch-expiry-dependency.md.

Implementation (planned)

To be added in follow-up commits on this branch, aligned with the spec—for example:

  • Generalize CCH config and actor beyond a single wrapped-BTC UDT; native CKB and allowlisted UDTs.
  • RPC: swap-acceptor subscription + response methods; order/API types as needed.
  • Tests and RPC docs updates.

@doitian doitian added this to Kanban Apr 8, 2026
@doitian doitian changed the title docs: add CCH multi-asset swap specification feat: CCH multi-asset swap Apr 8, 2026
@doitian doitian force-pushed the feat/cch-multi-asset-swap branch 2 times, most recently from 75583f0 to f55b11f Compare April 21, 2026 04:35
ian added 6 commits May 5, 2026 15:46
This document specifies cross-chain hub behavior for atomic swaps
between Bitcoin on Lightning and arbitrary Fiber assets (native CKB or
UDTs).
Step 1 of the cch-multi-asset-swap spec: drop the BTC-centric
`wrapped_btc_type_script[_args]` config fields in favor of a generic
`fiber_asset_allowlist` plus a `fixed_rate_assets` table. This is the
config-layer foundation for accepting swaps against arbitrary Fiber
assets (native CKB or any allowlisted UDT) instead of a single
hard-coded wrapped-BTC UDT.

- Introduce `CchAsset` (`Option<Script>`, where `None` = native CKB) and
  `FixedRateAsset { fiber_asset, sats_per_smallest_unit }` in
  `cch::config`, re-exported from `cch::mod`.
- `validate_standalone` now requires a non-empty allowlist; UDT type
  scripts are declared inline so no separate JSON-string parsing step
  is needed.
- Update the deployer yaml to declare the SimpleUDT entry with a zero
  `code_hash` placeholder; `tests/deploy/udt-init` recursively patches
  every cch `code_hash` placeholder to the real deployed
  SIMPLE_UDT data hash, so all CCH-enabled nodes (in-process and
  standalone) get a uniformly populated config.

Call sites in `cch/actor.rs`, `fiber-types::CchOrder`, the json-types
mirror, and the cch tests still reference the removed fields and will
be updated in Step 2 along with the order-type refactor; the tree
intentionally does not compile between this commit and Step 2.
Rename and extend the persisted CchOrder so a single record can describe
a swap between BTC and any allowlisted Fiber asset (native CKB or any
UDT), not just wrapped BTC.

Field changes:
  wrapped_btc_type_script: Script   -> fiber_type_script: Option<Script>
  amount_sats: u128                 -> btc_amount_msat: u128
  fee_sats: u128                    -> btc_fee_msat: u128
  (new)                             -> fiber_amount_smallest_unit: u128

The BTC leg is now denominated in millisatoshi (1 sat = 1000 msat) so
fee/exchange-rate math is rounding-free; the Fiber leg uses the smallest
unit of the target asset (shannon for native CKB, the UDT's smallest
denomination otherwise). `fiber_type_script = None` denotes native CKB.

Bincode migration mig_20260421_cch_multi_asset rewrites every existing
order at prefix 232: legacy single-asset records were 1:1 sat-to-UDT
smallest-unit, so fiber_amount_smallest_unit is set to the old
amount_sats and BTC amounts are scaled by 1000.

The fiber-lib tree intentionally does not compile after this commit;
call sites and tests are updated in the next commit.
Mechanically rewrites the CCH actor and JSON-types to use the new
CchOrder fields (btc_amount_msat, btc_fee_msat, fiber_amount_smallest_unit,
fiber_type_script) introduced in the previous commit. Behaviour is
unchanged: per-request asset selection, allowlist enforcement, and
fixed-rate computation are deferred to a follow-up; the actor uses a
placeholder asset resolver that returns the first allowlist entry.

- actor.rs: send_btc/receive_btc fee math now in msat throughout
- json-types: CchOrderResponse mirrors the new on-disk shape
- tests: update CchOrder/CchConfig literals to new fields; harness
  injects a default allowlist entry; rewrite the two fee-assertion
  tests to use msat-based expectations
- regenerate store schema
Step 3a of the multi-asset CCH refactor. Replaces the placeholder
single-asset resolver with real per-request asset selection:

- SendBTC / SendBTCParams gain a fiber_type_script: Option<Script>
  parameter (None = native CKB). The hub validates it against the
  configured fiber_asset_allowlist before creating the order, and the
  proxy Fiber invoice omits the UdtScript attribute for native CKB.
- ReceiveBTC reads the asset directly from the submitted Fiber invoice
  (UdtScript attribute, or None for native CKB) and validates it
  against the same allowlist.
- Replace CchError::WrappedBTCTypescriptMismatch with
  FiberAssetNotAllowlisted; update the existing rejection test and
  add coverage for (a) send_btc rejecting a non-allowlisted UDT and
  (b) send_btc accepting native CKB end-to-end when allowlisted.

Per-asset fixed-rate computation is still TODO (Step 3b); fee math
remains in msat with the current 1 sat \u2194 1 UDT smallest unit shortcut.
Replace the legacy hard-coded 1:1 sat-to-smallest-unit conversion with a
lookup against the configured `fixed_rate_assets` table, so the CCH can
quote orders for any allowlisted Fiber asset whose fixed rate is
published in config. Send/receive amounts are derived from the configured
`smallest_units_per_sat` (renamed from `sats_per_smallest_unit` to
match its actual semantics): on send_btc the Fiber-leg amount is
`btc_amount_msat * rate / 1000`, on receive_btc the BTC-leg is
`fiber * 1000 / rate` plus the BTC-denominated hub fee. Allowlisted
assets without a fixed-rate entry are rejected with the new
`NoFixedRateForAsset` error in lieu of the not-yet-implemented
proposal-driven path.
@doitian doitian force-pushed the feat/cch-multi-asset-swap branch from 2bdf38b to 8f188f6 Compare May 5, 2026 07:46
ian added 6 commits May 5, 2026 17:50
Rename the fixed-rate field in the deployer test config from the
obsolete 'sats_per_smallest_unit' to the canonical
'smallest_units_per_sat' accepted by the CCH config loader, and
update the surrounding comment to match.
Adds the operator-facing swap acceptor that gates send_btc /
receive_btc swaps whose Fiber asset is allowlisted but not in the
configured fixed_rate_assets, replacing the prior outright rejection.

Components:

* SwapAcceptorActor (cch/acceptor.rs): a top-level ractor actor that
  tracks pending proposals plus subscriber sinks, mints proposal_ids,
  enforces the per-proposal timeout, and resolves the first valid
  operator response (later responses for the same proposal_id are
  rejected).
* SwapProposal / SwapProposalResponse (fiber-types/cch.rs): the
  notification payload pushed to operators and the structured reply
  they submit.
* RPC (rpc/cch.rs):
    - subscribe_swap_proposals: server->client subscription that
      streams SwapProposal notifications.
    - submit_swap_proposal_response: companion client->server method
      the operator calls to resolve a proposal, correlated by
      proposal_id. Split out because jsonrpsee's SubscriptionSink is
      unidirectional, so inline replies on the subscription channel
      would require a bespoke WebSocket route outside the standard
      JSON-RPC stack.
* CchActor.send_btc / receive_btc: when no fixed rate is configured
  for the Fiber asset, build a SwapProposal, await the acceptor's
  decision, and only mint the counterparty invoice on accept. Reject
  and timeout surface as SwapProposalRejected / SwapProposalTimeout
  errors from the original RPC call rather than a separate
  pending_acceptor order state.
* Config: swap_proposal_timeout_seconds (default applied at load).
* biscuit.rs: auth rules for the two new RPC endpoints.

Tests cover the proposal-timeout path through send_btc.
Update the multi-asset swap spec to reflect the now-implemented
proposal/acceptor path. Drops the 'deferred' status banner and the
section-5 'Status: deferred' callout, and rewrites section 5 to
describe the shipped wire protocol:

* server->client subscribe_swap_proposals notification stream, plus
  a companion client->server submit_swap_proposal_response method,
  carried over the same WebSocket and correlated by a server-minted
  proposal_id.
* synchronous send_btc / receive_btc semantics: the original RPC
  blocks until the proposal is accepted (returning the order with
  the counterparty invoice), explicitly rejected, or times out. No
  pending_acceptor order state and no client polling for the
  counterparty invoice.
* swap_proposal_timeout_seconds config and the
  SwapProposalTimeout / SwapProposalRejected error surfaces.
* updated SwapProposal field list to match fiber-types.

Adds a Design rationale subsection explaining why the protocol is
split into a notification stream plus a companion method instead of
inline replies on the subscription channel: jsonrpsee's
SubscriptionSink is server->client only and JSON-RPC 2.0 has no
client-originated subscription frames, so inline replies would
require a bespoke axum / tokio-tungstenite route with its own
framing, correlation, error envelope, and biscuit-auth integration.
A companion JSON-RPC method preserves all of that for free while
remaining functionally equivalent (same socket, proposal_id-keyed
correlation, multiplexable proposals).

Brings sections 1, 2.3, 6, and the mermaid diagram in line with the
synchronous return + companion-method shape.
…e example

Cross-review against the implementation surfaced two documentation
gaps in the multi-asset swap spec.

* Section 5.6 implied the SwapProposal's btc_amount_msat for SendBTC
  was the raw Bolt11 amount, but the actor sends the total payout
  (Bolt11 amount + configured fee). Clarify the field semantics and
  note that the raw Bolt11 amount can be recovered as
  btc_amount_msat - fee_on_btc_side_msat.
* Section 6.2 left ReceiveBTC proposal-path operators to infer how
  to combine the configured fee with their chosen rate. Add a worked
  formula and numeric example matching the fast-path computation, so
  operators have a reference implementation for the BTC-leg amount
  they submit.
The OpenRPC generator requires JsonSchema on every RPC parameter, but
`fiber_types::SwapProposalResponse` and `fiber_types::SwapProposal` do
not (and should not) implement it. Mirror them in fiber-json-types as
`SubmitSwapProposalResponseParams` and `SwapProposal`, add the
matching conversions, and switch the CchRpc trait to the JSON variants
so OpenRPC spec generation and the RPC doc check both succeed.

Also regenerate crates/fiber-lib/src/rpc/README.md to include the new
swap-proposal entries.
After the fiber-asset allowlist enforcement landed, an absent
`fiber_type_script` is interpreted as native CKB which the test
config's allowlist (wrapped-BTC UDT only) rejects. Pass the wrapped-BTC
UDT script in the send_btc cross-chain-hub e2e bodies to keep the
suites green.
@doitian doitian marked this pull request as ready for review May 6, 2026 00:59
@doitian doitian requested a review from Copilot May 6, 2026 01:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR moves CCH from a single wrapped-BTC assumption to a multi-asset swap model for native CKB and allowlisted UDTs. Although the PR description says “spec only,” the diff already includes the first implementation slice across CCH runtime logic, RPC/API types, persistence, and test/deploy tooling.

Changes:

  • Generalizes CCH config, domain types, and actor logic around fiber_type_script, asset allowlists, fixed-rate assets, and operator-approved proposal flows.
  • Adds operator-facing swap proposal RPC/subscription endpoints, auth rules, JSON conversions, and CCH tests for the new flow.
  • Introduces persisted CchOrder shape changes, a migration/schema update, and supporting deploy/Bruno/doc updates.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
tests/nodes/deployer/config.yml Replaces single-asset CCH test config with allowlist/fixed-rate asset config.
tests/deploy/udt-init/src/main.rs Patches deployed UDT code hashes into generated CCH node configs.
tests/bruno/e2e/cross-chain-hub/02-create-send-btc-order.bru Adds fiber_type_script to integrated CCH Bruno send_btc request.
tests/bruno/e2e/cross-chain-hub-separate/02-create-send-btc-order.bru Adds fiber_type_script to standalone CCH Bruno send_btc request.
migrate/src/migrations/mod.rs Registers the new CCH multi-asset migration module.
migrate/src/migrations/mig_20260421_cch_multi_asset.rs Migrates stored CCH orders to the new multi-asset layout.
migrate/Cargo.toml Adds migration-only serde/CKB dependencies for the new order shape.
migrate/Cargo.lock Locks the added migration dependencies.
docs/specs/cch-multi-asset-swap.md Adds the multi-asset CCH spec and swap-acceptor protocol.
crates/fiber-types/src/lib.rs Re-exports new CCH proposal-related types.
crates/fiber-types/src/cch.rs Redefines CCH order/proposal domain types for multi-asset swaps.
crates/fiber-lib/src/store/.schema.json Updates persisted schema fingerprints for CCH type changes.
crates/fiber-lib/src/rpc/README.md Regenerates RPC docs for the new CCH APIs.
crates/fiber-lib/src/rpc/cch.rs Adds swap proposal RPC/subscription methods and CCH RPC timeout changes.
crates/fiber-lib/src/rpc/biscuit.rs Adds biscuit auth rules for operator acceptor RPCs.
crates/fiber-lib/src/cch/tests/state_machine_tests.rs Updates state-machine tests for the new CCH order fields.
crates/fiber-lib/src/cch/tests/scheduler_tests.rs Updates scheduler tests for the new CCH order layout.
crates/fiber-lib/src/cch/tests/dispatcher_tests.rs Updates dispatcher tests for the new CCH order layout.
crates/fiber-lib/src/cch/tests/actor_tests.rs Adds actor coverage for fixed-rate and proposal-path CCH behavior.
crates/fiber-lib/src/cch/mod.rs Exports new CCH acceptor/config types.
crates/fiber-lib/src/cch/error.rs Adds proposal-flow-specific CCH errors.
crates/fiber-lib/src/cch/config.rs Replaces single-asset config with allowlist, fixed-rate, and proposal-timeout config.
crates/fiber-lib/src/cch/actor.rs Implements multi-asset send/receive flows and operator proposal handling.
crates/fiber-lib/src/cch/acceptor.rs Implements proposal broadcast, response handling, and timeout logic.
crates/fiber-json-types/src/convert.rs Adds JSON conversion support for new CCH/proposal types.
crates/fiber-json-types/src/cch.rs Defines new JSON-RPC params/responses for multi-asset CCH.

Comment thread crates/fiber-lib/src/cch/actor.rs
Comment thread crates/fiber-lib/src/cch/actor.rs
Comment thread crates/fiber-lib/src/cch/actor.rs Outdated
Comment thread crates/fiber-lib/src/cch/actor.rs Outdated
Comment thread crates/fiber-lib/src/cch/actor.rs Outdated
Comment thread migrate/src/migrations/mig_20260421_cch_multi_asset.rs
Comment thread crates/fiber-lib/src/cch/acceptor.rs
Comment thread crates/fiber-lib/src/cch/actor.rs
Comment thread crates/fiber-lib/src/cch/actor.rs Outdated
Comment thread docs/specs/cch-multi-asset-swap.md
ian added 5 commits May 6, 2026 17:19
Reserve `payment_hash` against concurrent in-flight `send_btc` and
`receive_btc` work via an `Arc<Mutex<HashSet>>` guard so the heavy
detached task cannot race the store-level check-then-put and silently
overwrite an existing order. Hoist all `receive_btc` Fiber-invoice
validation (hash algorithm, final-TLC delta, expiry) before the
proposal flow so the operator is never asked to sign off on an
invoice the hub will subsequently reject. Recompute
`duration_since_epoch` after the proposal wait when deriving the
outgoing invoice expiry, since the operator-side wait can exceed the
remaining lifetime of the original timestamp. Round the SendBTC fast
path up via ceiling division so the hub never under-collects the
Fiber leg for sub-satoshi remainders. Derive `btc_fee_msat` for the
proposal-path `receive_btc` flow from the operator-supplied gross
total instead of leaving it at zero.

Reject `smallest_units_per_sat = 0` at startup via a new
`CchConfig::validate()` invoked from `fiber-bin`, so a misconfigured
fixed-rate entry cannot mint zero-amount invoices on `send_btc` or
panic on `receive_btc`. Expose `CchMessage::GetSwapProposalTimeoutSeconds`
and a test-only `TestGetAcceptor` so the RPC layer and unit tests can
discover the configured deadline / live acceptor without relying on
hardcoded values. Document on `SwapAcceptorMessage::SubmitResponse`
that any authenticated operator may resolve any pending proposal — the
acceptor does not bind responses to the original subscription
session.
…line

Pre-existing `send_btc` / `receive_btc` RPC handlers used a hard-coded
10-minute mailbox-call timeout, so an operator who configures
`swap_proposal_timeout_seconds` above that value sees a client-visible
RPC timeout while the detached task keeps waiting (and may still
create the order later, leaving the client unable to learn its id).
Query the configured value via `CchMessage::GetSwapProposalTimeoutSeconds`
when wiring the RPC module and pass it to the new
`CchRpcServerImpl::with_proposal_timeout` builder, with a small fixed
margin to absorb scheduling/IO jitter. Document on the trait method
that responses are not bound to the originating subscription
session.
The CCH multi-asset migration writes a shadow `NewCchOrder` whose
`fiber_type_script` is a `ckb_jsonrpc_types::Script`. The migrate
crate previously pinned `ckb-jsonrpc-types = "0.202"`, which serializes
the inner `ScriptHashType` differently from the v1.x type used by the
running `fiber-lib`. After running the migration the upgraded record
would therefore be unreadable by the node. Pin to the same major
version as the live code, keep the v0.202 dependency aliased as
`ckb-jsonrpc-types-legacy` for converting the v0.7.0 snapshot value,
and bridge the two via a JSON round-trip in `convert_script`.

Add a roundtrip test module in `tests/migration_tests.rs` that
constructs a fully-populated `OldCchOrder`, bincode-encodes it,
runs `migrate_cch_order`, and re-decodes / inspects the new shape.
A real `Bolt11Invoice` is built programmatically via
`lightning-invoice` + `bitcoin` (added as `[dev-dependencies]`) to
avoid hand-crafted bech32 strings going stale.
Replace the brute-force counter loop in
`test_send_btc_proposal_path_accept` with a query through the new
`TestGetAcceptor` / `TestPendingProposalIds` plumbing, so the test
discovers the actor-generated `proposal_id` instead of guessing it.
Add `test_receive_btc_proposal_path_reject` covering the
operator-rejects branch of ReceiveBTC and a follow-up sanity check
that submitting an unknown `proposal_id` returns
`SwapProposalUnknown`. Update `create_test_fiber_invoice_with_amount`
to include `HashAlgorithm::Sha256` and to pick a final-TLC delta
that always satisfies the ReceiveBTC validation, so the
`test_receive_btc_amount_*` tests reach the amount-overflow branch
they were written to exercise.
Match the spec to the implementation: rounding `btc_msat *
smallest_units_per_sat / 1000` up ensures the hub never under-collects
the Fiber leg for sub-satoshi remainders.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants