What it actually does
- The app polls the latest block (~5s) and takes its hash from
eth_getBlockByNumber. - The wallet signs that hash string with EIP-191
personal_sign(wallet.signMessage(blockHash)). POST /api/validator-attestsends{ blockNumber, blockHash, signature, signerAddress }.- The indexer recovers the signer with
ethers.verifyMessageand rejects the request if it does not equalsignerAddress. - It re-fetches the block from the node and rejects the request if the claimed hash does not match the canonical one, or if the block is more than 200 behind head.
- It inserts a row into
validator_attestations—UNIQUE(block_number, attestor), so a repeat is a no-op.
For the design that gives a device real, bonded, slashable weight — a light client that
verifies epoch headers against the BLS validator set it tracks independently, a
DeviceRegistry bond, checkpoint attestation with a fraud-proof window, and committee
sampling — see docs/DEVICE-VALIDATOR-LIGHT-CLIENT.md in the repository. That document
also benchmarks how Ethereum sync committees, Celo Plumo, Celestia DAS and Algorand sortition
handle the same problem. Replacing this endpoint with that flow is an open item there.Endpoints
POST /api/validator-attest
Submit a signed block hash.
Request
Responses
There is no bond, stake, jail or registry check anywhere in this handler.
GET /api/validator-attest/me/:address
Endorsement counters for a single address.
GET /api/validator-attest/recent?limit=50
Most recent endorsements across all submitters (max 200).
GET /api/validator-attest/leaderboard
Top 50 submitters by row count.
GET /api/validator-attest/stats
Totals across the table.
Client example (TypeScript + ethers v6)
Notes
- Sign the block hash string, e.g.
wallet.signMessage(block.hash).signMessageapplies the EIP-191 personal_sign prefix, and the server usesethers.verifyMessage, which expects exactly that. - The hash must come from the canonical chain. A stale or reorged hash fails the server-side
comparison against
eth_getBlockByNumber. - Freshness gate: blocks more than 200 behind head are rejected, which limits bulk backfilling of old signatures — it does not make forged participation expensive, because there is no bond.
(blockNumber, attestor)is unique; re-submitting returns 200 withduplicate: true.- Do not derive validator counts, security claims or “N validators” figures from this table. The number that would mean something is bonded, slashable, sampled weight, which this endpoint does not measure.