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

IF: Test: Transition to instant-finality with multiple producers #2224

Merged
merged 8 commits into from
Feb 8, 2024
4 changes: 2 additions & 2 deletions libraries/chain/block_header_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ block_header_state block_header_state::next(block_header_state_input& input) con

if(!proposer_policies.empty()) {
auto it = proposer_policies.begin();
// -1 since this is called after the block is built, this will be the active schedule for the next block
if (it->first.slot <= input.timestamp.slot - 1) {
// +1 since this is called after the block is built, this will be the active schedule for the next block
if (it->first.slot <= input.timestamp.slot + 1) {
result.active_proposer_policy = it->second;
result.header.schedule_version = header.schedule_version + 1;
result.active_proposer_policy->proposer_schedule.version = result.header.schedule_version;
Expand Down
4 changes: 4 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/subjective_billing_test.py ${CMAKE_CU
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/get_account_test.py ${CMAKE_CURRENT_BINARY_DIR}/get_account_test.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nodeos_high_transaction_test.py ${CMAKE_CURRENT_BINARY_DIR}/nodeos_high_transaction_test.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nodeos_retry_transaction_test.py ${CMAKE_CURRENT_BINARY_DIR}/nodeos_retry_transaction_test.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/transition_to_if.py ${CMAKE_CURRENT_BINARY_DIR}/transition_to_if.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/trx_finality_status_test.py ${CMAKE_CURRENT_BINARY_DIR}/trx_finality_status_test.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/trx_finality_status_forked_test.py ${CMAKE_CURRENT_BINARY_DIR}/trx_finality_status_forked_test.py COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/plugin_http_api_test.py ${CMAKE_CURRENT_BINARY_DIR}/plugin_http_api_test.py COPYONLY)
Expand Down Expand Up @@ -132,6 +133,9 @@ set_property(TEST cluster_launcher PROPERTY LABELS nonparallelizable_tests)
add_test(NAME cluster_launcher_if COMMAND tests/cluster_launcher.py --activate-if -v ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
set_property(TEST cluster_launcher_if PROPERTY LABELS nonparallelizable_tests)

add_test(NAME transition_to_if COMMAND tests/transition_to_if.py -v ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
set_property(TEST transition_to_if PROPERTY LABELS nonparallelizable_tests)

add_test(NAME ship_test COMMAND tests/ship_test.py -v --num-clients 10 --num-requests 5000 ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
set_property(TEST ship_test PROPERTY LABELS nonparallelizable_tests)
add_test(NAME ship_test_unix COMMAND tests/ship_test.py -v --num-clients 10 --num-requests 5000 ${UNSHARE} --unix-socket WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
Expand Down
55 changes: 42 additions & 13 deletions tests/TestHarness/Cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,8 @@ def connectGroup(group, producerNodes, bridgeNodes) :
node = Node(self.host, self.port + nodeNum, nodeNum, Path(instance.data_dir_name),
Path(instance.config_dir_name), eosdcmd, unstarted=instance.dont_start,
launch_time=launcher.launch_time, walletMgr=self.walletMgr, nodeosVers=self.nodeosVers)
node.keys = instance.keys
node.isProducer = len(instance.producers) > 0
if nodeNum == Node.biosNodeId:
self.biosNode = node
else:
Expand Down Expand Up @@ -993,34 +995,33 @@ def parseClusterKeys(totalNodes):
Utils.Print(f'Found {len(producerKeys)} producer keys')
return producerKeys

def activateInstantFinality(self, launcher, biosFinalizer, pnodes):
def activateInstantFinality(self, biosFinalizer=True):
# call setfinalizer
numFins = 0
for n in launcher.network.nodes.values():
if not n.keys or not n.keys[0].blspubkey:
for n in (self.nodes + [self.biosNode]):
if not n or not n.keys or not n.keys[0].blspubkey:
continue
if not n.producers:
if not n.isProducer:
continue
if n.index == Node.biosNodeId and not biosFinalizer:
if n.nodeId == 'bios' and not biosFinalizer:
continue
numFins = numFins + 1

threshold = int(numFins * 2 / 3 + 1)
if threshold > 2 and threshold == numFins:
# nodes are often stopped, so do not require all node votes
threshold = threshold - 1
# pnodes does not include biosNode
if Utils.Debug: Utils.Print(f"threshold: {threshold}, numFins: {numFins}, pnodes: {pnodes}")
if Utils.Debug: Utils.Print(f"threshold: {threshold}, numFins: {numFins}")
setFinStr = f'{{"finalizer_policy": {{'
setFinStr += f' "threshold": {threshold}, '
setFinStr += f' "finalizers": ['
finNum = 1
for n in launcher.network.nodes.values():
if n.index == Node.biosNodeId and not biosFinalizer:
for n in (self.nodes + [self.biosNode]):
Copy link
Member

Choose a reason for hiding this comment

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

Can this for loop be combined with the one above? They are in similar shape.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think so, need to have threshold calculated first.

if not n or not n.keys or not n.keys[0].blspubkey:
continue
if not n.keys or not n.keys[0].blspubkey:
if not n.isProducer:
continue
if not n.producers:
if n.nodeId == 'bios' and not biosFinalizer:
continue
setFinStr += f' {{"description": "finalizer #{finNum}", '
setFinStr += f' "weight":1, '
Expand All @@ -1044,6 +1045,7 @@ def activateInstantFinality(self, launcher, biosFinalizer, pnodes):
if not self.biosNode.waitForTransFinalization(transId, timeout=21*12*3):
Copy link
Member

Choose a reason for hiding this comment

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

So is the host function sent to the biosNode first even if biosFinalizer is false?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. Doesn't matter which node you look for the transaction on. Maybe we should rename this python method now. It has been there for a long time. It means is the transaction in a LIB block.

Copy link
Member

Choose a reason for hiding this comment

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

We should change the name now as a part of the big changes for IF.

Utils.Print("ERROR: Failed to validate transaction %s got rolled into a LIB block on server port %d." % (transId, biosNode.port))
return None
return True

def bootstrap(self, launcher, biosNode, totalNodes, prodCount, totalProducers, pfSetupPolicy, onlyBios=False, onlySetProds=False, loadSystemContract=True, activateIF=False, biosFinalizer=True):
"""Create 'prodCount' init accounts and deposits 10000000000 SYS in each. If prodCount is -1 will initialize all possible producers.
Expand Down Expand Up @@ -1109,7 +1111,9 @@ def bootstrap(self, launcher, biosNode, totalNodes, prodCount, totalProducers,
return None

if activateIF:
self.activateInstantFinality(launcher, biosFinalizer, self.productionNodesCount)
if not self.activateInstantFinality(biosFinalizer=biosFinalizer):
Utils.Print("ERROR: Activate instant finality failed")
return None

Utils.Print("Creating accounts: %s " % ", ".join(producerKeys.keys()))
producerKeys.pop(eosioName)
Expand Down Expand Up @@ -1208,7 +1212,7 @@ def createSystemAccount(accountName):
#
# Could activate instant finality here, but have to wait for finality which with all the producers takes a long time
# if activateIF:
# self.activateInstantFinality(launcher)
# self.activateInstantFinality()

eosioTokenAccount = copy.deepcopy(eosioAccount)
eosioTokenAccount.name = 'eosio.token'
Expand Down Expand Up @@ -1456,6 +1460,31 @@ def cleanup(self):
for f in self.filesToCleanup:
os.remove(f)

def setProds(self, producers):
"""Call setprods with list of producers"""
setProdsStr = '{"schedule": ['
firstTime = True
for name in producers:
if firstTime:
firstTime = False
else:
setProdsStr += ','
if not self.defProducerAccounts[name]:
Utils.Print(f"ERROR: no account key for {name}")
return None
key = self.defProducerAccounts[name].activePublicKey
setProdsStr += '{"producer_name":' + name + ',"authority": ["block_signing_authority_v0", {"threshold":1, "keys":[{"key":' + key + ', "weight":1}]}]}'

setProdsStr += ' ] }'
Utils.Print("setprods: %s" % (setProdsStr))
opts = "--permission eosio@active"
# pylint: disable=redefined-variable-type
trans = self.biosNode.pushMessage("eosio", "setprods", setProdsStr, opts)
if trans is None or not trans[0]:
Utils.Print("ERROR: Failed to set producer with cmd %s" % (setProdsStr))
return None
return True

# Create accounts, if account does not already exist, and validates that the last transaction is received on root node
def createAccounts(self, creator, waitForTransBlock=True, stakedDeposit=1000, validationNodeIndex=-1):
if self.accounts is None:
Expand Down
10 changes: 10 additions & 0 deletions tests/TestHarness/Node.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import sys
from pathlib import Path
from typing import List
from dataclasses import InitVar, dataclass, field, is_dataclass, asdict

from datetime import datetime
from datetime import timedelta
Expand All @@ -21,6 +22,14 @@
from .testUtils import unhandledEnumType
from .testUtils import ReturnType

@dataclass
class KeyStrings(object):
pubkey: str
privkey: str
blspubkey: str = None
blsprivkey: str = None
blspop: str = None

# pylint: disable=too-many-public-methods
class Node(Transactions):
# Node number is used as an addend to determine the node listen ports.
Expand Down Expand Up @@ -66,6 +75,7 @@ def __init__(self, host, port, nodeId: int, data_dir: Path, config_dir: Path, cm
self.config_dir=config_dir
self.launch_time=launch_time
self.isProducer=False
self.keys: List[KeyStrings] = field(default_factory=list)
self.configureVersion()

def configureVersion(self):
Expand Down
75 changes: 75 additions & 0 deletions tests/transition_to_if.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3

from TestHarness import Cluster, TestHelper, Utils, WalletMgr
from TestHarness.TestHelper import AppArgs

###############################################################
# transition_to_if
#
# Transition to instant-finality with multiple producers (at least 4).
#
###############################################################


Print=Utils.Print
errorExit=Utils.errorExit

appArgs = AppArgs()
args=TestHelper.parse_args({"-d","-s","--keep-logs","--dump-error-details","-v","--leave-running","--unshared"},
applicationSpecificArgs=appArgs)
pnodes=4
delay=args.d
topo=args.s
debug=args.v
prod_count = 1 # per node prod count
total_nodes=pnodes
dumpErrorDetails=args.dump_error_details

Utils.Debug=debug
testSuccessful=False

cluster=Cluster(unshared=args.unshared, keepRunning=args.leave_running, keepLogs=args.keep_logs)
walletMgr=WalletMgr(True)

try:
TestHelper.printSystemInfo("BEGIN")

cluster.setWalletMgr(walletMgr)

Print(f'producing nodes: {pnodes}, topology: {topo}, delay between nodes launch: {delay} second{"s" if delay != 1 else ""}')

Print("Stand up cluster")
# For now do not load system contract as it does not support setfinalizer
if cluster.launch(pnodes=pnodes, totalNodes=total_nodes, prodCount=prod_count, topo=topo, delay=delay, loadSystemContract=False,
activateIF=False) is False:
errorExit("Failed to stand up eos cluster.")

assert cluster.biosNode.getInfo(exitOnError=True)["head_block_producer"] != "eosio", "launch should have waited for production to change"

assert cluster.activateInstantFinality(biosFinalizer=False), "Activate instant finality failed"

assert cluster.biosNode.waitForLibToAdvance(), "Lib should advance after instant finality activated"
assert cluster.biosNode.waitForProducer("defproducera"), "Did not see defproducera"
assert cluster.biosNode.waitForHeadToAdvance(blocksToAdvance=13) # into next producer
assert cluster.biosNode.waitForLibToAdvance(), "Lib stopped advancing"

info = cluster.biosNode.getInfo(exitOnError=True)
assert (info["head_block_num"] - info["last_irreversible_block_num"]) < 9, "Instant finality enabled LIB diff should be small"

# launch setup node_00 (defproducera - defproducerf), node_01 (defproducerg - defproducerk),
# node_02 (defproducerl - defproducerp), node_03 (defproducerq - defproduceru)
# with setprods of (defproducera, defproducerg, defproducerl, defproducerq)
assert cluster.biosNode.waitForProducer("defproducerq"), "defproducerq did not produce"

# should take effect in first block of defproducerg slot (so defproducerh)
assert cluster.setProds(["defproducerb", "defproducerh", "defproducerm", "defproducerr"]), "setprods failed"
setProdsBlockNum = cluster.biosNode.getBlockNum()
cluster.biosNode.waitForBlock(setProdsBlockNum+12+12+1)
assert cluster.biosNode.getInfo(exitOnError=True)["head_block_producer"] == "defproducerh", "setprods should have taken effect"

testSuccessful=True
finally:
TestHelper.shutdown(cluster, walletMgr, testSuccessful=testSuccessful, dumpErrorDetails=dumpErrorDetails)

exitCode = 0 if testSuccessful else 1
exit(exitCode)
10 changes: 5 additions & 5 deletions unittests/producer_schedule_if_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ BOOST_FIXTURE_TEST_CASE( verify_producer_schedule_after_instant_finality_activat

// ---- Test first set of producers ----
// Send set prods action and confirm schedule correctness
set_producers(producers);
auto trace = set_producers(producers);
const auto first_prod_schd = get_producer_authorities(producers);
// TODO: update expected when lib for instant_finality is working, will change from 26 at that time, 4+12+12
confirm_schedule_correctness(first_prod_schd, 1, 26);
// called in first round so complete it, skip one round of 12 and start on next round, so block 24
confirm_schedule_correctness(first_prod_schd, 1, 24);

// ---- Test second set of producers ----
vector<account_name> second_set_of_producer = {
Expand All @@ -84,8 +84,8 @@ BOOST_FIXTURE_TEST_CASE( verify_producer_schedule_after_instant_finality_activat
// Send set prods action and confirm schedule correctness
set_producers(second_set_of_producer);
const auto second_prod_schd = get_producer_authorities(second_set_of_producer);
// TODO: update expected when lib for instant_finality is working, will change from 50 at that time, 26+12+12
confirm_schedule_correctness(second_prod_schd, 2, 50);
// called after block 24, so next,next is 48
confirm_schedule_correctness(second_prod_schd, 2, 48);

// ---- Test deliberately miss some blocks ----
const int64_t num_of_missed_blocks = 5000;
Expand Down
Loading