Commit 620d12cf authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

op-chain-ops: add cross domain utils (#3402)

* op-chain-ops: implement pending withdrawal fetching

We need a list of pending withdrawals to be able to
migrate the withdrawal hashes to the new withdrawal
hash scheme. This commit implements the script for
fetching pending withdrawals as well as a unit test
for the function responsible for fetching pending withdrawals.

From within `op-chain-ops`, the script can be ran as follows:

```golang
$ go run cmd/withdrawals/main.go
```

lint

* op-chain-ops: go mod tidy
Co-authored-by: default avatarmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: default avatarMatthew Slipper <me@matthewslipper.com>
parent fcfcf6e7
package main
import (
"encoding/json"
"errors"
"os"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v2"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
)
// TODO(tynes): handle connecting directly to a LevelDB based StateDB
func main() {
log.Root().SetHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(isatty.IsTerminal(os.Stderr.Fd()))))
app := &cli.App{
Name: "withdrawals",
Usage: "fetches all pending withdrawals",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "l1-rpc-url",
Value: "http://127.0.0.1:8545",
Usage: "RPC URL for an L1 Node",
},
&cli.StringFlag{
Name: "l2-rpc-url",
Value: "http://127.0.0.1:9545",
Usage: "RPC URL for an L2 Node",
},
&cli.StringFlag{
Name: "l1-cross-domain-messenger-address",
Usage: "Address of the L1CrossDomainMessenger",
},
&cli.Uint64Flag{
Name: "start",
Usage: "Start height to search for events",
},
&cli.Uint64Flag{
Name: "end",
Usage: "End height to search for events",
},
&cli.StringFlag{
Name: "outfile",
Usage: "Path to output file",
Value: "out.json",
},
},
Action: func(ctx *cli.Context) error {
l1RpcURL := ctx.String("l1-rpc-url")
l2RpcURL := ctx.String("l2-rpc-url")
l1Client, err := ethclient.Dial(l1RpcURL)
if err != nil {
return err
}
l2Client, err := ethclient.Dial(l2RpcURL)
if err != nil {
return err
}
backends := crossdomain.NewBackends(l1Client, l2Client)
l1xDomainMessenger := ctx.String("l1-cross-domain-messenger-address")
if l1xDomainMessenger == "" {
return errors.New("Must pass in L1CrossDomainMessenger address")
}
l1xDomainMessengerAddr := common.HexToAddress(l1xDomainMessenger)
messengers, err := crossdomain.NewMessengers(backends, l1xDomainMessengerAddr)
if err != nil {
return err
}
start := ctx.Uint64("start")
end := ctx.Uint64("end")
// All messages are expected to be version 0 messages
withdrawals, err := crossdomain.GetPendingWithdrawals(messengers, common.Big0, start, end)
if err != nil {
return err
}
outfile := ctx.String("outfile")
if err := writeJSONFile(outfile, withdrawals); err != nil {
return err
}
return nil
},
}
if err := app.Run(os.Args); err != nil {
log.Crit("error in migration", "err", err)
}
}
func writeJSONFile(outfile string, input interface{}) error {
f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
return enc.Encode(input)
}
package crossdomain
import (
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
// A PendingWithdrawal represents a withdrawal that has
// not been finalized on L1
type PendingWithdrawal struct {
Target common.Address `json:"target"`
Sender common.Address `json:"sender"`
Message []byte `json:"message"`
MessageNonce *big.Int `json:"nonce"`
GasLimit *big.Int `json:"gasLimit"`
TransactionHash common.Hash `json:"transactionHash"`
}
// Backends represents a set of backends for L1 and L2.
// These are used as the backends for the Messengers
type Backends struct {
L1 bind.ContractBackend
L2 bind.ContractBackend
}
func NewBackends(l1, l2 bind.ContractBackend) *Backends {
return &Backends{
L1: l1,
L2: l2,
}
}
// Messengers represents a pair of L1 and L2 cross domain messengers
// that are connected to the correct contract addresses
type Messengers struct {
L1 *bindings.L1CrossDomainMessenger
L2 *bindings.L2CrossDomainMessenger
}
// NewMessengers constructs Messengers. Passing in the address of the
// L1CrossDomainMessenger is required to connect to the
func NewMessengers(backends *Backends, l1CrossDomainMessenger common.Address) (*Messengers, error) {
l1Messenger, err := bindings.NewL1CrossDomainMessenger(l1CrossDomainMessenger, backends.L1)
if err != nil {
return nil, err
}
l2Messenger, err := bindings.NewL2CrossDomainMessenger(predeploys.L2CrossDomainMessengerAddr, backends.L2)
if err != nil {
return nil, err
}
return &Messengers{
L1: l1Messenger,
L2: l2Messenger,
}, nil
}
// GetPendingWithdrawals will fetch pending withdrawals by getting
// L2CrossDomainMessenger `SentMessage` events and then checking to see if the
// cross domain message hash has been finalized on L1. It will return a slice of
// PendingWithdrawals that have not been finalized on L1.
func GetPendingWithdrawals(messengers *Messengers, version *big.Int, start, end uint64) ([]PendingWithdrawal, error) {
withdrawals := make([]PendingWithdrawal, 0)
// This will not take into account "pending" state, this ensures that
// transactions in the mempool are upgraded as well.
opts := bind.FilterOpts{
Start: start,
}
// Only set the end block range if end is non zero. When end is zero, the
// filter will extend to the latest block.
if end != 0 {
opts.End = &end
}
messages, err := messengers.L2.FilterSentMessage(&opts, nil)
if err != nil {
return nil, err
}
defer messages.Close()
for messages.Next() {
event := messages.Event
msg := NewCrossDomainMessage(
event.MessageNonce,
&event.Sender,
&event.Target,
common.Big0,
event.GasLimit,
event.Message,
)
// Optional version check
if version != nil {
if version.Uint64() != msg.Version() {
return nil, fmt.Errorf("expected version %d, got version %d", version, msg.Version())
}
}
hash, err := msg.Hash()
if err != nil {
return nil, err
}
relayed, err := messengers.L1.SuccessfulMessages(&bind.CallOpts{}, hash)
if err != nil {
return nil, err
}
if !relayed {
log.Info("%s not yet relayed", event.Raw.TxHash)
withdrawal := PendingWithdrawal{
Target: event.Target,
Sender: event.Sender,
Message: event.Message,
MessageNonce: event.MessageNonce,
GasLimit: event.GasLimit,
TransactionHash: event.Raw.TxHash,
}
withdrawals = append(withdrawals, withdrawal)
} else {
log.Info("%s already relayed", event.Raw.TxHash)
}
}
return withdrawals, nil
}
package crossdomain_test
import (
"context"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"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/state"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
)
var (
// testKey is the same test key that geth uses
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
// chainID is the chain id used for simulated backends
chainID = big.NewInt(1337)
// testAccount represents the sender account for tests
testAccount = crypto.PubkeyToAddress(testKey.PublicKey)
)
// sendMessageArgs represents the input to `SendMessage`. The value
// is excluded specifically here because we want to simulate v0 messages
// as closely as possible.
type sendMessageArgs struct {
Target common.Address
Message []byte
MinGasLimit uint32
}
// setL1CrossDomainMessenger will set the L1CrossDomainMessenger into
// a state db that represents L1. It accepts a list of "successfulMessages"
// to be placed into the state. This allows for a subset of messages that
// were withdrawn on L2 to be placed into the L1 state to simulate
// a set of withdrawals that are not finalized on L1
func setL1CrossDomainMessenger(db vm.StateDB, successful []common.Hash) error {
bytecode, err := bindings.GetDeployedBytecode("L1CrossDomainMessenger")
if err != nil {
return err
}
db.CreateAccount(predeploys.DevL1CrossDomainMessengerAddr)
db.SetCode(predeploys.DevL1CrossDomainMessengerAddr, bytecode)
msgs := make(map[any]any)
for _, hash := range successful {
msgs[hash] = true
}
return state.SetStorage(
"L1CrossDomainMessenger",
predeploys.DevL1CrossDomainMessengerAddr,
state.StorageValues{
"successfulMessages": msgs,
},
db,
)
}
// setL2CrossDomainMessenger will set the L2CrossDomainMessenger into
// a state db that represents L2. It does not set any state as the only
// function called in this test is "sendMessage" which calls a hardcoded
// address that represents the L2ToL1MessagePasser
func setL2CrossDomainMessenger(db vm.StateDB) error {
bytecode, err := bindings.GetDeployedBytecode("L2CrossDomainMessenger")
if err != nil {
return err
}
db.CreateAccount(predeploys.L2CrossDomainMessengerAddr)
db.SetCode(predeploys.L2CrossDomainMessengerAddr, bytecode)
return state.SetStorage(
"L2CrossDomainMessenger",
predeploys.L2CrossDomainMessengerAddr,
state.StorageValues{
"successfulMessages": map[any]any{},
},
db,
)
}
// setL2ToL1MessagePasser will set the L2ToL1MessagePasser into a state
// db that represents L2. This must be set so the L2CrossDomainMessenger
// can call it as part of "sendMessage"
func setL2ToL1MessagePasser(db vm.StateDB) error {
bytecode, err := bindings.GetDeployedBytecode("L2ToL1MessagePasser")
if err != nil {
return err
}
db.CreateAccount(predeploys.L2ToL1MessagePasserAddr)
db.SetCode(predeploys.L2ToL1MessagePasserAddr, bytecode)
return state.SetStorage(
"L2ToL1MessagePasser",
predeploys.L2ToL1MessagePasserAddr,
state.StorageValues{},
db,
)
}
// sendCrossDomainMessage will send a L2 to L1 cross domain message.
// The state cannot just be set because logs must be generated by
// transaction execution
func sendCrossDomainMessage(
l2xdm *bindings.L2CrossDomainMessenger,
backend *backends.SimulatedBackend,
message *sendMessageArgs,
t *testing.T,
) *crossdomain.CrossDomainMessage {
opts, err := bind.NewKeyedTransactorWithChainID(testKey, chainID)
require.Nil(t, err)
tx, err := l2xdm.SendMessage(opts, message.Target, message.Message, message.MinGasLimit)
require.Nil(t, err)
backend.Commit()
receipt, err := backend.TransactionReceipt(context.Background(), tx.Hash())
require.Nil(t, err)
abi, _ := bindings.L2CrossDomainMessengerMetaData.GetAbi()
var msg crossdomain.CrossDomainMessage
// Ensure that we see the event so that a default CrossDomainMessage
// is not returned
seen := false
// Assume there is only 1 deposit per transaction
for _, log := range receipt.Logs {
event, _ := abi.EventByID(log.Topics[0])
// Not the event we are looking for
if event == nil {
continue
}
// Parse the legacy event
if event.Name == "SentMessage" {
e, _ := l2xdm.ParseSentMessage(*log)
msg.Target = &e.Target
msg.Sender = &e.Sender
msg.Data = e.Message
msg.Nonce = e.MessageNonce
msg.GasLimit = e.GasLimit
// Set seen to true to ensure that this event
// was observed
seen = true
}
// Parse the new extension event
if event.Name == "SentMessageExtension1" {
e, _ := l2xdm.ParseSentMessageExtension1(*log)
msg.Value = e.Value
}
}
require.True(t, seen)
return &msg
}
// TestGetPendingWithdrawals tests the high level function used
// to fetch pending withdrawals
func TestGetPendingWithdrawals(t *testing.T) {
// Create a L2 db
L2db := state.NewMemoryStateDB(nil)
// Set the test account and give it a large balance
L2db.CreateAccount(testAccount)
L2db.AddBalance(testAccount, big.NewInt(10000000000000000))
// Set the L2ToL1MessagePasser in the L2 state
err := setL2ToL1MessagePasser(L2db)
require.Nil(t, err)
// Set the L2CrossDomainMessenger in the L2 state
err = setL2CrossDomainMessenger(L2db)
require.Nil(t, err)
L2 := backends.NewSimulatedBackend(
L2db.Genesis().Alloc,
15000000,
)
L2CrossDomainMessenger, err := bindings.NewL2CrossDomainMessenger(
predeploys.L2CrossDomainMessengerAddr,
L2,
)
require.Nil(t, err)
// Create a set of test data that is made up of cross domain messages.
// There is a total of 6 cross domain messages. 3 of them are set to be
// finalized on L1 so 3 of them will be considered not finalized.
msgs := []*sendMessageArgs{
{
Target: common.Address{},
Message: []byte{},
MinGasLimit: 0,
},
{
Target: common.Address{0x01},
Message: []byte{0x01},
MinGasLimit: 0,
},
{
Target: common.Address{},
Message: []byte{},
MinGasLimit: 100,
},
{
Target: common.Address{19: 0x01},
Message: []byte{0xaa, 0xbb},
MinGasLimit: 10000,
},
{
Target: common.HexToAddress("0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263"),
Message: hexutil.MustDecode("0x095ea7b3000000000000000000000000c92e8bdf79f0507f65a392b0ab4667716bfe01100000000000000000000000000000000000000000000000000000000000000000"),
MinGasLimit: 50000,
},
{
Target: common.HexToAddress("0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"),
Message: []byte{},
MinGasLimit: 70511,
},
}
// For each test cross domain message, call "sendMessage" on the
// L2CrossDomainMessenger and compute the cross domain message hash
hashes := make([]common.Hash, len(msgs))
for i, msg := range msgs {
sent := sendCrossDomainMessage(L2CrossDomainMessenger, L2, msg, t)
hash, err := sent.Hash()
require.Nil(t, err)
hashes[i] = hash
}
// Create a L1 backend with a dev account
L1db := state.NewMemoryStateDB(nil)
L1db.CreateAccount(testAccount)
L1db.AddBalance(testAccount, big.NewInt(10000000000000000))
// Set the L1CrossDomainMessenger into the L1 state. Only set a subset
// of the messages as finalized, the first 3.
err = setL1CrossDomainMessenger(L1db, hashes[0:3])
require.Nil(t, err)
L1 := backends.NewSimulatedBackend(
L1db.Genesis().Alloc,
15000000,
)
backends := crossdomain.NewBackends(L1, L2)
messengers, err := crossdomain.NewMessengers(backends, predeploys.DevL1CrossDomainMessengerAddr)
require.Nil(t, err)
// Fetch the pending withdrawals
withdrawals, err := crossdomain.GetPendingWithdrawals(messengers, nil, 0, 100)
require.Nil(t, err)
// Since only half of the withdrawals were set as finalized on L1,
// the number of pending withdrawals should be 3
require.Equal(t, 3, len(withdrawals))
// The final 3 test cross domain messages should be equal to the
// fetched pending withdrawals. This shows that `GetPendingWithdrawals`
// fetched the correct messages
for i, msg := range msgs[3:] {
withdrawal := withdrawals[i]
require.Equal(t, msg.Target, withdrawal.Target)
require.Equal(t, msg.Message, withdrawal.Message)
require.Equal(t, uint64(msg.MinGasLimit), withdrawal.GasLimit.Uint64())
}
}
...@@ -193,6 +193,7 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 ...@@ -193,6 +193,7 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays=
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
...@@ -574,6 +575,7 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3 ...@@ -574,6 +575,7 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q=
github.com/status-im/keycard-go v0.0.0-20211109104530-b0e0482ba91d h1:vmirMegf1vqPJ+lDBxLQ0MAt3tz+JL57UPxu44JBOjA=
github.com/status-im/keycard-go v0.0.0-20211109104530-b0e0482ba91d/go.mod h1:97vT0Rym0wCnK4B++hNA3nCetr0Mh1KXaVxzSt1arjg= github.com/status-im/keycard-go v0.0.0-20211109104530-b0e0482ba91d/go.mod h1:97vT0Rym0wCnK4B++hNA3nCetr0Mh1KXaVxzSt1arjg=
github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570 h1:gIlAHnH1vJb5vwEjIp5kBj/eu99p/bl0Ay2goiPe5xE= github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570 h1:gIlAHnH1vJb5vwEjIp5kBj/eu99p/bl0Ay2goiPe5xE=
github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570/go.mod h1:8OR4w3TdeIHIh1g6EMY5p0gVNOovcWC+1vpc7naMuAw= github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570/go.mod h1:8OR4w3TdeIHIh1g6EMY5p0gVNOovcWC+1vpc7naMuAw=
...@@ -608,6 +610,7 @@ github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq// ...@@ -608,6 +610,7 @@ github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
...@@ -818,6 +821,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb ...@@ -818,6 +821,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
......
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