Exemplo n.º 1
0
def test_sysvar_set(ndauapi, ndau):
    name = random_string("fake-sysvar")
    value = random_string("fake-value")

    cur_value = json.loads(ndau(f"sysvar get {name}"))[name]
    if cur_value != "":
        pytest.skip("accidentally generated an existing sysvar")

    resp = requests.post(f"{ndauapi}/system/set/{name}",
                         data=f'"{value}"'.encode("utf8"))
    assert resp.status_code == requests.codes.ok

    # this endpoint must not modify the blockchain
    cur_value = json.loads(ndau(f"sysvar get {name}"))[name]
    assert cur_value == ""

    # this endpoint must return a transaction with certain properties
    tx = resp.json()

    # check expected fields
    assert set(tx.keys()) == {"name", "value", "sequence", "signatures"}
    assert tx["name"] == name
    assert tx["value"] == base64.b64encode(msgpack.dumps(value)).decode("utf8")
    assert tx["sequence"] == 0
    assert tx["signatures"] is None or tx["signatures"] == [
    ]  # doesn't matter which
Exemplo n.º 2
0
def test_transfer_lock(ndau, nonzero_tx_fees, set_up_account, zero_sib):
    """Test TransferLock transaction"""
    # Set up source account with funds.
    account1 = random_string("xferlock1")
    set_up_account(account1)

    # Create destination account, but don't set-validation or rfe to it
    # (otherwise transfer-lock fails).
    account2 = random_string("xferlock2")
    ndau(f"account new {account2}")

    orig_ndau = 10  # from set_up_account()
    orig_napu = int(orig_ndau * 1e8)
    xfer_ndau = 1  # We'll transfer this amount
    xfer_napu = int(xfer_ndau * 1e8)

    # One napu for the set-validation transaction.
    account_data1 = json.loads(ndau(f"account query {account1}"))
    assert account_data1["balance"] == orig_napu - constants.ONE_NAPU_FEE

    # TransferLock
    lock_months = 3
    ndau(f"transfer-lock {xfer_ndau} {account1} {account2} {lock_months}m")
    account_data1 = json.loads(ndau(f"account query {account1}"))
    account_data2 = json.loads(ndau(f"account query {account2}"))
    # Subtract one napu for the set-validation transaction, one for the transfer-lock.
    assert (
        account_data1["balance"] == orig_napu - xfer_napu - 2 * constants.ONE_NAPU_FEE
    )
    assert account_data1["lock"] is None
    # No set-validation transaction, no fee.  Just gain the amount transferred.
    assert account_data2["balance"] == xfer_napu
    assert account_data2["lock"] is not None
    assert account_data2["lock"]["unlocksOn"] is None
Exemplo n.º 3
0
def test_transfer_with_sib(ndau, nonzero_tx_fees, set_up_account, max_sib):
    """Test Transfer transaction"""
    # Set up accounts to transfer between.
    account1 = random_string("xfer1")
    set_up_account(account1)
    account2 = random_string("xfer2")
    set_up_account(account2)

    orig_ndau = 10  # from set_up_account()
    orig_napu = int(orig_ndau * 1e8)
    xfer_ndau = 1  # We'll transfer this amount
    xfer_napu = int(xfer_ndau * 1e8)

    # One napu for the set-validation transaction.
    account_data1 = json.loads(ndau(f"account query {account1}"))
    assert account_data1["balance"] == orig_napu - constants.ONE_NAPU_FEE

    # Transfer
    ndau(f"transfer {xfer_ndau} {account1} {account2}")
    account_data1 = json.loads(ndau(f"account query {account1}"))
    account_data2 = json.loads(ndau(f"account query {account2}"))
    # Subtract one napu for the set-validation transaction, one for the transfer.
    assert account_data1["balance"] == orig_napu - xfer_napu - 2 * constants.ONE_NAPU_FEE
    assert account_data1["lock"] is None
    # Subtract one napu for the set-validation transaction.
    assert account_data2["balance"] == orig_napu + xfer_napu - constants.ONE_NAPU_FEE
    assert account_data2["lock"] is None
Exemplo n.º 4
0
def test_sysvar_history(ndau, ndauapi):
    # Fill a new sysvar with some history so it won't conflict with another
    # sysvar's history.
    count = 3
    sysvar = random_string()
    for i in range(count):
        ndau(f"sysvar set {sysvar} bar{i}")

    # Get history.
    resp = requests.get(f"{ndauapi}/system/history/{sysvar}")
    assert resp.status_code == requests.codes.ok

    # Validate each element in the response.
    data = json.loads(resp.text)
    history = data["history"]
    assert len(history) == count
    last_height = 0
    for i in range(count):
        history_i = history[i]
        h = history_i["height"]
        v = history_i["value"]

        height = int(h)
        assert height > last_height
        last_height = height

        value = base64.b64decode(v).decode("utf-8")
        assert value == f"bar{i}"
Exemplo n.º 5
0
def test_sysvar_history(ndau):
    """Test system variable history"""
    # make up a fake sv and ensure it doesn't already exist
    fake_sv_name = random_string("fake-sysvar")
    sv_json = ndau(f"sysvar get {fake_sv_name}")
    print(sv_json)
    sv_data = json.loads(sv_json)[fake_sv_name]
    assert sv_data == ""

    # set it a few times
    data = ["one", "two", "three"]
    for val in data:
        ndau(f"sysvar set {fake_sv_name} {val}")

    # get history
    sv_json = ndau(f"sysvar history {fake_sv_name}")
    print(sv_json)
    sv_data = json.loads(sv_json)["history"]
    last_height = 0
    for i in range(len(data)):
        sv_data_i = sv_data[i]
        h = sv_data_i["height"]
        v = sv_data_i["value"]

        height = int(h)
        assert height > last_height
        last_height = height

        value = base64.b64decode(v).decode("utf-8")
        assert value == data[i]

    # ensure the blockchain is still alive
    ndau("info")
Exemplo n.º 6
0
def test_change_validation(ndau, set_up_account):
    """Test ChangeValidation transaction"""

    # Set up an account.
    account = random_string("change-validation")
    set_up_account(account)
    account_data = json.loads(ndau(f"account query {account}"))
    assert account_data["validationKeys"] is not None
    assert len(account_data["validationKeys"]) == 1
    key1 = account_data["validationKeys"][0]
    assert account_data["validationScript"] is None

    # Add
    ndau(f"account validation {account} add")
    account_data = json.loads(ndau(f"account query {account}"))
    assert account_data["validationKeys"] is not None
    assert len(account_data["validationKeys"]) == 2
    assert account_data["validationKeys"][0] == key1
    assert account_data["validationKeys"][1] != key1
    assert account_data["validationScript"] is None

    # Reset
    ndau(f"account validation {account} reset")
    account_data = json.loads(ndau(f"account query {account}"))
    assert account_data["validationKeys"] is not None
    assert len(account_data["validationKeys"]) == 1
    assert account_data["validationKeys"][0] != key1
    assert account_data["validationScript"] is None

    # SetScript
    ndau(f"account validation {account} set-script oAAgiA")
    account_data = json.loads(ndau(f"account query {account}"))
    assert account_data["validationScript"] == "oAAgiA=="
Exemplo n.º 7
0
def test_create_account(ndau, rfe, zero_tx_fees):
    """Create account, RFE to it, and check attributes"""
    _random_string = random_string("generic")
    known_ids = ndau("account list").splitlines()
    # make sure account does not already exist
    assert not any(_random_string in id_line for id_line in known_ids)
    # create new randomly named account
    ndau(f"account new {_random_string}")
    new_ids = ndau("account list").splitlines()
    # check that account now exists
    assert any(_random_string in id_line for id_line in new_ids)
    id_line = [s for s in new_ids if _random_string in s]
    # check that account has no validation keys
    assert "(0 tr keys)" in id_line[0]
    account_data = json.loads(ndau(f"account query {_random_string}"))
    assert account_data["validationKeys"] is None
    # RFE to account 10 ndau
    orig_ndau = 10
    orig_napu = 10 * 1e8
    rfe(orig_ndau, _random_string)
    account_data = json.loads(ndau(f"account query {_random_string}"))
    # check that account balance is 10 ndau
    assert account_data["balance"] == orig_napu
    # set validation, and check that account now has validation keys
    ndau(f"account set-validation {_random_string}")
    account_data = json.loads(ndau(f"account query {_random_string}"))
    assert account_data["validationKeys"] is not None
Exemplo n.º 8
0
def test_change_settlement_period(ndau, set_up_account):
    """Test ChangeSettlementPeriod transaction"""

    # Pick something that we wouldn't ever use as a default.  That way, we can
    # assert on the initial value is not this (rather than assserting on the
    # default value, which would fail if we ever changed it).  We will then
    # change the settlement period to this and assert.
    new_period = "2m3dt5h7m11s"

    # Set up a new account, which will have the default settlement period.
    account = random_string("settlement-period")
    set_up_account(account)
    account_data = json.loads(ndau(f"account query {account}"))
    assert account_data["recourseSettings"] is not None
    old_period = account_data["recourseSettings"]["period"]
    assert old_period is not None
    assert old_period != ""
    assert old_period != new_period
    assert account_data["recourseSettings"]["next"] is None

    # ChangeSettlementPeriod
    ndau(f"account change-recourse-period {account} {new_period}")
    account_data = json.loads(ndau(f"account query {account}"))
    assert account_data["recourseSettings"] is not None
    assert account_data["recourseSettings"]["period"] == old_period
    assert account_data["recourseSettings"]["next"] == new_period
Exemplo n.º 9
0
def current_block(set_up_account, ndauapi):
    # the hash doesn't get recorded unless there is a tx in it.
    # simplest way to accomplish that is to set up a random account real quick.
    set_up_account(random_string("create-block-for-hash"))
    resp = requests.get(f"{ndauapi}/block/current")
    if resp.status_code != requests.codes.ok:
        pytest.skip(f"failed to get current block data")
    return resp.json()
Exemplo n.º 10
0
def test_change_sysvar(ndau, rfe_to_ssv):
    """Test that changing a system variable doesn't kill the blockchain"""
    # make up a fake sv and ensure it doesn't already exist
    fake_sv_name = random_string("fake-sysvar")
    sv_json = ndau(f"sysvar get {fake_sv_name}")
    print(sv_json)
    sv_data = json.loads(sv_json)[fake_sv_name]
    assert sv_data == ""

    # set it
    data = random_string("fake-sysvar-data")
    ndau(f"sysvar set {fake_sv_name} --json '\"{data}\"'")

    # ensure it set properly
    sv_json = ndau(f"sysvar get {fake_sv_name}")
    print(sv_json)
    sv_data = json.loads(sv_json)[fake_sv_name]
    assert sv_data == data

    # ensure the blockchain is still alive
    ndau("info")
Exemplo n.º 11
0
def accounts_with_history(ndau, set_up_account):
    # set up some accounts with some history
    names = []
    for _ in range(3):
        name = random_string("test-acct")
        names.append(name)
        # use "set_up_account" instead of new, rfe, set-validation
        set_up_account(name)
    for _ in range(5):
        for source in names:
            for dest in names:
                if dest != source:
                    ndau(f"transfer --napu=1 {source} {dest}")

    return names
Exemplo n.º 12
0
def test_create_child_account(ndau, set_up_account):
    """Test CreateChildAccount transaction"""

    # Set up parent account.
    parent_account = random_string("create-parent")
    set_up_account(parent_account)

    # Set up delegation account
    delegation_account = random_string("child-delegate")
    set_up_account(delegation_account)

    # Declare a child account and create it.
    child_account = random_string("create-child")
    settlement_period = "2m3dt5h7m11s"
    ndau(
        f"account create-child {parent_account} {child_account} "
        f"-p={settlement_period} {delegation_account}"
    )

    # Ensure the child account was created properly.
    account_data = json.loads(ndau(f"account query {child_account}"))
    assert account_data["validationKeys"] is not None
    assert len(account_data["validationKeys"]) == 1
    parent_address = account_data["parent"]
    assert account_data["progenitor"] == parent_address
    assert account_data["recourseSettings"] is not None
    assert account_data["recourseSettings"]["period"] == settlement_period

    # See that the parent/progenitor address matches that of the parent account.
    account_data = json.loads(ndau(f"account query -a {parent_address}"))
    # This just proves that we get back non-degenerate account data, proving
    # the parent exists.
    assert len(account_data["validationKeys"]) > 0
    # This parent account is the progenitor (both are null).
    assert account_data["parent"] is None
    assert account_data["progenitor"] is None
Exemplo n.º 13
0
def test_tx_prevalidate_and_submit(ndauapi, ndau, ndautool_toml):
    # Any transaction will do.  Here we RFE to the rfe address.
    txtype = "ReleaseFromEndowment"
    name = random_string("prevalsubmit-acct")
    ndau(f"account new {name}")
    tx = json.loads(ndau(f"-j rfe 1 {name}"))

    # We need the tx in a temp file to get the signable bytes.
    tf = NamedTemporaryFile(mode="w+t")
    json.dump(tx, tf)
    tf.flush()
    tx_file = tf.name

    # We can calculate the expected tx hash before we submit the transaction.
    signable_bytes_b64 = ndau(f"signable-bytes --strip {txtype} {tx_file}")
    signable_bytes = base64.b64decode(signable_bytes_b64, validate=True)
    txhash = (
        base64.urlsafe_b64encode(hashlib.md5(signable_bytes).digest())
        .decode("utf-8")
        .strip("=")  # tx hashes do not include base64 padding characters
    )

    # We expect the next transactions to succeed when posted.
    want_body = f'"hash":"{txhash}"'
    want_status = requests.codes.ok

    # Prevalidate new tx.
    resp = requests.post(f"{ndauapi}/tx/prevalidate/{txtype}", json=tx)
    assert resp.status_code == want_status
    assert want_body in resp.text

    # Submit new tx.
    resp = requests.post(f"{ndauapi}/tx/submit/{txtype}", json=tx)
    assert resp.status_code == want_status
    assert want_body in resp.text

    # We'll repost the same transactions for expected no-ops.
    want_status = requests.codes.accepted

    # Prevalidate tx again.
    resp = requests.post(f"{ndauapi}/tx/prevalidate/{txtype}", json=tx)
    assert resp.status_code == want_status
    assert want_body in resp.text

    # Submit tx again.
    resp = requests.post(f"{ndauapi}/tx/submit/{txtype}", json=tx)
    assert resp.status_code == want_status
    assert want_body in resp.text
Exemplo n.º 14
0
def test_lock_notify(ndau, set_up_account):
    """Test Lock and Notify transactions"""

    # Set up account to lock.
    account = random_string("lock-notify")
    set_up_account(account)

    # Lock
    lock_months = 3
    ndau(f"account lock {account} {lock_months}m")
    account_data = json.loads(ndau(f"account query {account}"))
    assert account_data["lock"] is not None
    assert account_data["lock"]["unlocksOn"] is None

    # Notify
    ndau(f"account notify {account}")
    account_data = json.loads(ndau(f"account query {account}"))
    assert account_data["lock"] is not None
    assert account_data["lock"]["unlocksOn"] is not None
Exemplo n.º 15
0
def set_validation_json(ndau):
    name = random_string("json-acct")
    ndau(f"account new {name}")
    # JSG must rfe before set_validation to pay for tx fees
    ndau(f"rfe 10 {name}")
    return json.loads(ndau(f"-j account set-validation {name}"))
Exemplo n.º 16
0
def test_genesis(
    ndau, rfe, ndau_suppress_err, netconf, zero_tx_fees, node_rules_account
):
    # Set up a purchaser account.  We don't have to rfe to it to pay for
    # 0-napu set-validation tx fee.
    purchaser_account = random_string("genesis-purchaser")
    ndau(f"account new {purchaser_account}")
    ndau(f"account set-validation {purchaser_account}")

    # Put a lot of ndau in there so small EAI fee percentages are non-zero.
    ndau_locked = 1_000_000
    ndau(f"rfe {ndau_locked} {purchaser_account}")
    ndau(f"issue {ndau_locked}")

    # Lock it for a long time to maximize EAI.
    lock_years = 3
    ndau(f"account lock {purchaser_account} {lock_years}y")

    # Set up a node operator account with 1000 ndau needed to self-stake.
    stake_ndau = 1000
    node_account = random_string("genesis-node")
    ndau(f"account new {node_account}")
    # We can set-validation the accont before funding it since tx fees are zero.
    ndau(f"account set-validation {node_account}")
    rfe(stake_ndau, node_account)

    # Stake to node rules account
    ndau(
        f"account stake {node_account} "
        f"--rules-address={node_rules_account} --staketo-address={node_rules_account} "
        f"{stake_ndau}"
    )

    # Bytes lifted from tx_register_node_test.go.
    distribution_script_bytes = b"\xa0\x00\x88"
    distribution_script = base64.b64encode(distribution_script_bytes).decode("utf-8")
    err_msg = ndau_suppress_err(
        f"account register-node {node_account} {distribution_script}"
    )
    assert err_msg == "" or err_msg.startswith("acct is already staked")

    # Delegate purchaser account to node account.
    ndau(f"account delegate {purchaser_account} {node_account}")
    node_addr = ndau(f"account addr {node_account}")

    # ensure the delegation succeeded
    purchaser_acct_data = json.loads(ndau(f"account query {purchaser_account}"))
    assert purchaser_acct_data["delegationNode"] == node_addr

    # We want to ensure that every account which is supposed to get a cut of EAI
    # receives non-zero EAI on a CreditEAI tx. But which accounts are supposed
    # to receive EAI?
    # - Every account in the EAI fee table
    # - All accounts delegated to the node account
    # - _Not_ the node account itself

    eai_fee_table = json.loads(ndau(f"sysvar get {constants.EAI_FEE_TABLE_KEY}"))[
        constants.EAI_FEE_TABLE_KEY
    ]

    # start acct_balances from the fee table
    acct_balances = {
        addr: json.loads(ndau(f"account query -a {addr}"))["balance"]
        for fee in eai_fee_table
        if fee["To"] is not None
        for addr in fee["To"]
    }
    # add the purchaser_account as a proxy for every accoutn delegated to this node
    acct_balances[ndau(f"account addr {purchaser_account}")] = purchaser_acct_data[
        "balance"
    ]
    node_past_balance = json.loads(ndau(f"account query {node_account}"))["balance"]

    # Submit CreditEAI tx so that bpc operations can have ndau to
    # pay for changing sysvars.
    ndau(f"account credit-eai {node_account}")

    for addr, past_balance in acct_balances.items():
        current_balance = json.loads(ndau(f"account query -a {addr}"))["balance"]
        # it is outside the scope of this test to compute _how much_ EAI each account
        # should have earned; that's the province of EAI unit tests. We just want to
        # ensure that they all got credited.
        assert current_balance > past_balance

    # ensure the node didn't yet receive any EAI
    assert (
        node_past_balance
        == json.loads(ndau(f"account query {node_account}"))["balance"]
    )

    # Now  attempt to test NNR
    #
    # Unfortunately, we can only run this integration test once per day.  When
    # running against localnet, we can do a reset to test NNR repeatedly. We
    # use a value of 0 (any value will do) for deterministic nomination.

    # Set up a reward target account.  Tx fee is still zero so we don't
    # have to rfe to it.
    reward_account = random_string("genesis-node-reward")
    ndau(f"account new {reward_account}")
    ndau(f"account set-validation {reward_account}")
    ndau(f"account set-rewards-target {node_account} {reward_account}")

    nnr_result = ndau_suppress_err(f"nnr 0")
    if not nnr_result.startswith("not enough time since last NNR"):
        # Claim node rewards and see that the node operator gets his EAI in
        # the reward account.  We check the reward account.  If we didn't
        # set a reward target, then the node account would receive the ndau
        # here.  That was tested and worked, but since we can only do one
        # NNR per day, we test the more complex situation of awarding to a
        # target reward account.
        reward_result = ndau_suppress_err(f"account claim-node-reward {node_account}")
        # Despite sending a constant "random" number to the NNR calc, we
        # can't know which node will win; that depends on the state of the
        # network, which nodes are delegated, and which have balances.
        # What we can test is that if the one we're watching happened to win,
        # its reward target received its reward.
        if reward_result.startswith("winner was"):
            winner = reward_result.split()[2]
            if winner == node_addr:
                reward_balance = json.loads(ndau(f"account query {reward_account}"))[
                    "balance"
                ]
                # this works because this is an otherwise brand-new account,
                # which has never received any other rewards or transfers
                assert reward_balance > 0
Exemplo n.º 17
0
def test_command_validator_change(
    ndau, ndau_suppress_err, keytool, set_up_account, node_rules_account
):
    """Test CommandValidatorChange transaction"""

    # testing CVC necessarily involves testing the node to which we are
    # connected: that's the only one which shows up in the info section.
    # however, we have to make some assumptions. In particular, we assume
    # that we're connected to a TM localnet whose config data is in a
    # standardized location. If that's not in fact the case, then we have to
    # just skip this test.

    pvk = get_pvk()

    # Get info about the connected validator
    info = json.loads(ndau("info"))
    assert info["validator_info"] is not None
    assert info["validator_info"]["address"] == pvk["address"]

    info_pkb = bytes(info["validator_info"]["pub_key"])
    pvk_pkb = base64.b64decode(pvk["pub_key"]["value"])
    assert info_pkb == pvk_pkb

    # with this, we're satisfied that info contains the public-private keypair
    # used to construct this validator.
    #
    # First, create the ndau variants of these keys
    ndpvt = keytool(f"ed raw private {pvk['priv_key']['value']} --b64")
    ndpub = keytool(f"ed raw public {pvk['pub_key']['value']} --b64")
    address = keytool(f"addr {ndpub}")

    # Now, we need to inject that data into ndautool.toml appropriately
    conf_path = ndau("conf-path")
    with open(conf_path, "r") as f:
        cpd = toml.load(f)

    # do we already have an account referring to the node we're connected to?
    ln0 = None
    for account in cpd["accounts"]:
        # skip accounts which don't have public ownership keys
        if "ownership" not in account or "public" not in account["ownership"]:
            continue
        if account["ownership"]["public"] == ndpub:
            ln0 = account
            break

    # create an account if it doesn't exist
    if ln0 is None:
        name = random_string("localnet-0")
        ln0 = {
            "name": name,
            "address": address,
            "ownership": {"public": ndpub, "private": ndpvt},
        }
        cpd["accounts"].append(ln0)

    # Set validation rules for the account if it has none
    set_validation = None
    if "validation" not in ln0 or len(ln0["validation"]) == 0:
        # in order for this test to be repeatable, we need predictable validation keys
        # the ndau tool can't do this for us directly, so we have to work around it.
        # these keys are arbitrary constants
        ln0["validation"] = [
            {
                "public": "npuba8jadtbbebbp5iixnbv2kp5suzt35am2zu4gjg2e9t4ghzci97nj7a5mnrvx823883tpfa3f",  # noqa: E501 this line can't usefully be shortened
                "private": "npvtayjadtcbiahcbm8k5ik5piz5n86itab9ffx7qf244ayhnaqwz5fw3c4aj3zmqsy7wekya36fg72jm267sf6m3pdevncr27dd5ter8ye8spxyh349uxz8s684",  # noqa: E501 this one either
            }
        ]
        acct_data = json.loads(ndau("account query -a=" + ln0["address"]))
        pubkeys = [t["public"] for t in ln0["validation"]]

        valkeys = acct_data.get("validationKeys", [])
        if valkeys is None:
            valkeys = []
        if len(valkeys) == 0:
            valkeys = pubkeys
            set_validation = {
                "target": ln0["address"],
                "ownership": ndpub,
                "validation_keys": valkeys,
                "validation_script": None,
                "sequence": 1 + acct_data["sequence"],
            }

        valkeys.sort()
        pubkeys.sort()

        assert valkeys == pubkeys

    # now update ndautool.toml
    with open(conf_path, "w") as f:
        toml.dump(cpd, f)

    if set_validation is not None:
        txb64 = ndau(
            f"signable-bytes setvalidation", input=json.dumps(set_validation)
        )
        set_validation["signature"] = keytool(f"sign {ndpvt} {txb64} --b64")
        stdout = ndau("send setvalidation", input=json.dumps(set_validation))

    # rfe enough ndau to stake
    ndau(f'rfe 1000 {ln0["name"]}')
    # Stake to node rules account
    stdout = ndau_suppress_err(
        f"account stake {ln0['name']} "
        f"--rules-address={node_rules_account} --staketo-address={node_rules_account} "
        "1000"
    )
    if len(stdout.strip()) > 0:
        # the most likely error in this case is that the account is already staked
        # to the node rules account, and you can't have two primary stakes to the same
        # rules account. If that's the case, then everything is fine.
        print(stdout)

    stdout = ndau_suppress_err(
        # script from
        # https://github.com/ndau/commands/blob/master/
        #         cmd/chasm/examples/zero.chbin
        f"account register-node {ln0['name']} oAAgiA"
    )
    if len(stdout.strip()) > 0:
        # the most likely error in this case is that the node is already registered,
        # in which case everything is fine
        print(stdout)

    assert info["validator_info"]["voting_power"] is not None
    old_power = info["validator_info"]["voting_power"]

    # Cycle over a power range of 5, starting at the default power of 10.
    new_power = 10 + (old_power + 6) % 5

    # CVC
    ndau(f"cvc {ln0['name']} {new_power}")

    # Make up to 10 attempts for the change in power to propagate.
    new_voting_power_was_set = False
    for _ in range(10):
        info = json.loads(ndau("info"))
        assert info["validator_info"] is not None
        voting_power = info["validator_info"]["voting_power"]
        if voting_power == new_power:
            new_voting_power_was_set = True
            break
        assert voting_power == old_power
        sleep(1)
    assert new_voting_power_was_set