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:
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:
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:
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:
| Prefix | Suffix | Template hash |
|---|---|---|
| empty | empty | e572dff82304700b856a555ac3a4558d0df3646a3727816500270a93c66aac1e |
61 | 6263 | 405e183e2494cdbe2df89349cc0ffa5b77fb885ad97a1d5660ecd0692ef8142a |
6162 | 63 | a0968c014f3fc7bd1a7d9a8d1ad1177eb379bd2f05e56309eb4e20347c5e7eba |
00ff | 100080 | 6616a66757315de0221cb2acba729113cebde31f8d3ca7fa93878a0584b96905 |
Compiler divergence. KCC1 defines
Hashas 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) 39c2e5e2ff735bc8ec6a8c5c1d070026b760e68495e7bd16b6824119edeec282Both 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:
blake2b(R) = e9c4ef0278b3d9f5d6d234ae89e2e38f30462fb9b9eeb32ed7caa76625552706
blake2b(R_next) = c4497fd43fe47ddf0a83fb58130ad999ce587f0e00a52a5b380c3f51fa6fd511 The running program computes blake2b(prefix || new_state || suffix) itself and compares it against the output's script public key.
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.