diff --git a/clients/op-erigon/entrypoint.sh b/clients/op-erigon/entrypoint.sh index 502665d47b..72026ea8f6 100644 --- a/clients/op-erigon/entrypoint.sh +++ b/clients/op-erigon/entrypoint.sh @@ -54,7 +54,8 @@ set -u --authrpc.addr=0.0.0.0 \ --nodiscover \ --no-downloader \ - --maxpeers=0 \ + --maxpeers=50 \ + --nat extip:`hostname -i` \ --miner.gaslimit=$GAS_LIMIT \ --networkid=$CHAIN_ID \ $EXTRA_FLAGS \ diff --git a/optimism/helper.go b/optimism/helper.go index e2bb48de77..9f35464303 100644 --- a/optimism/helper.go +++ b/optimism/helper.go @@ -103,3 +103,35 @@ func WaitReceipt(ctx context.Context, client *ethclient.Client, hash common.Hash return receipt, nil } } + +func WaitPendingTransactionFromTxPool(ctx context.Context, client *OpL2EngineExtended, sender common.Address, tx *types.Transaction) (bool, error) { + nonce := fmt.Sprintf("%d", tx.Nonce()) + checkInclusion := func(pendingTxs map[string]*RPCTransactionHash) bool { + if len(pendingTxs) == 0 { + return false + } else if rpcTxHash, ok := pendingTxs[nonce]; !ok { + return false + } else if !bytes.Equal(rpcTxHash.Hash.Bytes(), tx.Hash().Bytes()) { + return false + } + return true + } + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + for { + txpoolcontent, err := client.TxPoolContent(ctx) + if err != nil { + return false, err + } + pendingTxs := txpoolcontent["pending"][sender.String()] + if !checkInclusion(pendingTxs) { + select { + case <-ctx.Done(): + return false, ctx.Err() + case <-ticker.C: + continue + } + } + return true, nil + } +} diff --git a/optimism/nodes.go b/optimism/nodes.go index 2764d718da..e3474be815 100644 --- a/optimism/nodes.go +++ b/optimism/nodes.go @@ -1,12 +1,17 @@ package optimism import ( + "context" "crypto/ecdsa" "fmt" + "github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/p2p" "github.com/ethereum-optimism/optimism/op-node/sources" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" + gethp2p "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/hive/hivesim" ) @@ -65,6 +70,60 @@ type OpL2Engine struct { ELNode } +// temporal extenstion of OpL2Engine +// Direct RPC wrapper will be eventually moved to op-erigon or op-geth +type OpL2EngineExtended struct { + *OpL2Engine +} + +func (e *OpL2EngineExtended) NodeInfo(ctx context.Context) (*gethp2p.NodeInfo, error) { + var output *gethp2p.NodeInfo + err := e.RPC().CallContext(ctx, &output, "admin_nodeInfo") + return output, err +} + +func (e *OpL2EngineExtended) Peers(ctx context.Context) (*gethp2p.PeerInfo, error) { + var output *gethp2p.PeerInfo + err := e.RPC().CallContext(ctx, &output, "admin_peers") + return output, err +} + +func (e *OpL2EngineExtended) AddPeer(ctx context.Context, enode string) (bool, error) { + var output bool + err := e.RPC().CallContext(ctx, &output, "admin_addPeer", enode) + return output, err +} + +// only interested in txhash for p2p testing +// will be updated to use op-geth's RPCTransaction struct +type RPCTransactionHash struct { + Hash common.Hash `json:"hash"` +} + +func (e *OpL2EngineExtended) TxPoolContent(ctx context.Context) (map[string]map[string]map[string]*RPCTransactionHash, error) { + // only return necessary information(tx hash) for testing tx gossip + var output map[string]map[string]map[string]*RPCTransactionHash + err := e.RPC().CallContext(ctx, &output, "txpool_content") + return output, err +} + +func (e *OpL2EngineExtended) ConnectPeer(ctx context.Context, neighbor *OpL2EngineExtended) error { + nodeInfo, err := neighbor.NodeInfo(ctx) + if err != nil { + return err + } + // sanity check by parsing result + _, err = enode.Parse(enode.ValidSchemes, nodeInfo.Enode) + if err != nil { + return err + } + _, err = e.AddPeer(ctx, nodeInfo.Enode) + if err != nil { + return err + } + return nil +} + type OpNode struct { *hivesim.Client p2pKey *ecdsa.PrivateKey diff --git a/simulators/optimism/p2p/main.go b/simulators/optimism/p2p/main.go index 8b67241538..8eb526e319 100644 --- a/simulators/optimism/p2p/main.go +++ b/simulators/optimism/p2p/main.go @@ -38,11 +38,86 @@ func main() { Description: `This test verifies that tx forwarding works`, Run: func(t *hivesim.T) { txForwardingTest(t) }, }) + suite.Add(&hivesim.TestSpec{ + Name: "tx gossiping", + Description: `This test verifies that tx gossiping works`, + Run: func(t *hivesim.T) { txGossipingTest(t) }, + }) sim := hivesim.New() hivesim.MustRunSuite(sim, suite) } +// txGossipingTest verifies that a transaction submitted to a replica execution client +// with tx gossiping enabled shows up on the sequencer tx pool. +func txGossipingTest(t *hivesim.T) { + d := optimism.NewDevnet(t) + sender := d.L2Vault.GenerateKey() + receiver := d.L2Vault.GenerateKey() + + d.InitChain(30, 4, 30, core.GenesisAlloc{sender: {Balance: big.NewInt(params.Ether)}}) + d.AddEth1() + d.WaitUpEth1(0, time.Second*10) + + d.AddOpL2() + d.AddOpNode(0, 0, false) + seqNode := d.GetOpL2Engine(0) + + d.AddOpL2() + d.AddOpNode(0, 1, false) + verifNode := d.GetOpL2Engine(1) + verifClient := verifNode.EthClient() + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + d.WaitUpOpL2Engine(0, time.Second*10) + wg.Done() + }() + go func() { + d.WaitUpOpL2Engine(1, time.Second*10) + wg.Done() + }() + + t.Log("waiting for nodes to come up") + wg.Wait() + + t.Logf("peering execution clients %s with %s", seqNode.IP.String(), verifNode.IP.String()) + seqNodeExtended := &optimism.OpL2EngineExtended{OpL2Engine: seqNode} + verifNodeExtended := &optimism.OpL2EngineExtended{OpL2Engine: verifNode} + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + require.NoError(t, seqNodeExtended.ConnectPeer(ctx, verifNodeExtended)) + + baseTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: optimism.L2ChainIDBig, + Nonce: 0, + To: &receiver, + Gas: 75000, + GasTipCap: big.NewInt(10 * params.GWei), + GasFeeCap: big.NewInt(20 * params.GWei), + Value: big.NewInt(0.0001 * params.Ether), + }) + + tx, err := d.L2Vault.SignTransaction(sender, baseTx) + require.Nil(t, err) + ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + require.NoError(t, verifClient.SendTransaction(ctx, tx)) + t.Log("sent tx to verifier, waiting for tx gossip") + + <-time.After(10 * time.Second) + + ctx, cancel = context.WithTimeout(context.Background(), 180*time.Second) + defer cancel() + found, err := optimism.WaitPendingTransactionFromTxPool(ctx, seqNodeExtended, sender, tx) + if !found { + t.Fatal("transaction not gossiped", "err", err) + } + t.Log("found gossiped transaction on sequencer") +} + // txForwardingTest verifies that a transaction submitted to a replica with tx forwarding enabled shows up on the sequencer. // TODO: The transaction shows up with `getTransaction`, but it remains pending and is not mined for some reason. // This is weird, but fine because it still shows that the transaction is received by the sequencer.