ClaimID formulas
Purpose: claim_id is the global, collision-resistant, route-agnostic identifier for a single monetizable fact. It guarantees one fact → one claim → one cashflow across all EDMA routes and mirrors.
A. Invariants (what the id must guarantee)
Uniqueness by evidence and identity: Same physical event with a different dossier (files, corrections) must not collide.
Determinism: Any honest client/server computing the id from the same inputs yields the same 32-byte value.
Domain separation: IDs from different lanes/schemas cannot collide.
Append-only lineage: Corrections/replacements mint a new claim_id and link replaces: old_claim_id. No mutation.
Cross-route safety: The same evidence cannot be used in Trade and Tokens at once.
B. Canonical definition (bytes and function)
We derive claim_id by hashing a domain-tagged, length-delimited tuple of typed fields.
claim_id = keccak256(
abi.encodePacked(
DOMAIN_TAG, // bytes8 = "EDMA/CLM"
VERSION_TAG, // bytes4 = 0x00000001
chainId_u256, // EVM chainId (uint256, ABI encoding)
laneTag, // bytes4: "TRAD" | "TOKN"
schemaHash, // bytes32 = keccak256(utf8(schema_id)), e.g. "TRADE.ON_BOARD.v1"
keyHash, // bytes32 = keccak256( key_blob ) (see Section D)
povHash // bytes32 = sha256(canonical_json_bytes) (Section 12)
)
)
Why include povHash? Two dossiers with the same identity fields but different evidence (e.g., corrected BL/PSI) produce different claim_ids, preventing “same BL, different PDFs” abuse.
All strings are UTF-8. All hashes are 32 bytes. We rely on Solidity’s abi.encodePacked typing for length-delimited packing; do not hand-concatenate raw strings.
C. Normalization rules (before computing keyHash)
Schemas (Sec. 12.3): decide what fields compose the identity; these rules decide how to normalize them prior to hashing:
string_id fields (BL numbers, serials, ports, project IDs): Trim ASCII whitespace; collapse internal spaces; uppercase A–Z; NFC normalize; forbid control chars.
Reject if not matching schema’s pattern: (^[A-Z0-9._-]+$ unless tighter).
Timestamps: RFC-3339 UTC "YYYY-MM-DDTHH:MM:SSZ" (no milliseconds unless schema mandates).
Decimals: numbers encoded as strings with schema precision (e.g., "396000" Wh; "39.600" if precision=3). No leading +, no exponent.
Arrays: SET → sort ascending (binary compare) before hashing (e.g., container_ids). SEQ → preserve order (e.g., temperature log intervals).
Programs/regions/enums: canonical enumerations defined by schema.
If any normalization fails: clients must surface an error and the Gate will return E_FORMAT_INVALID or E_SET_NOT_SORTED.
D. keyHash construction (per schema)
We hash a key blob—the minimal identity for the event/unit—after normalization. Each schema specifies its fields.
General form
keyHash = keccak256( abi.encode(
KEY_DOMAIN, // bytes8 e.g. "CLAIMKEY"
schema_id, // utf8 string exactly as used to compute schemaHash
key_fields... // normalized, typed values
))
D.1 Trade — On-Board & Sealed (TRADE.ON_BOARD.v1)
Identity = BL number, seal number, and the set of container IDs.
key_fields = (
bl_number: string_id,
seal_number: string_id,
sha256( join_sorted(container_ids_SET, "\n") ) // file-like digest of list
)
D.2 Trade — Customs Cleared (TRADE.CUSTOMS.v1)
Identity = customs entry no., country code, importer tax id.
key_fields = ( customs_entry_number, country_code, importer_tax_id )
D.3 Trade — Arrival & QA (TRADE.ARRIVAL_QA.v1)
Identity = ASN, DC id, time bucket
(to the hour, unless schema says otherwise).
key_fields = ( asn_number, dc_id, window_bucket(received_at, 3600s) )
D.4 Tokens — Energy (1 MWh) (TOKENS.ENERGY.MWH.v1)
Identity = device id, start_ts, end_ts, quantity_Wh.
key_fields = ( device_id, start_ts, end_ts, quantity_Wh )
D.5 Tokens — Carbon (VCM program serial) (TOKENS.CARBON.VCM.v1)
Identity = program, project_id, vintage, unit_serial.
key_fields = ( program, project_id, vintage, unit_serial )
D.6 Registry Mirror (REG.MIRROR.v1) (mapping, not a second claim)
Mirrors bind an external serial to an existing claim_id (they do not mint a new one). For the mirror record we store:
mirrorId = keccak256( abi.encodePacked("EDMA/MIR", schemaHash, program, serial_hash) )
and link: mirrorId → claim_id.
E. Splits, merges, and replacements (lineage rules)
Split shipments (Trade): each sub-lot mints an EMT with its own claim_id.
key_fields := ( bl_number, seal_number, sha256(join_sorted(subset_container_ids)) )
Note: include the subset; do not reuse the full list hash, or One-Claim will block the second child.Merge (downstream gates): a later gate may consume multiple child claim_ids; it lists them in the dossier; the downstream
claim_id
is derived from its own identity (e.g., ASN/DC window). Children are marked consumed.Replacement (revocation): a corrective dossier computes a new
claim_id
(new povHash and often new key fields) and stores replaces:old_claim_id
. Proof pages show both entries; we never mutate the old claim.
F. Collision resistance & safety notes
Including schemaHash + povHash: provides domain separation and second-preimage resilience.
Using abi.encode/encodePacked: avoids manual delimiter mistakes; do not raw-concat strings.
For sets: hashing the sorted list (as a file digest) prevents order-based collisions.
Normalization: eliminates accidental collisions from casing/whitespace/locale.
G. Reference implementations
Solidity (library excerpt)
library ClaimId {
bytes8 constant DOMAIN = 0x45444d412f434c4d; // "EDMA/CLM"
bytes4 constant V1 = 0x00000001;
function schemaHash(string memory schemaId) internal pure returns (bytes32) {
return keccak256(bytes(schemaId));
}
function keyHash_ONBOARD_v1(
string memory bl,
string memory seal,
bytes32 containersSha256
) internal pure returns (bytes32) {
return keccak256(abi.encode("CLAIMKEY", "TRADE.ON_BOARD.v1", bl, seal, containersSha256));
}
function claimId(
uint256 chainId,
bytes4 laneTag, // "TRAD" or "TOKN"
bytes32 schemaHash_,
bytes32 keyHash_,
bytes32 povHash
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(DOMAIN, V1, chainId, laneTag, schemaHash_, keyHash_, povHash));
}
}
TypeScript (deterministic encoder)
import { keccak256, solidityPackedKeccak256, sha256 } from "ethers";
const DOMAIN = "0x45444d412f434c4d"; // "EDMA/CLM"
const V1 = "0x00000001";
export function schemaHash(id: string): string {
return keccak256(new TextEncoder().encode(id));
}
export function sha256Hex(bytes: Uint8Array): string {
return "0x" + Buffer.from(crypto.subtle.digestSync("SHA-256", bytes)).toString("hex");
}
export function keyHashOnBoardV1(bl: string, seal: string, containersSha256: string): string {
return keccak256(
new TextEncoder().encode(
JSON.stringify(["CLAIMKEY","TRADE.ON_BOARD.v1", bl, seal, containersSha256])
)
);
}
// canonical claim id
export function claimId(
chainId: bigint,
laneTag: "TRAD" | "TOKN",
schemaHashHex: string,
keyHashHex: string,
povHashHex: string
): string {
return solidityPackedKeccak256(
["bytes8","bytes4","uint256","bytes4","bytes32","bytes32","bytes32"],
[DOMAIN, V1, chainId, ethers.encodeBytes32String(laneTag).slice(0,10), schemaHashHex, keyHashHex, povHashHex]
);
}
(In production, use the SDK’s canonicalizer and provided helpers; the snippet shows types and packing, not full error handling.)
H. Worked examples (process, not full hex)
H.1 Trade — On-Board (TRADE.ON_BOARD.v1)
Normalize
BL = "OOLU1234567890" → "OOLU1234567890"
SEAL = "seal9981" → "SEAL9981"
container_ids = ["CMAU000002","CMAU000001"] → sort → ["CMAU000001","CMAU000002"]
containersSha256 = sha256("CMAU000001\nCMAU000002")
keyHash =
keccak256("CLAIMKEY","TRADE.ON_BOARD.v1", BL, SEAL, containersSha256)
schemaHash =
keccak256("TRADE.ON_BOARD.v1")
povHash =
sha256(canonical_json_bytes)
(includes file digests for bl.pdf, seal.jpg, etc.)claim_id =
keccak256( DOMAIN, V1, chainId, "TRAD", schemaHash, keyHash, povHash )
H.2 Tokens — Carbon (TOKENS.CARBON.VCM.v1)
Normalize:
program="VERRA", project_id="VCS-1234", vintage="2024", unit_serial="…-00012345"
keyHash =
keccak256("CLAIMKEY","TOKENS.CARBON.VCM.v1", program, project_id, vintage, unit_serial)
schemaHash =
keccak256("TOKENS.CARBON.VCM.v1")
povHash =
sha256(canonical_json_bytes)
claim_id =
keccak256( DOMAIN, V1, chainId, "TOKN", schemaHash, keyHash, povHash )
I. Validation & tests (you should pass these)
Determinism: recompute claim_id on different clients → identical results.
Normalization: casing/whitespace variants yield same id; local-time timestamps are rejected.
Set/sequence: changing order of container_ids (SET) doesn’t change id; changing order of a SEQ field must.
Evidence-sensitive: same key fields but different povHash → different claim_id.
Append-only: replacement dossier yields new claim_id; Explorer shows replaces linkage.
Cross-route: attempt to reuse the same key fields in Trade & Tokens → different lanes; One-Claim still blocks duplicates within each lane.
Plain recap
Last updated