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:
| Field | Meaning |
|---|---|
lockingScriptTemplateHex | Template locking script with constructor/state placeholders resolved |
scriptHashHex | SHA-256 fingerprint of the template (see note below on placeholder templates) |
stateSerializationHex | Serialized state fields for stateful contracts |
stateHashHex | Hash 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);
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
txidand 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/lambitversion. - Constructor args and initial state are documented with worked examples.
-
scriptHashHexandlockingScriptTemplateHexmatch 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.