Proof-gated covenant ownership of KCC20 state: worked profile + TN10 receipts

Take 2. Splitting this out of the KCC20 spec thread per community
feedback; that thread is for converging the spec; this is a work built on
it.

TL;DR: a ZK-proof-gated covenant (3× consensus-executed on TN10) as
covenant-id/v1 owner of KCC20 state, plus a two-transition
authorize/execute pattern that keeps proof mass and token mass in separate
transactions.

I’ve been following that discussion while building in an adjacent lane;
the merged KCC20 DRAFT and the call for reference implementations make
this the right time to share it. This idea exercises a nuance of the spec
I haven’t seen implemented in kind on-chain: covenant-id/v1 (0x04)
ownership where the owner-covenant’s spend authorization is a
zero-knowledge proof verified in consensus. For composed shapes exceeding
block caps, the same covenant becomes a two-transition state machine —
authorize, then execute.

What runs today

A proof-gated settlement covenant, consensus-executed three times on TN10. The
covenant is keyless: its single spend path verifies a RISC Zero succinct
receipt through the Toccata ZK opcode pair (OpChainblockSeqCommit — 0xd4 on
TN10 — plus the zk-verify precompile) against a pinned image_id, and releases funds iff the
proof verifies. What the proof attests is an application-defined settlement
condition evaluated off-chain; the convention-relevant fact is that the
condition sits entirely behind the image_id — the covenant, the token program,
and the transaction shape are identical for any guest program.

# Step TN10 txid
1 fund 8d778fa855a79581f04b83bd1952b3a68d93f6b31f97aeeda7e696321f4e6707
1 claim (accepted +100 ms) 85dc14986b0869ed363749b05b76eb73c636be9c5c50055eddc7cc57f1121edb
2 fund (accepted +497 ms) 2853ec78df568d0c37dcb1cecfb9638dd06273b0154a42bcbafa76b872228259
2 claim (accepted +462 ms) d62b7297a740e3ae660637a6b8ba48f859a6955fcbdf40bcf74457426eeaa2a6
3 fund (accepted +422 ms) 019d13bf383d55b72e003df3847b2a63846c970ce9b0386d114d2ba74e4beee8
3 claim (accepted +265 ms) 45ed1e397ecba11dc0b47f4136014da779c41b393f6e3615e0cbca8624299776

Explorer: tn10.kaspa.stream/transactions/<txid>. Latencies are first-seen
to acceptance.

Invariants across all three, byte-exact: seal 222,668 B · full signature
script 223,435 B · computeBudget 2,501 units · transaction mass 474,095 g.
One budget unit meters 10,000 script-units; the precompile alone meters
25,000,039 script-units, and the tightest valid budget for a claim is 2,500
units. The seal is a literal push (OP_PUSHDATA4), so it costs 0
script-units.

Each claim was preceded by a forged claim carrying one corrupted journal byte:
mempool-accepted, consensus-rejected with failed to verify the signature script: ZK Integrity: Verification failed, and — because the Kaspa txid
excludes the signature script — the forged and honest claims share the same
txid. Demonstrated on all three settlements. The journal binds the
recipient, the value, and escrow_id (the funding txid, computable
pre-broadcast); the covenant program pins the payout scriptPubKey and the
output count. A mempool observer holding the honest seal can neither
redirect the payout nor authorize a different instance, and replay across
instances is structurally impossible.

The composed construct: proof-gated covenant as 0x04 owner of a KCC20 state

Spec references throughout are against kaspanet/kccs main as of
2026-08-27. Transaction shape (one atomic tx, two covenant families):

inputs
  [0] KCC20 token instance, family F        invokes transfer(next_states, witness)
                                            witness = 0x00 (normal), owner-auth
                                            bytes EMPTY per the 0x04 row
  [1] proof-gated covenant instance         invokes claim(...); sig script
                                            carries the 222,668-B seal
  [2..] optional F delegators, fee input    transfer_delegator(...) / plain

outputs
  [0] F continuation                        next_states[0] — validated twice, below
  [1] proof-gated covenant payout/continuation

Token side (per KCC-20 §2): input [0] is F’s leader (lowest-indexed input
carrying F’s covenant id, KCC-1 §9.1). It validates conservation over
next_states against the actual family continuation outputs, and validates
every next_states[i].owner_scheme against F’s configuration. The
authority-side approval check is the KCC-2 minimum:
OpCovInputCount(owner) > 0, satisfied by input [1]'s participation —
and KIP-20 covenant ids are lineage-bound, so no other input can carry the
owner id. No
signature, no witness bytes on the token side; the proof never touches F’s
program — which is why no change or accommodation in KCC-20 is being
requested here.

Authority side: the claim entrypoint keeps its existing checks — receipt
verification via the seq-commit pair + precompile against the pinned
image_id; journal digest derived on-stack, never supplied; journal fields
introspected against the transaction; escrow_id == this instance’s funding
txid. Four composed checks extend them:

1. next = KCC20State {
     amount:               <trade amount>,
     owner:                <recipient identifier per agreed scheme>,
     owner_scheme:         <recipient's scheme byte>,
     borrow_scheme:        0x00,                     // disabled/v1, imposed
     borrow_guard:         0x00^32,
     extension_commitment: <pinned at deposit>,      // preserved
   }
2. R_next = F.template.prefix || EncodeState(next) || F.template.suffix
   (KCC-1 §8.5; EncodeState per §8.1 — for this layout, 112 encoded bytes:
   09||amount_le8 · 21||owner · 02||scheme · 02||borrow_scheme · 21||guard ·
   21||ext, PushExplicit opcodes included)
3. require TemplateHash(F.prefix, F.suffix) == pinned_template_hash
   (KCC-1 §8.3 length-bound form; supplied prefix/suffix are otherwise
   unauthenticated per §8.5)
4. require outputs[0].scriptPubKey == v0 P2SH over Blake2b(R_next)
   (KCC-1 §7)

The journal grows by the packed form of next (106 bytes, KCC-1 §10.1
Packed): the proof then binds the exact successor state the same way it
already binds escrow_id. Whatever condition the guest evaluates, the
successor it authorizes is pinned in the statement itself. F’s leader
validates the same output [0] for conservation from its own shared context;
KCC-1 §9 scopes leader/delegator roles per covenant id, so the two
validations compose with no cross-obligation. This is the asymmetric ICC
pattern from the spec thread, with the controller’s authorization
being a proof rather than a key. It also answers the spec thread’s question of
what a transfer checks to accept a COVENANT_ID owner: OpCovInputCount on the token
side, full successor imposition on the authority side.

On “co-presence is not authorization”: for this authority the two
coincide, and the profile makes it normative. The authorization path is
single: it opens only on a valid receipt and pins the exact token
continuation it approves. Every other spend path must structurally exclude
token co-spends. Here the only other path is the deadline refund, which
constrains the transaction to a single output, so a refund can never satisfy
a token family’s approval check. The covenant cannot be co-present for an
unrelated reason.

To be precise about status: the six receipts above are the standalone
authority covenant, executed three times. The composed transaction waits on
its counterparty, not its design — it composes against the draft merged on
Aug 20, and the reference implementation includes the conformant test token
it will run against.

The authorize/execute pattern

Composed shapes exceeding caps have already come up in the spec thread, and any
single-transaction composition is bounded by the per-dimension block limits.
The general answer is an authorize/execute pair, which makes fit
independent of the token’s leader footprint. The pattern is not specific to
proof-gated authorities: any owner-covenant whose own validation is too
heavy to share a transaction with its token’s leader can settle this way.

The authority covenant becomes a two-transition state machine — funded →
settled → executed — each transition single-path and fully determined by
its inputs. The authorize transaction carries the authority’s full
validation (here, the proof) and transitions the covenant into a settled
state committing Blake2b(R_next) (32 bytes) with no token leg. The
execute transaction spends the settled covenant together with the token
family. The settled state’s only spend path is a state-equality check
requiring the family continuation output at the pinned scriptPubKey, so the
successor imposition survives the split intact and 0x04 ownership rides the
covenant’s lineage across the intermediate transition. The execute step is
permissionless: the authorize step fixed exactly one outcome, so the execute
transaction is fully determined except for the executor’s fee input. Any
party can construct and broadcast it; competing broadcasts differ only in
who pays the fee — the settlement outputs are identical by construction. The authorize transaction contains no token bytes and the
execute transaction contains no authority-validation bytes, so neither
shares a block budget with the other and fit is unconditional. Rule 1
(below) makes the split safe: the covenant-owned token UTXO cannot be
churned between the two steps.

Single-transaction composition stays the default when the combined footprint
fits; the pattern is the guarantee that heavy authorities and heavy tokens
still compose. Deposit shape, the settled state’s deadline and refund, and
execute-fee economics are specified in the reference implementation.

Composition rules the reference implementation encodes

  1. borrow_scheme = 0x00 on covenant-owned states, imposed at deposit and
    preserved at settlement. The outpoint-churn discussion in the spec thread
    applies with force when the owner is a covenant whose claim binds
    outpoints; a covenant-owned balance wants no inbound borrows at all, and
    the recent hash-chain hardening doesn’t change that case. Cost: zero —
    the 0x04 owner writes next_states anyway.
  2. F’s configuration must list 0x04 (0x03 only for the stateless one-shot
    case below). KCC-20 requires transfer to
    validate every next_states[i].owner_scheme against the config, so a
    token without 0x04 can’t receive a covenant-owned state in the first
    place. Check the program artifact; don’t assume the default.
  3. Asset identity is the pair (covenant family, extension_commitment) —
    §4 partitions fungibility on the commitment, so the deposit leg pins both
    and rejects mismatches.

0x03 vs 0x04: 0x03 commits to exact redeem bytes, and KCC-1 §7 makes any
embedded state byte change Blake2b(R), so a stateful authority breaks its
own 0x03 ownership on every transition. 0x04 survives state evolution. This
implementation uses 0x04; 0x03 fits only stateless one-shot authorities.

Composed mass: budget and measurements

Block mass limits are per-dimension consensus parameters — storage and
compute at 500,000 g each, transient at 1,000,000 g post-Toccata
(BlockMassLimits::with_shared_limit(500_000) +
new_transient_mass_limit = 1_000_000, params.rs, identical across
networks). The standalone settlement measures 474,095 g, byte-invariant
across all three executions. Its ~223 KB signature-script byte load sits
well inside the transient budget; even in the worst case — the full
474,095 g charged to one dimension — 25,905 g remains for a token leg in a
single transaction.

Two numbers land with the reference implementation and decide the default
lane per asset: the measured dimension decomposition of the composed
transaction, and the target token’s leader footprint against that budget.
The latter makes a token’s leader compute cost a composability property of
the token itself; the byte-cap measurements posted in the spec thread already ran
into this from the other direction. A further lever sits in the
precompile’s other native tag: a Groth16-wrapped receipt prices at 140,000
script-units with a seal of a few hundred bytes, trading proving-side wrap
cost for on-chain headroom.

Contribution

A proof-gated authority profile, condition-agnostic by design: any
application pins its own image_id and inherits the same covenant shape,
composition rules, and mass characteristics. Concretely: a companion doc in
the kcc-0002/reference-code.md shape (Silverscript minimum path, Argent
observes variant for token-side deep coupling), KCC-1 §11-style conformance
vectors (dispatch tags, the 112-byte state encoding, template hashes for the
composed continuation), and TN10-executed vectors — including the
forged/honest same-txid pairs above — once the composed shape runs.

1 Like

SilverScript v1-rc1 shipped four days ago, so I tested whether it can carry
the proof-gated pattern this thread is built on. Short answer: yes, at
production scale, with numbers.

Method: I re-authored my settlement gate in SilverScript — the same shape
consensus-settled three times in raw script (mass 474,095 byte-exact each
time). Compiled at the rc1 tag, deployed to TN10, spent with a real
R0Succinct receipt (seal 222,668 B). Two contracts: a minimal verify+payout
gate (17 lines, 197 B bytecode) and a replay-bound variant that derives the
journal digest on-stack and requires the journal’s funding-txid commitment
to equal the outpoint being spent (21 lines, 225 B).

Three consensus settlements resulted:

Contract Mass computeBudget Txid
Minimal gate 478,167 2,544 3b32c7bdcf22776c78bae81e4fd40fcfedeab994b94800997c60ddffc2cfde50
Replay-bound 478,311 2,544 94ad2ff39603412127938b57528da04dfbf78ca24276c2e1006f033f49759f71
Minimal, toolchain-encoded sig script 478,167 2,544 e409473c97d470d0b87ac909cb24e923bad707f3d704127a89d6fca2301764a3

Against the hand-built baseline: +0.86% mass, +1.76% compute. Nobody needs
raw script for this pattern anymore; the language costs almost nothing at
receipt scale.

The replay-bound variant is the part relevant to covenant ownership.
Against a live gate I submitted a cryptographically valid receipt whose
journal bound a different funding txid. Rejected:
script ran, but verification failed — the covenant’s own require, not the
proof layer. A perfect proof aimed at the wrong instance is refused by 28
bytes of compiled introspection. A tampered journal fails earlier and
differently (ZK Integrity: Verification failed). Both layers, distinct
errors, on-chain. That is the instance-binding any proof-gated ownership
profile needs, expressible in the language today.

Determinism held everywhere it matters for a spec: compilation is
bit-reproducible, the script-unit meter is payload-invariant (used =
25,446,424 across two different receipts; 2,543 rejects, 2,544 accepts),
and the +144 mass of the bound variant tracks its +144 sig-script bytes
exactly.

Four issues filed from the testing, all tooling/docs — the compiled path
itself held up:
README does not document the silverc compiler CLI · Issue #240 · kaspanet/silverscript · GitHub (README/CLI docs) ·
constructor-args JSON schema is undocumented; only shipped example is a single int · Issue #241 · kaspanet/silverscript · GitHub (constructor-args
schema) · Argument-count and argument-length mismatches are reported as "unsupported feature" · Issue #242 · kaspanet/silverscript · GitHub (error
classification) · Compiled artifact carries no compute-budget estimate; required commitment is discoverable only by node rejection · Issue #243 · kaspanet/silverscript · GitHub
(compute-budget estimation).

Everything above is verifiable from the chain via api-tn10.kaspa.org.
Fun to PR both contracts/fixtures as in-tree examples.
Will explore shape and location next.

Follow-up with mainnet numbers.

SilverScript v1.0.0 tagged (3ed9733). I recompiled both contracts at the tag
against the rc1 build: no change on the ABI surface; the generic bytecode is
bit-identical to the rc1 pins above. Only the constructor moved, and only the
payee argument, so the deployed replay-bound contract is the same 225 bytes
with a different 32-byte key at offset 170.

Then I settled it on mainnet. Same receipt shape as the TN10 rows (seal
222,668 B, 147-byte journal committing to the funding txid), same
computeBudget, same fee constant, new gate:

Step Txid Accepting block Mass
Fund gate (1.0 KAS) 3f6dd6cdfc0f058d40e080ba927ea8d92e9cfc967e19ddf4b8fd81a41af2160d 588680cd4929b055a7e473bd4ce227353489c230bfff108ee34ea5fa2f210d11 2,047
Claim, replay-bound, CB 2,544 fffc63594947e445c5b7c151fc397b868fa7183556ea20e09e49515e60d0564a b82a409f650bcb47411c58a2baf648a5d5a6f16bb8bd9f1c67daace417553e68 478,311

Mass 478,311 on mainnet, 478,311 on TN10 for the same contract: the meter
did not move between networks. Signature script 223,351 bytes both places.
Accepted about two seconds after submission.

The fee constant is still the 0.6 KAS the TN10 contracts bake. That is
~125× the mass floor for this transaction; I kept it because changing it
changes the generic bytecode and I wanted the mainnet row to reuse the
rc1-verified artifact unchanged. A real deployment would set it near the
floor.

Verifiable from api.kaspa.org by txid.