Skip to main content

Script Context

Lambit exposes transaction context through TxContext. The usual authoring shape is a trailing ctx parameter: if a positional method(schema, fn) callback declares one argument more than the schema fields, that final argument is a TxContextExpr proxy. The compiler appends only the ctx fields your method actually reads.

import { TypeTag, contract, eq, method } from '@opcat-labs/lambit';

const ContextGuard = contract(
'ContextGuard',
{ expectedHashOutputs: TypeTag.Hash256 },
({ props }) => ({
unlock: method(
{},
(ctx) => eq(ctx.hashOutputs, props.expectedHashOutputs),
),
}),
);

const contextGuard = ContextGuard({ expectedHashOutputs: '00'.repeat(32) });
console.log(contextGuard.artifact.contract); // ContextGuard

Context Fields

TxContext is the canonical OP_CAT sighash-preimage surface used by the compiler and runtime. It is exported both as a schema value and as a TxContextExpr type for helper functions that accept the trailing proxy.

FieldTypeTypical use
nVersionByteStringVersion-byte checks.
hashPrevoutsHash256Full input-set commitments.
inputIndexIntSelected input checks.
outpointByteStringCurrent outpoint checks.
spentScriptHashSha256Native state output construction.
spentDataHashSha256Current serialized state checks.
valueIntSpending amount for terminal outputs.
nSequenceByteStringSequence-byte checks.
hashSpentAmountsHash256Taproot-style spent amount commitments.
hashSpentScriptHashesHash256Spent script commitments.
hashSpentDataHashesHash256Spent data commitments.
hashSequencesHash256Sequence-vector commitments.
hashOutputsHash256Output-vector or same-index output commitment.
nLockTimeIntLocktime and height checks.
sigHashTypeSigHashTypeSelected sighash-path checks.

Byte-oriented preimage fields such as nVersion, outpoint, and nSequence stay exposed as raw bytes so contracts can inspect the exact serialized preimage form. nLockTime is decoded as an Int, so author code can use bigint-native comparisons while runtime timelock checks still enforce the underlying UInt32 bounds.

Runtime auto-fill is based on the canonical TxContext field names and types. Use the trailing ctx parameter when a method only needs a few fields. Use method.named(TxContext, (ctx) => ...) when you intentionally want the full context object in the ABI.

Native stateful compilation authenticates the pushed preimage by appending the selected sighash type byte to the DER signature in the commitment prologue. Successor-state outputs and single-output terminal paths use 0x03 (SIGHASH_SINGLE), while selected multi-output terminal paths use 0x01 (SIGHASH_ALL). Wallet-funded inputs are signed separately by the provider.

The assertOutputs(outputs) helper is the stateless full-output-vector covenant. Native stateful contracts use SIGHASH_SINGLE for state transitions and single-output terminal paths, so those hashOutputs commitments cover only the same-index output. Multi-output terminal paths switch to SIGHASH_ALL, which commits the full output vector.

Stateful methods may therefore:

  • return { next, check? } for successor-state outputs
  • return { outputs, check? } for terminal outputs
  • use branches(...) to choose between those two shapes on mutually exclusive runtime paths

Stateful compilation rejects user-authored hashOutputs dependencies in normal expressions; the compiler generates the same-index hashOutputs check needed for next transitions and selected terminal { outputs, check? } paths. Funded provider paths also reject source-backed contracts that contain any hashOutputs-dependent method, and artifact-only templates containing <hashOutputs>, because wallet funding can add change after the prepared context is fixed. Pass the provider to deployed.methods.<name>.prepareCall(args?, { provider }) when you want this funded-provider check to run before handing the prepared payload to an offline signer; deployed method .call(...) requires the provider and performs the same check before broadcast. Selected multi-output stateful terminal paths are also rejected in funded mode, because provider-added change would invalidate the SIGHASH_ALL output-vector commitment.

Inspecting Output Digests

import {
TypeTag,
buildArtifact,
buildDataOutput,
contract,
eq,
hash256,
method,
} from '@opcat-labs/lambit';

const OutputDigestGuard = contract(
'OutputDigestGuard',
{},
() => ({
unlock: method(
{ nextDataHash: TypeTag.Sha256 },
(nextDataHash, ctx) =>
eq(
ctx.hashOutputs,
hash256(buildDataOutput(ctx.spentScriptHash, ctx.value, nextDataHash)),
),
),
}),
);

const artifact = buildArtifact(OutputDigestGuard);
const unlockAbi = artifact.abi.find(
(entry) => entry.type === 'function' && entry.name === 'unlock',
);

console.log(unlockAbi?.params.map((param) => param.name));
// ['nextDataHash', 'spentScriptHash', 'value', 'hashOutputs']
console.log(artifact.hex.length > 0); // true

Full Context Checks

Use method.named(TxContext, (ctx) => ...) when a method really wants the full context object. The example below uses nVersion as a byte-exact version guard and nLockTime as a numeric timelock floor.

import {
TxContext,
TypeTag,
and,
buildArtifact,
buildDataOutput,
contract,
eq,
gte,
hash256,
method,
} from '@opcat-labs/lambit';

const PreimageGuard = contract(
'PreimageGuard',
{ expectedVersionBytes: TypeTag.ByteString, minLockTime: TypeTag.Int },
({ props }) => ({
unlock: method.named(
TxContext,
(ctx) => and(
eq(ctx.nVersion, props.expectedVersionBytes),
and(
gte(ctx.nLockTime, props.minLockTime),
eq(
ctx.hashOutputs,
hash256(buildDataOutput(ctx.spentScriptHash, ctx.value, ctx.spentDataHash)),
),
),
),
),
}),
);

const artifact = buildArtifact(PreimageGuard);
console.log(Object.keys(TxContext).length); // 15
console.log(artifact.abi[0]?.params.at(-1)?.name); // sigHashType

Helper Functions

Import TxContextExpr when a reusable helper needs to accept the trailing context proxy.

import {
type TxContextExpr,
contract,
eq,
hash256,
buildDataOutput,
method,
TypeTag,
} from '@opcat-labs/lambit';

function commitsNextData(ctx: TxContextExpr, nextDataHash: string) {
return eq(
ctx.hashOutputs,
hash256(buildDataOutput(ctx.spentScriptHash, ctx.value, nextDataHash)),
);
}

const HelperBackedGuard = contract(
'HelperBackedGuard',
{},
() => ({
unlock: method(
{ nextDataHash: TypeTag.Sha256 },
(nextDataHash, ctx) => commitsNextData(ctx, nextDataHash),
),
}),
);

console.log(HelperBackedGuard({}).artifact.contract); // HelperBackedGuard

Stateful Output Verification Lifecycle

import { contract, method, TypeTag, add, gt, buildArtifact, lit } from '@opcat-labs/lambit';

const Counter = contract(
'Counter',
{},
{ count: TypeTag.Int },
({ state }) => ({
increase: method({}, () => {
const nextCount = add(state.count, lit(1n));
return {
next: { count: nextCount },
check: gt(nextCount, state.count),
};
}),
}),
);

const artifact = buildArtifact(Counter);
// Native stateful compilation replaces every context marker with
// preimage-backed opcode sequences, so no UTF-8 placeholders survive in the
// deployed hex.
console.log(artifact.hex.includes('<hashOutputs>')); // false
console.log(artifact.hex.includes('<spentScriptHash>')); // false
console.log(artifact.hex.includes('<value>')); // false
note

buildDataOutput() and stateHash() are exported compiler helpers when you need explicit output-hash expressions.

caution

TxContext covers the runtime-injected spend/output placeholders available today. Keep examples consensus-meaningful; avoid placeholder self-equality checks such as eq(x, x) in production contracts.

Funded provider calls perform extra rejection checks before broadcast because wallet-added change can alter the output vector after a contract context was prepared.1

What's Next

Footnotes

  1. The compiler-generated stateful checks choose SIGHASH_SINGLE or SIGHASH_ALL per path, so funded flows must reject paths where provider change would invalidate the committed hashOutputs.