eth 0.4.12 → 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,197 +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
26
 
4
- include RLP::Sedes::Serializable
5
- extend Sedes
27
+ # Provides the `Tx` module supporting various transaction types.
28
+ module Tx
29
+ extend self
6
30
 
7
- set_serializable_fields({
8
- nonce: big_endian_int,
9
- gas_price: big_endian_int,
10
- gas_limit: big_endian_int,
11
- to: address,
12
- value: big_endian_int,
13
- data_bin: binary,
14
- v: big_endian_int,
15
- r: big_endian_int,
16
- s: big_endian_int
17
- })
31
+ # Provides a special transactoin error if transaction type is unknown.
32
+ class TransactionTypeError < TypeError; end
18
33
 
19
- attr_writer :signature
34
+ # Provides an decoder error if transaction cannot be decoded.
35
+ class DecoderError < StandardError; end
20
36
 
21
- def self.decode(data)
22
- data = Utils.hex_to_bin(data) if data.match(/\A(?:0x)?\h+\Z/)
23
- txh = deserialize(RLP.decode data).to_h
37
+ # Provides a parameter error if parameter types are invalid.
38
+ class ParameterError < TypeError; end
24
39
 
25
- txh[:chain_id] = Eth.chain_id_from_signature(txh)
40
+ # The minimum transaction gas limit required for a value transfer.
41
+ DEFAULT_GAS_LIMIT = 21_000.freeze
26
42
 
27
- self.new txh
28
- end
43
+ # The "default" transaction gas price of 20 GWei. Do not use.
44
+ DEFAULT_GAS_PRICE = (20 * Unit::GWEI).freeze
29
45
 
30
- def initialize(params)
31
- fields = {v: 0, r: 0, s: 0}.merge params
32
- fields[:to] = Utils.normalize_address(fields[:to])
46
+ # The calldata gas cost of a non-zero byte as per EIP-2028.
47
+ COST_NON_ZERO_BYTE = 16.freeze
33
48
 
34
- self.chain_id = (params[:chain_id]) ? params.delete(:chain_id) : Eth.chain_id
49
+ # The calldata gas cost of a zero byte.
50
+ COST_ZERO_BYTE = 4.freeze
35
51
 
36
- if params[:data]
37
- self.data = params.delete(:data)
38
- fields[:data_bin] = data_bin
39
- end
40
- serializable_initialize fields
52
+ # The access list gas cost of a storage key as per EIP-2930.
53
+ COST_STORAGE_KEY = 1_900.freeze
41
54
 
42
- check_transaction_validity
43
- end
55
+ # The access list gas cost of an address as per EIP-2930.
56
+ COST_ADDRESS = 2_400.freeze
44
57
 
45
- def unsigned_encoded
46
- us = unsigned
47
- RLP.encode(us, sedes: us.sedes)
48
- end
58
+ # The maximum transaction gas limit is bound by the block gas limit.
59
+ BLOCK_GAS_LIMIT = 25_000_000.freeze
49
60
 
50
- def signing_data
51
- Utils.bin_to_prefixed_hex unsigned_encoded
52
- end
61
+ # The legacy transaction type is 0.
62
+ TYPE_LEGACY = 0x00.freeze
53
63
 
54
- def encoded
55
- RLP.encode self
56
- end
64
+ # The EIP-2930 transaction type is 1.
65
+ TYPE_2930 = 0x01.freeze
57
66
 
58
- def hex
59
- Utils.bin_to_prefixed_hex encoded
60
- end
67
+ # The EIP-1559 transaction type is 2.
68
+ TYPE_1559 = 0x02.freeze
61
69
 
62
- def sign(key)
63
- sig = key.sign(unsigned_encoded)
64
- vrs = Utils.v_r_s_for sig
65
- self.v = (self.chain_id) ? ((self.chain_id * 2) + vrs[0] + 8) : vrs[0]
66
- self.r = vrs[1]
67
- self.s = vrs[2]
70
+ # The zero byte is 0x00.
71
+ ZERO_BYTE = "\x00".freeze
68
72
 
69
- clear_signature
70
- self
71
- 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)
72
84
 
73
- def to_h
74
- hash_keys.inject({}) do |hash, field|
75
- hash[field] = send field
76
- 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
77
89
  end
78
- end
79
90
 
80
- def from
81
- if ecdsa_signature
82
- public_key = OpenSsl.recover_compact(signature_hash, ecdsa_signature)
83
- Utils.public_key_to_address(public_key) if public_key
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
84
95
  end
85
- end
86
96
 
87
- def signature
88
- return @signature if @signature
89
- @signature = { v: v, r: r, s: s } if [v, r, s].all? && (v > 0)
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
90
100
  end
91
101
 
92
- def ecdsa_signature
93
- return @ecdsa_signature if @ecdsa_signature
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
94
120
 
95
- if [v, r, s].all? && (v > 0)
96
- s_v = (self.chain_id) ? (v - (self.chain_id * 2) - 8) : v
97
- @ecdsa_signature = [
98
- Utils.int_to_base256(s_v),
99
- Utils.zpad_int(r),
100
- Utils.zpad_int(s),
101
- ].join
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
102
127
  end
103
128
  end
104
129
 
105
- def hash
106
- "0x#{Utils.bin_to_hex Utils.keccak256_rlp(self)}"
107
- end
108
- alias_method :id, :hash
109
-
110
- def data_hex
111
- Utils.bin_to_prefixed_hex data_bin
112
- end
113
-
114
- def data_hex=(hex)
115
- self.data_bin = Utils.hex_to_bin(hex)
116
- end
117
-
118
- def data
119
- Eth.tx_data_hex? ? data_hex : data_bin
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}!"
120
151
  end
121
152
 
122
- def data=(string)
123
- Eth.tx_data_hex? ? self.data_hex=(string) : self.data_bin=(string)
124
- end
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
171
+ end
172
+ unless list.nil? or list.empty?
173
+ list.each do |entry|
125
174
 
126
- def chain_id
127
- @chain_id
128
- end
175
+ # count addresses
176
+ gas += COST_ADDRESS
129
177
 
130
- def chain_id=(cid)
131
- if cid != @chain_id
132
- self.v = 0
133
- self.r = 0
134
- self.s = 0
178
+ entry.last.each do |key|
135
179
 
136
- clear_signature
180
+ # count storage keys
181
+ gas += COST_STORAGE_KEY
182
+ end
183
+ end
137
184
  end
138
-
139
- @chain_id = (cid == 0) ? nil : cid
185
+ return gas
140
186
  end
141
187
 
142
- def prevent_replays?
143
- !self.chain_id.nil?
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
144
219
  end
145
220
 
146
- private
147
-
148
- def clear_signature
149
- @signature = nil
150
- @ecdsa_signature = nil
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]}!"
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
151
248
  end
152
249
 
153
- def hash_keys
154
- keys = self.class.serializable_fields.keys
155
- keys.delete(:data_bin)
156
- keys + [:data, :chain_id]
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
157
258
  end
158
259
 
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"
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
164
271
  end
272
+ return addr
165
273
  end
166
274
 
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
174
- end
175
-
176
- def signature_hash
177
- Utils.keccak256 unsigned_encoded
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
178
283
  end
179
284
 
180
- def unsigned
181
- Tx.new to_h.merge(v: (self.chain_id) ? self.chain_id : 0, r: 0, s: 0)
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
182
296
  end
183
297
 
184
- protected
185
-
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
319
 
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
194
327
  end
195
-
196
- UnsignedTx = Tx.exclude([:v, :r, :s])
197
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