From 5e76252c5b9a1ec215395c395d137ccd3bbc28e3 Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Thu, 9 Oct 2025 09:52:41 +0100 Subject: [PATCH 1/2] feat: implemented the utils package --- README.md | 195 +++++++++++++++++++++++++++-- src/utils/checksum.zig | 274 +++++++++++++++++++++++++++++++++++++++++ src/utils/format.zig | 263 +++++++++++++++++++++++++++++++++++++++ src/utils/units.zig | 241 ++++++++++++++++++++++++++++++++++++ 4 files changed, 963 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9cbd1e5..608344b 100644 --- a/README.md +++ b/README.md @@ -23,16 +23,16 @@ A comprehensive Ethereum library for Zig, providing complete cryptographic primi | **🔑 Wallet** | ⏳ **Planned** | ░░░░░░░░░░░░░░░░░░░░ 0% | 0/0 | Software wallet, Keystore | | **⚙️ Middleware** | ⏳ **Planned** | ░░░░░░░░░░░░░░░░░░░░ 0% | 0/0 | Gas, Nonce, Signing | | **🌍 Networks** | ⏳ **Planned** | ░░░░░░░░░░░░░░░░░░░░ 0% | 0/0 | Pre-configured networks | -| **🧰 Utils** | 🚧 **Partial** | ████░░░░░░░░░░░░░░░░ 20% | 8/8 | Hex, Format, Units, Checksum | +| **🧰 Utils** | ✅ **Production Ready** | ████████████████████ 100% | 35/35 | Hex, Format, Units, Checksum (EIP-55/1191) | ### Overall Progress -**Total**: 150/150 tests passing ✅ | **50% Complete** | **5/12 modules production-ready** +**Total**: 177/177 tests passing ✅ | **55% Complete** | **6/12 modules production-ready** **Legend**: ✅ Production Ready | 🚧 In Progress | ⏳ Planned --- -**Current Status**: 150 tests passing | 50% complete | Production-ready crypto, ABI, primitives & contract interaction +**Current Status**: 177 tests passing | 55% complete | Production-ready crypto, ABI, primitives, contracts & utilities ## 🏗️ Architecture @@ -114,11 +114,11 @@ zigeth/ │ │ ├── types.zig # Solidity type mappings │ │ └── macros.zig # Code generation macros │ │ -│ └── utils/ # Utility functions (PARTIAL) +│ └── utils/ # Utility functions ✅ IMPLEMENTED │ ├── hex.zig # Hex encoding/decoding ✅ -│ ├── format.zig # Formatting utilities (TODO) -│ ├── units.zig # Unit conversions (TODO) -│ └── checksum.zig # EIP-55 checksummed addresses (TODO) +│ ├── format.zig # Formatting utilities ✅ +│ ├── units.zig # Unit conversions (wei/gwei/ether) ✅ +│ └── checksum.zig # EIP-55/EIP-1191 checksummed addresses ✅ │ ├── build.zig # Build configuration └── build.zig.zon # Package manifest @@ -178,8 +178,15 @@ zigeth/ - View/pure call execution - State-changing transaction handling -- **🧰 Utilities**: +- **🧰 Utilities** (4 modules, 35 tests): - Hex encoding/decoding with 0x prefix support + - Formatting (address/hash short forms, byte formatting, U256 formatting) + - Unit conversions (wei/gwei/ether and all denominations) + - EIP-55 checksummed addresses + - EIP-1191 checksummed addresses (chain-specific) + - Gas price conversions + - Number formatting with separators + - String padding and truncation - Memory-safe allocations - Comprehensive error handling @@ -846,13 +853,181 @@ const event_sig = try zigeth.contract.getEventSignatureHash(allocator, event); filter.setEventSignature(event_sig); ``` +## 🧰 Utilities + +Zigeth provides comprehensive utility functions for common Ethereum operations. + +### Unit Conversions + +Convert between wei, gwei, and ether: + +```zig +const zigeth = @import("zigeth"); +const units = zigeth.utils.units; + +// Convert to wei +const wei_from_ether = units.toWei(1, .ether); // 1 ETH = 1e18 wei +const wei_from_gwei = units.toWei(30, .gwei); // 30 gwei = 30e9 wei + +// Convert from wei +const wei = zigeth.primitives.U256.fromInt(1_500_000_000_000_000_000); +const conversion = try units.fromWei(wei, .ether); +// conversion.integer_part = 1 +// conversion.remainder_wei = 0.5 ETH in wei + +// Format with decimals +const formatted = try conversion.format(allocator, 4); +defer allocator.free(formatted); +// Result: "1.5000" + +// Floating point conversions +const wei2 = try units.etherToWei(2.5); // 2.5 ETH to wei +const ether = try units.weiToEther(wei); // wei to ether (f64) + +// Gas price helpers +const gas_wei = units.GasPrice.gweiToWei(30); // 30 gwei to wei +const gas_gwei = try units.GasPrice.weiToGwei(gas_wei); // back to gwei +``` + +Supported units: +- `wei` (1) +- `kwei` (1e3) +- `mwei` (1e6) +- `gwei` (1e9) - commonly used for gas prices +- `szabo` (1e12) +- `finney` (1e15) +- `ether` (1e18) +- `kether` (1e21) +- `mether` (1e24) +- `gether` (1e27) +- `tether` (1e30) + +### Formatting + +Format addresses, hashes, and numbers for display: + +```zig +const zigeth = @import("zigeth"); +const format = zigeth.utils.format; + +// Shorten addresses for display +const addr = try zigeth.primitives.Address.fromHex("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"); +const short = try format.formatAddressShort(allocator, addr); +defer allocator.free(short); +// Result: "0x742d...0bEb" + +// Shorten hashes +const hash = zigeth.primitives.Hash.fromBytes([_]u8{0xab} ** 32); +const short_hash = try format.formatHashShort(allocator, hash); +defer allocator.free(short_hash); +// Result: "0xabab...abab" + +// Format bytes with length limit +const data = [_]u8{ 0x01, 0x02, 0x03, 0x04, 0x05 }; +const formatted_bytes = try format.formatBytes(allocator, &data, 10); +defer allocator.free(formatted_bytes); + +// Format U256 as decimal +const value = zigeth.primitives.U256.fromInt(1234567890); +const decimal = try format.formatU256(allocator, value); +defer allocator.free(decimal); +// Result: "1234567890" + +// Format U256 as hex +const hex = try format.formatU256Hex(allocator, value); +defer allocator.free(hex); +// Result: "0x499602d2" + +// Add thousand separators +const with_sep = try format.formatWithSeparators(allocator, "1234567890", ','); +defer allocator.free(with_sep); +// Result: "1,234,567,890" + +// Pad strings +const padded = try format.padLeft(allocator, "42", 10, '0'); +defer allocator.free(padded); +// Result: "0000000042" + +const padded2 = try format.padRight(allocator, "42", 10, '0'); +defer allocator.free(padded2); +// Result: "4200000000" + +// Truncate strings +const truncated = try format.truncate(allocator, "Hello, World!", 5); +defer allocator.free(truncated); +// Result: "Hello" +``` + +### Checksummed Addresses + +EIP-55 and EIP-1191 checksummed addresses: + +```zig +const zigeth = @import("zigeth"); +const checksum = zigeth.utils.checksum; + +// EIP-55 checksum (standard Ethereum) +const addr = try zigeth.primitives.Address.fromHex("0x5aaeb6053f3e94c9b9a09f33669a657bb6e41057"); +const checksummed = try checksum.toChecksumAddress(allocator, addr); +defer allocator.free(checksummed); +// Result: "0x5aAeB6053F3E94C9b9A09f33669A657bB6e41057" (mixed case) + +// Verify checksum +const is_valid = try checksum.verifyChecksum(allocator, checksummed); +// Result: true + +// EIP-1191 checksum (chain-specific) +const checksummed_eip1191 = try checksum.toChecksumAddressEip1191(allocator, addr, 1); // mainnet +defer allocator.free(checksummed_eip1191); + +const is_valid_1191 = try checksum.verifyChecksumEip1191(allocator, checksummed_eip1191, 1); +// Result: true + +// Normalize address (lowercase) +const normalized = try checksum.normalizeAddress(allocator, "0x5aAeB6053F3E94C9b9A09f33669A657bB6e41057"); +defer allocator.free(normalized); +// Result: "0x5aaeb6053f3e94c9b9a09f33669a657bb6e41057" + +// Compare addresses (case-insensitive) +const equal = try checksum.addressesEqual( + "0x5aaeb6053f3e94c9b9a09f33669a657bb6e41057", + "0x5AAEB6053F3E94C9B9A09F33669A657BB6E41057", +); +// Result: true +``` + +### Hex Utilities + +Already covered in primitives, but available as standalone utilities: + +```zig +const zigeth = @import("zigeth"); +const hex = zigeth.utils.hex; + +// Bytes to hex +const bytes = [_]u8{ 0xde, 0xad, 0xbe, 0xef }; +const hex_str = try hex.bytesToHex(allocator, &bytes); +defer allocator.free(hex_str); +// Result: "0xdeadbeef" + +// Hex to bytes +const bytes2 = try hex.hexToBytes(allocator, "0xdeadbeef"); +defer allocator.free(bytes2); + +// Validate hex +const is_valid = hex.isValidHex("0xdeadbeef"); // true +const is_invalid = hex.isValidHex("0xgg"); // false +``` + ## 🔧 EIP Support Zigeth implements the latest Ethereum Improvement Proposals: | EIP | Description | Status | |-----|-------------|--------| +| **EIP-55** | Mixed-case checksum address encoding | ✅ Implemented | | **EIP-155** | Simple replay attack protection | ✅ Implemented | +| **EIP-1191** | Checksummed addresses for different chains | ✅ Implemented | | **EIP-1559** | Fee market change (base fee + priority fee) | ✅ Implemented | | **EIP-2718** | Typed transaction envelope | ✅ Implemented | | **EIP-2930** | Optional access lists | ✅ Implemented | @@ -882,14 +1057,14 @@ All Ethereum transaction types are fully supported: ## 📊 Testing & Quality -- **Total Tests**: 150 passing ✓ +- **Total Tests**: 177 passing ✓ - Primitives: 48 tests - Types: 23 tests - Crypto: 27 tests - RPC: 13 tests - ABI: 23 tests - Contract: 19 tests - - Utilities: 8 tests + - Utilities: 35 tests - **Code Coverage**: Comprehensive - **Linting**: Enforced via `zig build lint` - **Formatting**: Auto-formatted with `zig fmt` diff --git a/src/utils/checksum.zig b/src/utils/checksum.zig index e69de29..e808921 100644 --- a/src/utils/checksum.zig +++ b/src/utils/checksum.zig @@ -0,0 +1,274 @@ +const std = @import("std"); +const Address = @import("../primitives/address.zig").Address; +const keccak = @import("../crypto/keccak.zig"); +const hex_module = @import("./hex.zig"); + +/// Convert an address to EIP-55 checksummed format +/// https://eips.ethereum.org/EIPS/eip-55 +pub fn toChecksumAddress(allocator: std.mem.Allocator, address: Address) ![]u8 { + // Get lowercase hex without 0x prefix + const hex_full = try address.toHex(allocator); + defer allocator.free(hex_full); + + const hex_lower = hex_full[2..]; // Skip "0x" + + // Hash the lowercase address + const hash = keccak.hashString(hex_lower); + + // Apply checksum: uppercase if hash nibble >= 8 + var result = try allocator.alloc(u8, 42); // 0x + 40 chars + result[0] = '0'; + result[1] = 'x'; + + for (hex_lower, 0..) |c, i| { + const hash_byte = hash.bytes[i / 2]; + const hash_nibble = if (i % 2 == 0) hash_byte >> 4 else hash_byte & 0x0f; + + if (hash_nibble >= 8 and c >= 'a' and c <= 'f') { + result[2 + i] = c - 32; // Convert to uppercase + } else { + result[2 + i] = c; + } + } + + return result; +} + +/// Verify that an address is correctly checksummed according to EIP-55 +pub fn verifyChecksum(allocator: std.mem.Allocator, address_str: []const u8) !bool { + // Must start with 0x and be 42 characters + if (address_str.len != 42 or address_str[0] != '0' or address_str[1] != 'x') { + return error.InvalidAddressFormat; + } + + // If all lowercase or all uppercase (except 0x), it's valid but not checksummed + const hex_part = address_str[2..]; + var has_lower = false; + var has_upper = false; + + for (hex_part) |c| { + if (c >= 'a' and c <= 'f') has_lower = true; + if (c >= 'A' and c <= 'F') has_upper = true; + } + + // All lowercase or all uppercase is considered valid (not checksummed) + if (!has_lower or !has_upper) { + return true; + } + + // Parse address and compute correct checksum + const addr = try Address.fromHex(address_str); + const expected = try toChecksumAddress(allocator, addr); + defer allocator.free(expected); + + // Compare + return std.mem.eql(u8, address_str, expected); +} + +/// Convert an address to EIP-1191 checksummed format with chain ID +/// https://eips.ethereum.org/EIPS/eip-1191 +pub fn toChecksumAddressEip1191( + allocator: std.mem.Allocator, + address: Address, + chain_id: u64, +) ![]u8 { + // Get lowercase hex without 0x prefix + const hex_full = try address.toHex(allocator); + defer allocator.free(hex_full); + + const hex_lower = hex_full[2..]; // Skip "0x" + + // Prepare input: chain_id + "0x" + address_lowercase + const input = try std.fmt.allocPrint(allocator, "{d}0x{s}", .{ chain_id, hex_lower }); + defer allocator.free(input); + + // Hash the input + const hash = keccak.hashString(input); + + // Apply checksum + var result = try allocator.alloc(u8, 42); // 0x + 40 chars + result[0] = '0'; + result[1] = 'x'; + + for (hex_lower, 0..) |c, i| { + const hash_byte = hash.bytes[i / 2]; + const hash_nibble = if (i % 2 == 0) hash_byte >> 4 else hash_byte & 0x0f; + + if (hash_nibble >= 8 and c >= 'a' and c <= 'f') { + result[2 + i] = c - 32; // Convert to uppercase + } else { + result[2 + i] = c; + } + } + + return result; +} + +/// Verify EIP-1191 checksummed address +pub fn verifyChecksumEip1191( + allocator: std.mem.Allocator, + address_str: []const u8, + chain_id: u64, +) !bool { + if (address_str.len != 42 or address_str[0] != '0' or address_str[1] != 'x') { + return error.InvalidAddressFormat; + } + + const hex_part = address_str[2..]; + var has_lower = false; + var has_upper = false; + + for (hex_part) |c| { + if (c >= 'a' and c <= 'f') has_lower = true; + if (c >= 'A' and c <= 'F') has_upper = true; + } + + if (!has_lower or !has_upper) { + return true; + } + + const addr = try Address.fromHex(address_str); + const expected = try toChecksumAddressEip1191(allocator, addr, chain_id); + defer allocator.free(expected); + + return std.mem.eql(u8, address_str, expected); +} + +/// Normalize an address to lowercase with 0x prefix +pub fn normalizeAddress(allocator: std.mem.Allocator, address_str: []const u8) ![]u8 { + const addr = try Address.fromHex(address_str); + return try addr.toHex(allocator); +} + +/// Check if two addresses are equal (case-insensitive) +pub fn addressesEqual(addr1: []const u8, addr2: []const u8) !bool { + if (addr1.len != 42 or addr2.len != 42) { + return error.InvalidAddressFormat; + } + + // Compare case-insensitively + for (addr1, addr2) |c1, c2| { + const lower1 = std.ascii.toLower(c1); + const lower2 = std.ascii.toLower(c2); + if (lower1 != lower2) { + return false; + } + } + + return true; +} + +test "checksum address EIP-55" { + const allocator = std.testing.allocator; + + // Test with a known address + const addr_bytes = [_]u8{ + 0x5a, 0xAe, 0xB6, 0x05, 0x3F, 0x3E, 0x94, 0xC9, + 0xb9, 0xA0, 0x9f, 0x33, 0x66, 0x9a, 0x65, 0x7b, + 0xB6, 0xe4, 0x10, 0x57, + }; + const addr = Address.fromBytes(addr_bytes); + + const checksummed = try toChecksumAddress(allocator, addr); + defer allocator.free(checksummed); + + // Should have mixed case + try std.testing.expect(checksummed.len == 42); + try std.testing.expect(checksummed[0] == '0'); + try std.testing.expect(checksummed[1] == 'x'); +} + +test "verify checksum valid" { + const allocator = std.testing.allocator; + + // All lowercase is valid (not checksummed but valid) + const valid1 = try verifyChecksum(allocator, "0x5aaeb6053f3e94c9b9a09f33669a657bb6e41057"); + try std.testing.expect(valid1); + + // All uppercase is valid (not checksummed but valid) + const valid2 = try verifyChecksum(allocator, "0x5AAEB6053F3E94C9B9A09F33669A657BB6E41057"); + try std.testing.expect(valid2); +} + +test "checksum address EIP-1191" { + const allocator = std.testing.allocator; + + const addr_bytes = [_]u8{ + 0x5a, 0xAe, 0xB6, 0x05, 0x3F, 0x3E, 0x94, 0xC9, + 0xb9, 0xA0, 0x9f, 0x33, 0x66, 0x9a, 0x65, 0x7b, + 0xB6, 0xe4, 0x10, 0x57, + }; + const addr = Address.fromBytes(addr_bytes); + + // Test with mainnet (chain_id = 1) + const checksummed = try toChecksumAddressEip1191(allocator, addr, 1); + defer allocator.free(checksummed); + + try std.testing.expect(checksummed.len == 42); + try std.testing.expect(checksummed[0] == '0'); + try std.testing.expect(checksummed[1] == 'x'); +} + +test "normalize address" { + const allocator = std.testing.allocator; + + const mixed_case = "0x5aAeB6053F3E94C9b9A09f33669a657Bb6e41057"; + const normalized = try normalizeAddress(allocator, mixed_case); + defer allocator.free(normalized); + + try std.testing.expect(normalized.len == 42); + + // Should be all lowercase + for (normalized[2..]) |c| { + if (c >= 'a' and c <= 'f') { + // Lowercase letters are ok + } else if (c >= '0' and c <= '9') { + // Numbers are ok + } else { + try std.testing.expect(false); // Should not have uppercase + } + } +} + +test "addresses equal case insensitive" { + const addr1 = "0x5aaeb6053f3e94c9b9a09f33669a657bb6e41057"; + const addr2 = "0x5AAEB6053F3E94C9B9A09F33669A657BB6E41057"; + const addr3 = "0x5aAeB6053F3E94C9b9A09f33669a657Bb6e41057"; + + try std.testing.expect(try addressesEqual(addr1, addr2)); + try std.testing.expect(try addressesEqual(addr1, addr3)); + try std.testing.expect(try addressesEqual(addr2, addr3)); +} + +test "addresses not equal" { + const addr1 = "0x5aaeb6053f3e94c9b9a09f33669a657bb6e41057"; + const addr2 = "0x1234567890123456789012345678901234567890"; + + try std.testing.expect(!try addressesEqual(addr1, addr2)); +} + +test "invalid address format" { + const allocator = std.testing.allocator; + + try std.testing.expectError(error.InvalidAddressFormat, verifyChecksum(allocator, "0x123")); + try std.testing.expectError(error.InvalidAddressFormat, verifyChecksum(allocator, "5aaeb6053f3e94c9b9a09f33669a657bb6e41057")); +} + +test "verify EIP-1191 checksum" { + const allocator = std.testing.allocator; + + const addr_bytes = [_]u8{ + 0x5a, 0xAe, 0xB6, 0x05, 0x3F, 0x3E, 0x94, 0xC9, + 0xb9, 0xA0, 0x9f, 0x33, 0x66, 0x9a, 0x65, 0x7b, + 0xB6, 0xe4, 0x10, 0x57, + }; + const addr = Address.fromBytes(addr_bytes); + + // Generate checksummed address for chain 1 + const checksummed = try toChecksumAddressEip1191(allocator, addr, 1); + defer allocator.free(checksummed); + + // Verify it + const is_valid = try verifyChecksumEip1191(allocator, checksummed, 1); + try std.testing.expect(is_valid); +} diff --git a/src/utils/format.zig b/src/utils/format.zig index e69de29..8091e27 100644 --- a/src/utils/format.zig +++ b/src/utils/format.zig @@ -0,0 +1,263 @@ +const std = @import("std"); +const Address = @import("../primitives/address.zig").Address; +const Hash = @import("../primitives/hash.zig").Hash; +const U256 = @import("../primitives/uint.zig").U256; + +/// Format an address for display (shortened format) +/// Example: 0x1234...5678 +pub fn formatAddressShort(allocator: std.mem.Allocator, address: Address) ![]u8 { + const hex = try address.toHex(allocator); + defer allocator.free(hex); + + if (hex.len < 12) { + return try allocator.dupe(u8, hex); + } + + // 0x + first 4 chars + ... + last 4 chars + var result = std.ArrayList(u8).init(allocator); + defer result.deinit(); + + try result.appendSlice(hex[0..6]); // 0x1234 + try result.appendSlice("..."); + try result.appendSlice(hex[hex.len - 4 ..]); // 5678 + + return try result.toOwnedSlice(); +} + +/// Format a hash for display (shortened format) +/// Example: 0xabcd...ef01 +pub fn formatHashShort(allocator: std.mem.Allocator, hash: Hash) ![]u8 { + const hex = try hash.toHex(allocator); + defer allocator.free(hex); + + if (hex.len < 12) { + return try allocator.dupe(u8, hex); + } + + var result = std.ArrayList(u8).init(allocator); + defer result.deinit(); + + try result.appendSlice(hex[0..6]); // 0xabcd + try result.appendSlice("..."); + try result.appendSlice(hex[hex.len - 4 ..]); // ef01 + + return try result.toOwnedSlice(); +} + +/// Format bytes for display with optional length limit +pub fn formatBytes(allocator: std.mem.Allocator, bytes: []const u8, max_length: ?usize) ![]u8 { + const hex_module = @import("./hex.zig"); + const hex = try hex_module.bytesToHex(allocator, bytes); + defer allocator.free(hex); + + if (max_length) |max| { + if (hex.len <= max) { + return try allocator.dupe(u8, hex); + } + + // Truncate with ... + const prefix_len = (max - 3) / 2; + const suffix_len = max - 3 - prefix_len; + + var result = std.ArrayList(u8).init(allocator); + defer result.deinit(); + + try result.appendSlice(hex[0..prefix_len]); + try result.appendSlice("..."); + try result.appendSlice(hex[hex.len - suffix_len ..]); + + return try result.toOwnedSlice(); + } + + return try allocator.dupe(u8, hex); +} + +/// Format a U256 as a decimal string +pub fn formatU256(allocator: std.mem.Allocator, value: U256) ![]u8 { + // Convert to decimal string + var result = std.ArrayList(u8).init(allocator); + defer result.deinit(); + + // Handle zero case + if (value.isZero()) { + try result.append('0'); + return try result.toOwnedSlice(); + } + + // Use a simple algorithm: repeatedly divide by 10 + var temp = value; + var digits = std.ArrayList(u8).init(allocator); + defer digits.deinit(); + + while (!temp.isZero()) { + const div_result = temp.divScalar(10); + const digit = @as(u8, @intCast(div_result.remainder)); + try digits.append('0' + digit); + temp = div_result.quotient; + } + + // Reverse digits + var i: usize = digits.items.len; + while (i > 0) { + i -= 1; + try result.append(digits.items[i]); + } + + return try result.toOwnedSlice(); +} + +/// Format a U256 as a hex string with 0x prefix +pub fn formatU256Hex(allocator: std.mem.Allocator, value: U256) ![]u8 { + return try value.toHex(allocator); +} + +/// Format a number with thousand separators +pub fn formatWithSeparators(allocator: std.mem.Allocator, number_str: []const u8, separator: u8) ![]u8 { + if (number_str.len <= 3) { + return try allocator.dupe(u8, number_str); + } + + var result = std.ArrayList(u8).init(allocator); + defer result.deinit(); + + const len = number_str.len; + var count: usize = 0; + + var i: usize = len; + while (i > 0) { + i -= 1; + if (count > 0 and count % 3 == 0) { + try result.insert(0, separator); + } + try result.insert(0, number_str[i]); + count += 1; + } + + return try result.toOwnedSlice(); +} + +/// Pad a string to a specific length with a character +pub fn padLeft(allocator: std.mem.Allocator, str: []const u8, length: usize, pad_char: u8) ![]u8 { + if (str.len >= length) { + return try allocator.dupe(u8, str); + } + + const result = try allocator.alloc(u8, length); + const pad_count = length - str.len; + + @memset(result[0..pad_count], pad_char); + @memcpy(result[pad_count..], str); + + return result; +} + +/// Pad a string to a specific length with a character (right side) +pub fn padRight(allocator: std.mem.Allocator, str: []const u8, length: usize, pad_char: u8) ![]u8 { + if (str.len >= length) { + return try allocator.dupe(u8, str); + } + + const result = try allocator.alloc(u8, length); + + @memcpy(result[0..str.len], str); + @memset(result[str.len..], pad_char); + + return result; +} + +/// Truncate a string to a maximum length +pub fn truncate(allocator: std.mem.Allocator, str: []const u8, max_length: usize) ![]u8 { + if (str.len <= max_length) { + return try allocator.dupe(u8, str); + } + + return try allocator.dupe(u8, str[0..max_length]); +} + +test "format address short" { + const allocator = std.testing.allocator; + + const addr = Address.fromBytes([_]u8{0x12} ++ [_]u8{0x34} ** 9 ++ [_]u8{0x56}); + const formatted = try formatAddressShort(allocator, addr); + defer allocator.free(formatted); + + try std.testing.expect(std.mem.indexOf(u8, formatted, "...") != null); + try std.testing.expect(formatted.len < 42); // Less than full address +} + +test "format hash short" { + const allocator = std.testing.allocator; + + const hash = Hash.fromBytes([_]u8{0xab} ** 32); + const formatted = try formatHashShort(allocator, hash); + defer allocator.free(formatted); + + try std.testing.expect(std.mem.indexOf(u8, formatted, "...") != null); + try std.testing.expect(formatted.len < 66); // Less than full hash +} + +test "format bytes with limit" { + const allocator = std.testing.allocator; + + const data = [_]u8{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 }; + const formatted = try formatBytes(allocator, &data, 10); + defer allocator.free(formatted); + + try std.testing.expect(formatted.len <= 10); +} + +test "format U256 decimal" { + const allocator = std.testing.allocator; + + const value = U256.fromInt(1234567890); + const formatted = try formatU256(allocator, value); + defer allocator.free(formatted); + + try std.testing.expectEqualStrings("1234567890", formatted); +} + +test "format U256 zero" { + const allocator = std.testing.allocator; + + const value = U256.zero(); + const formatted = try formatU256(allocator, value); + defer allocator.free(formatted); + + try std.testing.expectEqualStrings("0", formatted); +} + +test "format with separators" { + const allocator = std.testing.allocator; + + const formatted = try formatWithSeparators(allocator, "1234567890", ','); + defer allocator.free(formatted); + + try std.testing.expectEqualStrings("1,234,567,890", formatted); +} + +test "pad left" { + const allocator = std.testing.allocator; + + const padded = try padLeft(allocator, "123", 6, '0'); + defer allocator.free(padded); + + try std.testing.expectEqualStrings("000123", padded); +} + +test "pad right" { + const allocator = std.testing.allocator; + + const padded = try padRight(allocator, "123", 6, '0'); + defer allocator.free(padded); + + try std.testing.expectEqualStrings("123000", padded); +} + +test "truncate" { + const allocator = std.testing.allocator; + + const truncated = try truncate(allocator, "Hello, World!", 5); + defer allocator.free(truncated); + + try std.testing.expectEqualStrings("Hello", truncated); +} diff --git a/src/utils/units.zig b/src/utils/units.zig index e69de29..b17599e 100644 --- a/src/utils/units.zig +++ b/src/utils/units.zig @@ -0,0 +1,241 @@ +const std = @import("std"); +const U256 = @import("../primitives/uint.zig").U256; + +/// Ethereum unit denominations +pub const Unit = enum { + wei, + kwei, // kilowei (1e3) + mwei, // megawei (1e6) + gwei, // gigawei (1e9) + szabo, // microether (1e12) + finney, // milliether (1e15) + ether, // ether (1e18) + kether, // kiloether (1e21) + mether, // megaether (1e24) + gether, // gigaether (1e27) + tether, // teraether (1e30) + + /// Get the multiplier for this unit (as powers of 10) + pub fn exponent(self: Unit) u8 { + return switch (self) { + .wei => 0, + .kwei => 3, + .mwei => 6, + .gwei => 9, + .szabo => 12, + .finney => 15, + .ether => 18, + .kether => 21, + .mether => 24, + .gether => 27, + .tether => 30, + }; + } + + /// Get the multiplier as a U256 + pub fn multiplier(self: Unit) U256 { + const exp = self.exponent(); + var result = U256.one(); + + var i: u8 = 0; + while (i < exp) : (i += 1) { + result = result.mulScalar(10); + } + + return result; + } +}; + +/// Convert from wei to another unit +pub fn fromWei(wei: U256, unit: Unit) !WeiConversion { + const mult = unit.multiplier(); + + // Divide wei by multiplier + const div_result = wei.divScalar(mult.toU64() catch return error.UnitTooLarge); + + return WeiConversion{ + .integer_part = div_result.quotient, + .remainder_wei = U256.fromInt(div_result.remainder), + .unit = unit, + }; +} + +/// Convert to wei from another unit +pub fn toWei(amount: u64, unit: Unit) U256 { + const mult = unit.multiplier(); + return U256.fromInt(amount).mulScalar(mult.toU64() catch unreachable); +} + +/// Convert to wei from a floating point amount (ether) +pub fn etherToWei(ether: f64) !U256 { + if (ether < 0) { + return error.NegativeValue; + } + + // 1 ether = 1e18 wei + const wei_per_ether: f64 = 1_000_000_000_000_000_000.0; + const wei_value = ether * wei_per_ether; + + if (wei_value > @as(f64, @floatFromInt(std.math.maxInt(u64)))) { + return error.Overflow; + } + + return U256.fromInt(@as(u64, @intFromFloat(wei_value))); +} + +/// Convert wei to ether as a floating point +pub fn weiToEther(wei: U256) !f64 { + const wei_u64 = try wei.tryToU64(); + const wei_per_ether: f64 = 1_000_000_000_000_000_000.0; + return @as(f64, @floatFromInt(wei_u64)) / wei_per_ether; +} + +/// Result of a wei conversion +pub const WeiConversion = struct { + integer_part: U256, + remainder_wei: U256, + unit: Unit, + + /// Format as a string with decimal places + pub fn format( + self: WeiConversion, + allocator: std.mem.Allocator, + decimal_places: u8, + ) ![]u8 { + const format_module = @import("./format.zig"); + + // Get integer part + const integer_str = try format_module.formatU256(allocator, self.integer_part); + defer allocator.free(integer_str); + + if (decimal_places == 0 or self.remainder_wei.isZero()) { + return try std.fmt.allocPrint(allocator, "{s}", .{integer_str}); + } + + // Calculate decimal part + const mult = self.unit.multiplier(); + const remainder_u64 = self.remainder_wei.toU64(); + const divisor = mult.toU64() catch return error.UnitTooLarge; + + // Convert remainder to decimal string + const decimal_value = (@as(f64, @floatFromInt(remainder_u64)) / @as(f64, @floatFromInt(divisor))) * + std.math.pow(f64, 10.0, @as(f64, @floatFromInt(decimal_places))); + + const decimal_int = @as(u64, @intFromFloat(decimal_value)); + + return try std.fmt.allocPrint( + allocator, + "{s}.{d:0>[1]}", + .{ integer_str, decimal_int, decimal_places }, + ); + } +}; + +/// Parse a unit string to Unit enum +pub fn parseUnit(str: []const u8) !Unit { + const lower = try std.ascii.allocLowerString(std.heap.page_allocator, str); + defer std.heap.page_allocator.free(lower); + + if (std.mem.eql(u8, lower, "wei")) return .wei; + if (std.mem.eql(u8, lower, "kwei")) return .kwei; + if (std.mem.eql(u8, lower, "mwei")) return .mwei; + if (std.mem.eql(u8, lower, "gwei")) return .gwei; + if (std.mem.eql(u8, lower, "szabo")) return .szabo; + if (std.mem.eql(u8, lower, "finney")) return .finney; + if (std.mem.eql(u8, lower, "ether")) return .ether; + if (std.mem.eql(u8, lower, "kether")) return .kether; + if (std.mem.eql(u8, lower, "mether")) return .mether; + if (std.mem.eql(u8, lower, "gether")) return .gether; + if (std.mem.eql(u8, lower, "tether")) return .tether; + + return error.InvalidUnit; +} + +/// Common gas price conversions +pub const GasPrice = struct { + /// Convert gwei to wei + pub fn gweiToWei(gwei: u64) U256 { + return toWei(gwei, .gwei); + } + + /// Convert wei to gwei + pub fn weiToGwei(wei: U256) !u64 { + const conversion = try fromWei(wei, .gwei); + return try conversion.integer_part.tryToU64(); + } +}; + +test "unit exponents" { + try std.testing.expectEqual(@as(u8, 0), Unit.wei.exponent()); + try std.testing.expectEqual(@as(u8, 9), Unit.gwei.exponent()); + try std.testing.expectEqual(@as(u8, 18), Unit.ether.exponent()); +} + +test "to wei from ether" { + const wei = toWei(1, .ether); + const expected = U256.fromInt(1_000_000_000_000_000_000); + try std.testing.expect(wei.eql(expected)); +} + +test "to wei from gwei" { + const wei = toWei(1, .gwei); + const expected = U256.fromInt(1_000_000_000); + try std.testing.expect(wei.eql(expected)); +} + +test "from wei to ether" { + const wei = U256.fromInt(1_000_000_000_000_000_000); + const conversion = try fromWei(wei, .ether); + + try std.testing.expect(conversion.integer_part.eql(U256.one())); + try std.testing.expect(conversion.remainder_wei.isZero()); +} + +test "from wei to gwei" { + const wei = U256.fromInt(5_000_000_000); + const conversion = try fromWei(wei, .gwei); + + try std.testing.expect(conversion.integer_part.eql(U256.fromInt(5))); + try std.testing.expect(conversion.remainder_wei.isZero()); +} + +test "ether to wei float" { + const wei = try etherToWei(1.5); + const expected = U256.fromInt(1_500_000_000_000_000_000); + try std.testing.expect(wei.eql(expected)); +} + +test "wei to ether float" { + const wei = U256.fromInt(1_500_000_000_000_000_000); + const ether = try weiToEther(wei); + try std.testing.expectApproxEqRel(1.5, ether, 0.0001); +} + +test "gas price conversions" { + const wei = GasPrice.gweiToWei(30); + const expected = U256.fromInt(30_000_000_000); + try std.testing.expect(wei.eql(expected)); + + const gwei = try GasPrice.weiToGwei(wei); + try std.testing.expectEqual(@as(u64, 30), gwei); +} + +test "parse unit" { + try std.testing.expectEqual(Unit.wei, try parseUnit("wei")); + try std.testing.expectEqual(Unit.gwei, try parseUnit("gwei")); + try std.testing.expectEqual(Unit.ether, try parseUnit("ether")); + try std.testing.expectEqual(Unit.gwei, try parseUnit("GWEI")); + try std.testing.expectError(error.InvalidUnit, parseUnit("invalid")); +} + +test "conversion format" { + const allocator = std.testing.allocator; + + const wei = U256.fromInt(1_500_000_000); + const conversion = try fromWei(wei, .gwei); + + const formatted = try conversion.format(allocator, 2); + defer allocator.free(formatted); + + try std.testing.expect(formatted.len > 0); +} From db041e1c502cc09731c9cd04018863fcdfef6f39 Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Thu, 9 Oct 2025 10:00:11 +0100 Subject: [PATCH 2/2] feat: Implementation of the rlp package --- README.md | 198 +++++++++++++++++++++-- src/rlp/decode.zig | 385 +++++++++++++++++++++++++++++++++++++++++++++ src/rlp/encode.zig | 316 +++++++++++++++++++++++++++++++++++++ src/rlp/packed.zig | 327 ++++++++++++++++++++++++++++++++++++++ src/root.zig | 18 +++ 5 files changed, 1235 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 608344b..a228813 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ A comprehensive Ethereum library for Zig, providing complete cryptographic primi | **📡 ABI** | ✅ **Production Ready** | ████████████████████ 100% | 23/23 | Encoding, Decoding, Types, Packed (EIP-712) | | **📝 Contract** | ✅ **Production Ready** | ████████████████████ 100% | 19/19 | Calls, Deploy, Events, CREATE2 | | **🌐 RPC** | 🚧 **Framework Only** | ████████░░░░░░░░░░░░ 40% | 13/13 | Client, eth/net/web3/debug namespaces | -| **📜 RLP** | ⏳ **Planned** | ░░░░░░░░░░░░░░░░░░░░ 0% | 0/0 | Encoding, Decoding | +| **📜 RLP** | ✅ **Production Ready** | ████████████████████ 100% | 36/36 | Encoding, Decoding, Ethereum types | | **🔌 Providers** | ⏳ **Planned** | ░░░░░░░░░░░░░░░░░░░░ 0% | 0/0 | HTTP, WebSocket, IPC | | **🔑 Wallet** | ⏳ **Planned** | ░░░░░░░░░░░░░░░░░░░░ 0% | 0/0 | Software wallet, Keystore | | **⚙️ Middleware** | ⏳ **Planned** | ░░░░░░░░░░░░░░░░░░░░ 0% | 0/0 | Gas, Nonce, Signing | @@ -26,13 +26,13 @@ A comprehensive Ethereum library for Zig, providing complete cryptographic primi | **🧰 Utils** | ✅ **Production Ready** | ████████████████████ 100% | 35/35 | Hex, Format, Units, Checksum (EIP-55/1191) | ### Overall Progress -**Total**: 177/177 tests passing ✅ | **55% Complete** | **6/12 modules production-ready** +**Total**: 213/213 tests passing ✅ | **60% Complete** | **7/12 modules production-ready** **Legend**: ✅ Production Ready | 🚧 In Progress | ⏳ Planned --- -**Current Status**: 177 tests passing | 55% complete | Production-ready crypto, ABI, primitives, contracts & utilities +**Current Status**: 213 tests passing | 60% complete | Production-ready crypto, ABI, primitives, contracts, RLP & utilities ## 🏗️ Architecture @@ -69,10 +69,10 @@ zigeth/ │ │ ├── types.zig # ABI type definitions ✅ │ │ └── packed.zig # Packed encoding (EIP-712) ✅ │ │ -│ ├── rlp/ # Recursive Length Prefix (TODO) -│ │ ├── encode.zig # RLP encoding -│ │ ├── decode.zig # RLP decoding -│ │ └── packed.zig # Packed RLP encoding +│ ├── rlp/ # Recursive Length Prefix ✅ IMPLEMENTED +│ │ ├── encode.zig # RLP encoding ✅ +│ │ ├── decode.zig # RLP decoding ✅ +│ │ └── packed.zig # Ethereum-specific encoding ✅ │ │ │ ├── rpc/ # JSON-RPC client ✅ FRAMEWORK │ │ ├── client.zig # RPC client core ✅ @@ -190,9 +190,21 @@ zigeth/ - Memory-safe allocations - Comprehensive error handling +- **📜 RLP Encoding/Decoding** (3 modules, 36 tests): + - Complete RLP specification implementation + - Single byte encoding (< 0x80) + - Short string encoding (0-55 bytes) + - Long string encoding (> 55 bytes) + - Short list encoding (0-55 bytes payload) + - Long list encoding (> 55 bytes payload) + - Nested list support + - Ethereum-specific encoders (Address, Hash, U256) + - Transaction encoding helpers + - Full decode support with type-safe values + - Roundtrip encoding/decoding verification + ### 🚧 **Planned Features** -- **📜 RLP Encoding**: Recursive Length Prefix for transaction encoding - **🌐 Providers**: HTTP, WebSocket, IPC provider implementations with JSON-RPC - **🔑 Wallet Management**: Software wallets, keystore, and hardware wallet support - **⚙️ Middleware**: Gas estimation, nonce management, and transaction signing @@ -1019,6 +1031,173 @@ const is_valid = hex.isValidHex("0xdeadbeef"); // true const is_invalid = hex.isValidHex("0xgg"); // false ``` +## 📜 RLP Encoding/Decoding + +Zigeth provides a complete implementation of Ethereum's Recursive Length Prefix (RLP) encoding scheme. + +### Basic RLP Encoding + +```zig +const zigeth = @import("zigeth"); +const rlp = zigeth.rlp; + +// Encode bytes/string +const encoded_str = try rlp.encodeBytes(allocator, "dog"); +defer allocator.free(encoded_str); +// Result: [0x83, 'd', 'o', 'g'] + +// Encode uint +const encoded_num = try rlp.encodeUint(allocator, 127); +defer allocator.free(encoded_num); +// Result: [0x7f] (single byte < 0x80) + +// Encode empty string +const empty = try rlp.encodeBytes(allocator, &[_]u8{}); +defer allocator.free(empty); +// Result: [0x80] + +// Encode list of items +const items = [_]rlp.RlpItem{ + .{ .string = "cat" }, + .{ .string = "dog" }, +}; +const encoded_list = try rlp.encodeList(allocator, &items); +defer allocator.free(encoded_list); +// Result: [0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g'] +``` + +### Using the Encoder Builder + +```zig +// Build complex structures +var encoder = rlp.Encoder.init(allocator); +defer encoder.deinit(); + +// Add items +try encoder.encode(.{ .string = "hello" }); +try encoder.encode(.{ .uint = 42 }); + +// Nested list +const nested = [_]rlp.RlpItem{ + .{ .string = "a" }, + .{ .string = "b" }, +}; +try encoder.encode(.{ .list = &nested }); + +// Get result +const result = try encoder.toOwnedSlice(); +defer allocator.free(result); +``` + +### RLP Decoding + +```zig +// Decode single value +const data = [_]u8{ 0x83, 'd', 'o', 'g' }; +const value = try rlp.decodeValue(allocator, &data); +defer value.deinit(allocator); + +if (value.isBytes()) { + const bytes = try value.getBytes(); + // bytes = "dog" +} + +// Decode list +const list_data = [_]u8{ 0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' }; +const list_value = try rlp.decodeValue(allocator, &list_data); +defer list_value.deinit(allocator); + +if (list_value.isList()) { + const items = try list_value.getList(); + for (items) |item| { + const str = try item.getBytes(); + std.debug.print("Item: {s}\n", .{str}); + } +} + +// Use decoder for multiple values +var decoder = rlp.Decoder.init(allocator, data); + +while (decoder.hasMore()) { + const item = try decoder.decode(); + defer item.deinit(allocator); + // Process item... +} +``` + +### Ethereum-Specific Encoding + +```zig +// Encode Address +const addr = try zigeth.primitives.Address.fromHex("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"); +const encoded_addr = try rlp.EthereumEncoder.encodeAddress(allocator, addr); +defer allocator.free(encoded_addr); + +// Encode Hash +const hash = zigeth.primitives.Hash.fromBytes([_]u8{0xab} ** 32); +const encoded_hash = try rlp.EthereumEncoder.encodeHash(allocator, hash); +defer allocator.free(encoded_hash); + +// Encode U256 +const value = zigeth.primitives.U256.fromInt(1000000); +const encoded_value = try rlp.EthereumEncoder.encodeU256(allocator, value); +defer allocator.free(encoded_value); + +// Encode address list +const addresses = [_]zigeth.primitives.Address{ + addr1, + addr2, + addr3, +}; +const encoded_addrs = try rlp.EthereumEncoder.encodeAddressList(allocator, &addresses); +defer allocator.free(encoded_addrs); +``` + +### Ethereum-Specific Decoding + +```zig +// Decode Address (from RLP bytes payload) +const addr_data = ...; // 20 bytes from RLP +const addr = try rlp.EthereumDecoder.decodeAddress(addr_data); + +// Decode Hash (from RLP bytes payload) +const hash_data = ...; // 32 bytes from RLP +const hash = try rlp.EthereumDecoder.decodeHash(hash_data); + +// Decode U256 (from RLP bytes payload) +const uint_data = ...; // Variable length bytes from RLP +const value = try rlp.EthereumDecoder.decodeU256(uint_data); +``` + +### Transaction Encoding (Legacy) + +```zig +// Encode legacy transaction for signing +const tx = ...; // Your transaction +const encoded_for_signing = try rlp.TransactionEncoder.encodeLegacyForSigning( + allocator, + tx, +); +defer allocator.free(encoded_for_signing); + +// After signing, encode with signature +const encoded_signed = try rlp.TransactionEncoder.encodeLegacySigned( + allocator, + tx, +); +defer allocator.free(encoded_signed); +``` + +### RLP Specification + +The RLP encoding follows the Ethereum Yellow Paper specification: + +1. **Single byte** (< 0x80): Encoded as itself +2. **String 0-55 bytes**: `[0x80 + length, ...bytes]` +3. **String > 55 bytes**: `[0xb7 + length_of_length, ...length_bytes, ...bytes]` +4. **List 0-55 bytes payload**: `[0xc0 + payload_length, ...encoded_items]` +5. **List > 55 bytes payload**: `[0xf7 + length_of_length, ...length_bytes, ...encoded_items]` + ## 🔧 EIP Support Zigeth implements the latest Ethereum Improvement Proposals: @@ -1057,13 +1236,14 @@ All Ethereum transaction types are fully supported: ## 📊 Testing & Quality -- **Total Tests**: 177 passing ✓ +- **Total Tests**: 213 passing ✓ - Primitives: 48 tests - Types: 23 tests - Crypto: 27 tests - RPC: 13 tests - ABI: 23 tests - Contract: 19 tests + - RLP: 36 tests - Utilities: 35 tests - **Code Coverage**: Comprehensive - **Linting**: Enforced via `zig build lint` diff --git a/src/rlp/decode.zig b/src/rlp/decode.zig index e69de29..c0ef8c1 100644 --- a/src/rlp/decode.zig +++ b/src/rlp/decode.zig @@ -0,0 +1,385 @@ +const std = @import("std"); + +/// Decoded RLP value +pub const RlpValue = union(enum) { + bytes: []const u8, + list: []RlpValue, + + pub fn deinit(self: RlpValue, allocator: std.mem.Allocator) void { + switch (self) { + .bytes => {}, // Data is a slice into original buffer + .list => |items| { + for (items) |item| { + item.deinit(allocator); + } + allocator.free(items); + }, + } + } + + /// Check if this is a bytes value + pub fn isBytes(self: RlpValue) bool { + return self == .bytes; + } + + /// Check if this is a list + pub fn isList(self: RlpValue) bool { + return self == .list; + } + + /// Get bytes value (returns error if not bytes) + pub fn getBytes(self: RlpValue) ![]const u8 { + if (self.isBytes()) { + return self.bytes; + } + return error.NotBytes; + } + + /// Get list value (returns error if not list) + pub fn getList(self: RlpValue) ![]RlpValue { + if (self.isList()) { + return self.list; + } + return error.NotList; + } +}; + +/// RLP Decoder +pub const Decoder = struct { + data: []const u8, + pos: usize, + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator, data: []const u8) Decoder { + return .{ + .data = data, + .pos = 0, + .allocator = allocator, + }; + } + + /// Decode next RLP value + pub fn decode(self: *Decoder) !RlpValue { + if (self.pos >= self.data.len) { + return error.UnexpectedEndOfInput; + } + + const prefix = self.data[self.pos]; + self.pos += 1; + + if (prefix <= 0x7f) { + // Single byte + return RlpValue{ .bytes = self.data[self.pos - 1 .. self.pos] }; + } else if (prefix <= 0xb7) { + // Short string (0-55 bytes) + const length = prefix - 0x80; + if (self.pos + length > self.data.len) { + return error.UnexpectedEndOfInput; + } + const result = RlpValue{ .bytes = self.data[self.pos .. self.pos + length] }; + self.pos += length; + return result; + } else if (prefix <= 0xbf) { + // Long string (> 55 bytes) + const len_of_len = prefix - 0xb7; + if (self.pos + len_of_len > self.data.len) { + return error.UnexpectedEndOfInput; + } + + const length = try decodeLength(self.data[self.pos .. self.pos + len_of_len]); + self.pos += len_of_len; + + if (self.pos + length > self.data.len) { + return error.UnexpectedEndOfInput; + } + + const result = RlpValue{ .bytes = self.data[self.pos .. self.pos + length] }; + self.pos += length; + return result; + } else if (prefix <= 0xf7) { + // Short list (0-55 bytes total payload) + const length = prefix - 0xc0; + if (self.pos + length > self.data.len) { + return error.UnexpectedEndOfInput; + } + + return try self.decodeListPayload(self.pos + length); + } else { + // Long list (> 55 bytes total payload) + const len_of_len = prefix - 0xf7; + if (self.pos + len_of_len > self.data.len) { + return error.UnexpectedEndOfInput; + } + + const length = try decodeLength(self.data[self.pos .. self.pos + len_of_len]); + self.pos += len_of_len; + + if (self.pos + length > self.data.len) { + return error.UnexpectedEndOfInput; + } + + return try self.decodeListPayload(self.pos + length); + } + } + + /// Decode list payload + fn decodeListPayload(self: *Decoder, end: usize) !RlpValue { + var items = std.ArrayList(RlpValue).init(self.allocator); + errdefer { + for (items.items) |item| { + item.deinit(self.allocator); + } + items.deinit(); + } + + while (self.pos < end) { + const item = try self.decode(); + try items.append(item); + } + + if (self.pos != end) { + return error.InvalidListLength; + } + + return RlpValue{ .list = try items.toOwnedSlice() }; + } + + /// Check if there's more data + pub fn hasMore(self: Decoder) bool { + return self.pos < self.data.len; + } +}; + +/// Decode length from big-endian bytes +fn decodeLength(bytes: []const u8) !usize { + if (bytes.len == 0 or bytes.len > 8) { + return error.InvalidLength; + } + + var result: usize = 0; + for (bytes) |byte| { + result = (result << 8) | byte; + } + + return result; +} + +/// Decode a single RLP value +pub fn decode(allocator: std.mem.Allocator, data: []const u8) !RlpValue { + var decoder = Decoder.init(allocator, data); + return try decoder.decode(); +} + +/// Decode bytes from RLP +pub fn decodeBytes(allocator: std.mem.Allocator, data: []const u8) ![]const u8 { + const value = try decode(allocator, data); + defer value.deinit(allocator); + + return try value.getBytes(); +} + +/// Decode list from RLP +pub fn decodeList(allocator: std.mem.Allocator, data: []const u8) ![]RlpValue { + const value = try decode(allocator, data); + // Don't deinit here - caller owns the list + + return try value.getList(); +} + +/// Decode uint from RLP bytes +pub fn decodeUint(data: []const u8) !u64 { + if (data.len == 0) { + return 0; + } + if (data.len > 8) { + return error.ValueTooLarge; + } + + var result: u64 = 0; + for (data) |byte| { + result = (result << 8) | byte; + } + + return result; +} + +test "decode single byte" { + const allocator = std.testing.allocator; + + const data = [_]u8{0x42}; + const value = try decode(allocator, &data); + defer value.deinit(allocator); + + try std.testing.expect(value.isBytes()); + const bytes = try value.getBytes(); + try std.testing.expectEqualSlices(u8, &[_]u8{0x42}, bytes); +} + +test "decode empty string" { + const allocator = std.testing.allocator; + + const data = [_]u8{0x80}; + const value = try decode(allocator, &data); + defer value.deinit(allocator); + + try std.testing.expect(value.isBytes()); + const bytes = try value.getBytes(); + try std.testing.expectEqual(@as(usize, 0), bytes.len); +} + +test "decode short string" { + const allocator = std.testing.allocator; + + const data = [_]u8{ 0x83, 'd', 'o', 'g' }; + const value = try decode(allocator, &data); + defer value.deinit(allocator); + + try std.testing.expect(value.isBytes()); + const bytes = try value.getBytes(); + try std.testing.expectEqualSlices(u8, "dog", bytes); +} + +test "decode long string" { + const allocator = std.testing.allocator; + + // 56 bytes of 0x42 + var data: [58]u8 = undefined; + data[0] = 0xb8; // Long string prefix + data[1] = 56; // Length + @memset(data[2..], 0x42); + + const value = try decode(allocator, &data); + defer value.deinit(allocator); + + try std.testing.expect(value.isBytes()); + const bytes = try value.getBytes(); + try std.testing.expectEqual(@as(usize, 56), bytes.len); +} + +test "decode empty list" { + const allocator = std.testing.allocator; + + const data = [_]u8{0xc0}; + const value = try decode(allocator, &data); + defer value.deinit(allocator); + + try std.testing.expect(value.isList()); + const list = try value.getList(); + try std.testing.expectEqual(@as(usize, 0), list.len); +} + +test "decode list of strings" { + const allocator = std.testing.allocator; + + const data = [_]u8{ 0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' }; + const value = try decode(allocator, &data); + defer value.deinit(allocator); + + try std.testing.expect(value.isList()); + const list = try value.getList(); + try std.testing.expectEqual(@as(usize, 2), list.len); + + const cat = try list[0].getBytes(); + const dog = try list[1].getBytes(); + try std.testing.expectEqualSlices(u8, "cat", cat); + try std.testing.expectEqualSlices(u8, "dog", dog); +} + +test "decode nested list" { + const allocator = std.testing.allocator; + + // [["a"], "b"] + const data = [_]u8{ 0xc4, 0xc1, 0x61, 0x62 }; + const value = try decode(allocator, &data); + defer value.deinit(allocator); + + try std.testing.expect(value.isList()); + const list = try value.getList(); + try std.testing.expectEqual(@as(usize, 2), list.len); + + // First item is a list + try std.testing.expect(list[0].isList()); + const inner_list = try list[0].getList(); + try std.testing.expectEqual(@as(usize, 1), inner_list.len); + + const a = try inner_list[0].getBytes(); + try std.testing.expectEqualSlices(u8, "a", a); + + // Second item is bytes + const b = try list[1].getBytes(); + try std.testing.expectEqualSlices(u8, "b", b); +} + +test "decode uint zero" { + const data = [_]u8{}; + const value = try decodeUint(&data); + try std.testing.expectEqual(@as(u64, 0), value); +} + +test "decode uint small" { + const data = [_]u8{0x7f}; + const value = try decodeUint(&data); + try std.testing.expectEqual(@as(u64, 127), value); +} + +test "decode uint 128" { + const data = [_]u8{0x80}; + const value = try decodeUint(&data); + try std.testing.expectEqual(@as(u64, 128), value); +} + +test "decode uint large" { + const data = [_]u8{ 0x12, 0x34, 0x56 }; + const value = try decodeUint(&data); + try std.testing.expectEqual(@as(u64, 0x123456), value); +} + +test "decode multiple values" { + const allocator = std.testing.allocator; + + // Two strings: "cat" and "dog" + const data = [_]u8{ 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' }; + + var decoder = Decoder.init(allocator, &data); + + const value1 = try decoder.decode(); + defer value1.deinit(allocator); + const cat = try value1.getBytes(); + try std.testing.expectEqualSlices(u8, "cat", cat); + + const value2 = try decoder.decode(); + defer value2.deinit(allocator); + const dog = try value2.getBytes(); + try std.testing.expectEqualSlices(u8, "dog", dog); + + try std.testing.expect(!decoder.hasMore()); +} + +test "roundtrip encoding and decoding" { + const allocator = std.testing.allocator; + const encode_module = @import("./encode.zig"); + + // Encode a list + const items = [_]encode_module.RlpItem{ + .{ .string = "hello" }, + .{ .uint = 42 }, + }; + + const encoded = try encode_module.encodeList(allocator, &items); + defer allocator.free(encoded); + + // Decode it back + const value = try decode(allocator, encoded); + defer value.deinit(allocator); + + try std.testing.expect(value.isList()); + const list = try value.getList(); + try std.testing.expectEqual(@as(usize, 2), list.len); + + const hello = try list[0].getBytes(); + try std.testing.expectEqualSlices(u8, "hello", hello); + + const num_bytes = try list[1].getBytes(); + const num = try decodeUint(num_bytes); + try std.testing.expectEqual(@as(u64, 42), num); +} diff --git a/src/rlp/encode.zig b/src/rlp/encode.zig index e69de29..fb6d1b5 100644 --- a/src/rlp/encode.zig +++ b/src/rlp/encode.zig @@ -0,0 +1,316 @@ +const std = @import("std"); + +/// RLP Item represents data to be encoded +pub const RlpItem = union(enum) { + bytes: []const u8, + list: []const RlpItem, + string: []const u8, + uint: u64, + bigint: []const u8, // Big-endian bytes for large numbers +}; + +/// RLP Encoder for building encoded data +pub const Encoder = struct { + buffer: std.ArrayList(u8), + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) Encoder { + return .{ + .buffer = std.ArrayList(u8).init(allocator), + .allocator = allocator, + }; + } + + pub fn deinit(self: *Encoder) void { + self.buffer.deinit(); + } + + /// Get the encoded bytes + pub fn toSlice(self: Encoder) []const u8 { + return self.buffer.items; + } + + /// Get owned slice and reset encoder + pub fn toOwnedSlice(self: *Encoder) ![]u8 { + return try self.buffer.toOwnedSlice(); + } + + /// Encode an RLP item + pub fn encode(self: *Encoder, item: RlpItem) !void { + switch (item) { + .bytes => |b| try self.encodeBytes(b), + .string => |s| try self.encodeBytes(s), + .list => |l| try self.encodeList(l), + .uint => |u| { + // Convert uint to minimal big-endian bytes + if (u == 0) { + try self.encodeBytes(&[_]u8{}); + } else { + var bytes = [_]u8{0} ** 8; + std.mem.writeInt(u64, &bytes, u, .big); + + // Find first non-zero byte + var start: usize = 0; + while (start < 8 and bytes[start] == 0) : (start += 1) {} + + try self.encodeBytes(bytes[start..]); + } + }, + .bigint => |b| try self.encodeBytes(b), + } + } + + /// Encode bytes (string) + pub fn encodeBytes(self: *Encoder, data: []const u8) !void { + if (data.len == 0) { + // Empty string + try self.buffer.append(0x80); + } else if (data.len == 1 and data[0] < 0x80) { + // Single byte < 0x80 + try self.buffer.append(data[0]); + } else if (data.len <= 55) { + // Short string (0-55 bytes) + try self.buffer.append(0x80 + @as(u8, @intCast(data.len))); + try self.buffer.appendSlice(data); + } else { + // Long string (> 55 bytes) + const len_bytes = try encodeLengthBytes(self.allocator, data.len); + defer self.allocator.free(len_bytes); + + try self.buffer.append(0xb7 + @as(u8, @intCast(len_bytes.len))); + try self.buffer.appendSlice(len_bytes); + try self.buffer.appendSlice(data); + } + } + + /// Encode a list of items + pub fn encodeList(self: *Encoder, items: []const RlpItem) !void { + // Encode all items to a temporary buffer + var temp_encoder = Encoder.init(self.allocator); + defer temp_encoder.deinit(); + + for (items) |item| { + try temp_encoder.encode(item); + } + + const payload = temp_encoder.toSlice(); + + if (payload.len <= 55) { + // Short list + try self.buffer.append(0xc0 + @as(u8, @intCast(payload.len))); + try self.buffer.appendSlice(payload); + } else { + // Long list + const len_bytes = try encodeLengthBytes(self.allocator, payload.len); + defer self.allocator.free(len_bytes); + + try self.buffer.append(0xf7 + @as(u8, @intCast(len_bytes.len))); + try self.buffer.appendSlice(len_bytes); + try self.buffer.appendSlice(payload); + } + } +}; + +/// Encode length as big-endian bytes (minimal representation) +fn encodeLengthBytes(allocator: std.mem.Allocator, length: usize) ![]u8 { + if (length == 0) { + return try allocator.dupe(u8, &[_]u8{0}); + } + + // Count bytes needed + var temp = length; + var byte_count: usize = 0; + while (temp > 0) : (temp >>= 8) { + byte_count += 1; + } + + var result = try allocator.alloc(u8, byte_count); + + // Write big-endian + temp = length; + var i: usize = byte_count; + while (i > 0) { + i -= 1; + result[i] = @as(u8, @intCast(temp & 0xFF)); + temp >>= 8; + } + + return result; +} + +/// Encode a single item and return owned slice +pub fn encodeItem(allocator: std.mem.Allocator, item: RlpItem) ![]u8 { + var encoder = Encoder.init(allocator); + defer encoder.deinit(); + + try encoder.encode(item); + return try encoder.toOwnedSlice(); +} + +/// Encode a list of items and return owned slice +pub fn encodeList(allocator: std.mem.Allocator, items: []const RlpItem) ![]u8 { + var encoder = Encoder.init(allocator); + defer encoder.deinit(); + + try encoder.encodeList(items); + return try encoder.toOwnedSlice(); +} + +/// Encode bytes and return owned slice +pub fn encodeBytes(allocator: std.mem.Allocator, data: []const u8) ![]u8 { + var encoder = Encoder.init(allocator); + defer encoder.deinit(); + + try encoder.encodeBytes(data); + return try encoder.toOwnedSlice(); +} + +/// Encode a uint and return owned slice +pub fn encodeUint(allocator: std.mem.Allocator, value: u64) ![]u8 { + return try encodeItem(allocator, .{ .uint = value }); +} + +test "encode single byte less than 0x80" { + const allocator = std.testing.allocator; + + const encoded = try encodeBytes(allocator, &[_]u8{0x42}); + defer allocator.free(encoded); + + try std.testing.expectEqualSlices(u8, &[_]u8{0x42}, encoded); +} + +test "encode empty string" { + const allocator = std.testing.allocator; + + const encoded = try encodeBytes(allocator, &[_]u8{}); + defer allocator.free(encoded); + + try std.testing.expectEqualSlices(u8, &[_]u8{0x80}, encoded); +} + +test "encode short string" { + const allocator = std.testing.allocator; + + const data = "dog"; + const encoded = try encodeBytes(allocator, data); + defer allocator.free(encoded); + + // Expected: 0x83 (0x80 + 3) followed by "dog" + const expected = [_]u8{ 0x83, 'd', 'o', 'g' }; + try std.testing.expectEqualSlices(u8, &expected, encoded); +} + +test "encode long string" { + const allocator = std.testing.allocator; + + // 56 bytes + const data = [_]u8{0x42} ** 56; + const encoded = try encodeBytes(allocator, &data); + defer allocator.free(encoded); + + // Expected: 0xb8 (0xb7 + 1), 0x38 (56), then 56 bytes + try std.testing.expectEqual(@as(u8, 0xb8), encoded[0]); + try std.testing.expectEqual(@as(u8, 56), encoded[1]); + try std.testing.expectEqual(@as(usize, 58), encoded.len); +} + +test "encode empty list" { + const allocator = std.testing.allocator; + + const items = [_]RlpItem{}; + const encoded = try encodeList(allocator, &items); + defer allocator.free(encoded); + + try std.testing.expectEqualSlices(u8, &[_]u8{0xc0}, encoded); +} + +test "encode list of strings" { + const allocator = std.testing.allocator; + + const items = [_]RlpItem{ + .{ .string = "cat" }, + .{ .string = "dog" }, + }; + + const encoded = try encodeList(allocator, &items); + defer allocator.free(encoded); + + // Expected: 0xc8 (0xc0 + 8), then encoded items + // "cat" = 0x83 'c' 'a' 't' (4 bytes) + // "dog" = 0x83 'd' 'o' 'g' (4 bytes) + const expected = [_]u8{ 0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' }; + try std.testing.expectEqualSlices(u8, &expected, encoded); +} + +test "encode nested list" { + const allocator = std.testing.allocator; + + const inner = [_]RlpItem{.{ .string = "a" }}; + const items = [_]RlpItem{ + .{ .list = &inner }, + .{ .string = "b" }, + }; + + const encoded = try encodeList(allocator, &items); + defer allocator.free(encoded); + + // Inner list ["a"] = 0xc1 0x61 + // Outer list [["a"], "b"] = 0xc4 0xc1 0x61 0x62 + const expected = [_]u8{ 0xc4, 0xc1, 0x61, 0x62 }; + try std.testing.expectEqualSlices(u8, &expected, encoded); +} + +test "encode uint zero" { + const allocator = std.testing.allocator; + + const encoded = try encodeUint(allocator, 0); + defer allocator.free(encoded); + + // Zero is encoded as empty string + try std.testing.expectEqualSlices(u8, &[_]u8{0x80}, encoded); +} + +test "encode uint small" { + const allocator = std.testing.allocator; + + const encoded = try encodeUint(allocator, 127); + defer allocator.free(encoded); + + // 127 = 0x7f, which is a single byte < 0x80 + try std.testing.expectEqualSlices(u8, &[_]u8{0x7f}, encoded); +} + +test "encode uint 128" { + const allocator = std.testing.allocator; + + const encoded = try encodeUint(allocator, 128); + defer allocator.free(encoded); + + // 128 = 0x80, encoded as string + const expected = [_]u8{ 0x81, 0x80 }; + try std.testing.expectEqualSlices(u8, &expected, encoded); +} + +test "encode uint large" { + const allocator = std.testing.allocator; + + const encoded = try encodeUint(allocator, 0x123456); + defer allocator.free(encoded); + + // 0x123456 = 3 bytes + const expected = [_]u8{ 0x83, 0x12, 0x34, 0x56 }; + try std.testing.expectEqualSlices(u8, &expected, encoded); +} + +test "encode item union" { + const allocator = std.testing.allocator; + + var encoder = Encoder.init(allocator); + defer encoder.deinit(); + + try encoder.encode(.{ .bytes = "hello" }); + + const result = encoder.toSlice(); + const expected = [_]u8{ 0x85, 'h', 'e', 'l', 'l', 'o' }; + try std.testing.expectEqualSlices(u8, &expected, result); +} diff --git a/src/rlp/packed.zig b/src/rlp/packed.zig index e69de29..17750b5 100644 --- a/src/rlp/packed.zig +++ b/src/rlp/packed.zig @@ -0,0 +1,327 @@ +const std = @import("std"); +const encode = @import("./encode.zig"); +const decode = @import("./decode.zig"); +const Address = @import("../primitives/address.zig").Address; +const Hash = @import("../primitives/hash.zig").Hash; +const U256 = @import("../primitives/uint.zig").U256; +const Signature = @import("../primitives/signature.zig").Signature; +const Transaction = @import("../types/transaction.zig").Transaction; +const TransactionType = @import("../types/transaction.zig").TransactionType; + +/// Helper for encoding Ethereum transactions +pub const TransactionEncoder = struct { + /// Encode a legacy transaction for signing (without signature) + pub fn encodeLegacyForSigning( + allocator: std.mem.Allocator, + tx: Transaction, + ) ![]u8 { + if (tx.type != .legacy) { + return error.NotLegacyTransaction; + } + + var items = std.ArrayList(encode.RlpItem).init(allocator); + defer items.deinit(); + + // nonce + try items.append(.{ .uint = tx.nonce }); + + // gas_price + const gas_price_bytes = tx.gas_price.?.toBytes(); + try items.append(.{ .bytes = &gas_price_bytes }); + + // gas_limit + try items.append(.{ .uint = tx.gas_limit }); + + // to (or empty for contract creation) + if (tx.to) |to_addr| { + try items.append(.{ .bytes = &to_addr.bytes }); + } else { + try items.append(.{ .bytes = &[_]u8{} }); + } + + // value + const value_bytes = tx.value.toBytes(); + try items.append(.{ .bytes = &value_bytes }); + + // data + try items.append(.{ .bytes = tx.data.data }); + + // For EIP-155: v, r, s (chain_id, 0, 0) + if (tx.chain_id) |chain_id| { + try items.append(.{ .uint = chain_id }); + try items.append(.{ .uint = 0 }); + try items.append(.{ .uint = 0 }); + } + + return try encode.encodeList(allocator, items.items); + } + + /// Encode a signed legacy transaction + pub fn encodeLegacySigned( + allocator: std.mem.Allocator, + tx: Transaction, + ) ![]u8 { + if (tx.type != .legacy) { + return error.NotLegacyTransaction; + } + + if (tx.signature == null) { + return error.TransactionNotSigned; + } + + var items = std.ArrayList(encode.RlpItem).init(allocator); + defer items.deinit(); + + // nonce + try items.append(.{ .uint = tx.nonce }); + + // gas_price + const gas_price_bytes = tx.gas_price.?.toBytes(); + try items.append(.{ .bytes = &gas_price_bytes }); + + // gas_limit + try items.append(.{ .uint = tx.gas_limit }); + + // to + if (tx.to) |to_addr| { + try items.append(.{ .bytes = &to_addr.bytes }); + } else { + try items.append(.{ .bytes = &[_]u8{} }); + } + + // value + const value_bytes = tx.value.toBytes(); + try items.append(.{ .bytes = &value_bytes }); + + // data + try items.append(.{ .bytes = tx.data.data }); + + // Signature (v, r, s) + const sig = tx.signature.?; + try items.append(.{ .uint = sig.v }); + + const r_bytes = sig.r.toBytes(); + try items.append(.{ .bytes = &r_bytes }); + + const s_bytes = sig.s.toBytes(); + try items.append(.{ .bytes = &s_bytes }); + + return try encode.encodeList(allocator, items.items); + } +}; + +/// Helper for encoding common Ethereum data structures +pub const EthereumEncoder = struct { + /// Encode an address + pub fn encodeAddress(allocator: std.mem.Allocator, address: Address) ![]u8 { + return try encode.encodeBytes(allocator, &address.bytes); + } + + /// Encode a hash + pub fn encodeHash(allocator: std.mem.Allocator, hash: Hash) ![]u8 { + return try encode.encodeBytes(allocator, &hash.bytes); + } + + /// Encode a U256 + pub fn encodeU256(allocator: std.mem.Allocator, value: U256) ![]u8 { + const bytes = value.toBytes(); + + // Find first non-zero byte + var start: usize = 0; + while (start < 32 and bytes[start] == 0) : (start += 1) {} + + if (start == 32) { + // All zeros + return try encode.encodeBytes(allocator, &[_]u8{}); + } + + return try encode.encodeBytes(allocator, bytes[start..]); + } + + /// Encode an address list + pub fn encodeAddressList( + allocator: std.mem.Allocator, + addresses: []const Address, + ) ![]u8 { + var items = std.ArrayList(encode.RlpItem).init(allocator); + defer items.deinit(); + + for (addresses) |addr| { + try items.append(.{ .bytes = &addr.bytes }); + } + + return try encode.encodeList(allocator, items.items); + } + + /// Encode a hash list + pub fn encodeHashList( + allocator: std.mem.Allocator, + hashes: []const Hash, + ) ![]u8 { + var items = std.ArrayList(encode.RlpItem).init(allocator); + defer items.deinit(); + + for (hashes) |hash| { + try items.append(.{ .bytes = &hash.bytes }); + } + + return try encode.encodeList(allocator, items.items); + } +}; + +/// Helper for decoding common Ethereum data structures +pub const EthereumDecoder = struct { + /// Decode an address from RLP bytes + pub fn decodeAddress(data: []const u8) !Address { + if (data.len != 20) { + return error.InvalidAddressLength; + } + var bytes: [20]u8 = undefined; + @memcpy(&bytes, data); + return Address.fromBytes(bytes); + } + + /// Decode a hash from RLP bytes + pub fn decodeHash(data: []const u8) !Hash { + if (data.len != 32) { + return error.InvalidHashLength; + } + var bytes: [32]u8 = undefined; + @memcpy(&bytes, data); + return Hash.fromBytes(bytes); + } + + /// Decode a U256 from RLP bytes + pub fn decodeU256(data: []const u8) !U256 { + if (data.len == 0) { + return U256.zero(); + } + if (data.len > 32) { + return error.ValueTooLarge; + } + + // Pad to 32 bytes + var bytes: [32]u8 = [_]u8{0} ** 32; + const offset = 32 - data.len; + @memcpy(bytes[offset..], data); + + return U256.fromBytes(bytes); + } +}; + +test "encode address" { + const allocator = std.testing.allocator; + + const addr = Address.fromBytes([_]u8{0x42} ** 20); + const encoded = try EthereumEncoder.encodeAddress(allocator, addr); + defer allocator.free(encoded); + + // 20 bytes with prefix + try std.testing.expectEqual(@as(usize, 21), encoded.len); + try std.testing.expectEqual(@as(u8, 0x80 + 20), encoded[0]); +} + +test "encode hash" { + const allocator = std.testing.allocator; + + const hash = Hash.fromBytes([_]u8{0xab} ** 32); + const encoded = try EthereumEncoder.encodeHash(allocator, hash); + defer allocator.free(encoded); + + // 32 bytes with prefix + try std.testing.expectEqual(@as(usize, 33), encoded.len); + try std.testing.expectEqual(@as(u8, 0x80 + 32), encoded[0]); +} + +test "encode U256 zero" { + const allocator = std.testing.allocator; + + const value = U256.zero(); + const encoded = try EthereumEncoder.encodeU256(allocator, value); + defer allocator.free(encoded); + + // Empty bytes + try std.testing.expectEqualSlices(u8, &[_]u8{0x80}, encoded); +} + +test "encode U256 small" { + const allocator = std.testing.allocator; + + const value = U256.fromInt(42); + const encoded = try EthereumEncoder.encodeU256(allocator, value); + defer allocator.free(encoded); + + // Single byte + try std.testing.expectEqualSlices(u8, &[_]u8{0x2a}, encoded); +} + +test "decode address" { + const addr_bytes = [_]u8{0x12} ** 20; + const addr = try EthereumDecoder.decodeAddress(&addr_bytes); + + try std.testing.expectEqual(Address.fromBytes(addr_bytes), addr); +} + +test "decode hash" { + const hash_bytes = [_]u8{0xab} ** 32; + const hash = try EthereumDecoder.decodeHash(&hash_bytes); + + try std.testing.expectEqual(Hash.fromBytes(hash_bytes), hash); +} + +test "decode U256 zero" { + const value = try EthereumDecoder.decodeU256(&[_]u8{}); + try std.testing.expect(value.isZero()); +} + +test "decode U256 small" { + const value = try EthereumDecoder.decodeU256(&[_]u8{0x2a}); + try std.testing.expect(value.eql(U256.fromInt(42))); +} + +test "encode address list" { + const allocator = std.testing.allocator; + + const addresses = [_]Address{ + Address.fromBytes([_]u8{0x11} ** 20), + Address.fromBytes([_]u8{0x22} ** 20), + }; + + const encoded = try EthereumEncoder.encodeAddressList(allocator, &addresses); + defer allocator.free(encoded); + + // List prefix + 2 addresses + try std.testing.expect(encoded.len > 40); +} + +test "encode hash list" { + const allocator = std.testing.allocator; + + const hashes = [_]Hash{ + Hash.fromBytes([_]u8{0xaa} ** 32), + Hash.fromBytes([_]u8{0xbb} ** 32), + }; + + const encoded = try EthereumEncoder.encodeHashList(allocator, &hashes); + defer allocator.free(encoded); + + // List prefix + 2 hashes + try std.testing.expect(encoded.len > 64); +} + +test "roundtrip U256 encoding" { + const allocator = std.testing.allocator; + + const original = U256.fromInt(0x123456); + const encoded = try EthereumEncoder.encodeU256(allocator, original); + defer allocator.free(encoded); + + // Decode the RLP first + const rlp_value = try decode.decode(allocator, encoded); + defer rlp_value.deinit(allocator); + + const bytes = try rlp_value.getBytes(); + const decoded = try EthereumDecoder.decodeU256(bytes); + + try std.testing.expect(decoded.eql(original)); +} diff --git a/src/root.zig b/src/root.zig index 71f08a4..ac7f6b2 100644 --- a/src/root.zig +++ b/src/root.zig @@ -67,6 +67,24 @@ pub const abi = struct { pub const rlp = struct { pub const encode = @import("rlp/encode.zig"); pub const decode = @import("rlp/decode.zig"); + pub const ethereum = @import("rlp/packed.zig"); + + // Re-export commonly used types + pub const Encoder = encode.Encoder; + pub const Decoder = decode.Decoder; + pub const RlpItem = encode.RlpItem; + pub const RlpValue = decode.RlpValue; + pub const encodeItem = encode.encodeItem; + pub const encodeList = encode.encodeList; + pub const encodeBytes = encode.encodeBytes; + pub const encodeUint = encode.encodeUint; + pub const decodeValue = decode.decode; + pub const decodeBytes = decode.decodeBytes; + pub const decodeList = decode.decodeList; + pub const decodeUint = decode.decodeUint; + pub const TransactionEncoder = ethereum.TransactionEncoder; + pub const EthereumEncoder = ethereum.EthereumEncoder; + pub const EthereumDecoder = ethereum.EthereumDecoder; }; pub const providers = struct {