Skip to main content

Sighash Developer Guide

In OP_CAT smart contracts, there are two distinct types of signatures to understand:

  • Context SigHash Preimage — lets the contract read the current transaction's context (inputs, outputs, state, etc.)
  • User Signature — the user authorizes an NFT/Token transfer with their own private key

Sighash Type

The sighash type determines which parts of the transaction a signature commits to:

ValueTypeCoverage
0x01SIGHASH_ALLall inputs + all outputs (default, most common)
0x02SIGHASH_NONEall inputs, no commitment to outputs
0x03SIGHASH_SINGLEall inputs + the output at the same index
0x81SIGHASH_ANYONECANPAY \| ALLonly the current input + all outputs
0x82SIGHASH_ANYONECANPAY \| NONEonly the current input, no commitment to outputs
0x83SIGHASH_ANYONECANPAY \| SINGLEonly the current input + the output at the same index

Lambit has no per-method sighash decorator; the sighash type is chosen per spend instead:

  • Off-chain, at signing time. The runtime signs method arguments through psbt.getSig(inputIndex, { address | publicKey, sighashTypes }), which defaults to SIGHASH_ALL (DEFAULT_SIGHASH_TYPE). Pass sighashTypes to sign with a different type.
  • On-chain, at compile time. Native stateful spends commit the preimage with the sighash type selected for the path: SIGHASH_SINGLE (0x03) for successor-state outputs and single-output terminal paths, SIGHASH_ALL (0x01) for selected multi-output terminal paths. The compiler asserts the committed preimage's sigHashType so the selected type cannot be substituted.
  • In the contract. A method reads back the committed type with ctx.sigHashType (a 4-byte little-endian value) and can constrain it.

The sighash type has two effects:

  1. Determines the scope of the Context SigHash Preimage — the sighash type selected for the spend controls which aggregate hash fields in ctx cover all inputs/outputs (see the field table below).

  2. Determines the preimage verified by checkSigcheckSig verifies a User Signature against the transaction's sighash for the type embedded in the signature. The user must therefore sign off-chain with a matching sighash type (psbt.getSig's sighashTypes) for checkSig to pass.


Context SigHash Preimage

What it provides

When a public method executes, the runtime injects the current transaction's context into the method. Declare one more callback argument than the schema fields, and that final argument becomes a TxContextExpr proxy:

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

const MyContract = contract(
"MyContract",
{ expectedScriptHash: TypeTag.Sha256 },
({ props }) => ({
unlock: method({}, (ctx) => {
// read the current input index
const myInputIndex = ctx.inputIndex;

// verify the current input spends a specific locking script
const scriptMatches = eq(ctx.spentScriptHash, props.expectedScriptHash);

// whitelist the sighash type used for this spend (0x01 = SIGHASH_ALL)
const sigHashAll = eq(ctx.sigHashType, lit("01000000"));

return and(and(scriptMatches, sigHashAll), eq(myInputIndex, lit(0n)));
}),
}),
);

The compiler appends only the ctx fields the method actually reads, so unused fields never appear in the ABI or the deployed script. Use method.named(TxContext, (ctx) => ...) when you intentionally want the full context object in the ABI.

How to use it

The runtime handles everything automatically — you do not need to manually pass in or verify the preimage. Simply use the trailing ctx parameter directly inside contract methods:

// constrain transaction outputs (prevent tampering)
// - stateful methods: return { next, check } or { outputs, check } and the
// compiler commits the output vector
// - stateless methods: use the assertOutputs([...]) helper
const outputMatches = assertOutputs([...]);

// restrict which sighash type the caller may use
const typeAllowed = or(
eq(ctx.sigHashType, lit('01000000')), // SIGHASH_ALL
eq(ctx.sigHashType, lit('81000000')), // SIGHASH_ANYONECANPAY | ALL
);

Complete ctx fields

ctx exposes the canonical TxContext preimage surface. Byte-oriented fields such as nVersion, outpoint, and nSequence stay exposed as raw bytes so contracts can inspect the exact serialized preimage form; inputIndex and nLockTime are decoded as Int for bigint-native comparisons:

ctx.nVersion; // 4 bytes  — transaction version (raw bytes)
ctx.hashPrevouts; // 32 bytes — aggregate hash of all input prevouts
ctx.inputIndex; // Int — current input index
ctx.outpoint; // 36 bytes — prevout of the current input (txHash + outputIndex)
ctx.spentScriptHash; // 32 bytes — script hash of the UTXO spent by the current input
ctx.spentDataHash; // 32 bytes — state hash of the current input
ctx.value; // Int — satoshis of the current input
ctx.nSequence; // 4 bytes — sequence of the current input (raw bytes)
ctx.hashSpentAmounts; // 32 bytes — aggregate hash of all input amounts
ctx.hashSpentScriptHashes; // 32 bytes — aggregate hash of all input script hashes
ctx.hashSpentDataHashes; // 32 bytes — aggregate hash of all input state hashes
ctx.hashSequences; // 32 bytes — aggregate hash of all input sequences
ctx.hashOutputs; // 32 bytes — aggregate hash of all outputs
ctx.nLockTime; // Int — transaction lock time
ctx.sigHashType; // 4 bytes — sighash type flag (little-endian)

The coverage of aggregate hash fields varies by sighash type:

FieldALLNONESINGLEANYONECANPAY_*
hashPrevoutsall inputsall inputsall inputscurrent input only
hashSpentAmountsall inputsall inputsall inputscurrent input only
hashSpentScriptHashesall inputsall inputsall inputscurrent input only
hashSpentDataHashesall inputsall inputsall inputscurrent input only
hashOutputsall outputsemptyoutput at same indexdepends on base type

User Signature

When you need it

When a contract must be callable only by a specific user, verify a User Signature. The user signs the current transaction with their private key; the contract checks that the signature is valid and the corresponding public key matches the expected identity, ensuring only that user can execute the operation.

Common scenarios: NFT/Token ownership transfers, Guard deployer authorization, contract operations requiring an admin signature, etc.

Verifying inside a contract

import {
TypeTag,
and,
checkSig,
cond,
contract,
eq,
hash160,
len,
lit,
method,
} from "@opcat-labs/lambit";

const OwnerGate = contract(
"OwnerGate",
{},
{ owner: TypeTag.ByteString },
({ state }) => ({
unlock: method(
{ sig: TypeTag.Sig, pubkey: TypeTag.PubKey },
(sig, pubkey, ctx) =>
cond(
// contract-controlled: owner stores a 32-byte script hash
eq(len(state.owner), lit(32n)),
// verify the current input spends the owner contract
eq(state.owner, ctx.spentScriptHash),
// user-controlled: verify the pubkey identity and the signature
and(eq(hash160(pubkey), state.owner), checkSig(sig, pubkey)),
),
),
}),
);

Available signature verification methods

checkSig — standard ECDSA signature verification. The message is the transaction's sighash for the type embedded in the signature, so the signer must use a sighash type consistent with the spend:

checkSig(sig: ExprNode, pubKey: ExprNode): ExprNode

// example: the owner signs with the default SIGHASH_ALL
const OwnerVault = contract(
'OwnerVault',
{ ownerPkh: TypeTag.Ripemd160 },
({ props }) => ({
unlock: method(
{ sig: TypeTag.Sig, pubkey: TypeTag.PubKey },
(sig, pubkey) =>
and(eq(hash160(pubkey), props.ownerPkh), checkSig(sig, pubkey)),
),
}),
);

Accepting multiple sighash types

If the contract needs to accept signatures with multiple sighash types (e.g. both SIGHASH_ALL and ANYONECANPAY | ALL), read the committed type from ctx.sigHashType, assert it is in the whitelist, and sign off-chain with the matching sighashTypes:

const MultiSighash = contract(
"MultiSighash",
{ ownerPkh: TypeTag.Ripemd160 },
({ props }) => ({
unlock: method(
{ sig: TypeTag.Sig, pubkey: TypeTag.PubKey },
(sig, pubkey, ctx) => {
// 4-byte little-endian: SIGHASH_ALL = 0x01, ANYONECANPAY | ALL = 0x81
const sigHashAll = eq(ctx.sigHashType, lit("01000000"));
const sigHashAcpAll = eq(ctx.sigHashType, lit("81000000"));

// whitelist: only allow SIGHASH_ALL or ANYONECANPAY | ALL
const typeAllowed = or(sigHashAll, sigHashAcpAll);

return and(
and(eq(hash160(pubkey), props.ownerPkh), typeAllowed),
checkSig(sig, pubkey),
);
},
),
}),
);

When signing off-chain, specify the sighash type explicitly via the sighashTypes option of getSig:

const spend = await deployed.methods.unlock.call(
{ pubkey },
{
provider,
signer,
invoke: (psbt) => ({
// explicitly sign with ANYONECANPAY | ALL
sig: psbt.getSig(0, { address: ownerAddress, sighashTypes: [0x81] }),
}),
},
);

Signing off-chain

Use psbt.getSig() in the invoke callback to obtain a User Signature. It defaults to SIGHASH_ALL:

const spend = await deployed.methods.unlock.call(
{ pubkey },
{
provider,
signer,
invoke: (psbt) => ({
// defaults to SIGHASH_ALL
sig: psbt.getSig(0, { address: ownerAddress }),
}),
},
);

Notes

  • If a non-empty signature fails verification, the script terminates immediately (it does not return false). Ensure signatures are correct when building the transaction; otherwise nodes will reject the entire transaction.
  • The format (compressed/uncompressed) of the pubkey passed in must be consistent with how the owner hash (ownerPkh / state.owner) was generated, because hash160(pubkey) is compared against it.

Examples

NFT — owner-gated transfer

The current owner authorizes a transfer to a new owner with a User Signature, and the successor state records the new owner. checkSig binds the signature to this transaction, so it cannot be replayed on a different transfer:

import {
TypeTag,
and,
checkSig,
contract,
eq,
hash160,
method,
neq,
} from "@opcat-labs/lambit";

const Cat721 = contract(
"Cat721",
{},
{ ownerPkh: TypeTag.Ripemd160, localId: TypeTag.Int },
({ state }) => ({
transfer: method(
{
sig: TypeTag.Sig,
pubkey: TypeTag.PubKey,
newOwnerPkh: TypeTag.Ripemd160,
},
(sig, pubkey, newOwnerPkh) => ({
next: {
ownerPkh: newOwnerPkh,
localId: state.localId,
},
check: and(
// the signer must be the recorded owner
eq(hash160(pubkey), state.ownerPkh),
// ...and the owner must authorize this transfer
and(checkSig(sig, pubkey), neq(newOwnerPkh, state.ownerPkh)),
),
}),
),
}),
);

Off-chain call:

const spend = await deployed.methods.transfer.call(
{ pubkey: currentOwnerPubKey, newOwnerPkh },
{
provider,
signer,
invoke: (psbt) => ({
// the owner signs the current transaction; defaults to SIGHASH_ALL
sig: psbt.getSig(0, { publicKey: currentOwnerPubKey }),
}),
nextState: { ownerPkh: newOwnerPkh, localId },
},
);

Vault — owner signature + locktime

A stateless vault that releases funds only when an absolute locktime has matured and the owner has signed. This shows combining a User Signature with context guards such as checkLocktime:

import {
TypeTag,
and,
checkLocktime,
checkSig,
contract,
eq,
hash160,
method,
} from "@opcat-labs/lambit";

const TimedVault = contract(
"TimedVault",
{ owner: TypeTag.Ripemd160, releaseHeight: TypeTag.Int },
({ props }) => ({
unlock: method(
{ sig: TypeTag.Sig, pubkey: TypeTag.PubKey },
(sig, pubkey) =>
and(
checkLocktime(props.releaseHeight),
and(eq(hash160(pubkey), props.owner), checkSig(sig, pubkey)),
),
),
}),
);

API Reference

APILocationDescription
trailing ctx parameterdsl/method.tsCurrent transaction context for a method; only the fields read are compiled in
method.named(TxContext, (ctx) => ...)dsl/method.tsFull canonical context object in the ABI
checkSig(sig, pubKey)dsl/primitives.tsVerify a user signature against the transaction's sighash
ctx.sigHashTypedsl/contextTypes.tsSighash type used by the spend (4-byte little-endian)
psbt.getSig(inputIndex, { address \| publicKey, sighashTypes })runtime/psbt.tsOff-chain signing request resolved by the signer
DEFAULT_SIGHASH_TYPEruntime/bvm.tsRuntime default sighash type (SIGHASH_ALL)
hash160(pubkey) / pubKey2Addr(pubkey)dsl/primitives.tsDerive the 20-byte owner hash from a public key