Documentation

Architecture Overview

The Energy Attestation Service (EnergyAS) is built as a two-contract system on top of the Ethereum Attestation Service (EAS). This separation of concerns enables upgradeable validation logic without migrating state.

System Layers

The protocol consists of four layers, each with a distinct responsibility:

┌─────────────────────────────────────────────┐
│  EnergyAS Protocol (your application)       │
│  Watchers → Projects → Attestations         │
├─────────────────────────────────────────────┤
│  EnergyAttestationResolver (replaceable)    │
│  Validates attestation data + permissions   │
├─────────────────────────────────────────────┤
│  EnergyRegistry (permanent)                 │
│  Stores all state, events, and accumulators │
├─────────────────────────────────────────────┤
│  Ethereum Attestation Service (EAS)         │
│  Permissionless attestation protocol        │
└─────────────────────────────────────────────┘

Data Flow

When an attester submits energy readings, the data flows through the stack:

Attester calls eas.attest(schemaUID, data)
  → EAS stores the attestation
  → EAS calls resolver.onAttest(attestation)
    → Resolver decodes and validates the data
    → Resolver checks attester authorization
    → Resolver computes energyWh and toTimestamp
    → Resolver calls registry.recordAttestation(...)
      → Registry checks for duplicate periods
      → Registry updates energy accumulators
      → Registry emits EnergyAttested event

Record Lifecycle

Understanding what happens to a record from submission through correction helps when building on top of EnergyAS.

                    ┌─────────────────────────┐
                    │  Device / Script        │
                    │  calls sdk.attest()     │
                    └──────────┬──────────────┘
                               │
                    ┌──────────▼──────────────┐
                    │  EAS contract           │
                    │  stores raw attestation │
                    │  calls onAttest()       │
                    └──────────┬──────────────┘
                               │
                    ┌──────────▼──────────────┐
                    │  Resolver               │
                    │  validates permissions  │
                    │  decodes data           │
                    │  computes energyWh      │
                    └──────────┬──────────────┘
                               │
                    ┌──────────▼──────────────┐
                    │  Registry               │
                    │  checks period lock     │
                    │  updates accumulators   │
                    │  emits EnergyAttested   │
                    └──────────┬──────────────┘
                               │
              ┌────────────────┼────────────────┐
              │                │                │
   ┌──────────▼─────┐  ┌───────▼──────┐  ┌─────▼──────────┐
   │  On-chain      │  │  Subgraph    │  │  Accumulators  │
   │  EAS store     │  │  indexes     │  │  updated       │
   │  UID is final  │  │  within ~1min│  │  immediately   │
   └────────────────┘  └──────────────┘  └────────────────┘

Correction lifecycle

When a record needs to be corrected, the replacement flows through the same stack but carries the original UID as refUID:

overwriteAttestation({ refUID: originalUID, ... })
  → Resolver validates same project + period
  → Registry marks original as replaced
  → Registry applies delta to accumulators (newWh - oldWh)
  → Emits EnergyReplaced(oldUID, newUID, delta)
  → Subgraph sets original.replaced = true, links replacedBy / replaces

The original record is never deleted — it remains permanently on-chain and visible in the subgraph with replaced: true. This gives auditors a complete correction history.

Accumulator updates

Every submission immediately updates four counters in the Registry:

  • totalGeneratedWh per project (generators only)
  • totalConsumedWh per project (consumers only)
  • totalGeneratedWhByWatcher per organization
  • totalConsumedWhByWatcher per organization

Corrections update accumulators by delta: if the original reported 500 kWh and the correction reports 490 kWh, the accumulators decrease by 10 kWh. This happens atomically in the same transaction.

Subgraph indexing

The subgraph listens to EnergyAttested, EnergyReplaced, and all registry management events. Indexing typically takes under a minute after transaction confirmation. The subgraph also maintains DailyEnergySnapshot entities — pre-aggregated daily totals per project — updated automatically on each new record.

Design Principles

The architecture follows three core principles:

Permanent state, replaceable logic. The EnergyRegistry holds all data and is never replaced. The EnergyAttestationResolver contains validation logic and can be swapped without any data migration.

On-chain verification, off-chain evidence. Core data (readings, timestamps, energy totals) lives on-chain. Supporting evidence (audit PDFs, IoT exports) is anchored via optional IPFS metadata URIs.

Permissionless with tenant isolation. Anyone can register and participate, but each watcher's data is completely isolated from others at the EVM level.

Note

The registry contract uses OpenZeppelin's Ownable2Step for safe ownership transfer and the resolver uses Pausable for emergency stops — both are battle-tested patterns.