local devnet scripts

This commit is contained in:
Sean Darcy 2021-05-06 15:31:49 +10:00
parent 6d13152f4e
commit 2ddf644844
10 changed files with 823 additions and 9 deletions

View File

@ -97,7 +97,11 @@ static constexpr HardFork::Params testnet_hard_forks[] =
static constexpr HardFork::Params devnet_hard_forks[] =
{
{ network_version_7, 1, 0, 1599848400 },
{ network_version_16_pulse, 2, 0, 1599848400 }, // 2020-09-11 18:20 UTC
{ network_version_11_infinite_staking, 2, 0, 1599848400 },
{ network_version_12_checkpointing, 3, 0, 1599848400 },
{ network_version_13_enforce_checkpoints, 4, 0, 1599848400 },
{ network_version_15_ons, 5, 0, 1599848400 },
{ network_version_16_pulse, 99, 0, 1599848400 }, // 2020-09-11 18:20 UTC
};
uint64_t HardFork::get_hardcoded_hard_fork_height(network_type nettype, cryptonote::network_version version)

View File

@ -298,7 +298,7 @@ namespace config
inline constexpr uint16_t ZMQ_RPC_DEFAULT_PORT = 38858;
inline constexpr uint16_t QNET_DEFAULT_PORT = 38859;
inline constexpr boost::uuids::uuid const NETWORK_ID = { {
0xa9, 0xf7, 0x5c, 0x7d, 0x55, 0x17, 0xcb, 0x6b, 0x5a, 0xf4, 0x63, 0x79, 0x7a, 0x57, 0xab, 0xd3
0xa9, 0xf7, 0x5c, 0x7d, 0x55, 0x17, 0xcb, 0x6b, 0x5b, 0xf4, 0x63, 0x79, 0x7a, 0x57, 0xab, 0xd3
} };
inline constexpr std::string_view GENESIS_TX = "04011e1e01ff00018080c9db97f4fb2702fa27e905f604faa4eb084ee675faca77b0cfea9adec1526da33cae5e286f31624201dae05bf3fa1662b7fd373c92426763d921cf3745e10ee43edb510f690c656f247200000000000000000000000000000000000000000000000000000000000000000000"sv;
inline constexpr uint32_t GENESIS_NONCE = 12345;
@ -309,6 +309,8 @@ namespace config
"dV3EhSE1xXgSzswBgVioqFNTfcqGopvTrcYjs4YDLHUfU64DuHxFoEmbwoyipTidGiTXx5EuYdgzZhDLMTo9uEv82M4A7Uimp"sv, // hardfork v7-9
"dV3EhSE1xXgSzswBgVioqFNTfcqGopvTrcYjs4YDLHUfU64DuHxFoEmbwoyipTidGiTXx5EuYdgzZhDLMTo9uEv82M4A7Uimp"sv, // hardfork v10
};
inline constexpr auto UPTIME_PROOF_STARTUP_DELAY = 5s;
}
namespace fakechain {

View File

@ -14,15 +14,9 @@ namespace service_nodes {
// TODO(oxen): Move to oxen_economy, this will also need access to oxen::exp2
uint64_t get_staking_requirement(cryptonote::network_type m_nettype, uint64_t height, uint8_t hf_version)
{
if (m_nettype == cryptonote::TESTNET || m_nettype == cryptonote::FAKECHAIN)
if (m_nettype == cryptonote::TESTNET || m_nettype == cryptonote::FAKECHAIN || m_nettype == cryptonote::DEVNET)
return COIN * 100;
// For devnet we use the 10% of mainnet requirement at height (650k + H) so that we follow
// (proportionally) whatever staking changes happen on mainnet. (The 650k is because devnet
// launched at ~600k mainnet height, so this puts it a little ahead).
if (m_nettype == cryptonote::DEVNET)
return get_staking_requirement(cryptonote::MAINNET, 600000 + height, hf_version) / 10;
if (hf_version >= cryptonote::network_version_16_pulse)
return 15000'000000000;

1
utils/local-devnet/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
testdata

View File

@ -0,0 +1,40 @@
#!/usr/bin/python3
import sys
sys.path.append('../testdata')
import config
import requests
import argparse
import json
def instruct_daemon(method, params):
payload = json.dumps({"method": method, "params": params}, skipkeys=False)
print(payload)
headers = {'content-type': "application/json"}
try:
response = requests.request("POST", "http://"+config.listen_ip+":"+config.listen_port+"/json_rpc", data=payload, headers=headers)
return json.loads(response.text)
except requests.exceptions.RequestException as e:
print(e)
except:
print('No response from daemon, check daemon is running on this machine')
parser = argparse.ArgumentParser(description='Get Block.')
parser.add_argument("--height", help="An integer for the height to be queried", type=int)
args = parser.parse_args()
params = {"decode_as_json": True}
for arg in vars(args):
# params['height'] = args.height
if (getattr(args,arg) is not None):
params[arg] = getattr(args, arg)
# $ curl -X POST http://127.0.0.1:18081/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"get_block","params":{"height":912345}}' -H 'Content-Type: application/json'
answer = instruct_daemon('get_block', params)
print(json.dumps(answer, indent=4, sort_keys=True))

View File

@ -0,0 +1,34 @@
#!/usr/bin/python3
import sys
sys.path.append('../testdata')
import config
import requests
import json
def instruct_daemon(method, params):
payload = json.dumps({"method": method, "params": params})
headers = {'content-type': "application/json"}
try:
response = requests.request("POST", "http://"+config.listen_ip+":"+config.listen_port+"/json_rpc", data=payload, headers=headers)
return json.loads(response.text)
except requests.exceptions.RequestException as e:
print(e)
except:
print('No response from daemon, check daemon is running on this machine')
# curl -X POST $LISTENING/json_rpc \
# -H 'Content-Type: application/json' \
# -d @- << EOF
# {
# "jsonrpc": "2.0",
# "id": "0",
# "method": "get_height"
# }
answer = instruct_daemon('getheight', [])
print(json.dumps(answer, indent=4, sort_keys=True))

View File

@ -0,0 +1,27 @@
#!/usr/bin/python3
import sys
sys.path.append('../testdata')
import config
import requests
import json
def instruct_daemon(method, params):
payload = json.dumps({"method": method, "params": params}, skipkeys=False)
print(payload)
headers = {'content-type': "application/json"}
try:
response = requests.request("POST", "http://"+config.listen_ip+":"+config.listen_port+"/json_rpc", data=payload, headers=headers)
return json.loads(response.text)
except requests.exceptions.RequestException as e:
print(e)
except:
print('No response from daemon, check daemon is running on this machine')
service_node_pubkeys = []
answer = instruct_daemon('get_service_nodes', [])
print(json.dumps(answer, indent=4, sort_keys=True))

View File

@ -0,0 +1,37 @@
#!/usr/bin/python3
import sys
sys.path.append('../testdata')
import config
import requests
import argparse
import json
def instruct_daemon(method, params):
payload = json.dumps({"method": method, "params": params}, skipkeys=False)
print(payload)
headers = {'content-type': "application/json"}
try:
response = requests.request("POST", "http://"+config.listen_ip+":"+config.listen_port+"/json_rpc", data=payload, headers=headers)
return json.loads(response.text)
except requests.exceptions.RequestException as e:
print(e)
except:
print('No response from daemon, check daemon is running on this machine')
parser = argparse.ArgumentParser(description='Get Block.')
parser.add_argument("--hash", help="An integer for the height to be queried", type=int)
args = parser.parse_args()
if not (args.hash):
parser.error('No arguments provided.')
# $ curl -X POST http://127.0.0.1:18081/get_transactions -d '{"txs_hashes":["d6e48158472848e6687173a91ae6eebfa3e1d778e65252ee99d7515d63090408"]}' -H 'Content-Type: application/json'
answer = instruct_daemon('get_transactions', {"txs_hashes":[args.hash], "decode_as_json": True})
print(json.dumps(answer, indent=4, sort_keys=True))

View File

@ -0,0 +1,365 @@
#!/usr/bin/python3
import sys
import random
import requests
import subprocess
import time
# 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.
LISTEN_IP, NEXT_PORT = (
('127.' + '.'.join(str(random.randint(1, 254)) for _ in range(3)), 1100)
if sys.platform == 'linux' else
('127.0.0.1', random.randint(5000, 20000)))
verbose = True
# verbose = False
def next_port():
global NEXT_PORT
port = NEXT_PORT
NEXT_PORT += 1
return port
class ProcessExited(RuntimeError):
pass
class TransferFailed(RuntimeError):
def __init__(self, message, json):
super().__init__(message)
self.message = message
self.json = json
class RPCDaemon:
def __init__(self, name):
self.name = name
self.proc = None
self.terminated = False
def __del__(self):
self.stop()
def terminate(self, repeat=False):
"""Sends a TERM signal if one hasn't already been sent (or even if it has, with
repeat=True). Does not wait for exit."""
if self.proc and (repeat or not self.terminated):
self.proc.terminate()
self.terminated = True
def start(self):
global verbose
sout=subprocess.DEVNULL
if verbose == True:
sout=sys.stdout
verbose = False
if self.proc and self.proc.poll() is None:
raise RuntimeError("Cannot start process that is already running!")
self.proc = subprocess.Popen(self.arguments(),
stdin=subprocess.DEVNULL, stdout=sout, stderr=subprocess.DEVNULL)
self.terminated = False
def stop(self):
"""Tries stopping with a term at first, then a kill if the term hasn't worked after 10s"""
if self.proc:
self.terminate()
try:
self.proc.wait(timeout=10)
except subprocess.TimeoutExpired:
print("{} took more than 10s to exit, killing it".format(self.name))
self.proc.kill()
self.proc = None
def arguments(self):
"""Returns the startup arguments; default is just self.args, but subclasses can override."""
return self.args
def json_rpc(self, method, params=None, *, timeout=10):
"""Sends a json_rpc request to the rpc port. Returns the response object."""
if not self.proc:
raise RuntimeError("Cannot make rpc request before calling start()")
json = {
"jsonrpc": "2.0",
"id": "0",
"method": method,
}
if params:
json["params"] = params
return requests.post('http://{}:{}/json_rpc'.format(self.listen_ip, self.rpc_port), json=json, timeout=timeout)
def rpc(self, path, params=None, *, timeout=10):
"""Sends a non-json_rpc rpc request to the rpc port at path `path`, e.g. /get_info. Returns the response object."""
if not self.proc:
raise RuntimeError("Cannot make rpc request before calling start()")
return requests.post('http://{}:{}{}'.format(self.listen_ip, self.rpc_port, path), json=params, timeout=timeout)
def wait_for_json_rpc(self, method, params=None, *, timeout=10):
"""Calls `json_rpc', sleeping if it fails for up time `timeout' seconds. Returns the
response if it succeeds, raises the last exception if timeout is reached. If the process
exit, raises a RuntimeError"""
until = time.time() + timeout
now = time.time()
while now < until:
exit_status = self.proc.poll()
if exit_status is not None:
raise ProcessExited("{} exited ({}) while waiting for an RPC response".format(self.name, exit_status))
timeout = until - now
try:
return self.json_rpc(method, params, timeout=timeout)
except:
if time.time() + .25 >= until:
raise
time.sleep(.25)
now = time.time()
if now >= until:
raise
class Daemon(RPCDaemon):
base_args = ('--dev-allow-local-ips', '--fixed-difficulty=1', '--devnet', '--non-interactive')
def __init__(self, *,
oxend='oxend',
listen_ip=None, p2p_port=None, rpc_port=None, zmq_port=None, qnet_port=None, ss_port=None,
name=None,
datadir=None,
service_node=False,
log_level=2,
peers=()):
self.rpc_port = rpc_port or next_port()
if name is None:
name = 'oxend@{}'.format(self.rpc_port)
super().__init__(name)
self.listen_ip = listen_ip or LISTEN_IP
self.p2p_port = p2p_port or next_port()
self.zmq_port = zmq_port or next_port()
self.qnet_port = qnet_port or next_port()
self.ss_port = ss_port or next_port()
self.peers = []
self.args = [oxend] + list(self.__class__.base_args)
self.args += (
'--data-dir={}/oxen-{}-{}'.format(datadir or '.', self.listen_ip, self.rpc_port),
'--log-level={}'.format(log_level),
'--log-file=oxen.log'.format(self.listen_ip, self.p2p_port),
'--p2p-bind-ip={}'.format(self.listen_ip),
'--p2p-bind-port={}'.format(self.p2p_port),
'--rpc-admin={}:{}'.format(self.listen_ip, self.rpc_port),
'--quorumnet-port={}'.format(self.qnet_port),
)
for d in peers:
self.add_peer(d)
if service_node:
self.args += (
'--service-node',
'--service-node-public-ip={}'.format(self.listen_ip),
'--storage-server-port={}'.format(self.ss_port),
)
def arguments(self):
return self.args + [
'--add-exclusive-node={}:{}'.format(node.listen_ip, node.p2p_port) for node in self.peers]
def ready(self):
"""Waits for the daemon to get ready, i.e. for it to start returning something to a
`get_info` rpc request. Calls start() if it hasn't already been called."""
if not self.proc:
self.start()
self.wait_for_json_rpc("get_info")
def add_peer(self, node):
"""Adds a peer. Must be called before starting."""
if self.proc:
raise RuntimeError("add_peer needs to be called before start()")
self.peers.append(node)
def remove_peer(self, node):
"""Removes a peer. Must be called before starting."""
if self.proc:
raise RuntimeError("remove_peer needs to be called before start()")
self.peers.remove(node)
def mine_blocks(self, num_blocks, wallet, *, slow=True):
a = wallet.address()
self.rpc('/start_mining', {
"miner_address": a,
"threads_count": 1,
"num_blocks": num_blocks,
"slow_mining": slow
});
def height(self):
return self.rpc("/get_height").json()["height"]
def txpool_hashes(self):
return [x['id_hash'] for x in self.rpc("/get_transaction_pool").json()['transactions']]
def ping(self, *, storage=True, lokinet=True):
"""Sends fake storage server and lokinet pings to the running oxend"""
if storage:
self.json_rpc("storage_server_ping", { "version_major": 2, "version_minor": 1, "version_patch": 0 })
if lokinet:
self.json_rpc("lokinet_ping", { "version": [9,9,9] })
def send_uptime_proof(self):
"""Triggerst test uptime proof"""
self.json_rpc("test_trigger_uptime_proof")
def p2p_resync(self):
"""Triggers a p2p resync to happen soon (i.e. at the next p2p idle loop)."""
self.json_rpc("test_trigger_p2p_resync")
class Wallet(RPCDaemon):
base_args = ('--disable-rpc-login', '--non-interactive', '--password','', '--devnet', '--disable-rpc-long-poll',
'--daemon-ssl=disabled')
def __init__(
self,
node,
*,
rpc_wallet='oxen-wallet-rpc',
name=None,
datadir=None,
listen_ip=None,
rpc_port=None,
log_level=2):
self.listen_ip = listen_ip or LISTEN_IP
self.rpc_port = rpc_port or next_port()
self.node = node
self.name = name or 'wallet@{}'.format(self.rpc_port)
super().__init__(self.name)
self.walletdir = '{}/wallet-{}-{}'.format(datadir or '.', self.listen_ip, self.rpc_port)
self.args = [rpc_wallet] + list(self.__class__.base_args)
self.args += (
'--rpc-bind-ip={}'.format(self.listen_ip),
'--rpc-bind-port={}'.format(self.rpc_port),
'--log-level={}'.format(log_level),
'--log-file={}/log.txt'.format(self.walletdir),
'--daemon-address={}:{}'.format(node.listen_ip, node.rpc_port),
'--wallet-dir={}'.format(self.walletdir),
)
self.wallet_address = None
def ready(self, wallet="wallet", existing=False):
"""Makes the wallet ready, waiting for it to start up and create a new wallet (or load an
existing one, if `existing`) within the rpc wallet. Calls `start()` first if it hasn't
already been called. Does *not* explicitly refresh."""
if not self.proc:
self.start()
self.wallet_filename = wallet
if existing:
r = self.wait_for_json_rpc("open_wallet", {"filename": wallet, "password": ""})
else:
r = self.wait_for_json_rpc("create_wallet", {"filename": wallet, "password": "", "language": "English"})
if 'result' not in r.json():
raise RuntimeError("Cannot open or create wallet: {}".format(r['error'] if 'error' in r else 'Unexpected response: {}'.format(r)))
print('Started RPC Wallet - {}, on {}:{}'.format(self.name, self.listen_ip, self.rpc_port))
def refresh(self):
return self.json_rpc('refresh')
def address(self):
if not self.wallet_address:
self.wallet_address = self.json_rpc("get_address").json()["result"]["address"]
return self.wallet_address
def new_wallet(self):
self.wallet_address = None
r = self.wait_for_json_rpc("close_wallet")
if 'result' not in r.json():
raise RuntimeError("Cannot close current wallet: {}".format(r['error'] if 'error' in r else 'Unexpected response: {}'.format(r)))
if not hasattr(self, 'wallet_suffix'):
self.wallet_suffix = 2
else:
self.wallet_suffix += 1
r = self.wait_for_json_rpc("create_wallet", {"filename": "{}_{}".format(self.wallet_filename, self.wallet_suffix), "password": "", "language": "English"})
if 'result' not in r.json():
raise RuntimeError("Cannot create wallet: {}".format(r['error'] if 'error' in r else 'Unexpected response: {}'.format(r)))
def balances(self, refresh=False):
"""Returns (total, unlocked) balances. Can optionally refresh first."""
if refresh:
self.refresh()
b = self.json_rpc("get_balance").json()['result']
return (b['balance'], b['unlocked_balance'])
def transfer(self, to, amount=None, *, priority=None, sweep=False):
"""Attempts a transfer. Throws TransferFailed if it gets rejected by the daemon, otherwise
returns the 'result' key."""
if priority is None:
priority = 1
if sweep and not amount:
r = self.json_rpc("sweep_all", {"address": to.address(), "priority": priority})
elif amount and not sweep:
r = self.json_rpc("transfer_split", {"destinations": [{"address": to.address(), "amount": amount}], "priority": priority})
else:
raise RuntimeError("Wallet.transfer: either `sweep` or `amount` must be given")
r = r.json()
if 'error' in r:
raise TransferFailed("Transfer failed: {}".format(r['error']['message']), r)
return r['result']
def find_transfers(self, txids, in_=True, pool=True, out=True, pending=False, failed=False):
transfers = self.json_rpc('get_transfers', {'in':in_, 'pool':pool, 'out':out, 'pending':pending, 'failed':failed }).json()['result']
def find_tx(txid):
for type_, txs in transfers.items():
for tx in txs:
if tx['txid'] == txid:
return tx
return [find_tx(txid) for txid in txids]
def register_sn(self, sn):
r = sn.json_rpc("get_service_node_registration_cmd", {
"operator_cut": "100",
"contributions": [{"address": self.address(), "amount": 100000000000}],
"staking_requirement": 100000000000
}).json()
if 'error' in r:
raise RuntimeError("Registration cmd generation failed: {}".format(r['error']['message']))
cmd = r['result']['registration_cmd']
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']))

View File

@ -0,0 +1,310 @@
#!/usr/bin/python3
from daemons import Daemon, Wallet
import random
import time
import shutil
import os
from os import path
import asyncio
import glob
from datetime import datetime
import uuid
datadirectory="testdata"
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 * 1000000000)
def wait_for(callback, timeout=10):
expires = time.time() + timeout
while True:
try:
if callback():
return
except:
pass
if time.time() >= expires:
raise RuntimeError("task timeout expired")
time.sleep(.25)
verbose = True
def vprint(*args, timestamp=True, **kwargs):
global verbose
if verbose:
if timestamp:
print(datetime.now(), end=" ")
print(*args, **kwargs)
class SNNetwork:
def __init__(self, datadir, *, binpath='../../build/bin', sns=12, nodes=3):
self.datadir = datadir
if not os.path.exists(self.datadir):
os.makedirs(self.datadir)
self.binpath = binpath
vprint("Using '{}' for data files and logs".format(datadir))
nodeopts = dict(oxend=self.binpath+'/oxend', datadir=datadir)
self.sns = [Daemon(service_node=True, **nodeopts) for _ in range(sns)]
self.nodes = [Daemon(**nodeopts) for _ in range(nodes)]
self.all_nodes = self.sns + self.nodes
self.wallets = []
for name in ('Alice', 'Bob', 'Mike'):
self.wallets.append(Wallet(
node=self.nodes[len(self.wallets) % len(self.nodes)],
name=name,
rpc_wallet=self.binpath+'/oxen-wallet-rpc',
datadir=datadir))
self.alice, self.bob, self.mike = self.wallets
# Interconnections
for i in range(len(self.all_nodes)):
for j in (2, 3, 5, 7, 11):
k = (i + j) % len(self.all_nodes)
if i != k:
self.all_nodes[i].add_peer(self.all_nodes[k])
vprint("Starting new oxend service nodes with RPC on {} ports".format(self.sns[0].listen_ip), end="")
for sn in self.sns:
vprint(" {}".format(sn.rpc_port), end="", flush=True, timestamp=False)
sn.start()
vprint(timestamp=False)
vprint("Starting new regular oxend nodes with RPC on {} ports".format(self.nodes[0].listen_ip), end="")
for d in self.nodes:
vprint(" {}".format(d.rpc_port), end="", flush=True, timestamp=False)
d.start()
vprint(timestamp=False)
vprint("Waiting for all oxend's to get ready")
for d in self.all_nodes:
d.wait_for_json_rpc("get_info")
vprint("Oxends are ready. Starting wallets")
for w in self.wallets:
vprint("Starting new RPC wallet {w.name} at {w.listen_ip}:{w.rpc_port}".format(w=w))
w.start()
for w in self.wallets:
w.ready()
w.refresh()
vprint("Wallet {w.name} is ready: {a}".format(w=w, a=w.address()))
for w in self.wallets:
w.wait_for_json_rpc("refresh")
# Mine some blocks; we need 100 per SN registration, and we can nearly 600 on fakenet before
# it hits HF16 and kills mining rewards. This lets us submit the first 5 SN registrations a
# SN (at height 40, which is the earliest we can submit them without getting an occasional
# spurious "Not enough outputs to use" error).
# to unlock and the rest to have enough unlocked outputs for mixins), then more some more to
# earn SN rewards. We need 100 per SN registration, and each mined block gives us an input
# of 18.9, which means each registration requires 6 inputs. Thus we need a bare minimum of
# 6(N-5) blocks, plus the 30 lock time on coinbase TXes = 6N more blocks (after the initial
# 5 registrations).
self.mine(100)
vprint("Submitting first round of service node registrations: ", end="", flush=True)
for sn in self.sns[0:5]:
self.mike.register_sn(sn)
vprint(".", end="", flush=True, timestamp=False)
vprint(timestamp=False)
if len(self.sns) > 5:
vprint("Going back to mining", flush=True)
self.mine(6*len(self.sns))
self.print_wallet_balances()
vprint("Submitting more service node registrations: ", end="", flush=True)
for sn in self.sns[5:-1]:
self.mike.register_sn(sn)
vprint(".", end="", flush=True, timestamp=False)
vprint(timestamp=False)
vprint("Done.")
self.print_wallet_balances()
vprint("Mining 40 blocks (registrations + blink quorum lag) and waiting for nodes to sync")
self.sync_nodes(self.mine(40))
self.print_wallet_balances()
vprint("Sending fake lokinet/ss pings")
for sn in self.sns:
sn.ping()
all_service_nodes_proofed = lambda sn: all(x['quorumnet_port'] > 0 for x in
sn.json_rpc("get_n_service_nodes", {"fields":{"quorumnet_port":True}}).json()['result']['service_node_states'])
vprint("Waiting for proofs to propagate: ", end="", flush=True)
for sn in self.sns:
wait_for(lambda: all_service_nodes_proofed(sn), timeout=120)
vprint(".", end="", flush=True, timestamp=False)
vprint(timestamp=False)
for sn in self.sns[-1:]:
self.mike.register_sn(sn)
vprint(".", end="", flush=True, timestamp=False)
self.sync_nodes(self.mine(1))
time.sleep(10)
for sn in self.sns:
sn.send_uptime_proof()
vprint("Done.")
vprint("Local Devnet SN network setup complete!")
vprint("Communicate with daemon on ip: {} port: {}".format(self.sns[0].listen_ip,self.sns[0].rpc_port))
configfile=self.datadir+'config.py'
with open(configfile, 'w') as filetowrite:
filetowrite.write('#!/usr/bin/python3\n# -*- coding: utf-8 -*-\nlisten_ip=\"{}\"\nlisten_port=\"{}\"\nwallet_listen_ip=\"{}\"\nwallet_listen_port=\"{}\"\nwallet_address=\"{}\"\nexternal_address=\"{}\"'.format(self.sns[0].listen_ip,self.sns[0].rpc_port,self.mike.listen_ip,self.mike.rpc_port,self.mike.address(),self.bob.address()))
def refresh_wallets(self, *, extra=[]):
vprint("Refreshing wallets")
for w in self.wallets + extra:
w.refresh()
vprint("All wallets refreshed")
def mine(self, blocks=None, wallet=None, *, sync=False):
"""Mine some blocks to the given wallet (or self.mike if None) on the wallet's daemon.
Returns the daemon's height after mining the blocks. If blocks is omitted, mines enough to
confirm regular transfers (i.e. 10 blocks). If sync is specified, sync all nodes and then
refresh all wallets after mining."""
if wallet is None:
wallet = self.mike
if blocks is None:
blocks = 10
node = wallet.node
vprint("Mining {} blocks to wallet {.name}".format(blocks, wallet))
start_height = node.height()
end_height = start_height + blocks
node.mine_blocks(blocks, wallet)
while node.rpc("/mining_status").json()["active"]:
height = node.height()
vprint("Mined {}/{}".format(height, end_height))
time.sleep(0.05 if height >= end_height else 0.25)
height = node.height()
vprint("Mined {}/{}".format(height, end_height))
if sync:
self.sync_nodes(height)
self.refresh_wallets()
return height
def sync_nodes(self, height=None, *, extra=[], timeout=10):
"""Waits for all nodes to reach the given height, typically invoked after mine()"""
nodes = self.all_nodes + extra
heights = [x.height() for x in nodes]
if height is None:
height = max(heights)
if min(heights) >= height:
vprint("All nodes already synced to height >= {}".format(height))
return
vprint("Waiting for all nodes to sync to height {}".format(height))
last = None
expiry = time.time() + timeout
while nodes and time.time() < expiry:
if heights[-1] < height:
heights[-1] = nodes[-1].height()
if heights[-1] >= height:
heights.pop()
nodes.pop()
last = None
continue
if heights[-1] != last:
vprint("waiting for {} [{} -> {}]".format(nodes[-1].name, heights[-1], height))
last = heights[-1]
time.sleep(0.1)
if nodes:
raise RuntimeError("Timed out waiting for node syncing")
vprint("All nodes synced to height {}".format(height))
def sync(self, extra_nodes=[], extra_wallets=[]):
"""Synchronizes everything: waits for all nodes to sync, then refreshes all wallets. Can be
given external wallets/nodes to sync."""
self.sync_nodes(extra=extra_nodes)
self.refresh_wallets(extra=extra_wallets)
def print_wallet_balances(self):
"""Instructs the wallets to refresh and prints their balances (does nothing in non-verbose mode)"""
global verbose
if not verbose:
return
vprint("Balances:")
for w in self.wallets:
b = w.balances(refresh=True)
vprint(" {:5s}: {:.9f} (total) with {:.9f} (unlocked)".format(
w.name, b[0] * 1e-9, b[1] * 1e-9))
def __del__(self):
for n in self.all_nodes:
n.terminate()
for w in self.wallets:
w.terminate()
snn = None
def run():
global snn, verbose
if not snn:
if path.isdir(datadirectory+'/'):
shutil.rmtree(datadirectory+'/', ignore_errors=False, onerror=None)
vprint("new SNN")
snn = SNNetwork(datadir=datadirectory+'/')
else:
vprint("reusing SNN")
snn.alice.new_wallet()
snn.bob.new_wallet()
# Flush pools because some tests leave behind impossible txes
for n in snn.all_nodes:
assert n.json_rpc("flush_txpool").json()['result']['status'] == 'OK'
# Mine a few to clear out anything in the mempool that can be cleared
snn.mine(5, sync=True)
vprint("Alice has new wallet: {}".format(snn.alice.address()))
vprint("Bob has new wallet: {}".format(snn.bob.address()))
input("Use Ctrl-C to exit...")
loop = asyncio.get_event_loop()
try:
loop.run_forever()
except KeyboardInterrupt:
print(f'!!! AsyncApplication.run: got KeyboardInterrupt during start')
finally:
loop.close()
# Shortcuts for accessing the named wallets
def alice(net):
return net.alice
def bob(net):
return net.bob
def mike(net):
return net.mike
if __name__ == '__main__':
run()