From 665f5269caaf0565d4a50c6d42ffe1aaff8f67b2 Mon Sep 17 00:00:00 2001 From: Joshua Gutow Date: Tue, 14 Mar 2023 07:51:07 -0800 Subject: [PATCH] batch_decoder: Force close transactions --- op-node/cmd/batch_decoder/README.md | 11 ++- op-node/cmd/batch_decoder/main.go | 41 ++++++++++++ .../batch_decoder/reassemble/reassemble.go | 25 ++++--- op-node/rollup/derive/channel_out.go | 55 +++++++++++++++ op-node/rollup/derive/channel_out_test.go | 67 +++++++++++++++++++ 5 files changed, 188 insertions(+), 11 deletions(-) diff --git a/op-node/cmd/batch_decoder/README.md b/op-node/cmd/batch_decoder/README.md index 5c3b7639e..d2454ec3d 100644 --- a/op-node/cmd/batch_decoder/README.md +++ b/op-node/cmd/batch_decoder/README.md @@ -26,6 +26,16 @@ the transaction hash. into channels. It then stores the channels with metadata on disk where the file name is the Channel ID. +### Force Close + +`batch_decoder force-close` will create a transaction data that can be sent from the batcher address to +the batch inbox address which will force close the given channels. This will allow future channels to +be read without waiting for the channel timeout. It uses uses the results from `batch_decoder fetch` to +create the close transaction because the transaction it creates for a specific channel requires information +about if the channel has been closed or not. If it has been closed already but is missing specific frames +those frames need to be generated differently than simply closing the channel. + + ## JQ Cheat Sheet `jq` is a really useful utility for manipulating JSON files. @@ -48,7 +58,6 @@ jq "select(.is_ready == false)|[.id, .frames[0].inclusion_block, .frames[0].tran ## Roadmap - Parallel transaction fetching (CLI-3563) -- Create force-close channel tx data from channel ID (CLI-3564) - Pull the batches out of channels & store that information inside the ChannelWithMetadata (CLI-3565) - Transaction Bytes used - Total uncompressed (different from tx bytes) + compressed bytes diff --git a/op-node/cmd/batch_decoder/main.go b/op-node/cmd/batch_decoder/main.go index 552f88aa3..d63b32de6 100644 --- a/op-node/cmd/batch_decoder/main.go +++ b/op-node/cmd/batch_decoder/main.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/cmd/batch_decoder/fetch" "github.com/ethereum-optimism/optimism/op-node/cmd/batch_decoder/reassemble" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/urfave/cli" @@ -113,6 +114,46 @@ func main() { return nil }, }, + { + Name: "force-close", + Usage: "Create the tx data which will force close a channel", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Required: true, + Usage: "ID of the channel to close", + }, + cli.StringFlag{ + Name: "inbox", + Value: "0x0000000000000000000000000000000000000000", + Usage: "(Optional) Batch Inbox Address", + }, + cli.StringFlag{ + Name: "in", + Value: "/tmp/batch_decoder/transactions_cache", + Usage: "Cache directory for the found transactions", + }, + }, + Action: func(cliCtx *cli.Context) error { + var id derive.ChannelID + if err := (&id).UnmarshalText([]byte(cliCtx.String("id"))); err != nil { + log.Fatal(err) + } + frames := reassemble.LoadFrames(cliCtx.String("in"), common.HexToAddress(cliCtx.String("inbox"))) + var filteredFrames []derive.Frame + for _, frame := range frames { + if frame.Frame.ID == id { + filteredFrames = append(filteredFrames, frame.Frame) + } + } + data, err := derive.ForceCloseTxData(filteredFrames) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%x\n", data) + return nil + }, + }, } if err := app.Run(os.Args); err != nil { diff --git a/op-node/cmd/batch_decoder/reassemble/reassemble.go b/op-node/cmd/batch_decoder/reassemble/reassemble.go index a92c0363d..79b67dc04 100644 --- a/op-node/cmd/batch_decoder/reassemble/reassemble.go +++ b/op-node/cmd/batch_decoder/reassemble/reassemble.go @@ -38,14 +38,8 @@ type Config struct { OutDirectory string } -// Channels loads all transactions from the given input directory that are submitted to the -// specified batch inbox and then re-assembles all channels & writes the re-assembled channels -// to the out directory. -func Channels(config Config) { - if err := os.MkdirAll(config.OutDirectory, 0750); err != nil { - log.Fatal(err) - } - txns := loadTransactions(config.InDirectory, config.BatchInbox) +func LoadFrames(directory string, inbox common.Address) []FrameWithMetadata { + txns := loadTransactions(directory, inbox) // Sort first by block number then by transaction index inside the block number range. // This is to match the order they are processed in derivation. sort.Slice(txns, func(i, j int) bool { @@ -56,7 +50,17 @@ func Channels(config Config) { } }) - frames := transactionsToFrames(txns) + return transactionsToFrames(txns) +} + +// Channels loads all transactions from the given input directory that are submitted to the +// specified batch inbox and then re-assembles all channels & writes the re-assembled channels +// to the out directory. +func Channels(config Config) { + if err := os.MkdirAll(config.OutDirectory, 0750); err != nil { + log.Fatal(err) + } + frames := LoadFrames(config.InDirectory, config.BatchInbox) framesByChannel := make(map[derive.ChannelID][]FrameWithMetadata) for _, frame := range frames { framesByChannel[frame.Frame.ID] = append(framesByChannel[frame.Frame.ID], frame) @@ -143,6 +147,7 @@ func transactionsToFrames(txns []fetch.TransactionWithMetadata) []FrameWithMetad return out } +// if inbox is the zero address, it will load all frames func loadTransactions(dir string, inbox common.Address) []fetch.TransactionWithMetadata { files, err := os.ReadDir(dir) if err != nil { @@ -152,7 +157,7 @@ func loadTransactions(dir string, inbox common.Address) []fetch.TransactionWithM for _, file := range files { f := path.Join(dir, file.Name()) txm := loadTransactionsFile(f) - if txm.InboxAddr == inbox && txm.ValidSender { + if (inbox == common.Address{} || txm.InboxAddr == inbox) && txm.ValidSender { out = append(out, txm) } } diff --git a/op-node/rollup/derive/channel_out.go b/op-node/rollup/derive/channel_out.go index fd5657158..feef8ae7c 100644 --- a/op-node/rollup/derive/channel_out.go +++ b/op-node/rollup/derive/channel_out.go @@ -213,3 +213,58 @@ func BlockToBatch(block *types.Block) (*BatchData, error) { }, }, nil } + +// ForceCloseTxData generates the transaction data for a transaction which will force close +// a channel. It should be given every frame of that channel which has been submitted on +// chain. The frames should be given in order that they appear on L1. +func ForceCloseTxData(frames []Frame) ([]byte, error) { + if len(frames) == 0 { + return nil, errors.New("must provide at least one frame") + } + frameNumbers := make(map[uint16]struct{}) + id := frames[0].ID + closeNumber := uint16(0) + closed := false + for i, frame := range frames { + if !closed && frame.IsLast { + closeNumber = frame.FrameNumber + } + closed = closed || frame.IsLast + frameNumbers[frame.FrameNumber] = struct{}{} + if frame.ID != id { + return nil, fmt.Errorf("invalid ID in list: first ID: %v, %vth ID: %v", id, i, frame.ID) + } + } + + var out bytes.Buffer + out.WriteByte(DerivationVersion0) + + if !closed { + f := Frame{ + ID: id, + FrameNumber: 0, + Data: nil, + IsLast: true, + } + if err := f.MarshalBinary(&out); err != nil { + return nil, err + } + } else { + for i := uint16(0); i <= closeNumber; i++ { + if _, ok := frameNumbers[i]; ok { + continue + } + f := Frame{ + ID: id, + FrameNumber: i, + Data: nil, + IsLast: false, + } + if err := f.MarshalBinary(&out); err != nil { + return nil, err + } + } + } + + return out.Bytes(), nil +} diff --git a/op-node/rollup/derive/channel_out_test.go b/op-node/rollup/derive/channel_out_test.go index 42e927039..ea6eeac36 100644 --- a/op-node/rollup/derive/channel_out_test.go +++ b/op-node/rollup/derive/channel_out_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/require" @@ -49,3 +50,69 @@ func TestRLPByteLimit(t *testing.T) { require.Equal(t, err, rlp.ErrValueTooLarge) require.Equal(t, out2, "") } + +func TestForceCloseTxData(t *testing.T) { + id := [16]byte{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef} + tests := []struct { + frames []Frame + errors bool + output string + }{ + { + frames: []Frame{}, + errors: true, + output: "", + }, + { + frames: []Frame{Frame{FrameNumber: 0, IsLast: false}, Frame{ID: id, FrameNumber: 1, IsLast: true}}, + errors: true, + output: "", + }, + { + frames: []Frame{Frame{ID: id, FrameNumber: 0, IsLast: false}}, + errors: false, + output: "00deadbeefdeadbeefdeadbeefdeadbeef00000000000001", + }, + { + frames: []Frame{Frame{ID: id, FrameNumber: 0, IsLast: true}}, + errors: false, + output: "00", + }, + { + frames: []Frame{Frame{ID: id, FrameNumber: 1, IsLast: false}}, + errors: false, + output: "00deadbeefdeadbeefdeadbeefdeadbeef00000000000001", + }, + { + frames: []Frame{Frame{ID: id, FrameNumber: 1, IsLast: true}}, + errors: false, + output: "00deadbeefdeadbeefdeadbeefdeadbeef00000000000000", + }, + { + frames: []Frame{Frame{ID: id, FrameNumber: 2, IsLast: true}}, + errors: false, + output: "00deadbeefdeadbeefdeadbeefdeadbeef00000000000000deadbeefdeadbeefdeadbeefdeadbeef00010000000000", + }, + { + frames: []Frame{Frame{ID: id, FrameNumber: 1, IsLast: false}, Frame{ID: id, FrameNumber: 3, IsLast: true}}, + errors: false, + output: "00deadbeefdeadbeefdeadbeefdeadbeef00000000000000deadbeefdeadbeefdeadbeefdeadbeef00020000000000", + }, + { + frames: []Frame{Frame{ID: id, FrameNumber: 1, IsLast: false}, Frame{ID: id, FrameNumber: 3, IsLast: true}, Frame{ID: id, FrameNumber: 5, IsLast: true}}, + errors: false, + output: "00deadbeefdeadbeefdeadbeefdeadbeef00000000000000deadbeefdeadbeefdeadbeefdeadbeef00020000000000", + }, + } + + for i, test := range tests { + out, err := ForceCloseTxData(test.frames) + if test.errors { + require.NotNil(t, err, "Should error on tc %v", i) + require.Nil(t, out, "Should return no value in tc %v", i) + } else { + require.NoError(t, err, "Should not error on tc %v", i) + require.Equal(t, common.FromHex(test.output), out, "Should match output tc %v", i) + } + } +}