diff --git a/eth/logs.go b/eth/logs.go index 519044f..ed036e5 100644 --- a/eth/logs.go +++ b/eth/logs.go @@ -5,6 +5,8 @@ import ( "encoding/json" "github.com/pkg/errors" + + "github.com/INFURA/go-ethlibs/rlp" ) type Log struct { @@ -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 { diff --git a/eth/transaction_receipt.go b/eth/transaction_receipt.go index 832ef8d..e18d33d 100644 --- a/eth/transaction_receipt.go +++ b/eth/transaction_receipt.go @@ -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"` @@ -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") + } + 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() { + 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") + } +} diff --git a/eth/transaction_receipt_test.go b/eth/transaction_receipt_test.go index a6aaac5..de69256 100644 --- a/eth/transaction_receipt_test.go +++ b/eth/transaction_receipt_test.go @@ -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` + }) + */ + }) +}