eth 0.5.14 → 0.5.15

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.
@@ -0,0 +1,389 @@
1
+ # Copyright (c) 2016-2025 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
+ # Provides the {Eth} module.
16
+ module Eth
17
+
18
+ # Provides the `Tx` module supporting various transaction types.
19
+ module Tx
20
+
21
+ # Provides support for EIP-4844 transactions utilizing EIP-2718
22
+ # types and envelopes.
23
+ # Ref: https://eips.ethereum.org/EIPS/eip-4844
24
+ class Eip4844
25
+
26
+ # The EIP-155 Chain ID.
27
+ # Ref: https://eips.ethereum.org/EIPS/eip-155
28
+ attr_reader :chain_id
29
+
30
+ # The transaction nonce provided by the signer.
31
+ attr_reader :signer_nonce
32
+
33
+ # The transaction max priority fee per gas in Wei.
34
+ attr_reader :max_priority_fee_per_gas
35
+
36
+ # The transaction max fee per gas in Wei.
37
+ attr_reader :max_fee_per_gas
38
+
39
+ # The gas limit for the transaction.
40
+ attr_reader :gas_limit
41
+
42
+ # The recipient address.
43
+ attr_reader :destination
44
+
45
+ # The transaction amount in Wei.
46
+ attr_reader :amount
47
+
48
+ # The transaction data payload.
49
+ attr_reader :payload
50
+
51
+ # An optional EIP-2930 access list.
52
+ # Ref: https://eips.ethereum.org/EIPS/eip-2930
53
+ attr_reader :access_list
54
+
55
+ # The transaction max fee per blob gas in Wei.
56
+ attr_reader :max_fee_per_blob_gas
57
+
58
+ # The list of KZG commitment versioned hashes.
59
+ attr_reader :blob_versioned_hashes
60
+
61
+ # The signature's y-parity byte (not v).
62
+ attr_reader :signature_y_parity
63
+
64
+ # The signature `r` value.
65
+ attr_reader :signature_r
66
+
67
+ # The signature `s` value.
68
+ attr_reader :signature_s
69
+
70
+ # The sender address.
71
+ attr_reader :sender
72
+
73
+ # The transaction type.
74
+ attr_reader :type
75
+
76
+ # Create a type-3 (EIP-4844) transaction payload object that
77
+ # can be prepared for envelope, signature and broadcast.
78
+ # Ref: https://eips.ethereum.org/EIPS/eip-4844
79
+ #
80
+ # @param params [Hash] all necessary transaction fields.
81
+ # @option params [Integer] :chain_id the chain ID.
82
+ # @option params [Integer] :nonce the signer nonce.
83
+ # @option params [Integer] :priority_fee the max priority fee per gas.
84
+ # @option params [Integer] :max_gas_fee the max transaction fee per gas.
85
+ # @option params [Integer] :gas_limit the gas limit.
86
+ # @option params [Eth::Address] :from the sender address.
87
+ # @option params [Eth::Address] :to the receiver address.
88
+ # @option params [Integer] :value the transaction value.
89
+ # @option params [String] :data the transaction data payload.
90
+ # @option params [Array] :access_list an optional access list.
91
+ # @option params [Integer] :max_fee_per_blob_gas the max blob fee per gas.
92
+ # @option params [Array] :blob_versioned_hashes the blob versioned hashes.
93
+ # @raise [ParameterError] if gas limit is too low.
94
+ def initialize(params)
95
+ fields = { recovery_id: nil, r: 0, s: 0 }.merge params
96
+
97
+ # populate optional fields with serializable empty values
98
+ fields[:chain_id] = Tx.sanitize_chain fields[:chain_id]
99
+ fields[:from] = Tx.sanitize_address fields[:from]
100
+ fields[:to] = Tx.sanitize_address fields[:to]
101
+ fields[:value] = Tx.sanitize_amount fields[:value]
102
+ fields[:data] = Tx.sanitize_data fields[:data]
103
+
104
+ # ensure sane values for all mandatory fields
105
+ fields = Tx.validate_params fields
106
+ fields = Tx.validate_eip1559_params fields
107
+ fields = Tx.validate_eip4844_params fields
108
+ fields[:access_list] = Tx.sanitize_list fields[:access_list]
109
+ fields[:blob_versioned_hashes] = Tx.sanitize_hashes fields[:blob_versioned_hashes]
110
+
111
+ # ensure gas limit is not too low
112
+ minimum_cost = Tx.estimate_intrinsic_gas fields[:data], fields[:access_list]
113
+ raise ParameterError, "Transaction gas limit is too low, try #{minimum_cost}!" if fields[:gas_limit].to_i < minimum_cost
114
+
115
+ # populate class attributes
116
+ @signer_nonce = fields[:nonce].to_i
117
+ @max_priority_fee_per_gas = fields[:priority_fee].to_i
118
+ @max_fee_per_gas = fields[:max_gas_fee].to_i
119
+ @gas_limit = fields[:gas_limit].to_i
120
+ @sender = fields[:from].to_s
121
+ @destination = fields[:to].to_s
122
+ @amount = fields[:value].to_i
123
+ @payload = fields[:data]
124
+ @access_list = fields[:access_list]
125
+ @max_fee_per_blob_gas = fields[:max_fee_per_blob_gas].to_i
126
+ @blob_versioned_hashes = fields[:blob_versioned_hashes]
127
+
128
+ # the signature v is set to the chain id for unsigned transactions
129
+ @signature_y_parity = fields[:recovery_id]
130
+ @chain_id = fields[:chain_id]
131
+
132
+ # the signature fields are empty for unsigned transactions.
133
+ @signature_r = fields[:r]
134
+ @signature_s = fields[:s]
135
+
136
+ # last but not least, set the type.
137
+ @type = TYPE_4844
138
+ end
139
+
140
+ # Overloads the constructor for decoding raw transactions and creating unsigned copies.
141
+ konstructor :decode, :unsigned_copy
142
+
143
+ # Decodes a raw transaction hex into an {Eth::Tx::Eip4844}
144
+ # transaction object.
145
+ #
146
+ # @param hex [String] the raw transaction hex-string.
147
+ # @return [Eth::Tx::Eip4844] transaction payload.
148
+ # @raise [TransactionTypeError] if transaction type is invalid.
149
+ # @raise [ParameterError] if transaction is missing fields.
150
+ # @raise [DecoderError] if transaction decoding fails.
151
+ def decode(hex)
152
+ hex = Util.remove_hex_prefix hex
153
+ type = hex[0, 2]
154
+ raise TransactionTypeError, "Invalid transaction type #{type}!" if type.to_i(16) != TYPE_4844
155
+
156
+ bin = Util.hex_to_bin hex[2..]
157
+ tx = Rlp.decode bin
158
+
159
+ # decoded transactions always have 11 + 3 fields, even if they are empty or zero
160
+ raise ParameterError, "Transaction missing fields!" if tx.size < 11
161
+
162
+ # populate the 11 payload fields
163
+ chain_id = Util.deserialize_rlp_int tx[0]
164
+ nonce = Util.deserialize_rlp_int tx[1]
165
+ priority_fee = Util.deserialize_rlp_int tx[2]
166
+ max_gas_fee = Util.deserialize_rlp_int tx[3]
167
+ gas_limit = Util.deserialize_rlp_int tx[4]
168
+ to = Util.bin_to_hex tx[5]
169
+ value = Util.deserialize_rlp_int tx[6]
170
+ data = tx[7]
171
+ access_list = tx[8]
172
+ max_fee_per_blob_gas = Util.deserialize_rlp_int tx[9]
173
+ blob_versioned_hashes = tx[10]
174
+
175
+ # populate class attributes
176
+ @chain_id = chain_id.to_i
177
+ @signer_nonce = nonce.to_i
178
+ @max_priority_fee_per_gas = priority_fee.to_i
179
+ @max_fee_per_gas = max_gas_fee.to_i
180
+ @gas_limit = gas_limit.to_i
181
+ @destination = to.to_s
182
+ @amount = value.to_i
183
+ @payload = data
184
+ @access_list = access_list
185
+ @max_fee_per_blob_gas = max_fee_per_blob_gas.to_i
186
+ @blob_versioned_hashes = blob_versioned_hashes
187
+
188
+ # populate the 3 signature fields
189
+ if tx.size == 11
190
+ _set_signature(nil, 0, 0)
191
+ elsif tx.size == 14
192
+ recovery_id = Util.bin_to_hex(tx[11]).to_i(16)
193
+ r = Util.bin_to_hex tx[12]
194
+ s = Util.bin_to_hex tx[13]
195
+
196
+ # allows us to force-setting a signature if the transaction is signed already
197
+ _set_signature(recovery_id, r, s)
198
+ else
199
+ raise DecoderError, "Cannot decode EIP-4844 payload!"
200
+ end
201
+
202
+ # last but not least, set the type.
203
+ @type = TYPE_4844
204
+
205
+ unless recovery_id.nil?
206
+ # recover sender address
207
+ v = Chain.to_v recovery_id, chain_id
208
+ public_key = Signature.recover(unsigned_hash, "#{r.rjust(64, "0")}#{s.rjust(64, "0")}#{v.to_s(16)}", chain_id)
209
+ address = Util.public_key_to_address(public_key).to_s
210
+ @sender = Tx.sanitize_address address
211
+ else
212
+ # keep the 'from' field blank
213
+ @sender = Tx.sanitize_address nil
214
+ end
215
+ end
216
+
217
+ # Creates an unsigned copy of a transaction payload.
218
+ #
219
+ # @param tx [Eth::Tx::Eip4844] an EIP-4844 transaction payload.
220
+ # @return [Eth::Tx::Eip4844] an unsigned EIP-4844 transaction payload.
221
+ # @raise [TransactionTypeError] if transaction type does not match.
222
+ def unsigned_copy(tx)
223
+
224
+ # not checking transaction validity unless it's of a different class
225
+ raise TransactionTypeError, "Cannot copy transaction of different payload type!" unless tx.instance_of? Tx::Eip4844
226
+
227
+ # populate class attributes
228
+ @signer_nonce = tx.signer_nonce
229
+ @max_priority_fee_per_gas = tx.max_priority_fee_per_gas
230
+ @max_fee_per_gas = tx.max_fee_per_gas
231
+ @gas_limit = tx.gas_limit
232
+ @destination = tx.destination
233
+ @amount = tx.amount
234
+ @payload = tx.payload
235
+ @access_list = tx.access_list
236
+ @max_fee_per_blob_gas = tx.max_fee_per_blob_gas
237
+ @blob_versioned_hashes = tx.blob_versioned_hashes
238
+ @chain_id = tx.chain_id
239
+
240
+ # force-set signature to unsigned
241
+ _set_signature(nil, 0, 0)
242
+
243
+ # keep the 'from' field blank
244
+ @sender = Tx.sanitize_address nil
245
+
246
+ # last but not least, set the type.
247
+ @type = TYPE_4844
248
+ end
249
+
250
+ # Sign the transaction with a given key.
251
+ #
252
+ # @param key [Eth::Key] the key-pair to use for signing.
253
+ # @return [String] a transaction hash.
254
+ # @raise [Signature::SignatureError] if transaction is already signed.
255
+ # @raise [Signature::SignatureError] if sender address does not match signing key.
256
+ def sign(key)
257
+ if Tx.signed? self
258
+ raise Signature::SignatureError, "Transaction is already signed!"
259
+ end
260
+
261
+ # ensure the sender address matches the given key
262
+ unless @sender.nil? or sender.empty?
263
+ signer_address = Tx.sanitize_address key.address.to_s
264
+ from_address = Tx.sanitize_address @sender
265
+ raise Signature::SignatureError, "Signer does not match sender" unless signer_address == from_address
266
+ end
267
+
268
+ # sign a keccak hash of the unsigned, encoded transaction
269
+ signature = key.sign(unsigned_hash, @chain_id)
270
+ r, s, v = Signature.dissect signature
271
+ recovery_id = Chain.to_recovery_id v.to_i(16), @chain_id
272
+ @signature_y_parity = recovery_id
273
+ @signature_r = r
274
+ @signature_s = s
275
+ return hash
276
+ end
277
+
278
+ # Signs the transaction with a provided signature blob.
279
+ #
280
+ # @param signature [String] the concatenated `r`, `s`, and `v` values.
281
+ # @return [String] a transaction hash.
282
+ # @raise [Signature::SignatureError] if transaction is already signed.
283
+ # @raise [Signature::SignatureError] if sender address does not match signer.
284
+ def sign_with(signature)
285
+ if Tx.signed? self
286
+ raise Signature::SignatureError, "Transaction is already signed!"
287
+ end
288
+
289
+ # ensure the sender address matches the signature
290
+ unless @sender.nil? or sender.empty?
291
+ public_key = Signature.recover(unsigned_hash, signature, @chain_id)
292
+ signer_address = Tx.sanitize_address Util.public_key_to_address(public_key).to_s
293
+ from_address = Tx.sanitize_address @sender
294
+ raise Signature::SignatureError, "Signer does not match sender" unless signer_address == from_address
295
+ end
296
+
297
+ r, s, v = Signature.dissect signature
298
+ recovery_id = Chain.to_recovery_id v.to_i(16), @chain_id
299
+ send :_set_signature, recovery_id, r, s
300
+ return hash
301
+ end
302
+
303
+ # Encodes a raw transaction object, wraps it in an EIP-2718 envelope
304
+ # with an EIP-4844 type prefix.
305
+ #
306
+ # @return [String] a raw, RLP-encoded EIP-4844 type transaction object.
307
+ # @raise [Signature::SignatureError] if the transaction is not yet signed.
308
+ def encoded
309
+ unless Tx.signed? self
310
+ raise Signature::SignatureError, "Transaction is not signed!"
311
+ end
312
+ tx_data = []
313
+ tx_data.push Util.serialize_int_to_big_endian @chain_id
314
+ tx_data.push Util.serialize_int_to_big_endian @signer_nonce
315
+ tx_data.push Util.serialize_int_to_big_endian @max_priority_fee_per_gas
316
+ tx_data.push Util.serialize_int_to_big_endian @max_fee_per_gas
317
+ tx_data.push Util.serialize_int_to_big_endian @gas_limit
318
+ tx_data.push Util.hex_to_bin @destination
319
+ tx_data.push Util.serialize_int_to_big_endian @amount
320
+ tx_data.push Rlp::Sedes.binary.serialize @payload
321
+ tx_data.push Rlp::Sedes.infer(@access_list).serialize @access_list
322
+ tx_data.push Util.serialize_int_to_big_endian @max_fee_per_blob_gas
323
+ tx_data.push Rlp::Sedes.infer(@blob_versioned_hashes).serialize @blob_versioned_hashes
324
+ tx_data.push Util.serialize_int_to_big_endian @signature_y_parity
325
+ tx_data.push Util.serialize_int_to_big_endian @signature_r
326
+ tx_data.push Util.serialize_int_to_big_endian @signature_s
327
+ tx_encoded = Rlp.encode tx_data
328
+
329
+ # create an EIP-2718 envelope with EIP-4844 type payload
330
+ tx_type = Util.serialize_int_to_big_endian @type
331
+ return "#{tx_type}#{tx_encoded}"
332
+ end
333
+
334
+ # Gets the encoded, enveloped, raw transaction hex.
335
+ #
336
+ # @return [String] the raw transaction hex.
337
+ def hex
338
+ Util.bin_to_hex encoded
339
+ end
340
+
341
+ # Gets the transaction hash.
342
+ #
343
+ # @return [String] the transaction hash.
344
+ def hash
345
+ Util.bin_to_hex Util.keccak256 encoded
346
+ end
347
+
348
+ # Encodes the unsigned transaction payload in an EIP-4844 envelope,
349
+ # required for signing.
350
+ #
351
+ # @return [String] an RLP-encoded, unsigned, enveloped EIP-4844 transaction.
352
+ def unsigned_encoded
353
+ tx_data = []
354
+ tx_data.push Util.serialize_int_to_big_endian @chain_id
355
+ tx_data.push Util.serialize_int_to_big_endian @signer_nonce
356
+ tx_data.push Util.serialize_int_to_big_endian @max_priority_fee_per_gas
357
+ tx_data.push Util.serialize_int_to_big_endian @max_fee_per_gas
358
+ tx_data.push Util.serialize_int_to_big_endian @gas_limit
359
+ tx_data.push Util.hex_to_bin @destination
360
+ tx_data.push Util.serialize_int_to_big_endian @amount
361
+ tx_data.push Rlp::Sedes.binary.serialize @payload
362
+ tx_data.push Rlp::Sedes.infer(@access_list).serialize @access_list
363
+ tx_data.push Util.serialize_int_to_big_endian @max_fee_per_blob_gas
364
+ tx_data.push Rlp::Sedes.infer(@blob_versioned_hashes).serialize @blob_versioned_hashes
365
+ tx_encoded = Rlp.encode tx_data
366
+
367
+ # create an EIP-2718 envelope with EIP-4844 type payload (unsigned)
368
+ tx_type = Util.serialize_int_to_big_endian @type
369
+ return "#{tx_type}#{tx_encoded}"
370
+ end
371
+
372
+ # Gets the sign-hash required to sign a raw transaction.
373
+ #
374
+ # @return [String] a Keccak-256 hash of an unsigned transaction.
375
+ def unsigned_hash
376
+ Util.keccak256 unsigned_encoded
377
+ end
378
+
379
+ private
380
+
381
+ # Force-sets an existing signature of a decoded transaction.
382
+ def _set_signature(recovery_id, r, s)
383
+ @signature_y_parity = recovery_id
384
+ @signature_r = r
385
+ @signature_s = s
386
+ end
387
+ end
388
+ end
389
+ end
@@ -196,7 +196,7 @@ module Eth
196
196
  # @option params [Integer] :max_gas_fee the max transaction fee per gas.
197
197
  # @option params [Integer] :gas_limit the gas limit.
198
198
  # @option params [Eth::Address] :from the sender address.
199
- # @option params [Eth::Address] :to the reciever address.
199
+ # @option params [Eth::Address] :to the receiver address.
200
200
  # @option params [Integer] :value the transaction value.
201
201
  # @option params [String] :data the transaction data payload.
202
202
  # @option params [Array] :access_list an optional access list.
@@ -269,13 +269,13 @@ module Eth
269
269
  raise ParameterError, "Transaction missing fields!" if tx.size < 10
270
270
 
271
271
  # populate the 10 payload fields
272
- chain_id = Util.deserialize_big_endian_to_int tx[0]
273
- nonce = Util.deserialize_big_endian_to_int tx[1]
274
- priority_fee = Util.deserialize_big_endian_to_int tx[2]
275
- max_gas_fee = Util.deserialize_big_endian_to_int tx[3]
276
- gas_limit = Util.deserialize_big_endian_to_int tx[4]
272
+ chain_id = Util.deserialize_rlp_int tx[0]
273
+ nonce = Util.deserialize_rlp_int tx[1]
274
+ priority_fee = Util.deserialize_rlp_int tx[2]
275
+ max_gas_fee = Util.deserialize_rlp_int tx[3]
276
+ gas_limit = Util.deserialize_rlp_int tx[4]
277
277
  to = Util.bin_to_hex tx[5]
278
- value = Util.deserialize_big_endian_to_int tx[6]
278
+ value = Util.deserialize_rlp_int tx[6]
279
279
  data = tx[7]
280
280
  access_list = tx[8]
281
281
  authorization_list = tx[9]
@@ -389,6 +389,31 @@ module Eth
389
389
  return hash
390
390
  end
391
391
 
392
+ # Signs the transaction with a provided signature blob.
393
+ #
394
+ # @param signature [String] the concatenated `r`, `s`, and `v` values.
395
+ # @return [String] a transaction hash.
396
+ # @raise [Signature::SignatureError] if transaction is already signed.
397
+ # @raise [Signature::SignatureError] if sender address does not match signer.
398
+ def sign_with(signature)
399
+ if Tx.signed? self
400
+ raise Signature::SignatureError, "Transaction is already signed!"
401
+ end
402
+
403
+ # ensure the sender address matches the signature
404
+ unless @sender.nil? or sender.empty?
405
+ public_key = Signature.recover(unsigned_hash, signature, @chain_id)
406
+ signer_address = Tx.sanitize_address Util.public_key_to_address(public_key).to_s
407
+ from_address = Tx.sanitize_address @sender
408
+ raise Signature::SignatureError, "Signer does not match sender" unless signer_address == from_address
409
+ end
410
+
411
+ r, s, v = Signature.dissect signature
412
+ recovery_id = Chain.to_recovery_id v.to_i(16), @chain_id
413
+ send :_set_signature, recovery_id, r, s
414
+ return hash
415
+ end
416
+
392
417
  # Encodes a raw transaction object, wraps it in an EIP-2718 envelope
393
418
  # with an EIP-7702 type prefix.
394
419
  #
@@ -474,9 +499,9 @@ module Eth
474
499
 
475
500
  def deserialize_authorizations(authorization_list)
476
501
  authorization_list.map do |authorization_tuple|
477
- chain_id = Util.deserialize_big_endian_to_int authorization_tuple[0]
502
+ chain_id = Util.deserialize_rlp_int authorization_tuple[0]
478
503
  address = Util.bin_to_hex authorization_tuple[1]
479
- nonce = Util.deserialize_big_endian_to_int authorization_tuple[2]
504
+ nonce = Util.deserialize_rlp_int authorization_tuple[2]
480
505
  recovery_id = Util.bin_to_hex(authorization_tuple[3]).to_i(16)
481
506
  r = Util.bin_to_hex authorization_tuple[4]
482
507
  s = Util.bin_to_hex authorization_tuple[5]
data/lib/eth/tx/legacy.rb CHANGED
@@ -68,7 +68,7 @@ module Eth
68
68
  # @option params [Integer] :gas_price the gas price.
69
69
  # @option params [Integer] :gas_limit the gas limit.
70
70
  # @option params [Eth::Address] :from the sender address.
71
- # @option params [Eth::Address] :to the reciever address.
71
+ # @option params [Eth::Address] :to the receiver address.
72
72
  # @option params [Integer] :value the transaction value.
73
73
  # @option params [String] :data the transaction data payload.
74
74
  # @param chain_id [Integer] the EIP-155 Chain ID.
@@ -128,18 +128,18 @@ module Eth
128
128
  raise ParameterError, "Transaction missing fields!" if tx.size < 9
129
129
 
130
130
  # populate the 9 fields
131
- nonce = Util.deserialize_big_endian_to_int tx[0]
132
- gas_price = Util.deserialize_big_endian_to_int tx[1]
133
- gas_limit = Util.deserialize_big_endian_to_int tx[2]
131
+ nonce = Util.deserialize_rlp_int tx[0]
132
+ gas_price = Util.deserialize_rlp_int tx[1]
133
+ gas_limit = Util.deserialize_rlp_int tx[2]
134
134
  to = Util.bin_to_hex tx[3]
135
- value = Util.deserialize_big_endian_to_int tx[4]
135
+ value = Util.deserialize_rlp_int tx[4]
136
136
  data = tx[5]
137
137
  v = Util.bin_to_hex tx[6]
138
138
  r = Util.bin_to_hex tx[7]
139
139
  s = Util.bin_to_hex tx[8]
140
140
 
141
141
  # try to recover the chain id from v
142
- chain_id = Chain.to_chain_id Util.deserialize_big_endian_to_int tx[6]
142
+ chain_id = Chain.to_chain_id Util.deserialize_rlp_int tx[6]
143
143
 
144
144
  # populate class attributes
145
145
  @signer_nonce = nonce.to_i
@@ -223,6 +223,30 @@ module Eth
223
223
  return hash
224
224
  end
225
225
 
226
+ # Signs the transaction with a provided signature blob.
227
+ #
228
+ # @param signature [String] the concatenated `r`, `s`, and `v` values.
229
+ # @return [String] a transaction hash.
230
+ # @raise [Signature::SignatureError] if transaction is already signed.
231
+ # @raise [Signature::SignatureError] if sender address does not match signer.
232
+ def sign_with(signature)
233
+ if Tx.signed? self
234
+ raise Signature::SignatureError, "Transaction is already signed!"
235
+ end
236
+
237
+ # ensure the sender address matches the signature
238
+ unless @sender.nil? or sender.empty?
239
+ public_key = Signature.recover(unsigned_hash, signature, @chain_id)
240
+ signer_address = Tx.sanitize_address Util.public_key_to_address(public_key).to_s
241
+ from_address = Tx.sanitize_address @sender
242
+ raise Signature::SignatureError, "Signer does not match sender" unless signer_address == from_address
243
+ end
244
+
245
+ r, s, v = Signature.dissect signature
246
+ send :_set_signature, v, r, s
247
+ return hash
248
+ end
249
+
226
250
  # Encodes a raw transaction object.
227
251
  #
228
252
  # @return [String] a raw, RLP-encoded legacy transaction.
data/lib/eth/tx.rb CHANGED
@@ -17,6 +17,7 @@ require "konstructor"
17
17
  require "eth/chain"
18
18
  require "eth/tx/eip1559"
19
19
  require "eth/tx/eip2930"
20
+ require "eth/tx/eip4844"
20
21
  require "eth/tx/eip7702"
21
22
  require "eth/tx/legacy"
22
23
  require "eth/unit"
@@ -87,6 +88,8 @@ module Eth
87
88
 
88
89
  # Creates a new transaction of any type for given parameters and chain ID.
89
90
  # Required parameters are (optional in brackets):
91
+ # - EIP-4844: chain_id, nonce, priority_fee, max_gas_fee, gas_limit, max_fee_per_blob_gas, blob_versioned_hashes(, from, to,
92
+ # value, data, access_list)
90
93
  # - EIP-1559: chain_id, nonce, priority_fee, max_gas_fee, gas_limit(, from, to,
91
94
  # value, data, access_list)
92
95
  # - EIP-2930: chain_id, nonce, gas_price, gas_limit, access_list(, from, to,
@@ -99,9 +102,10 @@ module Eth
99
102
  # @param chain_id [Integer] the EIP-155 Chain ID (legacy transactions only).
100
103
  def new(params, chain_id = Chain::ETHEREUM)
101
104
 
102
- # if we deal with blobs, attempt EIP-4844 (not implemented)
105
+ # if we deal with blobs, attempt EIP-4844
103
106
  unless params[:max_fee_per_blob_gas].nil?
104
- raise NotimplementedError, "EIP-4844 blob transactions are not implemented"
107
+ params[:chain_id] = chain_id if params[:chain_id].nil?
108
+ return Tx::Eip4844.new params
105
109
  end
106
110
 
107
111
  # if we deal with authorizations, attempt EIP-7702
@@ -148,7 +152,7 @@ module Eth
148
152
  when TYPE_4844
149
153
 
150
154
  # EIP-4844 transaction (type 3)
151
- raise NotimplementedError, "EIP-4844 blob transactions are not implemented"
155
+ return Tx::Eip4844.decode hex
152
156
  when TYPE_7702
153
157
 
154
158
  # EIP-7702 transaction (type 4)
@@ -182,7 +186,7 @@ module Eth
182
186
  when TYPE_4844
183
187
 
184
188
  # EIP-4844 transaction (type 3)
185
- raise NotimplementedError, "EIP-4844 blob transactions are not implemented"
189
+ return Tx::Eip4844.unsigned_copy tx
186
190
  when TYPE_7702
187
191
 
188
192
  # EIP-7702 transaction (type 4)
@@ -278,6 +282,24 @@ module Eth
278
282
  return fields
279
283
  end
280
284
 
285
+ # Validates that the type-3 transaction blob fields are present
286
+ #
287
+ # @param fields [Hash] the transaction fields.
288
+ # @return [Hash] the validated transaction fields.
289
+ # @raise [ParameterError] if max blob fee or blob hashes are invalid.
290
+ def validate_eip4844_params(fields)
291
+ if fields[:max_fee_per_blob_gas].nil? or fields[:max_fee_per_blob_gas] < 0
292
+ raise ParameterError, "Invalid max blob fee #{fields[:max_fee_per_blob_gas]}!"
293
+ end
294
+ if fields[:blob_versioned_hashes].nil? or !fields[:blob_versioned_hashes].is_a? Array or fields[:blob_versioned_hashes].empty?
295
+ raise ParameterError, "Invalid blob versioned hashes #{fields[:blob_versioned_hashes]}!"
296
+ end
297
+ if fields[:to].nil? or fields[:to].empty?
298
+ raise ParameterError, "Invalid destination address #{fields[:to]}!"
299
+ end
300
+ return fields
301
+ end
302
+
281
303
  # Validates that the type-4 transaction field authorization list is present
282
304
  #
283
305
  # @param fields [Hash] the transaction fields.
@@ -372,6 +394,22 @@ module Eth
372
394
  return list
373
395
  end
374
396
 
397
+ # Populates the blob versioned hashes field with a serializable empty
398
+ # array in case it is undefined; also ensures the hashes are binary
399
+ # not hex.
400
+ #
401
+ # @param list [Array] the blob versioned hashes.
402
+ # @return [Array] the sanitized blob versioned hashes.
403
+ def sanitize_hashes(list)
404
+ list = [] if list.nil?
405
+ list.each_with_index do |value, index|
406
+ if Util.hex? value
407
+ list[index] = Util.hex_to_bin value
408
+ end
409
+ end
410
+ return list
411
+ end
412
+
375
413
  # Allows to check wether a transaction is signed already.
376
414
  #
377
415
  # @return [Bool] true if transaction is already signed.
data/lib/eth/util.rb CHANGED
@@ -72,8 +72,7 @@ module Eth
72
72
  # @param hex [String] a hex-string to be prefixed.
73
73
  # @return [String] a prefixed hex-string.
74
74
  def prefix_hex(hex)
75
- return hex if prefixed? hex
76
- return "0x#{hex}"
75
+ "0x#{remove_hex_prefix hex}"
77
76
  end
78
77
 
79
78
  # Removes the `0x` prefix of a hexa-decimal string.
@@ -93,12 +92,12 @@ module Eth
93
92
  prefix_hex bin_to_hex bin
94
93
  end
95
94
 
96
- # Checks if a string is hex-adecimal.
95
+ # Checks if a string is hexadecimal.
97
96
  #
98
97
  # @param str [String] a string to be checked.
99
- # @return [String] a match if true; `nil` if not.
98
+ # @return [MatchData, nil] a match if true; `nil` if not.
100
99
  def hex?(str)
101
- return false unless str.is_a? String
100
+ return unless str.is_a? String
102
101
  str = remove_hex_prefix str
103
102
  str.match /\A[0-9a-fA-F]*\z/
104
103
  end
@@ -108,7 +107,7 @@ module Eth
108
107
  # @param hex [String] a string to be checked.
109
108
  # @return [String] a match if true; `nil` if not.
110
109
  def prefixed?(hex)
111
- hex.match /\A0x/
110
+ hex.match /\A0x/i
112
111
  end
113
112
 
114
113
  # Serializes an unsigned integer to big endian.
@@ -129,7 +128,11 @@ module Eth
129
128
  # @param num [Integer] integer to be converted.
130
129
  # @return [String] packed, big-endian integer string.
131
130
  def int_to_big_endian(num)
132
- hex = num.to_s(16) unless hex? num
131
+ hex = if hex? num
132
+ remove_hex_prefix num
133
+ else
134
+ num.to_s(16)
135
+ end
133
136
  hex = "0#{hex}" if hex.size.odd?
134
137
  hex_to_bin hex
135
138
  end
@@ -142,6 +145,15 @@ module Eth
142
145
  Rlp::Sedes.big_endian_int.deserialize str.sub(/\A(\x00)+/, "")
143
146
  end
144
147
 
148
+ # Deserializes an RLP integer, enforcing minimal encoding.
149
+ #
150
+ # @param str [String] serialized big endian integer string.
151
+ # @return [Integer] a deserialized unsigned integer.
152
+ # @raise [Rlp::DeserializationError] if encoding is not minimal.
153
+ def deserialize_rlp_int(str)
154
+ Rlp::Sedes.big_endian_int.deserialize str
155
+ end
156
+
145
157
  # Converts a big endian to an interger.
146
158
  #
147
159
  # @param str [String] big endian to be converted.
data/lib/eth/version.rb CHANGED
@@ -22,7 +22,7 @@ module Eth
22
22
  MINOR = 5.freeze
23
23
 
24
24
  # Defines the patch version of the {Eth} module.
25
- PATCH = 14.freeze
25
+ PATCH = 15.freeze
26
26
 
27
27
  # Defines the version string of the {Eth} module.
28
28
  VERSION = [MAJOR, MINOR, PATCH].join(".").freeze