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:
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
context:
- 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:
name: hive-test-rpc
version: <<pipeline.git.revision>>
......@@ -1227,4 +1234,4 @@ workflows:
context:
- oplabs-gcr-release
requires:
- hold
\ No newline at end of file
- hold
......@@ -87,7 +87,7 @@ func (s *channelManager) Clear() {
func (s *channelManager) TxFailed(id txID) {
if data, ok := s.pendingTransactions[id]; ok {
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)
} else {
s.log.Warn("unknown transaction marked as failed", "id", id)
......
......@@ -8,6 +8,8 @@ import (
"os"
"strings"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/db"
"github.com/mattn/go-isatty"
......@@ -22,7 +24,6 @@ import (
"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/migration"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli"
......@@ -46,11 +47,6 @@ func main() {
Usage: "Path to ovm-addresses.json",
Required: true,
},
&cli.StringFlag{
Name: "evm-addresses",
Usage: "Path to evm-addresses.json",
Required: true,
},
&cli.StringFlag{
Name: "ovm-allowances",
Usage: "Path to ovm-allowances.json",
......@@ -62,8 +58,8 @@ func main() {
Required: true,
},
&cli.StringFlag{
Name: "evm-messages",
Usage: "Path to evm-messages.json",
Name: "witness-file",
Usage: "Path to witness file",
Required: true,
},
&cli.StringFlag{
......@@ -118,30 +114,35 @@ func main() {
return err
}
ovmAddresses, err := migration.NewAddresses(ctx.String("ovm-addresses"))
ovmAddresses, err := crossdomain.NewAddresses(ctx.String("ovm-addresses"))
if err != nil {
return err
}
evmAddresess, err := migration.NewAddresses(ctx.String("evm-addresses"))
ovmAllowances, err := crossdomain.NewAllowances(ctx.String("ovm-allowances"))
if err != nil {
return err
}
ovmAllowances, err := migration.NewAllowances(ctx.String("ovm-allowances"))
ovmMessages, err := crossdomain.NewSentMessageFromJSON(ctx.String("ovm-messages"))
if err != nil {
return err
}
ovmMessages, err := migration.NewSentMessage(ctx.String("ovm-messages"))
if err != nil {
return err
}
evmMessages, err := migration.NewSentMessage(ctx.String("evm-messages"))
evmMessages, evmAddresses, err := crossdomain.ReadWitnessData(ctx.String("witness-file"))
if err != nil {
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,
EvmAddresses: evmAddresess,
EvmAddresses: evmAddresses,
OvmAllowances: ovmAllowances,
OvmMessages: ovmMessages,
EvmMessages: evmMessages,
......
......@@ -18,8 +18,6 @@ import (
"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"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
......@@ -768,7 +766,7 @@ func newWithdrawals(ctx *cli.Context, l1ChainID *big.Int) ([]*crossdomain.Legacy
evmMsgs := ctx.String("evm-messages")
log.Debug("Migration data", "ovm-path", ovmMsgs, "evm-messages", evmMsgs)
ovmMessages, err := migration.NewSentMessage(ovmMsgs)
ovmMessages, err := crossdomain.NewSentMessageFromJSON(ovmMsgs)
if err != nil {
return nil, err
}
......@@ -777,20 +775,20 @@ func newWithdrawals(ctx *cli.Context, l1ChainID *big.Int) ([]*crossdomain.Legacy
// committed to in git.
if l1ChainID.Cmp(common.Big1) != 0 {
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 {
return nil, err
}
migrationData := migration.MigrationData{
migrationData := crossdomain.MigrationData{
OvmMessages: ovmMessages,
EvmMessages: evmMessages,
}
wds, err := migrationData.ToWithdrawals()
wds, _, err := migrationData.ToWithdrawals()
if err != nil {
return nil, err
}
......
......@@ -5,6 +5,7 @@ import (
"fmt"
"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/core/state"
"github.com/ethereum/go-ethereum/log"
......@@ -12,28 +13,40 @@ import (
var (
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
// 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.
slotsInp := make(map[common.Hash]*LegacyWithdrawal)
validSlotsInp := make(map[common.Hash]*LegacyWithdrawal)
for _, wd := range withdrawals {
slot, err := wd.StorageSlot()
if err != nil {
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.
var count int
var innerErr error
slotsAct := make(map[common.Hash]bool)
progress := util.ProgressLogger(1000, "Iterating legacy messages")
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
// of the ABI encoding of "true". Although there should not be any other storage slots, we
// can safely ignore anything that is not "true".
......@@ -59,24 +72,32 @@ func PreCheckWithdrawals(db *state.StateDB, withdrawals DangerousUnfilteredWithd
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.
var missing int
for slot := range slotsAct {
_, ok := slotsInp[slot]
if !ok {
return nil, ErrMissingSlotInWitness
_, okValid := validSlotsInp[slot]
_, okInvalid := invalidSlotsInp[slot]
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.
// We'll filter out any extra messages that are not in the legacy system.
filtered := make(SafeFilteredWithdrawals, 0)
for slot := range slotsInp {
for slot := range validSlotsInp {
_, ok := slotsAct[slot]
if !ok {
log.Info("filtering out unknown input message", "slot", slot.String())
continue
}
wd := slotsInp[slot]
wd := validSlotsInp[slot]
if wd.MessageSender != predeploys.L2CrossDomainMessengerAddr {
log.Info("filtering out message from sender other than the L2XDM", "sender", wd.MessageSender)
continue
......
......@@ -71,7 +71,7 @@ func TestPreCheckWithdrawals_InvalidSlotInStorage(t *testing.T) {
err = stateDB.Database().TrieDB().Commit(root, true)
require.NoError(t, err)
_, err = PreCheckWithdrawals(stateDB, nil)
_, err = PreCheckWithdrawals(stateDB, nil, nil)
require.ErrorIs(t, err, ErrUnknownSlotInMessagePasser)
}
......@@ -130,5 +130,5 @@ func runPrecheck(t *testing.T, dbWds []*LegacyWithdrawal, witnessWds []*LegacyWi
err = stateDB.Database().TrieDB().Commit(root, true)
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
import (
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
// DangerousUnfilteredWithdrawals is a list of raw withdrawal witness
......@@ -30,3 +33,35 @@ type WithdrawalMessage interface {
Hash() (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 (
"bufio"
"encoding/json"
"fmt"
"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/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
// `LegacyMessagePasser`. The `who` should always be the
// `L2CrossDomainMessenger` and the `msg` should be an abi encoded
......@@ -20,10 +24,10 @@ type SentMessage struct {
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
// `migration-data` package.
func NewSentMessage(path string) ([]*SentMessage, error) {
func NewSentMessageFromJSON(path string) ([]*SentMessage, error) {
file, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("cannot find sent message json at %s: %w", path, err)
......@@ -37,15 +41,81 @@ func NewSentMessage(path string) ([]*SentMessage, error) {
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
// struct. This is useful because the LegacyWithdrawal struct has helper
// 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))
copy(data, s.Msg)
copy(data[len(s.Msg):], s.Who[:])
var w crossdomain.LegacyWithdrawal
var w LegacyWithdrawal
if err := w.Decode(data); err != nil {
return nil, err
}
......@@ -117,26 +187,26 @@ type MigrationData struct {
EvmMessages []*SentMessage
}
func (m *MigrationData) ToWithdrawals() (crossdomain.DangerousUnfilteredWithdrawals, error) {
messages := make(crossdomain.DangerousUnfilteredWithdrawals, 0)
func (m *MigrationData) ToWithdrawals() (DangerousUnfilteredWithdrawals, []InvalidMessage, error) {
messages := make(DangerousUnfilteredWithdrawals, 0)
invalidMessages := make([]InvalidMessage, 0)
for _, msg := range m.OvmMessages {
wd, err := msg.ToLegacyWithdrawal()
if err != nil {
return nil, err
return nil, nil, fmt.Errorf("error serializing OVM message: %w", err)
}
messages = append(messages, wd)
if err != nil {
return nil, err
}
}
for _, msg := range m.EvmMessages {
wd, err := msg.ToLegacyWithdrawal()
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)
}
return messages, nil
return messages, invalidMessages, nil
}
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 (
"io"
"strings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
......@@ -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 {
for headNum > 0 {
hash := rawdb.ReadCanonicalHash(db, headNum)
receipts, err := migration.ReadLegacyReceipts(db, hash, headNum)
receipts, err := crossdomain.ReadLegacyReceipts(db, hash, headNum)
if err != nil {
return err
}
......
......@@ -4,9 +4,10 @@ import (
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"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/core/state"
"github.com/ethereum/go-ethereum/log"
......@@ -30,7 +31,7 @@ var (
func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, chainID int, noCheck bool) error {
// Chain params to use for integrity checking.
params := migration.ParamsByChainID[chainID]
params := crossdomain.ParamsByChainID[chainID]
if params == nil {
return fmt.Errorf("no chain params for %d", chainID)
}
......@@ -47,7 +48,7 @@ func MigrateLegacyETH(db *state.StateDB, addresses []common.Address, chainID int
// Migrate the legacy ETH to ETH.
log.Info("Migrating legacy ETH to ETH", "num-accounts", len(addresses))
totalMigrated := new(big.Int)
logAccountProgress := ProgressLogger(1000, "imported accounts")
logAccountProgress := util.ProgressLogger(1000, "imported accounts")
for addr := range deduped {
// No accounts should have a balance in state. If they do, bail.
if db.GetBalance(addr).Sign() > 0 {
......
package ether
import (
"errors"
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"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/core/state"
"github.com/ethereum/go-ethereum/ethdb"
......@@ -17,9 +19,9 @@ import (
// 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
// 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.
params := migration.ParamsByChainID[chainID]
params := crossdomain.ParamsByChainID[chainID]
if params == nil {
return nil, fmt.Errorf("no chain params for %d", chainID)
}
......@@ -53,7 +55,10 @@ func PreCheckBalances(ldb ethdb.Database, db *state.StateDB, addresses []common.
// slots that we know we can ignore (totalSupply, name, symbol).
var count int
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 {
progress()
// We can safely ignore specific slots (totalSupply, name, symbol).
if ignoredSlots[key] {
return true
......@@ -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
// expected supply delta.
totalFound := new(big.Int)
var unknown bool
for slot := range slotsAct {
slotType, ok := slotsInp[slot]
if !ok {
if noCheck {
log.Error("ignoring unknown storage slot in state", "slot", slot)
log.Error("ignoring unknown storage slot in state", "slot", slot.String())
} 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.
}
}
}
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
// than the actual migrated amount because self-destructs will remove ETH supply in a way that
......
......@@ -19,7 +19,6 @@ import (
"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-node/rollup/derive"
)
......@@ -89,7 +88,7 @@ var (
// PostCheckMigratedDB will check that the migration was performed correctly
func PostCheckMigratedDB(
ldb ethdb.Database,
migrationData migration.MigrationData,
migrationData crossdomain.MigrationData,
l1XDM *common.Address,
l1ChainID uint64,
finalSystemOwner common.Address,
......@@ -468,8 +467,8 @@ func PostCheckL1Block(db vm.StateDB, info *derive.L1BlockInfo) error {
return nil
}
func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossDomainMessenger *common.Address) error {
wds, err := data.ToWithdrawals()
func CheckWithdrawalsAfter(db vm.StateDB, data crossdomain.MigrationData, l1CrossDomainMessenger *common.Address) error {
wds, invalidMessages, err := data.ToWithdrawals()
if err != nil {
return err
}
......@@ -479,6 +478,7 @@ func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossD
// some witness data may references withdrawals that reverted.
oldToNewSlots := make(map[common.Hash]common.Hash)
wdsByOldSlot := make(map[common.Hash]*crossdomain.LegacyWithdrawal)
invalidMessagesByOldSlot := make(map[common.Hash]crossdomain.InvalidMessage)
for _, wd := range wds {
migrated, err := crossdomain.MigrateWithdrawal(wd, l1CrossDomainMessenger)
if err != nil {
......@@ -497,6 +497,15 @@ func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossD
oldToNewSlots[legacySlot] = migratedSlot
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
// migrated withdrawal.
......@@ -515,6 +524,17 @@ func CheckWithdrawalsAfter(db vm.StateDB, data migration.MigrationData, l1CrossD
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.
migratedSlot := oldToNewSlots[key]
if migratedSlot == (common.Hash{}) {
......
......@@ -8,7 +8,6 @@ import (
"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/ether"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
......@@ -35,7 +34,7 @@ type MigrationResult struct {
}
// 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.
hash := rawdb.ReadHeadHeaderHash(ldb)
log.Info("Reading chain tip from database", "hash", hash)
......@@ -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
// may be missing some messages or have some extra messages.
unfilteredWithdrawals, err := migrationData.ToWithdrawals()
unfilteredWithdrawals, invalidMessages, err := migrationData.ToWithdrawals()
if err != nil {
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
// will be thrown if there are any missing messages, and any extra messages will be removed.
var filteredWithdrawals crossdomain.SafeFilteredWithdrawals
if !noCheck {
log.Info("Checking withdrawals...")
filteredWithdrawals, err = crossdomain.PreCheckWithdrawals(db, unfilteredWithdrawals)
filteredWithdrawals, err = crossdomain.PreCheckWithdrawals(db, unfilteredWithdrawals, invalidMessages)
if err != nil {
return nil, fmt.Errorf("withdrawals mismatch: %w", err)
}
......
......@@ -5,8 +5,9 @@ import (
"math/big"
"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/migration"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethclient"
)
......@@ -30,28 +31,28 @@ type Config struct {
func Migrate(cfg *Config) (*genesis.MigrationResult, error) {
deployConfig := cfg.DeployConfig
ovmAddresses, err := migration.NewAddresses(cfg.OVMAddressesPath)
ovmAddresses, err := crossdomain.NewAddresses(cfg.OVMAddressesPath)
if err != nil {
return nil, err
}
evmAddresess, err := migration.NewAddresses(cfg.EVMAddressesPath)
evmAddresess, err := crossdomain.NewAddresses(cfg.EVMAddressesPath)
if err != nil {
return nil, err
}
ovmAllowances, err := migration.NewAllowances(cfg.OVMAllowancesPath)
ovmAllowances, err := crossdomain.NewAllowances(cfg.OVMAllowancesPath)
if err != nil {
return nil, err
}
ovmMessages, err := migration.NewSentMessage(cfg.OVMMessagesPath)
ovmMessages, err := crossdomain.NewSentMessageFromJSON(cfg.OVMMessagesPath)
if err != nil {
return nil, err
}
evmMessages, err := migration.NewSentMessage(cfg.EVMMessagesPath)
evmMessages, err := crossdomain.NewSentMessageFromJSON(cfg.EVMMessagesPath)
if err != nil {
return nil, err
}
migrationData := migration.MigrationData{
migrationData := crossdomain.MigrationData{
OvmAddresses: ovmAddresses,
EvmAddresses: evmAddresess,
OvmAllowances: ovmAllowances,
......
package ether
package util
import (
"github.com/ethereum/go-ethereum/log"
......
......@@ -77,7 +77,7 @@ func TestBatchInLastPossibleBlocks(gt *testing.T) {
}
// 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)
// Create the batch for L2 blocks 16 & 17
......
......@@ -184,42 +184,68 @@ func (s *L1Replica) L1Client(t Testing, cfg *rollup.Config) *sources.L1Client {
return l1F
}
// ActL1FinalizeNext finalizes the next block, which must be marked as safe before doing so (see ActL1SafeNext).
func (s *L1Replica) ActL1FinalizeNext(t Testing) {
func (s *L1Replica) UnsafeNum() uint64 {
head := s.l1Chain.CurrentBlock()
headNum := uint64(0)
if head != nil {
headNum = head.NumberU64()
}
return headNum
}
func (s *L1Replica) SafeNum() uint64 {
safe := s.l1Chain.CurrentSafeBlock()
safeNum := uint64(0)
if safe != nil {
safeNum = safe.NumberU64()
}
return safeNum
}
func (s *L1Replica) FinalizedNum() uint64 {
finalized := s.l1Chain.CurrentFinalizedBlock()
finalizedNum := uint64(0)
if finalized != nil {
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")
return
}
next := s.l1Chain.GetBlockByNumber(finalizedNum + 1)
if next == nil {
t.Fatalf("expected next block after finalized L1 block %d, safe head is ahead", finalizedNum)
newFinalized := s.l1Chain.GetBlockByNumber(num)
if newFinalized == nil {
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.
func (s *L1Replica) ActL1SafeNext(t Testing) {
safe := s.l1Chain.CurrentSafeBlock()
safeNum := uint64(0)
if safe != nil {
safeNum = safe.NumberU64()
}
next := s.l1Chain.GetBlockByNumber(safeNum + 1)
if next == nil {
t.InvalidAction("if head of chain is marked as safe then there's no next block")
// ActL1FinalizeNext finalizes the next block, which must be marked as safe before doing so (see ActL1SafeNext).
func (s *L1Replica) ActL1FinalizeNext(t Testing) {
n := s.FinalizedNum() + 1
s.ActL1Finalize(t, n)
}
// ActL1Safe marks the given unsafe block as safe.
func (s *L1Replica) ActL1Safe(t Testing, num uint64) {
newSafe := s.l1Chain.GetBlockByNumber(num)
if newSafe == nil {
t.InvalidAction("could not find L1 block %d, cannot label it as safe", num)
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 {
......
......@@ -196,6 +196,62 @@ func TestL2Finalization(gt *testing.T) {
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
// valid batches being submitted to the batch inbox. These batches should always be rejected
// and the safe L2 head should remain unaltered.
......
......@@ -4,8 +4,6 @@ import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
......@@ -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.
sequencer.ActL1HeadSignal(t)
sequencer.ActL2PipelineFull(t)
// TODO: CLI-3405 we can detect the inconsistency of the L1 origin of the unsafe L2 head:
// as verifier, there is no need to wait for sequencer to recognize it.
// Verifier should detect the inconsistency of the L1 origin and reset the pipeline to follow the reorg
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.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
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")
sequencer.ActL1HeadSignal(t)
// sequence more L2 blocks, until we actually need the next L1 origin
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
// After hitting a reset error, it resets derivation, and drops the old L1 chain
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
sequencer.ActBuildToL1HeadUnsafe(t)
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) {
c.P2P = p
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 (
"time"
"github.com/ethereum-optimism/optimism/op-node/eth"
ophttp "github.com/ethereum-optimism/optimism/op-node/http"
"github.com/ethereum/go-ethereum/log"
)
......@@ -161,7 +162,8 @@ func runServer() {
mux.HandleFunc("/logs", makeGzipHandler(logsHandler))
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)
}
}
......
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 (
"errors"
"fmt"
"net"
"net/http"
"strconv"
"time"
ophttp "github.com/ethereum-optimism/optimism/op-node/http"
"github.com/ethereum-optimism/optimism/op-service/metrics"
pb "github.com/libp2p/go-libp2p-pubsub/pb"
......@@ -528,12 +528,10 @@ func (m *Metrics) RecordSequencerSealingTime(duration time.Duration) {
// The server will be closed when the passed-in context is cancelled.
func (m *Metrics) Serve(ctx context.Context, hostname string, port int) error {
addr := net.JoinHostPort(hostname, strconv.Itoa(port))
server := &http.Server{
Addr: addr,
Handler: promhttp.InstrumentMetricHandler(
m.registry, promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}),
),
}
server := ophttp.NewHttpServer(promhttp.InstrumentMetricHandler(
m.registry, promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}),
))
server.Addr = addr
go func() {
<-ctx.Done()
server.Close()
......
......@@ -7,6 +7,7 @@ import (
"net/http"
"strconv"
ophttp "github.com/ethereum-optimism/optimism/op-node/http"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
......@@ -87,7 +88,7 @@ func (s *rpcServer) Start() error {
}
s.listenAddr = listener.Addr()
s.httpServer = &http.Server{Handler: mux}
s.httpServer = ophttp.NewHttpServer(mux)
go func() {
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)
......
......@@ -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 &p2p.PreparedSigner{Signer: p2p.NewLegacyLocalSigner(priv)}, nil
return &p2p.PreparedSigner{Signer: p2p.NewLocalSigner(priv)}, nil
}
// TODO: create remote signer
......
......@@ -49,7 +49,7 @@ func TestVerifyBlockSignature(t *testing.T) {
}{
{
name: "Legacy",
newSigner: NewLegacyLocalSigner,
newSigner: newLegacyLocalSigner,
},
{
name: "Updated",
......@@ -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) {
// 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.
timeout := time.After(time.Second * 10)
timeout := time.After(time.Second * 60)
var peersOfB []peer.ID
// 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
......
......@@ -64,10 +64,6 @@ type LocalSigner struct {
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 {
return &LocalSigner{priv: priv, hasher: SigningHash}
}
......
......@@ -224,7 +224,13 @@ func (eq *EngineQueue) Step(ctx context.Context) error {
}
outOfData := false
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 {
outOfData = true
} else if err != nil {
......@@ -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,
// and then marks the latest fully derived L2 block from this as finalized,
// or defaults to the current finalized L2 block.
......@@ -279,9 +317,15 @@ func (eq *EngineQueue) postProcessSafeL2() {
L2Block: eq.safeHead,
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 {
// if it's a now 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
// if it's a new L2 block that was derived from the same latest L1 block, then just update the entry
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)
}
}
}
......
......@@ -265,3 +265,575 @@ func TestEngineQueue_Finalize(t *testing.T) {
l1F.AssertExpectations(t)
eng.AssertExpectations(t)
}
func TestEngineQueue_ResetWhenUnsafeOriginNotCanonical(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo)
rng := rand.New(rand.NewSource(1234))
l1Time := uint64(2)
refA := testutils.RandomBlockRef(rng)
refB := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refA.Number + 1,
ParentHash: refA.Hash,
Time: refA.Time + l1Time,
}
refC := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refB.Number + 1,
ParentHash: refB.Hash,
Time: refB.Time + l1Time,
}
refD := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refC.Number + 1,
ParentHash: refC.Hash,
Time: refC.Time + l1Time,
}
refE := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refD.Number + 1,
ParentHash: refD.Hash,
Time: refD.Time + l1Time,
}
refF := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refE.Number + 1,
ParentHash: refE.Hash,
Time: refE.Time + l1Time,
}
refA0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: 0,
ParentHash: common.Hash{},
Time: refA.Time,
L1Origin: refA.ID(),
SequenceNumber: 0,
}
cfg := &rollup.Config{
Genesis: rollup.Genesis{
L1: refA.ID(),
L2: refA0.ID(),
L2Time: refA0.Time,
},
BlockTime: 1,
SeqWindowSize: 2,
}
refA1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refA0.Number + 1,
ParentHash: refA0.Hash,
Time: refA0.Time + cfg.BlockTime,
L1Origin: refA.ID(),
SequenceNumber: 1,
}
refB0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refA1.Number + 1,
ParentHash: refA1.Hash,
Time: refA1.Time + cfg.BlockTime,
L1Origin: refB.ID(),
SequenceNumber: 0,
}
refB1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refB0.Number + 1,
ParentHash: refB0.Hash,
Time: refB0.Time + cfg.BlockTime,
L1Origin: refB.ID(),
SequenceNumber: 1,
}
refC0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refB1.Number + 1,
ParentHash: refB1.Hash,
Time: refB1.Time + cfg.BlockTime,
L1Origin: refC.ID(),
SequenceNumber: 0,
}
refC1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refC0.Number + 1,
ParentHash: refC0.Hash,
Time: refC0.Time + cfg.BlockTime,
L1Origin: refC.ID(),
SequenceNumber: 1,
}
refD0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refC1.Number + 1,
ParentHash: refC1.Hash,
Time: refC1.Time + cfg.BlockTime,
L1Origin: refD.ID(),
SequenceNumber: 0,
}
refD1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refD0.Number + 1,
ParentHash: refD0.Hash,
Time: refD0.Time + cfg.BlockTime,
L1Origin: refD.ID(),
SequenceNumber: 1,
}
refE0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refD1.Number + 1,
ParentHash: refD1.Hash,
Time: refD1.Time + cfg.BlockTime,
L1Origin: refE.ID(),
SequenceNumber: 0,
}
refE1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refE0.Number + 1,
ParentHash: refE0.Hash,
Time: refE0.Time + cfg.BlockTime,
L1Origin: refE.ID(),
SequenceNumber: 1,
}
refF0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refE1.Number + 1,
ParentHash: refE1.Hash,
Time: refE1.Time + cfg.BlockTime,
L1Origin: refF.ID(),
SequenceNumber: 0,
}
refF1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refF0.Number + 1,
ParentHash: refF0.Hash,
Time: refF0.Time + cfg.BlockTime,
L1Origin: refF.ID(),
SequenceNumber: 1,
}
t.Log("refA", refA.Hash)
t.Log("refB", refB.Hash)
t.Log("refC", refC.Hash)
t.Log("refD", refD.Hash)
t.Log("refE", refE.Hash)
t.Log("refF", refF.Hash)
t.Log("refA0", refA0.Hash)
t.Log("refA1", refA1.Hash)
t.Log("refB0", refB0.Hash)
t.Log("refB1", refB1.Hash)
t.Log("refC0", refC0.Hash)
t.Log("refC1", refC1.Hash)
t.Log("refD0", refD0.Hash)
t.Log("refD1", refD1.Hash)
t.Log("refE0", refE0.Hash)
t.Log("refE1", refE1.Hash)
t.Log("refF0", refF0.Hash)
t.Log("refF1", refF1.Hash)
metrics := &testutils.TestDerivationMetrics{}
eng := &testutils.MockEngine{}
// we find the common point to initialize to by comparing the L1 origins in the L2 chain with the L1 chain
l1F := &testutils.MockL1Source{}
eng.ExpectL2BlockRefByLabel(eth.Finalized, refA1, nil)
eng.ExpectL2BlockRefByLabel(eth.Safe, refE0, nil)
eng.ExpectL2BlockRefByLabel(eth.Unsafe, refF1, nil)
// unsafe
l1F.ExpectL1BlockRefByNumber(refF.Number, refF, nil)
eng.ExpectL2BlockRefByHash(refF1.ParentHash, refF0, nil)
eng.ExpectL2BlockRefByHash(refF0.ParentHash, refE1, nil)
// meet previous safe, counts 1/2
l1F.ExpectL1BlockRefByNumber(refE.Number, refE, nil)
eng.ExpectL2BlockRefByHash(refE1.ParentHash, refE0, nil)
eng.ExpectL2BlockRefByHash(refE0.ParentHash, refD1, nil)
// now full seq window, inclusive
l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil)
eng.ExpectL2BlockRefByHash(refD1.ParentHash, refD0, nil)
eng.ExpectL2BlockRefByHash(refD0.ParentHash, refC1, nil)
// now one more L1 origin
l1F.ExpectL1BlockRefByNumber(refC.Number, refC, nil)
eng.ExpectL2BlockRefByHash(refC1.ParentHash, refC0, nil)
// parent of that origin will be considered safe
eng.ExpectL2BlockRefByHash(refC0.ParentHash, refB1, nil)
// and we fetch the L1 origin of that as starting point for engine queue
l1F.ExpectL1BlockRefByHash(refB.Hash, refB, nil)
l1F.ExpectL1BlockRefByHash(refB.Hash, refB, nil)
// and mock a L1 config for the last L2 block that references the L1 starting point
eng.ExpectSystemConfigByL2Hash(refB1.Hash, eth.SystemConfig{
BatcherAddr: common.Address{42},
Overhead: [32]byte{123},
Scalar: [32]byte{42},
GasLimit: 20_000_000,
}, nil)
prev := &fakeAttributesQueue{origin: refE}
eq := NewEngineQueue(logger, cfg, eng, metrics, prev, l1F)
require.ErrorIs(t, eq.Reset(context.Background(), eth.L1BlockRef{}, eth.SystemConfig{}), io.EOF)
require.Equal(t, refB1, eq.SafeL2Head(), "L2 reset should go back to sequence window ago: blocks with origin E and D are not safe until we reconcile, C is extra, and B1 is the end we look for")
require.Equal(t, refB, eq.Origin(), "Expecting to be set back derivation L1 progress to B")
require.Equal(t, refA1, eq.Finalized(), "A1 is recognized as finalized before we run any steps")
// First step after reset will do a fork choice update
require.True(t, eq.needForkchoiceUpdate)
eng.ExpectForkchoiceUpdate(&eth.ForkchoiceState{
HeadBlockHash: eq.unsafeHead.Hash,
SafeBlockHash: eq.safeHead.Hash,
FinalizedBlockHash: eq.finalized.Hash,
}, nil, &eth.ForkchoiceUpdatedResult{PayloadStatus: eth.PayloadStatusV1{Status: eth.ExecutionValid}}, nil)
err := eq.Step(context.Background())
require.NoError(t, err)
require.Equal(t, refF.ID(), eq.unsafeHead.L1Origin, "should have refF as unsafe head origin")
// L1 chain reorgs so new origin is at same slot as refF but on a different fork
prev.origin = eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refF.Number,
ParentHash: refE.Hash,
Time: refF.Time,
}
eq.UnsafeL2Head()
err = eq.Step(context.Background())
require.ErrorIs(t, err, ErrReset, "should reset pipeline due to mismatched origin")
l1F.AssertExpectations(t)
eng.AssertExpectations(t)
}
func TestVerifyNewL1Origin(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo)
rng := rand.New(rand.NewSource(1234))
l1Time := uint64(2)
refA := testutils.RandomBlockRef(rng)
refB := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refA.Number + 1,
ParentHash: refA.Hash,
Time: refA.Time + l1Time,
}
refC := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refB.Number + 1,
ParentHash: refB.Hash,
Time: refB.Time + l1Time,
}
refD := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refC.Number + 1,
ParentHash: refC.Hash,
Time: refC.Time + l1Time,
}
refE := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refD.Number + 1,
ParentHash: refD.Hash,
Time: refD.Time + l1Time,
}
refF := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refE.Number + 1,
ParentHash: refE.Hash,
Time: refE.Time + l1Time,
}
refG := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refF.Number + 1,
ParentHash: refF.Hash,
Time: refF.Time + l1Time,
}
refH := eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refG.Number + 1,
ParentHash: refG.Hash,
Time: refG.Time + l1Time,
}
refA0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: 0,
ParentHash: common.Hash{},
Time: refA.Time,
L1Origin: refA.ID(),
SequenceNumber: 0,
}
cfg := &rollup.Config{
Genesis: rollup.Genesis{
L1: refA.ID(),
L2: refA0.ID(),
L2Time: refA0.Time,
},
BlockTime: 1,
SeqWindowSize: 2,
}
refA1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refA0.Number + 1,
ParentHash: refA0.Hash,
Time: refA0.Time + cfg.BlockTime,
L1Origin: refA.ID(),
SequenceNumber: 1,
}
refB0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refA1.Number + 1,
ParentHash: refA1.Hash,
Time: refA1.Time + cfg.BlockTime,
L1Origin: refB.ID(),
SequenceNumber: 0,
}
refB1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refB0.Number + 1,
ParentHash: refB0.Hash,
Time: refB0.Time + cfg.BlockTime,
L1Origin: refB.ID(),
SequenceNumber: 1,
}
refC0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refB1.Number + 1,
ParentHash: refB1.Hash,
Time: refB1.Time + cfg.BlockTime,
L1Origin: refC.ID(),
SequenceNumber: 0,
}
refC1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refC0.Number + 1,
ParentHash: refC0.Hash,
Time: refC0.Time + cfg.BlockTime,
L1Origin: refC.ID(),
SequenceNumber: 1,
}
refD0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refC1.Number + 1,
ParentHash: refC1.Hash,
Time: refC1.Time + cfg.BlockTime,
L1Origin: refD.ID(),
SequenceNumber: 0,
}
refD1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refD0.Number + 1,
ParentHash: refD0.Hash,
Time: refD0.Time + cfg.BlockTime,
L1Origin: refD.ID(),
SequenceNumber: 1,
}
refE0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refD1.Number + 1,
ParentHash: refD1.Hash,
Time: refD1.Time + cfg.BlockTime,
L1Origin: refE.ID(),
SequenceNumber: 0,
}
refE1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refE0.Number + 1,
ParentHash: refE0.Hash,
Time: refE0.Time + cfg.BlockTime,
L1Origin: refE.ID(),
SequenceNumber: 1,
}
refF0 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refE1.Number + 1,
ParentHash: refE1.Hash,
Time: refE1.Time + cfg.BlockTime,
L1Origin: refF.ID(),
SequenceNumber: 0,
}
refF1 := eth.L2BlockRef{
Hash: testutils.RandomHash(rng),
Number: refF0.Number + 1,
ParentHash: refF0.Hash,
Time: refF0.Time + cfg.BlockTime,
L1Origin: refF.ID(),
SequenceNumber: 1,
}
t.Log("refA", refA.Hash)
t.Log("refB", refB.Hash)
t.Log("refC", refC.Hash)
t.Log("refD", refD.Hash)
t.Log("refE", refE.Hash)
t.Log("refF", refF.Hash)
t.Log("refG", refG.Hash)
t.Log("refH", refH.Hash)
t.Log("refA0", refA0.Hash)
t.Log("refA1", refA1.Hash)
t.Log("refB0", refB0.Hash)
t.Log("refB1", refB1.Hash)
t.Log("refC0", refC0.Hash)
t.Log("refC1", refC1.Hash)
t.Log("refD0", refD0.Hash)
t.Log("refD1", refD1.Hash)
t.Log("refE0", refE0.Hash)
t.Log("refE1", refE1.Hash)
t.Log("refF0", refF0.Hash)
t.Log("refF1", refF1.Hash)
metrics := &testutils.TestDerivationMetrics{}
tests := []struct {
name string
newOrigin eth.L1BlockRef
expectReset bool
expectedFetchBlocks map[uint64]eth.L1BlockRef
}{
{
name: "L1OriginBeforeUnsafeOrigin",
newOrigin: refD,
expectReset: false,
},
{
name: "Matching",
newOrigin: refF,
expectReset: false,
},
{
name: "BlockNumberEqualDifferentHash",
newOrigin: eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refF.Number,
ParentHash: refE.Hash,
Time: refF.Time,
},
expectReset: true,
},
{
name: "UnsafeIsParent",
newOrigin: refG,
expectReset: false,
},
{
name: "UnsafeIsParentNumberDifferentHash",
newOrigin: eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refG.Number,
ParentHash: testutils.RandomHash(rng),
Time: refG.Time,
},
expectReset: true,
},
{
name: "UnsafeIsOlderCanonical",
newOrigin: refH,
expectReset: false,
expectedFetchBlocks: map[uint64]eth.L1BlockRef{
refF.Number: refF,
},
},
{
name: "UnsafeIsOlderNonCanonical",
newOrigin: eth.L1BlockRef{
Hash: testutils.RandomHash(rng),
Number: refH.Number,
ParentHash: testutils.RandomHash(rng),
Time: refH.Time,
},
expectReset: true,
expectedFetchBlocks: map[uint64]eth.L1BlockRef{
// Second look up gets a different block in F's block number due to a reorg
refF.Number: {
Hash: testutils.RandomHash(rng),
Number: refF.Number,
ParentHash: refE.Hash,
Time: refF.Time,
},
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
eng := &testutils.MockEngine{}
// we find the common point to initialize to by comparing the L1 origins in the L2 chain with the L1 chain
l1F := &testutils.MockL1Source{}
eng.ExpectL2BlockRefByLabel(eth.Finalized, refA1, nil)
eng.ExpectL2BlockRefByLabel(eth.Safe, refE0, nil)
eng.ExpectL2BlockRefByLabel(eth.Unsafe, refF1, nil)
// unsafe
l1F.ExpectL1BlockRefByNumber(refF.Number, refF, nil)
eng.ExpectL2BlockRefByHash(refF1.ParentHash, refF0, nil)
eng.ExpectL2BlockRefByHash(refF0.ParentHash, refE1, nil)
for blockNum, block := range test.expectedFetchBlocks {
l1F.ExpectL1BlockRefByNumber(blockNum, block, nil)
}
// meet previous safe, counts 1/2
l1F.ExpectL1BlockRefByNumber(refE.Number, refE, nil)
eng.ExpectL2BlockRefByHash(refE1.ParentHash, refE0, nil)
eng.ExpectL2BlockRefByHash(refE0.ParentHash, refD1, nil)
// now full seq window, inclusive
l1F.ExpectL1BlockRefByNumber(refD.Number, refD, nil)
eng.ExpectL2BlockRefByHash(refD1.ParentHash, refD0, nil)
eng.ExpectL2BlockRefByHash(refD0.ParentHash, refC1, nil)
// now one more L1 origin
l1F.ExpectL1BlockRefByNumber(refC.Number, refC, nil)
eng.ExpectL2BlockRefByHash(refC1.ParentHash, refC0, nil)
// parent of that origin will be considered safe
eng.ExpectL2BlockRefByHash(refC0.ParentHash, refB1, nil)
// and we fetch the L1 origin of that as starting point for engine queue
l1F.ExpectL1BlockRefByHash(refB.Hash, refB, nil)
l1F.ExpectL1BlockRefByHash(refB.Hash, refB, nil)
// and mock a L1 config for the last L2 block that references the L1 starting point
eng.ExpectSystemConfigByL2Hash(refB1.Hash, eth.SystemConfig{
BatcherAddr: common.Address{42},
Overhead: [32]byte{123},
Scalar: [32]byte{42},
GasLimit: 20_000_000,
}, nil)
prev := &fakeAttributesQueue{origin: refE}
eq := NewEngineQueue(logger, cfg, eng, metrics, prev, l1F)
require.ErrorIs(t, eq.Reset(context.Background(), eth.L1BlockRef{}, eth.SystemConfig{}), io.EOF)
require.Equal(t, refB1, eq.SafeL2Head(), "L2 reset should go back to sequence window ago: blocks with origin E and D are not safe until we reconcile, C is extra, and B1 is the end we look for")
require.Equal(t, refB, eq.Origin(), "Expecting to be set back derivation L1 progress to B")
require.Equal(t, refA1, eq.Finalized(), "A1 is recognized as finalized before we run any steps")
// First step after reset will do a fork choice update
require.True(t, eq.needForkchoiceUpdate)
eng.ExpectForkchoiceUpdate(&eth.ForkchoiceState{
HeadBlockHash: eq.unsafeHead.Hash,
SafeBlockHash: eq.safeHead.Hash,
FinalizedBlockHash: eq.finalized.Hash,
}, nil, &eth.ForkchoiceUpdatedResult{PayloadStatus: eth.PayloadStatusV1{Status: eth.ExecutionValid}}, nil)
err := eq.Step(context.Background())
require.NoError(t, err)
require.Equal(t, refF.ID(), eq.unsafeHead.L1Origin, "should have refF as unsafe head origin")
// L1 chain reorgs so new origin is at same slot as refF but on a different fork
prev.origin = test.newOrigin
eq.UnsafeL2Head()
err = eq.Step(context.Background())
if test.expectReset {
require.ErrorIs(t, err, ErrReset, "should reset pipeline due to mismatched origin")
} else {
require.ErrorIs(t, err, io.EOF, "should not reset pipeline")
}
l1F.AssertExpectations(t)
eng.AssertExpectations(t)
})
}
}
package derive
import (
"encoding/json"
"errors"
"fmt"
)
......@@ -44,3 +45,7 @@ func (id ChannelID) String() string {
func (id ChannelID) TerminalString() string {
return fmt.Sprintf("%x..%x", id[:3], id[13:])
}
func (id ChannelID) MarshalJSON() ([]byte, error) {
return json.Marshal(id.String())
}
package crypto
import (
"bytes"
"context"
"crypto/ecdsa"
"errors"
......@@ -56,10 +57,10 @@ func SignerFactoryFromConfig(l log.Logger, privateKey, mnemonic, hdPath string,
fromAddress = common.HexToAddress(signerConfig.Address)
signer = func(chainID *big.Int) SignerFn {
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 signerClient.SignTransaction(ctx, chainID, tx)
return signerClient.SignTransaction(ctx, chainID, address, tx)
}
}
} else {
......
......@@ -122,6 +122,11 @@ func (m *SimpleTxManager) IncreaseGasPrice(ctx context.Context, tx *types.Transa
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
// Enforce a min priceBump on the tip. Do this before the feeCap is calculated
thresholdTip := new(big.Int).Mul(priceBumpPercent, tx.GasTipCap())
......@@ -129,6 +134,7 @@ func (m *SimpleTxManager) IncreaseGasPrice(ctx context.Context, tx *types.Transa
if tx.GasTipCapIntCmp(gasTipCap) >= 0 {
m.l.Debug("Reusing the previous tip", "previous", tx.GasTipCap(), "suggested", gasTipCap)
gasTipCap = tx.GasTipCap()
reusedTip = true
} else if thresholdTip.Cmp(gasTipCap) > 0 {
m.l.Debug("Overriding the tip to enforce a price bump", "previous", tx.GasTipCap(), "suggested", gasTipCap, "new", thresholdTip)
gasTipCap = thresholdTip
......@@ -150,11 +156,16 @@ func (m *SimpleTxManager) IncreaseGasPrice(ctx context.Context, tx *types.Transa
if tx.GasFeeCapIntCmp(gasFeeCap) >= 0 {
m.l.Debug("Reusing the previous fee cap", "previous", tx.GasFeeCap(), "suggested", gasFeeCap)
gasFeeCap = tx.GasFeeCap()
reusedFeeCap = true
} 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)
gasFeeCap = thresholdFeeCap
}
if reusedTip && reusedFeeCap {
return tx, nil
}
rawTx := &types.DynamicFeeTx{
ChainID: tx.ChainId(),
Nonce: tx.Nonce(),
......
......@@ -4,6 +4,7 @@ import (
"context"
"errors"
"math/big"
"math/rand"
"sync"
"testing"
"time"
......@@ -11,9 +12,12 @@ import (
"github.com/stretchr/testify/require"
"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/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
)
......@@ -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.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 (
optls "github.com/ethereum-optimism/optimism/op-service/tls"
"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/core/types"
"github.com/ethereum/go-ethereum/log"
......@@ -91,8 +92,8 @@ func (s *SignerClient) pingVersion() (string, error) {
return v, nil
}
func (s *SignerClient) SignTransaction(ctx context.Context, chainId *big.Int, tx *types.Transaction) (*types.Transaction, error) {
args := NewTransactionArgsFromTransaction(chainId, tx)
func (s *SignerClient) SignTransaction(ctx context.Context, chainId *big.Int, from common.Address, tx *types.Transaction) (*types.Transaction, error) {
args := NewTransactionArgsFromTransaction(chainId, from, tx)
var result hexutil.Bytes
if err := s.client.CallContext(ctx, &result, "eth_signTransaction", args); err != nil {
......
......@@ -31,12 +31,13 @@ type TransactionArgs struct {
}
// 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())
nonce := hexutil.Uint64(tx.Nonce())
gas := hexutil.Uint64(tx.Gas())
accesses := tx.AccessList()
args := &TransactionArgs{
From: &from,
Input: &data,
Nonce: &nonce,
Value: (*hexutil.Big)(tx.Value()),
......
......@@ -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
TODO put a gif here of using it
![preview](./assets/preview.gif)
## 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.
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.
......
# 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
#### `ATTESTATION_STATION_ADDRESS`
The deployment address for the attestation station currently deployed with create2 on Optimism and Optimism Goerli `0xEE36eaaD94d1Cc1d0eccaDb55C38bFfB6Be06C77`.
```typescript
......@@ -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.
```typescript
const key = await createKey(
'i.am.a.key.much.longer.than.32.bytes.long'
......@@ -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.
#### `parseAddress`
Turn bytes into an address.
......
......@@ -6,6 +6,20 @@
"types": "src/index.ts",
"module": "dist/index.cjs",
"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": {
"atst": "./dist/cli.js"
},
......
......@@ -15,13 +15,13 @@ cli
.option('--creator <string>', readOptionsValidators.creator.description!)
.option('--about <string>', readOptionsValidators.about.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),
})
.option('--rpc-url [url]', readOptionsValidators.rpcUrl.description!, {
.option('--rpc-url <url>', readOptionsValidators.rpcUrl.description!, {
default: readOptionsValidators.rpcUrl.parse(undefined),
})
.option('--contract [address]', readOptionsValidators.contract.description!, {
.option('--contract <address>', readOptionsValidators.contract.description!, {
default: readOptionsValidators.contract.parse(undefined),
})
.example(
......@@ -52,17 +52,17 @@ cli
'--private-key <string>',
writeOptionsValidators.privateKey.description!
)
.option('--data-type [string]', readOptionsValidators.dataType.description!, {
.option('--data-type <string>', readOptionsValidators.dataType.description!, {
default: writeOptionsValidators.dataType.parse(undefined),
})
.option('--about <string>', writeOptionsValidators.about.description!)
.option('--key <string>', writeOptionsValidators.key.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),
})
.option(
'--contract [address]',
'--contract <address>',
writeOptionsValidators.contract.description!,
{
default: writeOptionsValidators.contract.parse(undefined),
......
// constants
export { ATTESTATION_STATION_ADDRESS } from './constants/attestationStationAddress'
// lib
export { encodeRawKey } from './lib/encodeRawKey'
export { encodeRawKey, createKey } from './lib/createKey'
export { createValue, stringifyAttestationBytes } from './lib/createValue'
export {
readAttestation,
readAttestationAddress,
......@@ -15,7 +16,6 @@ export { prepareWriteAttestation } from './lib/prepareWriteAttestation'
export { prepareWriteAttestations } from './lib/prepareWriteAttestations'
export { writeAttestation } from './lib/writeAttestation'
export { abi } from './lib/abi'
export { stringifyAttestationBytes } from './lib/stringifyAttestationBytes'
export {
parseAttestationBytes,
parseAddress,
......@@ -28,5 +28,3 @@ export type { AttestationCreatedEvent } from './types/AttestationCreatedEvent'
export type { AttestationReadParams } from './types/AttestationReadParams'
export type { DataTypeOption } from './types/DataTypeOption'
export type { WagmiBytes } from './types/WagmiBytes'
// react
export * from './react'
import { describe, expect, it } from 'vitest'
import { encodeRawKey } from './encodeRawKey'
import { encodeRawKey } from './createKey'
describe(encodeRawKey.name, () => {
it('should return just the raw key if it is less than 32 bytes', () => {
......
......@@ -2,10 +2,24 @@ import { ethers } from 'ethers'
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) {
return ethers.utils.formatBytes32String(rawKey) as WagmiBytes
}
const hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(rawKey))
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 {
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
): WagmiBytes => {
bytes = bytes === '0x' ? '0x0' : bytes
......@@ -33,3 +42,14 @@ export const stringifyAttestationBytes = (
}
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 { Address } from 'wagmi'
import type { Address } from '@wagmi/core'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { abi } from '../lib/abi'
import { AttestationCreatedEvent } from '../types/AttestationCreatedEvent'
import { encodeRawKey } from './encodeRawKey'
import { encodeRawKey } from './createKey'
export const getEvents = async ({
creator = null,
......
......@@ -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
*/
export const parseAttestationBytes = <TDataType extends DataTypeOption>(
......
......@@ -4,13 +4,13 @@ import { formatBytes32String } from 'ethers/lib/utils.js'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { WagmiBytes } from '../types/WagmiBytes'
import { abi } from './abi'
import { stringifyAttestationBytes } from './stringifyAttestationBytes'
import { createValue } from './createValue'
export const prepareWriteAttestation = async (
about: Address,
key: string,
value: string | WagmiBytes | number | boolean,
chainId = 10,
chainId: number | undefined = undefined,
contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => {
let formattedKey: WagmiBytes
......@@ -27,6 +27,6 @@ export const prepareWriteAttestation = async (
abi,
functionName: 'attest',
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'
import { ATTESTATION_STATION_ADDRESS } from '../constants/attestationStationAddress'
import { WagmiBytes } from '../types/WagmiBytes'
import { abi } from './abi'
import { stringifyAttestationBytes } from './stringifyAttestationBytes'
import { createValue } from './createValue'
type Attestation = {
about: Address
......@@ -14,7 +14,7 @@ type Attestation = {
export const prepareWriteAttestations = async (
attestations: Attestation[],
chainId = 10,
chainId: number | undefined = undefined,
contractAddress: Address = ATTESTATION_STATION_ADDRESS
) => {
const formattedAttestations = attestations.map((attestation) => {
......@@ -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`
)
}
const formattedValue = stringifyAttestationBytes(
attestation.value
) as WagmiBytes
const formattedValue = createValue(attestation.value) as WagmiBytes
return {
about: attestation.about,
key: formattedKey,
......
......@@ -19,7 +19,6 @@ import { parseAttestationBytes } from './parseAttestationBytes'
* creator: creatorAddress,
* about: aboutAddress,
* key: 'my_key',
* allowFailure: false,
* },
* {
* creator: creatorAddress2,
......@@ -27,7 +26,6 @@ import { parseAttestationBytes } from './parseAttestationBytes'
* key: 'my_key',
* dataType: 'number',
* contractAddress: '0x1234',
* allowFailure: false,
* },
* )
*/
......@@ -40,7 +38,6 @@ export const readAttestations = async (
about,
key,
contractAddress = ATTESTATION_STATION_ADDRESS,
allowFailure = false,
} = attestation
if (key.length > 32) {
throw new Error(
......@@ -52,7 +49,6 @@ export const readAttestations = async (
abi,
functionName: 'attestations',
args: [creator, about, formatBytes32String(key) as WagmiBytes],
allowFailure,
} as const
})
......
......@@ -11,5 +11,5 @@ export interface AttestationReadParams {
key: string
dataType?: DataTypeOption
contractAddress?: Address
allowFailure?: boolean
chainId?: number
}
import { BigNumber } from 'ethers'
import { Address } from 'wagmi'
import type { Address } from '@wagmi/core'
import { DataTypeOption } from './DataTypeOption'
import { WagmiBytes } from './WagmiBytes'
......
......@@ -10,7 +10,7 @@ export default defineConfig({
*
* @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',
target: 'es2021',
// 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_acrossWords_works() (gas: 1418)
Bytes_slice_Test:test_slice_fromNonZeroIdx_works() (gas: 17154)
Bytes_slice_Test:test_slice_fromZeroIdx_works() (gas: 20694)
Bytes_slice_Test:test_slice_acrossMultipleWords_works() (gas: 9413)
Bytes_slice_Test:test_slice_acrossWords_works() (gas: 1430)
Bytes_slice_Test:test_slice_fromNonZeroIdx_works() (gas: 17240)
Bytes_slice_Test:test_slice_fromZeroIdx_works() (gas: 20826)
Bytes_toNibbles_Test:test_toNibbles_expectedResult128Bytes_works() (gas: 129874)
Bytes_toNibbles_Test:test_toNibbles_expectedResult5Bytes_works() (gas: 6132)
Bytes_toNibbles_Test:test_toNibbles_zeroLengthInput_works() (gas: 944)
......
......@@ -122,6 +122,58 @@ contract Bytes_slice_Test is Test {
vm.expectRevert("slice_overflow");
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 {
......
......@@ -14,6 +14,10 @@ that maintains 1:1 compatibility with Ethereum.
- [L2 Output Root Proposals](proposals.md)
- [Rollup Node](rollup-node.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)
- [Messengers](messengers.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