-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from jsign/jsign-secp
Support transaction signing and public key recovery
- Loading branch information
Showing
9 changed files
with
221 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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..].*; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.