The ledger
Verifying a record
You do not have to trust our dashboard. With a record, its proof and the contracts on Ethereum, anyone can check that an hour happened the way it says.
You do not have to trust our dashboard. With a record, its proof and a connection to Ethereum, anyone can check that an hour of rent happened the way the record says. This page lists the checks and shows them in one short TypeScript file.
What verifying proves
- The signature is valid. The record hashes to a digest, and the signature over that digest recovers to an address.
- The right custodian signed it. That address is the key the Registry lists for the record's site at that hour. Keys can rotate, so the Registry keeps their history.
- The machine is real. The machine ID is in the Registry, at that site, with status active for that hour.
- The numbers add up. GPU-seconds are at most 28,800, and gross equals GPU-seconds times the rate, divided by 3,600, exactly.
- Nothing is missing before it.
prevHashequals the digest of the same machine's record for the previous hour. - It is the record that was anchored. The Merkle proof leads from the digest to the root the custodian posted to the Ledger contract, within 10 minutes of the hour.
Together these mean the record was signed by the key responsible for that site, has not been changed since, and was committed on-chain before anyone was paid for it.
What you need
The record and its proof file, both in the IPFS bundle for the site and hour (see The record format). An Ethereum RPC URL, from your own node or any provider. Node 20 or later and the viem package. Nothing from CoresRent: no account, no API key.
In TypeScript with viem
This runs checks 1, 2, 4 and 6 for one record. Check 3 needs one more Registry call, and check 5 needs the previous hour's record; both are in the verifier.
import {
createPublicClient, http, parseAbi, hashTypedData, recoverAddress,
isAddressEqual, keccak256, concat, stringToHex, type Hex,
} from "viem";
import { mainnet } from "viem/chains";
import r from "./2026-09-27T14.json" with { type: "json" };
import proof from "./2026-09-27T14.proof.json" with { type: "json" };
const LEDGER = "0x47ac525AA85Efe47f42BB213Ca9e38B5f5C4DD83";
const REGISTRY = "0x025E5B9367D86fffBFCbE7b3485524F6CDc72953";
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;
const message = {
machineId: r.machineId,
site: r.site,
hourStart: BigInt(Date.parse(r.hourStart) / 1000),
samples: r.samples,
gpuSeconds: r.gpuSeconds,
energyWh: r.energyWh,
rateUsdcPerGpuHour: BigInt(r.rateUsdcPerGpuHour),
grossUsdc: BigInt(r.grossUsdc),
tenantRef: r.tenantRef as Hex,
prevHash: r.prevHash as Hex,
};
// 1. the digest the custodian signed, and who signed it
const digest = hashTypedData({ domain, types, primaryType: "MeterRecord", message });
const signer = await recoverAddress({ hash: digest, signature: r.signature as Hex });
// 2. was that the site's custodian key at that hour?
const client = createPublicClient({ chain: mainnet, transport: http(process.env.RPC_URL) });
const site = stringToHex(r.site, { size: 32 });
const key = await client.readContract({
address: REGISTRY,
abi: parseAbi(["function custodianKeyAt(bytes32 site, uint64 at) view returns (address)"]),
functionName: "custodianKeyAt",
args: [site, message.hourStart],
});
if (!isAddressEqual(signer, key)) throw new Error("not signed by the site's custodian key");
// 3. the arithmetic
if (message.gpuSeconds > 8 * 3600) throw new Error("more GPU time than the hour holds");
const expected = (BigInt(message.gpuSeconds) * message.rateUsdcPerGpuHour) / 3600n;
if (message.grossUsdc !== expected) throw new Error("gross does not match GPU time × rate");
// 4. is this record inside the root posted on the Ledger?
const [root] = await client.readContract({
address: LEDGER,
abi: parseAbi([
"function rootOf(bytes32 site, uint64 hourStart) view returns (bytes32 root, uint32 count, string cid, uint64 postedAt)",
]),
functionName: "rootOf",
args: [site, message.hourStart],
});
let node: Hex = digest;
for (const sibling of proof.siblings as Hex[]) {
node = BigInt(node) < BigInt(sibling)
? keccak256(concat([node, sibling]))
: keccak256(concat([sibling, node]));
}
if (node !== root) throw new Error("record is not in the posted root");
console.log("ok", r.machineId, r.hourStart);Checking the chain
One record can be genuine while an hour next to it has gone missing. To rule that out, walk a machine's records in order and check that each prevHash equals the digest of the record before it. A missing, edited or reordered hour breaks the chain at exactly that point. The verifier does this with chain.
Checking a whole day
At settlement the Distributor emits a Settled event with the day's gross rent, each cost line, the fee and the net amount. To check a payout, add up grossUsdc across every record in the pool for that UTC day and compare it with the event. Then check that the fee is exactly 8% of gross, and that each machine's net divided by its shares matches the rent per share in its RentCredited event.
If a check fails
Note which check failed and for which record, and send the file to security@cores.rent. A failed signature or root check is treated as a security incident. A gap between recorded gross and USDC received usually means a late tenant payment, and it is shown in the next settlement.
Last updated 5 October 2026.