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)
- An oracle calls
EnergyMeter.recordProduction(EnergyParams)on Sepolia. The contract emitsEnergyProduced(msg.sender, producer, readingId, wattHours). worker/watch.tssees the event, takes the tx hash, and calls the Attestcoin SDK: wait until the Sepolia block height is attested on Creditcoin (~8 min), thenproofBuilder.getProof(txHash)returns{ chainKey, headerNumber, txBytes, merkleProof, continuityProof }.- The worker calls
EnergyProofConsumer.execute(uint64 chainKey, uint64 blockHeight, bytes encodedTransaction, tuple merkleProof, tuple continuityProof)on Creditcoin with that proof data. AttestcoinReadercomputesqueryId(chainKey|blockHeight|txIndex), rejects a replay, calls the0x..0FD2precompileverifyAndEmit, and requires it to returntrue.EnergyProofConsumerdecodes the verified transaction bytes withEvmV1Decoder: requiresreceiptStatus == 1, finds theEnergyProducedlog by signature, requires the emitting contract to equal the configuredenergyMeteraddress, and extracts(oracle, producer, readingId, wattHours)from the log topics/data.- It bounds-checks
wattHours(0 < wattHours <= MAX_WATT_HOURS) and callsEnergyCreditLedger.credit(producer, wattHours, readingId, queryId, chainKey, blockHeight). - The ledger checks
!settled[readingId], sets it, incrementsbalanceOf[producer], stores structuredsettlementOf(readingId)data including source chain/block provenance, and emitsSettlementRecorded.
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) andEnergyCreditLedger.settled[readingId](per meter reading). EnergyCreditLedger.creditrequiresCONSUMER_ROLE. Direct settlement is impossible.- Meter onboarding is permissioned.
Registerseparates 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:
INativeQueryVerifierat0x0000000000000000000000000000000000000FD2on Creditcoin CC3 Testnet. - Off-chain:
@gluwa/usc-sdkproof builder againsthttps://prover.cc3-testnet.creditcoin.network.
On-chain: how the proof is consumed
consumer.execute(uint64 chainKey, uint64 blockHeight, bytes encodedTransaction, tuple merkleProof, tuple continuityProof):
- Compute a replay key
queryId = keccak256(chainKey, blockHeight, txIndex)wheretxIndex = VERIFIER.calculateTxIndex(merkleProof). Revert if seen. VERIFIER.verifyAndEmit(chainKey, blockHeight, encodedTransaction, {root, siblings}, {lowerEndpointDigest, roots}). Revert unless it returnstrue. This is the Attestcoin check: inclusion in an attested source block plus attestation-chain continuity.- Mark
processedQueries[queryId] = true, emitQueryProcessed. - 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 revertedrecordProduction()call settles nothing.getLogsByEventSignature(receipt, ENERGY_PRODUCED_SIG)— find theEnergyProducedlog; 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 underCONSUMER_ROLE; the structuredsettlementOf(readingId)value includes source chain/block provenance, and the ledger additionally rejects a repeatedreadingId.
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):
- Poll Sepolia for
EnergyProducedviaqueryFilter; dedupe by tx hash. 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} }.
- Gas: try
estimateGas; on the known precompile estimation revert, fall back to21000 + continuityRoots.length * 5000 + 20000. 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:0x0d1b7c614e07B47153293469d356b6bA80978BF1on Sepolia.EvmV1Decoder:0xaDcDaBD5b96Af2c89829128321d913CF939d8604on CC3.EnergyCreditLedger:0x100FEb2D822CBb32C4e8f047D43615AC8851Ed79on CC3.EnergyProofConsumer:0x9e3743dEC51b82BD83d7fF7557650BF1C75ee096on CC3.CONSUMER_ROLEwas granted toEnergyProofConsumerin transaction0x284e3f2059891c3be0d51af44aaecc68157d5176395a066ebbf8380e591e340.
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
-
.envfrom.env.example; Sepolia RPC + funded key; CC3 testnet CTC. - Obtain and link the USC
EvmV1Decoderlibrary on CC3 Testnet. - Deploy
EnergyMeteron Sepolia. - Deploy
EnergyCreditLedgeron CC3 Testnet. - Deploy
EnergyProofConsumer(sourceChainKey=1, energyMeter, ledger)on CC3 Testnet. - Grant
CONSUMER_ROLEto the deployed consumer and verifyledger.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
EnergyCreditLedgerstate change unlessVERIFIER.verifyAndEmitreturnedtruefor the submitted proof. ✅test_execute_invalidProofDoesNotChangeLedger - I2 A source transaction whose receipt status is not
1settles nothing. ✅test_execute_revertedSourceTxDoesNotSettle - I3 A verified transaction with no
EnergyProducedlog settles nothing. ✅test_execute_withoutEnergyLogIsRejected - I4 A log with the right signature but emitted by an address other than the
configured
energyMetersettles nothing. ✅test_execute_wrongEmitterDoesNotSettle - I5
chainKey != sourceChainKeyreverts 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 == 0orwattHours > MAX_WATT_HOURSreverts. ✅test_execute_zeroWattHoursIsRejected,test_execute_wattHoursAboveMaximumIsRejected
Replay
- I8 The same proof submitted twice: second call reverts
QueryAlreadyProcessed, ledger unchanged. ✅test_execute_replayIsRejectedand live replay transaction - I9 Two distinct proofs carrying the same
readingId: second revertsReadingAlreadySettled. ✅EnergyCreditLedgerTest.test_credit_rejectsReplayOfReadingId - I10
EnergyMeterrejects a reusedreadingIdat emission. ✅EnergyMeterTest.test_recordProduction_revertsOnReusedReadingId
Access control
- I11
EnergyCreditLedger.creditreverts for any caller withoutCONSUMER_ROLE. ✅test_credit_onlyConsumer - I12 Only an account with
DEFAULT_ADMIN_ROLEcan grantCONSUMER_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 exactlywattHours;totalCreditedtracks the sum. ✅test_credit_happyPath,test_credit_accumulatesAcrossReadings - I15 Event signature constant in
EnergyProofConsumerequalskeccak256("EnergyProduced(address,address,bytes32,uint32)"). ✅EnergyMeterTest.test_eventSignatureMatchesConsumerConstant
Griefing (accepted, non-issues)
- Anyone may call
executewith someone else’s valid proof. Effect: the rightful producer is credited, the caller pays gas. No mitigation needed. EnergyMeter.recordProductionis restricted toORACLE_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_ROLE → REGISTER_ROLE → ORACLE_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
IEnergyCreditLedgerdefines the settlement ABI and provenance record.INativeQueryVerifierExpandeddefines the Attestcoin precompile calls used byAttestcoinReader.
Foundry reference
Deployments and live evidence
Main application
Contracts
| Contract | Network | Address |
|---|---|---|
| EnergyMeter | Ethereum Sepolia | 0x0d1b7c614e07B47153293469d356b6bA80978BF1 |
| EvmV1Decoder | Creditcoin CC3 Testnet | 0xaDcDaBD5b96Af2c89829128321d913CF939d8604 |
| EnergyCreditLedger | Creditcoin CC3 Testnet | 0x100FEb2D822CBb32C4e8f047D43615AC8851Ed79 |
| EnergyProofConsumer | Creditcoin CC3 Testnet | 0x9e3743dEC51b82BD83d7fF7557650BF1C75ee096 |
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
| Dependency | Version | Purpose | License |
|---|---|---|---|
@gluwa/usc-contracts | 0.1.2 | EvmV1Decoder for verified source transactions | MIT |
@gluwa/usc-sdk | 0.18.0 | Attestcoin proof-builder client | See package license |
@openzeppelin/contracts | 5.4.0 | AccessControl role management | MIT |
forge-std | 1.16.2 | Foundry testing utilities | MIT / Apache-2.0 |
Protocol references
- Creditcoin USC / Attestcoin documentation
- Creditcoin USC chain environments
- Foundry
forge docreference