Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,39 @@ attestations.forEach(att => {
});
```

#### Parsing Attestation Payloads

The SDK provides utilities to parse and verify signed attestation payloads:

```typescript
import { parseAttestationPayload } from "@trufnetwork/sdk-js";
import { sha256, recoverAddress } from "ethers";

// Get signed attestation
const signed = await attestationAction.getSignedAttestation({
requestTxId: result.requestTxId,
});

// Extract canonical payload and signature
const canonicalPayload = signed.payload.slice(0, -65);
const signature = signed.payload.slice(-65);

// Verify signature
const digest = sha256(canonicalPayload);
const validatorAddress = recoverAddress(digest, {
r: "0x" + Buffer.from(signature.slice(0, 32)).toString("hex"),
s: "0x" + Buffer.from(signature.slice(32, 64)).toString("hex"),
v: signature[64]
});

// Parse and decode the payload
const parsed = parseAttestationPayload(canonicalPayload);
console.log(`Validator: ${validatorAddress}`);
console.log(`Query Results: ${parsed.result.length} rows`);
```

**📖 For complete documentation including signature verification, payload structure, result decoding, and EVM integration examples, see the [Attestation Payload Parsing](./docs/api-reference.md#attestation-payload-parsing) section in the API Reference.**

#### Attestation Payload Structure

The signed attestation payload is a binary blob containing:
Expand All @@ -284,7 +317,7 @@ The signed attestation payload is a binary blob containing:
5. Stream ID (32 bytes, length-prefixed)
6. Action ID (2 bytes)
7. Arguments (variable, length-prefixed)
8. Result (variable, length-prefixed)
8. Result (variable, ABI-encoded, length-prefixed)
9. Signature (65 bytes, secp256k1)

This payload can be passed to EVM smart contracts for on-chain verification using `ecrecover`.
Expand Down Expand Up @@ -450,6 +483,7 @@ For other bundlers or serverless platforms, consult their documentation on modul
| Get stream taxonomy | `composedAction.getTaxonomiesForStreams({streams, latestOnly})` |
| Request attestation | `attestationAction.requestAttestation({dataProvider, streamId, actionName, args, encryptSig, maxFee})` |
| Get signed attestation | `attestationAction.getSignedAttestation({requestTxId})` |
| Parse attestation payload | `parseAttestationPayload(canonicalPayload)` |
| List attestations | `attestationAction.listAttestations({requester, limit, offset, orderBy})` |
| Get transaction event | `transactionAction.getTransactionEvent({txId})` |
| List transaction fees | `transactionAction.listTransactionFees({wallet, mode, limit, offset})` |
Expand Down
161 changes: 161 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,167 @@ await primitiveAction.insertRecord({
- ⚡ **High-throughput data insertion** (independent records)
- ⚡ **Fire-and-forget operations** (with proper error handling)

## Attestation Payload Parsing

### `parseAttestationPayload(payload: Uint8Array): ParsedAttestationPayload`
Parses and decodes a canonical attestation payload (without the 65-byte signature).

#### Parameters
- `payload: Uint8Array` - Canonical payload bytes (full payload minus last 65 bytes)

#### Returns
- `ParsedAttestationPayload` object with:
- `version: number` - Protocol version (currently 1)
- `algorithm: number` - Signature algorithm (0 = secp256k1)
- `blockHeight: bigint` - Block height when attestation was created
- `dataProvider: string` - Data provider Ethereum address (hex format)
- `streamId: string` - Stream identifier
- `actionId: number` - Action identifier
- `arguments: any[]` - Decoded action arguments
- `result: DecodedRow[]` - Decoded query results as rows (see [`DecodedRow`](#decodedrow))

#### Example
```typescript
import { parseAttestationPayload } from "@trufnetwork/sdk-js";
import { sha256, recoverAddress } from "ethers";

// Get signed attestation
const attestationAction = client.loadAttestationAction();
const signedAttestation = await attestationAction.getSignedAttestation({
requestTxId: "0x..."
});

// Extract canonical payload (without signature)
const payloadBytes = signedAttestation.payload;
const canonicalPayload = payloadBytes.slice(0, -65);
const signature = payloadBytes.slice(-65);

// Verify signature
const digest = sha256(canonicalPayload);
const r = "0x" + Buffer.from(signature.slice(0, 32)).toString("hex");
const s = "0x" + Buffer.from(signature.slice(32, 64)).toString("hex");
const v = signature[64];
const validatorAddress = recoverAddress(digest, { r, s, v });

// Parse payload
const parsed = parseAttestationPayload(canonicalPayload);

console.log(`Validator: ${validatorAddress}`);
console.log(`Block: ${parsed.blockHeight}`);
console.log(`Provider: ${parsed.dataProvider}`);
console.log(`Stream: ${parsed.streamId}`);
console.log(`Results: ${parsed.result.length} rows`);

// Access query results
parsed.result.forEach((row, idx) => {
const [timestamp, value] = row.values;
console.log(`Row ${idx + 1}: timestamp=${timestamp}, value=${value}`);
});
```

### `DecodedRow`

Represents a decoded row from attestation query results.

#### Type Definition
```typescript
interface DecodedRow {
values: any[];
}
```

#### Fields
- `values: any[]` - Array of decoded column values
- For attestation results: `values[0]` is the timestamp (string), `values[1]` is the value (string)
- Values are decoded according to their data types (integers as BigInt, strings as string, etc.)

#### Example
```typescript
// Example DecodedRow from attestation result
const row: DecodedRow = {
values: [
"1704067200", // timestamp (Unix time as string)
"77.051806494788211665" // value (18-decimal fixed-point as string)
]
};

// Accessing row data
const [timestamp, value] = row.values;
console.log(`Timestamp: ${timestamp}, Value: ${value}`);
```

**Note**: When used in attestation results (via `parseAttestationPayload`), each `DecodedRow` contains exactly two values: a Unix timestamp and a decimal value string.

### Attestation Result Format

Query results in attestations are ABI-encoded as:
```solidity
abi.encode(uint256[] timestamps, int256[] values)
```

Where:
- **timestamps**: Array of Unix timestamps (uint256)
- **values**: Array of 18-decimal fixed-point integers (int256)

Example decoded output:
```javascript
[
{ values: ["1704067200", "77.051806494788211665"] },
{ values: ["1704153600", "78.718654581755352351"] },
// ...
]
```

### Complete Attestation Workflow

```typescript
// 1. Request attestation
const attestationAction = client.loadAttestationAction();
const result = await attestationAction.requestAttestation({
dataProvider: "0x4710a8d8f0d845da110086812a32de6d90d7ff5c",
streamId: "stai0000000000000000000000000000",
actionName: "get_record",
args: [...],
encryptSig: false,
maxFee: 1000000,
});

// 2. Wait for transaction confirmation
await client.waitForTx(result.requestTxId);

// 3. Poll for signature (validators sign asynchronously)
let signedAttestation;
for (let i = 0; i < 15; i++) {
try {
signedAttestation = await attestationAction.getSignedAttestation({
requestTxId: result.requestTxId,
});
if (signedAttestation.payload.length > 65) break;
} catch (e) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
}

// 4. Parse and verify
const canonicalPayload = signedAttestation.payload.slice(0, -65);
const signature = signedAttestation.payload.slice(-65);

const digest = sha256(canonicalPayload);
const validatorAddress = recoverAddress(digest, {
r: "0x" + Buffer.from(signature.slice(0, 32)).toString("hex"),
s: "0x" + Buffer.from(signature.slice(32, 64)).toString("hex"),
v: signature[64],
});

const parsed = parseAttestationPayload(canonicalPayload);

// 5. Use the verified data
console.log(`✅ Verified by: ${validatorAddress}`);
parsed.result.forEach(row => {
console.log(`Data: ${row.values}`);
});
```

## Performance Recommendations
- Use batch record insertions
- Implement client-side caching
Expand Down
81 changes: 74 additions & 7 deletions examples/attestation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import { NodeTNClient } from "../../src";
import { Wallet, sha256, recoverAddress } from "ethers";
import { parseAttestationPayload } from "../../src/util/AttestationEncoding";

async function main() {
// ===== 1. Setup Client =====
Expand Down Expand Up @@ -48,13 +49,14 @@ async function main() {
const dataProvider = "0x4710a8d8f0d845da110086812a32de6d90d7ff5c"; // AI Index data provider
const streamId = "stai0000000000000000000000000000"; // AI Index stream

// Query last 7 days of data
const now = Math.floor(Date.now() / 1000);
const weekAgo = now - 7 * 24 * 60 * 60;
// Query data from January 2024 (when the stream has data)
const startTime = 1704067200; // Jan 1, 2024 00:00:00 UTC
const endTime = startTime + (30 * 24 * 60 * 60); // 30 days later

console.log(`Data Provider: ${dataProvider}`);
console.log(`Stream ID: ${streamId}`);
console.log(`Time Range: ${new Date(weekAgo * 1000).toISOString()} to ${new Date(now * 1000).toISOString()}\n`);
console.log(`Time Range: ${new Date(startTime * 1000).toISOString()} to ${new Date(endTime * 1000).toISOString()}`);
console.log(` (Using historical data from January 2024)\n`);

// ===== 3. List My Recent Attestations =====
console.log("===== Listing Recent Attestations =====\n");
Expand Down Expand Up @@ -129,7 +131,30 @@ async function main() {
console.log(`✅ Validator Address: ${validatorAddress}`);
console.log(` This is the address you should use in your EVM smart contract's verify() function\n`);

console.log(` 💡 How to use this payload:`);
// ===== Parse Attestation Payload =====
console.log(`===== Parsing Attestation Payload =====`);

const parsed = parseAttestationPayload(canonicalPayload);

console.log(`📋 Attestation Details:`);
console.log(` Version: ${parsed.version}`);
console.log(` Algorithm: ${parsed.algorithm} (0 = secp256k1)`);
console.log(` Block Height: ${parsed.blockHeight}`);
console.log(` Data Provider: ${parsed.dataProvider}`);
console.log(` Stream ID: ${parsed.streamId}`);
console.log(` Action ID: ${parsed.actionId}\n`);

console.log(`📊 Attested Query Result (from get_record):`);
if (parsed.result.length === 0) {
console.log(` No records found`);
} else {
console.log(` Found ${parsed.result.length} row(s):\n`);
parsed.result.forEach((row, idx) => {
console.log(` Row ${idx + 1}: ${JSON.stringify(row.values)}`);
});
}

console.log(`\n 💡 How to use this payload:`);
console.log(` 1. Send this hex payload to your EVM smart contract`);
console.log(` 2. The contract can verify the signature using ecrecover`);
console.log(` 3. Parse the payload to extract the attested query results`);
Expand All @@ -154,8 +179,8 @@ async function main() {
args: [
dataProvider,
streamId,
weekAgo,
now,
startTime,
endTime,
null, // frozen_at (not used)
false, // use_cache (will be forced to false for determinism)
],
Expand Down Expand Up @@ -215,6 +240,48 @@ async function main() {
console.log(`First 64 bytes (hex): ${Buffer.from(signedAttestation.payload.slice(0, 64)).toString("hex")}`);
console.log(`Last 65 bytes (signature): ${Buffer.from(signedAttestation.payload.slice(-65)).toString("hex")}`);
console.log(`Full payload (hex): ${Buffer.from(signedAttestation.payload).toString("hex")}\n`);

// ===== Parse and Display the Attestation =====
try {
console.log(`===== Parsing Attestation Payload =====`);

const signatureOffset = signedAttestation.payload.length - 65;
const canonicalPayload = signedAttestation.payload.slice(0, signatureOffset);
const signature = signedAttestation.payload.slice(signatureOffset);

// Verify signature
const digest = sha256(canonicalPayload);
const r = "0x" + Buffer.from(signature.slice(0, 32)).toString("hex");
const s = "0x" + Buffer.from(signature.slice(32, 64)).toString("hex");
const v = signature[64];
const validatorAddress = recoverAddress(digest, { r, s, v });

console.log(`✅ Validator Address: ${validatorAddress}\n`);

// Parse payload
const parsed = parseAttestationPayload(canonicalPayload);

console.log(`📋 Attestation Details:`);
console.log(` Version: ${parsed.version}`);
console.log(` Algorithm: ${parsed.algorithm} (0 = secp256k1)`);
console.log(` Block Height: ${parsed.blockHeight}`);
console.log(` Data Provider: ${parsed.dataProvider}`);
console.log(` Stream ID: ${parsed.streamId}`);
console.log(` Action ID: ${parsed.actionId}\n`);

console.log(`📊 Attested Query Result (from get_record):`);
if (parsed.result.length === 0) {
console.log(` No records found`);
} else {
console.log(` Found ${parsed.result.length} row(s):\n`);
parsed.result.forEach((row, idx) => {
console.log(` Row ${idx + 1}: ${JSON.stringify(row.values)}`);
});
}
console.log("");
} catch (parseErr: any) {
console.log(`⚠️ Could not parse payload: ${parseErr.message}\n`);
}
}
} catch (err: any) {
// Check if it's an insufficient balance error
Expand Down
10 changes: 10 additions & 0 deletions src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ export { StreamId } from "./util/StreamId";
export { EthereumAddress } from "./util/EthereumAddress";
export { visibility } from "./util/visibility";

// Attestation encoding/decoding utilities
export {
parseAttestationPayload
} from "./util/AttestationEncoding";

export type {
DecodedRow,
ParsedAttestationPayload
} from "./util/AttestationEncoding";

// Contract values and types
export { StreamType } from "./contracts-api/contractValues";

Expand Down
Loading
Loading