Status: Draft
Version: 0.5.0
Related: structural-authorization-ckb.md, SPEC-TREASURY.md
This document specifies the core pattern only: a CKB cell whose type script permits spending when — and only when — a valid cell of an authorized type appears in the same transaction. No signature. No key. The condition is the authorization.
This is a pattern — a reusable composition of CKB's primitives (cells, type scripts, transaction introspection, since, capacity model) — not a primitive itself. Most applications use this pattern directly (bounties, bonds, escrows, assurance contracts, …). One notable adaptation — a shared, donatable pool with temporary claims and replenishment — is specified in SPEC-TREASURY.md.
- Roles: guarded cell, condition cell
- Authorization mechanism (structural, not key-based)
- Script responsibilities and mutual validation
- Capacity accounting invariants
- CKB deployment model (code hash + args)
- Security properties and threat model
- Application-specific condition logic (see application specs)
- Lifecycle patterns (single-phase vs multi-phase, see
SPEC-TREASURY.mdor application specs) - Concrete cell data layouts
- Frontend / mempool coordination
| Term | Definition |
|---|---|
| Capacity | CKB stored in a cell (capacity field), denominated in shannons (1 CKB = 10⁸ shannons). |
| Guarded cell | A live cell whose type script is the guard script. Holds capacity that can only move when a valid condition is met. |
| Condition cell | A cell whose type script is an authorized condition script. Its presence in a transaction is what authorizes spending the guarded cell. |
| Authorized condition type | The condition type script hash that a given guard instance will accept. Fixed in guard args. |
| Guard script | The type script on guarded cells. Checks for authorized condition cells and enforces capacity conservation. |
| Condition script | The type script on condition cells. Enforces whatever application rules govern creation and consumption of conditions. |
A CKB cell with an open lock whose type script permits spending only when a valid cell of a specific authorized type appears in the same transaction.
Authorization is structural, not key-based:
- No private key can spend the guarded cell directly.
- A spend is permitted only when the transaction contains a valid condition cell of the authorized type.
- The condition script enforces application rules (signatures, payloads, thresholds, time gates, preimages, etc.).
- Both scripts evaluate the same transaction in the same consensus pass. Neither calls the other. This is a property of CKB's execution model: all type scripts on all inputs and outputs of a transaction run independently against the same transaction context.
The core mechanism does not prescribe a lifecycle. How the condition cell relates to the guarded cell's spend depends on the application:
| Mode | Description | Example |
|---|---|---|
| Single-phase | Condition cell is created and/or consumed in the same TX that spends the guarded cell | Bounty: present hash preimage, claim funds — one TX |
| Multi-phase | Condition cell is created in one TX (funded from the guarded cell), then consumed in a later TX after some delay or event | Governance: create proposal, wait 72h, execute — two TXs |
The guard script does not distinguish these. It checks: "is there a valid condition cell of authorized type in this transaction?" The condition script and the application determine whether the condition cell persists between transactions.
graph LR
subgraph "Single-phase"
G1[Guarded cell] -->|spend| TX1[Transaction]
C1[Condition cell] -->|present in same TX| TX1
TX1 --> R1[Result]
end
graph LR
subgraph "Multi-phase"
G2a[Guarded cell] -->|TX 1: create condition| C2[Condition cell]
C2 -->|TX 2: consume condition| R2[Result]
G2a -->|TX 1: change| G2b[Guarded cell ʹ]
G2b -->|TX 2: replenished| G2c[Guarded cell ʹʹ]
end
All compliant implementations MUST satisfy these invariants.
| ID | Invariant |
|---|---|
| INV-S1 | A guarded cell input may appear only in transactions containing at least one valid condition cell of the authorized type. |
| INV-S2 | Every condition cell accepted by the guard MUST have a type script whose hash equals the guard's authorized_condition_script_hash (see §6.1). |
| INV-S3 | No lock script signature is required for guard authorization. Guard witness MUST be empty (0x). |
| INV-S4 | Adversarial cells that mimic condition layout but use wrong code_hash, hash_type, or args MUST be rejected. |
| INV-S5 | Condition cells validated by the guard MUST also pass their own type script (guaranteed by CKB consensus — all type scripts run). |
| ID | Invariant |
|---|---|
| INV-A1 | For every transaction: Σ(input.capacity) = Σ(output.capacity) + fee. |
| INV-A2 | The guard script MUST verify capacity conservation: the guarded cell's capacity is accounted for across outputs (condition cells, change outputs, result outputs). The specific conservation rule is defined by the application but MUST NOT allow silent capacity drain. |
| INV-A3 | Guarded cell outputs MUST meet minimum CKB occupancy for their byte size. |
| ID | Property |
|---|---|
| INV-L1 | Anyone may submit a valid transaction that spends a guarded cell (no privileged relayer). |
| INV-L2 | Anyone may add capacity by creating a new cell at the guarded cell's lock + type address. Note that this does execute the guard type script — on CKB, type scripts run for cells in both inputs and outputs — but in creation mode (guarded cell in outputs only, no guarded input), which the guard MUST permit without a condition cell. |
Deployment: One guard script binary referenced by code_hash. Per-instance parameters in args.
Args layout (core, 32 bytes minimum):
| offset | size | field |
|--------|------|-----------------------------------|
| 0 | 32 | authorized_condition_script_hash |
Applications MAY extend args with additional fields (version byte, capacity limits, etc.).
authorized_condition_script_hash: Blake2b hash of the full Script struct (code_hash + hash_type + args) for the condition type. This pins the exact condition script including its args, preventing cross-application confusion. For upgradeable deployments, using the Type ID hash is a permitted variant, but the default MUST be the full Script struct hash.
Guard lock script: MUST be an open lock (always_success) or equivalent zero-auth lock. The type script is the sole gatekeeper. A lock requiring a signature (like standard secp256k1) violates the structural authorization premise and is NOT permitted.
Guard script behavior:
When the guard script runs (guarded cell appears as input or output in a transaction), it:
- Scans the transaction for condition cells matching
authorized_condition_script_hash. - Verifies at least one valid condition cell is present (as input, output, or both — depending on application).
- Verifies capacity conservation (application-defined rule, but drain MUST NOT occur).
When a guarded cell appears only as an output (creation / donation), the guard script SHOULD permit creation without condition-cell presence — this enables donations and initial seeding. Applications MAY restrict creation further.
Each application deploys a condition script binary. The guard binds to one condition script hash via args.
Condition script responsibilities:
| Context | When | Responsibility |
|---|---|---|
| Creation | Condition cell appears in outputs | Validate data, capacity, references, creation rules |
| Consumption | Condition cell appears in inputs | Validate success/abort conditions, result outputs, witnesses |
Condition script MUST NOT:
- Assume it is called by the guard script.
- Trust return values from other scripts.
- Rely on transaction input ordering beyond what it verifies itself.
Condition script MUST:
- Fully constrain every output degree of freedom it grants (recipients, amounts, data). Transactions in this pattern carry no signatures, so any transaction in the mempool can be reconstructed by an observer before inclusion. Any output field the condition script does not pin — a payout address read from a witness instead of from cell data, an unchecked change output, an unbounded fee — is attacker-controlled. Treat every unpinned field as adversarially chosen.
- Where condition creation is permissionless, impose an economic cost on creation — a creation bond or minimum locked capacity, forfeited or time-locked on abuse — sized so that sustained occupancy of a singleton guarded cell (§7.4) is expensive. Permissionless-creation applications without a creation cost are trivially DoS-able: an attacker can occupy the singleton every block with valid-but-useless condition cells for transaction fees alone, starving legitimate users indefinitely.
- Declare the recipient binding mode of every payout path (identity-pinned or role-pinned, §5.5); for role-pinned paths, implement a binding mechanism that makes fulfillment and payout inseparable.
- Restrict conditional inputs to consensus-fixed and self-owned state (§5.6); document any foreign or off-chain input as an explicit trust assumption.
- Enumerate every branch with a default-reject
else— unknown transaction shapes MUST fail closed (Appendix B).
Condition args: Application-defined. Typical fields: protocol version, committee keys hash, campaign ID, etc.
A cell is a valid condition for guard purposes if and only if:
hash(cell.type_script) == guard.args.authorized_condition_script_hash
AND cell.type_script.code_hash resolves to deployed condition binary (via cell_deps)
AND condition script passes for this cell
Comparison MUST use the full script hash, not data layout alone (INV-S4).
The guard script infers context from the transaction structure:
- Guarded cell in inputs, condition cell in outputs: A condition is being created, funded from the guarded cell (multi-phase start, or single-phase where condition is created for immediate use).
- Guarded cell in inputs, condition cell in inputs: A condition is being fulfilled / consumed (multi-phase end, or single-phase where condition is consumed).
- Both: Condition cell in both inputs and outputs (rotation, batch operations, or single-phase with creation + consumption).
- Guarded cell in outputs only (no guarded input): Donation / creation. Guard script SHOULD permit without condition.
The guard script does not need to distinguish these rigidly. The minimum requirement is: if a guarded cell is being consumed (appears as input), at least one valid condition cell must be present in the transaction.
Caution — condition cells on both sides: consumption-mode checks MUST NOT ignore condition cells that are simultaneously created in the same transaction. A guard that detects "condition in inputs" and stops looking allows an attacker to smuggle newly created condition cells into a consumption transaction, bypassing whatever accounting governs creation. Guards and condition scripts MUST account for condition cells in inputs and outputs independently. Applications that do not define an explicit rotation/update mode SHOULD reject transactions with authorized condition cells on both sides (see
SPEC-TREASURY.md§6 for the treasury-specific rule).
The condition — what must be fulfilled — is always fixed at creation. Where the funds go when it is fulfilled is a separate design decision, and every application MUST declare which of two binding modes each payout path uses:
Identity-pinned. The recipient's lock script (or lock hash) is written into cell data when the cell is created: an escrow's refund address, a crowdfund's creator, a bond's two parties. The destination is a constant; nothing in the claiming transaction can vary it. This is the lowest-risk mode and the default choice whenever the recipient is knowable at creation.
Role-pinned. The recipient is whoever fulfills the condition — unknown at creation by design (open bounties, permissionless liquidation). Here the binding requirement is: fulfillment and payout MUST be cryptographically inseparable. A condition whose proof is copyable and whose payout destination is a free field of the claiming transaction is a bearer note in a public mempool — the moment the legitimate claimant broadcasts, anyone can copy the now-visible proof into their own transaction with the payout redirected. The condition script sees a valid proof either way. Acceptable binding mechanisms:
- Commit-reveal: transaction 1 creates a commitment cell binding
hash(proof ‖ claimant_lock_hash)(revealing nothing); transaction 2 reveals the proof and pays the lock committed in transaction 1. A front-runner who copies the reveal pays the original claimant. The reveal SHOULD require the commitment to have aged past asinceinterval, so a front-runner cannot race a fresh commitment against an observed reveal. - Proof-embedded recipient: the proof itself covers the payout destination — e.g., a signature over the recipient lock hash from a key that only the legitimate fulfiller can hold, or a preimage constructed as
hash(secret ‖ claimant_lock_hash).
The executor tip (SPEC-TREASURY.md INV-TA7) is the one deliberate exception: an unpinned recipient is acceptable only where the amount is capped and every other output is pinned, so a front-runner can change who earns the tip but not what the transaction does.
Every fact a condition script branches on belongs to one of four classes. Applications MUST audit every conditional input against this table. Only the first two classes preserve the trustless property; "on-chain" is not the safety criterion — who can change the value is.
| Class | Examples | Status |
|---|---|---|
| Consensus-fixed | since, capacities, script hashes, header median time (only where staleness disadvantages the spender — §7.5) |
Safe |
| Self-owned guarded state | Cells whose type scripts are part of this application's own deployment: pool data, receipts, proofs | Safe, iff those scripts are specified to the same standard as the guard |
| Foreign mutable on-chain state | Another protocol's registry, a DEX pool cell read as a price, any cell a third party can update | Functionally an oracle. The adversary does not attack your script; they move the variable. A designated writer is a trusted party; an open writer is an attacker. |
| Off-chain facts | Prices, delivery, identity, weather, real-world events | Requires an oracle. The application is trust-shifted (custodian removed, truth-teller remains), not trustless, and MUST document itself as such. |
| Layer | Fixed | Per-instance |
|---|---|---|
| Script binary | code_hash |
— |
| Guard instance | guard code_hash |
args { authorized_condition_script_hash, … } |
| Condition instance | condition code_hash |
args { application params } |
| Cell state | — | data field per application schema |
hash_type |
Use case |
|---|---|
data / data1 |
Immutable guard/condition (recommended for production after audit) |
type + Type ID |
Upgradeable; requires trust in Type ID cell lock holder |
Recommendation: Deploy on testnet with Type ID; freeze to data hash before mainnet funds.
"No one controls it" holds only if users can verify that the deployed code matches the audited source. Otherwise trust does not disappear — it shifts from a committee to whoever published the address. For any deployment intended to hold significant value:
- Script binaries MUST be built reproducibly: pinned toolchain and documented build steps that produce bit-identical output.
- The source code, build instructions, audit report, and resulting
code_hashvalues MUST be published together. - Announcement materials SHOULD include the full
Scriptstructs (code_hash,hash_type,args) so wallets and users can independently recompute the guarded cell's address rather than trusting a published string.
- Deploy condition script binary (cell with data).
- Compute
authorized_condition_script_hash. - Deploy guard script binary.
- Create initial guarded cell with
argsreferencing step 2 (seed capacity). - Publish address (lock + type) for donations / interaction.
| Property | Description |
|---|---|
| No key path | No signature unlocks guarded capacity. |
| Condition-only spend | Guarded cell moves only with authorized condition cell in the same TX. |
| Atomic mutual validation | Condition and guard scripts evaluated in one consensus context. This is a CKB execution model property that the pattern relies on — not something the pattern itself provides. |
| No reentrancy | No inter-script calls; no reentrancy surface. This is inherent to CKB's execution model. |
| Threat | Mitigation |
|---|---|
| Output injection (fake condition) | Full script hash check (INV-S4); condition cell validation |
| Condition script bug | Pre-audit, formal spec, testnet soak; immutability after deploy; outflow throttle bounds worst-case drain (§7.5) |
| Capacity drain via valid conditions | Application-level rate limits, minimum capacity rules, creation bonds |
| Stranded capacity (condition with no valid spend path) | Application MUST define recovery/abort mechanisms for multi-phase patterns |
| Type confusion across deployments | authorized_condition_script_hash binding |
| Griefing (spam conditions occupying a singleton guarded cell) | Creation bond / minimum locked capacity is REQUIRED when creation is permissionless (§5.2); transaction fees alone are insufficient |
| Mempool malleability / front-running (signature-free TX observed in mempool and reconstructed with altered outputs) | Condition script MUST pin every result-output degree of freedom — recipient, amount, data (§5.2). Unpinned fields are attacker-controlled at inclusion time. |
| Fee siphoning (unbounded fee on a keyless spend redirects guarded capacity to miners) | Guard or condition script MUST bound the fee, or require fees paid from separate fee-paying inputs (see SPEC-TREASURY.md INV-TA6) |
| Smuggled condition creation (condition cell created inside a consumption TX) | Guard accounts for condition cells in inputs and outputs independently (§5.4) |
| Bearer-proof front-running (role-pinned claim observed in mempool, proof copied, payout redirected) | Recipient binding (§5.5): commit-reveal or proof-embedded recipient — fulfillment and payout made cryptographically inseparable |
| Foreign mutable state as condition input (an "on-chain oracle" the adversary moves instead of attacking the script) | Input-class audit (§5.6): conditional inputs restricted to consensus-fixed or self-owned state; any exception documented as a trust assumption |
| Trusted at inception | Trustless in operation |
|---|---|
| Deployer chooses condition rules | No committee to move funds |
| Script bytecode (if immutable) | No ongoing admin keys |
| Initial seed capacity | Capacity additions are voluntary |
The permanence of bugs: The pattern is intentionally non-upgradeable by default (via hash_type: data). This is a feature, not a limitation. The "no one controls it" property becomes a liability if there is a logic error, but any post-deployment recovery mechanism reintroduces a trusted party. The response is a higher pre-deployment diligence bar:
- Formal specification of condition rules before implementation
- Comprehensive negative test coverage (see §8)
- Extended testnet soak (recommended: minimum 30 days with adversarial testing)
- Independent audit for any deployment holding significant value
UTXO contention: If the guarded cell is a singleton (common for pool-style applications), operations are strictly sequential. Two users cannot spend the same guarded cell in the same block. Applications MUST account for this. Throughput ceiling: ~1 operation per CKB block (~10s). Applications needing higher throughput should consider multiple guarded cells with merge/split rules.
Immutability means a condition-script bug has no post-deployment fix (§7.4). A guard-level outflow throttle bounds the blast radius without reintroducing a trusted party: even a fully compromised condition script can only drain a bounded amount per interval, leaving time for donors to stop donating and for remaining funds to be moved out through legitimate paths. Guards protecting significant value SHOULD implement it.
Mechanism:
- Guard
argsare extended (per §5.1) withmax_outflow_per_spend(8 bytes, shannons) andmin_spend_interval(8 bytes, seconds). - The guarded cell's
datacarries alast_spend_timemarker (8 bytes, unix milliseconds). - On any spend of the guarded cell, the guard additionally:
- Loads a block header via
header_depsand reads its median time (now). - Asserts
now >= last_spend_time + min_spend_interval(from the guarded input's data). - Asserts outflow — guarded input capacity minus guarded change-output capacity — is
<= max_outflow_per_spend. - Asserts the guarded change output's
datarecordslast_spend_time == now(the median time of the presented header).
- Loads a block header via
- Donation/creation mode is unaffected: inflow is never throttled.
Header-staleness note: the spender chooses the header_dep, but staleness only works against them. A stale header yields a smaller now, which makes check (2) harder to pass and records an earlier last_spend_time than real time in check (4) — wasting part of the spender's own budget window. Each successive spend must present a header whose median time is at least min_spend_interval newer than the previously recorded value, so a sequence of k spends requires ≥ k × min_spend_interval of real chain time.
Trade-off: throttling further serializes an already-sequential singleton (§7.4) and caps legitimate burst usage. Size max_outflow_per_spend × min_spend_interval per application: small enough that a total-compromise drain is survivable, large enough for the largest legitimate single operation.
Risk in this pattern is not proportional to value held or to code size. It is proportional to the size of the transaction space that must be exhaustively rejected. Value multiplies an attacker's incentive to search that space; it does not create the gaps. Use this model to place an application before building it.
Low-risk profile — an application is low-risk when all of the following hold:
- Decidable: every conditional input is consensus-fixed or self-owned state (§5.6). No foreign or off-chain facts.
- Terminal: funds flow one direction toward an end state and the cell dies. No accumulators, counters, or replenishment loops.
- Pinned: closed output set (§5.2) and declared recipient binding on every payout path (§5.5), with only bounded fee/tip unpinned.
- 1:1 blast radius: one guarded cell per obligation (bounty, escrow, bond). A bug in one instance loses one instance.
- Fail-safe direction: the worst plausible bug strands or refunds funds — never redirects them. The timeout/refund path SHOULD be the simplest code in the script and share nothing with the success path, so a broken success path leaves the safety net intact.
- Enumerable: the complete must-reject matrix can be written down and known complete.
Risk multipliers — each of these multiplies the transaction space to be rejected, roughly in ascending order of cost: complex data parsing; aggregation/batching (multiple guarded inputs or conditions per TX); multi-phase lifecycles (every phase adds states, every state needs an exit); shared pools (blast radius stops being 1:1); mutable shared state and accumulators (every TX becomes a state transition to verify under adversarial input); counting over dynamic sets (votes: sybil, double-count, replay, membership snapshots); foreign mutable or off-chain inputs (§5.6 — a category exit, not a increment).
Complexity is acceptable when it is disciplined. The danger is not complexity per se but unenumerable complexity. A complex application remains auditable exactly when:
- every branch is enumerated with a default-reject
else(unknown transaction shapes fail closed — Appendix B); - each transaction class has exactly one accounting equation, so every shannon's movement is attributable to one rule;
- the state machine is explicit, every state has an exit, and the transition table is complete;
- the whole decomposes into independently specified, independently testable invariants rather than one clever monolith.
These four properties generate the negative-test matrix mechanically. An application meeting this bar can be lower-risk than a "simple" one with a sloppy periphery.
Deployment ladder: build and soak in ascending risk order — 1:1 terminal cells (bounties, escrows, bonds) first; single-accumulator applications (crowdfunds, assurance contracts) second; pooled treasuries with counting (governance) last, funded small and throttled (§7.5). Each rung's audit and test corpus is reusable evidence for the next.
| ID | Test |
|---|---|
| T+01 | Valid spend with authorized condition cell present |
| T+02 | Capacity conservation holds across spend |
| T+03 | Empty witness on guarded cell input |
| T+04 | Donation (new guarded cell created) succeeds without condition cell |
| T+05 | Condition cell in outputs (creation context) — guard permits |
| T+06 | Condition cell in inputs (consumption context) — guard permits |
| T+07 | Spend within outflow throttle limits passes; change output records header median time (if throttle adopted, §7.5) |
| ID | Test |
|---|---|
| T-01 | Spend without any condition cell |
| T-02 | Spend with condition cell of wrong code_hash |
| T-03 | Spend with condition cell of correct code but wrong args |
| T-04 | Spend violating capacity conservation |
| T-05 | Spend with signature witness instead of empty (no proof) |
| T-06 | Condition cell from foreign deployment (wrong full script hash) |
| T-07 | Adversarial output mimicking condition layout but wrong type |
| T-08 | Malleated spend: valid condition present but a condition-pinned result output redirected (wrong recipient / amount / data) |
| T-09 | Consumption TX that also creates a new authorized condition cell (both-sides smuggling), where the application defines no rotation mode |
| T-10 | Spend exceeding max_outflow_per_spend (if throttle adopted, §7.5) |
| T-11 | Second spend before min_spend_interval has elapsed, or change output recording a last_spend_time other than the presented header's median time (if throttle adopted, §7.5) |
| T-12 | Front-run replay of a role-pinned claim: valid proof copied from an observed transaction with the payout recipient substituted — MUST fail under the declared binding mechanism (§5.5) |
- Randomized output injection during guarded cell spends
- Capacity sum invariant after every TX (INV-A1)
- Fuzz condition cell
datanear boundary lengths
Structural authorization as a mechanism — granting authority by the presence of another script in the transaction — is an established CKB idiom. This spec names, generalizes, and hardens that idiom; it does not invent it. Cross-chain prior art (Bitcoin covenants, Ergo ZK Treasury, Cardano eUTXO, Ethereum DAO tooling) is surveyed in structural-authorization-ckb-comments.md; the CKB-native lineage is:
| Prior art | Mechanism | Relationship to this spec |
|---|---|---|
| sUDT / xUDT owner mode | Token type script sets is_owner_mode when an input cell's lock script hash matches the owner lock hash in args; xUDT flags extend the match to input/output type scripts |
Authorization by script presence — but as an alternate path alongside ordinary key ownership. Here, presence is the sole authorization and no key path exists. |
| Cheque cell | Lock script permits claiming when the receiver's lock script appears among the transaction's inputs | Same presence test, applied on the lock side for a 1:1 payment; no generalized guard/condition binding. |
| Anyone-can-pay lock | Permits adding capacity/UDT to a cell without a signature | Analogous to this spec's donation/creation mode (INV-L2). |
| Script delegation idiom | Early CKB script-programming literature: "check that an input cell carries a specified script, and delegate validation to it" | The general trick this pattern is built from. |
What this spec adds over that lineage: (1) the condition cell's presence is the only spend authority on a keyless, open-lock cell; (2) the guard↔condition binding is explicit and pinned by full script hash (INV-S2/S4); (3) capacity-conservation invariants, a threat model, and test requirements are specified; (4) a lifecycle and replenishment adaptation is layered on top (SPEC-TREASURY.md).
- Pattern overview:
structural-authorization-ckb.md - Discussion / cross-chain prior art:
structural-authorization-ckb-comments.md - Treasury application:
SPEC-TREASURY.md - CKB Type ID: https://docs.nervos.org/docs/script/type-id
- CKB Script intro: https://docs.nervos.org/docs/script/intro-to-script
- CKB script validation model (type scripts run for inputs and outputs): https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0022-transaction-structure/0022-transaction-structure.md
- CKB
since/ transaction structure: https://docs.nervos.org/docs/tech-explanation/since - xUDT owner mode: https://docs.nervos.org/docs/common-scripts/xudt
- Cheque cell: https://github.com/duanyytop/ckb-cheque-script
- Anyone-can-pay lock: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0026-anyone-can-pay/0026-anyone-can-pay.md
- Script delegation idiom: https://xuejie.space/2019_09_06_introduction_to_ckb_script_programming_udt/
- Live implementation (governance): https://github.com/digitaldrreamer/ckb-transaction-firewall
- UTXO output injection: arXiv:2406.07700
fn guard_main():
authorized = load_args().authorized_condition_script_hash
if guarded_cell_in_inputs():
// Guarded cell is being spent — require authorization
condition_cells = find_condition_cells(authorized) // in inputs, outputs, or both
assert len(condition_cells) >= 1
assert capacity_conserved()
assert witness_is_empty()
else:
// Guarded cell appears only in outputs — creation / donation
// Permit by default (application MAY add creation restrictions)
return OK
fn condition_main():
if condition_in_outputs() && !condition_in_inputs():
return on_create() // validate creation rules
if condition_in_inputs() && !condition_in_outputs():
return on_consume() // validate fulfillment / abort
if condition_in_inputs() && condition_in_outputs():
return on_update() // validate rotation / update (if supported)
return ERR
Application specs implement on_create, on_consume, and optionally on_update.