block_given 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -1
- data/README.md +92 -55
- data/lib/block_given/abi/codec/decoder.rb +111 -0
- data/lib/block_given/abi/codec.rb +170 -0
- data/lib/block_given/abi/coder.rb +122 -11
- data/lib/block_given/abi/custom_error.rb +33 -1
- data/lib/block_given/abi/event.rb +85 -3
- data/lib/block_given/abi/function.rb +94 -2
- data/lib/block_given/abi/interface.rb +99 -4
- data/lib/block_given/abi/parameter.rb +60 -3
- data/lib/block_given/abi/standards/erc1155.rb +41 -0
- data/lib/block_given/abi/standards/erc20.rb +33 -0
- data/lib/block_given/abi/standards/erc4626.rb +41 -0
- data/lib/block_given/abi/standards/erc721.rb +47 -0
- data/lib/block_given/abi/standards.rb +124 -0
- data/lib/block_given/abi/type.rb +138 -0
- data/lib/block_given/chain.rb +114 -2
- data/lib/block_given/client.rb +322 -20
- data/lib/block_given/configuration.rb +85 -3
- data/lib/block_given/connectors/alchemy.rb +38 -4
- data/lib/block_given/connectors/base.rb +32 -5
- data/lib/block_given/connectors/http.rb +91 -5
- data/lib/block_given/connectors/stub.rb +70 -4
- data/lib/block_given/contract.rb +422 -26
- data/lib/block_given/crypto/keccak.rb +152 -0
- data/lib/block_given/crypto/secp256k1.rb +168 -0
- data/lib/block_given/crypto.rb +22 -0
- data/lib/block_given/eip712.rb +199 -0
- data/lib/block_given/errors.rb +134 -14
- data/lib/block_given/event.rb +51 -1
- data/lib/block_given/normalizer.rb +27 -2
- data/lib/block_given/poller.rb +177 -15
- data/lib/block_given/receipt.rb +63 -3
- data/lib/block_given/rlp.rb +146 -0
- data/lib/block_given/signed_transaction.rb +148 -47
- data/lib/block_given/transaction.rb +103 -12
- data/lib/block_given/transaction_envelope/fields.rb +104 -0
- data/lib/block_given/transaction_envelope.rb +183 -0
- data/lib/block_given/utils.rb +143 -9
- data/lib/block_given/version.rb +2 -1
- data/lib/block_given/wallet.rb +216 -35
- data/lib/block_given.rb +67 -4
- metadata +19 -23
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
module Crypto
|
|
5
|
+
# Keccak-256 as used by Ethereum: the original Keccak submission (padding byte `0x01`), not the
|
|
6
|
+
# standardized NIST SHA3-256 (padding byte `0x06`), which produces different digests.
|
|
7
|
+
#
|
|
8
|
+
# Pure Ruby implementation of the Keccak-f[1600] permutation with a 1088-bit rate.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
# @example
|
|
12
|
+
# BlockGiven::Crypto::Keccak.digest("").unpack1("H*")
|
|
13
|
+
# # => "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
|
|
14
|
+
module Keccak # rubocop:disable Metrics/ModuleLength
|
|
15
|
+
# Sponge rate in bytes for a 256-bit output (1600 - 2 * 256 bits).
|
|
16
|
+
RATE = 136
|
|
17
|
+
# Mask keeping lane arithmetic on 64 bits.
|
|
18
|
+
MASK = 0xFFFFFFFFFFFFFFFF
|
|
19
|
+
# Round constants of the iota step, one per round.
|
|
20
|
+
ROUND_CONSTANTS = [
|
|
21
|
+
0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000,
|
|
22
|
+
0x000000000000808B, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
|
|
23
|
+
0x000000000000008A, 0x0000000000000088, 0x0000000080008009, 0x000000008000000A,
|
|
24
|
+
0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 0x8000000000008003,
|
|
25
|
+
0x8000000000008002, 0x8000000000000080, 0x000000000000800A, 0x800000008000000A,
|
|
26
|
+
0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# Keccak-256 digest of a byte string.
|
|
32
|
+
#
|
|
33
|
+
# @param data [String] the bytes to hash (any encoding, read as binary)
|
|
34
|
+
# @return [String] the 32-byte digest as a binary String
|
|
35
|
+
def digest(data)
|
|
36
|
+
message = data.b
|
|
37
|
+
padding = RATE - (message.bytesize % RATE)
|
|
38
|
+
message << (padding == 1 ? "\x81".b : "\x01".b + ("\x00".b * (padding - 2)) + "\x80".b)
|
|
39
|
+
|
|
40
|
+
state = Array.new(25, 0)
|
|
41
|
+
(0...message.bytesize).step(RATE) do |offset|
|
|
42
|
+
message.byteslice(offset, RATE).unpack("Q<17").each_with_index { |lane, i| state[i] ^= lane }
|
|
43
|
+
state = permute(state)
|
|
44
|
+
end
|
|
45
|
+
state.first(4).pack("Q<4")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Applies the 24 rounds of Keccak-f[1600] to a state.
|
|
49
|
+
#
|
|
50
|
+
# Unrolled on purpose: lanes live in local variables (about four times faster than indexing an Array in
|
|
51
|
+
# the inner loops). The rho rotation offsets and the pi lane permutation of the specification are baked
|
|
52
|
+
# into the `b*` assignments (lane x + 5y rotated, then moved to y + 5 * ((2x + 3y) % 5)).
|
|
53
|
+
#
|
|
54
|
+
# @param state [Array<Integer>] 25 lanes of 64 bits, indexed by x + 5 * y
|
|
55
|
+
# @return [Array<Integer>] the permuted state, as a new Array
|
|
56
|
+
def permute(state) # rubocop:disable Metrics/MethodLength
|
|
57
|
+
a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12,
|
|
58
|
+
a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24 = state
|
|
59
|
+
ROUND_CONSTANTS.each do |rc| # rubocop:disable Metrics/BlockLength
|
|
60
|
+
c0 = a0 ^ a5 ^ a10 ^ a15 ^ a20
|
|
61
|
+
c1 = a1 ^ a6 ^ a11 ^ a16 ^ a21
|
|
62
|
+
c2 = a2 ^ a7 ^ a12 ^ a17 ^ a22
|
|
63
|
+
c3 = a3 ^ a8 ^ a13 ^ a18 ^ a23
|
|
64
|
+
c4 = a4 ^ a9 ^ a14 ^ a19 ^ a24
|
|
65
|
+
d0 = c4 ^ (((c1 << 1) | (c1 >> 63)) & MASK)
|
|
66
|
+
d1 = c0 ^ (((c2 << 1) | (c2 >> 63)) & MASK)
|
|
67
|
+
d2 = c1 ^ (((c3 << 1) | (c3 >> 63)) & MASK)
|
|
68
|
+
d3 = c2 ^ (((c4 << 1) | (c4 >> 63)) & MASK)
|
|
69
|
+
d4 = c3 ^ (((c0 << 1) | (c0 >> 63)) & MASK)
|
|
70
|
+
a0 ^= d0
|
|
71
|
+
a1 ^= d1
|
|
72
|
+
a2 ^= d2
|
|
73
|
+
a3 ^= d3
|
|
74
|
+
a4 ^= d4
|
|
75
|
+
a5 ^= d0
|
|
76
|
+
a6 ^= d1
|
|
77
|
+
a7 ^= d2
|
|
78
|
+
a8 ^= d3
|
|
79
|
+
a9 ^= d4
|
|
80
|
+
a10 ^= d0
|
|
81
|
+
a11 ^= d1
|
|
82
|
+
a12 ^= d2
|
|
83
|
+
a13 ^= d3
|
|
84
|
+
a14 ^= d4
|
|
85
|
+
a15 ^= d0
|
|
86
|
+
a16 ^= d1
|
|
87
|
+
a17 ^= d2
|
|
88
|
+
a18 ^= d3
|
|
89
|
+
a19 ^= d4
|
|
90
|
+
a20 ^= d0
|
|
91
|
+
a21 ^= d1
|
|
92
|
+
a22 ^= d2
|
|
93
|
+
a23 ^= d3
|
|
94
|
+
a24 ^= d4
|
|
95
|
+
b0 = a0
|
|
96
|
+
b10 = (((a1 << 1) | (a1 >> 63)) & MASK)
|
|
97
|
+
b20 = (((a2 << 62) | (a2 >> 2)) & MASK)
|
|
98
|
+
b5 = (((a3 << 28) | (a3 >> 36)) & MASK)
|
|
99
|
+
b15 = (((a4 << 27) | (a4 >> 37)) & MASK)
|
|
100
|
+
b16 = (((a5 << 36) | (a5 >> 28)) & MASK)
|
|
101
|
+
b1 = (((a6 << 44) | (a6 >> 20)) & MASK)
|
|
102
|
+
b11 = (((a7 << 6) | (a7 >> 58)) & MASK)
|
|
103
|
+
b21 = (((a8 << 55) | (a8 >> 9)) & MASK)
|
|
104
|
+
b6 = (((a9 << 20) | (a9 >> 44)) & MASK)
|
|
105
|
+
b7 = (((a10 << 3) | (a10 >> 61)) & MASK)
|
|
106
|
+
b17 = (((a11 << 10) | (a11 >> 54)) & MASK)
|
|
107
|
+
b2 = (((a12 << 43) | (a12 >> 21)) & MASK)
|
|
108
|
+
b12 = (((a13 << 25) | (a13 >> 39)) & MASK)
|
|
109
|
+
b22 = (((a14 << 39) | (a14 >> 25)) & MASK)
|
|
110
|
+
b23 = (((a15 << 41) | (a15 >> 23)) & MASK)
|
|
111
|
+
b8 = (((a16 << 45) | (a16 >> 19)) & MASK)
|
|
112
|
+
b18 = (((a17 << 15) | (a17 >> 49)) & MASK)
|
|
113
|
+
b3 = (((a18 << 21) | (a18 >> 43)) & MASK)
|
|
114
|
+
b13 = (((a19 << 8) | (a19 >> 56)) & MASK)
|
|
115
|
+
b14 = (((a20 << 18) | (a20 >> 46)) & MASK)
|
|
116
|
+
b24 = (((a21 << 2) | (a21 >> 62)) & MASK)
|
|
117
|
+
b9 = (((a22 << 61) | (a22 >> 3)) & MASK)
|
|
118
|
+
b19 = (((a23 << 56) | (a23 >> 8)) & MASK)
|
|
119
|
+
b4 = (((a24 << 14) | (a24 >> 50)) & MASK)
|
|
120
|
+
a0 = b0 ^ (~b1 & b2)
|
|
121
|
+
a1 = b1 ^ (~b2 & b3)
|
|
122
|
+
a2 = b2 ^ (~b3 & b4)
|
|
123
|
+
a3 = b3 ^ (~b4 & b0)
|
|
124
|
+
a4 = b4 ^ (~b0 & b1)
|
|
125
|
+
a5 = b5 ^ (~b6 & b7)
|
|
126
|
+
a6 = b6 ^ (~b7 & b8)
|
|
127
|
+
a7 = b7 ^ (~b8 & b9)
|
|
128
|
+
a8 = b8 ^ (~b9 & b5)
|
|
129
|
+
a9 = b9 ^ (~b5 & b6)
|
|
130
|
+
a10 = b10 ^ (~b11 & b12)
|
|
131
|
+
a11 = b11 ^ (~b12 & b13)
|
|
132
|
+
a12 = b12 ^ (~b13 & b14)
|
|
133
|
+
a13 = b13 ^ (~b14 & b10)
|
|
134
|
+
a14 = b14 ^ (~b10 & b11)
|
|
135
|
+
a15 = b15 ^ (~b16 & b17)
|
|
136
|
+
a16 = b16 ^ (~b17 & b18)
|
|
137
|
+
a17 = b17 ^ (~b18 & b19)
|
|
138
|
+
a18 = b18 ^ (~b19 & b15)
|
|
139
|
+
a19 = b19 ^ (~b15 & b16)
|
|
140
|
+
a20 = b20 ^ (~b21 & b22)
|
|
141
|
+
a21 = b21 ^ (~b22 & b23)
|
|
142
|
+
a22 = b22 ^ (~b23 & b24)
|
|
143
|
+
a23 = b23 ^ (~b24 & b20)
|
|
144
|
+
a24 = b24 ^ (~b20 & b21)
|
|
145
|
+
a0 ^= rc
|
|
146
|
+
end
|
|
147
|
+
[a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12,
|
|
148
|
+
a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module BlockGiven
|
|
7
|
+
module Crypto
|
|
8
|
+
# ECDSA over secp256k1, the curve Ethereum accounts use: key generation, public key derivation,
|
|
9
|
+
# deterministic signing (RFC 6979, low-s as required by EIP-2, with the recovery id) and public key recovery.
|
|
10
|
+
#
|
|
11
|
+
# Elliptic curve point multiplications go through the OpenSSL standard library (`OpenSSL::PKey::EC::Point`);
|
|
12
|
+
# scalars stay plain Ruby Integers. Signatures are byte-identical to libsecp256k1's and viem's for the same
|
|
13
|
+
# key and hash, since the nonce is derived deterministically from both.
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
module Secp256k1
|
|
17
|
+
# Order of the curve's base point.
|
|
18
|
+
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
19
|
+
# Field prime.
|
|
20
|
+
P = (2**256) - (2**32) - 977
|
|
21
|
+
# Largest `s` value of a canonical (low-s) signature.
|
|
22
|
+
HALF_N = N >> 1
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# The OpenSSL group for the curve, built once.
|
|
27
|
+
#
|
|
28
|
+
# @return [OpenSSL::PKey::EC::Group]
|
|
29
|
+
# @raise [BlockGiven::ConfigurationError] when the linked OpenSSL was built without secp256k1
|
|
30
|
+
def group
|
|
31
|
+
@group ||= OpenSSL::PKey::EC::Group.new("secp256k1")
|
|
32
|
+
rescue OpenSSL::PKey::EC::Group::Error => e
|
|
33
|
+
raise ConfigurationError, "the OpenSSL library Ruby is linked against does not support secp256k1 (#{e.message})"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Whether an Integer is a valid private key, i.e. in [1, N - 1].
|
|
37
|
+
#
|
|
38
|
+
# @param key [Integer]
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def valid_private_key?(key) = key.is_a?(Integer) && key.positive? && key < N
|
|
41
|
+
|
|
42
|
+
# Draws a uniformly random private key from `SecureRandom`.
|
|
43
|
+
#
|
|
44
|
+
# @return [Integer] a key in [1, N - 1]
|
|
45
|
+
def generate_private_key
|
|
46
|
+
loop do
|
|
47
|
+
key = bytes_to_int(SecureRandom.random_bytes(32))
|
|
48
|
+
return key if valid_private_key?(key)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The uncompressed public key of a private key.
|
|
53
|
+
#
|
|
54
|
+
# @param private_key [Integer] a key in [1, N - 1]
|
|
55
|
+
# @return [String] 65 binary bytes: `0x04`, then X and Y on 32 bytes each
|
|
56
|
+
def public_key(private_key)
|
|
57
|
+
group.generator.mul(OpenSSL::BN.new(private_key)).to_octet_string(:uncompressed)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Signs a 32-byte hash with a nonce derived per RFC 6979 (HMAC-SHA256), normalized to low-s.
|
|
61
|
+
#
|
|
62
|
+
# @param hash [String] the 32-byte message hash, binary
|
|
63
|
+
# @param private_key [Integer] a key in [1, N - 1]
|
|
64
|
+
# @return [Array(Integer, Integer, Integer)] `[r, s, recovery_id]`, `recovery_id` in 0..3 (bit 0: parity of
|
|
65
|
+
# the nonce point's Y, bit 1: its X overflowed N)
|
|
66
|
+
def sign(hash, private_key)
|
|
67
|
+
z = bytes_to_int(hash) % N
|
|
68
|
+
nonces(int_to_bytes(private_key), int_to_bytes(z)).each do |k|
|
|
69
|
+
point = group.generator.mul(OpenSSL::BN.new(k)).to_octet_string(:uncompressed)
|
|
70
|
+
x = bytes_to_int(point.byteslice(1, 32))
|
|
71
|
+
r = x % N
|
|
72
|
+
next if r.zero?
|
|
73
|
+
|
|
74
|
+
s = (blinded_inverse(k) * (z + (r * private_key))) % N
|
|
75
|
+
next if s.zero?
|
|
76
|
+
|
|
77
|
+
recovery_id = (point.getbyte(64) & 1) | (x >= N ? 2 : 0)
|
|
78
|
+
return s > HALF_N ? [r, N - s, recovery_id ^ 1] : [r, s, recovery_id]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Recovers the uncompressed public key that produced a signature over a hash.
|
|
83
|
+
#
|
|
84
|
+
# @param hash [String] the 32-byte message hash, binary
|
|
85
|
+
# @param r [Integer]
|
|
86
|
+
# @param s [Integer]
|
|
87
|
+
# @param recovery_id [Integer] 0..3
|
|
88
|
+
# @return [String] the 65-byte uncompressed public key, binary
|
|
89
|
+
# @raise [BlockGiven::InvalidArgumentError] when the signature is malformed or matches no curve point
|
|
90
|
+
def recover(hash, r, s, recovery_id)
|
|
91
|
+
unless r.positive? && r < N && s.positive? && s < N && (0..3).cover?(recovery_id)
|
|
92
|
+
raise InvalidArgumentError, "invalid signature"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
x = r + ((recovery_id >> 1) * N)
|
|
96
|
+
raise InvalidArgumentError, "invalid signature" if x >= P
|
|
97
|
+
|
|
98
|
+
point_r = decompress(x, recovery_id & 1)
|
|
99
|
+
r_inverse = r.pow(N - 2, N)
|
|
100
|
+
u1 = (-bytes_to_int(hash) * r_inverse) % N
|
|
101
|
+
u2 = (s * r_inverse) % N
|
|
102
|
+
public_key = point_r.mul(OpenSSL::BN.new(u2), OpenSSL::BN.new(u1))
|
|
103
|
+
raise InvalidArgumentError, "invalid signature" if public_key.infinity?
|
|
104
|
+
|
|
105
|
+
public_key.to_octet_string(:uncompressed)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Candidate nonces of RFC 6979 section 3.2 with HMAC-SHA256, in order (libsecp256k1's nonce function
|
|
109
|
+
# without extra entropy).
|
|
110
|
+
#
|
|
111
|
+
# @param key [String] the private key on 32 bytes
|
|
112
|
+
# @param message [String] the hash reduced modulo N, on 32 bytes
|
|
113
|
+
# @return [Enumerator<Integer>] endless sequence of nonces in [1, N - 1]
|
|
114
|
+
def nonces(key, message)
|
|
115
|
+
Enumerator.new do |yielder|
|
|
116
|
+
v = "\x01".b * 32
|
|
117
|
+
k = "\x00".b * 32
|
|
118
|
+
k = hmac(k, "#{v}\x00".b + key + message)
|
|
119
|
+
v = hmac(k, v)
|
|
120
|
+
k = hmac(k, "#{v}\x01".b + key + message)
|
|
121
|
+
v = hmac(k, v)
|
|
122
|
+
loop do
|
|
123
|
+
v = hmac(k, v)
|
|
124
|
+
candidate = bytes_to_int(v)
|
|
125
|
+
yielder << candidate if valid_private_key?(candidate)
|
|
126
|
+
k = hmac(k, "#{v}\x00".b)
|
|
127
|
+
v = hmac(k, v)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The modular inverse of a nonce, computed on a randomly blinded value so that its timing does not depend
|
|
133
|
+
# on the nonce itself.
|
|
134
|
+
#
|
|
135
|
+
# @param k [Integer] a nonce in [1, N - 1]
|
|
136
|
+
# @return [Integer] k^-1 mod N
|
|
137
|
+
def blinded_inverse(k)
|
|
138
|
+
blind = (bytes_to_int(SecureRandom.random_bytes(32)) % (N - 1)) + 1
|
|
139
|
+
((k * blind) % N).pow(N - 2, N) * blind % N
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The curve point with a given X coordinate and Y parity.
|
|
143
|
+
#
|
|
144
|
+
# @param x [Integer] X coordinate, below P
|
|
145
|
+
# @param y_parity [Integer] 0 for an even Y, 1 for an odd Y
|
|
146
|
+
# @return [OpenSSL::PKey::EC::Point]
|
|
147
|
+
# @raise [BlockGiven::InvalidArgumentError] when X is not on the curve
|
|
148
|
+
def decompress(x, y_parity)
|
|
149
|
+
OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new((2 + y_parity).chr + int_to_bytes(x), 2))
|
|
150
|
+
rescue OpenSSL::PKey::EC::Point::Error
|
|
151
|
+
raise InvalidArgumentError, "invalid signature"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @param key [String] HMAC key
|
|
155
|
+
# @param data [String]
|
|
156
|
+
# @return [String] the 32-byte HMAC-SHA256, binary
|
|
157
|
+
def hmac(key, data) = OpenSSL::HMAC.digest("SHA256", key, data)
|
|
158
|
+
|
|
159
|
+
# @param bytes [String] big-endian binary
|
|
160
|
+
# @return [Integer]
|
|
161
|
+
def bytes_to_int(bytes) = bytes.unpack1("H*").to_i(16)
|
|
162
|
+
|
|
163
|
+
# @param int [Integer] a non-negative Integer below 2**256
|
|
164
|
+
# @return [String] 32 big-endian bytes, binary
|
|
165
|
+
def int_to_bytes(int) = [int.to_s(16).rjust(64, "0")].pack("H*")
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "crypto/keccak"
|
|
4
|
+
require_relative "crypto/secp256k1"
|
|
5
|
+
|
|
6
|
+
module BlockGiven
|
|
7
|
+
# Cryptographic primitives Ethereum needs, in pure Ruby on top of the OpenSSL standard library: Keccak-256
|
|
8
|
+
# ({Keccak}) and ECDSA over secp256k1 ({Secp256k1}). No native extension is involved beyond OpenSSL.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module Crypto
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# The address controlled by a public key: the last 20 bytes of the keccak-256 of its X and Y coordinates.
|
|
15
|
+
#
|
|
16
|
+
# @param public_key [String] the 65-byte uncompressed public key, binary (`0x04` prefix included)
|
|
17
|
+
# @return [String] the EIP-55 checksummed address
|
|
18
|
+
def address(public_key)
|
|
19
|
+
Utils.checksum_address(Utils.bin_to_hex(Keccak.digest(public_key.byteslice(1, 64)).byteslice(12, 20)))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
# EIP-712 typed structured data hashing (`encodeType`, `encodeData`, `hashStruct`, domain separator).
|
|
5
|
+
#
|
|
6
|
+
# The payload is a Hash with `types`, `primaryType`, `domain` and `message`, with Symbol or String keys.
|
|
7
|
+
# When `types` has no `EIP712Domain` entry it is derived from the domain keys present, in the canonical
|
|
8
|
+
# order (`name`, `version`, `chainId`, `verifyingContract`, `salt`), as viem does.
|
|
9
|
+
#
|
|
10
|
+
# Values: integers as Integer, decimal String or `0x` hex String; addresses and `bytes` / `bytesN` as `0x`
|
|
11
|
+
# hex; `string` as text; arrays as Arrays (hashed per the specification, element by element); structs as
|
|
12
|
+
# Hashes keyed by field name.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
module Eip712
|
|
16
|
+
# Domain fields in the order EIP-712 lists them, with their types, used when `EIP712Domain` is omitted.
|
|
17
|
+
DOMAIN_FIELDS = [
|
|
18
|
+
%w[name string], %w[version string], %w[chainId uint256], %w[verifyingContract address], %w[salt bytes32]
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# The digest to sign: keccak256("\x19\x01" || domainSeparator || hashStruct(message)).
|
|
24
|
+
#
|
|
25
|
+
# @param typed_data [Hash] `types`, `primaryType`, `domain` and `message`
|
|
26
|
+
# @return [String] the 32-byte digest, binary
|
|
27
|
+
# @raise [BlockGiven::InvalidArgumentError] when a section is missing, a type is unknown or a value does not
|
|
28
|
+
# fit its type
|
|
29
|
+
def hash(typed_data)
|
|
30
|
+
data = typed_data.to_h.transform_keys(&:to_s)
|
|
31
|
+
%w[types primaryType domain message].each do |key|
|
|
32
|
+
missing = data[key].nil? || (key != "domain" && data[key].empty?)
|
|
33
|
+
raise InvalidArgumentError, "typed data #{key} is missing" if missing
|
|
34
|
+
end
|
|
35
|
+
domain = data["domain"].transform_keys(&:to_s)
|
|
36
|
+
types = normalize_types(data["types"], domain)
|
|
37
|
+
Crypto::Keccak.digest("\x19\x01".b + hash_struct("EIP712Domain", domain, types) +
|
|
38
|
+
hash_struct(data["primaryType"].to_s, data["message"], types))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# keccak256(typeHash || encodeData(fields)) of a struct value.
|
|
42
|
+
#
|
|
43
|
+
# @param type [String] the struct name
|
|
44
|
+
# @param value [Hash] field values by name (Symbol or String keys)
|
|
45
|
+
# @param types [Hash{String => Array<Array(String, String)>}] normalized types: struct name to [name, type]
|
|
46
|
+
# @return [String] 32 bytes, binary
|
|
47
|
+
# @raise [BlockGiven::InvalidArgumentError] when the value is not a Hash or misses a field
|
|
48
|
+
def hash_struct(type, value, types)
|
|
49
|
+
raise InvalidArgumentError, "#{type} expects a Hash, got #{value.inspect}" unless value.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
raise InvalidArgumentError, "unknown EIP-712 type #{type}" unless types.key?(type)
|
|
52
|
+
|
|
53
|
+
fields = value.transform_keys(&:to_s)
|
|
54
|
+
encoded = types[type].map do |name, field_type|
|
|
55
|
+
raise InvalidArgumentError, "#{type}.#{name} is missing" unless fields.key?(name)
|
|
56
|
+
|
|
57
|
+
encode_field(field_type, fields[name], types)
|
|
58
|
+
end
|
|
59
|
+
Crypto::Keccak.digest(Crypto::Keccak.digest(encode_type(type, types)) + encoded.join)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# `Name(type1 name1,...)` followed by the referenced structs sorted by name.
|
|
63
|
+
#
|
|
64
|
+
# @param type [String] the struct name
|
|
65
|
+
# @param types [Hash{String => Array<Array(String, String)>}]
|
|
66
|
+
# @return [String]
|
|
67
|
+
def encode_type(type, types)
|
|
68
|
+
([type] + dependencies(type, types).reject { |t| t == type }.sort).map do |name|
|
|
69
|
+
"#{name}(#{types.fetch(name).map { |field, field_type| "#{field_type} #{field}" }.join(',')})"
|
|
70
|
+
end.join
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Every struct reachable from a type, the type itself included.
|
|
74
|
+
#
|
|
75
|
+
# @param type [String]
|
|
76
|
+
# @param types [Hash{String => Array<Array(String, String)>}]
|
|
77
|
+
# @param found [Array<String>] accumulator
|
|
78
|
+
# @return [Array<String>]
|
|
79
|
+
def dependencies(type, types, found = [])
|
|
80
|
+
base = type.sub(/(\[\d*\])+\z/, "")
|
|
81
|
+
return found if found.include?(base) || !types.key?(base)
|
|
82
|
+
|
|
83
|
+
found << base
|
|
84
|
+
types[base].each { |field| dependencies(field[1], types, found) }
|
|
85
|
+
found
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The 32-byte encoding of one field value.
|
|
89
|
+
#
|
|
90
|
+
# @param type [String] the field type
|
|
91
|
+
# @param value [Object]
|
|
92
|
+
# @param types [Hash{String => Array<Array(String, String)>}]
|
|
93
|
+
# @return [String] 32 bytes, binary
|
|
94
|
+
# @raise [BlockGiven::InvalidArgumentError] when the value does not fit the type
|
|
95
|
+
def encode_field(type, value, types)
|
|
96
|
+
if (array = type.match(/\A(.+)\[(\d*)\]\z/))
|
|
97
|
+
raise InvalidArgumentError, "#{type} expects an Array, got #{value.inspect}" unless value.is_a?(Array)
|
|
98
|
+
if !array[2].empty? && value.size != array[2].to_i
|
|
99
|
+
raise InvalidArgumentError, "#{type} expects #{array[2]} elements, got #{value.size}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
return Crypto::Keccak.digest(value.map { |element| encode_field(array[1], element, types) }.join)
|
|
103
|
+
end
|
|
104
|
+
return hash_struct(type, value, types) if types.key?(type)
|
|
105
|
+
return Crypto::Keccak.digest(value.to_s) if type == "string"
|
|
106
|
+
return Crypto::Keccak.digest(hex_bytes(type, value)) if type == "bytes"
|
|
107
|
+
|
|
108
|
+
encode_atomic(type, value)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The 32-byte word of an atomic value.
|
|
112
|
+
#
|
|
113
|
+
# @param type [String] `uintN`, `intN`, `address`, `bool` or `bytesN`
|
|
114
|
+
# @param value [Object]
|
|
115
|
+
# @return [String] 32 bytes, binary
|
|
116
|
+
# @raise [BlockGiven::InvalidArgumentError] for an unknown type or an out-of-range value
|
|
117
|
+
def encode_atomic(type, value)
|
|
118
|
+
case type
|
|
119
|
+
when /\A(u?)int(\d*)\z/
|
|
120
|
+
bits = ::Regexp.last_match(2).empty? ? 256 : ::Regexp.last_match(2).to_i
|
|
121
|
+
word(integer(type, value), bits, signed: ::Regexp.last_match(1).empty?)
|
|
122
|
+
when "address"
|
|
123
|
+
raise InvalidArgumentError, "invalid address #{value.inspect}" unless Utils.address?(value)
|
|
124
|
+
|
|
125
|
+
Utils.hex_to_bin(Utils.pad_hex(value))
|
|
126
|
+
when "bool"
|
|
127
|
+
raise InvalidArgumentError, "bool expects true/false, got #{value.inspect}" unless [true, false].include?(value)
|
|
128
|
+
|
|
129
|
+
word(value ? 1 : 0, 8, signed: false)
|
|
130
|
+
when /\Abytes(\d+)\z/
|
|
131
|
+
bytes = hex_bytes(type, value)
|
|
132
|
+
raise InvalidArgumentError, "#{type} value is too long" if bytes.bytesize > ::Regexp.last_match(1).to_i
|
|
133
|
+
|
|
134
|
+
bytes.ljust(32, "\x00".b)
|
|
135
|
+
else raise InvalidArgumentError, "unknown EIP-712 type #{type}"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @param type [String] for error messages
|
|
140
|
+
# @param value [Integer, String] Integer, decimal String or `0x` hex String
|
|
141
|
+
# @return [Integer]
|
|
142
|
+
# @raise [BlockGiven::InvalidArgumentError] when the value is not an integer
|
|
143
|
+
def integer(type, value)
|
|
144
|
+
return value if value.is_a?(Integer)
|
|
145
|
+
return Utils.hex_to_int(value) if Utils.hex?(value) && value.size > 2
|
|
146
|
+
return Integer(value, 10) if value.is_a?(String)
|
|
147
|
+
|
|
148
|
+
raise InvalidArgumentError, "#{type} expects an integer, got #{value.inspect}"
|
|
149
|
+
rescue ::ArgumentError
|
|
150
|
+
raise InvalidArgumentError, "#{type} expects an integer, got #{value.inspect}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# A two's complement 32-byte word.
|
|
154
|
+
#
|
|
155
|
+
# @param value [Integer]
|
|
156
|
+
# @param bits [Integer] the type width
|
|
157
|
+
# @param signed [Boolean]
|
|
158
|
+
# @return [String] 32 bytes, binary
|
|
159
|
+
# @raise [BlockGiven::InvalidArgumentError] when the value does not fit
|
|
160
|
+
def word(value, bits, signed:)
|
|
161
|
+
range = signed ? (-(2**(bits - 1))...(2**(bits - 1))) : (0...(2**bits))
|
|
162
|
+
raise InvalidArgumentError, "#{value} does not fit in #{'u' unless signed}int#{bits}" unless range.cover?(value)
|
|
163
|
+
|
|
164
|
+
[(value % (2**256)).to_s(16).rjust(64, "0")].pack("H*")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @param type [String] for error messages
|
|
168
|
+
# @param value [String] `0x` hex
|
|
169
|
+
# @return [String] the bytes, binary
|
|
170
|
+
# @raise [BlockGiven::InvalidArgumentError] when the value is not hex
|
|
171
|
+
def hex_bytes(type, value)
|
|
172
|
+
raise InvalidArgumentError, "#{type} expects 0x hex: #{value.inspect}" unless value.to_s.match?(/\A0x(\h\h)*\z/)
|
|
173
|
+
|
|
174
|
+
Utils.hex_to_bin(value)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Struct definitions as `{ "Name" => [[field, type], ...] }`, `EIP712Domain` derived when absent.
|
|
178
|
+
#
|
|
179
|
+
# @param types [Hash] struct name to an Array of `{ name:, type: }`
|
|
180
|
+
# @param domain [Hash{String => Object}] the domain values
|
|
181
|
+
# @return [Hash{String => Array<Array(String, String)>}]
|
|
182
|
+
def normalize_types(types, domain)
|
|
183
|
+
normalized = types.to_h { |name, fields| [name.to_s, fields.map { |f| field_pair(name, f) }] }
|
|
184
|
+
normalized["EIP712Domain"] ||= DOMAIN_FIELDS.select { |name, _type| domain.key?(name) }
|
|
185
|
+
normalized
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @param struct [String, Symbol] for error messages
|
|
189
|
+
# @param field [Hash] `{ name:, type: }`, Symbol or String keys
|
|
190
|
+
# @return [Array(String, String)] name and type
|
|
191
|
+
# @raise [BlockGiven::InvalidArgumentError] when the definition is malformed
|
|
192
|
+
def field_pair(struct, field)
|
|
193
|
+
field = field.to_h.transform_keys(&:to_s)
|
|
194
|
+
raise InvalidArgumentError, "malformed field in #{struct}: #{field.inspect}" unless field["name"] && field["type"]
|
|
195
|
+
|
|
196
|
+
[field["name"].to_s, field["type"].to_s]
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|