diff --git a/src/bip44.zig b/src/bip44.zig index d07689b..642fb59 100644 --- a/src/bip44.zig +++ b/src/bip44.zig @@ -182,6 +182,13 @@ pub fn generatePublicFromAccountPublicKey(extended_pubkey: bip32.ExtendedPublicK return index_extended_pubkey.key; } +pub fn generatePrivateFromAccountPrivateKey(extended_privkey: bip32.ExtendedPrivateKey, change: u32, index: u32) ![32]u8 { + const change_extended_privkey = try bip32.deriveChildFromExtendedPrivateKey(extended_privkey, change); + const index_extended_privkey = try bip32.deriveChildFromExtendedPrivateKey(change_extended_privkey, index); + + return index_extended_privkey.privatekey; +} + test "generateAccount" { const addr_serialized = "tprv8ZgxMBicQKsPefj8cBDzcXJYcnvWBLQwG9sAvKyAYRPiLtdZXvdAmqtjzeHbX7ZX2LY8Sfb7SaLSJbGCFBPMFZdnmv4D7UebvyLTC974BA4".*; const master_extended_privkey = try bip32.ExtendedPrivateKey.fromAddress(addr_serialized); diff --git a/src/crypto/ecdsa.zig b/src/crypto/ecdsa.zig index 7c1aac7..2e800c7 100644 --- a/src/crypto/ecdsa.zig +++ b/src/crypto/ecdsa.zig @@ -50,7 +50,7 @@ pub const Signature = struct { // nonce is used in tests to recreate deterministic signature. // I don't like this parameter, using the same nonce can expose the private key, but I havent found any better solution pub fn sign(pk: [32]u8, z: [32]u8, comptime nonce: ?u256) Signature { - comptime if (nonce == null and is_test == false) { + comptime if (nonce != null and is_test == false) { unreachable; }; const n = crypto.secp256k1_number_of_points; diff --git a/src/db/db.zig b/src/db/db.zig index 846a90e..05248ab 100644 --- a/src/db/db.zig +++ b/src/db/db.zig @@ -116,18 +116,31 @@ pub fn saveInputsAndMarkOutputs(db: *sqlite.Db, inputs: std.ArrayList(Input)) !v try stmt_commit.exec(.{}, .{}); } -pub fn getOutput(db: *sqlite.Db, txid: [64]u8, vout: u32) !?Output { - const sql_output = "SELECT txid, vout, amount FROM outputs WHERE txid = ? AND vout = ?"; +pub fn existsOutput(db: *sqlite.Db, txid: [64]u8, vout: u32) !bool { + const sql = "SELECT COUNT(*) AS total FROM outputs WHERE txid = ? AND vout = ?"; + var stmt = try db.prepare(sql); + defer stmt.deinit(); + const row = try stmt.one(struct { total: usize }, .{}, .{ .txid = txid, .vout = vout }); + return row.?.total > 1; +} + +pub fn getOutput(allocator: std.mem.Allocator, db: *sqlite.Db, txid: [64]u8, vout: u32) !?Output { + const sql_output = "SELECT txid, vout, amount, unspent, path FROM outputs WHERE txid = ? AND vout = ?"; var stmt = try db.prepare(sql_output); defer stmt.deinit(); - const row = try stmt.one(struct { txid: [64]u8, vout: u32, amount: u64 }, .{}, .{ .txid = txid, .vout = vout }); + const row = try stmt.oneAlloc(struct { txid: [64]u8, vout: u32, amount: u64, unspent: bool, path: []u8 }, allocator, .{}, .{ .txid = txid, .vout = vout }); + if (row != null) { + defer allocator.free(row.?.path); return Output{ .txid = row.?.txid, .vout = row.?.vout, .amount = row.?.amount, + .unspent = row.?.unspent, + .keypath = try KeyPath(5).fromStr(row.?.path), }; } + return null; } @@ -174,11 +187,11 @@ pub fn getBalance(db: *sqlite.Db, current_block: usize) !u64 { } // Memory ownership to the caller -pub fn getDescriptors(allocator: std.mem.Allocator, db: *sqlite.Db) ![]Descriptor { - const sql = "SELECT extended_key, path, private FROM descriptors;"; +pub fn getDescriptors(allocator: std.mem.Allocator, db: *sqlite.Db, private: bool) ![]Descriptor { + const sql = "SELECT extended_key, path, private FROM descriptors WHERE private = ?;"; var stmt = try db.prepare(sql); defer stmt.deinit(); - const rows = try stmt.all(struct { extended_key: [111]u8, path: []const u8, private: bool }, allocator, .{}, .{}); + const rows = try stmt.all(struct { extended_key: [111]u8, path: []const u8, private: bool }, allocator, .{}, .{ .private = private }); defer { for (rows) |row| { allocator.free(row.path); @@ -194,7 +207,7 @@ pub fn getDescriptors(allocator: std.mem.Allocator, db: *sqlite.Db) ![]Descripto return descriptors; } -pub fn getDescriptor(allocator: std.mem.Allocator, db: *sqlite.Db, path: []u8, private: bool) !?Descriptor { +pub fn getDescriptor(allocator: std.mem.Allocator, db: *sqlite.Db, path: []const u8, private: bool) !?Descriptor { const sql = "SELECT extended_key, path, private FROM descriptors WHERE path=? AND private=? LIMIT 1;"; var stmt = try db.prepare(sql); defer stmt.deinit(); @@ -249,20 +262,40 @@ fn sqliteKeypathLastIndex(str: []const u8) u32 { return k.path[4]; } -pub fn getLastUsedIndexFromOutputs(db: *sqlite.Db) !?u32 { - const sql_count = "SELECT COUNT(*) as total from outputs;"; +pub fn getLastUsedIndexFromOutputs(db: *sqlite.Db, base_path: []u8) !?u32 { + const sql_count = "SELECT COUNT(*) as total from outputs WHERE path LIKE ?;"; var stmt_count = try db.prepare(sql_count); defer stmt_count.deinit(); - const row_count = try stmt_count.one(struct { total: usize }, .{}, .{}); + const row_count = try stmt_count.one(struct { total: usize }, .{}, .{base_path}); if (row_count.?.total == 0) { return null; } try db.createScalarFunction("KEYPATH_LAST_INDEX", sqliteKeypathLastIndex, .{}); - const sql = "SELECT MAX(KEYPATH_LAST_INDEX(path)) AS last FROM outputs;"; + const sql = "SELECT MAX(KEYPATH_LAST_INDEX(path)) AS last FROM outputs WHERE path LIKE ?;"; var stmt = try db.prepare(sql); defer stmt.deinit(); - const row = try stmt.one(struct { last: u32 }, .{}, .{}); + const row = try stmt.one(struct { last: u32 }, .{}, .{base_path}); assert(row != null); // a row must exists since the count is > 0 return row.?.last; } + +pub fn getUnspentOutputs(allocator: std.mem.Allocator, db: *sqlite.Db) ![]Output { + const sql = "SELECT txid, vout, amount, unspent, path FROM outputs WHERE unspent = true"; + var stmt = try db.prepare(sql); + defer stmt.deinit(); + const rows = try stmt.all(struct { txid: [64]u8, vout: u32, amount: u64, unspent: bool, path: []u8 }, allocator, .{}, .{}); + + defer { + for (rows) |row| { + allocator.free(row.path); + } + allocator.free(rows); + } + + var outputs = try allocator.alloc(Output, rows.len); + for (rows, 0..) |row, i| { + outputs[i] = Output{ .txid = row.txid, .vout = row.vout, .amount = row.amount, .unspent = row.unspent, .keypath = try KeyPath(5).fromStr(row.path) }; + } + return outputs; +} diff --git a/src/indexer.zig b/src/indexer.zig index 4bec0f3..f5eadb8 100644 --- a/src/indexer.zig +++ b/src/indexer.zig @@ -72,7 +72,7 @@ pub fn main() !void { } // Load descriptors - const descriptors = try db.getDescriptors(allocator, &database); + const descriptors = try db.getDescriptors(allocator, &database, false); // only public descriptors defer allocator.free(descriptors); for (descriptors) |descriptor| { @@ -284,10 +284,10 @@ fn getInputsFor(allocator: std.mem.Allocator, database: *sqlite.Db, transactions continue; } const input = transaction.inputs.items[i]; - const existing = try db.getOutput(database, input.prevout.?.txid, input.prevout.?.vout); - if (existing != null) { + const exists = try db.existsOutput(database, input.prevout.?.txid, input.prevout.?.vout); + if (exists == true) { const txid = try transaction.getTXID(); - try inputs.append(.{ .txid = txid, .output_txid = existing.?.txid, .output_vout = existing.?.vout }); + try inputs.append(.{ .txid = txid, .output_txid = input.prevout.?.txid, .output_vout = input.prevout.?.vout }); } } } diff --git a/src/tx.zig b/src/tx.zig index fcaa9c0..4734676 100644 --- a/src/tx.zig +++ b/src/tx.zig @@ -206,6 +206,17 @@ pub const Transaction = struct { try writer.print(" script pubkey: {s}\n", .{output.script_pubkey}); try writer.print(" amount: {d}\n\n", .{output.amount}); } + + if (self.witness.items.len > 0) { + try writer.print("Witness: \n", .{}); + for (0..self.witness.items.len) |i| { + const witness = self.witness.items[i]; + for (0..witness.stack_items.items.len) |j| { + const stack_item = witness.stack_items.items[j]; + try writer.print(" item: {s}\n", .{stack_item.item}); + } + } + } } }; @@ -307,7 +318,7 @@ test "createTx" { } // [72]u8 = 64 txid + 8 vout -pub fn signTx(allocator: std.mem.Allocator, tx: *Transaction, privkey: [32]u8, pubkeys: std.AutoHashMap([72]u8, bip32.PublicKey), comptime nonce: ?u256) !void { +pub fn signTx(allocator: std.mem.Allocator, tx: *Transaction, privkeys: std.AutoHashMap([72]u8, [32]u8), pubkeys: std.AutoHashMap([72]u8, bip32.PublicKey), comptime nonce: ?u256) !void { const inputs_preimage_hash = try getTxInputsPreImageHash(allocator, tx.inputs.items); const inputs_sequences_preimage_hash = try getTxInputsSequencesPreImageHash(allocator, tx.inputs.items); const outputs_preimage_hash = try getTxOutputsPreImageHash(allocator, tx.outputs.items); @@ -321,6 +332,7 @@ pub fn signTx(allocator: std.mem.Allocator, tx: *Transaction, privkey: [32]u8, p try utils.intToHexStr(u32, @byteSwap(input.prevout.?.vout), &vout_hex); _ = try std.fmt.bufPrint(&key, "{s}{s}", .{ input.prevout.?.txid, vout_hex }); const pubkey = pubkeys.get(key).?; + const privkey = privkeys.get(key).?; const preimage_hash = try getPreImageHash(tx.version, inputs_preimage_hash, inputs_sequences_preimage_hash, outputs_preimage_hash, tx.locktime, input, pubkey, sighash_type); const witness = try createWitness(allocator, preimage_hash, privkey, pubkey, sighash_type, nonce); try tx.addWitness(witness); diff --git a/src/walle.zig b/src/walle.zig index c829fa5..599d278 100644 --- a/src/walle.zig +++ b/src/walle.zig @@ -7,9 +7,54 @@ const Network = @import("const.zig").Network; const utils = @import("utils.zig"); const address = @import("address.zig"); const script = @import("script.zig"); +const Output = @import("tx.zig").Output; +const tx = @import("tx.zig"); +const sqlite = @import("sqlite"); +const crypto = @import("crypto"); fn showHelp() void { - std.debug.print("Valid commands: walletcreate, walletimport\nFor more information use walle help", .{}); + std.debug.print("Valid commands: createwallet, newaddr, listoutputs, send\nFor more information use walle help", .{}); +} + +fn generateNextAvailableAddress(allocator: std.mem.Allocator, database: *sqlite.Db, descriptor: bip44.Descriptor, change: u8, network: Network) !address.Address { + const descriptor_path = try descriptor.keypath.toStr(allocator, null); + const base_path = try allocator.alloc(u8, descriptor_path.len + 4); + defer allocator.free(base_path); + _ = try std.fmt.bufPrint(base_path, "{s}/{d}/%", .{ descriptor_path, change }); + const res = try db.getLastUsedIndexFromOutputs(database, base_path); + var next_index: u32 = 0; + if (res != null) { + next_index = res.?; + } + + // Generate index with change = 1 for external address + const account_pubkey = try bip32.ExtendedPublicKey.fromAddress(descriptor.extended_key); + const pubkey = try bip44.generatePublicFromAccountPublicKey(account_pubkey, change, next_index); + const pubkey_hash = try pubkey.toHashHex(); + const s = try script.p2wpkh(allocator, &pubkey_hash); + const addr = try address.deriveP2WPKHAddress(allocator, s, network); + return addr; +} + +fn getDescriptorPath(network: Network) ![9]u8 { + var descriptor_path: [9]u8 = undefined; + const cointype: u8 = if (network == .mainnet) bip44.bitcoin_coin_type else bip44.bitcoin_testnet_coin_type; + _ = try std.fmt.bufPrint(&descriptor_path, "{d}'/{d}'/{d}'", .{ bip44.bip_84_purpose, cointype, 0 }); + + return descriptor_path; +} + +fn addressToScript(allocator: std.mem.Allocator, addr: []const u8) ![]u8 { + var pubkey_hash: [20]u8 = undefined; + _ = try crypto.Bech32Decoder.decode(&pubkey_hash, addr); + var pubkey_hash_hex: [40]u8 = undefined; + _ = try std.fmt.bufPrint(&pubkey_hash_hex, "{x}", .{std.fmt.fmtSliceHexLower(&pubkey_hash)}); + const s = try script.p2wpkh(allocator, &pubkey_hash_hex); + defer s.deinit(); + const cap = s.hexCap(); + const buffer = try allocator.alloc(u8, cap); + try s.toHex(buffer); + return buffer; } pub fn main() !void { @@ -17,8 +62,10 @@ pub fn main() !void { const allocator = gpa.allocator(); const Commands = enum { - walletcreate, - addrnew, + createwallet, + newaddr, + listoutputs, + send, }; const args = std.process.argsAlloc(allocator) catch { @@ -42,9 +89,9 @@ pub fn main() !void { try db.initDB(&database); switch (cmd.?) { - .walletcreate => { + .createwallet => { if (args.len < 3 or std.mem.eql(u8, args[2], "help")) { - std.debug.print("Create new wallet\nwalle walletcreate \n", .{}); + std.debug.print("Create new wallet\nwalle createwallet \n", .{}); return; } @@ -91,33 +138,126 @@ pub fn main() !void { std.debug.print("Wallet initialized\n", .{}); }, - .addrnew => { + .newaddr => { if (args.len < 3 or std.mem.eql(u8, args[2], "help")) { - std.debug.print("Create new wallet\nwalle addrnew \n", .{}); + std.debug.print("Create new wallet\nwalle newaddr \n", .{}); return; } const network: Network = if (std.mem.eql(u8, args[2], "mainnet")) .mainnet else .testnet; - const res = try db.getLastUsedIndexFromOutputs(&database); - var next_index: u32 = 0; - if (res != null) { - next_index = res.?; - } - - var descriptor_path = "84'/1'/0'".*; + const descriptor_path = try getDescriptorPath(network); const descriptor = try db.getDescriptor(allocator, &database, &descriptor_path, false); if (descriptor == null) { std.debug.print("A wallet do not exists. Please create it using walletcreate\n", .{}); return; } - // Generate index with change = 1 for external address - const account_pubkey = try bip32.ExtendedPublicKey.fromAddress(descriptor.?.extended_key); - const pubkey = try bip44.generatePublicFromAccountPublicKey(account_pubkey, bip44.change_external_chain, next_index); - const pubkey_hash = try pubkey.toHashHex(); - const s = try script.p2wpkh(allocator, &pubkey_hash); - const addr = try address.deriveP2WPKHAddress(allocator, s, network); + const addr = try generateNextAvailableAddress(allocator, &database, descriptor.?, bip44.change_external_chain, network); defer addr.deinit(); std.debug.print("addr {s}\n", .{addr.val}); }, + .listoutputs => { + if (args.len > 2) { + std.debug.print("List all the outputs\n", .{}); + return; + } + + const outputs = try db.getUnspentOutputs(allocator, &database); + defer allocator.free(outputs); + + var balance: usize = 0; + std.debug.print("Available outputs\n", .{}); + for (outputs) |output| { + std.debug.print("Output txid={s} vout={d} -> amount = {d}\n", .{ output.txid, output.vout, output.amount }); + balance += output.amount; + } + std.debug.print("Avaiable balance = {d}\n", .{balance}); + }, + .send => { + // TODO: change 3 to 7 + if (args.len < 3 or std.mem.eql(u8, args[2], "help")) { + std.debug.print("Send BTC to an address (support only segwit address)\nwalle send [ ]\n", .{}); + return; + } + + const network: Network = if (std.mem.eql(u8, args[2], "mainnet")) .mainnet else .testnet; + const destination_address = args[3]; + const amount = try std.fmt.parseInt(usize, args[4], 10); + const total_outputs = try std.fmt.parseInt(usize, args[5], 10); + + std.debug.print("Sending to {s} an amount of {d} using {d} outputs\n", .{ destination_address, amount, total_outputs }); + + const outputs = try allocator.alloc(Output, total_outputs); + var total_available_amount: usize = 0; + for (0..total_outputs) |i| { + const txid: [64]u8 = args[6 + (i * 2)][0..64].*; + const vout = try std.fmt.parseInt(u32, args[7 + (i * 2)], 10); + + const output = try db.getOutput(allocator, &database, txid, vout); + if (output == null) { + std.debug.print("Specified output with txid {s} and vout {d} does not exist\n", .{ txid, vout }); + return; + } + if (output.?.unspent.? != true) { + std.debug.print("Specified output with txid {s} and vout {d} is already spent\n", .{ txid, vout }); + return; + } + total_available_amount += output.?.amount; + outputs[i] = output.?; + } + + const mining_fee = 100; + if (total_available_amount < amount - mining_fee) { + std.debug.print("Specified outputs amount is {d} while the amount you're trying to spend is {d}.\n", .{ total_available_amount, amount }); + return; + } + + const descriptor_path = try getDescriptorPath(network); + const descriptor = try db.getDescriptor(allocator, &database, &descriptor_path, false); + if (descriptor == null) { + std.debug.print("A wallet do not exists. Please create it using walletcreate\n", .{}); + return; + } + + var pubkeys = std.AutoHashMap([72]u8, bip32.PublicKey).init(allocator); + var privkeys = std.AutoHashMap([72]u8, [32]u8).init(allocator); + const tx_inputs = try allocator.alloc(tx.TxInput, outputs.len); + for (outputs, 0..) |output, i| { + tx_inputs[i] = try tx.TxInput.init(allocator, output, "", 4294967293); // This sequence will enable both locktime and rbf + var map_key: [72]u8 = undefined; + var vout_hex: [8]u8 = undefined; + try utils.intToHexStr(u32, @byteSwap(output.vout), &vout_hex); + _ = try std.fmt.bufPrint(&map_key, "{s}{s}", .{ output.txid, vout_hex }); + + const output_descriptor_path = try output.keypath.?.toStr(allocator, 3); + + const public_descriptor = try db.getDescriptor(allocator, &database, output_descriptor_path, false); + const private_descriptor = try db.getDescriptor(allocator, &database, output_descriptor_path, true); + + const extended_pubkey = try bip32.ExtendedPublicKey.fromAddress(public_descriptor.?.extended_key); + const pubkey = try bip44.generatePublicFromAccountPublicKey(extended_pubkey, output.keypath.?.path[3], output.keypath.?.path[4]); + + const extended_privkey = try bip32.ExtendedPrivateKey.fromAddress(private_descriptor.?.extended_key); + const privkey = try bip44.generatePrivateFromAccountPrivateKey(extended_privkey, output.keypath.?.path[3], output.keypath.?.path[4]); + + try pubkeys.put(map_key, pubkey); + try privkeys.put(map_key, privkey); + } + const change_amount = total_available_amount - amount - mining_fee; + const tx_outputs_cap: u8 = if (change_amount > 0) 2 else 1; + const tx_outputs = try allocator.alloc(tx.TxOutput, tx_outputs_cap); + const script_pubkey_output = try addressToScript(allocator, destination_address); + tx_outputs[0] = try tx.TxOutput.init(allocator, amount, script_pubkey_output); + if (change_amount > 0) { + // Change address + const change_addr = try generateNextAvailableAddress(allocator, &database, descriptor.?, bip44.change_internal_chain, network); + const script_pubkey_change = try addressToScript(allocator, change_addr.val); + tx_outputs[1] = try tx.TxOutput.init(allocator, change_amount, script_pubkey_change); + } + + var send_transaction = try tx.createTx(allocator, tx_inputs, tx_outputs); + + try tx.signTx(allocator, &send_transaction, privkeys, pubkeys, null); + std.debug.print("send transaction\n{}\n", .{send_transaction}); + }, } }