nd: add bitcoin core report sent to UI every min
this adds a very simple bitcoind RPC client able to make a few queries like the blockchain and network status. the daemon uses the client to get the bitcoind status and sends a report through comms periodically. there's also a little playground program which simply dumps a query result to stderr. built on demand with "zig build btcrpc".
parent
7d1ab5cb78
commit
cb2e98cd8b
@ -0,0 +1,296 @@
|
||||
//! a bitcoin core RPC client.
|
||||
|
||||
const std = @import("std");
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const Atomic = std.atomic.Atomic;
|
||||
const base64enc = std.base64.standard.Encoder;
|
||||
|
||||
pub const Client = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
cookiepath: []const u8,
|
||||
addr: []const u8 = "127.0.0.1",
|
||||
port: u16 = 8332,
|
||||
|
||||
// each request gets a new ID with a value of reqid.fetchAdd(1, .Monotonic)
|
||||
reqid: Atomic(u64) = Atomic(u64).init(1),
|
||||
|
||||
pub const Method = enum {
|
||||
getblockchaininfo,
|
||||
getblockhash,
|
||||
getmempoolinfo,
|
||||
getnetworkinfo,
|
||||
};
|
||||
|
||||
pub const RpcError = error{
|
||||
// json-rpc 2.0
|
||||
RpcInvalidRequest,
|
||||
RpcMethodNotFound,
|
||||
RpcInvalidParams,
|
||||
RpcInternalError,
|
||||
RpcParseError,
|
||||
// general purpose errors
|
||||
RpcMiscError,
|
||||
RpcTypeError,
|
||||
RpcInvalidAddressOrKey,
|
||||
RpcOutOfMemory,
|
||||
RpcInvalidParameter,
|
||||
RpcDatabaseError,
|
||||
RpcDeserializationError,
|
||||
RpcVerifyError,
|
||||
RpcVerifyRejected,
|
||||
RpcVerifyAlreadyInChain,
|
||||
RpcInWarmup,
|
||||
RpcMethodDeprecated,
|
||||
// p2p client errors
|
||||
RpcClientNotConnected,
|
||||
RpcClientInInitialDownload,
|
||||
RpcClientNodeAlreadyAdded,
|
||||
RpcClientNodeNotAdded,
|
||||
RpcClientNodeNotConnected,
|
||||
RpcClientInvalidIpOrSubnet,
|
||||
RpcClientP2pDisabled,
|
||||
RpcClientNodeCapacityReached,
|
||||
// chain errors
|
||||
RpcClientMempoolDisabled,
|
||||
};
|
||||
|
||||
pub fn Result(comptime m: Method) type {
|
||||
return struct {
|
||||
value: ResultValue(m),
|
||||
arena: *ArenaAllocator,
|
||||
|
||||
pub fn deinit(self: @This()) void {
|
||||
const allocator = self.arena.child_allocator;
|
||||
self.arena.deinit();
|
||||
allocator.destroy(self.arena);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn ResultValue(comptime m: Method) type {
|
||||
return switch (m) {
|
||||
.getblockchaininfo => BlockchainInfo,
|
||||
.getblockhash => []const u8,
|
||||
.getmempoolinfo => MempoolInfo,
|
||||
.getnetworkinfo => NetworkInfo,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn MethodArgs(comptime m: Method) type {
|
||||
return switch (m) {
|
||||
.getblockchaininfo, .getmempoolinfo, .getnetworkinfo => void,
|
||||
.getblockhash => struct { height: u64 },
|
||||
};
|
||||
}
|
||||
|
||||
fn RpcRequest(comptime m: Method) type {
|
||||
return struct {
|
||||
jsonrpc: []const u8 = "1.0",
|
||||
id: u64,
|
||||
method: []const u8,
|
||||
params: MethodArgs(m),
|
||||
};
|
||||
}
|
||||
|
||||
fn RpcResponse(comptime m: Method) type {
|
||||
return struct {
|
||||
id: u64,
|
||||
result: ?ResultValue(m),
|
||||
@"error": ?struct {
|
||||
code: isize,
|
||||
//message: ?[]const u8, // no use for it atm
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// makes an RPC call to the addr:port endpoint.
|
||||
/// the returned value must be deinit'ed when done.
|
||||
pub fn call(self: *Client, comptime method: Method, args: MethodArgs(method)) !Result(method) {
|
||||
const addrport = try std.net.Address.resolveIp(self.addr, self.port);
|
||||
const reqbytes = try self.formatreq(method, args);
|
||||
defer self.allocator.free(reqbytes);
|
||||
|
||||
// connect and send the request
|
||||
const stream = try std.net.tcpConnectToAddress(addrport);
|
||||
defer stream.close();
|
||||
const reader = stream.reader();
|
||||
_ = try stream.writer().writeAll(reqbytes);
|
||||
|
||||
// read and parse the response
|
||||
try skipResponseHeaders(reader, 4096);
|
||||
const body = try reader.readAllAlloc(self.allocator, 1 << 20); // 1Mb should be enough for all response types
|
||||
defer self.allocator.free(body);
|
||||
return self.parseResponse(method, body);
|
||||
}
|
||||
|
||||
/// reads all response headers, at most `limit` bytes, and returns the index
|
||||
/// at which response body starts or error.EndOfStream.
|
||||
/// single header length must be at most `limit` or 1024, whichever is smaller.
|
||||
fn skipResponseHeaders(r: anytype, comptime limit: usize) !void {
|
||||
var n: usize = 0;
|
||||
var buf: [@min(1024, limit)]u8 = undefined;
|
||||
while (true) {
|
||||
const slice = try r.readUntilDelimiter(&buf, '\n');
|
||||
n += slice.len + 1; // delimiter is not included in the slice
|
||||
if (n > limit) {
|
||||
return error.StreamTooLong;
|
||||
}
|
||||
if (slice.len == 0 or (slice.len == 1 and slice[0] == '\r')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parseResponse(self: Client, comptime m: Method, b: []const u8) !Result(m) {
|
||||
var result = Result(m){
|
||||
.value = undefined,
|
||||
.arena = try self.allocator.create(ArenaAllocator),
|
||||
};
|
||||
errdefer self.allocator.destroy(result.arena);
|
||||
result.arena.* = ArenaAllocator.init(self.allocator);
|
||||
|
||||
var jstream = std.json.TokenStream.init(b);
|
||||
const jopt = std.json.ParseOptions{ .allocator = result.arena.allocator(), .ignore_unknown_fields = true };
|
||||
const resp = try std.json.parse(RpcResponse(m), &jstream, jopt);
|
||||
|
||||
errdefer result.arena.deinit();
|
||||
if (resp.@"error") |errfield| {
|
||||
return rpcErrorFromCode(errfield.code) orelse error.UnknownError;
|
||||
}
|
||||
if (resp.result == null) {
|
||||
return error.NullResult;
|
||||
}
|
||||
|
||||
result.value = resp.result.?;
|
||||
return result;
|
||||
}
|
||||
|
||||
fn formatreq(self: *Client, comptime m: Method, args: MethodArgs(m)) ![]const u8 {
|
||||
const req = RpcRequest(m){
|
||||
.id = self.reqid.fetchAdd(1, .Monotonic),
|
||||
.method = @tagName(m),
|
||||
.params = args,
|
||||
};
|
||||
var jreq = std.ArrayList(u8).init(self.allocator);
|
||||
defer jreq.deinit();
|
||||
try std.json.stringify(req, .{}, jreq.writer());
|
||||
|
||||
const auth = try self.getAuthBase64();
|
||||
defer self.allocator.free(auth);
|
||||
|
||||
var bytes = std.ArrayList(u8).init(self.allocator);
|
||||
const w = bytes.writer();
|
||||
try w.writeAll("POST / HTTP/1.0\r\n");
|
||||
//try w.writeAll("Host: 127.0.0.1\n", .{});
|
||||
try w.writeAll("Connection: close\r\n");
|
||||
try w.print("Authorization: Basic {s}\r\n", .{auth});
|
||||
try w.writeAll("Accept: application/json-rpc\r\n");
|
||||
try w.writeAll("Content-Type: application/json-rpc\r\n");
|
||||
try w.print("Content-Length: {d}\r\n", .{jreq.items.len});
|
||||
try w.writeAll("\r\n");
|
||||
try w.writeAll(jreq.items);
|
||||
return bytes.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn getAuthBase64(self: Client) ![]const u8 {
|
||||
const file = try std.fs.openFileAbsolute(self.cookiepath, .{ .mode = .read_only });
|
||||
defer file.close();
|
||||
const cookie = try file.readToEndAlloc(self.allocator, 1024);
|
||||
defer self.allocator.free(cookie);
|
||||
var auth = try self.allocator.alloc(u8, base64enc.calcSize(cookie.len));
|
||||
return base64enc.encode(auth, cookie);
|
||||
}
|
||||
|
||||
// taken from bitcoind source code.
|
||||
// see https://github.com/bitcoin/bitcoin/blob/64440bb73/src/rpc/protocol.h#L23
|
||||
fn rpcErrorFromCode(code: isize) ?RpcError {
|
||||
return switch (code) {
|
||||
// json-rpc 2.0
|
||||
-32600 => error.RpcInvalidRequest,
|
||||
-32601 => error.RpcMethodNotFound,
|
||||
-32602 => error.RpcInvalidParams,
|
||||
-32603 => error.RpcInternalError,
|
||||
-32700 => error.RpcParseError,
|
||||
// general purpose errors
|
||||
-1 => error.RpcMiscError,
|
||||
-3 => error.RpcTypeError,
|
||||
-5 => error.RpcInvalidAddressOrKey,
|
||||
-7 => error.RpcOutOfMemory,
|
||||
-8 => error.RpcInvalidParameter,
|
||||
-20 => error.RpcDatabaseError,
|
||||
-22 => error.RpcDeserializationError,
|
||||
-25 => error.RpcVerifyError,
|
||||
-26 => error.RpcVerifyRejected,
|
||||
-27 => error.RpcVerifyAlreadyInChain,
|
||||
-28 => error.RpcInWarmup,
|
||||
-32 => error.RpcMethodDeprecated,
|
||||
// p2p client errors
|
||||
-9 => error.RpcClientNotConnected,
|
||||
-10 => error.RpcClientInInitialDownload,
|
||||
-23 => error.RpcClientNodeAlreadyAdded,
|
||||
-24 => error.RpcClientNodeNotAdded,
|
||||
-29 => error.RpcClientNodeNotConnected,
|
||||
-30 => error.RpcClientInvalidIpOrSubnet,
|
||||
-31 => error.RpcClientP2pDisabled,
|
||||
-34 => error.RpcClientNodeCapacityReached,
|
||||
// chain errors
|
||||
-33 => error.RpcClientMempoolDisabled,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const BlockchainInfo = struct {
|
||||
chain: []const u8,
|
||||
blocks: u64,
|
||||
headers: u64,
|
||||
bestblockhash: []const u8,
|
||||
difficulty: f64,
|
||||
time: u64, // block time, unix epoch
|
||||
mediantime: u64, // median block time, unix epoch
|
||||
verificationprogress: f32, // estimate in [0..1]
|
||||
initialblockdownload: bool,
|
||||
size_on_disk: u64,
|
||||
pruned: bool,
|
||||
//pruneheight: ?u64, // present if pruning is enabled
|
||||
//automatic_prunning: ?bool, // present if pruning is enabled
|
||||
//prune_target_size: ?u64, // present if automatic is enabled
|
||||
warnings: []const u8,
|
||||
};
|
||||
|
||||
pub const MempoolInfo = struct {
|
||||
loaded: bool, // whether the mempool is fully loaded
|
||||
size: usize, // tx count
|
||||
bytes: u64, // sum of all virtual transaction sizes as per BIP-141 (discounted witness data)
|
||||
usage: u64, // total memory usage
|
||||
total_fee: f32, // total fees in BTC ignoring modified fees through prioritisetransaction
|
||||
maxmempool: u64, // memory usage cap, in bytes
|
||||
mempoolminfee: f32, // min fee rate in BTC/kvB for tx to be accepted
|
||||
minrelaytxfee: f32, // current min relay fee rate
|
||||
incrementalrelayfee: f32, // min fee rate increment for replacement, in BTC/kvB
|
||||
unbroadcastcount: u64, // number of transactions that haven't passed initial broadcast yet
|
||||
fullrbf: bool, // whether the mempool accepts RBF without replaceability signaling inspection
|
||||
};
|
||||
|
||||
pub const NetworkInfo = struct {
|
||||
version: u32,
|
||||
subversion: []const u8,
|
||||
protocolversion: u32,
|
||||
connections: u16, // in + out
|
||||
connections_in: u16,
|
||||
connections_out: u16,
|
||||
networkactive: bool,
|
||||
networks: []struct {
|
||||
name: []const u8, // ipv4, ipv6, onion, i2p, cjdns
|
||||
limited: bool, // whether this network is limited with -onlynet flag
|
||||
reachable: bool,
|
||||
},
|
||||
relayfee: f32, // min rate, in BTC/kvB
|
||||
incrementalfee: f32, // min rate increment for RBF in BTC/vkB
|
||||
localaddresses: []struct {
|
||||
address: []const u8,
|
||||
port: u16,
|
||||
score: i16,
|
||||
},
|
||||
warnings: []const u8,
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
const std = @import("std");
|
||||
const base64enc = std.base64.standard.Encoder;
|
||||
const bitcoinrpc = @import("bitcoindrpc");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer if (gpa_state.deinit()) {
|
||||
std.debug.print("memory leaks detected!", .{});
|
||||
};
|
||||
const gpa = gpa_state.allocator();
|
||||
|
||||
var client = bitcoinrpc.Client{
|
||||
.allocator = gpa,
|
||||
.cookiepath = "/ssd/bitcoind/mainnet/.cookie",
|
||||
};
|
||||
|
||||
const res = try client.call(.getmempoolinfo, {});
|
||||
defer res.deinit();
|
||||
std.debug.print("{any}\n", .{res.value});
|
||||
}
|
Reference in New Issue