eth 0.4.18 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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