Skip to content

Commit 09afbfc

Browse files
committed
Issue 62 PR 05: Event-hash integrity reconciliation
1 parent c73d0cf commit 09afbfc

4 files changed

Lines changed: 48 additions & 23 deletions

File tree

SPEC.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ Token Host MUST emit, per collection:
995995
- `RecordDeleted(collectionId, recordId, actor, timestamp, isHardDelete)`
996996
- `RecordTransferred(collectionId, recordId, fromOwner, toOwner, actor, timestamp)` (only for collections with transfers enabled)
997997

998-
`dataHash` and `changedFieldsHash` SHOULD be keccak256 of ABI-encoded values to allow indexers to detect mismatches without storing full payloads in events. Token Host MAY additionally emit field-level events for frequently queried fields.
998+
`dataHash` and `changedFieldsHash` SHOULD be keccak256 of ABI-encoded values to allow indexers to detect mismatches without storing full payloads in events. In v1, `changedFieldsHash` MAY carry the post-update record hash rather than a minimal delta-only hash, so long as the generator applies the rule deterministically and documents it. Token Host MAY additionally emit field-level events for frequently queried fields.
999999

10001000
#### 7.9.1 Event indexing for narrow subscriptions (normative)
10011001

@@ -1103,6 +1103,8 @@ This design ensures:
11031103
- identical records across environments produce identical hashes,
11041104
- indexers can recompute the hash from decoded record values to detect RPC inconsistencies.
11051105

1106+
For update events, the generator MAY place this post-update `recordHash` into the `changedFieldsHash` event slot in v1. That preserves a stable event surface while still giving indexers an integrity primitive tied to the resulting stored record state.
1107+
11061108
### 7.15 Error model
11071109

11081110
Generated contracts MUST use explicit, distinguishable reverts. For gas efficiency, Token Host SHOULD prefer Solidity custom errors over string revert reasons.

packages/generator/src/solidity/generateAppSolidity.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ function queryIndexKeyExpr(fieldType: FieldType, expr: string): string {
7979
: `keccak256(abi.encode(${expr}))`;
8080
}
8181

82+
function chunkArray<T>(items: T[], size: number): T[][] {
83+
if (size <= 0) return [items.slice()];
84+
const out: T[][] = [];
85+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
86+
return out;
87+
}
88+
8289
class W {
8390
private lines: string[] = [];
8491
private indent = 0;
@@ -130,10 +137,6 @@ function bytes32FromSha256(schemaHash: string): string {
130137
return `0x${m[1]}`;
131138
}
132139

133-
function recordHashFnName(collectionName: string): string {
134-
return `_hashRecord${collectionName}`;
135-
}
136-
137140
export function generateAppSolidity(schema: ThsSchema, options: GenerateAppSolidityOptions = {}): { path: string; contents: string } {
138141
const w = new W();
139142

@@ -257,7 +260,7 @@ export function generateAppSolidity(schema: ThsSchema, options: GenerateAppSolid
257260
w.line('(bool ok, bytes memory res) = address(this).delegatecall(calls[i]);');
258261
w.block('if (!ok)', () => {
259262
// bubble up revert data (best-effort)
260-
w.block('assembly', () => {
263+
w.block('assembly ("memory-safe")', () => {
261264
w.line('revert(add(res, 32), mload(res))');
262265
});
263266
});
@@ -359,14 +362,6 @@ export function generateAppSolidity(schema: ThsSchema, options: GenerateAppSolid
359362
});
360363
w.line();
361364

362-
// Record hashing helper (used for event integrity without stack-too-deep risk).
363-
// Note: this uses ABI encoding of the full record tuple; callers can recompute
364-
// from decoded record values.
365-
w.block(`function ${recordHashFnName(C)}(${record} memory r) internal pure returns (bytes32)`, () => {
366-
w.line(`return keccak256(abi.encode(COLLECTION_ID_${C}, r));`);
367-
});
368-
w.line();
369-
370365
w.block(`function _init${record}(${record} storage r, uint256 id) internal`, () => {
371366
w.line('r.id = id;');
372367
w.line('r.createdAt = block.timestamp;');
@@ -380,17 +375,45 @@ export function generateAppSolidity(schema: ThsSchema, options: GenerateAppSolid
380375
});
381376
w.line();
382377

378+
const createFieldChunks = chunkArray(c.fields, 4);
379+
for (let i = 0; i < createFieldChunks.length; i++) {
380+
const chunk = createFieldChunks[i]!;
381+
w.block(`function _applyCreate${C}Fields_${i}(${record} storage r, ${createInputStruct} calldata input) internal`, () => {
382+
for (const f of chunk) {
383+
w.line(`r.${f.name} = input.${f.name};`);
384+
}
385+
});
386+
w.line();
387+
}
388+
383389
w.block(`function _applyCreate${C}Fields(${record} storage r, ${createInputStruct} calldata input) internal`, () => {
384-
for (const f of c.fields) {
385-
w.line(`r.${f.name} = input.${f.name};`);
390+
for (let i = 0; i < createFieldChunks.length; i++) {
391+
w.line(`_applyCreate${C}Fields_${i}(r, input);`);
386392
}
387393
});
388394
w.line();
389395

396+
const recordHashParts = [
397+
'COLLECTION_ID_' + C,
398+
'r.id',
399+
'r.createdAt',
400+
'r.createdBy',
401+
'r.owner',
402+
'r.updatedAt',
403+
'r.updatedBy',
404+
'r.isDeleted',
405+
'r.deletedAt',
406+
'r.version',
407+
...c.fields.map((f) => `r.${f.name}`)
408+
];
409+
w.block(`function _recordHash${C}(${record} storage r) internal view returns (bytes32)`, () => {
410+
w.line(`return keccak256(abi.encode(${recordHashParts.join(', ')}));`);
411+
});
412+
w.line();
413+
390414
w.block(`function _emitCreated${C}(uint256 id) internal`, () => {
391-
w.line(`${record} memory m = ${cVar}Records[id];`);
392-
w.line(`bytes32 dataHash = ${recordHashFnName(C)}(m);`);
393-
w.line(`emit RecordCreated(COLLECTION_ID_${C}, id, _msgSender(), block.timestamp, dataHash);`);
415+
w.line(`${record} storage r = ${cVar}Records[id];`);
416+
w.line(`emit RecordCreated(COLLECTION_ID_${C}, id, _msgSender(), block.timestamp, _recordHash${C}(r));`);
394417
});
395418
w.line();
396419

@@ -805,9 +828,7 @@ export function generateAppSolidity(schema: ThsSchema, options: GenerateAppSolid
805828
w.line('r.updatedAt = block.timestamp;');
806829
w.line('r.updatedBy = _msgSender();');
807830
w.line('r.version += 1;');
808-
w.line(`${record} memory m = r;`);
809-
w.line(`bytes32 changedFieldsHash = ${recordHashFnName(C)}(m);`);
810-
w.line(`emit RecordUpdated(COLLECTION_ID_${C}, id, _msgSender(), block.timestamp, changedFieldsHash);`);
831+
w.line(`emit RecordUpdated(COLLECTION_ID_${C}, id, _msgSender(), block.timestamp, _recordHash${C}(r));`);
811832
});
812833
w.line();
813834
}

test/testCliBenchmarkRegistryBuild.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('th build (benchmark registry schema)', function () {
2626
const appSol = fs.readFileSync(path.join(outDir, 'contracts', 'App.sol'), 'utf-8');
2727
expect(appSol).to.include('struct CreateBenchmarkRunInput');
2828
expect(appSol).to.include('function createBenchmarkRun(CreateBenchmarkRunInput calldata input)');
29+
expect(appSol).to.include('function _recordHashBenchmarkRun');
2930

3031
const compiled = JSON.parse(fs.readFileSync(path.join(outDir, 'compiled', 'App.json'), 'utf-8'));
3132
expect(String(compiled.compilerProfile || '')).to.match(/auto|large-app/);

test/testCrudGenerator.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ describe('Spec-aligned CRUD generator', function () {
5252
expect(appSol.contents).to.include('event RecordCreated');
5353
expect(appSol.contents).to.include('function createCandidate');
5454
expect(appSol.contents).to.include('function createJobPosting');
55+
expect(appSol.contents).to.include('function _recordHashCandidate');
56+
expect(appSol.contents).to.include('function _recordHashJobPosting');
5557

5658
const { errors } = compileSolidity(appSol.path, appSol.contents, 'App');
5759
expect(errors.map((e) => e.formattedMessage || e.message).join('\n')).to.equal('');
5860
});
5961
});
60-

0 commit comments

Comments
 (0)