## Introduction My wife was among EPF Cohort 5 participants who had the opportunity to attend the recently concluded DevconSEA in Bangkok. I remember how she started and the challenges she faced. Looking back, the end result seems to have been worth it for her. Inspired by her journey, I decided to explore what it takes to be a Protocol Developer. I encountered many rabbit holes along the way. At one point, I attempted to run a node on my system but was met with numerous issues, which clearly pointed to limitations in my system resources. When I discovered the Portal Network, I knew it was something I wanted to contribute to. That's how I learned about Ultralight from the EthereumJS team. After studying their codebase, I browsed through their issue tracker to find a good starting point. This led to my journey of [converting UltralightProvider into a pure EIP-1193 JSON-RPC Provider](https://github.com/ethereumjs/ultralight/issues/687). In this post, I'll share my recent contribution to UltralightProvider, where I implemented EIP-1193 compliance. This change makes the provider more standardized, easier to use, and more maintainable. I'll explain what EIP-1193 is, why it matters, and show how to use the updated provider. ## What is EIP-1193? [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) is an Ethereum standard that defines a JavaScript API for blockchain providers. It establishes a simple, consistent interface that all providers should implement. The core of this standard is the `request` method, which handles all provider interactions through a unified interface. ## The Changes The main changes I implemented were: 1. Removing direct Ethereum dependencies to make the provider more modular 2. Introducing the standardized `request` method 3. Implementing proper error handling according to the EIP-1193 specification ## Using the Updated Provider Let's look at how to use the EIP-1193 compliant UltralightProvider. First, we'll set up two basic nodes to demonstrate how one nodes communicate to one another, manages content storage and retrieval, and look into peer discovery. While functional, the original implementation was not EIP-1193 compliant due to the direct use of `ether` instances for handling interactions, which limited modularity and interoperability. ## Node Creation and Setup ```typescript async function createSimpleNode(port: number) { const privateKey = await keys.generateKeyPair('secp256k1') const enr = SignableENR.createFromPrivateKey(privateKey) const nodeAddr = multiaddr(`/ip4/127.0.0.1/udp/${port}`) enr.setLocationMultiaddr(nodeAddr) const node = await UltralightProvider.create({ transport: TransportLayer.NODE, supportedNetworks: [ { networkId: NetworkId.HistoryNetwork }, { networkId: NetworkId.StateNetwork }, ], config: { enr, bindAddrs: { ip4: nodeAddr, }, privateKey, }, }) return node } ``` We started by creating two independent nodes using `createSimpleNode()`. Each node is configured with: - A `secp256k1` private key for cryptographic operations - An ENR (Ethereum Node Record) containing node identity and location - A UDP multiaddr binding on localhost with different ports (9090 and 9091) - Support for the History Network (NetworkId.HistoryNetwork) The `secp256k1` private key is crucial for secure node identification and signing operations in the Ethereum network, using the same elliptic curve cryptography as Ethereum itself. ENR (Ethereum Node Record) acts as a node's identity card, containing its network location, public key, and supported protocols, allowing other nodes to discover and verify its authenticity. The combination of UDP multiaddr binding and History Network support enables nodes to communicate peer-to-peer over UDP while specifically handling historical state data in the Portal Network. ## Node Communication Architecture The UltralightProvider creates nodes with: - Transport Layer: NODE implementation - Network Support: History Network (0x500b) - Configuration: ENR and binding addresses - Private key for secure communication We have transport layer implementation also for Web and Mobile. The History Network (0x500b) is a specialized network within Portal Network that maintains and synchronizes historical chain data, allowing nodes to serve historical block headers, receipts, and state. These are list of subnetworks and their corresponding values for reference: ```typescript export enum NetworkId { StateNetwork = '0x500a', HistoryNetwork = '0x500b', BeaconChainNetwork = '0x500c', CanonicalTxIndexNetwork = '0x500d', VerkleStateNetwork = '0x500e', TransactionGossipNetwork = '0x500f', Angelfood_StateNetwork = '0x504a', Angelfood_HistoryNetwork = '0x504b', Angelfood_BeaconChainNetwork = '0x504c', Angelfood_CanonicalTxIndexNetwork = '0x504d', Angelfood_VerkleStateNetwork = '0x504e', Angelfood_TransactionGossipNetwork = '0x504f', UTPNetwork = '0x757470', } ``` The configuration binds these components together by using ENR for node identity/discovery and the private key for secure message signing, creating a complete node that can participate in the decentralized network. ## Peer Discovery ```typescript console.log('Starting nodes...') await node1.portal.start() await node2.portal.start() console.log('Node1 running:', await node1.portal.discv5.isStarted()) console.log('Node2 running:', await node2.portal.discv5.isStarted()) console.log('\nTesting peer discovery...') console.log('Node1 bind address:', node1.portal.discv5.enr.ip) console.log('Node2 bind address:', node2.portal.discv5.enr.ip) console.log('Node1 ENR:', node1.portal.discv5.enr.toENR()) ``` The implementation shows basic peer discovery functionality by: - Starting both nodes via portal.start() - Verifying node operation status - Displaying ENR information and binding addresses ## Content Operation ```typescript console.log('\nTesting content operations...') const content = new TextEncoder().encode('Hello Portal Network!') const contentKey = new Uint8Array(32).fill(1) console.log('Content before storage:', await historyNetwork.findContentLocally(contentKey) ? 'exists' : 'does not exist') console.log('Storing content...') await historyNetwork.store(contentKey, content) const storedContent = await historyNetwork.findContentLocally(contentKey) if (storedContent) { console.log('Content successfully stored!') console.log('Retrieved content:', new TextDecoder().decode(storedContent)) } else { console.log('Content not found after storage attempt') } ``` Here we demonstrate basic content operations: - Creates test content ("Hello Portal Network!") - Generates a content key (32-byte array filled with 1s) - Checks for content existence before storage - Stores content using historyNetwork.store() - Verifies storage success by retrieving content ### Error Handling and Cleanup ```typescript= ... } catch (error) { console.error('Error during demonstration:', error) } finally { console.log('\nShutting down nodes...') await node1.portal.stop() await node2.portal.stop() } ``` This is a simple resource management: - Try-catch block for error handling - Finally block ensuring node shutdown - Graceful termination of both nodes using portal.stop() The complete code looks like this: ```typescript import { SignableENR } from '@chainsafe/enr' import { keys } from '@libp2p/crypto' import { multiaddr } from '@multiformats/multiaddr' import { UltralightProvider } from '@portalnetwork/ultralight' import { TransportLayer, NetworkId } from '@portalnetwork/ultralight' async function createSimpleNode(port: number) { const privateKey = await keys.generateKeyPair('secp256k1') const enr = SignableENR.createFromPrivateKey(privateKey) const nodeAddr = multiaddr(`/ip4/127.0.0.1/udp/${port}`) enr.setLocationMultiaddr(nodeAddr) const node = await UltralightProvider.create({ transport: TransportLayer.NODE, supportedNetworks: [{ networkId: NetworkId.HistoryNetwork }], config: { enr, bindAddrs: { ip4: nodeAddr, }, privateKey, }, }) return node } async function demonstrateBasicFunctionality() { const node1 = await createSimpleNode(9090) const node2 = await createSimpleNode(9091) try { await node1.portal.start() await node2.portal.start() const historyNetwork = node1.portal.network()['0x500b']! const content = new TextEncoder().encode('Hello Portal Network!') const contentKey = new Uint8Array(32).fill(1) await historyNetwork.store(contentKey, content) const storedContent = await historyNetwork.findContentLocally(contentKey) if (storedContent) { console.log('Retrieved content:', new TextDecoder().decode(storedContent)) } } finally { await node1.portal.stop() await node2.portal.stop() } } ``` To execute this script, run `demonstrateBasicFunctionality().catch(console.error)` You should be able to see on the terminal: ```typescript Creating two nodes... Starting nodes... Node1 running: true Node2 running: true Testing content operations... Content before storage: does not exist Storing content... Content successfully stored! Retrieved content: Hello Portal Network! Testing peer discovery... Node1 bind address: 127.0.0.1 Node2 bind address: 127.0.0.1 Node1 ENR: ENR { kvs: Map(5) { 'id' => Uint8Array(2) [ 118, 52 ], 'secp256k1' => <Buffer 02 e2 fe 3c 89 98 bb f0 02 b3 d8 c3 3f e9 8a 0c 25 0b 65 89 ff 0f 0b f3 b7 62 9d de 1f 5b 36 b9 d7>, 'ip' => Uint8Array(4) [ 127, 0, 0, 1 ], 'udp' => Uint8Array(2) [ 35, 130 ], 'c' => Uint8Array(7) [ 117, 32, 48, 46, 48, 46, 49 ] }, seq: 4n, signature: <Buffer f9 33 3b c8 0b 05 ad 16 57 54 f8 4e 6d ca e1 76 9f 7f f3 c3 5c 74 74 bf 0c 95 91 31 33 f0 f3 bd 04 d4 06 2a e7 aa 8f c1 6a 6c e5 95 48 d6 11 bf 77 4f ... 14 more bytes>, nodeId: '93a36e6c2106fec1d2a4231d1121e127437eab24b9c88bc39339ee3980dbbeee', encoded: <Buffer f8 8d b8 40 f9 33 3b c8 0b 05 ad 16 57 54 f8 4e 6d ca e1 76 9f 7f f3 c3 5c 74 74 bf 0c 95 91 31 33 f0 f3 bd 04 d4 06 2a e7 aa 8f c1 6a 6c e5 95 48 d6 ... 93 more bytes> } Shutting down nodes... ``` ## Benefits of EIP-1193 Compliance 1. **Standardization**: The provider now follows a well-established standard, making it easier for developers to understand and use. 2. **Interoperability**: It's easier to integrate with other tools and libraries that expect EIP-1193 compliance. 3. **Better Error Handling**: Standardized error reporting makes debugging easier. 4. **Future-Proof**: As the ecosystem grows, having a standardized interface makes it easier to add new features. ## Future Improvements While the current implementation provides EIP-1193 compliance, there are several potential improvements for the future: 1. Adding more standardized methods for Portal Network-specific operations 2. Implementing subscription capabilities as defined in EIP-1193 3. Enhanced error types for Portal Network-specific scenarios ## Conclusion The script provides a foundation for building more complex Portal Network applications, showing how nodes can be created, connected, and used for content storage and retrieval in a decentralized network. Implementing EIP-1193 compliance for UltralightProvider was an important step toward better standardization and interoperability. The changes make the provider easier to use, maintain, and integrate with other tools. The Portal Network ecosystem benefits from having standardized interfaces, and this update brings us closer to that goal. ## Resources - [EIP-1193 Specification](https://eips.ethereum.org/EIPS/eip-1193) - [UltralightProvider Documentation](https://github.com/ethereumjs/ultralight) - [Portal Network Specifications](https://github.com/ethereum/portal-network-specs)