Rúnar

Advanced Testing

The runar-testing package exports TestContract for most testing needs, but three additional utilities cover scenarios that TestContract does not: validating compiled Bitcoin Script through the BSV SDK interpreter, executing contracts at the AST level, and predicting state transitions from ANF IR. This page documents each of them.

When to Use Each Tool

ToolPackagePurpose
TestContractrunar-testingGeneral-purpose testing. Compiles, interprets, and validates contracts in a single call. Use this for most tests.
ScriptExecutionContractrunar-testingEnd-to-end script validation. Compiles a contract to Bitcoin Script hex and executes it through the BSV SDK’s Spend interpreter. Use this to confirm that compiled output actually runs on-chain.
RunarInterpreterrunar-testingAST-level execution. Directly interprets the parsed Runar AST without compiling. Use this for semantic analysis, differential testing against the compiled output, or when you need access to interpreter-level values.
computeNewState()runar-sdkState prediction. Walks the ANF IR to compute state transitions without touching Bitcoin Script. The SDK uses this internally to auto-compute newState for stateful contract calls.

ScriptExecutionContract

ScriptExecutionContract compiles a Runar contract with baked constructor arguments and executes the resulting locking/unlocking scripts through the BSV SDK’s production-grade Spend class. Unlike TestContract (which uses the reference interpreter on the AST), this class validates the compiled Bitcoin Script hex end-to-end.

Imported from runar-testing:

import { ScriptExecutionContract } from 'runar-testing';

Properties

PropertyTypeDescription
artifactRunarArtifactThe compiled artifact (ABI, AST, script hex, etc.)
scriptHexstringThe compiled locking script as a hex string

ScriptExecutionContract.fromSource()

Compiles a Runar contract from source with baked constructor arguments.

static fromSource(
  source: string,
  constructorArgs: Record<string, bigint | boolean | string>,
  fileName?: string,
  compileOptions?: Partial<CompileOptions>,
): ScriptExecutionContract
  • source — The Runar contract source code as a string.
  • constructorArgs — Constructor parameter values baked into the locking script.
  • fileName — Optional file name for error messages.
  • compileOptions — Optional additional compiler options (merged with fileName and constructorArgs).

Throws if compilation fails, with a message containing all error diagnostics.

const src = readFileSync('./contracts/Arithmetic.runar.ts', 'utf8');
const contract = ScriptExecutionContract.fromSource(
  src,
  { target: 27n },
  'Arithmetic.runar.ts',
);

execute()

Executes a public method against the compiled locking script. For pure-computation contracts that do not use checkSig or checkPreimage.

execute(methodName: string, args: unknown[]): ScriptExecResult
  • methodName — The name of the public method to call.
  • args — Positional arguments matching the method’s parameter list.

Returns a ScriptExecResult:

interface ScriptExecResult {
  success: boolean;
  error?: string;
}

Example:

const result = contract.execute('verify', [3n, 7n]);
expect(result.success).toBe(true);

Arguments are encoded to Bitcoin Script push data based on the ABI parameter types. If a contract has multiple public methods, a method selector index is automatically pushed onto the unlocking script.

executeSigned()

Executes a method that requires a real OP_CHECKSIG by constructing a transaction context with a valid DER signature.

executeSigned(
  methodName: string,
  args: unknown[],
  sigArgIndex: number,
  privateKey: PrivateKey,
): ScriptExecResult
  • methodName — The name of the public method.
  • args — Positional arguments. The entry at sigArgIndex is a placeholder that will be replaced with the computed signature.
  • sigArgIndex — The index in args where the signature should be inserted.
  • privateKey — A PrivateKey from @bsv/sdk used to produce the signature.

The method builds a BIP-143 sighash preimage with SIGHASH_ALL | SIGHASH_FORKID, signs it with the private key, and creates a Spend with the same transaction context so that OP_CHECKSIG succeeds.

import { PrivateKey } from '@bsv/sdk';

const pk = new PrivateKey('aa'.repeat(32), 16);
const pubKeyHash = ScriptExecutionContract.pubKeyHashHex(pk);

const contract = ScriptExecutionContract.fromSource(src, {
  ownerPKH: pubKeyHash,
});

const pubKey = ScriptExecutionContract.pubKeyHex(pk);
const result = contract.executeSigned(
  'unlock',
  ['placeholder', pubKey],  // sig placeholder at index 0
  0,                          // sigArgIndex
  pk,
);
expect(result.success).toBe(true);

Static Helpers

ScriptExecutionContract.pubKeyHex(privateKey: PrivateKey): string

Returns the compressed public key as a hex string for the given private key.

ScriptExecutionContract.pubKeyHashHex(privateKey: PrivateKey): string

Returns the hash160 (RIPEMD-160 of SHA-256) of the compressed public key as a hex string. Useful for constructing P2PKH-style constructor arguments.

Supported Argument Types

The following ABI parameter types are supported for argument encoding:

ABI TypeTypeScript TypeEncoding
bigintbigint or numberScript number (minimally encoded)
booleanbooleanOP_1 (true) or OP_0 (false)
ByteStringstring (hex)Push data
PubKeystring (hex)Push data
Sigstring (hex)Push data
Sha256string (hex)Push data
Ripemd160string (hex)Push data
Addrstring (hex)Push data
SigHashPreimagestring (hex)Push data

RunarInterpreter

RunarInterpreter is a definitional interpreter that directly executes the Runar AST without compiling to Bitcoin Script. It serves as a semantic oracle: given the same inputs, the interpreter and the compiled Bitcoin Script should produce equivalent results. This makes it invaluable for differential testing and AST-level analysis.

Imported from runar-testing:

import { RunarInterpreter } from 'runar-testing';
import type { RunarValue, InterpreterResult } from 'runar-testing';

Value Types

The interpreter operates on RunarValue, a tagged union:

type RunarValue =
  | { kind: 'bigint'; value: bigint }
  | { kind: 'boolean'; value: boolean }
  | { kind: 'bytes'; value: Uint8Array }
  | { kind: 'void' };

Constructor

constructor(properties: Record<string, RunarValue>)

Creates an interpreter instance with the given contract properties (constructor state).

const interp = new RunarInterpreter({
  threshold: { kind: 'bigint', value: 100n },
  owner: { kind: 'bytes', value: new Uint8Array([0x02, ...]) },
});

executeMethod()

Executes a public method on a parsed Runar contract AST.

executeMethod(
  contract: ContractNode,
  methodName: string,
  args: Record<string, RunarValue>,
): InterpreterResult
  • contract — The parsed ContractNode (Runar AST).
  • methodName — Name of the public method to execute.
  • args — Method arguments as a name-to-value map.

Returns an InterpreterResult:

interface InterpreterResult {
  success: boolean;
  error?: string;
  returnValue?: RunarValue;
}

When success is false, error contains the reason — typically a failed assert or an undefined variable.

import type { ContractNode } from 'runar-ir-schema';

// `contract` is a parsed ContractNode from the Runar compiler
const result = interp.executeMethod(contract, 'verify', {
  a: { kind: 'bigint', value: 5n },
  b: { kind: 'bigint', value: 3n },
});

expect(result.success).toBe(true);

State and Output Management

MethodSignatureDescription
setContract(contract: ContractNode): voidStores a contract reference on the interpreter instance.
setMockPreimage(overrides: Record<string, bigint>): voidOverride mock preimage fields (locktime, amount, version, sequence).
setMockPreimageBytes(overrides: Record<string, Uint8Array>): voidOverride mock preimage byte fields.
resetOutputs(): voidClears the accumulated output list.
getOutputs(): { satoshis: RunarValue; stateValues: Record<string, RunarValue> }[]Returns a copy of the outputs created during execution.
getState(): Record<string, RunarValue>Returns the current property state of the interpreter.

What the Interpreter Supports

The interpreter handles the full Runar expression language:

  • Literals: bigint_literal, bool_literal, bytestring_literal
  • Variables: identifier, property_access
  • Operators: all binary_expr operators (+, -, *, /, %, ==, !=, <, <=, >, >=, &&, ||, &, |, ^, <<, >>) and unary_expr operators (-, !, ~)
  • Control flow: if_statement, for_statement, return_statement, ternary_expr
  • Calls: call_expr for builtins (checkSig, checkMultiSig, sha256, hash256, hash160, ripemd160, num2bin, bin2num, cat, substr, reverseBytes, len, abs, min, max, within)
  • Indexing: index_access for byte indexing, member_expr for .length
  • Mutation: increment_expr, decrement_expr, assignment (to variables and properties)
  • Crypto: real hash computations (sha256, hash256, hash160, ripemd160); real verification for WOTS, SLH-DSA, ECDSA, and Rabin signatures; checkSig and checkMultiSig return true (mocked)

computeNewState()

computeNewState() is a lightweight ANF interpreter that walks the compiled artifact’s ANF IR to compute state transitions. The SDK uses it internally to auto-compute newState for stateful contract calls so callers do not need to duplicate contract logic.

Imported from runar-sdk:

import { computeNewState } from 'runar-sdk';

Signature

function computeNewState(
  anf: ANFProgram,
  methodName: string,
  currentState: Record<string, unknown>,
  args: Record<string, unknown>,
): Record<string, unknown>
  • anf — The ANFProgram from the compiled artifact (artifact.anf).
  • methodName — The public method to execute. Throws if not found.
  • currentState — Current contract state as a property-name-to-value map.
  • args — Method arguments as a parameter-name-to-value map. Implicit parameters (_changePKH, _changeAmount, _newAmount, txPreimage) are automatically skipped.

Returns the merged state: { ...currentState, ...stateDelta }, where stateDelta contains only the properties that changed.

Example

import { computeNewState } from 'runar-sdk';

// artifact.anf is the ANF IR from a compiled Counter contract
const newState = computeNewState(
  artifact.anf,
  'increment',
  { count: 5n },
  {},  // increment takes no user-facing args
);

expect(newState.count).toBe(6n);

Supported ANF Operations

The interpreter handles the following ANF value kinds:

ANF KindBehavior
load_paramLoads a method parameter from the environment
load_propLoads a contract property from the environment
load_constReturns the constant value; handles @ref:name aliases
bin_opArithmetic (+, -, *, /, %), comparison (==, !=, <, <=, >, >=), logical (&&, ||), bitwise (&, |, ^, <<, >>); supports both numeric and byte-string operands
unary_opNegation (-), logical not (!), bitwise not (~)
callBuilt-in functions (see below)
method_callPrivate method calls — executes the method body from the ANF, propagates state changes
ifConditional branching
loopBounded iteration with an iteration variable
assertSkipped (on-chain script handles enforcement)
update_propUpdates a property in the state delta
add_outputExtracts state changes from stateValues array, mapping to mutable properties by declaration order

Skipped (On-Chain Only) Operations

These operations are no-ops in the simulator because they only have meaning during on-chain script execution:

  • check_preimage
  • deserialize_state
  • get_state_script
  • add_raw_output

Built-in Functions

The call kind delegates to these built-in functions:

CategoryFunctionsBehavior
Crypto (mocked)checkSig, checkMultiSig, checkPreimageAlways return true
Crypto (real)sha256, hash256, hash160, ripemd160Real hash computations via @bsv/sdk
Byte opsnum2bin, bin2num, cat, substr, reverseBytes, lenReal implementations on hex strings
Mathabs, min, max, within, safediv, safemod, clamp, sign, pow, sqrt, gcd, divmod, log2, bool, mulDiv, percentOfBigInt arithmetic
Preimage intrinsicsextractOutputHash, extractAmountReturn dummy values ('00'.repeat(32))

How the SDK Uses computeNewState

When calling a stateful contract method through the SDK, computeNewState is invoked automatically if the artifact contains ANF IR:

// Inside the SDK's Contract class (simplified)
if (this.artifact.anf) {
  const computed = computeNewState(
    this.artifact.anf,
    methodName,
    this._state,
    namedArgs,
  );
  this._state = { ...this._state, ...computed };
}

This means you typically do not need to call computeNewState directly — the SDK handles state prediction transparently. Direct use is helpful when you need to predict state changes in isolation, for example in a test harness or a UI that previews state transitions before broadcasting.