Skip to content

Commit

Permalink
feat(EIP-4844): eth.Transaction/Receipt EIP-4844 support
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanschneider committed Sep 13, 2023
1 parent dd45a30 commit 3e1b28a
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 8 deletions.
12 changes: 12 additions & 0 deletions eth/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,18 @@ func (d *Data256) RLP() rlp.Value {
}
}

type Hashes []Data32

func (slice Hashes) RLP() rlp.Value {
v := rlp.Value{
List: make([]rlp.Value, len(slice)),
}
for i := range slice {
v.List[i].String = slice[i].String()
}
return v
}

type hasBytes interface {
Bytes() []byte
}
Expand Down
63 changes: 63 additions & 0 deletions eth/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var (
TransactionTypeLegacy = int64(0x0) // TransactionTypeLegacy refers to pre-EIP-2718 transactions.
TransactionTypeAccessList = int64(0x1) // TransactionTypeAccessList refers to EIP-2930 transactions.
TransactionTypeDynamicFee = int64(0x2) // TransactionTypeDynamicFee refers to EIP-1559 transactions.
TransactionTypeBlob = int64(0x3) // TransactionTypeBlob refers to EIP-4844 "blob" transactions.
)

type Transaction struct {
Expand Down Expand Up @@ -52,10 +53,28 @@ type Transaction struct {
// EIP-2930 accessList
AccessList *AccessList `json:"accessList,omitempty"`

// EIP-4844 blob fields
MaxFeePerBlobGas *Quantity `json:"maxFeePerBlobGas,omitempty"`
BlobVersionedHashes Hashes `json:"blobVersionedHashes,omitempty"`

// EIP-4844 Blob transactions in "Network Representation" include the additional
// fields from the BlobsBundleV1 engine API schema. However, these fields are not
// available at the execution layer and thus not expected to be seen when
// dealing with JSONRPC representations of transactions, and are excluded from
// JSON Marshalling. As such, this field is only populated when decoding a
// raw transaction in "Network Representation" and the fields must be accessed directly.
BlobBundle *BlobsBundleV1 `json:"-"`

// Keep the source so we can recreate its expected representation
source string
}

type BlobsBundleV1 struct {
Blobs []Data `json:"blobs"`
Commitments []Data `json:"commitments"`
Proofs []Data `json:"proofs"`
}

type NewPendingTxBodyNotificationParams struct {
Subscription string `json:"subscription"`
Result Transaction `json:"result"`
Expand Down Expand Up @@ -122,6 +141,22 @@ func (t *Transaction) RequiredFields() error {
if t.MaxPriorityFeePerGas == nil {
fields = append(fields, "maxPriorityFeePerGas")
}
case TransactionTypeBlob:
if t.ChainId == nil {
fields = append(fields, "chainId")
}
if t.MaxFeePerBlobGas == nil {
fields = append(fields, "maxFeePerBlobGas")
}
if t.BlobVersionedHashes == nil {
fields = append(fields, "blobVersionedHashes")
}
if t.To == nil {
// Contract creation not supported in blob txs
fields = append(fields, "to")
}
default:
return errors.New("unsupported transaction type")
}

if len(fields) > 0 {
Expand Down Expand Up @@ -217,6 +252,34 @@ func (t *Transaction) RawRepresentation() (*Data, error) {
} else {
return NewData(typePrefix + encodedPayload[2:])
}
case TransactionTypeBlob:
// We introduce a new EIP-2718 transaction, “blob transaction”, where the TransactionType is BLOB_TX_TYPE and the TransactionPayload is the RLP serialization of the following TransactionPayloadBody:
//[chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, max_fee_per_blob_gas, blob_versioned_hashes, y_parity, r, s]
typePrefix, err := t.Type.RLP().Encode()
if err != nil {
return nil, err
}
payload := rlp.Value{List: []rlp.Value{
t.ChainId.RLP(),
t.Nonce.RLP(),
t.MaxPriorityFeePerGas.RLP(),
t.MaxFeePerGas.RLP(),
t.Gas.RLP(),
t.To.RLP(),
t.Value.RLP(),
{String: t.Input.String()},
t.AccessList.RLP(),
t.MaxFeePerBlobGas.RLP(),
t.BlobVersionedHashes.RLP(),
t.YParity.RLP(),
t.R.RLP(),
t.S.RLP(),
}}
if encodedPayload, err := payload.Encode(); err != nil {
return nil, err
} else {
return NewData(typePrefix + encodedPayload[2:])
}
default:
return nil, errors.New("unsupported transaction type")
}
Expand Down
139 changes: 135 additions & 4 deletions eth/transaction_from_raw.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ func (t *Transaction) FromRaw(input string) error {
r Quantity
s Quantity
accessList AccessList
maxFeePerBlobGas Quantity
blobVersionedHashes []Hash
)

if !strings.HasPrefix(input, "0x") {
Expand Down Expand Up @@ -150,6 +152,108 @@ func (t *Transaction) FromRaw(input string) error {
t.Hash = raw.Hash()
t.From = *sender
return nil
case firstByte == byte(TransactionTypeBlob):
// EIP-4844 transaction
var (
body rlp.Value
blobs []Data
commitments []Data
proofs []Data
)
// 0x03 || rlp([tx_payload_body, blobs, commitments, proofs])
payload := "0x" + input[4:]
if err := rlpDecodeList(payload, &body, &blobs, &commitments, &proofs); err != nil {
return err
}
if len(body.List) != 14 {
return errors.New("blob transaction invalid tx body length")
}
// TransactionPayloadBody is itself an RLP list of:
// [chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, max_fee_per_blob_gas, blob_versioned_hashes, y_parity, r, s]
if err := rlpDecodeList(&body, &chainId, &nonce, &maxPriorityFeePerGas, &maxFeePerGas, &gasLimit, &to, &value, &data, &accessList, &maxFeePerBlobGas, &blobVersionedHashes, &v, &r, &s); err != nil {
return errors.Wrap(err, "could not decode RLP components")
}

if len(blobVersionedHashes) != len(blobs) || len(blobs) != len(commitments) || len(commitments) != len(proofs) {
return errors.New("mismatched blob field counts")
}

// TODO: at this point we could verify these two constraints
//
// - blobVersionedHashes[i] = "0x01" + sha256(commitments[i])[4:]
// - the KZG commitments match the corresponding blobs and proofs
//
// However, this all requires additionally implementing a sha256 method
// for eth.Data and the use of a KZG proof framework, both of which
// feel outside the scope of this package, especially considering
// that these fields are not exposed at the JSONRPC layer which is
// our primary focus.
//
// In pseudocode this all would look something like:
//
// for i := range blobVersionedHashes {
// blobHash := commitments[i].Sha256()
// versionedHash := "0x01" + blobHash[4:]
// if blobVersionedHashes[i] != versionedHash {
// return errors.New("incorrect blob versioned hash")
// }
// if err := kzg_verify_proofs(commitments[i], blobs[i], proofs[i]); err != nil {
// return err
// }
// }
//

if r.Int64() == 0 && s.Int64() == 0 {
return errors.New("unsigned transactions not supported")
}

t.Type = OptionalQuantityFromInt(int(firstByte))
t.ChainId = &chainId
t.Nonce = nonce
t.MaxPriorityFeePerGas = &maxPriorityFeePerGas
t.MaxFeePerGas = &maxFeePerGas
t.Gas = gasLimit
t.To = to
t.Value = value
t.Input = data
t.AccessList = &accessList
t.MaxFeePerBlobGas = &maxFeePerBlobGas
t.BlobVersionedHashes = blobVersionedHashes
t.V = v
t.YParity = &v
t.R = r
t.S = s

t.BlobBundle = &BlobsBundleV1{
Blobs: blobs,
Commitments: commitments,
Proofs: proofs,
}

signature, err := NewEIP155Signature(r, s, v)
if err != nil {
return err
}

signingHash, err := t.SigningHash(signature.chainId)
if err != nil {
return err
}

sender, err := signature.Recover(signingHash)
if err != nil {
return err
}

raw, err := t.RawRepresentation()
if err != nil {
return err
}

t.Hash = raw.Hash()
t.From = *sender
return nil

case firstByte > 0x7f:
// In EIP-2718 types larger than 0x7f are reserved since they potentially conflict with legacy RLP encoded
// transactions. As such we can attempt to decode any such transactions as legacy format and attempt to
Expand Down Expand Up @@ -205,6 +309,8 @@ func (t *Transaction) FromRaw(input string) error {
// rlpDecodeList decodes an RLP list into the passed in receivers. Currently only the receiver types needed for
// legacy and EIP-2930 transactions are implemented, new receivers can easily be added in the for loop.
//
// input is either a string or pointer to an rlp.Value, if it's a string then it's assumed to be RLP encoded and is decoded first
//
// Note that when calling this function, the receivers MUST be pointers never values, and for "optional" receivers
// such as Address a pointer to a pointer must be passed. For example:
//
Expand All @@ -215,10 +321,17 @@ func (t *Transaction) FromRaw(input string) error {
// err := rlpDecodeList(payload, &addr, &nonce)
//
// TODO: Consider making this function public once all receiver types in the eth package are supported.
func rlpDecodeList(input string, receivers ...interface{}) error {
decoded, err := rlp.From(input)
if err != nil {
return err
func rlpDecodeList(input interface{}, receivers ...interface{}) error {
var decoded *rlp.Value
switch i := input.(type) {
case string:
if d, err := rlp.From(i); err != nil {
return err
} else {
decoded = d
}
case *rlp.Value:
decoded = i
}

if len(decoded.List) < len(receivers) {
Expand Down Expand Up @@ -250,6 +363,24 @@ func rlpDecodeList(input string, receivers ...interface{}) error {
return errors.Wrapf(err, "could not decode list item %d to Data", i)
}
*receiver = *d
case *[]Data:
*receiver = make([]Data, len(value.List))
for j := range value.List {
d, err := NewData(value.List[j].String)
if err != nil {
return errors.Wrapf(err, "could not decode list item %d %d to Data", i, j)
}
(*receiver)[j] = *d
}
case *[]Data32:
*receiver = make([]Data32, len(value.List))
for j := range value.List {
d, err := NewData32(value.List[j].String)
if err != nil {
return errors.Wrapf(err, "could not decode list item %d %d to Data", i, j)
}
(*receiver)[j] = *d
}
case *rlp.Value:
*receiver = value
case *AccessList:
Expand Down
54 changes: 54 additions & 0 deletions eth/transaction_from_raw_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package eth_test

import (
"encoding/json"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -29,6 +31,32 @@ func TestTransaction_FromRaw(t *testing.T) {
require.True(t, tx.IsProtected())
}

// polygon
func TestTransaction_PolygonFromRaw(t *testing.T) {
input := "0xf88a228523702ac8c182c350944c078868285cf84f5501b64e9008e7da8236711580a47f8661a1000000000000000000000000000000000000000000023b56e96692a478da0000820135a058a0bcf00d4db5a85eca237a1f021236df2af619d1dbda1b1071a08b8ee512f9a000f960dd89b8232792959f5c59a9594acce1c77da320e1532aa36bce31a42159"
tx := eth.Transaction{}
err := tx.FromRaw(input)
require.NoError(t, err)
require.Equal(t, "0xa92d0a000c44a8c548360b5c7e2d940e430dbb091048791315233d6ec656c7d1", tx.Hash.String())
require.Equal(t, eth.MustAddress("0x7f54c0a57b5bcdfef12688a04068e987c1fe31a4").String(), tx.From.String())

b, err := json.MarshalIndent(&tx, "", " ")
require.NoError(t, err)
t.Logf("Decoded tx: \n%s", string(b))

signature, err := tx.Signature()
require.NoError(t, err)
r, s, v := signature.EIP155Values()
require.Equal(t, tx.R, r)
require.Equal(t, tx.S, s)
require.Equal(t, tx.V, v)
chainId, err := signature.ChainId()
require.NoError(t, err)
require.Equal(t, eth.QuantityFromInt64(0x89), *chainId)

require.True(t, tx.IsProtected())
}

func TestTransaction_FromRaw_Invalid_Payloads(t *testing.T) {
{
input := `0x7f00000000`
Expand Down Expand Up @@ -133,6 +161,32 @@ func TestTransaction_FromRawEIP2930(t *testing.T) {
}
}

func TestTransaction_FromRawEIP4844(t *testing.T) {
hash := `0xdc50d6f000e5f46a205c7d84107240d43e10f5c16d2e6ee8035a0da9200dcebe`
// This raw tx was generated from go-ethereum's unit tests. Since the blobs, commitments and proofs are all zero I've "compressed"
// the transaction by eliminating a run of 262148 consecutive zeros, hence the odd way this is constructed to keep the unit test
// payload a reasonable size.
rawBlob := strings.Repeat(`00`, 131072)
raw := (`0x03fa020125f8b7010516058261a894030405000000000000000000000000000000000063b20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00fe1a0010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c44401401a069263c625a202b369d6a507123da6da09913411a798569800ab6acc8ea025fc8a07a55f584d158ece16253c9c4c932d07eb8bdff4870076c9b7e37aecd074e3528fa020004ba020000` +
// the blob is 128KB of zeros
rawBlob +
// and append the empty string commitments and proofs, RLP encoded as single entry lists
strings.Repeat(`f1b0c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`, 2))

tx := eth.Transaction{}
err := tx.FromRaw(raw)
require.NoError(t, err)
require.Equal(t, hash, tx.Hash.String())
require.NotEmpty(t, tx.BlobVersionedHashes)
require.NotNil(t, tx.BlobBundle)
require.Equal(t, len(tx.BlobVersionedHashes), len(tx.BlobBundle.Blobs))
require.Equal(t, len(tx.BlobVersionedHashes), len(tx.BlobBundle.Commitments))
require.Equal(t, len(tx.BlobVersionedHashes), len(tx.BlobBundle.Proofs))
require.Equal(t, rawBlob, tx.BlobBundle.Blobs[0].String()[2:])
require.Equal(t, "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", tx.BlobBundle.Commitments[0].String())
require.Equal(t, "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", tx.BlobBundle.Proofs[0].String())
}

func TestTransaction_FromRawEIP2930_YoloV3_NetherMind(t *testing.T) {
// These sample txs taken from https://github.com/NethermindEth/nethermind/blob/2577b245bdf2d67d02585d2ea2e3c7a02c292ccf/src/Nethermind/Nethermind.Core.Test/Encoding/TxDecoderTests.cs#L78
chainId := eth.MustQuantity("0x796f6c6f7633")
Expand Down
4 changes: 4 additions & 0 deletions eth/transaction_receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ type TransactionReceipt struct {
Root *Data32 `json:"root,omitempty"`
Status *Quantity `json:"status,omitempty"`
EffectiveGasPrice *Quantity `json:"effectiveGasPrice,omitempty"`

// EIP-4844 Receipt Fields
BlobGasPrice *Quantity `json:"blobGasPrice,omitempty"`
BlobGasUsed *Quantity `json:"blobGasUsed,omitempty"`
}

// TransactionType returns the transactions EIP-2718 type, or TransactionTypeLegacy for pre-EIP-2718 transactions.
Expand Down
Loading

0 comments on commit 3e1b28a

Please sign in to comment.