Skip to content

Commit

Permalink
GH-255 Rework forking test so it can be triggered and resolved on com…
Browse files Browse the repository at this point in the history
…mand. Also greatly simplified the fork python script.
  • Loading branch information
heifner committed Dec 2, 2022
1 parent 01cdc24 commit 3957a55
Show file tree
Hide file tree
Showing 4 changed files with 346 additions and 690 deletions.
4 changes: 1 addition & 3 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nodeos_multiple_version_protocol_feat
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nodeos_extra_packed_data_test.py ${CMAKE_CURRENT_BINARY_DIR}/nodeos_extra_packed_data_test.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nodeos_trust_evm_test.py ${CMAKE_CURRENT_BINARY_DIR}/nodeos_trust_evm_test.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nodeos_trust_evm_server.py ${CMAKE_CURRENT_BINARY_DIR}/nodeos_trust_evm_server.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nodeos_trust_evm_forked_chain_test.py ${CMAKE_CURRENT_BINARY_DIR}/nodeos_trust_evm_forked_chain_test.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nodeos_trust_evm_fork_server.py ${CMAKE_CURRENT_BINARY_DIR}/nodeos_trust_evm_fork_server.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/validate-dirty-db.py ${CMAKE_CURRENT_BINARY_DIR}/validate-dirty-db.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/launcher_test.py ${CMAKE_CURRENT_BINARY_DIR}/launcher_test.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/keosd_auto_launch_test.py ${CMAKE_CURRENT_BINARY_DIR}/keosd_auto_launch_test.py COPYONLY)
Expand Down Expand Up @@ -84,8 +84,6 @@ set_property(TEST block_log_retain_blocks_test PROPERTY LABELS nonparallelizable
# comment out for now since prerequisites are not available
#add_test(NAME nodeos_trust_evm_test COMMAND tests/nodeos_trust_evm_test.py -v --clean-run --dump-error-detail WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
#set_property(TEST nodeos_trust_evm_test PROPERTY LABELS nonparallelizable_tests)
#add_test(NAME nodeos_trust_evm_forked_chain_test COMMAND tests/nodeos_trust_evm_forked_chain_test.py -v --clean-run --dump-error-detail WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
#set_property(TEST nodeos_trust_evm_forked_chain_test PROPERTY LABELS nonparallelizable_tests)

option(ABIEOS_ONLY_LIBRARY "define and build the ABIEOS library" ON)
set(ABIEOS_INSTALL_COMPONENT "dev")
Expand Down
342 changes: 342 additions & 0 deletions tests/nodeos_trust_evm_fork_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
#!/usr/bin/env python3
import random
import os
import json
import time
import signal
import calendar
from datetime import datetime

from flask import Flask, request, jsonify
from flask_cors import CORS
from eth_hash.auto import keccak
import requests
import json

from binascii import unhexlify
from TestHarness import Cluster, TestHelper, Utils, WalletMgr
from TestHarness.TestHelper import AppArgs
from core_symbol import CORE_SYMBOL


###############################################################
# nodeos_trust_evm_fork_server
#
# Set up a TrustEVM env
#
# This test sets up 2 producing nodes and one "bridge" node using test_control_api_plugin.
# One producing node has 3 of the elected producers and the other has 1 of the elected producers.
# All the producers are named in alphabetical order, so that the 3 producers, in the one production node, are
# scheduled first, followed by the 1 producer in the other producer node. Each producing node is only connected
# to the other producing node via the "bridge" node.
# The bridge node has the test_control_api_plugin, which exposes a restful interface that the test script uses to kill
# the "bridge" node when /fork endpoint called.
#
# --trust-evm-contract-root should point to root of TrustEVM contract build dir
# --genesis-json file to save generated EVM genesis json
# --read-endpoint trustnode-rpc endpoint (read endpoint)
#
# Example:
# ./tests/nodeos_trust_fork_server.py --trust-evm-contract-root ~/ext/TrustEVM/contract/build --leave-running
#
# Launches wallet at port: 9899
# Example: bin/cleos --wallet-url http://127.0.0.1:9899 ...
#
# Sets up endpoint on port 5000
# / - for req['method'] == "eth_sendRawTransaction"
# /fork - create forked chain, does not return until a fork has started
# /restore - resolve fork and stabilize chain
#
# Dependencies:
# pip install eth-hash requests flask flask-cors
###############################################################

Print=Utils.Print
errorExit=Utils.errorExit

appArgs=AppArgs()
appArgs.add(flag="--trust-evm-contract-root", type=str, help="TrustEVM contract build dir", default=None)
appArgs.add(flag="--genesis-json", type=str, help="File to save generated genesis json", default="trust-evm-genesis.json")
appArgs.add(flag="--read-endpoint", type=str, help="EVM read enpoint (trustevm-rpc)", default="http://localhost:8881")

args=TestHelper.parse_args({"--keep-logs","--dump-error-details","-v","--leave-running","--clean-run" }, applicationSpecificArgs=appArgs)
debug=args.v
killEosInstances= not args.leave_running
dumpErrorDetails=args.dump_error_details
keepLogs=args.keep_logs
killAll=args.clean_run
trustEvmContractRoot=args.trust_evm_contract_root
gensisJson=args.genesis_json
readEndpoint=args.read_endpoint

assert trustEvmContractRoot is not None, "--trust-evm-contract-root is required"

totalProducerNodes=2
totalNonProducerNodes=1
totalNodes=totalProducerNodes+totalNonProducerNodes
maxActiveProducers=21
totalProducers=maxActiveProducers

seed=1
Utils.Debug=debug
testSuccessful=False

random.seed(seed) # Use a fixed seed for repeatability.
cluster=Cluster(walletd=True)
walletMgr=WalletMgr(True)


try:
TestHelper.printSystemInfo("BEGIN")

cluster.setWalletMgr(walletMgr)
cluster.killall(allInstances=killAll)
cluster.cleanup()
walletMgr.killall(allInstances=killAll)
walletMgr.cleanup()

# *** setup topogrophy ***

# "bridge" shape connects defprocera through defproducerc (3 in node0) to each other and defproduceru (1 in node01)
# and the only connection between those 2 groups is through the bridge node

specificExtraNodeosArgs={}
# Connect SHiP to node01 so it will switch forks as they are resolved
specificExtraNodeosArgs[1]="--plugin eosio::state_history_plugin --state-history-endpoint 127.0.0.1:8999 --trace-history --chain-state-history --disable-replay-opts "
# producer nodes will be mapped to 0 through totalProducerNodes-1, so the number totalProducerNodes will be the non-producing node
specificExtraNodeosArgs[totalProducerNodes]="--plugin eosio::test_control_api_plugin "
extraNodeosArgs="--contracts-console"

Print("Stand up cluster")
if cluster.launch(topo="bridge", pnodes=totalProducerNodes,
totalNodes=totalNodes, totalProducers=totalProducers,
useBiosBootFile=False, extraNodeosArgs=extraNodeosArgs, specificExtraNodeosArgs=specificExtraNodeosArgs) is False:
Utils.cmdError("launcher")
Utils.errorExit("Failed to stand up eos cluster.")

Print("Validating system accounts after bootstrap")
cluster.validateAccounts(None)

Print ("Wait for Cluster stabilization")
# wait for cluster to start producing blocks
if not cluster.waitOnClusterBlockNumSync(3):
errorExit("Cluster never stabilized")
Print ("Cluster stabilized")

prodNode = cluster.getNode(0)
prodNode0 = prodNode
prodNode1 = cluster.getNode(1)
nonProdNode = cluster.getNode(2)

accounts=cluster.createAccountKeys(6)
if accounts is None:
Utils.errorExit("FAILURE - create keys")

evmAcc = accounts[0]
evmAcc.name = "evmevmevmevm"
evmAcc.activePrivateKey="5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"
evmAcc.activePublicKey="EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV"
accounts[1].name="tester111111" # needed for voting
accounts[2].name="tester222222" # needed for voting
accounts[3].name="tester333333" # needed for voting
accounts[4].name="tester444444" # needed for voting
accounts[5].name="tester555555" # needed for voting

testWalletName="test"

Print("Creating wallet \"%s\"." % (testWalletName))
testWallet=walletMgr.create(testWalletName, [cluster.eosioAccount,accounts[0],accounts[1],accounts[2],accounts[3],accounts[4],accounts[5]])

for _, account in cluster.defProducerAccounts.items():
walletMgr.importKey(account, testWallet, ignoreDupKeyWarning=True)

for i in range(0, totalNodes):
node=cluster.getNode(i)
node.producers=Cluster.parseProducers(i)
numProducers=len(node.producers)
for prod in node.producers:
prodName = cluster.defProducerAccounts[prod].name
if prodName == "defproducera" or prodName == "defproducerb" or prodName == "defproducerc" or prodName == "defproduceru":
Print("Register producer %s" % cluster.defProducerAccounts[prod].name)
trans=node.regproducer(cluster.defProducerAccounts[prod], "http::/mysite.com", 0, waitForTransBlock=False, exitOnError=True)

# create accounts via eosio as otherwise a bid is needed
for account in accounts:
Print("Create new account %s via %s with private key: %s" % (account.name, cluster.eosioAccount.name, account.activePrivateKey))
trans=nonProdNode.createInitializeAccount(account, cluster.eosioAccount, stakedDeposit=0, waitForTransBlock=True, stakeNet=10000, stakeCPU=10000, buyRAM=10000000, exitOnError=True)
transferAmount="100000000.0000 {0}".format(CORE_SYMBOL)
Print("Transfer funds %s from account %s to %s" % (transferAmount, cluster.eosioAccount.name, account.name))
nonProdNode.transferFunds(cluster.eosioAccount, account, transferAmount, "test transfer", waitForTransBlock=True)
trans=nonProdNode.delegatebw(account, 20000000.0000, 20000000.0000, waitForTransBlock=False, exitOnError=True)

# *** vote using accounts ***

cluster.waitOnClusterSync(blockAdvancing=3)

# vote a,b,c u
voteProducers=[]
voteProducers.append("defproducera")
voteProducers.append("defproducerb")
voteProducers.append("defproducerc")
voteProducers.append("defproduceru")
for account in accounts:
Print("Account %s vote for producers=%s" % (account.name, voteProducers))
trans=prodNode.vote(account, voteProducers, exitOnError=True, waitForTransBlock=False)

#verify nodes are in sync and advancing
cluster.waitOnClusterSync(blockAdvancing=3)
Print("Shutdown unneeded bios node")
cluster.biosNode.kill(signal.SIGTERM)

# setup evm

contractDir=trustEvmContractRoot + "/evm_runtime"
wasmFile="evm_runtime.wasm"
abiFile="evm_runtime.abi"
Utils.Print("Publish evm_runtime contract")
trans = prodNode.publishContract(evmAcc, contractDir, wasmFile, abiFile, waitForTransBlock=True)
transId=prodNode.getTransId(trans)
blockNum = prodNode.getBlockNumByTransId(transId)
block = prodNode.getBlock(blockNum)
Utils.Print("Block Id: ", block["id"])
Utils.Print("Block timestamp: ", block["timestamp"])

genesis_info = {
"alloc": {},
"coinbase": "0x0000000000000000000000000000000000000000",
"config": {
"chainId": 15555,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"noproof": {}
},
"difficulty": "0x01",
"extraData": "TrustEVM",
"gasLimit": "0x7ffffffffff",
"mixHash": "0x"+block["id"],
"nonce": hex(1000),
"timestamp": hex(int(calendar.timegm(datetime.strptime(block["timestamp"].split(".")[0], '%Y-%m-%dT%H:%M:%S').timetuple())))
}

# accounts: {
# mnemonic: "test test test test test test test test test test test junk",
# path: "m/44'/60'/0'/0",
# initialIndex: 0,
# count: 20,
# passphrase: "",
# }

addys = {
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266":"0x038318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75,0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8":"0x02ba5734d8f7091719471e7f7ed6b9df170dc70cc661ca05e688601ad984f068b0,0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC":"0x039d9031e97dd78ff8c15aa86939de9b1e791066a0224e331bc962a2099a7b1f04,0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
"0x90F79bf6EB2c4f870365E785982E1f101E93b906":"0x0220b871f3ced029e14472ec4ebc3c0448164942b123aa6af91a3386c1c403e0eb,0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6",
"0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65":"0x03bf6ee64a8d2fdc551ec8bb9ef862ef6b4bcb1805cdc520c3aa5866c0575fd3b5,0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
"0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc":"0x0337b84de6947b243626cc8b977bb1f1632610614842468dfa8f35dcbbc55a515e,0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba",
"0x976EA74026E726554dB657fA54763abd0C3a0aa9":"0x029a4ab212cb92775d227af4237c20b81f4221e9361d29007dfc16c79186b577cb,0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e",
"0x14dC79964da2C08b23698B3D3cc7Ca32193d9955":"0x0201f2bf1fa920e77a43c7aec2587d0b3814093420cc59a9b3ad66dd5734dda7be,0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356",
"0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f":"0x03931e7fda8da226f799f791eefc9afebcd7ae2b1b19a03c5eaa8d72122d9fe74d,0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97",
"0xa0Ee7A142d267C1f36714E4a8F75612F20a79720":"0x023255458e24278e31d5940f304b16300fdff3f6efd3e2a030b5818310ac67af45,0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6",
"0xBcd4042DE499D14e55001CcbB24a551F3b954096":"0x030bb316cf4dbaeff8df7c6a8f3c55a11c72f7f2f8d79c274f27cdce2220f36371,0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897",
"0x71bE63f3384f5fb98995898A86B02Fb2426c5788":"0x02e393c954d127d79d56b594a46df6b2e053f49446759eac612dbe12ade3095c67,0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82",
"0xFABB0ac9d68B0B445fB7357272Ff202C5651694a":"0x02463e7db0f9c35ba7ae68a8098f1024019b90191281276eeef294acb3f1354b0a,0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1",
"0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec":"0x037805be0fc5186c4437306b531c8e981d3922e5fc81d72d527931b995445fb78e,0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd",
"0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097":"0x03407a862c69cbc66dceea40079757697abcf931043f5a5f128a56ae6e51bdbce7,0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa",
"0xcd3B766CCDd6AE721141F452C550Ca635964ce71":"0x02d5ab34891bfe989bfa557f983cb5d031c4acc845ad990baaefb58ca1db0e7716,0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61",
"0x2546BcD3c84621e976D8185a91A922aE77ECEc30":"0x0364c1c85d9aa8081a8ef94c35379fa7532942b2d8cbbd1e3ea71c0e3609b96cc0,0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0",
"0xbDA5747bFD65F08deb54cb465eB87D40e51B197E":"0x02c216848622dfc38a2ad2a921f524103cf654a22b8679736ecedc4901453ea3f7,0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd",
"0xdD2FD4581271e230360230F9337D5c0430Bf44C0":"0x02302a94bd084ff317493db7c2fe07e0935c0f6d3e6772d6af3c58e92abebfb402,0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0",
"0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199":"0x027bf824b28c4bf11ce553fa746a18754949ab4959e2ea73465778d14179211f8c,0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e"
}

for k in addys:
trans = prodNode.pushMessage(evmAcc.name, "setbal", '{"addy":"' + k[2:].lower() + '", "bal":"0000000000000000000000000000000010000000000000000000000000000000"}', '-p evmevmevmevm')
genesis_info["alloc"][k.lower()] = {"balance":"0x10000000000000000000000000000000"}
prodNode.waitForTransBlockIfNeeded(trans[1], True)

if gensisJson[0] != '/': gensisJson = os.path.realpath(gensisJson)
f=open(gensisJson,"w")
f.write(json.dumps(genesis_info))
f.close()
Utils.Print("#####################################################")
Utils.Print("Generated EVM json genesis file in: %s" % gensisJson)
Utils.Print("")
Utils.Print("You can now run:")
Utils.Print(" trustevm-node --plugin=blockchain_plugin --ship-endpoint=127.0.0.1:8999 --genesis-json=%s --chain-data=/tmp --verbosity=4" % gensisJson)
Utils.Print(" trustevm-rpc --trust-evm-node=127.0.0.1:8080 --http-port=0.0.0.0:8881 --chaindata=/tmp --api-spec=eth,debug,net,trace")
Utils.Print("")
Utils.Print("Web3 endpoint:")
Utils.Print(" http://localhost:5000")

app = Flask(__name__)
CORS(app)

@app.route("/fork", methods=["POST"])
def fork():
Print("Sending command to kill bridge node to separate the 2 producer groups.")
forkAtProducer="defproducera"
prodNode1Prod="defproduceru"
preKillBlockNum=nonProdNode.getBlockNum()
preKillBlockProducer=nonProdNode.getBlockProducerByNum(preKillBlockNum)
nonProdNode.killNodeOnProducer(producer=forkAtProducer, whereInSequence=1)
Print("Current block producer %s fork will be at producer %s" % (preKillBlockProducer, forkAtProducer))
prodNode0.waitForProducer(forkAtProducer)
prodNode1.waitForProducer(prodNode1Prod)
if nonProdNode.verifyAlive(): # if on defproducera, need to wait again
prodNode0.waitForProducer(forkAtProducer)
prodNode1.waitForProducer(prodNode1Prod)

if nonProdNode.verifyAlive():
Print("Bridge did not shutdown")
return "Bridge did not shutdown"

Print("Fork started")
return "Fork started"

@app.route("/restore", methods=["POST"])
def restore():
Print("Relaunching the non-producing bridge node to connect the producing nodes again")

if nonProdNode.verifyAlive():
return "bridge is already running"

if not nonProdNode.relaunch():
Utils.errorExit("Failure - (non-production) node %d should have restarted" % (nonProdNode.nodeNum))

return "restored fork should resolve"

@app.route("/", methods=["POST"])
def default():
def forward_request(req):
if req['method'] == "eth_sendRawTransaction":
actData = {"ram_payer":"evmevmevmevm", "rlptx":req['params'][0][2:]}
prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm')
return {
"id": req['id'],
"jsonrpc": "2.0",
"result": '0x'+keccak(unhexlify(req['params'][0][2:])).hex()
}
return requests.post(readEndpoint, json.dumps(req), headers={"Content-Type":"application/json"}).json()

request_data = request.get_json()
if type(request_data) == dict:
return jsonify(forward_request(request_data))

res = []
for r in request_data:
res.append(forward_request(r))

return jsonify(res)

app.run(host='0.0.0.0', port=5000)

finally:
TestHelper.shutdown(cluster, walletMgr, testSuccessful=testSuccessful, killEosInstances=killEosInstances, killWallet=killEosInstances, keepLogs=keepLogs, cleanRun=killAll, dumpErrorDetails=dumpErrorDetails)

exitCode = 0 if testSuccessful else 1
exit(exitCode)
Loading

0 comments on commit 3957a55

Please sign in to comment.