Class: Eth::Tx::Eip7702

Inherits:
Object
  • Object
show all
Defined in:
lib/eth/tx/eip7702.rb

Overview

Provides support for EIP-7702 transactions utilizing EIP-2718 types and envelopes. Ref: https://eips.ethereum.org/EIPS/eip-7702

Defined Under Namespace

Classes: Authorization

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(params) ⇒ Eip7702

Create a type-4 (EIP-7702) transaction payload object that can be prepared for envelope, signature and broadcast. Ref: https://eips.ethereum.org/EIPS/eip-7702

Parameters:

  • params (Hash) —

    all necessary transaction fields.

Options Hash (params):

  • :chain_id (Integer) —

    the chain ID.

  • :nonce (Integer) —

    the signer nonce.

  • :priority_fee (Integer) —

    the max priority fee per gas.

  • :max_gas_fee (Integer) —

    the max transaction fee per gas.

  • :gas_limit (Integer) —

    the gas limit.

  • :from (Eth::Address) —

    the sender address.

  • :to (Eth::Address) —

    the receiver address.

  • :value (Integer) —

    the transaction value.

  • :data (String) —

    the transaction data payload.

  • :access_list (Array) —

    an optional access list.

  • :authorization_list (Array) —

    the list of authorization instances (a list of Eth::Tx::Eip7702::Authorization instances).

Raises:



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/eth/tx/eip7702.rb', line 229

def initialize(params)
  fields = { recovery_id: nil, r: 0, s: 0 }.merge params

  # populate optional fields with serializable empty values
  fields[:chain_id] = Tx.sanitize_chain fields[:chain_id]
  fields[:from] = Tx.sanitize_address fields[:from]
  fields[:to] = Tx.sanitize_address fields[:to]
  fields[:value] = Tx.sanitize_amount fields[:value]
  fields[:data] = Tx.sanitize_data fields[:data]

  # ensure sane values for all mandatory fields
  fields = Tx.validate_params fields
  fields = Tx.validate_eip1559_params fields
  fields = Tx.validate_eip7702_params fields
  fields[:access_list] = Tx.sanitize_list fields[:access_list]

  # ensure gas limit is not too low
  minimum_cost = Tx.estimate_intrinsic_gas fields[:data], fields[:access_list]
  raise ParameterError, "Transaction gas limit is too low, try #{minimum_cost}!" if fields[:gas_limit].to_i < minimum_cost

  # populate class attributes
  @signer_nonce = fields[:nonce].to_i
  @max_priority_fee_per_gas = fields[:priority_fee].to_i
  @max_fee_per_gas = fields[:max_gas_fee].to_i
  @gas_limit = fields[:gas_limit].to_i
  @sender = fields[:from].to_s
  @destination = fields[:to].to_s
  @amount = fields[:value].to_i
  @payload = fields[:data]
  @access_list = fields[:access_list]
  @authorization_list = fields[:authorization_list]

  # the signature v is set to the chain id for unsigned transactions
  @signature_y_parity = fields[:recovery_id]
  @chain_id = fields[:chain_id]

  # the signature fields are empty for unsigned transactions.
  @signature_r = fields[:r]
  @signature_s = fields[:s]

  # last but not least, set the type.
  @type = TYPE_7702
end

Instance Attribute Details

#access_list ⇒ Object (readonly)

An optional EIP-2930 access list. Ref: https://eips.ethereum.org/EIPS/eip-2930



192
193
194
# File 'lib/eth/tx/eip7702.rb', line 192

def access_list
  @access_list
end

#amount ⇒ Object (readonly)

The transaction amount in Wei.



185
186
187
# File 'lib/eth/tx/eip7702.rb', line 185

def amount
  @amount
end

#authorization_list ⇒ Object (readonly)

The list of of Authorization instances



195
196
197
# File 'lib/eth/tx/eip7702.rb', line 195

def authorization_list
  @authorization_list
end

#chain_id ⇒ Object (readonly)

The EIP-155 Chain ID. Ref: https://eips.ethereum.org/EIPS/eip-155



167
168
169
# File 'lib/eth/tx/eip7702.rb', line 167

def chain_id
  @chain_id
end

#destination ⇒ Object (readonly)

The recipient address.



182
183
184
# File 'lib/eth/tx/eip7702.rb', line 182

def destination
  @destination
end

#gas_limit ⇒ Object (readonly)

The gas limit for the transaction.



179
180
181
# File 'lib/eth/tx/eip7702.rb', line 179

def gas_limit
  @gas_limit
end

#max_fee_per_gas ⇒ Object (readonly)

The transaction max fee per gas in Wei.



176
177
178
# File 'lib/eth/tx/eip7702.rb', line 176

def max_fee_per_gas
  @max_fee_per_gas
end

#max_priority_fee_per_gas ⇒ Object (readonly)

The transaction max priority fee per gas in Wei.



173
174
175
# File 'lib/eth/tx/eip7702.rb', line 173

def max_priority_fee_per_gas
  @max_priority_fee_per_gas
end

#payload ⇒ Object (readonly)

The transaction data payload.



188
189
190
# File 'lib/eth/tx/eip7702.rb', line 188

def payload
  @payload
end

#sender ⇒ Object (readonly)

The sender address.



207
208
209
# File 'lib/eth/tx/eip7702.rb', line 207

def sender
  @sender
end

#signature_r ⇒ Object (readonly)

The signature r value.



201
202
203
# File 'lib/eth/tx/eip7702.rb', line 201

def signature_r
  @signature_r
end

#signature_s ⇒ Object (readonly)

The signature s value.



204
205
206
# File 'lib/eth/tx/eip7702.rb', line 204

def signature_s
  @signature_s
end

#signature_y_parity ⇒ Object (readonly)

The signature's y-parity byte (not v).



198
199
200
# File 'lib/eth/tx/eip7702.rb', line 198

def signature_y_parity
  @signature_y_parity
end

#signer_nonce ⇒ Object (readonly)

The transaction nonce provided by the signer.



170
171
172
# File 'lib/eth/tx/eip7702.rb', line 170

def signer_nonce
  @signer_nonce
end

#type ⇒ Object (readonly)

The transaction type.



210
211
212
# File 'lib/eth/tx/eip7702.rb', line 210

def type
  @type
end

Instance Method Details

#decode(hex) ⇒ Eth::Tx::Eip7702

Decodes a raw transaction hex into an Eth::Tx::Eip7702 transaction object.

Parameters:

  • hex (String) —

    the raw transaction hex-string.

Returns:

Raises:



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/eth/tx/eip7702.rb', line 284

def decode(hex)
  hex = Util.remove_hex_prefix hex
  type = hex[0, 2]
  raise TransactionTypeError, "Invalid transaction type #{type.inspect}!" if type.to_i(16) != TYPE_7702

  bin = Util.hex_to_bin hex[2..]
  tx = Rlp.decode bin

  # decoded transactions always have 9 + 3 fields, even if they are empty or zero
  raise ParameterError, "Transaction missing fields!" if tx.size < 10

  # populate the 10 payload fields
  chain_id = Util.deserialize_rlp_int tx[0]
  nonce = Util.deserialize_rlp_int tx[1]
  priority_fee = Util.deserialize_rlp_int tx[2]
  max_gas_fee = Util.deserialize_rlp_int tx[3]
  gas_limit = Util.deserialize_rlp_int tx[4]
  to = Util.bin_to_hex tx[5]
  value = Util.deserialize_rlp_int tx[6]
  data = tx[7]
  access_list = tx[8]
  authorization_list = tx[9]
  authorizations = deserialize_authorizations(authorization_list)

  # populate class attributes
  @chain_id = chain_id.to_i
  @signer_nonce = nonce.to_i
  @max_priority_fee_per_gas = priority_fee.to_i
  @max_fee_per_gas = max_gas_fee.to_i
  @gas_limit = gas_limit.to_i
  @destination = to.to_s
  @amount = value.to_i
  @payload = data
  @access_list = access_list
  @authorization_list = authorizations

  # populate the 3 signature fields
  if tx.size == 10
    _set_signature(nil, 0, 0)
  elsif tx.size == 13
    recovery_id = Util.bin_to_hex(tx[10]).to_i(16)
    r = Util.bin_to_hex tx[11]
    s = Util.bin_to_hex tx[12]

    # allows us to force-setting a signature if the transaction is signed already
    _set_signature(recovery_id, r, s)
  else
    raise DecoderError, "Cannot decode EIP-7702 payload!"
  end

  # last but not least, set the type.
  @type = TYPE_7702

  unless recovery_id.nil?
    # recover sender address
    v = Chain.to_v recovery_id, chain_id
    public_key = Signature.recover(unsigned_hash, "#{r.rjust(64, "0")}#{s.rjust(64, "0")}#{v.to_s(16)}", chain_id)
    address = Util.public_key_to_address(public_key).to_s
    @sender = Tx.sanitize_address address
  else
    # keep the 'from' field blank
    @sender = Tx.sanitize_address nil
  end
end

#encoded ⇒ String

Encodes a raw transaction object, wraps it in an EIP-2718 envelope with an EIP-7702 type prefix.

Returns:

  • (String) —

    a raw, RLP-encoded EIP-7702 type transaction object.

Raises:



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/eth/tx/eip7702.rb', line 446

def encoded
  unless Tx.signed? self
    raise Signature::SignatureError, "Transaction is not signed!"
  end
  tx_data = []
  tx_data.push Util.serialize_int_to_big_endian @chain_id
  tx_data.push Util.serialize_int_to_big_endian @signer_nonce
  tx_data.push Util.serialize_int_to_big_endian @max_priority_fee_per_gas
  tx_data.push Util.serialize_int_to_big_endian @max_fee_per_gas
  tx_data.push Util.serialize_int_to_big_endian @gas_limit
  tx_data.push Util.hex_to_bin @destination
  tx_data.push Util.serialize_int_to_big_endian @amount
  tx_data.push Rlp::Sedes.binary.serialize @payload
  tx_data.push Rlp::Sedes.infer(@access_list).serialize @access_list

  authorization_list = @authorization_list.map { |authorization| authorization.raw }
  tx_data.push Rlp::Sedes.infer(authorization_list).serialize authorization_list

  tx_data.push Util.serialize_int_to_big_endian @signature_y_parity
  tx_data.push Util.serialize_int_to_big_endian @signature_r
  tx_data.push Util.serialize_int_to_big_endian @signature_s
  tx_encoded = Rlp.encode tx_data

  # create an EIP-2718 envelope with EIP-7702 type payload
  tx_type = Util.serialize_int_to_big_endian @type
  return "#{tx_type}#{tx_encoded}"
end

#hash ⇒ String

Gets the transaction hash.

Returns:

  • (String) —

    the transaction hash.



484
485
486
# File 'lib/eth/tx/eip7702.rb', line 484

def hash
  Util.bin_to_hex Util.keccak256 encoded
end

#hex ⇒ String

Gets the encoded, enveloped, raw transaction hex.

Returns:

  • (String) —

    the raw transaction hex.



477
478
479
# File 'lib/eth/tx/eip7702.rb', line 477

def hex
  Util.bin_to_hex encoded
end

#sign(key) ⇒ String

Sign the transaction with a given key.

Parameters:

  • key (Eth::Key) —

    the key-pair to use for signing.

Returns:

  • (String) —

    a transaction hash.

Raises:



394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/eth/tx/eip7702.rb', line 394

def sign(key)
  if Tx.signed? self
    raise Signature::SignatureError, "Transaction is already signed!"
  end

  # ensure the sender address matches the given key
  unless @sender.nil? or sender.empty?
    signer_address = Tx.sanitize_address key.address.to_s
    from_address = Tx.sanitize_address @sender
    raise Signature::SignatureError, "Signer does not match sender" unless signer_address == from_address
  end

  # sign a keccak hash of the unsigned, encoded transaction
  signature = key.sign(unsigned_hash, @chain_id)
  r, s, v = Signature.dissect signature
  recovery_id = Chain.to_recovery_id v.to_i(16), @chain_id
  @signature_y_parity = recovery_id
  @signature_r = r
  @signature_s = s
  return hash
end

#sign_with(signature) ⇒ String

Signs the transaction with a provided signature blob.

Parameters:

  • signature (String) —

    the concatenated r, s, and v values.

Returns:

  • (String) —

    a transaction hash.

Raises:



422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/eth/tx/eip7702.rb', line 422

def sign_with(signature)
  if Tx.signed? self
    raise Signature::SignatureError, "Transaction is already signed!"
  end

  # ensure the sender address matches the signature
  unless @sender.nil? or sender.empty?
    public_key = Signature.recover(unsigned_hash, signature, @chain_id)
    signer_address = Tx.sanitize_address Util.public_key_to_address(public_key).to_s
    from_address = Tx.sanitize_address @sender
    raise Signature::SignatureError, "Signer does not match sender" unless signer_address == from_address
  end

  r, s, v = Signature.dissect signature
  recovery_id = Chain.to_recovery_id v.to_i(16), @chain_id
  send :_set_signature, recovery_id, r, s
  return hash
end

#unsigned_copy(tx) ⇒ Eth::Tx::Eip7702

Creates an unsigned copy of a transaction payload (keeping the signatures on the authorizations).

Parameters:

Returns:

Raises:



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/eth/tx/eip7702.rb', line 354

def unsigned_copy(tx)

  # not checking transaction validity unless it's of a different class
  raise TransactionTypeError, "Cannot copy transaction of different payload type!" unless tx.instance_of? Tx::Eip7702

  # populate class attributes
  @signer_nonce = tx.signer_nonce
  @max_priority_fee_per_gas = tx.max_priority_fee_per_gas
  @max_fee_per_gas = tx.max_fee_per_gas
  @gas_limit = tx.gas_limit
  @destination = tx.destination
  @amount = tx.amount
  @payload = tx.payload
  @access_list = tx.access_list
  @chain_id = tx.chain_id

  @authorization_list = tx.authorization_list.map do |authorization|
    Authorization.new(chain_id: authorization.chain_id,
                      address: authorization.address,
                      nonce: authorization.nonce,
                      recovery_id: authorization.signature_y_parity,
                      r: authorization.signature_r,
                      s: authorization.signature_s)
  end
  # force-set signature to unsigned
  _set_signature(nil, 0, 0)

  # keep the 'from' field blank
  @sender = Tx.sanitize_address nil

  # last but not least, set the type.
  @type = TYPE_7702
end

#unsigned_encoded ⇒ String

Encodes the unsigned transaction payload in an EIP-7702 envelope, required for signing.

Returns:

  • (String) —

    an RLP-encoded, unsigned, enveloped EIP-7702 transaction.



492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
# File 'lib/eth/tx/eip7702.rb', line 492

def unsigned_encoded
  tx_data = []
  tx_data.push Util.serialize_int_to_big_endian @chain_id
  tx_data.push Util.serialize_int_to_big_endian @signer_nonce
  tx_data.push Util.serialize_int_to_big_endian @max_priority_fee_per_gas
  tx_data.push Util.serialize_int_to_big_endian @max_fee_per_gas
  tx_data.push Util.serialize_int_to_big_endian @gas_limit
  tx_data.push Util.hex_to_bin @destination
  tx_data.push Util.serialize_int_to_big_endian @amount
  tx_data.push Rlp::Sedes.binary.serialize @payload
  tx_data.push Rlp::Sedes.infer(@access_list).serialize @access_list

  authorization_list = @authorization_list.map { |authorization| authorization.raw }
  tx_data.push Rlp::Sedes.infer(authorization_list).serialize authorization_list

  tx_encoded = Rlp.encode tx_data

  # create an EIP-2718 envelope with EIP-7702 type payload (unsigned)
  tx_type = Util.serialize_int_to_big_endian @type

  return "#{tx_type}#{tx_encoded}"
end

#unsigned_hash ⇒ String

Gets the sign-hash required to sign a raw transaction.

Returns:

  • (String) —

    a Keccak-256 hash of an unsigned transaction.



518
519
520
# File 'lib/eth/tx/eip7702.rb', line 518

def unsigned_hash
  Util.keccak256 unsigned_encoded
end