package beaconclient_test import ( "context" "encoding/hex" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/pkg/errors" zcommon "github.com/protolambda/zrnt/eth2/beacon/common" "github.com/protolambda/zrnt/eth2/configs" "github.com/protolambda/ztyp/tree" log "github.com/sirupsen/logrus" "github.com/vulcanize/ipld-eth-beacon-indexer/pkg/beaconclient" "github.com/vulcanize/ipld-eth-beacon-indexer/pkg/database/sql" "math/big" "os" "strconv" "time" ) var _ = Describe("e2emerge", Label("e2e"), func() { e2eConfig := TestConfig e2eConfig.port = getEnvInt("TEST_E2E_LIGHTHOUSE_PORT", 5052) e2eConfig.performBeaconStateProcessing = false e2eConfig.performBeaconBlockProcessing = true level, _ := log.ParseLevel("debug") log.SetLevel(level) Describe("Run the application against a Merge testnet", func() { Context("When we send a TX to geth", func() { It("We should see the TX included in the ExecutionPayload of a BeaconBlock", func() { bc := setUpTest(e2eConfig, "0") go bc.CaptureHead() tx, _ := sendTestTx() beaconBlock := waitForTxToBeIndexed(bc.Db, tx) Expect(beaconBlock).ToNot(BeNil()) }) }) }) }) type SentTx struct { hash string raw []byte blockNo uint64 blockHash string txIndex uint } func (tx *SentTx) RawHex() string { return "0x" + hex.EncodeToString(tx.raw) } func waitForTxToBeIndexed(db sql.Database, tx *SentTx) *beaconclient.DbSignedBeaconBlock { var beaconBlock *beaconclient.DbSignedBeaconBlock = nil for i := 0; i < 30; i++ { time.Sleep(time.Second) record := lookForTxInDb(db, tx) if nil != record { beaconBlock = record log.Debugf("Found ETH1 TX %s in SignedBeaconBlock %d/%s", tx.hash, beaconBlock.Slot, beaconBlock.BlockRoot) break } } return beaconBlock } func lookForTxInDb(db sql.Database, tx *SentTx) *beaconclient.DbSignedBeaconBlock { sqlStatement := `SELECT * FROM eth_beacon.signed_block WHERE payload_block_number = $1 AND payload_block_hash = $2 AND payload_transactions_root = $3` // We can pre-calculate the root and query on it because we are only sending a single TX at a time. // Otherwise we would need to lookup the root by block num+hash, then do a proof that its txs // root includes our TX. var ptxs = zcommon.PayloadTransactions{tx.raw} txRoot := ptxs.HashTreeRoot(configs.Mainnet, tree.GetHashFn()) var slot uint64 var blockRoot, parentBlock, eth1DataBlockHash, mhKey string var blockNumber, timestamp uint64 var blockHash, parentHash, stateRoot, receiptsRoot, transactionsRoot string err := db. QueryRow(context.Background(), sqlStatement, tx.blockNo, tx.blockHash, "0x"+hex.EncodeToString(txRoot[:])). Scan(&slot, &blockRoot, &parentBlock, ð1DataBlockHash, &mhKey, &blockNumber, ×tamp, &blockHash, &parentHash, &stateRoot, &receiptsRoot, &transactionsRoot) if nil != err { return nil } return &beaconclient.DbSignedBeaconBlock{ Slot: slot, BlockRoot: blockRoot, ParentBlock: parentBlock, Eth1DataBlockHash: eth1DataBlockHash, MhKey: mhKey, ExecutionPayloadHeader: &beaconclient.DbExecutionPayloadHeader{ BlockNumber: blockNumber, Timestamp: timestamp, BlockHash: blockHash, ParentHash: parentHash, StateRoot: stateRoot, ReceiptsRoot: receiptsRoot, TransactionsRoot: transactionsRoot, }, } } func sendTestTx() (*SentTx, error) { ctx := context.Background() eth, err := createClient() Expect(err).ToNot(HaveOccurred()) //TODO: Pull from test config / env. tx, err := sendTransaction( ctx, eth, getEnvStr("TEST_E2E_FROM_ADDR", "0xe6ce22afe802caf5ff7d3845cec8c736ecc8d61f"), getEnvStr("TEST_E2E_TO_ADDR", "0xe22AD83A0dE117bA0d03d5E94Eb4E0d80a69C62a"), int64(getEnvInt("TEST_E2E_AMOUNT", 10)), getEnvStr("TEST_E2E_SIGNING_KEY", "0x888814df89c4358d7ddb3fa4b0213e7331239a80e1f013eaa7b2deca2a41a218"), ) Expect(err).ToNot(HaveOccurred()) txBin, err := tx.MarshalBinary() Expect(err).ToNot(HaveOccurred()) for i := 0; i <= 30; i++ { time.Sleep(time.Second) receipt, _ := eth.TransactionReceipt(ctx, tx.Hash()) if nil != receipt { sentTx := &SentTx{ hash: tx.Hash().String(), raw: txBin, blockNo: receipt.BlockNumber.Uint64(), blockHash: receipt.BlockHash.String(), txIndex: receipt.TransactionIndex, } log.Debugf("Sent ETH1 TX %s (Block No: %d, Block Hash: %s)", sentTx.hash, sentTx.blockNo, sentTx.blockHash) return sentTx, nil } } err = errors.New("Timed out waiting for TX.") Expect(err).ToNot(HaveOccurred()) return nil, err } func createClient() (*ethclient.Client, error) { return ethclient.Dial(getEnvStr("TEST_E2E_GETH_URL", "http://localhost:8545")) } // sendTransaction sends a transaction with 1 ETH to a specified address. func sendTransaction(ctx context.Context, eth *ethclient.Client, fromAddr string, toAddr string, amount int64, signingKey string) (*types.Transaction, error) { var ( from = common.HexToAddress(fromAddr) to = common.HexToAddress(toAddr) sk = crypto.ToECDSAUnsafe(common.FromHex(signingKey)) value = big.NewInt(amount) gasLimit = uint64(getEnvInt("TEST_E2E_GAS_LIMIT", 21000)) ) // Retrieve the chainid (needed for signer) chainid, err := eth.ChainID(ctx) if err != nil { return nil, err } // Retrieve the pending nonce nonce, err := eth.PendingNonceAt(ctx, from) if err != nil { return nil, err } // Get suggested gas price tipCap, _ := eth.SuggestGasTipCap(ctx) feeCap, _ := eth.SuggestGasPrice(ctx) log.Info("Tip cap: ", tipCap) log.Info("Fee cap: ", feeCap) // Create a new transaction tx := types.NewTx( &types.DynamicFeeTx{ ChainID: chainid, Nonce: nonce, GasTipCap: tipCap, GasFeeCap: feeCap, Gas: gasLimit, To: &to, Value: value, Data: nil, }) // Sign the transaction using our keys signedTx, _ := types.SignTx(tx, types.NewLondonSigner(chainid), sk) // Send the transaction to our node return signedTx, eth.SendTransaction(ctx, signedTx) } func getEnvStr(envVar string, def string) string { value, set := os.LookupEnv(envVar) if set { return value } else { return def } } func getEnvInt(envVar string, def int) int { value, set := os.LookupEnv(envVar) if set { number, err := strconv.Atoi(value) if err != nil { return def } return number } else { return def } }