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

# Starknet Verifier

> Integrate Molpha's Cairo verifier: call verify(), map gateway payloads, and own freshness/replay in your consumer contract.

Source: [`molpha/molpha-starknet`](https://github.com/molpha/molpha-starknet) · Cairo / Scarb `2.18+` · snforge `0.61+`

The Starknet Verifier is a **stateless** on-chain check: given a `DataUpdate` and an aggregate Schnorr signature, it answers one question — *did enough selected Molpha nodes sign this exact payload under the stated registry version?* It does not store feeds, rounds, or consumed updates. Your contract owns freshness, replay, feed authorization, and value decoding.

<Note>
  **Solana is the canonical chain.** Feeds, staking, and the authoritative node registry live on Solana. Each Starknet deployment mirrors the node public-key set as versioned per-`(version, index)` coordinate maps and exposes `verify` as a view entrypoint. The signed message intentionally omits `chainId`, so one gateway payload verifies on Starknet and every supported EVM chain unchanged.
</Note>

## Quick start

1. Pin a deployed verifier address from [Deployments](/verifiers/deployments).
2. Map a gateway `DataUpdateResult` with [`buildStarknetVerifierArgs`](/sdk/reference#buildstarknetverifierargs) (or build the structs yourself).
3. Call `verify` and **require the boolean** — a bad signature returns `false`, it does not revert.
4. Enforce your own freshness / replay / `feed_id` checks before using `value`.

```cairo theme={null}
use verifier::interface::{
    DataUpdate, IVerifierDispatcher, IVerifierDispatcherTrait, SchnorrSignature,
};

let v = IVerifierDispatcher { contract_address: VERIFIER };
assert(v.verify(data_update, sig), 'molpha: not verified');
assert(data_update.feed_id == EXPECTED_FEED, 'molpha: wrong feed');
// enforce staleness / replay, then decode data_update.value and settle
```

Full consumer walkthrough: [Verify on Starknet](/guides/verify-on-starknet).

***

## What the contract does (and does not)

| Does                                                                                      | Does not                                              |
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| Mirror up to 256 node pubkeys in versioned storage maps                                   | Aggregate signatures off-chain or publish feed values |
| Derive a deterministic per-round signer group                                             | Enforce timestamp freshness or replay windows         |
| Sum submitted signers' keys and check one Schnorr signature via native secp256k1 syscalls | Decide whether a `feed_id` is authorized for your app |
| Keep historical registry versions permanently verifiable                                  | Persist that an update was consumed                   |

Shared crypto (selection, message hash, Schnorr identity): [Cryptography](/protocol/cryptography). On Starknet the final curve check computes `s·G + (Q−e)·P` directly and compares its Ethereum-style address to the commitment — mathematically identical to the EVM `ecrecover` path.

***

## Install / depend on the contracts

```bash theme={null}
git clone https://github.com/molpha/molpha-starknet.git
cd molpha-starknet
scarb build
snforge test
cd verifier && scarb run bench   # optional: L2 gas benchmarks
```

As a Scarb dependency (example):

```toml theme={null}
[dependencies]
verifier = { git = "https://github.com/molpha/molpha-starknet.git" }
```

```cairo theme={null}
use verifier::interface::{
    DataUpdate, IVerifierDispatcher, IVerifierDispatcherTrait, SchnorrSignature,
};
use verifier::verifier::Verifier;
```

Key paths in the repo:

| Path                                   | Purpose                               |
| -------------------------------------- | ------------------------------------- |
| `verifier/src/verifier.cairo`          | Registry + `verify`                   |
| `verifier/src/interface.cairo`         | Structs and `IVerifier` API           |
| `verifier/src/schnorr.cairo`           | secp256k1 Schnorr via native syscalls |
| `verifier/src/node_group_bitmap.cairo` | Unbiased signer-group derivation      |
| `verifier/src/secp256k1_utils.cairo`   | Decompress, EC add/mul, eth-address   |
| `scripts/deploy_testnet.sh`            | Sepolia deploy                        |
| `scripts/add_node.sh`                  | Single-node registration + PoP        |

***

## Calldata: structs

Defined in `interface.cairo`.

### `DataUpdate`

```cairo theme={null}
struct DataUpdate {
    feed_id: u256,             // chain-agnostic feed id
    registry_version: u32,     // snapshot the signature was built against
    signatures_required: u32,  // threshold for this update
    value: u256,               // opaque 32-byte result (feed-defined encoding)
    canonical_timestamp: u64,  // unix seconds; part of the signed message
}
```

### `SchnorrSignature`

```cairo theme={null}
struct SchnorrSignature {
    signature: u256,      // aggregate Schnorr scalar `s`
    commitment: felt252,  // eth-address of nonce point `R` (low 160 bits)
    signers_bitmap: u256, // bit (i-1) set iff 1-based registry index i signed
}
```

### Gateway → on-chain mapping

| Gateway `DataUpdateResult` | On-chain field                    |
| -------------------------- | --------------------------------- |
| `feedId`                   | `DataUpdate.feed_id`              |
| `registryVersion`          | `DataUpdate.registry_version`     |
| `signaturesRequired`       | `DataUpdate.signatures_required`  |
| `valuePacked`              | `DataUpdate.value`                |
| `timestamp`                | `DataUpdate.canonical_timestamp`  |
| `s`                        | `SchnorrSignature.signature`      |
| `commitmentAddr`           | `SchnorrSignature.commitment`     |
| `signersBitmap`            | `SchnorrSignature.signers_bitmap` |

```ts theme={null}
import { buildStarknetVerifierArgs } from "@molpha/sdk";

const { dataUpdate, schnorrSignature } = buildStarknetVerifierArgs(result);
// pass into verifier.verify(dataUpdate, schnorrSignature) via starknet.js / your dispatcher
```

***

## `verify`

```cairo theme={null}
fn verify(
    self: @TContractState,
    data_update: DataUpdate,
    schnorr_data: SchnorrSignature,
) -> bool;
```

### Pipeline (in order)

1. `data_update.registry_version <=` latest stored version (else `'Invalid registry version'`).
2. Snapshot has at least one node (else `'No nodes'`).
3. Structural guards: non-zero `signatures_required`, `signers_bitmap`, `signature`, `commitment`.
4. `popcount(signers_bitmap) >= signatures_required` (else `'Not enough signatures'`).
5. Derive selection set; require `signers_bitmap ⊆ selection_bitmap` (else `'Signer not selected'`).
6. Sum signers' public keys in ascending index order; verify Schnorr over the canonical message.
7. Return `true` / `false` for crypto validity — **do not treat `false` as a revert**.

<Warning>
  Always `assert(verifier.verify(...))`. A cryptographically invalid signature returns `false` without reverting. Structural / policy failures revert with short panic strings.
</Warning>

### Signed message

```text theme={null}
message = keccak256(
  keccak256("MOLPHA_MESSAGE_V1") ‖
  feed_id ‖
  registry_version ‖
  signatures_required ‖
  signers_bitmap ‖
  value ‖
  canonical_timestamp
)
```

Big-endian field packing matches the EVM `abi.encodePacked` layout. No `chainId`. Same digest verifies on every EVM and Starknet deployment.

### Selection seed and group size

```text theme={null}
selection_seed = keccak256(
  keccak256("MOLPHA_SELECTION_V1") ‖
  feed_id ‖
  registry_version ‖
  canonical_timestamp
)

group_size = min(signatures_required + redundancy_buffer, node_count)
```

`node_group_bitmap` expands the seed with `keccak256(seed ‖ keccak256("MOLPHA_SELECTION_DERIVE") ‖ counter)`, samples without replacement (bias-rejecting `u32` limbs), and uses the complement path when `group_size > node_count / 2`.

### Bitmap convention

**Bit `i - 1` ↔ 1-based registry index `i`.** Index `0` in each snapshot is the running aggregate key, not a signer. Node indices can change after `remove_node` (swap-and-pop); resolve live indices with `get_node_index(node)`.

***

## Consumer checklist

Do these in your contract (or off-chain client) around every `verify` call:

1. **Require** the boolean return value.
2. **Authorize** `feed_id` (and optionally `signatures_required`) for your product.
3. **Freshness** — compare `canonical_timestamp` to your clock / block time.
4. **Replay** — track last timestamp / digest if the same valid payload must not settle twice.
5. **Decode** `value` with your feed's encoding; treat it as opaque `u256` until then.
6. Pass the **exact** `registry_version` used when the nodes signed (historical versions stay valid).

***

## Registry & admin API

Only `protocol_admin` can mutate the registry or redundancy buffer.

| Function                                   | Notes                                                                      |
| ------------------------------------------ | -------------------------------------------------------------------------- |
| `add_node(compressed_pubkey, pop)`         | Decompresses key, checks PoP, appends, copies prior set into a new version |
| `remove_node(node)`                        | Subtracts key, swap-and-pop if not tail, new version                       |
| `set_redundancy_buffer(u256)`              | Admin-set; used as `group_size = signatures_required + buffer`             |
| `transfer_protocol_admin(ContractAddress)` | Non-zero address required                                                  |

### Proof-of-possession (registration only)

```text theme={null}
pop_digest = keccak256(
  keccak256("MOLPHA_VALIDATOR_V1") ‖
  contract_address ‖   // 32-byte Starknet address
  compressed_pubkey
)
```

PoP is verified at `add_node` and is not part of `verify`. The EVM verifier hashes a 20-byte `address(this)` instead; nodes register per deployment with a chain-specific PoP. Cross-chain `verify` payloads are unaffected.

### Reads

| Function                                           | Returns                                      |
| -------------------------------------------------- | -------------------------------------------- |
| `get_registry_version()`                           | Latest version index                         |
| `get_total_nodes()`                                | Active node count (latest)                   |
| `is_node(felt252)` / `get_node_index(felt252)`     | Membership / 1-based index (`0` if inactive) |
| `get_aggregate_key()`                              | Plain-sum `(x, y)` of the latest set         |
| `get_protocol_admin()` / `get_redundancy_buffer()` | Public getters                               |

Registry version `0` is the empty constructor snapshot. See [Registry Versions](/concepts/registry-versions).

### Storage model

Unlike EVM SSTORE2 blobs, Cairo stores each key as `key_x` / `key_y` maps keyed by packed `(version, index)`. Every `add_node` / `remove_node` copies the prior node set forward into a new immutable version — same permanence guarantee as EVM.

***

## Errors

`verify` / admin paths panic with these short strings when inputs are malformed or policy fails. Invalid crypto after passing guards returns `false` instead.

| Panic string                                                                                                                  | Typical cause                        |
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `'Invalid registry version'`                                                                                                  | Version greater than latest          |
| `'No nodes'`                                                                                                                  | Empty registry for that version      |
| `'Zero signatures required'` / `'Zero signers bitmap'` / `'Zero signature'` / `'Zero commitment'`                             | Zeroed fields                        |
| `'Not enough signatures'`                                                                                                     | Bitmap popcount below threshold      |
| `'Signer not selected'`                                                                                                       | Signer outside derived selection set |
| `'Not protocol admin'` / `'Zero admin'` / `'Node already added'` / `'Not node'` / `'Max nodes reached'` / `'Invalid PoP'` / … | Admin / registration failures        |

***

## Gas (reference)

Measured with `scarb run bench` / `snforge test benchmarks --gas-report` (snforge `0.61`). Baseline fixture: **10 nodes**, `redundancy_buffer=2`, `signatures_required=3`, 5 signers.

| Selector                | L2 gas (approx.)  |
| ----------------------- | ----------------- |
| `verify` (success)      | \~24.7M           |
| `add_node` (1st / 10th) | \~22.8M / \~25.6M |
| `remove_node`           | \~5.1M            |
| `get_total_nodes`       | \~49k             |
| `get_aggregate_key`     | \~116k            |

Signer-scaling for `verify` (18-node registry, redundancy buffer `2`):

| Signers |     L2 gas | group\_size |
| ------: | ---------: | ----------: |
|       3 | 24 469 138 |           5 |
|       5 | 24 871 970 |           5 |
|       9 | 27 487 149 |           9 |
|      12 | 26 560 466 |          12 |
|      18 | 25 540 671 |          18 |

Cost is not strictly linear in signer count: `node_group_bitmap::derive` switches algorithms when `group_size > n/2` or `= n`. Aggregation + Schnorr still dominate.

***

## Deploy & operate nodes

Deploy to Starknet Sepolia with `scripts/deploy_testnet.sh` (requires a configured `sncast` account and `PROTOCOL_ADMIN` in `.env`). Default redundancy buffer is `2` (`REDUNDANCY_BUFFER`).

```bash theme={null}
cp .env.example .env
# set PROTOCOL_ADMIN, optional REDUNDANCY_BUFFER / STARKNET_RPC_URL
./scripts/deploy_testnet.sh
```

Register nodes (admin only) with `scripts/add_node.sh` — signs a secp256k1 PoP and invokes `add_node`. Prefer passing public PoP material in production; never commit private keys.

```bash theme={null}
npm install --prefix scripts
./scripts/add_node.sh
# or:
./scripts/add_node.sh 0xVERIFIER_ADDRESS 0xNODE_PRIVATE_KEY
```

Addresses and testnet status: [Deployments](/verifiers/deployments).

***

## Security surface

* Treat every deployment as a **verification component**, not a full oracle product. Review registry ops, consumer freshness/replay, and feed authorization before production use.
* Cross-chain parity: unmodified EVM-produced signatures are accepted in `parity.cairo` / `e2e.cairo` tests.
* Protocol trust assumptions: [Security Model](/protocol/security-model).
