Skip to content

Commit

Permalink
Merge pull request #5075 from ethereum-optimism/jg/batch_decoder_forc…
Browse files Browse the repository at this point in the history
…e_close

batch_decoder: Force close channels
  • Loading branch information
mergify[bot] authored Mar 16, 2023
2 parents 60129ad + c03a2fe commit fc889ee
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 11 deletions.
11 changes: 10 additions & 1 deletion op-node/cmd/batch_decoder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
41 changes: 41 additions & 0 deletions op-node/cmd/batch_decoder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 15 additions & 10 deletions op-node/cmd/batch_decoder/reassemble/reassemble.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
}
Expand Down
55 changes: 55 additions & 0 deletions op-node/rollup/derive/channel_out.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
67 changes: 67 additions & 0 deletions op-node/rollup/derive/channel_out_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
}

0 comments on commit fc889ee

Please sign in to comment.