Add SN reg/stake/unstake tests

This commit is contained in:
Jason Rhinelander 2023-04-28 19:46:15 -03:00
parent 2f18b2f7da
commit aa9ff6a6d3
No known key found for this signature in database
GPG Key ID: C4992CE7A88D4262
4 changed files with 178 additions and 22 deletions

View File

@ -65,3 +65,8 @@ def alice(net):
@pytest.fixture
def bob(net):
return net.bob
# Gives you an (unstaked) sn
@pytest.fixture
def sn(net):
return net.unstaked_sns[0]

View File

@ -1,6 +1,7 @@
import pytest
import time
import re
from functools import partial
from service_node_network import coins, vprint
from ledgerapi import LedgerAPI
@ -61,7 +62,7 @@ def test_send(net, mike, alice, hal, ledger):
run_with_interactions(
ledger,
lambda: hal.transfer(alice, coins(42.5)),
partial(hal.transfer, alice, coins(42.5)),
ExactScreen(["Processing TX"]),
MatchScreen([r"^Confirm Fee$", r"^(0.01\d{1,7})$"], store_fee, fail_index=1),
Do.right,
@ -83,6 +84,7 @@ def test_send(net, mike, alice, hal, ledger):
Do.left,
ExactScreen(["Accept"]),
Do.both,
ExactScreen(["Processing TX"]),
)
net.mine(1)
@ -129,7 +131,7 @@ def test_multisend(net, mike, alice, bob, hal, ledger):
hal.timeout = 120 # creating this tx with the ledger takes ages
run_with_interactions(
ledger,
lambda: hal.multi_transfer((alice, bob, alice, alice, hal), coins(18, 19, 20, 21, 22)),
partial(hal.multi_transfer, (alice, bob, alice, alice, hal), coins(18, 19, 20, 21, 22)),
ExactScreen(["Processing TX"]),
MatchScreen([r"^Confirm Fee$", r"^(0.\d{1,9})$"], store_fee, fail_index=1),
Do.right,
@ -165,6 +167,7 @@ def test_multisend(net, mike, alice, bob, hal, ledger):
Do.right,
ExactScreen(["Accept"]),
Do.both,
ExactScreen(["Processing TX"]),
timeout=120,
)
@ -183,6 +186,130 @@ def test_multisend(net, mike, alice, bob, hal, ledger):
assert alice.balances(refresh=True) == coins(18 + 20 + 21, 0)
assert bob.balances(refresh=True) == coins(19, 0)
net.mine(9)
assert hal.balances(refresh=True) == tuple([remaining] * 2)
assert alice.balances(refresh=True) == tuple(coins([18 + 20 + 21] * 2))
assert hal.balances(refresh=True) == (remaining,) * 2
assert alice.balances(refresh=True) == coins((18 + 20 + 21,) * 2)
assert bob.balances(refresh=True) == coins(19, 19)
def check_sn_rewards(net, hal, sn, starting_bal, reward):
net.mine(5) # 5 blocks until it starts earning rewards (testnet/fakenet)
hal_bal = hal.balances(refresh=True)
batch_offset = None
assert hal_bal == coins(starting_bal, 0)
# We don't know where our batch payment occurs yet, but let's look for it:
for i in range(20):
net.mine(1)
if hal.balances(refresh=True)[0] > coins(starting_bal):
batch_offset = sn.height() % 20
break
assert batch_offset is not None
hal_bal = hal.balances()
net.mine(19)
assert hal.balances(refresh=True)[0] == hal_bal[0]
net.mine(1) # Should be our batch height
assert hal.balances(refresh=True)[0] == hal_bal[0] + coins(20 * reward)
def test_sn_register(net, mike, hal, ledger, sn):
mike.transfer(hal, coins(101))
net.mine()
assert hal.balances(refresh=True) == coins(101, 101)
fee = None
def store_fee(_, m):
nonlocal fee
fee = float(m[1][1])
run_with_interactions(
ledger,
partial(hal.register_sn, sn),
ExactScreen(["Processing Stake"]),
MatchScreen([r"^Confirm Fee$", r"^(0.01\d{1,7})$"], store_fee, fail_index=1),
Do.right,
ExactScreen(["Accept"]),
Do.both,
ExactScreen(["Confirm Stake", "100.0"], fail_index=1),
Do.right,
ExactScreen(["Accept"]),
Do.right,
ExactScreen(["Reject"]),
Do.left,
Do.both,
ExactScreen(["Processing Stake"]),
)
# We are half the SN network, so get half of the block reward per block:
reward = 0.5 * 16.5
check_sn_rewards(net, hal, sn, 101 - fee, reward)
def test_sn_stake(net, mike, alice, hal, ledger, sn):
mike.multi_transfer([hal, alice], coins(13.02, 87.02))
net.mine()
assert hal.balances(refresh=True) == coins(13.02, 13.02)
assert alice.balances(refresh=True) == coins(87.02, 87.02)
alice.register_sn(sn, stake=coins(87))
net.mine(1)
fee = None
def store_fee(_, m):
nonlocal fee
fee = float(m[1][1])
run_with_interactions(
ledger,
partial(hal.stake_sn, sn, coins(13)),
ExactScreen(["Processing Stake"]),
MatchScreen([r"^Confirm Fee$", r"^(0.01\d{1,7})$"], store_fee, fail_index=1),
Do.right,
ExactScreen(["Accept"]),
Do.both,
ExactScreen(["Confirm Stake", "13.0"], fail_index=1),
Do.right,
ExactScreen(["Accept"]),
Do.right,
ExactScreen(["Reject"]),
Do.left,
Do.both,
ExactScreen(["Processing Stake"]),
)
# Our SN is 1 or 2 registered, so we get 50% of the 16.5 reward, 10% is removed for operator
# fee, then hal gets 13/100 of the rest:
reward = 0.5 * 16.5 * 0.9 * 0.13
check_sn_rewards(net, hal, sn, 13 - fee, reward)
def test_sn_unstake(net, mike, hal, ledger, sn):
# Do the full registration:
test_sn_register(net, mike, hal, ledger, sn)
run_with_interactions(
ledger,
partial(hal.unstake_sn, sn),
ExactScreen(["Confirm Service", "Node Unlock"]),
Do.right,
ExactScreen(["Accept"]),
Do.right,
ExactScreen(["Reject"]),
Do.left,
Do.both,
)
# A fakechain unlock takes 30 blocks, plus add another 20 just so we are sure we've received the
# last batch reward:
net.mine(30 + 20)
hal_bal = hal.balances(refresh=True)
net.mine(20)
assert hal.balances(refresh=True) == hal_bal

View File

@ -6,6 +6,16 @@ import requests
import subprocess
import time
def coins(*args):
if len(args) != 1:
return tuple(coins(x) for x in args)
x = args[0]
if type(x) in (tuple, list):
return type(x)(coins(i) for i in x)
return round(x * 1_000_000_000)
# On linux we can pick a random 127.x.y.z IP which is highly likely to not have anything listening
# on it (so we make bind conflicts highly unlikely). On most other OSes we have to listen on
# 127.0.0.1 instead, so we pick a random starting port instead to try to minimize bind conflicts.
@ -208,6 +218,9 @@ class Daemon(RPCDaemon):
{"miner_address": a, "threads_count": 1, "num_blocks": num_blocks, "slow_mining": slow},
)
def sn_pubkey(self):
return self.json_rpc("get_service_keys").json()["result"]["service_node_pubkey"]
def height(self):
return self.rpc("/get_height").json()["height"]
@ -414,20 +427,39 @@ class Wallet(RPCDaemon):
return [find_tx(txid) for txid in txids]
def register_sn(self, sn):
def register_sn(self, sn, stake=coins(100), fee=10):
r = sn.json_rpc(
"get_service_node_registration_cmd",
{
"operator_cut": "100",
"contributions": [{"address": self.address(), "amount": 100000000000}],
"staking_requirement": 100000000000,
"operator_cut": "100" if stake == coins(100) else f"{fee}",
"contributions": [{"address": self.address(), "amount": stake}],
"staking_requirement": coins(100),
},
).json()
if "error" in r:
raise RuntimeError(f"Registration cmd generation failed: {r['error']['message']}")
cmd = r["result"]["registration_cmd"]
if cmd == "":
# everything about this command is dumb, include its error handling
raise RuntimeError(f"Registration cmd generation failed: {r['result']['status']}")
r = self.json_rpc("register_service_node", {"register_service_node_str": cmd}).json()
if "error" in r:
raise RuntimeError(
"Failed to submit service node registration tx: {}".format(r["error"]["message"])
)
def stake_sn(self, sn, stake):
r = self.json_rpc(
"stake",
{"destination": self.address(), "amount": stake, "service_node_key": sn.sn_pubkey()},
).json()
if "error" in r:
raise RuntimeError(f"Failed to submit stake: {r['error']['message']}")
def unstake_sn(self, sn):
r = self.json_rpc("request_stake_unlock", {"service_node_key": sn.sn_pubkey()}).json()
if "error" in r:
raise RuntimeError(f"Failed to submit unstake: {r['error']['message']}")
if not r["result"]["unlocked"]:
raise RuntimeError(f"Failed to submit unstake: {r['result']['msg']}")

View File

@ -16,7 +16,7 @@
# wallets and nodes each time (see the fixture for details).
from daemons import Daemon, Wallet
from daemons import Daemon, Wallet, coins
import vprint as v
from vprint import vprint
import random
@ -26,15 +26,6 @@ import uuid
import pytest
def coins(*args):
if len(args) != 1:
return tuple(coins(x) for x in args)
x = args[0]
if type(x) in (tuple, list):
return type(x)(coins(i) for i in x)
return round(x * 1_000_000_000)
def wait_for(callback, timeout=10):
expires = time.time() + timeout
while True:
@ -49,7 +40,7 @@ def wait_for(callback, timeout=10):
class SNNetwork:
def __init__(self, datadir, *, binpath, sns=20, nodes=3):
def __init__(self, datadir, *, binpath, sns=20, nodes=3, unstaked_sns=0):
self.datadir = datadir
self.binpath = binpath
@ -58,9 +49,10 @@ class SNNetwork:
nodeopts = dict(oxend=self.binpath + "/oxend", datadir=datadir)
self.sns = [Daemon(service_node=True, **nodeopts) for _ in range(sns)]
self.unstaked_sns = [Daemon(service_node=True, **nodeopts) for _ in range(unstaked_sns)]
self.nodes = [Daemon(**nodeopts) for _ in range(nodes)]
self.all_nodes = self.sns + self.nodes
self.all_nodes = self.sns + self.unstaked_sns + self.nodes
self.wallets = []
for name in ("Alice", "Bob", "Mike"):
@ -86,7 +78,7 @@ class SNNetwork:
"Starting new oxend service nodes with RPC on {} ports".format(self.sns[0].listen_ip),
end="",
)
for sn in self.sns:
for sn in self.sns + self.unstaked_sns:
vprint(" {}".format(sn.rpc_port), end="", flush=True, timestamp=False)
sn.start()
vprint(timestamp=False)
@ -308,7 +300,7 @@ def basic_net(pytestconfig, tmp_path, binary_dir):
v.verbose = pytestconfig.getoption("verbose") >= 2
if v.verbose:
print("\nConstructing initial node network")
snn = SNNetwork(datadir=tmp_path, binpath=binary_dir, sns=1, nodes=1)
snn = SNNetwork(datadir=tmp_path, binpath=binary_dir, sns=1, nodes=1, unstaked_sns=1)
else:
snn.alice.new_wallet()
snn.bob.new_wallet()