Vestige protocol · v1.0.0

Proof of interaction.

Two keys co-sign a recent block. The chain records that they met, and when. Passkeys and device keys, no server to trust.

One address, every chain
0xc5b8A9e7175c102d51116249F121A65079224b6f
The contract
  • No owner, no admin, no upgrades
  • Verified, same CREATE2 address everywhere
  • Under 100k gas with the P-256 precompile

01 / How it works

A block hash is a timestamp nobody can fake

No one can predict a block's hash before the block exists. Sign over it and you prove you signed after that block. Get included in a later block and you prove it happened before that one. Vestige brackets an encounter between those two blocks.

The window of a co-signature A signs the hash of block n. B co-signs and the transaction is included at block n plus age. The contract accepts it only while age is at most the maxAge that A signed. block n A signs its hash n + age B co-signs, cosign() included n + maxAge window closes: StaleBlock maxAge is inside what A signs: nobody downstream can widen it
  1. The signer states

    A signs block n, its hash, and a window of maxAge blocks, bound to this chain and this contract. A can be a passkey or a raw P-256 device key.

  2. The cosigner answers

    B's passkey signs a challenge built from A's statement and A's key. It needs a fingerprint, a face or a PIN: the contract requires user verification.

  3. The chain records

    Anyone submits cosign(). The contract checks the hash, the window and both signatures, then emits Cosigned. A relayer can pay the gas but cannot change a thing.

02 / Two modes

A door that signs, or a hand to shake

The contract only knows signature formats. How you present them to people is the product.

Beacon · SignatureKind.P256

An unattended device signs every block

A kiosk, door reader or sensor holds a P-256 key and re-signs the latest block as it arrives. A visitor scans its QR code or taps its NFC tag and co-signs with a passkey. The device needs no wallet and no gas.

  • Check-ins and proof of attendance
  • Custody handovers at a fixed point
  • Device attestations from secure hardware

Handshake · SignatureKind.WebAuthn

Two people, two passkeys

A taps once to sign the statement with their passkey. B then has the window A declared to co-sign with theirs. Nobody installs a wallet; the phone's own authenticator does the cryptography.

  • Meeting someone at an event
  • Courier to recipient handoffs
  • Peer-to-peer vouching

03 / Guarantees

What a Cosigned event proves

Every check runs on-chain and reverts with a named error. The lab lets you try each attack and watch the contract refuse it.

Proves

  • After block n. B signed over A's statement, which carries a hash no one could know earlier.
  • Within A's window. The inclusion block is at most maxAge blocks later, and maxAge is signed by A.
  • These two keys. A forged hash, a swapped key, a replay or a self co-sign each fails with its own error.
  • A real person for B. Passkey assertions must carry the user-verification flag.

Does not prove

  • Physical proximity. That comes from the transport: a QR on a wall, an NFC tap, a BLE advert. Choose one that fits your threat model.
  • Who owns a key. Mapping keys to people, venues or rewards is your application's job.
  • Anything on chains with dishonest blockhash. Arbitrum, for one, returns pseudo-random hashes; Vestige is not deployed there.

04 / Integrate

Read the record, or gate on it

verify() is a view with every check and no state change; cosign() also records. Both return the cosignId and the age in blocks, so your contract can demand a tighter window than A's.

YourCheckIn.sol
// Only this venue's device, at most 10 blocks old.
(bytes32 cosignId, uint256 age) =
    vestige.verify(statement, cosigner, auth);
require(vestige.keyId(statement.signer) == venueKey);
require(age <= 10, "too old");
require(!used[cosignId]);
used[cosignId] = true;
read-record.js · ethers v6
const vestige = new ethers.Contract(
  "0xc5b8A9e7175c102d51116249F121A65079224b6f",
  abi, provider);
// Every encounter a given passkey co-signed:
const q = vestige.filters.Cosigned(null, null, keyId);
const logs = await vestige.queryFilter(q, from);
for (const log of logs) {
  const { signerKey, age } = log.args;
}

The introduction walks through building both signatures in a browser →

05 / Deployments

Live and verified

Deployed through Arachnid's CREATE2 factory with a zero salt, so the address commits to the bytecode. Anyone can put Vestige on a new EVM chain, and only this exact code can land there.

solc 0.8.28 · optimizer 200 runs · cancun · runtime keccak 0xfa43f529…a1fde02e

Co-sign a block with your own passkey.

The lab runs the whole ceremony in your browser against Base Sepolia, Monad testnet or Sepolia. Signing and verify() are free; recording needs a little testnet gas in a browser wallet.