eth 0.4.18 → 0.5.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.
data/lib/eth/tx.rb CHANGED
@@ -1,196 +1,328 @@
1
+ # Copyright (c) 2016-2022 The Ruby-Eth Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "rlp"
16
+ require "konstructor"
17
+
18
+ require "eth/chain"
19
+ require "eth/tx/eip1559"
20
+ require "eth/tx/eip2930"
21
+ require "eth/tx/legacy"
22
+ require "eth/unit"
23
+
24
+ # Provides the `Eth` module.
1
25
  module Eth
2
- class Tx
3
- include RLP::Sedes::Serializable
4
- extend Sedes
5
-
6
- set_serializable_fields({
7
- nonce: big_endian_int,
8
- gas_price: big_endian_int,
9
- gas_limit: big_endian_int,
10
- to: address,
11
- value: big_endian_int,
12
- data_bin: binary,
13
- v: big_endian_int,
14
- r: big_endian_int,
15
- s: big_endian_int,
16
- })
17
-
18
- attr_writer :signature
19
-
20
- def self.decode(data)
21
- data = Utils.hex_to_bin(data) if data.match(/\A(?:0x)?\h+\Z/)
22
- txh = deserialize(RLP.decode data).to_h
23
-
24
- txh[:chain_id] = Eth.chain_id_from_signature(txh)
25
-
26
- self.new txh
27
- end
28
26
 
29
- def initialize(params)
30
- fields = { v: 0, r: 0, s: 0 }.merge params
31
- fields[:to] = Utils.normalize_address(fields[:to])
27
+ # Provides the `Tx` module supporting various transaction types.
28
+ module Tx
29
+ extend self
32
30
 
33
- self.chain_id = (params[:chain_id]) ? params.delete(:chain_id) : Eth.chain_id
31
+ # Provides a special transactoin error if transaction type is unknown.
32
+ class TransactionTypeError < TypeError; end
34
33
 
35
- if params[:data]
36
- self.data = params.delete(:data)
37
- fields[:data_bin] = data_bin
38
- end
39
- serializable_initialize fields
34
+ # Provides an decoder error if transaction cannot be decoded.
35
+ class DecoderError < StandardError; end
40
36
 
41
- check_transaction_validity
42
- end
37
+ # Provides a parameter error if parameter types are invalid.
38
+ class ParameterError < TypeError; end
43
39
 
44
- def unsigned_encoded
45
- us = unsigned
46
- RLP.encode(us, sedes: us.sedes)
47
- end
40
+ # The minimum transaction gas limit required for a value transfer.
41
+ DEFAULT_GAS_LIMIT = 21_000.freeze
48
42
 
49
- def signing_data
50
- Utils.bin_to_prefixed_hex unsigned_encoded
51
- end
43
+ # The "default" transaction gas price of 20 GWei. Do not use.
44
+ DEFAULT_GAS_PRICE = (20 * Unit::GWEI).freeze
52
45
 
53
- def encoded
54
- RLP.encode self
55
- end
46
+ # The calldata gas cost of a non-zero byte as per EIP-2028.
47
+ COST_NON_ZERO_BYTE = 16.freeze
56
48
 
57
- def hex
58
- Utils.bin_to_prefixed_hex encoded
59
- end
49
+ # The calldata gas cost of a zero byte.
50
+ COST_ZERO_BYTE = 4.freeze
60
51
 
61
- def sign(key)
62
- sig = key.sign(unsigned_encoded)
63
- vrs = Utils.v_r_s_for sig
64
- self.v = (self.chain_id) ? ((self.chain_id * 2) + vrs[0] + 8) : vrs[0]
65
- self.r = vrs[1]
66
- self.s = vrs[2]
52
+ # The access list gas cost of a storage key as per EIP-2930.
53
+ COST_STORAGE_KEY = 1_900.freeze
67
54
 
68
- clear_signature
69
- self
70
- end
55
+ # The access list gas cost of an address as per EIP-2930.
56
+ COST_ADDRESS = 2_400.freeze
71
57
 
72
- def to_h
73
- hash_keys.inject({}) do |hash, field|
74
- hash[field] = send field
75
- hash
76
- end
77
- end
58
+ # The maximum transaction gas limit is bound by the block gas limit.
59
+ BLOCK_GAS_LIMIT = 25_000_000.freeze
78
60
 
79
- def from
80
- if ecdsa_signature
81
- public_key = OpenSsl.recover_compact(signature_hash, ecdsa_signature)
82
- Utils.public_key_to_address(public_key) if public_key
83
- end
84
- end
61
+ # The legacy transaction type is 0.
62
+ TYPE_LEGACY = 0x00.freeze
85
63
 
86
- def signature
87
- return @signature if @signature
88
- @signature = { v: v, r: r, s: s } if [v, r, s].all? && (v > 0)
89
- end
64
+ # The EIP-2930 transaction type is 1.
65
+ TYPE_2930 = 0x01.freeze
90
66
 
91
- def ecdsa_signature
92
- return @ecdsa_signature if @ecdsa_signature
67
+ # The EIP-1559 transaction type is 2.
68
+ TYPE_1559 = 0x02.freeze
93
69
 
94
- if [v, r, s].all? && (v > 0)
95
- s_v = (self.chain_id) ? (v - (self.chain_id * 2) - 8) : v
96
- @ecdsa_signature = [
97
- Utils.int_to_base256(s_v),
98
- Utils.zpad_int(r),
99
- Utils.zpad_int(s),
100
- ].join
101
- end
102
- end
70
+ # The zero byte is 0x00.
71
+ ZERO_BYTE = "\x00".freeze
103
72
 
104
- def hash
105
- "0x#{Utils.bin_to_hex Utils.keccak256_rlp(self)}"
106
- end
73
+ # Creates a new transaction of any type for given parameters and chain ID.
74
+ # Required parameters are (optional in brackets):
75
+ # - EIP-1559: chain_id, nonce, priority_fee, max_gas_fee, gas_limit(, from, to,
76
+ # value, data, access_list)
77
+ # - EIP-2930: chain_id, nonce, gas_price, gas_limit, access_list(, from, to,
78
+ # value, data)
79
+ # - Legacy: nonce, gas_price, gas_lmit(, from, to, value, data)
80
+ #
81
+ # @param params [Hash] all necessary transaction fields.
82
+ # @param chain_id [Integer] the EIP-155 Chain ID (legacy transactions only).
83
+ def new(params, chain_id = Chain::ETHEREUM)
107
84
 
108
- alias_method :id, :hash
85
+ # if we deal with max gas fee parameter, attempt EIP-1559
86
+ unless params[:max_gas_fee].nil?
87
+ params[:chain_id] = chain_id if params[:chain_id].nil?
88
+ return Tx::Eip1559.new params
89
+ end
109
90
 
110
- def data_hex
111
- Utils.bin_to_prefixed_hex data_bin
112
- end
91
+ # if we deal with access list parameter, attempt EIP-2930
92
+ unless params[:access_list].nil?
93
+ params[:chain_id] = chain_id if params[:chain_id].nil?
94
+ return Tx::Eip2930.new params
95
+ end
113
96
 
114
- def data_hex=(hex)
115
- self.data_bin = Utils.hex_to_bin(hex)
97
+ # if nothing else, go with legacy transactions
98
+ chain_id = params[:chain_id] if !params[:chain_id].nil? and params[:chain_id] != chain_id
99
+ return Tx::Legacy.new params, chain_id
116
100
  end
117
101
 
118
- def data
119
- Eth.tx_data_hex? ? data_hex : data_bin
120
- end
102
+ # Decodes a transaction hex of any known type (2, 1, or legacy).
103
+ #
104
+ # @param hex [String] the raw transaction hex-string.
105
+ # @return [Eth::Tx] transaction payload.
106
+ # @raise [TransactionTypeError] if the transaction type is unknown.
107
+ def decode(hex)
108
+ hex = Util.remove_hex_prefix hex
109
+ type = hex[0, 2].to_i(16)
110
+ case type
111
+ when TYPE_1559
112
+
113
+ # EIP-1559 transaction (type 2)
114
+ return Tx::Eip1559.decode hex
115
+ when TYPE_2930
116
+
117
+ # EIP-2930 transaction (type 1)
118
+ return Tx::Eip2930.decode hex
119
+ else
121
120
 
122
- def data=(string)
123
- Eth.tx_data_hex? ? self.data_hex = (string) : self.data_bin = (string)
121
+ # Legacy transaction if first byte is RLP (>= 192)
122
+ if type >= 0xc0
123
+ return Tx::Legacy.decode hex
124
+ else
125
+ raise TransactionTypeError, "Cannot decode unknown transaction type #{type}!"
126
+ end
127
+ end
124
128
  end
125
129
 
126
- def chain_id
127
- @chain_id
130
+ # Creates an unsigned copy of any transaction object.
131
+ #
132
+ # @param tx [Eth::Tx] any transaction payload.
133
+ # @return [Eth::Tx] an unsigned transaction payload of the same type.
134
+ # @raise [TransactionTypeError] if the transaction type is unknown.
135
+ def unsigned_copy(tx)
136
+ case tx.type
137
+ when TYPE_1559
138
+
139
+ # EIP-1559 transaction (type 2)
140
+ return Tx::Eip1559.unsigned_copy tx
141
+ when TYPE_2930
142
+
143
+ # EIP-2930 transaction (type 1)
144
+ return Tx::Eip2930.unsigned_copy tx
145
+ when TYPE_LEGACY
146
+
147
+ # Legacy transaction ("type 0")
148
+ return Tx::Legacy.unsigned_copy tx
149
+ end
150
+ raise TransactionTypeError, "Cannot copy unknown transaction type #{tx.type}!"
128
151
  end
129
152
 
130
- def chain_id=(cid)
131
- if cid != @chain_id
132
- self.v = 0
133
- self.r = 0
134
- self.s = 0
135
-
136
- clear_signature
153
+ # Estimates intrinsic gas for provided call data (EIP-2028) and
154
+ # access lists (EIP-2930).
155
+ #
156
+ # @param data [String] the call data.
157
+ # @param list [Array] the access list.
158
+ # @return [Integer] the estimated intrinsic gas cost.
159
+ def estimate_intrinsic_gas(data = "", list = [])
160
+ gas = DEFAULT_GAS_LIMIT
161
+ unless data.nil? or data.empty?
162
+ data = Util.hex_to_bin data if Util.is_hex? data
163
+
164
+ # count zero bytes
165
+ zero = data.count ZERO_BYTE
166
+ gas += zero * COST_ZERO_BYTE
167
+
168
+ # count non-zero bytes
169
+ none = data.size - zero
170
+ gas += none * COST_NON_ZERO_BYTE
137
171
  end
172
+ unless list.nil? or list.empty?
173
+ list.each do |entry|
138
174
 
139
- @chain_id = (cid == 0) ? nil : cid
140
- end
141
-
142
- def prevent_replays?
143
- !self.chain_id.nil?
144
- end
175
+ # count addresses
176
+ gas += COST_ADDRESS
145
177
 
146
- private
178
+ entry.last.each do |key|
147
179
 
148
- def clear_signature
149
- @signature = nil
150
- @ecdsa_signature = nil
180
+ # count storage keys
181
+ gas += COST_STORAGE_KEY
182
+ end
183
+ end
184
+ end
185
+ return gas
151
186
  end
152
187
 
153
- def hash_keys
154
- keys = self.class.serializable_fields.keys
155
- keys.delete(:data_bin)
156
- keys + [:data, :chain_id]
188
+ # Validates the common type-2 transaction fields such as nonce, priority
189
+ # fee, max gas fee, gas limit, amount, and access list.
190
+ #
191
+ # @param fields [Hash] the transaction fields.
192
+ # @return [Hash] the validated transaction fields.
193
+ # @raise [ParameterError] if nonce is an invalid integer.
194
+ # @raise [ParameterError] if priority fee is invalid.
195
+ # @raise [ParameterError] if max gas fee is invalid.
196
+ # @raise [ParameterError] if gas limit is invalid.
197
+ # @raise [ParameterError] if amount is invalid.
198
+ # @raise [ParameterError] if access list is invalid.
199
+ def validate_params(fields)
200
+ unless fields[:nonce] >= 0
201
+ raise ParameterError, "Invalid signer nonce #{fields[:nonce]}!"
202
+ end
203
+ unless fields[:priority_fee] >= 0
204
+ raise ParameterError, "Invalid gas priority fee #{fields[:priority_fee]}!"
205
+ end
206
+ unless fields[:max_gas_fee] >= 0
207
+ raise ParameterError, "Invalid max gas fee #{fields[:max_gas_fee]}!"
208
+ end
209
+ unless fields[:gas_limit] >= DEFAULT_GAS_LIMIT and fields[:gas_limit] <= BLOCK_GAS_LIMIT
210
+ raise ParameterError, "Invalid gas limit #{fields[:gas_limit]}!"
211
+ end
212
+ unless fields[:value] >= 0
213
+ raise ParameterError, "Invalid transaction value #{fields[:value]}!"
214
+ end
215
+ unless fields[:access_list].nil? or fields[:access_list].is_a? Array
216
+ raise ParameterError, "Invalid access list #{fields[:access_list]}!"
217
+ end
218
+ return fields
157
219
  end
158
220
 
159
- def check_transaction_validity
160
- if [gas_price, gas_limit, value, nonce].max > UINT_MAX
161
- raise InvalidTransaction, "Values way too high!"
162
- elsif gas_limit < intrinsic_gas_used
163
- raise InvalidTransaction, "Gas limit too low"
221
+ # Validates the common legacy transaction fields such as nonce, gas
222
+ # price, gas limit, amount, and access list.
223
+ #
224
+ # @param fields [Hash] the transaction fields.
225
+ # @return [Hash] the validated transaction fields.
226
+ # @raise [ParameterError] if nonce is an invalid integer.
227
+ # @raise [ParameterError] if gas price is invalid.
228
+ # @raise [ParameterError] if gas limit is invalid.
229
+ # @raise [ParameterError] if amount is invalid.
230
+ # @raise [ParameterError] if access list is invalid.
231
+ def validate_legacy_params(fields)
232
+ unless fields[:nonce] >= 0
233
+ raise ParameterError, "Invalid signer nonce #{fields[:nonce]}!"
164
234
  end
235
+ unless fields[:gas_price] >= 0
236
+ raise ParameterError, "Invalid gas price #{fields[:gas_price]}!"
237
+ end
238
+ unless fields[:gas_limit] >= DEFAULT_GAS_LIMIT and fields[:gas_limit] <= BLOCK_GAS_LIMIT
239
+ raise ParameterError, "Invalid gas limit #{fields[:gas_limit]}!"
240
+ end
241
+ unless fields[:value] >= 0
242
+ raise ParameterError, "Invalid transaction value #{fields[:value]}!"
243
+ end
244
+ unless fields[:access_list].nil? or fields[:access_list].is_a? Array
245
+ raise ParameterError, "Invalid access list #{fields[:access_list]}!"
246
+ end
247
+ return fields
165
248
  end
166
249
 
167
- def intrinsic_gas_used
168
- num_zero_bytes = data_bin.count(BYTE_ZERO)
169
- num_non_zero_bytes = data_bin.size - num_zero_bytes
170
-
171
- Gas::GTXCOST +
172
- Gas::GTXDATAZERO * num_zero_bytes +
173
- Gas::GTXDATANONZERO * num_non_zero_bytes
250
+ # Populates the transaction chain id field with a serializable default
251
+ # value (1) in case it is undefined.
252
+ #
253
+ # @param id [Integer] the transaction chain id.
254
+ # @return [Integer] the sanitized transaction chain id.
255
+ def sanitize_chain(id)
256
+ id = Chain::ETHEREUM if id.nil?
257
+ return id
174
258
  end
175
259
 
176
- def signature_hash
177
- Utils.keccak256 unsigned_encoded
260
+ # Populates the transaction destination address with a serializable
261
+ # empty value in case it is undefined; also ensures the address is
262
+ # checksummed but not prefixed for consistency.
263
+ #
264
+ # @param addr [String] the transaction destination address.
265
+ # @return [String] the sanitized transaction destination address.
266
+ def sanitize_address(addr)
267
+ addr = "" if addr.nil?
268
+ if addr.is_a? String and !addr.empty?
269
+ addr = Address.new(addr).to_s
270
+ addr = Util.remove_hex_prefix addr
271
+ end
272
+ return addr
178
273
  end
179
274
 
180
- def unsigned
181
- Tx.new to_h.merge(v: (self.chain_id) ? self.chain_id : 0, r: 0, s: 0)
275
+ # Populates the transaction value field with a serializable empty value
276
+ # in case it is undefined.
277
+ #
278
+ # @param val [Integer] the transaction value.
279
+ # @return [Integer] the sanitized transaction value.
280
+ def sanitize_amount(val)
281
+ val = 0 if val.nil?
282
+ return val
182
283
  end
183
284
 
184
- protected
285
+ # Populates the transaction payload field with a serializable empty value
286
+ # in case it is undefined; also ensures the data is binary not hex.
287
+ #
288
+ # @param data [String] the transaction payload data.
289
+ # @return [String] the sanitized transaction payload data.
290
+ def sanitize_data(data)
291
+ data = "" if data.nil?
292
+
293
+ # ensure payload to be binary if it's hex, otherwise we'll treat it raw
294
+ data = Util.hex_to_bin data if Util.is_hex? data
295
+ return data
296
+ end
185
297
 
186
- def sedes
187
- if self.prevent_replays? && !(Eth.replayable_v? v)
188
- self.class
189
- else
190
- UnsignedTx
298
+ # Populates the transaction access list field with a serializable empty
299
+ # array in case it is undefined; also ensures the nested data is binary
300
+ # not hex.
301
+ #
302
+ # @param list [Array] the transaction access list.
303
+ # @return [Array] the sanitized transaction access list.
304
+ def sanitize_list(list)
305
+ list = [] if list.nil?
306
+ list.each_with_index do |value, index|
307
+ if value.is_a? Array
308
+
309
+ # recursively check the entire array
310
+ list[index] = sanitize_list value
311
+ elsif Util.is_hex? value
312
+
313
+ # only modify if we find a hex value
314
+ list[index] = Util.hex_to_bin value
315
+ end
191
316
  end
317
+ return list
192
318
  end
193
- end
194
319
 
195
- UnsignedTx = Tx.exclude([:v, :r, :s])
320
+ # Allows to check wether a transaction is signed already.
321
+ #
322
+ # @return [Bool] true if transaction is already signed.
323
+ def is_signed?(tx)
324
+ !tx.signature_r.nil? and tx.signature_r != 0 and
325
+ !tx.signature_s.nil? and tx.signature_s != 0
326
+ end
327
+ end
196
328
  end
data/lib/eth/unit.rb ADDED
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2016-2022 The Ruby-Eth Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "bigdecimal"
16
+
17
+ # Provides the `Eth` module.
18
+ module Eth
19
+
20
+ # Provides constants for common Ethereum units.
21
+ module Unit
22
+ extend self
23
+
24
+ # Ethereum unit 1 wei := 0.000000000000000001 Ether.
25
+ WEI = BigDecimal("1e0").freeze
26
+
27
+ # Ethereum unit 1 babbage := 0.000000000000001 Ether or 1_000 wei.
28
+ BABBAGE = BigDecimal("1e3").freeze
29
+
30
+ # Ethereum unit 1 lovelace := 0.000000000001 Ether or 1_000_000 wei.
31
+ LOVELACE = BigDecimal("1e6").freeze
32
+
33
+ # Ethereum unit 1 shannon := 0.000000001 Ether or 1_000_000_000 wei.
34
+ SHANNON = BigDecimal("1e9").freeze
35
+
36
+ # Ethereum unit 1 szabo := 0.000_001 Ether or 1_000_000_000_000 wei.
37
+ SZABO = BigDecimal("1e12").freeze
38
+
39
+ # Ethereum unit 1 finney := 0.001 Ether or 1_000_000_000_000_000 wei.
40
+ FINNEY = BigDecimal("1e15").freeze
41
+
42
+ # Ethereum unit 1 Ether := 1_000_000_000_000_000_000 wei.
43
+ ETHER = BigDecimal("1e18").freeze
44
+
45
+ # Ethereum unit 1 Gwei := 0.000000001 Ether or 1_000_000_000 wei.
46
+ # Same as shannon, but more commonly used (billion wei).
47
+ GWEI = SHANNON.freeze
48
+ end
49
+ end