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

The Value ABI

How KCC1 turns typed values into script bytes — canonical type names, the two push encodings, integer gotchas, array and record lowering, and value locations.

Kaspa script has no types. Everything on the stack is a byte string. KCC1’s Value ABI is the agreement that says this byte string is an int, that one is a pubkey, and here is exactly how each becomes bytes.

This is the alphabet the rest of the series is written in — state layouts, dispatch tags, and token standards are all built from these rules.

Program ABI

The Program ABI is a covenant’s public interface: its entrypoints, argument types, records, state layout, and template views. KCC1 deliberately does not prescribe a file format for it. It can be JSON, it can be hand-written on a napkin. What KCC1 fixes is the meaning of what it declares.

That matters because of the previous page: an unspent covenant reveals only a hash. Without an ABI published somewhere out of band, the bytes revealed at spend time are unintelligible.

Canonical type names

Every type has exactly one spelling. These strings are load-bearing — they feed directly into dispatch tags, so a typo changes which entrypoint you are calling.

TypeCanonical name
Integerint
Booleanbool
Bytebyte
UTF-8 stringstring
32-byte public keypubkey
65-byte transaction signaturesig
64-byte data signaturedatasig
Fixed byte string of length Nbyte[N]
Fixed array of T, length NTypeName(T)[N]
Dynamic array of TTypeName(T)[]
Recordits exact, case-sensitive record name

N is base ten, no sign, no leading zeros. Identifiers must match [A-Za-z_][A-Za-z0-9_]*.

One trap: a variable-length byte string is byte[], and it must be spelled that way even in a dispatch-tag preimage. It is not bytes and not byte[0].

Two push encodings

KCC1 uses two encodings that differ only for one-byte payloads:

  • PushMinimal — used for invocation arguments. Takes advantage of the small-integer opcodes.
  • PushExplicit — used for encoded state. Always uses a length-prefixed push.
ConditionPushMinimal
n = 0OP_0
n = 1 and b = 0110OP_1OP_16
n = 1 and b = 81OP_1NEGATE
1 ≤ n ≤ 75, otherwiseOP_DATA_n ‖ b
76 ≤ n ≤ 255OP_PUSHDATA1 ‖ n ‖ b
256 ≤ n ≤ 65535OP_PUSHDATA2 ‖ LE16(n) ‖ b
65536 ≤ n ≤ 2³²−1OP_PUSHDATA4 ‖ LE32(n) ‖ b

PushExplicit uses OP_0 for the empty payload and the length-based forms for everything else — it never emits OP_1OP_16 or OP_1NEGATE.

PushMinimal(01) 51 OP_1 — one byte total
PushExplicit(01) 0101 OP_DATA_1 ‖ 01 — two bytes
The same one-byte payload, encoded both ways. Why the split exists: state must be a fixed-width, mechanically rewritable region, so it cannot use variable-length shortcuts.

This is not arbitrary fussiness. State bytes get spliced by position, so a value whose encoded width changes with its content would shift every field after it. PushExplicit guarantees a type’s encoded width depends only on its type. This is the foundation of templates.

Integers

The KCC1 int range is −(2⁶³−1) to 2⁶³−1, and an integer is encoded two different ways depending on where it appears:

PositionEncoding
Invocation argumentPushMinimal over the minimal ScriptNum representation
State payloadEight-byte little-endian, signed-magnitude
int 17 as an argument 0111 OP_DATA_1 ‖ 11 — minimal ScriptNum
int 17 as state 081100000000000000 OP_DATA_8 ‖ 8-byte LE
The same value in both positions.

The signed-magnitude gotcha

Kaspa script numbers are sign-magnitude, not two’s complement. The magnitude goes in the bytes and the sign is a flag in the high bit of the most significant byte.

int -5 as a KCC1 state payload : 0500000000000080
the same value in two's complement : fbffffffffffffff

Positive values are identical under both schemes, so this bug hides until the first negative number reaches production. If you are writing a codec, encode the absolute value and set the top bit of the last byte — do not reach for your language’s standard signed-integer packing.

Scalar payloads

TypePayload
inteight-byte ScriptNum; minimal ScriptNum as a standalone argument
boolexactly 00 (false) or 01 (true)
bytethe byte value
stringUTF-8 bytes, no terminator
pubkeyexactly 32 bytes
sigexactly 65 bytes
datasigexactly 64 bytes
byte[N]exactly N bytes

A standalone bool argument uses OP_0 or OP_1.

Arrays

The payload of an array is simply its elements’ fixed payloads concatenated in index order. Two consequences:

  • A dynamic array T[] is only allowed when T has a fixed, non-zero width. Its length is inferred as len(payload) / width(T), and a payload that is not a whole multiple of the width is invalid.
  • Inside an array, an int uses its eight-byte fixed payload — never the minimal form. The minimal form only applies to a standalone int argument.

An array is one stack element, not one element per item.

Records

A record is an ordered list of named fields. Field order is part of the ABI — reordering fields is a breaking change even if the names are unchanged.

A record argument is recursively expanded into its fields’ encodings, in declaration order. It gets no wrapper push of its own. So a three-field record becomes three stack elements.

Arrays of records are transposed

This is the least intuitive rule in KCC1, and it shows up immediately in real standards like KCC20.

An array of records is not encoded record-by-record. It is grouped field-by-field:

for records r_0 … r_(n-1) with fields f_0 … f_(m-1):

  element 0:  r_0.f_0, r_1.f_0, …, r_(n-1).f_0
  element 1:  r_0.f_1, r_1.f_1, …, r_(n-1).f_1
  …
  element m-1: r_0.f_(m-1), …, r_(n-1).f_(m-1)

You get one stack element per field, each holding that field’s value from every record. If you know the “array of structs → struct of arrays” transposition, it is exactly that.

Worked example — two records of type Entry { byte[2] tag; int amount; } with values (0xaabb, 400) and (0xccdd, 600):

Entry[] lowered to two stack elements 22 bytes
element 0 — all tags 04aabbccdd OP_DATA_4 ‖ aabb ‖ ccdd
element 1 — all amounts 1090010000000000005802000000000000 OP_DATA_16 ‖ 400 ‖ 600, each 8 bytes LE
Not (tag,amount),(tag,amount) — but (all tags),(all amounts). Lengths must agree: 4/2 = 2 records, 16/8 = 2 records.

The rules that keep this decodable:

  • Every grouped field payload must decode to the same element count. Above, both give 2.
  • An array of records is permitted only when every recursively lowered leaf field has a positive fixed width.
  • Nested records lower by the same rule, recursively.

That width restriction applies only to arrays of records. A record outside an array may contain variable-width fields, since recursive lowering gives each leaf its own push.

Argument sequence

PushArguments(arguments) is the concatenation, in parameter order, of the canonical pushes for all recursively lowered arguments. It may be zero, one, or many push instructions — there is no count prefix and no separator. Decoding depends entirely on knowing the entrypoint’s declared types.

Conformance vector

For the entrypoint step(int,byte[4],bool,byte) called with (17, 01020304, true, 01):

Complete signature-script argument section 14 bytes
int 17 0111
byte[4] 0401020304
bool true 51 OP_1
byte 01 51 also OP_1
dispatch tag 042c49ed65 OP_DATA_4 ‖ tag
KCC1 §11.1. Note that bool true and byte 0x01 produce identical bytes — types live in the ABI, never in the encoding.

That last point deserves emphasis: the bytes are not self-describing. 51 is OP_1, and only the ABI says whether it means true, the byte 0x01, or the integer 1. Decode against the wrong signature and you will get plausible-looking nonsense rather than an error.

Value locations

A value location names a logical value inside a decoded covenant, so other specs can point at one without re-describing it. It is a root plus a path:

ValueLocation = (root, path)
root = named(name) | argument(entrypoint, index)
  • A named root refers to a value the Program ABI exposes by name. The complete covenant state is the named root state.
  • An argument root selects one argument of one exact entrypoint by zero-based index. The index counts declared logical arguments, not the stack elements produced by record lowering.

The path is a sequence of field segments (select a record field) and index segments (select an array element), evaluated after decoding. Paths refer to the logical value tree — never to byte offsets or push positions.

root = state
path = field("example_state_values"), index(0)

shorthand: state.example_state_values[0]

Value locations are semantic ABI information. They never appear in covenant bytecode. KCC2 uses them to point at whichever value identifies a covenant’s controller.

Semantic annotations

An ABI may attach a meaning to a value without changing its type. Public key hashes, program hashes, and Covenant IDs are all byte[32] — annotations record which is which.

An annotation must not change the canonical type name, the encoding or decoding, record lowering, an entrypoint’s signature, or its dispatch tag. It is documentation with teeth, not a new type.

Next

With the encoding rules settled, Entrypoints & Dispatch covers how a program exposes more than one callable branch and how the caller selects between them.

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