├── .all-contributorsrc ├── .github ├── CODEOWNERS └── workflows │ ├── check.yml │ └── docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── bitcoin.conf.example ├── build.zig ├── build.zig.zon ├── build_helpers.zig ├── docs ├── architecture.md ├── img │ └── btczee.png └── roadmap.md └── src ├── address └── address.zig ├── benchmarks.zig ├── config └── config.zig ├── core └── mempool.zig ├── lib.zig ├── main.zig ├── network ├── message │ └── utils.zig ├── network.zig ├── p2p.zig ├── peer.zig ├── protocol │ ├── lib.zig │ ├── messages │ │ ├── addr.zig │ │ ├── block.zig │ │ ├── cmpctblock.zig │ │ ├── feefilter.zig │ │ ├── filteradd.zig │ │ ├── filterclear.zig │ │ ├── filterload.zig │ │ ├── getaddr.zig │ │ ├── getblocks.zig │ │ ├── getblocktxn.zig │ │ ├── getdata.zig │ │ ├── headers.zig │ │ ├── inv.zig │ │ ├── lib.zig │ │ ├── mempool.zig │ │ ├── merkleblock.zig │ │ ├── notfound.zig │ │ ├── ping.zig │ │ ├── pong.zig │ │ ├── sendcmpct.zig │ │ ├── sendheaders.zig │ │ ├── verack.zig │ │ └── version.zig │ └── types │ │ ├── InventoryItem.zig │ │ └── NetworkAddress.zig ├── rpc.zig └── wire │ └── lib.zig ├── node ├── ibd.zig └── node.zig ├── primitives └── lib.zig ├── script ├── engine.zig ├── lib.zig ├── opcodes │ ├── arithmetic.zig │ └── constant.zig ├── scriptBuilder.zig └── stack.zig ├── storage └── storage.zig ├── types ├── block.zig ├── block_header.zig ├── hash.zig ├── input.zig ├── lib.zig ├── outpoint.zig ├── output.zig ├── script.zig └── transaction.zig ├── util ├── cmd │ └── ArgParser.zig ├── mem │ └── read.zig ├── net │ ├── echo.zig │ ├── lib.zig │ ├── net.zig │ ├── packet.zig │ ├── socket_utils.zig │ └── utils.zig ├── sync │ ├── mpmc.zig │ └── ref.zig └── time │ ├── estimate.zig │ ├── lib.zig │ └── time.zig └── vanitygen.zig /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "btczee", 3 | "projectOwner": "zig-bitcoin", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "gitmoji", 12 | "contributors": [ 13 | { 14 | "login": "AbdelStark", 15 | "name": "A₿del ∞/21M 🐺 - 🐱", 16 | "avatar_url": "https://avatars.githubusercontent.com/u/45264458?v=4", 17 | "profile": "https://github.com/AbdelStark", 18 | "contributions": [ 19 | "code", 20 | "ideas", 21 | "mentoring", 22 | "projectManagement", 23 | "research", 24 | "review" 25 | ] 26 | }, 27 | { 28 | "login": "lana-shanghai", 29 | "name": "lanaivina", 30 | "avatar_url": "https://avatars.githubusercontent.com/u/31368580?v=4", 31 | "profile": "https://github.com/lana-shanghai", 32 | "contributions": [ 33 | "code" 34 | ] 35 | }, 36 | { 37 | "login": "tdelabro", 38 | "name": "Timothée Delabrouille", 39 | "avatar_url": "https://avatars.githubusercontent.com/u/34384633?v=4", 40 | "profile": "https://github.com/tdelabro", 41 | "contributions": [ 42 | "code" 43 | ] 44 | }, 45 | { 46 | "login": "okhaimie-dev", 47 | "name": "okhai", 48 | "avatar_url": "https://avatars.githubusercontent.com/u/57156589?v=4", 49 | "profile": "https://okhaimie.com/", 50 | "contributions": [ 51 | "code" 52 | ] 53 | }, 54 | { 55 | "login": "supreme2580", 56 | "name": "Supreme Labs", 57 | "avatar_url": "https://avatars.githubusercontent.com/u/100731397?v=4", 58 | "profile": "https://github.com/supreme2580", 59 | "contributions": [ 60 | "code" 61 | ] 62 | }, 63 | { 64 | "login": "varun-doshi", 65 | "name": "Varun Doshi", 66 | "avatar_url": "https://avatars.githubusercontent.com/u/61531351?v=4", 67 | "profile": "https://varun-doshi.vercel.app/", 68 | "contributions": [ 69 | "code" 70 | ] 71 | } 72 | ], 73 | "contributorsPerLine": 7, 74 | "linkToUsage": true 75 | } 76 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @tdelabro 2 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Zig Test and Benchmark 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | ZIG_VERSION: 0.13.0 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: goto-bus-stop/setup-zig@v2 19 | with: 20 | version: ${{ env.ZIG_VERSION }} 21 | 22 | - name: Unit testing 23 | run: zig build test --summary all 24 | 25 | - name: Run benchmarks 26 | run: zig build bench -Doptimize=ReleaseFast 27 | 28 | - name: Run node 29 | run: | 30 | zig build -Doptimize=Debug run -- node & 31 | BITCOIN_NODE_PID=$! 32 | sleep 1 33 | kill $BITCOIN_NODE_PID 34 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate and Deploy Docs 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: write-all 8 | 9 | env: 10 | ZIG_VERSION: 0.13.0 11 | 12 | jobs: 13 | docs: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: goto-bus-stop/setup-zig@v2 19 | with: 20 | version: ${{ env.ZIG_VERSION }} 21 | - run: zig build docs 22 | 23 | - name: Deploy 24 | if: ${{ github.ref == 'refs/heads/main' }} && steps.check_changes.outcome == 'success' 25 | uses: peaceiris/actions-gh-pages@v3 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | publish_dir: zig-out/docs 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | profile.json 3 | **/.DS_Store 4 | 5 | .zig-cache 6 | zig-out 7 | 8 | .bitcoin 9 | .vscode 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 A₿del ∞/21M 🐺 - 🐱 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bitcoin.conf.example: -------------------------------------------------------------------------------- 1 | # Bitcoin configuration file 2 | 3 | # Network-related settings 4 | testnet=0 5 | port=8333 6 | rpcport=8332 7 | 8 | # Data directory 9 | datadir=.bitcoin -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const build_helpers = @import("build_helpers.zig"); 3 | const package_name = "btczee"; 4 | const package_path = "src/lib.zig"; 5 | 6 | // List of external dependencies that this package requires. 7 | const external_dependencies = [_]build_helpers.Dependency{ 8 | .{ 9 | .name = "clap", 10 | .module_name = "clap", 11 | }, 12 | .{ 13 | .name = "httpz", 14 | .module_name = "httpz", 15 | }, 16 | .{ 17 | .name = "lmdb", 18 | .module_name = "lmdb", 19 | }, 20 | .{ 21 | .name = "bitcoin-primitives", 22 | .module_name = "bitcoin-primitives", 23 | }, 24 | .{ 25 | .name = "libxev", 26 | .module_name = "xev", 27 | }, 28 | }; 29 | 30 | pub fn build(b: *std.Build) !void { 31 | // Standard target options allows the person running `zig build` to choose 32 | // what target to build for. Here we do not override the defaults, which 33 | // means any target is allowed, and the default is native. Other options 34 | // for restricting supported target set are available. 35 | const target = b.standardTargetOptions(.{}); 36 | 37 | // Standard optimization options allow the person running `zig build` to select 38 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 39 | // set a preferred release mode, allowing the user to decide how to optimize. 40 | const optimize = b.standardOptimizeOption(.{}); 41 | 42 | // ************************************************************** 43 | // * HANDLE DEPENDENCY MODULES * 44 | // ************************************************************** 45 | 46 | // This array can be passed to add the dependencies to lib, executable, tests, etc using `addModule` function. 47 | const deps = build_helpers.generateModuleDependencies( 48 | b, 49 | &external_dependencies, 50 | .{ 51 | .optimize = optimize, 52 | .target = target, 53 | }, 54 | ) catch unreachable; 55 | 56 | // ************************************************************** 57 | // * BTCZEE AS A MODULE * 58 | // ************************************************************** 59 | // expose btczee as a module 60 | _ = b.addModule(package_name, .{ 61 | .root_source_file = b.path(package_path), 62 | .imports = deps, 63 | }); 64 | 65 | // ************************************************************** 66 | // * BTCZEE AS A LIBRARY * 67 | // ************************************************************** 68 | const lib = b.addStaticLibrary(.{ 69 | .name = "btczee", 70 | .root_source_file = b.path("src/lib.zig"), 71 | .target = target, 72 | .optimize = optimize, 73 | }); 74 | // Add dependency modules to the library. 75 | for (deps) |mod| lib.root_module.addImport( 76 | mod.name, 77 | mod.module, 78 | ); 79 | // This declares intent for the library to be installed into the standard 80 | // location when the user invokes the "install" step (the default step when 81 | // running `zig build`). 82 | b.installArtifact(lib); 83 | 84 | // ************************************************************** 85 | // * VANITYGEN AS AN EXECUTABLE * 86 | // ************************************************************** 87 | { 88 | const exe = b.addExecutable(.{ 89 | .name = "vanitygen", 90 | .root_source_file = b.path("src/vanitygen.zig"), 91 | .target = target, 92 | .optimize = optimize, 93 | .single_threaded = false, 94 | .omit_frame_pointer = true, 95 | .strip = false, 96 | }); 97 | // Add dependency modules to the executable. 98 | for (deps) |mod| exe.root_module.addImport( 99 | mod.name, 100 | mod.module, 101 | ); 102 | 103 | exe.root_module.addImport("btczee", &lib.root_module); 104 | 105 | b.installArtifact(exe); 106 | 107 | const run_cmd = b.addRunArtifact(exe); 108 | run_cmd.step.dependOn(b.getInstallStep()); 109 | 110 | if (b.args) |args| { 111 | run_cmd.addArgs(args); 112 | } 113 | 114 | const run_step = b.step("vanitygen-run", "Run the app"); 115 | run_step.dependOn(&run_cmd.step); 116 | } 117 | 118 | // ************************************************************** 119 | // * BTCZEE AS AN EXECUTABLE * 120 | // ************************************************************** 121 | { 122 | const exe = b.addExecutable(.{ 123 | .name = "btczee", 124 | .root_source_file = b.path("src/main.zig"), 125 | .target = target, 126 | .optimize = optimize, 127 | }); 128 | // Add dependency modules to the executable. 129 | for (deps) |mod| exe.root_module.addImport( 130 | mod.name, 131 | mod.module, 132 | ); 133 | 134 | b.installArtifact(exe); 135 | 136 | const run_cmd = b.addRunArtifact(exe); 137 | run_cmd.step.dependOn(b.getInstallStep()); 138 | 139 | if (b.args) |args| { 140 | run_cmd.addArgs(args); 141 | } 142 | 143 | const run_step = b.step("run", "Run the app"); 144 | run_step.dependOn(&run_cmd.step); 145 | } 146 | 147 | // ************************************************************** 148 | // * CHECK FOR FAST FEEDBACK LOOP * 149 | // ************************************************************** 150 | // Tip taken from: `https://kristoff.it/blog/improving-your-zls-experience/` 151 | { 152 | const exe_check = b.addExecutable(.{ 153 | .name = "btczee", 154 | .root_source_file = b.path("src/main.zig"), 155 | .target = target, 156 | .optimize = optimize, 157 | }); 158 | // Add dependency modules to the executable. 159 | for (deps) |mod| exe_check.root_module.addImport( 160 | mod.name, 161 | mod.module, 162 | ); 163 | 164 | const check_test = b.addTest(.{ 165 | .root_source_file = b.path("src/lib.zig"), 166 | .target = target, 167 | }); 168 | 169 | // This step is used to check if btczee compiles, it helps to provide a faster feedback loop when developing. 170 | const check = b.step("check", "Check if btczee compiles"); 171 | check.dependOn(&exe_check.step); 172 | check.dependOn(&check_test.step); 173 | } 174 | 175 | // ************************************************************** 176 | // * UNIT TESTS * 177 | // ************************************************************** 178 | 179 | const lib_unit_tests = b.addTest(.{ 180 | .root_source_file = b.path("src/lib.zig"), 181 | .target = target, 182 | }); 183 | 184 | // Add dependency modules to the library. 185 | for (deps) |mod| lib_unit_tests.root_module.addImport( 186 | mod.name, 187 | mod.module, 188 | ); 189 | 190 | const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); 191 | 192 | const exe_unit_tests = b.addTest(.{ 193 | .root_source_file = b.path("src/main.zig"), 194 | .target = target, 195 | }); 196 | 197 | const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); 198 | 199 | const test_step = b.step("test", "Run unit tests"); 200 | test_step.dependOn(&run_lib_unit_tests.step); 201 | test_step.dependOn(&run_exe_unit_tests.step); 202 | 203 | // ************************************************************** 204 | // * BENCHMARKS * 205 | // ************************************************************** 206 | 207 | // Add benchmark step 208 | const bench = b.addExecutable(.{ 209 | .name = "benchmark", 210 | .root_source_file = b.path("src/benchmarks.zig"), 211 | .target = target, 212 | .optimize = .ReleaseFast, 213 | }); 214 | 215 | bench.root_module.addImport("zul", b.dependency("zul", .{}).module("zul")); 216 | 217 | const run_bench = b.addRunArtifact(bench); 218 | 219 | // Add option for report generation 220 | const report_option = b.option(bool, "report", "Generate benchmark report (default: false)") orelse false; 221 | 222 | // Pass the report option to the benchmark executable 223 | if (report_option) { 224 | run_bench.addArg("--report"); 225 | } 226 | 227 | // Pass any additional arguments to the benchmark executable 228 | if (b.args) |args| { 229 | run_bench.addArgs(args); 230 | } 231 | 232 | const bench_step = b.step("bench", "Run benchmarks"); 233 | bench_step.dependOn(&run_bench.step); 234 | 235 | // ************************************************************** 236 | // * DOCUMENTATION * 237 | // ************************************************************** 238 | // Add documentation generation step 239 | const install_docs = b.addInstallDirectory(.{ 240 | .source_dir = lib.getEmittedDocs(), 241 | .install_dir = .prefix, 242 | .install_subdir = "docs", 243 | }); 244 | 245 | const docs_step = b.step("docs", "Generate documentation"); 246 | docs_step.dependOn(&install_docs.step); 247 | } 248 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "btczee", 3 | .version = "0.0.1", 4 | .dependencies = .{ 5 | .zul = .{ 6 | .url = "https://github.com/karlseguin/zul/archive/ae0c27350c0db6b460f22cba30b6b0c4a02d1ffd.zip", 7 | .hash = "1220457e2c8867f6734520d9b335f01e1d851d6fe7adaa7f6f0756158acaf6c5e87f", 8 | }, 9 | .lmdb = .{ 10 | .url = "git+https://github.com/zig-bitcoin/zig-lmdb#eb7436d091464131551759b0f80d4f1d1a15ece1", 11 | .hash = "1220f9e1eb744c8dc2750c1e6e1ceb1c2d521bedb161ddead1a6bb772032e576d74a", 12 | }, 13 | .@"bitcoin-primitives" = .{ 14 | .url = "git+https://github.com/zig-bitcoin/bitcoin-primitives#6595846b34c8c157175c52380f5c7cc6fc9ca108", 15 | .hash = "12208a138853cd57db1c5e3348d60a74aa54d5c0a63393b6367098f1c150a0c31438", 16 | }, 17 | .clap = .{ 18 | .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.9.1.tar.gz", 19 | .hash = "122062d301a203d003547b414237229b09a7980095061697349f8bef41be9c30266b", 20 | }, 21 | .libxev = .{ 22 | .url = "https://github.com/mitchellh/libxev/archive/main.tar.gz", 23 | .hash = "1220612bc023c21d75234882ec9a8c6a1cbd9d642da3dfb899297f14bb5bd7b6cd78", 24 | }, 25 | .httpz = .{ 26 | .url = "https://github.com/karlseguin/http.zig/archive/zig-0.13.tar.gz", 27 | .hash = "12208c1f2c5f730c4c03aabeb0632ade7e21914af03e6510311b449458198d0835d6", 28 | }, 29 | }, 30 | .paths = .{ 31 | "build.zig", 32 | "build.zig.zon", 33 | "src", 34 | "LICENSE", 35 | "README.md", 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /build_helpers.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Represents a dependency on an external package. 4 | pub const Dependency = struct { 5 | name: []const u8, 6 | module_name: []const u8, 7 | }; 8 | 9 | /// Generate an array of Build.Module.Import from the external dependencies. 10 | /// # Arguments 11 | /// * `b` - The build object. 12 | /// * `external_dependencies` - The external dependencies. 13 | /// * `dependencies_opts` - The options to use when generating the dependency modules. 14 | /// # Returns 15 | /// A new array of Build.Module.Import. 16 | pub fn generateModuleDependencies( 17 | b: *std.Build, 18 | external_dependencies: []const Dependency, 19 | dependencies_opts: anytype, 20 | ) ![]std.Build.Module.Import { 21 | var dependency_modules = std.ArrayList(*std.Build.Module).init(b.allocator); 22 | defer _ = dependency_modules.deinit(); 23 | 24 | // Populate dependency modules. 25 | for (external_dependencies) |dep| { 26 | const module = b.dependency( 27 | dep.name, 28 | dependencies_opts, 29 | ).module(dep.module_name); 30 | _ = dependency_modules.append(module) catch unreachable; 31 | } 32 | return try toModuleDependencyArray( 33 | b.allocator, 34 | dependency_modules.items, 35 | external_dependencies, 36 | ); 37 | } 38 | 39 | /// Convert an array of Build.Module pointers to an array of Build.Module.Import. 40 | /// # Arguments 41 | /// * `allocator` - The allocator to use for the new array. 42 | /// * `modules` - The array of Build.Module pointers to convert. 43 | /// * `ext_deps` - The array of external dependencies. 44 | /// # Returns 45 | /// A new array of Build.Module.Import. 46 | fn toModuleDependencyArray( 47 | allocator: std.mem.Allocator, 48 | modules: []const *std.Build.Module, 49 | ext_deps: []const Dependency, 50 | ) ![]std.Build.Module.Import { 51 | var deps = std.ArrayList(std.Build.Module.Import).init(allocator); 52 | defer deps.deinit(); 53 | 54 | for ( 55 | modules, 56 | 0.., 57 | ) |module_ptr, i| { 58 | try deps.append(.{ 59 | .name = ext_deps[i].name, 60 | .module = module_ptr, 61 | }); 62 | } 63 | 64 | return deps.toOwnedSlice(); 65 | } 66 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # btczee Architecture 2 | 3 | This document describes the high-level architecture of btczee, a Bitcoin implementation in Zig. The project is structured into several key components, each responsible for specific functionality within the Bitcoin protocol. 4 | 5 | ## Components 6 | 7 | ### 1. Node 8 | 9 | **Location**: `src/node/node.zig` 10 | 11 | The Node component is the central coordination point for the Bitcoin node. It initializes and manages other components, handles the node's lifecycle, and coordinates communication between different parts of the system. 12 | 13 | ### 2. Network 14 | 15 | **Location**: `src/network/` 16 | 17 | The Network component handles all peer-to-peer communication within the Bitcoin network, as well as the RPC interface for interacting with the node. It's divided into several subcomponents: 18 | 19 | - `p2p.zig`: Manages the P2P network layer 20 | - `peer.zig`: Handles individual peer connections 21 | - `protocol.zig`: Implements the Bitcoin network protocol 22 | - `rpc.zig`: Provides an RPC interface for interacting with the node 23 | 24 | ### 3. Mempool 25 | 26 | **Location**: `src/core/mempool.zig` 27 | 28 | The Mempool component manages the node's memory pool of unconfirmed transactions. It handles transaction validation, fee estimation, and transaction selection for block creation (if mining is enabled). 29 | 30 | ### 4. Wallet 31 | 32 | **Location**: `src/wallet/wallet.zig` 33 | 34 | The Wallet component manages the user's Bitcoin wallet, including key management, transaction creation and signing, and balance tracking. 35 | 36 | ### 5. Storage 37 | 38 | **Location**: `src/storage/storage.zig` 39 | 40 | The Storage component handles persistent storage of blockchain data, including blocks, transactions, and the UTXO set. 41 | 42 | ### 6. Miner 43 | 44 | **Location**: `src/miner/miner.zig` 45 | 46 | The Miner component implements Bitcoin mining functionality, including block template creation and Proof-of-Work calculation. 47 | 48 | ### 7. Types 49 | 50 | **Location**: `src/types/` 51 | 52 | The Types component defines core Bitcoin data structures: 53 | 54 | - `block.zig`: Defines the Block structure 55 | - `transaction.zig`: Defines the Transaction structure 56 | 57 | ### 8. Primitives 58 | 59 | **Location**: `src/primitives/lib.zig` 60 | 61 | The Primitives component provides low-level Bitcoin primitives and utilities used throughout the codebase. 62 | 63 | ### 9. Config 64 | 65 | **Location**: `src/config/config.zig` 66 | 67 | The Config component manages node configuration and settings. 68 | 69 | ## Component Interactions 70 | 71 | The following diagram illustrates the high-level interactions between components: 72 | 73 | ```mermaid 74 | graph TD 75 | Node[Node] --> Network 76 | Node --> Mempool 77 | Node --> Wallet 78 | Node --> Storage 79 | Node --> Miner 80 | 81 | Network --> Mempool 82 | Network --> Storage 83 | 84 | Mempool --> Wallet 85 | Mempool --> Storage 86 | 87 | Wallet --> Storage 88 | 89 | Miner --> Mempool 90 | Miner --> Storage 91 | 92 | subgraph "Core Components" 93 | Node 94 | Network 95 | Mempool 96 | Wallet 97 | Storage 98 | Miner 99 | end 100 | 101 | subgraph "Supporting Components" 102 | Types 103 | Primitives 104 | Config 105 | end 106 | 107 | Node -.-> Types 108 | Node -.-> Primitives 109 | Node -.-> Config 110 | 111 | classDef core fill:#f9f,stroke:#333,stroke-width:2px; 112 | classDef support fill:#bbf,stroke:#333,stroke-width:1px; 113 | class Node,Network,Mempool,Wallet,Storage,Miner core; 114 | class Types,Primitives,Config support; 115 | ``` 116 | 117 | ## Interaction Descriptions 118 | 119 | 1. **Node and Network**: The Node initializes the Network component, which handles all incoming and outgoing network communication. The Network component notifies the Node of new blocks and transactions. 120 | 2. **Node and Mempool**: The Node uses the Mempool to validate and store unconfirmed transactions. The Mempool notifies the Node of changes in the transaction pool. 121 | 3. **Node and Wallet**: The Node interacts with the Wallet for transaction creation and signing. The Wallet notifies the Node of new transactions that need to be broadcast. 122 | 4. **Node and Storage**: The Node uses the Storage component to persist and retrieve blockchain data, including blocks and the UTXO set. 123 | 5. **Node and Miner**: The Node coordinates with the Miner for block creation and Proof-of-Work calculation. 124 | 6. **Network and Mempool**: The Network component passes new transactions to the Mempool for validation and storage. 125 | 7. **Network and Storage**: The Network component retrieves block and transaction data from Storage for peer requests. 126 | 8. **Mempool and Wallet**: The Mempool interacts with the Wallet to validate transactions and check for double-spends. 127 | Mempool and Storage: The Mempool uses Storage to persist the mempool state and check against the UTXO set. 128 | 9. **Wallet and Storage**: The Wallet uses Storage to persist wallet data and retrieve historical transaction information. 129 | 10. **Miner and Mempool**: The Miner requests transactions from the Mempool for block creation. 130 | 11. **Miner and Storage**: The Miner interacts with Storage to retrieve necessary data for block creation and to store newly mined blocks. 131 | -------------------------------------------------------------------------------- /docs/img/btczee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zig-bitcoin/btczee/0c66e2c0b4051b6cb9bb0de8bb1b13964111b18c/docs/img/btczee.png -------------------------------------------------------------------------------- /docs/roadmap.md: -------------------------------------------------------------------------------- 1 | ## Roadmap [in progress] 2 | 3 | ### Cryptographic primitives 4 | 5 | - [ ] SHA-256 6 | - [ ] RIPEMD160 7 | - [ ] ECDSA 8 | 9 | ### Primitives (data structures) 10 | 11 | - [ ] Transaction structure 12 | - [ ] Block structure 13 | - [ ] Merkle tree 14 | 15 | ### Networking 16 | 17 | - [ ] Peer-to-peer network implementation 18 | - [ ] Message handling and serialization 19 | 20 | ### Transaction 21 | 22 | - [ ] Transaction checks: CoinBase, duplicate inputs, overflows 23 | - [ ] Transaction validation 24 | 25 | ### Chain 26 | 27 | - [ ] Block validation 28 | - [ ] Chain management 29 | - [ ] UTXO set 30 | 31 | ### Consensus 32 | 33 | - [ ] Parameters: genesis hash, BIP block numbers, difficulty 34 | - [ ] Proof of work algorithm 35 | - [ ] Difficulty adjustment 36 | 37 | ### Wallet 38 | 39 | - [ ] Address generation 40 | - [ ] Transaction creation and signing 41 | 42 | ### Miner 43 | 44 | - [ ] Block creation 45 | - [ ] Mining algorithm and rewards 46 | 47 | ### Bitcoin Script 48 | 49 | - [ ] Bitcoin Script interpreter 50 | 51 | ### Mempool 52 | 53 | - [ ] Transaction pool management 54 | 55 | ### RPC 56 | 57 | - [ ] API calls to the node 58 | -------------------------------------------------------------------------------- /src/address/address.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const bitcoin_primitives = @import("bitcoin-primitives"); 3 | 4 | const NetworkKind = @import("../network/network.zig").NetworkKind; 5 | const Sha256 = std.crypto.hash.sha2.Sha256; 6 | const Hash160 = bitcoin_primitives.hashes.Hash160; 7 | 8 | /// The different types of addresses. 9 | pub const AddressType = enum { 10 | /// Pay to pubkey hash. 11 | p2pkh, 12 | /// Pay to script hash. 13 | p2sh, 14 | /// Pay to witness pubkey hash. 15 | p2wpkh, 16 | /// Pay to witness script hash. 17 | p2wsh, 18 | /// Pay to taproot. 19 | p2tr, 20 | }; 21 | 22 | // TODO move to crypto 23 | /// A hash of a public key. 24 | pub const PubkeyHash = struct { 25 | // hash160 26 | inner: [Hash160.digest_length]u8, 27 | }; 28 | 29 | /// SegWit version of a public key hash. 30 | pub const WpubkeyHash = struct { 31 | // hash160 32 | inner: [Hash160.digest_length]u8, 33 | }; 34 | 35 | /// A hash of Bitcoin Script bytecode. 36 | pub const ScriptHash = struct { 37 | // hash160 38 | inner: [Hash160.digest_length]u8, 39 | }; 40 | /// SegWit version of a Bitcoin Script bytecode hash. 41 | pub const WScriptHash = struct { 42 | // sha256 hash 43 | inner: [Sha256.digest_length]u8, 44 | }; 45 | 46 | /// Known bech32 human-readable parts. 47 | /// 48 | /// This is the human-readable part before the separator (`1`) in a bech32 encoded address e.g., 49 | /// the "bc" in "bc1p2wsldez5mud2yam29q22wgfh9439spgduvct83k3pm50fcxa5dps59h4z5". 50 | pub const KnownHrp = enum { 51 | /// The main Bitcoin network. 52 | mainnet, 53 | /// The test networks, testnet and signet. 54 | testnets, 55 | /// The regtest network. 56 | regtest, 57 | }; 58 | 59 | // TODO move blockdata constants 60 | /// Mainnet (bitcoin) pubkey address prefix. 61 | pub const pubkey_address_prefix_main: u8 = 0; // 0x00 62 | /// Test (tesnet, signet, regtest) pubkey address prefix. 63 | pub const pubkey_address_prefix_test: u8 = 111; // 0x6f 64 | 65 | pub const Address = union(enum) { 66 | p2pkh: struct { hash: PubkeyHash, network: NetworkKind }, 67 | p2sh: struct { hash: ScriptHash, network: NetworkKind }, 68 | // TODO WitnessProgram 69 | // segwit: struct { program: WitnessProgram, hrp: KnownHrp }, 70 | 71 | /// inint p2pkh address 72 | pub fn initP2pkh(hash: PubkeyHash, network: NetworkKind) Address { 73 | return .{ 74 | .p2pkh = .{ 75 | .hash = hash, 76 | .network = network, 77 | }, 78 | }; 79 | } 80 | 81 | // TODO make other types of address 82 | /// Encoding address to string 83 | /// caller responsible to free data 84 | pub fn toString(self: Address) !std.BoundedArray(u8, 50) { 85 | var buf: [50]u8 = undefined; 86 | switch (self) { 87 | .p2pkh => |addr| { 88 | const prefixed: [21]u8 = [1]u8{switch (addr.network) { 89 | .main => pubkey_address_prefix_main, 90 | .@"test" => pubkey_address_prefix_test, 91 | }} ++ addr.hash.inner; 92 | 93 | var encoder = bitcoin_primitives.base58.Encoder{}; 94 | 95 | var res = try std.BoundedArray(u8, 50).init(0); 96 | 97 | res.resize(encoder.encodeCheck(&res.buffer, &buf, &prefixed)) catch unreachable; 98 | 99 | return res; 100 | }, 101 | // TODO: implement another types of address 102 | else => unreachable, 103 | } 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /src/benchmarks.zig: -------------------------------------------------------------------------------- 1 | //! Benchmark module handles the benchmarking of btczee. 2 | //! Currently, it is using the zul library to run the benchmarks. 3 | //! The benchmarks can be run with: 4 | //! ``` 5 | //! zig build bench 6 | //! ``` 7 | const std = @import("std"); 8 | 9 | const zul = @import("zul"); 10 | 11 | pub fn main() !void { 12 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 13 | defer _ = gpa.deinit(); 14 | const allocator = gpa.allocator(); 15 | 16 | const args = try std.process.argsAlloc(allocator); 17 | defer std.process.argsFree(allocator, args); 18 | 19 | try benchmarkZul(allocator); 20 | } 21 | 22 | const Context = struct {}; 23 | 24 | fn bech32(_: Context, _: std.mem.Allocator, _: *std.time.Timer) !void {} 25 | 26 | fn benchmarkZul(_: std.mem.Allocator) !void { 27 | const ctx = Context{}; 28 | 29 | (try zul.benchmark.runC(ctx, bech32, .{})).print("bech32"); 30 | } 31 | -------------------------------------------------------------------------------- /src/config/config.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const DnsSeed = struct { inner: [:0]const u8 }; 4 | 5 | /// Global configuration for the node 6 | /// 7 | /// This is loaded from the `bitcoin.conf` file 8 | /// Must be loaded before any other modules are used. 9 | /// Must be compatible with Bitcoin Core's `bitcoin.conf` format. 10 | pub const Config = struct { 11 | const Self = @This(); 12 | /// Protocol version 13 | pub const PROTOCOL_VERSION: i32 = 70015; 14 | 15 | /// Known network ids 16 | pub const BitcoinNetworkId = struct { 17 | pub const MAINNET: [4]u8 = .{ 0xf9, 0xbe, 0xb4, 0xd9 }; 18 | pub const REGTEST: [4]u8 = .{ 0xfa, 0xbf, 0xd5, 0xda }; 19 | pub const TESTNET3: [4]u8 = .{ 0x0b, 0x11, 0x09, 0x07 }; 20 | pub const SIGNET: [4]u8 = .{ 0x0a, 0x03, 0xcf, 0x40 }; 21 | }; 22 | 23 | const DNS_SEEDS = [1]DnsSeed{ 24 | .{ .inner = "seed.bitcoin.sipa.be" }, 25 | // Those are two other seeds that we will keep here for later. 26 | // We are still building and I don't want to spam the whole network everytime I reboot. 27 | // "seed.bitcoin.sprovoost.nl", 28 | // "seed.btc.petertodd.net", 29 | }; 30 | 31 | allocator: std.mem.Allocator, 32 | /// RPC port 33 | rpc_port: u16 = 8332, 34 | /// P2P port 35 | p2p_port: u16 = 8333, 36 | /// Data directory 37 | datadir: [:0]const u8 = ".bitcoin", 38 | /// Services supported 39 | services: u64 = 0, 40 | /// Protocol version supported 41 | protocol_version: i32 = PROTOCOL_VERSION, 42 | /// Network Id 43 | network_id: [4]u8 = BitcoinNetworkId.MAINNET, 44 | 45 | /// Load the configuration from a file 46 | /// 47 | /// # Arguments 48 | /// - `allocator`: Memory allocator 49 | /// - `filename`: Path to the configuration file 50 | /// 51 | /// # Returns 52 | /// - `Config`: Configuration 53 | /// # Errors 54 | /// - Failed to read the file 55 | /// - Failed to parse the file 56 | pub fn load(allocator: std.mem.Allocator, filename: []const u8) !Config { 57 | const file = try std.fs.cwd().openFile(filename, .{}); 58 | defer file.close(); 59 | 60 | var buf_reader = std.io.bufferedReader(file.reader()); 61 | var in_stream = buf_reader.reader(); 62 | 63 | var config = Config{ 64 | .allocator = allocator, 65 | }; 66 | 67 | var buf: [1024]u8 = undefined; 68 | while (try in_stream.readUntilDelimiterOrEof(&buf, '\n')) |line| { 69 | var it = std.mem.splitSequence(u8, line, "="); 70 | const key = it.next() orelse continue; 71 | const value = it.next() orelse continue; 72 | 73 | if (std.mem.eql(u8, key, "rpcport")) { 74 | config.rpc_port = try std.fmt.parseInt(u16, value, 10); 75 | } else if (std.mem.eql(u8, key, "port")) { 76 | config.p2p_port = try std.fmt.parseInt(u16, value, 10); 77 | } else if (std.mem.eql(u8, key, "network")) { 78 | if (std.mem.eql(u8, value, &BitcoinNetworkId.MAINNET)) { 79 | config.network_id = BitcoinNetworkId.MAINNET; 80 | } else if (std.mem.eql(u8, value, &BitcoinNetworkId.REGTEST)) { 81 | config.network_id = BitcoinNetworkId.REGTEST; 82 | } else if (std.mem.eql(u8, value, &BitcoinNetworkId.TESTNET3)) { 83 | config.network_id = BitcoinNetworkId.TESTNET3; 84 | } else if (std.mem.eql(u8, value, &BitcoinNetworkId.SIGNET)) { 85 | config.network_id = BitcoinNetworkId.SIGNET; 86 | } else { 87 | return error.UnknownNetworkId; 88 | } 89 | } else if (std.mem.eql(u8, key, "datadir")) { 90 | config.datadir = try allocator.dupeZ(u8, value); 91 | } else if (std.mem.eql(u8, key, "services")) { 92 | config.services = try std.fmt.parseInt(u64, value, 10); 93 | } else if (std.mem.eql(u8, key, "protocol")) { 94 | config.protocol_version = try std.fmt.parseInt(i32, value, 10); 95 | } 96 | } 97 | 98 | return config; 99 | } 100 | 101 | pub fn dnsSeeds(self: *const Self) [1]DnsSeed { 102 | _ = self; 103 | return DNS_SEEDS; 104 | } 105 | 106 | pub fn bestBlock(self: *const Self) i32 { 107 | _ = self; 108 | // Should probably read it from db in the future 109 | return 0; 110 | } 111 | 112 | pub fn deinit(self: *Self) void { 113 | self.allocator.free(self.datadir); 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /src/core/mempool.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Config = @import("../config/config.zig").Config; 3 | const tx = @import("../types/lib.zig"); 4 | 5 | /// Transaction descriptor containing a transaction in the mempool along with additional metadata. 6 | const TxDesc = struct { 7 | tx: *tx.Transaction, 8 | added_time: i64, 9 | height: i32, 10 | fee: i64, 11 | fee_per_kb: i64, 12 | starting_priority: f64, 13 | }; 14 | 15 | /// Mempool for validating and storing standalone transactions until they are mined into a block. 16 | pub const Mempool = struct { 17 | allocator: std.mem.Allocator, 18 | config: *const Config, 19 | pool: std.AutoHashMap(tx.Hash, *TxDesc), 20 | orphans: std.AutoHashMap(tx.Hash, *tx.Transaction), 21 | orphans_by_prev: std.AutoHashMap(tx.OutPoint, std.AutoHashMap(tx.Hash, *tx.Transaction)), 22 | outpoints: std.AutoHashMap(tx.OutPoint, *tx.Transaction), 23 | last_updated: i64, 24 | 25 | /// Initialize the mempool 26 | /// 27 | /// # Arguments 28 | /// - `allocator`: Memory allocator 29 | /// - `config`: Configuration 30 | /// 31 | /// # Returns 32 | /// - `Mempool`: Initialized mempool 33 | pub fn init(allocator: std.mem.Allocator, config: *const Config) !Mempool { 34 | return Mempool{ 35 | .allocator = allocator, 36 | .config = config, 37 | .pool = std.AutoHashMap(tx.Hash, *TxDesc).init(allocator), 38 | .orphans = std.AutoHashMap(tx.Hash, *tx.Transaction).init(allocator), 39 | .orphans_by_prev = std.AutoHashMap(tx.OutPoint, std.AutoHashMap(tx.Hash, *tx.Transaction)).init(allocator), 40 | .outpoints = std.AutoHashMap(tx.OutPoint, *tx.Transaction).init(allocator), 41 | .last_updated = 0, 42 | }; 43 | } 44 | 45 | /// Deinitialize the mempool 46 | pub fn deinit(self: *Mempool) void { 47 | self.pool.deinit(); 48 | self.orphans.deinit(); 49 | self.orphans_by_prev.deinit(); 50 | self.outpoints.deinit(); 51 | } 52 | 53 | /// Add a transaction to the mempool 54 | /// 55 | /// # Arguments 56 | /// - `transaction`: Transaction to add 57 | /// - `height`: Current blockchain height 58 | /// - `fee`: Transaction fee 59 | /// 60 | /// # Returns 61 | /// - `?*TxDesc`: Added transaction descriptor or null if not added 62 | pub fn addTransaction(self: *Mempool, transaction: *tx.Transaction, height: i32, fee: i64) !?*TxDesc { 63 | const hash = transaction.hash(); 64 | 65 | // Check if the transaction is already in the pool 66 | if (self.pool.contains(hash)) { 67 | return null; 68 | } 69 | 70 | // Create a new transaction descriptor 71 | const tx_desc = try self.allocator.create(TxDesc); 72 | tx_desc.* = TxDesc{ 73 | .tx = transaction, 74 | .added_time = std.time.milliTimestamp(), 75 | .height = height, 76 | .fee = fee, 77 | .fee_per_kb = @divTrunc(fee * 1000, @as(i64, @intCast(transaction.hintEncodedLen()))), 78 | .starting_priority = try self.calculatePriority(transaction, height), 79 | }; 80 | 81 | // Add the transaction to the pool 82 | try self.pool.put(hash, tx_desc); 83 | 84 | // Add the transaction outpoints to the outpoints map 85 | for (transaction.inputs) |input| { 86 | try self.outpoints.put(input.previous_outpoint, transaction); 87 | } 88 | 89 | // Update the last updated timestamp 90 | self.last_updated = std.time.milliTimestamp(); 91 | 92 | return tx_desc; 93 | } 94 | 95 | /// Remove a transaction from the mempool 96 | /// 97 | /// # Arguments 98 | /// - `hash`: Hash of the transaction to remove 99 | /// - `remove_redeemers`: Whether to remove transactions that redeem outputs of this transaction 100 | pub fn removeTransaction(self: *Mempool, hash: tx.Hash, remove_redeemers: bool) void { 101 | const tx_desc = self.pool.get(hash) orelse return; 102 | 103 | if (remove_redeemers) { 104 | // Remove any transactions which rely on this one 105 | for (tx_desc.tx.outputs, 0..) |_, i| { 106 | const outpoint = tx.OutPoint{ .hash = hash, .index = @as(u32, @intCast(i)) }; 107 | if (self.outpoints.get(outpoint)) |redeemer| { 108 | self.removeTransaction(redeemer.hash(), true); 109 | } 110 | } 111 | } 112 | 113 | // Remove the transaction from the pool 114 | _ = self.pool.remove(hash); 115 | 116 | // Remove the outpoints from the outpoints map 117 | for (tx_desc.tx.inputs) |input| { 118 | _ = self.outpoints.remove(input.previous_outpoint); 119 | } 120 | 121 | // Update the last updated timestamp 122 | self.last_updated = std.time.milliTimestamp(); 123 | 124 | // Free the transaction descriptor 125 | self.allocator.destroy(tx_desc); 126 | } 127 | 128 | /// Calculate the priority of a transaction 129 | /// 130 | /// # Arguments 131 | /// - `transaction`: Transaction to calculate priority for 132 | /// - `height`: Current blockchain height 133 | /// 134 | /// # Returns 135 | /// - `f64`: Calculated priority 136 | fn calculatePriority(self: *Mempool, transaction: *tx.Transaction, height: i32) !f64 { 137 | _ = self; 138 | var priority: f64 = 0; 139 | for (transaction.inputs) |input| { 140 | // TODO: Fetch the UTXO from the chain 141 | _ = input; 142 | const utxo = .{ .value = 1000, .height = 100 }; 143 | const input_value = utxo.value; 144 | const input_age = @as(f64, @floatFromInt(height - utxo.height)); 145 | priority += @as(f64, @floatFromInt(input_value)) * input_age; 146 | } 147 | 148 | priority /= @as(f64, @floatFromInt(transaction.hintEncodedLen())); 149 | 150 | return priority; 151 | } 152 | 153 | /// Check if a transaction is in the mempool 154 | /// 155 | /// # Arguments 156 | /// - `hash`: Hash of the transaction to check 157 | /// 158 | /// # Returns 159 | /// - `bool`: True if the transaction is in the mempool, false otherwise 160 | pub fn containsTransaction(self: *const Mempool, hash: tx.Hash) bool { 161 | return self.pool.contains(hash); 162 | } 163 | 164 | /// Get the number of transactions in the mempool 165 | /// 166 | /// # Returns 167 | /// - `usize`: Number of transactions in the mempool 168 | pub fn count(self: *const Mempool) usize { 169 | return self.pool.count(); 170 | } 171 | 172 | /// Get the last time the mempool was updated 173 | /// 174 | /// # Returns 175 | /// - `i64`: Last update time in milliseconds 176 | pub fn lastUpdated(self: *const Mempool) i64 { 177 | return self.last_updated; 178 | } 179 | }; 180 | 181 | test "Mempool" { 182 | const testing = std.testing; 183 | const allocator = testing.allocator; 184 | 185 | var config = Config{ 186 | .allocator = allocator, 187 | .rpc_port = 8332, 188 | .p2p_port = 8333, 189 | .protocol_version = Config.PROTOCOL_VERSION, 190 | .network_id = Config.BitcoinNetworkId.MAINNET, 191 | .services = 1, 192 | .datadir = "/tmp/btczee", 193 | }; 194 | var mempool = try Mempool.init(allocator, &config); 195 | defer mempool.deinit(); 196 | 197 | // Create a mock transaction 198 | var transaction = try tx.Transaction.init(allocator); 199 | defer transaction.deinit(); 200 | try transaction.addInput(tx.OutPoint{ .hash = tx.Hash.newZeroed(), .index = 0 }); 201 | try transaction.addOutput(50000, try tx.Script.init(allocator)); 202 | 203 | // Add the transaction to the mempool 204 | const tx_desc = try mempool.addTransaction(&transaction, 101, 1000); 205 | try testing.expect(tx_desc != null); 206 | 207 | // Check if the transaction is in the mempool 208 | try testing.expect(mempool.containsTransaction(transaction.hash())); 209 | 210 | // Check the mempool count 211 | try testing.expectEqual(@as(usize, 1), mempool.count()); 212 | 213 | // Remove the transaction from the mempool 214 | mempool.removeTransaction(transaction.hash(), false); 215 | 216 | // Check if the transaction is no longer in the mempool 217 | try testing.expect(!mempool.containsTransaction(transaction.hash())); 218 | 219 | // Check the mempool count after removal 220 | try testing.expectEqual(@as(usize, 0), mempool.count()); 221 | } 222 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | //! btczee is a Bitcoin protocol implementation in Zig. 2 | //! It can be used as a binary or a library. 3 | //! Warning: This is still a work in progress and is not yet ready for production use. 4 | //! 5 | //! btczee can be run as: 6 | //! - a wallet 7 | //! - a miner 8 | //! - a full node 9 | //! 10 | //! btczee is licensed under the MIT license. 11 | pub const config = @import("config/config.zig"); 12 | pub const mempool = @import("core/mempool.zig"); 13 | pub const p2p = @import("network/p2p.zig"); 14 | pub const rpc = @import("network/rpc.zig"); 15 | pub const network = @import("network/network.zig"); 16 | pub const storage = @import("storage/storage.zig"); 17 | pub const node = @import("node/node.zig"); 18 | pub const script = @import("script/lib.zig"); 19 | pub const address = @import("address/address.zig"); 20 | pub const wire = @import("network/wire/lib.zig"); 21 | 22 | test { 23 | @import("std").testing.refAllDeclsRecursive(@This()); 24 | } 25 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | // 2 | // ___. __ 3 | // \_ |___/ |_ ____ ________ ____ ____ 4 | // | __ \ __\/ ___\\___ // __ \_/ __ \ 5 | // | \_\ \ | \ \___ / /\ ___/\ ___/ 6 | // |___ /__| \___ >_____ \\___ >\___ > 7 | // \/ \/ \/ \/ \/ 8 | // 9 | // Bitcoin Implementation in Zig 10 | // ============================= 11 | 12 | //==== Imports ====// 13 | const std = @import("std"); 14 | 15 | const Config = @import("config/config.zig").Config; 16 | const Mempool = @import("core/mempool.zig").Mempool; 17 | const Storage = @import("storage/storage.zig").Storage; 18 | const P2P = @import("network/p2p.zig").P2P; 19 | const RPC = @import("network/rpc.zig").RPC; 20 | const Node = @import("node/node.zig").Node; 21 | const ArgParser = @import("util/cmd/ArgParser.zig"); 22 | 23 | // We set this so that std.log knows not to log .debug level messages 24 | // which libraries we import will use 25 | pub const std_options: std.Options = .{ 26 | // Set the log level to info 27 | .log_level = .info, 28 | }; 29 | 30 | //==== Main Entry Point ====// 31 | pub fn main() !void { 32 | // Initialize the allocator 33 | var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; 34 | const allocator = gpa_state.allocator(); 35 | defer _ = gpa_state.deinit(); 36 | 37 | // Parse command-line arguments 38 | const args = try std.process.argsAlloc(allocator); 39 | defer std.process.argsFree(allocator, args); 40 | 41 | // Set up buffered stdout 42 | var stdout_buffered = std.io.bufferedWriter(std.io.getStdOut().writer()); 43 | const stdout = stdout_buffered.writer(); 44 | 45 | // Run the main program logic 46 | try mainFull(.{ 47 | .allocator = allocator, 48 | .args = args[1..], 49 | .stdout = stdout.any(), 50 | }); 51 | 52 | // Flush the buffered stdout 53 | return stdout_buffered.flush(); 54 | } 55 | 56 | //==== Main Program Logic ====// 57 | pub fn mainFull(options: struct { 58 | allocator: std.mem.Allocator, 59 | args: []const []const u8, 60 | stdout: std.io.AnyWriter, 61 | }) !void { 62 | var program = Program{ 63 | .allocator = options.allocator, 64 | .args = .{ .args = options.args }, 65 | .stdout = options.stdout, 66 | }; 67 | 68 | return program.mainCommand(); 69 | } 70 | 71 | //==== Program Structure ====// 72 | const Program = @This(); 73 | 74 | allocator: std.mem.Allocator, 75 | args: ArgParser, 76 | stdout: std.io.AnyWriter, 77 | 78 | //==== Usage Messages ====// 79 | const main_usage = 80 | \\Usage: btczee [command] [args] 81 | \\ 82 | \\Commands: 83 | \\ node 84 | \\ wallet 85 | \\ help Display this message 86 | \\ 87 | ; 88 | 89 | const node_sub_usage = 90 | \\Usage: 91 | \\ btczee node [command] [args] 92 | \\ btczee node [options] [ids]... 93 | \\ 94 | \\Commands: 95 | \\ help Display this message 96 | \\ 97 | ; 98 | 99 | const wallet_sub_usage = 100 | \\Usage: 101 | \\ btczee wallet [command] [args] 102 | \\ 103 | \\Commands: 104 | \\ create Create a new wallet 105 | \\ load Load an existing wallet 106 | \\ help Display this message 107 | \\ 108 | ; 109 | 110 | //==== Command Handlers ====// 111 | 112 | // Main Command Handler 113 | pub fn mainCommand(program: *Program) !void { 114 | while (program.args.next()) { 115 | if (program.args.flag(&.{"node"})) 116 | return program.nodeSubCommand(); 117 | if (program.args.flag(&.{"wallet"})) 118 | return program.walletSubCommand(); 119 | if (program.args.flag(&.{ "-h", "--help", "help" })) 120 | return program.stdout.writeAll(main_usage); 121 | if (program.args.positional()) |_| { 122 | try std.io.getStdErr().writeAll(main_usage); 123 | return error.InvalidArgument; 124 | } 125 | } 126 | try std.io.getStdErr().writeAll(main_usage); 127 | return error.InvalidArgument; 128 | } 129 | 130 | // Node Subcommand Handler 131 | fn nodeSubCommand(program: *Program) !void { 132 | if (program.args.next()) { 133 | if (program.args.flag(&.{ "-h", "--help", "help" })) 134 | return program.stdout.writeAll(node_sub_usage); 135 | } 136 | return program.runNodeCommand(); 137 | } 138 | 139 | // Wallet Subcommand Handler 140 | fn walletSubCommand(program: *Program) !void { 141 | if (program.args.next()) { 142 | if (program.args.flag(&.{"create"})) 143 | return program.walletCreateCommand(); 144 | if (program.args.flag(&.{"load"})) 145 | return program.walletLoadCommand(); 146 | if (program.args.flag(&.{ "-h", "--help", "help" })) 147 | return program.stdout.writeAll(wallet_sub_usage); 148 | } 149 | try std.io.getStdErr().writeAll(wallet_sub_usage); 150 | return error.InvalidArgument; 151 | } 152 | 153 | //==== Command Implementations ====// 154 | 155 | // Node Command Implementation 156 | fn runNodeCommand(program: *Program) !void { 157 | // Load configuration 158 | var config = try Config.load(program.allocator, "bitcoin.conf.example"); 159 | defer config.deinit(); 160 | 161 | // Initialize components 162 | var mempool = try Mempool.init(program.allocator, &config); 163 | defer mempool.deinit(); 164 | var storage = try Storage.init(&config); 165 | defer storage.deinit(); 166 | var p2p = try P2P.init(program.allocator, &config); 167 | defer p2p.deinit(); 168 | var rpc = try RPC.init(program.allocator, &config, &mempool, &storage); 169 | defer rpc.deinit(); 170 | 171 | var node = try Node.init(program.allocator, &mempool, &storage, &p2p, &rpc); 172 | defer node.deinit(); 173 | 174 | // Start the node 175 | try node.start(); 176 | } 177 | 178 | // Wallet Create Command Implementation 179 | fn walletCreateCommand(program: *Program) !void { 180 | return program.stdout.writeAll("Wallet creation not implemented yet\n"); 181 | } 182 | 183 | // Wallet Load Command Implementation 184 | fn walletLoadCommand(program: *Program) !void { 185 | return program.stdout.writeAll("Wallet loading not implemented yet\n"); 186 | } 187 | 188 | //==== End of File ====// 189 | -------------------------------------------------------------------------------- /src/network/message/utils.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn convertIPv4ToIPv6(address: std.net.Address) std.net.Address { 4 | // Convert IPv4 to IPv6-mapped IPv4 address 5 | const ipv4_address = address.in; 6 | 7 | var ipv6_mapped: [16]u8 = [_]u8{0} ** 16; 8 | 9 | // Set bytes 10 and 11 to 0xff 10 | ipv6_mapped[10] = 0xff; 11 | ipv6_mapped[11] = 0xff; 12 | 13 | // Copy the IPv4 address into the last 4 bytes of the IPv6-mapped address 14 | const ipv4_bytes = std.mem.asBytes(&ipv4_address.sa.addr); 15 | @memcpy(ipv6_mapped[12..16], ipv4_bytes[0..4]); 16 | 17 | return std.net.Address.initIp6(ipv6_mapped, ipv4_address.getPort(), 0, 0); 18 | } 19 | -------------------------------------------------------------------------------- /src/network/network.zig: -------------------------------------------------------------------------------- 1 | /// What kind of network we are on. 2 | pub const NetworkKind = enum { 3 | /// The Bitcoin mainnet network. 4 | main, 5 | /// Some kind of testnet network. 6 | @"test", 7 | 8 | pub fn fromNetwork(n: Network) NetworkKind { 9 | return n.toKind(); 10 | } 11 | }; 12 | 13 | /// The cryptocurrency network to act on. 14 | pub const Network = enum { 15 | /// Mainnet Bitcoin. 16 | bitcoin, 17 | /// Bitcoin's testnet network. 18 | testnet, 19 | /// Bitcoin's signet network. 20 | signet, 21 | /// Bitcoin's regtest network. 22 | regtest, 23 | 24 | pub fn toKind(self: Network) NetworkKind { 25 | return switch (self) { 26 | .bitcoin => .main, 27 | .testnet, .signet, .regtest => .@"test", 28 | }; 29 | } 30 | 31 | // TODO: fromMagic and etc 32 | }; 33 | -------------------------------------------------------------------------------- /src/network/p2p.zig: -------------------------------------------------------------------------------- 1 | //! P2P module handles the peer-to-peer networking of btczee. 2 | //! It is responsible for the connection to other nodes in the network. 3 | //! It can receive and send messages to other nodes, based on the Bitcoin protocol. 4 | const std = @import("std"); 5 | const net = std.net; 6 | const Config = @import("../config/config.zig").Config; 7 | const Peer = @import("peer.zig").Peer; 8 | const Boundness = @import("peer.zig").Boundness; 9 | 10 | /// P2P network handler. 11 | pub const P2P = struct { 12 | /// Allocator. 13 | allocator: std.mem.Allocator, 14 | /// Configuration. 15 | config: *const Config, 16 | /// List of peers. 17 | peers: std.ArrayList(*Peer), 18 | /// Thread pool for listening to peers 19 | peer_thread_pool: *std.Thread.Pool, 20 | /// Listener. 21 | listener: ?net.Server, 22 | 23 | /// Initialize the P2P network handler. 24 | pub fn init(allocator: std.mem.Allocator, config: *const Config) !P2P { 25 | const pool = try allocator.create(std.Thread.Pool); 26 | try std.Thread.Pool.init(pool, .{ .allocator = allocator }); 27 | return P2P{ 28 | .allocator = allocator, 29 | .config = config, 30 | .peers = std.ArrayList(*Peer).init(allocator), 31 | .listener = null, 32 | .peer_thread_pool = pool, 33 | }; 34 | } 35 | 36 | /// Deinitialize the P2P network handler. 37 | pub fn deinit(self: *P2P) void { 38 | if (self.listener) |*l| l.deinit(); 39 | self.peer_thread_pool.deinit(); 40 | self.allocator.destroy(self.peer_thread_pool); 41 | for (self.peers.items) |peer| { 42 | peer.deinit(); 43 | } 44 | self.peers.deinit(); 45 | } 46 | 47 | /// Start the P2P network handler. 48 | pub fn start(self: *P2P) !void { 49 | std.log.info("Starting P2P network on port {}", .{self.config.p2p_port}); 50 | 51 | var n_outboud_peer: u8 = 0; 52 | seeds: for (self.config.dnsSeeds()) |seed| { 53 | const address_list = try std.net.getAddressList(self.allocator, seed.inner, 8333); 54 | for (address_list.addrs) |address| { 55 | var peer = Peer.init(self.allocator, self.config, address, Boundness.outbound) catch continue; 56 | try self.peers.append(peer); 57 | peer.handshake() catch continue; 58 | try self.peer_thread_pool.spawn(Peer.listen, .{peer}); 59 | 60 | n_outboud_peer += 1; 61 | // TODO: replace the hardcoded value with one from config 62 | if (n_outboud_peer == 8) { 63 | break :seeds; 64 | } 65 | } 66 | } 67 | std.log.info("Connected to {d} nodes", .{n_outboud_peer}); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/network/peer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const net = std.net; 3 | const protocol = @import("./protocol/lib.zig"); 4 | const wire = @import("./wire/lib.zig"); 5 | const Config = @import("../config/config.zig").Config; 6 | const MessageUtils = @import("./message/utils.zig"); 7 | const NetworkAddress = @import("./protocol/types/NetworkAddress.zig").NetworkAddress; 8 | 9 | pub const Boundness = enum { 10 | inbound, 11 | outbound, 12 | 13 | pub fn isOutbound(self: Boundness) bool { 14 | return self == Boundness.outbound; 15 | } 16 | pub fn isInbound(self: Boundness) bool { 17 | return self == Boundness.inbound; 18 | } 19 | }; 20 | 21 | /// Represents a peer connection in the Bitcoin network 22 | pub const Peer = struct { 23 | allocator: std.mem.Allocator, 24 | config: *const Config, 25 | stream: net.Stream, 26 | address: net.Address, 27 | protocol_version: ?i32 = null, 28 | services: ?u64 = null, 29 | last_seen: i64, 30 | boundness: Boundness, 31 | should_listen: bool = false, 32 | 33 | /// Initialize a new peer 34 | pub fn init(allocator: std.mem.Allocator, config: *const Config, address: std.net.Address, boundness: Boundness) !*Peer { 35 | const stream = try std.net.tcpConnectToAddress(address); 36 | const peer = try allocator.create(Peer); 37 | 38 | peer.* = .{ 39 | .allocator = allocator, 40 | .config = config, 41 | .stream = stream, 42 | .address = address, 43 | .last_seen = std.time.timestamp(), 44 | .boundness = boundness, 45 | }; 46 | return peer; 47 | } 48 | 49 | /// Clean up peer resources 50 | pub fn deinit(self: *Peer) void { 51 | self.stream.close(); 52 | self.allocator.destroy(self); 53 | } 54 | 55 | /// Start peer operations 56 | pub fn handshake(self: *Peer) !void { 57 | std.log.info("Starting peer connection with {}", .{self.address}); 58 | if (self.boundness.isOutbound()) { 59 | try self.negociateProtocolOutboundConnection(); 60 | } else { 61 | // Not implemented yet 62 | unreachable; 63 | } 64 | 65 | self.should_listen = true; 66 | std.log.info("Connected to {}", .{self.address}); 67 | } 68 | 69 | fn negociateProtocolOutboundConnection(self: *Peer) !void { 70 | try self.sendVersionMessage(); 71 | 72 | while (true) { 73 | const received_message = wire.receiveMessage(self.allocator, self.stream.reader(), self.config.network_id) catch |e| { 74 | switch (e) { 75 | // The node can be on another version of the protocol, using messages we are not aware of 76 | error.UnknownMessage => continue, 77 | else => return e, 78 | } 79 | } orelse continue; 80 | 81 | switch (received_message) { 82 | .version => |vm| { 83 | self.protocol_version = @min(self.config.protocol_version, vm.version); 84 | self.services = vm.addr_from.services; 85 | // send verack message 86 | try self.sendVerackMessage(); 87 | }, 88 | 89 | .verack => return, 90 | else => return error.InvalidHandshake, 91 | } 92 | } 93 | } 94 | 95 | /// Send version message to peer 96 | fn sendVersionMessage(self: *const Peer) !void { 97 | const address = if (self.address.any.family == std.posix.AF.INET) MessageUtils.convertIPv4ToIPv6(self.address) else self.address; 98 | 99 | const message = protocol.messages.VersionMessage.new( 100 | self.config.protocol_version, 101 | NetworkAddress{ 102 | .services = self.config.services, 103 | .ip = std.mem.zeroes([16]u8), 104 | .port = 0, 105 | }, 106 | NetworkAddress{ 107 | .services = 0, 108 | .ip = address.in6.sa.addr, 109 | .port = address.in6.getPort(), 110 | }, 111 | std.crypto.random.int(u64), 112 | self.config.bestBlock(), 113 | ); 114 | 115 | try wire.sendMessage( 116 | self.allocator, 117 | self.stream.writer(), 118 | self.config.protocol_version, 119 | self.config.network_id, 120 | message, 121 | ); 122 | } 123 | 124 | /// Send verack message to peer 125 | fn sendVerackMessage(self: *const Peer) !void { 126 | const verack_message = protocol.messages.VerackMessage{}; // Empty message, as verack doesn't carry data 127 | try wire.sendMessage( 128 | self.allocator, 129 | self.stream.writer(), 130 | self.config.protocol_version, 131 | self.config.network_id, 132 | verack_message, 133 | ); 134 | } 135 | 136 | pub fn listen(self: *Peer) void { 137 | std.log.info("Listening for messages from {any}", .{self.address}); 138 | while (self.should_listen) { 139 | const message = wire.receiveMessage(self.allocator, self.stream.reader(), self.config.network_id) catch |e| switch (e) { 140 | // The node can be on another version of the protocol, using messages we are not aware of 141 | error.UnknownMessage => continue, 142 | else => { 143 | self.should_listen = false; 144 | continue; 145 | }, 146 | } orelse continue; 147 | 148 | switch (message) { 149 | // We only received those during handshake, seeing them again is an error 150 | .version, .verack => self.should_listen = false, 151 | .feefilter => |feefilter_message| { 152 | std.log.info("Received feefilter message with feerate: {}", .{feefilter_message.feerate}); 153 | // TODO: Implement logic to filter transactions based on the received feerate 154 | }, 155 | // TODO: handle other messages correctly 156 | else => |*m| { 157 | std.log.info("Peer {any} sent a `{s}` message", .{ self.address, m.name() }); 158 | continue; 159 | }, 160 | } 161 | } 162 | } 163 | }; 164 | -------------------------------------------------------------------------------- /src/network/protocol/lib.zig: -------------------------------------------------------------------------------- 1 | pub const messages = @import("./messages/lib.zig"); 2 | pub const NetworkAddress = @import("types/NetworkAddress.zig"); 3 | pub const InventoryItem = @import("types/InventoryItem.zig"); 4 | 5 | /// Network services 6 | pub const ServiceFlags = struct { 7 | pub const NODE_NETWORK: u64 = 0x1; 8 | pub const NODE_GETUTXO: u64 = 0x2; 9 | pub const NODE_BLOOM: u64 = 0x4; 10 | pub const NODE_WITNESS: u64 = 0x8; 11 | pub const NODE_XTHIN: u64 = 0x10; 12 | pub const NODE_NETWORK_LIMITED: u64 = 0x0400; 13 | }; 14 | 15 | pub const CommandNames = struct { 16 | pub const VERSION = "version"; 17 | pub const VERACK = "verack"; 18 | pub const ADDR = "addr"; 19 | pub const INV = "inv"; 20 | pub const GETDATA = "getdata"; 21 | pub const NOTFOUND = "notfound"; 22 | pub const GETBLOCKS = "getblocks"; 23 | pub const GETHEADERS = "getheaders"; 24 | pub const TX = "tx"; 25 | pub const BLOCK = "block"; 26 | pub const HEADERS = "headers"; 27 | pub const GETADDR = "getaddr"; 28 | pub const MEMPOOL = "mempool"; 29 | pub const CHECKORDER = "checkorder"; 30 | pub const SUBMITORDER = "submitorder"; 31 | pub const REPLY = "reply"; 32 | pub const PING = "ping"; 33 | pub const PONG = "pong"; 34 | pub const REJECT = "reject"; 35 | pub const FILTERLOAD = "filterload"; 36 | pub const FILTERADD = "filteradd"; 37 | pub const FILTERCLEAR = "filterclear"; 38 | pub const MERKLEBLOCK = "merkleblock"; 39 | pub const ALERT = "alert"; 40 | pub const SENDHEADERS = "sendheaders"; 41 | pub const FEEFILTER = "feefilter"; 42 | pub const SENDCMPCT = "sendcmpct"; 43 | pub const CMPCTBLOCK = "cmpctblock"; 44 | pub const GETBLOCKTXN = "getblocktxn"; 45 | pub const BLOCKTXN = "blocktxn"; 46 | }; 47 | -------------------------------------------------------------------------------- /src/network/protocol/messages/addr.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const genericChecksum = @import("lib.zig").genericChecksum; 4 | const NetworkAddress = @import("../types/NetworkAddress.zig").NetworkAddress; 5 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 6 | const genericSerialize = @import("lib.zig").genericSerialize; 7 | 8 | const Endian = std.builtin.Endian; 9 | const Sha256 = std.crypto.hash.sha2.Sha256; 10 | 11 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 12 | 13 | pub const NetworkIPAddr = struct { 14 | time: u32, // Unix epoch time 15 | address: NetworkAddress, 16 | 17 | // NetworkIPAddr eql 18 | pub fn eql(self: *const NetworkIPAddr, other: *const NetworkIPAddr) bool { 19 | return self.time == other.time and self.address.eql(&other.address); 20 | } 21 | 22 | pub fn serializeToWriter(self: *const NetworkIPAddr, writer: anytype) !void { 23 | try writer.writeInt(u32, self.time, .little); 24 | try self.address.serializeToWriter(writer); 25 | } 26 | 27 | pub fn deserializeReader(reader: anytype) !NetworkIPAddr { 28 | return NetworkIPAddr{ 29 | .time = try reader.readInt(u32, .little), 30 | .address = try NetworkAddress.deserializeReader(reader), 31 | }; 32 | } 33 | }; 34 | 35 | /// AddrMessage represents the "addr" message 36 | /// 37 | /// https://developer.bitcoin.org/reference/p2p_networking.html#addr 38 | pub const AddrMessage = struct { 39 | ip_addresses: []NetworkIPAddr, 40 | 41 | const Self = @This(); 42 | 43 | pub inline fn name() *const [12]u8 { 44 | return protocol.CommandNames.ADDR ++ [_]u8{0} ** 8; 45 | } 46 | 47 | /// Returns the message checksum 48 | /// 49 | /// Computed as `Sha256(Sha256(self.serialize()))[0..4]` 50 | pub fn checksum(self: *const AddrMessage) [4]u8 { 51 | return genericChecksum(self); 52 | } 53 | 54 | /// Free the `user_agent` if there is one 55 | pub fn deinit(self: AddrMessage, allocator: std.mem.Allocator) void { 56 | allocator.free(self.ip_addresses); 57 | } 58 | 59 | /// Serialize the message as bytes and write them to the Writer. 60 | /// 61 | /// `w` should be a valid `Writer`. 62 | pub fn serializeToWriter(self: *const AddrMessage, w: anytype) !void { 63 | try CompactSizeUint.new(self.ip_addresses.len).encodeToWriter(w); 64 | for (self.ip_addresses) |*addr| { 65 | try addr.serializeToWriter(w); 66 | } 67 | } 68 | 69 | /// Serialize a message as bytes and return them. 70 | pub fn serialize(self: *const AddrMessage, allocator: std.mem.Allocator) ![]u8 { 71 | return genericSerialize(self, allocator); 72 | } 73 | 74 | /// Deserialize a Reader bytes as a `AddrMessage` 75 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !AddrMessage { 76 | const ip_address_count = try CompactSizeUint.decodeReader(r); 77 | 78 | // Allocate space for IP addresses 79 | const ip_addresses = try allocator.alloc(NetworkIPAddr, ip_address_count.value()); 80 | errdefer allocator.free(ip_addresses); 81 | 82 | for (ip_addresses) |*ip_address| { 83 | ip_address.* = try NetworkIPAddr.deserializeReader(r); 84 | } 85 | 86 | return AddrMessage{ 87 | .ip_addresses = ip_addresses, 88 | }; 89 | } 90 | 91 | /// Deserialize bytes into a `AddrMessage` 92 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !AddrMessage { 93 | return genericDeserializeSlice(Self, allocator, bytes); 94 | } 95 | 96 | pub fn hintSerializedLen(self: AddrMessage) usize { 97 | // 4 + 8 + 16 + 2 98 | const fixed_length_per_ip = 30; 99 | const count = CompactSizeUint.new(self.ip_addresses.len).hint_encoded_len(); 100 | return count + self.ip_addresses.len * fixed_length_per_ip; 101 | } 102 | 103 | pub fn eql(self: *const AddrMessage, other: *const AddrMessage) bool { 104 | if (self.ip_addresses.len != other.ip_addresses.len) return false; 105 | 106 | const count = @as(usize, self.ip_addresses.len); 107 | for (0..count) |i| { 108 | if (!self.ip_addresses[i].eql(&other.ip_addresses[i])) return false; 109 | } 110 | 111 | return true; 112 | } 113 | }; 114 | 115 | // TESTS 116 | test "ok_full_flow_AddrMessage" { 117 | const test_allocator = std.testing.allocator; 118 | { 119 | const ip_addresses = try test_allocator.alloc(NetworkIPAddr, 1); 120 | defer test_allocator.free(ip_addresses); 121 | 122 | ip_addresses[0] = NetworkIPAddr{ .time = 1414012889, .address = NetworkAddress{ 123 | .services = 1, 124 | .ip = [16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 51 }, 125 | .port = 8080, 126 | } }; 127 | const am = AddrMessage{ 128 | .ip_addresses = ip_addresses[0..], 129 | }; 130 | 131 | // Serialize 132 | const payload = try am.serialize(test_allocator); 133 | defer test_allocator.free(payload); 134 | 135 | // Deserialize 136 | const deserialized_am = try AddrMessage.deserializeSlice(test_allocator, payload); 137 | 138 | // Test equality 139 | try std.testing.expect(am.eql(&deserialized_am)); 140 | 141 | defer test_allocator.free(deserialized_am.ip_addresses); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/network/protocol/messages/block.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const native_endian = @import("builtin").target.cpu.arch.endian(); 3 | const protocol = @import("../lib.zig"); 4 | const genericChecksum = @import("lib.zig").genericChecksum; 5 | const genericSerialize = @import("lib.zig").genericSerialize; 6 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 7 | 8 | const ServiceFlags = protocol.ServiceFlags; 9 | 10 | const readBytesExact = @import("../../../util/mem/read.zig").readBytesExact; 11 | 12 | const Endian = std.builtin.Endian; 13 | const Sha256 = std.crypto.hash.sha2.Sha256; 14 | 15 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 16 | const Types = @import("../../../types/lib.zig"); 17 | const Transaction = Types.Transaction; 18 | const BlockHeader = Types.BlockHeader; 19 | 20 | /// BlockMessage represents the "block" message 21 | /// 22 | /// https://developer.bitcoin.org/reference/p2p_networking.html#block 23 | pub const BlockMessage = struct { 24 | block_header: BlockHeader, 25 | txns: []Transaction, 26 | 27 | const Self = @This(); 28 | 29 | pub inline fn name() *const [12]u8 { 30 | return protocol.CommandNames.BLOCK ++ [_]u8{0} ** 7; 31 | } 32 | 33 | pub fn checksum(self: BlockMessage) [4]u8 { 34 | return genericChecksum(self); 35 | } 36 | 37 | pub fn deinit(self: *BlockMessage, allocator: std.mem.Allocator) void { 38 | for (self.txns) |*txn| { 39 | txn.deinit(); 40 | } 41 | allocator.free(self.txns); 42 | } 43 | 44 | /// Serialize the message as bytes and write them to the Writer. 45 | /// 46 | /// `w` should be a valid `Writer`. 47 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 48 | comptime { 49 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects r to have fn 'writeInt'."); 50 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects r to have fn 'writeAll'."); 51 | } 52 | 53 | try self.block_header.serializeToWriter(w); 54 | 55 | try CompactSizeUint.new(self.txns.len).encodeToWriter(w); 56 | 57 | for (self.txns) |txn| { 58 | try txn.serializeToWriter(w); 59 | } 60 | } 61 | 62 | /// Serialize a message as bytes and return them. 63 | pub fn serialize(self: *const BlockMessage, allocator: std.mem.Allocator) ![]u8 { 64 | return genericSerialize(self, allocator); 65 | } 66 | 67 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !BlockMessage { 68 | comptime { 69 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 70 | if (!std.meta.hasFn(@TypeOf(r), "readNoEof")) @compileError("Expects r to have fn 'readNoEof'."); 71 | if (!std.meta.hasFn(@TypeOf(r), "readAll")) @compileError("Expects r to have fn 'readAll'."); 72 | if (!std.meta.hasFn(@TypeOf(r), "readByte")) @compileError("Expects r to have fn 'readByte'."); 73 | } 74 | 75 | var block_message: Self = undefined; 76 | 77 | block_message.block_header = try BlockHeader.deserializeReader(r); 78 | 79 | const txns_count = try CompactSizeUint.decodeReader(r); 80 | 81 | block_message.txns = try allocator.alloc(Transaction, txns_count.value()); 82 | errdefer allocator.free(block_message.txns); 83 | 84 | var i: usize = 0; 85 | while (i < txns_count.value()) : (i += 1) { 86 | const tx = try Transaction.deserializeReader(allocator, r); 87 | block_message.txns[i] = tx; 88 | } 89 | 90 | return block_message; 91 | } 92 | 93 | /// Deserialize bytes into a `VersionMessage` 94 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 95 | return genericDeserializeSlice(Self, allocator, bytes); 96 | } 97 | 98 | pub fn hintSerializedLen(self: BlockMessage) usize { 99 | const header_length = BlockHeader.serializedLen(); 100 | const txs_number_length = CompactSizeUint.new(self.txns.len).hint_encoded_len(); 101 | var txs_length: usize = 0; 102 | for (self.txns) |txn| { 103 | txs_length += txn.hintEncodedLen(); 104 | } 105 | return header_length + txs_number_length + txs_length; 106 | } 107 | }; 108 | 109 | // TESTS 110 | test "ok_full_flow_BlockMessage" { 111 | const OpCode = @import("../../../script/opcodes/constant.zig").Opcode; 112 | const allocator = std.testing.allocator; 113 | const OutPoint = Types.OutPoint; 114 | const Hash = Types.Hash; 115 | const Script = Types.Script; 116 | 117 | { 118 | var tx = try Transaction.init(allocator); 119 | 120 | try tx.addInput(OutPoint{ .hash = Hash.newZeroed(), .index = 0 }); 121 | 122 | { 123 | var script_pubkey = try Script.init(allocator); 124 | defer script_pubkey.deinit(); 125 | try script_pubkey.push(&[_]u8{ OpCode.OP_DUP.toBytes(), OpCode.OP_0.toBytes(), OpCode.OP_1.toBytes() }); 126 | try tx.addOutput(50000, script_pubkey); 127 | } 128 | 129 | var txns = try allocator.alloc(Transaction, 1); 130 | // errdefer allocator.free(txns); 131 | txns[0] = tx; 132 | 133 | var msg = BlockMessage{ 134 | .block_header = BlockHeader{ 135 | .version = 1, 136 | .prev_block = [_]u8{0} ** 32, 137 | .merkle_root = [_]u8{0} ** 32, 138 | .timestamp = 1, 139 | .nbits = 1, 140 | .nonce = 1, 141 | }, 142 | .txns = txns, 143 | }; 144 | defer msg.deinit(allocator); 145 | 146 | const payload = try msg.serialize(allocator); 147 | defer allocator.free(payload); 148 | var deserialized_msg = try BlockMessage.deserializeSlice(allocator, payload); 149 | defer deserialized_msg.deinit(allocator); 150 | 151 | try std.testing.expectEqual(msg.block_header.version, deserialized_msg.block_header.version); 152 | try std.testing.expect(std.mem.eql(u8, &msg.block_header.prev_block, &deserialized_msg.block_header.prev_block)); 153 | try std.testing.expect(std.mem.eql(u8, &msg.block_header.merkle_root, &deserialized_msg.block_header.merkle_root)); 154 | try std.testing.expect(msg.block_header.timestamp == deserialized_msg.block_header.timestamp); 155 | try std.testing.expect(msg.block_header.nbits == deserialized_msg.block_header.nbits); 156 | try std.testing.expect(msg.block_header.nonce == deserialized_msg.block_header.nonce); 157 | 158 | for (msg.txns, 0..) |txn, i| { 159 | const deserialized_txn = deserialized_msg.txns[i]; 160 | try std.testing.expect(txn.eql(deserialized_txn)); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/network/protocol/messages/cmpctblock.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const Transaction = @import("../../../types/transaction.zig"); 4 | 5 | const Sha256 = std.crypto.hash.sha2.Sha256; 6 | const BlockHeader = @import("../../../types/block_header.zig"); 7 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 8 | const genericChecksum = @import("lib.zig").genericChecksum; 9 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 10 | 11 | pub const CmpctBlockMessage = struct { 12 | header: BlockHeader, 13 | nonce: u64, 14 | short_ids: []u64, 15 | prefilled_txns: []PrefilledTransaction, 16 | 17 | const Self = @This(); 18 | 19 | pub const PrefilledTransaction = struct { 20 | index: usize, 21 | tx: Transaction, 22 | }; 23 | 24 | pub fn name() *const [12]u8 { 25 | return protocol.CommandNames.CMPCTBLOCK; 26 | } 27 | 28 | pub fn checksum(self: *const Self) [4]u8 { 29 | return genericChecksum(self); 30 | } 31 | 32 | pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { 33 | allocator.free(self.short_ids); 34 | for (self.prefilled_txns) |*txn| { 35 | txn.tx.deinit(); 36 | } 37 | allocator.free(self.prefilled_txns); 38 | } 39 | 40 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 41 | comptime { 42 | if (!@hasDecl(@TypeOf(w), "writeInt")) { 43 | @compileError("Writer must have a writeInt method"); 44 | } 45 | } 46 | 47 | try self.header.serializeToWriter(w); 48 | try w.writeInt(u64, self.nonce, .little); 49 | 50 | const short_ids_count = CompactSizeUint.new(self.short_ids.len); 51 | try short_ids_count.encodeToWriter(w); 52 | for (self.short_ids) |id| { 53 | try w.writeInt(u64, id, .little); 54 | } 55 | 56 | const prefilled_txns_count = CompactSizeUint.new(self.prefilled_txns.len); 57 | try prefilled_txns_count.encodeToWriter(w); 58 | 59 | for (self.prefilled_txns) |txn| { 60 | try CompactSizeUint.new(txn.index).encodeToWriter(w); 61 | try txn.tx.serializeToWriter(w); 62 | } 63 | } 64 | 65 | pub fn serializeToSlice(self: *const Self, buffer: []u8) !void { 66 | var fbs = std.io.fixedBufferStream(buffer); 67 | try self.serializeToWriter(fbs.writer()); 68 | } 69 | 70 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 71 | const serialized_len = self.hintSerializedLen(); 72 | if (serialized_len == 0) return &.{}; 73 | const ret = try allocator.alloc(u8, serialized_len); 74 | errdefer allocator.free(ret); 75 | 76 | try self.serializeToSlice(ret); 77 | 78 | return ret; 79 | } 80 | 81 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 82 | comptime { 83 | if (!@hasDecl(@TypeOf(r), "readInt")) { 84 | @compileError("Reader must have a readInt method"); 85 | } 86 | } 87 | 88 | const header = try BlockHeader.deserializeReader(r); 89 | const nonce = try r.readInt(u64, .little); 90 | 91 | const short_ids_count = try CompactSizeUint.decodeReader(r); 92 | const short_ids = try allocator.alloc(u64, short_ids_count.value()); 93 | errdefer allocator.free(short_ids); 94 | 95 | for (short_ids) |*id| { 96 | id.* = try r.readInt(u64, .little); 97 | } 98 | 99 | const prefilled_txns_count = try CompactSizeUint.decodeReader(r); 100 | const prefilled_txns = try allocator.alloc(PrefilledTransaction, prefilled_txns_count.value()); 101 | errdefer allocator.free(prefilled_txns); 102 | 103 | for (prefilled_txns) |*txn| { 104 | const index = try CompactSizeUint.decodeReader(r); 105 | const tx = try Transaction.deserializeReader(allocator, r); 106 | 107 | txn.* = PrefilledTransaction{ 108 | .index = index.value(), 109 | .tx = tx, 110 | }; 111 | } 112 | 113 | return Self{ 114 | .header = header, 115 | .nonce = nonce, 116 | .short_ids = short_ids, 117 | .prefilled_txns = prefilled_txns, 118 | }; 119 | } 120 | 121 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 122 | return genericDeserializeSlice(Self, allocator, bytes); 123 | } 124 | 125 | pub fn hintSerializedLen(self: *const Self) usize { 126 | var len: usize = 80 + 8; // BlockHeader + nonce 127 | len += CompactSizeUint.new(self.short_ids.len).hint_encoded_len(); 128 | len += self.short_ids.len * 8; 129 | len += CompactSizeUint.new(self.prefilled_txns.len).hint_encoded_len(); 130 | for (self.prefilled_txns) |txn| { 131 | len += CompactSizeUint.new(txn.index).hint_encoded_len(); 132 | len += txn.tx.hintEncodedLen(); 133 | } 134 | return len; 135 | } 136 | 137 | pub fn eql(self: *const Self, other: *const Self) bool { 138 | if (self.header.version != other.header.version or 139 | !std.mem.eql(u8, &self.header.prev_block, &other.header.prev_block) or 140 | !std.mem.eql(u8, &self.header.merkle_root, &other.header.merkle_root) or 141 | self.header.timestamp != other.header.timestamp or 142 | self.header.nbits != other.header.nbits or 143 | self.header.nonce != other.header.nonce or 144 | self.nonce != other.nonce) return false; 145 | 146 | if (self.short_ids.len != other.short_ids.len) return false; 147 | for (self.short_ids, other.short_ids) |a, b| { 148 | if (a != b) return false; 149 | } 150 | if (self.prefilled_txns.len != other.prefilled_txns.len) return false; 151 | for (self.prefilled_txns, other.prefilled_txns) |a, b| { 152 | if (a.index != b.index or !a.tx.eql(b.tx)) return false; 153 | } 154 | return true; 155 | } 156 | }; 157 | 158 | test "CmpctBlockMessage serialization and deserialization" { 159 | const testing = std.testing; 160 | const Hash = @import("../../../types/hash.zig"); 161 | const Script = @import("../../../types/script.zig"); 162 | const OutPoint = @import("../../../types/outpoint.zig"); 163 | const OpCode = @import("../../../script/opcodes/constant.zig").Opcode; 164 | 165 | const test_allocator = testing.allocator; 166 | 167 | // Create a sample BlockHeader 168 | const header = BlockHeader{ 169 | .version = 1, 170 | .prev_block = [_]u8{0} ** 32, // Zero-filled array of 32 bytes 171 | .merkle_root = [_]u8{0} ** 32, // Zero-filled array of 32 bytes 172 | .timestamp = 1631234567, 173 | .nbits = 0x1d00ffff, 174 | .nonce = 12345, 175 | }; 176 | 177 | // Create sample short_ids 178 | const short_ids = try test_allocator.alloc(u64, 2); 179 | defer test_allocator.free(short_ids); 180 | short_ids[0] = 123456789; 181 | short_ids[1] = 987654321; 182 | 183 | // Create a sample Transaction 184 | var tx = try Transaction.init(test_allocator); 185 | defer tx.deinit(); 186 | try tx.addInput(OutPoint{ .hash = Hash.newZeroed(), .index = 0 }); 187 | { 188 | var script_pubkey = try Script.init(test_allocator); 189 | defer script_pubkey.deinit(); 190 | try script_pubkey.push(&[_]u8{ OpCode.OP_DUP.toBytes(), OpCode.OP_HASH160.toBytes(), OpCode.OP_EQUALVERIFY.toBytes(), OpCode.OP_CHECKSIG.toBytes() }); 191 | try tx.addOutput(50000, script_pubkey); 192 | } 193 | 194 | // Create sample prefilled_txns 195 | const prefilled_txns = try test_allocator.alloc(CmpctBlockMessage.PrefilledTransaction, 1); 196 | defer test_allocator.free(prefilled_txns); 197 | prefilled_txns[0] = .{ 198 | .index = 0, 199 | .tx = tx, 200 | }; 201 | 202 | // Create CmpctBlockMessage 203 | const msg = CmpctBlockMessage{ 204 | .header = header, 205 | .nonce = 9876543210, 206 | .short_ids = short_ids, 207 | .prefilled_txns = prefilled_txns, 208 | }; 209 | 210 | // Test serialization 211 | const serialized = try msg.serialize(test_allocator); 212 | defer test_allocator.free(serialized); 213 | 214 | // Test deserialization 215 | var deserialized = try CmpctBlockMessage.deserializeSlice(test_allocator, serialized); 216 | defer deserialized.deinit(test_allocator); 217 | 218 | // Verify deserialized data 219 | try std.testing.expect(msg.eql(&deserialized)); 220 | 221 | // Test hintSerializedLen 222 | const hint_len = msg.hintSerializedLen(); 223 | try testing.expect(hint_len > 0); 224 | try testing.expect(hint_len == serialized.len); 225 | } 226 | -------------------------------------------------------------------------------- /src/network/protocol/messages/feefilter.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const Sha256 = std.crypto.hash.sha2.Sha256; 4 | const genericChecksum = @import("lib.zig").genericChecksum; 5 | const genericSerialize = @import("lib.zig").genericSerialize; 6 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 7 | 8 | /// FeeFilterMessage represents the "feefilter" message 9 | /// 10 | /// https://github.com/bitcoin/bips/blob/master/bip-0133.mediawiki 11 | pub const FeeFilterMessage = struct { 12 | feerate: u64, 13 | 14 | const Self = @This(); 15 | 16 | pub fn name() *const [12]u8 { 17 | return protocol.CommandNames.FEEFILTER ++ [_]u8{0} ** 4; 18 | } 19 | 20 | /// Returns the message checksum 21 | /// 22 | /// Computed as `Sha256(Sha256(self.serialize()))[0..4]` 23 | pub fn checksum(self: *const Self) [4]u8 { 24 | return genericChecksum(self); 25 | } 26 | 27 | 28 | /// Serialize the message as bytes and write them to the Writer. 29 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 30 | try w.writeInt(u64, self.feerate, .little); 31 | } 32 | 33 | /// Serialize a message as bytes and return them. 34 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 35 | return genericSerialize(self, allocator); 36 | } 37 | 38 | /// Deserialize a Reader bytes as a `FeeFilterMessage` 39 | pub fn deserializeReader(_: std.mem.Allocator, r: anytype) !Self { 40 | comptime { 41 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 42 | } 43 | 44 | var fm: Self = undefined; 45 | fm.feerate = try r.readInt(u64, .little); 46 | return fm; 47 | } 48 | 49 | /// Deserialize bytes into a `FeeFilterMessage` 50 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 51 | return genericDeserializeSlice(Self, allocator, bytes); 52 | } 53 | 54 | pub fn hintSerializedLen(_: *const Self) usize { 55 | return 8; // feerate is u64 (8 bytes) 56 | } 57 | 58 | pub fn new(feerate: u64) Self { 59 | return .{ 60 | .feerate = feerate, 61 | }; 62 | } 63 | }; 64 | 65 | // TESTS 66 | test "ok_fullflow_feefilter_message" { 67 | const allocator = std.testing.allocator; 68 | 69 | { 70 | const msg = FeeFilterMessage.new(48508); 71 | const payload = try msg.serialize(allocator); 72 | defer allocator.free(payload); 73 | const deserialized_msg = try FeeFilterMessage.deserializeSlice(allocator, payload); 74 | try std.testing.expectEqual(msg.feerate, deserialized_msg.feerate); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/network/protocol/messages/filteradd.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const genericChecksum = @import("lib.zig").genericChecksum; 4 | const genericSerialize = @import("lib.zig").genericSerialize; 5 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 6 | 7 | /// FilterAddMessage represents the "filteradd" message 8 | /// 9 | /// https://developer.bitcoin.org/reference/p2p_networking.html#filteradd 10 | pub const FilterAddMessage = struct { 11 | element: []const u8, 12 | 13 | const Self = @This(); 14 | 15 | pub fn name() *const [12]u8 { 16 | return protocol.CommandNames.FILTERADD ++ [_]u8{0} ** 3; 17 | } 18 | 19 | /// Returns the message checksum 20 | pub fn checksum(self: *const Self) [4]u8 { 21 | return genericChecksum(self); 22 | } 23 | 24 | /// Serialize the message as bytes and write them to the Writer. 25 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 26 | comptime { 27 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects w to have fn 'writeAll'."); 28 | } 29 | try w.writeAll(self.element); 30 | } 31 | 32 | /// Serialize a message as bytes and return them. 33 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 34 | return genericSerialize(self, allocator); 35 | } 36 | 37 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 38 | comptime { 39 | if (!std.meta.hasFn(@TypeOf(r), "readAllAlloc")) @compileError("Expects r to have fn 'readAllAlloc'."); 40 | } 41 | const element = try r.readAllAlloc(allocator, 520); 42 | return Self{ .element = element }; 43 | } 44 | 45 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 46 | return genericDeserializeSlice(Self, allocator, bytes); 47 | } 48 | 49 | pub fn hintSerializedLen(self: *const Self) usize { 50 | return self.element.len; 51 | } 52 | 53 | pub fn deinit(self: *const Self, allocator: std.mem.Allocator) void { 54 | allocator.free(self.element); 55 | } 56 | }; 57 | 58 | // TESTS 59 | test "ok_fullflow_filteradd_message" { 60 | const allocator = std.testing.allocator; 61 | 62 | const element = "test_element"; 63 | var msg = FilterAddMessage{ .element = element }; 64 | 65 | const payload = try msg.serialize(allocator); 66 | defer allocator.free(payload); 67 | 68 | var deserialized_msg = try FilterAddMessage.deserializeSlice(allocator, payload); 69 | defer deserialized_msg.deinit(allocator); 70 | 71 | try std.testing.expectEqualSlices(u8, element, deserialized_msg.element); 72 | } 73 | -------------------------------------------------------------------------------- /src/network/protocol/messages/filterclear.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const default_checksum = @import("lib.zig").default_checksum; 4 | 5 | /// FilterClear represents the "filterclear" message 6 | /// 7 | /// https://developer.bitcoin.org/reference/p2p_networking.html#filterclear 8 | pub const FilterClearMessage = struct { 9 | // FilterClear message do not contain any payload, thus there is no field 10 | 11 | pub inline fn name() *const [12]u8 { 12 | return protocol.CommandNames.FILTERCLEAR; 13 | } 14 | 15 | pub fn checksum(self: FilterClearMessage) [4]u8 { 16 | _ = self; 17 | return default_checksum; 18 | } 19 | 20 | /// Serialize a message as bytes and return them. 21 | pub fn serialize(self: *const FilterClearMessage, allocator: std.mem.Allocator) ![]u8 { 22 | _ = self; 23 | _ = allocator; 24 | return &.{}; 25 | } 26 | 27 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !FilterClearMessage { 28 | _ = allocator; 29 | _ = r; 30 | return FilterClearMessage{}; 31 | } 32 | 33 | pub fn hintSerializedLen(self: FilterClearMessage) usize { 34 | _ = self; 35 | return 0; 36 | } 37 | }; 38 | 39 | // TESTS 40 | test "ok_full_flow_FilterClearMessage" { 41 | const allocator = std.testing.allocator; 42 | 43 | { 44 | const msg = FilterClearMessage{}; 45 | 46 | const payload = try msg.serialize(allocator); 47 | defer allocator.free(payload); 48 | const deserialized_msg = try FilterClearMessage.deserializeReader(allocator, payload); 49 | _ = deserialized_msg; 50 | 51 | try std.testing.expect(payload.len == 0); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/network/protocol/messages/filterload.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const genericChecksum = @import("lib.zig").genericChecksum; 4 | const genericSerialize = @import("lib.zig").genericSerialize; 5 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 6 | 7 | const Sha256 = std.crypto.hash.sha2.Sha256; 8 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 9 | 10 | /// FilterLoadMessage represents the "filterload" message 11 | /// 12 | /// https://developer.bitcoin.org/reference/p2p_networking.html#filterload 13 | pub const FilterLoadMessage = struct { 14 | filter: []const u8, 15 | hash_func: u32, 16 | tweak: u32, 17 | flags: u8, 18 | 19 | const Self = @This(); 20 | 21 | pub fn name() *const [12]u8 { 22 | return protocol.CommandNames.FILTERLOAD ++ [_]u8{0} ** 2; 23 | } 24 | 25 | /// Returns the message checksum 26 | pub fn checksum(self: *const Self) [4]u8 { 27 | return genericChecksum(self); 28 | } 29 | 30 | /// Serialize the message as bytes and write them to the Writer. 31 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 32 | comptime { 33 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects w to have fn 'writeInt'."); 34 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects w to have fn 'writeAll'."); 35 | } 36 | 37 | const compact_filter_len = CompactSizeUint.new(self.filter.len); 38 | try compact_filter_len.encodeToWriter(w); 39 | 40 | try w.writeAll(self.filter); 41 | try w.writeInt(u32, self.hash_func, .little); 42 | try w.writeInt(u32, self.tweak, .little); 43 | try w.writeInt(u8, self.flags, .little); 44 | } 45 | 46 | /// Serialize a message as bytes and return them. 47 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 48 | return genericSerialize(self, allocator); 49 | } 50 | 51 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 52 | comptime { 53 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 54 | if (!std.meta.hasFn(@TypeOf(r), "readNoEof")) @compileError("Expects r to have fn 'readNoEof'."); 55 | } 56 | 57 | const filter_len = (try CompactSizeUint.decodeReader(r)).value(); 58 | const filter = try allocator.alloc(u8, filter_len); 59 | errdefer allocator.free(filter); 60 | try r.readNoEof(filter); 61 | 62 | const hash_func = try r.readInt(u32, .little); 63 | const tweak = try r.readInt(u32, .little); 64 | const flags = try r.readInt(u8, .little); 65 | 66 | return Self{ 67 | .filter = filter, 68 | .hash_func = hash_func, 69 | .tweak = tweak, 70 | .flags = flags, 71 | }; 72 | } 73 | 74 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 75 | return genericDeserializeSlice(Self, allocator, bytes); 76 | } 77 | 78 | pub fn hintSerializedLen(self: *const Self) usize { 79 | const fixed_length = 4 + 4 + 1; // hash_func (4 bytes) + tweak (4 bytes) + flags (1 byte) 80 | const compact_filter_len = CompactSizeUint.new(self.filter.len).hint_encoded_len(); 81 | return compact_filter_len + self.filter.len + fixed_length; 82 | } 83 | 84 | pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { 85 | allocator.free(self.filter); 86 | } 87 | }; 88 | 89 | test "ok_fullflow_filterload_message" { 90 | const allocator = std.testing.allocator; 91 | 92 | const filter = "this is a test filter"; 93 | var fl = FilterLoadMessage{ 94 | .filter = filter, 95 | .hash_func = 0xdeadbeef, 96 | .tweak = 0xfeedface, 97 | .flags = 0x02, 98 | }; 99 | 100 | const payload = try fl.serialize(allocator); 101 | defer allocator.free(payload); 102 | 103 | var deserialized_fl = try FilterLoadMessage.deserializeSlice(allocator, payload); 104 | defer deserialized_fl.deinit(allocator); 105 | 106 | try std.testing.expectEqualSlices(u8, filter, deserialized_fl.filter); 107 | try std.testing.expect(fl.hash_func == deserialized_fl.hash_func); 108 | try std.testing.expect(fl.tweak == deserialized_fl.tweak); 109 | try std.testing.expect(fl.flags == deserialized_fl.flags); 110 | } 111 | -------------------------------------------------------------------------------- /src/network/protocol/messages/getaddr.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const default_checksum = @import("lib.zig").default_checksum; 4 | 5 | /// GetaddrMessage represents the "getaddr" message 6 | /// 7 | /// https://developer.bitcoin.org/reference/p2p_networking.html#getaddr 8 | pub const GetaddrMessage = struct { 9 | // getaddr message do not contain any payload, thus there is no field 10 | 11 | pub fn name() *const [12]u8 { 12 | return protocol.CommandNames.GETADDR ++ [_]u8{0} ** 5; 13 | } 14 | 15 | pub fn checksum(self: GetaddrMessage) [4]u8 { 16 | _ = self; 17 | return default_checksum; 18 | } 19 | 20 | /// Serialize a message as bytes and return them. 21 | pub fn serialize(self: *const GetaddrMessage, allocator: std.mem.Allocator) ![]u8 { 22 | _ = self; 23 | _ = allocator; 24 | return &.{}; 25 | } 26 | 27 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !GetaddrMessage { 28 | _ = allocator; 29 | _ = r; 30 | return GetaddrMessage{}; 31 | } 32 | 33 | pub fn hintSerializedLen(self: GetaddrMessage) usize { 34 | _ = self; 35 | return 0; 36 | } 37 | }; 38 | 39 | // TESTS 40 | test "ok_full_flow_GetaddrMessage" { 41 | const allocator = std.testing.allocator; 42 | 43 | { 44 | const msg = GetaddrMessage{}; 45 | 46 | const payload = try msg.serialize(allocator); 47 | defer allocator.free(payload); 48 | const deserialized_msg = try GetaddrMessage.deserializeReader(allocator, payload); 49 | _ = deserialized_msg; 50 | 51 | try std.testing.expect(payload.len == 0); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/network/protocol/messages/getblocks.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const genericChecksum = @import("lib.zig").genericChecksum; 4 | const genericSerialize = @import("lib.zig").genericSerialize; 5 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 6 | 7 | const Sha256 = std.crypto.hash.sha2.Sha256; 8 | 9 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 10 | 11 | /// GetblocksMessage represents the "getblocks" message 12 | /// 13 | /// https://developer.bitcoin.org/reference/p2p_networking.html#getblocks 14 | pub const GetblocksMessage = struct { 15 | version: i32, 16 | header_hashes: [][32]u8, 17 | stop_hash: [32]u8, 18 | 19 | const Self = @This(); 20 | 21 | pub fn name() *const [12]u8 { 22 | return protocol.CommandNames.GETBLOCKS ++ [_]u8{0} ** 5; 23 | } 24 | 25 | /// Returns the message checksum 26 | /// 27 | /// Computed as `Sha256(Sha256(self.serialize()))[0..4]` 28 | pub fn checksum(self: *const GetblocksMessage) [4]u8 { 29 | return genericChecksum(self); 30 | } 31 | 32 | /// Free the `header_hashes` 33 | pub fn deinit(self: *GetblocksMessage, allocator: std.mem.Allocator) void { 34 | allocator.free(self.header_hashes); 35 | } 36 | 37 | /// Serialize the message as bytes and write them to the Writer. 38 | /// 39 | /// `w` should be a valid `Writer`. 40 | pub fn serializeToWriter(self: *const GetblocksMessage, w: anytype) !void { 41 | comptime { 42 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects r to have fn 'writeInt'."); 43 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects r to have fn 'writeAll'."); 44 | } 45 | 46 | try w.writeInt(i32, self.version, .little); 47 | const compact_hash_count = CompactSizeUint.new(self.header_hashes.len); 48 | try compact_hash_count.encodeToWriter(w); 49 | for (self.header_hashes) |header_hash| { 50 | try w.writeAll(&header_hash); 51 | } 52 | try w.writeAll(&self.stop_hash); 53 | } 54 | 55 | /// Serialize a message as bytes and return them. 56 | pub fn serialize(self: *const GetblocksMessage, allocator: std.mem.Allocator) ![]u8 { 57 | return genericSerialize(self, allocator); 58 | } 59 | 60 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !GetblocksMessage { 61 | comptime { 62 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 63 | if (!std.meta.hasFn(@TypeOf(r), "readNoEof")) @compileError("Expects r to have fn 'readNoEof'."); 64 | } 65 | 66 | var gb: GetblocksMessage = undefined; 67 | 68 | gb.version = try r.readInt(i32, .little); 69 | 70 | // Read CompactSize hash_count 71 | const compact_hash_count = try CompactSizeUint.decodeReader(r); 72 | 73 | // Allocate space for header_hashes based on hash_count 74 | const header_hashes = try allocator.alloc([32]u8, compact_hash_count.value()); 75 | 76 | for (header_hashes) |*hash| { 77 | try r.readNoEof(hash); 78 | } 79 | gb.header_hashes = header_hashes; 80 | 81 | // Read the stop_hash (32 bytes) 82 | try r.readNoEof(&gb.stop_hash); 83 | return gb; 84 | } 85 | 86 | /// Deserialize bytes into a `GetblocksMessage` 87 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 88 | return genericDeserializeSlice(Self, allocator, bytes); 89 | } 90 | 91 | pub fn hintSerializedLen(self: *const GetblocksMessage) usize { 92 | const fixed_length = 4 + 32; // version (4 bytes) + stop_hash (32 bytes) 93 | const compact_hash_count_len = CompactSizeUint.new(self.header_hashes.len).hint_encoded_len(); 94 | const header_hashes_len = self.header_hashes.len * 32; // hash (32 bytes) 95 | return fixed_length + compact_hash_count_len + header_hashes_len; 96 | } 97 | 98 | pub fn eql(self: *const GetblocksMessage, other: *const GetblocksMessage) bool { 99 | if (self.version != other.version or self.header_hashes.len != other.header_hashes.len) { 100 | return false; 101 | } 102 | 103 | if (self.header_hashes.len != other.header_hashes.len) { 104 | return false; 105 | } 106 | 107 | for (0..self.header_hashes.len) |i| { 108 | if (!std.mem.eql(u8, self.header_hashes[i][0..], other.header_hashes[i][0..])) { 109 | return false; 110 | } 111 | } 112 | 113 | if (!std.mem.eql(u8, &self.stop_hash, &other.stop_hash)) { 114 | return false; 115 | } 116 | 117 | return true; 118 | } 119 | }; 120 | 121 | // TESTS 122 | test "ok_full_flow_GetBlocksMessage" { 123 | const allocator = std.testing.allocator; 124 | 125 | // With some header_hashes 126 | { 127 | const gb = GetblocksMessage{ 128 | .version = 42, 129 | .header_hashes = try allocator.alloc([32]u8, 2), 130 | .stop_hash = [_]u8{0} ** 32, 131 | }; 132 | defer allocator.free(gb.header_hashes); 133 | 134 | // Fill in the header_hashes 135 | 136 | for (gb.header_hashes) |*hash| { 137 | for (hash) |*byte| { 138 | byte.* = 0xab; 139 | } 140 | } 141 | 142 | const payload = try gb.serialize(allocator); 143 | defer allocator.free(payload); 144 | 145 | const deserialized_gb = try GetblocksMessage.deserializeSlice(allocator, payload); 146 | 147 | try std.testing.expect(gb.eql(&deserialized_gb)); 148 | defer allocator.free(deserialized_gb.header_hashes); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/network/protocol/messages/getblocktxn.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | 4 | const BlockHeader = @import("../../../types/block_header.zig"); 5 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 6 | const genericChecksum = @import("lib.zig").genericChecksum; 7 | const genericSerialize = @import("lib.zig").genericSerialize; 8 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 9 | /// GetBlockTxnMessage represents the "GetBlockTxn" message 10 | /// 11 | /// https://developer.bitcoin.org/reference/p2p_networking.html#getblocktxn 12 | pub const GetBlockTxnMessage = struct { 13 | block_hash: [32]u8, 14 | indexes: []u64, 15 | 16 | const Self = @This(); 17 | 18 | pub fn name() *const [12]u8 { 19 | return protocol.CommandNames.GETBLOCKTXN ++ [_]u8{0}; 20 | } 21 | 22 | /// Returns the message checksum 23 | pub fn checksum(self: *const Self) [4]u8 { 24 | return genericChecksum(self); 25 | } 26 | 27 | /// Free the allocated memory 28 | pub fn deinit(self: *const Self, allocator: std.mem.Allocator) void { 29 | allocator.free(self.indexes); 30 | } 31 | 32 | /// Serialize the message as bytes and write them to the Writer. 33 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 34 | comptime { 35 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects r to have fn 'writeAll"); 36 | } 37 | try w.writeAll(&self.block_hash); 38 | const indexes_count = CompactSizeUint.new(self.indexes.len); 39 | try indexes_count.encodeToWriter(w); 40 | for (self.indexes) |*index| { 41 | const compact_index = CompactSizeUint.new(index.*); 42 | try compact_index.encodeToWriter(w); 43 | } 44 | } 45 | 46 | /// Serialize a message as bytes and write them to the buffer. 47 | /// 48 | /// buffer.len must be >= than self.hintSerializedLen() 49 | pub fn serializeToSlice(self: *const Self, buffer: []u8) !void { 50 | var fbs = std.io.fixedBufferStream(buffer); 51 | try self.serializeToWriter(fbs.writer()); 52 | } 53 | 54 | /// Serialize a message as bytes and return them. 55 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 56 | return try genericSerialize(self, allocator); 57 | } 58 | 59 | /// Returns the hint of the serialized length of the message. 60 | pub fn hintSerializedLen(self: *const Self) usize { 61 | // 32 bytes for the block hash 62 | const fixed_length = 32; 63 | 64 | const indexes_count_length: usize = CompactSizeUint.new(self.indexes.len).hint_encoded_len(); 65 | 66 | var compact_indexes_length: usize = 0; 67 | for (self.indexes) |index| { 68 | compact_indexes_length += CompactSizeUint.new(index).hint_encoded_len(); 69 | } 70 | 71 | const variable_length = indexes_count_length + compact_indexes_length; 72 | 73 | return fixed_length + variable_length; 74 | } 75 | 76 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 77 | var blockhash: [32]u8 = undefined; 78 | try r.readNoEof(&blockhash); 79 | 80 | const indexes_count = try CompactSizeUint.decodeReader(r); 81 | const indexes = try allocator.alloc(u64, indexes_count.value()); 82 | errdefer allocator.free(indexes); 83 | 84 | for (indexes) |*index| { 85 | const compact_index = try CompactSizeUint.decodeReader(r); 86 | index.* = compact_index.value(); 87 | } 88 | 89 | return new(blockhash, indexes); 90 | } 91 | 92 | /// Deserialize bytes into a `GetBlockTxnMessage` 93 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 94 | return try genericDeserializeSlice(GetBlockTxnMessage, allocator, bytes); 95 | } 96 | 97 | pub fn new(block_hash: [32]u8, indexes: []u64) Self { 98 | return .{ 99 | .block_hash = block_hash, 100 | .indexes = indexes, 101 | }; 102 | } 103 | }; 104 | 105 | test "GetBlockTxnMessage serialization and deserialization" { 106 | const test_allocator = std.testing.allocator; 107 | 108 | const block_hash: [32]u8 = [_]u8{0} ** 32; 109 | const indexes = try test_allocator.alloc(u64, 1); 110 | indexes[0] = 123; 111 | const msg = GetBlockTxnMessage.new(block_hash, indexes); 112 | 113 | defer msg.deinit(test_allocator); 114 | 115 | const serialized = try msg.serialize(test_allocator); 116 | defer test_allocator.free(serialized); 117 | 118 | const deserialized = try GetBlockTxnMessage.deserializeSlice(test_allocator, serialized); 119 | defer deserialized.deinit(test_allocator); 120 | 121 | try std.testing.expectEqual(msg.block_hash, deserialized.block_hash); 122 | try std.testing.expectEqual(msg.indexes[0], msg.indexes[0]); 123 | try std.testing.expectEqual(msg.hintSerializedLen(), 32 + 1 + 1); 124 | } 125 | -------------------------------------------------------------------------------- /src/network/protocol/messages/getdata.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 3 | const message = @import("./lib.zig"); 4 | const genericChecksum = @import("lib.zig").genericChecksum; 5 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 6 | const genericSerialize = @import("lib.zig").genericSerialize; 7 | 8 | const Sha256 = std.crypto.hash.sha2.Sha256; 9 | 10 | const protocol = @import("../lib.zig"); 11 | 12 | pub const GetdataMessage = struct { 13 | inventory: []const protocol.InventoryItem, 14 | const Self = @This(); 15 | 16 | pub inline fn name() *const [12]u8 { 17 | return protocol.CommandNames.GETDATA ++ [_]u8{0} ** 5; 18 | } 19 | 20 | /// Returns the message checksum 21 | /// 22 | /// Computed as `Sha256(Sha256(self.serialize()))[0..4]` 23 | pub fn checksum(self: *const GetdataMessage) [4]u8 { 24 | return genericChecksum(self); 25 | } 26 | 27 | /// Free the `inventory` 28 | pub fn deinit(self: GetdataMessage, allocator: std.mem.Allocator) void { 29 | allocator.free(self.inventory); 30 | } 31 | 32 | /// Serialize the message as bytes and write them to the Writer. 33 | /// 34 | /// `w` should be a valid `Writer`. 35 | pub fn serializeToWriter(self: *const GetdataMessage, w: anytype) !void { 36 | const count = CompactSizeUint.new(self.inventory.len); 37 | try count.encodeToWriter(w); 38 | 39 | for (self.inventory) |item| { 40 | try item.encodeToWriter(w); 41 | } 42 | } 43 | 44 | pub fn serialize(self: *const GetdataMessage, allocator: std.mem.Allocator) ![]u8 { 45 | return genericSerialize(self, allocator); 46 | } 47 | 48 | /// Serialize a message as bytes and write them to the buffer. 49 | /// 50 | /// buffer.len must be >= than self.hintSerializedLen() 51 | pub fn serializeToSlice(self: *const GetdataMessage, buffer: []u8) !void { 52 | var fbs = std.io.fixedBufferStream(buffer); 53 | const writer = fbs.writer(); 54 | try self.serializeToWriter(writer); 55 | } 56 | 57 | pub fn hintSerializedLen(self: *const GetdataMessage) usize { 58 | var length: usize = 0; 59 | 60 | // Adding the length of CompactSizeUint for the count 61 | const count = CompactSizeUint.new(self.inventory.len); 62 | length += count.hint_encoded_len(); 63 | 64 | // Adding the length of each inventory item 65 | length += self.inventory.len * (4 + 32); // Type (4 bytes) + Hash (32 bytes) 66 | 67 | return length; 68 | } 69 | 70 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !GetdataMessage { 71 | 72 | const compact_count = try CompactSizeUint.decodeReader(r); 73 | const count = compact_count.value(); 74 | if (count == 0) { 75 | return GetdataMessage{ 76 | .inventory = &[_]protocol.InventoryItem{}, 77 | }; 78 | } 79 | 80 | const inventory = try allocator.alloc(protocol.InventoryItem, count); 81 | errdefer allocator.free(inventory); 82 | 83 | for (inventory) |*item| { 84 | item.* = try protocol.InventoryItem.decodeReader(r); 85 | } 86 | 87 | return GetdataMessage{ 88 | .inventory = inventory, 89 | }; 90 | } 91 | 92 | /// Deserialize bytes into a `GetdataMessage` 93 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 94 | return genericDeserializeSlice(Self, allocator, bytes); 95 | } 96 | 97 | 98 | pub fn eql(self: *const GetdataMessage, other: *const GetdataMessage) bool { 99 | if (self.inventory.len != other.inventory.len) return false; 100 | 101 | for (0..self.inventory.len) |i| { 102 | const item_self = self.inventory[i]; 103 | const item_other = other.inventory[i]; 104 | if (!item_self.eql(&item_other)) { 105 | return false; 106 | } 107 | } 108 | 109 | return true; 110 | } 111 | }; 112 | 113 | 114 | // TESTS 115 | 116 | test "ok_full_flow_GetdataMessage" { 117 | const allocator = std.testing.allocator; 118 | 119 | // With some inventory items 120 | { 121 | const inventory_items = [_]protocol.InventoryItem{ 122 | .{ .type = 1, .hash = [_]u8{0xab} ** 32 }, 123 | .{ .type = 2, .hash = [_]u8{0xcd} ** 32 }, 124 | .{ .type = 2, .hash = [_]u8{0xef} ** 32 }, 125 | }; 126 | 127 | const gd = GetdataMessage{ 128 | .inventory = inventory_items[0..], 129 | }; 130 | 131 | const payload = try gd.serialize(allocator); 132 | defer allocator.free(payload); 133 | 134 | const deserialized_gd = try GetdataMessage.deserializeSlice(allocator, payload); 135 | 136 | try std.testing.expect(gd.eql(&deserialized_gd)); 137 | 138 | // Free allocated memory for deserialized inventory 139 | defer allocator.free(deserialized_gd.inventory); 140 | } 141 | } -------------------------------------------------------------------------------- /src/network/protocol/messages/headers.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const genericChecksum = @import("lib.zig").genericChecksum; 4 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 5 | 6 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 7 | 8 | const BlockHeader = @import("../../../types/lib.zig").BlockHeader; 9 | 10 | /// HeadersMessage represents the "headers" message 11 | /// 12 | /// https://developer.bitcoin.org/reference/p2p_networking.html#headers 13 | pub const HeadersMessage = struct { 14 | headers: []BlockHeader, 15 | 16 | const Self = @This(); 17 | 18 | pub inline fn name() *const [12]u8 { 19 | return protocol.CommandNames.HEADERS ++ [_]u8{0} ** 5; 20 | } 21 | 22 | pub fn checksum(self: HeadersMessage) [4]u8 { 23 | return genericChecksum(self); 24 | } 25 | 26 | pub fn deinit(self: *HeadersMessage, allocator: std.mem.Allocator) void { 27 | allocator.free(self.headers); 28 | } 29 | 30 | /// Serialize the message as bytes and write them to the Writer. 31 | /// 32 | /// `w` should be a valid `Writer`. 33 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 34 | comptime { 35 | if (!std.meta.hasFn(@TypeOf(w), "writeByte")) @compileError("Expects r to have fn 'writeByte'."); 36 | } 37 | try CompactSizeUint.new(self.headers.len).encodeToWriter(w); 38 | 39 | for (self.headers) |header| { 40 | try header.serializeToWriter(w); 41 | try w.writeByte(0); 42 | } 43 | } 44 | 45 | /// Serialize a message as bytes and write them to the buffer. 46 | /// 47 | /// buffer.len must be >= than self.hintSerializedLen() 48 | pub fn serializeToSlice(self: *const Self, buffer: []u8) !void { 49 | var fbs = std.io.fixedBufferStream(buffer); 50 | try self.serializeToWriter(fbs.writer()); 51 | } 52 | 53 | /// Serialize a message as bytes and return them. 54 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 55 | const serialized_len = self.hintSerializedLen(); 56 | if (serialized_len != 0) { 57 | const ret = try allocator.alloc(u8, serialized_len); 58 | errdefer allocator.free(ret); 59 | 60 | try self.serializeToSlice(ret); 61 | 62 | return ret; 63 | } else { 64 | return &.{}; 65 | } 66 | } 67 | 68 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 69 | comptime { 70 | if (!std.meta.hasFn(@TypeOf(r), "readByte")) @compileError("Expects r to have fn 'readByte'."); 71 | } 72 | 73 | const headers_count = try CompactSizeUint.decodeReader(r); 74 | 75 | var headers = try allocator.alloc(BlockHeader, headers_count.value()); 76 | errdefer allocator.free(headers); 77 | 78 | for (0..headers_count.value()) |i| { 79 | headers[i] = try BlockHeader.deserializeReader(r); 80 | _ = try r.readByte(); 81 | } 82 | 83 | return Self{ .headers = headers }; 84 | } 85 | 86 | /// Deserialize bytes into a `HeaderMessage` 87 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 88 | return genericDeserializeSlice(Self, allocator, bytes); 89 | } 90 | 91 | pub fn hintSerializedLen(self: Self) usize { 92 | const headers_number_length = CompactSizeUint.new(self.headers.len).hint_encoded_len(); 93 | const headers_length = self.headers.len * (BlockHeader.serializedLen() + 1); 94 | return headers_number_length + headers_length; 95 | } 96 | }; 97 | 98 | // TESTS 99 | 100 | test "ok_fullflow_headers_message" { 101 | const allocator = std.testing.allocator; 102 | 103 | { 104 | // payload example from https://developer.bitcoin.org/reference/p2p_networking.html#headers 105 | const payload = [_]u8{ 106 | 0x01, // header count 107 | // block header 108 | 0x02, 0x00, 0x00, 0x00, // block version: 2 109 | 0xb6, 0xff, 0x0b, 0x1b, 0x16, 0x80, 0xa2, 0x86, // hash of previous block 110 | 0x2a, 0x30, 0xca, 0x44, 0xd3, 0x46, 0xd9, 0xe8, // hash of previous block 111 | 0x91, 0x0d, 0x33, 0x4b, 0xeb, 0x48, 0xca, 0x0c, // hash of previous block 112 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hash of previous block 113 | 0x9d, 0x10, 0xaa, 0x52, 0xee, 0x94, 0x93, 0x86, // merkle root 114 | 0xca, 0x93, 0x85, 0x69, 0x5f, 0x04, 0xed, 0xe2, // merkle root 115 | 0x70, 0xdd, 0xa2, 0x08, 0x10, 0xde, 0xcd, 0x12, // merkle root 116 | 0xbc, 0x9b, 0x04, 0x8a, 0xaa, 0xb3, 0x14, 0x71, // merkle root 117 | 0x24, 0xd9, 0x5a, 0x54, // unix time (1415239972) 118 | 0x30, 0xc3, 0x1b, 0x18, // bits 119 | 0xfe, 0x9f, 0x08, 0x64, // nonce 120 | // end of block header 121 | 0x00, // transaction count 122 | }; 123 | 124 | var deserialized_msg = try HeadersMessage.deserializeSlice(allocator, &payload); 125 | defer deserialized_msg.deinit(allocator); 126 | 127 | const expected_block_header = BlockHeader{ 128 | .version = 2, 129 | .prev_block = [_]u8{ 130 | 0xb6, 0xff, 0x0b, 0x1b, 0x16, 0x80, 0xa2, 0x86, 131 | 0x2a, 0x30, 0xca, 0x44, 0xd3, 0x46, 0xd9, 0xe8, 132 | 0x91, 0x0d, 0x33, 0x4b, 0xeb, 0x48, 0xca, 0x0c, 133 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 134 | }, 135 | .merkle_root = [_]u8{ 136 | 0x9d, 0x10, 0xaa, 0x52, 0xee, 0x94, 0x93, 0x86, 137 | 0xca, 0x93, 0x85, 0x69, 0x5f, 0x04, 0xed, 0xe2, 138 | 0x70, 0xdd, 0xa2, 0x08, 0x10, 0xde, 0xcd, 0x12, 139 | 0xbc, 0x9b, 0x04, 0x8a, 0xaa, 0xb3, 0x14, 0x71, 140 | }, 141 | .timestamp = 1415239972, 142 | .nbits = 404472624, 143 | .nonce = 1678286846, 144 | }; 145 | 146 | try std.testing.expectEqual(1, deserialized_msg.headers.len); 147 | try std.testing.expect(expected_block_header.eql(&deserialized_msg.headers[0])); 148 | 149 | const serialized_payload = try deserialized_msg.serialize(allocator); 150 | defer allocator.free(serialized_payload); 151 | 152 | try std.testing.expect(std.mem.eql(u8, &payload, serialized_payload)); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/network/protocol/messages/inv.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 3 | const message = @import("./lib.zig"); 4 | const genericChecksum = @import("lib.zig").genericChecksum; 5 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 6 | 7 | const Sha256 = std.crypto.hash.sha2.Sha256; 8 | 9 | const protocol = @import("../lib.zig"); 10 | 11 | pub const InvMessage = struct { 12 | inventory: []const protocol.InventoryItem, 13 | const Self = @This(); 14 | 15 | pub inline fn name() *const [12]u8 { 16 | return protocol.CommandNames.INV ++ [_]u8{0} ** 5; 17 | } 18 | 19 | /// Returns the message checksum 20 | /// 21 | /// Computed as `Sha256(Sha256(self.serialize()))[0..4]` 22 | pub fn checksum(self: *const InvMessage) [4]u8 { 23 | return genericChecksum(self); 24 | } 25 | 26 | /// Free the `inventory` 27 | pub fn deinit(self: InvMessage, allocator: std.mem.Allocator) void { 28 | allocator.free(self.inventory); 29 | } 30 | 31 | /// Serialize the message as bytes and write them to the Writer. 32 | /// 33 | /// `w` should be a valid `Writer`. 34 | pub fn serializeToWriter(self: *const InvMessage, w: anytype) !void { 35 | const count = CompactSizeUint.new(self.inventory.len); 36 | try count.encodeToWriter(w); 37 | 38 | for (self.inventory) |item| { 39 | try item.encodeToWriter(w); 40 | } 41 | } 42 | 43 | pub fn serialize(self: *const InvMessage, allocator: std.mem.Allocator) ![]u8 { 44 | const serialized_len = self.hintSerializedLen(); 45 | 46 | const ret = try allocator.alloc(u8, serialized_len); 47 | errdefer allocator.free(ret); 48 | 49 | try self.serializeToSlice(ret); 50 | 51 | return ret; 52 | } 53 | 54 | /// Serialize a message as bytes and write them to the buffer. 55 | /// 56 | /// buffer.len must be >= than self.hintSerializedLen() 57 | pub fn serializeToSlice(self: *const InvMessage, buffer: []u8) !void { 58 | var fbs = std.io.fixedBufferStream(buffer); 59 | const writer = fbs.writer(); 60 | try self.serializeToWriter(writer); 61 | } 62 | 63 | pub fn hintSerializedLen(self: *const InvMessage) usize { 64 | var length: usize = 0; 65 | 66 | // Adding the length of CompactSizeUint for the count 67 | const count = CompactSizeUint.new(self.inventory.len); 68 | length += count.hint_encoded_len(); 69 | 70 | // Adding the length of each inventory item 71 | length += self.inventory.len * (4 + 32); // Type (4 bytes) + Hash (32 bytes) 72 | 73 | return length; 74 | } 75 | 76 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !InvMessage { 77 | 78 | const compact_count = try CompactSizeUint.decodeReader(r); 79 | const count = compact_count.value(); 80 | if (count == 0) { 81 | return InvMessage{ 82 | .inventory = &[_]protocol.InventoryItem{}, 83 | }; 84 | } 85 | 86 | const inventory = try allocator.alloc(protocol.InventoryItem, count); 87 | errdefer allocator.free(inventory); 88 | 89 | for (inventory) |*item| { 90 | item.* = try protocol.InventoryItem.decodeReader(r); 91 | } 92 | 93 | return InvMessage{ 94 | .inventory = inventory, 95 | }; 96 | } 97 | 98 | /// Deserialize bytes into a `InvMessage` 99 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 100 | return genericDeserializeSlice(Self, allocator, bytes); 101 | } 102 | 103 | 104 | pub fn eql(self: *const InvMessage, other: *const InvMessage) bool { 105 | if (self.inventory.len != other.inventory.len) return false; 106 | 107 | for (0..self.inventory.len) |i| { 108 | const item_self = self.inventory[i]; 109 | const item_other = other.inventory[i]; 110 | if (!item_self.eql(&item_other)) { 111 | return false; 112 | } 113 | } 114 | 115 | return true; 116 | } 117 | }; 118 | 119 | 120 | // TESTS 121 | test "ok_full_flow_inv_message" { 122 | const allocator = std.testing.allocator; 123 | 124 | // With some inventory items 125 | { 126 | const inventory_items = [_]protocol.InventoryItem{ 127 | .{ .type = 1, .hash = [_]u8{0xab} ** 32 }, 128 | .{ .type = 2, .hash = [_]u8{0xcd} ** 32 }, 129 | .{ .type = 2, .hash = [_]u8{0xef} ** 32 }, 130 | }; 131 | 132 | const gd = InvMessage{ 133 | .inventory = inventory_items[0..], 134 | }; 135 | 136 | const payload = try gd.serialize(allocator); 137 | defer allocator.free(payload); 138 | 139 | const deserialized_gd = try InvMessage.deserializeSlice(allocator, payload); 140 | 141 | try std.testing.expect(gd.eql(&deserialized_gd)); 142 | 143 | // Free allocated memory for deserialized inventory 144 | defer allocator.free(deserialized_gd.inventory); 145 | } 146 | } -------------------------------------------------------------------------------- /src/network/protocol/messages/lib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | pub const VersionMessage = @import("version.zig").VersionMessage; 3 | pub const VerackMessage = @import("verack.zig").VerackMessage; 4 | pub const MempoolMessage = @import("mempool.zig").MempoolMessage; 5 | pub const GetaddrMessage = @import("getaddr.zig").GetaddrMessage; 6 | pub const BlockMessage = @import("block.zig").BlockMessage; 7 | pub const GetblocksMessage = @import("getblocks.zig").GetblocksMessage; 8 | pub const PingMessage = @import("ping.zig").PingMessage; 9 | pub const PongMessage = @import("pong.zig").PongMessage; 10 | pub const AddrMessage = @import("addr.zig").AddrMessage; 11 | pub const MerkleBlockMessage = @import("merkleblock.zig").MerkleBlockMessage; 12 | pub const FeeFilterMessage = @import("feefilter.zig").FeeFilterMessage; 13 | pub const SendCmpctMessage = @import("sendcmpct.zig").SendCmpctMessage; 14 | pub const FilterClearMessage = @import("filterclear.zig").FilterClearMessage; 15 | pub const GetdataMessage = @import("getdata.zig").GetdataMessage; 16 | pub const Block = @import("block.zig").BlockMessage; 17 | pub const FilterAddMessage = @import("filteradd.zig").FilterAddMessage; 18 | const Sha256 = std.crypto.hash.sha2.Sha256; 19 | pub const NotFoundMessage = @import("notfound.zig").NotFoundMessage; 20 | pub const SendHeadersMessage = @import("sendheaders.zig").SendHeadersMessage; 21 | pub const FilterLoadMessage = @import("filterload.zig").FilterLoadMessage; 22 | pub const GetBlockTxnMessage = @import("getblocktxn.zig").GetBlockTxnMessage; 23 | pub const HeadersMessage = @import("headers.zig").HeadersMessage; 24 | pub const CmpctBlockMessage = @import("cmpctblock.zig").CmpctBlockMessage; 25 | pub const InvMessage = @import("inv.zig").InvMessage; 26 | 27 | pub const MessageTypes = enum { 28 | version, 29 | verack, 30 | mempool, 31 | getaddr, 32 | getblocks, 33 | ping, 34 | pong, 35 | addr, 36 | merkleblock, 37 | sendcmpct, 38 | feefilter, 39 | filterclear, 40 | block, 41 | filteradd, 42 | notfound, 43 | sendheaders, 44 | filterload, 45 | getblocktxn, 46 | getdata, 47 | headers, 48 | cmpctblock, 49 | inv 50 | }; 51 | 52 | 53 | pub const Message = union(MessageTypes) { 54 | version: VersionMessage, 55 | verack: VerackMessage, 56 | mempool: MempoolMessage, 57 | getaddr: GetaddrMessage, 58 | getblocks: GetblocksMessage, 59 | ping: PingMessage, 60 | pong: PongMessage, 61 | addr: AddrMessage, 62 | merkleblock: MerkleBlockMessage, 63 | sendcmpct: SendCmpctMessage, 64 | feefilter: FeeFilterMessage, 65 | filterclear: FilterClearMessage, 66 | block: Block, 67 | filteradd: FilterAddMessage, 68 | notfound: NotFoundMessage, 69 | sendheaders: SendHeadersMessage, 70 | filterload: FilterLoadMessage, 71 | getblocktxn: GetBlockTxnMessage, 72 | getdata: GetdataMessage, 73 | headers: HeadersMessage, 74 | cmpctblock: CmpctBlockMessage, 75 | inv: InvMessage, 76 | 77 | pub fn name(self: Message) *const [12]u8 { 78 | return switch (self) { 79 | .version => |m| @TypeOf(m).name(), 80 | .verack => |m| @TypeOf(m).name(), 81 | .mempool => |m| @TypeOf(m).name(), 82 | .getaddr => |m| @TypeOf(m).name(), 83 | .getblocks => |m| @TypeOf(m).name(), 84 | .ping => |m| @TypeOf(m).name(), 85 | .pong => |m| @TypeOf(m).name(), 86 | .addr => |m| @TypeOf(m).name(), 87 | .merkleblock => |m| @TypeOf(m).name(), 88 | .sendcmpct => |m| @TypeOf(m).name(), 89 | .feefilter => |m| @TypeOf(m).name(), 90 | .filterclear => |m| @TypeOf(m).name(), 91 | .block => |m| @TypeOf(m).name(), 92 | .filteradd => |m| @TypeOf(m).name(), 93 | .notfound => |m| @TypeOf(m).name(), 94 | .sendheaders => |m| @TypeOf(m).name(), 95 | .filterload => |m| @TypeOf(m).name(), 96 | .getblocktxn => |m| @TypeOf(m).name(), 97 | .getdata => |m| @TypeOf(m).name(), 98 | .headers => |m| @TypeOf(m).name(), 99 | .cmpctblock => |m| @TypeOf(m).name(), 100 | .inv => |m| @TypeOf(m).name(), 101 | }; 102 | } 103 | 104 | pub fn deinit(self: *Message, allocator: std.mem.Allocator) void { 105 | switch (self.*) { 106 | .version => |*m| m.deinit(allocator), 107 | .getblocks => |*m| m.deinit(allocator), 108 | .ping => {}, 109 | .pong => {}, 110 | .addr => |m| m.deinit(allocator), 111 | .merkleblock => |*m| m.deinit(allocator), 112 | .block => |*m| m.deinit(allocator), 113 | .filteradd => |*m| m.deinit(allocator), 114 | .getdata => |*m| m.deinit(allocator), 115 | .cmpctblock => |*m| m.deinit(allocator), 116 | .sendheaders => {}, 117 | .filterload => {}, 118 | .getblocktxn => |*m| m.deinit(allocator), 119 | .headers => |*m| m.deinit(allocator), 120 | .inv => |*m| m.deinit(allocator), 121 | else => {} 122 | } 123 | } 124 | 125 | pub fn checksum(self: *Message) [4]u8 { 126 | return switch (self.*) { 127 | .version => |*m| m.checksum(), 128 | .verack => |*m| m.checksum(), 129 | .mempool => |*m| m.checksum(), 130 | .getaddr => |*m| m.checksum(), 131 | .getblocks => |*m| m.checksum(), 132 | .ping => |*m| m.checksum(), 133 | .pong => |*m| m.checksum(), 134 | .merkleblock => |*m| m.checksum(), 135 | .sendcmpct => |*m| m.checksum(), 136 | .feefilter => |*m| m.checksum(), 137 | .filterclear => |*m| m.checksum(), 138 | .block => |*m| m.checksum(), 139 | .filteradd => |*m| m.checksum(), 140 | .notfound => |*m| m.checksum(), 141 | .sendheaders => |*m| m.checksum(), 142 | .filterload => |*m| m.checksum(), 143 | .getblocktxn => |*m| m.checksum(), 144 | .addr => |*m| m.checksum(), 145 | .getdata => |*m| m.checksum(), 146 | .headers => |*m| m.checksum(), 147 | .cmpctblock => |*m| m.checksum(), 148 | .inv => |*m| m.checksum(), 149 | }; 150 | } 151 | 152 | pub fn hintSerializedLen(self: *Message) usize { 153 | return switch (self.*) { 154 | .version => |*m| m.hintSerializedLen(), 155 | .verack => |*m| m.hintSerializedLen(), 156 | .mempool => |*m| m.hintSerializedLen(), 157 | .getaddr => |*m| m.hintSerializedLen(), 158 | .getblocks => |*m| m.hintSerializedLen(), 159 | .ping => |*m| m.hintSerializedLen(), 160 | .pong => |*m| m.hintSerializedLen(), 161 | .merkleblock => |*m| m.hintSerializedLen(), 162 | .sendcmpct => |*m| m.hintSerializedLen(), 163 | .feefilter => |*m| m.hintSerializedLen(), 164 | .filterclear => |*m| m.hintSerializedLen(), 165 | .block => |*m| m.hintSerializedLen(), 166 | .filteradd => |*m| m.hintSerializedLen(), 167 | .notfound => |m| m.hintSerializedLen(), 168 | .sendheaders => |m| m.hintSerializedLen(), 169 | .filterload => |*m| m.hintSerializedLen(), 170 | .getblocktxn => |*m| m.hintSerializedLen(), 171 | .addr => |*m| m.hintSerializedLen(), 172 | .getdata => |m| m.hintSerializedLen(), 173 | .headers => |*m| m.hintSerializedLen(), 174 | .cmpctblock => |*m| m.hintSerializedLen(), 175 | .inv => |*m| m.hintSerializedLen(), 176 | }; 177 | } 178 | }; 179 | 180 | pub const default_checksum: [4]u8 = [_]u8{ 0x5d, 0xf6, 0xe0, 0xe2 }; 181 | 182 | pub fn genericChecksum(m: anytype) [4]u8 { 183 | comptime { 184 | if (!std.meta.hasMethod(@TypeOf(m), "serializeToWriter")) @compileError("Expects m to have fn 'serializeToWriter'."); 185 | } 186 | 187 | var digest: [32]u8 = undefined; 188 | var hasher = Sha256.init(.{}); 189 | m.serializeToWriter(hasher.writer()) catch unreachable; 190 | hasher.final(&digest); 191 | 192 | Sha256.hash(&digest, &digest, .{}); 193 | 194 | return digest[0..4].*; 195 | } 196 | 197 | pub fn genericSerialize(m: anytype, allocator: std.mem.Allocator) ![]u8 { 198 | comptime { 199 | if (!std.meta.hasMethod(@TypeOf(m), "hintSerializedLen")) @compileError("Expects m to have fn 'hintSerializedLen'."); 200 | if (!std.meta.hasMethod(@TypeOf(m), "serializeToWriter")) @compileError("Expects m to have fn 'serializeToWriter'."); 201 | } 202 | const serialized_len = m.hintSerializedLen(); 203 | 204 | const buffer = try allocator.alloc(u8, serialized_len); 205 | errdefer allocator.free(buffer); 206 | 207 | var fbs = std.io.fixedBufferStream(buffer); 208 | try m.serializeToWriter(fbs.writer()); 209 | 210 | return buffer; 211 | } 212 | 213 | pub fn genericDeserializeSlice(comptime T: type, allocator: std.mem.Allocator, bytes: []const u8) !T { 214 | if (!std.meta.hasMethod(T, "deserializeReader")) @compileError("Expects T to have fn 'deserializeReader'."); 215 | 216 | var fbs = std.io.fixedBufferStream(bytes); 217 | const reader = fbs.reader(); 218 | 219 | return try T.deserializeReader(allocator, reader); 220 | } 221 | -------------------------------------------------------------------------------- /src/network/protocol/messages/mempool.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const default_checksum = @import("lib.zig").default_checksum; 4 | 5 | /// MempoolMessage represents the "mempool" message 6 | /// 7 | /// https://developer.bitcoin.org/reference/p2p_networking.html#mempool 8 | pub const MempoolMessage = struct { 9 | // mempool message do not contain any payload, thus there is no field 10 | 11 | pub fn name() *const [12]u8 { 12 | return protocol.CommandNames.MEMPOOL ++ [_]u8{0} ** 5; 13 | } 14 | 15 | pub fn checksum(self: MempoolMessage) [4]u8 { 16 | _ = self; 17 | return default_checksum; 18 | } 19 | 20 | /// Serialize a message as bytes and return them. 21 | pub fn serialize(self: *const MempoolMessage, allocator: std.mem.Allocator) ![]u8 { 22 | _ = self; 23 | _ = allocator; 24 | return &.{}; 25 | } 26 | 27 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !MempoolMessage { 28 | _ = allocator; 29 | _ = r; 30 | return MempoolMessage{}; 31 | } 32 | 33 | pub fn hintSerializedLen(self: MempoolMessage) usize { 34 | _ = self; 35 | return 0; 36 | } 37 | }; 38 | 39 | // TESTS 40 | test "ok_full_flow_MempoolMessage" { 41 | const allocator = std.testing.allocator; 42 | 43 | { 44 | const msg = MempoolMessage{}; 45 | 46 | const payload = try msg.serialize(allocator); 47 | defer allocator.free(payload); 48 | const deserialized_msg = try MempoolMessage.deserializeReader(allocator, payload); 49 | _ = deserialized_msg; 50 | 51 | try std.testing.expect(payload.len == 0); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/network/protocol/messages/merkleblock.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | 4 | const Sha256 = std.crypto.hash.sha2.Sha256; 5 | const BlockHeader = @import("../../../types/block_header.zig"); 6 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 7 | const genericChecksum = @import("lib.zig").genericChecksum; 8 | const genericSerialize = @import("lib.zig").genericSerialize; 9 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 10 | 11 | /// MerkleBlockMessage represents the "MerkleBlock" message 12 | /// 13 | /// https://developer.bitcoin.org/reference/p2p_networking.html#merkleblock 14 | pub const MerkleBlockMessage = struct { 15 | block_header: BlockHeader, 16 | transaction_count: u32, 17 | hashes: [][32]u8, 18 | flags: []u8, 19 | 20 | const Self = @This(); 21 | 22 | pub fn name() *const [12]u8 { 23 | return protocol.CommandNames.MERKLEBLOCK; 24 | } 25 | 26 | /// Returns the message checksum 27 | pub fn checksum(self: *const Self) [4]u8 { 28 | return genericChecksum(self); 29 | } 30 | 31 | /// Free the allocated memory 32 | pub fn deinit(self: *const Self, allocator: std.mem.Allocator) void { 33 | allocator.free(self.flags); 34 | allocator.free(self.hashes); 35 | } 36 | 37 | /// Serialize the message as bytes and write them to the Writer. 38 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 39 | comptime { 40 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects w to have fn 'writeInt'."); 41 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects w to have fn 'writeAll'."); 42 | } 43 | try self.block_header.serializeToWriter(w); 44 | try w.writeInt(u32, self.transaction_count, .little); 45 | const hash_count = CompactSizeUint.new(self.hashes.len); 46 | try hash_count.encodeToWriter(w); 47 | 48 | for (self.hashes) |*hash| { 49 | try w.writeAll(hash); 50 | } 51 | const flag_bytes = CompactSizeUint.new(self.flags.len); 52 | 53 | try flag_bytes.encodeToWriter(w); 54 | try w.writeAll(self.flags); 55 | } 56 | 57 | /// Serialize a message as bytes and return them. 58 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 59 | return genericSerialize(self, allocator); 60 | } 61 | 62 | /// Returns the hint of the serialized length of the message. 63 | pub fn hintSerializedLen(self: *const Self) usize { 64 | // 80 bytes for the block header, 4 bytes for the transaction count 65 | const fixed_length = 84; 66 | const hash_count_len: usize = CompactSizeUint.new(self.hashes.len).hint_encoded_len(); 67 | const compact_hashes_len = 32 * self.hashes.len; 68 | const flag_bytes_len: usize = CompactSizeUint.new(self.flags.len).hint_encoded_len(); 69 | const flags_len = self.flags.len; 70 | const variable_length = hash_count_len + compact_hashes_len + flag_bytes_len + flags_len; 71 | return fixed_length + variable_length; 72 | } 73 | 74 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 75 | comptime { 76 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 77 | if (!std.meta.hasFn(@TypeOf(r), "readNoEof")) @compileError("Expects r to have fn 'readNoEof'."); 78 | } 79 | 80 | var merkle_block_message: Self = undefined; 81 | merkle_block_message.block_header = try BlockHeader.deserializeReader(r); 82 | merkle_block_message.transaction_count = try r.readInt(u32, .little); 83 | 84 | // Read CompactSize hash_count 85 | const hash_count = try CompactSizeUint.decodeReader(r); 86 | merkle_block_message.hashes = try allocator.alloc([32]u8, hash_count.value()); 87 | errdefer allocator.free(merkle_block_message.hashes); 88 | 89 | for (merkle_block_message.hashes) |*hash| { 90 | try r.readNoEof(hash); 91 | } 92 | 93 | // Read CompactSize flags_count 94 | const flags_count = try CompactSizeUint.decodeReader(r); 95 | merkle_block_message.flags = try allocator.alloc(u8, flags_count.value()); 96 | errdefer allocator.free(merkle_block_message.flags); 97 | 98 | try r.readNoEof(merkle_block_message.flags); 99 | return merkle_block_message; 100 | } 101 | 102 | /// Deserialize bytes into a `MerkleBlockMessage` 103 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 104 | return genericDeserializeSlice(Self, allocator, bytes); 105 | } 106 | 107 | pub fn new(block_header: BlockHeader, transaction_count: u32, hashes: [][32]u8, flags: []u8) Self { 108 | return .{ 109 | .block_header = block_header, 110 | .transaction_count = transaction_count, 111 | .hashes = hashes, 112 | .flags = flags, 113 | }; 114 | } 115 | }; 116 | 117 | test "MerkleBlockMessage serialization and deserialization" { 118 | const test_allocator = std.testing.allocator; 119 | 120 | const block_header = BlockHeader{ 121 | .version = 1, 122 | .prev_block = [_]u8{0} ** 32, 123 | .merkle_root = [_]u8{1} ** 32, 124 | .timestamp = 1234567890, 125 | .nbits = 0x1d00ffff, 126 | .nonce = 987654321, 127 | }; 128 | const hashes = try test_allocator.alloc([32]u8, 3); 129 | 130 | const flags = try test_allocator.alloc(u8, 1); 131 | const transaction_count = 1; 132 | const msg = MerkleBlockMessage.new(block_header, transaction_count, hashes, flags); 133 | 134 | defer msg.deinit(test_allocator); 135 | 136 | // Fill in the header_hashes 137 | for (msg.hashes) |*hash| { 138 | for (hash) |*byte| { 139 | byte.* = 0xab; 140 | } 141 | } 142 | 143 | flags[0] = 0x1; 144 | 145 | const serialized = try msg.serialize(test_allocator); 146 | defer test_allocator.free(serialized); 147 | 148 | const deserialized = try MerkleBlockMessage.deserializeSlice(test_allocator, serialized); 149 | defer deserialized.deinit(test_allocator); 150 | 151 | try std.testing.expectEqual(msg.block_header, deserialized.block_header); 152 | try std.testing.expectEqual(msg.transaction_count, deserialized.transaction_count); 153 | try std.testing.expectEqualSlices([32]u8, msg.hashes, deserialized.hashes); 154 | try std.testing.expectEqualSlices(u8, msg.flags, deserialized.flags); 155 | 156 | try std.testing.expectEqual(msg.hintSerializedLen(), 84 + 1 + 32 * 3 + 1 + 1); 157 | } 158 | -------------------------------------------------------------------------------- /src/network/protocol/messages/notfound.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const Sha256 = std.crypto.hash.sha2.Sha256; 4 | const genericChecksum = @import("lib.zig").genericChecksum; 5 | const genericSerialize = @import("lib.zig").genericSerialize; 6 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 7 | 8 | /// NotFoundMessage represents the "notfound" message 9 | /// 10 | /// https://developer.bitcoin.org/reference/p2p_networking.html#notfound 11 | pub const NotFoundMessage = struct { 12 | inventory: []const protocol.InventoryItem, 13 | 14 | const Self = @This(); 15 | 16 | pub fn name() *const [12]u8 { 17 | return protocol.CommandNames.NOTFOUND ++ [_]u8{0} ** 4; 18 | } 19 | 20 | /// Returns the message checksum 21 | /// 22 | /// Computed as `Sha256(Sha256(self.serialize()))[0..4]` 23 | pub fn checksum(self: *const Self) [4]u8 { 24 | return genericChecksum(self); 25 | } 26 | 27 | /// Serialize the message as bytes and write them to the Writer. 28 | pub fn serializeToWriter(self: *const Self, writer: anytype) !void { 29 | try writer.writeInt(u32, @intCast(self.inventory.len), .little); 30 | for (self.inventory) |inv| { 31 | try inv.encodeToWriter(writer); 32 | } 33 | } 34 | 35 | /// Serialize a message as bytes and return them. 36 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 37 | return genericSerialize(self, allocator); 38 | } 39 | 40 | /// Deserialize a Reader bytes as a `NotFoundMessage` 41 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 42 | comptime { 43 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 44 | } 45 | 46 | const count = try r.readInt(u32, .little); 47 | const inventory = try allocator.alloc(protocol.InventoryItem, count); 48 | errdefer allocator.free(inventory); 49 | 50 | for (inventory) |*inv| { 51 | inv.* = try protocol.InventoryItem.decodeReader(r); 52 | } 53 | 54 | return Self{ 55 | .inventory = inventory, 56 | }; 57 | } 58 | 59 | /// Deserialize bytes into a `NotFoundMessage` 60 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 61 | return genericDeserializeSlice(Self, allocator, bytes); 62 | } 63 | 64 | pub fn hintSerializedLen(self: *const Self) usize { 65 | return 4 + self.inventory.len * (4 + 32); // count (4 bytes) + (type (4 bytes) + hash (32 bytes)) * count 66 | } 67 | 68 | pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { 69 | allocator.free(self.inventory); 70 | } 71 | 72 | pub fn new(inventory: []const protocol.InventoryItem) Self { 73 | return .{ 74 | .inventory = inventory, 75 | }; 76 | } 77 | }; 78 | 79 | // TESTS 80 | test "ok_fullflow_notfound_message" { 81 | const allocator = std.testing.allocator; 82 | 83 | { 84 | const inventory = [_]protocol.InventoryItem{ 85 | .{ .type = 1, .hash = [_]u8{0xab} ** 32 }, 86 | .{ .type = 2, .hash = [_]u8{0xcd} ** 32 }, 87 | }; 88 | var msg = NotFoundMessage.new(&inventory); 89 | const payload = try msg.serialize(allocator); 90 | defer allocator.free(payload); 91 | var deserialized_msg = try NotFoundMessage.deserializeSlice(allocator, payload); 92 | defer deserialized_msg.deinit(allocator); 93 | 94 | try std.testing.expectEqual(msg.inventory.len, deserialized_msg.inventory.len); 95 | for (msg.inventory, deserialized_msg.inventory) |orig, deserialized| { 96 | try std.testing.expectEqual(orig.type, deserialized.type); 97 | try std.testing.expectEqualSlices(u8, &orig.hash, &deserialized.hash); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/network/protocol/messages/ping.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const Sha256 = std.crypto.hash.sha2.Sha256; 4 | const genericChecksum = @import("lib.zig").genericChecksum; 5 | const genericSerialize = @import("lib.zig").genericSerialize; 6 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 7 | 8 | /// PingMessage represents the "Ping" message 9 | /// 10 | /// https://developer.bitcoin.org/reference/p2p_networking.html#ping 11 | pub const PingMessage = struct { 12 | nonce: u64, 13 | 14 | const Self = @This(); 15 | 16 | pub fn name() *const [12]u8 { 17 | return protocol.CommandNames.PING ++ [_]u8{0} ** 8; 18 | } 19 | 20 | /// Returns the message checksum 21 | /// 22 | /// Computed as `Sha256(Sha256(self.serialize()))[0..4]` 23 | pub fn checksum(self: *const Self) [4]u8 { 24 | return genericChecksum(self); 25 | } 26 | 27 | /// Serialize a message as bytes and return them. 28 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 29 | return genericSerialize(self, allocator); 30 | } 31 | 32 | /// Serialize the message as bytes and write them to the Writer. 33 | /// 34 | /// `w` should be a valid `Writer`. 35 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 36 | comptime { 37 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects r to have fn 'writeInt'."); 38 | } 39 | 40 | try w.writeInt(u64, self.nonce, .little); 41 | } 42 | 43 | /// Returns the hint of the serialized length of the message 44 | pub fn hintSerializedLen(_: *const Self) usize { 45 | // 8 bytes for nonce 46 | return 8; 47 | } 48 | 49 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 50 | return genericDeserializeSlice(Self, allocator, bytes); 51 | } 52 | 53 | /// Deserialize a Reader bytes as a `VersionMessage` 54 | pub fn deserializeReader(_: std.mem.Allocator, r: anytype) !Self { 55 | comptime { 56 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 57 | } 58 | 59 | var vm: Self = undefined; 60 | 61 | vm.nonce = try r.readInt(u64, .little); 62 | return vm; 63 | } 64 | 65 | pub fn new(nonce: u64) Self { 66 | return .{ 67 | .nonce = nonce, 68 | }; 69 | } 70 | }; 71 | 72 | // TESTS 73 | test "ok_fullflow_ping_message" { 74 | const allocator = std.testing.allocator; 75 | 76 | { 77 | const msg = PingMessage.new(0x1234567890abcdef); 78 | const payload = try msg.serialize(allocator); 79 | defer allocator.free(payload); 80 | const deserialized_msg = try PingMessage.deserializeSlice(allocator, payload); 81 | try std.testing.expectEqual(msg.nonce, deserialized_msg.nonce); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/network/protocol/messages/pong.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const Sha256 = std.crypto.hash.sha2.Sha256; 4 | const genericChecksum = @import("lib.zig").genericChecksum; 5 | const genericSerialize = @import("lib.zig").genericSerialize; 6 | const genericDeserializeSlice = @import("lib.zig").genericDeserializeSlice; 7 | 8 | /// PongMessage represents the "Pong" message 9 | /// 10 | /// https://developer.bitcoin.org/reference/p2p_networking.html#pong 11 | pub const PongMessage = struct { 12 | nonce: u64, 13 | 14 | const Self = @This(); 15 | 16 | pub fn name() *const [12]u8 { 17 | return protocol.CommandNames.PONG ++ [_]u8{0} ** 8; 18 | } 19 | 20 | /// Returns the message checksum 21 | /// 22 | /// Computed as `Sha256(Sha256(self.serialize()))[0..4]` 23 | pub fn checksum(self: *const Self) [4]u8 { 24 | return genericChecksum(self); 25 | } 26 | 27 | /// Serialize a message as bytes and return them. 28 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 29 | return genericSerialize(self, allocator); 30 | } 31 | 32 | /// Serialize the message as bytes and write them to the Writer. 33 | /// 34 | /// `w` should be a valid `Writer`. 35 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 36 | comptime { 37 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects r to have fn 'writeInt'."); 38 | } 39 | 40 | try w.writeInt(u64, self.nonce, .little); 41 | } 42 | 43 | /// Returns the hint of the serialized length of the message 44 | pub fn hintSerializedLen(_: *const Self) usize { 45 | // 8 bytes for nonce 46 | return 8; 47 | } 48 | 49 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 50 | return genericDeserializeSlice(Self, allocator, bytes); 51 | } 52 | 53 | /// Deserialize a Reader bytes as a `VersionMessage` 54 | pub fn deserializeReader(_: std.mem.Allocator, r: anytype) !Self { 55 | comptime { 56 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 57 | } 58 | 59 | var vm: Self = undefined; 60 | 61 | vm.nonce = try r.readInt(u64, .little); 62 | return vm; 63 | } 64 | 65 | pub fn new(nonce: u64) Self { 66 | return .{ 67 | .nonce = nonce, 68 | }; 69 | } 70 | }; 71 | 72 | // TESTS 73 | test "ok_fullflow_pong_message" { 74 | const allocator = std.testing.allocator; 75 | 76 | { 77 | const msg = PongMessage.new(0x1234567890abcdef); 78 | const payload = try msg.serialize(allocator); 79 | defer allocator.free(payload); 80 | const deserialized_msg = try PongMessage.deserializeSlice(allocator, payload); 81 | try std.testing.expectEqual(msg.nonce, deserialized_msg.nonce); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/network/protocol/messages/sendcmpct.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | 4 | const Sha256 = std.crypto.hash.sha2.Sha256; 5 | 6 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 7 | const genericChecksum = @import("lib.zig").genericChecksum; 8 | const genericSerialize = @import("lib.zig").genericSerialize; 9 | 10 | /// SendCmpctMessage represents the "sendcmpct" message 11 | /// 12 | /// https://developer.bitcoin.org/reference/p2p_networking.html#sendcmpct 13 | pub const SendCmpctMessage = struct { 14 | announce: bool, 15 | version: u64, 16 | 17 | pub fn name() *const [12]u8 { 18 | return protocol.CommandNames.SENDCMPCT ++ [_]u8{0} ** 3; 19 | } 20 | 21 | /// Returns the message checksum 22 | /// 23 | /// Computed as `Sha256(Sha256(self.serialize()))[0..4]` 24 | pub fn checksum(self: *const SendCmpctMessage) [4]u8 { 25 | return genericChecksum(self); 26 | } 27 | /// Serialize the message as bytes and write them to the Writer. 28 | pub fn serializeToWriter(self: *const SendCmpctMessage, w: anytype) !void { 29 | comptime { 30 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects writer to have 'writeInt'."); 31 | } 32 | // Write announce (1 byte) 33 | try w.writeInt(u8, if (self.announce) 0x01 else 0x00, .little); 34 | // Write version (8 bytes, little-endian) 35 | try w.writeInt(u64, self.version, .little); 36 | } 37 | 38 | /// Serialize a message as bytes and return them. 39 | pub fn serialize(self: *const SendCmpctMessage, allocator: std.mem.Allocator) ![]u8 { 40 | return genericSerialize(self, allocator); 41 | } 42 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !SendCmpctMessage { 43 | _ = allocator; 44 | comptime { 45 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects reader to have 'readInt'."); 46 | } 47 | 48 | var msg: SendCmpctMessage = undefined; 49 | 50 | // Read announce (1 byte) 51 | const announce_byte = try r.readByte(); 52 | 53 | msg.announce = announce_byte != 0x00; 54 | 55 | // Read version (8 bytes, little-endian) 56 | msg.version = try r.readInt(u64, .little); 57 | 58 | return msg; 59 | } 60 | 61 | pub fn hintSerializedLen(self: *const SendCmpctMessage) usize { 62 | _ = self; 63 | return 1 + 8; 64 | } 65 | // Equality check 66 | pub fn eql(self: *const SendCmpctMessage, other: *const SendCmpctMessage) bool { 67 | return self.announce == other.announce and self.version == other.version; 68 | } 69 | }; 70 | 71 | // TESTS 72 | test "ok_full_flow_SendCmpctMessage" { 73 | const allocator = std.testing.allocator; 74 | 75 | const msg = SendCmpctMessage{ 76 | .announce = true, 77 | .version = 1, 78 | }; 79 | 80 | // Serialize the message 81 | const payload = try msg.serialize(allocator); 82 | defer allocator.free(payload); 83 | 84 | // Deserialize the message 85 | var fbs = std.io.fixedBufferStream(payload); 86 | const reader = fbs.reader(); 87 | const deserialized_msg = try SendCmpctMessage.deserializeReader(allocator, reader); 88 | 89 | try std.testing.expect(msg.eql(&deserialized_msg)); 90 | } 91 | -------------------------------------------------------------------------------- /src/network/protocol/messages/sendheaders.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const default_checksum = @import("lib.zig").default_checksum; 4 | 5 | /// SendHeaders represents the "getaddr" message 6 | /// 7 | /// https://developer.bitcoin.org/reference/p2p_networking.html#sendheaders 8 | pub const SendHeadersMessage = struct { 9 | // sendheaders message do not contain any payload, thus there is no field 10 | const Self = @This(); 11 | 12 | pub fn new() Self { 13 | return .{}; 14 | } 15 | 16 | pub fn name() *const [12]u8 { 17 | return protocol.CommandNames.SENDHEADERS; 18 | } 19 | 20 | pub fn checksum(self: Self) [4]u8 { 21 | _ = self; 22 | return default_checksum; 23 | } 24 | 25 | /// Serialize a message as bytes and return them. 26 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 27 | _ = self; 28 | _ = allocator; 29 | return &.{}; 30 | } 31 | 32 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 33 | _ = allocator; 34 | _ = r; 35 | return SendHeadersMessage{}; 36 | } 37 | 38 | pub fn hintSerializedLen(self: Self) usize { 39 | _ = self; 40 | return 0; 41 | } 42 | }; 43 | 44 | // TESTS 45 | test "ok_full_flow_SendHeaders" { 46 | const allocator = std.testing.allocator; 47 | 48 | { 49 | const msg = SendHeadersMessage{}; 50 | 51 | const payload = try msg.serialize(allocator); 52 | defer allocator.free(payload); 53 | const deserialized_msg = try SendHeadersMessage.deserializeReader(allocator, payload); 54 | _ = deserialized_msg; 55 | 56 | try std.testing.expect(payload.len == 0); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/network/protocol/messages/verack.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("../lib.zig"); 3 | const default_checksum = @import("lib.zig").default_checksum; 4 | 5 | /// VerackMessage represents the "verack" message 6 | /// 7 | /// https://developer.bitcoin.org/reference/p2p_networking.html#verack 8 | pub const VerackMessage = struct { 9 | // verack message do not contain any payload, thus there is no field 10 | 11 | pub fn name() *const [12]u8 { 12 | return protocol.CommandNames.VERACK ++ [_]u8{0} ** 6; 13 | } 14 | 15 | pub fn checksum(self: VerackMessage) [4]u8 { 16 | _ = self; 17 | return default_checksum; 18 | } 19 | 20 | /// Serialize a message as bytes and return them. 21 | pub fn serialize(self: *const VerackMessage, allocator: std.mem.Allocator) ![]u8 { 22 | _ = self; 23 | _ = allocator; 24 | return &.{}; 25 | } 26 | 27 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !VerackMessage { 28 | _ = allocator; 29 | _ = r; 30 | return VerackMessage{}; 31 | } 32 | 33 | pub fn hintSerializedLen(self: VerackMessage) usize { 34 | _ = self; 35 | return 0; 36 | } 37 | }; 38 | 39 | // TESTS 40 | test "ok_full_flow_VerackMessage" { 41 | const allocator = std.testing.allocator; 42 | 43 | { 44 | const msg = VerackMessage{}; 45 | 46 | const payload = try msg.serialize(allocator); 47 | defer allocator.free(payload); 48 | const deserialized_msg = try VerackMessage.deserializeReader(allocator, payload); 49 | _ = deserialized_msg; 50 | 51 | try std.testing.expect(payload.len == 0); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/network/protocol/types/InventoryItem.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | type: u32, 4 | hash: [32]u8, 5 | 6 | pub fn encodeToWriter(self: *const @This(), w: anytype) !void { 7 | comptime { 8 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects r to have fn 'writeInt'."); 9 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects r to have fn 'writeAll'."); 10 | } 11 | try w.writeInt(u32, self.type, .little); 12 | try w.writeAll(&self.hash); 13 | } 14 | 15 | pub fn decodeReader(r: anytype) !@This() { 16 | comptime { 17 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects reader to have fn 'readInt'."); 18 | if (!std.meta.hasFn(@TypeOf(r), "readNoEof")) @compileError("Expects reader to have fn 'readNoEof'."); 19 | } 20 | 21 | const item_type = try r.readInt(u32, .little); 22 | var hash: [32]u8 = undefined; 23 | try r.readNoEof(&hash); 24 | 25 | return @This(){ 26 | .type = item_type, 27 | .hash = hash, 28 | }; 29 | } 30 | 31 | pub fn eql(self: *const @This(), other: *const @This()) bool { 32 | return self.type == other.type and std.mem.eql(u8, &self.hash, &other.hash); 33 | } -------------------------------------------------------------------------------- /src/network/protocol/types/NetworkAddress.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const NetworkAddress = struct { 4 | ip: [16]u8, 5 | port: u16, 6 | services: u64, 7 | 8 | pub fn eql(self: *const NetworkAddress, other: *const NetworkAddress) bool { 9 | return std.mem.eql(u8, &self.ip, &other.ip) and 10 | self.port == other.port and 11 | self.services == other.services; 12 | } 13 | 14 | pub fn serializeToWriter(self: *const NetworkAddress, writer: anytype) !void { 15 | try writer.writeInt(u64, self.services, .little); 16 | try writer.writeAll(&self.ip); 17 | try writer.writeInt(u16, self.port, .big); 18 | } 19 | 20 | pub fn deserializeReader(reader: anytype) !NetworkAddress { 21 | return NetworkAddress{ 22 | .services = try reader.readInt(u64, .little), 23 | .ip = try reader.readBytesNoEof(16), 24 | .port = try reader.readInt(u16, .big), 25 | }; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/network/rpc.zig: -------------------------------------------------------------------------------- 1 | //! RPC module handles the RPC server of btczee. 2 | //! It is responsible for the communication between the node and the clients. 3 | //! See https://developer.bitcoin.org/reference/rpc/ 4 | const std = @import("std"); 5 | const Config = @import("../config/config.zig").Config; 6 | const Mempool = @import("../core/mempool.zig").Mempool; 7 | const Storage = @import("../storage/storage.zig").Storage; 8 | const httpz = @import("httpz"); 9 | 10 | /// RPC Server handler. 11 | /// 12 | /// The RPC server is responsible for handling the RPC requests from the clients. 13 | /// 14 | pub const RPC = struct { 15 | /// Allocator . 16 | allocator: std.mem.Allocator, 17 | /// Configuration. 18 | config: *const Config, 19 | /// Transaction pool. 20 | mempool: *Mempool, 21 | /// Blockchain storage. 22 | storage: *Storage, 23 | /// Initialize the RPC server. 24 | /// # Arguments 25 | /// - `allocator`: Allocator. 26 | /// - `config`: Configuration. 27 | /// - `mempool`: Transaction pool. 28 | /// - `storage`: Blockchain storage. 29 | /// # Returns 30 | /// - `RPC`: RPC server. 31 | pub fn init( 32 | allocator: std.mem.Allocator, 33 | config: *const Config, 34 | mempool: *Mempool, 35 | storage: *Storage, 36 | ) !RPC { 37 | const rpc = RPC{ 38 | .allocator = allocator, 39 | .config = config, 40 | .mempool = mempool, 41 | .storage = storage, 42 | }; 43 | 44 | return rpc; 45 | } 46 | 47 | /// Deinitialize the RPC server. 48 | /// Clean up the RPC server resources. 49 | pub fn deinit(self: *RPC) void { 50 | _ = self; 51 | } 52 | 53 | /// Start the RPC server. 54 | /// The RPC server will start a HTTP server and listen on the RPC port. 55 | pub fn start(self: *RPC) !void { 56 | std.log.info("Starting RPC server on port {}", .{self.config.rpc_port}); 57 | 58 | var server = try httpz.Server().init(self.allocator, .{ .port = self.config.rpc_port }); 59 | var router = server.router(); 60 | // Register routes. 61 | router.get("/", index); 62 | router.get("/error", @"error"); 63 | 64 | std.log.info("RPC server listening on http://localhost:{d}/\n", .{self.config.rpc_port}); 65 | 66 | // Starts the server, this is blocking. 67 | // TODO: Make it non-blocking. cc @StringNick 68 | //try server.listen(); 69 | } 70 | }; 71 | 72 | const Handler = struct { 73 | 74 | // If the handler defines a special "notFound" function, it'll be called 75 | // when a request is made and no route matches. 76 | pub fn notFound(_: *httpz.Request, res: *httpz.Response) !void { 77 | res.status = 404; 78 | res.body = "NOPE!"; 79 | } 80 | 81 | // If the handler defines the special "uncaughtError" function, it'll be 82 | // called when an action returns an error. 83 | // Note that this function takes an additional parameter (the error) and 84 | // returns a `void` rather than a `!void`. 85 | pub fn uncaughtError(req: *httpz.Request, res: *httpz.Response, err: anyerror) void { 86 | std.debug.print("uncaught http error at {s}: {}\n", .{ req.url.path, err }); 87 | 88 | // Alternative to res.content_type = .TYPE 89 | // useful for dynamic content types, or content types not defined in 90 | // httpz.ContentType 91 | res.headers.add("content-type", "text/html; charset=utf-8"); 92 | 93 | res.status = 505; 94 | res.body = "(╯°□°)╯︵ ┻━┻"; 95 | } 96 | }; 97 | 98 | fn index(_: *httpz.Request, res: *httpz.Response) !void { 99 | res.body = 100 | \\ 101 | \\

Running Bitcoin. 102 | ; 103 | } 104 | 105 | fn @"error"(_: *httpz.Request, _: *httpz.Response) !void { 106 | return error.ActionError; 107 | } 108 | -------------------------------------------------------------------------------- /src/node/ibd.zig: -------------------------------------------------------------------------------- 1 | const P2P = @import("../network/p2p.zig").P2P; 2 | 3 | pub const IBD = struct { 4 | p2p: *P2P, 5 | 6 | pub fn init(p2p: *P2P) IBD { 7 | return .{ 8 | .p2p = p2p, 9 | }; 10 | } 11 | 12 | pub fn start(_: *IBD) !void {} 13 | }; 14 | -------------------------------------------------------------------------------- /src/node/node.zig: -------------------------------------------------------------------------------- 1 | //! Contains functionalities to run a Bitcoin full node on the Bitcoin network. 2 | //! It enables the validation of transactions and blocks, the exchange of transactions and blocks with other peers. 3 | const std = @import("std"); 4 | const Mempool = @import("../core/mempool.zig").Mempool; 5 | const Storage = @import("../storage/storage.zig").Storage; 6 | const P2P = @import("../network/p2p.zig").P2P; 7 | const RPC = @import("../network/rpc.zig").RPC; 8 | const IBD = @import("ibd.zig").IBD; 9 | 10 | /// Node is a struct that contains all the components of a Bitcoin full node. 11 | pub const Node = struct { 12 | /// Allocator. 13 | allocator: std.mem.Allocator, 14 | /// Transaction pool. 15 | mempool: *Mempool, 16 | /// Blockchain storage. 17 | storage: *Storage, 18 | /// P2P network handler. 19 | p2p: *P2P, 20 | rpc: *RPC, 21 | /// Whether the node is stopped. 22 | stopped: bool, 23 | /// Condition variable to wait for the node to start. 24 | started: std.Thread.Condition, 25 | /// Mutex to synchronize access to the node. 26 | mutex: std.Thread.Mutex, 27 | /// IBD handler. 28 | ibd: IBD, 29 | 30 | /// Initialize the node. 31 | /// 32 | /// # Arguments 33 | /// - `mempool`: Transaction pool. 34 | /// - `storage`: Blockchain storage. 35 | /// - `p2p`: P2P network handler. 36 | /// - `rpc`: RPC server. 37 | pub fn init(allocator: std.mem.Allocator, mempool: *Mempool, storage: *Storage, p2p: *P2P, rpc: *RPC) !Node { 38 | return Node{ 39 | .allocator = allocator, 40 | .mempool = mempool, 41 | .storage = storage, 42 | .p2p = p2p, 43 | .rpc = rpc, 44 | .stopped = false, 45 | .started = std.Thread.Condition{}, 46 | .mutex = std.Thread.Mutex{}, 47 | .ibd = IBD.init(p2p), 48 | }; 49 | } 50 | 51 | /// Deinitialize the node. 52 | /// Cleans up the resources used by the node. 53 | pub fn deinit(self: *Node) void { 54 | _ = self; 55 | } 56 | 57 | /// Start the node. 58 | /// 59 | /// # Arguments 60 | /// - `mempool`: Transaction pool. 61 | /// - `storage`: Blockchain storage. 62 | /// - `p2p`: P2P network handler. 63 | /// - `rpc`: RPC server. 64 | pub fn start(self: *Node) !void { 65 | std.log.info("Starting btczee node...", .{}); 66 | self.mutex.lock(); 67 | defer self.mutex.unlock(); 68 | 69 | // Start P2P network 70 | try self.p2p.start(); 71 | 72 | // Start RPC server 73 | try self.rpc.start(); 74 | 75 | // Start Initial Block Download 76 | try self.ibd.start(); 77 | 78 | self.started.signal(); 79 | 80 | // Main event loop 81 | while (!self.stopped) { 82 | std.time.sleep(5 * std.time.ns_per_s); 83 | } 84 | std.log.info("Node stopped", .{}); 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /src/primitives/lib.zig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zig-bitcoin/btczee/0c66e2c0b4051b6cb9bb0de8bb1b13964111b18c/src/primitives/lib.zig -------------------------------------------------------------------------------- /src/script/lib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | pub const engine = @import("engine.zig"); 3 | pub const stack = @import("stack.zig"); 4 | pub const arithmetic = @import("opcodes/arithmetic.zig"); 5 | const StackError = @import("stack.zig").StackError; 6 | 7 | /// Maximum number of bytes pushable to the stack 8 | const MAX_SCRIPT_ELEMENT_SIZE = 520; 9 | 10 | /// Maximum number of non-push operations per script 11 | const MAX_OPS_PER_SCRIPT = 201; 12 | 13 | /// Maximum number of public keys per multisig 14 | const MAX_PUBKEYS_PER_MULTISIG = 20; 15 | 16 | /// Maximum script length in bytes 17 | const MAX_SCRIPT_SIZE = 10000; 18 | 19 | /// Maximum number of values on execution stack 20 | const MAX_STACK_SIZE = 1000; 21 | 22 | /// Arithmetic opcodes can't take inputs larger than this 23 | const MAX_SCRIPT_NUM_LENGTH = 4; 24 | 25 | /// ScriptFlags represents flags for verifying scripts 26 | pub const ScriptFlags = packed struct { 27 | verify_none: bool = false, 28 | verify_p2sh: bool = false, 29 | verify_strictenc: bool = false, 30 | verify_dersig: bool = false, 31 | verify_low_s: bool = false, 32 | verify_nulldummy: bool = false, 33 | verify_sigpushonly: bool = false, 34 | verify_minimaldata: bool = false, 35 | verify_discourage_upgradable_nops: bool = false, 36 | verify_cleanstack: bool = false, 37 | verify_checklocktimeverify: bool = false, 38 | verify_checksequenceverify: bool = false, 39 | verify_witness: bool = false, 40 | verify_discourage_upgradable_witness_program: bool = false, 41 | verify_minimalif: bool = false, 42 | verify_nullfail: bool = false, 43 | verify_witness_pubkeytype: bool = false, 44 | verify_const_scriptcode: bool = false, 45 | }; 46 | 47 | /// Represents a Bitcoin script 48 | pub const Script = struct { 49 | data: []const u8, 50 | 51 | /// Initialize a new Script from bytes 52 | pub fn init(data: []const u8) Script { 53 | return .{ .data = data }; 54 | } 55 | 56 | /// Get the length of the script 57 | pub fn len(self: Script) usize { 58 | return self.data.len; 59 | } 60 | 61 | /// Check if the script is empty 62 | pub fn isEmpty(self: Script) bool { 63 | return self.len() == 0; 64 | } 65 | }; 66 | 67 | /// Errors that can occur during script execution 68 | pub const EngineError = error{ 69 | /// Script ended unexpectedly 70 | ScriptTooShort, 71 | /// OP_VERIFY failed 72 | VerifyFailed, 73 | /// OP_RETURN encountered 74 | EarlyReturn, 75 | /// Encountered an unknown opcode 76 | UnknownOpcode, 77 | /// Encountered a disabled opcode 78 | DisabledOpcode, 79 | } || StackError; 80 | 81 | /// Decode bytes as a boolean 82 | /// 83 | /// false can be represented as empty array [], positive zero [0x0 ... 0x0] and negative zero [0x0 ... 0x0 0x80]. 84 | /// any other sequence of bytes means true. 85 | pub fn asBool(bytes: []const u8) bool { 86 | for (0..bytes.len) |i| { 87 | if (bytes[i] != 0 and (i != bytes.len - 1 or bytes[i] != 0)) { 88 | return true; 89 | } 90 | } 91 | return false; 92 | } 93 | 94 | /// Decode bytesas the little endian, bit flag signed, variable-length representation of an i32 95 | /// 96 | /// Will error if the input does not represent an int beetween ScriptNum.MIN and ScriptNum.MAX, 97 | /// meaning that it cannot read back overflown numbers. 98 | pub fn asInt(bytes: []const u8) StackError!i32 { 99 | if (bytes.len > 4) { 100 | return StackError.InvalidValue; 101 | } 102 | if (bytes.len == 0) { 103 | return 0; 104 | } 105 | 106 | const is_negative = if (bytes[bytes.len - 1] & 0x80 != 0) true else false; 107 | var owned_bytes = std.mem.zeroes([4]u8); 108 | @memcpy(owned_bytes[0..bytes.len], bytes[0..bytes.len]); 109 | // Erase the sign bit 110 | owned_bytes[bytes.len - 1] &= 0x7f; 111 | 112 | const abs_value = std.mem.readInt(i32, &owned_bytes, .little); 113 | 114 | return if (is_negative) -abs_value else abs_value; 115 | } 116 | 117 | /// A struct allowing for safe reading and writing of bitcoin numbers as well as performing mathematical operations. 118 | /// 119 | /// Bitcoin numbers are represented on the stack as 0 to 4 bytes little endian variable-lenght integer, 120 | /// with the most significant bit reserved for the sign flag. 121 | /// In the msb is already used an additionnal bytes will be added to carry the flag. 122 | /// Eg. 0xff is encoded as [0xff, 0x00]. 123 | /// 124 | /// Thus both `0x80` and `0x00` can be read as zero, while it should be written as [0]u8{}. 125 | /// It also implies that the largest negative number representable is not i32.MIN but i32.MIN + 1 == -i32.MAX. 126 | /// 127 | /// The mathematical operation performed on those number are allowd to overflow, making the result expand to 5 bytes. 128 | /// Eg. ScriptNum.MAX + 1 will be encoded [0x0, 0x0, 0x0. 0x80, 0x0]. 129 | /// Those overflowed value can successfully be writen back onto the stack as [5]u8, but any attempt to read them bac 130 | /// as number will fail. They can still be read in other way tho (bool, array, etc). 131 | /// 132 | /// In order to handle this possibility of overflow the ScripNum are internally represented as i36, not i32. 133 | pub const ScriptNum = struct { 134 | /// The type used to internaly represent and do math onto the ScriptNum 135 | pub const InnerReprType = i36; 136 | /// The greatest valid number handled by the protocol 137 | pub const MAX: i32 = std.math.maxInt(i32); 138 | /// The lowest valid number handled by the protocol 139 | pub const MIN: i32 = std.math.minInt(i32) + 1; 140 | 141 | value: Self.InnerReprType, 142 | 143 | const Self = @This(); 144 | 145 | pub fn new(value: i32) Self { 146 | return .{ .value = value }; 147 | } 148 | 149 | /// Encode `Self.value` as variable-lenght integer 150 | /// 151 | /// In case of overflow, it can return as much as 5 bytes. 152 | pub fn toBytes(self: Self, allocator: std.mem.Allocator) ![]u8 { 153 | if (self.value == 0) { 154 | return allocator.alloc(u8, 0); 155 | } 156 | 157 | const is_negative = self.value < 0; 158 | const bytes: [8]u8 = @bitCast(std.mem.nativeToLittle(u64, @abs(self.value))); 159 | 160 | var i: usize = 8; 161 | while (i > 0) { 162 | i -= 1; 163 | if (bytes[i] != 0) { 164 | i = i; 165 | break; 166 | } 167 | } 168 | const additional_byte: usize = @intFromBool(bytes[i] & 0x80 != 0); 169 | var elem = try allocator.alloc(u8, i + 1 + additional_byte); 170 | errdefer allocator.free(elem); 171 | for (0..elem.len) |idx| elem[idx] = 0; 172 | 173 | @memcpy(elem[0 .. i + 1], bytes[0 .. i + 1]); 174 | if (is_negative) { 175 | elem[elem.len - 1] |= 0x80; 176 | } 177 | 178 | return elem; 179 | } 180 | 181 | /// Add `rhs` to `self` 182 | /// 183 | /// * Safety: both arguments should be valid Bitcoin integer values (non overflown) 184 | pub fn add(self: Self, rhs: Self) Self { 185 | const result = std.math.add(Self.InnerReprType, self.value, rhs.value) catch unreachable; 186 | return .{ .value = result }; 187 | } 188 | /// Substract `rhs` to `self` 189 | /// 190 | /// * Safety: both arguments should be valid Bitcoin integer values (non overflown) 191 | pub fn sub(self: Self, rhs: Self) Self { 192 | const result = std.math.sub(Self.InnerReprType, self.value, rhs.value) catch unreachable; 193 | return .{ .value = result }; 194 | } 195 | /// Increment `self` by 1 196 | /// 197 | /// * Safety: `self` should be a valid Bitcoin integer values (non overflown) 198 | pub fn addOne(self: Self) Self { 199 | const result = std.math.add(Self.InnerReprType, self.value, 1) catch unreachable; 200 | return .{ .value = result }; 201 | } 202 | /// Decrement `self` by 1 203 | /// 204 | /// * Safety: `self` should be a valid Bitcoin integer values (non overflown) 205 | pub fn subOne(self: Self) Self { 206 | const result = std.math.sub(Self.InnerReprType, self.value, 1) catch unreachable; 207 | return .{ .value = result }; 208 | } 209 | /// Return the absolute value of `self` 210 | /// 211 | /// * Safety: `self` should be a valid Bitcoin integer values (non overflown) 212 | pub fn abs(self: Self) Self { 213 | return if (self.value < 0) .{ .value = std.math.negate(self.value) catch unreachable } else self; 214 | } 215 | /// Return the opposite of `self` 216 | /// 217 | /// * Safety: `self` should be a valid Bitcoin integer values (non overflown) 218 | pub fn negate(self: Self) Self { 219 | return .{ .value = std.math.negate(self.value) catch unreachable }; 220 | } 221 | }; 222 | -------------------------------------------------------------------------------- /src/storage/storage.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Config = @import("../config/config.zig").Config; 3 | const Block = @import("../types/block.zig"); 4 | const lmdb = @import("lmdb"); 5 | 6 | /// Storage handler. 7 | /// 8 | /// The storage is responsible for handling the blockchain data. 9 | pub const Storage = struct { 10 | config: *const Config, 11 | env: lmdb.Environment, 12 | 13 | /// Initialize the storage 14 | /// 15 | /// Will create the full path to the directory if it doesn't already exist. 16 | pub fn init(config: *const Config) !Storage { 17 | const datadir = config.datadir; 18 | try std.fs.cwd().makePath(datadir); 19 | 20 | // Init the db env 21 | // `max_dbs` is set to 1: 22 | // - "blocks" 23 | const env = try lmdb.Environment.init(datadir, .{ .max_dbs = 1 }); 24 | 25 | return Storage{ 26 | .config = config, 27 | .env = env, 28 | }; 29 | } 30 | 31 | /// Deinitialize the storage 32 | /// 33 | /// Release the lmdb environment handle. 34 | pub fn deinit(self: Storage) void { 35 | self.env.deinit(); 36 | } 37 | 38 | /// Return a Transaction handle 39 | pub fn initTransaction(self: Storage) !Transaction { 40 | const txn = try lmdb.Transaction.init(self.env, .{ .mode = .ReadWrite }); 41 | return Transaction{ .txn = txn }; 42 | } 43 | }; 44 | 45 | /// A Storage transaction 46 | pub const Transaction = struct { 47 | txn: lmdb.Transaction, 48 | 49 | /// Abandon the Transaction without applying any change 50 | pub fn abort(self: Transaction) void { 51 | self.txn.abort(); 52 | } 53 | 54 | /// Serialize and store a block in database 55 | pub fn storeBlock(allocator: std.mem.Allocator, txn: Transaction, block: *Block) !void { 56 | const blocks = try txn.txn.database("blocks", .{ .create = true }); 57 | try blocks.set(&block.hash, try block.serizalize(allocator)); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/types/block.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | hash: [32]u8, 4 | height: i32, 5 | 6 | const Self = @This(); 7 | 8 | pub fn serizalize(self: *Self, allocator: std.mem.Allocator) ![]u8 { 9 | _ = self; 10 | const ret = try allocator.alloc(u8, 0); 11 | return ret; 12 | } 13 | -------------------------------------------------------------------------------- /src/types/block_header.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | version: i32, 4 | prev_block: [32]u8, 5 | merkle_root: [32]u8, 6 | timestamp: u32, 7 | nbits: u32, 8 | nonce: u32, 9 | 10 | const Self = @This(); 11 | 12 | pub fn serializeToWriter(self: *const Self, writer: anytype) !void { 13 | comptime { 14 | if (!std.meta.hasFn(@TypeOf(writer), "writeInt")) @compileError("Expects r to have fn 'writeInt'."); 15 | if (!std.meta.hasFn(@TypeOf(writer), "writeAll")) @compileError("Expects r to have fn 'writeAll'."); 16 | } 17 | 18 | try writer.writeInt(i32, self.version, .little); 19 | try writer.writeAll(std.mem.asBytes(&self.prev_block)); 20 | try writer.writeAll(std.mem.asBytes(&self.merkle_root)); 21 | try writer.writeInt(u32, self.timestamp, .little); 22 | try writer.writeInt(u32, self.nbits, .little); 23 | try writer.writeInt(u32, self.nonce, .little); 24 | } 25 | 26 | pub fn deserializeReader(r: anytype) !Self { 27 | var bh: Self = undefined; 28 | bh.version = try r.readInt(i32, .little); 29 | try r.readNoEof(&bh.prev_block); 30 | try r.readNoEof(&bh.merkle_root); 31 | bh.timestamp = try r.readInt(u32, .little); 32 | bh.nbits = try r.readInt(u32, .little); 33 | bh.nonce = try r.readInt(u32, .little); 34 | 35 | return bh; 36 | } 37 | 38 | pub fn serializedLen() usize { 39 | return 80; 40 | } 41 | 42 | pub fn eql(self: *const Self, other: *const Self) bool { 43 | if (self.version != other.version) { 44 | return false; 45 | } 46 | 47 | if (!std.mem.eql(u8, &self.prev_block, &other.prev_block)) { 48 | return false; 49 | } 50 | 51 | if (!std.mem.eql(u8, &self.merkle_root, &other.merkle_root)) { 52 | return false; 53 | } 54 | 55 | if (self.timestamp != other.timestamp) { 56 | return false; 57 | } 58 | 59 | if (self.nbits != other.nbits) { 60 | return false; 61 | } 62 | 63 | if (self.nonce != other.nonce) { 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | -------------------------------------------------------------------------------- /src/types/hash.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Self = @This(); 4 | 5 | bytes: [32]u8, 6 | 7 | /// Create a zero hash 8 | pub fn newZeroed() Self { 9 | return Self{ .bytes = [_]u8{0} ** 32 }; 10 | } 11 | 12 | /// Check if two hashes are equal 13 | pub fn eql(self: Self, other: Self) bool { 14 | return std.mem.eql(u8, &self.bytes, &other.bytes); 15 | } 16 | -------------------------------------------------------------------------------- /src/types/input.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const OutPoint = @import("outpoint.zig"); 3 | const Script = @import("script.zig"); 4 | 5 | previous_outpoint: OutPoint, 6 | script_sig: Script, 7 | sequence: u32, 8 | 9 | const Self = @This(); 10 | 11 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 12 | comptime { 13 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects w to have field 'writeInt'."); 14 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects w to have field 'writeAll'."); 15 | } 16 | 17 | try self.previous_outpoint.serializeToWriter(w); 18 | try self.script_sig.serializeToWriter(w); 19 | try w.writeInt(u32, self.sequence, .little); 20 | } 21 | 22 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 23 | comptime { 24 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 25 | if (!std.meta.hasFn(@TypeOf(r), "readNoEof")) @compileError("Expects r to have fn 'readNoEof'."); 26 | if (!std.meta.hasFn(@TypeOf(r), "readAll")) @compileError("Expects r to have fn 'readAll'."); 27 | if (!std.meta.hasFn(@TypeOf(r), "readByte")) @compileError("Expects r to have fn 'readByte'."); 28 | } 29 | 30 | var input: Self = undefined; 31 | input.previous_outpoint = try OutPoint.deserializeReader(allocator, r); 32 | input.script_sig = try Script.deserializeReader(allocator, r); 33 | input.sequence = try r.readInt(u32, .little); 34 | 35 | return input; 36 | } 37 | 38 | pub fn deinit(self: *Self) void { 39 | self.script_sig.deinit(); 40 | } 41 | -------------------------------------------------------------------------------- /src/types/lib.zig: -------------------------------------------------------------------------------- 1 | //! Types module contains the definitions of the data structures used in the node. 2 | 3 | /// Block 4 | pub const Block = @import("block.zig"); 5 | pub const BlockHeader = @import("block_header.zig"); 6 | pub const Hash = @import("hash.zig"); 7 | pub const OutPoint = @import("outpoint.zig"); 8 | pub const Input = @import("input.zig"); 9 | pub const Output = @import("output.zig"); 10 | pub const Script = @import("script.zig"); 11 | pub const Transaction = @import("transaction.zig"); 12 | -------------------------------------------------------------------------------- /src/types/outpoint.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Hash = @import("hash.zig"); 3 | const readBytesExact = @import("../util/mem/read.zig").readBytesExact; 4 | 5 | hash: Hash, 6 | index: u32, 7 | 8 | const Self = @This(); 9 | 10 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 11 | comptime { 12 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects w to have field 'writeInt'."); 13 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects w to have field 'writeAll'."); 14 | } 15 | 16 | try w.writeAll(&self.hash.bytes); 17 | try w.writeInt(u32, self.index, .little); 18 | } 19 | 20 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 21 | comptime { 22 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 23 | if (!std.meta.hasFn(@TypeOf(r), "readNoEof")) @compileError("Expects r to have fn 'readNoEof'."); 24 | if (!std.meta.hasFn(@TypeOf(r), "readAll")) @compileError("Expects r to have fn 'readAll'."); 25 | if (!std.meta.hasFn(@TypeOf(r), "readByte")) @compileError("Expects r to have fn 'readByte'."); 26 | } 27 | 28 | var outpoint: Self = undefined; 29 | 30 | const hash_raw_bytes = try readBytesExact(allocator, r, 32); 31 | defer allocator.free(hash_raw_bytes); 32 | @memcpy(&outpoint.hash.bytes, hash_raw_bytes); 33 | 34 | outpoint.index = try r.readInt(u32, .little); 35 | 36 | return outpoint; 37 | } 38 | -------------------------------------------------------------------------------- /src/types/output.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Script = @import("script.zig"); 3 | 4 | value: i64, 5 | script_pubkey: Script, 6 | 7 | const Self = @This(); 8 | 9 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 10 | comptime { 11 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects w to have field 'writeInt'."); 12 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects w to have field 'writeAll'."); 13 | } 14 | 15 | try w.writeInt(i64, self.value, .little); 16 | try self.script_pubkey.serializeToWriter(w); 17 | } 18 | 19 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 20 | comptime { 21 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 22 | if (!std.meta.hasFn(@TypeOf(r), "readNoEof")) @compileError("Expects r to have fn 'readNoEof'."); 23 | if (!std.meta.hasFn(@TypeOf(r), "readAll")) @compileError("Expects r to have fn 'readAll'."); 24 | if (!std.meta.hasFn(@TypeOf(r), "readByte")) @compileError("Expects r to have fn 'readByte'."); 25 | } 26 | 27 | var output: Self = undefined; 28 | 29 | output.value = try r.readInt(i64, .little); 30 | output.script_pubkey = try Script.deserializeReader(allocator, r); 31 | 32 | return output; 33 | } 34 | 35 | pub fn deinit(self: *Self) void { 36 | self.script_pubkey.deinit(); 37 | } 38 | -------------------------------------------------------------------------------- /src/types/script.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 3 | const readBytesExact = @import("../util/mem/read.zig").readBytesExact; 4 | 5 | bytes: std.ArrayList(u8), 6 | allocator: std.mem.Allocator, 7 | 8 | const Self = @This(); 9 | 10 | /// Initialize a new script 11 | pub fn init(allocator: std.mem.Allocator) !Self { 12 | return Self{ 13 | .bytes = std.ArrayList(u8).init(allocator), 14 | .allocator = allocator, 15 | }; 16 | } 17 | 18 | /// Deinitialize the script 19 | pub fn deinit(self: *Self) void { 20 | self.bytes.deinit(); 21 | } 22 | 23 | /// Add data to the script 24 | pub fn push(self: *Self, data: []const u8) !void { 25 | try self.bytes.appendSlice(data); 26 | } 27 | 28 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 29 | comptime { 30 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects w to have field 'writeInt'."); 31 | if (!std.meta.hasFn(@TypeOf(w), "writeAll")) @compileError("Expects w to have field 'writeAll'."); 32 | } 33 | 34 | const script_len = CompactSizeUint.new(self.bytes.items.len); 35 | try script_len.encodeToWriter(w); 36 | try w.writeAll(self.bytes.items); 37 | } 38 | 39 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 40 | comptime { 41 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 42 | if (!std.meta.hasFn(@TypeOf(r), "readNoEof")) @compileError("Expects r to have fn 'readNoEof'."); 43 | if (!std.meta.hasFn(@TypeOf(r), "readAll")) @compileError("Expects r to have fn 'readAll'."); 44 | if (!std.meta.hasFn(@TypeOf(r), "readByte")) @compileError("Expects r to have fn 'readByte'."); 45 | } 46 | 47 | var script: Self = undefined; 48 | 49 | const script_len = try CompactSizeUint.decodeReader(r); 50 | script.bytes = try std.ArrayList(u8).initCapacity(allocator, script_len.value()); 51 | try r.readNoEof(script.bytes.items); 52 | 53 | const bytes = try readBytesExact(allocator, r, script_len.value()); 54 | defer allocator.free(bytes); 55 | errdefer allocator.free(bytes); 56 | 57 | try script.bytes.appendSlice(bytes); 58 | 59 | return script; 60 | } 61 | -------------------------------------------------------------------------------- /src/types/transaction.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const CompactSizeUint = @import("bitcoin-primitives").types.CompatSizeUint; 4 | const readBytesExact = @import("../util/mem/read.zig").readBytesExact; 5 | const Input = @import("input.zig"); 6 | const Output = @import("output.zig"); 7 | const Script = @import("script.zig"); 8 | const OutPoint = @import("outpoint.zig"); 9 | const Hash = @import("hash.zig"); 10 | 11 | version: i32, 12 | inputs: []Input, 13 | outputs: []Output, 14 | lock_time: u32, 15 | allocator: std.mem.Allocator, 16 | 17 | const Self = @This(); 18 | 19 | pub fn serializeToWriter(self: *const Self, w: anytype) !void { 20 | comptime { 21 | if (!std.meta.hasFn(@TypeOf(w), "writeInt")) @compileError("Expects w to have field 'writeInt'."); 22 | } 23 | 24 | const compact_input_len = CompactSizeUint.new(self.inputs.len); 25 | const compact_output_len = CompactSizeUint.new(self.outputs.len); 26 | 27 | try w.writeInt(i32, self.version, .little); 28 | 29 | try compact_input_len.encodeToWriter(w); 30 | 31 | for (self.inputs) |input| { 32 | try input.serializeToWriter(w); 33 | } 34 | 35 | try compact_output_len.encodeToWriter(w); 36 | for (self.outputs) |output| { 37 | try output.serializeToWriter(w); 38 | } 39 | 40 | try w.writeInt(u32, self.lock_time, .little); 41 | } 42 | 43 | /// Serialize a message as bytes and write them to the buffer. 44 | /// 45 | /// buffer.len must be >= than self.hintSerializedLen() 46 | pub fn serializeToSlice(self: *const Self, buffer: []u8) !void { 47 | var fbs = std.io.fixedBufferStream(buffer); 48 | try self.serializeToWriter(fbs.writer()); 49 | } 50 | 51 | /// Serialize a message as bytes and return them. 52 | pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { 53 | const serialized_len = self.hintEncodedLen(); 54 | 55 | const ret = try allocator.alloc(u8, serialized_len); 56 | errdefer allocator.free(ret); 57 | 58 | try self.serializeToSlice(ret); 59 | 60 | return ret; 61 | } 62 | 63 | pub fn deserializeReader(allocator: std.mem.Allocator, r: anytype) !Self { 64 | comptime { 65 | if (!std.meta.hasFn(@TypeOf(r), "readInt")) @compileError("Expects r to have fn 'readInt'."); 66 | if (!std.meta.hasFn(@TypeOf(r), "readByte")) @compileError("Expects r to have fn 'readByte'."); 67 | } 68 | 69 | var tx: Self = try Self.init(allocator); 70 | 71 | tx.version = try r.readInt(i32, .little); 72 | 73 | const compact_input_len = try CompactSizeUint.decodeReader(r); 74 | tx.inputs = try allocator.alloc(Input, compact_input_len.value()); 75 | errdefer allocator.free(tx.inputs); 76 | 77 | var i: usize = 0; 78 | while (i < compact_input_len.value()) : (i += 1) { 79 | tx.inputs[i] = try Input.deserializeReader(allocator, r); 80 | } 81 | 82 | const compact_output_len = try CompactSizeUint.decodeReader(r); 83 | tx.outputs = try allocator.alloc(Output, compact_output_len.value()); 84 | errdefer allocator.free(tx.outputs); 85 | 86 | var j: usize = 0; 87 | while (j < compact_output_len.value()) : (j += 1) { 88 | tx.outputs[j] = try Output.deserializeReader(allocator, r); 89 | } 90 | tx.lock_time = try r.readInt(u32, .little); 91 | 92 | return tx; 93 | } 94 | 95 | /// Deserialize bytes into Self 96 | pub fn deserializeSlice(allocator: std.mem.Allocator, bytes: []const u8) !Self { 97 | var fbs = std.io.fixedBufferStream(bytes); 98 | return try Self.deserializeReader(allocator, fbs.reader()); 99 | } 100 | 101 | /// Initialize a new transaction 102 | pub fn init(allocator: std.mem.Allocator) !Self { 103 | return Self{ 104 | .version = 1, 105 | .inputs = &[_]Input{}, 106 | .outputs = &[_]Output{}, 107 | .lock_time = 0, 108 | .allocator = allocator, 109 | }; 110 | } 111 | 112 | /// Deinitialize the transaction 113 | pub fn deinit(self: *Self) void { 114 | for (self.inputs) |*input| { 115 | input.deinit(); 116 | } 117 | for (self.outputs) |*output| { 118 | output.deinit(); 119 | } 120 | self.allocator.free(self.inputs); 121 | self.allocator.free(self.outputs); 122 | } 123 | 124 | /// Add an input to the transaction 125 | pub fn addInput(self: *Self, previous_outpoint: OutPoint) !void { 126 | const script_sig = try Script.init(self.allocator); 127 | const new_capacity = self.inputs.len + 1; 128 | var new_inputs = try self.allocator.realloc(self.inputs, new_capacity); 129 | 130 | self.inputs = new_inputs; 131 | 132 | new_inputs[self.inputs.len - 1] = Input{ 133 | .previous_outpoint = previous_outpoint, 134 | .script_sig = script_sig, 135 | .sequence = 0xffffffff, 136 | }; 137 | } 138 | 139 | /// Add an output to the transaction 140 | pub fn addOutput(self: *Self, value: i64, script_pubkey: Script) !void { 141 | var new_script = try Script.init(self.allocator); 142 | try new_script.push(script_pubkey.bytes.items); 143 | 144 | const new_capacity = self.outputs.len + 1; 145 | var new_outputs = try self.allocator.realloc(self.outputs, new_capacity); 146 | 147 | self.outputs = new_outputs; 148 | 149 | new_outputs[self.outputs.len - 1] = Output{ 150 | .value = value, 151 | .script_pubkey = new_script, 152 | }; 153 | } 154 | 155 | /// Calculate the transaction hash 156 | pub fn hash(self: *const Self) Hash { 157 | var h: [32]u8 = undefined; 158 | std.crypto.hash.sha2.Sha256.hash(@as([]const u8, std.mem.asBytes(&self.version)), &h, .{}); 159 | return Hash{ .bytes = h }; 160 | } 161 | 162 | /// Calculate the size of the transaction 163 | pub fn hintEncodedLen(self: *const Self) usize { 164 | // This is a simplified size calculation. In a real implementation, 165 | // you would need to account for segregated witness data if present. 166 | var size: usize = 8; // Version (4 bytes) + LockTime (4 bytes) 167 | size += self.inputs.len * 41; // Simplified input size 168 | size += self.outputs.len * 33; // Simplified output size 169 | return size; 170 | } 171 | 172 | pub fn eql(self: Self, other: Self) bool { 173 | // zig fmt: off 174 | return self.version == other.version 175 | and self.inputs.len == other.inputs.len 176 | and self.outputs.len == other.outputs.len 177 | and self.lock_time == other.lock_time 178 | and for (self.inputs, other.inputs) |a, b| { 179 | if ( 180 | !a.previous_outpoint.hash.eql(b.previous_outpoint.hash) 181 | or a.previous_outpoint.index != b.previous_outpoint.index 182 | or !std.mem.eql(u8, a.script_sig.bytes.items, b.script_sig.bytes.items) 183 | or a.sequence != b.sequence 184 | ) break false; 185 | } else true 186 | and for (self.outputs, other.outputs) |c, d| { 187 | if ( 188 | c.value != d.value 189 | or !std.mem.eql(u8, c.script_pubkey.bytes.items, d.script_pubkey.bytes.items) 190 | ) break false; 191 | } else true; 192 | // zig fmt: on 193 | } 194 | 195 | test "Transaction basics" { 196 | const testing = std.testing; 197 | const allocator = testing.allocator; 198 | const OpCode = @import("../script/opcodes/constant.zig").Opcode; 199 | 200 | var tx = try Self.init(allocator); 201 | defer tx.deinit(); 202 | 203 | try tx.addInput(OutPoint{ .hash = Hash.newZeroed(), .index = 0 }); 204 | 205 | { 206 | var script_pubkey = try Script.init(allocator); 207 | defer script_pubkey.deinit(); 208 | try script_pubkey.push(&[_]u8{ OpCode.OP_DUP.toBytes(), OpCode.OP_0.toBytes(), OpCode.OP_1.toBytes() }); 209 | try tx.addOutput(50000, script_pubkey); 210 | } 211 | 212 | try testing.expectEqual(1, tx.inputs.len); 213 | try testing.expectEqual(1, tx.outputs.len); 214 | try testing.expectEqual(50000, tx.outputs[0].value); 215 | 216 | _ = tx.hash(); 217 | 218 | const tx_size = tx.hintEncodedLen(); 219 | try testing.expect(tx_size > 0); 220 | } 221 | 222 | test "Transaction serialization" { 223 | const testing = std.testing; 224 | const allocator = testing.allocator; 225 | const OpCode = @import("../script/opcodes/constant.zig").Opcode; 226 | 227 | var tx = try Self.init(allocator); 228 | defer tx.deinit(); 229 | 230 | try tx.addInput(OutPoint{ .hash = Hash.newZeroed(), .index = 0 }); 231 | 232 | { 233 | var script_pubkey = try Script.init(allocator); 234 | defer script_pubkey.deinit(); 235 | try script_pubkey.push(&[_]u8{ OpCode.OP_DUP.toBytes(), OpCode.OP_0.toBytes(), OpCode.OP_1.toBytes() }); 236 | try tx.addOutput(50000, script_pubkey); 237 | } 238 | 239 | const payload = try tx.serialize(allocator); 240 | defer allocator.free(payload); 241 | 242 | var deserialized_tx = try Self.deserializeSlice(allocator, payload); 243 | defer deserialized_tx.deinit(); 244 | 245 | try testing.expect(tx.eql(deserialized_tx)); 246 | } 247 | -------------------------------------------------------------------------------- /src/util/cmd/ArgParser.zig: -------------------------------------------------------------------------------- 1 | /// Copied from: https://github.com/Hejsil/aniz/blob/master/src/ArgParser.zig 2 | /// TODO: Move to using zig-clap as dependency 3 | /// https://github.com/Hejsil/zig-clap 4 | args: []const []const u8, 5 | index: usize = 0, 6 | 7 | consumed: bool = false, 8 | 9 | pub fn next(parser: *ArgParser) bool { 10 | parser.consumed = parser.index >= parser.args.len; 11 | return !parser.consumed; 12 | } 13 | 14 | pub fn flag(parser: *ArgParser, names: []const []const u8) bool { 15 | if (parser.consumed) 16 | return false; 17 | 18 | for (names) |name| { 19 | if (!std.mem.eql(u8, parser.args[parser.index], name)) 20 | continue; 21 | 22 | parser.consumed = true; 23 | parser.index += 1; 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | pub fn option(parser: *ArgParser, names: []const []const u8) ?[]const u8 { 31 | if (parser.consumed) 32 | return null; 33 | 34 | const arg = parser.args[parser.index]; 35 | for (names) |name| { 36 | if (!std.mem.startsWith(u8, arg, name)) 37 | continue; 38 | if (!std.mem.startsWith(u8, arg[name.len..], "=")) 39 | continue; 40 | 41 | parser.consumed = true; 42 | parser.index += 1; 43 | return arg[name.len + 1 ..]; 44 | } 45 | 46 | if (parser.index + 1 < parser.args.len) { 47 | if (parser.flag(names)) 48 | return parser.eat(); 49 | } 50 | 51 | return null; 52 | } 53 | 54 | pub fn positional(parser: *ArgParser) ?[]const u8 { 55 | if (parser.consumed) 56 | return null; 57 | 58 | return parser.eat(); 59 | } 60 | 61 | fn eat(parser: *ArgParser) []const u8 { 62 | defer parser.index += 1; 63 | return parser.args[parser.index]; 64 | } 65 | 66 | test flag { 67 | var parser = ArgParser{ .args = &.{ 68 | "-a", "--beta", "command", 69 | } }; 70 | 71 | try std.testing.expect(parser.flag(&.{ "-a", "--alpha" })); 72 | try std.testing.expect(!parser.flag(&.{ "-b", "--beta" })); 73 | try std.testing.expect(!parser.flag(&.{"command"})); 74 | 75 | try std.testing.expect(parser.next()); 76 | try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); 77 | try std.testing.expect(parser.flag(&.{ "-b", "--beta" })); 78 | try std.testing.expect(!parser.flag(&.{"command"})); 79 | 80 | try std.testing.expect(parser.next()); 81 | try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); 82 | try std.testing.expect(!parser.flag(&.{ "-b", "--beta" })); 83 | try std.testing.expect(parser.flag(&.{"command"})); 84 | 85 | try std.testing.expect(!parser.next()); 86 | try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); 87 | try std.testing.expect(!parser.flag(&.{ "-b", "--beta" })); 88 | try std.testing.expect(!parser.flag(&.{"command"})); 89 | } 90 | 91 | fn expectEqualOptionalString(m_expect: ?[]const u8, m_actual: ?[]const u8) !void { 92 | if (m_expect) |expect| { 93 | try std.testing.expect(m_actual != null); 94 | try std.testing.expectEqualStrings(expect, m_actual.?); 95 | } else { 96 | try std.testing.expect(m_actual == null); 97 | } 98 | } 99 | 100 | test option { 101 | var parser = ArgParser{ .args = &.{ 102 | "-a", 103 | "a_value", 104 | "--beta=b_value", 105 | "command", 106 | "command_value", 107 | } }; 108 | 109 | try expectEqualOptionalString("a_value", parser.option(&.{ "-a", "--alpha" })); 110 | try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); 111 | try expectEqualOptionalString(null, parser.option(&.{"command"})); 112 | 113 | try std.testing.expect(parser.next()); 114 | try expectEqualOptionalString(null, parser.option(&.{ "-a", "--alpha" })); 115 | try expectEqualOptionalString("b_value", parser.option(&.{ "-b", "--beta" })); 116 | try expectEqualOptionalString(null, parser.option(&.{"command"})); 117 | 118 | try std.testing.expect(parser.next()); 119 | try expectEqualOptionalString(null, parser.option(&.{ "-a", "--alpha" })); 120 | try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); 121 | try expectEqualOptionalString("command_value", parser.option(&.{"command"})); 122 | 123 | try std.testing.expect(!parser.next()); 124 | try expectEqualOptionalString(null, parser.option(&.{ "-a", "--alpha" })); 125 | try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); 126 | try expectEqualOptionalString(null, parser.option(&.{"command"})); 127 | } 128 | 129 | test positional { 130 | var parser = ArgParser{ .args = &.{ 131 | "-a", 132 | "--beta", 133 | "command", 134 | } }; 135 | 136 | try expectEqualOptionalString("-a", parser.positional()); 137 | try std.testing.expect(parser.next()); 138 | try expectEqualOptionalString("--beta", parser.positional()); 139 | try std.testing.expect(parser.next()); 140 | try expectEqualOptionalString("command", parser.positional()); 141 | try std.testing.expect(!parser.next()); 142 | try expectEqualOptionalString(null, parser.positional()); 143 | } 144 | 145 | test "all" { 146 | var parser = ArgParser{ .args = &.{ 147 | "-a", 148 | "--beta", 149 | "b_value", 150 | "-c=c_value", 151 | "command", 152 | } }; 153 | 154 | try std.testing.expect(parser.flag(&.{ "-a", "--alpha" })); 155 | try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); 156 | try expectEqualOptionalString(null, parser.option(&.{ "-c", "--center" })); 157 | try expectEqualOptionalString(null, parser.positional()); 158 | 159 | try std.testing.expect(parser.next()); 160 | try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); 161 | try expectEqualOptionalString("b_value", parser.option(&.{ "-b", "--beta" })); 162 | try expectEqualOptionalString(null, parser.option(&.{ "-c", "--center" })); 163 | try expectEqualOptionalString(null, parser.positional()); 164 | 165 | try std.testing.expect(parser.next()); 166 | try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); 167 | try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); 168 | try expectEqualOptionalString("c_value", parser.option(&.{ "-c", "--center" })); 169 | try expectEqualOptionalString(null, parser.positional()); 170 | 171 | try std.testing.expect(parser.next()); 172 | try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); 173 | try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); 174 | try expectEqualOptionalString(null, parser.option(&.{ "-c", "--center" })); 175 | try expectEqualOptionalString("command", parser.positional()); 176 | 177 | try std.testing.expect(!parser.next()); 178 | try std.testing.expect(!parser.flag(&.{ "-a", "--alpha" })); 179 | try expectEqualOptionalString(null, parser.option(&.{ "-b", "--beta" })); 180 | try expectEqualOptionalString(null, parser.option(&.{ "-c", "--center" })); 181 | try expectEqualOptionalString(null, parser.positional()); 182 | } 183 | 184 | const ArgParser = @This(); 185 | 186 | const std = @import("std"); 187 | -------------------------------------------------------------------------------- /src/util/mem/read.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn readBytesExact(allocator: std.mem.Allocator, r: anytype, bytes_nb: u64) ![]u8 { 4 | comptime { 5 | if (!std.meta.hasFn(@TypeOf(r), "readAll")) @compileError("Expects r to have fn 'readNoEof'."); 6 | } 7 | 8 | const bytes = try allocator.alloc(u8, bytes_nb); 9 | errdefer allocator.free(bytes); 10 | try r.readNoEof(bytes); 11 | return bytes; 12 | } 13 | -------------------------------------------------------------------------------- /src/util/net/echo.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const net = @import("net.zig"); 3 | const ShredVersion = @import("../core/shred.zig").ShredVersion; 4 | const SocketAddr = @import("net.zig").SocketAddr; 5 | const Atomic = std.atomic.Value; 6 | const assert = std.debug.assert; 7 | const bincode = @import("../bincode/bincode.zig"); 8 | const httpz = @import("httpz"); 9 | 10 | const MAX_PORT_COUNT_PER_MSG: usize = 4; 11 | const HEADER_LENGTH: usize = 4; 12 | 13 | const IpEchoServerMessage = struct { 14 | tcp_ports: [MAX_PORT_COUNT_PER_MSG]u16 = [_]u16{0} ** MAX_PORT_COUNT_PER_MSG, 15 | udp_ports: [MAX_PORT_COUNT_PER_MSG]u16 = [_]u16{0} ** MAX_PORT_COUNT_PER_MSG, 16 | 17 | const Self = @This(); 18 | 19 | pub fn init(tcp_ports: []u16, udp_ports: []u16) Self { 20 | assert(tcp_ports.len <= MAX_PORT_COUNT_PER_MSG and udp_ports.len <= MAX_PORT_COUNT_PER_MSG); 21 | var self = Self{}; 22 | 23 | std.mem.copyForwards(u16, &self.tcp_ports, tcp_ports); 24 | std.mem.copyForwards(u16, &self.udp_ports, udp_ports); 25 | 26 | return self; 27 | } 28 | }; 29 | 30 | const IpEchoServerResponse = struct { 31 | // Public IP address of request echoed back to the node. 32 | address: net.IpAddr, 33 | // Cluster shred-version of the node running the server. 34 | shred_version: ?ShredVersion, 35 | 36 | const Self = @This(); 37 | 38 | pub fn init(addr: net.IpAddr) Self { 39 | return Self{ 40 | .address = addr, 41 | .shred_version = ShredVersion{ .value = 0 }, 42 | }; 43 | } 44 | }; 45 | 46 | pub const Server = struct { 47 | allocator: std.mem.Allocator, 48 | server: httpz.ServerCtx(void, void), 49 | exit: *const Atomic(bool), 50 | port: u16, 51 | killed: Atomic(bool), 52 | 53 | const Self = @This(); 54 | 55 | pub fn init( 56 | allocator: std.mem.Allocator, 57 | port: u16, 58 | exit: *const Atomic(bool), 59 | ) Self { 60 | return Self{ 61 | .allocator = allocator, 62 | .server = httpz.Server().init(allocator, .{ .port = port }) catch unreachable, 63 | .exit = exit, 64 | .port = port, 65 | .killed = Atomic(bool).init(false), 66 | }; 67 | } 68 | 69 | pub fn deinit( 70 | self: *Self, 71 | ) void { 72 | // self.kill(); 73 | self.server.deinit(); 74 | } 75 | 76 | pub fn kill( 77 | self: *Self, 78 | ) void { 79 | if (!self.killed.swap(true, .seq_cst)) { 80 | self.server.stop(); 81 | } 82 | } 83 | 84 | pub fn listenAndServe( 85 | self: *Self, 86 | ) !std.Thread { 87 | var router = self.server.router(); 88 | router.post("/", handleEchoRequest); 89 | return self.server.listenInNewThread(); 90 | } 91 | }; 92 | 93 | pub fn handleEchoRequest(req: *httpz.Request, res: *httpz.Response) !void { 94 | std.debug.print("handling echo request\n", .{}); 95 | 96 | const body = req.body() orelse return try returnBadRequest(res); 97 | var ip_echo_server_message = try std.json.parseFromSlice(IpEchoServerMessage, res.arena, body, .{}); 98 | defer ip_echo_server_message.deinit(); 99 | 100 | // convert a u32 to Ipv4 101 | const socket_addr = SocketAddr.fromIpV4Address(res.conn.address); 102 | 103 | try std.json.stringify(IpEchoServerResponse.init(net.IpAddr{ .ipv4 = socket_addr.V4.ip }), .{}, res.writer()); 104 | } 105 | 106 | pub fn returnBadRequest( 107 | resp: *httpz.Response, 108 | ) !void { 109 | resp.status = 400; 110 | resp.headers.add("content-type", "application/json"); 111 | resp.body = 112 | \\ "{\"error\":\"bad request.\"}" 113 | ; 114 | } 115 | 116 | pub fn returnNotFound( 117 | resp: *httpz.Response, 118 | ) !void { 119 | resp.status = 404; 120 | } 121 | 122 | pub fn requestIpEcho( 123 | allocator: std.mem.Allocator, 124 | addr: std.net.Address, 125 | message: IpEchoServerMessage, 126 | ) !IpEchoServerResponse { 127 | // connect + send 128 | const conn = try std.net.tcpConnectToAddress(addr); 129 | defer conn.close(); 130 | try conn.writeAll(&(.{0} ** HEADER_LENGTH)); 131 | try bincode.write(conn.writer(), message, .{}); 132 | try conn.writeAll("\n"); 133 | 134 | // get response 135 | var buff: [32]u8 = undefined; 136 | const len = try conn.readAll(&buff); 137 | var bufferStream = std.io.fixedBufferStream(buff[HEADER_LENGTH..len]); 138 | return try bincode.read(allocator, IpEchoServerResponse, bufferStream.reader(), .{}); 139 | } 140 | -------------------------------------------------------------------------------- /src/util/net/lib.zig: -------------------------------------------------------------------------------- 1 | pub const net = @import("net.zig"); 2 | pub const echo = @import("echo.zig"); 3 | pub const packet = @import("packet.zig"); 4 | pub const socket_utils = @import("socket_utils.zig"); 5 | 6 | pub const IpAddr = net.IpAddr; 7 | pub const SocketAddr = net.SocketAddr; 8 | pub const Packet = packet.Packet; 9 | pub const SocketThread = socket_utils.SocketThread; 10 | 11 | pub const requestIpEcho = echo.requestIpEcho; 12 | pub const enablePortReuse = net.enablePortReuse; 13 | pub const endpointToString = net.endpointToString; 14 | 15 | pub const SOCKET_TIMEOUT_US = socket_utils.SOCKET_TIMEOUT_US; 16 | pub const PACKET_DATA_SIZE = packet.PACKET_DATA_SIZE; 17 | -------------------------------------------------------------------------------- /src/util/net/packet.zig: -------------------------------------------------------------------------------- 1 | const network = @import("zig-network"); 2 | const utils = @import("./utils.zig"); 3 | const BitFlags = utils.BitFlags; 4 | 5 | /// Maximum over-the-wire size of a Transaction 6 | /// 1280 is IPv6 minimum MTU 7 | /// 40 bytes is the size of the IPv6 header 8 | /// 8 bytes is the size of the fragment header 9 | pub const PACKET_DATA_SIZE: usize = 1232; 10 | 11 | pub const Packet = struct { 12 | data: [PACKET_DATA_SIZE]u8, 13 | size: usize, 14 | addr: network.EndPoint, 15 | flags: BitFlags(Flag) = .{}, 16 | 17 | const Self = @This(); 18 | 19 | pub fn init(addr: network.EndPoint, data: [PACKET_DATA_SIZE]u8, size: usize) Self { 20 | return .{ 21 | .addr = addr, 22 | .data = data, 23 | .size = size, 24 | }; 25 | } 26 | 27 | pub fn default() Self { 28 | return .{ 29 | .addr = .{ .port = 0, .address = .{ .ipv4 = network.Address.IPv4.any } }, 30 | .data = undefined, 31 | .size = 0, 32 | }; 33 | } 34 | }; 35 | 36 | /// TODO this violates separation of concerns. it's unusual for network-specific 37 | /// type definitions to include information that's specific to application 38 | /// components (like repair) 39 | /// 40 | /// it would be nice to find another approach that is equally easy to use, 41 | /// without sacrificing safety, performance, or readability. 42 | pub const Flag = enum(u8) { 43 | discard = 0b0000_0001, 44 | // forwarded = 0b0000_0010, 45 | repair = 0b0000_0100, 46 | // simple_vote_tx = 0b0000_1000, 47 | // tracer_packet = 0b0001_0000, 48 | // round_compute_unit_price = 0b0010_0000, 49 | }; 50 | -------------------------------------------------------------------------------- /src/util/net/socket_utils.zig: -------------------------------------------------------------------------------- 1 | const Allocator = std.mem.Allocator; 2 | const Atomic = std.atomic.Value; 3 | const UdpSocket = @import("zig-network").Socket; 4 | const Packet = @import("packet.zig").Packet; 5 | const PACKET_DATA_SIZE = @import("packet.zig").PACKET_DATA_SIZE; 6 | const Channel = @import("../sync/channel.zig").Channel; 7 | const std = @import("std"); 8 | 9 | pub const SOCKET_TIMEOUT_US: usize = 1 * std.time.us_per_s; 10 | pub const PACKETS_PER_BATCH: usize = 64; 11 | 12 | pub fn readSocket( 13 | allocator: std.mem.Allocator, 14 | socket_: UdpSocket, 15 | incoming_channel: *Channel(std.ArrayList(Packet)), 16 | exit: *const std.atomic.Value(bool), 17 | ) !void { 18 | // NOTE: we set to non-blocking to periodically check if we should exit 19 | var socket = socket_; 20 | try socket.setReadTimeout(SOCKET_TIMEOUT_US); 21 | 22 | inf_loop: while (!exit.load(.unordered)) { 23 | // init a new batch 24 | var packet_batch = try std.ArrayList(Packet).initCapacity( 25 | allocator, 26 | PACKETS_PER_BATCH, 27 | ); 28 | errdefer packet_batch.deinit(); 29 | 30 | // recv packets into batch 31 | while (packet_batch.items.len != packet_batch.capacity) { 32 | var packet: Packet = Packet.default(); 33 | const recv_meta = socket.receiveFrom(&packet.data) catch |err| switch (err) { 34 | error.WouldBlock => { 35 | if (packet_batch.items.len > 0) break; 36 | if (exit.load(.unordered)) { 37 | packet_batch.deinit(); 38 | break :inf_loop; 39 | } 40 | continue; 41 | }, 42 | else => |e| return e, 43 | }; 44 | const bytes_read = recv_meta.numberOfBytes; 45 | if (bytes_read == 0) return error.SocketClosed; 46 | packet.addr = recv_meta.sender; 47 | packet.size = bytes_read; 48 | packet_batch.appendAssumeCapacity(packet); 49 | } 50 | 51 | packet_batch.shrinkAndFree(packet_batch.items.len); 52 | try incoming_channel.send(packet_batch); 53 | } 54 | 55 | std.log.debug("readSocket loop closed", .{}); 56 | } 57 | 58 | pub fn sendSocket( 59 | socket: UdpSocket, 60 | outgoing_channel: *Channel(std.ArrayList(Packet)), 61 | exit: *const std.atomic.Value(bool), 62 | ) error{ SocketSendError, OutOfMemory, ChannelClosed }!void { 63 | var packets_sent: u64 = 0; 64 | 65 | while (!exit.load(.unordered)) { 66 | const maybe_packet_batches = try outgoing_channel.try_drain(); 67 | const packet_batches = maybe_packet_batches orelse { 68 | // sleep for 1ms 69 | // std.time.sleep(std.time.ns_per_ms * 1); 70 | continue; 71 | }; 72 | defer { 73 | for (packet_batches) |*packet_batch| { 74 | packet_batch.deinit(); 75 | } 76 | outgoing_channel.allocator.free(packet_batches); 77 | } 78 | 79 | for (packet_batches) |*packet_batch| { 80 | for (packet_batch.items) |*p| { 81 | const bytes_sent = socket.sendTo(p.addr, p.data[0..p.size]) catch |e| { 82 | std.log.debug("send_socket error: {s}", .{@errorName(e)}); 83 | continue; 84 | }; 85 | packets_sent +|= 1; 86 | std.debug.assert(bytes_sent == p.size); 87 | } 88 | } 89 | } 90 | std.log.debug("sendSocket loop closed", .{}); 91 | } 92 | 93 | /// A thread that is dedicated to either sending or receiving data over a socket. 94 | /// The included channel can be used communicate with that thread. 95 | /// 96 | /// The channel only supports one: either sending or receiving, depending how it 97 | /// was initialized. While you *could* send data to the channel for a "receiver" 98 | /// socket, the underlying thread won't actually read the data from the channel. 99 | pub const SocketThread = struct { 100 | channel: *Channel(std.ArrayList(Packet)), 101 | exit: *std.atomic.Value(bool), 102 | handle: std.Thread, 103 | 104 | const Self = @This(); 105 | 106 | pub fn initSender(allocator: Allocator, socket: UdpSocket, exit: *Atomic(bool)) !Self { 107 | const channel = Channel(std.ArrayList(Packet)).init(allocator, 0); 108 | return .{ 109 | .channel = channel, 110 | .exit = exit, 111 | .handle = try std.Thread.spawn(.{}, sendSocket, .{ socket, channel, exit }), 112 | }; 113 | } 114 | 115 | pub fn initReceiver(allocator: Allocator, socket: UdpSocket, exit: *Atomic(bool)) !Self { 116 | const channel = Channel(std.ArrayList(Packet)).init(allocator, 0); 117 | return .{ 118 | .channel = channel, 119 | .exit = exit, 120 | .handle = try std.Thread.spawn(.{}, readSocket, .{ allocator, socket, channel, exit }), 121 | }; 122 | } 123 | 124 | pub fn deinit(self: Self) void { 125 | self.exit.store(true, .unordered); 126 | self.handle.join(); 127 | // close the channel first, so that we can drain without waiting for new items 128 | self.channel.close(); 129 | if (self.channel.drain()) |lists| { 130 | for (lists) |list| list.deinit(); 131 | self.channel.allocator.free(lists); 132 | } 133 | self.channel.deinit(); 134 | } 135 | }; 136 | 137 | pub const BenchmarkPacketProcessing = struct { 138 | pub const min_iterations = 3; 139 | pub const max_iterations = 5; 140 | 141 | pub const BenchmarkArgs = struct { 142 | n_packets: usize, 143 | name: []const u8 = "", 144 | }; 145 | 146 | pub const args = [_]BenchmarkArgs{ 147 | BenchmarkArgs{ 148 | .n_packets = 100_000, 149 | .name = "100k_msgs", 150 | }, 151 | }; 152 | 153 | pub fn benchmarkReadSocket(bench_args: BenchmarkArgs) !u64 { 154 | const n_packets = bench_args.n_packets; 155 | const allocator = std.heap.page_allocator; 156 | 157 | var channel = Channel(std.ArrayList(Packet)).init(allocator, n_packets); 158 | defer channel.deinit(); 159 | 160 | var socket = try UdpSocket.create(.ipv4, .udp); 161 | try socket.bindToPort(0); 162 | try socket.setReadTimeout(1000000); // 1 second 163 | 164 | const to_endpoint = try socket.getLocalEndPoint(); 165 | 166 | var exit = std.atomic.Value(bool).init(false); 167 | 168 | var handle = try std.Thread.spawn(.{}, readSocket, .{ allocator, socket, channel, &exit, .noop }); 169 | var recv_handle = try std.Thread.spawn(.{}, benchmarkChannelRecv, .{ channel, n_packets }); 170 | 171 | var rand = std.rand.DefaultPrng.init(0); 172 | var packet_buf: [PACKET_DATA_SIZE]u8 = undefined; 173 | var timer = try std.time.Timer.start(); 174 | 175 | // NOTE: send more packets than we need because UDP drops some 176 | for (1..(n_packets * 2 + 1)) |i| { 177 | rand.fill(&packet_buf); 178 | _ = try socket.sendTo(to_endpoint, &packet_buf); 179 | 180 | // 10Kb per second 181 | // each packet is 1k bytes 182 | // = 10 packets per second 183 | if (i % 10 == 0) { 184 | const elapsed = timer.read(); 185 | if (elapsed < std.time.ns_per_s) { 186 | std.time.sleep(std.time.ns_per_s - elapsed); 187 | } 188 | } 189 | } 190 | // std.debug.print("sent all packets.. waiting on receiver\r", .{}); 191 | 192 | recv_handle.join(); 193 | const elapsed = timer.read(); 194 | 195 | exit.store(true, .unordered); 196 | handle.join(); 197 | 198 | return elapsed; 199 | } 200 | }; 201 | 202 | pub fn benchmarkChannelRecv( 203 | channel: *Channel(std.ArrayList(Packet)), 204 | n_values_to_receive: usize, 205 | ) !void { 206 | var count: usize = 0; 207 | while (true) { 208 | const values = (try channel.try_drain()) orelse { 209 | continue; 210 | }; 211 | for (values) |packet_batch| { 212 | count += packet_batch.items.len; 213 | } 214 | // std.debug.print("recv packet count: {d}\r", .{count}); 215 | if (count >= n_values_to_receive) { 216 | break; 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/util/net/utils.zig: -------------------------------------------------------------------------------- 1 | pub fn BitFlags(comptime FlagEnum: type) type { 2 | return packed struct { 3 | state: @typeInfo(FlagEnum).Enum.tag_type = 0, 4 | 5 | const Self = @This(); 6 | 7 | pub const Flag = FlagEnum; 8 | 9 | pub fn from(flag: FlagEnum) Self { 10 | return .{ .state = @intFromEnum(flag) }; 11 | } 12 | 13 | pub fn set(self: *Self, flag: FlagEnum) void { 14 | self.state |= @intFromEnum(flag); 15 | } 16 | 17 | pub fn unset(self: *Self, flag: FlagEnum) void { 18 | self.state &= ~@intFromEnum(flag); 19 | } 20 | 21 | pub fn isSet(self: *const Self, flag: FlagEnum) bool { 22 | return self.state & @intFromEnum(flag) == @intFromEnum(flag); 23 | } 24 | 25 | pub fn intersects(self: *const Self, flag: FlagEnum) bool { 26 | return self.state & @intFromEnum(flag) != 0; 27 | } 28 | 29 | pub fn intersection(self: Self, flag: FlagEnum) Self { 30 | return .{ .state = self.state & @intFromEnum(flag) }; 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/util/time/estimate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const sig = @import("../sig.zig"); 3 | 4 | // TODO: change to writer interface when std.log has mproved 5 | pub fn printTimeEstimate( 6 | // timer should be started at the beginning of the loop 7 | timer: *sig.time.Timer, 8 | total: usize, 9 | i: usize, 10 | comptime name: []const u8, 11 | other_info: ?[]const u8, 12 | ) void { 13 | if (i == 0 or total == 0) return; 14 | if (i > total) { 15 | if (other_info) |info| { 16 | std.log.info("{s} [{s}]: {d}/{d} (?%) (est: ? elp: {s})", .{ 17 | name, 18 | info, 19 | i, 20 | total, 21 | timer.read(), 22 | }); 23 | } else { 24 | std.log.info("{s}: {d}/{d} (?%) (est: ? elp: {s})", .{ 25 | name, 26 | i, 27 | total, 28 | timer.read(), 29 | }); 30 | } 31 | return; 32 | } 33 | 34 | const p_done = i * 100 / total; 35 | const left = total - i; 36 | 37 | const elapsed = timer.read().asNanos(); 38 | const ns_per_vec = elapsed / i; 39 | const ns_left = ns_per_vec * left; 40 | 41 | if (other_info) |info| { 42 | std.log.info("{s} [{s}]: {d}/{d} ({d}%) (est: {s} elp: {s})", .{ 43 | name, 44 | info, 45 | i, 46 | total, 47 | p_done, 48 | std.fmt.fmtDuration(ns_left), 49 | timer.read(), 50 | }); 51 | } else { 52 | std.log.info("{s}: {d}/{d} ({d}%) (est: {s} elp: {s})", .{ 53 | name, 54 | i, 55 | total, 56 | p_done, 57 | std.fmt.fmtDuration(ns_left), 58 | timer.read(), 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/util/time/lib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | pub const estimate = @import("estimate.zig"); 3 | pub const time = @import("time.zig"); 4 | 5 | pub const Instant = time.Instant; 6 | pub const Timer = time.Timer; 7 | pub const Duration = time.Duration; 8 | 9 | /// returns current timestamp in milliseconds 10 | pub fn getWallclockMs() u64 { 11 | return @intCast(std.time.milliTimestamp()); 12 | } 13 | --------------------------------------------------------------------------------