Verification Guide

How automatic portfolio verification works

The live flow is create or resume epoch -> Lit verification -> public attestation -> onchain submit. The worker builds a delayed public epoch from the latest priced portfolio snapshot, fetches the live custody inventory through a Lit action, verifies deterministic hashes server-side, publishes public JSON artifacts, and then appends the same attestation to the configured registry network.

What gets published

  • Epoch JSON with the delayed portfolio snapshot and canonical `portfolioHash` and `holdingsHash`
  • Verification JSON with the live holdings hash, verifier metadata, and Lit execution details
  • Attestation JSON with payload hash, PKP proof bundle, artifact URIs, token metadata, and onchain submission fields when available

What stays private

  • The custody account credentials and refresh token
  • The Steam login handshake used by the Lit action to create a fresh session
  • Operational details outside the published epoch and verification artifacts

Flow

1. `POST /api/internal/epochs/verify-and-submit` targets the next required registry epoch. If that epoch already exists locally, the worker resumes it instead of creating a newer one.
2. If the next registry epoch does not exist yet, the worker creates it from the latest fully priced portfolio snapshot and stores the delayed public artifact.
3. The worker publishes the exact Lit action code to IPFS, loads the configured Lit runtime, and executes the custody check against Steam.
4. Lit returns the raw inventory, payload hashes, `inventorySnapshotHash`, `proofTimestamp`, and a PKP signature bundle tied to the action CID.
5. The backend recomputes the inventory payload hash and canonical live holdings hash server-side, then compares the live holdings hash to the epoch holdings hash.
6. Only after both the holdings comparison and the PKP proof pass does the worker publish the attestation payload hash and submit that epoch onchain in strict append-only order.

Artifacts

For any epoch, fetch these three artifacts:

curl https://glockrock.com/api/portfolio/epochs/1
curl https://glockrock.com/api/portfolio/epochs/1/verification
curl https://glockrock.com/api/portfolio/epochs/1/attestation
  1. Confirm `epoch.holdingsHash === verification.liveHoldingsHash` for a verified epoch.
  2. Confirm `attestation.hashes.epochHoldingsHash === epoch.holdingsHash`.
  3. Confirm `attestation.hashes.liveHoldingsHash === verification.liveHoldingsHash`.
  4. Confirm `attestation.artifact.verificationUri` and `attestation.artifact.attestationUri` point back to the same public endpoints you fetched.

Public `status = verified` means the local Lit verification already passed. The `onchain` block can still be empty while the registry submission has not happened yet or has not been persisted back into the public artifact.

Onchain registry

Each verified epoch is also submitted to the currently configured registry network, which for the current production-facing test flow is usually Ethereum Mainnet. The registry stores the epoch number, status, payload hash, inventory proof fields, signer, verification timestamp, and artifact URIs, so the history is not only published by the app but also anchored onchain.

Registry: 0xDe8F75c68B7cDe165a61F28bD915314545957bd8
Explorer: https://etherscan.io/address/0xDe8F75c68B7cDe165a61F28bD915314545957bd8#code
  1. Open the current epoch attestation JSON and copy `onchain.registryAddress`, `onchain.txHash`, and `payloadHash`.
  2. Open the registry contract and call `getEpoch(epochNo)` for the same epoch number.
  3. Confirm that onchain `payloadHash`, `epochHoldingsHash`, `liveHoldingsHash`, and `inventorySnapshotHash` match the public artifacts.
  4. Confirm that `isValidInventorySnapshotSignature(epochNo) === true` onchain.
  5. Confirm that the registry address and transaction hash in the attestation JSON match the explorer record.

The registry enforces append-only ordering. Epoch `N + 1` cannot be submitted before epoch `N`, and the worker now explicitly targets `latestRegistryEpochNo + 1` to avoid gaps.

How to verify the PKP signature

The current proof model signs the message `grx-inventory-proof:v1:{proofTimestamp}:{inventorySnapshotHash}:{actionIpfsCid}`. That binds the PKP signature to the exact canonical live inventory snapshot hash, the exact Lit action CID, and the proof timestamp.

Fetch the attestation JSON and inspect these fields:

signature.proofTimestamp
signature.signedPayload
verifier.actionIpfsCid
hashes.inventorySnapshotHash
signature.inventoryVerifierAddress
signature.signedDigest
signature.signature
  1. Rebuild the canonical live inventory snapshot hash locally and confirm that it equals `hashes.inventorySnapshotHash`.
  2. Confirm `signature.signedPayload === "$proofTimestamp+inventorySnapshotHash+actionIpfsCid"` for current-format epochs.
  3. Rebuild the proof message from `signature.proofTimestamp`, `hashes.inventorySnapshotHash`, and `verifier.actionIpfsCid`.
  4. Verify that `signature.signature` is valid for that proof message under the inventory verifier wallet address.
  5. Open `verifier.actionIpfsCid` through any IPFS gateway and inspect the exact Lit action code used for the verification run.

Example Node.js verification flow:

import { hashMessage, verifyMessage } from "ethers";

const inventoryVerifierAddress = "0xc67D8809Cb0eEdb05d68023a1D4088AaB3D94611";
const proofTimestamp = 1774123781;
const actionIpfsCid = "QmPgJoPjmvFdMxjakCBsw52Abt5q7VUTwDik4PHEZjBkEA";
const inventorySnapshotHash = "0xdd90830c3166f15ae19a9815b3c726263a8ac131e7ba54265ca17bdf56dbdbcc";
const signedDigest = "0x418f83251ae26ad2dcb5dbea1a42408cc7eec4189934edf09cf6854af0199d35";
const signature = "0xab71312cf436cc8f440493dc5b188f21cc8b55aa672157d10223a154b031ce4c1f29045f6ddbe64c4533de1f0130a8da946526d4c7a0853e368bc1690aaba4cd1b";
const proofMessage = `grx-inventory-proof:v1:${proofTimestamp}:${inventorySnapshotHash}:${actionIpfsCid}`;

const expectedDigest = hashMessage(proofMessage);
const recovered = verifyMessage(proofMessage, signature);

console.log({
  expectedDigest,
  signedDigest,
  recovered,
  ok: recovered.toLowerCase() === inventoryVerifierAddress.toLowerCase() && expectedDigest.toLowerCase() === signedDigest.toLowerCase(),
});

If `ok === true`, the inventory verifier wallet really signed the canonical inventory snapshot hash exposed in the public attestation. The registry contract now also stores that proof with the epoch and exposes a matching onchain validity check for the same personal-sign flow.

How to verify the onchain signer

The onchain signer is a separate proof from the PKP signature. The registry contract only accepts epochs that were signed by its configured `verifierSigner`. This is the signature checked by `verifyAttestation(...)` and by `submitEpochAttestation(...)`.

What this check proves:

  • The epoch struct written to the contract was signed by the contract-authorized verifier signer.
  • The onchain epoch record was not inserted by an arbitrary wallet.
  • The payload hash stored onchain matches the public attestation payload hash for the same epoch.

Example flow with `viem`:

import { createPublicClient, decodeFunctionData, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";

const registryAddress = "0xDe8F75c68B7cDe165a61F28bD915314545957bd8";
const submitTxHash = "0xf9da6f40985ea04d4fe52d3ed9482dab0ccfba950c834f401bd19d3355cbac92";

const registryAbi = parseAbi([
  "function verifierSigner() view returns (address)",
  "function getEpoch(uint256 epochNo) view returns ((uint256 epochNo,uint8 status,bytes32 portfolioHash,bytes32 epochHoldingsHash,bytes32 liveHoldingsHash,bytes32 payloadHash,bytes32 inventorySnapshotHash,string actionIpfsCid,uint64 inventoryProofTimestamp,uint64 verifiedAt,string epochUri,string verificationUri,string attestationUri,address inventoryVerifierAddress,bytes inventoryVerifierSignature,address signer,uint64 submittedAt))",
  "function verifyAttestation((uint256 epochNo,uint8 status,bytes32 portfolioHash,bytes32 epochHoldingsHash,bytes32 liveHoldingsHash,bytes32 payloadHash,uint64 verifiedAt,string epochUri,string verificationUri,string attestationUri) attestation, bytes signature) view returns (address)",
  "function isValidInventorySnapshotSignature(uint256 epochNo) view returns (bool)",
  "function submitEpochAttestation((uint256 epochNo,uint8 status,bytes32 portfolioHash,bytes32 epochHoldingsHash,bytes32 liveHoldingsHash,bytes32 payloadHash,uint64 verifiedAt,string epochUri,string verificationUri,string attestationUri) attestation, bytes signature, (uint64 proofTimestamp,bytes32 inventorySnapshotHash,string actionIpfsCid,address inventoryVerifierAddress,bytes inventoryVerifierSignature) inventoryProof)"
]);

const client = createPublicClient({
  chain: mainnet,
  transport: http(process.env.REGISTRY_RPC_URL),
});

const tx = await client.getTransaction({ hash: submitTxHash });
const decoded = decodeFunctionData({
  abi: registryAbi,
  data: tx.input,
});

const [attestation, signature] = decoded.args;

const recovered = await client.readContract({
  address: registryAddress,
  abi: registryAbi,
  functionName: "verifyAttestation",
  args: [attestation, signature],
});

const verifierSigner = await client.readContract({
  address: registryAddress,
  abi: registryAbi,
  functionName: "verifierSigner",
});

const storedEpoch = await client.readContract({
  address: registryAddress,
  abi: registryAbi,
  functionName: "getEpoch",
  args: [attestation.epochNo],
});

const inventoryProofOk = await client.readContract({
  address: registryAddress,
  abi: registryAbi,
  functionName: "isValidInventorySnapshotSignature",
  args: [attestation.epochNo],
});

console.log({
  recovered,
  verifierSigner,
  inventoryProofOk,
  payloadHashMatches: storedEpoch.payloadHash.toLowerCase() === attestation.payloadHash.toLowerCase(),
  ok: recovered.toLowerCase() === verifierSigner.toLowerCase() && inventoryProofOk,
});

If `ok === true`, then the exact attestation struct that was submitted onchain was signed by the signer that the registry contract trusts, and the stored PKP inventory proof is valid onchain for that same epoch.

In short:

  • `PKP signature` = Lit-side proof for the current inventory snapshot hash, bound to the exact action CID and proof timestamp.
  • `onchain signer signature` = contract-side authorization for appending a new epoch to the registry.

Recent epochs

EpochStatusVerifiedPayload hashOnchainArtifacts
#1verified2026-03-21T20:09:42.970Z0xd4c001112dc044f0224c7e2e0241007a26977c0e8b9eae2fc0b194c5a43bcb9bEtherscanepochverifyattest

What this proves

Proves
  • The published epoch snapshot and the Lit-fetched live inventory produced the same holdings hash.
  • The public inventory proof was signed by the published PKP verifier key for the exact action CID and proof timestamp.
  • The same attestation payload hash and inventory proof fields were stored in the configured registry for that epoch.
  • The verifier code reference can be pinned by immutable IPFS CID.
Does not prove
  • Real-time public visibility into custody at every second.
  • Trustless Steam custody, because Steam remains a Web2 source.
  • Any promise of fixed returns or risk-free operation.