Skip to main content
Source: 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.
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.

Quick start

  1. Pin a deployed verifier address from Deployments.
  2. Map a gateway DataUpdateResult with 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.
Full consumer walkthrough: Verify on Starknet.

What the contract does (and does not)

Shared crypto (selection, message hash, Schnorr identity): 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

As a Scarb dependency (example):
Key paths in the repo:

Calldata: structs

Defined in interface.cairo.

DataUpdate

SchnorrSignature

Gateway → on-chain mapping


verify

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.
Always assert(verifier.verify(...)). A cryptographically invalid signature returns false without reverting. Structural / policy failures revert with short panic strings.

Signed message

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

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.

Proof-of-possession (registration only)

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

Registry version 0 is the empty constructor snapshot. See 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.

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. Signer-scaling for verify (18-node registry, redundancy buffer 2): 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).
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.
Addresses and testnet status: 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.