Commit d684e5ca authored by Ori Pomerantz's avatar Ori Pomerantz Committed by GitHub

Merge branch 'develop' into qbzzt/atst-docs

parents 388cacdb 1786f370
---
'@eth-optimism/atst': minor
---
Remove broken allowFailures as option
---
'@eth-optimism/atst': minor
---
Move react api to @eth-optimism/atst/react so react isn't required to run the core sdk
---
'@eth-optimism/atst': patch
---
Fixed bug with atst not defaulting to currently connected chain
---
'@eth-optimism/atst': minor
---
Deprecate parseAttestationBytes and createRawKey in favor for createKey, createValue
...@@ -1132,6 +1132,13 @@ workflows: ...@@ -1132,6 +1132,13 @@ workflows:
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
context: context:
- oplabs-gcr - oplabs-gcr
- docker-publish:
name: chain-mon-docker-publish
docker_file: ./ops/docker/Dockerfile.packages
docker_name: chain-mon
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
context:
- oplabs-gcr
- hive-test: - hive-test:
name: hive-test-rpc name: hive-test-rpc
version: <<pipeline.git.revision>> version: <<pipeline.git.revision>>
...@@ -1227,4 +1234,4 @@ workflows: ...@@ -1227,4 +1234,4 @@ workflows:
context: context:
- oplabs-gcr-release - oplabs-gcr-release
requires: requires:
- hold - hold
\ No newline at end of file
...@@ -87,7 +87,7 @@ func (s *channelManager) Clear() { ...@@ -87,7 +87,7 @@ func (s *channelManager) Clear() {
func (s *channelManager) TxFailed(id txID) { func (s *channelManager) TxFailed(id txID) {
if data, ok := s.pendingTransactions[id]; ok { if data, ok := s.pendingTransactions[id]; ok {
s.log.Trace("marked transaction as failed", "id", id) s.log.Trace("marked transaction as failed", "id", id)
s.pendingChannel.PushFrame(id, data) s.pendingChannel.PushFrame(id, data[1:]) // strip the version byte
delete(s.pendingTransactions, id) delete(s.pendingTransactions, id)
} else { } else {
s.log.Warn("unknown transaction marked as failed", "id", id) s.log.Warn("unknown transaction marked as failed", "id", id)
......
...@@ -8,6 +8,8 @@ import ( ...@@ -8,6 +8,8 @@ import (
"os" "os"
"strings" "strings"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/db" "github.com/ethereum-optimism/optimism/op-chain-ops/db"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
...@@ -22,7 +24,6 @@ import ( ...@@ -22,7 +24,6 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/hardhat" "github.com/ethereum-optimism/optimism/op-bindings/hardhat"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli" "github.com/urfave/cli"
...@@ -46,11 +47,6 @@ func main() { ...@@ -46,11 +47,6 @@ func main() {
Usage: "Path to ovm-addresses.json", Usage: "Path to ovm-addresses.json",
Required: true, Required: true,
}, },
&cli.StringFlag{
Name: "evm-addresses",
Usage: "Path to evm-addresses.json",
Required: true,
},
&cli.StringFlag{ &cli.StringFlag{
Name: "ovm-allowances", Name: "ovm-allowances",
Usage: "Path to ovm-allowances.json", Usage: "Path to ovm-allowances.json",
...@@ -62,8 +58,8 @@ func main() { ...@@ -62,8 +58,8 @@ func main() {
Required: true, Required: true,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "evm-messages", Name: "witness-file",
Usage: "Path to evm-messages.json", Usage: "Path to witness file",
Required: true, Required: true,
}, },
&cli.StringFlag{ &cli.StringFlag{
...@@ -118,30 +114,35 @@ func main() { ...@@ -118,30 +114,35 @@ func main() {
return err return err
} }
ovmAddresses, err := migration.NewAddresses(ctx.String("ovm-addresses")) ovmAddresses, err := crossdomain.NewAddresses(ctx.String("ovm-addresses"))
if err != nil { if err != nil {
return err return err
} }
evmAddresess, err := migration.NewAddresses(ctx.String("evm-addresses")) ovmAllowances, err := crossdomain.NewAllowances(ctx.String("ovm-allowances"))
if err != nil { if err != nil {
return err return err
} }
ovmAllowances, err := migration.NewAllowances(ctx.String("ovm-allowances")) ovmMessages, err := crossdomain.NewSentMessageFromJSON(ctx.String("ovm-messages"))
if err != nil { if err != nil {
return err return err
} }
ovmMessages, err := migration.NewSentMessage(ctx.String("ovm-messages")) evmMessages, evmAddresses, err := crossdomain.ReadWitnessData(ctx.String("witness-file"))
if err != nil {
return err
}
evmMessages, err := migration.NewSentMessage(ctx.String("evm-messages"))
if err != nil { if err != nil {
return err return err
} }
migrationData := migration.MigrationData{ log.Info(
"Loaded witness data",
"ovmAddresses", len(ovmAddresses),
"evmAddresses", len(evmAddresses),
"ovmAllowances", len(ovmAllowances),
"ovmMessages", len(ovmMessages),
"evmMessages", len(evmMessages),
)
migrationData := crossdomain.MigrationData{
OvmAddresses: ovmAddresses, OvmAddresses: ovmAddresses,
EvmAddresses: evmAddresess, EvmAddresses: evmAddresses,
OvmAllowances: ovmAllowances, OvmAllowances: ovmAllowances,
OvmMessages: ovmMessages, OvmMessages: ovmMessages,
EvmMessages: evmMessages, EvmMessages: evmMessages,
......
...@@ -18,8 +18,6 @@ import ( ...@@ -18,8 +18,6 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
...@@ -768,7 +766,7 @@ func newWithdrawals(ctx *cli.Context, l1ChainID *big.Int) ([]*crossdomain.Legacy ...@@ -768,7 +766,7 @@ func newWithdrawals(ctx *cli.Context, l1ChainID *big.Int) ([]*crossdomain.Legacy
evmMsgs := ctx.String("evm-messages") evmMsgs := ctx.String("evm-messages")
log.Debug("Migration data", "ovm-path", ovmMsgs, "evm-messages", evmMsgs) log.Debug("Migration data", "ovm-path", ovmMsgs, "evm-messages", evmMsgs)
ovmMessages, err := migration.NewSentMessage(ovmMsgs) ovmMessages, err := crossdomain.NewSentMessageFromJSON(ovmMsgs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -777,20 +775,20 @@ func newWithdrawals(ctx *cli.Context, l1ChainID *big.Int) ([]*crossdomain.Legacy ...@@ -777,20 +775,20 @@ func newWithdrawals(ctx *cli.Context, l1ChainID *big.Int) ([]*crossdomain.Legacy
// committed to in git. // committed to in git.
if l1ChainID.Cmp(common.Big1) != 0 { if l1ChainID.Cmp(common.Big1) != 0 {
log.Info("not using ovm messages because its not mainnet") log.Info("not using ovm messages because its not mainnet")
ovmMessages = []*migration.SentMessage{} ovmMessages = []*crossdomain.SentMessage{}
} }
evmMessages, err := migration.NewSentMessage(evmMsgs) evmMessages, err := crossdomain.NewSentMessageFromJSON(evmMsgs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
migrationData := migration.MigrationData{ migrationData := crossdomain.MigrationData{
OvmMessages: ovmMessages, OvmMessages: ovmMessages,
EvmMessages: evmMessages, EvmMessages: evmMessages,
} }
wds, err := migrationData.ToWithdrawals() wds, _, err := migrationData.ToWithdrawals()
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
package migration package crossdomain
import ( import (
"errors" "errors"
......
package migration package crossdomain
import ( import (
"math/big" "math/big"
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/util"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -12,28 +13,40 @@ import ( ...@@ -12,28 +13,40 @@ import (
var ( var (
ErrUnknownSlotInMessagePasser = errors.New("unknown slot in legacy message passer") ErrUnknownSlotInMessagePasser = errors.New("unknown slot in legacy message passer")
ErrMissingSlotInWitness = errors.New("missing storage slot in witness data") ErrMissingSlotInWitness = errors.New("missing storage slot in witness data (see logs for details)")
) )
// PreCheckWithdrawals checks that the given list of withdrawals represents all withdrawals made // PreCheckWithdrawals checks that the given list of withdrawals represents all withdrawals made
// in the legacy system and filters out any extra withdrawals not included in the legacy system. // in the legacy system and filters out any extra withdrawals not included in the legacy system.
func PreCheckWithdrawals(db *state.StateDB, withdrawals DangerousUnfilteredWithdrawals) (SafeFilteredWithdrawals, error) { func PreCheckWithdrawals(db *state.StateDB, withdrawals DangerousUnfilteredWithdrawals, invalidMessages []InvalidMessage) (SafeFilteredWithdrawals, error) {
// Convert each withdrawal into a storage slot, and build a map of those slots. // Convert each withdrawal into a storage slot, and build a map of those slots.
slotsInp := make(map[common.Hash]*LegacyWithdrawal) validSlotsInp := make(map[common.Hash]*LegacyWithdrawal)
for _, wd := range withdrawals { for _, wd := range withdrawals {
slot, err := wd.StorageSlot() slot, err := wd.StorageSlot()
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot check withdrawals: %w", err) return nil, fmt.Errorf("cannot check withdrawals: %w", err)
} }
slotsInp[slot] = wd validSlotsInp[slot] = wd
}
// Convert each invalid message into a storage slot, and build a map of those slots.
invalidSlotsInp := make(map[common.Hash]InvalidMessage)
for _, msg := range invalidMessages {
slot, err := msg.StorageSlot()
if err != nil {
return nil, fmt.Errorf("cannot check invalid messages: %w", err)
}
invalidSlotsInp[slot] = msg
} }
// Build a mapping of the slots of all messages actually sent in the legacy system. // Build a mapping of the slots of all messages actually sent in the legacy system.
var count int var count int
var innerErr error var innerErr error
slotsAct := make(map[common.Hash]bool) slotsAct := make(map[common.Hash]bool)
progress := util.ProgressLogger(1000, "Iterating legacy messages")
err := db.ForEachStorage(predeploys.LegacyMessagePasserAddr, func(key, value common.Hash) bool { err := db.ForEachStorage(predeploys.LegacyMessagePasserAddr, func(key, value common.Hash) bool {
progress()
// When a message is inserted into the LegacyMessagePasser, it is stored with the value // When a message is inserted into the LegacyMessagePasser, it is stored with the value
// of the ABI encoding of "true". Although there should not be any other storage slots, we // of the ABI encoding of "true". Although there should not be any other storage slots, we
// can safely ignore anything that is not "true". // can safely ignore anything that is not "true".
...@@ -59,24 +72,32 @@ func PreCheckWithdrawals(db *state.StateDB, withdrawals DangerousUnfilteredWithd ...@@ -59,24 +72,32 @@ func PreCheckWithdrawals(db *state.StateDB, withdrawals DangerousUnfilteredWithd
log.Info("Iterated legacy messages", "count", count) log.Info("Iterated legacy messages", "count", count)
// Iterate over the list of actual slots and check that we have an input message for each one. // Iterate over the list of actual slots and check that we have an input message for each one.
var missing int
for slot := range slotsAct { for slot := range slotsAct {
_, ok := slotsInp[slot] _, okValid := validSlotsInp[slot]
if !ok { _, okInvalid := invalidSlotsInp[slot]
return nil, ErrMissingSlotInWitness if !okValid && !okInvalid {
log.Error("missing storage slot", "slot", slot.String())
missing++
} }
} }
if missing > 0 {
log.Error("missing storage slots in witness data", "count", missing)
return nil, ErrMissingSlotInWitness
}
// Iterate over the list of input messages and check that we have a known slot for each one. // Iterate over the list of input messages and check that we have a known slot for each one.
// We'll filter out any extra messages that are not in the legacy system. // We'll filter out any extra messages that are not in the legacy system.
filtered := make(SafeFilteredWithdrawals, 0) filtered := make(SafeFilteredWithdrawals, 0)
for slot := range slotsInp { for slot := range validSlotsInp {
_, ok := slotsAct[slot] _, ok := slotsAct[slot]
if !ok { if !ok {
log.Info("filtering out unknown input message", "slot", slot.String()) log.Info("filtering out unknown input message", "slot", slot.String())
continue continue
} }
wd := slotsInp[slot] wd := validSlotsInp[slot]
if wd.MessageSender != predeploys.L2CrossDomainMessengerAddr { if wd.MessageSender != predeploys.L2CrossDomainMessengerAddr {
log.Info("filtering out message from sender other than the L2XDM", "sender", wd.MessageSender) log.Info("filtering out message from sender other than the L2XDM", "sender", wd.MessageSender)
continue continue
......
...@@ -71,7 +71,7 @@ func TestPreCheckWithdrawals_InvalidSlotInStorage(t *testing.T) { ...@@ -71,7 +71,7 @@ func TestPreCheckWithdrawals_InvalidSlotInStorage(t *testing.T) {
err = stateDB.Database().TrieDB().Commit(root, true) err = stateDB.Database().TrieDB().Commit(root, true)
require.NoError(t, err) require.NoError(t, err)
_, err = PreCheckWithdrawals(stateDB, nil) _, err = PreCheckWithdrawals(stateDB, nil, nil)
require.ErrorIs(t, err, ErrUnknownSlotInMessagePasser) require.ErrorIs(t, err, ErrUnknownSlotInMessagePasser)
} }
...@@ -130,5 +130,5 @@ func runPrecheck(t *testing.T, dbWds []*LegacyWithdrawal, witnessWds []*LegacyWi ...@@ -130,5 +130,5 @@ func runPrecheck(t *testing.T, dbWds []*LegacyWithdrawal, witnessWds []*LegacyWi
err = stateDB.Database().TrieDB().Commit(root, true) err = stateDB.Database().TrieDB().Commit(root, true)
require.NoError(t, err) require.NoError(t, err)
return PreCheckWithdrawals(stateDB, witnessWds) return PreCheckWithdrawals(stateDB, witnessWds, nil)
} }
MSG|0x4200000000000000000000000000000000000007|cafa81dc000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001a4cbd4ece900000000000000000000000099c9fc46f92e8a1c0dec1b1747d010903e884be1000000000000000000000000420000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000019bd000000000000000000000000000000000000000000000000000000000000000e4a9f9e675000000000000000000000000d533a949740bb3306d119cc777fa900ba034cd520000000000000000000000000994206dfe8de6ec6920ff4d779b0d950605fb53000000000000000000000000e3a44dd2a8c108be56a78635121ec914074da16d000000000000000000000000e3a44dd2a8c108be56a78635121ec914074da16d0000000000000000000000000000000000000000000001b0ac98ab3858d7547800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
MSG|0x8B1d477410344785ff1DF52500032E6D5f532EE4|cafa81dc000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030420690000000000000000000000000000000000000000000000000000000000
ETH|0x6340d44c5174588B312F545eEC4a42f8a514eF50
\ No newline at end of file
package crossdomain package crossdomain
import ( import (
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
) )
// DangerousUnfilteredWithdrawals is a list of raw withdrawal witness // DangerousUnfilteredWithdrawals is a list of raw withdrawal witness
...@@ -30,3 +33,35 @@ type WithdrawalMessage interface { ...@@ -30,3 +33,35 @@ type WithdrawalMessage interface {
Hash() (common.Hash, error) Hash() (common.Hash, error)
StorageSlot() (common.Hash, error) StorageSlot() (common.Hash, error)
} }
// InvalidMessage represents a message to the L1 message passer that
// cannot be decoded as a withdrawal. They are defined as a separate
// type in order to completely disambiguate them from any other
// message.
type InvalidMessage SentMessage
func (msg *InvalidMessage) Encode() ([]byte, error) {
out := make([]byte, len(msg.Msg)+20)
copy(out, msg.Msg)
copy(out[len(msg.Msg):], msg.Who.Bytes())
return out, nil
}
func (msg *InvalidMessage) Hash() (common.Hash, error) {
bytes, err := msg.Encode()
if err != nil {
return common.Hash{}, fmt.Errorf("cannot hash: %w", err)
}
return crypto.Keccak256Hash(bytes), nil
}
func (msg *InvalidMessage) StorageSlot() (common.Hash, error) {
hash, err := msg.Hash()
if err != nil {
return common.Hash{}, fmt.Errorf("cannot compute storage slot: %w", err)
}
preimage := make([]byte, 64)
copy(preimage, hash.Bytes())
return crypto.Keccak256Hash(preimage), nil
}
package crossdomain
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestInvalidMessage(t *testing.T) {
tests := []struct {
name string
msg InvalidMessage
slot common.Hash
}{
{
name: "unparseable x-domain message on mainnet",
msg: InvalidMessage{
Who: common.HexToAddress("0x8b1d477410344785ff1df52500032e6d5f532ee4"),
Msg: common.FromHex("0x042069"),
},
slot: common.HexToHash("0x2a49ae6579c3878f10cf87ecdbebc6c4e2b2159ffe2b1af88af6ca9697fc32cb"),
},
{
name: "valid x-domain message on mainnet for validation",
msg: InvalidMessage{
Who: common.HexToAddress("0x4200000000000000000000000000000000000007"),
Msg: common.FromHex("" +
"0xcbd4ece900000000000000000000000099c9fc46f92e8a1c0dec1b1747d01090" +
"3e884be100000000000000000000000042000000000000000000000000000000" +
"0000001000000000000000000000000000000000000000000000000000000000" +
"0000008000000000000000000000000000000000000000000000000000000000" +
"00019be200000000000000000000000000000000000000000000000000000000" +
"000000e4a9f9e675000000000000000000000000a0b86991c6218b36c1d19d4a" +
"2e9eb0ce3606eb480000000000000000000000007f5c764cbc14f9669b88837c" +
"a1490cca17c31607000000000000000000000000a420b2d1c0841415a695b81e" +
"5b867bcd07dff8c9000000000000000000000000c186fa914353c44b2e33ebe0" +
"5f21846f1048beda000000000000000000000000000000000000000000000000" +
"00000000295d681d000000000000000000000000000000000000000000000000" +
"00000000000000c0000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"00000000",
),
},
slot: common.HexToHash("0x8f8f6be7a4c5048f46ca41897181d17c10c39365ead5ac27c23d1e8e466d0ed5"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// StorageSlot() tests Hash() and Encode() so we don't
// need to test these separately.
slot, err := test.msg.StorageSlot()
require.NoError(t, err)
require.Equal(t, test.slot, slot)
})
}
}
package migration package crossdomain
import ( import (
"bufio"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
) )
// SentMessageJSON represents an entry in the JSON file that is created by // SentMessage represents an entry in the JSON file that is created by
// the `migration-data` package. Each entry represents a call to the // the `migration-data` package. Each entry represents a call to the
// `LegacyMessagePasser`. The `who` should always be the // `LegacyMessagePasser`. The `who` should always be the
// `L2CrossDomainMessenger` and the `msg` should be an abi encoded // `L2CrossDomainMessenger` and the `msg` should be an abi encoded
...@@ -20,10 +24,10 @@ type SentMessage struct { ...@@ -20,10 +24,10 @@ type SentMessage struct {
Msg hexutil.Bytes `json:"msg"` Msg hexutil.Bytes `json:"msg"`
} }
// NewSentMessageJSON will read a JSON file from disk given a path to the JSON // NewSentMessageFromJSON will read a JSON file from disk given a path to the JSON
// file. The JSON file this function reads from disk is an output from the // file. The JSON file this function reads from disk is an output from the
// `migration-data` package. // `migration-data` package.
func NewSentMessage(path string) ([]*SentMessage, error) { func NewSentMessageFromJSON(path string) ([]*SentMessage, error) {
file, err := os.ReadFile(path) file, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot find sent message json at %s: %w", path, err) return nil, fmt.Errorf("cannot find sent message json at %s: %w", path, err)
...@@ -37,15 +41,81 @@ func NewSentMessage(path string) ([]*SentMessage, error) { ...@@ -37,15 +41,81 @@ func NewSentMessage(path string) ([]*SentMessage, error) {
return j, nil return j, nil
} }
// ReadWitnessData will read messages and addresses from a raw l2geth state
// dump file.
func ReadWitnessData(path string) ([]*SentMessage, OVMETHAddresses, error) {
f, err := os.Open(path)
if err != nil {
return nil, nil, fmt.Errorf("cannot open witness data file: %w", err)
}
defer f.Close()
scan := bufio.NewScanner(f)
var witnesses []*SentMessage
addresses := make(map[common.Address]bool)
for scan.Scan() {
line := scan.Text()
splits := strings.Split(line, "|")
if len(splits) < 2 {
return nil, nil, fmt.Errorf("invalid line: %s", line)
}
switch splits[0] {
case "MSG":
if len(splits) != 3 {
return nil, nil, fmt.Errorf("invalid line: %s", line)
}
msg := splits[2]
// Make sure that the witness data has a 0x prefix
if !strings.HasPrefix(msg, "0x") {
msg = "0x" + msg
}
abi, err := bindings.LegacyMessagePasserMetaData.GetAbi()
if err != nil {
return nil, nil, fmt.Errorf("failed to get abi: %w", err)
}
msgB := hexutil.MustDecode(msg)
method, err := abi.MethodById(msgB[:4])
if err != nil {
return nil, nil, fmt.Errorf("failed to get method: %w", err)
}
out, err := method.Inputs.Unpack(msgB[4:])
if err != nil {
return nil, nil, fmt.Errorf("failed to unpack: %w", err)
}
cast, ok := out[0].([]byte)
if !ok {
return nil, nil, fmt.Errorf("failed to cast to bytes")
}
witnesses = append(witnesses, &SentMessage{
Who: common.HexToAddress(splits[1]),
Msg: cast,
})
case "ETH":
addresses[common.HexToAddress(splits[1])] = true
default:
return nil, nil, fmt.Errorf("invalid line: %s", line)
}
}
return witnesses, addresses, nil
}
// ToLegacyWithdrawal will convert a SentMessageJSON to a LegacyWithdrawal // ToLegacyWithdrawal will convert a SentMessageJSON to a LegacyWithdrawal
// struct. This is useful because the LegacyWithdrawal struct has helper // struct. This is useful because the LegacyWithdrawal struct has helper
// functions on it that can compute the withdrawal hash and the storage slot. // functions on it that can compute the withdrawal hash and the storage slot.
func (s *SentMessage) ToLegacyWithdrawal() (*crossdomain.LegacyWithdrawal, error) { func (s *SentMessage) ToLegacyWithdrawal() (*LegacyWithdrawal, error) {
data := make([]byte, len(s.Who)+len(s.Msg)) data := make([]byte, len(s.Who)+len(s.Msg))
copy(data, s.Msg) copy(data, s.Msg)
copy(data[len(s.Msg):], s.Who[:]) copy(data[len(s.Msg):], s.Who[:])
var w crossdomain.LegacyWithdrawal var w LegacyWithdrawal
if err := w.Decode(data); err != nil { if err := w.Decode(data); err != nil {
return nil, err return nil, err
} }
...@@ -117,26 +187,26 @@ type MigrationData struct { ...@@ -117,26 +187,26 @@ type MigrationData struct {
EvmMessages []*SentMessage EvmMessages []*SentMessage
} }
func (m *MigrationData) ToWithdrawals() (crossdomain.DangerousUnfilteredWithdrawals, error) { func (m *MigrationData) ToWithdrawals() (DangerousUnfilteredWithdrawals, []InvalidMessage, error) {
messages := make(crossdomain.DangerousUnfilteredWithdrawals, 0) messages := make(DangerousUnfilteredWithdrawals, 0)
invalidMessages := make([]InvalidMessage, 0)
for _, msg := range m.OvmMessages { for _, msg := range m.OvmMessages {
wd, err := msg.ToLegacyWithdrawal() wd, err := msg.ToLegacyWithdrawal()
if err != nil { if err != nil {
return nil, err return nil, nil, fmt.Errorf("error serializing OVM message: %w", err)
} }
messages = append(messages, wd) messages = append(messages, wd)
if err != nil {
return nil, err
}
} }
for _, msg := range m.EvmMessages { for _, msg := range m.EvmMessages {
wd, err := msg.ToLegacyWithdrawal() wd, err := msg.ToLegacyWithdrawal()
if err != nil { if err != nil {
return nil, err log.Warn("Discovered mal-formed withdrawal", "who", msg.Who, "data", msg.Msg)
invalidMessages = append(invalidMessages, InvalidMessage(*msg))
continue
} }
messages = append(messages, wd) messages = append(messages, wd)
} }
return messages, nil return messages, invalidMessages, nil
} }
func (m *MigrationData) Addresses() []common.Address { func (m *MigrationData) Addresses() []common.Address {
......
package crossdomain
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestRead(t *testing.T) {
witnesses, addresses, err := ReadWitnessData("testdata/witness.txt")
require.NoError(t, err)
require.Equal(t, []*SentMessage{
{
Who: common.HexToAddress("0x4200000000000000000000000000000000000007"),
Msg: common.FromHex(
"0xcbd4ece900000000000000000000000099c9fc46f92e8a1c0dec1b1747d01090" +
"3e884be100000000000000000000000042000000000000000000000000000000" +
"0000001000000000000000000000000000000000000000000000000000000000" +
"0000008000000000000000000000000000000000000000000000000000000000" +
"00019bd000000000000000000000000000000000000000000000000000000000" +
"000000e4a9f9e675000000000000000000000000d533a949740bb3306d119cc7" +
"77fa900ba034cd520000000000000000000000000994206dfe8de6ec6920ff4d" +
"779b0d950605fb53000000000000000000000000e3a44dd2a8c108be56a78635" +
"121ec914074da16d000000000000000000000000e3a44dd2a8c108be56a78635" +
"121ec914074da16d0000000000000000000000000000000000000000000001b0" +
"ac98ab3858d75478000000000000000000000000000000000000000000000000" +
"00000000000000c0000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"00000000",
),
},
{
Who: common.HexToAddress("0x8b1d477410344785ff1df52500032e6d5f532ee4"),
Msg: common.FromHex("0x042069"),
},
}, witnesses)
require.Equal(t, OVMETHAddresses{
common.HexToAddress("0x6340d44c5174588B312F545eEC4a42f8a514eF50"): true,
}, addresses)
}
...@@ -8,9 +8,9 @@ import ( ...@@ -8,9 +8,9 @@ import (
"io" "io"
"strings" "strings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
...@@ -105,7 +105,7 @@ func IterateAllowanceList(r io.Reader, cb AllowanceCB) error { ...@@ -105,7 +105,7 @@ func IterateAllowanceList(r io.Reader, cb AllowanceCB) error {
func IterateMintEvents(db ethdb.Database, headNum uint64, cb AddressCBWithHead, progressCb func(uint64)) error { func IterateMintEvents(db ethdb.Database, headNum uint64, cb AddressCBWithHead, progressCb func(uint64)) error {
for headNum > 0 { for headNum > 0 {
hash := rawdb.ReadCanonicalHash(db, headNum) hash := rawdb.ReadCanonicalHash(db, headNum)
receipts, err := migration.ReadLegacyReceipts(db, hash, headNum) receipts, err := crossdomain.ReadLegacyReceipts(db, hash, headNum)
if err != nil { if err != nil {
return err return err
} }
......
...@@ -4,9 +4,10 @@ import ( ...@@ -4,9 +4,10 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration" "github.com/ethereum-optimism/optimism/op-chain-ops/util"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -30,7 +31,7 @@ var ( ...@@ -30,7 +31,7 @@ var (
func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, chainID int, noCheck bool) error { func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, chainID int, noCheck bool) error {
// Chain params to use for integrity checking. // Chain params to use for integrity checking.
params := migration.ParamsByChainID[chainID] params := crossdomain.ParamsByChainID[chainID]
if params == nil { if params == nil {
return fmt.Errorf("no chain params for %d", chainID) return fmt.Errorf("no chain params for %d", chainID)
} }
...@@ -47,7 +48,7 @@ func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, chainID int ...@@ -47,7 +48,7 @@ func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, chainID int
// Migrate the legacy ETH to ETH. // Migrate the legacy ETH to ETH.
log.Info("Migrating legacy ETH to ETH", "num-accounts", len(addresses)) log.Info("Migrating legacy ETH to ETH", "num-accounts", len(addresses))
totalMigrated := new(big.Int) totalMigrated := new(big.Int)
logAccountProgress := ProgressLogger(1000, "imported accounts") logAccountProgress := util.ProgressLogger(1000, "imported accounts")
for addr := range deduped { for addr := range deduped {
// No accounts should have a balance in state. If they do, bail. // No accounts should have a balance in state. If they do, bail.
if db.GetBalance(addr).Sign() > 0 { if db.GetBalance(addr).Sign() > 0 {
......
package ether package ether
import ( import (
"errors"
"fmt" "fmt"
"math/big" "math/big"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration" "github.com/ethereum-optimism/optimism/op-chain-ops/util"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
...@@ -17,9 +19,9 @@ import ( ...@@ -17,9 +19,9 @@ import (
// slots in the LegacyERC20ETH contract. We don't have to filter out extra addresses like we do for // slots in the LegacyERC20ETH contract. We don't have to filter out extra addresses like we do for
// withdrawals because we'll simply carry the balance of a given address to the new system, if the // withdrawals because we'll simply carry the balance of a given address to the new system, if the
// account is extra then it won't have any balance and nothing will happen. // account is extra then it won't have any balance and nothing will happen.
func PreCheckBalances(ldb ethdb.Database, db *state.StateDB, addresses []common.Address, allowances []*migration.Allowance, chainID int, noCheck bool) ([]common.Address, error) { func PreCheckBalances(ldb ethdb.Database, db *state.StateDB, addresses []common.Address, allowances []*crossdomain.Allowance, chainID int, noCheck bool) ([]common.Address, error) {
// Chain params to use for integrity checking. // Chain params to use for integrity checking.
params := migration.ParamsByChainID[chainID] params := crossdomain.ParamsByChainID[chainID]
if params == nil { if params == nil {
return nil, fmt.Errorf("no chain params for %d", chainID) return nil, fmt.Errorf("no chain params for %d", chainID)
} }
...@@ -53,7 +55,10 @@ func PreCheckBalances(ldb ethdb.Database, db *state.StateDB, addresses []common. ...@@ -53,7 +55,10 @@ func PreCheckBalances(ldb ethdb.Database, db *state.StateDB, addresses []common.
// slots that we know we can ignore (totalSupply, name, symbol). // slots that we know we can ignore (totalSupply, name, symbol).
var count int var count int
slotsAct := make(map[common.Hash]common.Hash) slotsAct := make(map[common.Hash]common.Hash)
progress := util.ProgressLogger(1000, "Read OVM_ETH storage slot")
err := db.ForEachStorage(predeploys.LegacyERC20ETHAddr, func(key, value common.Hash) bool { err := db.ForEachStorage(predeploys.LegacyERC20ETHAddr, func(key, value common.Hash) bool {
progress()
// We can safely ignore specific slots (totalSupply, name, symbol). // We can safely ignore specific slots (totalSupply, name, symbol).
if ignoredSlots[key] { if ignoredSlots[key] {
return true return true
...@@ -75,13 +80,16 @@ func PreCheckBalances(ldb ethdb.Database, db *state.StateDB, addresses []common. ...@@ -75,13 +80,16 @@ func PreCheckBalances(ldb ethdb.Database, db *state.StateDB, addresses []common.
// keep track of the total balance to be migrated and throw if the total supply exceeds the // keep track of the total balance to be migrated and throw if the total supply exceeds the
// expected supply delta. // expected supply delta.
totalFound := new(big.Int) totalFound := new(big.Int)
var unknown bool
for slot := range slotsAct { for slot := range slotsAct {
slotType, ok := slotsInp[slot] slotType, ok := slotsInp[slot]
if !ok { if !ok {
if noCheck { if noCheck {
log.Error("ignoring unknown storage slot in state", "slot", slot) log.Error("ignoring unknown storage slot in state", "slot", slot.String())
} else { } else {
log.Crit("unknown storage slot in state: %s", slot) unknown = true
log.Error("unknown storage slot in state", "slot", slot.String())
continue
} }
} }
...@@ -102,6 +110,9 @@ func PreCheckBalances(ldb ethdb.Database, db *state.StateDB, addresses []common. ...@@ -102,6 +110,9 @@ func PreCheckBalances(ldb ethdb.Database, db *state.StateDB, addresses []common.
} }
} }
} }
if unknown {
return nil, errors.New("unknown storage slots in state (see logs for details)")
}
// Verify the supply delta. Recorded total supply in the LegacyERC20ETH contract may be higher // Verify the supply delta. Recorded total supply in the LegacyERC20ETH contract may be higher
// than the actual migrated amount because self-destructs will remove ETH supply in a way that // than the actual migrated amount because self-destructs will remove ETH supply in a way that
......
...@@ -19,7 +19,6 @@ import ( ...@@ -19,7 +19,6 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
) )
...@@ -89,7 +88,7 @@ var ( ...@@ -89,7 +88,7 @@ var (
// PostCheckMigratedDB will check that the migration was performed correctly // PostCheckMigratedDB will check that the migration was performed correctly
func PostCheckMigratedDB( func PostCheckMigratedDB(
ldb ethdb.Database, ldb ethdb.Database,
migrationData migration.MigrationData, migrationData crossdomain.MigrationData,
l1XDM *common.Address, l1XDM *common.Address,
l1ChainID uint64, l1ChainID uint64,
finalSystemOwner common.Address, finalSystemOwner common.Address,
...@@ -468,8 +467,8 @@ func PostCheckL1Block(db vm.StateDB, info *derive.L1BlockInfo) error { ...@@ -468,8 +467,8 @@ func PostCheckL1Block(db vm.StateDB, info *derive.L1BlockInfo) error {
return nil return nil
} }
func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossDomainMessenger *common.Address) error { func CheckWithdrawalsAfter(db vm.StateDB, data crossdomain.MigrationData, l1CrossDomainMessenger *common.Address) error {
wds, err := data.ToWithdrawals() wds, invalidMessages, err := data.ToWithdrawals()
if err != nil { if err != nil {
return err return err
} }
...@@ -479,6 +478,7 @@ func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossD ...@@ -479,6 +478,7 @@ func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossD
// some witness data may references withdrawals that reverted. // some witness data may references withdrawals that reverted.
oldToNewSlots := make(map[common.Hash]common.Hash) oldToNewSlots := make(map[common.Hash]common.Hash)
wdsByOldSlot := make(map[common.Hash]*crossdomain.LegacyWithdrawal) wdsByOldSlot := make(map[common.Hash]*crossdomain.LegacyWithdrawal)
invalidMessagesByOldSlot := make(map[common.Hash]crossdomain.InvalidMessage)
for _, wd := range wds { for _, wd := range wds {
migrated, err := crossdomain.MigrateWithdrawal(wd, l1CrossDomainMessenger) migrated, err := crossdomain.MigrateWithdrawal(wd, l1CrossDomainMessenger)
if err != nil { if err != nil {
...@@ -497,6 +497,15 @@ func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossD ...@@ -497,6 +497,15 @@ func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossD
oldToNewSlots[legacySlot] = migratedSlot oldToNewSlots[legacySlot] = migratedSlot
wdsByOldSlot[legacySlot] = wd wdsByOldSlot[legacySlot] = wd
} }
for _, im := range invalidMessages {
invalidSlot, err := im.StorageSlot()
if err != nil {
return fmt.Errorf("cannot compute legacy storage slot: %w", err)
}
invalidMessagesByOldSlot[invalidSlot] = im
}
log.Info("computed withdrawal storage slots", "migrated", len(oldToNewSlots), "invalid", len(invalidMessagesByOldSlot))
// Now, iterate over each legacy withdrawal and check if there is a corresponding // Now, iterate over each legacy withdrawal and check if there is a corresponding
// migrated withdrawal. // migrated withdrawal.
...@@ -515,6 +524,17 @@ func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossD ...@@ -515,6 +524,17 @@ func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossD
return false return false
} }
// Make sure invalid slots don't get migrated.
_, isInvalidSlot := invalidMessagesByOldSlot[key]
if isInvalidSlot {
value := db.GetState(predeploys.L2ToL1MessagePasserAddr, key)
if value != abiFalse {
innerErr = fmt.Errorf("expected invalid slot not to be migrated, but got %s", value)
return false
}
return true
}
// Grab the migrated slot. // Grab the migrated slot.
migratedSlot := oldToNewSlots[key] migratedSlot := oldToNewSlots[key]
if migratedSlot == (common.Hash{}) { if migratedSlot == (common.Hash{}) {
......
...@@ -8,7 +8,6 @@ import ( ...@@ -8,7 +8,6 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/ether" "github.com/ethereum-optimism/optimism/op-chain-ops/ether"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
...@@ -35,7 +34,7 @@ type MigrationResult struct { ...@@ -35,7 +34,7 @@ type MigrationResult struct {
} }
// MigrateDB will migrate an l2geth legacy Optimism database to a Bedrock database. // MigrateDB will migrate an l2geth legacy Optimism database to a Bedrock database.
func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, migrationData *migration.MigrationData, commit, noCheck bool) (*MigrationResult, error) { func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, migrationData *crossdomain.MigrationData, commit, noCheck bool) (*MigrationResult, error) {
// Grab the hash of the tip of the legacy chain. // Grab the hash of the tip of the legacy chain.
hash := rawdb.ReadHeadHeaderHash(ldb) hash := rawdb.ReadHeadHeaderHash(ldb)
log.Info("Reading chain tip from database", "hash", hash) log.Info("Reading chain tip from database", "hash", hash)
...@@ -114,17 +113,19 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m ...@@ -114,17 +113,19 @@ func MigrateDB(ldb ethdb.Database, config *DeployConfig, l1Block *types.Block, m
// Convert all input messages into legacy messages. Note that this list is not yet filtered and // Convert all input messages into legacy messages. Note that this list is not yet filtered and
// may be missing some messages or have some extra messages. // may be missing some messages or have some extra messages.
unfilteredWithdrawals, err := migrationData.ToWithdrawals() unfilteredWithdrawals, invalidMessages, err := migrationData.ToWithdrawals()
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot serialize withdrawals: %w", err) return nil, fmt.Errorf("cannot serialize withdrawals: %w", err)
} }
log.Info("Read withdrawals from witness data", "unfiltered", len(unfilteredWithdrawals), "invalid", len(invalidMessages))
// We now need to check that we have all of the withdrawals that we expect to have. An error // We now need to check that we have all of the withdrawals that we expect to have. An error
// will be thrown if there are any missing messages, and any extra messages will be removed. // will be thrown if there are any missing messages, and any extra messages will be removed.
var filteredWithdrawals crossdomain.SafeFilteredWithdrawals var filteredWithdrawals crossdomain.SafeFilteredWithdrawals
if !noCheck { if !noCheck {
log.Info("Checking withdrawals...") log.Info("Checking withdrawals...")
filteredWithdrawals, err = crossdomain.PreCheckWithdrawals(db, unfilteredWithdrawals) filteredWithdrawals, err = crossdomain.PreCheckWithdrawals(db, unfilteredWithdrawals, invalidMessages)
if err != nil { if err != nil {
return nil, fmt.Errorf("withdrawals mismatch: %w", err) return nil, fmt.Errorf("withdrawals mismatch: %w", err)
} }
......
...@@ -5,8 +5,9 @@ import ( ...@@ -5,8 +5,9 @@ import (
"math/big" "math/big"
"path/filepath" "path/filepath"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
) )
...@@ -30,28 +31,28 @@ type Config struct { ...@@ -30,28 +31,28 @@ type Config struct {
func Migrate(cfg *Config) (*genesis.MigrationResult, error) { func Migrate(cfg *Config) (*genesis.MigrationResult, error) {
deployConfig := cfg.DeployConfig deployConfig := cfg.DeployConfig
ovmAddresses, err := migration.NewAddresses(cfg.OVMAddressesPath) ovmAddresses, err := crossdomain.NewAddresses(cfg.OVMAddressesPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
evmAddresess, err := migration.NewAddresses(cfg.EVMAddressesPath) evmAddresess, err := crossdomain.NewAddresses(cfg.EVMAddressesPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ovmAllowances, err := migration.NewAllowances(cfg.OVMAllowancesPath) ovmAllowances, err := crossdomain.NewAllowances(cfg.OVMAllowancesPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ovmMessages, err := migration.NewSentMessage(cfg.OVMMessagesPath) ovmMessages, err := crossdomain.NewSentMessageFromJSON(cfg.OVMMessagesPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
evmMessages, err := migration.NewSentMessage(cfg.EVMMessagesPath) evmMessages, err := crossdomain.NewSentMessageFromJSON(cfg.EVMMessagesPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
migrationData := migration.MigrationData{ migrationData := crossdomain.MigrationData{
OvmAddresses: ovmAddresses, OvmAddresses: ovmAddresses,
EvmAddresses: evmAddresess, EvmAddresses: evmAddresess,
OvmAllowances: ovmAllowances, OvmAllowances: ovmAllowances,
......
package ether package util
import ( import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
......
...@@ -77,7 +77,7 @@ func TestBatchInLastPossibleBlocks(gt *testing.T) { ...@@ -77,7 +77,7 @@ func TestBatchInLastPossibleBlocks(gt *testing.T) {
} }
// 8 L1 blocks with 17 L2 blocks is the unsafe state. // 8 L1 blocks with 17 L2 blocks is the unsafe state.
// Because wew consistently batch submitted we are one epoch behind the unsafe head with the safe head // Because we consistently batch submitted we are one epoch behind the unsafe head with the safe head
verifyChainStateOnSequencer(8, 17, 8, 15, 7) verifyChainStateOnSequencer(8, 17, 8, 15, 7)
// Create the batch for L2 blocks 16 & 17 // Create the batch for L2 blocks 16 & 17
......
...@@ -184,42 +184,68 @@ func (s *L1Replica) L1Client(t Testing, cfg *rollup.Config) *sources.L1Client { ...@@ -184,42 +184,68 @@ func (s *L1Replica) L1Client(t Testing, cfg *rollup.Config) *sources.L1Client {
return l1F return l1F
} }
// ActL1FinalizeNext finalizes the next block, which must be marked as safe before doing so (see ActL1SafeNext). func (s *L1Replica) UnsafeNum() uint64 {
func (s *L1Replica) ActL1FinalizeNext(t Testing) { head := s.l1Chain.CurrentBlock()
headNum := uint64(0)
if head != nil {
headNum = head.NumberU64()
}
return headNum
}
func (s *L1Replica) SafeNum() uint64 {
safe := s.l1Chain.CurrentSafeBlock() safe := s.l1Chain.CurrentSafeBlock()
safeNum := uint64(0) safeNum := uint64(0)
if safe != nil { if safe != nil {
safeNum = safe.NumberU64() safeNum = safe.NumberU64()
} }
return safeNum
}
func (s *L1Replica) FinalizedNum() uint64 {
finalized := s.l1Chain.CurrentFinalizedBlock() finalized := s.l1Chain.CurrentFinalizedBlock()
finalizedNum := uint64(0) finalizedNum := uint64(0)
if finalized != nil { if finalized != nil {
finalizedNum = finalized.NumberU64() finalizedNum = finalized.NumberU64()
} }
if safeNum <= finalizedNum { return finalizedNum
}
// ActL1Finalize finalizes a later block, which must be marked as safe before doing so (see ActL1SafeNext).
func (s *L1Replica) ActL1Finalize(t Testing, num uint64) {
safeNum := s.SafeNum()
finalizedNum := s.FinalizedNum()
if safeNum < num {
t.InvalidAction("need to move forward safe block before moving finalized block") t.InvalidAction("need to move forward safe block before moving finalized block")
return return
} }
next := s.l1Chain.GetBlockByNumber(finalizedNum + 1) newFinalized := s.l1Chain.GetBlockByNumber(num)
if next == nil { if newFinalized == nil {
t.Fatalf("expected next block after finalized L1 block %d, safe head is ahead", finalizedNum) t.Fatalf("expected block at %d after finalized L1 block %d, safe head is ahead", num, finalizedNum)
} }
s.l1Chain.SetFinalized(next) s.l1Chain.SetFinalized(newFinalized)
} }
// ActL1SafeNext marks the next unsafe block as safe. // ActL1FinalizeNext finalizes the next block, which must be marked as safe before doing so (see ActL1SafeNext).
func (s *L1Replica) ActL1SafeNext(t Testing) { func (s *L1Replica) ActL1FinalizeNext(t Testing) {
safe := s.l1Chain.CurrentSafeBlock() n := s.FinalizedNum() + 1
safeNum := uint64(0) s.ActL1Finalize(t, n)
if safe != nil { }
safeNum = safe.NumberU64()
} // ActL1Safe marks the given unsafe block as safe.
next := s.l1Chain.GetBlockByNumber(safeNum + 1) func (s *L1Replica) ActL1Safe(t Testing, num uint64) {
if next == nil { newSafe := s.l1Chain.GetBlockByNumber(num)
t.InvalidAction("if head of chain is marked as safe then there's no next block") if newSafe == nil {
t.InvalidAction("could not find L1 block %d, cannot label it as safe", num)
return return
} }
s.l1Chain.SetSafe(next) s.l1Chain.SetSafe(newSafe)
}
// ActL1SafeNext marks the next unsafe block as safe.
func (s *L1Replica) ActL1SafeNext(t Testing) {
n := s.SafeNum() + 1
s.ActL1Safe(t, n)
} }
func (s *L1Replica) Close() error { func (s *L1Replica) Close() error {
......
...@@ -196,6 +196,62 @@ func TestL2Finalization(gt *testing.T) { ...@@ -196,6 +196,62 @@ func TestL2Finalization(gt *testing.T) {
require.Equal(t, heightToSubmit, sequencer.SyncStatus().FinalizedL2.Number, "unknown/bad finalized L1 blocks are ignored") require.Equal(t, heightToSubmit, sequencer.SyncStatus().FinalizedL2.Number, "unknown/bad finalized L1 blocks are ignored")
} }
// TestL2FinalizationWithSparseL1 tests that safe L2 blocks can be finalized even if we do not regularly get a L1 finalization signal
func TestL2FinalizationWithSparseL1(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
miner, engine, sequencer := setupSequencerTest(t, sd, log)
sequencer.ActL2PipelineFull(t)
miner.ActEmptyBlock(t)
sequencer.ActL1HeadSignal(t)
sequencer.ActBuildToL1Head(t)
startStatus := sequencer.SyncStatus()
require.Less(t, startStatus.SafeL2.Number, startStatus.UnsafeL2.Number, "sequencer has unsafe L2 block")
batcher := NewL2Batcher(log, sd.RollupCfg, &BatcherCfg{
MinL1TxSize: 0,
MaxL1TxSize: 128_000,
BatcherKey: dp.Secrets.Batcher,
}, sequencer.RollupClient(), miner.EthClient(), engine.EthClient())
batcher.ActSubmitAll(t)
// include in L1
miner.ActL1StartBlock(12)(t)
miner.ActL1IncludeTx(dp.Addresses.Batcher)(t)
miner.ActL1EndBlock(t)
// Make 2 L1 blocks without batches
miner.ActEmptyBlock(t)
miner.ActEmptyBlock(t)
// See the L1 head, and traverse the pipeline to it
sequencer.ActL1HeadSignal(t)
sequencer.ActL2PipelineFull(t)
updatedStatus := sequencer.SyncStatus()
require.Equal(t, updatedStatus.SafeL2.Number, updatedStatus.UnsafeL2.Number, "unsafe L2 block is now safe")
require.Less(t, updatedStatus.FinalizedL2.Number, updatedStatus.UnsafeL2.Number, "submitted block is not yet finalized")
// Now skip straight to the head with L1 signals (sequencer has traversed the L1 blocks, but they did not have L2 contents)
headL1Num := miner.UnsafeNum()
miner.ActL1Safe(t, headL1Num)
miner.ActL1Finalize(t, headL1Num)
sequencer.ActL1SafeSignal(t)
sequencer.ActL1FinalizedSignal(t)
// Now see if the signals can be processed
sequencer.ActL2PipelineFull(t)
finalStatus := sequencer.SyncStatus()
// Verify the signal was processed, even though we signalled a later L1 block than the one with the batch.
require.Equal(t, finalStatus.FinalizedL2.Number, finalStatus.UnsafeL2.Number, "sequencer submitted its L2 block and it finalized")
}
// TestGarbageBatch tests the behavior of an invalid/malformed output channel frame containing // TestGarbageBatch tests the behavior of an invalid/malformed output channel frame containing
// valid batches being submitted to the batch inbox. These batches should always be rejected // valid batches being submitted to the batch inbox. These batches should always be rejected
// and the safe L2 head should remain unaltered. // and the safe L2 head should remain unaltered.
......
...@@ -4,8 +4,6 @@ import ( ...@@ -4,8 +4,6 @@ import (
"math/big" "math/big"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
...@@ -144,24 +142,19 @@ func TestL2Sequencer_SequencerOnlyReorg(gt *testing.T) { ...@@ -144,24 +142,19 @@ func TestL2Sequencer_SequencerOnlyReorg(gt *testing.T) {
// so it'll keep the L2 block with the old L1 origin, since no conflict is detected. // so it'll keep the L2 block with the old L1 origin, since no conflict is detected.
sequencer.ActL1HeadSignal(t) sequencer.ActL1HeadSignal(t)
sequencer.ActL2PipelineFull(t) sequencer.ActL2PipelineFull(t)
// TODO: CLI-3405 we can detect the inconsistency of the L1 origin of the unsafe L2 head: // Verifier should detect the inconsistency of the L1 origin and reset the pipeline to follow the reorg
// as verifier, there is no need to wait for sequencer to recognize it.
newStatus := sequencer.SyncStatus() newStatus := sequencer.SyncStatus()
require.Equal(t, status.HeadL1.Hash, newStatus.UnsafeL2.L1Origin.Hash, "still have old bad L1 origin") require.Zero(t, newStatus.UnsafeL2.L1Origin.Number, "back to genesis block with good L1 origin, drop old unsafe L2 chain with bad L1 origins")
require.NotEqual(t, status.HeadL1.Hash, newStatus.HeadL1.Hash, "did see the new L1 head change") require.NotEqual(t, status.HeadL1.Hash, newStatus.HeadL1.Hash, "did see the new L1 head change")
require.Equal(t, newStatus.HeadL1.Hash, newStatus.CurrentL1.Hash, "did sync the new L1 head as verifier") require.Equal(t, newStatus.HeadL1.Hash, newStatus.CurrentL1.Hash, "did sync the new L1 head as verifier")
// the block N+1 cannot build on the old N which still refers to the now orphaned L1 origin // the block N+1 cannot build on the old N which still refers to the now orphaned L1 origin
require.Equal(t, status.UnsafeL2.L1Origin.Number, newStatus.HeadL1.Number-1, "seeing N+1 to attempt to build on N") require.Equal(t, status.UnsafeL2.L1Origin.Number, newStatus.HeadL1.Number-1, "seeing N+1 to attempt to build on N")
require.NotEqual(t, status.UnsafeL2.L1Origin.Hash, newStatus.HeadL1.ParentHash, "but N+1 cannot fit on N") require.NotEqual(t, status.UnsafeL2.L1Origin.Hash, newStatus.HeadL1.ParentHash, "but N+1 cannot fit on N")
sequencer.ActL1HeadSignal(t)
// sequence more L2 blocks, until we actually need the next L1 origin // After hitting a reset error, it resets derivation, and drops the old L1 chain
sequencer.ActBuildToL1HeadExclUnsafe(t)
// We expect block building to fail when the next L1 block is not consistent with the existing L1 origin
sequencer.ActL2StartBlockCheckErr(t, derive.ErrReset)
// After hitting a reset error, it reset derivation, and drops the old L1 chain
sequencer.ActL2PipelineFull(t) sequencer.ActL2PipelineFull(t)
require.Zero(t, sequencer.SyncStatus().UnsafeL2.L1Origin.Number, "back to genesis block with good L1 origin, drop old unsafe L2 chain with bad L1 origins")
// Can build new L2 blocks with good L1 origin // Can build new L2 blocks with good L1 origin
sequencer.ActBuildToL1HeadUnsafe(t) sequencer.ActBuildToL1HeadUnsafe(t)
require.Equal(t, newStatus.HeadL1.Hash, sequencer.SyncStatus().UnsafeL2.L1Origin.Hash, "build L2 chain with new correct L1 origins") require.Equal(t, newStatus.HeadL1.Hash, sequencer.SyncStatus().UnsafeL2.L1Origin.Hash, "build L2 chain with new correct L1 origins")
......
...@@ -464,7 +464,7 @@ func (cfg SystemConfig) Start() (*System, error) { ...@@ -464,7 +464,7 @@ func (cfg SystemConfig) Start() (*System, error) {
c.P2P = p c.P2P = p
if c.Driver.SequencerEnabled { if c.Driver.SequencerEnabled {
c.P2PSigner = &p2p.PreparedSigner{Signer: p2p.NewLegacyLocalSigner(cfg.Secrets.SequencerP2P)} c.P2PSigner = &p2p.PreparedSigner{Signer: p2p.NewLocalSigner(cfg.Secrets.SequencerP2P)}
} }
} }
......
package fetch
import (
"context"
"encoding/json"
"fmt"
"log"
"math/big"
"os"
"path"
"time"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
type TransactionWithMeta struct {
TxIndex uint64 `json:"tx_index"`
InboxAddr common.Address `json:"inbox_address"`
BlockNumber uint64 `json:"block_number"`
BlockHash common.Hash `json:"block_hash"`
ChainId uint64 `json:"chain_id"`
Sender common.Address `json:"sender"`
ValidSender bool `json:"valid_sender"`
Frames []derive.Frame `json:"frames"`
FrameErr string `json:"frame_parse_error"`
ValidFrames bool `json:"valid_data"`
Tx *types.Transaction `json:"tx"`
}
type Config struct {
Start, End uint64
ChainID *big.Int
BatchInbox common.Address
BatchSenders map[common.Address]struct{}
OutDirectory string
}
func Batches(client *ethclient.Client, config Config) (totalValid, totalInvalid int) {
if err := os.MkdirAll(config.OutDirectory, 0750); err != nil {
log.Fatal(err)
}
number := new(big.Int).SetUint64(config.Start)
signer := types.LatestSignerForChainID(config.ChainID)
for i := config.Start; i < config.End; i++ {
valid, invalid := fetchBatchesPerBlock(client, number, signer, config)
totalValid += valid
totalInvalid += invalid
number = number.Add(number, common.Big1)
}
return
}
func fetchBatchesPerBlock(client *ethclient.Client, number *big.Int, signer types.Signer, config Config) (validBatchCount, invalidBatchCount int) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
block, err := client.BlockByNumber(ctx, number)
if err != nil {
log.Fatal(err)
}
for i, tx := range block.Transactions() {
if tx.To() != nil && *tx.To() == config.BatchInbox {
sender, err := signer.Sender(tx)
if err != nil {
log.Fatal(err)
}
validSender := true
if _, ok := config.BatchSenders[sender]; !ok {
fmt.Printf("Found a transaction (%s) from an invalid sender (%s)\n", tx.Hash().String(), sender.String())
invalidBatchCount += 1
validSender = false
}
validFrames := true
frameError := ""
frames, err := derive.ParseFrames(tx.Data())
if err != nil {
fmt.Printf("Found a transaction (%s) with invalid data: %v\n", tx.Hash().String(), err)
validFrames = false
frameError = err.Error()
}
if validSender && validFrames {
validBatchCount += 1
} else {
invalidBatchCount += 1
}
txm := &TransactionWithMeta{
Tx: tx,
Sender: sender,
ValidSender: validSender,
TxIndex: uint64(i),
BlockNumber: block.NumberU64(),
BlockHash: block.Hash(),
ChainId: config.ChainID.Uint64(),
InboxAddr: config.BatchInbox,
Frames: frames,
FrameErr: frameError,
ValidFrames: validFrames,
}
filename := path.Join(config.OutDirectory, fmt.Sprintf("%s.json", tx.Hash().String()))
file, err := os.Create(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
enc := json.NewEncoder(file)
if err := enc.Encode(txm); err != nil {
log.Fatal(err)
}
}
}
return
}
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/ethereum-optimism/optimism/op-node/cmd/batch_decoder/fetch"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "batch-decoder"
app.Usage = "Optimism Batch Decoding Utility"
app.Commands = []cli.Command{
{
Name: "fetch",
Usage: "Fetches batches in the specified range",
Flags: []cli.Flag{
cli.IntFlag{
Name: "start",
Required: true,
Usage: "First block (inclusive) to fetch",
},
cli.IntFlag{
Name: "end",
Required: true,
Usage: "Last block (exclusive) to fetch",
},
cli.StringFlag{
Name: "inbox",
Required: true,
Usage: "Batch Inbox Address",
},
cli.StringFlag{
Name: "sender",
Required: true,
Usage: "Batch Sender Address",
},
cli.StringFlag{
Name: "out",
Value: "/tmp/batch_decoder/transactions_cache",
Usage: "Cache directory for the found transactions",
},
cli.StringFlag{
Name: "l1",
Required: true,
Usage: "L1 RPC URL",
EnvVar: "L1_RPC",
},
},
Action: func(cliCtx *cli.Context) error {
client, err := ethclient.Dial(cliCtx.String("l1"))
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
chainID, err := client.ChainID(ctx)
if err != nil {
log.Fatal(err)
}
config := fetch.Config{
Start: uint64(cliCtx.Int("start")),
End: uint64(cliCtx.Int("end")),
ChainID: chainID,
BatchSenders: map[common.Address]struct{}{
common.HexToAddress(cliCtx.String("sender")): struct{}{},
},
BatchInbox: common.HexToAddress(cliCtx.String("inbox")),
OutDirectory: cliCtx.String("out"),
}
totalValid, totalInvalid := fetch.Batches(client, config)
fmt.Printf("Fetched batches in range [%v,%v). Found %v valid & %v invalid batches\n", config.Start, config.End, totalValid, totalInvalid)
fmt.Printf("Fetch Config: Chain ID: %v. Inbox Address: %v. Valid Senders: %v.\n", config.ChainID, config.BatchInbox, config.BatchSenders)
fmt.Printf("Wrote transactions with batches to %v\n", config.OutDirectory)
return nil
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
...@@ -19,6 +19,7 @@ import ( ...@@ -19,6 +19,7 @@ import (
"time" "time"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
ophttp "github.com/ethereum-optimism/optimism/op-node/http"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -161,7 +162,8 @@ func runServer() { ...@@ -161,7 +162,8 @@ func runServer() {
mux.HandleFunc("/logs", makeGzipHandler(logsHandler)) mux.HandleFunc("/logs", makeGzipHandler(logsHandler))
log.Info("running webserver...") log.Info("running webserver...")
if err := http.Serve(l, mux); err != nil && !errors.Is(err, http.ErrServerClosed) { httpServer := ophttp.NewHttpServer(mux)
if err := httpServer.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Crit("http server failed", "message", err) log.Crit("http server failed", "message", err)
} }
} }
......
package http
import (
"net/http"
"github.com/ethereum/go-ethereum/rpc"
)
// Use default timeouts from Geth as battle tested default values
var timeouts = rpc.DefaultHTTPTimeouts
func NewHttpServer(handler http.Handler) *http.Server {
return &http.Server{
Handler: handler,
ReadTimeout: timeouts.ReadTimeout,
ReadHeaderTimeout: timeouts.ReadHeaderTimeout,
WriteTimeout: timeouts.WriteTimeout,
IdleTimeout: timeouts.IdleTimeout,
}
}
...@@ -7,10 +7,10 @@ import ( ...@@ -7,10 +7,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/http"
"strconv" "strconv"
"time" "time"
ophttp "github.com/ethereum-optimism/optimism/op-node/http"
"github.com/ethereum-optimism/optimism/op-service/metrics" "github.com/ethereum-optimism/optimism/op-service/metrics"
pb "github.com/libp2p/go-libp2p-pubsub/pb" pb "github.com/libp2p/go-libp2p-pubsub/pb"
...@@ -528,12 +528,10 @@ func (m *Metrics) RecordSequencerSealingTime(duration time.Duration) { ...@@ -528,12 +528,10 @@ func (m *Metrics) RecordSequencerSealingTime(duration time.Duration) {
// The server will be closed when the passed-in context is cancelled. // The server will be closed when the passed-in context is cancelled.
func (m *Metrics) Serve(ctx context.Context, hostname string, port int) error { func (m *Metrics) Serve(ctx context.Context, hostname string, port int) error {
addr := net.JoinHostPort(hostname, strconv.Itoa(port)) addr := net.JoinHostPort(hostname, strconv.Itoa(port))
server := &http.Server{ server := ophttp.NewHttpServer(promhttp.InstrumentMetricHandler(
Addr: addr, m.registry, promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}),
Handler: promhttp.InstrumentMetricHandler( ))
m.registry, promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}), server.Addr = addr
),
}
go func() { go func() {
<-ctx.Done() <-ctx.Done()
server.Close() server.Close()
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
ophttp "github.com/ethereum-optimism/optimism/op-node/http"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
...@@ -87,7 +88,7 @@ func (s *rpcServer) Start() error { ...@@ -87,7 +88,7 @@ func (s *rpcServer) Start() error {
} }
s.listenAddr = listener.Addr() s.listenAddr = listener.Addr()
s.httpServer = &http.Server{Handler: mux} s.httpServer = ophttp.NewHttpServer(mux)
go func() { go func() {
if err := s.httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { // todo improve error handling if err := s.httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { // todo improve error handling
s.log.Error("http server failed", "err", err) s.log.Error("http server failed", "err", err)
......
...@@ -24,7 +24,7 @@ func LoadSignerSetup(ctx *cli.Context) (p2p.SignerSetup, error) { ...@@ -24,7 +24,7 @@ func LoadSignerSetup(ctx *cli.Context) (p2p.SignerSetup, error) {
return nil, fmt.Errorf("failed to read batch submitter key: %w", err) return nil, fmt.Errorf("failed to read batch submitter key: %w", err)
} }
return &p2p.PreparedSigner{Signer: p2p.NewLegacyLocalSigner(priv)}, nil return &p2p.PreparedSigner{Signer: p2p.NewLocalSigner(priv)}, nil
} }
// TODO: create remote signer // TODO: create remote signer
......
...@@ -49,7 +49,7 @@ func TestVerifyBlockSignature(t *testing.T) { ...@@ -49,7 +49,7 @@ func TestVerifyBlockSignature(t *testing.T) {
}{ }{
{ {
name: "Legacy", name: "Legacy",
newSigner: NewLegacyLocalSigner, newSigner: newLegacyLocalSigner,
}, },
{ {
name: "Updated", name: "Updated",
...@@ -102,3 +102,7 @@ func TestVerifyBlockSignature(t *testing.T) { ...@@ -102,3 +102,7 @@ func TestVerifyBlockSignature(t *testing.T) {
}) })
} }
} }
func newLegacyLocalSigner(priv *ecdsa.PrivateKey) *LocalSigner {
return &LocalSigner{priv: priv, hasher: LegacySigningHash}
}
...@@ -315,7 +315,7 @@ func TestDiscovery(t *testing.T) { ...@@ -315,7 +315,7 @@ func TestDiscovery(t *testing.T) {
// B and C don't know each other yet, but both have A as a bootnode. // B and C don't know each other yet, but both have A as a bootnode.
// It should only be a matter of time for them to connect, if they discover each other via A. // It should only be a matter of time for them to connect, if they discover each other via A.
timeout := time.After(time.Second * 10) timeout := time.After(time.Second * 60)
var peersOfB []peer.ID var peersOfB []peer.ID
// B should be connected to the bootnode (A) it used (it's a valid optimism node to connect to here) // B should be connected to the bootnode (A) it used (it's a valid optimism node to connect to here)
// C should also be connected, although this one might take more time to discover // C should also be connected, although this one might take more time to discover
......
...@@ -64,10 +64,6 @@ type LocalSigner struct { ...@@ -64,10 +64,6 @@ type LocalSigner struct {
hasher func(domain [32]byte, chainID *big.Int, payloadBytes []byte) (common.Hash, error) hasher func(domain [32]byte, chainID *big.Int, payloadBytes []byte) (common.Hash, error)
} }
func NewLegacyLocalSigner(priv *ecdsa.PrivateKey) *LocalSigner {
return &LocalSigner{priv: priv, hasher: LegacySigningHash}
}
func NewLocalSigner(priv *ecdsa.PrivateKey) *LocalSigner { func NewLocalSigner(priv *ecdsa.PrivateKey) *LocalSigner {
return &LocalSigner{priv: priv, hasher: SigningHash} return &LocalSigner{priv: priv, hasher: SigningHash}
} }
......
...@@ -224,7 +224,13 @@ func (eq *EngineQueue) Step(ctx context.Context) error { ...@@ -224,7 +224,13 @@ func (eq *EngineQueue) Step(ctx context.Context) error {
} }
outOfData := false outOfData := false
if len(eq.safeAttributes) == 0 { if len(eq.safeAttributes) == 0 {
eq.origin = eq.prev.Origin() newOrigin := eq.prev.Origin()
// Check if the L2 unsafe head origin is consistent with the new origin
if err := eq.verifyNewL1Origin(ctx, newOrigin); err != nil {
return err
}
eq.origin = newOrigin
eq.postProcessSafeL2() // make sure we track the last L2 safe head for every new L1 block
if next, err := eq.prev.NextAttributes(ctx, eq.safeHead); err == io.EOF { if next, err := eq.prev.NextAttributes(ctx, eq.safeHead); err == io.EOF {
outOfData = true outOfData = true
} else if err != nil { } else if err != nil {
...@@ -245,6 +251,38 @@ func (eq *EngineQueue) Step(ctx context.Context) error { ...@@ -245,6 +251,38 @@ func (eq *EngineQueue) Step(ctx context.Context) error {
} }
} }
// verifyNewL1Origin checks that the L2 unsafe head still has a L1 origin that is on the canonical chain.
// If the unsafe head origin is after the new L1 origin it is assumed to still be canonical.
// The check is only required when moving to a new L1 origin.
func (eq *EngineQueue) verifyNewL1Origin(ctx context.Context, newOrigin eth.L1BlockRef) error {
if newOrigin == eq.origin {
return nil
}
unsafeOrigin := eq.unsafeHead.L1Origin
if newOrigin.Number == unsafeOrigin.Number && newOrigin.ID() != unsafeOrigin {
return NewResetError(fmt.Errorf("l1 origin was inconsistent with l2 unsafe head origin, need reset to resolve: l1 origin: %v; unsafe origin: %v",
newOrigin.ID(), unsafeOrigin))
}
// Avoid requesting an older block by checking against the parent hash
if newOrigin.Number == unsafeOrigin.Number+1 && newOrigin.ParentHash != unsafeOrigin.Hash {
return NewResetError(fmt.Errorf("l2 unsafe head origin is no longer canonical, need reset to resolve: canonical hash: %v; unsafe origin hash: %v",
newOrigin.ParentHash, unsafeOrigin.Hash))
}
if newOrigin.Number > unsafeOrigin.Number+1 {
// If unsafe origin is further behind new origin, check it's still on the canonical chain.
canonical, err := eq.l1Fetcher.L1BlockRefByNumber(ctx, unsafeOrigin.Number)
if err != nil {
return NewTemporaryError(fmt.Errorf("failed to fetch canonical L1 block at slot: %v; err: %w", unsafeOrigin.Number, err))
}
if canonical.ID() != unsafeOrigin {
eq.log.Error("Resetting due to origin mismatch")
return NewResetError(fmt.Errorf("l2 unsafe head origin is no longer canonical, need reset to resolve: canonical: %v; unsafe origin: %v",
canonical, unsafeOrigin))
}
}
return nil
}
// tryFinalizeL2 traverses the past L1 blocks, checks if any has been finalized, // tryFinalizeL2 traverses the past L1 blocks, checks if any has been finalized,
// and then marks the latest fully derived L2 block from this as finalized, // and then marks the latest fully derived L2 block from this as finalized,
// or defaults to the current finalized L2 block. // or defaults to the current finalized L2 block.
...@@ -279,9 +317,15 @@ func (eq *EngineQueue) postProcessSafeL2() { ...@@ -279,9 +317,15 @@ func (eq *EngineQueue) postProcessSafeL2() {
L2Block: eq.safeHead, L2Block: eq.safeHead,
L1Block: eq.origin.ID(), L1Block: eq.origin.ID(),
}) })
last := &eq.finalityData[len(eq.finalityData)-1]
eq.log.Debug("extended finality-data", "last_l1", last.L1Block, "last_l2", last.L2Block)
} else { } else {
// if it's a now L2 block that was derived from the same latest L1 block, then just update the entry // if it's a new L2 block that was derived from the same latest L1 block, then just update the entry
eq.finalityData[len(eq.finalityData)-1].L2Block = eq.safeHead last := &eq.finalityData[len(eq.finalityData)-1]
if last.L2Block != eq.safeHead { // avoid logging if there are no changes
last.L2Block = eq.safeHead
eq.log.Debug("updated finality-data", "last_l1", last.L1Block, "last_l2", last.L2Block)
}
} }
} }
......
This diff is collapsed.
package derive package derive
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
) )
...@@ -44,3 +45,7 @@ func (id ChannelID) String() string { ...@@ -44,3 +45,7 @@ func (id ChannelID) String() string {
func (id ChannelID) TerminalString() string { func (id ChannelID) TerminalString() string {
return fmt.Sprintf("%x..%x", id[:3], id[13:]) return fmt.Sprintf("%x..%x", id[:3], id[13:])
} }
func (id ChannelID) MarshalJSON() ([]byte, error) {
return json.Marshal(id.String())
}
package crypto package crypto
import ( import (
"bytes"
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"errors" "errors"
...@@ -56,10 +57,10 @@ func SignerFactoryFromConfig(l log.Logger, privateKey, mnemonic, hdPath string, ...@@ -56,10 +57,10 @@ func SignerFactoryFromConfig(l log.Logger, privateKey, mnemonic, hdPath string,
fromAddress = common.HexToAddress(signerConfig.Address) fromAddress = common.HexToAddress(signerConfig.Address)
signer = func(chainID *big.Int) SignerFn { signer = func(chainID *big.Int) SignerFn {
return func(ctx context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) { return func(ctx context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) {
if address.String() != signerConfig.Address { if !bytes.Equal(address[:], fromAddress[:]) {
return nil, fmt.Errorf("attempting to sign for %s, expected %s: ", address, signerConfig.Address) return nil, fmt.Errorf("attempting to sign for %s, expected %s: ", address, signerConfig.Address)
} }
return signerClient.SignTransaction(ctx, chainID, tx) return signerClient.SignTransaction(ctx, chainID, address, tx)
} }
} }
} else { } else {
......
...@@ -122,6 +122,11 @@ func (m *SimpleTxManager) IncreaseGasPrice(ctx context.Context, tx *types.Transa ...@@ -122,6 +122,11 @@ func (m *SimpleTxManager) IncreaseGasPrice(ctx context.Context, tx *types.Transa
gasTipCap = tip gasTipCap = tip
} }
// Return the same transaction if we don't update any fields.
// We do this because ethereum signatures are not deterministic and therefore the transaction hash will change
// when we re-sign the tx. We don't want to do that because we want to see ErrAlreadyKnown instead of ErrReplacementUnderpriced
var reusedTip, reusedFeeCap bool
// new = old * (100 + priceBump) / 100 // new = old * (100 + priceBump) / 100
// Enforce a min priceBump on the tip. Do this before the feeCap is calculated // Enforce a min priceBump on the tip. Do this before the feeCap is calculated
thresholdTip := new(big.Int).Mul(priceBumpPercent, tx.GasTipCap()) thresholdTip := new(big.Int).Mul(priceBumpPercent, tx.GasTipCap())
...@@ -129,6 +134,7 @@ func (m *SimpleTxManager) IncreaseGasPrice(ctx context.Context, tx *types.Transa ...@@ -129,6 +134,7 @@ func (m *SimpleTxManager) IncreaseGasPrice(ctx context.Context, tx *types.Transa
if tx.GasTipCapIntCmp(gasTipCap) >= 0 { if tx.GasTipCapIntCmp(gasTipCap) >= 0 {
m.l.Debug("Reusing the previous tip", "previous", tx.GasTipCap(), "suggested", gasTipCap) m.l.Debug("Reusing the previous tip", "previous", tx.GasTipCap(), "suggested", gasTipCap)
gasTipCap = tx.GasTipCap() gasTipCap = tx.GasTipCap()
reusedTip = true
} else if thresholdTip.Cmp(gasTipCap) > 0 { } else if thresholdTip.Cmp(gasTipCap) > 0 {
m.l.Debug("Overriding the tip to enforce a price bump", "previous", tx.GasTipCap(), "suggested", gasTipCap, "new", thresholdTip) m.l.Debug("Overriding the tip to enforce a price bump", "previous", tx.GasTipCap(), "suggested", gasTipCap, "new", thresholdTip)
gasTipCap = thresholdTip gasTipCap = thresholdTip
...@@ -150,11 +156,16 @@ func (m *SimpleTxManager) IncreaseGasPrice(ctx context.Context, tx *types.Transa ...@@ -150,11 +156,16 @@ func (m *SimpleTxManager) IncreaseGasPrice(ctx context.Context, tx *types.Transa
if tx.GasFeeCapIntCmp(gasFeeCap) >= 0 { if tx.GasFeeCapIntCmp(gasFeeCap) >= 0 {
m.l.Debug("Reusing the previous fee cap", "previous", tx.GasFeeCap(), "suggested", gasFeeCap) m.l.Debug("Reusing the previous fee cap", "previous", tx.GasFeeCap(), "suggested", gasFeeCap)
gasFeeCap = tx.GasFeeCap() gasFeeCap = tx.GasFeeCap()
reusedFeeCap = true
} else if thresholdFeeCap.Cmp(gasFeeCap) > 0 { } else if thresholdFeeCap.Cmp(gasFeeCap) > 0 {
m.l.Debug("Overriding the fee cap to enforce a price bump", "previous", tx.GasFeeCap(), "suggested", gasFeeCap, "new", thresholdFeeCap) m.l.Debug("Overriding the fee cap to enforce a price bump", "previous", tx.GasFeeCap(), "suggested", gasFeeCap, "new", thresholdFeeCap)
gasFeeCap = thresholdFeeCap gasFeeCap = thresholdFeeCap
} }
if reusedTip && reusedFeeCap {
return tx, nil
}
rawTx := &types.DynamicFeeTx{ rawTx := &types.DynamicFeeTx{
ChainID: tx.ChainId(), ChainID: tx.ChainId(),
Nonce: tx.Nonce(), Nonce: tx.Nonce(),
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"math/big" "math/big"
"math/rand"
"sync" "sync"
"testing" "testing"
"time" "time"
...@@ -11,9 +12,12 @@ import ( ...@@ -11,9 +12,12 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-node/testutils"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -727,3 +731,42 @@ func TestIncreaseGasPriceUseLargeIncrease(t *testing.T) { ...@@ -727,3 +731,42 @@ func TestIncreaseGasPriceUseLargeIncrease(t *testing.T) {
require.True(t, newTx.GasFeeCap().Cmp(feeCap) == 0, "new tx fee cap must be equal L1") require.True(t, newTx.GasFeeCap().Cmp(feeCap) == 0, "new tx fee cap must be equal L1")
require.True(t, newTx.GasTipCap().Cmp(borkedBackend.gasTip) == 0, "new tx tip must be equal L1") require.True(t, newTx.GasTipCap().Cmp(borkedBackend.gasTip) == 0, "new tx tip must be equal L1")
} }
// TestIncreaseGasPriceReusesTransaction asserts that if the L1 basefee & tip remain the
// same, the transaction is returned with the same signature values. The means that the error
// when submitting the transaction to the network is ErrAlreadyKnown instead of ErrReplacementUnderpriced
func TestIncreaseGasPriceReusesTransaction(t *testing.T) {
t.Parallel()
borkedBackend := failingBackend{
gasTip: big.NewInt(10),
baseFee: big.NewInt(45),
}
pk := testutils.InsecureRandomKey(rand.New(rand.NewSource(123)))
signer := opcrypto.PrivateKeySignerFn(pk, big.NewInt(10))
mgr := &SimpleTxManager{
Config: Config{
ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: 1,
SafeAbortNonceTooLowCount: 3,
Signer: func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) {
return signer(from, tx)
},
From: crypto.PubkeyToAddress(pk.PublicKey),
},
name: "TEST",
backend: &borkedBackend,
l: testlog.Logger(t, log.LvlCrit),
}
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: big.NewInt(10),
GasFeeCap: big.NewInt(100),
})
ctx := context.Background()
newTx, err := mgr.IncreaseGasPrice(ctx, tx)
require.NoError(t, err)
require.Equal(t, tx.Hash(), newTx.Hash())
}
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
optls "github.com/ethereum-optimism/optimism/op-service/tls" optls "github.com/ethereum-optimism/optimism/op-service/tls"
"github.com/ethereum-optimism/optimism/op-service/tls/certman" "github.com/ethereum-optimism/optimism/op-service/tls/certman"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -91,8 +92,8 @@ func (s *SignerClient) pingVersion() (string, error) { ...@@ -91,8 +92,8 @@ func (s *SignerClient) pingVersion() (string, error) {
return v, nil return v, nil
} }
func (s *SignerClient) SignTransaction(ctx context.Context, chainId *big.Int, tx *types.Transaction) (*types.Transaction, error) { func (s *SignerClient) SignTransaction(ctx context.Context, chainId *big.Int, from common.Address, tx *types.Transaction) (*types.Transaction, error) {
args := NewTransactionArgsFromTransaction(chainId, tx) args := NewTransactionArgsFromTransaction(chainId, from, tx)
var result hexutil.Bytes var result hexutil.Bytes
if err := s.client.CallContext(ctx, &result, "eth_signTransaction", args); err != nil { if err := s.client.CallContext(ctx, &result, "eth_signTransaction", args); err != nil {
......
...@@ -31,12 +31,13 @@ type TransactionArgs struct { ...@@ -31,12 +31,13 @@ type TransactionArgs struct {
} }
// NewTransactionArgsFromTransaction creates a TransactionArgs struct from an EIP-1559 transaction // NewTransactionArgsFromTransaction creates a TransactionArgs struct from an EIP-1559 transaction
func NewTransactionArgsFromTransaction(chainId *big.Int, tx *types.Transaction) *TransactionArgs { func NewTransactionArgsFromTransaction(chainId *big.Int, from common.Address, tx *types.Transaction) *TransactionArgs {
data := hexutil.Bytes(tx.Data()) data := hexutil.Bytes(tx.Data())
nonce := hexutil.Uint64(tx.Nonce()) nonce := hexutil.Uint64(tx.Nonce())
gas := hexutil.Uint64(tx.Gas()) gas := hexutil.Uint64(tx.Gas())
accesses := tx.AccessList() accesses := tx.AccessList()
args := &TransactionArgs{ args := &TransactionArgs{
From: &from,
Input: &data, Input: &data,
Nonce: &nonce, Nonce: &nonce,
Value: (*hexutil.Big)(tx.Value()), Value: (*hexutil.Big)(tx.Value()),
......
...@@ -38,13 +38,15 @@ The typescript sdk provides a clean [wagmi](https://wagmi.sh/) based interface f ...@@ -38,13 +38,15 @@ The typescript sdk provides a clean [wagmi](https://wagmi.sh/) based interface f
The cli provides a convenient cli for interacting with the attestation station contract The cli provides a convenient cli for interacting with the attestation station contract
TODO put a gif here of using it ![preview](./assets/preview.gif)
## React API ## React API
For react hooks we recomend using the [wagmi cli](https://wagmi.sh/cli/getting-started) with the [etherscan plugin](https://wagmi.sh/cli/plugins/etherscan) and [react plugin](https://wagmi.sh/cli/plugins/react) to automatically generate react hooks around the attestation station. For react hooks we recomend using the [wagmi cli](https://wagmi.sh/cli/getting-started) with the [etherscan plugin](https://wagmi.sh/cli/plugins/etherscan) and [react plugin](https://wagmi.sh/cli/plugins/react) to automatically generate react hooks around the attestation station.
Use `parseAttestationBytes` and `stringifyAttestationBytes` to parse and stringify attestations before passing them into wagmi hooks. Use `createKey` and `createValue` to convert your raw keys and values into bytes that can be used in the attestation station contract calls
Use `parseString`, `parseBool`, `parseAddress` and `parseNumber` to convert values returned by attestation station to their correct data type.
For convenience we also export the hooks here. For convenience we also export the hooks here.
......
# Assets
## preview.gif
A gif preview of using the cli
## preview.tape
The script to record the preview.gif with [vhs](https://github.com/charmbracelet/vhs)
To execute:
1. [Download vhs](https://github.com/charmbracelet/vhs)
2. Install the local version of atst
```bash
npm uninstall @eth-optimism/atst -g && npm i . -g && atst --version
```
3. Start anvil
```bash
anvil --fork-url https://mainnet.optimism.io
```
4. Record tape vhs < assets/preview.tape
```bash
vhs < assets/preview.tape
```
5. The tape will be outputted to `assets/preview.gif`
# VHS File source
# https://github.com/charmbracelet/vhs
#
# Output:
# Output <path>.gif Create a GIF output at the given <path>
# Output <path>.mp4 Create an MP4 output at the given <path>
# Output <path>.webm Create a WebM output at the given <path>
#
# Settings:
# Set FontSize <number> Set the font size of the terminal
# Set FontFamily <string> Set the font family of the terminal
# Set Height <number> Set the height of the terminal
# Set Width <number> Set the width of the terminal
# Set LetterSpacing <float> Set the font letter spacing (tracking)
# Set LineHeight <float> Set the font line height
# Set Theme <string> Set the theme of the terminal (JSON)
# Set Padding <number> Set the padding of the terminal
# Set Framerate <number> Set the framerate of the recording
# Set PlaybackSpeed <float> Set the playback speed of the recording
#
# Sleep:
# Sleep <time> Sleep for a set amount of <time> in seconds
#
# Type:
# Type[@<time>] "<characters>" Type <characters> into the terminal with a
# <time> delay between each character
#
# Keys:
# Backspace[@<time>] [number] Press the Backspace key
# Down[@<time>] [number] Press the Down key
# Enter[@<time>] [number] Press the Enter key
# Space[@<time>] [number] Press the Space key
# Tab[@<time>] [number] Press the Tab key
# Left[@<time>] [number] Press the Left Arrow key
# Right[@<time>] [number] Press the Right Arrow key
# Up[@<time>] [number] Press the Up Arrow key
# Down[@<time>] [number] Press the Down Arrow key
# Ctrl+<key> Press the Control key + <key> (e.g. Ctrl+C)
#
# Display:
# Hide Hide the subsequent commands from the output
# Show Show the subsequent commands in the output
Output assets/preview.gif
Set FontSize 16
Set Width 1920
Set Height 1080
Type "atst write --key attitude --about 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --value 'feeling very optimistic' --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545"
Enter
Sleep 2000ms
Type "atst read --key attitude --about 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --creator 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://localhost:8545"
Enter
Sleep 2000ms
Type "atst write --key impress-level --about 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --value 10 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545"
Enter
Sleep 2000ms
Type "atst read --key impress-level --about 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --creator 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://localhost:8545"
Enter
Sleep 2000ms
Type "atst --help"
Enter
Sleep 2000ms
...@@ -192,6 +192,7 @@ These definitions allow you to communicate with AttestationStation, but are not ...@@ -192,6 +192,7 @@ These definitions allow you to communicate with AttestationStation, but are not
#### `ATTESTATION_STATION_ADDRESS` #### `ATTESTATION_STATION_ADDRESS`
The deployment address for the attestation station currently deployed with create2 on Optimism and Optimism Goerli `0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77`. The deployment address for the attestation station currently deployed with create2 on Optimism and Optimism Goerli `0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77`.
```typescript ```typescript
...@@ -211,7 +212,6 @@ import { abi } from '@eth-optimism/atst' ...@@ -211,7 +212,6 @@ import { abi } from '@eth-optimism/atst'
`createKey` hashes keys longer than 31 bytes, because the atst key size is limited to 32 bytes. `createKey` hashes keys longer than 31 bytes, because the atst key size is limited to 32 bytes.
```typescript ```typescript
const key = await createKey( const key = await createKey(
'i.am.a.key.much.longer.than.32.bytes.long' 'i.am.a.key.much.longer.than.32.bytes.long'
...@@ -221,7 +221,6 @@ const key = await createKey( ...@@ -221,7 +221,6 @@ const key = await createKey(
createKey will keep the key as is if it is shorter than 32 bytes and otherwise run it through keccak256. createKey will keep the key as is if it is shorter than 32 bytes and otherwise run it through keccak256.
#### `parseAddress` #### `parseAddress`
Turn bytes into an address. Turn bytes into an address.
......
...@@ -6,6 +6,20 @@ ...@@ -6,6 +6,20 @@
"types": "src/index.ts", "types": "src/index.ts",
"module": "dist/index.cjs", "module": "dist/index.cjs",
"license": "MIT", "license": "MIT",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./react": {
"types": "./src/react.ts",
"default": "./dist/react.js",
"import": "./dist/react.js",
"require": "./dist/react.cjs"
}
},
"bin": { "bin": {
"atst": "./dist/cli.js" "atst": "./dist/cli.js"
}, },
......
...@@ -15,13 +15,13 @@ cli ...@@ -15,13 +15,13 @@ cli
.option('--creator <string>', readOptionsValidators.creator.description!) .option('--creator <string>', readOptionsValidators.creator.description!)
.option('--about <string>', readOptionsValidators.about.description!) .option('--about <string>', readOptionsValidators.about.description!)
.option('--key <string>', readOptionsValidators.key.description!) .option('--key <string>', readOptionsValidators.key.description!)
.option('--data-type [string]', readOptionsValidators.dataType.description!, { .option('--data-type <string>', readOptionsValidators.dataType.description!, {
default: readOptionsValidators.dataType.parse(undefined), default: readOptionsValidators.dataType.parse(undefined),
}) })
.option('--rpc-url [url]', readOptionsValidators.rpcUrl.description!, { .option('--rpc-url <url>', readOptionsValidators.rpcUrl.description!, {
default: readOptionsValidators.rpcUrl.parse(undefined), default: readOptionsValidators.rpcUrl.parse(undefined),
}) })
.option('--contract [address]', readOptionsValidators.contract.description!, { .option('--contract <address>', readOptionsValidators.contract.description!, {
default: readOptionsValidators.contract.parse(undefined), default: readOptionsValidators.contract.parse(undefined),
}) })
.example( .example(
...@@ -52,17 +52,17 @@ cli ...@@ -52,17 +52,17 @@ cli
'--private-key <string>', '--private-key <string>',
writeOptionsValidators.privateKey.description! writeOptionsValidators.privateKey.description!
) )
.option('--data-type [string]', readOptionsValidators.dataType.description!, { .option('--data-type <string>', readOptionsValidators.dataType.description!, {
default: writeOptionsValidators.dataType.parse(undefined), default: writeOptionsValidators.dataType.parse(undefined),
}) })
.option('--about <string>', writeOptionsValidators.about.description!) .option('--about <string>', writeOptionsValidators.about.description!)
.option('--key <string>', writeOptionsValidators.key.description!) .option('--key <string>', writeOptionsValidators.key.description!)
.option('--value <string>', writeOptionsValidators.value.description!) .option('--value <string>', writeOptionsValidators.value.description!)
.option('--rpc-url [url]', writeOptionsValidators.rpcUrl.description!, { .option('--rpc-url <url>', writeOptionsValidators.rpcUrl.description!, {
default: writeOptionsValidators.rpcUrl.parse(undefined), default: writeOptionsValidators.rpcUrl.parse(undefined),
}) })
.option( .option(
'--contract [address]', '--contract <address>',
writeOptionsValidators.contract.description!, writeOptionsValidators.contract.description!,
{ {
default: writeOptionsValidators.contract.parse(undefined), default: writeOptionsValidators.contract.parse(undefined),
......
// constants // constants
export { ATTESTATION_STATION_ADDRESS } from './constants/attestationStationAddress' export { ATTESTATION_STATION_ADDRESS } from './constants/attestationStationAddress'
// lib // lib
export { encodeRawKey } from './lib/encodeRawKey' export { encodeRawKey, createKey } from './lib/createKey'
export { createValue, stringifyAttestationBytes } from './lib/createValue'
export { export {
readAttestation, readAttestation,
readAttestationAddress, readAttestationAddress,
...@@ -15,7 +16,6 @@ export { prepareWriteAttestation } from './lib/prepareWriteAttestation' ...@@ -15,7 +16,6 @@ export { prepareWriteAttestation } from './lib/prepareWriteAttestation'
export { prepareWriteAttestations } from './lib/prepareWriteAttestations' export { prepareWriteAttestations } from './lib/prepareWriteAttestations'
export { writeAttestation } from './lib/writeAttestation' export { writeAttestation } from './lib/writeAttestation'
export { abi } from './lib/abi' export { abi } from './lib/abi'
export { stringifyAttestationBytes } from './lib/stringifyAttestationBytes'
export { export {
parseAttestationBytes, parseAttestationBytes,
parseAddress, parseAddress,
...@@ -28,5 +28,3 @@ export type { AttestationCreatedEvent } from './types/AttestationCreatedEvent' ...@@ -28,5 +28,3 @@ export type { AttestationCreatedEvent } from './types/AttestationCreatedEvent'
export type { AttestationReadParams } from './types/AttestationReadParams' export type { AttestationReadParams } from './types/AttestationReadParams'
export type { DataTypeOption } from './types/DataTypeOption' export type { DataTypeOption } from './types/DataTypeOption'
export type { WagmiBytes } from './types/WagmiBytes' export type { WagmiBytes } from './types/WagmiBytes'
// react
export * from './react'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { encodeRawKey } from './encodeRawKey' import { encodeRawKey } from './createKey'
describe(encodeRawKey.name, () => { describe(encodeRawKey.name, () => {
it('should return just the raw key if it is less than 32 bytes', () => { it('should return just the raw key if it is less than 32 bytes', () => {
......
...@@ -2,10 +2,24 @@ import { ethers } from 'ethers' ...@@ -2,10 +2,24 @@ import { ethers } from 'ethers'
import { WagmiBytes } from '../types/WagmiBytes' import { WagmiBytes } from '../types/WagmiBytes'
export const encodeRawKey = (rawKey: string): WagmiBytes => { /**
* Creates an attesation key from a raw string
* Converts to bytes32 if key is less than 32 bytes
* Hashes key if key is greater than 32 bytes
*/
export const createKey = (rawKey: string): WagmiBytes => {
if (rawKey.length < 32) { if (rawKey.length < 32) {
return ethers.utils.formatBytes32String(rawKey) as WagmiBytes return ethers.utils.formatBytes32String(rawKey) as WagmiBytes
} }
const hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(rawKey)) const hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(rawKey))
return (hash.slice(0, 64) + 'ff') as WagmiBytes return (hash.slice(0, 64) + 'ff') as WagmiBytes
} }
/**
* @deprecated use createKey instead
* Will be removed in v1.0.0
*/
export const encodeRawKey: typeof createKey = (rawKey) => {
console.warn('encodeRawKey is deprecated, use createKey instead')
return createKey(rawKey)
}
...@@ -9,7 +9,16 @@ import { ...@@ -9,7 +9,16 @@ import {
import { WagmiBytes } from '../types/WagmiBytes' import { WagmiBytes } from '../types/WagmiBytes'
export const stringifyAttestationBytes = ( /**
* Turns a value into bytes to make an attestation
*
* @example
* createValue('hello world') // '0x68656c6c6f20776f726c64'
* createValue(123) // '0x7b'
* createValue(true) // '0x1'
* createValue(BigNumber.from(10)) // '0xa'
*/
export const createValue = (
bytes: WagmiBytes | string | Address | number | boolean | BigNumber bytes: WagmiBytes | string | Address | number | boolean | BigNumber
): WagmiBytes => { ): WagmiBytes => {
bytes = bytes === '0x' ? '0x0' : bytes bytes = bytes === '0x' ? '0x0' : bytes
...@@ -33,3 +42,14 @@ export const stringifyAttestationBytes = ( ...@@ -33,3 +42,14 @@ export const stringifyAttestationBytes = (
} }
throw new Error(`unrecognized bytes type ${bytes satisfies never}`) throw new Error(`unrecognized bytes type ${bytes satisfies never}`)
} }
/**
* @deprecated use createValue instead
* Will be removed in v1.0.0
*/
export const stringifyAttestationBytes: typeof createValue = (bytes) => {
console.warn(
'stringifyAttestationBytes is deprecated, use createValue instead'
)
return createValue(bytes)
}
import { ethers } from 'ethers' import { ethers } from 'ethers'
import { Address } from 'wagmi' import type { Address } from '@wagmi/core'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress' import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { abi } from '../lib/abi' import { abi } from '../lib/abi'
import { AttestationCreatedEvent } from '../types/AttestationCreatedEvent' import { AttestationCreatedEvent } from '../types/AttestationCreatedEvent'
import { encodeRawKey } from './encodeRawKey' import { encodeRawKey } from './createKey'
export const getEvents = async ({ export const getEvents = async ({
creator = null, creator = null,
......
...@@ -41,6 +41,9 @@ export const parseAddress = (rawAttestation: WagmiBytes): Address => { ...@@ -41,6 +41,9 @@ export const parseAddress = (rawAttestation: WagmiBytes): Address => {
} }
/** /**
* @deprecated use parseString, parseBool, parseNumber, or parseAddress instead
* Will be removed in v1.0.0
* @internal
* Parses a raw attestation * Parses a raw attestation
*/ */
export const parseAttestationBytes = <TDataType extends DataTypeOption>( export const parseAttestationBytes = <TDataType extends DataTypeOption>(
......
...@@ -4,13 +4,13 @@ import { formatBytes32String } from 'ethers/lib/utils.js' ...@@ -4,13 +4,13 @@ import { formatBytes32String } from 'ethers/lib/utils.js'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress' import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { WagmiBytes } from '../types/WagmiBytes' import { WagmiBytes } from '../types/WagmiBytes'
import { abi } from './abi' import { abi } from './abi'
import { stringifyAttestationBytes } from './stringifyAttestationBytes' import { createValue } from './createValue'
export const prepareWriteAttestation = async ( export const prepareWriteAttestation = async (
about: Address, about: Address,
key: string, key: string,
value: string | WagmiBytes | number | boolean, value: string | WagmiBytes | number | boolean,
chainId = 10, chainId: number | undefined = undefined,
contractAddress: Address = ATTESTATION_STATION_ADDRESS contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => { ) => {
let formattedKey: WagmiBytes let formattedKey: WagmiBytes
...@@ -27,6 +27,6 @@ export const prepareWriteAttestation = async ( ...@@ -27,6 +27,6 @@ export const prepareWriteAttestation = async (
abi, abi,
functionName: 'attest', functionName: 'attest',
chainId, chainId,
args: [about, formattedKey, stringifyAttestationBytes(value) as WagmiBytes], args: [about, formattedKey, createValue(value) as WagmiBytes],
}) })
} }
...@@ -4,7 +4,7 @@ import { formatBytes32String } from 'ethers/lib/utils.js' ...@@ -4,7 +4,7 @@ import { formatBytes32String } from 'ethers/lib/utils.js'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress' import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { WagmiBytes } from '../types/WagmiBytes' import { WagmiBytes } from '../types/WagmiBytes'
import { abi } from './abi' import { abi } from './abi'
import { stringifyAttestationBytes } from './stringifyAttestationBytes' import { createValue } from './createValue'
type Attestation = { type Attestation = {
about: Address about: Address
...@@ -14,7 +14,7 @@ type Attestation = { ...@@ -14,7 +14,7 @@ type Attestation = {
export const prepareWriteAttestations = async ( export const prepareWriteAttestations = async (
attestations: Attestation[], attestations: Attestation[],
chainId = 10, chainId: number | undefined = undefined,
contractAddress: Address = ATTESTATION_STATION_ADDRESS contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => { ) => {
const formattedAttestations = attestations.map((attestation) => { const formattedAttestations = attestations.map((attestation) => {
...@@ -27,9 +27,7 @@ export const prepareWriteAttestations = async ( ...@@ -27,9 +27,7 @@ export const prepareWriteAttestations = async (
`key is longer than 32 bytes: ${attestation.key}. Try using a shorter key or using 'encodeRawKey' to encode the key into 32 bytes first` `key is longer than 32 bytes: ${attestation.key}. Try using a shorter key or using 'encodeRawKey' to encode the key into 32 bytes first`
) )
} }
const formattedValue = stringifyAttestationBytes( const formattedValue = createValue(attestation.value) as WagmiBytes
attestation.value
) as WagmiBytes
return { return {
about: attestation.about, about: attestation.about,
key: formattedKey, key: formattedKey,
......
...@@ -19,7 +19,6 @@ import { parseAttestationBytes } from './parseAttestationBytes' ...@@ -19,7 +19,6 @@ import { parseAttestationBytes } from './parseAttestationBytes'
* creator: creatorAddress, * creator: creatorAddress,
* about: aboutAddress, * about: aboutAddress,
* key: 'my_key', * key: 'my_key',
* allowFailure: false,
* }, * },
* { * {
* creator: creatorAddress2, * creator: creatorAddress2,
...@@ -27,7 +26,6 @@ import { parseAttestationBytes } from './parseAttestationBytes' ...@@ -27,7 +26,6 @@ import { parseAttestationBytes } from './parseAttestationBytes'
* key: 'my_key', * key: 'my_key',
* dataType: 'number', * dataType: 'number',
* contractAddress: '0x1234', * contractAddress: '0x1234',
* allowFailure: false,
* }, * },
* ) * )
*/ */
...@@ -40,7 +38,6 @@ export const readAttestations = async ( ...@@ -40,7 +38,6 @@ export const readAttestations = async (
about, about,
key, key,
contractAddress = ATTESTATION_STATION_ADDRESS, contractAddress = ATTESTATION_STATION_ADDRESS,
allowFailure = false,
} = attestation } = attestation
if (key.length > 32) { if (key.length > 32) {
throw new Error( throw new Error(
...@@ -52,7 +49,6 @@ export const readAttestations = async ( ...@@ -52,7 +49,6 @@ export const readAttestations = async (
abi, abi,
functionName: 'attestations', functionName: 'attestations',
args: [creator, about, formatBytes32String(key) as WagmiBytes], args: [creator, about, formatBytes32String(key) as WagmiBytes],
allowFailure,
} as const } as const
}) })
......
...@@ -11,5 +11,5 @@ export interface AttestationReadParams { ...@@ -11,5 +11,5 @@ export interface AttestationReadParams {
key: string key: string
dataType?: DataTypeOption dataType?: DataTypeOption
contractAddress?: Address contractAddress?: Address
allowFailure?: boolean chainId?: number
} }
import { BigNumber } from 'ethers' import { BigNumber } from 'ethers'
import { Address } from 'wagmi' import type { Address } from '@wagmi/core'
import { DataTypeOption } from './DataTypeOption' import { DataTypeOption } from './DataTypeOption'
import { WagmiBytes } from './WagmiBytes' import { WagmiBytes } from './WagmiBytes'
......
...@@ -10,7 +10,7 @@ export default defineConfig({ ...@@ -10,7 +10,7 @@ export default defineConfig({
* *
* @see https://tsup.egoist.dev/#building-cli-app * @see https://tsup.egoist.dev/#building-cli-app
*/ */
entry: ['src/index.ts', 'src/cli.ts'], entry: ['src/index.ts', 'src/cli.ts', 'src/react.ts'],
outDir: 'dist', outDir: 'dist',
target: 'es2021', target: 'es2021',
// will create a .js file for commonjs and a .cjs file for esm // will create a .js file for commonjs and a .cjs file for esm
......
Bytes_slice_Test:test_slice_acrossMultipleWords_works() (gas: 9423) Bytes_slice_Test:test_slice_acrossMultipleWords_works() (gas: 9413)
Bytes_slice_Test:test_slice_acrossWords_works() (gas: 1418) Bytes_slice_Test:test_slice_acrossWords_works() (gas: 1430)
Bytes_slice_Test:test_slice_fromNonZeroIdx_works() (gas: 17154) Bytes_slice_Test:test_slice_fromNonZeroIdx_works() (gas: 17240)
Bytes_slice_Test:test_slice_fromZeroIdx_works() (gas: 20694) Bytes_slice_Test:test_slice_fromZeroIdx_works() (gas: 20826)
Bytes_toNibbles_Test:test_toNibbles_expectedResult128Bytes_works() (gas: 129874) Bytes_toNibbles_Test:test_toNibbles_expectedResult128Bytes_works() (gas: 129874)
Bytes_toNibbles_Test:test_toNibbles_expectedResult5Bytes_works() (gas: 6132) Bytes_toNibbles_Test:test_toNibbles_expectedResult5Bytes_works() (gas: 6132)
Bytes_toNibbles_Test:test_toNibbles_zeroLengthInput_works() (gas: 944) Bytes_toNibbles_Test:test_toNibbles_zeroLengthInput_works() (gas: 944)
......
...@@ -122,6 +122,58 @@ contract Bytes_slice_Test is Test { ...@@ -122,6 +122,58 @@ contract Bytes_slice_Test is Test {
vm.expectRevert("slice_overflow"); vm.expectRevert("slice_overflow");
Bytes.slice(_input, _start, _length); Bytes.slice(_input, _start, _length);
} }
/**
* @notice Tests that the `slice` function correctly updates the free memory pointer depending
* on the length of the slice.
*/
function testFuzz_slice_memorySafety_succeeds(
bytes memory _input,
uint256 _start,
uint256 _length
) public {
// The start should never be more than the length of the input bytes array - 1
vm.assume(_start < _input.length);
// The length should never be more than the length of the input bytes array - the starting
// slice index.
vm.assume(_length <= _input.length - _start);
// Grab the free memory pointer before the slice operation
uint256 initPtr;
assembly {
initPtr := mload(0x40)
}
// Slice the input bytes array from `_start` to `_start + _length`
bytes memory slice = Bytes.slice(_input, _start, _length);
// Grab the free memory pointer after the slice operation
uint256 finalPtr;
assembly {
finalPtr := mload(0x40)
}
// The free memory pointer should have been updated properly
if (_length == 0) {
// If the slice length is zero, only 32 bytes of memory should have been allocated.
assertEq(finalPtr, initPtr + 0x20);
} else {
// If the slice length is greater than zero, the memory allocated should be the
// length of the slice rounded up to the next 32 byte word + 32 bytes for the
// length of the byte array.
//
// Note that we use a slightly less efficient, but equivalent method of rounding
// up `_length` to the next multiple of 32 than is used in the `slice` function.
// This is to diff test the method used in `slice`.
assertEq(finalPtr, initPtr + 0x20 + (((_length + 0x1F) >> 5) << 5));
// Sanity check for equivalence of the rounding methods.
assertEq(((_length + 0x1F) >> 5) << 5, (_length + 0x1F) & ~uint256(0x1F));
}
// The slice length should be equal to `_length`
assertEq(slice.length, _length);
}
} }
contract Bytes_toNibbles_Test is Test { contract Bytes_toNibbles_Test is Test {
......
...@@ -14,6 +14,10 @@ that maintains 1:1 compatibility with Ethereum. ...@@ -14,6 +14,10 @@ that maintains 1:1 compatibility with Ethereum.
- [L2 Output Root Proposals](proposals.md) - [L2 Output Root Proposals](proposals.md)
- [Rollup Node](rollup-node.md) - [Rollup Node](rollup-node.md)
- [Rollup Node P2p](rollup-node-p2p.md) - [Rollup Node P2p](rollup-node-p2p.md)
- [L2 Chain Derivation](derivation.md)
- [Network Upgrades](network-upgrades.md)
- [System Config](system_config.md)
- [Batch Submitter](batcher.md)
- [Guaranteed Gas Market](guaranteed-gas-market.md) - [Guaranteed Gas Market](guaranteed-gas-market.md)
- [Messengers](messengers.md) - [Messengers](messengers.md)
- [Bridges](bridges.md) - [Bridges](bridges.md)
......
# Network Upgrades
Network upgrades, also known as forks or hardforks, implement consensus-breaking changes.
These changes are transitioned into deterministically across all nodes through an activation rule.
This document lists the network upgrades of the OP Stack, starting after the Bedrock upgrade.
Prospective upgrades may be listed as proposals, but are not governed through these specifications.
Activation rule parameters of network upgrades are configured in respective chain configurations,
and not part of this specification.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Activation rules](#activation-rules)
- [L2 Block-number based activation](#l2-block-number-based-activation)
- [L2 Block-timestamp based activation](#l2-block-timestamp-based-activation)
- [Post-Bedrock Network upgrades](#post-bedrock-network-upgrades)
- [Regolith](#regolith)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Activation rules
The below L2-block based activation rules may be applied in two contexts:
- The rollup node, specified through the rollup configuration (known as `rollup.json`),
referencing L2 blocks (or block input-attributes) that pass through the derivation pipeline.
- The execution engine, specified through the chain configuration (known as the `config` part of `genesis.json`),
referencing blocks or input-attributes that are part of, or applied to, the L2 chain.
### L2 Block-number based activation
Activation rule: `x != null && x >= upgradeNumber`
Starting at, and including, the L2 `block` with `block.number == x`, the upgrade rules apply.
If the upgrade block-number `x` is not specified in the configuration, the upgrade is ignored.
This applies to the L2 block number, not to the L1-origin block number.
This means that an L2 upgrade may be inactive, and then active, without changing the L1-origin.
This block number based method has commonly been used in L1 up until the Bellatrix/Paris upgrade, a.k.a. The Merge,
which was upgraded through special rules.
### L2 Block-timestamp based activation
Activation rule: `x != null && x >= upgradeTime`
Starting at, and including, the L2 `block` with `block.timestamp == x`, the upgrade rules apply.
If the upgrade block-timestamp `x` is not specified in the configuration, the upgrade is ignored.
This applies to the L2 block timestamp, not to the L1-origin block timestamp.
This means that an L2 upgrade may be inactive, and then active, without changing the L1-origin.
This timestamp based method has become the default on L1 after the Bellatrix/Paris upgrade, a.k.a. The Merge,
because it can be planned in accordance with beacon-chain epochs and slots.
Note that the L2 version is not limited to timestamps that match L1 beacon-chain slots or epochs.
A timestamp may be chosen to be synchronous with a specific slot or epoch on L1,
but the matching L1-origin information may not be present at the time of activation on L2.
## Post-Bedrock Network upgrades
### Regolith
The Regolith upgrade, named after a material best described as "deposited dust on top of a layer of bedrock",
implements minor changes to deposit processing, based on reports of the Sherlock Audit-contest and findings in
the Bedrock Optimism Goerli testnet.
Summary of changes:
- The `isSystemTx` boolean is disabled, system transactions now use the same gas accounting rules as regular deposits.
- The actual deposit gas-usage is recorded in the receipt of the deposit transaction,
and subtracted from the L2 block gas-pool.
Unused gas of deposits is not refunded with ETH however, as it is burned on L1.
- The `nonce` value of the deposit sender account, before the transaction state-transition, is recorded in a new
optional field (`depositNonce`), extending the transaction receipt (i.e. not present in pre-Regolith receipts).
- The recorded deposit `nonce` is used to correct the transaction and receipt metadata in RPC responses,
including the `contractAddress` field of deposits that deploy contracts.
- The `gas` and `depositNonce` data is committed to as part of the consensus-representation of the receipt,
enabling the data to be safely synced between independent L2 nodes.
The [deposit specification](./deposits.md) specifies the changes of the Regolith upgrade in more detail.
The Regolith upgrade uses a *L2 block-timestamp* activation-rule, and is specified in both the
rollup-node (`regolith_time`) and execution engine (`config.regolithTime`).
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment