Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests for invalid transaction handling #18

Merged
merged 1 commit into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
276 changes: 276 additions & 0 deletions system_tests/espresso_test.go
Original file line number Diff line number Diff line change
@@ -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),
Copy link

@nomaxg nomaxg Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this might be cleaner with some sort mock Txn Interface:

type MockTxn struct {
    Type MockTxnType // Valid or invalid
     Amount uint64
     ...etc
    
}

Then you can build test cases with arrays of MockTxnTemplates and create a single function that parses them into the correct txn bytes

Copy link
Member

@ImJeremyHe ImJeremyHe Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point. But currently mocking txs is still not easy. There is no any readily available functions to mock the malformed and invalid txs and I don't have any good idea about how to implement this interface yet. I think it might be better to optimize here after we know the test system well.

Copy link
Author

@sveitser sveitser Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link

@nomaxg nomaxg Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that generators (or whatever we end up using to generate txns) can do this wrapping - we can even hardcode the VM ID

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
sveitser marked this conversation as resolved.
Show resolved Hide resolved
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
ImJeremyHe marked this conversation as resolved.
Show resolved Hide resolved

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")
}
}
Loading