diff --git a/examples/attestation/README.md b/examples/attestation/README.md index 2cc4bfd..333daa9 100644 --- a/examples/attestation/README.md +++ b/examples/attestation/README.md @@ -17,14 +17,14 @@ Attestations enable validators to cryptographically sign query results, providin ## Prerequisites - Node.js >= 18 -- A wallet with TRUF tokens for transaction fees -- Private key with access to TRUF.NETWORK +- (Optional) A wallet with TRUF tokens if you want to request new attestations +- (Optional) Private key - defaults to test key if not provided ## Setup 1. **Install Dependencies**: ```bash - cd /home/micbun/trufnetwork/sdk-js + # From the sdk-js root directory npm install ``` @@ -42,25 +42,30 @@ Attestations enable validators to cryptographically sign query results, providin ### Quick Start (No Configuration) ```bash -# Run with default test key -npm run example:attestation -``` +# From the sdk-js root directory, navigate to the example +cd examples/attestation -Or directly: -```bash -npx tsx examples/attestation/index.ts +# Run with default test key +npm start ``` ### With Your Own Wallet +If you want to use your own private key instead of the test key: + ```bash -# Set your private key -export PRIVATE_KEY="0x..." +# From the sdk-js root directory, navigate to the example +cd examples/attestation + +# Set your private key (replace with your actual private key) +export PRIVATE_KEY="0x1234567890abcdef..." # Run the example -npm run example:attestation +npm start ``` +**Note**: Replace `0x1234567890abcdef...` with your actual 64-character hexadecimal private key. + ## Expected Output ``` @@ -72,10 +77,44 @@ Wallet address: 0x... ===== Requesting Attestation ===== Data Provider: 0x4710a8d8f0d845da110086812a32de6d90d7ff5c Stream ID: stai0000000000000000000000000000 -Time Range: 2025-10-14T... to 2025-10-21T... +Time Range: ... + +===== Listing Recent Attestations ===== + +Found ... attestations for 0x...: + +[1] Request TX: ... + Created at block: ... + Signed at block: ... + Attestation hash: ... + Encrypted: No + +... + +===== Retrieving Signed Attestation Payload ===== +Found ... signed attestation(s), retrieving the first one... + +āœ… Retrieved signed attestation for TX: ... + Payload size: ... bytes + First 64 bytes (hex): ... + Last 65 bytes (signature): ... + Full payload (hex): ... + +===== Extracting Validator Information ===== +āœ… Validator Address: 0x... + This is the address you should use in your EVM smart contract's verify() function + + šŸ’” How to use this payload: + 1. Send this hex payload to your EVM smart contract + 2. The contract can verify the signature using ecrecover + 3. Parse the payload to extract the attested query results + 4. Use the verified data in your on-chain logic + +===== Attempting to Request New Attestation ===== +āš ļø NOTE: This requires at least 40 TRUF balance for attestation fee āœ… Attestation requested! -Request TX ID: 0x... +Request TX ID: ... Waiting for transaction confirmation... āœ… Transaction confirmed! @@ -83,43 +122,16 @@ Waiting for transaction confirmation... ===== Waiting for Validator Signature ===== The leader validator will sign the attestation asynchronously (typically 1-2 blocks)... -āœ… Signed attestation received after 3 attempts! - -Payload size: 450 bytes -First 64 bytes (hex): 010000... -Last 65 bytes (signature): a7b3c2... - -===== Listing Recent Attestations ===== - -Found 5 attestations for 0x...: +āœ… Signed attestation received after ... attempts! -[1] Request TX: 0x... - Created at block: 12345 - Signed at block: 12347 - Attestation hash: abc123... - Encrypted: No - -... +Payload size: ... bytes +First 64 bytes (hex): ... +Last 65 bytes (signature): ... +Full payload (hex): ... ===== Summary ===== āœ… Successfully requested and retrieved a signed attestation! -Next steps: -- Use the payload in EVM smart contracts for verification -- Implement signature verification using ecrecover -- Parse the canonical payload to extract query results - -The signed attestation payload contains: -1. Version (1 byte) -2. Algorithm (1 byte, 0 = secp256k1) -3. Block height (8 bytes) -4. Data provider (20 bytes, length-prefixed) -5. Stream ID (32 bytes, length-prefixed) -6. Action ID (2 bytes) -7. Arguments (variable, length-prefixed) -8. Result (variable, length-prefixed) -9. Signature (65 bytes, secp256k1) - ✨ Example completed successfully! ``` @@ -167,26 +179,26 @@ function verifyAttestation(bytes memory payload, address expectedValidator) publ The signature can be verified using `ecrecover`: ```typescript -import { ethers } from "ethers"; +import { sha256, recoverAddress } from "ethers"; // Extract signature from payload (last 65 bytes) -const signature = payload.slice(-65); -const r = signature.slice(0, 32); -const s = signature.slice(32, 64); +const payload = signedPayload.payload; +const signatureOffset = payload.length - 65; +const canonicalPayload = payload.slice(0, signatureOffset); +const signature = payload.slice(signatureOffset); + +// Hash the canonical payload with SHA256 +const digest = sha256(canonicalPayload); + +// Recover validator address from signature +// The signature format is [R || S || V] where V is {27,28} +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]; -// Reconstruct message hash (SHA256 of canonical payload without signature) -const canonical = payload.slice(0, -65); -const messageHash = ethers.utils.sha256(canonical); - -// Recover signer -const recoveredAddress = ethers.utils.recoverAddress(messageHash, { - r: ethers.utils.hexlify(r), - s: ethers.utils.hexlify(s), - v: v -}); +const validatorAddress = recoverAddress(digest, { r, s, v }); -console.log(`Signer: ${recoveredAddress}`); +console.log(`Validator Address: ${validatorAddress}`); ``` ## Troubleshooting @@ -220,12 +232,12 @@ Ensure your private key is correctly formatted: ```typescript interface RequestAttestationInput { - dataProvider: string; // 0x-prefixed address (42 chars) - streamId: string; // 32 characters - actionName: string; // Action to attest - args: any[]; // Action arguments - encryptSig: boolean; // Must be false (MVP) - maxFee: number; // Maximum fee willing to pay + dataProvider: string; // 0x-prefixed address (42 chars) + streamId: string; // 32 characters + actionName: string; // Action to attest + args: any[]; // Action arguments + encryptSig: boolean; // Must be false (MVP) + maxFee: number | string | bigint; // Maximum fee willing to pay (in wei) } ``` @@ -251,8 +263,8 @@ interface ListAttestationsInput { ## Related Documentation - [SDK-JS Documentation](../../README.md) -- [Attestation Implementation Plan](../../../DataAttestation/SDK_JS_Attestation_Implementation_Plan.md) - [TRUF.NETWORK Documentation](https://docs.truf.network) +- [EVM Attestation Contracts](https://github.com/trufnetwork/evm-contracts/tree/main/contracts/attestation) ## Support diff --git a/examples/attestation/index.ts b/examples/attestation/index.ts index f885b37..1d3982f 100644 --- a/examples/attestation/index.ts +++ b/examples/attestation/index.ts @@ -13,7 +13,7 @@ */ import { NodeTNClient } from "../../src"; -import { Wallet } from "ethers"; +import { Wallet, sha256, recoverAddress } from "ethers"; async function main() { // ===== 1. Setup Client ===== @@ -21,13 +21,12 @@ async function main() { // Use default test private key if PRIVATE_KEY not set const privateKey = process.env.PRIVATE_KEY || "0x0000000000000000000000000000000000000000000000000000000000000001"; + const wallet = new Wallet(privateKey); if (!process.env.PRIVATE_KEY) { - console.log("āš ļø WARNING: Using default test private key (address: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf)"); - console.log(" Set PRIVATE_KEY environment variable to use your own wallet\n"); + console.log(`āš ļø WARNING: No PRIVATE_KEY environment variable provided, using hardcoded private key (wallet address: ${wallet.address})`); + console.log(` Attestation requests may fail if this is a test key or wallet without sufficient TRUF balance\n`); } - - const wallet = new Wallet(privateKey); const client = new NodeTNClient({ endpoint: process.env.ENDPOINT || "https://gateway.mainnet.truf.network", signerInfo: { @@ -43,7 +42,7 @@ async function main() { // ===== 2. Load Attestation Action ===== const attestationAction = client.loadAttestationAction(); - // ===== 3. Request Attestation ===== + // ===== Request Attestation Metadata ===== console.log("===== Requesting Attestation ====="); const dataProvider = "0x4710a8d8f0d845da110086812a32de6d90d7ff5c"; // AI Index data provider @@ -57,80 +56,7 @@ async function main() { console.log(`Stream ID: ${streamId}`); console.log(`Time Range: ${new Date(weekAgo * 1000).toISOString()} to ${new Date(now * 1000).toISOString()}\n`); - const requestResult = await attestationAction.requestAttestation({ - dataProvider, - streamId, - actionName: "get_record", // Attest the get_record query - args: [ - dataProvider, - streamId, - weekAgo, - now, - null, // frozen_at (not used) - false, // use_cache (will be forced to false for determinism) - ], - encryptSig: false, // Encryption not implemented in MVP - maxFee: "40000000000000000000", // Maximum fee in wei (1 TRUF = 1e18 wei) - }); - - console.log(`āœ… Attestation requested!`); - console.log(`Request TX ID: ${requestResult.requestTxId}\n`); - - // ===== 4. Wait for Transaction Confirmation ===== - console.log("Waiting for transaction confirmation..."); - - try { - await client.waitForTx(requestResult.requestTxId, 30000); // 30 second timeout - console.log("āœ… Transaction confirmed!\n"); - } catch (err) { - console.error("āŒ Transaction failed or timed out:", err); - process.exit(1); - } - - // ===== 5. Wait for Validator Signature ===== - console.log("===== Waiting for Validator Signature ====="); - console.log("The leader validator will sign the attestation asynchronously (typically 1-2 blocks)...\n"); - - let signedAttestation = null; - const maxAttempts = 15; // 30 seconds max (2 seconds per attempt) - - for (let i = 0; i < maxAttempts; i++) { - try { - const result = await attestationAction.getSignedAttestation({ - requestTxId: requestResult.requestTxId, - }); - - // Check if we got a valid payload (canonical + signature) - if (result.payload && result.payload.length > 65) { - signedAttestation = result; - console.log(`āœ… Signed attestation received after ${i + 1} attempts!`); - break; - } - } catch (err) { - // Not signed yet, continue polling - } - - // Progress indicator - process.stdout.write(`Attempt ${i + 1}/${maxAttempts}...`); - if (i < maxAttempts - 1) { - process.stdout.write("\r"); - await new Promise((resolve) => setTimeout(resolve, 2000)); - } else { - process.stdout.write("\n"); - } - } - - if (!signedAttestation) { - console.error("\nāŒ Attestation not signed within timeout period"); - console.log("The attestation may still be signed later. Try polling manually."); - process.exit(1); - } - - console.log(`\nPayload size: ${signedAttestation.payload.length} bytes`); - 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")}\n`); - - // ===== 6. List My Recent Attestations ===== + // ===== 3. List My Recent Attestations ===== console.log("===== Listing Recent Attestations =====\n"); // Get requester address as bytes @@ -139,7 +65,7 @@ async function main() { const attestations = await attestationAction.listAttestations({ requester: myAddressBytes, - limit: 10, + limit: 3, offset: 0, orderBy: "created_height desc", }); @@ -155,9 +81,162 @@ async function main() { console.log(""); }); - // ===== 7. Summary ===== + // ===== Demonstrate get_signed_attestation if we have a signed attestation ===== + const signedAtts = attestations.filter(att => att.signedHeight !== null); + if (signedAtts.length > 0) { + console.log("===== Retrieving Signed Attestation Payload ====="); + console.log(`Found ${signedAtts.length} signed attestation(s), retrieving the first one...\n`); + + const firstSigned = signedAtts[0]; + try { + const signedPayload = await attestationAction.getSignedAttestation({ + requestTxId: firstSigned.requestTxId, + }); + + console.log(`āœ… Retrieved signed attestation for TX: ${firstSigned.requestTxId}`); + + const payload = signedPayload.payload; + console.log(` Payload size: ${payload.length} bytes`); + console.log(` First 64 bytes (hex): ${Buffer.from(payload.slice(0, 64)).toString("hex")}`); + console.log(` Last 65 bytes (signature): ${Buffer.from(payload.slice(-65)).toString("hex")}`); + console.log(` Full payload (hex): ${Buffer.from(payload).toString("hex")}`); + + // ===== Extract Validator Public Key from Payload ===== + console.log(`\n===== Extracting Validator Information =====`); + + // Validate payload has minimum length (at least 1 byte data + 65 bytes signature) + if (payload.length < 66) { + console.log(`āš ļø Payload too short (${payload.length} bytes), expected at least 66 bytes\n`); + throw new Error(`Invalid payload format: too short (${payload.length} bytes)`); + } + + const signatureOffset = payload.length - 65; + const canonicalPayload = payload.slice(0, signatureOffset); + const signature = payload.slice(signatureOffset); + + // Hash the canonical payload with SHA256 (as per attestation spec) + const digest = sha256(canonicalPayload); + + // Recover validator address from signature + // The signature format is [R || S || V] where V is {27,28} + // ethers expects this format: { r, s, v } + 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}`); + 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:`); + 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`); + console.log(` 4. Use the verified data in your on-chain logic (e.g., settle bets, trigger payments)`); + } catch (err: any) { + console.log(`āš ļø Could not retrieve signed attestation: ${err.message}\n`); + } + } + + // ===== 4. Attempt to Request New Attestation (may fail if insufficient balance) ===== + console.log("\n===== Attempting to Request New Attestation ====="); + console.log("āš ļø NOTE: This requires at least 40 TRUF balance for attestation fee\n"); + + let requestResult; + let signedAttestation = null; + + try { + requestResult = await attestationAction.requestAttestation({ + dataProvider, + streamId, + actionName: "get_record", // Attest the get_record query + args: [ + dataProvider, + streamId, + weekAgo, + now, + null, // frozen_at (not used) + false, // use_cache (will be forced to false for determinism) + ], + encryptSig: false, // Encryption not implemented in MVP + maxFee: "40000000000000000000", // Maximum fee in wei (1 TRUF = 1e18 wei) + }); + + console.log(`āœ… Attestation requested!`); + console.log(`Request TX ID: ${requestResult.requestTxId}\n`); + + // Wait for Transaction Confirmation + console.log("Waiting for transaction confirmation..."); + await client.waitForTx(requestResult.requestTxId, 30000); // 30 second timeout + console.log("āœ… Transaction confirmed!\n"); + + // ===== Wait for Validator Signature ===== + console.log("===== Waiting for Validator Signature ====="); + console.log("The leader validator will sign the attestation asynchronously (typically 1-2 blocks)...\n"); + + const maxAttempts = 15; // 30 seconds max (2 seconds per attempt) + + for (let i = 0; i < maxAttempts; i++) { + try { + const result = await attestationAction.getSignedAttestation({ + requestTxId: requestResult.requestTxId, + }); + + // Check if we got a valid payload (canonical + signature) + if (result.payload && result.payload.length > 65) { + signedAttestation = result; + console.log(`āœ… Signed attestation received after ${i + 1} attempts!`); + break; + } + } catch (err: any) { + // Not signed yet, continue polling + // Log unexpected errors for debugging (skip expected "not found" or "not signed" errors) + if (err.message && !err.message.toLowerCase().includes("not found") && !err.message.toLowerCase().includes("not signed")) { + console.log(` āš ļø Warning: Unexpected error during polling: ${err.message}`); + } + } + + // Progress indicator + process.stdout.write(`Attempt ${i + 1}/${maxAttempts}...`); + if (i < maxAttempts - 1) { + process.stdout.write("\r"); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } else { + process.stdout.write("\n"); + } + } + + if (!signedAttestation) { + console.error("\nāŒ Attestation not signed within timeout period"); + console.log("The attestation may still be signed later. Try polling manually."); + } else { + console.log(`\nPayload size: ${signedAttestation.payload.length} bytes`); + 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`); + } + } catch (err: any) { + // Check if it's an insufficient balance error + if (err.message && err.message.includes("Insufficient balance")) { + console.log("\nāš ļø WARNING: Insufficient balance for attestation (requires 40 TRUF)"); + console.log(" This is expected for test keys or wallets without sufficient TRUF balance"); + console.log(" The listing functionality above still works and demonstrates the SDK capabilities\n"); + } else { + // Re-throw other errors + console.error("\nāŒ Unexpected error during attestation request:", err.message); + throw err; + } + } + + // ===== Summary ===== console.log("===== Summary ====="); - console.log("āœ… Successfully requested and retrieved a signed attestation!"); + if (signedAttestation) { + console.log("āœ… Successfully requested and retrieved a signed attestation!"); + } else { + console.log("āœ… Successfully demonstrated attestation listing functionality!"); + console.log("āš ļø Attestation request skipped due to insufficient balance (expected for test wallets)"); + } console.log("\nNext steps:"); console.log("- Use the payload in EVM smart contracts for verification"); console.log("- Implement signature verification using ecrecover"); diff --git a/examples/attestation/package.json b/examples/attestation/package.json new file mode 100644 index 0000000..22a0ffe --- /dev/null +++ b/examples/attestation/package.json @@ -0,0 +1,11 @@ +{ + "name": "attestation", + "version": "1.0.0", + "description": "This example demonstrates how to request, wait for, and retrieve signed data attestations from TRUF.NETWORK.", + "main": "index.js", + "scripts": { + "start": "tsx index.ts" + }, + "author": "", + "license": "ISC" +}