> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Set L2 Verification Example

> A worked inclusion check — fetch the commitment, recompute the root, compare.

# Set L2 Verification Example

A complete inclusion check, end to end. The goal is to confirm an event is in the anchored history
using only the chain and a proof path — nothing that requires trusting the sequencer.

## Inputs

| Input            | Source                                                                |
| ---------------- | --------------------------------------------------------------------- |
| Batch commitment | On-chain, from `SetRegistry`                                          |
| Event fields     | tenant/store id, sequence number, event signing hash, agent signature |
| Merkle proof     | `GetInclusionProof` on the sequencer's gRPC v2 service                |

## 1. Retrieve the batch commitment

Read the commitment for the batch covering your event's sequence number. This gives you the
`events_root` — the value everything else is checked against.

```bash theme={null}
# via the sequencer's gRPC v2 service
grpcurl -d '{"batch_id":"<BATCH_UUID>"}' \
  $SEQUENCER_HOST:50051 sequencer.v2.Sequencer/GetCommitment
```

<Warning>
  Read `events_root` **from the chain**, not from this response. The API value is convenient for
  development; the on-chain value is what makes the check trust-minimised. If you compare an
  API-supplied root against an API-supplied proof, you have verified nothing.
</Warning>

## 2. Get the Merkle proof

```bash theme={null}
grpcurl -d '{"event_id":"<EVENT_UUID>"}' \
  $SEQUENCER_HOST:50051 sequencer.v2.Sequencer/GetInclusionProof
```

The response carries the sibling hashes along the path from your event's leaf to the root, and the
leaf's index — which tells you the order to concatenate at each level.

## 3. Recompute the root locally

Trees are SHA-256, but **every hash is domain-separated** — a bare `sha256(a || b)` will not
reproduce the root. The constants come from VES v1.0:

| Domain       | Prefix            | Used for                  |
| ------------ | ----------------- | ------------------------- |
| Leaf         | `VES_LEAF_V1`     | Event leaves              |
| Node         | `VES_NODE_V1`     | Internal nodes            |
| Padding leaf | `VES_PAD_LEAF_V1` | Padding to a power of two |

A leaf is not simply the hash of the payload. It commits to the stream identity and position too:

```
leaf_preimage = "VES_LEAF_V1"
             || UUID(tenant_id)
             || UUID(store_id)
             || U64_BE(sequence_number)
             || event_signing_hash   (32 bytes)
             || agent_signature      (64 bytes)

leaf_hash = SHA256(leaf_preimage)
```

<Warning>
  Leaves are **padded to the next power of two** with a constant
  `PAD_LEAF = SHA256("VES_PAD_LEAF_V1")`. Skip the padding and your root will be wrong for any batch
  whose event count isn't already a power of two.
</Warning>

```js theme={null}
import { createHash } from 'node:crypto';

const sha256 = (...parts) => {
  const h = createHash('sha256');
  for (const p of parts) h.update(p);
  return h.digest();
};

const DOMAIN_LEAF = Buffer.from('VES_LEAF_V1');
const DOMAIN_NODE = Buffer.from('VES_NODE_V1');
const PAD_LEAF    = sha256(Buffer.from('VES_PAD_LEAF_V1'));

const u64be = (n) => {
  const b = Buffer.alloc(8);
  b.writeBigUInt64BE(BigInt(n));
  return b;
};

function leafHash({ tenantId, storeId, sequenceNumber, eventSigningHash, agentSignature }) {
  return sha256(
    DOMAIN_LEAF,
    uuidBytes(tenantId),        // 16 bytes
    uuidBytes(storeId),         // 16 bytes
    u64be(sequenceNumber),
    eventSigningHash,           // 32 bytes
    agentSignature,             // 64 bytes
  );
}

const nodeHash = (left, right) => sha256(DOMAIN_NODE, left, right);

/** Walk sibling hashes from the leaf up to the root. */
function computeRoot(leaf, siblings, index) {
  let hash = leaf;
  for (const sibling of siblings) {
    hash = (index & 1)
      ? nodeHash(sibling, hash)   // we are the right child
      : nodeHash(hash, sibling);  // we are the left child
    index >>= 1;
  }
  return hash;
}
```

## 4. Compare

```js theme={null}
import { timingSafeEqual } from 'node:crypto';

const leaf     = leafHash(eventParams);
const computed = computeRoot(leaf, proof.siblings, proof.index);
const onChain  = await setRegistry.eventsRoot(batchId);   // read from the chain

const included = computed.length === onChain.length
  && timingSafeEqual(computed, onChain);

console.log(included ? 'included' : 'NOT included — do not trust this event');
```

If the roots match, the event is in the batch that was anchored on-chain. If they don't, either a
leaf field differs by one byte, the padding was skipped, or the event was never committed.

## Then check continuity

Inclusion proves your event is present. It does **not** prove nothing else was removed. Each
commitment chains via `prev_state_root`, so walk that chain in
[strict mode](/set/stateset-set-l2-verification#verify-state-continuity) when you need the history
to be complete rather than merely containing your event.

## Then check compliance

For encrypted payloads, inclusion says the event exists but nothing about whether a policy held.
That's what [compliance proofs](/set/stateset-set-l2-compliance-proof) are for.

## Related

* [Set L2 Verification](/set/stateset-set-l2-verification) — all three checks
* [Set L2 Architecture](/set/stateset-set-l2-architecture) — the commitment structure
