Skip to content

Why This Exists

CKB transactions are final once nodes accept them. There is no undo, no clawback, and no “we meant to call the checker” flag on chain.

If your only defense is a function you hope runs before sign, you are betting that every build, every code path, and every compromised dependency will always call it. The firewall exists so that bet is not entirely on application discipline.

Picture a service that moves CKB for users, whether that is an agent, a wallet backend, or a payout worker.

Something upstream hands your builder a destination lock_args. It came from a tool result, a parsed invoice, or text that rode in through a prompt. It points at an attacker, but the address still looks plausible in logs and in whatever your operator sees on screen.

Your code assembles an unsigned transaction with that output. The path that actually runs in production never calls the SDK: a dead feature flag, an old binary, a forked payout path, or a compromised process that skips the check on purpose. The transaction is signed and broadcast. CKB nodes validate the locks and signatures they enforce, not your internal checklist.

If the spent cell does not use the firewall lock, the blacklist never enters validation. The transfer confirms. You find out from an explorer, an accounting mismatch, or an angry user. The CKB is gone.

If the spent cell does use the firewall lock and the destination is on the registry, nodes reject the transaction during validation, even though your runtime skipped the SDK. The spend never becomes final.

That gap between “we intended to check” and “the chain enforced a check” is what this project closes.

Developers often picture safety like this:

[ Your app ] → SDK check → sign → broadcast → CKB nodes
^^^^^^^^
"we're safe here"

In practice, everything left of sign sits in the same trust zone as the bug or the attacker: the process that builds the transaction, the libraries on that machine, agent or CI code that can rewrite outputs after your main path ran, and config that turns checks off in production.

Once the transaction is signed, nodes only enforce what the lock scripts require.

LayerWho runs itWhat it can guarantee
SDK pre-flightYour processFast feedback if that process is honest and actually called
Firewall lockEvery CKB nodeThe same blacklist rule at consensus for cells that use this lock

The SDK is still worth running: you get a clear error before broadcast instead of a rejected transaction. It is not a substitute for the lock when the threat model includes “our code might not run the check.”

These all assume the destination is already on the community registry. The firewall does not discover brand-new scam addresses until governance lists them.

Funds never behind the firewall lock. The SDK is in the repo and in the docs, but user cells still use a standard lock. The bad transfer in the walkthrough confirms with no consensus backstop.

Only the happy path is guarded. A manual admin send, a migration script, or an emergency payout tool builds transactions without the same check. One unguarded path is enough.

The registry dep is wrong. The transaction points at the wrong live cell, omits the registry dependency, or carries malformed BLKL data. Application code may not catch it before sign; the lock is meant to fail closed rather than accept an ambiguous registry.

Monitoring is the last line. An alert fires after confirmation. By then you are documenting loss, not preventing it.

  • Addresses not yet on the registry
  • Attacks that do not boil down to “send to this lock_args
  • Compromised governance keys or registry updates that pass on-chain validation

See Security model for the full boundary.