[DIS] CKB Integration for Rosen Bridge

I re-read the technical assessment, the implementation plan, and the latest ACP/address-length update. The long-address issue is not an isolated edge case. It exposes the same architectural boundary in several places.

The visible problems are:

  1. Bridge semantics are validated after economically meaningful CKB state has already changed.
    The published design explicitly allows Rosen metadata to be appended to a bridge-owned ACP Cell while xUDT/ACP enforce only the CKB-side state rules. Watchers and Guards validate the bridge meaning later, off-chain. The recent address-length finding proves that an on-chain-valid transition can still be unsafe at the Rosen layer.

  2. The ACP Cell combines several different coordinates in one mutable object.
    It is simultaneously a deposit sink, token container, metadata carrier, CKB state-rent container and concurrency resource. That coupling is why a metadata-field problem can become a CKB-capacity/resource problem without violating the underlying token rules.

  3. “Largest expected metadata payload” is an assumption, not an enforced invariant.
    The implementation plan sizes bridge ACP Cells around an expected maximum payload (for example, 144 base + 75 bytes for Cardano). The latest fromAddress finding demonstrates that the actual input surface can exceed those sizing assumptions. A security boundary cannot depend on an expected maximum that the protocol does not itself enforce.

  4. The documented message schema and the currently discussed message surface are already drifting.
    The published wire format describes toChain, fees and variable toAddress, while the latest developer note explicitly discusses attached fromAddress as a resource-relevant field. Whether fromAddress is encoded directly or reconstructed elsewhere, there must be one canonical, versioned schema and one source of truth for every component that interprets a bridge request.

  5. toChain is an unversioned global index.
    The plan states that a one-byte value is baked into every request and that UI, Watchers and Guards “must agree” on the same SUPPORTED_CHAINS assignment. Agreement by deployment coordination is not the same as an invariant. The request itself does not describe a schema/configuration epoch that proves what that byte meant when the source event was created.

  6. The bridge operation is distributed across several identity planes without one explicit immutable binding.
    A complete operation should bind at least: source OutPoint/transaction, canonical source block, confirmation state, request schema/configuration version, parsed intent, and the destination action that Guards approve/sign. The current design describes those pieces in separate scanner, extractor, configuration and TSS layers, but the published plan does not describe one operation identity that binds them end-to-end.

  7. ACP concurrency is also a lifecycle problem, not only a pool-size problem.
    The UI may select ACP Cells that are already represented by pending transactions, and when available Cells are exhausted the design can chain from an unconfirmed output. max_ancestors_count bounds mempool depth; it does not define recovery semantics. The important invariant is what happens to every dependent bridge request when an ancestor is rejected, evicted, replaced, or invalidated.

  8. Deposit and custody paths share the same TSS authority root.
    This may be an intentional Rosen trust-model choice, but it means the two CKB paths are not independent compromise domains. The same shared key ultimately authorizes ACP consolidation and custody spending. “Security parity” at the threshold-signature level should not be confused with isolation at the system level.

  9. The 50-confirmation rule is a model assumption, not the complete cross-chain finality invariant.
    The stated value is derived from a particular adversarial-hash/orphan-rate model and then doubled. The scanner can roll back local source-chain state, but the real bridge invariant is stronger: no irreversible destination action should be committed under a source interpretation that another valid service/configuration epoch could later reject.

  10. The release gate itself has drifted.
    The approved proposal states that a formal external audit occurs before final testing and public mainnet release, and separately says internal review is not a replacement for a formal audit. The current implementation plan labels mainnet release as “external audit optional,” while the discussion also describes audit as “not really mandatory.” Those are materially different acceptance conditions.

The common root is therefore not “an address is too long.”

It is a validation/projection boundary:

CKB-valid state transition
→ bridge metadata projection
→ Watcher/Guard interpretation
→ TSS authorization
→ destination-chain state

The first plane can accept a state that the later planes interpret differently or reject for reasons that were not enforced when the shared value-bearing Cell was mutated.

That is exactly what the long-address case revealed.

You can cap address length, adjust fees, resize ACP Cells, add parser checks, increase the pool and patch individual manifestations. Those fixes may be necessary, but they do not remove the architectural cause. Each new field, destination chain, parser, configuration update and ACP lifecycle rule creates another cross-layer invariant that must remain synchronized.

The stronger design requirement is that bridge intent, semantic bounds, configuration/schema epoch and source-event identity must be bound before that intent is allowed to become an economically meaningful bridge event — and the same immutable identity must survive through observation, approval, signing and destination execution.

Otherwise the system can keep improving locally while increasing the number of cross-layer conditions that must never diverge.

This is still a surface architectural review. I have deliberately not gone into implementation-level exploitation or a full code audit.

2 Likes

Hey @tianji, you look familiar!! Thanks for the review, it gave us a good excuse to publish a proper progress update.

Yeah. That’s why the design changed since the last update. In July we went through the failure modes with the Rosen team and dropped ACP. Deposits are now normal transfers to the Rosen lock, one new cell per request, like Rosen’s other UTXO chains. ACP sizing and concurrency points no longer apply.

The rest is still WIP, none of it upstream yet:

  • Metadata is one witness at index inputs.length: [1B version][4B output index][4B input index][Rosen payload]. Default SighashAll commits it, the depositor signs it. At most 87 bytes, enforced: the decoder rejects a wrong size, every Rosen codec rejects addresses above 60 bytes.
  • fromAddress is the consumed OutPoint of the selected input, not a user string.
  • Rosen limits toAddress to 60 bytes on every chain, so v1 CKB payout locks have at most 26-byte args. This limit is from Rosen, not from us.
  • Guards only spend confirmed custody cells. No chaining on unconfirmed outputs.

The chain byte and payload format belong to Rosen’s shared binary decoder, used by Bitcoin and most other Rosen chains. CKB inherits it as-is. Changing it is a bridge-wide migration, not something one new chain can do. Our envelope version byte covers the CKB framing. That framing hard-codes the payload header layout, so it is not fully independent of the payload.

The destination action Guards sign is keyed by an event id derived from the source transaction id. Guard re-loads the canonical source block, re-extracts, and compares every field at event acceptance. Only the schema version stays unbound, same upstream point as above.

Right, it is intentional: the deposit path does not touch the bridge key now. No ACP pool, no consolidation signing, users create and sign their own cells. Custody sharing one ECDSA root with the other chains is on purpose. From what I understand, spending from cold is a manual human process, kept out of the public code on purpose. Sweep triggers when hot balance passes the high mark and moves everything above the low mark to cold.

Agreed, that’s why 50 became 150: CKB mining pools are concentrated. More depth does not fix a real cartel, it raises the bar for normal reorgs. 150 is a first value, we will check it against real block data before launch. After payment there is no automatic rollback, so depth lowers that risk, it does not remove it.

I leave this one to the DAO talk.

Checking bridge meaning off-chain is how Rosen works, not a CKB hole. Bitcoin does not check it on-chain either. What I got wrong was letting a bad request change shared state. Dropping ACP fixes that: a bad request only costs the sender their own deposit, and v1 promises no refund.

Custody input selection, no-fit behavior and fee rules are still open. I am pushing on these myself until I have a full solution I am happy with. The Rosen team is there when I have questions.

Love & Peace, Phroi

3 Likes

Hello @phroi,
this is a materially different architecture.

Dropping ACP removes the shared mutable-state and concurrency surface I was referring to. One user-created cell per request, signed metadata, and confirmed-only custody inputs are all correct moves. The original CKB-local shared-state problem is resolved.

The remaining issue is now sharper: the deposit object and the signed bridge intent do not currently share the same identity boundary.

CKB transaction hashes exclude witnesses. The canonical block commits to the witness separately, and “SighashAll” authenticates the request envelope, so witness authenticity is not the problem. The structure is now:

T = raw CKB transaction
W = signed request envelope in witness[inputs.length]
txid = H(T)
eventId = f(txid)
action = G(T, W, decoder/config)

The destination action depends on “W”, but Rosen’s event ID is derived from “txid”, which does not commit to “W”.

Reloading the canonical source block and comparing every parsed field is a strong acceptance check. It does not, however, automatically make the event identity itself a commitment to the signed intent. That matters for duplicate suppression, recovery/reindexing, migrations, and any case where the same source object is reconstructed under a different parser or configuration epoch.

For CKB I would separate two identities explicitly:

deposit identity = (source txid, output index)
intent identity = hash(raw request envelope, decoder/schema epoch)

The accepted event should bind them one-to-one. If Rosen’s bridge-wide event-ID format cannot change, the CKB integration still needs to persist and verify an equivalent request digest as part of its accepted-event and duplicate-payment state.

This also makes the unbound schema version a release-critical point rather than a minor upstream detail. “toChain” is an unversioned global index, while the CKB envelope version only versions the outer framing. Re-extracting an old transaction with the current decoder is not the same as proving that it is being interpreted under the same chain table and payload rules under which it was accepted.

The accepted event therefore needs an immutable decoder/configuration epoch, or an equivalent bridge-wide migration invariant. Otherwise the source bytes remain fixed while their bridge meaning can drift.

There are four exact questions I would close before treating this route as complete:

  1. Is exactly one Rosen request permitted per CKB transaction?

    “One cell per request” does not by itself prohibit multiple request cells in one transaction. If multiple requests are possible, a txid-only event ID collapses distinct output-level events. If only one is allowed, that must be enforced by the extractor, not assumed by the UI.

  2. What makes the selected input canonical?

    “fromAddress” is now an OutPoint selected by “input index”. The extractor should prove why that input represents the source side of the deposit rather than accepting any consumed input chosen by the envelope. Exact bounds and length checks establish syntactic validity; they do not establish this semantic relationship.

  3. When is source finality revalidated relative to destination signing?

    Raising the depth from 50 to 150 is a reasonable operational change, and you already stated its correct limitation. The remaining invariant is temporal: if “event acceptance” and Guard signing are the same decision boundary, fine. If an accepted event can wait before signing, the canonical block, confirmation state, and parsed request must be revalidated immediately before the irreversible destination action, or the accepted state must expire and be invalidated on a reorg.

  4. How is custody closure defined?

    Input selection, no-fit behaviour, and fees are not secondary implementation details; they determine value conservation and idempotence. At minimum the implementation should enforce:

selected inputs = payout + Rosen-controlled change + fee
one event cannot be reserved or signed twice
no-fit produces a deterministic retry/failure state, never partial progress
hot/cold sweeping cannot race with payout selection
rejected deposits are classified as non-liabilities

On authority: moving deposits out of the bridge-owned ACP path resolves the original ingress-authority concern. The shared ECDSA custody root is then an intentional Rosen-wide compromise domain, not a CKB-specific defect. It should simply be stated as such in the threat model, together with the hot/cold reconciliation boundary.

I agree that off-chain interpretation is how Rosen works and is not, by itself, a CKB hole. The security question is narrower:

Is that off-chain meaning immutably bound to the exact on-chain deposit, parser epoch, and destination action before the action becomes irreversible?

The complete invariant is now:

signed request
→ canonical source inclusion
→ immutable interpretation
→ unique deposit/intent binding
→ Guard approval
→ exact destination action

If each arrow is one-to-one and the source state is checked at the final action boundary, the new design closes cleanly.

If “txid” remains the sole event identity while the signed intent lives in the witness and the schema epoch remains external, the ACP problem is gone, but the cross-plane identity gap remains.

4 Likes

Trying to follow the forum preference for shorter, incremental comments, I’ll keep this to one concrete continuation of the previous review.

The previous comment ended on the need for one immutable identity across source observation, interpretation, approval, signing and destination execution. Looking one layer deeper, there is a CKB-specific identity problem here:

txid is not sufficient to identify a Rosen bridge event.

CKB itself distinguishes these objects. The transaction hash is calculated from the raw transaction excluding witnesses, while an individual Cell is identified by an OutPoint = transaction hash + output index. The signing layer then operates on a separate sighash_all context that incorporates the transaction and relevant witnesses.

The current integration plan, however, still describes getActualTxId as mapping the transaction hash to the canonical CKB identity.

That creates a dangerous compression boundary:

transaction identity
    ≠ Cell identity
    ≠ signing-evidence identity

A minimal architectural requirement would be to keep at least the source-event identity and the signing/approval identity separate instead of collapsing both into sourceTxId.

For example, structurally:

SourceEventId =
    H(
      "rosen.ckb.event.v1"
      || txHash
      || outputIndex
      || schemaEpoch
      || canonicalIntentHash
    )

ApprovalId =
    H(
      "rosen.ckb.approval.v1"
      || SourceEventId
      || destinationPayloadHash
      || sighashAll
      || guardKeyEpoch
    )

This is not a prescription for those exact fields. The invariant is the important part:

two source Cells capable of producing different economic meaning must never share one bridge-event identity, and two different signing objects must never share one approval identity.

That leaves several implementation questions that should be answered explicitly:

  1. If one CKB transaction contains multiple bridge-relevant outputs, how are the resulting Rosen events distinguished?
  2. Do Watcher and Guard bind to the same exact OutPoint, or only to the parent transaction hash?
  3. Can approval/signature state ever be reused when the raw transaction identity is unchanged but the signing context is not?

This is the same boundary described in the previous review, now at implementation level:

a CKB-valid transaction is not yet a uniquely identified Rosen event.

3 Likes

@phroi .

I am not revisiting the ACP design, the txid/OutPoint/witness distinction, the decoder epoch, or source-finality revalidation. Those were covered in the earlier comments.

The next issue is different:

Watcher/Guard redundancy can still collapse into one shared interpretation failure

Rosen’s security model relies on two roles:

Watchers observe and approve an event
→ Guards independently verify it
→ Guards authorize the destination action

That is strong role separation.

But role separation is not automatically semantic independence.

In the published CKB implementation plan:

  • the Watcher path uses CkbRpcRosenExtractor;
  • the Observation Extractor wires the CKB scanner to the CKB Rosen extractor;
  • the Guard path uses the universal CkbRosenExtractor;
  • both extract the same Rosen fields;
  • both live under the same @rosen-bridge/rosen-extractor package surface;
  • and all services must share the same SUPPORTED_CHAINS interpretation.

This creates a common-mode boundary.

If Watchers and Guards use the same parsing rule, the same chain-index table, the same field-length assumptions, and the same configuration epoch, then many independent operators can still agree on the same incorrect bridge intent.

Consensus proves that participants agreed.

It does not prove that they interpreted the CKB source object correctly.

The distinction is:

independent operators
!=
independent observations
!=
independent semantic implementations

A shared decoder defect can survive both security layers.

For example, the same common interpretation could be wrong about:

  • which output is the bridge request;
  • whether one transaction contains one or several requests;
  • which input/OutPoint defines fromAddress;
  • which witness envelope belongs to the request;
  • the xUDT amount or native CKB amount;
  • fee endianness or field boundaries;
  • trailing or duplicated metadata;
  • the meaning of a toChain index under a changed chain table;
  • or the decoder/configuration epoch under which the source bytes were accepted.

In that situation, Watchers can approve the event and Guards can “independently verify” it while both are reproducing one shared parser/configuration error.

The required invariant

The Guard layer should reconstruct the bridge intent from primary CKB evidence rather than merely validate a normalized object produced by the Watcher layer.

A release-level invariant could be:

WatcherIntentDigest
    = GuardIntentDigest
    = CanonicalExpectedIntentDigest

where both digests are bound to the same:

source OutPoint
canonical source block hash
finality state
raw transaction hash
witness / signed-envelope digest
token identity
amount
destination chain
destination address
bridge fee
network fee
schema epoch
configuration / chain-table epoch

A mismatch must not be resolved by counting more signatures over either interpretation.

It should produce a distinct terminal gate:

INTERPRETATION_CONFLICT
→ no destination signing
→ source deposit quarantined
→ explicit review / recovery route

Runtime evidence should preserve both interpretations

For each accepted event, I would persist an interpretation receipt from each layer:

source_event_id
source_outpoint
source_block_hash
source_finality_receipt

watcher_decoder_id
watcher_decoder_hash
watcher_config_epoch
watcher_intent_digest

guard_decoder_id
guard_decoder_hash
guard_config_epoch
guard_intent_digest

comparison_result
destination_payload_hash

This would show whether the second layer actually reconstructed the same economic intent independently, or simply consumed the first layer’s interpretation.

If the Watcher and Guard paths intentionally reuse the same decoder implementation, that is not automatically wrong. But then the system should describe the guarantee accurately:

two independent role approvals
over one shared semantic implementation

That provides authority redundancy, but not parser diversity.

The missing protection would then need to come from an independent reference decoder, a formal canonical encoding specification, or a differential test oracle that is not implemented through the same production parsing function.

Minimal differential test corpus

I would require the Watcher path, Guard path, and independent reference oracle to produce identical intent digests for at least:

  1. one valid request in one transaction;
  2. multiple bridge-relevant outputs in one transaction;
  3. one request output plus unrelated outputs;
  4. a different selected input index;
  5. changed witness/request-envelope bytes with unchanged raw transaction identity where applicable;
  6. unknown schema version;
  7. old and new toChain table epochs;
  8. truncated address length;
  9. valid payload plus trailing bytes;
  10. fee and amount boundary values;
  11. duplicated request fields;
  12. malformed input that one parser accepts and another rejects.

The important test is not that the same decoder returns the same result twice.

The important test is that independently implemented interpretation paths converge on the same canonical intent — and fail closed when they do not.

Custody closure on disagreement

There is one final consequence.

If a canonical source deposit is already controlled by the bridge, but semantic qualification fails or the two layers disagree, the source value still exists.

It must not disappear into a generic “rejected event” state.

The source-side accounting should classify it explicitly:

OBSERVED
→ QUALIFIED
→ ACCEPTED
→ DESTINATION_RESERVED
→ SIGNED / SETTLED

or:

OBSERVED
→ INTERPRETATION_CONFLICT
→ QUARANTINED_SOURCE_DEPOSIT
→ REFUNDED / MANUALLY_RESOLVED

Every bridge-controlled source deposit should therefore end in exactly one of two classes:

accepted bridge liability
or
closed recovery/refund state

There should be no state equivalent to:

bridge acquired custody
+ event rejected
+ no destination liability
+ no recovery obligation

Why this is the remaining boundary

The previous changes improve the local CKB object significantly: one request object, signed metadata, exact source identity, and stronger finality handling.

The next question is whether Rosen’s two security layers are also independent at the level that matters most:

the economic meaning reconstructed from that object.

If the Watcher and Guard interpretation receipts are independently produced, bound to one immutable source object and configuration epoch, and disagreement blocks signing while preserving a recovery path, this boundary closes cleanly.

These are still outer-layer correctness boundaries, not the deepest protocol-core failure modes. The next pass goes further into custody, state-transition, concurrency, and recovery invariants. Continued..

1 Like

Correct. Dropping ACP closes the original shared-cell problem. The remaining questions are how Rosen identifies a deposit, when Guard checks it, what Watcher and Guard verify independently, and how unsupported deposits are handled. The repository records Sonami’s proposed implementation contract. The CKB-specific rules below are selected requirements, not landed Rosen code, and final launch values and evidence still require deployment signoff:

1. Deposit ID and payment ID

The deposit ID and payment ID answer different questions. CKB’s raw transaction hash does not include witnesses, where the Rosen request is stored. Once the transaction is confirmed, however, the CKB block commits both the transaction and its witnesses.

The two identities are:

  • The deposit keeps Rosen’s shared blake2b(sourceTxId) event key. The first-release parser accepts at most one Rosen request per transaction, so that key cannot refer to two CKB requests.
  • The outgoing payment gets a separate agreementId, calculated from the complete unsigned payment proposal. It includes the fixed signature placeholders, eventId, and txType, so every approval names one exact payment proposal. It does not identify the deposit. Other chains keep the existing TxAgreement approval by txId.

It is, by contract. The specified parser reads one fixed witness, witnesses[inputs.length]. That witness selects one deposit output and one source input. Other Rosen-lock outputs in the same transaction do not create more bridge events.

The selected input records the consumed source OutPoint named by the request; it does not prove that this cell funded the deposit output. It does not authorize the payment, choose a refund address, or control any funds. Rosen’s shared EventTrigger carries it in fromAddress; the CKB extractor derives that value as box:<previous_output.tx_hash>.<previous_output.index_decimal>.

Despite its name, getActualTxId does not identify the deposit. Guard uses it later when recording the destination payment transaction ID for reward distribution. CKB returns that payment hash unchanged, like Bitcoin.

The witness has one accepted layout: one fixed position, fixed-size output and input numbers, then Rosen’s existing request data. The parser rejects anything else. There is no version byte because there is no second format to distinguish yet. Adding one now would create compatibility rules without solving a current problem.

2. When the deposit is checked

Waiting for more confirmations reduces reorg risk; it does not create another identity. The remaining proposal was to check the deposit again immediately before paying the user:

The current Rosen flow admits an event through two one-time checks. The CKB plan keeps their ownership separate:

  • getTxConfirmation uses the transaction ID to require canonical committed status and the configured depth.
  • AbstractChain.verifyEvent uses Guard’s own node to fetch the claimed block, find the transaction, parse the request again, and compare every field that affects the payment.

After those checks, Guard builds the payment from the stored event. It does not parse the source transaction again before signing, and it does not automatically undo an accepted event after a reorg. The confirmation wait is therefore the real protection. Guard initially requires 150 CKB blocks, about 23 minutes at recent block times, while Watcher keeps its existing deployment-supplied global commitment gate. The integration does not add a separate CKB-only rollback process.

3. What Watchers and Guards verify independently

That is the shared Guard path, and the CKB plan follows it. Guard fetches CKB data through its own node and parses the request itself. It does not trust the Watcher’s parsed result as proof. The independence is:

  • Watchers and Guards are operated separately and query separate nodes.
  • They apply the same Rosen extraction contract from Utils over the same request format: Watcher through the network extractor, Guard through the serialized-transaction extractor.

This gives Rosen independent operators and chain views, but not parser diversity. The extractor semantics are shared, so a shared semantic bug could affect both layers. The plan uses one strict request format, tests malformed requests, and checks CKB serialization and hashing against official examples and CCC, the CKB ecosystem JavaScript SDK. It does not add an independently written second decoder in production.

When Guard’s result differs from the Watcher event, verifyEvent fails and the request is not accepted for payment. Guard does not check the source again before signing, and there is no separate interpretation-conflict state.

4. Custody and unsupported deposits

The detailed custody rules are in the transaction-chaining section. At a high level:

  • One accepted event produces one complete payment transaction through Guard’s shared generateTransaction flow.
  • The design requires every automatic transaction Guard constructs to use spare room to consume small bridge-owned cells and return change sized for ordinary payments. After each added cell, Guard recalculates the outputs, fee, and any cold-storage transfer before deciding whether that version still fits. Manual signing keeps its exact caller-supplied inputs. This keeps future transactions easier to build without adding separate cleanup transactions.
  • The first CKB release spends only cells committed on chain. It does not build new payments on top of unconfirmed change, which avoids extra agreement, retry, and recovery rules.
  • Before approving a payment, Guard asks its own node for every input again. Bitcoin Runes uses the same general pattern. A CKB transaction carries only each input’s OutPoint, so Guard must fetch the previous cells to check their locks, contents, and value.
  • If confirmed funds are unavailable, inputs conflict, or the cell search reaches its safety limit, the request waits and retries. Guard never sends a partial payment.

Each Guard avoids inputs already used by its own pending work or local transaction pool. If two Guards choose the same cell, CKB allows only one spend to confirm. If they choose different cells for the same event, CKB cannot detect the duplicate payment, so Rosen’s shared event handling must prevent it. A multi-Guard test must prove that only one payment survives before rollout.

That state can exist. The first release makes the boundary explicit rather than leaving it accidental. Missing, malformed, or unsupported request data creates no valid event, so it creates no bridge payment or automatic refund.

The bridge owes a destination payment only for a valid event on a supported route. Recovering an unsupported transfer is a separate operator decision outside this integration.

An unqualified wallet path is slightly different. If a well-formed request confirms, Watcher and Guard process those confirmed bytes as the request; they do not check which wallet created it. The path is still unsafe for the sender because the bridge cannot prove that the wallet or network preserved what the sender intended.

The design therefore binds each accepted request to a specific CKB witness and input without adding a second event ID or parser. If a concrete transaction sequence breaks this model, please post it.

Love & Peace, Phroi

6 Likes

Re: #150 — Frozen-revision review

Appreciate the update. One chronology point needs to be fixed first: comments #147#149 (Aug 18–23) referred to the Mar 9 revision (6312fdf9…). The new contract (commit b62254b2…, Aug 31) may legitimately fix, clarify, supersede, or make parts of that review no longer applicable. It must not be used retroactively to suggest that the earlier review was looking at the wrong public design. These are distinct system snapshots.

For future reviews, the clean way to avoid this is to pin each assessment as:

documentation revision + code commit + forum cutoff

and classify later changes as:

CONFIRMED / FIXED / PARTIALLY FIXED / CLARIFIED / ACCEPTED RISK / NOT APPLICABLE / NEW GAP.

That prevents revision mixing in either direction.

Clarified points

  • Event identification. The statement “txid is insufficient to identify a Rosen bridge event” was overly broad for the current contract. A raw CKB txid still does not commit the witness-carried request semantics; however, under the current guarantees — one accepted request per transaction, canonical block witness commitment, and Guard re-extraction — a txid-derived Rosen event key can be unique within the protocol.

  • Source input. If fromAddress is strictly provenance-only and is never promoted into funding attribution, refund authority, payment authorization, or any other stronger semantic claim, then the extractor need not prove that this input economically funded the selected output.

  • Version byte. With one explicit framing and fail-closed parsing, the absence of a version byte is not a current blocker.

These clarifications do not alter the overall review outcome.

Confirmed protocol boundaries

  • Guard does not re-parse the source immediately before signing and does not automatically roll back an already accepted event after a later reorg. The 150-block depth is therefore the actual finality assumption.

  • Watcher and Guard have independent operators and chain views but intentionally share extractor semantics. That gives independent observation, not parser diversity.

  • Same-event transactions with disjoint CKB inputs require a multi-Guard convergence test before rollout. CKB consensus alone cannot suppress a duplicate payment if two different valid payment transactions spend different inputs for the same event.

  • The state custody acquired + no valid event + no automatic payment/refund/recovery can exist by design for malformed or unsupported deposits.

These are no longer ambiguities. They are explicit protocol/risk boundaries.

The ACP redesign has shifted the critical review surface. Dropping ACP correctly addressed the original shared-state problem, but it redistributed the proof burden onto identity, persistence, asynchronous signing, distributed convergence, finality, and recovery.

It is therefore essential to distinguish between current behavior, planned fixes, and evidence that a fix actually closes the invariant. A written requirement or work item is not runtime proof.

Critical areas requiring verification before rollout

  1. Signing completion vs terminal state. Terminal-state monotonicity must be proven under delayed callbacks. The pinned Guard path allows a signing callback to remain live after the transaction row changes state, while the later signed update is keyed by raw txId. The cross-repo analysis itself acknowledges the risk of a delayed callback moving an invalid or completed row back to signed. The Aug-31 work item already calls for a status-conditioned/atomic fix; the remaining requirement is evidence that the exact implementation and tests make terminal states monotonic.

  2. agreementId authority vs raw-txId persistence. Authorization is wrapper/request scoped, while TransactionEntity remains centered on witness-excluding raw txId. The distinction between wrapper-specific failure and permanent raw-transaction failure must therefore survive suppression, invalidation, retry, persistence, and late-callback handling across the full lifecycle.

  3. Cross-Guard convergence. Local ordering such as txSignSemaphore does not establish cluster-wide convergence under different arrival orders. This must be validated before rollout for competing same-event candidates, including disjoint-input candidates, restart, late delivery, signing, confirmation, and differing local transaction-pool views. Correctness of one Guard in isolation is not proof of correctness of the Guard cluster.

  4. Manual /sign authority path. This path must provide validation guarantees equivalent to the normal route. No authority path should be able to reach TSS/signing with weaker payment, fee, no-burn, transaction-condition, or chain-specific validation merely because it entered through a manual/prebuilt route.

  5. Witness ownership. The protocol now reserves witnesses[inputs.length] for the Rosen request, while output type scripts can also address witnesses by output index. The builder’s reorder/reject rule must therefore be treated as an explicit compatibility invariant for every supported sender/type-script combination, not inferred from the plain-xUDT happy path.

  6. Temporary ambiguity vs permanent invalidation. RPC ambiguity, TSS failure, stale-node state, unavailable data, or another temporary UNKNOWN must never be promoted into permanent raw-transaction invalidity. This requires branch-level verification because a single semantic collapse from UNKNOWN to INVALID changes terminal state and can affect later reconciliation.

  7. Confirmed-event irreversibility. If there is no ordinary rejection transition after confirmed-event insertion, every condition that can make the CKB target permanently unpayable must be known and rejected before that insertion point. Any permanent target-invalidity condition discoverable only afterward creates a trapped confirmed state.

  8. Benchmark scope vs distributed closure. Local tests — including the 126-test custody harness — are useful evidence, but they are not equivalent to multi-Guard/multi-node deployment evidence. Readiness claims must not promote local/single-payment coverage into proof of distributed correctness unless ordering, cold-storage paths, mixed-node behavior, restart/recovery, late delivery, conflicting local views, and cluster convergence are directly exercised.

The relevant question is not whether the superseded ACP review was “wrong.” The relevant question is whether the new state closes the new invariants it now depends on.


P.S. — Areas unverifiable from public materials

The following are not additional findings or design objections. They are boundaries that cannot be closed either way from the public material currently available, and therefore should remain explicit rollout-checklist items.

  • Decimal/denomination conversion and upward rounding. Verify every path where source and destination assets use different decimal precision. In particular, verify that precision reduction or upward rounding cannot create unintended over-credit in destination liability, fees, settlement accounting, or recovery accounting, including boundary values and mixed-decimal assets. From the public material available, it is not possible to establish whether downstream divisibility/floor constraints fully neutralize this case.

  • toChain mapping stability across versions/configurations. The compact chain identifier depends on a shared SUPPORTED_CHAINS interpretation. Verify that Guard, Watcher, SDK, and every other consumer cannot interpret the same encoded value differently during rolling upgrades, stale configuration, mixed-version clusters, reordered mappings, or future chain additions.

  • agreementId vs policy/config identity. If agreementId does not commit to every policy/configuration coordinate that can materially change transaction interpretation or signing eligibility, verify that the same agreement cannot be evaluated under different policy/config snapshots by different Guards or by different lifecycle stages. This is especially important during rolling upgrades and configuration drift.

  • fromAddress as provenance-only. If provenance-only is the intended contract, verify that no downstream Guard, Watcher, accounting, recovery, policy, or reporting component silently promotes the same field into ownership, funding attribution, authorization, refund entitlement, or any other stronger semantic claim.

  • Contract vs exact deployed implementation. Several important protections are now described as required fixes or rollout gates in the Aug-31 work item. Validate the final system against the exact commit set, configuration set, node mix, and deployment topology that will actually run. The written contract or work plan must not be treated as evidence that the corresponding runtime property is already closed.

These points cannot be independently closed from the public material currently available. They should therefore remain explicit until the deployed system provides the evidence needed to confirm or reject them.

1 Like