Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

EnergyProof CTC

EnergyProof turns an energy reading on Ethereum Sepolia into a verified energy credit on Creditcoin. Attestcoin verifies the source transaction before the Creditcoin ledger changes state.

The live prototype settled 10 readings from 3 producers for 12,975 Wh and rejected a replay with QueryAlreadyProcessed.

This documentation is published at passat-b6-tdi.github.io/energy-proof-ctc. The GitBook mirror is available at b0gdaniy.gitbook.io/energy-proof.

The main application is live at energy-proof.b0gdaniy.xyz. The source repository is github.com/passat-b6-tdi/energy-proof-ctc.

Start with How it works, then read the Attestcoin integration and Security invariants.

Architecture

Components

  Ethereum Sepolia (source, chain key 1)        Creditcoin CC3 Testnet (102031)
  ┌───────────────────────────────┐             ┌──────────────────────────────────┐
  │ EnergyMeter.sol               │             │ EnergyProofConsumer.sol          │
  │  recordProduction(EnergyParams)│            │  is AttestcoinReader             │
  │  emits EnergyProduced(        │             │  execute(chainKey, block, ...)  │
  │    oracle, producer,          │             │   1. computeQueryId              │
  │    readingId, wattHours)      │             │   2. reject if processed (replay)│
  └──────────────┬────────────────┘             │   3. VERIFIER.verifyAndEmit(...) │
                 │                              │   4. decode tx (EvmV1Decoder)   │
                 │  (1) tx + event log          │      require receiptStatus == 1 │
                 ▼                              │      find EnergyProduced log    │
  ┌───────────────────────────────┐             │      require source == METER    │
  │ worker/watch.ts               │             │   5. bounds-check wattHours     │
  │  poll EnergyProduced          │  (4) submit │   6. LEDGER.credit(...)         │
  │  generateProofFor(txHash) ────┼────proof───▶ └──────────────┬──────────────────┘
  │   - wait attested height      │                            │
  │   - proofBuilder.getProof     │             ┌──────────────▼──────────────────┐
  │  submit to consumer.execute   │             │ EnergyCreditLedger.sol          │
  └───────────────┬───────────────┘             │  credit(producer, wh, readingId,│
                  │                             │         queryId) onlyConsumer   │
                  │  (2) GET proof              │  settled[readingId] guard       │
                  ▼                             │  balanceOf[producer] += wh      │
  ┌───────────────────────────────┐             │  emits SettlementRecorded      │
  │ Attestcoin proof builder      │             └────────────────────────────────┘
  │ prover.cc3-testnet...         │
  │ + NativeQueryVerifier 0x..0FD2 │  (3) attestation reached on Creditcoin
  └───────────────────────────────┘

Flow (happy path)

  1. An oracle calls EnergyMeter.recordProduction(EnergyParams) on Sepolia. The contract emits EnergyProduced(msg.sender, producer, readingId, wattHours).
  2. worker/watch.ts sees the event, takes the tx hash, and calls the Attestcoin SDK: wait until the Sepolia block height is attested on Creditcoin (~8 min), then proofBuilder.getProof(txHash) returns { chainKey, headerNumber, txBytes, merkleProof, continuityProof }.
  3. The worker calls EnergyProofConsumer.execute(uint64 chainKey, uint64 blockHeight, bytes encodedTransaction, tuple merkleProof, tuple continuityProof) on Creditcoin with that proof data.
  4. AttestcoinReader computes queryId (chainKey|blockHeight|txIndex), rejects a replay, calls the 0x..0FD2 precompile verifyAndEmit, and requires it to return true.
  5. EnergyProofConsumer decodes the verified transaction bytes with EvmV1Decoder: requires receiptStatus == 1, finds the EnergyProduced log by signature, requires the emitting contract to equal the configured energyMeter address, and extracts (oracle, producer, readingId, wattHours) from the log topics/data.
  6. It bounds-checks wattHours (0 < wattHours <= MAX_WATT_HOURS) and calls EnergyCreditLedger.credit(producer, wattHours, readingId, queryId, chainKey, blockHeight).
  7. The ledger checks !settled[readingId], sets it, increments balanceOf[producer], stores structured settlementOf(readingId) data including source chain/block provenance, and emits SettlementRecorded.

Trust model

  • No trust in the worker. The worker only relays bytes; it cannot forge a credit. Every field the ledger acts on comes from a transaction the Attestcoin precompile has verified as included in an attested Sepolia block.
  • No trust in the caller of execute. Anyone may submit a valid proof; the proof is self-authenticating. Griefing (submitting someone else’s proof) only credits the rightful producer sooner and burns the caller’s gas.
  • Two independent replay guards. AttestcoinReader.processedQueries[queryId] (one reading per source transaction) and EnergyCreditLedger.settled[readingId] (per meter reading).
  • EnergyCreditLedger.credit requires CONSUMER_ROLE. Direct settlement is impossible.
  • Meter onboarding is permissioned. Register separates the admin, registrar, and oracle roles. This controls who may submit source readings; it does not replace Attestcoin proof verification.

Why this fits DePIN

EnergyMeter stands in for a metering device / sensor on one network; settlement and incentive accounting happen on Creditcoin, driven entirely by cross-chain data that Attestcoin has attested. The cross-chain decision does not trust a centralised relay; source-reading submission is separately controlled by the meter’s ORACLE_ROLE.

Attestcoin Protocol integration

This is the technical documentation required by the BUIDL CTC submission rules: what is set up, and how the project uses the Attestcoin Protocol as a core feature.

Status: verified on Creditcoin CC3 Testnet. Deployment addresses and a live end-to-end replay test are recorded in deployments/README.md.

Where Attestcoin sits in the design

EnergyProof’s entire settlement decision depends on Attestcoin readability. The Creditcoin contract EnergyProofConsumer cannot credit anyone without a proof that the NativeQueryVerifier precompile accepts. Attestcoin is not a side feature that could be removed — remove it and there is no verified input, so no credit.

  • Primitive used: readability (attestation + transaction proving). Writability is out of scope for this season and unused.
  • Source chain: Ethereum Sepolia, Attestcoin chain key 1.
  • Precompile: INativeQueryVerifier at 0x0000000000000000000000000000000000000FD2 on Creditcoin CC3 Testnet.
  • Off-chain: @gluwa/usc-sdk proof builder against https://prover.cc3-testnet.creditcoin.network.

On-chain: how the proof is consumed

consumer.execute(uint64 chainKey, uint64 blockHeight, bytes encodedTransaction, tuple merkleProof, tuple continuityProof):

  1. Compute a replay key queryId = keccak256(chainKey, blockHeight, txIndex) where txIndex = VERIFIER.calculateTxIndex(merkleProof). Revert if seen.
  2. VERIFIER.verifyAndEmit(chainKey, blockHeight, encodedTransaction, {root, siblings}, {lowerEndpointDigest, roots}). Revert unless it returns true. This is the Attestcoin check: inclusion in an attested source block plus attestation-chain continuity.
  3. Mark processedQueries[queryId] = true, emit QueryProcessed.
  4. Hand the now-verified transaction bytes to EnergyProofConsumer.

EnergyProofConsumer._onVerifiedTransaction then, using @gluwa/usc-contracts EvmV1Decoder on the verified bytes:

  • getTransactionType + isValidTransactionType — sanity on the tx envelope.
  • decodeReceiptFields(...).receiptStatus == 1 — the source tx must have succeeded; a reverted recordProduction() call settles nothing.
  • getLogsByEventSignature(receipt, ENERGY_PRODUCED_SIG) — find the EnergyProduced log; revert if absent. The source transaction must contain exactly one matching log, so one reading is processed per source transaction.
  • log.address_ == energyMeter — the log must come from our metering contract, not any other contract that emits the same signature.
  • log.topics == [sig, oracle, producer, readingId], log.data == abi.encode(wattHours) — shape check, then extract the four event fields.
  • 0 < wattHours <= MAX_WATT_HOURS — domain bounds.
  • EnergyCreditLedger.credit(producer, wattHours, readingId, queryId, chainKey, blockHeight) — records the credit under CONSUMER_ROLE; the structured settlementOf(readingId) value includes source chain/block provenance, and the ledger additionally rejects a repeated readingId.

Two independent replay guards: processedQueries[queryId] (per source transaction) and settled[readingId] (per meter reading).

Off-chain: how the proof is built

worker/watch.ts (see also worker/produce.ts):

  1. Poll Sepolia for EnergyProduced via queryFilter; dedupe by tx hash.
  2. generateProofFor(txHash, chainKey=1, PROOF_BUILDER_URL, creditcoinRpc, sepoliaRpc):
    • sepoliaRpc.getTransaction(txHash) → block number.
    • PrecompileChainInfoProvider(creditcoinRpc).getLatestAttestedHeightAndHash(1).
    • ProofBuilder(1, PROOF_BUILDER_URL).waitUntilHeightAttested(1, blockNumber, 15_000, 1_200_000) — typically ~8 minutes.
    • proofBuilder.getProof(txHash){ chainKey, headerNumber, txBytes, merkleProof{root,siblings}, continuityProof{lowerEndpointDigest,roots} }.
  3. Gas: try estimateGas; on the known precompile estimation revert, fall back to 21000 + continuityRoots.length * 5000 + 20000.
  4. consumer.execute(chainKey, headerNumber, txBytes, merkleProof, continuityProof, { gasLimit }).

The worker is untrusted infrastructure: it only relays bytes. It cannot forge a credit because every field the ledger acts on comes from a transaction the precompile verified.

Verified deployment

  • EnergyMeter: 0x0d1b7c614e07B47153293469d356b6bA80978BF1 on Sepolia.
  • EvmV1Decoder: 0xaDcDaBD5b96Af2c89829128321d913CF939d8604 on CC3.
  • EnergyCreditLedger: 0x100FEb2D822CBb32C4e8f047D43615AC8851Ed79 on CC3.
  • EnergyProofConsumer: 0x9e3743dEC51b82BD83d7fF7557650BF1C75ee096 on CC3.
  • CONSUMER_ROLE was granted to EnergyProofConsumer in transaction 0x284e3f2059891c3be0d51af44aaecc68157d5176395a066ebbf8380e591e340.

The demo settled 10 readings for 12,975 Wh. Replaying the first proof produced QueryAlreadyProcessed in transaction 0x9df20568f6787feac0a91d550782b96c27778df46912ee215b99e4d85133b302; the ledger balance and total stayed unchanged.

Setup checklist

  • .env from .env.example; Sepolia RPC + funded key; CC3 testnet CTC.
  • Obtain and link the USC EvmV1Decoder library on CC3 Testnet.
  • Deploy EnergyMeter on Sepolia.
  • Deploy EnergyCreditLedger on CC3 Testnet.
  • Deploy EnergyProofConsumer(sourceChainKey=1, energyMeter, ledger) on CC3 Testnet.
  • Grant CONSUMER_ROLE to the deployed consumer and verify ledger.hasRole(CONSUMER_ROLE, consumer).
  • Record all addresses in deployments/README.md.

Security invariants

Each invariant maps to at least one test. All listed invariants have automated coverage; the live replay transaction is recorded in deployments/README.md.

Verification gate

  • I1 No EnergyCreditLedger state change unless VERIFIER.verifyAndEmit returned true for the submitted proof. ✅ test_execute_invalidProofDoesNotChangeLedger
  • I2 A source transaction whose receipt status is not 1 settles nothing. ✅ test_execute_revertedSourceTxDoesNotSettle
  • I3 A verified transaction with no EnergyProduced log settles nothing. ✅ test_execute_withoutEnergyLogIsRejected
  • I4 A log with the right signature but emitted by an address other than the configured energyMeter settles nothing. ✅ test_execute_wrongEmitterDoesNotSettle
  • I5 chainKey != sourceChainKey reverts even if the proof verifies. ✅ test_execute_wrongChainIsRejected
  • I6 Malformed log shape (topics length, data length) reverts. ✅ test_execute_malformedTopicsAreRejected, test_execute_malformedDataIsRejected
  • I7 wattHours == 0 or wattHours > MAX_WATT_HOURS reverts. ✅ test_execute_zeroWattHoursIsRejected, test_execute_wattHoursAboveMaximumIsRejected

Replay

  • I8 The same proof submitted twice: second call reverts QueryAlreadyProcessed, ledger unchanged. ✅ test_execute_replayIsRejected and live replay transaction
  • I9 Two distinct proofs carrying the same readingId: second reverts ReadingAlreadySettled. ✅ EnergyCreditLedgerTest.test_credit_rejectsReplayOfReadingId
  • I10 EnergyMeter rejects a reused readingId at emission. ✅ EnergyMeterTest.test_recordProduction_revertsOnReusedReadingId

Access control

  • I11 EnergyCreditLedger.credit reverts for any caller without CONSUMER_ROLE. ✅ test_credit_onlyConsumer
  • I12 Only an account with DEFAULT_ADMIN_ROLE can grant CONSUMER_ROLE; a caller without that role cannot write to the ledger. ✅ test_onlyAdminCanGrantConsumerRole
  • I13 credit(address(0), ...) reverts. ✅ test_credit_rejectsZeroProducer

Value integrity

  • I14 balanceOf[producer] increases by exactly wattHours; totalCredited tracks the sum. ✅ test_credit_happyPath, test_credit_accumulatesAcrossReadings
  • I15 Event signature constant in EnergyProofConsumer equals keccak256("EnergyProduced(address,address,bytes32,uint32)"). ✅ EnergyMeterTest.test_eventSignatureMatchesConsumerConstant

Griefing (accepted, non-issues)

  • Anyone may call execute with someone else’s valid proof. Effect: the rightful producer is credited, the caller pays gas. No mitigation needed.
  • EnergyMeter.recordProduction is restricted to ORACLE_ROLE. A recorded reading still has to be attested and proven before it can settle.

Contract reference

The Solidity contracts are documented with NatSpec. Generate the full Foundry reference locally with:

npm run docs:contracts

This runs forge doc and writes the generated mdBook to docs/generated-contracts/. Serve it locally with:

forge doc --out docs/generated-contracts --serve --port 4000

The generated directory is intentionally excluded from Git because it is a build artifact. The source of truth is the NatSpec in contracts/.

Contracts

EnergyMeter

Source-chain meter for bounded production readings. An account with ORACLE_ROLE submits EnergyParams; every unique readingId emits one EnergyProduced event.

Register

Role registry inherited by EnergyMeter:

DEFAULT_ADMIN_ROLEREGISTER_ROLEORACLE_ROLE.

AttestcoinReader

Creditcoin base contract that verifies a source transaction through the native Attestcoin verifier and rejects a processed queryId.

EnergyProofConsumer

Validates the verified source transaction, requires exactly one event from the configured EnergyMeter, checks the event fields, and forwards the settlement with source provenance to the ledger.

EnergyCreditLedger

Creditcoin settlement ledger. Only CONSUMER_ROLE can create credits, and each readingId can be settled once. settlementOf(readingId) exposes the producer, amount, query ID, source chain key, and source block height.

Interfaces

  • IEnergyCreditLedger defines the settlement ABI and provenance record.
  • INativeQueryVerifierExpanded defines the Attestcoin precompile calls used by AttestcoinReader.

Foundry reference

Deployments and live evidence

Main application

Open EnergyProof

Contracts

ContractNetworkAddress
EnergyMeterEthereum Sepolia0x0d1b7c614e07B47153293469d356b6bA80978BF1
EvmV1DecoderCreditcoin CC3 Testnet0xaDcDaBD5b96Af2c89829128321d913CF939d8604
EnergyCreditLedgerCreditcoin CC3 Testnet0x100FEb2D822CBb32C4e8f047D43615AC8851Ed79
EnergyProofConsumerCreditcoin CC3 Testnet0x9e3743dEC51b82BD83d7fF7557650BF1C75ee096

The consumer has CONSUMER_ROLE on the ledger. Wiring transaction: 0x284e3f….

Demo evidence

  • 10 readings settled from 3 producers.
  • Total settled production: 12,975 Wh.
  • Source transaction: Sepolia.
  • Successful settlement: Creditcoin.
  • Rejected replay: Creditcoin.

The replay reverted with QueryAlreadyProcessed. The producer balance stayed at 3,075 and totalCredited stayed at 12,975.

Licenses and references

EnergyProof is released under the MIT license.

Dependencies

DependencyVersionPurposeLicense
@gluwa/usc-contracts0.1.2EvmV1Decoder for verified source transactionsMIT
@gluwa/usc-sdk0.18.0Attestcoin proof-builder clientSee package license
@openzeppelin/contracts5.4.0AccessControl role managementMIT
forge-std1.16.2Foundry testing utilitiesMIT / Apache-2.0

Protocol references

Source repositories