mirror of https://github.com/oxen-io/oxen-core.git
local devnet scripts
This commit is contained in:
parent
6d13152f4e
commit
2ddf644844
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
testdata
|
|
@ -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))
|
||||
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
@ -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']))
|
|
@ -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()
|
Loading…
Reference in New Issue