Skip to content

Commit

Permalink
Merge pull request #13 from jsign/jsign-secp
Browse files Browse the repository at this point in the history
Support transaction signing and public key recovery
  • Loading branch information
jsign authored Dec 5, 2023
2 parents ecea4b6 + 7f70657 commit b6bc704
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 64 deletions.
10 changes: 8 additions & 2 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ pub fn build(b: *std.Build) void {
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});

const mod_rlp = b.dependency("zig-rlp", .{}).module("zig-rlp");

// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});

const mod_rlp = b.dependency("zig-rlp", .{ .target = target, .optimize = optimize }).module("rlp");
const depSecp256k1 = b.dependency("zig-eth-secp256k1", .{ .target = target, .optimize = optimize });
const mod_secp256k1 = depSecp256k1.module("zig-eth-secp256k1");

const ethash = b.addStaticLibrary(.{
.name = "ethash",
.optimize = optimize,
Expand Down Expand Up @@ -96,6 +98,8 @@ pub fn build(b: *std.Build) void {
exe.linkLibrary(evmone);
exe.linkLibC();
exe.addModule("zig-rlp", mod_rlp);
exe.linkLibrary(depSecp256k1.artifact("secp256k1"));
exe.addModule("zig-eth-secp256k1", mod_secp256k1);

// This declares intent for the executable to be installed into the
// standard location when the user invokes the "install" step (the default
Expand Down Expand Up @@ -146,6 +150,8 @@ pub fn build(b: *std.Build) void {
unit_tests.linkLibrary(evmone);
unit_tests.linkLibC();
unit_tests.addModule("zig-rlp", mod_rlp);
unit_tests.linkLibrary(depSecp256k1.artifact("secp256k1"));
unit_tests.addModule("zig-eth-secp256k1", mod_secp256k1);

const run_unit_tests = b.addRunArtifact(unit_tests);
run_unit_tests.has_side_effects = true;
Expand Down
8 changes: 6 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
.version = "0.0.1-beta-0",
.dependencies = .{
.@"zig-rlp" = .{
.url = "https://github.com/gballet/zig-rlp/archive/refs/tags/v0.0.1-beta-0.tar.gz",
.hash = "122000e0811d6cb4758f6122b1de2d384efa32b9b2714caec2236f6b34b0529d699c",
.url = "https://github.com/gballet/zig-rlp/archive/refs/tags/v0.0.1-beta-3.tar.gz",
.hash = "1220d36c04cfa7040278000a869ebde502ac1bffc77757deef631396c26f7a2bf8b4",
},
.@"zig-eth-secp256k1" = .{
.url = "https://github.com/jsign/zig-eth-secp256k1/archive/refs/tags/v0.1.0-beta-3.tar.gz",
.hash = "1220fcf062f8ee89b343e1588ac3cc002f37ee3f72841dd7f9493d9c09acad7915a3",
},
},
}
50 changes: 50 additions & 0 deletions src/crypto/ecdsa.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const std = @import("std");
const secp256k1 = @import("zig-eth-secp256k1");

pub const Signature = [65]u8;
pub const Message = [32]u8;
pub const PrivateKey = [32]u8;
pub const CompressedPublicKey = [33]u8;
pub const UncompressedPublicKey = [65]u8;

pub const Signer = struct {
sec: secp256k1.Secp256k1,

pub fn init() !Signer {
return Signer{
.sec = try secp256k1.Secp256k1.init(),
};
}

pub fn erecover(self: Signer, sig: Signature, msg: Message) !UncompressedPublicKey {
return try self.sec.recoverPubkey(msg, sig);
}

pub fn sign(self: Signer, msg: Message, privkey: PrivateKey) !Signature {
return try self.sec.sign(msg, privkey);
}
};

// The following test values where generated using geth, as a reference.
const hashed_msg = hexToBytes("0x05e0e0ff09b01e5626daac3165b82afa42be29197b82e8a5a8800740ee7519d2");
const private_key = hexToBytes("0xf457cd3bd0186e342d243ea40ad78fe8e81743f90852e87074e68d8c94c2a42e");
const signature = hexToBytes("0x5a62891eb3e26f3a2344f93a7bad7fe5e670dc45cbdbf0e5bbdba4399238b5e6614caf592f96ee273a2bf018a976e7bf4b63777f9e53ce819d96c5035611400600");
const uncompressed_pubkey = hexToBytes("0x04682bade67348db99074fcaaffef29394192e7e227a2bdb49f930c74358060c6a42df70f7ef8aadd94854abe646e047142fad42811e325afbec4753342d630b1e");
const compressed_pubkey = hexToBytes("0x02682bade67348db99074fcaaffef29394192e7e227a2bdb49f930c74358060c6a");

test "erecover" {
const signer = try Signer.init();
const got_pubkey = try signer.erecover(signature, hashed_msg);
try std.testing.expectEqual(uncompressed_pubkey, got_pubkey);
}

// TODO: must to hexutils when a current PR gets merged.
fn hexToBytes(comptime hex: []const u8) [hex.len / 2 - if (std.mem.startsWith(u8, hex, "0x")) 1 else 0]u8 {
var target = hex;
if (std.mem.startsWith(u8, hex, "0x")) {
target = hex[2..];
}
var ret: [target.len / 2]u8 = undefined;
_ = std.fmt.hexToBytes(&ret, target) catch unreachable;
return ret;
}
7 changes: 7 additions & 0 deletions src/crypto/hasher.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const Keccak = @import("std").crypto.hash.sha3.Keccak256;

pub fn keccak256(data: []const u8) [32]u8 {
var ret: [32]u8 = undefined;
Keccak.hash(data, &ret, .{});
return ret;
}
17 changes: 13 additions & 4 deletions src/exec-spec-tests/execspectests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const BlockHeader = types.BlockHeader;
const vm = @import("../vm/vm.zig");
const VM = vm.VM;
const StateDB = vm.StateDB;
const TxnSigner = @import("../signer/signer.zig").TxnSigner;
const ecdsa = @import("../crypto/ecdsa.zig");
const log = std.log.scoped(.execspectests);

const HexString = []const u8;
Expand Down Expand Up @@ -69,6 +71,7 @@ pub const FixtureTest = struct {
var evm = VM.init(&db);

// 2. Execute blocks.
const txn_signer = try TxnSigner.init();
for (self.blocks) |encoded_block| {
var out = try allocator.alloc(u8, encoded_block.rlp.len / 2);
defer allocator.free(out);
Expand All @@ -79,10 +82,10 @@ pub const FixtureTest = struct {
var txns = try allocator.alloc(Transaction, encoded_block.transactions.len);
defer allocator.free(txns);
for (encoded_block.transactions, 0..) |tx_hex, i| {
txns[i] = try tx_hex.to_vm_transaction(allocator);
txns[i] = try tx_hex.to_vm_transaction(allocator, txn_signer);
}

try evm.run_block(block, txns);
try evm.run_block(allocator, txn_signer, block, txns);
}

// 3. Verify that the post state matches what the fixture `postState` claims is true.
Expand Down Expand Up @@ -145,7 +148,7 @@ pub const TransactionHex = struct {
data: HexString,
gasLimit: HexString,

pub fn to_vm_transaction(self: *const TransactionHex, allocator: Allocator) !Transaction {
pub fn to_vm_transaction(self: TransactionHex, allocator: Allocator, txn_signer: TxnSigner) !Transaction {
const type_ = try std.fmt.parseInt(u8, self.type[2..], 16);
const chain_id = try std.fmt.parseInt(u256, self.chainId[2..], 16);
const nonce = try std.fmt.parseUnsigned(u64, self.nonce[2..], 16);
Expand All @@ -160,7 +163,13 @@ pub const TransactionHex = struct {
_ = try std.fmt.hexToBytes(data, self.data[2..]);
const gas_limit = try std.fmt.parseUnsigned(u64, self.gasLimit[2..], 16);

return Transaction.init(type_, chain_id, nonce, gas_price, value, to, data, gas_limit);
var txn = Transaction.init(type_, chain_id, nonce, gas_price, value, to, data, gas_limit);
var privkey: ecdsa.PrivateKey = undefined;
_ = try std.fmt.hexToBytes(&privkey, self.secretKey[2..]);
const sig = try txn_signer.sign(allocator, txn, privkey);
txn.setSignature(sig.v, sig.r, sig.s);

return txn;
}
};

Expand Down
29 changes: 19 additions & 10 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ const VM = @import("vm/vm.zig").VM;
const StateDB = @import("vm/statedb.zig");
const Block = types.Block;
const Transaction = types.Transaction;
const TxnSigner = @import("signer/signer.zig").TxnSigner;

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
var allocator = gpa.allocator();

std.log.info("Welcome to phant! 🐘", .{});
const txn_signer = try TxnSigner.init();

// Create block.
const block = Block{
Expand All @@ -37,14 +39,19 @@ pub fn main() !void {

// Create some dummy transaction.
const txn = Transaction{
.type = 0,
.chain_id = 1,
.nonce = 0,
.gas_price = 10,
.value = 0,
.to = [_]u8{0} ** 18 ++ [_]u8{ 0x41, 0x42 },
.data = &[_]u8{},
.gas_limit = 100_000,
.data = .{
.type = 0,
.chain_id = 1,
.nonce = 0,
.gas_price = 10,
.value = 0,
.to = [_]u8{0} ** 18 ++ [_]u8{ 0x41, 0x42 },
.data = &[_]u8{},
.gas_limit = 100_000,
},
.r = 0,
.s = 0,
.v = 0,
};

// Create the corresponding AccountState for txn.to, in particular with relevant bytecode
Expand All @@ -53,7 +60,8 @@ pub fn main() !void {
0x61, 0x41, 0x42, // PUSH2 0x4142
0x31, // BALANCE
};
var account_state = try AccountState.init(allocator, txn.get_from(), 0, 1_000_000, &code);
const sender_addr = try txn_signer.get_sender(allocator, txn);
var account_state = try AccountState.init(allocator, sender_addr, 0, 1_000_000, &code);
defer account_state.deinit();

// Create the statedb, with the created account state.
Expand All @@ -64,7 +72,7 @@ pub fn main() !void {
var vm = VM.init(&statedb);

// Execute block with txns.
vm.run_block(block, &[_]Transaction{txn}) catch |err| {
vm.run_block(allocator, txn_signer, block, &[_]Transaction{txn}) catch |err| {
std.log.err("error executing transaction: {}", .{err});
return;
};
Expand All @@ -77,4 +85,5 @@ test "tests" {
_ = @import("exec-spec-tests/execspectests.zig");
_ = @import("types/types.zig");
_ = @import("vm/vm.zig");
_ = @import("crypto/ecdsa.zig");
}
40 changes: 40 additions & 0 deletions src/signer/signer.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ecdsa = @import("../crypto/ecdsa.zig");
const Transaction = @import("../types/transaction.zig");
const Address = @import("../types/types.zig").Address;
const hasher = @import("../crypto/hasher.zig");

// TODO: TxnSigner should be generalized to:
// - Only accept correct transactions types depending on the fork we're in.
// - Handle "v" correctly depending on transaction type.
// For now it's a post London signer, and only support 1559 txns.
pub const TxnSigner = struct {
ecdsa_signer: ecdsa.Signer,

pub fn init() !TxnSigner {
return TxnSigner{
.ecdsa_signer = try ecdsa.Signer.init(),
};
}

pub fn sign(self: TxnSigner, allocator: Allocator, txn: Transaction, privkey: ecdsa.PrivateKey) !struct { r: u256, s: u256, v: u8 } {
const txn_hash = try txn.hash(allocator);

const ecdsa_sig = try self.ecdsa_signer.sign(txn_hash, privkey);
const r = std.mem.readIntSlice(u256, ecdsa_sig[0..32], std.builtin.Endian.Big);
const s = std.mem.readIntSlice(u256, ecdsa_sig[32..64], std.builtin.Endian.Big);
const v = ecdsa_sig[64];
return .{ .r = r, .s = s, .v = v };
}

pub fn get_sender(self: TxnSigner, allocator: Allocator, txn: Transaction) !Address {
const txn_hash = try txn.hash(allocator);
var sig: ecdsa.Signature = undefined;
std.mem.writeIntSlice(u256, sig[0..32], txn.r, std.builtin.Endian.Big);
std.mem.writeIntSlice(u256, sig[32..64], txn.s, std.builtin.Endian.Big);
sig[64] = txn.v;
const pubkey = try self.ecdsa_signer.erecover(sig, txn_hash);
return hasher.keccak256(pubkey[1..])[12..].*;
}
};
84 changes: 57 additions & 27 deletions src/types/transaction.zig
Original file line number Diff line number Diff line change
@@ -1,39 +1,69 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const rlp = @import("zig-rlp");
const hasher = @import("../crypto/hasher.zig");
const types = @import("types.zig");
const Address = types.Address;

// TODO(jsign): consider using union to support txntypes
type: u8,
chain_id: u256,
nonce: u64,
gas_price: u256,
value: u256,
to: ?Address,
data: []const u8,
gas_limit: u64,
// TODO(jsign): create Transaction type that is the union of transaction types.
const Txn = @This();

pub const TxnData = struct {
type: u8,
chain_id: u256,
nonce: u64,
gas_price: u256,
value: u256,
to: ?Address,
data: []const u8,
gas_limit: u64,
};

data: TxnData,
v: u8,
r: u256,
s: u256,

// init initializes a transaction without signature fields.
// TODO(jsign): comment about data ownership.
pub fn init(type_: u8, chain_id: u256, nonce: u64, gas_price: u256, value: u256, to: ?Address, data: []const u8, gas_limit: u64) @This() {
pub fn init(
type_: u8,
chain_id: u256,
nonce: u64,
gas_price: u256,
value: u256,
to: ?Address,
data: []const u8,
gas_limit: u64,
) Txn {
return @This(){
.type = type_,
.chain_id = chain_id,
.nonce = nonce,
.gas_price = gas_price,
.value = value,
.to = to,
.data = data,
.gas_limit = gas_limit,
.data = .{
.type = type_,
.chain_id = chain_id,
.nonce = nonce,
.gas_price = gas_price,
.value = value,
.to = to,
.data = data,
.gas_limit = gas_limit,
},
.v = 0,
.r = 0,
.s = 0,
};
}

// TODO(jsign): use some secp256k1 library.
pub fn get_from(_: *const @This()) Address {
const from: Address = comptime blk: {
var buf: Address = undefined;
_ = std.fmt.hexToBytes(&buf, "a94f5374Fce5edBC8E2a8697C15331677e6EbF0B") catch unreachable;
break :blk buf;
};
return from;
pub fn setSignature(self: *Txn, v: u8, r: u256, s: u256) void {
self.*.v = v;
self.*.r = r;
self.*.s = s;
}

// TODO(jsign): add helper to get txn hash.
pub fn hash(self: Txn, allocator: Allocator) !types.Hash32 {
var out = std.ArrayList(u8).init(allocator);
defer out.deinit();

try rlp.serialize(TxnData, allocator, self.data, &out);

return hasher.keccak256(out.items);
}
Loading

0 comments on commit b6bc704

Please sign in to comment.