This endpoint carries no consensus weight, and nothing about it is on-chain.
  • The signature is verified with ethers.verifyMessage inside the indexer process (yscan/indexer/src/attestations.ts), not by an on-chain ecrecover.
  • The accepted result is written into an off-chain SQLite table (validator_attestations), not into contract storage.
  • The block was already finalised by the four server validators before the phone ever saw its hash, so the signature cannot influence whether it is accepted.
  • The endpoint does not check the 10,000 YCE bond in ValidatorRegistry, or any other stake. An unbonded address is accepted exactly like a bonded one.
Treat it as what it is: an off-chain endorsement and liveness signal, useful for dashboards and participation counters. It is not validation.

What it actually does

  1. The app polls the latest block (~5s) and takes its hash from eth_getBlockByNumber.
  2. The wallet signs that hash string with EIP-191 personal_sign (wallet.signMessage(blockHash)).
  3. POST /api/validator-attest sends { blockNumber, blockHash, signature, signerAddress }.
  4. The indexer recovers the signer with ethers.verifyMessage and rejects the request if it does not equal signerAddress.
  5. 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.
  6. It inserts a row into validator_attestationsUNIQUE(block_number, attestor), so a repeat is a no-op.
Every one of those steps happens after finality, on the server, over data the server itself supplied. The signature proves the phone holds the key and echoed a hash it was given. It does not prove the phone verified anything, and it changes no on-chain state.
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
Body 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). signMessage applies the EIP-191 personal_sign prefix, and the server uses ethers.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 with duplicate: 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.