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 15, 2023
1 parent dd45a30 commit 9e026b6
Show file tree
Hide file tree
Showing 10 changed files with 674 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

Large diffs are not rendered by default.

134 changes: 134 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 @@ -212,6 +247,105 @@ func (t *Transaction) RawRepresentation() (*Data, error) {
t.R.RLP(),
t.S.RLP(),
}}
if encodedPayload, err := payload.Encode(); err != nil {
return nil, err
} 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")
}
}

// NetworkRepresentation returns the transaction encoded as a raw hexadecimal data string suitable
// for network transmission, or an error. For Blob transactions this includes the blob payload.
func (t *Transaction) NetworkRepresentation() (*Data, error) {
if err := t.RequiredFields(); err != nil {
return nil, err
}

switch t.TransactionType() {
case TransactionTypeLegacy, TransactionTypeAccessList, TransactionTypeDynamicFee:
// For most transaction types, the "Raw" and "Network" representations are the same
return t.RawRepresentation()
case TransactionTypeBlob:
// Blob transactions have two network representations. During transaction gossip responses (PooledTransactions),
// the EIP-2718 TransactionPayload of the blob transaction is wrapped to become:
//
// rlp([tx_payload_body, blobs, commitments, proofs])
typePrefix, err := t.Type.RLP().Encode()
if err != nil {
return nil, err
}

if t.BlobBundle == nil {
return nil, errors.New("network representation of blob txs requires populated blob data")
}

body := 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(),
}}
dataList := func(data []Data) rlp.Value {
v := rlp.Value{
List: make([]rlp.Value, 0, len(data)),
}
for i := range data {
v.List = append(v.List, data[i].RLP())
}
return v
}
blobs := dataList(t.BlobBundle.Blobs)
commitments := dataList(t.BlobBundle.Commitments)
proofs := dataList(t.BlobBundle.Proofs)

payload := rlp.Value{List: []rlp.Value{
body,
blobs,
commitments,
proofs,
}}

if encodedPayload, err := payload.Encode(); err != nil {
return nil, err
} else {
Expand Down
157 changes: 153 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,126 @@ 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
hasBlobs bool = false
)
// The raw tx can be a full "Network Representation" tx of the form:
// 0x03 || rlp([tx_payload_body, blobs, commitments, proofs])
//
// Or just the tx payload body:
// 0x03 || rlp([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])
payload := "0x" + input[4:]
decoded, err := rlp.From(payload)

switch len(decoded.List) {
case 14:
body = *decoded
hasBlobs = false
case 4:
// 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(decoded, &body, &blobs, &commitments, &proofs); err != nil {
return err
}
hasBlobs = true
default:
return errors.New("blob transaction invalid tx RLP length")
}

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 hasBlobs && (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

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

signature, err := NewEIP2718Signature(chainId, 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 +327,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 +339,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 +381,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
Loading

0 comments on commit 9e026b6

Please sign in to comment.