Skip to main content

How to Verify a Contract

Verification means proving that an on-chain locking script and state output match a published Lambit artifact plus the constructor and state arguments you claim were used at deploy time.

Unlike class/decorator stacks that ship a dedicated explorer plugin, Lambit verification today is local and artifact-driven: rebuild the instance from the artifact, compare script hashes and locking scripts, then exercise method verification offline before you trust a spend path.

What you are comparing

Every runtime instance carries identifiers derived from the compiled template:

FieldMeaning
lockingScriptTemplateHexTemplate locking script with constructor/state placeholders resolved
scriptHashHexSHA-256 fingerprint of the template (see note below on placeholder templates)
stateSerializationHexSerialized state fields for stateful contracts
stateHashHexHash commitment to the current state vector

You can read these from a bound contract before deploy:

import { contract, method, TypeTag, add, gt, 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 counter = Counter({});
const instance = counter.init({ count: 0n });

console.log(instance.lockingScriptTemplateHex);
console.log(instance.scriptHashHex);
console.log(instance.stateSerializationHex);
console.log(instance.stateHashHex);
Script hash stability

For templates that still contain runtime context placeholders (<ctx_*> tokens), scriptHashHex is computed from the placeholder-bearing template string so stateful instances keep a stable identifier across context-dependent spends. Always verify using the same constructor args, state, and artifact revision together.

Step 1 — Deploy or locate the UTXO

Complete a deploy flow first. The Hello World tutorial walks through deploying a Hashlock contract to OP_CAT testnet and records the deployment transaction id.

After deployment, note:

  • deployment txid and output index (vout)
  • satoshis on the contract UTXO
  • constructor arguments used when binding props
  • initial state for stateful contracts

Inspect the transaction on the OP_CAT testnet explorer and confirm the contract output carries the locking script you expect.

Step 2 — Rebuild the instance locally

Load the same artifact and inputs you claim were used on chain.

Bound-contract path (source available):

const hashlock = Hashlock({ hash: expectedHash });
const instance = hashlock.init();

Artifact-first path (published package or CLI output):

import { readFileSync } from 'node:fs';
import { createInstance } from '@opcat-labs/lambit';

const artifact = JSON.parse(readFileSync('./artifacts/Hashlock.json', 'utf8'));
const instance = createInstance({
artifact,
constructorArgs: { hash: expectedHash },
});

Compare the rebuilt values against what you observed on chain:

console.log('scriptHashHex', instance.scriptHashHex);
console.log('lockingScriptTemplateHex', instance.lockingScriptTemplateHex);

If either value differs, the artifact revision, constructor args, or state do not match the deployed UTXO.

Step 3 — Decode and inspect the artifact script

Use compiler debug helpers when you need to see opcode boundaries or placeholder tokens:

import { decodeArtifactHex, snapshotArtifact } from '@opcat-labs/lambit';

const artifact = hashlock.artifact;

console.log(decodeArtifactHex(artifact.hex));
console.log(snapshotArtifact(artifact));

This step is especially useful when a verification mismatch comes from placeholder substitution, library embedding order, or assertion metadata you did not expect.

Step 4 — Verify methods offline

Method verification runs the compiled unlocking script through the BVM with a synthetic spend context. It does not broadcast.

await hashlock.methods.unlock.verify(
{},
{ preimage: expectedPreimage },
{
context: {
satoshis: 1_000n,
txid: '22'.repeat(32),
vout: 0,
},
signer,
invoke: (psbt) => ({
sig: psbt.getSig(0, { address: addr }),
}),
},
);

For stateful contracts, also check pure transitions:

const next = counter.methods.increase.next({ count: 0n });
// { count: 1n }

See How to Test a Contract and How to Debug a Contract for the full offline workflow.

Step 5 — Dry-run a spend against a known outpoint

When you deployed through a provider in the same process, reload the tracked UTXO and prepare a call without broadcasting:

import { attachDeployedMethods, createTestnetProvider } from '@opcat-labs/lambit';

const provider = createTestnetProvider({ wif: process.env.TESTNET_WIF!, network: 'testnet' });

const stored = await provider.getUtxo(deployedTxid, 0);
if (!stored) {
throw new Error('UTXO not found in this provider session');
}

const deployed = attachDeployedMethods(stored);
const prepared = await deployed.methods.unlock.prepareCall(
{ preimage: expectedPreimage },
{ provider, signer, invoke },
);

console.log(prepared.unlockingScriptHex);
console.log(prepared.messageHex);

For third-party deployments, stop after Steps 2–4: compare scriptHashHex and lockingScriptTemplateHex, then run methods.<name>.verify(...). Providers track UTXOs they created in the current session; rebuilding the instance from a published artifact is the portable verification path.

If preparation succeeds locally but broadcast fails, the problem is usually funding, feerate, stale UTXOs, or a network-level rejection — not artifact mismatch.

Verification checklist

Use this checklist when you publish or audit a contract package:

  • Artifact JSON is pinned to a tagged source revision and @opcat-labs/lambit version.
  • Constructor args and initial state are documented with worked examples.
  • scriptHashHex and lockingScriptTemplateHex match the deployed UTXO for those inputs.
  • Every public method passes methods.<name>.verify(...) for representative args.
  • Stateful methods.<name>.next(...) results match the successor state you expect on chain.
  • Prepared calls succeed against the attached outpoint before you ask third parties to sign.