Skip to content

Conversation

@outerlook
Copy link
Collaborator

@outerlook outerlook commented Oct 18, 2025

Description

  • Added TrufAttestation.sol library to parse and validate TrufNetwork attestations, including structured representation and error handling.
  • Introduced TrufAttestationHarness.sol for testing the library's functionalities.
  • Updated documentation to include the Attestation Library guide, detailing usage and payload structure.
  • Enhanced testing framework with comprehensive unit tests for the TrufAttestation library, ensuring robust validation and verification processes.
  • Added helper functions in TypeScript for building canonical attestations and signatures, facilitating easier integration in consumer contracts.

Related Problem

How Has This Been Tested?

Summary by CodeRabbit

  • New Features

    • On-chain attestation parsing, signature verification, hashing, decoding, and action-aware handling.
    • TypeScript utilities to build and sign canonical attestations; re-exported in the public API.
    • Lightweight on-chain harness exposing attestation helpers for easier integration.
    • Sepolia network config made optional via environment variables.
  • Documentation

    • New Attestation Library guide and Developer Guide examples and best practices.
  • Tests

    • Comprehensive tests and a golden fixture covering attestation flows; test helpers added.

- Added TrufAttestation.sol library to parse and validate TrufNetwork attestations, including structured representation and error handling.
- Introduced TrufAttestationHarness.sol for testing the library's functionalities.
- Updated documentation to include the Attestation Library guide, detailing usage and payload structure.
- Enhanced testing framework with comprehensive unit tests for the TrufAttestation library, ensuring robust validation and verification processes.
- Added helper functions in TypeScript for building canonical attestations and signatures, facilitating easier integration in consumer contracts.
@outerlook outerlook self-assigned this Oct 18, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 18, 2025

Walkthrough

Adds a Solidity TrufAttestation library and harness for parsing, hashing, verifying, and decoding canonical attestations; TypeScript canonical builders and test helpers; comprehensive tests and a golden fixture; new documentation and README updates; re-exports in the package API and conditional Sepolia handling in Hardhat config.

Changes

Cohort / File(s) Summary
Core Solidity Attestation
contracts/attestation/TrufAttestation.sol, contracts/attestation/TrufAttestationHarness.sol
New library and harness introducing Action enum, VERSION_V1, ALGORITHM_SECP256K1, custom errors, Attestation and DataPoint structs; implements parse(), verify(), hash(), decodeDataPoints(), metadata(), body(), toAction(); harness exposes external pure wrappers.
TypeScript Canonical Builders
src/attestation.ts, test/helpers/attestation.ts
New canonical encoder and signer utilities: CanonicalAttestationFields type, buildCanonicalAttestation/buildCanonical, buildSignedAttestation/buildPayload, strict length checks (provider 20 bytes, streamId 32 bytes, signature 65 bytes), big-endian integer encodings and length-prefix helpers.
Tests & Fixtures
test/attestation/TrufAttestation.test.ts, test/attestation/fixtures/attestation_golden.json
New comprehensive test suite exercising parse/hash/verify/decode/metadata/body with generated and golden payloads; adds deterministic golden fixture JSON.
Documentation
README.md, docs/AttestationLibrary.md, docs/DeveloperGuide.md
README updated to mention attestation consumer and verification library; new docs/AttestationLibrary.md documents canonical payload layout, usage examples and TypeScript helpers; DeveloperGuide.md updated with TrufAttestation subsection and links.
Exports & Config
src/index.ts, hardhat.config.cts
src/index.ts re-exports ./attestation and ./lib; hardhat.config.cts loads dotenv/config, sets BCRYPTO_FORCE_FALLBACK default, exposes ETHEREUM_SEPOLIA_RPC_URL env, and makes Sepolia network and accounts conditional on env vars (ETHEREUM_SEPOLIA_RPC_URL, PRIVATE_KEY).

Sequence Diagram(s)

sequenceDiagram
    participant TS as TypeScript Builder
    participant Dev as Developer
    participant Consumer as Consumer Contract
    participant Lib as TrufAttestation (Solidity)
    participant ECDSA as ecrecover

    Dev->>TS: buildSignedAttestation(fields, signature)
    TS->>TS: validate lengths, encode canonical, append signature
    TS-->>Dev: payload

    Dev->>Consumer: submit(payload)
    Consumer->>Lib: parse(payload)
    Lib->>Lib: validate version/algorithm, split signature, decode fields
    Lib-->>Consumer: Attestation struct

    Consumer->>Lib: verify(att, expectedValidator)
    Lib->>Lib: sha256(canonical fields)
    Lib->>ECDSA: ecrecover(hash, v,r,s)
    ECDSA-->>Lib: recovered signer
    Lib-->>Consumer: verification result

    Consumer->>Lib: decodeDataPoints(att)
    Lib->>Lib: abi.decode(result) -> (timestamps[], values[])
    Lib-->>Consumer: DataPoint[]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly linked issues (from PR-linked list)

Poem

🐇 A byte, a hop, a canonical cheer,

I pack the fields and hold the sig near.
Provider fixed, stream set true, timestamps align,
ECDSA hums—validator's sign.
Hop on, builders—the attestations are fine!

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title Check ❓ Inconclusive The pull request title "feat: attestations" is extremely generic and vague. While it is technically related to the changeset, the title does not meaningfully convey what is actually being added. The PR introduces a complete Solidity library (TrufAttestation.sol), a test harness, comprehensive documentation, unit tests, and TypeScript helpers—yet the title merely states the broad domain ("attestations") without clarifying that a parser library, documentation, and testing infrastructure are being added. A teammate scanning the git history would not understand the scope or nature of the primary changes from this title alone. Consider revising the title to be more specific and descriptive. Examples: "feat: add TrufAttestation Solidity library for parsing and verifying attestations", "feat: add attestation parser library with documentation and tests", or "feat: implement TrufAttestation library and supporting documentation". The new title should clarify that a reusable library, documentation, and testing infrastructure are being introduced.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues Check ✅ Passed The pull request comprehensively addresses the requirements specified in both linked issues. For issue #6, it introduces TrufAttestation.sol with the required struct definitions (Attestation and DataPoint), implements parse() with payload validation and signature extraction, provides verify() using ECDSA for signature validation, implements decodeDataPoints() for data point decoding, and includes custom error types as required. For issue #8, the PR updates the README with attestation support, adds the new docs/AttestationLibrary.md guide with canonical payload structure documentation, creates TypeScript helper functions (buildCanonicalAttestation and buildSignedAttestation) in src/attestation.ts to expose canonical encoding functionality, and cross-links from the DeveloperGuide.md. The test suite, test harness, and golden fixtures provide the supporting validation infrastructure.
Out of Scope Changes Check ✅ Passed The code changes are well-aligned with the linked issue requirements. The core library (TrufAttestation.sol), documentation (README, AttestationLibrary.md, DeveloperGuide.md), TypeScript helpers (src/attestation.ts), and test infrastructure (test files and fixtures) all directly support the stated objectives from issues #6 and #8. The hardhat.config.cts changes (adding dotenv initialization and making sepolia network configuration conditional) represent infrastructure improvements to support testing and environment flexibility, which are reasonable supporting changes for test execution though not explicitly mentioned in the issues. No changes appear to contradict or fall outside the scope of the stated objectives.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/attestation

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13ddedc and e9c5eeb.

📒 Files selected for processing (1)
  • test/helpers/attestation.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/helpers/attestation.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
hardhat.config.cts (1)

15-44: Only the last TASK_COMPILE_SOLIDITY override runs; first one is shadowed.

Calling setAction twice on the same subtask replaces the previous action; the artifacts/package.json write never executes. Merge both writes into a single override.

Apply:

-subtask(TASK_COMPILE_SOLIDITY).setAction(async (_, { config }, runSuper) => {
-  const superRes = await runSuper();
-
-  try {
-    await writeFile(
-      join(config.paths.artifacts, "package.json"),
-      '{ "type": "commonjs" }'
-    );
-  } catch (error) {
-    console.error("Error writing package.json: ", error);
-  }
-
-  return superRes;
-});
-
-subtask(TASK_COMPILE_SOLIDITY).setAction(async (_, { config }, runSuper) => {
-  const superRes = await runSuper();
-
-  try {
-    await writeFile(
-      join(config.paths.root, "typechain-types", "package.json"),
-      '{ "type": "commonjs" }'
-    );
-  } catch (error) {
-    console.error("Error writing package.json: ", error);
-  }
-
-  return superRes;
-});
+subtask(TASK_COMPILE_SOLIDITY).setAction(async (args, { config }, runSuper) => {
+  const superRes = await runSuper(args);
+  const writes = [
+    writeFile(join(config.paths.artifacts, "package.json"), '{ "type": "commonjs" }'),
+    writeFile(join(config.paths.root, "typechain-types", "package.json"), '{ "type": "commonjs" }'),
+  ];
+  for (const p of writes) {
+    try { await p; } catch (error) {
+      console.error("Error writing package.json: ", error);
+    }
+  }
+  return superRes;
+});
🧹 Nitpick comments (18)
hardhat.config.cts (1)

70-75: Omit undefined accounts property; guard it via conditional spread.

Avoid passing accounts: undefined; let Hardhat default when no PRIVATE_KEY.

Apply:

-    ...(ETHEREUM_SEPOLIA_RPC_URL && {
-      sepolia: {
-        url: ETHEREUM_SEPOLIA_RPC_URL,
-        accounts: PRIVATE_KEY ? [PRIVATE_KEY] : undefined,
-      },
-    }),
+    ...(ETHEREUM_SEPOLIA_RPC_URL ? {
+      sepolia: {
+        url: ETHEREUM_SEPOLIA_RPC_URL,
+        ...(PRIVATE_KEY ? { accounts: [PRIVATE_KEY] } : {}),
+      },
+    } : {}),
README.md (1)

82-84: Nice cross-link. Consider pinning the canonical.go reference.

Optional: link to a specific commit SHA to avoid docs drifting if the node repo changes file layout.

docs/DeveloperGuide.md (1)

146-146: Minor: consider linking to a sample script/CLI for EOA interactions.

Optional: a one-liner with cast/etherscan tx example improves dev UX.

docs/AttestationLibrary.md (2)

49-55: Import path: prefer package root over “/src” for consumers.

Recommend import { … } from "@trufnetwork/evm-contracts" (or whatever the package export is) to decouple docs from repo layout.


19-31: Helpful layout table. Add explicit note that the hash is sha256 (not keccak256).

Avoids common confusion for EVM devs expecting keccak256. A one-liner here would help.

test/attestation/TrufAttestation.test.ts (1)

162-189: Good negative case for unsupported algorithm. Add two more negatives to match spec.

Add tests for:

  • decodeDataPoints: mismatched array lengths must revert.
  • parse: truncated payload should revert with invalid length.

Apply near the end of the file:

@@
   it("reverts verification for unsupported algorithm", async function () {
@@
   });
+
+  it("reverts when result arrays have mismatched lengths", async function () {
+    const harness = await (await ethers.getContractFactory("TrufAttestationHarness")).deploy();
+    const wallet = ethers.Wallet.createRandom();
+    const badResult = ethers.AbiCoder.defaultAbiCoder().encode(
+      ["uint256[]", "int256[]"],
+      [[1n, 2n], [0n]] // lengths differ
+    );
+    const fields: CanonicalFields = {
+      version: 1,
+      algorithm: 0,
+      blockHeight: 1n,
+      dataProvider: wallet.address,
+      streamId: ethers.hexlify(ethers.randomBytes(32)),
+      actionId: 1,
+      args: ethers.getBytes("0x"),
+      result: ethers.getBytes(badResult),
+    };
+    const canonical = buildCanonical(fields);
+    const digest = ethers.sha256(canonical);
+    const sig = ethers.Signature
+      .from(wallet.signingKey.sign(ethers.getBytes(digest))).serialized;
+    const payload = buildPayload(fields, ethers.getBytes(sig));
+    await expect(harness.decodeDataPoints(payload)).to.be.reverted; // consider asserting custom error
+  });
+
+  it("reverts on truncated payload (invalid length)", async function () {
+    const harness = await (await ethers.getContractFactory("TrufAttestationHarness")).deploy();
+    const bytes = ethers.getBytes(GOLDEN_PAYLOAD);
+    const truncated = ethers.hexlify(bytes.slice(0, bytes.length - 1));
+    await expect(harness.parse(truncated)).to.be.reverted; // consider asserting AttestationInvalidLength
+  });

If there is a specific custom error for the length mismatch, please confirm its name from contracts/attestation/TrufAttestation.sol and assert it here.

test/helpers/attestation.ts (3)

29-35: Add explicit range validation for version/algorithm/actionId to prevent silent truncation.

Masking with & 0xff truncates out-of-range values without warning. Validate ranges and fail fast (and enforce actionId 1..255 to match the Solidity library’s expectations).

Apply this diff inside buildCanonical before assembling pieces:

   const stream = ethers.getBytes(fields.streamId);
   if (stream.length !== 32) {
     throw new Error("streamId must be 32 bytes");
   }

+  if (!Number.isInteger(fields.version) || fields.version < 0 || fields.version > 0xff) {
+    throw new Error("version must be in [0,255]");
+  }
+  if (!Number.isInteger(fields.algorithm) || fields.algorithm < 0 || fields.algorithm > 0xff) {
+    throw new Error("algorithm must be in [0,255]");
+  }
+  if (!Number.isInteger(fields.actionId) || fields.actionId < 1 || fields.actionId > 0xff) {
+    throw new Error("actionId must be in [1,255]");
+  }

50-60: Validate integer ranges before encoding (avoid negative/overflow masking).

Currently values are bit-masked; negative inputs or out-of-range values will silently wrap.

 function encodeUint16BE(value: number): Buffer {
-  const buffer = Buffer.alloc(2);
-  buffer.writeUInt16BE(value & 0xffff, 0);
+  if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
+    throw new Error("uint16 out of range");
+  }
+  const buffer = Buffer.alloc(2);
+  buffer.writeUInt16BE(value, 0);
   return buffer;
 }

 function encodeUint64BE(value: bigint): Buffer {
-  const buffer = Buffer.alloc(8);
-  buffer.writeBigUInt64BE(value & BigInt("0xffffffffffffffff"), 0);
+  if (value < 0n || value > 0xffffffffffffffffn) {
+    throw new Error("uint64 out of range");
+  }
+  const buffer = Buffer.alloc(8);
+  buffer.writeBigUInt64BE(value, 0);
   return buffer;
 }

42-48: Normalize signature v (0/1 → 27/28) for OZ ECDSA compatibility.

OpenZeppelin’s ECDSA.recover typically expects v ∈ {27,28}. Normalizing improves interop with signers that emit 0/1.

 export function buildPayload(fields: CanonicalFields, signature: Uint8Array): Uint8Array {
   if (signature.length !== 65) {
     throw new Error("signature must be 65 bytes");
   }
-  const canonical = buildCanonical(fields);
-  return Buffer.concat([Buffer.from(canonical), Buffer.from(signature)]);
+  const canonical = buildCanonical(fields);
+  const sig = Buffer.from(signature);
+  if (sig[64] === 0 || sig[64] === 1) sig[64] = sig[64] + 27; // normalize v
+  return Buffer.concat([Buffer.from(canonical), sig]);
 }

If your signer already outputs 27/28 this is a no-op. Please confirm your signer behavior in tests.

src/attestation.ts (5)

31-38: Guard against silent truncation; enforce actionId domain.

Add range checks for version, algorithm (0..255) and actionId (1..255) before encoding.

 export function buildCanonicalAttestation(fields: CanonicalAttestationFields): Uint8Array {
   const provider = ethers.getBytes(fields.dataProvider);
@@
   const args = Uint8Array.from(fields.args);
   const result = Uint8Array.from(fields.result);
+
+  if (!Number.isInteger(fields.version) || fields.version < 0 || fields.version > 0xff) {
+    throw new Error("version must be in [0,255]");
+  }
+  if (!Number.isInteger(fields.algorithm) || fields.algorithm < 0 || fields.algorithm > 0xff) {
+    throw new Error("algorithm must be in [0,255]");
+  }
+  if (!Number.isInteger(fields.actionId) || fields.actionId < 1 || fields.actionId > 0xff) {
+    throw new Error("actionId must be in [1,255]");
+  }

56-66: Validate integer ranges in encoders.

Match on-chain expectations and fail fast on invalid inputs.

 function encodeUint16BE(value: number): Buffer {
-  const buffer = Buffer.alloc(2);
-  buffer.writeUInt16BE(value & 0xffff, 0);
+  if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
+    throw new Error("uint16 out of range");
+  }
+  const buffer = Buffer.alloc(2);
+  buffer.writeUInt16BE(value, 0);
   return buffer;
 }
 
 function encodeUint64BE(value: bigint): Buffer {
-  const buffer = Buffer.alloc(8);
-  buffer.writeBigUInt64BE(value & BigInt("0xffffffffffffffff"), 0);
+  if (value < 0n || value > 0xffffffffffffffffn) {
+    throw new Error("uint64 out of range");
+  }
+  const buffer = Buffer.alloc(8);
+  buffer.writeBigUInt64BE(value, 0);
   return buffer;
 }

48-54: Normalize signature v (0/1 → 27/28) for on-chain recover.

Prevents verification failures when signers output 0/1.

 export function buildSignedAttestation(fields: CanonicalAttestationFields, signature: Uint8Array): Uint8Array {
   if (signature.length !== 65) {
     throw new Error("signature must be 65 bytes");
   }
   const canonical = buildCanonicalAttestation(fields);
-  return Buffer.concat([Buffer.from(canonical), Buffer.from(signature)]);
+  const sig = Buffer.from(signature);
+  if (sig[64] === 0 || sig[64] === 1) sig[64] = sig[64] + 27; // normalize v
+  return Buffer.concat([Buffer.from(canonical), sig]);
 }

If you prefer not to mutate inputs, copy and normalize into a new buffer (as above).


17-18: Deduplicate with tests to avoid drift.

test/helpers/attestation.ts re-implements the same logic. Consider importing these exported helpers in tests instead of duplicating them.


31-42: Optional: Prefer Uint8Array-only assembly for browser portability.

Using Buffer ties this to Node; if you expect browser use, switch to Uint8Array concatenation (e.g., precompute total length, allocate once, and set slices).

contracts/attestation/TrufAttestationHarness.sol (1)

7-9: Remove unused using statements.

using TrufAttestation for bytes; and for TrufAttestation.Attestation; aren’t used in this harness.

contracts/attestation/TrufAttestation.sol (3)

76-85: Add a minimal header-length precheck to fail fast.

Before copying, you can reject payloads whose canonical portion is smaller than the smallest valid header (no args/result): 80 bytes canonical + 65 signature = 145 bytes total.

 function parse(bytes calldata payload) internal pure returns (Attestation memory att) {
-    if (payload.length <= SIGNATURE_LENGTH) revert AttestationInvalidLength();
+    if (payload.length <= SIGNATURE_LENGTH) revert AttestationInvalidLength();
+    unchecked {
+        // 1(ver)+1(alg)+8(height)+ (4+20 provider)+ (4+32 stream)+2(action)+4(len0)+4(len0) = 80
+        if (payload.length < SIGNATURE_LENGTH + 80) revert AttestationInvalidLength();
+    }

126-133: Optional: Normalize v (0/1 → 27/28) before recover to improve interop.

Some signers emit 0/1. Normalizing avoids false negatives without weakening security.

 function verify(Attestation memory att, address expectedValidator) internal pure returns (bool) {
     if (att.signature.length != SIGNATURE_LENGTH) revert AttestationInvalidSignatureLength();
     if (att.algorithm != ALGORITHM_SECP256K1) revert AttestationInvalidAlgorithm(att.algorithm);

     bytes32 digest = hash(att);
-    address recovered = ECDSA.recover(digest, att.signature);
+    bytes memory sig = att.signature;
+    // If v is 0/1, normalize to 27/28 in-place
+    assembly {
+        let v := byte(0, mload(add(add(sig, 0x20), 64)))
+        if lt(v, 27) { mstore8(add(add(sig, 0x20), 64), add(v, 27)) }
+    }
+    address recovered = ECDSA.recover(digest, sig);
     return recovered == expectedValidator;
 }

Please confirm your signing flow’s v semantics to justify adding this normalization.


228-245: Gas: replace byte-by-byte copy with a word copy in _readLengthPrefixed.

The loop copies one byte at a time. A 32-byte word copy (with a small tail handler) reduces gas for large blobs.

I can provide an assembly memcpy variant if you want to prioritize this; kept out here to avoid footguns in memory bounds handling.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e59c3df and 7d23d60.

📒 Files selected for processing (11)
  • README.md (2 hunks)
  • contracts/attestation/TrufAttestation.sol (1 hunks)
  • contracts/attestation/TrufAttestationHarness.sol (1 hunks)
  • docs/AttestationLibrary.md (1 hunks)
  • docs/DeveloperGuide.md (2 hunks)
  • hardhat.config.cts (3 hunks)
  • src/attestation.ts (1 hunks)
  • src/index.ts (1 hunks)
  • test/attestation/TrufAttestation.test.ts (1 hunks)
  • test/attestation/fixtures/attestation_golden.json (1 hunks)
  • test/helpers/attestation.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
test/attestation/TrufAttestation.test.ts (1)
test/helpers/attestation.ts (3)
  • CanonicalFields (3-12)
  • buildCanonical (14-40)
  • buildPayload (42-48)
🔇 Additional comments (11)
hardhat.config.cts (1)

45-46: Sanity-check PRIVATE_KEY format before use.

Ensure it’s a 0x-prefixed 32‑byte hex string; otherwise Hardhat will fail at runtime.

You can add a runtime assert:

if (PRIVATE_KEY && !/^0x[0-9a-fA-F]{64}$/.test(PRIVATE_KEY)) {
  throw new Error("Invalid PRIVATE_KEY format; expected 0x + 64 hex chars");
}
README.md (1)

5-5: Good addition—concise context for the new attestation library.

docs/DeveloperGuide.md (1)

67-70: Clear, actionable addition.

Section concisely explains when/how to use the library and where to look next.

test/attestation/fixtures/attestation_golden.json (1)

1-21: Golden fixture is well-formed and aligns with tests.

test/attestation/TrufAttestation.test.ts (2)

37-105: End-to-end happy-path looks solid.


107-160: Golden-path checks are thorough.

src/index.ts (1)

1-2: This review comment contradicts the package configuration and should be disregarded.

The original review assumes "@trufnetwork/evm-contracts" is a public npm package that consumers will import from. However, package.json explicitly declares "private": true, which prevents publication to npm registries. Additionally, there are no build scripts, no exports/main/types configuration, and no compiled output directory—indicating this package is not currently set up for external distribution.

If the package is genuinely meant to be private (development-only or internal), the review comment is incorrect. If it should be public, that requires separate changes: marking "private": false, adding build scripts, configuring exports/main/types in package.json, and generating compiled outputs.

Likely an incorrect or invalid review comment.

contracts/attestation/TrufAttestationHarness.sol (1)

14-44: LGTM on wrapper coverage and signatures.

Thin, pure wrappers for parse/hash/verify/decodeDataPoints/metadata/body/toAction align with the library API.

contracts/attestation/TrufAttestation.sol (2)

211-221: Canonical encoding matches TS (BE, length-prefixes).

Bytes layout (1+1+8, 4+20, 4+32, 2, 4+len(args), 4+len(result)) and big-endian ordering align with helpers.


275-283: Action bounds are well-handled.

_normalizeActionId and _requireKnownActionId properly reject 0 and out-of-range values.

test/helpers/attestation.ts (1)

1-1: Review comment is incorrect—no version mismatch exists.

Both src and tests use ethers v6. The package versions show ethers v6.13.4 in dependencies and @nomicfoundation/hardhat-ethers v^3.0.8 in devDependencies. @nomicfoundation/hardhat-ethers v3 is compatible with ethers v6, so tests importing ethers from Hardhat receive the same v6 library as src code. The claimed v5 vs v6 mismatch does not exist, and getBytes (a v6 function) will work correctly.

Likely an incorrect or invalid review comment.

@holdex
Copy link

holdex bot commented Oct 18, 2025

Time Submission Status

Member Status Time Action Last Update
@outerlook ❌ Missing - ⚠️ Submit time -
MicBun ✅ Submitted 20min Update time Oct 21, 2025, 1:55 PM

- Replaced the dotenv import position and updated the syntax for setting the BCRYPTO_FORCE_FALLBACK environment variable to use the nullish coalescing assignment operator.
- Streamlined the configuration file for better readability and maintainability.
@outerlook outerlook requested a review from MicBun October 18, 2025 18:36
@MicBun
Copy link
Member

MicBun commented Oct 19, 2025

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Oct 19, 2025

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
docs/AttestationLibrary.md (1)

1-62: LGTM: Comprehensive attestation library documentation.

The documentation provides clear usage guidance, payload structure, and best practices. The Solidity quickstart and TypeScript helpers sections are particularly helpful for developers.

Optional enhancement: Consider clarifying the signature format in the table at line 29:

-| `signature (65 bytes)` | Validator secp256k1 signature. |
+| `signature (65 bytes)` | Validator secp256k1 signature (32-byte r + 32-byte s + 1-byte v). |

This would help developers understand the signature structure when working with raw bytes.

contracts/attestation/TrufAttestationHarness.sol (1)

7-9: Remove unused using directives

The contract never calls extension methods (payload.parse(), att.toAction()). Drop these to avoid warnings.

-    using TrufAttestation for bytes;
-    using TrufAttestation for TrufAttestation.Attestation;
+
contracts/attestation/TrufAttestation.sol (4)

126-133: Prefer non‑reverting verification via ECDSA.tryRecover

ECDSA.recover reverts on malformed signatures, turning a boolean check into a hard revert. Using tryRecover keeps verify total, returning false on bad sigs while still guarding length/algorithm.

     function verify(Attestation memory att, address expectedValidator) internal pure returns (bool) {
         if (att.signature.length != SIGNATURE_LENGTH) revert AttestationInvalidSignatureLength();
         if (att.algorithm != ALGORITHM_SECP256K1) revert AttestationInvalidAlgorithm(att.algorithm);
 
         bytes32 digest = hash(att);
-        address recovered = ECDSA.recover(digest, att.signature);
-        return recovered == expectedValidator;
+        (address recovered, ECDSA.RecoverError err) = ECDSA.tryRecover(digest, att.signature);
+        if (err != ECDSA.RecoverError.NoError) {
+            return false;
+        }
+        return recovered == expectedValidator;
     }

75-85: Avoid copying the entire canonical blob to memory (optional perf)

You copy payload[:len-65] into bytes memory, then parse. With ^0.8.27, you can slice calldata to work zero‑copy and only allocate small chunks as needed (or refactor helpers to read from calldata).

-        uint256 canonicalLength = payload.length - SIGNATURE_LENGTH;
-        bytes memory canonical = new bytes(canonicalLength);
-        bytes memory signature = new bytes(SIGNATURE_LENGTH);
-        assembly {
-            calldatacopy(add(canonical, 0x20), payload.offset, canonicalLength)
-            calldatacopy(add(signature, 0x20), add(payload.offset, canonicalLength), SIGNATURE_LENGTH)
-        }
+        uint256 canonicalLength = payload.length - SIGNATURE_LENGTH;
+        bytes calldata canonical = payload[:canonicalLength];
+        bytes calldata signature = payload[canonicalLength:];
+        // If adopting this, adjust downstream readers to accept calldata where possible.

Please confirm calldata slicing availability in your compiler settings.


228-246: Use word-sized copies in _readLengthPrefixed (optional micro‑opt)

Byte‑by‑byte loop is gas‑heavy. A simple 32‑byte chunked copy in assembly reduces gas for large blobs. Keep the bounds checks as-is.

-        chunk = new bytes(length);
-        for (uint256 i; i < length; ) {
-            chunk[i] = data[offset + i];
-            unchecked {
-                ++i;
-            }
-        }
+        chunk = new bytes(length);
+        assembly {
+            let src := add(add(data, 0x20), offset)
+            let dst := add(chunk, 0x20)
+            // copy full words
+            for { let end := add(src, and(add(length, 31), not(31))) } lt(src, end) { src := add(src, 32) dst := add(dst, 32) } {
+                mstore(dst, mload(src))
+            }
+            // copy tail bytes
+            let rem := and(length, 31)
+            if gt(rem, 0) {
+                // mask = 0xffff.. << (32 - rem)*8
+                let mask := shl(mul(sub(32, rem), 8), not(0))
+                let srcw := mload(src)
+                let dstw := mload(dst)
+                mstore(dst, or(and(srcw, not(mask)), and(dstw, mask)))
+            }
+        }

200-209: Parsing allows unknown actionId; toAction enforces range (OK, just noting)

Current behavior accepts any 1..255 at parse and only enforces enum bounds when converting to Action. This is fine if callers always use toAction before branching. If you prefer earlier fail-fast, move _requireKnownActionId into parse.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e59c3df and 13ddedc.

📒 Files selected for processing (11)
  • README.md (2 hunks)
  • contracts/attestation/TrufAttestation.sol (1 hunks)
  • contracts/attestation/TrufAttestationHarness.sol (1 hunks)
  • docs/AttestationLibrary.md (1 hunks)
  • docs/DeveloperGuide.md (2 hunks)
  • hardhat.config.cts (3 hunks)
  • src/attestation.ts (1 hunks)
  • src/index.ts (1 hunks)
  • test/attestation/TrufAttestation.test.ts (1 hunks)
  • test/attestation/fixtures/attestation_golden.json (1 hunks)
  • test/helpers/attestation.ts (1 hunks)
🔇 Additional comments (15)
hardhat.config.cts (2)

1-2: LGTM! Previous critical issue correctly addressed.

The dotenv import is now at the top before any environment variable access, and the nullish coalescing assignment operator (??=) correctly allows .env values to take precedence while providing a default.


70-75: Verify the intended behavior when PRIVATE_KEY is not set.

When ETHEREUM_SEPOLIA_RPC_URL is defined but PRIVATE_KEY is not, Hardhat will use default test accounts (which have no funds on Sepolia). This will cause deployment or transaction attempts to fail with "insufficient funds" errors, which may confuse developers.

Is this intentional to support read-only operations, or should the config warn/error when ETHEREUM_SEPOLIA_RPC_URL is set without PRIVATE_KEY?

src/attestation.ts (2)

1-12: LGTM: Clean type definition.

The type definition correctly models the canonical attestation fields as specified in the documentation, with appropriate types for each field.


48-72: LGTM: Helper functions correctly implement encoding.

The signature validation, endianness handling, and length-prefixing logic are all correct. The 65-byte signature format aligns with ECDSA secp256k1 signatures (r + s + v).

README.md (2)

5-5: LGTM: Clear description update.

The updated description appropriately mentions the attestation verification library alongside existing functionality.


82-84: LGTM: Well-structured attestation library documentation.

The new section provides a clear overview with appropriate cross-references to the detailed guide and implementation.

docs/DeveloperGuide.md (1)

67-69: LGTM: Clear integration documentation.

The new subsection appropriately documents the attestation library integration points and references the detailed guide.

test/attestation/fixtures/attestation_golden.json (1)

1-21: LGTM: Well-structured golden fixture.

The fixture provides comprehensive deterministic test data with all necessary fields for validating parsing, hashing, verification, and decoding logic.

test/attestation/TrufAttestation.test.ts (4)

1-31: LGTM: Clean test setup with golden fixture.

The test file properly loads the golden fixture and extracts constants for deterministic testing.


38-105: LGTM: Comprehensive end-to-end test.

This test validates the complete attestation flow:

  • Canonical payload construction
  • Signing with a random wallet
  • Parsing and field validation
  • Hash computation
  • Signature verification (both positive and negative cases)
  • Data point decoding
  • Metadata and body extraction

The test coverage is thorough and assertions are appropriate.


107-160: LGTM: Thorough golden fixture validation.

This test validates parsing, verification, and decoding against a deterministic golden fixture, ensuring:

  • Correct field extraction
  • Action ID resolution including the edge case for unsupported IDs
  • Data point decoding with proper decimal handling
  • Signature verification
  • Canonical and signature byte extraction

The assertions against the golden values provide strong regression protection.


162-189: LGTM: Proper error handling test.

The test correctly validates that unsupported algorithms (algorithm: 2) trigger the AttestationInvalidAlgorithm error during verification, ensuring the library fails fast for unsupported signature schemes.

src/index.ts (1)

1-2: No action required—the "./lib" module exists as src/lib.ts.

The verification confirms that src/lib.ts exists and is correctly referenced by the export statement in src/index.ts. The module is present in the codebase as expected.

Likely an incorrect or invalid review comment.

contracts/attestation/TrufAttestationHarness.sol (1)

10-44: Harness surface looks good

Thin, pure wrappers are appropriate for testing the library behavior. No issues spotted.

contracts/attestation/TrufAttestation.sol (1)

11-17: Constants, enums, and custom errors read cleanly

Good coverage of invariants (version, algorithm, lengths) and action surfacing. Names and visibility are consistent.

Also applies to: 28-34, 35-51

… src/attestation

- Replaced the existing implementation of canonical attestation building and payload creation with new functions from the src/attestation module.
- Updated type definitions to align with the new structure, enhancing code clarity and maintainability.
@outerlook outerlook requested review from MicBun and removed request for MicBun October 20, 2025 16:09
Copy link
Member

@MicBun MicBun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm

@MicBun MicBun merged commit dd34067 into main Oct 20, 2025
4 of 5 checks passed
@MicBun MicBun deleted the feat/attestation branch October 20, 2025 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Problem: lack guidance for using the attestation library Problem: attestations lack an EVM parser library

3 participants