From 0ce1972a22ed9eb13d5c7e24767d5d251f5fb84b Mon Sep 17 00:00:00 2001 From: Melvillian Date: Tue, 4 Nov 2025 14:21:38 -0500 Subject: [PATCH] docs(flashtestations-sdk): prepare SDK for initial public release with comprehensive documentation This commit prepares the flashtestations-sdk for its initial public release by: 1. Overhauling documentation and cleaning up the public API surface. 2. Transforming the README from a generic TSDX template into comprehensive SDK documentation including installation instructions, API reference, error handling guide, advanced usage patterns, and practical examples. 3. Unused example files (getBlock.ts and getTransactionReceipt.ts) have been removed In a future commit we will update the version number to 1.0.0 and publish the SDK to npm improving VerificationResult handling --- sdks/flashtestations-sdk/.gitignore | 2 + sdks/flashtestations-sdk/README.md | 346 +++++++++++++++--- sdks/flashtestations-sdk/examples/getBlock.ts | 55 --- .../examples/getFlashtestationTx.ts | 27 +- .../examples/getTransactionReceipt.ts | 66 ---- .../examples/verifyBlock.ts | 28 +- sdks/flashtestations-sdk/package.json | 3 +- sdks/flashtestations-sdk/src/index.ts | 45 ++- sdks/flashtestations-sdk/src/types/index.ts | 47 ++- .../src/verification/service.ts | 99 +++-- .../test/verification/service.test.ts | 83 +++-- 11 files changed, 518 insertions(+), 283 deletions(-) delete mode 100644 sdks/flashtestations-sdk/examples/getBlock.ts delete mode 100644 sdks/flashtestations-sdk/examples/getTransactionReceipt.ts diff --git a/sdks/flashtestations-sdk/.gitignore b/sdks/flashtestations-sdk/.gitignore index 4c9d7c35a..89c77d417 100644 --- a/sdks/flashtestations-sdk/.gitignore +++ b/sdks/flashtestations-sdk/.gitignore @@ -2,3 +2,5 @@ .DS_Store node_modules dist + +.claude-output/ diff --git a/sdks/flashtestations-sdk/README.md b/sdks/flashtestations-sdk/README.md index 93eb55df4..9eb979b05 100644 --- a/sdks/flashtestations-sdk/README.md +++ b/sdks/flashtestations-sdk/README.md @@ -1,103 +1,337 @@ -# TSDX User Guide +# Flashtestations SDK -Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. +## Overview -> This TSDX setup is meant for developing libraries (not apps!) that can be published to NPM. If you’re looking to build a Node app, you could use `ts-node-dev`, plain `ts-node`, or simple `tsc`. +Flashtestations are cryptographic proofs that blockchain blocks were built by Trusted Execution Environments (TEEs) running a specific version of [Flashbot's op-rbuilder](https://github.com/flashbots/op-rbuilder/tree/main), which is the TEE-based builder used to build blocks on Unichain. This SDK allows you to verify whether blocks on Unichain networks were built by the expected versions of op-rbuilder running in a TEE. Unlike on other blockchains where you have no guarantee and thus must trust the block builder to build blocks [fairly](https://www.paradigm.xyz/2024/06/priority-is-all-you-need), with flashtestations you can cryptographically verify that a Unichain block has been built with a particular version of op-rbuilder. -> If you’re new to TypeScript, checkout [this handy cheatsheet](https://devhints.io/typescript) +The TEE devices that run Unichain's builder software provide hardware-enforced isolation and attestation, enabling transparent and verifiable block building. Each TEE workload (i.e. a specific version of op-rbuilder running in a TEE) is uniquely identified by measurement registers that cryptographically commit to the exact software running inside the TEE. When op-rbuilder builds a block on Unichain, it emits a "flashtestation" transaction as the last transaction in the block that proves which workload built that block. -## Commands +This SDK simplifies the verification process by providing a single function to check if a block contains a valid flashtestation matching your expected workload ID. For more background on flashtestations and TEE-based block building, see the [flashtestations spec](https://github.com/flashbots/rollup-boost/blob/main/specs/flashtestations.md) and the [flashtestations smart contracts](https://github.com/flashbots/flashtestations). -TSDX scaffolds your new library inside `/src`. +## Getting Started -To run TSDX, use: +### Installation ```bash -npm start # or yarn start +npm install flashtestations-sdk +# or +yarn add flashtestations-sdk ``` -This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. +### Quick Start + +```typescript +import { verifyFlashtestationInBlock } from 'flashtestations-sdk'; + +async function main() { + // Verify if the latest block on Unichain Mainnet was built by a specific TEE workload + const result = await verifyFlashtestationInBlock( + '0x306ab4fe782dde50a97584b6d4cad9375f7b5d02199c4c78821ad6622670c6b7', // Your expected workload ID + 'latest', // Block to verify (can be 'latest', 'pending', 'safe', 'finalized', number, or hash) + { chainId: 130 } // Unichain Mainnet + ); + + if (result.isBuiltByExpectedTee) { + console.log('✓ Block was built by the expected TEE workload!'); + console.log(`Workload ID: ${result.workloadMetadata.workloadId}`); + console.log(`Commit Hash: ${result.workloadMetadata.commitHash}`); + console.log(`Builder Address: ${result.workloadMetadata.builderAddress}`); + console.log(`Version: ${result.workloadMetadata.version}`); + } else { + console.log('\n✗ Block was NOT built by the specified TEE workload\n'); + + if (result.workloadMetadata) { + console.log('Block was built by a different TEE workload:'); + console.log(`Workload ID: ${result.workloadMetadata.workloadId}`); + console.log(`Commit Hash: ${result.workloadMetadata.commitHash}`); + console.log(`Builder Address: ${result.workloadMetadata.builderAddress}`); + console.log(`Version: ${result.workloadMetadata.version}`); + console.log( + `Source Locators: ${ + result.workloadMetadata.sourceLocators.length > 0 + ? result.workloadMetadata.sourceLocators.join(', ') + : 'None' + }` + ); + } else { + console.log('The block does not contain a flashtestation transaction'); + } + } +} + +// run the quick start +main(); +``` + +## Supported Chains + +| Chain | Chain ID | Status | RPC Configuration | +| ---------------- | -------- | ---------- | ----------------- | +| Unichain Mainnet | 130 | Production | Auto-configured | +| Unichain Sepolia | 1301 | Testnet | Auto-configured | + +## How Do I Acquire a Particular op-rbuilder's Workload ID? + +The Flashtestations protocol exists to let you cryptographically verify that a particular version of op-rbuilder is in fact building the latest block's on Unichain. To cryptographically identify these op-rbuilder versions across all of the various components (the TEE, the smart contracts, and SDK) we use a 32-byte workload ID, which is a [hash of the important components of the TEE attestation](https://github.com/flashbots/flashtestations/blob/7cc7f68492fe672a823dd2dead649793aac1f216/src/BlockBuilderPolicy.sol#L224). But this workload ID tells us nothing about what op-rbuilder source code the builder operators used to build the final Linux OS image that runs on the TEE. We need a trustless (i.e. locally verifiable) method for calculating the workload ID, given a version of op-rbuilder. + +With a small caveat we'll explain shortly, that process is what the [flashbots-images](https://github.com/flashbots/flashbots-images/commits/main/) repo is for. Using this repo and a simple bash command, we build a Linux OS image containing a specific version of op-rbuilder (identified by its git commit hash), and then calculate the workload ID directly from this Linux OS image. This completes the full chain of trustless verification; given a particular commit hash of flashbots-images (which has hardcoded into it a particular version of op-rbuilder), we can locally build and compute the workload ID, and then pass that to the SDK's `verifyFlashtestationInBlock` function to verify "is Unichain building blocks with the latest version of op-rbuilder?". + +Please see the [flashbots-images](https://github.com/flashbots/flashbots-images/commits/main/) repo instructions on how to calculate a workload ID. + +## API Reference + +### verifyFlashtestationInBlock + +Verify if a block was built by a TEE running a specific workload. + +```typescript +async function verifyFlashtestationInBlock( + workloadIdOrRegisters: string | WorkloadMeasureRegisters, + blockParameter: BlockParameter, + config: ClientConfig +): Promise; +``` + +**Parameters:** + +| Parameter | Type | Description | +| --------------------- | ------------------------------------ | --------------------------------------------------------------------------- | +| workloadIdOrRegisters | `string \| WorkloadMeasureRegisters` | Workload ID (32-byte hex string) or measurement registers to compute the ID | +| blockParameter | `BlockParameter` | Block identifier: tag ('latest', 'earliest', etc.), number, or hash | +| config | `ClientConfig` | Configuration object with `chainId` and optional `rpcUrl` | + +**Returns:** `Promise` + +| Field | Type | Description | +| -------------------- | ---------------- | ------------------------------------------------------------------- | +| isBuiltByExpectedTee | `boolean` | Whether the block was built by the expected TEE workload | +| workloadId | `string \| null` | Workload ID that built the block (null if not TEE-built) | +| commitHash | `string \| null` | Git commit hash of the workload source code (null if not TEE-built) | +| blockExplorerLink | `string \| null` | Block explorer URL (null if not available) | +| builderAddress | `string` | Address of the block builder (optional) | +| version | `number` | Flashtestation protocol version | +| sourceLocators | `string[]` | Source code locations (e.g., GitHub URLs) | + +**Throws:** + +- `NetworkError` - RPC connection failed or network request error +- `BlockNotFoundError` - Block does not exist +- `ValidationError` - Invalid measurement registers +- `ChainNotSupportedError` - Chain ID not supported + +**See [Error Handling](#error-handling) for examples of handling these errors.** + +### Utility Functions + +#### computeWorkloadId + +Compute a workload ID from TEE measurement registers. Useful for debugging or pre-computing IDs. + +```typescript +function computeWorkloadId(registers: WorkloadMeasureRegisters): string; +``` + +Returns the workload ID as a hex string. -To do a one-off build, use `npm run build` or `yarn build`. +#### getSupportedChains -To run tests, use `npm test` or `yarn test`. +Get list of all supported chain IDs. -## Configuration +```typescript +function getSupportedChains(): number[]; +``` -Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. +Returns an array of supported chain IDs: `[130, 1301, 22444422, 33611633]` -### Jest +#### isChainSupported -Jest tests are set up to run with `npm test` or `yarn test`. +Check if a chain ID is supported. -### Bundle Analysis +```typescript +function isChainSupported(chainId: number): boolean; +``` -[`size-limit`](https://github.com/ai/size-limit) is set up to calculate the real cost of your library with `npm run size` and visualize the bundle with `npm run analyze`. +Returns `true` if the chain is supported, `false` otherwise. -#### Setup Files +#### getChainConfig -This is the folder structure we set up for you: +Get the full configuration for a chain. -```txt -/src - index.tsx # EDIT THIS -/test - blah.test.tsx # EDIT THIS -.gitignore -package.json -README.md # EDIT THIS -tsconfig.json +```typescript +function getChainConfig(chainId: number): ChainConfig; ``` -### Rollup +Returns a `ChainConfig` object with chain details (name, contract address, RPC URL, block explorer URL). -TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. +**Throws:** `ChainNotSupportedError` if the chain is not supported. -### TypeScript +## Error Handling -`tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. +The SDK provides custom error classes for specific failure scenarios. -## Continuous Integration +### NetworkError -### GitHub Actions +Thrown when RPC connection fails or network requests error out. -Two actions are added by default: +```typescript +import { verifyFlashtestationInBlock, NetworkError } from 'flashtestations-sdk'; -- `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix -- `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit) +try { + const result = await verifyFlashtestationInBlock('0xabcd...', 'latest', { + chainId: 1301, + rpcUrl: 'https://invalid-rpc.example.com', + }); +} catch (error) { + if (error instanceof NetworkError) { + console.error('Network error:', error.message); + console.error('Cause:', error.cause); + // Retry with exponential backoff or fallback RPC + } +} +``` -## Optimizations +### BlockNotFoundError -Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: +Thrown when the specified block does not exist on the chain. -```js -// ./types/index.d.ts -declare var __DEV__: boolean; +```typescript +import { BlockNotFoundError } from 'flashtestations-sdk'; + +try { + const result = await verifyFlashtestationInBlock('0xabcd...', 999999999, { + chainId: 1301, + }); +} catch (error) { + if (error instanceof BlockNotFoundError) { + console.error('Block not found:', error.blockParameter); + // Try a different block or handle gracefully + } +} +``` -// inside your code... -if (__DEV__) { - console.log('foo'); +### ValidationError + +Thrown when measurement registers are invalid (wrong format or length). + +```typescript +import { ValidationError } from 'flashtestations-sdk'; + +try { + const invalidRegisters = { + tdAttributes: '0x00', // Too short! + xFAM: '0x0000000000000003', + // ... other fields + }; + const result = await verifyFlashtestationInBlock(invalidRegisters, 'latest', { + chainId: 1301, + }); +} catch (error) { + if (error instanceof ValidationError) { + console.error('Validation error:', error.message); + console.error('Field:', error.field); + // Fix the invalid field + } } ``` -You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. +### ChainNotSupportedError -## Module Formats +Thrown when trying to use an unsupported chain ID. -CJS, ESModules, and UMD module formats are supported. +```typescript +import { ChainNotSupportedError } from 'flashtestations-sdk'; -The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. +try { + const result = await verifyFlashtestationInBlock('0xabcd...', 'latest', { + chainId: 999, // Not supported + }); +} catch (error) { + if (error instanceof ChainNotSupportedError) { + console.error('Chain not supported:', error.chainId); + console.error('Supported chains:', error.supportedChains); + // Use one of the supported chains + } +} +``` + +### Error Handling Best Practices + +- **Retry on NetworkError**: Implement exponential backoff for transient network failures +- **Validate inputs early**: Check chain support with `isChainSupported()` before calling verification +- **Handle missing blocks gracefully**: `BlockNotFoundError` may indicate the block hasn't been mined yet +- **Log error context**: All custom errors include additional context properties for debugging +- **Use fallback RPC endpoints**: Provide alternative `rpcUrl` options for better reliability + +## Advanced Usage + +### Computing Workload IDs + +If you need to compute workload IDs separately (e.g., for caching or debugging), use the `computeWorkloadId` utility: + +```typescript +import { + computeWorkloadId, + WorkloadMeasureRegisters, +} from 'flashtestations-sdk'; + +const registers: WorkloadMeasureRegisters = { + tdAttributes: '0x0000000000000000', + xFAM: '0x0000000000000003', + mrTd: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + mrConfigId: + '0x0000000000000000000000000000000000000000000000000000000000000000', + rtMr0: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + rtMr1: '0xef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abab', + rtMr2: '0x234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeff', + rtMr3: '0x67890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234d', +}; + +const workloadId = computeWorkloadId(registers); +console.log('Computed workload ID:', workloadId); + +// Use the computed ID for verification +const result = await verifyFlashtestationInBlock(workloadId, 'latest', { + chainId: 1301, +}); +``` -## Named Exports +The `computeWorkloadId` function implements the same keccak256 hashing algorithm used on-chain, ensuring consistency between off-chain computation and on-chain verification. -Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. +## Examples -## Including Styles +See the [examples/](./examples) directory for complete runnable examples: -There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. +- `verifyBlock.ts` - Verify blocks with workload ID +- `getFlashtestationTx.ts` - Retrieve flashtestation transaction data -For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. +**Running examples:** -## Publishing to NPM +```bash +# Set your workload ID +export WORKLOAD_ID=0x1234567890abcdef... + +# Run the verification example +npx tsx examples/verifyBlock.ts +``` -We recommend using [np](https://github.com/sindresorhus/np). +## Development + +### Building the SDK + +```bash +yarn build +``` + +This compiles the TypeScript source to CommonJS, ESM, and TypeScript declaration files in the `dist/` directory. + +### Running Tests + +```bash +yarn test +``` + +### Linting + +```bash +yarn lint +``` diff --git a/sdks/flashtestations-sdk/examples/getBlock.ts b/sdks/flashtestations-sdk/examples/getBlock.ts deleted file mode 100644 index 9b24fd877..000000000 --- a/sdks/flashtestations-sdk/examples/getBlock.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { createRpcClient } from '../src/rpc/client'; - -/** - * Example: Fetch the most recent block from Unichain Sepolia - * - * This example demonstrates how to: - * 1. Create an RPC client for Unichain Sepolia (chain ID 1301) - * 2. Fetch the latest block using the 'latest' block tag - * 3. Display block information - */ -async function main() { - try { - // Create RPC client for Unichain Sepolia (chain ID 1301) - const client = createRpcClient({ - chainId: 1301, // Unichain Sepolia testnet - // Optional: provide custom RPC URL - // rpcUrl: 'https://sepolia.unichain.org', - // Optional: configure retry behavior - // maxRetries: 3, - // initialRetryDelay: 1000, - }); - - console.log('Fetching latest block from Unichain Sepolia...\n'); - - // Fetch the latest block - const block = await client.getBlock('latest'); - - // Display block information - console.log('Block Information:'); - console.log('=================='); - console.log(`Block Number: ${block.number}`); - console.log(`Block Hash: ${block.hash}`); - console.log(`Parent Hash: ${block.parentHash}`); - console.log(`Timestamp: ${block.timestamp} (${new Date(Number(block.timestamp) * 1000).toISOString()})`); - console.log(`Gas Used: ${block.gasUsed}`); - console.log(`Gas Limit: ${block.gasLimit}`); - console.log(`Base Fee Per Gas: ${block.baseFeePerGas ?? 'N/A'}`); - console.log(`Transactions: ${block.transactions.length}`); - console.log(`Miner/Validator: ${block.miner}`); - - // You can also fetch specific blocks: - // - By block number: await client.getBlock(12345) - // - By bigint number: await client.getBlock(BigInt(12345)) - // - By block hash: await client.getBlock('0x...') - // - By hex number: await client.getBlock('0x3039') - // - Other tags: 'earliest', 'finalized', 'safe', 'pending' - - } catch (error) { - console.error('Error fetching block:', error); - process.exit(1); - } -} - -// Run the example -main(); diff --git a/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts b/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts index 7251b7759..e5bc8e5da 100644 --- a/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts +++ b/sdks/flashtestations-sdk/examples/getFlashtestationTx.ts @@ -1,29 +1,22 @@ -import { createRpcClient } from '../src/rpc/client'; +import { getFlashtestationTx } from '../src/index'; /** - * Example: Check if a transaction is a flashtestation transaction + * Example: Check if a block contains a flashtestation transaction * * This example demonstrates how to: - * 1. Create an RPC client for Unichain Sepolia (chain ID 1301) - * 2. Check if a transaction emitted the BlockBuilderProofVerified event - * 3. Retrieve the full transaction data if it's a flashtestation transaction - * 4. Handle the case where the transaction is not a flashtestation + * 1. Use the getFlashtestationTx function to fetch flashtestation data from a block + * 2. Retrieve the full flashtestation event data if present + * 3. Handle the case where the block does not contain a flashtestation transaction */ async function main() { try { - // Create RPC client for Unichain Sepolia (chain ID 1301) - const client = createRpcClient({ + // Fetch flashtestation transaction from the latest block on Unichain Sepolia + const tx = await getFlashtestationTx('latest', { chainId: 1301, // Unichain Sepolia testnet // Optional: provide custom RPC URL // rpcUrl: 'https://sepolia.unichain.org', - // Optional: configure retry behavior - // maxRetries: 3, - // initialRetryDelay: 1000, }); - // Check if this transaction is a flashtestation - const tx = await client.getFlashtestationTx('latest'); - if (tx) { // This is a flashtestation transaction console.log('\n✓ This is a flashtestation transaction!\n'); @@ -37,10 +30,8 @@ async function main() { console.log(`Source Locators: ${tx.sourceLocators.length > 0 ? tx.sourceLocators.join(', ') : 'None'}`); } else { // This is not a flashtestation transaction - console.log('\n✗ This is not a flashtestation transaction.'); - console.log('\nThe transaction either:'); - console.log(' 1. Does not exist'); - console.log(' 2. Did not emit the BlockBuilderProofVerified event'); + console.log('\n✗ This is not a flashtestation transaction'); + console.log('\nThe transaction did not emit the BlockBuilderProofVerified event'); } } catch (error) { diff --git a/sdks/flashtestations-sdk/examples/getTransactionReceipt.ts b/sdks/flashtestations-sdk/examples/getTransactionReceipt.ts deleted file mode 100644 index 1b8a2f1de..000000000 --- a/sdks/flashtestations-sdk/examples/getTransactionReceipt.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createRpcClient } from '../src/rpc/client'; - -/** - * Example: Fetch a transaction receipt from Unichain Sepolia - * - * This example demonstrates how to: - * 1. Create an RPC client for Unichain Sepolia (chain ID 1301) - * 2. Fetch a transaction receipt by transaction hash - * 3. Display transaction receipt information - */ -async function main() { - try { - // Create RPC client for Unichain Sepolia (chain ID 1301) - const client = createRpcClient({ - chainId: 1301, // Unichain Sepolia testnet - // Optional: provide custom RPC URL - // rpcUrl: 'https://sepolia.unichain.org', - // Optional: configure retry behavior - // maxRetries: 3, - // initialRetryDelay: 1000, - }); - - // fetch an arbitrary transaction hash from the latest block - const txHash = await client.getBlock('latest').then(block => block.transactions[0]) as `0x${string}`; - - // Fetch the transaction receipt - const receipt = await client.getTransactionReceipt(txHash as `0x${string}`); - - // Display transaction receipt information - console.log('Transaction Receipt:'); - console.log('===================='); - console.log(`Transaction Hash: ${receipt.transactionHash}`); - console.log(`Block Number: ${receipt.blockNumber}`); - console.log(`Block Hash: ${receipt.blockHash}`); - console.log(`From: ${receipt.from}`); - console.log(`To: ${receipt.to ?? 'Contract Creation'}`); - console.log(`Contract Address: ${receipt.contractAddress ?? 'N/A'}`); - console.log(`Status: ${receipt.status === 'success' ? '✓ Success' : '✗ Failed'}`); - console.log(`Gas Used: ${receipt.gasUsed}`); - console.log(`Effective Gas Price: ${receipt.effectiveGasPrice}`); - console.log(`Cumulative Gas Used: ${receipt.cumulativeGasUsed}`); - console.log(`Transaction Index: ${receipt.transactionIndex}`); - console.log(`Logs: ${receipt.logs.length} log(s)`); - - // Display logs if present - if (receipt.logs.length > 0) { - console.log('\nLog Details:'); - receipt.logs.forEach((log, index) => { - console.log(`\n Log ${index}:`); - console.log(` Address: ${log.address}`); - console.log(` Topics: ${log.topics.length}`); - console.log(` Data: ${log.data.slice(0, 66)}${log.data.length > 66 ? '...' : ''}`); - }); - } - - // Display bloom filter info - console.log(`\nLogs Bloom: ${receipt.logsBloom.slice(0, 66)}...`); - - } catch (error) { - console.error('Error fetching transaction receipt:', error); - process.exit(1); - } -} - -// Run the example -main(); diff --git a/sdks/flashtestations-sdk/examples/verifyBlock.ts b/sdks/flashtestations-sdk/examples/verifyBlock.ts index 7eb247703..db4e06b06 100644 --- a/sdks/flashtestations-sdk/examples/verifyBlock.ts +++ b/sdks/flashtestations-sdk/examples/verifyBlock.ts @@ -1,4 +1,4 @@ -import { verifyFlashtestationInBlock } from '../src/verification/service'; +import { verifyFlashtestationInBlock } from '../src/index'; /** * Example: Verify if a block was built by a specific TEE workload @@ -43,19 +43,27 @@ async function main() { if (result.isBuiltByExpectedTee) { console.log('\n✓ Block was built by the specified TEE workload!\n'); - console.log(`Workload ID: ${result.workloadId}`); - console.log(`Commit Hash: ${result.commitHash}`); - console.log(`Builder Address: ${result.builderAddress}`); - console.log(`Version: ${result.version}`); - console.log(`Source Locators: ${result.sourceLocators && result.sourceLocators.length > 0 ? result.sourceLocators.join(', ') : 'None'}`) + console.log(`Workload ID: ${result.workloadMetadata.workloadId}`); + console.log(`Commit Hash: ${result.workloadMetadata.commitHash}`); + console.log(`Builder Address: ${result.workloadMetadata.builderAddress}`); + console.log(`Version: ${result.workloadMetadata.version}`); + console.log(`Source Locators: ${result.workloadMetadata.sourceLocators && result.workloadMetadata.sourceLocators.length > 0 ? result.workloadMetadata.sourceLocators.join(', ') : 'None'}`) if (result.blockExplorerLink) { console.log(`Block Explorer: ${result.blockExplorerLink}`); } } else { - console.log('\n✗ Block was NOT built by the specified TEE workload.\n'); - console.log('The block either:'); - console.log(' 1. Does not contain a flashtestation transaction'); - console.log(' 2. Contains a flashtestation from a different workload'); + console.log('\n✗ Block was NOT built by the specified TEE workload\n'); + + if (result.workloadMetadata) { + console.log('Block was built by a different TEE workload:'); + console.log(`Workload ID: ${result.workloadMetadata.workloadId}`); + console.log(`Commit Hash: ${result.workloadMetadata.commitHash}`); + console.log(`Builder Address: ${result.workloadMetadata.builderAddress}`); + console.log(`Version: ${result.workloadMetadata.version}`); + console.log(`Source Locators: ${result.workloadMetadata.sourceLocators.length > 0 ? result.workloadMetadata.sourceLocators.join(', ') : 'None'}`) + } else { + console.log('The block does not contain a flashtestation transaction') + } } // You can also verify specific blocks: diff --git a/sdks/flashtestations-sdk/package.json b/sdks/flashtestations-sdk/package.json index 7061d2513..1aa38cb1e 100644 --- a/sdks/flashtestations-sdk/package.json +++ b/sdks/flashtestations-sdk/package.json @@ -6,7 +6,8 @@ "keywords": [ "flashteststations", "ethereum", - "flashbots" + "flashbots", + "unichain" ], "version": "0.1.0", "license": "MIT", diff --git a/sdks/flashtestations-sdk/src/index.ts b/sdks/flashtestations-sdk/src/index.ts index 11168379c..ec86a66b2 100644 --- a/sdks/flashtestations-sdk/src/index.ts +++ b/sdks/flashtestations-sdk/src/index.ts @@ -1,7 +1,38 @@ -// TODO: Remove this -export const sum = (a: number, b: number) => { - if ('development' === process.env.NODE_ENV) { - console.log('boop'); - } - return a + b; -}; +/** + * Flashtestations SDK - Verify TEE-built blocks on Unichain + * + * This SDK provides tools to verify whether blockchain blocks were built by + * Trusted Execution Environments (TEEs) running specific workload software. + */ + +// Main verification function +export { verifyFlashtestationInBlock, getFlashtestationTx } from './verification/service'; + + +// Core types +export type { + VerificationResult, + WorkloadMeasureRegisters, + BlockParameter, + FlashtestationEvent, + ChainConfig, + ClientConfig, +} from './types'; + +// Error classes for programmatic error handling +export { + NetworkError, + BlockNotFoundError, + ValidationError, + ChainNotSupportedError, +} from './types'; + +// Chain configuration utilities +export { + getSupportedChains, + isChainSupported, + getChainConfig, +} from './config/chains'; + +// Workload ID computation utility +export { computeWorkloadId } from './crypto/workload'; diff --git a/sdks/flashtestations-sdk/src/types/index.ts b/sdks/flashtestations-sdk/src/types/index.ts index dfa6686fd..6bdf95e7e 100644 --- a/sdks/flashtestations-sdk/src/types/index.ts +++ b/sdks/flashtestations-sdk/src/types/index.ts @@ -1,23 +1,32 @@ -/** - * Result of flashtestation verification - */ -export interface VerificationResult { - /** Whether the block was built by a TEE running the specified workload */ - isBuiltByExpectedTee: boolean; - /** workload ID of the TEE workload, null if not TEE-built */ - workloadId: string | null; - /** Commit hash of the TEE workload source code, null if not TEE-built */ - commitHash: string | null; - /** Block explorer link for the block, null if not TEE-built */ - blockExplorerLink: string | null; +export type WorkloadMetadata = { + /** workload ID of the TEE workload*/ + workloadId: string; + /** Commit hash of the TEE workload source code */ + commitHash: string; /** Address of the block builder, optional */ - builderAddress?: string; + builderAddress: string; /** Version of the flashtestation protocol, optional */ version: number; /** Source locators (e.g., GitHub URLs) for the workload source code, optional for backwards compatibility */ - sourceLocators?: string[]; + sourceLocators: string[]; } +/** + * Result of flashtestation verification + */ +export type VerificationResult = | { + /** Block was built by the expected TEE workload */ + isBuiltByExpectedTee: true; + blockExplorerLink: string | null; + workloadMetadata: WorkloadMetadata; +} +| { + /** Block was NOT built by the expected TEE workload */ + isBuiltByExpectedTee: false; + blockExplorerLink: string | null; + workloadMetadata: WorkloadMetadata | null; +}; + /** * TEE workload measurement registers used for workload ID computation */ @@ -74,6 +83,16 @@ export interface ChainConfig { blockExplorerUrl: string; } +/** + * Minimal configuration options for the JSON-RPC client to interact with the blockchain + */ +export interface ClientConfig { + /** Chain ID to network */ + chainId: number; + /** Optional custom RPC URL (overrides default) */ + rpcUrl?: string; +} + /** * Block parameter for identifying blocks */ diff --git a/sdks/flashtestations-sdk/src/verification/service.ts b/sdks/flashtestations-sdk/src/verification/service.ts index 8d61f4973..cde170662 100644 --- a/sdks/flashtestations-sdk/src/verification/service.ts +++ b/sdks/flashtestations-sdk/src/verification/service.ts @@ -5,16 +5,47 @@ import { BlockParameter, VerificationResult, WorkloadMeasureRegisters, + ClientConfig, + FlashtestationEvent, } from '../types'; /** - * Configuration options for verification + * Fetch the flashtestation transaction from a specific block + * + * This function retrieves the flashtestation transaction (if any) from the specified block. + * Unlike verifyFlashtestationInBlock, this does not perform any workload verification - it + * simply returns the raw flashtestation event data. + * + * @param blockParameter - Block identifier (tag, number, or hash), defaults to 'latest' + * @param config - Configuration for chain and RPC connection + * @returns FlashtestationEvent if the block contains a flashtestation transaction, null otherwise + * @throws NetworkError if RPC connection fails + * @throws BlockNotFoundError if block doesn't exist + * + * @example + * // Get flashtestation transaction from the latest block + * const flashtestation = await getFlashtestationTx('latest', { chainId: 1301 }); + * if (flashtestation) { + * console.log('Workload ID:', flashtestation.workloadId); + * console.log('Commit Hash:', flashtestation.commitHash); + * } + * + * @example + * // Get flashtestation transaction from a specific block number + * const flashtestation = await getFlashtestationTx(12345, { chainId: 1301 }); */ -export interface VerificationConfig { - /** Chain ID to verify on */ - chainId: number; - /** Optional custom RPC URL (overrides default) */ - rpcUrl?: string; +export async function getFlashtestationTx( + blockParameter: BlockParameter = 'latest', + config: ClientConfig +): Promise { + // Create RPC client + const client = new RpcClient({ + chainId: config.chainId, + rpcUrl: config.rpcUrl, + }); + + // Get the flashtestation transaction from the block + return await client.getFlashtestationTx(blockParameter); } /** @@ -61,7 +92,7 @@ export interface VerificationConfig { export async function verifyFlashtestationInBlock( workloadIdOrRegisters: string | WorkloadMeasureRegisters, blockParameter: BlockParameter, - config: VerificationConfig + config: ClientConfig ): Promise { // Determine if we need to compute workload ID from registers let workloadId: string; @@ -93,27 +124,8 @@ export async function verifyFlashtestationInBlock( if (!flashtestationEvent) { return { isBuiltByExpectedTee: false, - workloadId: null, - commitHash: null, blockExplorerLink: null, - version: 0, - }; - } - - // Normalize event workload ID for comparison - const eventWorkloadId = flashtestationEvent.workloadId.toLowerCase(); - - // Compare workload IDs (byte-wise comparison) - const workloadMatches = workloadId === eventWorkloadId; - - if (!workloadMatches) { - // Block was built by a TEE, but not the one we're looking for - return { - isBuiltByExpectedTee: false, - workloadId: null, - commitHash: null, - blockExplorerLink: null, - version: 0, + workloadMetadata: null, }; } @@ -130,14 +142,37 @@ export async function verifyFlashtestationInBlock( blockExplorerLink = `${blockExplorerBaseUrl}/block/${block.number}`; } + // Normalize event workload ID for comparison + const eventWorkloadId = flashtestationEvent.workloadId.toLowerCase(); + + // Compare workload IDs (byte-wise comparison) + const workloadMatches = workloadId === eventWorkloadId; + + if (!workloadMatches) { + // Block was built by a TEE, but not the one we're looking for + return { + isBuiltByExpectedTee: false, + blockExplorerLink: blockExplorerLink, + workloadMetadata: { + workloadId: flashtestationEvent.workloadId, + commitHash: flashtestationEvent.commitHash, + builderAddress: flashtestationEvent.caller, + version: flashtestationEvent.version, + sourceLocators: flashtestationEvent.sourceLocators, + } + }; + } + // Block was built by the specified TEE workload return { isBuiltByExpectedTee: true, - workloadId: flashtestationEvent.workloadId, - commitHash: flashtestationEvent.commitHash, blockExplorerLink: blockExplorerLink, - builderAddress: flashtestationEvent.caller, - version: flashtestationEvent.version, - sourceLocators: flashtestationEvent.sourceLocators, + workloadMetadata: { + workloadId: flashtestationEvent.workloadId, + commitHash: flashtestationEvent.commitHash, + builderAddress: flashtestationEvent.caller, + version: flashtestationEvent.version, + sourceLocators: flashtestationEvent.sourceLocators, + } }; } diff --git a/sdks/flashtestations-sdk/test/verification/service.test.ts b/sdks/flashtestations-sdk/test/verification/service.test.ts index 9d1123f9b..00d3002e7 100644 --- a/sdks/flashtestations-sdk/test/verification/service.test.ts +++ b/sdks/flashtestations-sdk/test/verification/service.test.ts @@ -55,6 +55,7 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'abc123def456', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], }; const mockBlock = { @@ -73,11 +74,14 @@ describe('verifyFlashtestationInBlock', () => { expect(result).toEqual({ isBuiltByExpectedTee: true, - commitHash: 'abc123def456', blockExplorerLink: 'https://sepolia.uniscan.xyz/block/12345', - builderAddress: '0xbuilder123', - version: 1, - workloadId: workloadId, + workloadMetadata: { + workloadId: workloadId, + commitHash: 'abc123def456', + builderAddress: '0xbuilder123', + version: 1, + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }, }); expect(mockGetFlashtestationTx).toHaveBeenCalledWith('latest'); @@ -92,6 +96,7 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'abc123def456', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], }; const mockBlock = { @@ -119,6 +124,7 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'abc123def456', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], }; const mockBlock = { @@ -149,10 +155,8 @@ describe('verifyFlashtestationInBlock', () => { expect(result).toEqual({ isBuiltByExpectedTee: false, - commitHash: null, blockExplorerLink: null, - version: 0, - workloadId: null, + workloadMetadata: null, }); expect(mockGetFlashtestationTx).toHaveBeenCalledWith('latest'); @@ -167,9 +171,17 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'abc123def456', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + + }; + + const mockBlock = { + number: BigInt(12345), + hash: '0xblockhash', }; mockGetFlashtestationTx.mockResolvedValue(mockEvent); + mockGetBlock.mockResolvedValue(mockBlock); const result = await verifyFlashtestationInBlock( workloadId, @@ -179,14 +191,18 @@ describe('verifyFlashtestationInBlock', () => { expect(result).toEqual({ isBuiltByExpectedTee: false, - commitHash: null, - blockExplorerLink: null, - version: 0, - workloadId: null, + blockExplorerLink: 'https://sepolia.uniscan.xyz/block/12345', + workloadMetadata: { + workloadId: differentWorkloadId, + commitHash: 'abc123def456', + builderAddress: '0xbuilder123', + version: 1, + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }, }); expect(mockGetFlashtestationTx).toHaveBeenCalledWith('latest'); - expect(mockGetBlock).not.toHaveBeenCalled(); + expect(mockGetBlock).toHaveBeenCalledWith('latest'); }); it('should handle null block explorer URL', async () => { @@ -291,6 +307,7 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'register-commit', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], }; const mockBlock = { @@ -310,11 +327,14 @@ describe('verifyFlashtestationInBlock', () => { expect(mockComputeWorkloadId).toHaveBeenCalledWith(registers); expect(result).toEqual({ isBuiltByExpectedTee: true, - commitHash: 'register-commit', blockExplorerLink: 'https://sepolia.uniscan.xyz/block/99999', - builderAddress: '0xbuilder456', - version: 1, - workloadId: computedWorkloadId, + workloadMetadata: { + workloadId: computedWorkloadId, + commitHash: 'register-commit', + builderAddress: '0xbuilder456', + version: 1, + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }, }); }); @@ -336,6 +356,7 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'register-commit', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], }; const mockBlock = { @@ -355,11 +376,14 @@ describe('verifyFlashtestationInBlock', () => { expect(mockComputeWorkloadId).toHaveBeenCalledWith(uppercaseRegisters); expect(result).toEqual({ isBuiltByExpectedTee: true, - commitHash: 'register-commit', blockExplorerLink: 'https://sepolia.uniscan.xyz/block/99999', - builderAddress: '0xbuilder456', - version: 1, - workloadId: computedWorkloadId, + workloadMetadata: { + workloadId: computedWorkloadId, + commitHash: 'register-commit', + builderAddress: '0xbuilder456', + version: 1, + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }, }); }); @@ -371,9 +395,16 @@ describe('verifyFlashtestationInBlock', () => { version: 1, blockContentHash: '0xblockhash' as `0x${string}`, commitHash: 'register-commit', + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }; + + const mockBlock = { + number: BigInt(99999), + hash: '0xblockhash', }; mockGetFlashtestationTx.mockResolvedValue(mockEvent); + mockGetBlock.mockResolvedValue(mockBlock); const result = await verifyFlashtestationInBlock( registers, @@ -384,10 +415,14 @@ describe('verifyFlashtestationInBlock', () => { expect(mockComputeWorkloadId).toHaveBeenCalledWith(registers); expect(result).toEqual({ isBuiltByExpectedTee: false, - commitHash: null, - blockExplorerLink: null, - version: 0, - workloadId: null, + blockExplorerLink: 'https://sepolia.uniscan.xyz/block/99999', + workloadMetadata: { + workloadId: differentWorkloadId, + commitHash: 'register-commit', + builderAddress: '0xbuilder456', + version: 1, + sourceLocators: ['https://github.com/flashbots/flashbots-images/commit/b7c707667393cc4c0173786ee32ec3a79009b04f'], + }, }); });