Kaspalytics Learn is under (heavy) construction. Check back regularly!
Updated: Aug 14, 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 page describes how a covenant functions as a state machine. 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.

A real 58-byte program cut into its three parts:

R - counter covenant, state.start = 0, state.len = 12
58 bytes
prefix (empty) idx[0] · 0 bytes
encoded_state 08070000000000000002aabb idx[0] · 12 bytes
template.suffix 01085279519358cd7e010252797e7eb976c97601ae937cbc7eaa02000001aa7e01207e7c7e01877e00c388757551 idx[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. With a second entrypoint, the compiler needs one byte of dispatch-preservation bytecode in front, moving state.start to 1 - see Entrypoints & Dispatch. The state contents are unchanged; only the 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. Note that it includes the push opcodes - it is executable bytecode, not a serialization blob embedded in the script. When the program runs, those pushes are what put the state on the stack.

The state of a real KCC20 token covenant, field by field:

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

Note 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

The trailing-bytes rule prevents 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 are the redeem script of that input, and are within bounds, allows a covenant to be tricked into reading attacker-chosen data as its own state.

Template hashes

A template needs a stable identifier so a covenant can prove that its successor uses the same logic. 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:

6 bytes
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 the 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

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: 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 a common covenant vulnerability. A covenant that splices caller-supplied prefix and suffix bytes without verifying them computes the address of a program the caller controls - and then enforces 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