Kaspalytics Learn is a work in progress.
Updated: Aug 2, 2026

State, Templates & Continuations

How KCC1 carves mutable state out of a covenant program — the state range, template prefix and suffix, template hashes, and how a continuation program is reconstructed byte-for-byte.

This is the page where covenants stop being “scripts with extra opcodes” and start being state machines. Everything here follows from one fact established on the envelope page: the program R contains its own state, and its address is Blake2b(R).

The cut

Covenant state is the ordered values a program carries, encoded into a contiguous range of its own bytecode. The Program ABI describes where that range is with two non-negative integers:

state.start
state.len

Everything else is the template — an ordered prefix and suffix:

template.prefix = R[0 : state.start]
encoded_state   = R[state.start : state.start + state.len]
template.suffix = R[state.start + state.len :]

R = template.prefix || encoded_state || template.suffix

A stateless program uses state.start = 0 and state.len = 0, so its template is ("", R) — everything is template, nothing is mutable.

Here is a real 58-byte program cut into its three parts:

R — counter covenant, state.start = 0, state.len = 12 58 bytes
prefix (empty) @0 · 0 bytes
encoded_state 08070000000000000002aabb @0 · 12 bytes
template.suffix 01085279519358cd7e010252797e7eb976c97601ae937cbc7eaa02000001aa7e01207e7c7e01877e00c388757551 @12 · 46 bytes
This contract has a single entrypoint, so nothing needs to precede the state and the prefix is empty.

The prefix is empty here only because there is one entrypoint. Add a second and the compiler needs one byte of dispatch-preservation bytecode in front, pushing state.start to 1 — see Entrypoints & Dispatch. The state contents are unchanged; only its offset moves.

State encoding

State fields are recursively lowered in declaration order, and each resulting field is encoded as:

encoded_field = PushExplicit(StatePayload(field_type, field_value))

The encoded state is those encodings concatenated. Critically, it includes the push opcodes — it is executable bytecode, not a serialization blob parked inside the script. When the program runs, those pushes are what put the state on the stack.

Here is the state of a real KCC20 token covenant, field by field:

KCC20 encoded_state — 46 bytes 46 bytes
OP_DATA_32 20 @0
ownerIdentifier 1111111111111111111111111111111111111111111111111111111111111111 @1 · byte[32]
OP_DATA_1 01 @33
identifierType 00 @34 · byte — 0x00 = pubkey
OP_DATA_8 08 @35
amount e803000000000000 @36 · int — 1000, LE
OP_DATA_1 01 @44
isMinter 00 @45 · bool — false
Real output from the SilverScript compiler. Every field is PushExplicit, so each type has a fixed encoded width.

Notice identifierType and isMinter: both are 01 00, and only the ABI distinguishes a byte from a bool. This is the same point as in The Value ABI — the bytes are not self-describing.

Decoder obligations

A state decoder must consume exactly one push per lowered field, and the consumed bytes must exactly equal PushExplicit(payload). It must reject:

  • malformed pushes
  • invalid payloads
  • missing fields
  • trailing bytes

That last one matters more than it looks. Being strict about trailing bytes is what stops a crafted program from carrying a state region that decodes “successfully” while hiding extra data the covenant never accounted for.

Reading a consumed instance’s state

An unspent P2SH output exposes only Blake2b(R). When it is consumed, R becomes the final data element of that input’s signature script, so state is read as:

R[state.start : state.start + state.len]

The required order is: validate the P2SH invocation structure and the range bounds first, then decode. Reading at an offset without checking that the bytes really are the redeem script of that input — and really are within bounds — is how covenants get tricked into reading attacker-chosen data as their own state.

Template hashes

A template needs a stable identifier so a covenant can prove “my successor uses the same logic I do”. That identifier is:

TemplateHash(prefix, suffix) =
    Hash(LE64(len(prefix)) || prefix || LE64(len(suffix)) || suffix)

The two length fields are not padding. They bind the boundary between prefix and suffix. Without them, different cuts would collide:

prefix 61 + suffix 6263 616263 hash 405e183e…
prefix 6162 + suffix 63 616263 hash a0968c01…
Why the lengths are there: identical concatenations, different cuts. With length prefixes the hashes differ; with a naive Hash(prefix || suffix) they would be the same.

KCC1 states this as a requirement: implementations must not replace the construction with Hash(prefix || suffix). And a claimed template hash must be recomputed from both parts before those parts are used to rebuild a program.

Conformance vectors:

PrefixSuffixTemplate hash
emptyemptye572dff82304700b856a555ac3a4558d0df3646a3727816500270a93c66aac1e
616263405e183e2494cdbe2df89349cc0ffa5b77fb885ad97a1d5660ecd0692ef8142a
616263a0968c014f3fc7bd1a7d9a8d1ad1177eb379bd2f05e56309eb4e20347c5e7eba
00ff1000806616a66757315de0221cb2acba729113cebde31f8d3ca7fa93878a0584b96905

Compiler divergence. KCC1 defines Hash as BLAKE3, so template hashes are BLAKE3. The current SilverScript compiler computes template hashes with BLAKE2b over the same length-prefixed construction. For our counter template the two disagree:

BLAKE3  (KCC1)          b966c1438f5a487eceb9caf910c0d477b75f11f245f396187f271ffc7da34e42
BLAKE2b (SilverScript)  39c2e5e2ff735bc8ec6a8c5c1d070026b760e68495e7bd16b6824119edeec282

Both documents are Draft. The construction — length-prefixed, boundary-binding — is the same in both; only the hash function differs.

Building a continuation

Now everything composes. For a continuation under the same template:

encoded_next_state = EncodeState(next_state)
R_next = template.prefix || encoded_next_state || template.suffix

and the expected continuation script public key is the version-0 P2SH script committing to Blake2b(R_next).

This is verifiable rather than theoretical. Recompiling the identical counter source with count = 8 instead of 7 produces a program that is byte-for-byte what the splice predicts:

R (count = 7) 58 bytes
encoded_state 08070000000000000002aabb
template.suffix 0108527951 … +36 … c388757551
R_next (count = 8) 58 bytes
encoded_next_state 08080000000000000002aabb one byte differs
template.suffix — identical 0108527951 … +36 … c388757551
blake2b(R)      = e9c4ef0278b3d9f5d6d234ae89e2e38f30462fb9b9eeb32ed7caa76625552706
blake2b(R_next) = c4497fd43fe47ddf0a83fb58130ad999ce587f0e00a52a5b380c3f51fa6fd511
Inputs
Consumed instance count = 7
P2SH → e9c4ef…2706
signature_script reveals R
transition
Outputs
Continuation count = 8
P2SH → c4497f…d511
script requires exactly this spk

The running program computes blake2b(prefix || new_state || suffix) itself and compares it against the output's script public key.

A same-template continuation. Same logic, advanced state, new address.

Authenticating the template

Everything above assumes the template bytes are trustworthy. They are witness data, so by default they are not.

A covenant must authenticate the template used to construct every continuation. Supplied prefix and suffix bytes are not authenticated unless verified against the expected template hash.

The two cases:

  • Same-template continuation — the template must match the consumed covenant’s own authenticated template. In practice the program already has these bytes: they are its own, and the P2SH commitment it just satisfied vouches for them.
  • Different-template continuation — the protocol must define how the target template is authorized. Common approaches are pinning an expected template hash in state, or reading the template from a co-spent input’s redeem script and checking it against a pinned hash.

Skipping this check is the classic covenant vulnerability. A covenant that splices caller-supplied prefix and suffix bytes without verifying them will happily compute the address of a program that does whatever the caller wants — and then enforce that the funds go there.

SilverScript surfaces the distinction directly in its builtins: validateOutputState for same-template continuations, and validateOutputStateWithTemplate / validateOutputStateWithInputTemplate for foreign templates, both of which require an expectedTemplateHash argument.

Next

Template Views & Virtual Elements — how to let part of the state change while pinning the rest, and how to commit to values too large to embed.

References

Kaspalytics strives to provide accurate data - our highest level of effort is given to data validation and maintenance. However, we cannot guarantee 100% accuracy. Data is subject to inaccuracies and change.

Contact | © 2026 Kaspalytics