mirror of
https://github.com/oxen-io/oxen-core.git
synced 2023-12-14 02:22:56 +01:00
Add SN reg/stake/unstake tests
This commit is contained in:
parent
2f18b2f7da
commit
aa9ff6a6d3
4 changed files with 178 additions and 22 deletions
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']}")
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue