diff --git a/go.mod b/go.mod index 5f7e4a5af3..c5f5e8a4f0 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/ipfs/go-libipfs v0.6.2 github.com/ipfs/interface-go-ipfs-core v0.11.0 github.com/ipfs/kubo v0.19.1 + github.com/jarcoal/httpmock v1.3.1 github.com/knadh/koanf v1.4.0 github.com/libp2p/go-libp2p v0.27.8 github.com/multiformats/go-multiaddr v0.9.0 diff --git a/go.sum b/go.sum index 938b5851c0..faef3f7a07 100644 --- a/go.sum +++ b/go.sum @@ -834,6 +834,8 @@ github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQ github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jbenet/go-cienv v0.0.0-20150120210510-1bb1476777ec/go.mod h1:rGaEvXB4uRSZMmzKNLoXvTu1sfx+1kv/DojUlPrSZGs= github.com/jbenet/go-cienv v0.1.0 h1:Vc/s0QbQtoxX8MwwSLWWh+xNNZvM3Lw7NsTcHrvvhMc= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= @@ -1191,6 +1193,7 @@ github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpe github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= diff --git a/system_tests/espresso_test.go b/system_tests/espresso_test.go new file mode 100644 index 0000000000..acf3440569 --- /dev/null +++ b/system_tests/espresso_test.go @@ -0,0 +1,276 @@ +package arbtest + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "net/http" + "testing" + "time" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/jarcoal/httpmock" + "github.com/offchainlabs/nitro/arbos/espresso" + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/validator/server_api" + "github.com/offchainlabs/nitro/validator/valnode" +) + +var ( + validationPort = 54320 + broadcastPort = 9642 +) + +func espresso_block_txs_generators(t *testing.T, l2Info *BlockchainTestInfo) map[int][][]byte { + return map[int][][]byte{ + 5: onlyMalformedTxs(t), + 15: userTxs(t, l2Info), + 25: func(t *testing.T) [][]byte { + // Contains malformed txes, valid transactions and invalid transactions + r := [][]byte{} + r = append(r, onlyMalformedTxs(t)...) + r = append(r, userTxs(t, l2Info)...) + return r + }(t), + } +} + +func onlyMalformedTxs(t *testing.T) [][]byte { + return [][]byte{ + {1, 2, 3}, + {1, 2, 3}, + {4, 5, 6}, + {1, 2, 4}, + } +} + +// Two valid transactions and two invalid transactions with invalid nonces +func userTxs(t *testing.T, l2Info *BlockchainTestInfo) [][]byte { + tx1 := l2Info.PrepareTx("Faucet", "Owner", 3e7, big.NewInt(1e16), nil) + tx1Bin, err := json.Marshal(tx1) + if err != nil { + panic(err) + } + tx2 := l2Info.PrepareTx("Owner", "Faucet", 3e7, big.NewInt(1e16), nil) + tx2Bin, err := json.Marshal(tx2) + if err != nil { + panic(err) + } + // 2 valid transactions here + return [][]byte{ + tx1Bin, + tx1Bin, + tx2Bin, + tx2Bin, + } +} + +func createMockHotShot(ctx context.Context, t *testing.T, l2Info *BlockchainTestInfo) (func(), int) { + httpmock.Activate() + + httpmock.RegisterResponder( + "GET", + `=~http://127.0.0.1:50000/availability/header/(\d+)`, + func(req *http.Request) (*http.Response, error) { + log.Info("GET", "url", req.URL) + block := uint64(httpmock.MustGetSubmatchAsUint(req, 1)) + header := espresso.Header{ + // Since we don't realize the validation of espresso yet, + // mock a simple nmt root here + // See: arbos/espresso/nmt.go + TransactionsRoot: espresso.NmtRoot{Root: []byte{}}, + Metadata: espresso.Metadata{ + L1Head: block, + Timestamp: uint64(time.Now().Unix()), + }, + } + return httpmock.NewJsonResponse(200, header) + }) + + generators := espresso_block_txs_generators(t, l2Info) + + httpmock.RegisterResponder( + "GET", + `=~http://127.0.0.1:50000/availability/block/(\d+)/namespace/100`, + func(req *http.Request) (*http.Response, error) { + txes := []espresso.Transaction{} + block := int(httpmock.MustGetSubmatchAsInt(req, 1)) + data, ok := generators[block] + // Since we don't realize the validation of espresso yet, + // we can mock the proof easily. + // See: arbos/espresso/nmt.go + dummyProof, _ := json.Marshal(map[int]int{0: 0}) + if block > 100 { + // make the debug message cleaner + return httpmock.NewJsonResponse(404, 0) + } + log.Info("GET", "url", req.URL) + if !ok { + r := espresso.NamespaceResponse{ + Proof: (*json.RawMessage)(&dummyProof), + Transactions: &[]espresso.Transaction{}, + } + return httpmock.NewJsonResponse(200, r) + } + for _, rawTx := range data { + tx := espresso.Transaction{ + Vm: 100, + Payload: rawTx, + } + txes = append(txes, tx) + } + resp := espresso.NamespaceResponse{ + Proof: (*json.RawMessage)(&dummyProof), + Transactions: &txes, + } + return httpmock.NewJsonResponse(200, resp) + }) + + return httpmock.DeactivateAndReset, len(generators) +} + +func createL2Node(ctx context.Context, t *testing.T, hotshot_url string) (*TestClient, info, func()) { + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + builder.takeOwnership = false + builder.nodeConfig.DelayedSequencer.Enable = true + builder.nodeConfig.Sequencer = true + builder.nodeConfig.Espresso = true + builder.execConfig.Sequencer.Enable = true + builder.execConfig.Sequencer.Espresso = true + builder.execConfig.Sequencer.EspressoNamespace = 100 + builder.execConfig.Sequencer.HotShotUrl = hotshot_url + + builder.nodeConfig.Feed.Output.Enable = true + builder.nodeConfig.Feed.Output.Port = fmt.Sprintf("%d", broadcastPort) + + cleanup := builder.Build(t) + return builder.L2, builder.L2Info, cleanup +} + +func createValidatorAndPosterNode(ctx context.Context, t *testing.T) (*TestClient, func()) { + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + builder.nodeConfig.Feed.Input.URL = []string{fmt.Sprintf("ws://127.0.0.1:%d", broadcastPort)} + builder.nodeConfig.BatchPoster.Enable = true + builder.nodeConfig.BlockValidator.Enable = true + builder.nodeConfig.BlockValidator.ValidationServer.URL = fmt.Sprintf("ws://127.0.0.1:%d", validationPort) + cleanup := builder.Build(t) + return builder.L2, cleanup +} + +func createValidationNode(ctx context.Context, t *testing.T) func() { + stackConf := node.DefaultConfig + stackConf.HTTPPort = 0 + stackConf.DataDir = "" + stackConf.WSHost = "127.0.0.1" + stackConf.WSPort = validationPort + stackConf.WSModules = []string{server_api.Namespace} + stackConf.P2P.NoDiscovery = true + stackConf.P2P.ListenAddr = "" + + valnode.EnsureValidationExposedViaAuthRPC(&stackConf) + config := &valnode.TestValidationConfig + + stack, err := node.New(&stackConf) + Require(t, err) + + configFetcher := func() *valnode.Config { return config } + valnode, err := valnode.CreateValidationNode(configFetcher, stack, nil) + Require(t, err) + + err = stack.Start() + Require(t, err) + + err = valnode.Start(ctx) + Require(t, err) + + go func() { + <-ctx.Done() + stack.Close() + }() + + return func() { + valnode.GetExec().Stop() + stack.Close() + } + +} + +func waitFor(t *testing.T, ctxinput context.Context, condition func() bool) error { + ctx, cancel := context.WithTimeout(ctxinput, 30*time.Second) + defer cancel() + + for { + if condition() { + return nil + } + select { + case <-time.After(time.Second): + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func TestEspresso(t *testing.T) { + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + l2Node, l2Info, cleanL2Node := createL2Node(ctx, t, "http://127.0.0.1:50000") + defer cleanL2Node() + + cleanHotShot, blockCnt := createMockHotShot(ctx, t, l2Info) + defer cleanHotShot() + + // An initial message for genesis block and every non-empty espresso block + // should lead to a message + expectedMsgCnt := 1 + blockCnt + + err := waitFor(t, ctx, func() bool { + cnt, err := l2Node.ConsensusNode.TxStreamer.GetMessageCount() + if err != nil { + panic(err) + } + expected := arbutil.MessageIndex(expectedMsgCnt) + return cnt >= expected + }) + Require(t, err) + + cleanValNode := createValidationNode(ctx, t) + defer cleanValNode() + + node, cleanup := createValidatorAndPosterNode(ctx, t) + defer cleanup() + + // Check the validated message + err = waitFor(t, ctx, func() bool { + cnt := node.ConsensusNode.BlockValidator.Validated(t) + expected := arbutil.MessageIndex(expectedMsgCnt) + return cnt >= expected + }) + Require(t, err) + + blockNum, err := l2Node.Client.BlockNumber(ctx) + Require(t, err) + + if blockNum != uint64(blockCnt) { + Fatal(t, "every non-empty espresso block should lead to one L2 block") + } + + block2, err := l2Node.Client.BlockByNumber(ctx, big.NewInt(2)) + Require(t, err) + + // Every arbitrum block has one internal tx + if len(block2.Body().Transactions) != 3 { + Fatal(t, "block 2 should contain 2 valid transactions") + } + + block3, err := l2Node.Client.BlockByNumber(ctx, big.NewInt(3)) + Require(t, err) + + if len(block3.Body().Transactions) != 3 { + Fatal(t, "block 3 should contain 2 valid transactions") + } +}