Skip to content

Commit 9e8ab8b

Browse files
authored
feat: view the result of attested data (#141)
* feat: view the result of attested data * docs: simplyfy readme * chore: apply suggestion
1 parent f049ffe commit 9e8ab8b

File tree

5 files changed

+885
-9
lines changed

5 files changed

+885
-9
lines changed

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,39 @@ attestations.forEach(att => {
274274
});
275275
```
276276

277+
#### Parsing Attestation Payloads
278+
279+
The SDK provides utilities to parse and verify signed attestation payloads:
280+
281+
```typescript
282+
import { parseAttestationPayload } from "@trufnetwork/sdk-js";
283+
import { sha256, recoverAddress } from "ethers";
284+
285+
// Get signed attestation
286+
const signed = await attestationAction.getSignedAttestation({
287+
requestTxId: result.requestTxId,
288+
});
289+
290+
// Extract canonical payload and signature
291+
const canonicalPayload = signed.payload.slice(0, -65);
292+
const signature = signed.payload.slice(-65);
293+
294+
// Verify signature
295+
const digest = sha256(canonicalPayload);
296+
const validatorAddress = recoverAddress(digest, {
297+
r: "0x" + Buffer.from(signature.slice(0, 32)).toString("hex"),
298+
s: "0x" + Buffer.from(signature.slice(32, 64)).toString("hex"),
299+
v: signature[64]
300+
});
301+
302+
// Parse and decode the payload
303+
const parsed = parseAttestationPayload(canonicalPayload);
304+
console.log(`Validator: ${validatorAddress}`);
305+
console.log(`Query Results: ${parsed.result.length} rows`);
306+
```
307+
308+
**📖 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.**
309+
277310
#### Attestation Payload Structure
278311

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

290323
This payload can be passed to EVM smart contracts for on-chain verification using `ecrecover`.
@@ -450,6 +483,7 @@ For other bundlers or serverless platforms, consult their documentation on modul
450483
| Get stream taxonomy | `composedAction.getTaxonomiesForStreams({streams, latestOnly})` |
451484
| Request attestation | `attestationAction.requestAttestation({dataProvider, streamId, actionName, args, encryptSig, maxFee})` |
452485
| Get signed attestation | `attestationAction.getSignedAttestation({requestTxId})` |
486+
| Parse attestation payload | `parseAttestationPayload(canonicalPayload)` |
453487
| List attestations | `attestationAction.listAttestations({requester, limit, offset, orderBy})` |
454488
| Get transaction event | `transactionAction.getTransactionEvent({txId})` |
455489
| List transaction fees | `transactionAction.listTransactionFees({wallet, mode, limit, offset})` |

docs/api-reference.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,167 @@ await primitiveAction.insertRecord({
747747
-**High-throughput data insertion** (independent records)
748748
-**Fire-and-forget operations** (with proper error handling)
749749

750+
## Attestation Payload Parsing
751+
752+
### `parseAttestationPayload(payload: Uint8Array): ParsedAttestationPayload`
753+
Parses and decodes a canonical attestation payload (without the 65-byte signature).
754+
755+
#### Parameters
756+
- `payload: Uint8Array` - Canonical payload bytes (full payload minus last 65 bytes)
757+
758+
#### Returns
759+
- `ParsedAttestationPayload` object with:
760+
- `version: number` - Protocol version (currently 1)
761+
- `algorithm: number` - Signature algorithm (0 = secp256k1)
762+
- `blockHeight: bigint` - Block height when attestation was created
763+
- `dataProvider: string` - Data provider Ethereum address (hex format)
764+
- `streamId: string` - Stream identifier
765+
- `actionId: number` - Action identifier
766+
- `arguments: any[]` - Decoded action arguments
767+
- `result: DecodedRow[]` - Decoded query results as rows (see [`DecodedRow`](#decodedrow))
768+
769+
#### Example
770+
```typescript
771+
import { parseAttestationPayload } from "@trufnetwork/sdk-js";
772+
import { sha256, recoverAddress } from "ethers";
773+
774+
// Get signed attestation
775+
const attestationAction = client.loadAttestationAction();
776+
const signedAttestation = await attestationAction.getSignedAttestation({
777+
requestTxId: "0x..."
778+
});
779+
780+
// Extract canonical payload (without signature)
781+
const payloadBytes = signedAttestation.payload;
782+
const canonicalPayload = payloadBytes.slice(0, -65);
783+
const signature = payloadBytes.slice(-65);
784+
785+
// Verify signature
786+
const digest = sha256(canonicalPayload);
787+
const r = "0x" + Buffer.from(signature.slice(0, 32)).toString("hex");
788+
const s = "0x" + Buffer.from(signature.slice(32, 64)).toString("hex");
789+
const v = signature[64];
790+
const validatorAddress = recoverAddress(digest, { r, s, v });
791+
792+
// Parse payload
793+
const parsed = parseAttestationPayload(canonicalPayload);
794+
795+
console.log(`Validator: ${validatorAddress}`);
796+
console.log(`Block: ${parsed.blockHeight}`);
797+
console.log(`Provider: ${parsed.dataProvider}`);
798+
console.log(`Stream: ${parsed.streamId}`);
799+
console.log(`Results: ${parsed.result.length} rows`);
800+
801+
// Access query results
802+
parsed.result.forEach((row, idx) => {
803+
const [timestamp, value] = row.values;
804+
console.log(`Row ${idx + 1}: timestamp=${timestamp}, value=${value}`);
805+
});
806+
```
807+
808+
### `DecodedRow`
809+
810+
Represents a decoded row from attestation query results.
811+
812+
#### Type Definition
813+
```typescript
814+
interface DecodedRow {
815+
values: any[];
816+
}
817+
```
818+
819+
#### Fields
820+
- `values: any[]` - Array of decoded column values
821+
- For attestation results: `values[0]` is the timestamp (string), `values[1]` is the value (string)
822+
- Values are decoded according to their data types (integers as BigInt, strings as string, etc.)
823+
824+
#### Example
825+
```typescript
826+
// Example DecodedRow from attestation result
827+
const row: DecodedRow = {
828+
values: [
829+
"1704067200", // timestamp (Unix time as string)
830+
"77.051806494788211665" // value (18-decimal fixed-point as string)
831+
]
832+
};
833+
834+
// Accessing row data
835+
const [timestamp, value] = row.values;
836+
console.log(`Timestamp: ${timestamp}, Value: ${value}`);
837+
```
838+
839+
**Note**: When used in attestation results (via `parseAttestationPayload`), each `DecodedRow` contains exactly two values: a Unix timestamp and a decimal value string.
840+
841+
### Attestation Result Format
842+
843+
Query results in attestations are ABI-encoded as:
844+
```solidity
845+
abi.encode(uint256[] timestamps, int256[] values)
846+
```
847+
848+
Where:
849+
- **timestamps**: Array of Unix timestamps (uint256)
850+
- **values**: Array of 18-decimal fixed-point integers (int256)
851+
852+
Example decoded output:
853+
```javascript
854+
[
855+
{ values: ["1704067200", "77.051806494788211665"] },
856+
{ values: ["1704153600", "78.718654581755352351"] },
857+
// ...
858+
]
859+
```
860+
861+
### Complete Attestation Workflow
862+
863+
```typescript
864+
// 1. Request attestation
865+
const attestationAction = client.loadAttestationAction();
866+
const result = await attestationAction.requestAttestation({
867+
dataProvider: "0x4710a8d8f0d845da110086812a32de6d90d7ff5c",
868+
streamId: "stai0000000000000000000000000000",
869+
actionName: "get_record",
870+
args: [...],
871+
encryptSig: false,
872+
maxFee: 1000000,
873+
});
874+
875+
// 2. Wait for transaction confirmation
876+
await client.waitForTx(result.requestTxId);
877+
878+
// 3. Poll for signature (validators sign asynchronously)
879+
let signedAttestation;
880+
for (let i = 0; i < 15; i++) {
881+
try {
882+
signedAttestation = await attestationAction.getSignedAttestation({
883+
requestTxId: result.requestTxId,
884+
});
885+
if (signedAttestation.payload.length > 65) break;
886+
} catch (e) {
887+
await new Promise(resolve => setTimeout(resolve, 2000));
888+
}
889+
}
890+
891+
// 4. Parse and verify
892+
const canonicalPayload = signedAttestation.payload.slice(0, -65);
893+
const signature = signedAttestation.payload.slice(-65);
894+
895+
const digest = sha256(canonicalPayload);
896+
const validatorAddress = recoverAddress(digest, {
897+
r: "0x" + Buffer.from(signature.slice(0, 32)).toString("hex"),
898+
s: "0x" + Buffer.from(signature.slice(32, 64)).toString("hex"),
899+
v: signature[64],
900+
});
901+
902+
const parsed = parseAttestationPayload(canonicalPayload);
903+
904+
// 5. Use the verified data
905+
console.log(`✅ Verified by: ${validatorAddress}`);
906+
parsed.result.forEach(row => {
907+
console.log(`Data: ${row.values}`);
908+
});
909+
```
910+
750911
## Performance Recommendations
751912
- Use batch record insertions
752913
- Implement client-side caching

examples/attestation/index.ts

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import { NodeTNClient } from "../../src";
1616
import { Wallet, sha256, recoverAddress } from "ethers";
17+
import { parseAttestationPayload } from "../../src/util/AttestationEncoding";
1718

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

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

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

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

132-
console.log(` 💡 How to use this payload:`);
134+
// ===== Parse Attestation Payload =====
135+
console.log(`===== Parsing Attestation Payload =====`);
136+
137+
const parsed = parseAttestationPayload(canonicalPayload);
138+
139+
console.log(`📋 Attestation Details:`);
140+
console.log(` Version: ${parsed.version}`);
141+
console.log(` Algorithm: ${parsed.algorithm} (0 = secp256k1)`);
142+
console.log(` Block Height: ${parsed.blockHeight}`);
143+
console.log(` Data Provider: ${parsed.dataProvider}`);
144+
console.log(` Stream ID: ${parsed.streamId}`);
145+
console.log(` Action ID: ${parsed.actionId}\n`);
146+
147+
console.log(`📊 Attested Query Result (from get_record):`);
148+
if (parsed.result.length === 0) {
149+
console.log(` No records found`);
150+
} else {
151+
console.log(` Found ${parsed.result.length} row(s):\n`);
152+
parsed.result.forEach((row, idx) => {
153+
console.log(` Row ${idx + 1}: ${JSON.stringify(row.values)}`);
154+
});
155+
}
156+
157+
console.log(`\n 💡 How to use this payload:`);
133158
console.log(` 1. Send this hex payload to your EVM smart contract`);
134159
console.log(` 2. The contract can verify the signature using ecrecover`);
135160
console.log(` 3. Parse the payload to extract the attested query results`);
@@ -154,8 +179,8 @@ async function main() {
154179
args: [
155180
dataProvider,
156181
streamId,
157-
weekAgo,
158-
now,
182+
startTime,
183+
endTime,
159184
null, // frozen_at (not used)
160185
false, // use_cache (will be forced to false for determinism)
161186
],
@@ -215,6 +240,48 @@ async function main() {
215240
console.log(`First 64 bytes (hex): ${Buffer.from(signedAttestation.payload.slice(0, 64)).toString("hex")}`);
216241
console.log(`Last 65 bytes (signature): ${Buffer.from(signedAttestation.payload.slice(-65)).toString("hex")}`);
217242
console.log(`Full payload (hex): ${Buffer.from(signedAttestation.payload).toString("hex")}\n`);
243+
244+
// ===== Parse and Display the Attestation =====
245+
try {
246+
console.log(`===== Parsing Attestation Payload =====`);
247+
248+
const signatureOffset = signedAttestation.payload.length - 65;
249+
const canonicalPayload = signedAttestation.payload.slice(0, signatureOffset);
250+
const signature = signedAttestation.payload.slice(signatureOffset);
251+
252+
// Verify signature
253+
const digest = sha256(canonicalPayload);
254+
const r = "0x" + Buffer.from(signature.slice(0, 32)).toString("hex");
255+
const s = "0x" + Buffer.from(signature.slice(32, 64)).toString("hex");
256+
const v = signature[64];
257+
const validatorAddress = recoverAddress(digest, { r, s, v });
258+
259+
console.log(`✅ Validator Address: ${validatorAddress}\n`);
260+
261+
// Parse payload
262+
const parsed = parseAttestationPayload(canonicalPayload);
263+
264+
console.log(`📋 Attestation Details:`);
265+
console.log(` Version: ${parsed.version}`);
266+
console.log(` Algorithm: ${parsed.algorithm} (0 = secp256k1)`);
267+
console.log(` Block Height: ${parsed.blockHeight}`);
268+
console.log(` Data Provider: ${parsed.dataProvider}`);
269+
console.log(` Stream ID: ${parsed.streamId}`);
270+
console.log(` Action ID: ${parsed.actionId}\n`);
271+
272+
console.log(`📊 Attested Query Result (from get_record):`);
273+
if (parsed.result.length === 0) {
274+
console.log(` No records found`);
275+
} else {
276+
console.log(` Found ${parsed.result.length} row(s):\n`);
277+
parsed.result.forEach((row, idx) => {
278+
console.log(` Row ${idx + 1}: ${JSON.stringify(row.values)}`);
279+
});
280+
}
281+
console.log("");
282+
} catch (parseErr: any) {
283+
console.log(`⚠️ Could not parse payload: ${parseErr.message}\n`);
284+
}
218285
}
219286
} catch (err: any) {
220287
// Check if it's an insufficient balance error

src/internal.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ export { StreamId } from "./util/StreamId";
2121
export { EthereumAddress } from "./util/EthereumAddress";
2222
export { visibility } from "./util/visibility";
2323

24+
// Attestation encoding/decoding utilities
25+
export {
26+
parseAttestationPayload
27+
} from "./util/AttestationEncoding";
28+
29+
export type {
30+
DecodedRow,
31+
ParsedAttestationPayload
32+
} from "./util/AttestationEncoding";
33+
2434
// Contract values and types
2535
export { StreamType } from "./contracts-api/contractValues";
2636

0 commit comments

Comments
 (0)