The ledger
The record format
The exact shape of a meter record: the EIP-712 domain and types, the rules for each field, how records chain together, and how an hour of records becomes one root on the Ledger.
This is the exact shape of a meter record: how it is encoded, what it signs, how records chain together, and how an hour of records at one site becomes a single root on Ethereum. It is the reference the verifier is built from.
Files and encoding
One record is one JSON file for one machine and one UTC hour. Files are UTF-8 with no byte order mark. Money is in USDC base units (6 decimals) written as decimal strings, so no reader ever rounds through a floating point number. Time is UTC. Energy is whole watt-hours. See Reading a record for a complete example.
The typed data
Records are signed as EIP-712 typed data with the custodian's secp256k1 key. The domain binds a signature to mainnet and to the Ledger contract, so it cannot be replayed on another chain or for another deployment.
const domain = {
name: "CoresRent Meter",
version: "1",
chainId: 1,
verifyingContract: "0x47ac525AA85Efe47f42BB213Ca9e38B5f5C4DD83", // Ledger
} as const;
const types = {
MeterRecord: [
{ name: "machineId", type: "string" },
{ name: "site", type: "string" },
{ name: "hourStart", type: "uint64" },
{ name: "samples", type: "uint16" },
{ name: "gpuSeconds", type: "uint32" },
{ name: "energyWh", type: "uint32" },
{ name: "rateUsdcPerGpuHour", type: "uint64" },
{ name: "grossUsdc", type: "uint64" },
{ name: "tenantRef", type: "bytes32" },
{ name: "prevHash", type: "bytes32" },
],
} as const;In the JSON file, hourStart is written as an ISO 8601 string and signed as Unix seconds. version, custodian and signature sit beside the signed fields and are not part of the typed data.
Field rules
| Field | Type | Rule |
|---|---|---|
| machineId | string | Pattern CR-SITE-RACK-SLOT, e.g. CR-IAD4-07-04. Must exist in the Registry. |
| site | string | A site code from the Registry, e.g. IAD-4. |
| hourStart | uint64 | Unix seconds, a multiple of 3,600. |
| samples | uint16 | 0 to 60. |
| gpuSeconds | uint32 | 0 to samples × 60 × 8. At most 28,800. |
| energyWh | uint32 | Whole Wh read at the PDU outlet. |
| rateUsdcPerGpuHour | uint64 | USDC base units. 0 if no lease was active. |
| grossUsdc | uint64 | Exactly gpuSeconds × rate ÷ 3,600, rounded down. |
| tenantRef | bytes32 | keccak256 of the lease ID and a per-lease salt. Zero if idle. |
| prevHash | bytes32 | EIP-712 digest of the previous hour's record. Zero for a machine's first hour. |
One record carries one rate. If a sled served two leases at different prices in the same hour, rateUsdcPerGpuHour is the GPU-second weighted average and grossUsdc is the exact sum of both leases.
The hash chain
Every record carries the digest of the same machine's record for the hour before. A machine's history is a chain that runs from its first hour in service to now. A machine that is offline still gets a record for every hour, with samples at 0, so the chain never has a gap by design. A gap, an edit or a reordering is detectable by anyone holding two neighbouring records.
The hourly root
- Leaves are record digests. Each leaf is the EIP-712 digest of one record: the same hash the custodian signed.
- Leaves are sorted. All digests for one site and one hour are sorted in ascending order.
- Pairs are hashed in order. Each parent is
keccak256of its two children, smaller first. An odd node at the end of a level is carried up unchanged. - The root is posted. The site's custodian key calls
postRooton the Ledger within 10 minutes of the hour, with the record count and the bundle's IPFS content ID. A root cannot be replaced once posted.
function postRoot(bytes32 site, uint64 hourStart, bytes32 root, uint32 count, string calldata cid) external;
function rootOf(bytes32 site, uint64 hourStart)
external view returns (bytes32 root, uint32 count, string memory cid, uint64 postedAt);
event RootPosted(bytes32 indexed site, uint64 indexed hourStart, bytes32 root, uint32 count, string cid);The published bundle
Each site-hour is published to IPFS as one folder. The content ID is the one stored with the root, so the bundle you fetch is provably the one that was anchored.
IAD-4/2026-09-27T14/
manifest.json # site, hourStart, count, root, machine IDs in leaf order
CR-IAD4-07-03.json
CR-IAD4-07-03.proof.json # { "leaf": "0x…", "siblings": ["0x…", "0x…"] }
CR-IAD4-07-04.json
CR-IAD4-07-04.proof.json
CR-IAD4-07-05.json
CR-IAD4-07-05.proof.json
CR-IAD4-07-06.json
CR-IAD4-07-06.proof.json # one pair per sled at the site: 4 at IAD-4Versioning
version goes up when a field is added. The EIP-712 domain version changes only if what is signed changes. Old records stay valid forever under the version they were signed with, and the verifier knows every version.
Last updated 5 October 2026.