Skip to content
Draft
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
4 changes: 4 additions & 0 deletions bridges/evm-storage-bridge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
package-lock.json
*.log
111 changes: 111 additions & 0 deletions bridges/evm-storage-bridge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Textile EVM Storage Bridge

A decentralized identity and off-chain storage bridge connecting EVM networks (Ethereum, Polygon, Arbitrum, Base) to Textile Hub Buckets, ThreadDB, and Filecoin storage deals.

## Overview

The Textile EVM Storage Bridge solves the interoperability barrier between EVM smart contracts and Textile distributed storage infrastructure. It provides:

1. **Deterministic Decentralized Identity (DID):** Derives Libp2p/Ed25519 identity keypairs deterministically from standard EVM wallet signatures (EIP-191 and EIP-712). Users control their Textile Buckets and ThreadDB instances directly from MetaMask or any Web3 wallet without managing separate seed phrases.
2. **On-Chain Storage Anchoring:** An EVM smart contract (`TextileStorageBridge.sol`) that records dataset CIDs, bucket associations, size metrics, and Filecoin storage deal metadata on-chain.
3. **Cryptographic Oracle Attestation:** Secp256k1 oracle signing certifying that off-chain data has been persisted and verified against its content-addressed root CID before state transitions are executed on-chain.
4. **EIP-712 Storage Intents:** Gasless or relayed storage authorizations signed by dataset owners and verified cryptographically.

## Architecture

```
+-------------------+ EIP-191 / EIP-712 Sign +-----------------------------+
| EVM Wallet | -----------------------------------> | TextileEvmBridge (Identity) |
| (MetaMask / Web3) | | - Deterministic PBKDF2 |
+-------------------+ | - RFC 8410 Ed25519 Keypair |
| +-----------------------------+
| |
| Data Staging | Authenticated API Access
v v
+-------------------+ Attestation Request +-----------------------------+
| Textile Buckets / | -----------------------------------> | Textile Hub |
| ThreadDB | | (IPFS / Filecoin Deals) |
+-------------------+ +-----------------------------+
| |
| Proof Generation | Storage Deal Activation
v v
+-------------------+ verifyAndAnchorAttestation +-----------------------------+
| TextileOracle | -----------------------------------> | TextileStorageBridge |
| (secp256k1 Sign) | | (Solidity Smart Contract) |
+-------------------+ +-----------------------------+
```

## Core Modules

| Module | File | Description |
| :--- | :--- | :--- |
| **Types** | `src/types.ts` | Shared type definitions for identities, datasets, deals, and attestations |
| **Identity** | `src/identity.ts` | Deterministic Ed25519 derivation, EIP-191 challenges, and EIP-712 verification |
| **Oracle** | `src/oracle.ts` | Secp256k1 attestation generation, verification, and nonce progression |
| **Bridge** | `src/bridge.ts` | Coordinator linking Hub storage, CID computation, and contract calldata |
| **Contract** | `src/contracts/TextileStorageBridge.sol` | Solidity registry enforcing access control and attestation verification |

## Getting Started

### Prerequisites

- Node.js >= 18.0.0
- npm >= 9.0.0

### Installation

```bash
cd bridges/evm-storage-bridge
npm install
```

### Build

```bash
npm run build
```

### Test Suite Execution

Run the complete cryptographic and integration test suite:

```bash
npm test
```

## Usage Example

```typescript
import { Wallet } from "ethers";
import { TextileEvmBridge, TextileOracle } from "@textile/evm-storage-bridge";

const chainId = 137;
const contractAddress = "0x1111111111111111111111111111111111111111";
const oracleWallet = Wallet.createRandom();
const userWallet = Wallet.createRandom();

const bridge = new TextileEvmBridge({
hubHost: "https://api.hub.textile.io",
chainId,
contractAddress,
oracleAddress: oracleWallet.address,
});

const identity = await bridge.authenticateWallet(userWallet);

const dataset = bridge.stageDataset(
Buffer.from("Sample payload"),
"user-bucket",
"data/records.json"
);

const oracle = new TextileOracle(oracleWallet);
const attestation = await bridge.requestAttestation(oracle, dataset);

const registrationCalldata = bridge.encodeRegisterDataset(dataset, "metadata-uri");
const attestationCalldata = bridge.encodeVerifyAndAnchorAttestation(attestation);
```

## License

MIT
30 changes: 30 additions & 0 deletions bridges/evm-storage-bridge/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@textile/evm-storage-bridge",
"version": "1.0.0",
"description": "Decentralized Identity and Storage Bridge connecting EVM Blockchains with Textile Hub, Buckets, and Filecoin",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "tsc && node --test dist/test/bridge.test.js"
},
"keywords": [
"textile",
"ethereum",
"evm",
"filecoin",
"ipfs",
"bridge",
"decentralized-identity",
"oracle"
],
"author": "Textile Community",
"license": "MIT",
"dependencies": {
"ethers": "^5.7.2"
},
"devDependencies": {
"@types/node": "^18.19.0",
"typescript": "^4.9.5"
}
}
197 changes: 197 additions & 0 deletions bridges/evm-storage-bridge/src/bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import * as crypto from "crypto";
import { utils, Wallet } from "ethers";
import {
AttestationProof,
BridgeConfig,
BridgedIdentity,
PinnedDataset,
StorageDealMetadata,
} from "./types";
import {
createIdentityChallenge,
deriveTextileIdentityFromEvmSignature,
} from "./identity";
import { TextileOracle } from "./oracle";

/**
* Human-readable ABI for the TextileStorageBridge smart contract.
*/
export const TEXTILE_STORAGE_BRIDGE_ABI = [
"constructor(address initialOracle)",
"function owner() view returns (address)",
"function oracle() view returns (address)",
"function setOracle(address newOracle)",
"function transferOwnership(address newOwner)",
"function registerDataset(string cid, string bucketKey, uint256 sizeBytes, string metadataUri)",
"function anchorFilecoinDeal(string cid, uint64 dealId, string miner, uint64 durationBlocks)",
"function verifyAndAnchorAttestation(bytes32 dataHash, string cid, uint256 timestamp, uint256 nonce, bytes signature)",
"function registerIdentity(string textilePublicKey)",
"function getDataset(string cid) view returns (tuple(string bucketKey, uint256 sizeBytes, string metadataUri, address owner, uint256 registeredAt))",
"function getFilecoinDeal(string cid) view returns (tuple(uint64 dealId, string miner, uint64 durationBlocks, uint256 anchoredAt))",
"function getIdentity(address user) view returns (tuple(string textilePublicKey, uint256 registeredAt))",
"function isAttestationProcessed(bytes32 dataHash, string cid, uint256 nonce) view returns (bool)",
"event DatasetRegistered(string indexed cidKey, string cid, address indexed owner, string bucketKey, uint256 sizeBytes)",
"event FilecoinDealAnchored(string indexed cidKey, string cid, uint64 dealId, string miner, uint64 durationBlocks)",
"event AttestationVerified(bytes32 indexed dataHash, string cid, uint256 timestamp, uint256 nonce)",
"event IdentityRegistered(address indexed user, string textilePublicKey)",
"event OracleUpdated(address indexed previousOracle, address indexed newOracle)",
"event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)",
];

/**
* Main coordinator orchestrating EVM blockchain interactions with Textile Hub Buckets and Filecoin.
*/
export class TextileEvmBridge {
private config: BridgeConfig;
private contractInterface: utils.Interface;

/**
* Initializes the TextileEvmBridge instance.
*
* @param config Bridge configuration containing contract and network parameters.
*/
public constructor(config: BridgeConfig) {
this.config = config;
this.contractInterface = new utils.Interface(TEXTILE_STORAGE_BRIDGE_ABI);
}

/**
* Returns current bridge configuration.
*
* @returns BridgeConfig object.
*/
public getConfig(): BridgeConfig {
return { ...this.config };
}

/**
* Authenticates an EVM wallet to deterministically generate a Textile Ed25519 identity.
*
* @param wallet Ethers Wallet or signer instance.
* @param customNonce Optional nonce string for the challenge.
* @returns Generated BridgedIdentity ready for Textile Hub API authentication.
*/
public async authenticateWallet(
wallet: Wallet,
customNonce?: string
): Promise<BridgedIdentity> {
const nonce = customNonce ?? crypto.randomBytes(16).toString("hex");
const challenge = createIdentityChallenge(wallet.address, nonce);
const signature = await wallet.signMessage(challenge);
return deriveTextileIdentityFromEvmSignature(wallet.address, signature, challenge);
}

/**
* Computes a standard SHA-256 multihash CID string for a binary dataset.
*
* @param data Binary payload array.
* @returns Canonical CID string.
*/
public static computeCid(data: Uint8Array): string {
const hash = crypto.createHash("sha256").update(data).digest();
const multihash = Buffer.concat([Buffer.from([0x12, 0x20]), hash]);
return `bafk${multihash.toString("hex")}`;
}

/**
* Computes keccak256 hash of a binary payload.
*
* @param data Binary payload array.
* @returns Hex-encoded 32-byte hash string with 0x prefix.
*/
public static computeDataHash(data: Uint8Array): string {
return utils.keccak256(data);
}

/**
* Prepares and stages a local dataset for Textile Buckets storage.
*
* @param data Binary content of the dataset.
* @param bucketKey Target bucket identifier.
* @param path Target path within the bucket.
* @returns Fully formed PinnedDataset object.
*/
public stageDataset(data: Uint8Array, bucketKey: string, path: string): PinnedDataset {
const cid = TextileEvmBridge.computeCid(data);
const dataHash = TextileEvmBridge.computeDataHash(data);
return {
bucketKey,
path,
cid,
dataHash,
sizeBytes: data.byteLength,
pinnedAt: Math.floor(Date.now() / 1000),
};
}

/**
* Generates a cryptographic attestation proof through the authorized oracle service.
*
* @param oracle Active TextileOracle service.
* @param dataset Staged PinnedDataset.
* @returns AttestationProof certified by the oracle.
*/
public async requestAttestation(
oracle: TextileOracle,
dataset: PinnedDataset
): Promise<AttestationProof> {
return oracle.createAttestation(dataset.dataHash, dataset.cid, dataset.pinnedAt);
}

/**
* Encodes contract calldata for registerDataset function.
*
* @param dataset Pinned dataset metadata.
* @param metadataUri Supplementary metadata URI.
* @returns Hex-encoded calldata string.
*/
public encodeRegisterDataset(dataset: PinnedDataset, metadataUri: string): string {
return this.contractInterface.encodeFunctionData("registerDataset", [
dataset.cid,
dataset.bucketKey,
dataset.sizeBytes,
metadataUri,
]);
}

/**
* Encodes contract calldata for anchorFilecoinDeal function.
*
* @param deal Filecoin storage deal metadata.
* @returns Hex-encoded calldata string.
*/
public encodeAnchorFilecoinDeal(deal: StorageDealMetadata): string {
return this.contractInterface.encodeFunctionData("anchorFilecoinDeal", [
deal.cid,
deal.dealId,
deal.miner,
deal.durationBlocks,
]);
}

/**
* Encodes contract calldata for verifyAndAnchorAttestation function.
*
* @param proof Cryptographic attestation proof.
* @returns Hex-encoded calldata string.
*/
public encodeVerifyAndAnchorAttestation(proof: AttestationProof): string {
return this.contractInterface.encodeFunctionData("verifyAndAnchorAttestation", [
proof.dataHash,
proof.cid,
proof.timestamp,
proof.nonce,
proof.signature,
]);
}

/**
* Encodes contract calldata for registerIdentity function.
*
* @param textilePublicKey Hexadecimal string of derived Ed25519 public key.
* @returns Hex-encoded calldata string.
*/
public encodeRegisterIdentity(textilePublicKey: string): string {
return this.contractInterface.encodeFunctionData("registerIdentity", [textilePublicKey]);
}
}
Loading