Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions eth/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/json"

"github.com/pkg/errors"

"github.com/INFURA/go-ethlibs/rlp"
)

type Log struct {
Expand All @@ -23,6 +25,20 @@ type Log struct {
Type *string `json:"type,omitempty"`
}

func (l *Log) RLP() rlp.Value {
topics := make([]rlp.Value, len(l.Topics))
for i := range l.Topics {
topics[i] = l.Topics[i].RLP()
}
return rlp.Value{
List: []rlp.Value{
l.Address.RLP(),
{List: topics},
l.Data.RLP(),
},
}
}

type addrOrArray []Address

func (a *addrOrArray) UnmarshalJSON(data []byte) error {
Expand Down
130 changes: 130 additions & 0 deletions eth/transaction_receipt.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package eth

import (
"errors"
"fmt"
"strings"

"github.com/INFURA/go-ethlibs/rlp"
)

type TransactionReceipt struct {
Type *Quantity `json:"type,omitempty"`
TransactionHash Hash `json:"transactionHash"`
Expand Down Expand Up @@ -30,3 +38,125 @@ func (t *TransactionReceipt) TransactionType() int64 {

return t.Type.Int64()
}

// RequiredFields inspects the Transaction Type and returns an error if any required fields are missing
func (t *TransactionReceipt) RequiredFields() error {
var fields []string
switch t.TransactionType() {
case TransactionTypeLegacy:
// LegacyReceipt is rlp([status, cumulativeGasUsed, logsBloom, logs])
// only .Status is a pointer at the moment
// NOTE: pre-EIP-658 receipts include root and not status: rlp([root, cumulativeGasUsed, logsBloom, logs])
if t.Root != nil && t.Status != nil {
return fmt.Errorf("receipt contains both root and status")
}
if t.Root == nil && t.Status == nil {
fields = append(fields, "one of status or root")
}
return nil
case TransactionTypeAccessList:
// The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulativeGasUsed, logsBloom, logs]).
// Same as TransactionTypeLegacy.
if t.Status == nil {
fields = append(fields, "status")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see fields is appended to here & also for TransactionTypeLegacy (line 54) but nil is returned unconditionally, is that intentional?

}
return nil
case TransactionTypeDynamicFee:
// The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulative_transaction_gas_used, logs_bloom, logs]).
// Same as TransactionTypeLegacy.
if t.Status == nil {
fields = append(fields, "status")
}
case TransactionTypeBlob:
// The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulative_transaction_gas_used, logs_bloom, logs])
if t.Status == nil {
fields = append(fields, "status")
}
// NOTE: neither blobGasPrice nor blobGasUsed are actually included in the receipt so should _not_ be checked here
}

if len(fields) > 0 {
return fmt.Errorf("missing required field(s) %s for transaction type", strings.Join(fields, ","))
}

return nil
}

func (t *TransactionReceipt) RawRepresentation() (*Data, error) {
if err := t.RequiredFields(); err != nil {
return nil, err
}

logsRLP := func() rlp.Value {
list := make([]rlp.Value, len(t.Logs))
for i := range t.Logs {
list[i] = t.Logs[i].RLP()
}
return rlp.Value{List: list}
}

switch t.TransactionType() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be a case for TransactionTypeBlob here?

case TransactionTypeLegacy:
// LegacyReceipt is rlp([status, cumulativeGasUsed, logsBloom, logs])
// pre-EIP-658 receipts include root and not status: rlp([root, cumulativeGasUsed, logsBloom, logs])
var message rlp.Value
if t.Status != nil {
message = rlp.Value{List: []rlp.Value{
t.Status.RLP(),
t.CumulativeGasUsed.RLP(),
t.LogsBloom.RLP(),
logsRLP(),
}}
} else if t.Root != nil {
message = rlp.Value{List: []rlp.Value{
t.Root.RLP(),
t.CumulativeGasUsed.RLP(),
t.LogsBloom.RLP(),
logsRLP(),
}}
}
if encoded, err := message.Encode(); err != nil {
return nil, err
} else {
return NewData(encoded)
}
case TransactionTypeAccessList:
// The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulativeGasUsed, logsBloom, logs]).
// Same as TransactionTypeLegacy.
typePrefix, err := t.Type.RLP().Encode()
if err != nil {
return nil, err
}
payload := rlp.Value{List: []rlp.Value{
t.Status.RLP(),
t.CumulativeGasUsed.RLP(),
t.LogsBloom.RLP(),
logsRLP(),
}}
if encodedPayload, err := payload.Encode(); err != nil {
return nil, err
} else {
return NewData(typePrefix + encodedPayload[2:])
}
case TransactionTypeDynamicFee:
// The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulative_transaction_gas_used, logs_bloom, logs]).
// Same as TransactionTypeLegacy.
typePrefix, err := t.Type.RLP().Encode()
if err != nil {
return nil, err
}
payload := rlp.Value{List: []rlp.Value{
t.Status.RLP(),
t.CumulativeGasUsed.RLP(),
t.LogsBloom.RLP(),
logsRLP(),
}}
if encodedPayload, err := payload.Encode(); err != nil {
return nil, err
} else {
return NewData(typePrefix + encodedPayload[2:])
}
default:
return nil, errors.New("unsupported transaction type")
}
}
49 changes: 49 additions & 0 deletions eth/transaction_receipt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,52 @@ func TestTransactionReceipt_4844(t *testing.T) {
require.NoError(t, err)
require.JSONEq(t, raw, string(b))
}

func TestTransactionReceipt_RLP(t *testing.T) {
t.Run("Sepolia 1559 Receipt", func(t *testing.T) {
payload := `{"blockHash":"0x59d58395078eb687813eeade09f4ccd3a40084e607c3b0e0b987794c12be48cc","blockNumber":"0xd0275","contractAddress":null,"cumulativeGasUsed":"0xaebb","effectiveGasPrice":"0x59682f07","from":"0x1b57edab586cbdabd4d914869ae8bb78dbc05571","gasUsed":"0xaebb","logs":[{"address":"0x830bf80a3839b300291915e7c67b70d90823ffed","blockHash":"0x59d58395078eb687813eeade09f4ccd3a40084e607c3b0e0b987794c12be48cc","blockNumber":"0xd0275","data":"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000","logIndex":"0x0","removed":false,"topics":["0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c","0x0000000000000000000000001b57edab586cbdabd4d914869ae8bb78dbc05571"],"transactionHash":"0x30296f5f32972c7c3b39963cfd91073000cb882c294adc2dcf0ac9ca34d67bd2","transactionIndex":"0x0"}],"logsBloom":"0x00800000000000000000000000000000000000000000100000020000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000400000000000000000","status":"0x1","to":"0x830bf80a3839b300291915e7c67b70d90823ffed","transactionHash":"0x30296f5f32972c7c3b39963cfd91073000cb882c294adc2dcf0ac9ca34d67bd2","transactionIndex":"0x0","type":"0x2"}`
receipt := eth.TransactionReceipt{}
err := json.Unmarshal([]byte(payload), &receipt)
require.NoError(t, err)

require.Equal(t, eth.TransactionTypeDynamicFee, receipt.TransactionType())

raw, err := receipt.RawRepresentation()
require.NoError(t, err)
expectedRawRepresentation := `0x02f901850182aebbb9010000800000000000000000000000000000000000000000100000020000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000400000000000000000f87cf87a94830bf80a3839b300291915e7c67b70d90823ffedf842a0e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109ca00000000000000000000000001b57edab586cbdabd4d914869ae8bb78dbc05571a00000000000000000000000000000000000000000000000000de0b6b3a7640000`

require.Equal(t, expectedRawRepresentation, raw.String())

/*
// TODO: we don't have trie hashing support needed to verify the receipts root
t.Run("receipts root", func(t *testing.T) {

// The above transaction was the only one in Sepolia block 0x59d58395078eb687813eeade09f4ccd3a40084e607c3b0e0b987794c12be48cc with the following receipts root
expectedReceiptRoot := `0x7800894d3a17b7f4ce8f17f96740e13696982605164eb4465bdd8a313d0953a5`
})
*/
})
t.Run("Mainnet Pre-Byzantium Receipt", func(t *testing.T) {
payload := `{"blockHash":"0x11d68b50f327f5ebac40b9487cacf4b6c6fb8ddabd852bbddac16dfc2d4ca6a7","blockNumber":"0x22dd9c","contractAddress":null,"cumulativeGasUsed":"0x47b760","effectiveGasPrice":"0x5d21dba00","from":"0x33daedabab9085bd1a94460a652e7ffff592dfe3","gasUsed":"0x47b760","logs":[],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","root":"0xc2b35ef55562a517f53c5a8eab65255b6c68950a7b980d2d0e970190c3528228","to":"0x33ccbd4db4372b8e829343d781e2f949223ff4b1","transactionHash":"0xf94b37f170c234eacc203d683808bba6f671f12d8e87c71d246d4aee03deb579","transactionIndex":"0x0","type":"0x0"}`
receipt := eth.TransactionReceipt{}
err := json.Unmarshal([]byte(payload), &receipt)
require.NoError(t, err)

require.Equal(t, eth.TransactionTypeLegacy, receipt.TransactionType())

raw, err := receipt.RawRepresentation()
require.NoError(t, err)
expectedRawRepresentation := `0xf90129a0c2b35ef55562a517f53c5a8eab65255b6c68950a7b980d2d0e970190c35282288347b760b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0`

require.Equal(t, expectedRawRepresentation, raw.String())

/*
// TODO: we don't have trie hashing support needed to verify the receipts root
t.Run("receipts root", func(t *testing.T) {

// The above transaction was the only one in Mainnet block 0x11d68b50f327f5ebac40b9487cacf4b6c6fb8ddabd852bbddac16dfc2d4ca6a7 with the following receipts root
expectedReceiptRoot := `0x45be296e6c6b0215eeee992909e19e659c224452ae04cb00f7662e65fce81ce1`
})
*/
})
}