Chapter 5: Operations and Opcodes
PRE-ALPHA WARNING: This is a pre-alpha version of The Sigma Book. Content may be incomplete, inaccurate, or subject to change. Do not use as a source of truth. For authoritative information, consult the official repositories:
- sigmastate-interpreter — Reference Scala implementation
- sigma-rust — Rust implementation
- ergo — Ergo node
Prerequisites
- Understanding of bytecode as numeric instruction encodings
- Single-byte vs multi-byte encoding trade-offs
- Prior chapters: Chapter 4 for value node types, Chapter 2 for type codes that occupy the lower opcode range
Learning Objectives
By the end of this chapter, you will be able to:
- Explain the opcode encoding scheme and why constants share space with operations
- Navigate the complete opcode space (0x00-0xFF) and identify operation categories
- Describe the three cost descriptor types (
FixedCost,PerItemCost,TypeBasedCost) - Understand how short-circuit evaluation affects cost calculation
Opcode Encoding Scheme
Every ErgoTree operation is identified by a single-byte opcode12:
Opcode Space Layout:
┌────────────────────────────────────────────────────────┐
│ 0x00 │ Reserved (Undefined) │
├────────────┼───────────────────────────────────────────┤
│ 0x01-0x70 │ Constant type codes (optimized encoding) │
├────────────┼───────────────────────────────────────────┤
│ 0x71 │ Function type marker (LastConstantCode+1) │
├────────────┼───────────────────────────────────────────┤
│ 0x72-0xFF │ Operation codes (newOpCode 1-143) │
└────────────┴───────────────────────────────────────────┘
This layout is an optimization: constant values in the range 0x01-0x70 encode their type code directly as the opcode, saving one byte per constant in the serialized tree. The type code simultaneously identifies both what the value is and how to deserialize it. Operations occupy the upper range (0x72-0xFF), providing 143 distinct operation codes.
const OpCode = struct {
value: u8,
pub const FIRST_DATA_TYPE: u8 = 1;
pub const LAST_DATA_TYPE: u8 = 111;
pub const LAST_CONSTANT_CODE: u8 = 112; // LAST_DATA_TYPE + 1
pub fn new(shift: u8) OpCode {
return .{ .value = LAST_CONSTANT_CODE + shift };
}
pub fn isConstant(byte: u8) bool {
return byte >= FIRST_DATA_TYPE and byte <= LAST_CONSTANT_CODE;
}
};
Opcode Definitions
const OpCodes = struct {
// Variables (0x71-0x74)
pub const TaggedVariable = OpCode.new(1); // 113
pub const ValUse = OpCode.new(2); // 114
pub const ConstantPlaceholder = OpCode.new(3); // 115
pub const SubstConstants = OpCode.new(4); // 116
// Conversions (0x7A-0x7E)
pub const LongToByteArray = OpCode.new(10); // 122
pub const ByteArrayToBigInt = OpCode.new(11); // 123
pub const ByteArrayToLong = OpCode.new(12); // 124
pub const Downcast = OpCode.new(13); // 125
pub const Upcast = OpCode.new(14); // 126
// Literals (0x7F-0x86)
pub const True = OpCode.new(15); // 127
pub const False = OpCode.new(16); // 128
pub const UnitConstant = OpCode.new(17); // 129
pub const GroupGenerator = OpCode.new(18); // 130
pub const Coll = OpCode.new(19); // 131
pub const CollOfBoolConst = OpCode.new(21); // 133
pub const Tuple = OpCode.new(22); // 134
// Tuple access (0x87-0x8C)
pub const Select1 = OpCode.new(23); // 135
pub const Select2 = OpCode.new(24); // 136
pub const Select3 = OpCode.new(25); // 137
pub const Select4 = OpCode.new(26); // 138
pub const Select5 = OpCode.new(27); // 139
pub const SelectField = OpCode.new(28); // 140
// Relations (0x8F-0x98)
pub const Lt = OpCode.new(31); // 143
pub const Le = OpCode.new(32); // 144
pub const Gt = OpCode.new(33); // 145
pub const Ge = OpCode.new(34); // 146
pub const Eq = OpCode.new(35); // 147
pub const Neq = OpCode.new(36); // 148
pub const If = OpCode.new(37); // 149
pub const And = OpCode.new(38); // 150
pub const Or = OpCode.new(39); // 151
pub const AtLeast = OpCode.new(40); // 152
// Arithmetic (0x99-0xA2)
pub const Minus = OpCode.new(41); // 153
pub const Plus = OpCode.new(42); // 154
pub const Xor = OpCode.new(43); // 155
pub const Multiply = OpCode.new(44); // 156
pub const Division = OpCode.new(45); // 157
pub const Modulo = OpCode.new(46); // 158
pub const Exponentiate = OpCode.new(47); // 159
pub const MultiplyGroup = OpCode.new(48); // 160
pub const Min = OpCode.new(49); // 161
pub const Max = OpCode.new(50); // 162
// Context (0xA3-0xAC)
pub const Height = OpCode.new(51); // 163
pub const Inputs = OpCode.new(52); // 164
pub const Outputs = OpCode.new(53); // 165
pub const LastBlockUtxoRootHash = OpCode.new(54); // 166
pub const Self = OpCode.new(55); // 167
pub const MinerPubkey = OpCode.new(60); // 172
// Collections (0xAD-0xB8)
pub const Map = OpCode.new(61); // 173
pub const Exists = OpCode.new(62); // 174
pub const ForAll = OpCode.new(63); // 175
pub const Fold = OpCode.new(64); // 176
pub const SizeOf = OpCode.new(65); // 177
pub const ByIndex = OpCode.new(66); // 178
pub const Append = OpCode.new(67); // 179
pub const Slice = OpCode.new(68); // 180
pub const Filter = OpCode.new(69); // 181
pub const AvlTree = OpCode.new(70); // 182
pub const FlatMap = OpCode.new(72); // 184
// Box access (0xC1-0xC7)
pub const ExtractAmount = OpCode.new(81); // 193
pub const ExtractScriptBytes = OpCode.new(82); // 194
pub const ExtractBytes = OpCode.new(83); // 195
pub const ExtractBytesWithNoRef = OpCode.new(84); // 196
pub const ExtractId = OpCode.new(85); // 197
pub const ExtractRegisterAs = OpCode.new(86); // 198
pub const ExtractCreationInfo = OpCode.new(87); // 199
// Crypto (0xCB-0xD3)
pub const CalcBlake2b256 = OpCode.new(91); // 203
pub const CalcSha256 = OpCode.new(92); // 204
pub const ProveDlog = OpCode.new(93); // 205
pub const ProveDHTuple = OpCode.new(94); // 206
pub const SigmaPropBytes = OpCode.new(96); // 208
pub const BoolToSigmaProp = OpCode.new(97); // 209
pub const TrivialFalse = OpCode.new(98); // 210
pub const TrivialTrue = OpCode.new(99); // 211
// Blocks (0xD4-0xDD)
pub const DeserializeContext = OpCode.new(100); // 212
pub const DeserializeRegister = OpCode.new(101); // 213
pub const ValDef = OpCode.new(102); // 214
pub const FunDef = OpCode.new(103); // 215
pub const BlockValue = OpCode.new(104); // 216
pub const FuncValue = OpCode.new(105); // 217
pub const FuncApply = OpCode.new(106); // 218
pub const PropertyCall = OpCode.new(107); // 219
pub const MethodCall = OpCode.new(108); // 220
pub const Global = OpCode.new(109); // 221
// Options (0xDE-0xE6)
pub const SomeValue = OpCode.new(110); // 222
pub const NoneValue = OpCode.new(111); // 223
pub const GetVar = OpCode.new(115); // 227
pub const OptionGet = OpCode.new(116); // 228
pub const OptionGetOrElse = OpCode.new(117); // 229
pub const OptionIsDefined = OpCode.new(118); // 230
// Sigma props (0xEA-0xED)
pub const SigmaAnd = OpCode.new(122); // 234
pub const SigmaOr = OpCode.new(123); // 235
pub const BinOr = OpCode.new(124); // 236
pub const BinAnd = OpCode.new(125); // 237
// Bitwise (0xEE-0xFB)
pub const DecodePoint = OpCode.new(126); // 238
pub const LogicalNot = OpCode.new(127); // 239
pub const Negation = OpCode.new(128); // 240
pub const BitInversion = OpCode.new(129); // 241
pub const BitOr = OpCode.new(130); // 242
pub const BitAnd = OpCode.new(131); // 243
pub const BinXor = OpCode.new(132); // 244
pub const BitXor = OpCode.new(133); // 245
pub const BitShiftRight = OpCode.new(134); // 246
pub const BitShiftLeft = OpCode.new(135); // 247
pub const BitShiftRightZeroed = OpCode.new(136); // 248
// Special (0xFE-0xFF)
pub const Context = OpCode.new(142); // 254
pub const XorOf = OpCode.new(143); // 255
};
Opcode Categories Summary
| Category | Range | Count | Description |
|---|---|---|---|
| Variables | 113-116 | 4 | Variable references, placeholders |
| Conversions | 122-126 | 5 | Type conversions |
| Literals | 127-134 | 8 | Boolean, unit, collections |
| Tuple access | 135-140 | 6 | Field selection |
| Relations | 143-152 | 10 | Comparisons, conditionals |
| Arithmetic | 153-162 | 10 | Math operations |
| Context | 163-172 | 6 | Transaction context |
| Collections | 173-184 | 10 | Collection operations |
| Box access | 193-199 | 7 | Box property access |
| Crypto | 203-211 | 9 | Hashing, sigma props |
| Blocks | 212-221 | 10 | Definitions, lambdas |
| Options | 222-230 | 7 | Option operations |
| Sigma props | 234-237 | 4 | Sigma composition |
| Bitwise | 238-248 | 11 | Bit operations |
Arithmetic Operations
Arithmetic operations use type-based costing34:
const ArithOp = struct {
op_code: OpCode,
left: *const Value,
right: *const Value,
pub fn eval(self: *const ArithOp, env: *const DataEnv, E: *Evaluator) !Any {
const x = try self.left.eval(env, E);
const y = try self.right.eval(env, E);
const cost = switch (self.left.tpe) {
.big_int, .unsigned_big_int => 30,
else => 15,
};
E.addCost(FixedCost{ .value = cost }, self.op_code);
return switch (self.op_code.value) {
OpCodes.Plus.value => arithPlus(x, y, self.left.tpe),
OpCodes.Minus.value => arithMinus(x, y, self.left.tpe),
OpCodes.Multiply.value => arithMultiply(x, y, self.left.tpe),
OpCodes.Division.value => arithDivision(x, y, self.left.tpe),
OpCodes.Modulo.value => arithModulo(x, y, self.left.tpe),
OpCodes.Min.value => arithMin(x, y, self.left.tpe),
OpCodes.Max.value => arithMax(x, y, self.left.tpe),
else => error.UnknownOpcode,
};
}
};
fn arithPlus(x: Any, y: Any, tpe: SType) !Any {
// NOTE: ErgoTree arithmetic uses modular (wrapping) semantics for primitives.
// The +% operator in Zig performs wrapping addition, matching this behavior.
// In production, use @addWithOverflow for explicit overflow detection when
// the application requires overflow errors. See ZIGMA_STYLE.md.
return switch (tpe) {
.byte => .{ .byte = x.byte +% y.byte },
.short => .{ .short = x.short +% y.short },
.int => .{ .int = x.int +% y.int },
.long => .{ .long = x.long +% y.long },
.big_int => .{ .big_int = x.big_int.add(y.big_int) },
else => unreachable,
};
}
Arithmetic Cost Table
| Operation | Primitive Cost | BigInt Cost |
|---|---|---|
| Plus (+) | 15 | 20 |
| Minus (-) | 15 | 20 |
| Multiply (*) | 15 | 30 |
| Division (/) | 15 | 30 |
| Modulo (%) | 15 | 30 |
| Min/Max | 15 | 20 |
Relation Operations
Comparison operations5:
const Relation = struct {
op_code: OpCode,
left: *const Value,
right: *const Value,
pub fn eval(self: *const Relation, env: *const DataEnv, E: *Evaluator) !bool {
const lv = try self.left.eval(env, E);
const rv = try self.right.eval(env, E);
const cost: u32 = switch (self.op_code.value) {
OpCodes.Eq.value, OpCodes.Neq.value => 3, // Equality cheap
else => 15, // Ordering comparisons
};
E.addCost(FixedCost{ .value = cost }, self.op_code);
return switch (self.op_code.value) {
OpCodes.Lt.value => compare(lv, rv, self.left.tpe) < 0,
OpCodes.Le.value => compare(lv, rv, self.left.tpe) <= 0,
OpCodes.Gt.value => compare(lv, rv, self.left.tpe) > 0,
OpCodes.Ge.value => compare(lv, rv, self.left.tpe) >= 0,
OpCodes.Eq.value => equalValues(lv, rv),
OpCodes.Neq.value => !equalValues(lv, rv),
else => error.UnknownOpcode,
};
}
};
Logical Operations
Short-circuit evaluation with per-item cost6:
const LogicalAnd = struct {
input: *const Value, // Collection[Boolean]
pub const COST = PerItemCost{
.base = 10,
.per_chunk = 5,
.chunk_size = 32,
};
pub fn eval(self: *const LogicalAnd, env: *const DataEnv, E: *Evaluator) !bool {
const coll = try self.input.eval(env, E);
const items = coll.coll.bools;
var result = true;
var i: usize = 0;
// Short-circuit: stop on first false
while (i < items.len and result) : (i += 1) {
result = result and items[i];
}
// Cost based on actual items processed
E.addSeqCost(COST, i, OpCodes.And);
return result;
}
};
const BinaryAnd = struct {
left: *const Value,
right: *const Value,
pub const COST = FixedCost{ .value = 20 };
pub fn eval(self: *const BinaryAnd, env: *const DataEnv, E: *Evaluator) !bool {
const l = try self.left.eval(env, E);
E.addCost(COST, OpCodes.BinAnd);
// Short-circuit: don't evaluate right if left is false
if (!l.boolean) return false;
return (try self.right.eval(env, E)).boolean;
}
};
Cost Descriptors
Every operation has an associated cost that the interpreter accumulates during evaluation. If the total cost exceeds the block limit, execution fails—this prevents denial-of-service attacks via expensive computations. Three cost descriptor types model different operation characteristics7:
/// Fixed cost regardless of input
const FixedCost = struct {
value: u32, // JitCost units
};
/// Cost scales with input size
const PerItemCost = struct {
base: u32, // Fixed overhead
per_chunk: u32, // Cost per chunk
chunk_size: u32, // Items per chunk
pub fn calculate(self: PerItemCost, n_items: usize) u32 {
const chunks = (n_items + self.chunk_size - 1) / self.chunk_size;
return self.base + @intCast(chunks) * self.per_chunk;
}
};
/// Cost depends on operand type
const TypeBasedCost = struct {
primitive_cost: u32,
big_int_cost: u32,
pub fn forType(self: TypeBasedCost, tpe: SType) u32 {
return switch (tpe) {
.big_int, .unsigned_big_int => self.big_int_cost,
else => self.primitive_cost,
};
}
};
Context Operations
Access transaction context8:
const ContextOps = struct {
pub const Height = struct {
pub const COST = FixedCost{ .value = 26 };
pub fn eval(_: *const @This(), _: *const DataEnv, E: *Evaluator) i32 {
E.addCost(COST, OpCodes.Height);
return E.context.pre_header.height;
}
};
pub const Inputs = struct {
pub const COST = FixedCost{ .value = 10 };
pub fn eval(_: *const @This(), _: *const DataEnv, E: *Evaluator) []const Box {
E.addCost(COST, OpCodes.Inputs);
return E.context.inputs;
}
};
pub const Outputs = struct {
pub const COST = FixedCost{ .value = 10 };
pub fn eval(_: *const @This(), _: *const DataEnv, E: *Evaluator) []const Box {
E.addCost(COST, OpCodes.Outputs);
return E.context.outputs;
}
};
pub const SelfBox = struct {
pub const COST = FixedCost{ .value = 10 };
pub fn eval(_: *const @This(), _: *const DataEnv, E: *Evaluator) *const Box {
E.addCost(COST, OpCodes.Self);
return E.context.self_box;
}
};
};
Box Property Access
Extract box properties9:
const ExtractAmount = struct {
box: *const Value,
pub const COST = FixedCost{ .value = 12 };
pub fn eval(self: *const ExtractAmount, env: *const DataEnv, E: *Evaluator) !i64 {
const b = try self.box.eval(env, E);
E.addCost(COST, OpCodes.ExtractAmount);
return b.box.value;
}
};
const ExtractId = struct {
box: *const Value,
pub const COST = FixedCost{ .value = 12 };
pub fn eval(self: *const ExtractId, env: *const DataEnv, E: *Evaluator) ![32]u8 {
const b = try self.box.eval(env, E);
E.addCost(COST, OpCodes.ExtractId);
return b.box.id();
}
};
const ExtractRegisterAs = struct {
box: *const Value,
register_id: u4, // 0-9
pub const COST = FixedCost{ .value = 12 };
pub fn eval(self: *const ExtractRegisterAs, env: *const DataEnv, E: *Evaluator) !?Constant {
const b = try self.box.eval(env, E);
E.addCost(COST, OpCodes.ExtractRegisterAs);
return b.box.registers[self.register_id];
}
};
Cryptographic Operations
Hash and sigma prop operations10:
const CalcBlake2b256 = struct {
input: *const Value, // Coll[Byte]
pub const COST = PerItemCost{
.base = 117,
.per_chunk = 1,
.chunk_size = 128,
};
pub fn eval(self: *const CalcBlake2b256, env: *const DataEnv, E: *Evaluator) ![32]u8 {
const bytes = try self.input.eval(env, E);
E.addSeqCost(COST, bytes.coll.bytes.len, OpCodes.CalcBlake2b256);
var hasher = std.crypto.hash.blake2.Blake2b256.init(.{});
hasher.update(bytes.coll.bytes);
return hasher.finalResult();
}
};
const CalcSha256 = struct {
input: *const Value,
pub const COST = PerItemCost{
.base = 79,
.per_chunk = 1,
.chunk_size = 64,
};
pub fn eval(self: *const CalcSha256, env: *const DataEnv, E: *Evaluator) ![32]u8 {
const bytes = try self.input.eval(env, E);
E.addSeqCost(COST, bytes.coll.bytes.len, OpCodes.CalcSha256);
var hasher = std.crypto.hash.sha2.Sha256.init(.{});
hasher.update(bytes.coll.bytes);
return hasher.finalResult();
}
};
Summary
This chapter detailed the opcode encoding scheme that gives each ErgoTree operation a unique byte identifier:
- Opcode space is split between constant type codes (0x01-0x70) and operation codes (0x72-0xFF), with constants using their type code directly to save one byte per value
- Operation categories group related functionality: variables, conversions, relations, arithmetic, context access, collections, box properties, cryptography, blocks, options, sigma propositions, and bitwise operations
- Cost descriptors come in three types:
FixedCostfor constant-time operations,PerItemCostfor operations that scale with input size, andTypeBasedCostfor operations where BigInt is more expensive than primitive types - Short-circuit evaluation in logical operations (
AND,OR,BinaryAnd,BinaryOr) stops early when the result is determined, with costs calculated based on actual items processed - Context operations provide access to transaction data:
HEIGHT,INPUTS,OUTPUTS,SELFbox, and miner public key
Next: Chapter 6: Methods on Types
Scala: OpCodes.scala
Rust: op_code.rs:10-100
Scala: trees.scala:704-827
Rust: bin_op.rs
Scala: trees.scala:908-1100
Scala: trees.scala (AND, OR)
Scala: CostKind.scala
Scala: trees.scala (context operations)
Scala: trees.scala (box accessors)
Scala: trees.scala (crypto operations)