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:
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:
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:
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 the 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
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:
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 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.