From bc0cca3223230e6f21dade2d784901787f379a2b Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Thu, 23 Feb 2023 15:03:32 +1000 Subject: [PATCH] feat(core/types,internal/ethapi): In regolith, record the actual nonce used for deposit transactions The nonce is available as DepositNonce on the receipt and is used to calculate the correct ContractAddress. --- core/state_processor.go | 13 ++- core/types/gen_receipt_json.go | 4 + core/types/receipt.go | 48 +++++++++- core/types/receipt_test.go | 158 ++++++++++++++++++++++++++++++++- internal/ethapi/api.go | 3 + 5 files changed, 220 insertions(+), 6 deletions(-) diff --git a/core/state_processor.go b/core/state_processor.go index 85b81ef95..b0134fb70 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -103,6 +103,11 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, gp *GasPool txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) + nonce := tx.Nonce() + if msg.IsDepositTx() && config.IsOptimismRegolith(evm.Context.Time) { + nonce = statedb.GetNonce(msg.From()) + } + // Apply the transaction to the current state (included in the env). result, err := ApplyMessage(evm, msg, gp) if err != nil { @@ -129,9 +134,15 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, gp *GasPool receipt.TxHash = tx.Hash() receipt.GasUsed = result.UsedGas + if msg.IsDepositTx() && config.IsOptimismRegolith(evm.Context.Time) { + // The actual nonce for deposit transactions is only recorded from Regolith onwards. + // Before the Regolith fork the DepositNonce must remain nil + receipt.DepositNonce = &nonce + } + // If the transaction created a contract, store the creation address in the receipt. if msg.To() == nil { - receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce()) + receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, nonce) } // Set the receipt logs and create the bloom filter. diff --git a/core/types/gen_receipt_json.go b/core/types/gen_receipt_json.go index a506116f3..e476e5ab5 100644 --- a/core/types/gen_receipt_json.go +++ b/core/types/gen_receipt_json.go @@ -72,6 +72,7 @@ func (r *Receipt) UnmarshalJSON(input []byte) error { L1GasUsed *hexutil.Big `json:"l1GasUsed,omitempty"` L1Fee *hexutil.Big `json:"l1Fee,omitempty"` FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` + DepositNonce *hexutil.Uint64 `json:"depositNonce,omitempty"` } var dec Receipt if err := json.Unmarshal(input, &dec); err != nil { @@ -130,5 +131,8 @@ func (r *Receipt) UnmarshalJSON(input []byte) error { if dec.FeeScalar != nil { r.FeeScalar = dec.FeeScalar } + if dec.DepositNonce != nil { + r.DepositNonce = (*uint64)(dec.DepositNonce) + } return nil } diff --git a/core/types/receipt.go b/core/types/receipt.go index d651dc614..832b246c3 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -63,6 +63,9 @@ type Receipt struct { TxHash common.Hash `json:"transactionHash" gencodec:"required"` ContractAddress common.Address `json:"contractAddress"` GasUsed uint64 `json:"gasUsed" gencodec:"required"` + // DepositNonce was introduced in Regolith to store the actual nonce used by deposit transactions + // The state transition process ensures this is only set for Regolith deposit transactions. + DepositNonce *uint64 `json:"depositNonce,omitempty"` // Inclusion information: These fields provide information about the inclusion of the // transaction corresponding to this receipt. @@ -101,11 +104,24 @@ type receiptRLP struct { Logs []*Log } +type depositReceiptRlp struct { + PostStateOrStatus []byte + CumulativeGasUsed uint64 + Bloom Bloom + Logs []*Log + // DepositNonce was introduced in Regolith to store the actual nonce used by deposit transactions. + // Must be nil for any transactions prior to Regolith or that aren't deposit transactions. + DepositNonce *uint64 `rlp:"optional"` +} + // storedReceiptRLP is the storage encoding of a receipt. type storedReceiptRLP struct { PostStateOrStatus []byte CumulativeGasUsed uint64 Logs []*Log + // DepositNonce was introduced in Regolith to store the actual nonce used by deposit transactions. + // Must be nil for any transactions prior to Regolith or that aren't deposit transactions. + DepositNonce *uint64 `rlp:"optional"` } // LegacyOptimismStoredReceiptRLP is the pre bedrock storage encoding of a @@ -209,7 +225,13 @@ func (r *Receipt) EncodeRLP(w io.Writer) error { // encodeTyped writes the canonical encoding of a typed receipt to w. func (r *Receipt) encodeTyped(data *receiptRLP, w *bytes.Buffer) error { w.WriteByte(r.Type) - return rlp.Encode(w, data) + switch r.Type { + case DepositTxType: + withNonce := depositReceiptRlp{data.PostStateOrStatus, data.CumulativeGasUsed, data.Bloom, data.Logs, r.DepositNonce} + return rlp.Encode(w, withNonce) + default: + return rlp.Encode(w, data) + } } // MarshalBinary returns the consensus encoding of the receipt. @@ -271,7 +293,7 @@ func (r *Receipt) decodeTyped(b []byte) error { return errShortTypedReceipt } switch b[0] { - case DynamicFeeTxType, AccessListTxType, DepositTxType: + case DynamicFeeTxType, AccessListTxType: var data receiptRLP err := rlp.DecodeBytes(b[1:], &data) if err != nil { @@ -279,6 +301,15 @@ func (r *Receipt) decodeTyped(b []byte) error { } r.Type = b[0] return r.setFromRLP(data) + case DepositTxType: + var data depositReceiptRlp + err := rlp.DecodeBytes(b[1:], &data) + if err != nil { + return err + } + r.Type = b[0] + r.DepositNonce = data.DepositNonce + return r.setFromRLP(receiptRLP{data.PostStateOrStatus, data.CumulativeGasUsed, data.Bloom, data.Logs}) default: return ErrTxTypeNotSupported } @@ -342,6 +373,9 @@ func (r *ReceiptForStorage) EncodeRLP(_w io.Writer) error { } } w.ListEnd(logList) + if r.DepositNonce != nil { + w.WriteUint64(*r.DepositNonce) + } w.ListEnd(outerList) return w.Flush() } @@ -402,7 +436,9 @@ func decodeStoredReceiptRLP(r *ReceiptForStorage, blob []byte) error { r.CumulativeGasUsed = stored.CumulativeGasUsed r.Logs = stored.Logs r.Bloom = CreateBloom(Receipts{(*Receipt)(r)}) - + if stored.DepositNonce != nil { + r.DepositNonce = stored.DepositNonce + } return nil } @@ -458,7 +494,11 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu if txs[i].To() == nil { // Deriving the signer is expensive, only do if it's actually needed from, _ := Sender(signer, txs[i]) - rs[i].ContractAddress = crypto.CreateAddress(from, txs[i].Nonce()) + nonce := txs[i].Nonce() + if rs[i].DepositNonce != nil { + nonce = *rs[i].DepositNonce + } + rs[i].ContractAddress = crypto.CreateAddress(from, nonce) } // The used gas can be calculated based on previous r if i == 0 { diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go index f44bb80b0..c8f71f55f 100644 --- a/core/types/receipt_test.go +++ b/core/types/receipt_test.go @@ -18,6 +18,7 @@ package types import ( "bytes" + "fmt" "math" "math/big" "reflect" @@ -27,6 +28,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" ) var ( @@ -80,6 +82,42 @@ var ( }, Type: DynamicFeeTxType, } + depositReceiptNoNonce = &Receipt{ + Status: ReceiptStatusFailed, + CumulativeGasUsed: 1, + Logs: []*Log{ + { + Address: common.BytesToAddress([]byte{0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + { + Address: common.BytesToAddress([]byte{0x01, 0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + }, + Type: DepositTxType, + } + nonce = uint64(1234) + depositReceiptWithNonce = &Receipt{ + Status: ReceiptStatusFailed, + CumulativeGasUsed: 1, + DepositNonce: &nonce, + Logs: []*Log{ + { + Address: common.BytesToAddress([]byte{0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + { + Address: common.BytesToAddress([]byte{0x01, 0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + }, + Type: DepositTxType, + } ) func TestDecodeEmptyTypedReceipt(t *testing.T) { @@ -117,7 +155,12 @@ func TestDeriveFields(t *testing.T) { Gas: 3, GasPrice: big.NewInt(3), }), + NewTx(&DepositTx{ + Value: big.NewInt(3), + Gas: 4, + }), } + depNonce := uint64(7) // Create the corresponding receipts receipts := Receipts{ &Receipt{ @@ -154,6 +197,26 @@ func TestDeriveFields(t *testing.T) { ContractAddress: common.BytesToAddress([]byte{0x03, 0x33, 0x33}), GasUsed: 3, }, + &Receipt{ + Type: DepositTxType, + PostState: common.Hash{3}.Bytes(), + CumulativeGasUsed: 10, + Logs: []*Log{ + {Address: common.BytesToAddress([]byte{0x33})}, + {Address: common.BytesToAddress([]byte{0x03, 0x33})}, + }, + TxHash: txs[3].Hash(), + ContractAddress: common.BytesToAddress([]byte{0x03, 0x33, 0x33}), + GasUsed: 4, + DepositNonce: &depNonce, + }, + } + + nonces := []uint64{ + txs[0].Nonce(), + txs[1].Nonce(), + txs[2].Nonce(), + *receipts[3].DepositNonce, // Deposit tx should use deposit nonce } // Clear all the computed fields and re-derive them number := big.NewInt(1) @@ -190,7 +253,7 @@ func TestDeriveFields(t *testing.T) { t.Errorf("receipts[%d].ContractAddress = %s, want %s", i, receipts[i].ContractAddress.String(), (common.Address{}).String()) } from, _ := Sender(signer, txs[i]) - contractAddress := crypto.CreateAddress(from, txs[i].Nonce()) + contractAddress := crypto.CreateAddress(from, nonces[i]) if txs[i].To() == nil && receipts[i].ContractAddress != contractAddress { t.Errorf("receipts[%d].ContractAddress = %s, want %s", i, receipts[i].ContractAddress.String(), contractAddress.String()) } @@ -342,6 +405,38 @@ func TestReceiptUnmarshalBinary(t *testing.T) { } } +func TestBedrockDepositReceiptUnchanged(t *testing.T) { + expectedRlp := common.FromHex("7EF90156A003000000000000000000000000000000000000000000000000000000000000000AB9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F0D7940000000000000000000000000000000000000033C001D7940000000000000000000000000000000000000333C002") + // Deposit receipt with no nonce + receipt := &Receipt{ + Type: DepositTxType, + PostState: common.Hash{3}.Bytes(), + CumulativeGasUsed: 10, + Logs: []*Log{ + {Address: common.BytesToAddress([]byte{0x33}), Data: []byte{1}, Topics: []common.Hash{}}, + {Address: common.BytesToAddress([]byte{0x03, 0x33}), Data: []byte{2}, Topics: []common.Hash{}}, + }, + TxHash: common.Hash{}, + ContractAddress: common.BytesToAddress([]byte{0x03, 0x33, 0x33}), + GasUsed: 4, + } + + rlp, err := receipt.MarshalBinary() + require.NoError(t, err) + require.Equal(t, expectedRlp, rlp) + + // Consensus values should be unchanged after reparsing + parsed := new(Receipt) + err = parsed.UnmarshalBinary(rlp) + require.NoError(t, err) + require.Equal(t, receipt.Status, parsed.Status) + require.Equal(t, receipt.CumulativeGasUsed, parsed.CumulativeGasUsed) + require.Equal(t, receipt.Bloom, parsed.Bloom) + require.EqualValues(t, receipt.Logs, parsed.Logs) + // And still shouldn't have a nonce + require.Nil(t, parsed.DepositNonce) +} + func clearComputedFieldsOnReceipts(t *testing.T, receipts Receipts) { t.Helper() @@ -380,3 +475,64 @@ func clearComputedFieldsOnLog(t *testing.T, log *Log) { log.TxIndex = math.MaxUint32 log.Index = math.MaxUint32 } + +func TestRoundTripReceipt(t *testing.T) { + tests := []struct { + name string + rcpt *Receipt + }{ + {name: "Legacy", rcpt: legacyReceipt}, + {name: "AccessList", rcpt: accessListReceipt}, + {name: "EIP1559", rcpt: eip1559Receipt}, + {name: "DepositNoNonce", rcpt: depositReceiptNoNonce}, + {name: "DepositWithNonce", rcpt: depositReceiptWithNonce}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + data, err := test.rcpt.MarshalBinary() + require.NoError(t, err) + + d := &Receipt{} + err = d.UnmarshalBinary(data) + require.NoError(t, err) + require.Equal(t, test.rcpt, d) + }) + + t.Run(fmt.Sprintf("%sRejectExtraData", test.name), func(t *testing.T) { + data, err := test.rcpt.MarshalBinary() + require.NoError(t, err) + data = append(data, 1, 2, 3, 4) + d := &Receipt{} + err = d.UnmarshalBinary(data) + require.Error(t, err) + }) + } +} + +func TestRoundTripReceiptForStorage(t *testing.T) { + tests := []struct { + name string + rcpt *Receipt + }{ + {name: "Legacy", rcpt: legacyReceipt}, + {name: "AccessList", rcpt: accessListReceipt}, + {name: "EIP1559", rcpt: eip1559Receipt}, + {name: "DepositNoNonce", rcpt: depositReceiptNoNonce}, + {name: "DepositWithNonce", rcpt: depositReceiptWithNonce}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + data, err := rlp.EncodeToBytes((*ReceiptForStorage)(test.rcpt)) + require.NoError(t, err) + + d := &ReceiptForStorage{} + err = rlp.DecodeBytes(data, d) + require.NoError(t, err) + // Only check the stored fields - the others are derived later + require.Equal(t, test.rcpt.Status, d.Status) + require.Equal(t, test.rcpt.CumulativeGasUsed, d.CumulativeGasUsed) + require.Equal(t, test.rcpt.Logs, d.Logs) + require.Equal(t, test.rcpt.DepositNonce, d.DepositNonce) + }) + } +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 8dfc0a32d..a334aa672 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1826,6 +1826,9 @@ func (s *TransactionAPI) GetTransactionReceipt(ctx context.Context, hash common. fields["l1Fee"] = (*hexutil.Big)(receipt.L1Fee) fields["l1FeeScalar"] = receipt.FeeScalar.String() } + if s.b.ChainConfig().Optimism != nil && tx.IsDepositTx() && receipt.DepositNonce != nil { + fields["depositNonce"] = hexutil.Uint64(*receipt.DepositNonce) + } // Assign the effective gas price paid if !s.b.ChainConfig().IsLondon(bigblock) { fields["effectiveGasPrice"] = hexutil.Uint64(tx.GasPrice().Uint64())