A Kaspa transaction spends existing UTXOs and creates new UTXOs.
An output locks value with a scriptPublicKey. An input spends a previous
output by referencing its outpoint and providing a signature_script.
outpoint = transaction_id || output_index
For a standard Schnorr pay-to-public-key output, the locking script is:
scriptPublicKey.version = 0
scriptPublicKey.script = OP_DATA_32 <32-byte public key> OP_CHECKSIG
When a transaction spends this output:
input.signature_script = <signature>
The input is valid when this execution flow ends with exactly one truthy stack item:
stack = []
execute input.signature_script:
push <signature>
stack = [signature]
execute previous_output.scriptPublicKey:
push <32-byte public key>
stack = [signature, public_key]
OP_CHECKSIG:
pop public_key
pop signature
verify signature against this transaction input
push true | false
final check:
require stack == [true]
In P2PK, the previous output's scriptPublicKey contains the full script to
execute: <public key> OP_CHECKSIG.
Pay-to-script-hash splits that model in two:
- the output contains a small hash-check script;
- the spending input reveals the real script, called the
redeem_script.
The redeem_script is ordinary script bytecode. It may be a multisig script,
SilverScript bytecode, or another script program. It is not stored in the
output; only its 32-byte BLAKE2b hash is:
scriptPublicKey.version = 0
scriptPublicKey.script = OP_BLAKE2B OP_DATA_32 <32-byte blake2b(redeem_script)> OP_EQUAL
When a P2SH output is spent, the input provides the stack inputs for that redeem script, then pushes the redeem script itself as the final stack item:
input.signature_script =
<redeem-script input pushes>
<data-push prefix for redeem_script>
<redeem_script bytes>
For P2PK, the stack input was just <signature>. For P2SH, the stack inputs
depend on the redeem script: signatures for a multisig script, or entrypoint
arguments and a selector for a SilverScript contract.
The redeem script must be the last stack item produced by the
signature_script. The data-push prefix is just the script encoding that says
"push the next N bytes as one data item." It is needed because the redeem script
bytes are data at this point, not opcodes to execute yet.
For example, if the redeem script is 120 bytes:
redeem_script = <120 bytes of compiled script>
then the signature_script must contain:
OpPushData1 120 <120 redeem-script bytes>
Execution has two phases:
- The P2SH output script hashes the final stack item produced by the input's
signature_script. This item must be the redeem script bytes. The hash must equal the hash committed in the output. - If that hash check succeeds, the engine restores the stack from before the P2SH hash check, pops the last item as the redeem script, and executes that script against the remaining redeem-script inputs.
SilverScript source compiles into a script template. For a deployed contract
instance, the P2SH redeem_script is the concrete script for that instance.
For stateless contracts, this is just the compiled script logic:
redeem_script = compiled_script
For stateful contracts, the redeem script also contains the current encoded state:
redeem_script = template_prefix || encoded_state || template_suffix
For SilverScript, the redeem-script inputs are the entrypoint argument pushes and, for multi-entrypoint contracts, the selector push. A complete P2SH spend has:
signature_script =
<entrypoint argument pushes>
[<entrypoint selector push>]
<data-push prefix for redeem_script>
<redeem_script bytes>
Single-entrypoint contracts do not need a selector. Multi-entrypoint contracts use a selector so the same redeem script can dispatch to one of several entrypoints.
A SilverScript contract can be stateless or stateful.
Stateless means the redeem script contains no compiler-managed state segment.
It can still validate signatures, inspect the spending transaction, enforce
output rules, and be deployed as P2SH. It just has no State value.
Stateful means the contract has compiler-managed fields. The redeem script is still just compiled script bytecode, but the compiler records where the encoded state lives inside that bytecode:
state_start
state_len
For a stateful contract:
encoded_state = redeem_script[state_start .. state_start + state_len]
state_len > 0
When a verifier removes that state range, the surrounding bytes are:
template_prefix = redeem_script[..state_start]
template_suffix = redeem_script[state_start + state_len..]
For a stateless contract:
state_len = 0
The state segment is a concatenation of field chunks in lowered contract field order:
encoded_state =
chunk(field_0) ||
chunk(field_1) ||
...
chunk(field_n)
chunk(field_i) =
pushdata_prefix(payload_len_i) || fixed_payload_i
Common payload sizes are:
| SilverScript type | Payload size |
|---|---|
int |
8 bytes, little-endian |
bool |
1 byte |
byte |
1 byte |
pubkey |
32 bytes |
sig |
65 bytes |
datasig |
64 bytes |
byte[N] |
N bytes |
int[N] |
N * 8 bytes |
The SilverScript template is the redeem script with the state segment removed:
template_prefix = redeem_script[..state_start]
template_suffix = redeem_script[state_start + state_len..]
template_hash = blake2b(template_prefix || template_suffix)
For a stateless contract:
template_prefix || template_suffix = redeem_script
template_hash = blake2b(redeem_script)
The template lets a verifier distinguish "same contract logic with different state" from "different contract logic." A state transition changes the P2SH hash because the redeem script changes, but the template hash remains the same when only the state segment changes.
The current compiler emits these relevant shapes:
single-entrypoint, stateful:
redeem_script = encoded_state || entrypoint_body
state_start = 0
single-entrypoint, stateless:
redeem_script = entrypoint_body
state_start = 0
state_len = 0
multi-entrypoint, stateful:
redeem_script =
OP_TOALTSTACK ||
encoded_state ||
OP_FROMALTSTACK ||
selector_dispatch
state_start = 1
multi-entrypoint, stateless:
redeem_script = selector_dispatch
state_start = 0
state_len = 0
In the multi-entrypoint stateful form, OP_TOALTSTACK preserves the selector
while the encoded state fields are pushed. OP_FROMALTSTACK restores the
selector for the dispatch logic.
SilverScript covenant code uses transaction introspection to constrain the outputs of the spending transaction.
The core state-routing builtins are:
validateOutputState(output_index, new_state)
validateOutputStateWithTemplate(
output_index,
new_state,
template_prefix,
template_suffix,
expected_template_hash
)
readInputState(input_index)
readInputStateWithTemplate(
input_index,
template_prefix_len,
template_suffix_len,
expected_template_hash
)
validateOutputState(...) validates a continuation into the same template. The
compiler encodes new_state, rebuilds:
template_prefix || encoded(new_state) || template_suffix
and checks that the selected output has the matching P2SH script public key.
validateOutputStateWithTemplate(...) does the same for a foreign template, but
only after checking that the supplied prefix and suffix match the expected
template hash.
readInputState(...) and readInputStateWithTemplate(...) decode state from a
P2SH input's revealed redeem script. This is why the P2SH layout matters: the
previous state's bytes are not visible in the previous output, but they are
revealed when that output is spent.
SilverScript and covenant binding are related, but not identical.
A SilverScript contract compiles to a P2SH redeem script:
scriptPublicKey = P2SH(blake2b(redeem_script))
That P2SH hash proves which script must be revealed and executed. A covenant
binding is separate consensus metadata on the UTXO: an optional covenant_id
that identifies the covenant lineage.
So there are two different commitments:
- the P2SH hash says "this is the script for this output";
- the covenant ID says "this output belongs to this covenant lineage".
This distinction matters for stateful SilverScript. The current state is encoded inside the redeem script:
redeem_script = template_prefix || encoded_state || template_suffix
If the state changes, the redeem script changes, and therefore the P2SH hash may change too. The covenant ID is the stable lineage identifier across those state-specific script hashes.
validateOutputState(...) checks the script/state side. It rebuilds the expected
next redeem script from the template plus new_state, then checks that the
selected output has the matching P2SH script public key.
It does not, by itself, select a covenant-bound continuation output. For that, the contract should use covenant introspection. In a simple one-input continuation:
byte[32] cov_id = OpInputCovenantId(this.activeInputIndex);
require(OpAuthOutputCount(this.activeInputIndex) == 1);
int next_idx = OpAuthOutputIdx(this.activeInputIndex, 0);
require(OpOutputCovenantId(next_idx) == cov_id);
validateOutputState(next_idx, new_state);The important idea is that next_idx comes from the covenant authorization
context, not from a hardcoded transaction output index.
For larger N:M flows, scripts usually operate over the shared covenant group
instead, using OpCovInputCount, OpCovOutputCount, OpCovInputIdx, and
OpCovOutputIdx for the active covenant_id.
SilverScript covenant declarations generate this plumbing for common patterns (see all supported patterns here):
#[covenant.singleton(mode = transition)]
function step(State prev_state, int delta) : (State) {
return({ value: prev_state.value + delta });
}This kind of declaration lowers to code that selects the authorized continuation output and validates its next state.
In short: P2SH identifies the script for a particular state; the covenant ID identifies the lineage that state belongs to. A complete covenant verifier usually needs both.
For a P2PK payment, the output reveals the whole spending rule. For a P2SH SilverScript covenant, a live output reveals only:
P2SH(blake2b(redeem_script))
It does not reveal:
- the redeem script;
- the embedded state, if the contract is stateful;
- the entrypoint used to create the output;
- the arguments used in the transition.
The redeem script and previous state become visible only when a covenant output is spent. A verifier that wants to know current state must therefore:
- verify the published source and compiler identity;
- verify the genesis transaction and covenant ID;
- replay every accepted transition from genesis;
- reconstruct each expected next redeem script and P2SH output;
- verify the final replay outpoints are still unspent.
Replay proves the state after the submitted history. A live UTXO check is still needed to claim that state is current.
- A SilverScript output is P2SH; when used as a covenant, it is P2SH plus a covenant binding.
- The P2SH output commits to a redeem script hash, not to readable state.
- The redeem script is revealed in the spending input.
- Stateful SilverScript stores state inside the redeem script.
- A state transition replaces only the encoded state segment when continuing in the same template.
- The verifier reconstructs expected redeem scripts from source, prior state, transaction data, and supported SilverScript state-routing patterns.
- Current state requires both accepted transition replay and a final live UTXO check.