Skip to content

Security Model

The firewall is an outgoing payment filter. It prevents a wallet cell from being spent in a transaction that sends to a blacklisted address. It does not block incoming payments, does not filter other inputs in the same transaction, and does not affect counterparties that interact with contracts you use.

If a blacklisted entity sends CKB to your wallet, you receive it. If your wallet cell uses the firewall lock and you try to send to a blacklisted address, the transaction is rejected at consensus.

  • Sends to known blacklisted lock or type args, enforced by every CKB node
  • Application code that skips a client-side check — the lock runs regardless
  • Compromised agent runtimes, prompt injection, or hijacked tool outputs that produce a bad destination address
  • Addresses not yet on the blacklist
  • Attack vectors that do not boil down to “send to this address”
  • Incoming transfers from blacklisted addresses
  • Wallet cells that do not use the firewall lock
  • A compromised governance key producing an authorized blacklist update
  • Entries are sorted strictly by identifier bytes; duplicates cause RegistryNotSorted and are rejected
  • lock_args and type_args are checked independently, controlled by the flags byte in the lock args
  • The registry cell outpoint moves after every governance update — always fetch a fresh outpoint before building a transaction

Fail-closed means when in doubt, reject — there is no “allow by default”:

Blacklist entries can have an expiresAt unix timestamp. The firewall reads the chain’s median block time (MTP) to evaluate expiry — and median block time requires header_deps in the spending transaction.

If a firewall-protected cell is spent without header_deps, the median time evaluates to zero, and all temporary entries are treated as permanently active. The transaction does not fail with a clear error — it silently enforces the wrong policy, blocking spends it should allow.

Always include a recent block hash in header_deps when spending a firewall-protected cell if the registry cell contains time-based entries.

Registry updates and in-flight transactions

Section titled “Registry updates and in-flight transactions”

A governance update consumes the old registry cell and creates a new one. Any pending user transaction that holds a reference to the old cell as a cell_dep will fail at the miner level once the governance transaction confirms — the referenced dep cell no longer exists.

For this reason, governance updates should be announced before submission, and submitted at low-traffic periods when fewer in-flight transactions are likely to be affected.

Only one registry update can be in-flight at a time. The registry is a single cell; two simultaneously-approved governance proposals will race, and one will fail because the cell it references has already been consumed. The CLI does not automatically detect or retry this race.

What the governance lock enforces on-chain

Section titled “What the governance lock enforces on-chain”

The governance-lock script enforces three things:

Review window. The anchored PBLK proposal input’s since field must encode a relative median-time-past delay >= review_delay_ms from the GOV1 v4 witness. CKB consensus refuses to include the transaction until that proposal input has aged by the required delay. This is not a CLI rule — bypassing the CLI and building the transaction manually does not circumvent it.

Validator vote threshold. The script reads the validator Merkle root and threshold from the BLKL v2 governance header embedded in the registry cell. Each yes vote in WitnessArgs.lock carries the validator pubkey, vote signature, and Merkle proof. Fewer than threshold valid validator yes votes → reject.

The execute CLI also verifies vote signatures and Merkle proofs before building the transaction, but the on-chain governance lock is the final authority.