From 15ef75178c62ca32331b4b44a9d4bcc5a5b19b00 Mon Sep 17 00:00:00 2001 From: drcpu Date: Mon, 3 Feb 2025 19:44:22 +0100 Subject: [PATCH 01/83] [scripts] Remove executable state --- scripts/add_blocks.py | 0 scripts/confirm_blocks.py | 0 scripts/delete_blocks.py | 0 scripts/delete_transactions.py | 0 scripts/manage_wips.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 scripts/add_blocks.py mode change 100755 => 100644 scripts/confirm_blocks.py mode change 100755 => 100644 scripts/delete_blocks.py mode change 100755 => 100644 scripts/delete_transactions.py mode change 100755 => 100644 scripts/manage_wips.py diff --git a/scripts/add_blocks.py b/scripts/add_blocks.py old mode 100755 new mode 100644 diff --git a/scripts/confirm_blocks.py b/scripts/confirm_blocks.py old mode 100755 new mode 100644 diff --git a/scripts/delete_blocks.py b/scripts/delete_blocks.py old mode 100755 new mode 100644 diff --git a/scripts/delete_transactions.py b/scripts/delete_transactions.py old mode 100755 new mode 100644 diff --git a/scripts/manage_wips.py b/scripts/manage_wips.py old mode 100755 new mode 100644 From c0033e1e3ea4ed05d64ac0ed895521fe3eb78021 Mon Sep 17 00:00:00 2001 From: drcpu Date: Mon, 3 Feb 2025 20:30:52 +0100 Subject: [PATCH 02/83] [api] Remove option to use SASL authentication in memcached instance since pylibmc 1.6.3 fails on it --- README.md | 27 ++------------------------- api/connect.py | 2 -- caching/addresses.py | 4 ++-- caching/client.py | 2 -- explorer.example.toml | 4 ---- scripts/delete_memcached_key.py | 2 -- scripts/get_memcached_key.py | 2 -- util/memcached.py | 4 +--- 8 files changed, 5 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 5ef20542..705bd9fc 100644 --- a/README.md +++ b/README.md @@ -44,30 +44,7 @@ After installing memcached (see Dependencies), the memcached daemon will now hav sudo vim /etc/memcached.conf ``` -Increase the amount of memory in MB memcached can use by editing the value after the `-m` flag and at the bottom of the file add `-S` to enable SASL authentication. All other parameters can be kept at their default values. - -Create the SASL configuration: -``` -sudo mkdir /etc/sasl2 -sudo vim /etc/sasl2/memcached.conf -``` - -Add below lines to the configuration file and save it: -``` -mech_list: plain -log_level: 5 -sasldb_path: /etc/sasl2/memcached-sasldb2 -``` - -Create a user for memcached and enter your password twice: -``` -sudo saslpasswd2 -a memcached -c -f /etc/sasl2/memcached-sasldb2 -``` - -Change the ownership of the SASL database so the memcache user can access it. Note that if you changed the memcache user in the `memcached.conf` file, you also need to modify it in below command: -``` -sudo chown memcache:memcache /etc/sasl2/memcached-sasldb2 -``` +Increase the amount of memory in MB memcached can use by editing the value after the `-m` flag. All other parameters can be kept at their default values. Restart the memcached daemon with the following command: ``` @@ -86,7 +63,7 @@ sudo journalctl -u memcached You can fetch memcached statistics using below command. ``` -memcstat --servers="127.0.0.1" --username --password +memcstat --servers="127.0.0.1" ``` ## Creating the database diff --git a/api/connect.py b/api/connect.py index 7b5a71bb..b31198d2 100644 --- a/api/connect.py +++ b/api/connect.py @@ -28,8 +28,6 @@ def create_cache(config, mock=False): caching_config = config["api"]["caching"] cache = MemcachedPool( caching_config["server"].split(","), - caching_config["user"], - caching_config["password"], caching_config["threads"], caching_config["blocking"], ) diff --git a/caching/addresses.py b/caching/addresses.py index 1dc7914b..48b278f6 100644 --- a/caching/addresses.py +++ b/caching/addresses.py @@ -202,7 +202,7 @@ def client(self, logging_queue, config, connection, address_stack, epoch_address # Create cache client cache_config = self.config["api"]["caching"] servers = cache_config["server"].split(",") - memcached_client = pylibmc.Client(servers, binary=True, username=cache_config["user"], password=cache_config["password"], behaviors={"tcp_nodelay": True, "ketama": True}) + memcached_client = pylibmc.Client(servers, binary=True, behaviors={"tcp_nodelay": True, "ketama": True}) # Check if we recently received a request for this address if memcached_client.get(f"{address}"): @@ -414,7 +414,7 @@ def cache_address_data(self, logging_queue, label, address, address_function, ti # Create memcached client cache_config = self.config["api"]["caching"] servers = cache_config["server"].split(",") - memcached_client = pylibmc.Client(servers, binary=True, username=cache_config["user"], password=cache_config["password"], behaviors={"tcp_nodelay": True, "ketama": True}) + memcached_client = pylibmc.Client(servers, binary=True, behaviors={"tcp_nodelay": True, "ketama": True}) # Attempt to cache the address data try: diff --git a/caching/client.py b/caching/client.py index 59da0210..7eaab572 100644 --- a/caching/client.py +++ b/caching/client.py @@ -48,8 +48,6 @@ def __init__(self, config, node_timeout=0, named_cursor=False): self.memcached_client = pylibmc.Client( servers, binary=True, - username=cache_config["user"], - password=cache_config["password"], behaviors={"tcp_nodelay": True, "ketama": True}, ) diff --git a/explorer.example.toml b/explorer.example.toml index afa1bf0f..222f9648 100644 --- a/explorer.example.toml +++ b/explorer.example.toml @@ -93,16 +93,12 @@ level_file = "info" level_stdout = "info" # server: IP address of the running memcached server, defaults to localhost -# user: username for the memcached server -# password: password for the memcached server # threads: the number of threads to use in a memcached connection pool # blocking: wait until a connection is free to execute the request # node_retries: times to retry fetching data from a Witnet node # plot_directory: directory where to save the plots generated by plotting scripts [api.caching] server = "" -user = "" -password = "" threads = "" blocking = "" node_retries = 5 diff --git a/scripts/delete_memcached_key.py b/scripts/delete_memcached_key.py index e76ea723..80d74547 100644 --- a/scripts/delete_memcached_key.py +++ b/scripts/delete_memcached_key.py @@ -13,8 +13,6 @@ def create_memcached_client(config): memcached_client = pylibmc.Client( servers, binary=True, - username=cache_config["user"], - password=cache_config["password"], behaviors={"tcp_nodelay": True, "ketama": True}, ) diff --git a/scripts/get_memcached_key.py b/scripts/get_memcached_key.py index 73b80298..f42e2160 100644 --- a/scripts/get_memcached_key.py +++ b/scripts/get_memcached_key.py @@ -13,8 +13,6 @@ def create_memcached_client(config): memcached_client = pylibmc.Client( servers, binary=True, - username=cache_config["user"], - password=cache_config["password"], behaviors={"tcp_nodelay": True, "ketama": True}, ) diff --git a/util/memcached.py b/util/memcached.py index 6c9b759e..6aa17304 100644 --- a/util/memcached.py +++ b/util/memcached.py @@ -2,11 +2,9 @@ import pylibmc class MemcachedPool(object): - def __init__(self, servers, username, password, threads, blocking): + def __init__(self, servers, threads, blocking): self.memcached_client = pylibmc.Client( servers, - username=username, - password=password, binary=True, behaviors={ "tcp_nodelay": True, # Faster IO From 9009df03b4bab6b9234515c4b923b60ed534c550 Mon Sep 17 00:00:00 2001 From: drcpu Date: Mon, 3 Feb 2025 21:38:13 +0100 Subject: [PATCH 03/83] [blockchain] Differentiate between mainnet and testnet at a configuration and database level --- .gitignore | 3 +- .pre-commit-config.yaml | 2 +- api/blueprints/search/epoch_blueprint.py | 1 + api/blueprints/search/hash_blueprint.py | 6 +- blockchain/explorer.py | 36 +++---- blockchain/objects/address.py | 30 ++++-- blockchain/objects/block.py | 125 ++++++++++------------ blockchain/objects/data_request_report.py | 18 ++-- blockchain/objects/wip.py | 24 +++-- blockchain/transactions/transaction.py | 46 ++++---- blockchain/witnet_database.py | 12 +-- caching/client.py | 2 +- caching/data_request_reports.py | 2 +- create_database.py | 120 +++++++++++++-------- explorer.example.toml | 4 + mockups/database.py | 18 +++- node/consensus_constants.py | 9 +- scripts/add_blocks.py | 6 +- scripts/confirm_blocks.py | 2 +- scripts/delete_blocks.py | 2 +- scripts/delete_transactions.py | 2 +- scripts/insert_consensus_constants.py | 2 +- tests/conftest.py | 8 +- util/database_manager.py | 10 +- util/database_pool.py | 10 +- 25 files changed, 268 insertions(+), 232 deletions(-) diff --git a/.gitignore b/.gitignore index 96a0f3f7..78b40b19 100755 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,8 @@ venv analysis/* -explorer.toml +explorer.mainnet.toml +explorer.testnet.toml **/*.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2127837..9a29a0fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -files: api/|blockchain/|mockups/|schemas/|scripts/|tests/ +files: create_*|api/|blockchain/|mockups/|schemas/|scripts/|tests/ repos: - repo: https://github.com/PyCQA/isort rev: 5.13.2 diff --git a/api/blueprints/search/epoch_blueprint.py b/api/blueprints/search/epoch_blueprint.py index 530bc1a8..8ede6575 100644 --- a/api/blueprints/search/epoch_blueprint.py +++ b/api/blueprints/search/epoch_blueprint.py @@ -71,6 +71,7 @@ def get(self, args): # Fetch block from a node block = Block( + config, consensus_constants, block_epoch=epoch, logger=logger, diff --git a/api/blueprints/search/hash_blueprint.py b/api/blueprints/search/hash_blueprint.py index 69b040d0..67827c7b 100644 --- a/api/blueprints/search/hash_blueprint.py +++ b/api/blueprints/search/hash_blueprint.py @@ -176,6 +176,7 @@ def get(self, args, pagination_parameters): if hash_type == "block": # Fetch block from a node block = Block( + config, consensus_constants, block_hash=hash_value, logger=logger, @@ -545,9 +546,10 @@ def get(self, args, pagination_parameters): # Create data request report for this hash else: data_request_report = DataRequestReport( - hash_type[:-4], - hash_value, + config, consensus_constants, + hash_value, + hash_type[:-4], logger=logger, database=database, ) diff --git a/blockchain/explorer.py b/blockchain/explorer.py index bb38d1bc..5edbcaee 100644 --- a/blockchain/explorer.py +++ b/blockchain/explorer.py @@ -39,24 +39,24 @@ def __init__(self, config, log_queue): # Set up logging queue for logging from different processes self.log_queue = log_queue - # Get configuration to connect to the node pool - self.node_config = config["node-pool"] + # Save the configuration + self.config = config # Create nodes to connect to the node pool self.insert_blocks_node = WitnetNode( - self.node_config, + config["node-pool"], timeout=30, log_queue=self.log_queue, log_label="node-insert", ) self.confirm_blocks_node = WitnetNode( - self.node_config, + config["node-pool"], timeout=30, log_queue=self.log_queue, log_label="node-confirm", ) self.insert_pending_node = WitnetNode( - self.node_config, + config["node-pool"], timeout=30, log_queue=self.log_queue, log_label="node-pending", @@ -64,21 +64,18 @@ def __init__(self, config, log_queue): # Get consensus constants self.consensus_constants = ConsensusConstants( - config=config, error_retry=error_retry + config=self.config, error_retry=error_retry ) - # Get configuration to connect to the database - self.database_config = config["database"] - # Create database objects self.insert_blocks_database = WitnetDatabase( - self.database_config, log_queue=self.log_queue, log_label="db-insert" + self.config, log_queue=self.log_queue, log_label="db-insert" ) self.confirm_blocks_database = WitnetDatabase( - self.database_config, log_queue=self.log_queue, log_label="db-confirm" + self.config, log_queue=self.log_queue, log_label="db-confirm" ) self.mempool_database = WitnetDatabase( - self.database_config, log_queue=self.log_queue, log_label="db-pending" + self.config, log_queue=self.log_queue, log_label="db-pending" ) # Get configuration to connect to the address caching server @@ -99,13 +96,14 @@ def terminate(self): def insert_block(self, database, block_hash_hex_str, block, epoch, tapi_periods): # Create block object and parse it to a JSON object block = Block( + self.config, self.consensus_constants, + block=block, block_hash=block_hash_hex_str, log_queue=self.log_queue, - database_config=self.database_config, - block=block, + database=self.insert_blocks_database, tapi_periods=tapi_periods, - node_config=self.node_config, + witnet_node=self.insert_blocks_node, ) block_json = block.process_block("explorer") @@ -570,10 +568,10 @@ def insert_mempool_transactions(self, log_queue): mapped_transactions, queried_transactions = 0, 0 data_request = DataRequest( + self.config, self.consensus_constants, + database=self.mempool_database, logger=logger, - database_config=self.database_config, - node_config=self.node_config, ) for transaction in transactions_pool["data_request"]: if transaction in mapped_data_requests: @@ -613,10 +611,10 @@ def insert_mempool_transactions(self, log_queue): mapped_transactions, queried_transactions = 0, 0 value_transfer = ValueTransfer( + self.config, self.consensus_constants, + database=self.mempool_database, logger=logger, - database_config=self.database_config, - node_config=self.node_config, ) for transaction in transactions_pool["value_transfer"]: if transaction in mapped_value_transfers: diff --git a/blockchain/objects/address.py b/blockchain/objects/address.py index c8bade78..1911a3b3 100644 --- a/blockchain/objects/address.py +++ b/blockchain/objects/address.py @@ -1,10 +1,13 @@ import time +from psycopg.sql import SQL, Literal + from blockchain.transactions.reveal import translate_reveal from blockchain.transactions.tally import translate_tally from node.consensus_constants import ConsensusConstants from node.witnet_node import WitnetNode from util.common_functions import calculate_block_reward +from util.data_transformer import re_sql from util.database_manager import DatabaseManager @@ -49,7 +52,7 @@ def initialize_connections(self): # Connect to the database if necessary if self.db_mngr is None: self.db_mngr = DatabaseManager( - self.config["database"], named_cursor=False, logger=self.logger + self.config, named_cursor=False, logger=self.logger ) # Connect to node pool @@ -140,14 +143,14 @@ def get_value_transfers_in(self): LEFT JOIN blocks ON value_transfer_txns.epoch=blocks.epoch WHERE - output_addresses @> ARRAY[%s]::CHAR(42)[] AND + output_addresses @> ARRAY[%s]::CHAR({length})[] AND NOT (%s = ANY(input_addresses)) ORDER BY blocks.epoch DESC """ result = self.db_mngr.sql_return_all( - sql, + SQL(re_sql(sql)).format(length=Literal(len(self.address))), parameters=[self.address, self.address], ) @@ -228,12 +231,15 @@ def get_value_transfers_out(self): LEFT JOIN blocks ON value_transfer_txns.epoch=blocks.epoch WHERE - input_addresses @> ARRAY[%s]::CHAR(42)[] + input_addresses @> ARRAY[%s]::CHAR({length})[] ORDER BY blocks.epoch DESC """ - result = self.db_mngr.sql_return_all(sql, parameters=[self.address]) + result = self.db_mngr.sql_return_all( + SQL(re_sql(sql)).format(length=Literal(len(self.address))), + parameters=[self.address], + ) value_transfers_out = [] if result: @@ -383,12 +389,15 @@ def get_mints(self): LEFT JOIN blocks ON mint_txns.epoch=blocks.epoch WHERE - mint_txns.output_addresses @> ARRAY[%s]::CHAR(42)[] + mint_txns.output_addresses @> ARRAY[%s]::CHAR({length})[] ORDER BY mint_txns.epoch DESC """ - result = self.db_mngr.sql_return_all(sql, parameters=[self.address]) + result = self.db_mngr.sql_return_all( + SQL(re_sql(sql)).format(length=Literal(len(self.address))), + parameters=[self.address], + ) mints = [] if result: @@ -543,12 +552,15 @@ def get_data_requests_created(self): ON tally_txns.epoch=blocks.epoch WHERE - data_request_txns.input_addresses @> ARRAY[%s]::CHAR(42)[] + data_request_txns.input_addresses @> ARRAY[%s]::CHAR({length})[] ORDER BY data_request_txns.epoch DESC """ - result = self.db_mngr.sql_return_all(sql, parameters=[self.address]) + result = self.db_mngr.sql_return_all( + SQL(re_sql(sql)).format(length=Literal(len(self.address))), + parameters=[self.address], + ) data_requests_created = [] if result: diff --git a/blockchain/objects/block.py b/blockchain/objects/block.py index 9e70ba83..af9fcc90 100644 --- a/blockchain/objects/block.py +++ b/blockchain/objects/block.py @@ -16,18 +16,20 @@ class Block(object): def __init__( self, + config, consensus_constants, + block=None, block_hash="", block_epoch=-1, logger=None, log_queue=None, database=None, - database_config=None, - block=None, tapi_periods=None, witnet_node=None, - node_config=None, ): + self.config = config + + self.block = block self.block_hash = block_hash self.block_epoch = block_epoch @@ -49,27 +51,20 @@ def __init__( if database: self.database = database - elif database_config: - self.database = DatabaseManager(database_config, logger=self.logger) else: - self.database = None - - if database_config: - self.database_config = database_config + self.database = DatabaseManager(config, logger=self.logger) - if node_config: - self.node_config = node_config - - self.witnet_node = None + # Connect to node pool if witnet_node: self.witnet_node = witnet_node + else: + self.witnet_node = WitnetNode(config["node-pool"], logger=self.logger) self.current_epoch = (int(time.time()) - self.start_time) // self.epoch_period + # Attempt to create the block if block is None: self.block = self.get_block() - else: - self.block = block self.block_json = None self.tapi_periods = tapi_periods @@ -82,21 +77,14 @@ def configure_logging_process(self, queue, label): root.setLevel(logging.DEBUG) def get_block(self): - # Connect to node pool - if self.witnet_node is None: - self.witnet_node = WitnetNode(self.node_config, logger=self.logger) - # No block hash specified, check if we can fetch it based on a block epoch if self.block_hash == "": # Log and return warnings if necessary if self.block_epoch == -1: return self.return_block_error("No block hash or block epoch specified") - if not self.database: - return self.return_block_error("No database found to fetch block hash") # Fetch block hash from the database - sql = ( - """ + sql = """ SELECT block_hash, epoch @@ -105,9 +93,9 @@ def get_block(self): WHERE epoch=%s """ - % self.block_epoch + block_hash = self.database.sql_return_one( + sql, parameters=[self.block_epoch] ) - block_hash = self.database.sql_return_one(sql) if block_hash: self.block_hash = block_hash[0].hex() @@ -220,27 +208,26 @@ def process_mint_txn(self): txn_hash = self.block["txns_hashes"]["mint"] json_txn = self.block["txns"]["mint"] block_signature = self.block["block_sig"]["public_key"] - mint = Mint(self.consensus_constants, logger=self.logger) + mint = Mint( + self.config, + self.consensus_constants, + database=self.database, + logger=self.logger, + witnet_node=self.witnet_node, + ) mint.set_transaction(txn_hash, self.block_epoch, json_txn=json_txn) return mint.process_transaction(block_signature) def process_value_transfer_txns(self, call_from): value_transfer_txns = [] if len(self.block["txns_hashes"]["value_transfer"]) > 0: - if self.witnet_node: - value_transfer = ValueTransfer( - self.consensus_constants, - logger=self.logger, - database=self.database, - witnet_node=self.witnet_node, - ) - else: - value_transfer = ValueTransfer( - self.consensus_constants, - logger=self.logger, - database=self.database, - node_config=self.node_config, - ) + value_transfer = ValueTransfer( + self.config, + self.consensus_constants, + database=self.database, + logger=self.logger, + witnet_node=self.witnet_node, + ) for i, (txn_hash, txn_weight) in enumerate( zip( self.block["txns_hashes"]["value_transfer"], @@ -259,20 +246,13 @@ def process_value_transfer_txns(self, call_from): def process_data_request_txns(self, call_from): data_request_transactions = [] if len(self.block["txns_hashes"]["data_request"]) > 0: - if self.witnet_node: - data_request = DataRequest( - self.consensus_constants, - logger=self.logger, - database=self.database, - witnet_node=self.witnet_node, - ) - else: - data_request = DataRequest( - self.consensus_constants, - logger=self.logger, - database=self.database, - node_config=self.node_config, - ) + data_request = DataRequest( + self.config, + self.consensus_constants, + database=self.database, + logger=self.logger, + witnet_node=self.witnet_node, + ) for i, (txn_hash, txn_weight) in enumerate( zip( self.block["txns_hashes"]["data_request"], @@ -291,20 +271,13 @@ def process_data_request_txns(self, call_from): def process_commit_txns(self, call_from): commit_transactions = [] if len(self.block["txns_hashes"]["commit"]) > 0: - if self.witnet_node: - commit = Commit( - self.consensus_constants, - logger=self.logger, - database=self.database, - witnet_node=self.witnet_node, - ) - else: - commit = Commit( - self.consensus_constants, - logger=self.logger, - database=self.database, - node_config=self.node_config, - ) + commit = Commit( + self.config, + self.consensus_constants, + database=self.database, + logger=self.logger, + witnet_node=self.witnet_node, + ) for i, txn_hash in enumerate(self.block["txns_hashes"]["commit"]): json_txn = self.block["txns"]["commit_txns"][i] commit.set_transaction(txn_hash, self.block_epoch, json_txn=json_txn) @@ -314,7 +287,13 @@ def process_commit_txns(self, call_from): def process_reveal_txns(self, call_from): reveal_transactions = [] if len(self.block["txns_hashes"]["reveal"]) > 0: - reveal = Reveal(self.consensus_constants, logger=self.logger) + reveal = Reveal( + self.config, + self.consensus_constants, + database=self.database, + logger=self.logger, + witnet_node=self.witnet_node, + ) for i, txn_hash in enumerate(self.block["txns_hashes"]["reveal"]): json_txn = self.block["txns"]["reveal_txns"][i] reveal.set_transaction(txn_hash, self.block_epoch, json_txn=json_txn) @@ -324,7 +303,13 @@ def process_reveal_txns(self, call_from): def process_tally_txns(self, call_from): tally_transactions = [] if len(self.block["txns_hashes"]["tally"]) > 0: - tally = Tally(self.consensus_constants, logger=self.logger) + tally = Tally( + self.config, + self.consensus_constants, + database=self.database, + logger=self.logger, + witnet_node=self.witnet_node, + ) for i, txn_hash in enumerate(self.block["txns_hashes"]["tally"]): json_txn = self.block["txns"]["tally_txns"][i] tally.set_transaction(txn_hash, self.block_epoch, json_txn=json_txn) diff --git a/blockchain/objects/data_request_report.py b/blockchain/objects/data_request_report.py index d4b5ab80..4d47e688 100644 --- a/blockchain/objects/data_request_report.py +++ b/blockchain/objects/data_request_report.py @@ -14,22 +14,22 @@ class DataRequestReport(object): def __init__( self, - transaction_type, - transaction_hash, + config, consensus_constants, + transaction_hash, + transaction_type, logger=None, log_queue=None, database=None, - database_config=None, ): - self.transaction_type = transaction_type - self.transaction_hash = transaction_hash - self.consensus_constants = consensus_constants self.start_time = consensus_constants.checkpoint_zero_timestamp self.epoch_period = consensus_constants.checkpoints_period self.collateral_minimum = consensus_constants.collateral_minimum + self.transaction_hash = transaction_hash + self.transaction_type = transaction_type + # Set up logger if logger: self.logger = logger @@ -42,12 +42,10 @@ def __init__( if database: self.database = database - elif database_config: + else: self.database = DatabaseManager( - database_config, logger=self.logger, custom_types=["utxo", "filter"] + config, logger=self.logger, custom_types=["utxo", "filter"] ) - else: - self.database = None def configure_logging_process(self, queue, label): handler = logging.handlers.QueueHandler(queue) diff --git a/blockchain/objects/wip.py b/blockchain/objects/wip.py index 87534290..98398c2f 100644 --- a/blockchain/objects/wip.py +++ b/blockchain/objects/wip.py @@ -10,24 +10,31 @@ class WIP(object): def __init__( self, + config=None, database=None, - database_config=None, witnet_node=None, - node_config=None, mockup=False, ): if database: self.db_mngr = database self.fetch_wips() - elif database_config: - self.db_mngr = DatabaseManager(database_config) + elif config is not None: + self.db_mngr = DatabaseManager(config) self.fetch_wips() + else: + AssertionError( + "Need to pass a database object or configuration settings to create one" + ) self.witnet_node = None if witnet_node is not None: self.witnet_node = witnet_node - - self.node_config = node_config + elif config is not None: + self.witnet_node = WitnetNode(config["node-pool"]) + else: + AssertionError( + "Need to pass a witnet node object or configuration settings to create one" + ) self.mockup = mockup if self.mockup: @@ -234,9 +241,6 @@ def process_tapi(self): sys.stderr.write("Cannot process TAPI signals on a mockup\n") return - if self.witnet_node is None: - self.witnet_node = WitnetNode(self.node_config) - for wip in self.wips: ( wip_id, @@ -409,7 +413,7 @@ def main(): config = toml.load(options.config_file) # Run some tests - wip = WIP(database_config=config["database"], node_config=config["node-pool"]) + wip = WIP(config=config) assert wip.is_wip0008_active(191999) is False assert wip.is_wip0008_active(192000) is True diff --git a/blockchain/transactions/transaction.py b/blockchain/transactions/transaction.py index 247553ff..a2b00799 100644 --- a/blockchain/transactions/transaction.py +++ b/blockchain/transactions/transaction.py @@ -15,12 +15,11 @@ class Transaction(object): def __init__( self, + config, consensus_constants, - logger=None, database=None, - database_config=None, + logger=None, witnet_node=None, - node_config=None, ): self.start_time = consensus_constants.checkpoint_zero_timestamp self.epoch_period = consensus_constants.checkpoints_period @@ -29,19 +28,10 @@ def __init__( # Connect to the database if database is not None: self.database = database - elif database_config is not None: + else: self.database = DatabaseManager( - database_config, logger=logger, custom_types=["utxo", "filter"] + config, logger=logger, custom_types=["utxo", "filter"] ) - else: - self.database = None - - # Save node pool config - self.node_config = node_config - - self.witnet_node = None - if witnet_node is not None: - self.witnet_node = witnet_node # Set up logger if logger: @@ -49,17 +39,23 @@ def __init__( else: self.logger = None + # Connect to the witnet node pool + if witnet_node is not None: + self.witnet_node = witnet_node + else: + self.witnet_node = WitnetNode(config["node-pool"], logger=self.logger) + # Create address generator - self.address_generator = AddressGenerator("wit") + address_prefix = None + if config["environment"]["network"] == "mainnet": + address_prefix = "wit" + elif config["environment"]["network"] == "testnet": + address_prefix = "twit" + assert address_prefix, "Need to properly set the network type" + self.address_generator = AddressGenerator(address_prefix) # Create Protobuf encoder - self.protobuf_encoder = None - if database is not None: - self.protobuf_encoder = ProtobufEncoder(WIP(database=database)) - elif database_config is not None: - self.protobuf_encoder = ProtobufEncoder( - WIP(database_config=database_config) - ) + self.protobuf_encoder = ProtobufEncoder(WIP(database=self.database)) def configure_logging_process(self, queue, label): handler = logging.handlers.QueueHandler(queue) @@ -99,8 +95,6 @@ def calculate_addresses(self, signatures): return addresses def get_inputs(self, txn_inputs): - assert self.database is not None - input_utxos, input_values = [], [] for txn_input in txn_inputs: # Get the transaction and output index from the output pointer @@ -199,10 +193,6 @@ def get_outputs(self, txn_outputs): return output_addresses, output_values, timelocks def get_transaction_from_node(self, txn_hash): - # Create connection to the node pool - if self.witnet_node is None: - self.witnet_node = WitnetNode(self.node_config, logger=self.logger) - transaction = self.witnet_node.get_transaction(txn_hash) while "error" in transaction: # All our nodes in the pool were busy, retry as soon as possible diff --git a/blockchain/witnet_database.py b/blockchain/witnet_database.py index 40dcbdd2..9082f1b0 100644 --- a/blockchain/witnet_database.py +++ b/blockchain/witnet_database.py @@ -7,7 +7,7 @@ class WitnetDatabase(object): def __init__( self, - db_config, + config, named_cursor=False, logger=None, log_queue=None, @@ -23,7 +23,7 @@ def __init__( self.logger = None self.db_mngr = DatabaseManager( - db_config, named_cursor=named_cursor, logger=self.logger + config, named_cursor=named_cursor, logger=self.logger ) # Register types created for this database @@ -591,12 +591,12 @@ def terminate(self): self.finalize() self.db_mngr.terminate() - def sql_return_one(self, sql): - result = self.db_mngr.sql_return_one(sql) + def sql_return_one(self, sql, parameters=None): + result = self.db_mngr.sql_return_one(sql, parameters=parameters) return result - def sql_return_all(self, sql): - result = self.db_mngr.sql_return_all(sql) + def sql_return_all(self, sql, parameters=None): + result = self.db_mngr.sql_return_all(sql, parameters=parameters) return result def sql_execute_many(self, sql, data): diff --git a/caching/client.py b/caching/client.py index 7eaab572..eb632aa0 100644 --- a/caching/client.py +++ b/caching/client.py @@ -26,7 +26,7 @@ def __init__(self, config, node_timeout=0, named_cursor=False): # Connect to database try: self.database = DatabaseManager( - config["database"], + config, named_cursor=named_cursor, logger=self.logger, custom_types=["utxo", "filter"], diff --git a/caching/data_request_reports.py b/caching/data_request_reports.py index 104b1c07..2839a6a3 100644 --- a/caching/data_request_reports.py +++ b/caching/data_request_reports.py @@ -132,7 +132,7 @@ def process_data_requests(self, force_update): def cache_data_request_report(self, txn_hash, epoch, inner_start): # Build data request report - data_request = DataRequestReport("data_request", txn_hash, self.consensus_constants, logger=self.logger, database=self.database) + data_request = DataRequestReport(self.config, self.consensus_constants, txn_hash, "data_request", logger=self.logger, database=self.database) try: data_request_report = data_request.get_report() if "error" in data_request_report: diff --git a/create_database.py b/create_database.py index 355dd278..e1c695b9 100644 --- a/create_database.py +++ b/create_database.py @@ -1,17 +1,26 @@ import optparse -import psycopg import subprocess import sys + +import psycopg import toml +from psycopg.sql import SQL, Literal + def execute_command(command): - p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + p = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + ) stdout, stderr = p.communicate() if len(stderr) > 0: sys.stderr.write(stderr + "\n") sys.exit(1) return stdout + def check_version(): stdout = execute_command("psql --version") major, minor = stdout.decode("utf-8").split()[2].split(".") @@ -19,22 +28,28 @@ def check_version(): sys.stderr.write("Minimum required version of PostgreSQL is 15") sys.exit(1) + def create_user(user, password): # Check if user exists - stdout = execute_command(f"sudo -u postgres psql -c \"SELECT 1 FROM pg_roles WHERE rolname='{user}'\"") + stdout = execute_command( + f"sudo -u postgres psql -c \"SELECT 1 FROM pg_roles WHERE rolname='{user}'\"" + ) # Does the user exist? - if stdout == b' ?column? \n----------\n(0 rows)\n\n': + if stdout == b" ?column? \n----------\n(0 rows)\n\n": # Create user if password == "": - execute_command(f"sudo -u postgres psql -c \"CREATE USER {user}\"") + execute_command(f'sudo -u postgres psql -c "CREATE USER {user}"') else: - execute_command(f"sudo -u postgres psql -c \"CREATE USER {user} PASSWORD '{password}'\"") + execute_command( + f"sudo -u postgres psql -c \"CREATE USER {user} PASSWORD '{password}'\"" + ) # Allow user to create databases - execute_command(f"sudo -u postgres psql -c \"ALTER USER {user} CREATEDB\"") + execute_command(f'sudo -u postgres psql -c "ALTER USER {user} CREATEDB"') print(f"Ceated user '{user}'") else: print(f"User '{user}' already exists") + def create_database(name, user): connection, cursor = connect_to_database("postgres") cursor.execute(f"SELECT 1 FROM pg_catalog.pg_database WHERE datname='{name}'") @@ -45,6 +60,7 @@ def create_database(name, user): else: print(f"Database '{name}' already exists") + def connect_to_database(name, user="", password=""): try: if user == "": @@ -64,6 +80,7 @@ def connect_to_database(name, user="", password=""): sys.exit(2) return connection, cursor + def execute_create_statement(connection, cursor, sql): try: cursor.execute(sql) @@ -71,6 +88,7 @@ def execute_create_statement(connection, cursor, sql): sys.stderr.write(f"Could not execute SQL statement '{sql}', error: {e}\n") connection.commit() + def create_enums(connection, cursor): enums = [ """DO $$ @@ -91,7 +109,6 @@ def create_enums(connection, cursor): END $$; COMMIT;""", - """DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'retrieve_kind') THEN @@ -105,7 +122,6 @@ def create_enums(connection, cursor): END $$; COMMIT;""", - """DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'network_stat') THEN @@ -131,6 +147,7 @@ def create_enums(connection, cursor): print("Created all enums") + def create_types(connection, cursor): types = [ """DO $$ @@ -144,7 +161,6 @@ def create_types(connection, cursor): END $$; COMMIT;""", - """DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'filter') THEN @@ -163,11 +179,12 @@ def create_types(connection, cursor): print("Created all types") -def create_tables(connection, cursor): + +def create_tables(config, connection, cursor): tables = [ """CREATE TABLE IF NOT EXISTS addresses ( id INT GENERATED ALWAYS AS IDENTITY, - address CHAR(42) PRIMARY KEY, + address CHAR({length}) PRIMARY KEY, label VARCHAR(64), active INT, block INT, @@ -178,13 +195,11 @@ def create_tables(connection, cursor): reveal INT, tally INT );""", - """CREATE TABLE IF NOT EXISTS hashes ( hash BYTEA PRIMARY KEY, type hash_type NOT NULL, epoch INT );""", - """CREATE TABLE IF NOT EXISTS blocks ( block_hash BYTEA PRIMARY KEY, value_transfer SMALLINT NOT NULL, @@ -200,33 +215,30 @@ def create_tables(connection, cursor): confirmed BOOLEAN NOT NULL, reverted BOOLEAN DEFAULT false );""", - """CREATE TABLE IF NOT EXISTS mint_txns ( txn_hash BYTEA PRIMARY KEY, - miner CHAR(42) NOT NULL, - output_addresses CHAR(42) ARRAY NOT NULL, + miner CHAR({length}) NOT NULL, + output_addresses CHAR({length}) ARRAY NOT NULL, output_values BIGINT ARRAY NOT NULL, epoch INT NOT NULL );""", - """CREATE TABLE IF NOT EXISTS value_transfer_txns ( txn_hash BYTEA PRIMARY KEY, - input_addresses CHAR(42) ARRAY NOT NULL, + input_addresses CHAR({length}) ARRAY NOT NULL, input_values BIGINT ARRAY NOT NULL, input_utxos utxo ARRAY NOT NULL, - output_addresses CHAR(42) ARRAY NOT NULL, + output_addresses CHAR({length}) ARRAY NOT NULL, output_values BIGINT ARRAY NOT NULL, timelocks BIGINT ARRAY NOT NULL, weight INT NOT NULL, epoch INT NOT NULL );""", - """CREATE TABLE IF NOT EXISTS data_request_txns ( txn_hash BYTEA PRIMARY KEY, - input_addresses CHAR(42) ARRAY NOT NULL, + input_addresses CHAR({length}) ARRAY NOT NULL, input_values BIGINT ARRAY NOT NULL, input_utxos utxo ARRAY NOT NULL, - output_address CHAR(42), + output_address CHAR({length}), output_value BIGINT, witnesses SMALLINT NOT NULL, witness_reward BIGINT NOT NULL, @@ -247,50 +259,44 @@ def create_tables(connection, cursor): DRO_bytes_hash BYTEA NOT NULL, epoch INT NOT NULL );""", - """CREATE TABLE IF NOT EXISTS commit_txns ( txn_hash BYTEA PRIMARY KEY, - txn_address CHAR(42) NOT NULL, + txn_address CHAR({length}) NOT NULL, input_values BIGINT ARRAY NOT NULL, input_utxos utxo ARRAY NOT NULL, output_value BIGINT, data_request BYTEA NOT NULL, epoch INT NOT NULL );""", - """CREATE TABLE IF NOT EXISTS reveal_txns ( txn_hash BYTEA PRIMARY KEY, - txn_address CHAR(42) NOT NULL, + txn_address CHAR({length}) NOT NULL, data_request BYTEA NOT NULL, result BYTEA NOT NULL, success BOOL NOT NULL, epoch INT NOT NULL );""", - """CREATE TABLE IF NOT EXISTS tally_txns ( txn_hash BYTEA PRIMARY KEY, - output_addresses CHAR(42) ARRAY NOT NULL, + output_addresses CHAR({length}) ARRAY NOT NULL, output_values BIGINT ARRAY NOT NULL, data_request BYTEA NOT NULL, - error_addresses CHAR(42) ARRAY NOT NULL, - liar_addresses CHAR(42) ARRAY NOT NULL, + error_addresses CHAR({length}) ARRAY NOT NULL, + liar_addresses CHAR({length}) ARRAY NOT NULL, result BYTEA NOT NULL, success BOOL NOT NULL, epoch INT NOT NULL );""", - """CREATE TABLE IF NOT EXISTS data_request_mempool ( timestamp INT NOT NULL, fee BIGINT ARRAY NOT NULL, weight INT ARRAY NOT NULL );""", - """CREATE TABLE IF NOT EXISTS value_transfer_mempool ( timestamp INT NOT NULL, fee BIGINT ARRAY NOT NULL, weight INT ARRAY NOT NULL );""", - """CREATE TABLE IF NOT EXISTS wips ( id SMALLINT GENERATED ALWAYS AS IDENTITY, title VARCHAR NOT NULL, @@ -302,7 +308,6 @@ def create_tables(connection, cursor): tapi_bit SMALLINT, tapi_json JSONB );""", - """CREATE TABLE IF NOT EXISTS network_stats ( stat network_stat NOT NULL, from_epoch INT, @@ -310,17 +315,14 @@ def create_tables(connection, cursor): data JSONB NOT NULL, UNIQUE NULLS NOT DISTINCT (stat, from_epoch, to_epoch) );""", - """CREATE TABLE IF NOT EXISTS data_request_reports ( data_request_hash BYTEA PRIMARY KEY, report JSONB NOT NULL );""", - """CREATE TABLE IF NOT EXISTS cron_data ( key VARCHAR PRIMARY KEY, data INT NOT NULL );""", - """CREATE TABLE IF NOT EXISTS consensus_constants ( key VARCHAR PRIMARY KEY, int_val BIGINT, @@ -329,10 +331,25 @@ def create_tables(connection, cursor): ] for table in tables: - execute_create_statement(connection, cursor, table) + if config["environment"]["network"] == "mainnet": + execute_create_statement( + connection, + cursor, + SQL(table).format(length=Literal(42)), + ) + elif config["environment"]["network"] == "testnet": + execute_create_statement( + connection, + cursor, + SQL(table).format(length=Literal(43)), + ) + else: + sys.stderr.write("Network type needs to be either mainnet or testnet") + sys.exit(1) print("Created all tables") + def create_indexes(connection, cursor): indexes = [ "CREATE INDEX IF NOT EXISTS idx_block_epoch ON blocks (epoch);", @@ -355,27 +372,42 @@ def create_indexes(connection, cursor): print("Created all indexes") + def main(): parser = optparse.OptionParser() - parser.add_option("--config-file", type="string", default="explorer.toml", dest="config_file") + parser.add_option( + "--config-file", + type="string", + default="explorer.toml", + dest="config_file", + ) options, args = parser.parse_args() config = toml.load(options.config_file) check_version() - create_user(config["database"]["user"], config["database"]["password"]) + database_name = f"{config['database']['name']}_{config['environment']['network']}" + database_user = config["database"]["user"] + database_password = config["database"]["password"] - create_database(config["database"]["name"], config["database"]["user"]) - connection, cursor = connect_to_database(config["database"]["name"], config["database"]["user"], config["database"]["password"]) + create_user(database_user, database_password) + + create_database(database_name, database_user) + connection, cursor = connect_to_database( + database_name, + database_user, + database_password, + ) create_enums(connection, cursor) create_types(connection, cursor) - create_tables(connection, cursor) + create_tables(config, connection, cursor) create_indexes(connection, cursor) + if __name__ == "__main__": main() diff --git a/explorer.example.toml b/explorer.example.toml index 222f9648..887c5091 100644 --- a/explorer.example.toml +++ b/explorer.example.toml @@ -1,3 +1,7 @@ +# network: set the environment on which the explorer operates (mainnet or testnet) +[environment] +network = "mainnet" + # user: postgresql database user # name: postgresql database name # password: postgresql database password diff --git a/mockups/database.py b/mockups/database.py index a9c45856..9d1f1f2b 100644 --- a/mockups/database.py +++ b/mockups/database.py @@ -30,7 +30,7 @@ def transform_sql(self, sql, parameters): # commit is a reserved keyword, replace it with 'commit' if "commit," in sql: sql = sql.replace("commit,", '"commit",') - # replace 'in-array' where-clause: @> ARRAY[?]::CHAR(42)[] + # replace 'in-array' where-clause: @> ARRAY[?]::CHAR(42)[] for mainnet if "@> ARRAY[?]::CHAR(42)[]" in sql: sql = sql.replace("@> ARRAY[?]::CHAR(42)[]", "LIKE ?") new_parameters = [] @@ -40,6 +40,16 @@ def transform_sql(self, sql, parameters): else: new_parameters.append(parameter) parameters = new_parameters + # replace 'in-array' where-clause: @> ARRAY[?]::CHAR(43)[] for testnet + if "@> ARRAY[?]::CHAR(43)[]" in sql: + sql = sql.replace("@> ARRAY[?]::CHAR(43)[]", "LIKE ?") + new_parameters = [] + for parameter in parameters: + if parameter.startswith("twit1"): + new_parameters.append("%" + parameter + "%") + else: + new_parameters.append(parameter) + parameters = new_parameters # replace 'not-in-array' where-clause: NOT (? = ANY(...)) pattern = "NOT (? = ANY(" if pattern in sql: @@ -47,9 +57,7 @@ def transform_sql(self, sql, parameters): for i, line in enumerate(sql_lines): if pattern in line: column = line.replace(pattern, "") - column = column.replace(")", "") - column = column.strip() - sql_lines[i] = f"{column} NOT LIKE ?" + sql_lines[i] = column.replace("))", " NOT LIKE ?") break sql = " ".join(sql_lines) # replace the ANY operator @@ -178,6 +186,8 @@ def composable_as_string(self, composable, encoding="utf-8"): return "".join([self.composable_as_string(x, encoding) for x in composable]) elif isinstance(composable, psycopg.sql.SQL): return composable.as_string(None) + elif isinstance(composable, psycopg.sql.Literal): + return composable.as_string(None) else: return " ,".join(composable._obj) diff --git a/node/consensus_constants.py b/node/consensus_constants.py index 323236b1..c4e1ab01 100644 --- a/node/consensus_constants.py +++ b/node/consensus_constants.py @@ -20,15 +20,10 @@ def __init__( # First try to fetch the consensus constants from the database database_created = False if database is None: - database = DatabaseManager(config["database"]) + database = DatabaseManager(config) database_created = True - sql = """ - SELECT - * - FROM - consensus_constants - """ + sql = "SELECT * FROM consensus_constants" if hasattr(database, "named_cursor") and database.named_cursor: database.reset_cursor() fetched_consensus_constants = database.sql_return_all(sql) diff --git a/scripts/add_blocks.py b/scripts/add_blocks.py index 2ad793eb..1a143267 100644 --- a/scripts/add_blocks.py +++ b/scripts/add_blocks.py @@ -34,6 +34,7 @@ def add_block( if block_epoch: block = Block( + config, consensus_constants, block_epoch=block_epoch, database=db_mngr, @@ -42,6 +43,7 @@ def add_block( ) else: block = Block( + config, consensus_constants, block_hash=block_hash, database=db_mngr, @@ -55,7 +57,7 @@ def add_block( epoch = block_json["details"]["epoch"] print(f"Adding block {block_json['details']['hash']} for epoch {epoch}") - witnet_database = WitnetDatabase(config["database"]) + witnet_database = WitnetDatabase(config) witnet_database.insert_block(block_json) witnet_database.insert_mint_txn(block_json["transactions"]["mint"], epoch) for txn_details in block_json["transactions"]["value_transfer"]: @@ -88,7 +90,7 @@ def main(): options, args = parser.parse_args() config = toml.load(options.config_file) - db_mngr = DatabaseManager(config["database"]) + db_mngr = DatabaseManager(config) witnet_node = WitnetNode(config["node-pool"], timeout=300) consensus_constants = ConsensusConstants(database=db_mngr, witnet_node=witnet_node) diff --git a/scripts/confirm_blocks.py b/scripts/confirm_blocks.py index ccefa6b8..33a3bd27 100644 --- a/scripts/confirm_blocks.py +++ b/scripts/confirm_blocks.py @@ -27,7 +27,7 @@ def main(): options, args = parser.parse_args() config = toml.load(options.config_file) - db_mngr = DatabaseManager(config["database"]) + db_mngr = DatabaseManager(config) if options.epochs is not None: epochs_to_confirm = [int(epoch) for epoch in options.epochs.split(",")] diff --git a/scripts/delete_blocks.py b/scripts/delete_blocks.py index 82c6d424..b0821701 100644 --- a/scripts/delete_blocks.py +++ b/scripts/delete_blocks.py @@ -125,7 +125,7 @@ def main(): options, args = parser.parse_args() config = toml.load(options.config_file) - db_mngr = DatabaseManager(config["database"]) + db_mngr = DatabaseManager(config) if options.epochs is not None: epochs_to_confirm = [int(epoch) for epoch in options.epochs.split(",")] diff --git a/scripts/delete_transactions.py b/scripts/delete_transactions.py index d7ea3648..fa8aa672 100644 --- a/scripts/delete_transactions.py +++ b/scripts/delete_transactions.py @@ -36,7 +36,7 @@ def main(): options, args = parser.parse_args() config = toml.load(options.config_file) - db_mngr = DatabaseManager(config["database"]) + db_mngr = DatabaseManager(config) if options.hashes is not None: hashes = options.hashes.split(",") diff --git a/scripts/insert_consensus_constants.py b/scripts/insert_consensus_constants.py index dd7abd92..200c30f9 100644 --- a/scripts/insert_consensus_constants.py +++ b/scripts/insert_consensus_constants.py @@ -20,7 +20,7 @@ def get_consensus_constants(config): def insert_consensus_constants(config, consensus_constants): - db_mngr = DatabaseManager(config["database"]) + db_mngr = DatabaseManager(config) for key, value in consensus_constants.items(): if isinstance(value, int) or isinstance(value, float): diff --git a/tests/conftest.py b/tests/conftest.py index 735519fa..c3798a4c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,12 @@ import json import pytest +import toml -from tests.schemas.include.test_post_transaction_schema import ( # noqa: F401 - value_transfer, -) + +@pytest.fixture +def config(): + return toml.load(open("explorer.testnet.toml")) @pytest.fixture diff --git a/util/database_manager.py b/util/database_manager.py index 77d9e49e..77b3b06c 100644 --- a/util/database_manager.py +++ b/util/database_manager.py @@ -3,11 +3,11 @@ import sys class DatabaseManager(object): - def __init__(self, db_config, named_cursor=False, logger=None, custom_types=[]): - self.db_user = db_config["user"] - self.db_name = db_config["name"] - self.db_pass = db_config["password"] - self.fetch_rows = db_config["fetch_rows"] + def __init__(self, config, named_cursor=False, logger=None, custom_types=[]): + self.db_user = config["database"]["user"] + self.db_name = f"{config['database']['name']}_{config['environment']['network']}" + self.db_pass = config["database"]["password"] + self.fetch_rows = config["database"]["fetch_rows"] self.named_cursor = named_cursor diff --git a/util/database_pool.py b/util/database_pool.py index c123f4c2..0086702c 100644 --- a/util/database_pool.py +++ b/util/database_pool.py @@ -6,11 +6,11 @@ class DatabasePool(object): def __init__(self, config, logger=None): - self.user = config["user"] - self.database = config["name"] - self.password = config["password"] - self.fetch_rows = config["fetch_rows"] - self.min_connections = config["min_connections"] + self.user = config["database"]["user"] + self.database = f"{config['database']['name']}_{config['environment']['network']}" + self.password = config["database"]["password"] + self.fetch_rows = config["database"]["fetch_rows"] + self.min_connections = config["database"]["min_connections"] self.logger = logger From fcfb6e57fd759972d6ff6a650faf7af0c4dceb04 Mon Sep 17 00:00:00 2001 From: drcpu Date: Tue, 4 Feb 2025 20:13:01 +0100 Subject: [PATCH 04/83] [blockchain] Create configuration class to store TOML config, WIP and consensus constants --- api/blueprints/address/details_blueprint.py | 1 - .../address/value_transfers_blueprint.py | 1 - api/blueprints/search/epoch_blueprint.py | 8 --- api/blueprints/search/hash_blueprint.py | 22 ++---- blockchain/config.py | 4 ++ blockchain/explorer.py | 70 ++++++++----------- blockchain/objects/address.py | 13 ++-- blockchain/objects/block.py | 31 +++----- blockchain/objects/data_request_history.py | 4 +- blockchain/objects/data_request_report.py | 41 ++++------- blockchain/objects/wip.py | 16 +---- blockchain/transactions/transaction.py | 27 ++++--- blockchain/witnet_database.py | 4 +- caching/addresses.py | 9 ++- caching/balance_list.py | 21 +++--- caching/blocks.py | 29 ++++---- caching/client.py | 9 --- caching/data_request_reports.py | 27 ++++--- caching/home_stats.py | 23 +++--- caching/network_stats.py | 38 ++++++---- caching/reputation_list.py | 16 ++--- caching/tapi_list.py | 18 ++--- mockups/data/create_mockup_database.py | 12 +++- node/consensus_constants.py | 9 +-- node/witnet_client_pool.py | 7 +- node/witnet_node.py | 6 +- scripts/add_blocks.py | 25 +++---- scripts/confirm_blocks.py | 6 +- scripts/delete_blocks.py | 10 +-- scripts/insert_consensus_constants.py | 15 ++-- scripts/manage_wips.py | 8 ++- tests/conftest.py | 11 ++- util/database_manager.py | 6 +- util/database_pool.py | 6 +- 34 files changed, 265 insertions(+), 288 deletions(-) create mode 100644 blockchain/config.py diff --git a/api/blueprints/address/details_blueprint.py b/api/blueprints/address/details_blueprint.py index 164e017e..35e335ba 100644 --- a/api/blueprints/address/details_blueprint.py +++ b/api/blueprints/address/details_blueprint.py @@ -56,7 +56,6 @@ def get(self, args): address = Address( arg_address, - config, database=database, witnet_node=witnet_node, logger=logger, diff --git a/api/blueprints/address/value_transfers_blueprint.py b/api/blueprints/address/value_transfers_blueprint.py index c360e29c..b8e8d70f 100644 --- a/api/blueprints/address/value_transfers_blueprint.py +++ b/api/blueprints/address/value_transfers_blueprint.py @@ -75,7 +75,6 @@ def get(self, args, pagination_parameters): ) address = Address( arg_address, - config, database=database, witnet_node=witnet_node, logger=logger, diff --git a/api/blueprints/search/epoch_blueprint.py b/api/blueprints/search/epoch_blueprint.py index 8ede6575..e1b663b7 100644 --- a/api/blueprints/search/epoch_blueprint.py +++ b/api/blueprints/search/epoch_blueprint.py @@ -63,16 +63,8 @@ def get(self, args): ) return cached_block, 200, {"X-Version": "1.0.0"} - # Create consensus constants - consensus_constants = ConsensusConstants( - database=database, - witnet_node=witnet_node, - ) - # Fetch block from a node block = Block( - config, - consensus_constants, block_epoch=epoch, logger=logger, database=database, diff --git a/api/blueprints/search/hash_blueprint.py b/api/blueprints/search/hash_blueprint.py index 67827c7b..19734d27 100644 --- a/api/blueprints/search/hash_blueprint.py +++ b/api/blueprints/search/hash_blueprint.py @@ -168,16 +168,9 @@ def get(self, args, pagination_parameters): cache_config = config["api"]["caching"] - consensus_constants = ConsensusConstants( - database=database, - witnet_node=witnet_node, - ) - if hash_type == "block": # Fetch block from a node block = Block( - config, - consensus_constants, block_hash=hash_value, logger=logger, database=database, @@ -254,7 +247,11 @@ def get(self, args, pagination_parameters): # Create mint transaction and get the details from the database if hash_type == "mint_txn": - mint = Mint(consensus_constants, logger=logger, database=database) + mint = Mint( + logger=logger, + database=database, + witnet_node=witnet_node, + ) try: mint_txn = mint.get_transaction_from_database(hash_value) except ValidationError as err_info: @@ -315,9 +312,9 @@ def get(self, args, pagination_parameters): # Create value transfer transaction and get the details from the database if hash_type == "value_transfer_txn": value_transfer = ValueTransfer( - consensus_constants, logger=logger, database=database, + witnet_node=witnet_node, ) try: value_transfer_txn = value_transfer.get_transaction_from_database( @@ -386,7 +383,6 @@ def get(self, args, pagination_parameters): if simple: if hash_type == "data_request_txn": data_request = DataRequest( - consensus_constants, logger=logger, database=database, witnet_node=witnet_node, @@ -425,7 +421,6 @@ def get(self, args, pagination_parameters): ) elif hash_type == "commit_txn": commit = Commit( - consensus_constants, logger=logger, database=database, witnet_node=witnet_node, @@ -465,7 +460,6 @@ def get(self, args, pagination_parameters): ) elif hash_type == "reveal_txn": reveal = Reveal( - consensus_constants, logger=logger, database=database, witnet_node=witnet_node, @@ -505,7 +499,6 @@ def get(self, args, pagination_parameters): ) elif hash_type == "tally_txn": tally = Tally( - consensus_constants, logger=logger, database=database, witnet_node=witnet_node, @@ -546,8 +539,6 @@ def get(self, args, pagination_parameters): # Create data request report for this hash else: data_request_report = DataRequestReport( - config, - consensus_constants, hash_value, hash_type[:-4], logger=logger, @@ -649,7 +640,6 @@ def get(self, args, pagination_parameters): if hash_type in ("DRO_bytes_hash", "RAD_bytes_hash"): # Create data request history data_request_history = DataRequestHistory( - consensus_constants, logger, database, ) diff --git a/blockchain/config.py b/blockchain/config.py new file mode 100644 index 00000000..8ba6c826 --- /dev/null +++ b/blockchain/config.py @@ -0,0 +1,4 @@ +class BlockchainConfig(object): + config = {} + wip = None + consensus_constants = None diff --git a/blockchain/explorer.py b/blockchain/explorer.py index 5edbcaee..cb0dc20f 100644 --- a/blockchain/explorer.py +++ b/blockchain/explorer.py @@ -15,7 +15,9 @@ import toml +from blockchain.config import BlockchainConfig from blockchain.objects.block import Block +from blockchain.objects.wip import WIP from blockchain.transactions.data_request import DataRequest from blockchain.transactions.value_transfer import ValueTransfer from blockchain.witnet_database import WitnetDatabase @@ -27,10 +29,12 @@ class BlockExplorer(object): - def __init__(self, config, log_queue): - error_retry = config["explorer"]["error_retry"] - - self.mempool_interval = config["explorer"]["mempool_interval"] + def __init__(self, log_queue): + # Get some configuration parameters + self.config = BlockchainConfig.config + self.poll_interval = self.config["explorer"]["poll_interval"] + self.addresses_config = self.config["api"]["caching"]["scripts"]["addresses"] + self.mempool_interval = self.config["explorer"]["mempool_interval"] # Set up logger self.configure_logging_process(log_queue, "explorer") @@ -39,48 +43,37 @@ def __init__(self, config, log_queue): # Set up logging queue for logging from different processes self.log_queue = log_queue - # Save the configuration - self.config = config - # Create nodes to connect to the node pool self.insert_blocks_node = WitnetNode( - config["node-pool"], timeout=30, log_queue=self.log_queue, log_label="node-insert", ) self.confirm_blocks_node = WitnetNode( - config["node-pool"], timeout=30, log_queue=self.log_queue, log_label="node-confirm", ) self.insert_pending_node = WitnetNode( - config["node-pool"], timeout=30, log_queue=self.log_queue, log_label="node-pending", ) - # Get consensus constants - self.consensus_constants = ConsensusConstants( - config=self.config, error_retry=error_retry - ) - # Create database objects self.insert_blocks_database = WitnetDatabase( - self.config, log_queue=self.log_queue, log_label="db-insert" + log_queue=self.log_queue, + log_label="db-insert", ) self.confirm_blocks_database = WitnetDatabase( - self.config, log_queue=self.log_queue, log_label="db-confirm" + log_queue=self.log_queue, + log_label="db-confirm", ) self.mempool_database = WitnetDatabase( - self.config, log_queue=self.log_queue, log_label="db-pending" + log_queue=self.log_queue, + log_label="db-pending", ) - # Get configuration to connect to the address caching server - self.addresses_config = config["api"]["caching"]["scripts"]["addresses"] - def configure_logging_process(self, queue, label): handler = logging.handlers.QueueHandler(queue) root = logging.getLogger(label) @@ -96,8 +89,6 @@ def terminate(self): def insert_block(self, database, block_hash_hex_str, block, epoch, tapi_periods): # Create block object and parse it to a JSON object block = Block( - self.config, - self.consensus_constants, block=block, block_hash=block_hash_hex_str, log_queue=self.log_queue, @@ -264,7 +255,7 @@ def insert_blocks_and_transactions(self, log_queue, unconfirmed_blocks_queue): ) # If we are adding the first block, initialize last_block_hash with the bootstrap_hash if last_block_hash == "": - last_block_hash = self.consensus_constants.bootstrap_hash + last_block_hash = BlockchainConfig.consensus_constants.bootstrap_hash # sleep until the next poll interval next_poll_interval = ( @@ -335,8 +326,8 @@ def confirm_blocks_and_transactions(self, log_queue, unconfirmed_blocks_queue): logger = logging.getLogger("explorer-confirm") # Calculate superepoch period from consensus constants - superblock_period = self.consensus_constants.superblock_period - checkpoints_period = self.consensus_constants.checkpoints_period + superblock_period = BlockchainConfig.consensus_constants.superblock_period + checkpoints_period = BlockchainConfig.consensus_constants.checkpoints_period # Connect to the addresses caching server caching_server = SocketManager( @@ -533,10 +524,7 @@ def insert_mempool_transactions(self, log_queue): current_epoch = self.insert_pending_node.get_current_epoch() if current_epoch == 0: - current_epoch = calculate_current_epoch( - self.consensus_constants.checkpoint_zero_timestamp, - self.consensus_constants.checkpoints_period, - ) + current_epoch = calculate_current_epoch() transactions_pool = self.insert_pending_node.get_mempool() # If all nodes are busy retry in short bursts to get the request through @@ -568,8 +556,6 @@ def insert_mempool_transactions(self, log_queue): mapped_transactions, queried_transactions = 0, 0 data_request = DataRequest( - self.config, - self.consensus_constants, database=self.mempool_database, logger=logger, ) @@ -611,8 +597,6 @@ def insert_mempool_transactions(self, log_queue): mapped_transactions, queried_transactions = 0, 0 value_transfer = ValueTransfer( - self.config, - self.consensus_constants, database=self.mempool_database, logger=logger, ) @@ -731,7 +715,8 @@ def select_logging_level(level): return logging.CRITICAL -def configure_logging_listener(config): +def configure_logging_listener(): + config = BlockchainConfig.config root = logging.getLogger() logging.Formatter.converter = time.gmtime @@ -780,8 +765,8 @@ def configure_logging_listener(config): root.addHandler(console_handler) -def logging_listener(config, queue): - configure_logging_listener(config) +def logging_listener(queue): + configure_logging_listener() while True: try: @@ -803,16 +788,19 @@ def main(): ) options, args = parser.parse_args() - # Load config file - config = toml.load(options.config_file) + # Create blockchain configuration object + BlockchainConfig.config = toml.load(options.config_file) + BlockchainConfig.wip = WIP() + error_retry = BlockchainConfig.config["explorer"]["error_retry"] + BlockchainConfig.consensus_constants = ConsensusConstants(error_retry=error_retry) # Start logging process log_queue = Queue() - listener_process = Process(target=logging_listener, args=(config, log_queue)) + listener_process = Process(target=logging_listener, args=(log_queue,)) listener_process.start() # Create explorer - explorer = BlockExplorer(config, log_queue) + explorer = BlockExplorer(log_queue) # Create queue to pass data about unconfirmed blocks unconfirmed_blocks_queue = Queue() diff --git a/blockchain/objects/address.py b/blockchain/objects/address.py index 1911a3b3..5458e44d 100644 --- a/blockchain/objects/address.py +++ b/blockchain/objects/address.py @@ -2,6 +2,7 @@ from psycopg.sql import SQL, Literal +from blockchain.config import BlockchainConfig from blockchain.transactions.reveal import translate_reveal from blockchain.transactions.tally import translate_tally from node.consensus_constants import ConsensusConstants @@ -15,7 +16,6 @@ class Address(object): def __init__( self, address, - config, database=None, witnet_node=None, logger=None, @@ -24,9 +24,6 @@ def __init__( # Set address self.address = address.strip() - # Save config - self.config = config - # Initialize database manager if provided self.db_mngr = None if database: @@ -51,19 +48,17 @@ def __init__( def initialize_connections(self): # Connect to the database if necessary if self.db_mngr is None: - self.db_mngr = DatabaseManager( - self.config, named_cursor=False, logger=self.logger - ) + self.db_mngr = DatabaseManager(named_cursor=False, logger=self.logger) # Connect to node pool if self.witnet_node is None: - self.witnet_node = WitnetNode(self.config["node-pool"], logger=self.logger) + self.witnet_node = WitnetNode(logger=self.logger) # Save consensus constants consensus_constants = ConsensusConstants( database=self.db_mngr, witnet_node=self.witnet_node, - error_retry=self.config["api"]["error_retry"], + error_retry=BlockchainConfig.config["api"]["error_retry"], ) self.start_time = consensus_constants.checkpoint_zero_timestamp self.epoch_period = consensus_constants.checkpoints_period diff --git a/blockchain/objects/block.py b/blockchain/objects/block.py index af9fcc90..292f14a1 100644 --- a/blockchain/objects/block.py +++ b/blockchain/objects/block.py @@ -2,6 +2,7 @@ import logging.handlers import time +from blockchain.config import BlockchainConfig from blockchain.transactions.commit import Commit from blockchain.transactions.data_request import DataRequest from blockchain.transactions.mint import Mint @@ -16,8 +17,6 @@ class Block(object): def __init__( self, - config, - consensus_constants, block=None, block_hash="", block_epoch=-1, @@ -27,17 +26,15 @@ def __init__( tapi_periods=None, witnet_node=None, ): - self.config = config - self.block = block self.block_hash = block_hash self.block_epoch = block_epoch - self.consensus_constants = consensus_constants - self.collateral_minimum = consensus_constants.collateral_minimum - self.start_time = consensus_constants.checkpoint_zero_timestamp - self.epoch_period = consensus_constants.checkpoints_period - self.superblock_period = consensus_constants.superblock_period + self.consensus_constants = BlockchainConfig.consensus_constants + self.collateral_minimum = self.consensus_constants.collateral_minimum + self.start_time = self.consensus_constants.checkpoint_zero_timestamp + self.epoch_period = self.consensus_constants.checkpoints_period + self.superblock_period = self.consensus_constants.superblock_period # Set up logger if logger: @@ -52,13 +49,13 @@ def __init__( if database: self.database = database else: - self.database = DatabaseManager(config, logger=self.logger) + self.database = DatabaseManager(logger=self.logger) # Connect to node pool if witnet_node: self.witnet_node = witnet_node else: - self.witnet_node = WitnetNode(config["node-pool"], logger=self.logger) + self.witnet_node = WitnetNode(logger=self.logger) self.current_epoch = (int(time.time()) - self.start_time) // self.epoch_period @@ -209,8 +206,6 @@ def process_mint_txn(self): json_txn = self.block["txns"]["mint"] block_signature = self.block["block_sig"]["public_key"] mint = Mint( - self.config, - self.consensus_constants, database=self.database, logger=self.logger, witnet_node=self.witnet_node, @@ -222,8 +217,6 @@ def process_value_transfer_txns(self, call_from): value_transfer_txns = [] if len(self.block["txns_hashes"]["value_transfer"]) > 0: value_transfer = ValueTransfer( - self.config, - self.consensus_constants, database=self.database, logger=self.logger, witnet_node=self.witnet_node, @@ -247,8 +240,6 @@ def process_data_request_txns(self, call_from): data_request_transactions = [] if len(self.block["txns_hashes"]["data_request"]) > 0: data_request = DataRequest( - self.config, - self.consensus_constants, database=self.database, logger=self.logger, witnet_node=self.witnet_node, @@ -272,8 +263,6 @@ def process_commit_txns(self, call_from): commit_transactions = [] if len(self.block["txns_hashes"]["commit"]) > 0: commit = Commit( - self.config, - self.consensus_constants, database=self.database, logger=self.logger, witnet_node=self.witnet_node, @@ -288,8 +277,6 @@ def process_reveal_txns(self, call_from): reveal_transactions = [] if len(self.block["txns_hashes"]["reveal"]) > 0: reveal = Reveal( - self.config, - self.consensus_constants, database=self.database, logger=self.logger, witnet_node=self.witnet_node, @@ -304,8 +291,6 @@ def process_tally_txns(self, call_from): tally_transactions = [] if len(self.block["txns_hashes"]["tally"]) > 0: tally = Tally( - self.config, - self.consensus_constants, database=self.database, logger=self.logger, witnet_node=self.witnet_node, diff --git a/blockchain/objects/data_request_history.py b/blockchain/objects/data_request_history.py index d783912a..c580b32b 100644 --- a/blockchain/objects/data_request_history.py +++ b/blockchain/objects/data_request_history.py @@ -1,5 +1,6 @@ from psycopg.sql import SQL, Identifier +from blockchain.config import BlockchainConfig from blockchain.transactions.data_request import ( build_retrieval, translate_filters, @@ -13,8 +14,9 @@ class DataRequestHistory(object): - def __init__(self, consensus_constants, logger, database): + def __init__(self, logger, database): # Copy relevant consensus constants + consensus_constants = BlockchainConfig.consensus_constants self.start_time = consensus_constants.checkpoint_zero_timestamp self.epoch_period = consensus_constants.checkpoints_period diff --git a/blockchain/objects/data_request_report.py b/blockchain/objects/data_request_report.py index 4d47e688..be74d739 100644 --- a/blockchain/objects/data_request_report.py +++ b/blockchain/objects/data_request_report.py @@ -1,6 +1,7 @@ import logging import logging.handlers +from blockchain.config import BlockchainConfig from blockchain.transactions.commit import Commit from blockchain.transactions.data_request import DataRequest from blockchain.transactions.reveal import Reveal @@ -14,18 +15,16 @@ class DataRequestReport(object): def __init__( self, - config, - consensus_constants, transaction_hash, transaction_type, logger=None, log_queue=None, database=None, ): - self.consensus_constants = consensus_constants - self.start_time = consensus_constants.checkpoint_zero_timestamp - self.epoch_period = consensus_constants.checkpoints_period - self.collateral_minimum = consensus_constants.collateral_minimum + self.consensus_constants = BlockchainConfig.consensus_constants + self.start_time = self.consensus_constants.checkpoint_zero_timestamp + self.epoch_period = self.consensus_constants.checkpoints_period + self.collateral_minimum = self.consensus_constants.collateral_minimum self.transaction_hash = transaction_hash self.transaction_type = transaction_type @@ -44,7 +43,7 @@ def __init__( self.database = database else: self.database = DatabaseManager( - config, logger=self.logger, custom_types=["utxo", "filter"] + logger=self.logger, custom_types=["utxo", "filter"] ) def configure_logging_process(self, queue, label): @@ -61,23 +60,17 @@ def get_data_request_hash(self): self.logger.info(f"data_request, get_report({data_request_hash})") elif self.transaction_type == "commit": self.logger.info(f"commit, get_report({self.transaction_hash})") - self.commit = Commit( - self.consensus_constants, logger=self.logger, database=self.database - ) + self.commit = Commit(logger=self.logger, database=self.database) data_request_hash = self.commit.get_data_request_hash(self.transaction_hash) self.logger.info(f"data_request, get_report({data_request_hash})") elif self.transaction_type == "reveal": self.logger.info(f"reveal, get_report({self.transaction_hash})") - self.reveal = Reveal( - self.consensus_constants, logger=self.logger, database=self.database - ) + self.reveal = Reveal(logger=self.logger, database=self.database) data_request_hash = self.reveal.get_data_request_hash(self.transaction_hash) self.logger.info(f"data_request, get_report({data_request_hash})") elif self.transaction_type == "tally": self.logger.info(f"tally, get_report({self.transaction_hash})") - self.tally = Tally( - self.consensus_constants, logger=self.logger, database=self.database - ) + self.tally = Tally(logger=self.logger, database=self.database) data_request_hash = self.tally.get_data_request_hash(self.transaction_hash) self.logger.info(f"data_request, get_report({data_request_hash})") return data_request_hash @@ -120,32 +113,24 @@ def get_report(self): def get_data_request_details(self): self.logger.info(f"get_data_request_details({self.data_request_hash})") - data_request = DataRequest( - self.consensus_constants, logger=self.logger, database=self.database - ) + data_request = DataRequest(logger=self.logger, database=self.database) self.data_request = data_request.get_transaction_from_database( self.data_request_hash ) def get_commit_details(self): self.logger.info(f"get_commit_details({self.data_request_hash})") - commit = Commit( - self.consensus_constants, logger=self.logger, database=self.database - ) + commit = Commit(logger=self.logger, database=self.database) self.commits = commit.get_commits_for_data_request(self.data_request_hash) def get_reveal_details(self): self.logger.info(f"get_reveal_details({self.data_request_hash})") - reveal = Reveal( - self.consensus_constants, logger=self.logger, database=self.database - ) + reveal = Reveal(logger=self.logger, database=self.database) self.reveals = reveal.get_reveals_for_data_request(self.data_request_hash) def get_tally_details(self): self.logger.info(f"get_tally_details({self.data_request_hash})") - tally = Tally( - self.consensus_constants, logger=self.logger, database=self.database - ) + tally = Tally(logger=self.logger, database=self.database) self.tally = tally.get_tally_for_data_request(self.data_request_hash) def add_missing_reveals(self): diff --git a/blockchain/objects/wip.py b/blockchain/objects/wip.py index 98398c2f..bb0d7ab1 100644 --- a/blockchain/objects/wip.py +++ b/blockchain/objects/wip.py @@ -10,31 +10,21 @@ class WIP(object): def __init__( self, - config=None, database=None, witnet_node=None, mockup=False, ): if database: self.db_mngr = database - self.fetch_wips() - elif config is not None: - self.db_mngr = DatabaseManager(config) - self.fetch_wips() else: - AssertionError( - "Need to pass a database object or configuration settings to create one" - ) + self.db_mngr = DatabaseManager() + self.fetch_wips() self.witnet_node = None if witnet_node is not None: self.witnet_node = witnet_node - elif config is not None: - self.witnet_node = WitnetNode(config["node-pool"]) else: - AssertionError( - "Need to pass a witnet node object or configuration settings to create one" - ) + self.witnet_node = WitnetNode() self.mockup = mockup if self.mockup: diff --git a/blockchain/transactions/transaction.py b/blockchain/transactions/transaction.py index a2b00799..e650a1a4 100644 --- a/blockchain/transactions/transaction.py +++ b/blockchain/transactions/transaction.py @@ -4,6 +4,7 @@ from psycopg.sql import SQL, Identifier +from blockchain.config import BlockchainConfig from blockchain.objects.wip import WIP from node.witnet_node import WitnetNode from util.address_generator import AddressGenerator @@ -15,22 +16,21 @@ class Transaction(object): def __init__( self, - config, - consensus_constants, database=None, logger=None, witnet_node=None, ): - self.start_time = consensus_constants.checkpoint_zero_timestamp - self.epoch_period = consensus_constants.checkpoints_period - self.collateral_minimum = consensus_constants.collateral_minimum + self.consensus_constants = BlockchainConfig.consensus_constants + self.start_time = self.consensus_constants.checkpoint_zero_timestamp + self.epoch_period = self.consensus_constants.checkpoints_period + self.collateral_minimum = self.consensus_constants.collateral_minimum # Connect to the database if database is not None: self.database = database else: self.database = DatabaseManager( - config, logger=logger, custom_types=["utxo", "filter"] + logger=logger, custom_types=["utxo", "filter"] ) # Set up logger @@ -43,19 +43,26 @@ def __init__( if witnet_node is not None: self.witnet_node = witnet_node else: - self.witnet_node = WitnetNode(config["node-pool"], logger=self.logger) + self.witnet_node = WitnetNode(logger=self.logger) + + # Get the network type + network_type = BlockchainConfig.config["environment"]["network"] # Create address generator address_prefix = None - if config["environment"]["network"] == "mainnet": + if network_type == "mainnet": address_prefix = "wit" - elif config["environment"]["network"] == "testnet": + elif network_type in ("pytest", "testnet"): address_prefix = "twit" assert address_prefix, "Need to properly set the network type" self.address_generator = AddressGenerator(address_prefix) # Create Protobuf encoder - self.protobuf_encoder = ProtobufEncoder(WIP(database=self.database)) + if network_type in ("mainnet", "testnet"): + self.protobuf_encoder = ProtobufEncoder(WIP(database=self.database)) + elif network_type == "pytest": + self.protobuf_encoder = ProtobufEncoder(WIP(mockup=True)) + assert self.protobuf_encoder, "Need to properly set the network type" def configure_logging_process(self, queue, label): handler = logging.handlers.QueueHandler(queue) diff --git a/blockchain/witnet_database.py b/blockchain/witnet_database.py index 9082f1b0..cbf6678c 100644 --- a/blockchain/witnet_database.py +++ b/blockchain/witnet_database.py @@ -7,7 +7,6 @@ class WitnetDatabase(object): def __init__( self, - config, named_cursor=False, logger=None, log_queue=None, @@ -23,7 +22,8 @@ def __init__( self.logger = None self.db_mngr = DatabaseManager( - config, named_cursor=named_cursor, logger=self.logger + named_cursor=named_cursor, + logger=self.logger, ) # Register types created for this database diff --git a/caching/addresses.py b/caching/addresses.py index 48b278f6..c939a6d8 100644 --- a/caching/addresses.py +++ b/caching/addresses.py @@ -22,7 +22,10 @@ from multiprocessing import Queue from multiprocessing import Manager +from blockchain.config import BlockchainConfig +from blockchain.consensus_constants import ConsensusConstants from blockchain.objects.address import Address +from blockchain.objects.wip import WIP from schemas.address.block_view_schema import BlockView from schemas.address.data_request_view_schema import DataRequestCreatedView, DataRequestSolvedView @@ -304,7 +307,7 @@ def client(self, logging_queue, config, connection, address_stack, epoch_address for function, m_address in zip(functions, monitor_addresses): # Create address object - address = Address(m_address, config, logger=logger, connect=False) + address = Address(m_address, logger=logger, connect=False) # Complete the request # This block of code is surrounded with a try-except to catch a known Python bug with the Manager multi-processing Pool @@ -433,6 +436,10 @@ def main(): # Load config file config = toml.load(options.config_file) + BlockchainConfig.config = toml.load(options.config_file) + BlockchainConfig.wip = WIP() + BlockchainConfig.consensus_constants = ConsensusConstants() + # Start logging process logging_queue = Manager().Queue() listener = PickleProcess(target=create_logging_listener, args=(config["api"]["caching"]["scripts"]["addresses"], logging_queue)) diff --git a/caching/balance_list.py b/caching/balance_list.py index caf9c454..f9e4f714 100644 --- a/caching/balance_list.py +++ b/caching/balance_list.py @@ -4,24 +4,25 @@ import time import toml +from blockchain.config import BlockchainConfig from caching.client import Client from schemas.network.balances_schema import NetworkBalancesResponse from util.data_transformer import re_sql from util.logger import configure_logger class BalanceList(Client): - def __init__(self, config): + def __init__(self): + bl_cfg = BlockchainConfig.config["api"]["caching"]["scripts"]["balance_list"] + # Setup logger - log_filename = config["api"]["caching"]["scripts"]["balance_list"]["log_file"] - log_level = config["api"]["caching"]["scripts"]["balance_list"]["level_file"] - self.logger = configure_logger("balance-list", log_filename, log_level) + self.logger = configure_logger("balance-list", bl_cfg["log_file"], bl_cfg["level_file"]) # Read some Witnet node parameters - self.node_retries = config["api"]["caching"]["node_retries"] - self.timeout = config["api"]["caching"]["scripts"]["balance_list"]["timeout"] - self.node_timeout = config["api"]["caching"]["scripts"]["balance_list"]["node_timeout"] + self.node_retries = BlockchainConfig.config["api"]["caching"]["node_retries"] + self.timeout = bl_cfg["timeout"] + self.node_timeout = bl_cfg["node_timeout"] - super().__init__(config, node_timeout=self.node_timeout) + super().__init__(BlockchainConfig.config, node_timeout=self.node_timeout) def build(self): start = time.perf_counter() @@ -157,10 +158,10 @@ def main(): sys.exit(1) # Load config file - config = toml.load(options.config_file) + BlockchainConfig.config = toml.load(options.config_file) # Create BalanceList cache - balance_list = BalanceList(config) + balance_list = BalanceList() # Save BalanceList in memcached instance on success if balance_list.build(): balance_list.save() diff --git a/caching/blocks.py b/caching/blocks.py index de25427e..2a4c15db 100644 --- a/caching/blocks.py +++ b/caching/blocks.py @@ -6,28 +6,29 @@ from marshmallow import ValidationError -from caching.client import Client +from blockchain.config import BlockchainConfig +from blockchain.consensus_constants import ConsensusConstants +from blockchain.objects.wip import WIP from blockchain.objects.block import Block +from caching.client import Client from util.data_transformer import re_sql from util.logger import configure_logger from util.memcached import calculate_timeout from util.common_sql import sql_last_block class Blocks(Client): - def __init__(self, config): - # Setup logger - log_filename = config["api"]["caching"]["scripts"]["blocks"]["log_file"] - log_level = config["api"]["caching"]["scripts"]["blocks"]["level_file"] - self.logger = configure_logger("block", log_filename, log_level) + def __init__(self): + b_cfg = BlockchainConfig.config["api"]["caching"]["scripts"]["blocks"] - super().__init__(config) + # Setup logger + self.logger = configure_logger("block", b_cfg["log_file"], b_cfg["level_file"]) - self.node_config = config["node-pool"] + super().__init__(BlockchainConfig.config) # Fetch configured timeout for block cache expiry - self.memcached_timeout = config["api"]["caching"]["scripts"]["blocks"]["timeout"] + self.memcached_timeout = b_cfg["timeout"] # Calculate how many epochs in the past this script has to cache blocks - self.lookback_epochs = int(config["api"]["caching"]["scripts"]["blocks"]["timeout"] / self.consensus_constants.checkpoints_period) + self.lookback_epochs = int(b_cfg["timeout"] / self.consensus_constants.checkpoints_period) self.superblock_period = self.consensus_constants.superblock_period @@ -130,7 +131,7 @@ def process(self, force_update): def build_block(self, block_hash, epoch): # Build block - block = Block(self.consensus_constants, block_hash=block_hash, logger=self.logger, database=self.database, database_config=self.config["database"], node_config=self.node_config) + block = Block(block_hash=block_hash, logger=self.logger, database=self.database) json_block = block.process_block("api") if "error" in json_block: self.logger.warning(f"Could not fetch block {block_hash} for epoch {epoch}") @@ -166,10 +167,12 @@ def main(): sys.exit(1) # Load config file - config = toml.load(options.config_file) + BlockchainConfig.config = toml.load(options.config_file) + BlockchainConfig.consensus_constants = ConsensusConstants() + BlockchainConfig.wip = WIP() # Create block cache - blocks = Blocks(config) + blocks = Blocks() blocks.process(options.force_update) if __name__ == "__main__": diff --git a/caching/client.py b/caching/client.py index eb632aa0..a817ad81 100644 --- a/caching/client.py +++ b/caching/client.py @@ -15,7 +15,6 @@ def __init__(self, config, node_timeout=0, named_cursor=False): # Connect to node pool try: self.witnet_node = WitnetNode( - config["node-pool"], timeout=node_timeout, logger=self.logger, ) @@ -26,18 +25,10 @@ def __init__(self, config, node_timeout=0, named_cursor=False): # Connect to database try: self.database = DatabaseManager( - config, named_cursor=named_cursor, logger=self.logger, custom_types=["utxo", "filter"], ) - if named_cursor: - self.database_client = DatabaseManager( - config["database"], - named_cursor=False, - logger=self.logger, - custom_types=["utxo", "filter"], - ) except psycopg.OperationalError: self.logger.error("Could not connect to the database!") sys.exit(1) diff --git a/caching/data_request_reports.py b/caching/data_request_reports.py index 2839a6a3..3e40c2b4 100644 --- a/caching/data_request_reports.py +++ b/caching/data_request_reports.py @@ -7,7 +7,10 @@ from marshmallow import ValidationError +from blockchain.config import BlockchainConfig +from blockchain.consensus_constants import ConsensusConstants from blockchain.objects.data_request_report import DataRequestReport +from blockchain.objects.wip import WIP from caching.client import Client from util.data_transformer import re_sql from util.logger import configure_logger @@ -15,20 +18,20 @@ from util.common_sql import sql_last_block class DataRequestReports(Client): - def __init__(self, config): + def __init__(self): + drr_cfg = BlockchainConfig.config["api"]["caching"]["scripts"]["data_request_reports"] + # Setup logger - log_filename = config["api"]["caching"]["scripts"]["data_request_reports"]["log_file"] - log_level = config["api"]["caching"]["scripts"]["data_request_reports"]["level_file"] - self.logger = configure_logger("report", log_filename, log_level) + self.logger = configure_logger("report", drr_cfg["log_file"], drr_cfg["level_file"]) - super().__init__(config) + super().__init__(BlockchainConfig.config) # Fetch configured timeout for data request report cache expiry - self.memcached_timeout = config["api"]["caching"]["scripts"]["data_request_reports"]["timeout"] + self.memcached_timeout = drr_cfg["timeout"] # Calculate how many epochs in the past this script has to cache data request reports - self.lookback_epochs = int(config["api"]["caching"]["scripts"]["data_request_reports"]["timeout"] / self.consensus_constants.checkpoints_period) + self.lookback_epochs = int(drr_cfg["timeout"] / self.consensus_constants.checkpoints_period) - self.cache_time_warning = config["api"]["caching"]["scripts"]["data_request_reports"]["cache_time_warning"] + self.cache_time_warning = drr_cfg["cache_time_warning"] def process_data_requests(self, force_update): start = time.perf_counter() @@ -132,7 +135,7 @@ def process_data_requests(self, force_update): def cache_data_request_report(self, txn_hash, epoch, inner_start): # Build data request report - data_request = DataRequestReport(self.config, self.consensus_constants, txn_hash, "data_request", logger=self.logger, database=self.database) + data_request = DataRequestReport(txn_hash, "data_request", logger=self.logger, database=self.database) try: data_request_report = data_request.get_report() if "error" in data_request_report: @@ -183,10 +186,12 @@ def main(): sys.exit(1) # Load config file - config = toml.load(options.config_file) + BlockchainConfig.config = toml.load(options.config_file) + BlockchainConfig.consensus_constants = ConsensusConstants() + BlockchainConfig.wip = WIP() # Create data request report cache - report_cache = DataRequestReports(config) + report_cache = DataRequestReports() report_cache.process_data_requests(options.force_update) if __name__ == "__main__": diff --git a/caching/home_stats.py b/caching/home_stats.py index bd5d694c..cb5cda4b 100644 --- a/caching/home_stats.py +++ b/caching/home_stats.py @@ -5,28 +5,29 @@ import time import toml -from caching.client import Client +from blockchain.config import BlockchainConfig +from blockchain.consensus_constants import ConsensusConstants from blockchain.objects.wip import WIP +from caching.client import Client from schemas.misc.home_schema import HomeBlock, HomeNetworkStats, HomeTransaction, HomeResponse from schemas.network.supply_schema import NetworkSupply from util.data_transformer import re_sql from util.logger import configure_logger class HomeStats(Client): - def __init__(self, config): + def __init__(self): + h_cfg = BlockchainConfig.config["api"]["caching"]["scripts"]["home_stats"] + # Setup logger - log_filename = config["api"]["caching"]["scripts"]["home_stats"]["log_file"] - log_level = config["api"]["caching"]["scripts"]["home_stats"]["level_file"] - self.logger = configure_logger("home", log_filename, log_level) + self.logger = configure_logger("home", h_cfg["log_file"], h_cfg["level_file"]) - super().__init__(config) + super().__init__(BlockchainConfig.config) # Assign some of the consensus constants self.start_time = self.consensus_constants.checkpoint_zero_timestamp self.epoch_period = self.consensus_constants.checkpoints_period - wips = WIP(database_config=config["database"], node_config=config["node-pool"]) - self.wip0027_activation_epoch = wips.get_activation_epoch("WIP0027") + self.wip0027_activation_epoch = BlockchainConfig.wip.get_activation_epoch("WIP0027") # Initialize previous variables self.current_epoch = int((time.time() - self.start_time) / self.epoch_period) @@ -328,10 +329,12 @@ def main(): sys.exit(1) # Load config file - config = toml.load(options.config_file) + BlockchainConfig.config = toml.load(options.config_file) + BlockchainConfig.consensus_constants = ConsensusConstants() + BlockchainConfig.wip = WIP() # Create home cache - home_cache = HomeStats(config) + home_cache = HomeStats() home_cache.collect_home_stats() home_cache.save_home_stats() diff --git a/caching/network_stats.py b/caching/network_stats.py index fb133e17..4844bf46 100644 --- a/caching/network_stats.py +++ b/caching/network_stats.py @@ -5,23 +5,31 @@ import time import toml +from blockchain.config import BlockchainConfig +from blockchain.consensus_constants import ConsensusConstants +from blockchain.objects.wip import WIP from caching.client import Client from caching.network_stats_functions import aggregate_nodes, read_from_database -from blockchain.objects.wip import WIP - -from util.data_transformer import re_sql from util.common_functions import calculate_block_reward +from util.database_manager import DatabaseManager +from util.data_transformer import re_sql from util.logger import configure_logger class NetworkStats(Client): - def __init__(self, config, reset): + def __init__(self, reset): + ns_cfg = BlockchainConfig.config["api"]["caching"]["scripts"]["network_stats"] + # Setup logger - log_filename = config["api"]["caching"]["scripts"]["network_stats"]["log_file"] - log_level = config["api"]["caching"]["scripts"]["network_stats"]["level_file"] - self.logger = configure_logger("network", log_filename, log_level) + self.logger = configure_logger("network", ns_cfg["log_file"], ns_cfg["level_file"]) - timeout = config["api"]["caching"]["scripts"]["network_stats"]["node_timeout"] - super().__init__(config, node_timeout=timeout, named_cursor=True) + super().__init__(BlockchainConfig.config, node_timeout=ns_cfg["node_timeout"], named_cursor=True) + + # Also create a database client without a named cursor + self.database_client = DatabaseManager( + named_cursor=False, + logger=self.logger, + custom_types=["utxo", "filter"], + ) # Assign some of the consensus constants self.start_time = self.consensus_constants.checkpoint_zero_timestamp @@ -30,9 +38,7 @@ def __init__(self, config, reset): self.initial_block_reward = self.consensus_constants.initial_block_reward # Granularity at which network statistics are aggregated - self.aggregation_epochs = config["api"]["caching"]["scripts"]["network_stats"]["aggregation_epochs"] - - self.wips = WIP(database_config=config["database"], node_config=config["node-pool"]) + self.aggregation_epochs = ns_cfg["aggregation_epochs"] self.last_update_time = int(time.time()) @@ -607,7 +613,7 @@ def get_burn_rate_per_period(self, reset): previous_epoch = epoch - if not self.wips.is_wip0027_active(epoch): + if not BlockchainConfig.wip.is_wip0027_active(epoch): continue # Check if the next aggregation period was reached @@ -794,10 +800,12 @@ def main(): sys.exit(1) # Load config file - config = toml.load(options.config_file) + BlockchainConfig.config = toml.load(options.config_file) + BlockchainConfig.consensus_constants = ConsensusConstants() + BlockchainConfig.wip = WIP() # Create network cache - network_cache = NetworkStats(config, options.reset) + network_cache = NetworkStats(options.reset) network_cache.build_network_stats(options.reset) network_cache.save_network() diff --git a/caching/reputation_list.py b/caching/reputation_list.py index ae26df82..04bb01ee 100644 --- a/caching/reputation_list.py +++ b/caching/reputation_list.py @@ -4,21 +4,21 @@ import time import toml +from blockchain.config import BlockchainConfig from caching.client import Client from schemas.network.reputation_schema import NetworkReputationResponse from util.logger import configure_logger class ReputationList(Client): - def __init__(self, config): + def __init__(self): # Setup logger - log_filename = config["api"]["caching"]["scripts"]["reputation_list"]["log_file"] - log_level = config["api"]["caching"]["scripts"]["reputation_list"]["level_file"] - self.logger = configure_logger("reputation", log_filename, log_level) + rl_cfg = BlockchainConfig.config["api"]["caching"]["scripts"]["reputation_list"] + self.logger = configure_logger("reputation", rl_cfg["log_file"], rl_cfg["level_file"]) # Read some Witnet node parameters - self.node_retries = config["api"]["caching"]["node_retries"] + self.node_retries = BlockchainConfig.config["api"]["caching"]["node_retries"] - super().__init__(config) + super().__init__(BlockchainConfig.config) def get_reputation(self): start = time.perf_counter() @@ -85,10 +85,10 @@ def main(): sys.exit(1) # Load config file - config = toml.load(options.config_file) + BlockchainConfig.config = toml.load(options.config_file) # Create reputation cache - reputation_cache = ReputationList(config) + reputation_cache = ReputationList() # Only save reputation to the memcached instance on fetch and process success if reputation_cache.get_reputation(): reputation_cache.save_reputation() diff --git a/caching/tapi_list.py b/caching/tapi_list.py index e366367f..13efc0ad 100644 --- a/caching/tapi_list.py +++ b/caching/tapi_list.py @@ -8,6 +8,8 @@ import matplotlib.pyplot as plt import matplotlib.colors + +from blockchain.config import BlockchainConfig from caching.client import Client from schemas.network.tapi_schema import NetworkTapiResponse from util.data_transformer import re_sql @@ -15,17 +17,17 @@ from util.common_sql import sql_last_block class TapiList(Client): - def __init__(self, config): - self.plot_dir = config["api"]["caching"]["plot_directory"] + def __init__(self): + tl_cfg = BlockchainConfig.config["api"]["caching"]["scripts"]["tapi_list"] + + self.plot_dir = BlockchainConfig.config["api"]["caching"]["plot_directory"] if not os.path.exists(self.plot_dir): os.makedirs(self.plot_dir) # Setup logger - log_filename = config["api"]["caching"]["scripts"]["tapi_list"]["log_file"] - log_level = config["api"]["caching"]["scripts"]["tapi_list"]["level_file"] - self.logger = configure_logger("tapi", log_filename, log_level) + self.logger = configure_logger("tapi", tl_cfg["log_file"], tl_cfg["level_file"]) - super().__init__(config) + super().__init__() # Assign some of the consensus constants self.start_time = self.consensus_constants.checkpoint_zero_timestamp @@ -296,10 +298,10 @@ def main(): options, args = parser.parse_args() # Load config file - config = toml.load(options.config_file) + BlockchainConfig.config = toml.load(options.config_file) # create TAPI cache - tapi_cache = TapiList(config) + tapi_cache = TapiList() tapi_cache.collect_tapi_data() tapi_cache.save_tapi() diff --git a/mockups/data/create_mockup_database.py b/mockups/data/create_mockup_database.py index 617ca3ce..36c19301 100644 --- a/mockups/data/create_mockup_database.py +++ b/mockups/data/create_mockup_database.py @@ -4,7 +4,14 @@ import toml -from util.database_manager import DatabaseManager +from blockchain.config import BlockchainConfig +from blockchain.objects.wip import WIP +from blockchain.transactions.reveal import translate_reveal +from blockchain.transactions.tally import translate_tally +from node.witnet_node import WitnetNode +from util.address_generator import AddressGenerator +from util.data_transformer import bytes2hex +from util.protobuf_encoder import ProtobufEncoder def create_tables(database): @@ -1769,7 +1776,7 @@ def insert_wips(database): def get_epoch_data(config, epochs): - database = DatabaseManager(config["database"], custom_types=["utxo", "filter"]) + database = DatabaseManager(custom_types=["utxo", "filter"]) epoch_data = {} hashes_seen = set() @@ -2177,6 +2184,7 @@ def main(): # fmt: on config = toml.load(args.config_file) + BlockchainConfig.config = config create_tables(args.database) diff --git a/node/consensus_constants.py b/node/consensus_constants.py index c4e1ab01..a9081b8e 100644 --- a/node/consensus_constants.py +++ b/node/consensus_constants.py @@ -7,7 +7,6 @@ class ConsensusConstants(object): def __init__( self, - config=None, database=None, witnet_node=None, error_retry=0, @@ -15,12 +14,10 @@ def __init__( mock_parameters={}, ): if not mock: - assert config or database, "Need to pass a configuration dictionary or a database connection" - # First try to fetch the consensus constants from the database database_created = False if database is None: - database = DatabaseManager(config) + database = DatabaseManager() database_created = True sql = "SELECT * FROM consensus_constants" @@ -32,11 +29,9 @@ def __init__( # If that did not work, fetch them from a node if not fetched_consensus_constants: - assert config or witnet_node, "Need to pass a configuration dictionary or a witnet node connection" - witnet_node_created = False if not witnet_node: - witnet_node = WitnetNode(config["node-pool"]) + witnet_node = WitnetNode() witnet_node_created = True consensus_constants = witnet_node.get_consensus_constants() diff --git a/node/witnet_client_pool.py b/node/witnet_client_pool.py index 7d786b71..d58d29df 100644 --- a/node/witnet_client_pool.py +++ b/node/witnet_client_pool.py @@ -1,14 +1,15 @@ from contextlib import contextmanager from queue import Queue +from blockchain.config import BlockchainConfig from node.witnet_node import WitnetNode class WitnetClientPool(Queue): - def __init__(self, config): - clients = config["nodes"]["number"] + def __init__(self): + clients = BlockchainConfig.config["node-pool"]["nodes"]["number"] Queue.__init__(self, clients) for i in range(clients): - self.put(WitnetNode(config)) + self.put(WitnetNode()) def init_app(self, app): app.extensions = getattr(app, "extensions", {}) diff --git a/node/witnet_node.py b/node/witnet_node.py index 6ed5c21c..ab3a416d 100644 --- a/node/witnet_node.py +++ b/node/witnet_node.py @@ -7,13 +7,17 @@ import socket import sys +from blockchain.config import BlockchainConfig from util.data_transformer import hex2bytes from util.socket_manager import SocketManager class WitnetNode(object): request_id = 1 - def __init__(self, node_config, timeout=0, logger=None, log_queue=None, log_label=""): + def __init__(self, timeout=0, logger=None, log_queue=None, log_label=""): + # Get the node pool configuration + node_config = BlockchainConfig.config["node-pool"] + # If a timeout is specified, save it here so it can be propagated into the request self.request_timeout = timeout if timeout != node_config["default_timeout"] else 0 diff --git a/scripts/add_blocks.py b/scripts/add_blocks.py index 1a143267..3bdf210b 100644 --- a/scripts/add_blocks.py +++ b/scripts/add_blocks.py @@ -3,7 +3,9 @@ import toml +from blockchain.config import BlockchainConfig from blockchain.objects.block import Block +from blockchain.objects.wip import WIP from blockchain.witnet_database import WitnetDatabase from node.consensus_constants import ConsensusConstants from node.witnet_node import WitnetNode @@ -11,10 +13,8 @@ def add_block( - config, db_mngr, witnet_node, - consensus_constants, block_epoch=None, block_hash=None, ): @@ -34,8 +34,6 @@ def add_block( if block_epoch: block = Block( - config, - consensus_constants, block_epoch=block_epoch, database=db_mngr, witnet_node=witnet_node, @@ -43,8 +41,6 @@ def add_block( ) else: block = Block( - config, - consensus_constants, block_hash=block_hash, database=db_mngr, witnet_node=witnet_node, @@ -57,7 +53,7 @@ def add_block( epoch = block_json["details"]["epoch"] print(f"Adding block {block_json['details']['hash']} for epoch {epoch}") - witnet_database = WitnetDatabase(config) + witnet_database = WitnetDatabase() witnet_database.insert_block(block_json) witnet_database.insert_mint_txn(block_json["transactions"]["mint"], epoch) for txn_details in block_json["transactions"]["value_transfer"]: @@ -89,10 +85,13 @@ def main(): ) options, args = parser.parse_args() - config = toml.load(options.config_file) - db_mngr = DatabaseManager(config) - witnet_node = WitnetNode(config["node-pool"], timeout=300) - consensus_constants = ConsensusConstants(database=db_mngr, witnet_node=witnet_node) + # Create blockchain configuration object + BlockchainConfig.config = toml.load(options.config_file) + BlockchainConfig.wip = WIP() + BlockchainConfig.consensus_constants = ConsensusConstants() + + db_mngr = DatabaseManager() + witnet_node = WitnetNode() if options.epochs is not None: epochs_to_add = [int(epoch) for epoch in options.epochs.split(",")] @@ -110,19 +109,15 @@ def main(): if epochs_to_add: for block_epoch in epochs_to_add: add_block( - config, db_mngr, witnet_node, - consensus_constants, block_epoch=block_epoch, ) else: for block_hash in hashes_to_add: add_block( - config, db_mngr, witnet_node, - consensus_constants, block_hash=block_hash, ) diff --git a/scripts/confirm_blocks.py b/scripts/confirm_blocks.py index 33a3bd27..0af5cd9b 100644 --- a/scripts/confirm_blocks.py +++ b/scripts/confirm_blocks.py @@ -3,6 +3,7 @@ import toml +from blockchain.config import BlockchainConfig from util.database_manager import DatabaseManager @@ -26,8 +27,9 @@ def main(): ) options, args = parser.parse_args() - config = toml.load(options.config_file) - db_mngr = DatabaseManager(config) + # Create blockchain configuration object + BlockchainConfig.config = toml.load(options.config_file) + db_mngr = DatabaseManager() if options.epochs is not None: epochs_to_confirm = [int(epoch) for epoch in options.epochs.split(",")] diff --git a/scripts/delete_blocks.py b/scripts/delete_blocks.py index b0821701..d53019a4 100644 --- a/scripts/delete_blocks.py +++ b/scripts/delete_blocks.py @@ -3,6 +3,7 @@ import toml +from blockchain.config import BlockchainConfig from util.database_manager import DatabaseManager @@ -23,9 +24,7 @@ def delete_block(db_mngr, epoch): result = db_mngr.sql_update_table(sql_statement, parameters=[epoch]) print(f"Deleted {result} value transfer transaction(s) for epoch {epoch}") - sql_statement = "DELETE FROM data_request_txns WHERE data_request_txns.epoch=%s" % ( - epoch, - ) + sql_statement = "DELETE FROM data_request_txns WHERE data_request_txns.epoch=%s" result = db_mngr.sql_update_table(sql_statement, parameters=[epoch]) print(f"Deleted {result} data request transaction(s) for epoch {epoch}") @@ -124,8 +123,9 @@ def main(): ) options, args = parser.parse_args() - config = toml.load(options.config_file) - db_mngr = DatabaseManager(config) + # Create blockchain configuration object + BlockchainConfig.config = toml.load(options.config_file) + db_mngr = DatabaseManager() if options.epochs is not None: epochs_to_confirm = [int(epoch) for epoch in options.epochs.split(",")] diff --git a/scripts/insert_consensus_constants.py b/scripts/insert_consensus_constants.py index 200c30f9..6c380f84 100644 --- a/scripts/insert_consensus_constants.py +++ b/scripts/insert_consensus_constants.py @@ -3,12 +3,13 @@ import toml +from blockchain.config import BlockchainConfig from node.witnet_node import WitnetNode from util.database_manager import DatabaseManager -def get_consensus_constants(config): - witnet_node = WitnetNode(config["node-pool"]) +def get_consensus_constants(): + witnet_node = WitnetNode() response = witnet_node.get_consensus_constants() while type(response) is dict and "error" in response: @@ -19,8 +20,8 @@ def get_consensus_constants(config): return response["result"] -def insert_consensus_constants(config, consensus_constants): - db_mngr = DatabaseManager(config) +def insert_consensus_constants(consensus_constants): + db_mngr = DatabaseManager() for key, value in consensus_constants.items(): if isinstance(value, int) or isinstance(value, float): @@ -64,11 +65,11 @@ def main(): ) args = parser.parse_args() - config = toml.load(args.config_file) + BlockchainConfig.config = toml.load(args.config_file) - consensus_constants = get_consensus_constants(config) + consensus_constants = get_consensus_constants() - insert_consensus_constants(config, consensus_constants) + insert_consensus_constants(consensus_constants) if __name__ == "__main__": diff --git a/scripts/manage_wips.py b/scripts/manage_wips.py index 95eba9df..acc55fe2 100644 --- a/scripts/manage_wips.py +++ b/scripts/manage_wips.py @@ -1,7 +1,9 @@ import optparse import toml -from objects.wip import WIP + +from blockchain.config import BlockchainConfig +from blockchain.objects.wip import WIP def main(): @@ -40,9 +42,9 @@ def main(): options, args = parser.parse_args() - config = toml.load(options.config_file) + BlockchainConfig.config = toml.load(options.config_file) - wip = WIP(config["database"], config["node-pool"]) + wip = WIP() if options.print: wip.print_wips() diff --git a/tests/conftest.py b/tests/conftest.py index c3798a4c..41d8840c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,17 @@ import json import pytest -import toml +from blockchain.config import BlockchainConfig +from blockchain.consensus_constants import ConsensusConstants +from blockchain.objects.wip import WIP -@pytest.fixture + +@pytest.fixture(autouse=True) def config(): - return toml.load(open("explorer.testnet.toml")) + BlockchainConfig.config = {"environment": {"network": "pytest"}} + BlockchainConfig.wip = WIP(mockup=True) + BlockchainConfig.consensus_constants = ConsensusConstants(mockup=True) @pytest.fixture diff --git a/util/database_manager.py b/util/database_manager.py index 77b3b06c..db9fb325 100644 --- a/util/database_manager.py +++ b/util/database_manager.py @@ -2,8 +2,12 @@ from psycopg.types.composite import CompositeInfo, register_composite import sys +from blockchain.config import BlockchainConfig + class DatabaseManager(object): - def __init__(self, config, named_cursor=False, logger=None, custom_types=[]): + def __init__(self, named_cursor=False, logger=None, custom_types=[]): + config = BlockchainConfig.config + self.db_user = config["database"]["user"] self.db_name = f"{config['database']['name']}_{config['environment']['network']}" self.db_pass = config["database"]["password"] diff --git a/util/database_pool.py b/util/database_pool.py index 0086702c..fbb7057a 100644 --- a/util/database_pool.py +++ b/util/database_pool.py @@ -4,8 +4,12 @@ import psycopg_pool from psycopg.types.composite import CompositeInfo, register_composite +from blockchain.config import BlockchainConfig + class DatabasePool(object): - def __init__(self, config, logger=None): + def __init__(self, logger=None): + config = BlockchainConfig.config + self.user = config["database"]["user"] self.database = f"{config['database']['name']}_{config['environment']['network']}" self.password = config["database"]["password"] From fbeaba3c6986edb08165efbd14b87af8c2b46c79 Mon Sep 17 00:00:00 2001 From: drcpu Date: Tue, 4 Feb 2025 22:30:51 +0100 Subject: [PATCH 05/83] [api] Modify API blueprints to use BlockchainConfig --- api/__init__.py | 27 +++++++++---------- api/blueprints/address/blocks_blueprint.py | 2 -- .../data_requests_created_blueprint.py | 2 -- .../address/data_requests_solved_blueprint.py | 2 -- api/blueprints/address/details_blueprint.py | 1 - api/blueprints/address/mints_blueprint.py | 2 -- .../address/value_transfers_blueprint.py | 1 - api/blueprints/misc/status_blueprint.py | 4 ++- .../network/blockchain_blueprint.py | 9 +++---- api/blueprints/network/mempool_blueprint.py | 3 ++- .../network/statistics_blueprint.py | 4 ++- api/blueprints/network/tapi_blueprint.py | 4 ++- api/blueprints/search/epoch_blueprint.py | 5 ++-- api/blueprints/search/hash_blueprint.py | 4 ++- .../transaction/mempool_blueprint.py | 4 ++- .../transaction/priority_blueprint.py | 4 ++- api/connect.py | 25 ++++++++--------- api/gunicorn_config.py | 2 -- api/wsgi.py | 14 +++++++++- mockups/config.py | 3 +++ tests/api/conftest.py | 3 ++- 21 files changed, 70 insertions(+), 55 deletions(-) diff --git a/api/__init__.py b/api/__init__.py index 66da97eb..e6b6740a 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -1,4 +1,3 @@ -import toml from flask import Flask from flask_smorest import Api, Blueprint @@ -38,12 +37,13 @@ create_database, create_witnet_node, ) -from api.gunicorn_config import toml_config -from mockups.config import mock_config +from blockchain.config import BlockchainConfig +from blockchain.consensus_constants import ConsensusConstants +from blockchain.objects.wip import WIP from util.logger import configure_rotating_logger -def create_app(mock=False): +def create_app(config, mockup=False): # Create app app = Flask(__name__) app.config["JSON_SORT_KEYS"] = False @@ -66,27 +66,26 @@ def create_app(mock=False): "show-header": "false", } - if not mock: - explorer_config = toml.load(toml_config) - else: - explorer_config = mock_config - app.config["explorer"] = explorer_config + # Create blockchain configuration object + BlockchainConfig.config = config + BlockchainConfig.wip = WIP(mockup=mockup) + BlockchainConfig.consensus_constants = ConsensusConstants(mockup=mockup) # Setup logger - log_file = explorer_config["api"]["log"]["log_file"] + log_file = BlockchainConfig.config["api"]["log"]["log_file"] app.extensions["logger"] = configure_rotating_logger("api", log_file, "info") # Create connections to external resources - address_caching_server = create_address_caching_server(explorer_config, mock=mock) + address_caching_server = create_address_caching_server(mockup=mockup) address_caching_server.init_app(app, "address_caching_server") - cache = create_cache(explorer_config, mock=mock) + cache = create_cache(mockup=mockup) cache.init_app(app) - database = create_database(explorer_config, mock=mock) + database = create_database(mockup=mockup) database.init_app(app) - witnet_node = create_witnet_node(explorer_config, mock=mock) + witnet_node = create_witnet_node(mockup=mockup) witnet_node.init_app(app) # Create top-level blueprints diff --git a/api/blueprints/address/blocks_blueprint.py b/api/blueprints/address/blocks_blueprint.py index 9bb6bf63..4c2f8764 100644 --- a/api/blueprints/address/blocks_blueprint.py +++ b/api/blueprints/address/blocks_blueprint.py @@ -45,7 +45,6 @@ class AddressBlocks(MethodView): def get(self, args, pagination_parameters): address_caching_server = current_app.extensions["address_caching_server"] cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] @@ -73,7 +72,6 @@ def get(self, args, pagination_parameters): ) address = Address( arg_address, - config, database=database, witnet_node=witnet_node, logger=logger, diff --git a/api/blueprints/address/data_requests_created_blueprint.py b/api/blueprints/address/data_requests_created_blueprint.py index f3995598..449b9f4b 100644 --- a/api/blueprints/address/data_requests_created_blueprint.py +++ b/api/blueprints/address/data_requests_created_blueprint.py @@ -45,7 +45,6 @@ class AddressDataRequestsCreated(MethodView): def get(self, args, pagination_parameters): address_caching_server = current_app.extensions["address_caching_server"] cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] @@ -79,7 +78,6 @@ def get(self, args, pagination_parameters): ) address = Address( arg_address, - config, database=database, witnet_node=witnet_node, logger=logger, diff --git a/api/blueprints/address/data_requests_solved_blueprint.py b/api/blueprints/address/data_requests_solved_blueprint.py index 48487d8b..ff66c5ec 100644 --- a/api/blueprints/address/data_requests_solved_blueprint.py +++ b/api/blueprints/address/data_requests_solved_blueprint.py @@ -45,7 +45,6 @@ class AddressDataRequestsSolved(MethodView): def get(self, args, pagination_parameters): address_caching_server = current_app.extensions["address_caching_server"] cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] @@ -75,7 +74,6 @@ def get(self, args, pagination_parameters): ) address = Address( arg_address, - config, database=database, witnet_node=witnet_node, logger=logger, diff --git a/api/blueprints/address/details_blueprint.py b/api/blueprints/address/details_blueprint.py index 35e335ba..e57c6815 100644 --- a/api/blueprints/address/details_blueprint.py +++ b/api/blueprints/address/details_blueprint.py @@ -43,7 +43,6 @@ class AddressDetails(MethodView): ) def get(self, args): address_caching_server = current_app.extensions["address_caching_server"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] diff --git a/api/blueprints/address/mints_blueprint.py b/api/blueprints/address/mints_blueprint.py index 55e23b4a..97a22438 100644 --- a/api/blueprints/address/mints_blueprint.py +++ b/api/blueprints/address/mints_blueprint.py @@ -45,7 +45,6 @@ class AddressMints(MethodView): def get(self, args, pagination_parameters): address_caching_server = current_app.extensions["address_caching_server"] cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] @@ -75,7 +74,6 @@ def get(self, args, pagination_parameters): ) address = Address( arg_address, - config, database=database, witnet_node=witnet_node, logger=logger, diff --git a/api/blueprints/address/value_transfers_blueprint.py b/api/blueprints/address/value_transfers_blueprint.py index b8e8d70f..5f99981b 100644 --- a/api/blueprints/address/value_transfers_blueprint.py +++ b/api/blueprints/address/value_transfers_blueprint.py @@ -45,7 +45,6 @@ class AddressValueTransfers(MethodView): def get(self, args, pagination_parameters): address_caching_server = current_app.extensions["address_caching_server"] cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] diff --git a/api/blueprints/misc/status_blueprint.py b/api/blueprints/misc/status_blueprint.py index 9f64cdc6..f87f040c 100644 --- a/api/blueprints/misc/status_blueprint.py +++ b/api/blueprints/misc/status_blueprint.py @@ -3,6 +3,7 @@ from flask_smorest import Blueprint, abort from marshmallow import ValidationError +from blockchain.config import BlockchainConfig from schemas.misc.abort_schema import AbortSchema from schemas.misc.status_schema import StatusResponse from schemas.misc.version_schema import VersionSchema @@ -41,11 +42,12 @@ class Status(MethodView): ) def get(self): cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] + config = BlockchainConfig.config + logger.info("status()") status = cache.get("status") diff --git a/api/blueprints/network/blockchain_blueprint.py b/api/blueprints/network/blockchain_blueprint.py index 7eda5537..662b95da 100644 --- a/api/blueprints/network/blockchain_blueprint.py +++ b/api/blueprints/network/blockchain_blueprint.py @@ -3,7 +3,7 @@ from flask_smorest import Blueprint, abort from marshmallow import ValidationError -from node.consensus_constants import ConsensusConstants +from blockchain.config import BlockchainConfig from schemas.misc.abort_schema import AbortSchema from schemas.misc.version_schema import VersionSchema from schemas.network.blockchain_schema import NetworkBlockchainResponse @@ -47,11 +47,12 @@ class NetworkBlockchain(MethodView): @network_blockchain_blueprint.paginate(page_size=50, max_page_size=1000) def get(self, pagination_parameters): cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] + config = BlockchainConfig.config + logger.info( f"network_blockchain({pagination_parameters.page}, {pagination_parameters.page_size})" ) @@ -66,10 +67,6 @@ def get(self, pagination_parameters): logger.info(f"Could not find {cache_key} in memcached cache") # Get the expected epoch - consensus_constants = ConsensusConstants( - database=database, - witnet_node=witnet_node, - ) expected_epoch = calculate_current_epoch( consensus_constants.checkpoint_zero_timestamp, consensus_constants.checkpoints_period, diff --git a/api/blueprints/network/mempool_blueprint.py b/api/blueprints/network/mempool_blueprint.py index 1050e9d5..0acf5ff2 100644 --- a/api/blueprints/network/mempool_blueprint.py +++ b/api/blueprints/network/mempool_blueprint.py @@ -7,6 +7,7 @@ from marshmallow import ValidationError from psycopg.sql import SQL, Identifier +from blockchain.config import BlockchainConfig from schemas.misc.abort_schema import AbortSchema from schemas.misc.version_schema import VersionSchema from schemas.network.mempool_schema import NetworkMempoolArgs, NetworkMempoolResponse @@ -53,7 +54,7 @@ def get(self, args): database = current_app.extensions["database"] logger = current_app.extensions["logger"] - config = current_app.config["explorer"] + config = BlockchainConfig.config # Use the last 24h if "start_epoch" not in args or "stop_epoch" not in args: diff --git a/api/blueprints/network/statistics_blueprint.py b/api/blueprints/network/statistics_blueprint.py index 92d46741..a886bb7b 100644 --- a/api/blueprints/network/statistics_blueprint.py +++ b/api/blueprints/network/statistics_blueprint.py @@ -4,6 +4,7 @@ from flask_smorest import Blueprint, abort from marshmallow import ValidationError +from blockchain.config import BlockchainConfig from caching.network_stats_functions import aggregate_nodes, read_from_database from schemas.misc.abort_schema import AbortSchema from schemas.misc.version_schema import VersionSchema @@ -64,10 +65,11 @@ class NetworkStatistics(MethodView): ) def get(self, args): cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] + config = BlockchainConfig.config + logger.info( f"network_statistics({args['key']}, {args.get('start_epoch', 0)}, {args.get('stop_epoch', 0)})" ) diff --git a/api/blueprints/network/tapi_blueprint.py b/api/blueprints/network/tapi_blueprint.py index dabb7f52..795445bd 100644 --- a/api/blueprints/network/tapi_blueprint.py +++ b/api/blueprints/network/tapi_blueprint.py @@ -8,6 +8,7 @@ from marshmallow import ValidationError from PIL import Image +from blockchain.config import BlockchainConfig from schemas.misc.abort_schema import AbortSchema from schemas.misc.version_schema import VersionSchema from schemas.network.tapi_schema import NetworkTapiArgs, NetworkTapiResponse @@ -46,10 +47,11 @@ class NetworkTapi(MethodView): ) def get(self, args): cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] + config = BlockchainConfig.config + logger.info(f"network_tapi({args['return_all']})") # Fetching known TAPI ids diff --git a/api/blueprints/search/epoch_blueprint.py b/api/blueprints/search/epoch_blueprint.py index e1b663b7..0abaf6c3 100644 --- a/api/blueprints/search/epoch_blueprint.py +++ b/api/blueprints/search/epoch_blueprint.py @@ -4,8 +4,8 @@ from flask_smorest import Blueprint, abort from marshmallow import ValidationError +from blockchain.config import BlockchainConfig from blockchain.objects.block import Block -from node.consensus_constants import ConsensusConstants from schemas.misc.abort_schema import AbortSchema from schemas.misc.version_schema import VersionSchema from schemas.search.epoch_schema import SearchEpochArgs, SearchEpochResponse @@ -44,11 +44,12 @@ class SearchEpoch(MethodView): ) def get(self, args): cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] + config = BlockchainConfig.config + epoch = args["value"] logger.info(f"search_epoch({epoch})") diff --git a/api/blueprints/search/hash_blueprint.py b/api/blueprints/search/hash_blueprint.py index 19734d27..f38fa944 100644 --- a/api/blueprints/search/hash_blueprint.py +++ b/api/blueprints/search/hash_blueprint.py @@ -4,6 +4,7 @@ from flask_smorest import Blueprint, abort from marshmallow import ValidationError +from blockchain.config import BlockchainConfig from blockchain.objects.block import Block from blockchain.objects.data_request_history import DataRequestHistory from blockchain.objects.data_request_report import DataRequestReport @@ -62,11 +63,12 @@ class SearchHash(MethodView): @search_hash_blueprint.paginate(page_size=50, max_page_size=1000) def get(self, args, pagination_parameters): cache = current_app.extensions["cache"] - config = current_app.config["explorer"] database = current_app.extensions["database"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] + config = BlockchainConfig.config + hash_value = args["value"] simple = args["simple"] logger.info(f"search_hash({hash_value}, {simple})") diff --git a/api/blueprints/transaction/mempool_blueprint.py b/api/blueprints/transaction/mempool_blueprint.py index 1ad3f6a6..6bf5a19e 100644 --- a/api/blueprints/transaction/mempool_blueprint.py +++ b/api/blueprints/transaction/mempool_blueprint.py @@ -4,6 +4,7 @@ from flask_smorest import Blueprint, abort from marshmallow import ValidationError +from blockchain.config import BlockchainConfig from schemas.misc.abort_schema import AbortSchema from schemas.misc.version_schema import VersionSchema from schemas.transaction.mempool_schema import ( @@ -45,10 +46,11 @@ class TransactionMempool(MethodView): ) def get(self, args): cache = current_app.extensions["cache"] - config = current_app.config["explorer"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] + config = BlockchainConfig.config + logger.info(f"network_mempool({args['type']})") mempool = cache.get("transaction_mempool") diff --git a/api/blueprints/transaction/priority_blueprint.py b/api/blueprints/transaction/priority_blueprint.py index e44cad63..eb808c1a 100644 --- a/api/blueprints/transaction/priority_blueprint.py +++ b/api/blueprints/transaction/priority_blueprint.py @@ -3,6 +3,7 @@ from flask_smorest import Blueprint, abort from marshmallow import ValidationError +from blockchain.config import BlockchainConfig from schemas.misc.abort_schema import AbortSchema from schemas.misc.version_schema import VersionSchema from schemas.transaction.priority_schema import ( @@ -44,10 +45,11 @@ class TransactionPriority(MethodView): ) def get(self, args): cache = current_app.extensions["cache"] - config = current_app.config["explorer"] logger = current_app.extensions["logger"] witnet_node = current_app.extensions["witnet_node"] + config = BlockchainConfig.config + priority_key = args["key"] logger.info(f"get_priority({priority_key})") diff --git a/api/connect.py b/api/connect.py index b31198d2..981ba4e6 100644 --- a/api/connect.py +++ b/api/connect.py @@ -1,3 +1,4 @@ +from blockchain.config import BlockchainConfig from mockups.cache import MockCache from mockups.database import MockDatabase from mockups.socket_manager import MockSocketManager @@ -8,11 +9,11 @@ from util.socket_manager import SocketManager -def create_address_caching_server(config, mock=False): - if mock: +def create_address_caching_server(mockup=False): + if mockup: address_caching_server = MockSocketManager() else: - caching_config = config["api"]["caching"] + caching_config = BlockchainConfig.config["api"]["caching"] address_caching_server = SocketManager( caching_config["scripts"]["addresses"]["host"], caching_config["scripts"]["addresses"]["port"], @@ -21,11 +22,11 @@ def create_address_caching_server(config, mock=False): return address_caching_server -def create_cache(config, mock=False): - if mock: +def create_cache(mockup=False): + if mockup: cache = MockCache() else: - caching_config = config["api"]["caching"] + caching_config = BlockchainConfig.config["api"]["caching"] cache = MemcachedPool( caching_config["server"].split(","), caching_config["threads"], @@ -34,17 +35,17 @@ def create_cache(config, mock=False): return cache -def create_database(config, mock=False): - if mock: +def create_database(mockup=False): + if mockup: database = MockDatabase() else: - database = DatabasePool(config["database"]) + database = DatabasePool() return database -def create_witnet_node(config, mock=False): - if mock: +def create_witnet_node(mockup=False): + if mockup: witnet_node = MockWitnetNode() else: - witnet_node = WitnetClientPool(config["node-pool"]) + witnet_node = WitnetClientPool() return witnet_node diff --git a/api/gunicorn_config.py b/api/gunicorn_config.py index 172c7ff9..3e756757 100644 --- a/api/gunicorn_config.py +++ b/api/gunicorn_config.py @@ -1,7 +1,5 @@ # Reference https://docs.gunicorn.org/en/latest/settings.html -toml_config = "explorer.toml" - # Server Socket bind = ["0.0.0.0:5000"] backlog = 2048 diff --git a/api/wsgi.py b/api/wsgi.py index 852a82b2..37ac71c6 100644 --- a/api/wsgi.py +++ b/api/wsgi.py @@ -1,3 +1,15 @@ +import sys + +import toml + from api import create_app -app = create_app() +config_file = sys.argv[-1] +assert config_file in ( + "explorer.toml", + "explorer.mainnet.toml", + "explorer.testnet.toml", +) +config = toml.load(config_file) + +app = create_app(config) diff --git a/mockups/config.py b/mockups/config.py index ea803ef3..41aedf25 100644 --- a/mockups/config.py +++ b/mockups/config.py @@ -1,4 +1,7 @@ mock_config = { + "environment": { + "network": "pytest", + }, "api": { "caching": { "views": { diff --git a/tests/api/conftest.py b/tests/api/conftest.py index b8e067d8..85b890f1 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -3,11 +3,12 @@ import pytest from api import create_app +from mockups.config import mock_config @pytest.fixture def client(): - app = create_app(mock=True) + app = create_app(mock_config, mockup=True) return app.test_client() From 23f9b795ad957edb1fdb754996fe96a205491ef5 Mon Sep 17 00:00:00 2001 From: drcpu Date: Wed, 5 Feb 2025 19:29:10 +0100 Subject: [PATCH 06/83] [blockchain] Read mockup WIP data from mockup database --- blockchain/objects/wip.py | 130 +++++++++++++------------------------- 1 file changed, 45 insertions(+), 85 deletions(-) diff --git a/blockchain/objects/wip.py b/blockchain/objects/wip.py index bb0d7ab1..cc5aa6ec 100644 --- a/blockchain/objects/wip.py +++ b/blockchain/objects/wip.py @@ -3,6 +3,8 @@ import toml +from mockups.database import MockDatabase +from mockups.witnet_node import MockWitnetNode from node.witnet_node import WitnetNode from util.database_manager import DatabaseManager @@ -16,6 +18,8 @@ def __init__( ): if database: self.db_mngr = database + elif mockup: + self.db_mngr = MockDatabase() else: self.db_mngr = DatabaseManager() self.fetch_wips() @@ -23,13 +27,11 @@ def __init__( self.witnet_node = None if witnet_node is not None: self.witnet_node = witnet_node + elif mockup: + self.witnet_node = MockWitnetNode() else: self.witnet_node = WitnetNode() - self.mockup = mockup - if self.mockup: - self.create_mockup() - def fetch_wips(self): sql = """ SELECT @@ -49,30 +51,6 @@ def fetch_wips(self): """ self.wips = self.db_mngr.sql_return_all(sql) - def create_mockup(self): - wips = [ - { - "id": 1, - "title": "WIP0014-0016", - "activation_epoch": 549141, - }, - { - "id": 2, - "title": "WIP0017-0018-0019", - "activation_epoch": 683541, - }, - { - "id": 3, - "title": "WIP0020-0021", - "activation_epoch": 1059861, - }, - ] - try: - self.set_mockup(wips) - except KeyError as e: - sys.stderr.write(f"Could not set mockup: {e}") - sys.exit(1) - def set_mockup(self, wips): # Check the passed parameter has the correct type if not isinstance(wips, list): @@ -93,40 +71,32 @@ def set_mockup(self, wips): def print_wips(self): for wip in self.wips: - if self.mockup: - wip_id, title, activation_epoch = wip - else: - ( - wip_id, - title, - description, - urls, - activation_epoch, - tapi_start_epoch, - tapi_stop_epoch, - tapi_bit, - ) = wip + ( + wip_id, + title, + description, + urls, + activation_epoch, + tapi_start_epoch, + tapi_stop_epoch, + tapi_bit, + ) = wip print(f"Entry {wip_id}") print(f"\tTitle: {title}") - if not self.mockup: - print(f"\tDescription: {description}") - for counter, url in enumerate(urls): - print(f"\tURL of WIP {counter + 1}: {url}") + print(f"\tDescription: {description}") + for counter, url in enumerate(urls): + print(f"\tURL of WIP {counter + 1}: {url}") print( f"\tActivation epoch: {activation_epoch if activation_epoch else 'not activated'}" ) - if not self.mockup: - if tapi_start_epoch: - print(f"\tStarted at epoch: {tapi_start_epoch}") - if tapi_stop_epoch: - print(f"\tStopped at epoch: {tapi_stop_epoch}") - if tapi_bit: - print(f"\tUsing signaling bit: {tapi_bit}") + if tapi_start_epoch: + print(f"\tStarted at epoch: {tapi_start_epoch}") + if tapi_stop_epoch: + print(f"\tStopped at epoch: {tapi_stop_epoch}") + if tapi_bit: + print(f"\tUsing signaling bit: {tapi_bit}") def add_wip(self): - if self.mockup: - raise TypeError("Cannot add a WIP on a mockup") - # Read the WIP title wip_title = input("Specify the title of the WIP? ") @@ -227,10 +197,6 @@ def add_wip(self): ) def process_tapi(self): - if self.mockup: - sys.stderr.write("Cannot process TAPI signals on a mockup\n") - return - for wip in self.wips: ( wip_id, @@ -300,19 +266,16 @@ def process_tapi(self): def get_activation_epoch(self, wip_title): # Find TAPI of interest based on its title for wip in self.wips: - if self.mockup: - wip_id, title, activation_epoch = wip - else: - ( - wip_id, - title, - description, - urls, - activation_epoch, - tapi_start_epoch, - tapi_stop_epoch, - tapi_bit, - ) = wip + ( + wip_id, + title, + description, + urls, + activation_epoch, + tapi_start_epoch, + tapi_stop_epoch, + tapi_bit, + ) = wip if wip_title == title: return activation_epoch return None @@ -320,19 +283,16 @@ def get_activation_epoch(self, wip_title): def is_wip_active(self, epoch, wip_title): # Find TAPI of interest based on its title for wip in self.wips: - if self.mockup: - wip_id, title, activation_epoch = wip - else: - ( - wip_id, - title, - description, - urls, - activation_epoch, - tapi_start_epoch, - tapi_stop_epoch, - tapi_bit, - ) = wip + ( + wip_id, + title, + description, + urls, + activation_epoch, + tapi_start_epoch, + tapi_stop_epoch, + tapi_bit, + ) = wip if wip_title == title: if activation_epoch and epoch >= activation_epoch: return True From b7fa2044c6c0d233d56974718f90f802ccd3550f Mon Sep 17 00:00:00 2001 From: drcpu Date: Wed, 5 Feb 2025 20:14:00 +0100 Subject: [PATCH 07/83] [wip] Add functions to check activation of WIP0028 and wit/2 --- blockchain/objects/wip.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/blockchain/objects/wip.py b/blockchain/objects/wip.py index cc5aa6ec..f1910762 100644 --- a/blockchain/objects/wip.py +++ b/blockchain/objects/wip.py @@ -352,6 +352,12 @@ def is_wip0026_active(self, epoch): def is_wip0027_active(self, epoch): return self.is_wip_active(epoch, wip_title="WIP0027") + def is_wip0028_active(self, epoch): + return self.is_wip_active(epoch, wip_title="WIP0028") + + def is_wit2_active(self, epoch): + return self.is_wip_active(epoch, wip_title="wit/2") + def main(): parser = optparse.OptionParser() From 1fc9b8beb9890aadf4a4256e0d30b9738bf6ea51 Mon Sep 17 00:00:00 2001 From: drcpu Date: Wed, 5 Feb 2025 21:13:15 +0100 Subject: [PATCH 08/83] [blockchain] Move consensus constants to blockchain module --- api/blueprints/search/hash_blueprint.py | 1 - {node => blockchain}/consensus_constants.py | 6 +++++- blockchain/explorer.py | 2 +- blockchain/objects/address.py | 2 +- caching/client.py | 2 +- scripts/add_blocks.py | 2 +- tests/blockchain/conftest.py | 2 +- 7 files changed, 10 insertions(+), 7 deletions(-) rename {node => blockchain}/consensus_constants.py (98%) diff --git a/api/blueprints/search/hash_blueprint.py b/api/blueprints/search/hash_blueprint.py index f38fa944..0c426145 100644 --- a/api/blueprints/search/hash_blueprint.py +++ b/api/blueprints/search/hash_blueprint.py @@ -14,7 +14,6 @@ from blockchain.transactions.reveal import Reveal from blockchain.transactions.tally import Tally from blockchain.transactions.value_transfer import ValueTransfer -from node.consensus_constants import ConsensusConstants from schemas.misc.abort_schema import AbortSchema from schemas.misc.version_schema import VersionSchema from schemas.search.hash_schema import SearchHashArgs, SearchHashResponse diff --git a/node/consensus_constants.py b/blockchain/consensus_constants.py similarity index 98% rename from node/consensus_constants.py rename to blockchain/consensus_constants.py index a9081b8e..e22b86d6 100644 --- a/node/consensus_constants.py +++ b/blockchain/consensus_constants.py @@ -11,7 +11,7 @@ def __init__( witnet_node=None, error_retry=0, mock=False, - mock_parameters={}, + mock_parameters=None, ): if not mock: # First try to fetch the consensus constants from the database @@ -101,6 +101,10 @@ def __init__( "superblock_signing_committee_size" ] else: + assert ( + isinstance(mock_parameters) is dict + ), "Expected a dictionary with mock parameters" + if "activity_period" in mock_parameters: self.activity_period = mock_parameters["activity_period"] if "bootstrap_hash" in mock_parameters: diff --git a/blockchain/explorer.py b/blockchain/explorer.py index cb0dc20f..b5b91b49 100644 --- a/blockchain/explorer.py +++ b/blockchain/explorer.py @@ -16,12 +16,12 @@ import toml from blockchain.config import BlockchainConfig +from blockchain.consensus_constants import ConsensusConstants from blockchain.objects.block import Block from blockchain.objects.wip import WIP from blockchain.transactions.data_request import DataRequest from blockchain.transactions.value_transfer import ValueTransfer from blockchain.witnet_database import WitnetDatabase -from node.consensus_constants import ConsensusConstants from node.witnet_node import WitnetNode from util.common_functions import calculate_current_epoch from util.common_sql import sql_last_confirmed_block diff --git a/blockchain/objects/address.py b/blockchain/objects/address.py index 5458e44d..65ff1424 100644 --- a/blockchain/objects/address.py +++ b/blockchain/objects/address.py @@ -3,9 +3,9 @@ from psycopg.sql import SQL, Literal from blockchain.config import BlockchainConfig +from blockchain.consensus_constants import ConsensusConstants from blockchain.transactions.reveal import translate_reveal from blockchain.transactions.tally import translate_tally -from node.consensus_constants import ConsensusConstants from node.witnet_node import WitnetNode from util.common_functions import calculate_block_reward from util.data_transformer import re_sql diff --git a/caching/client.py b/caching/client.py index a817ad81..4e91508b 100644 --- a/caching/client.py +++ b/caching/client.py @@ -2,7 +2,7 @@ import pylibmc import sys -from node.consensus_constants import ConsensusConstants +from blockchain.consensus_constants import ConsensusConstants from node.witnet_node import WitnetNode from util.socket_manager import SocketManager from util.database_manager import DatabaseManager diff --git a/scripts/add_blocks.py b/scripts/add_blocks.py index 3bdf210b..00a4a8a0 100644 --- a/scripts/add_blocks.py +++ b/scripts/add_blocks.py @@ -4,10 +4,10 @@ import toml from blockchain.config import BlockchainConfig +from blockchain.consensus_constants import ConsensusConstants from blockchain.objects.block import Block from blockchain.objects.wip import WIP from blockchain.witnet_database import WitnetDatabase -from node.consensus_constants import ConsensusConstants from node.witnet_node import WitnetNode from util.database_manager import DatabaseManager diff --git a/tests/blockchain/conftest.py b/tests/blockchain/conftest.py index ae544ec7..a32343ed 100644 --- a/tests/blockchain/conftest.py +++ b/tests/blockchain/conftest.py @@ -1,8 +1,8 @@ import pytest +from blockchain.consensus_constants import ConsensusConstants from mockups.database import MockDatabase from mockups.witnet_node import MockWitnetNode -from node.consensus_constants import ConsensusConstants @pytest.fixture From 58fdfebd9a3c0fb661a42a130289264f64fe92ea Mon Sep 17 00:00:00 2001 From: drcpu Date: Wed, 5 Feb 2025 21:19:51 +0100 Subject: [PATCH 09/83] [blockchain] Fix comparison bug in consensus constants --- blockchain/consensus_constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blockchain/consensus_constants.py b/blockchain/consensus_constants.py index e22b86d6..8dd42155 100644 --- a/blockchain/consensus_constants.py +++ b/blockchain/consensus_constants.py @@ -46,11 +46,11 @@ def __init__( else: consensus_constants = {} for key, int_val, str_val in fetched_consensus_constants: - if int_val: + if int_val is not None: if key == "reputation_penalization_factor": int_val = int_val / 100 consensus_constants[key] = int_val - if str_val: + if str_val is not None: if key == "bootstrap_hash" or key == "genesis_hash": str_val = str_val[0] consensus_constants[key] = str_val From cfcae1987beaf24fc92ea3e95887646c1f7e8c29 Mon Sep 17 00:00:00 2001 From: drcpu Date: Wed, 5 Feb 2025 22:34:34 +0100 Subject: [PATCH 10/83] [schemas] Add validation exception for genesis block --- schemas/component/mint_schema.py | 3 ++- schemas/include/input_utxo_schema.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/schemas/component/mint_schema.py b/schemas/component/mint_schema.py index 599d4bd0..92967415 100644 --- a/schemas/component/mint_schema.py +++ b/schemas/component/mint_schema.py @@ -14,7 +14,8 @@ class MintTransaction(Schema): @validates_schema def validate_inputs(self, args, **kwargs): errors = {} - if len(args["output_addresses"]) < 1: + # Epoch is not defined here yet, but will be in the inheriting classes + if args["epoch"] > 0 and len(args["output_addresses"]) < 1: errors["output_addresses"] = "Need at least one output address." if len(args["output_addresses"]) != len(args["output_values"]): errors["output_values"] = ( diff --git a/schemas/include/input_utxo_schema.py b/schemas/include/input_utxo_schema.py index 9a1b724c..b1f0ce38 100644 --- a/schemas/include/input_utxo_schema.py +++ b/schemas/include/input_utxo_schema.py @@ -65,5 +65,6 @@ def transform_hashes_after_load(self, args, **kwargs): @validates_schema def validate_inputs(self, args, **kwargs): - if len(args["input_utxos"]) < 1: + # Epoch is not defined here yet, but will be in the inheriting classes + if ("epoch" not in args or args["epoch"] > 0) and len(args["input_utxos"]) < 1: raise ValidationError({"input_utxos": "Need at least one input UTXO."}) From edc26868b700329268b85ba9d34b6d26066687a2 Mon Sep 17 00:00:00 2001 From: drcpu Date: Thu, 6 Feb 2025 21:26:20 +0100 Subject: [PATCH 11/83] [database] Set autocommit to true to properly create database --- create_database.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/create_database.py b/create_database.py index e1c695b9..3213688e 100644 --- a/create_database.py +++ b/create_database.py @@ -52,6 +52,7 @@ def create_user(user, password): def create_database(name, user): connection, cursor = connect_to_database("postgres") + connection.autocommit = True cursor.execute(f"SELECT 1 FROM pg_catalog.pg_database WHERE datname='{name}'") result = cursor.fetchone() if not result: @@ -59,6 +60,8 @@ def create_database(name, user): print(f"Created database '{name}'") else: print(f"Database '{name}' already exists") + cursor.close() + connection.close() def connect_to_database(name, user="", password=""): From a141e32159975deccac068a56a917b6892d692a5 Mon Sep 17 00:00:00 2001 From: drcpu Date: Thu, 6 Feb 2025 21:25:13 +0100 Subject: [PATCH 12/83] [database] Add HTTP-HEAD retrieve kind --- create_database.py | 1 + 1 file changed, 1 insertion(+) diff --git a/create_database.py b/create_database.py index 3213688e..9dafaa6c 100644 --- a/create_database.py +++ b/create_database.py @@ -118,6 +118,7 @@ def create_enums(connection, cursor): CREATE TYPE retrieve_kind AS ENUM ( 'Unknown', 'HTTP-GET', + 'HTTP-HEAD', 'HTTP-POST', 'RNG' ); From be0b84c77323c3fb03d97a1f7717f0f5f9cc8a11 Mon Sep 17 00:00:00 2001 From: drcpu Date: Thu, 6 Feb 2025 21:25:52 +0100 Subject: [PATCH 13/83] [database] Add tables and indexes for stake and unstake transactions --- create_database.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/create_database.py b/create_database.py index 9dafaa6c..ade7546e 100644 --- a/create_database.py +++ b/create_database.py @@ -106,7 +106,9 @@ def create_enums(connection, cursor): 'DRO_bytes_hash', 'commit_txn', 'reveal_txn', - 'tally_txn' + 'tally_txn', + 'stake_txn', + 'unstake_txn' ); END IF; END @@ -211,8 +213,12 @@ def create_tables(config, connection, cursor): commit SMALLINT NOT NULL, reveal SMALLINT NOT NULL, tally SMALLINT NOT NULL, + stake SMALLINT NOT NULL, + unstake SMALLINT NOT NULL, dr_weight INT NOT NULL, vt_weight INT NOT NULL, + st_weight INT NOT NULL, + ut_weight INT NOT NULL, block_weight INT NOT NULL, epoch INT NOT NULL, tapi_signals INT, @@ -291,6 +297,29 @@ def create_tables(config, connection, cursor): success BOOL NOT NULL, epoch INT NOT NULL );""", + """CREATE TABLE IF NOT EXISTS stake_txns ( + txn_hash BYTEA PRIMARY KEY, + input_addresses CHAR(42) ARRAY NOT NULL, + input_values BIGINT ARRAY NOT NULL, + input_utxos utxo ARRAY NOT NULL, + output_address CHAR(42), + output_value BIGINT, + weight INT NOT NULL, + validator CHAR(42) NOT NULL, + withdrawer CHAR(42) NOT NULL, + stake_value BIGINT NOT NULL, + epoch INT NOT NULL + );""", + """CREATE TABLE IF NOT EXISTS unstake_txns ( + txn_hash BYTEA PRIMARY KEY, + validator CHAR(42) NOT NULL, + withdrawer CHAR(42) NOT NULL, + output_value BIGINT NOT NULL, + fee BIGINT NOT NULL, + nonce BIGINT NOT NULL, + weight INT NOT NULL, + epoch INT NOT NULL + );""", """CREATE TABLE IF NOT EXISTS data_request_mempool ( timestamp INT NOT NULL, fee BIGINT ARRAY NOT NULL, @@ -369,6 +398,12 @@ def create_indexes(connection, cursor): "CREATE INDEX IF NOT EXISTS idx_reveal_txn_epoch ON reveal_txns (epoch);", "CREATE INDEX IF NOT EXISTS idx_tally_txn_epoch ON tally_txns (epoch);", "CREATE INDEX IF NOT EXISTS idx_value_transfer_txn_epoch ON value_transfer_txns (epoch);", + "CREATE INDEX IF NOT EXISTS idx_stake_txn_epoch ON stake_txns (epoch);", + "CREATE INDEX IF NOT EXISTS idx_stake_txn_validator ON stake_txns USING HASH (validator);", + "CREATE INDEX IF NOT EXISTS idx_stake_txn_withdrawer ON stake_txns USING HASH (withdrawer);", + "CREATE INDEX IF NOT EXISTS idx_unstake_txn_epoch ON unstake_txns (epoch);", + "CREATE INDEX IF NOT EXISTS idx_stake_txn_validator ON unstake_txns USING HASH (validator);", + "CREATE INDEX IF NOT EXISTS idx_stake_txn_withdrawer ON unstake_txns USING HASH (withdrawer);", ] for index in indexes: From a1f85c94742aeacc55812d9246a99f4b23879337 Mon Sep 17 00:00:00 2001 From: drcpu Date: Sat, 8 Feb 2025 11:01:15 +0100 Subject: [PATCH 14/83] [blockchain] Calculate and store total transactions fees for a block in the database --- blockchain/objects/block.py | 21 +++++++++++++++++++ blockchain/transactions/commit.py | 13 +++++++++++- blockchain/transactions/data_request.py | 20 ++++++++++++++++++ blockchain/transactions/reveal.py | 14 +++++++++++++ blockchain/witnet_database.py | 4 +++- create_database.py | 1 + schemas/component/block_schema.py | 1 + schemas/component/commit_schema.py | 1 + schemas/component/reveal_schema.py | 4 +++- tests/schemas/component/test_block_schema.py | 4 +++- tests/schemas/component/test_commit_schema.py | 7 +++++-- tests/schemas/component/test_reveal_schema.py | 8 +++++-- 12 files changed, 90 insertions(+), 8 deletions(-) diff --git a/blockchain/objects/block.py b/blockchain/objects/block.py index 292f14a1..23866c02 100644 --- a/blockchain/objects/block.py +++ b/blockchain/objects/block.py @@ -146,6 +146,8 @@ def process_block(self, call_from): }, } + self.block_json["details"]["txns_fees"] = self.calculate_txns_fees() + if call_from == "explorer": self.block_json["tapi"] = self.process_tapi_signals() return BlockForExplorer().load(self.block_json) @@ -301,6 +303,25 @@ def process_tally_txns(self, call_from): tally_transactions.append(tally.process_transaction(call_from)) return tally_transactions + def calculate_txns_fees(self): + txns_fees = 0 + + transactions = self.block_json["transactions"] + for value_transfer in transactions["value_transfer"]: + txns_fees += value_transfer["fee"] + for data_request in transactions["data_request"]: + txns_fees += data_request["miner_fee"] + for commit in transactions["commit"]: + txns_fees += commit["fee"] + for reveal in transactions["reveal"]: + txns_fees += reveal["fee"] + for stake in transactions["stake"]: + txns_fees += stake["fee"] + for unstake in transactions["unstake"]: + txns_fees += unstake["fee"] + + return txns_fees + def process_tapi_signals(self): is_tapi = False if self.tapi_periods: diff --git a/blockchain/transactions/commit.py b/blockchain/transactions/commit.py index 7cc1a637..eb39e0d0 100644 --- a/blockchain/transactions/commit.py +++ b/blockchain/transactions/commit.py @@ -24,7 +24,18 @@ def process_transaction(self, call_from): input_utxos, input_values = self.get_inputs(self.json_txn["body"]["collateral"]) _, output_values, _ = self.get_outputs(self.json_txn["body"]["outputs"]) - self.txn_details["collateral"] = sum(input_values) - sum(output_values) + # Get the reward for the miner of this transaction + fee = DataRequest().get_commit_and_reveal_fee_for_data_request( + self.txn_details["data_request"] + ) + if "fee" in fee: + self.txn_details["fee"] = fee["fee"] + else: + hash_value = self.txn_details["hash"] + data_request = self.txn_details["data_request"] + raise Exception( + f"Could not find fee for commit transaction {hash_value} for data request {data_request}" + ) if call_from == "explorer": self.txn_details["input_utxos"] = input_utxos diff --git a/blockchain/transactions/data_request.py b/blockchain/transactions/data_request.py index 6d9e414f..c2ffddb5 100644 --- a/blockchain/transactions/data_request.py +++ b/blockchain/transactions/data_request.py @@ -359,6 +359,26 @@ def get_transaction_from_database(self, data_request_hash): else: return {"error": "transaction not found"} + def get_commit_and_reveal_fee_for_data_request(self, data_request_hash): + sql = """ + SELECT + data_request_txns.commit_and_reveal_fee + FROM + data_request_txns + WHERE + data_request_txns.txn_hash=%s + LIMIT 1 + """ + result = self.database.sql_return_one( + sql, + parameters=[bytearray.fromhex(data_request_hash)], + ) + + if result: + return {"fee": result[0]} + else: + return {"error": "transaction not found"} + def calculate_fees( self, witnesses, diff --git a/blockchain/transactions/reveal.py b/blockchain/transactions/reveal.py index ae90f0ad..c4d314ba 100644 --- a/blockchain/transactions/reveal.py +++ b/blockchain/transactions/reveal.py @@ -2,6 +2,7 @@ import cbor2 +from blockchain.transactions.data_request import DataRequest from blockchain.transactions.transaction import Transaction from schemas.component.reveal_schema import ( RevealTransactionForApi, @@ -21,6 +22,19 @@ def process_transaction(self, call_from): # Data request transaction hash self.txn_details["data_request"] = self.json_txn["body"]["dr_pointer"] + # Get the reward for the miner of this transaction + fee = DataRequest().get_commit_and_reveal_fee_for_data_request( + self.txn_details["data_request"] + ) + if "fee" in fee: + self.txn_details["fee"] = fee["fee"] + else: + hash_value = self.txn_details["hash"] + data_request = self.txn_details["data_request"] + raise Exception( + f"Could not find fee for reveal transaction {hash_value} for data request {data_request}" + ) + # Translate revealed value success, reveal_translation = translate_reveal( self.txn_hash, self.json_txn["body"]["reveal"] diff --git a/blockchain/witnet_database.py b/blockchain/witnet_database.py index cbf6678c..2529d68b 100644 --- a/blockchain/witnet_database.py +++ b/blockchain/witnet_database.py @@ -71,6 +71,7 @@ def insert_block(self, block_json): block_json["details"]["data_request_weight"], block_json["details"]["value_transfer_weight"], block_json["details"]["weight"], + block_json["details"]["txns_fees"], block_json["details"]["epoch"], block_json["tapi"], block_json["details"]["confirmed"], @@ -324,10 +325,11 @@ def finalize_insert(self, epoch): dr_weight, vt_weight, block_weight, + txns_fees, epoch, tapi_signals, confirmed - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT ON CONSTRAINT blocks_pkey DO UPDATE SET diff --git a/create_database.py b/create_database.py index ade7546e..86d2d067 100644 --- a/create_database.py +++ b/create_database.py @@ -220,6 +220,7 @@ def create_tables(config, connection, cursor): st_weight INT NOT NULL, ut_weight INT NOT NULL, block_weight INT NOT NULL, + txns_fees BIGINT NOT NULL, epoch INT NOT NULL, tapi_signals INT, confirmed BOOLEAN NOT NULL, diff --git a/schemas/component/block_schema.py b/schemas/component/block_schema.py index 9f6acf9a..80ef51ec 100644 --- a/schemas/component/block_schema.py +++ b/schemas/component/block_schema.py @@ -41,6 +41,7 @@ class BlockDetails(TimestampComponent): required=True, ) weight = fields.Integer(validate=validate.Range(min=0), required=True) + txns_fees = fields.Integer(validate=validate.Range(min=0), required=True) confirmed = fields.Boolean(required=True) reverted = fields.Boolean(required=True) diff --git a/schemas/component/commit_schema.py b/schemas/component/commit_schema.py index d1924c54..fa4c64a1 100644 --- a/schemas/component/commit_schema.py +++ b/schemas/component/commit_schema.py @@ -14,6 +14,7 @@ class CommitTransactionForApi(BaseApiTransaction, AddressSchema): class CommitTransactionForBlock(BaseTransaction, AddressSchema): collateral = fields.Int(validate=validate.Range(min=1e9), required=True) data_request = fields.Str(validate=is_valid_hash, required=True) + fee = fields.Int(validate=validate.Range(min=0), required=True) class CommitTransactionForDataRequest(BaseApiTransaction, AddressSchema): diff --git a/schemas/component/reveal_schema.py b/schemas/component/reveal_schema.py index 23d1e454..2438dd10 100644 --- a/schemas/component/reveal_schema.py +++ b/schemas/component/reveal_schema.py @@ -1,4 +1,4 @@ -from marshmallow import fields, pre_load +from marshmallow import fields, pre_load, validate from schemas.include.address_schema import AddressSchema from schemas.include.base_transaction_schema import BaseApiTransaction, BaseTransaction @@ -13,6 +13,7 @@ class RevealTransactionForApi(BaseApiTransaction, AddressSchema): class RevealTransactionForBlock(BaseTransaction, AddressSchema): data_request = fields.Str(validate=is_valid_hash, required=True) + fee = fields.Int(validate=validate.Range(min=0), required=True) reveal = fields.String(required=True) success = fields.Boolean(required=True) @@ -45,5 +46,6 @@ def remove_leading_0x(self, args, **kwargs): class RevealTransactionForExplorer(BaseTransaction, AddressSchema): data_request = fields.Str(validate=is_valid_hash, required=True) + fee = fields.Int(validate=validate.Range(min=0), required=True) reveal = BytearrayField(required=True) success = fields.Boolean(required=True) diff --git a/tests/schemas/component/test_block_schema.py b/tests/schemas/component/test_block_schema.py index 017567af..98c4db6f 100644 --- a/tests/schemas/component/test_block_schema.py +++ b/tests/schemas/component/test_block_schema.py @@ -19,6 +19,7 @@ def block_details(): "data_request_weight": 1000, "value_transfer_weight": 1000, "weight": 0, + "txns_fees": 10, "confirmed": True, "reverted": False, } @@ -41,7 +42,7 @@ def test_block_details_failure_missing(): data = {} with pytest.raises(ValidationError) as err_info: BlockDetails().load(data) - assert len(err_info.value.messages) == 8 + assert len(err_info.value.messages) == 9 assert err_info.value.messages["hash"][0] == "Missing data for required field." assert err_info.value.messages["epoch"][0] == "Missing data for required field." assert err_info.value.messages["timestamp"][0] == "Missing data for required field." @@ -54,6 +55,7 @@ def test_block_details_failure_missing(): == "Missing data for required field." ) assert err_info.value.messages["weight"][0] == "Missing data for required field." + assert err_info.value.messages["txns_fees"][0] == "Missing data for required field." assert err_info.value.messages["confirmed"][0] == "Missing data for required field." assert err_info.value.messages["reverted"][0] == "Missing data for required field." diff --git a/tests/schemas/component/test_commit_schema.py b/tests/schemas/component/test_commit_schema.py index 9a5c41ac..fee3365c 100644 --- a/tests/schemas/component/test_commit_schema.py +++ b/tests/schemas/component/test_commit_schema.py @@ -62,6 +62,7 @@ def commit_transaction_for_block(): "address": "wit100000000000000000000000000000000r0v4g2", "collateral": 1e10, "data_request": "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef0123456789", + "fee": 1, } @@ -84,7 +85,7 @@ def test_commit_transaction_for_block_failure_missing(): data = {} with pytest.raises(ValidationError) as err_info: CommitTransactionForBlock().load(data) - assert len(err_info.value.messages) == 5 + assert len(err_info.value.messages) == 6 assert err_info.value.messages["hash"][0] == "Missing data for required field." assert err_info.value.messages["epoch"][0] == "Missing data for required field." assert err_info.value.messages["address"][0] == "Missing data for required field." @@ -94,6 +95,7 @@ def test_commit_transaction_for_block_failure_missing(): assert ( err_info.value.messages["data_request"][0] == "Missing data for required field." ) + assert err_info.value.messages["fee"][0] == "Missing data for required field." @pytest.fixture @@ -184,7 +186,7 @@ def test_commit_transaction_for_explorer_failure_missing(): data = {} with pytest.raises(ValidationError) as err_info: CommitTransactionForExplorer().load(data) - assert len(err_info.value.messages) == 8 + assert len(err_info.value.messages) == 9 assert err_info.value.messages["hash"][0] == "Missing data for required field." assert err_info.value.messages["epoch"][0] == "Missing data for required field." assert err_info.value.messages["address"][0] == "Missing data for required field." @@ -194,6 +196,7 @@ def test_commit_transaction_for_explorer_failure_missing(): assert ( err_info.value.messages["data_request"][0] == "Missing data for required field." ) + assert err_info.value.messages["fee"][0] == "Missing data for required field." assert ( err_info.value.messages["input_utxos"][0] == "Missing data for required field." ) diff --git a/tests/schemas/component/test_reveal_schema.py b/tests/schemas/component/test_reveal_schema.py index 9e1cd1c2..db75fa34 100644 --- a/tests/schemas/component/test_reveal_schema.py +++ b/tests/schemas/component/test_reveal_schema.py @@ -51,6 +51,7 @@ def reveal_transaction_for_block(): "epoch": 1, "address": "wit100000000000000000000000000000000r0v4g2", "data_request": "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef0123456789", + "fee": 1, "reveal": "124205", "success": True, } @@ -75,13 +76,14 @@ def test_reveal_transaction_for_block_failure_missing(): data = {} with pytest.raises(ValidationError) as err_info: RevealTransactionForBlock().load(data) - assert len(err_info.value.messages) == 6 + assert len(err_info.value.messages) == 7 assert err_info.value.messages["hash"][0] == "Missing data for required field." assert err_info.value.messages["epoch"][0] == "Missing data for required field." assert err_info.value.messages["address"][0] == "Missing data for required field." assert ( err_info.value.messages["data_request"][0] == "Missing data for required field." ) + assert err_info.value.messages["fee"][0] == "Missing data for required field." assert err_info.value.messages["reveal"][0] == "Missing data for required field." assert err_info.value.messages["success"][0] == "Missing data for required field." @@ -142,6 +144,7 @@ def reveal_transaction_for_explorer(): "epoch": 1, "address": "wit100000000000000000000000000000000r0v4g2", "data_request": "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef0123456789", + "fee": 1, "reveal": bytearray([26, 0, 1, 229, 45]), "success": True, } @@ -175,12 +178,13 @@ def test_reveal_transaction_for_explorer_failure_missing(): data = {} with pytest.raises(ValidationError) as err_info: RevealTransactionForExplorer().load(data) - assert len(err_info.value.messages) == 6 + assert len(err_info.value.messages) == 7 assert err_info.value.messages["hash"][0] == "Missing data for required field." assert err_info.value.messages["epoch"][0] == "Missing data for required field." assert err_info.value.messages["address"][0] == "Missing data for required field." assert ( err_info.value.messages["data_request"][0] == "Missing data for required field." ) + assert err_info.value.messages["fee"][0] == "Missing data for required field." assert err_info.value.messages["reveal"][0] == "Missing data for required field." assert err_info.value.messages["success"][0] == "Missing data for required field." From d82159a30670b9ae11dc18615aea774a3520ccfa Mon Sep 17 00:00:00 2001 From: drcpu Date: Sun, 9 Feb 2025 15:58:38 +0100 Subject: [PATCH 15/83] [database] Rework mockup database to use testnet data and move its creation script to the module top level --- create_mockup_database.py | 2933 ++++++++++++++++++++++++ mockups/data/create_mockup_database.py | 2206 ------------------ mockups/data/database.sqlite3 | Bin 4583424 -> 1728512 bytes requirements/tests.txt | 1 + 4 files changed, 2934 insertions(+), 2206 deletions(-) create mode 100644 create_mockup_database.py delete mode 100644 mockups/data/create_mockup_database.py diff --git a/create_mockup_database.py b/create_mockup_database.py new file mode 100644 index 00000000..fe27b851 --- /dev/null +++ b/create_mockup_database.py @@ -0,0 +1,2933 @@ +import argparse +import json +import os +import sqlite3 + +import toml +import tqdm + +from blockchain.config import BlockchainConfig +from blockchain.objects.wip import WIP +from blockchain.transactions.reveal import translate_reveal +from blockchain.transactions.tally import translate_tally +from node.witnet_node import WitnetNode +from util.address_generator import AddressGenerator +from util.data_transformer import bytes2hex +from util.protobuf_encoder import ProtobufEncoder + + +def create_tables(database): + connection = sqlite3.connect(database) + cursor = connection.cursor() + tables = [ + """ + CREATE TABLE IF NOT EXISTS addresses ( + id INT, + address TEXT PRIMARY KEY, + label TEXT, + active INT, + block INT, + mint INT, + value_transfer INT, + data_request INT, + 'commit' INT, + reveal INT, + tally INT + ) + """, + """ + CREATE TABLE IF NOT EXISTS hashes ( + hash TEXT PRIMARY KEY, + type TEXT NOT NULL, + epoch INT + ) + """, + """ + CREATE TABLE IF NOT EXISTS blocks ( + block_hash TEXT PRIMARY KEY, + value_transfer INT NOT NULL, + data_request INT NOT NULL, + 'commit' INT NOT NULL, + reveal INT NOT NULL, + tally INT NOT NULL, + stake INT NOT NULL, + unstake INT NOT NULL, + dr_weight INT NOT NULL, + vt_weight INT NOT NULL, + st_weight INT NOT NULL, + ut_weight INT NOT NULL, + block_weight INT NOT NULL, + txns_fees INT NOT NULL, + epoch INT NOT NULL, + tapi_signals INT, + confirmed TEXT NOT NULL, + reverted TEXT + ) + """, + """ + CREATE TABLE IF NOT EXISTS mint_txns ( + txn_hash TEXT PRIMARY KEY, + miner TEXT NOT NULL, + output_addresses TEXT NOT NULL, + output_values TEXT NOT NULL, + epoch INT NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS value_transfer_txns ( + txn_hash TEXT PRIMARY KEY, + input_addresses TEXT NOT NULL, + input_values TEXT NOT NULL, + input_utxos TEXT NOT NULL, + output_addresses TEXT NOT NULL, + output_values TEXT NOT NULL, + timelocks TEXT NOT NULL, + weight INT NOT NULL, + epoch INT NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS data_request_txns ( + txn_hash TEXT PRIMARY KEY, + input_addresses TEXT NOT NULL, + input_values TEXT NOT NULL, + input_utxos TEXT NOT NULL, + output_address TEXT, + output_value INT, + witnesses INT NOT NULL, + witness_reward INT NOT NULL, + collateral INT NOT NULL, + consensus_percentage INT NOT NULL, + commit_and_reveal_fee INT NOT NULL, + weight INT NOT NULL, + kinds TEXT NOT NULL, + urls TEXT NOT NULL, + headers TEXT NOT NULL, + bodies TEXT NOT NULL, + scripts TEXT NOT NULL, + aggregate_filters TEXT NOT NULL, + aggregate_reducer TEXT NOT NULL, + tally_filters TEXT NOT NULL, + tally_reducer TEXT NOT NULL, + RAD_bytes_hash TEXT NOT NULL, + DRO_bytes_hash TEXT NOT NULL, + epoch INT NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS commit_txns ( + txn_hash TEXT PRIMARY KEY, + txn_address TEXT NOT NULL, + input_values TEXT NOT NULL, + input_utxos TEXT NOT NULL, + output_value INT, + data_request TEXT NOT NULL, + epoch INT NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS reveal_txns ( + txn_hash TEXT PRIMARY KEY, + txn_address TEXT NOT NULL, + data_request TEXT NOT NULL, + result TEXT NOT NULL, + success TEXT NOT NULL, + epoch INT NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS tally_txns ( + txn_hash TEXT PRIMARY KEY, + output_addresses TEXT NOT NULL, + output_values TEXT NOT NULL, + data_request TEXT NOT NULL, + error_addresses TEXT NOT NULL, + liar_addresses TEXT NOT NULL, + result TEXT NOT NULL, + success TEXT NOT NULL, + epoch INT NOT NULL + ) + """, + """CREATE TABLE IF NOT EXISTS stake_txns ( + txn_hash TEXT PRIMARY KEY, + input_addresses TEXT NOT NULL, + input_values INT NOT NULL, + input_utxos TEXT NOT NULL, + change_address TEXT, + change_value INT, + weight INT NOT NULL, + validator TEXT NOT NULL, + withdrawer TEXT NOT NULL, + stake_value INT NOT NULL, + epoch INT NOT NULL + );""", + """CREATE TABLE IF NOT EXISTS unstake_txns ( + txn_hash TEXT PRIMARY KEY, + validator TEXT NOT NULL, + withdrawer TEXT NOT NULL, + unstake_value INT NOT NULL, + fee INT NOT NULL, + nonce INT NOT NULL, + weight INT NOT NULL, + epoch INT NOT NULL + );""", + """ + CREATE TABLE IF NOT EXISTS data_request_mempool ( + timestamp INT NOT NULL, + fee TEXT NOT NULL, + weight TEXT NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS value_transfer_mempool ( + timestamp INT NOT NULL, + fee TEXT NOT NULL, + weight TEXT NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS stake_mempool ( + timestamp INT NOT NULL, + fee TEXT NOT NULL, + weight TEXT NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS unstake_mempool ( + timestamp INT NOT NULL, + fee TEXT NOT NULL, + weight TEXT NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS wips ( + id INT, + title TEXT NOT NULL, + description TEXT NOT NULL, + urls TEXT NOT NULL, + activation_epoch INT, + tapi_start_epoch INT, + tapi_stop_epoch INT, + tapi_bit INT, + tapi_json TEXT + ) + """, + """ + CREATE TABLE IF NOT EXISTS consensus_constants ( + key TEXT PRIMARY KEY, + int_val INT, + str_val TEXT + ) + """, + """ + CREATE TABLE IF NOT EXISTS network_stats ( + stat TEXT NOT NULL, + from_epoch INT, + to_epoch INT, + data TEXT NOT NULL + ) + """, + ] + for sql in tables: + cursor.execute(sql) + connection.commit() + + +def insert_address_data(database): + address_data = [ + [ + 1, + "twit1najvm34rta4vnkpfax8kk0vhpntg5lgdz8wc33", + "label 1", + 2024098, + 68, + 68, + 90, + 4272, + 1866, + 1866, + 3051, + ], + [ + 2, + "twit1w9vaa7we6h8qyc3uawdwnp9n40602hdgsxkzf6", + "label 2", + 1657960, + 21, + 21, + 29, + 2246, + 658, + 658, + 1406, + ], + [ + 3, + "twit1z8p6qp2f5z6j2nfex3kme5qee0vurpvd8yn5hj", + None, + 1075951, + 133, + 131, + 7, + 0, + 744, + 743, + 744, + ], + [ + 4, + "twit1xshwjs5huexwfxkldvue7kc6cx230vuhtmme2s", + None, + 1100848, + 130, + 124, + 8, + 0, + 1278, + 1275, + 1278, + ], + [ + 5, + "twit17ue5kphnvajes4y05s525e6y9hjr48tpxc3ruc", + None, + 1113874, + 126, + 125, + 9, + 0, + 851, + 851, + 851, + ], + [ + 6, + "twit1my5tgl0r3lsft38kz748zaa7z48dd9994aq895", + None, + 1100849, + 122, + 113, + 13, + 0, + 1354, + 1352, + 1354, + ], + [ + 7, + "twit1d90hma685ghdw33c30svx9889sdckw7kq4j4uf", + None, + 1110639, + 120, + 119, + 9, + 0, + 738, + 738, + 738, + ], + [ + 8, + "twit1xc6002zplgwjdhgnrdxu9gpsg6d9czueeshnsz", + None, + 1088845, + 118, + 106, + 7, + 0, + 951, + 951, + 951, + ], + [ + 9, + "twit1azrj8h2mg6dnq7nem8cp7c2mqcs887djd4e2wh", + None, + 1101872, + 118, + 115, + 12, + 0, + 1401, + 1399, + 1401, + ], + [ + 10, + "twit1j3eyct469cpkz63kxx7mhffhltv5q7v45mgda3", + None, + 1110633, + 117, + 113, + 9, + 0, + 850, + 846, + 850, + ], + [ + 11, + "twit1dvj7cshhvcqttua9afmlfkhk7vwvh0qwfjnwgc", + None, + 1100843, + 116, + 104, + 8, + 0, + 773, + 773, + 773, + ], + [ + 12, + "twit1v9emzryhvu9czp39tyaz76de056g9wdtk5cfcz", + None, + 1088839, + 111, + 107, + 9, + 0, + 1036, + 1035, + 1036, + ], + [ + 13, + "twit1yr9807edzm4l4k7thmw7duufvmm4mkzgqfsnr6", + None, + 1845148, + 47, + 47, + 6, + 0, + 3181, + 3181, + 3181, + ], + [ + 14, + "twit1ajwk8tcajkwuqq5984rt07km7w9etgrvn59kfa", + None, + 1842795, + 73, + 73, + 5, + 0, + 2977, + 2975, + 2977, + ], + [ + 15, + "twit1pr02yrydlm0qd7jhtj0mp4355aj2g3gcy9smaq", + None, + 1824847, + 64, + 64, + 6, + 0, + 2862, + 2861, + 2862, + ], + [ + 16, + "twit15wd25cpstddkvxvfdzydcsnwym3d4mvwhjn2u2", + None, + 1856712, + 41, + 41, + 7, + 0, + 2830, + 2826, + 2830, + ], + [ + 17, + "twit1l3yps6rl2ct8tleh2v632mcwts5s4cuhrvzmmu", + None, + 1819644, + 52, + 52, + 7, + 0, + 2792, + 2785, + 2792, + ], + [ + 18, + "twit15k0huw65x8wkq3p06dmqxf3a46cdhkuljculsm", + None, + 1389040, + 30, + 30, + 7, + 0, + 2767, + 2762, + 2767, + ], + [ + 19, + "twit1nwgm7dz3m339h3uxhyflvr6yfutk7kvfy6auff", + None, + 1258429, + 22, + 22, + 8, + 0, + 2716, + 2709, + 2716, + ], + [ + 20, + "twit1w2hxy8h86p43wx9g722mx8ygs0dsdqlut3n7gw", + None, + 1393126, + 33, + 33, + 6, + 0, + 2700, + 2698, + 2700, + ], + [ + 21, + "twit16m7pxuajzs8jsgwqnukqk39v3qc8dxypqahqdj", + None, + 1259231, + 34, + 34, + 6, + 0, + 2644, + 2639, + 2644, + ], + [ + 22, + "twit15rv5eypwq54u6u3xc2ckd7vfyj53nnddmrmryt", + None, + 2048900, + 62, + 62, + 7, + 0, + 3021, + 3019, + 3021, + ], + [ + 23, + "twit133wnd4ueheeme4dxjjy052s73sjf8uslah70s4", + None, + 1071415, + 26, + 26, + 2, + 46955, + 340, + 340, + 29808, + ], + [ + 24, + "twit1k6vpqaajekgyx5whdp4ag7dp2h2627dcfjyngs", + None, + 1071457, + 13, + 13, + 2, + 0, + 441, + 441, + 441, + ], + [ + 25, + "twit18j3jsfn6y6lc43v4x5tn3lgk8vzlj37hx6esqy", + None, + 1071481, + 12, + 12, + 2, + 0, + 503, + 501, + 503, + ], + [ + 26, + "twit1aufqegyctt7h64eyyp2u6a68ultgkla5gnsxw8", + None, + 1071495, + 16, + 16, + 2, + 0, + 353, + 353, + 353, + ], + [ + 27, + "twit14ef2z0l3l9plkuuqvcsqnq2azc86c2v7udjfeg", + None, + 1071507, + 10, + 10, + 2, + 0, + 278, + 278, + 278, + ], + [ + 28, + "twit1dsxjndrkhpmgmd6nmt95nvmtqf667e5pmq2eqr", + None, + 1071527, + 11, + 11, + 2, + 0, + 164, + 164, + 164, + ], + [ + 29, + "twit1ewhsfsjz5gdern8kaz8x5yd9ph04lyg3ewxssc", + None, + 1071543, + 4, + 4, + 3, + 0, + 104, + 103, + 104, + ], + [ + 30, + "twit1g5n9m4s0lqa2am0edgfevnppu8y207yzjdhqag", + None, + 1071547, + 9, + 9, + 2, + 0, + 211, + 211, + 211, + ], + [ + 31, + "twit1r7gszcq5hu2xvxhpufgs56c0m47tuwtnl3qdpa", + None, + 1071554, + 8, + 8, + 2, + 0, + 261, + 261, + 261, + ], + [ + 32, + "twit10c45atl93vaudvpcmqtl8pqgt5lwzkk9r9mvrs", + None, + 1071563, + 7, + 7, + 3, + 0, + 224, + 224, + 224, + ], + [ + 33, + "twit1a82dxlj8cy3afpxna6kqgq2r9plraf9raqq35m", + None, + 1071421, + 27, + 27, + 2, + 0, + 1121, + 1121, + 1121, + ], + [ + 34, + "twit1gzntj0duaqjgexcjnwhgww9f564l7edx0khl6y", + None, + 1071434, + 22, + 22, + 2, + 0, + 348, + 348, + 348, + ], + [ + 35, + "twit1ccm40u8d8z6ps28tkx6f6ruh340l77psgwhf95", + None, + 1071451, + 17, + 17, + 2, + 0, + 361, + 360, + 361, + ], + [ + 36, + "twit1m6xt7zh3km2u9xzceqzaepkl5nj5qr4ss8zfed", + None, + 1071473, + 21, + 21, + 2, + 0, + 525, + 525, + 525, + ], + [ + 37, + "twit1zxg8m7t6rqs0mpptcmmd8fr5hxe3hu7e849xr8", + None, + 1071488, + 6, + 6, + 3, + 0, + 99, + 99, + 99, + ], + [ + 38, + "twit1m3u3hhs9fvqgt5a8d0zm7w7mhsytgz53pyf9hz", + None, + 1071761, + 14, + 14, + 2, + 0, + 210, + 209, + 210, + ], + [ + 39, + "twit1xe6j5kf8a20xvhqjf7jsej06k45pxhu99u6dza", + None, + 1071750, + 20, + 20, + 3, + 0, + 756, + 754, + 756, + ], + [ + 40, + "twit1kzmck6qyy503lme89lj02jxc3nuj45wg4l8r4c", + None, + 1071734, + 14, + 14, + 2, + 0, + 398, + 398, + 398, + ], + [ + 41, + "twit1e49dg9me24esfmz8gkvhhszr7dvasc77mmq6nj", + None, + 1071421, + 9, + 9, + 4, + 0, + 145, + 145, + 145, + ], + [ + 42, + "twit1astv06rz2m4pg9gaq5l79k34sh9w0j5nj2xe6k", + None, + 1071426, + 29, + 29, + 2, + 0, + 573, + 573, + 573, + ], + [ + 43, + "twit19ejd5fgeypn9d8f6r8st2jkdx89e3vmp4eu3zv", + None, + 1071425, + 21, + 21, + 2, + 0, + 473, + 473, + 473, + ], + [ + 44, + "twit1m5wsdsv7ard8va55tut5jk06nrx2gmxhgphqhl", + None, + 1071428, + 14, + 14, + 2, + 0, + 300, + 300, + 300, + ], + [ + 45, + "twit1ulra63922mnls2rvktaqh4hhrruama9eqjnkc8", + None, + 1071433, + 5, + 5, + 4, + 0, + 179, + 179, + 179, + ], + [ + 46, + "twit1n4gsyx57khmancx0cmzg2q975vvqayq55rlg0y", + None, + 1071436, + 21, + 21, + 2, + 0, + 509, + 498, + 509, + ], + [ + 47, + "twit1fz0wz7dcpanadsdlke75zgjlhqxuv2vusvte8f", + None, + 1071434, + 11, + 11, + 2, + 0, + 123, + 122, + 123, + ], + [ + 48, + "twit140uxs8r7asudphc7zzqc2ze8fk5ytkd6auxjf4", + None, + 1071440, + 7, + 7, + 2, + 0, + 151, + 151, + 151, + ], + [ + 49, + "twit1mts8na78rrdg9j405uh5t9lklkpsdfm8tfze95", + None, + 1071441, + 15, + 15, + 2, + 0, + 307, + 307, + 307, + ], + [ + 50, + "twit1k58yzyjse9frx5yx7xfg0g43mnuv5xw0y4venf", + None, + 1259154, + 35, + 35, + 3, + 0, + 1010, + 1010, + 1010, + ], + [ + 51, + "twit1epgja4fpkyupwlxjawnth39usajywqdh9cxxlx", + None, + 1071430, + 21, + 21, + 2, + 0, + 101, + 101, + 101, + ], + [ + 52, + "twit1gue84sf650hns8qsq4kn2x7awy29mrhdexdste", + None, + 1071439, + 31, + 31, + 2, + 0, + 819, + 818, + 819, + ], + ] + + sql = """ + INSERT INTO + addresses + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + connection = sqlite3.connect(database) + cursor = connection.cursor() + cursor.executemany(sql, address_data) + connection.commit() + + +def insert_consensus_constants(database): + consensus_constants = [ + ["activity_period", 2000, None], + [ + "bootstrap_hash", + None, + "[666564676f6573627272727c2f3030312f3738392f3432382f6130312e676966]", + ], + [ + "bootstrapping_committee", + None, + "[twit1f0am8c97q2ygkz3q6jyd2x29s8zaxqlxcqltxx,twit1pc8jzqph4t0md02e6fgwgsw26yll20p98c3pgh,twit1mseplfttj5vvm8r7d5pn5je9dd02el4hw4w4cp,twit1najvm34rta4vnkpfax8kk0vhpntg5lgdz8wc33]", + ], + ["checkpoint_zero_timestamp", 1738180800, None], + ["checkpoints_period", 45, None], + ["collateral_age", 1000, None], + ["collateral_minimum", 100000000, None], + ["epochs_with_minimum_difficulty", 2000, None], + ["extra_rounds", 3, None], + [ + "genesis_hash", + None, + "[5aaafb853ce897c1431ee5babc5533650d37c4fab44045bde9c74a3fff8c080e]", + ], + ["halving_period", 3500000, None], + ["initial_block_reward", 250000000000, None], + ["max_dr_weight", 80000, None], + ["max_vt_weight", 20000, None], + ["minimum_difficulty", 0, None], + ["mining_backup_factor", 8, None], + ["mining_replication_factor", 3, None], + ["reputation_expire_alpha_diff", 20000, None], + ["reputation_issuance", 1, None], + ["reputation_issuance_stop", 1048576, None], + ["reputation_penalization_factor", 50, None], + ["superblock_committee_decreasing_period", 5, None], + ["superblock_committee_decreasing_step", 5, None], + ["superblock_period", 10, None], + ["superblock_signing_committee_size", 100, None], + ["wit2_checkpoints_period", 20, None], + ["wit2_minimum_total_stake_nanowits", 30_000_000_000_000_000, None], + ["wit2_activation_delay_epochs", 101, None], + ["wit2_maximum_stake_block_weight", 10_000_000, None], + ["wit2_maximum_unstake_block_weight", 5_000, None], + ["wit2_unstaking_delay_seconds", 3_600, None], + ["wit2_min_stake_nanowits", 10_000_000_000_000, None], + ["wit2_max_stake_nanowits", 10_000_000_000_000_000, None], + ["wit2_block_reward", 50_000_000_000, None], + ] + sql = """ + INSERT INTO + consensus_constants + VALUES + (?, ?, ?) + """ + connection = sqlite3.connect(database) + cursor = connection.cursor() + cursor.executemany(sql, consensus_constants) + connection.commit() + + +def insert_network_stats(database): + network_stats = [ + [ + "rollbacks", + None, + None, + [ + [1740387825, 110000, 110009, 10], + [1740187825, 100000, 100019, 20], + [1739789225, 80070, 80099, 30], + ], + ], + ["epoch", None, None, 115910], + [ + "miners", + None, + None, + { + "amount": 416061, + "top-100": [ + [3, 133], + [4, 130], + [5, 126], + [6, 122], + [7, 120], + [8, 118], + [9, 118], + [10, 117], + [11, 116], + [12, 111], + ], + }, + ], + [ + "data_request_solvers", + None, + None, + { + "amount": 574157, + "top-100": [ + [13, 3181], + [14, 2977], + [15, 2862], + [16, 2830], + [17, 2792], + [18, 2767], + [19, 2716], + [20, 2700], + [21, 2644], + [22, 2597], + ], + }, + ], + [ + "miners", + 100000, + 101000, + { + "23": 1, + "24": 1, + "25": 1, + "26": 1, + "27": 1, + "28": 1, + "29": 1, + "30": 1, + "31": 1, + "32": 1, + }, + ], + [ + "miners", + 101000, + 102000, + { + "33": 1, + "34": 1, + "35": 2, + "36": 1, + "37": 1, + "38": 1, + "26": 1, + "39": 1, + "40": 1, + "30": 1, + }, + ], + [ + "data_request_solvers", + 100000, + 101000, + { + "41": 1, + "42": 4, + "43": 2, + "44": 1, + "45": 9, + "46": 5, + "47": 1, + "34": 1, + "48": 1, + "49": 2, + }, + ], + [ + "data_request_solvers", + 101000, + 102000, + { + "50": 4, + "41": 9, + "42": 20, + "43": 10, + "51": 1, + "45": 16, + "46": 16, + "34": 7, + "52": 1, + "49": 7, + }, + ], + [ + "data_requests", + 100000, + 101000, + [ + 1066, + 1057, + 3909, + 0, + 5, + {"2": 5, "10": 813, "100": 248}, + {"1": 248, "500000": 5, "100000": 813}, + {"100000000": 5, "2500000000": 248, "5000000000": 813}, + ], + ], + [ + "data_requests", + 101000, + 102000, + [ + 1079, + 1073, + 4110, + 0, + 5, + {"2": 5, "10": 826, "100": 248}, + {"1": 248, "500000": 5, "100000": 826}, + {"100000000": 5, "2500000000": 248, "5000000000": 826}, + ], + ], + ["lie_rate", 100000, 101000, [32940, 367, 310, 239]], + ["lie_rate", 101000, 102000, [33070, 271, 321, 220]], + ["lie_rate", 2023000, 2024000, [29702, 305, 544, 98]], + ["lie_rate", 2024000, 2025000, [32242, 654, 594, 135]], + ["burn_rate", 100000, 101000, [0, 0]], + ["burn_rate", 101000, 102000, [0, 0]], + ["burn_rate", 2023000, 2024000, [0, 543000000000]], + ["burn_rate", 2024000, 2025000, [0, 550500000000]], + ["value_transfers", 100000, 101000, [312]], + ["value_transfers", 101000, 102000, [266]], + ["value_transfers", 2023000, 2024000, [57]], + ["value_transfers", 2024000, 2025000, [98]], + ] + + sql = """ + INSERT INTO + network_stats + VALUES + (?, ?, ?, ?) + """ + connection = sqlite3.connect(database) + cursor = connection.cursor() + cursor.executemany( + sql, [[ns[0], ns[1], ns[2], json.dumps(ns[3])] for ns in network_stats] + ) + connection.commit() + + +def insert_pending_transaction(database): + connection = sqlite3.connect(database) + cursor = connection.cursor() + + pending_data_requests = [ + [1740752780, [166666670], [2225]], + [1740752790, [166666670], [2225]], + [1740752800, [166666670], [2225]], + [1740752810, [166666670], [2225]], + [1740752820, [166666670], [2225]], + [1740752830, [166666670], [2225]], + [1740753050, [166666670], [2520]], + [1740758250, [166666670], [2520]], + [1740758260, [166666670], [2690]], + [1740758270, [166666670], [2690]], + ] + + sql = """ + INSERT INTO + data_request_mempool + VALUES + (?, ?, ?) + """ + cursor.executemany( + sql, + [ + [pdr[0], json.dumps(pdr[1]), json.dumps(pdr[2])] + for pdr in pending_data_requests + ], + ) + + pending_value_transfers = [ + [1740752780, [1000], [853]], + [1740752790, [1000], [853]], + [1740752800, [1000], [853]], + [1740752810, [1000, 1000], [853, 853]], + [1740752820, [1000], [853]], + [1740752830, [1000], [853]], + [1740753050, [1000], [626]], + [1740758250, [1000], [18133]], + [1740758260, [1000], [18133]], + [1740758270, [1000], [18133]], + ] + + sql = """ + INSERT INTO + value_transfer_mempool + VALUES + (?, ?, ?) + """ + cursor.executemany( + sql, + [ + [pvt[0], json.dumps(pvt[1]), json.dumps(pvt[2])] + for pvt in pending_value_transfers + ], + ) + + pending_stakes = [ + [1740752780, [1000], [939]], + [1740752790, [1000], [939]], + [1740752800, [1000], [9983]], + [1740752810, [1000, 2000], [407, 274]], + [1740752820, [1000], [274]], + [1740752830, [1000], [274]], + [1740753050, [1000], [274]], + [1740758250, [1000], [2136]], + [1740758260, [1000], [1737]], + [1740758270, [1000], [2402]], + ] + + sql = """ + INSERT INTO + stake_mempool + VALUES + (?, ?, ?) + """ + cursor.executemany( + sql, + [[pvt[0], json.dumps(pvt[1]), json.dumps(pvt[2])] for pvt in pending_stakes], + ) + + pending_unstakes = [ + [1740752780, [1000], [153]], + [1740752790, [1000], [153]], + [1740752800, [1000], [153]], + [1740752810, [1000, 2000], [153, 153]], + [1740752820, [1000], [153]], + [1740752830, [1000], [153]], + [1740753050, [1000], [153]], + [1740758250, [1000], [153]], + [1740758260, [1000], [153]], + [1740758270, [1000], [153]], + ] + + sql = """ + INSERT INTO + unstake_mempool + VALUES + (?, ?, ?) + """ + cursor.executemany( + sql, + [[pvt[0], json.dumps(pvt[1]), json.dumps(pvt[2])] for pvt in pending_unstakes], + ) + + connection.commit() + + +def insert_wips(database): + wips = [ + [ + 1, + "WIP0008", + "Limit data request concurrency", + ["https://github.com/witnet/WIPs/blob/master/wip-0008.md"], + 0, + None, + None, + None, + None, + ], + [ + 2, + "WIP0009-0011-0012", + "Adjust mining probability (WIP0009), improve superblock voting (WIP0011) and set minimum mining difficulty (WIP0012)", + [ + "https://github.com/witnet/WIPs/blob/master/wip-0009.md,https://github.com/witnet/WIPs/blob/master/wip-0011.md,https://github.com/witnet/WIPs/blob/master/wip-0012.md" + ], + 0, + None, + None, + None, + None, + ], + [ + 3, + "THIRD_HARD_FORK", + "Set a maximum eligibility for data requests", + ["https://github.com/witnet/witnet-rust/pull/1957"], + 0, + None, + None, + None, + None, + ], + [ + 4, + "WIP0014-0016", + "Activation of TAPI itself (WIP0014) and setting a minimum data request mining difficulty (WIP0016)", + [ + "https://github.com/witnet/WIPs/blob/master/wip-0014.md,https://github.com/witnet/WIPs/blob/master/wip-0016.md" + ], + 0, + 0, + 0, + 0, + None, + ], + [ + 5, + "WIP0017-0018-0019", + "Add a median RADON reducer (WIP0017), modify the UnhandledIntercept RADON error (WIP0018) and add RNG functionality to Witnet (WIP0019)", + [ + "https://github.com/witnet/WIPs/blob/master/wip-0017.md,https://github.com/witnet/WIPs/blob/master/wip-0018.md,https://github.com/witnet/WIPs/blob/master/wip-0019.md" + ], + 0, + 0, + 0, + 1, + None, + ], + [ + 6, + "WIP0020-0021", + "Add support HTTP-POST (WIP0020) and add an XML parsing operator (WIP0021)", + [ + "https://github.com/witnet/WIPs/blob/master/wip-0020.md,https://github.com/witnet/WIPs/blob/master/wip-0021.md" + ], + 0, + 0, + 0, + 2, + None, + ], + [ + 7, + "WIP0022 (defeated)", + "Set a data request reward collateral ratio", + ["https://github.com/witnet/WIPs/blob/master/wip-0022.md"], + None, + 0, + 0, + 3, + { + "bit": 3, + "urls": ["https://github.com/witnet/WIPs/blob/master/wip-0022.md"], + "rates": [ + { + "global_rate": 1.171875, + "periodic_rate": 31.5, + "relative_rate": 31.5, + }, + { + "global_rate": 2.392113095238095, + "periodic_rate": 32.800000000000004, + "relative_rate": 32.15, + }, + { + "global_rate": 3.616071428571429, + "periodic_rate": 32.9, + "relative_rate": 32.4, + }, + { + "global_rate": 4.769345238095238, + "periodic_rate": 31.0, + "relative_rate": 32.05, + }, + { + "global_rate": 6.045386904761905, + "periodic_rate": 34.300000000000004, + "relative_rate": 32.5, + }, + { + "global_rate": 7.3065476190476195, + "periodic_rate": 33.900000000000006, + "relative_rate": 32.733333333333334, + }, + { + "global_rate": 8.537946428571429, + "periodic_rate": 33.1, + "relative_rate": 32.785714285714285, + }, + { + "global_rate": 9.873511904761905, + "periodic_rate": 35.9, + "relative_rate": 33.175, + }, + { + "global_rate": 11.116071428571427, + "periodic_rate": 33.4, + "relative_rate": 33.2, + }, + { + "global_rate": 12.566964285714285, + "periodic_rate": 39.0, + "relative_rate": 33.78, + }, + { + "global_rate": 14.166666666666666, + "periodic_rate": 43.0, + "relative_rate": 34.61818181818182, + }, + { + "global_rate": 15.982142857142856, + "periodic_rate": 48.8, + "relative_rate": 35.8, + }, + { + "global_rate": 17.87202380952381, + "periodic_rate": 50.8, + "relative_rate": 36.95384615384615, + }, + { + "global_rate": 19.750744047619047, + "periodic_rate": 50.5, + "relative_rate": 37.92142857142857, + }, + { + "global_rate": 21.648065476190474, + "periodic_rate": 51.0, + "relative_rate": 38.79333333333334, + }, + { + "global_rate": 23.645833333333332, + "periodic_rate": 53.7, + "relative_rate": 39.725, + }, + { + "global_rate": 25.691964285714285, + "periodic_rate": 55.00000000000001, + "relative_rate": 40.62352941176471, + }, + { + "global_rate": 27.67857142857143, + "periodic_rate": 53.400000000000006, + "relative_rate": 41.333333333333336, + }, + { + "global_rate": 29.694940476190478, + "periodic_rate": 54.2, + "relative_rate": 42.01052631578948, + }, + { + "global_rate": 31.685267857142858, + "periodic_rate": 53.5, + "relative_rate": 42.585, + }, + { + "global_rate": 33.757440476190474, + "periodic_rate": 55.7, + "relative_rate": 43.20952380952381, + }, + { + "global_rate": 35.967261904761905, + "periodic_rate": 59.4, + "relative_rate": 43.945454545454545, + }, + { + "global_rate": 38.36309523809524, + "periodic_rate": 64.4, + "relative_rate": 44.83478260869565, + }, + { + "global_rate": 40.94122023809524, + "periodic_rate": 69.3, + "relative_rate": 45.85416666666667, + }, + { + "global_rate": 43.47470238095238, + "periodic_rate": 68.1000000000001, + "relative_rate": 46.744, + }, + { + "global_rate": 46.29092261904762, + "periodic_rate": 75.7, + "relative_rate": 47.857692307692304, + }, + { + "global_rate": 48.98065476190476, + "periodic_rate": 82.1590909090909, + "relative_rate": 48.98065476190476, + }, + ], + "title": "WIP0022 (defeated)", + "active": False, + "tapi_id": 7, + "finished": True, + "activated": False, + "stop_time": 1678356045, + "start_time": 1677146445, + "stop_epoch": 1682000, + "description": "Set a data request reward collateral ratio", + "start_epoch": 1655120, + "last_updated": 1695549179, + "current_epoch": 2064069, + "global_acceptance_rate": 48.98065476190476, + "relative_acceptance_rate": 48.98065476190476, + }, + ], + [ + 8, + "WIP0023 (defeated)", + "Burn slashed collateral", + ["https://github.com/witnet/WIPs/blob/master/wip-0023.md"], + None, + 0, + 0, + 4, + None, + ], + [ + 9, + "WIP0024 (defeated)", + "Improve the processing of numbers in oracle queries", + ["https://github.com/witnet/WIPs/blob/master/wip-0024.md"], + None, + 0, + 0, + 5, + None, + ], + [ + 10, + "WIP0025 (defeated)", + "Follow HTTP redirects in retrievals", + ["https://github.com/witnet/WIPs/blob/master/wip-0025.md"], + None, + 0, + 0, + 6, + None, + ], + [ + 11, + "WIP0026 (defeated)", + "Introduce a new EncodeReveal RADON error", + ["https://github.com/witnet/WIPs/blob/master/wip-0026.md"], + None, + 0, + 0, + 7, + None, + ], + [ + 12, + "WIP0027 (defeated)", + "Increase the age requirement for using transaction outputs as collateral", + ["https://github.com/witnet/WIPs/blob/master/wip-0027.md"], + None, + 0, + 0, + 8, + None, + ], + [ + 13, + "WIP0022", + "Set a data request reward collateral ratio", + ["https://github.com/witnet/WIPs/blob/master/wip-0022.md"], + 0, + 0, + 0, + 3, + { + "bit": 3, + "urls": ["https://github.com/witnet/WIPs/blob/master/wip-0022.md"], + "rates": [ + { + "global_rate": 3.0729166666666665, + "periodic_rate": 82.6, + "relative_rate": 82.6, + }, + { + "global_rate": 6.045386904761905, + "periodic_rate": 79.9, + "relative_rate": 81.25, + }, + { + "global_rate": 9.08110119047619, + "periodic_rate": 81.6, + "relative_rate": 81.36666666666666, + }, + { + "global_rate": 12.001488095238095, + "periodic_rate": 78.5, + "relative_rate": 80.65, + }, + { + "global_rate": 14.832589285714285, + "periodic_rate": 76.1, + "relative_rate": 79.74, + }, + { + "global_rate": 17.83110119047619, + "periodic_rate": 80.60000000000001, + "relative_rate": 79.88333333333333, + }, + { + "global_rate": 20.788690476190478, + "periodic_rate": 79.5, + "relative_rate": 79.82857142857142, + }, + { + "global_rate": 23.783482142857142, + "periodic_rate": 80.5, + "relative_rate": 79.9125, + }, + { + "global_rate": 26.6889880952381, + "periodic_rate": 78.1000000000001, + "relative_rate": 79.71111111111111, + }, + { + "global_rate": 29.70982142857143, + "periodic_rate": 81.2, + "relative_rate": 79.86, + }, + { + "global_rate": 32.641369047619044, + "periodic_rate": 78.8, + "relative_rate": 79.76363636363637, + }, + { + "global_rate": 35.61383928571429, + "periodic_rate": 79.9, + "relative_rate": 79.77499999999999, + }, + { + "global_rate": 38.64955357142857, + "periodic_rate": 81.6, + "relative_rate": 79.91538461538461, + }, + { + "global_rate": 41.75595238095238, + "periodic_rate": 83.5, + "relative_rate": 80.17142857142858, + }, + { + "global_rate": 44.851190476190474, + "periodic_rate": 83.2, + "relative_rate": 80.37333333333333, + }, + { + "global_rate": 47.97247023809524, + "periodic_rate": 83.89999999999999, + "relative_rate": 80.59375, + }, + { + "global_rate": 51.06026785714286, + "periodic_rate": 83.0, + "relative_rate": 80.73529411764706, + }, + { + "global_rate": 54.055059523809526, + "periodic_rate": 80.5, + "relative_rate": 80.72222222222221, + }, + { + "global_rate": 57.12797619047619, + "periodic_rate": 82.6, + "relative_rate": 80.82105263157895, + }, + { + "global_rate": 60.25297619047619, + "periodic_rate": 84.0, + "relative_rate": 80.97999999999999, + }, + { + "global_rate": 63.370535714285715, + "periodic_rate": 83.8, + "relative_rate": 81.11428571428571, + }, + { + "global_rate": 66.45833333333333, + "periodic_rate": 83.0, + "relative_rate": 81.2, + }, + { + "global_rate": 69.58333333333333, + "periodic_rate": 84.0, + "relative_rate": 81.32173913043478, + }, + { + "global_rate": 72.85714285714285, + "periodic_rate": 88.0, + "relative_rate": 81.6, + }, + { + "global_rate": 76.02306547619048, + "periodic_rate": 85.1, + "relative_rate": 81.74, + }, + { + "global_rate": 79.21875, + "periodic_rate": 85.9, + "relative_rate": 81.89999999999999, + }, + { + "global_rate": 82.14657738095238, + "periodic_rate": 89.43181818181817, + "relative_rate": 82.14657738095238, + }, + ], + "title": "WIP0022", + "active": False, + "tapi_id": 13, + "finished": True, + "activated": True, + "stop_time": 1679565645, + "start_time": 1678356045, + "stop_epoch": 1708880, + "description": "Set a data request reward collateral ratio", + "start_epoch": 1682000, + "last_updated": 1695549180, + "current_epoch": 2064069, + "global_acceptance_rate": 82.14657738095238, + "relative_acceptance_rate": 82.14657738095238, + }, + ], + [ + 14, + "WIP0023", + "Burn slashed collateral", + ["https://github.com/witnet/WIPs/blob/master/wip-0023.md"], + 0, + 0, + 0, + 4, + None, + ], + [ + 15, + "WIP0024", + "Improve the processing of numbers in oracle queries", + ["https://github.com/witnet/WIPs/blob/master/wip-0024.md"], + 0, + 0, + 0, + 5, + None, + ], + [ + 16, + "WIP0025", + "Follow HTTP redirects in retrievals", + ["https://github.com/witnet/WIPs/blob/master/wip-0025.md"], + 0, + 0, + 0, + 6, + None, + ], + [ + 17, + "WIP0026", + "Introduce a new EncodeReveal RADON error", + ["https://github.com/witnet/WIPs/blob/master/wip-0026.md"], + 0, + 0, + 0, + 7, + None, + ], + [ + 18, + "WIP0027", + "Increase the age requirement for using transaction outputs as collateral", + ["https://github.com/witnet/WIPs/blob/master/wip-0027.md"], + 0, + 0, + 0, + 8, + None, + ], + [ + 19, + "WIP0028", + "Enable upgrade to wit/2", + ["https://github.com/witnet/WIPs/blob/master/wip-0028.md"], + 101, + 0, + 80, + 9, + None, + ], + [ + 20, + "wit/2", + "Activate wit/2", + ["https://github.com/witnet/WIPs/blob/master/wip-0028.md"], + 281, + 101, + 181, + -1, + None, + ], + ] + sql = """ + INSERT INTO + wips + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + connection = sqlite3.connect(database) + cursor = connection.cursor() + cursor.executemany( + sql, + [ + [ + wip[0], + wip[1], + wip[2], + json.dumps(wip[3]), + wip[4], + wip[5], + wip[6], + wip[7], + json.dumps(wip[8]) if wip[8] else None, + ] + for wip in wips + ], + ) + connection.commit() + + +def get_input_addresses(address_generator, signatures): + return [ + address_generator.signature_to_address( + signature["public_key"]["compressed"], + signature["public_key"]["bytes"], + ) + for signature in signatures + ] + + +def get_input_values(witnet_node, requested_txns, output_pointers): + input_values = [] + + for txn_input in output_pointers: + txn_hash = txn_input["output_pointer"].split(":")[0] + txn_idx = int(txn_input["output_pointer"].split(":")[1]) + + if txn_hash in requested_txns: + input_txn = requested_txns[txn_hash] + else: + transaction = witnet_node.get_transaction(txn_hash) + input_txn = transaction["result"]["transaction"] + requested_txns[txn_hash] = input_txn + + txn_type = list(input_txn.keys())[0] + if txn_type in ("Tally", "Mint"): + outputs = input_txn[txn_type]["outputs"] + # Append the correct output to the list of input_values + input_values.append(outputs[txn_idx]["value"]) + elif txn_type in ( + "DataRequest", + "Commit", + "ValueTransfer", + ): + outputs = input_txn[txn_type]["body"]["outputs"] + # Append the correct output to the list of input_values + input_values.append(outputs[txn_idx]["value"]) + elif txn_type == "Stake": + output = input_txn[txn_type]["body"]["change"] + # Append the correct output to the list of input_values + input_values.append(output["value"]) + elif txn_type == "Unstake": + output = input_txn[txn_type]["body"]["withdrawal"] + # Append the correct output to the list of input_values + input_values.append(output["value"]) + + return input_values + + +def calculate_block_fees(witnet_node, requested_txns, block): + block_fees = 0 + + for value_transfer in block["txns"]["value_transfer_txns"]: + vt_body = value_transfer["body"] + input_values = get_input_values( + witnet_node, + requested_txns, + vt_body["inputs"], + ) + block_fees += sum(input_values) + block_fees -= sum([output["value"] for output in vt_body["outputs"]]) + + for data_request in block["txns"]["data_request_txns"]: + dr_output = data_request["body"]["dr_output"] + witnesses = dr_output["witnesses"] + witness_reward = dr_output["witness_reward"] + commit_and_reveal_fee = dr_output["commit_and_reveal_fee"] + dro_fee = witnesses * (witness_reward + 2 * commit_and_reveal_fee) + + input_values = get_input_values( + witnet_node, + requested_txns, + data_request["body"]["inputs"], + ) + if len(data_request["body"]["outputs"]) > 0: + output_value = data_request["body"]["outputs"][0]["value"] + else: + output_value = 0 + + miner_fee = sum(input_values) - output_value - dro_fee + + block_fees += miner_fee + + for commit in block["txns"]["commit_txns"]: + dr_pointer = commit["body"]["dr_pointer"] + if dr_pointer in requested_txns: + data_request = requested_txns[dr_pointer] + else: + transaction = witnet_node.get_transaction(dr_pointer) + data_request = transaction["result"]["transaction"] + requested_txns[dr_pointer] = data_request + dr_output = data_request["DataRequest"]["body"]["dr_output"] + block_fees += dr_output["commit_and_reveal_fee"] + + for reveal in block["txns"]["reveal_txns"]: + dr_pointer = reveal["body"]["dr_pointer"] + if dr_pointer in requested_txns: + data_request = requested_txns[dr_pointer] + else: + transaction = witnet_node.get_transaction(reveal["body"]["dr_pointer"]) + data_request = transaction["result"]["transaction"] + requested_txns[dr_pointer] = data_request + dr_output = data_request["DataRequest"]["body"]["dr_output"] + block_fees += dr_output["commit_and_reveal_fee"] + + for stake in block["txns"]["stake_txns"]: + st_body = stake["body"] + input_values = get_input_values( + witnet_node, + requested_txns, + st_body["inputs"], + ) + block_fees += sum(input_values) + if st_body["change"]: + block_fees -= st_body["change"]["value"] + block_fees -= st_body["output"]["value"] + + for unstake in block["txns"]["unstake_txns"]: + block_fees += unstake["body"]["fee"] + + return block_fees + + +def parse_mint(address_generator, txn_hash, block_sig, mint_txn, epoch): + miner = get_input_addresses(address_generator, [block_sig])[0] + + return [ + f"\\x{txn_hash}", + miner, + str([",".join([output["pkh"] for output in mint_txn["outputs"]])]).replace( + "'", "" + ), + str( + [",".join([str(output["value"]) for output in mint_txn["outputs"]])] + ).replace("'", ""), + epoch, + ] + + +def parse_value_transfer( + address_generator, + witnet_node, + requested_txns, + txn_hash, + value_transfer, + txn_weight, + epoch, +): + input_values = get_input_values( + witnet_node, + requested_txns, + value_transfer["body"]["inputs"], + ) + + return [ + f"\\x{txn_hash}", + str( + get_input_addresses(address_generator, value_transfer["signatures"]) + ).replace("'", ""), + str(input_values), + str( + [ + f"\\x{txn_input['output_pointer']}" + for txn_input in value_transfer["body"]["inputs"] + ] + ) + .replace("'", "") + .replace("\\\\", "\\"), + str([output["pkh"] for output in value_transfer["body"]["outputs"]]).replace( + "'", "" + ), + str([output["value"] for output in value_transfer["body"]["outputs"]]), + str([output["time_lock"] for output in value_transfer["body"]["outputs"]]), + txn_weight, + epoch, + ] + + +def parse_data_request( + address_generator, + witnet_node, + requested_txns, + txn_hash, + data_request, + txn_weight, + RAD_hash, + DRO_hash, + epoch, +): + dr_outputs = data_request["body"]["outputs"] + dr_output = data_request["body"]["dr_output"] + rad_request = dr_output["data_request"] + + input_addresses = get_input_addresses(address_generator, data_request["signatures"]) + input_values = get_input_values( + witnet_node, + requested_txns, + data_request["body"]["inputs"], + ) + + return [ + f"\\x{txn_hash}", + str(input_addresses).replace("'", ""), + str(input_values), + str( + [ + f"\\x{txn_input['output_pointer']}" + for txn_input in data_request["body"]["inputs"] + ] + ) + .replace("'", "") + .replace("\\\\", "\\"), + dr_outputs[0]["pkh"] if len(dr_outputs) > 0 else "", + dr_outputs[0]["value"] if len(dr_outputs) > 0 else 0, + dr_output["witnesses"], + dr_output["witness_reward"], + dr_output["collateral"], + dr_output["min_consensus_percentage"], + dr_output["commit_and_reveal_fee"], + txn_weight, + "{" + + ",".join([retrieve["kind"] for retrieve in rad_request["retrieve"]]) + + "}", + str( + [ + ",".join( + [ + retrieve["url"] if "url" in retrieve else "'" + for retrieve in rad_request["retrieve"] + ] + ) + ] + ).replace("'", ""), + str( + [ + ",".join( + [ + ( + str(retrieve["header"]).replace("'", "") + if "header" in retrieve + else str([""]).replace("'", "") + ) + for retrieve in rad_request["retrieve"] + ] + ) + ] + ).replace("'", ""), + str( + [ + ",".join( + [ + ( + f"\\x{bytes2hex(retrieve['body'])}" + if "body" in retrieve + else "\\x" + ) + for retrieve in rad_request["retrieve"] + ] + ) + ] + ) + .replace("'", "") + .replace("\\\\", "\\"), + str( + [ + ",".join( + [ + f"\\x{bytes2hex(retrieve['script'])}" + for retrieve in rad_request["retrieve"] + ] + ) + ] + ) + .replace("'", "") + .replace("\\\\", "\\"), + str( + [ + f"filter({aggregate_filter['op']}, \\x{bytes2hex(aggregate_filter['args'])})" + for aggregate_filter in rad_request["aggregate"]["filters"] + ] + ) + .replace("'", "") + .replace("\\\\", "\\"), + str([rad_request["aggregate"]["reducer"]]), + str( + [ + f"filter({tally_filter['op']}, \\x{bytes2hex(tally_filter['args'])})" + for tally_filter in rad_request["tally"]["filters"] + ] + ) + .replace("'", "") + .replace("\\\\", "\\"), + str([rad_request["tally"]["reducer"]]), + f"\\x{RAD_hash}", + f"\\x{DRO_hash}", + epoch, + ] + + +def parse_commit( + address_generator, + witnet_node, + requested_txns, + txn_hash, + commit, + epoch, +): + input_values = get_input_values( + witnet_node, + requested_txns, + commit["body"]["collateral"], + ) + + return [ + f"\\x{txn_hash}", + get_input_addresses(address_generator, commit["signatures"])[0], + str(input_values), + str( + [ + f"\\x{txn_input['output_pointer']}" + for txn_input in commit["body"]["collateral"] + ] + ) + .replace("'", "") + .replace("\\\\", "\\"), + ( + commit["body"]["outputs"][0]["value"] + if len(commit["body"]["outputs"]) > 0 + else 0 + ), + f"\\x{commit['body']['dr_pointer']}", + epoch, + ] + + +def parse_reveal(address_generator, txn_hash, reveal, epoch): + success, _ = translate_reveal(txn_hash, reveal["body"]["reveal"]) + + return [ + f"\\x{txn_hash}", + get_input_addresses(address_generator, reveal["signatures"])[0], + f"\\x{reveal['body']['dr_pointer']}", + f"\\x{bytes2hex(reveal['body']['reveal'])}", + str(success), + epoch, + ] + + +def parse_tally(txn_hash, tally, epoch): + success, _ = translate_tally(txn_hash, tally["tally"]) + + return [ + f"\\x{txn_hash}", + str([output["pkh"] for output in tally["outputs"]]).replace("'", ""), + str([output["value"] for output in tally["outputs"]]), + f"\\x{tally['dr_pointer']}", + str(tally["error_committers"]), + str(list(set(tally["out_of_consensus"]) - set(tally["error_committers"]))), + f"\\x{bytes2hex(tally['tally'])}", + str(success), + epoch, + ] + + +def parse_stake( + address_generator, + witnet_node, + requested_txns, + txn_hash, + stake, + weight, + epoch, +): + input_addresses = get_input_addresses(address_generator, stake["signatures"]) + + return [ + f"\\x{txn_hash}", + str(input_addresses).replace("'", ""), + str(get_input_values(witnet_node, requested_txns, stake["body"]["inputs"])), + str( + [ + f"\\x{txn_input['output_pointer']}" + for txn_input in stake["body"]["inputs"] + ] + ) + .replace("'", "") + .replace("\\\\", "\\"), + (stake["body"]["change"]["pkh"] if stake["body"]["change"] else None), + (stake["body"]["change"]["value"] if stake["body"]["change"] else None), + weight, + stake["body"]["output"]["key"]["validator"], + stake["body"]["output"]["key"]["withdrawer"], + stake["body"]["output"]["value"], + epoch, + ] + + +def parse_unstake(txn_hash, unstake, weight, epoch): + return [ + f"\\x{txn_hash}", + unstake["body"]["operator"], + unstake["body"]["withdrawal"]["pkh"], + unstake["body"]["withdrawal"]["value"], + unstake["body"]["fee"], + unstake["body"]["nonce"], + weight, + epoch, + ] + + +def get_block_data_from_node(blocks, requested_txns): + address_generator = AddressGenerator("twit") + witnet_node = WitnetNode() + + hashes_seen = set() + block_data = { + "hash_data": [], + "block_data": [], + "mint_data": [], + "value_transfer_data": [], + "data_request_data": [], + "commit_data": [], + "reveal_data": [], + "tally_data": [], + "stake_data": [], + "unstake_data": [], + } + input_txns = set() + + for block_hash in tqdm.tqdm( + blocks, + bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt}", + ): + block = witnet_node.get_block(block_hash)["result"] + + epoch = block["block_header"]["beacon"]["checkpoint"] + txns = block["txns"] + txns_hashes = block["txns_hashes"] + txns_weights = block["txns_weights"] + + dr_weight = block["dr_weight"] + vt_weight = block["vt_weight"] + st_weight = block["st_weight"] if "st_weight" in block else 0 + ut_weight = block["ut_weight"] if "ut_weight" in block else 0 + + block_fees = calculate_block_fees(witnet_node, requested_txns, block) + + # Add block data + block_data["hash_data"].append([f"\\x{block_hash}", "block", epoch]) + block_data["block_data"].append( + [ + f"\\x{block_hash}", + len(txns_hashes["value_transfer"]), + len(txns_hashes["data_request"]), + len(txns_hashes["commit"]), + len(txns_hashes["reveal"]), + len(txns_hashes["tally"]), + len(txns_hashes["stake"]) if "stake" in txns_hashes else 0, + len(txns_hashes["unstake"]) if "unstake" in txns_hashes else 0, + dr_weight, + vt_weight, + st_weight, + ut_weight, + dr_weight + vt_weight + st_weight + ut_weight, + block_fees, + epoch, + block["block_header"]["signals"], + "True" if block["confirmed"] else "False", + "False" if block["confirmed"] else "True", + ] + ) + + # Process mint transaction + mint_hash = txns_hashes["mint"] + hashes_seen.update([mint_hash]) + block_data["hash_data"].append([f"\\x{mint_hash}", "mint_txn", epoch]) + block_data["mint_data"].append( + parse_mint( + address_generator, + mint_hash, + block["block_sig"], + txns["mint"], + epoch, + ) + ) + + # Process value transfer transactions + hashes_seen.update(txns_hashes["value_transfer"]) + for txn_hash, value_transfer, txn_weight in zip( + txns_hashes["value_transfer"], + txns["value_transfer_txns"], + txns_weights["value_transfer"], + ): + block_data["hash_data"].append( + [f"\\x{txn_hash}", "value_transfer_txn", epoch] + ) + + for txn_input in value_transfer["body"]["inputs"]: + input_txns.add(txn_input["output_pointer"].split(":")[0]) + + block_data["value_transfer_data"].append( + parse_value_transfer( + address_generator, + witnet_node, + requested_txns, + txn_hash, + value_transfer, + txn_weight, + epoch, + ) + ) + + # Process data request transactions + protobuf_encoder = ProtobufEncoder(WIP(mockup=True)) + hashes_seen.update(txns_hashes["data_request"]) + for txn_hash, data_request, txn_weight in zip( + txns_hashes["data_request"], + block["txns"]["data_request_txns"], + txns_weights["data_request"], + ): + block_data["hash_data"].append( + [f"\\x{txn_hash}", "data_request_txn", epoch] + ) + + dr_body = data_request["body"] + + for txn_input in dr_body["inputs"]: + input_txns.add(txn_input["output_pointer"].split(":")[0]) + + protobuf_encoder.set_transaction(data_request) + RAD_hash, _ = protobuf_encoder.get_RAD_bytecode(epoch) + DRO_hash, _ = protobuf_encoder.get_DRO_bytecode(epoch) + + if RAD_hash not in hashes_seen: + hashes_seen.add(RAD_hash) + block_data["hash_data"].append( + [f"\\x{RAD_hash}", "RAD_bytes_hash", epoch] + ) + if DRO_hash not in hashes_seen: + hashes_seen.add(DRO_hash) + block_data["hash_data"].append( + [f"\\x{DRO_hash}", "DRO_bytes_hash", epoch] + ) + + block_data["data_request_data"].append( + parse_data_request( + address_generator, + witnet_node, + requested_txns, + txn_hash, + data_request, + txn_weight, + RAD_hash, + DRO_hash, + epoch, + ) + ) + + # Process commit transactions + hashes_seen.update(txns_hashes["commit"]) + for txn_hash, commit in zip( + txns_hashes["commit"], block["txns"]["commit_txns"] + ): + block_data["hash_data"].append([f"\\x{txn_hash}", "commit_txn", epoch]) + + for txn_input in commit["body"]["collateral"]: + input_txns.add(txn_input["output_pointer"].split(":")[0]) + + block_data["commit_data"].append( + parse_commit( + address_generator, + witnet_node, + requested_txns, + txn_hash, + commit, + epoch, + ) + ) + + # Process reveal transactions + hashes_seen.update(txns_hashes["reveal"]) + for txn_hash, reveal in zip( + txns_hashes["reveal"], block["txns"]["reveal_txns"] + ): + block_data["hash_data"].append([f"\\x{txn_hash}", "reveal_txn", epoch]) + block_data["reveal_data"].append( + parse_reveal( + address_generator, + txn_hash, + reveal, + epoch, + ) + ) + + # Process tally transactions + hashes_seen.update(txns_hashes["tally"]) + for txn_hash, tally in zip(txns_hashes["tally"], block["txns"]["tally_txns"]): + block_data["hash_data"].append([f"\\x{txn_hash}", "tally_txn", epoch]) + block_data["tally_data"].append(parse_tally(txn_hash, tally, epoch)) + + # Process stake transactions + if "stake" in txns_hashes: + hashes_seen.update(txns_hashes["stake"]) + for txn_hash, stake, txn_weight in zip( + txns_hashes["stake"], + block["txns"]["stake_txns"], + txns_weights["stake"], + ): + block_data["hash_data"].append([f"\\x{txn_hash}", "stake_txn", epoch]) + + for txn_input in stake["body"]["inputs"]: + input_txns.add(txn_input["output_pointer"].split(":")[0]) + + block_data["stake_data"].append( + parse_stake( + address_generator, + witnet_node, + requested_txns, + txn_hash, + stake, + txn_weight, + epoch, + ) + ) + + # Process unstake transactions + if "unstake" in txns_hashes: + hashes_seen.update(txns_hashes["unstake"]) + for txn_hash, unstake, weight in zip( + txns_hashes["unstake"], + block["txns"]["unstake_txns"], + txns_weights["unstake"], + ): + block_data["hash_data"].append([f"\\x{txn_hash}", "unstake_txn", epoch]) + block_data["unstake_data"].append( + parse_unstake( + txn_hash, + unstake, + weight, + epoch, + ) + ) + + # Filter out input transactions we already saw in one of the processed blocks + input_txns = list(input_txns) + for txns in block_data.values(): + for txn in txns: + if txn[0][2:] in input_txns: + input_txns.remove(txn[0][2:]) + + return block_data, input_txns + + +def get_input_transactions(transactions, requested_txns): + address_generator = AddressGenerator("twit") + protobuf_encoder = ProtobufEncoder(WIP(mockup=True)) + witnet_node = WitnetNode() + + block_data = { + "hash_data": [], + "block_data": [], + "mint_data": [], + "value_transfer_data": [], + "data_request_data": [], + "commit_data": [], + "reveal_data": [], + "tally_data": [], + "stake_data": [], + "unstake_data": [], + } + + blocks_processed = set() + for txn_hash in tqdm.tqdm( + transactions, + bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt}", + ): + input_txn = witnet_node.get_transaction(txn_hash)["result"] + block_hash = input_txn["blockHash"] + epoch = input_txn["blockEpoch"] + + # Add block metadata (if necessary) + if block_hash not in blocks_processed: + block = witnet_node.get_block(block_hash)["result"] + txns_hashes = block["txns_hashes"] + + dr_weight = block["dr_weight"] + vt_weight = block["vt_weight"] + st_weight = block["st_weight"] if "st_weight" in block else 0 + ut_weight = block["ut_weight"] if "ut_weight" in block else 0 + + block_fees = calculate_block_fees(witnet_node, requested_txns, block) + + block_data["block_data"].append( + [ + f"\\x{block_hash}", + len(txns_hashes["value_transfer"]), + len(txns_hashes["data_request"]), + len(txns_hashes["commit"]), + len(txns_hashes["reveal"]), + len(txns_hashes["tally"]), + len(txns_hashes["stake"]) if "stake" in txns_hashes else 0, + len(txns_hashes["unstake"]) if "unstake" in txns_hashes else 0, + dr_weight, + vt_weight, + st_weight, + ut_weight, + dr_weight + vt_weight + st_weight + ut_weight, + block_fees, + epoch, + block["block_header"]["signals"], + "True" if block["confirmed"] else "False", + "False" if block["confirmed"] else "True", + ] + ) + blocks_processed.add(block_hash) + + # Process mint transaction + mint_hash = txns_hashes["mint"] + block_data["hash_data"].append([f"\\x{mint_hash}", "mint_txn", epoch]) + block_data["mint_data"].append( + parse_mint( + address_generator, + mint_hash, + block["block_sig"], + block["txns"]["mint"], + epoch, + ) + ) + + txn_type = list(input_txn["transaction"].keys())[0] + if txn_type == "ValueTransfer": + block_data["hash_data"].append( + [f"\\x{txn_hash}", "value_transfer_txn", epoch] + ) + + value_transfer = input_txn["transaction"]["ValueTransfer"] + + block_data["value_transfer_data"].append( + parse_value_transfer( + address_generator, + witnet_node, + requested_txns, + txn_hash, + value_transfer, + input_txn["weight"], + epoch, + ) + ) + elif txn_type == "DataRequest": + block_data["hash_data"].append( + [f"\\x{txn_hash}", "data_request_txn", epoch] + ) + + data_request = input_txn["transaction"]["DataRequest"] + + protobuf_encoder.set_transaction(data_request) + RAD_hash, _ = protobuf_encoder.get_RAD_bytecode(epoch) + DRO_hash, _ = protobuf_encoder.get_DRO_bytecode(epoch) + + block_data["data_request_data"].append( + parse_data_request( + address_generator, + witnet_node, + requested_txns, + txn_hash, + data_request, + input_txn["weight"], + RAD_hash, + DRO_hash, + epoch, + ) + ) + elif txn_type == "Commit": + block_data["hash_data"].append([f"\\x{txn_hash}", "commit_txn", epoch]) + + commit = input_txn["transaction"]["Commit"] + + block_data["commit_data"].append( + parse_commit( + address_generator, + witnet_node, + requested_txns, + txn_hash, + commit, + epoch, + ) + ) + elif txn_type == "Tally": + block_data["hash_data"].append([f"\\x{txn_hash}", "tally_txn", epoch]) + + tally = input_txn["transaction"]["Tally"] + + block_data["tally_data"].append(parse_tally(txn_hash, tally, epoch)) + elif txn_type == "Stake": + block_data["hash_data"].append([f"\\x{txn_hash}", "stake_txn", epoch]) + + stake = input_txn["transaction"]["Stake"] + + block_data["stake_data"].append( + parse_stake( + address_generator, + witnet_node, + requested_txns, + txn_hash, + stake, + input_txn["weight"], + epoch, + ) + ) + elif txn_type == "Unstake": + block_data["hash_data"].append([f"\\x{txn_hash}", "unstake_txn", epoch]) + + unstake = input_txn["transaction"]["Unstake"] + + block_data["unstake_data"].append( + parse_unstake( + txn_hash, + unstake, + input_txn["weight"], + epoch, + ) + ) + + return block_data + + +def insert_data(database, epoch_data): + connection = sqlite3.connect(database) + cursor = connection.cursor() + + sql = """ + INSERT INTO + hashes + VALUES + (?, ?, ?) + """ + cursor.executemany(sql, epoch_data["hash_data"]) + + sql = """ + INSERT INTO + blocks + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + cursor.executemany(sql, epoch_data["block_data"]) + + sql = """ + INSERT INTO + mint_txns + VALUES + (?, ?, ?, ?, ?) + """ + cursor.executemany(sql, epoch_data["mint_data"]) + + sql = """ + INSERT INTO + value_transfer_txns + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + cursor.executemany(sql, epoch_data["value_transfer_data"]) + + sql = """ + INSERT INTO + data_request_txns + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + cursor.executemany(sql, epoch_data["data_request_data"]) + + sql = """ + INSERT INTO + commit_txns + VALUES + (?, ?, ?, ?, ?, ?, ?) + """ + cursor.executemany(sql, epoch_data["commit_data"]) + + sql = """ + INSERT INTO + reveal_txns + VALUES + (?, ?, ?, ?, ?, ?) + """ + cursor.executemany(sql, epoch_data["reveal_data"]) + + sql = """ + INSERT INTO + tally_txns + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + cursor.executemany(sql, epoch_data["tally_data"]) + + sql = """ + INSERT INTO + stake_txns + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + cursor.executemany(sql, epoch_data["stake_data"]) + + sql = """ + INSERT INTO + unstake_txns + VALUES + (?, ?, ?, ?, ?, ?, ?, ?) + """ + cursor.executemany(sql, epoch_data["unstake_data"]) + + connection.commit() + + +def main(): + parser = argparse.ArgumentParser( + prog="CreateMockupDatabase", + description="Create mockup database for running tests", + ) + parser.add_argument( + "--config-file", + type=str, + default="explorer.toml", + dest="config_file", + ) + args = parser.parse_args() + + # Parse the configuration + config = toml.load(args.config_file) + BlockchainConfig.config = config + + # Create database + database = ( + f"{os.path.realpath(os.path.dirname(__file__))}/mockups/data/database.sqlite3" + ) + if os.path.exists(database): + os.remove(database) + + create_tables(database) + + # Define blocks containing transactions for tests using mockups + blocks = [ + # Mint transaction (pre-wit/2): 55b1e998e00e62a71664b52191450788fe0c4a62bab32cc995cf89281845998f + "efbe6fbdabe9754441e424de95e540ef35da4087f407f0b9b968043ae1c4960a", + # Value transfer (change): de12b689901bb1a9ddd5e3034d6d20146f8ffdf2176438b7513b64775923853c + "e737df5565547ad8136b6aa52ce1f4f234f947168be6763d6ac7d336ee45f2ce", + # Value transfer (no change): e22cad472149be98e113cfdd6b8da1a91195452ffcdf279d0e2b7fa581e8880d + "82a014777204c746245fbf4cdf7048a3ffc266be004621122304c5b40f273641", + # Data request (HTTP-GET, pre-wit/2): d05b3d4622dd7f5bbb54fb597f944343d6d1367286fcef53e071c48531a2caac + "65214181d85e0457db23358e577cf4269a2e0a6c0b8a029c4f99a8fccbc308fa", + # Commits for data request: d05b3d4622dd7f5bbb54fb597f944343d6d1367286fcef53e071c48531a2caac + "e1130ee3282ee37ca32081d5468ff4d4ebfb8eeb23a1e0f279d114369bd88aa2", + # Reveals for data request: d05b3d4622dd7f5bbb54fb597f944343d6d1367286fcef53e071c48531a2caac + "a89c04ff368f322367ac39954a8928aedb60627302ab5808155d364c01f39b20", + # Tally for data request: d05b3d4622dd7f5bbb54fb597f944343d6d1367286fcef53e071c48531a2caac + "acfb91728ffdd9c497c5e9c01ee823f8692f6c1f8bb4832a3658acc424aac864", + # Extra blocks for DRO history + "63f227b6ccac2a67b22fcbf1df018d1c0e56680e247e9fd25a300af07f5ffa9a", + "6d048ed60137b565976b2d5f14d7e6cc5027c439871342342019143dc5506188", + # Extra blocks for RAD history + "b16d2f83b7f087b1796292085cc16b4549c25bb4ca380951d9b55726a1ad92e7", + "943d9718fcac0ba35c0f13ed224774bbd15078117758807b97b6ffd9dcf1a861", + "8fed9c50e9ad39075ceec659b12658c2afb48accdced4ab7fac4307f0a48d523", + "ade52c026bbbbf463cd97324bbbd10e46e6b89956e6361c16cb1a0ed3ba82b9f", + # Mint transaction (post-wit/2): ebda819d3e69db289ed43e66c58c8dfd155c3de1ece316f1bccceca47446e556 + # Data request (HTTP-GET, post-wit/2): 3bf3a7d038ecc3ca3da27083919653453ddce9f1e06a8be10fc360893cd87919 + "40e8448d93d4e28e30e310cb1502918830343711bc673f6dd7d359c15d3442f6", + # Commits for data request: 3bf3a7d038ecc3ca3da27083919653453ddce9f1e06a8be10fc360893cd87919 + "d7569187caac509846bae2d94fffe3c240830b0f3e687bed9591e238c53137bb", + # Reveals for data request: 3bf3a7d038ecc3ca3da27083919653453ddce9f1e06a8be10fc360893cd87919 + "352817e548d5a308eca11a001d637e06a8deec97a00f36e307c8f8be6be582fb", + # Tally for data request: 3bf3a7d038ecc3ca3da27083919653453ddce9f1e06a8be10fc360893cd87919 + "1386fb207837f26ea69ae332ecb8e200b63a8fc35911b1dc3df46f81967cf6ad", + # Data request (RNG, tooManyWitnesses): 81bd191c77e5ce8a795911846c6adf1e0337ba5828e945c7d38531b511478f41 + "f641f12371b6136fadc0444d58dd189c297be6a702aa7d00f9ab28f6634992cb", + # Stake transaction (change): 7fdbfba5239650557dae61e0ae94bec383dcdc96a2e8b7f77a36b916ad1b0a0b + "bae93cc611419c17c0d6b004b218c4c90a7a2220c351607615e919dbb32aeab6", + # Stake transaction (no change): 0ea59be6f3c154836b26387dcdfbb5372f47a338706b8f108da342b699f0f1b3 + "5f14f59d63fc6da7128f12f3d55bd7c6af26c3b7266b1bba12613ac40b14a554", + # Unstake transaction: c66e40e48763678b4ada2442e59b5a04b69a620a2bf72799abf0cd652a5a68e7 + "d25914d1190f1b2dba0cd4119919d9f32e962a6b9d703b224c58ac090c90cd0a", + # Blocks for API tests + "be325f40cf9a05e9066caf39027660d1ef3ba03c135e7667ff7d6d59ae5affc3", + "d3d7e127e1789f9c098eafc584f187555cb1b0e9ef5ab2b99bd0fde9dde989b0", + "70ac2d6d10dbdc99d5258b9ea1275dd3f7e3431efc95703a8b682e470705e1c5", + "06e32259a77efd5125e0e435cfe1eb003af3983e782e083ea7a035b2b4ab21ae", + "f8c50b004b475bb88da1e7e1183906de9506e8779ef5633625d949b83a24dbf1", + "d83762224a9a44bab0055458739bd97c6fe7dbfb955768bdca42157ba1b9116f", + "f993c9526c8c5784852fbc4b66de654aa7bd0a09531d904e16340260d2d1de92", + # Blocks for stake views (twit1pc8jzqph4t0md02e6fgwgsw26yll20p98c3pgh) + "795cc4c0c2e9ff54df6c9b93d2f3548f87118bb20d7936d831134e2336be6e16", + "b63def3b79fdeb86b9ffdb4b472e8fef3550a1cc0396851118c66299a24864cb", + "f60b61ffb71f780a5feab03dbd7f0e0a650290a0dbb8287888fe439a300279a8", + "5ae3f5a4375876dff6168212985f19aa2f36275afe204304ae897ebd74fcd464", + "1c434e5c1b500f85489fe0de90c576cea1c8d2cc0e7ccefa05deba9bb84d58a7", + "94d124e41865847babe1efe0ad0ee5fb6de90d5cc1549527a742d5062c2c3267", + "229a00c51d0f4e4de69b808e0b6bf25dfaab6e22f130d71af987579acfbf1b92", + "6c7118c2dd8fa6a71f509543babb70b1426fe095db08760ac5a3c18e6430514b", + "b357b5bffb815287ed944486edad5d70bbf299d510abe1f086f7d95778cd63fc", + "a7ad269f0a027696a75ff9d9b80c9842a0184e0ab390a289bdd769dd230181a9", + # Blocks for unstake views (twit1f0am8c97q2ygkz3q6jyd2x29s8zaxqlxcqltxx) + "4dda7a67e0548faeb0bdcfd56ec85b0245b5e34ffd8e4f5d53bfd7da4df70545", + "e12da3586652fd66b9f47704ce5afadb4ba348ce8791f9b6419b7806f65c9429", + "464cb477db28ad0288fa6875e1499e14373e81fc052bb89e532bc0bc664af01c", + "fcccc8f0d7246a8b21e93641efaf482f0e9c9dfb24fa99048a5596dad20aa9a3", + "a3cf0abdcd43310fddd7aa330337f8a7b09db7cffdc685e7b12d1b1ad6c3e2b0", + "94150ab5fd4ed773acf37bfe42442e96969eb2ca8674042bf342c5212fa2a77f", + "3d0ba78c2c838c38413e2e3877e9816529451aa27287ee6e0a3dd708e96350f2", + "962bbb92febf38a57183205ae0aa18b7e779dab5a755193bb3c621a5bba57e7c", + "a1530ed89009225efef860aa64f2ebc38f15d5c98224f7191490f79c851b65dd", + "5039888ee1ecc5807fe8e6b64d306293cd51d2f77ea88e35a8bbbb2c35016f43", + # Blocks for data requests created (twit19rnvq8yjrmpa6tjdahct4pd49ha5lmvxjeyfha) + "618f492c12a7500115df8d6f28859f40b8c5ab209981056d328638dad0d554bf", + "908869efd655ee5854af970ac2c8ccdfbcc1f1d00d7e3cdd076a740b4ea09e5a", + "1881f92594596d69d5b06954c7607bc950fea73aa24e62b12ece99e99e8310ef", + "51ce0e234ee3addf8c8ecdcac6edfc83e7cb0874cc3f44d5608a57df5d0669c8", + "00e8ec994c00aaf47217f18355d5bbda3a79d7bd6d0da653104dc02694f40606", + "a7c2b91babb1a5438649de9ae5335abf97a37e87ba61096db1ef29eaf0d6f28a", + "744cd2f1cac4db669eefc4f2b9261e4f9e335bebb3f615da3a65f3aa385773ea", + "4bbe4da4a38b69714dec8a692bc28fcbf928f9e937a128708d286542d6c1261d", + "f3dea1ec4687408c88cd928c04e2216758e6d580be1d3359b78047445565f27d", + "7c2da9767443895d6734b8c5351651faf7e34d7800390512fba897f6fd61b732", + "c566a897fa0b17285cb399ac0a02580b2042b24b48a9bfa2c5d08162acc4070e", + "91b99a3d2c245b5acde86037972eff3ca9840d8036312481fc99f3f37eb1f019", + "4723a5b6453317c1498f6cbc7acbd06e1d37eadde47c4afcd3f205af3148a1dd", + "b393029ef97f8d383eb1224da9c69ded46eee2e582675f191d234def95069765", + "1889c6830e542ca96dc0cd37f027b556271447495c9a28489e886533995f03a0", + "d964dcf3007b79a7d8ffe943da507d88c295a2c6bfb719e49fa84465e5e4629d", + "e8ad17773ef86e2b7eca020becf8dd0967c8adda4c9af9c309344524e2afd3f7", + "91722537ea0ae4b24562af091b8f25c2356d27f39c5d62c7723faf87a50da09b", + "be2960f91c1cafba5fd5789042400885e85bfc37f4436e4ad9932f1487dbc882", + "c56fefced51d4f168a234b58a19b8197712ac03a64b4e8635286381c5db90e3d", + # Blocks for data requests solved (twit1a0dkarqa5w36quq37tvyxthlhl5049wg63z3zu) + "c2bd2164686f638f56de50e42211cf9d9d9749be0c533fe1b83bfe5f0b5ddff4", + "b166923b6d09e1f290e9bc77981d048710b9983b743dc3d678a5add913ad6c84", + "8fe004a453259b5f2f77416df9d80d649f19bfc680110d2297530520b2abf30b", + "ccda8d2bdf8526602fdc20fdfc977fc62baae340306b43289ddf7f5514d8a754", + "821a27b283c2f29dec0c34e81376a7846ce5b77e249ad0348d74841aeb0ef7ca", + "0022665de7f57e83e10124c39d3c707143c93148d198d312bb0f3b8a0598a776", + "0d55ae3a454fd7ce9679db924efc9243f7a3a57432a411ed3284c6dfd649c93d", + "b47c4be0d3bb8bbb289ca5b23a19bbed612d88a69959fd94f4322b98a1eaa422", + "f2e9716e00028b1e0453d71cc8241a7f4e77a9b9d5449491e8b148279314d25e", + "c44fcd2b036afcc0950b55f92e5b67f3c9e085a8575a52db27775b3817b0aa11", + "851dfe4973ed5a5057589e2a920d28f450082505447bc9afcefbc2ffd148f2b6", + "3e9f0fb4a83542fa8824b0c30f99a13f1cfc0a17aa12643f55b522a15dcfebc3", + "793459983cf7e4ff99a49084312563a578c8d86bae1750b469b186db43178466", + "4a48653480311669181dbb1ad45b8e781bb994cfbe82c6f1abadabc330801e6e", + "49e646a7581746127bdb1480a7b853929f75c7387b1a5a7d9384ab181a2f081b", + "8e550ea561d6833645caeb7b16f78d9c933b92af089f4ab2cbae780b43c3cd5e", + "9d712d32a7703aaf352d3c1edf413042f2fd1219d3d8c689f40066172e16a889", + "ff2e8ca9f7c8fad66757aaeab4ffbd62209882507f82070d4b9124ee5da728ad", + "75f83aae492d00b27595cdf49eff7834bbcc39e85eb2ad8ac97801008bd95bf1", + "cb3d0b7f6f2de20305951b2b08b010d216e5efefb7d996086d9d0e36fc9e0828", + "0a81063013e59fa48abe1aab2b71d101bd821de450d6f43ac0fc25a72e53b5c8", + "09902eb9afd1ee10384fce301d831e055065ee5956ecabc324df3b5d5cde1ea0", + "bc531265f3d7cbcce253d6a39ef4d0d006f9d55c3484bb45b4c0323461a8afb5", + "faa1f16ce532b13d18f05e8b69b224c7760f73c787cc38010f81dbf4082a3656", + "80497e52923082a5210b938506e0584735b22cc6584abe45d78c83a95041feb1", + "63ab64e57228c36d57464dde2dfbdc00908728da51516e5113848c4f1fdd7bdd", + "3aca83907e9719ae39bc532700e97b6f31b980159186c7c0faa9948231d27696", + "626acdf90845f5f250ca516ef2b71c0c2c432274b3847e62c21d12cdcafee42a", + ] + + # First insert the WIPs in the database + insert_wips(database) + + # Now, create a WIP object using the inserted data + BlockchainConfig.wip = WIP(mockup=True) + + # Parse and insert data from above defined blocks + print("Getting full block data") + requested_txns = {} + block_data, input_txns = get_block_data_from_node(blocks, requested_txns) + insert_data(database, block_data) + + # Parse and insert all transactions used as input in the transactions extracted from above blocks + print("Getting all input transactions") + inputs_data = get_input_transactions(input_txns, requested_txns) + insert_data(database, inputs_data) + + insert_address_data(database) + + insert_consensus_constants(database) + + insert_network_stats(database) + + insert_pending_transaction(database) + + +if __name__ == "__main__": + main() diff --git a/mockups/data/create_mockup_database.py b/mockups/data/create_mockup_database.py deleted file mode 100644 index 36c19301..00000000 --- a/mockups/data/create_mockup_database.py +++ /dev/null @@ -1,2206 +0,0 @@ -import argparse -import json -import sqlite3 - -import toml - -from blockchain.config import BlockchainConfig -from blockchain.objects.wip import WIP -from blockchain.transactions.reveal import translate_reveal -from blockchain.transactions.tally import translate_tally -from node.witnet_node import WitnetNode -from util.address_generator import AddressGenerator -from util.data_transformer import bytes2hex -from util.protobuf_encoder import ProtobufEncoder - - -def create_tables(database): - connection = sqlite3.connect(database) - cursor = connection.cursor() - tables = [ - """ - CREATE TABLE IF NOT EXISTS addresses ( - id INT, - address TEXT PRIMARY KEY, - label TEXT, - active INT, - block INT, - mint INT, - value_transfer INT, - data_request INT, - 'commit' INT, - reveal INT, - tally INT - ) - """, - """ - CREATE TABLE IF NOT EXISTS hashes ( - hash TEXT PRIMARY KEY, - type TEXT NOT NULL, - epoch INT - ) - """, - """ - CREATE TABLE IF NOT EXISTS blocks ( - block_hash TEXT PRIMARY KEY, - value_transfer INT NOT NULL, - data_request INT NOT NULL, - 'commit' INT NOT NULL, - reveal INT NOT NULL, - tally INT NOT NULL, - dr_weight INT NOT NULL, - vt_weight INT NOT NULL, - block_weight INT NOT NULL, - epoch INT NOT NULL, - tapi_signals INT, - confirmed TEXT NOT NULL, - reverted TEXT - ) - """, - """ - CREATE TABLE IF NOT EXISTS mint_txns ( - txn_hash TEXT PRIMARY KEY, - miner TEXT NOT NULL, - output_addresses TEXT NOT NULL, - output_values TEXT NOT NULL, - epoch INT NOT NULL - ) - """, - """ - CREATE TABLE IF NOT EXISTS value_transfer_txns ( - txn_hash TEXT PRIMARY KEY, - input_addresses TEXT NOT NULL, - input_values TEXT NOT NULL, - input_utxos TEXT NOT NULL, - output_addresses TEXT NOT NULL, - output_values TEXT NOT NULL, - timelocks TEXT NOT NULL, - weight INT NOT NULL, - epoch INT NOT NULL - ) - """, - """ - CREATE TABLE IF NOT EXISTS data_request_txns ( - txn_hash TEXT PRIMARY KEY, - input_addresses TEXT NOT NULL, - input_values TEXT NOT NULL, - input_utxos TEXT NOT NULL, - output_address TEXT, - output_value INT, - witnesses INT NOT NULL, - witness_reward INT NOT NULL, - collateral INT NOT NULL, - consensus_percentage INT NOT NULL, - commit_and_reveal_fee INT NOT NULL, - weight INT NOT NULL, - kinds TEXT NOT NULL, - urls TEXT NOT NULL, - headers TEXT NOT NULL, - bodies TEXT NOT NULL, - scripts TEXT NOT NULL, - aggregate_filters TEXT NOT NULL, - aggregate_reducer TEXT NOT NULL, - tally_filters TEXT NOT NULL, - tally_reducer TEXT NOT NULL, - RAD_bytes_hash TEXT NOT NULL, - DRO_bytes_hash TEXT NOT NULL, - epoch INT NOT NULL - ) - """, - """ - CREATE TABLE IF NOT EXISTS commit_txns ( - txn_hash TEXT PRIMARY KEY, - txn_address TEXT NOT NULL, - input_values TEXT NOT NULL, - input_utxos TEXT NOT NULL, - output_value INT, - data_request TEXT NOT NULL, - epoch INT NOT NULL - ) - """, - """ - CREATE TABLE IF NOT EXISTS reveal_txns ( - txn_hash TEXT PRIMARY KEY, - txn_address TEXT NOT NULL, - data_request TEXT NOT NULL, - result TEXT NOT NULL, - success TEXT NOT NULL, - epoch INT NOT NULL - ) - """, - """ - CREATE TABLE IF NOT EXISTS tally_txns ( - txn_hash TEXT PRIMARY KEY, - output_addresses TEXT NOT NULL, - output_values TEXT NOT NULL, - data_request TEXT NOT NULL, - error_addresses TEXT NOT NULL, - liar_addresses TEXT NOT NULL, - result TEXT NOT NULL, - success TEXT NOT NULL, - epoch INT NOT NULL - ) - """, - """ - CREATE TABLE IF NOT EXISTS data_request_mempool ( - timestamp INT NOT NULL, - fee TEXT NOT NULL, - weight TEXT NOT NULL - ) - """, - """ - CREATE TABLE IF NOT EXISTS value_transfer_mempool ( - timestamp INT NOT NULL, - fee TEXT NOT NULL, - weight TEXT NOT NULL - ) - """, - """ - CREATE TABLE IF NOT EXISTS wips ( - id INT, - title TEXT NOT NULL, - description TEXT NOT NULL, - urls TEXT NOT NULL, - activation_epoch INT, - tapi_start_epoch INT, - tapi_stop_epoch INT, - tapi_bit INT, - tapi_json TEXT - ) - """, - """ - CREATE TABLE IF NOT EXISTS consensus_constants ( - key TEXT PRIMARY KEY, - int_val INT, - str_val TEXT - ) - """, - """ - CREATE TABLE IF NOT EXISTS network_stats ( - stat TEXT NOT NULL, - from_epoch INT, - to_epoch INT, - data TEXT NOT NULL - ) - """, - ] - for sql in tables: - cursor.execute(sql) - connection.commit() - - -def insert_address_data(database): - address_data = [ - [ - 1, - "wit1drcpu0xc2akfcqn8r69vw70pj8fzjhjypdcfsq", - "drcpu0", - 2024098, - 68, - 68, - 90, - 4272, - 1866, - 1866, - 3051, - ], - [ - 2, - "wit1drcpu2gf386tm29mh62cce0seun76rrvk5nca6", - "drcpu2", - 1657960, - 21, - 21, - 29, - 2246, - 658, - 658, - 1406, - ], - [ - 3, - "wit1z8p6qp2f5z6j2nfex3kme5qee0vurpvd8yn5hj", - None, - 1075951, - 133, - 131, - 7, - 0, - 744, - 743, - 744, - ], - [ - 4, - "wit1xshwjs5huexwfxkldvue7kc6cx230vuhtmme2s", - None, - 1100848, - 130, - 124, - 8, - 0, - 1278, - 1275, - 1278, - ], - [ - 5, - "wit17ue5kphnvajes4y05s525e6y9hjr48tpxc3ruc", - None, - 1113874, - 126, - 125, - 9, - 0, - 851, - 851, - 851, - ], - [ - 6, - "wit1my5tgl0r3lsft38kz748zaa7z48dd9994aq895", - None, - 1100849, - 122, - 113, - 13, - 0, - 1354, - 1352, - 1354, - ], - [ - 7, - "wit1d90hma685ghdw33c30svx9889sdckw7kq4j4uf", - None, - 1110639, - 120, - 119, - 9, - 0, - 738, - 738, - 738, - ], - [ - 8, - "wit1xc6002zplgwjdhgnrdxu9gpsg6d9czueeshnsz", - None, - 1088845, - 118, - 106, - 7, - 0, - 951, - 951, - 951, - ], - [ - 9, - "wit1azrj8h2mg6dnq7nem8cp7c2mqcs887djd4e2wh", - None, - 1101872, - 118, - 115, - 12, - 0, - 1401, - 1399, - 1401, - ], - [ - 10, - "wit1j3eyct469cpkz63kxx7mhffhltv5q7v45mgda3", - None, - 1110633, - 117, - 113, - 9, - 0, - 850, - 846, - 850, - ], - [ - 11, - "wit1dvj7cshhvcqttua9afmlfkhk7vwvh0qwfjnwgc", - None, - 1100843, - 116, - 104, - 8, - 0, - 773, - 773, - 773, - ], - [ - 12, - "wit1v9emzryhvu9czp39tyaz76de056g9wdtk5cfcz", - None, - 1088839, - 111, - 107, - 9, - 0, - 1036, - 1035, - 1036, - ], - [ - 13, - "wit1yr9807edzm4l4k7thmw7duufvmm4mkzgqfsnr6", - None, - 1845148, - 47, - 47, - 6, - 0, - 3181, - 3181, - 3181, - ], - [ - 14, - "wit1ajwk8tcajkwuqq5984rt07km7w9etgrvn59kfa", - None, - 1842795, - 73, - 73, - 5, - 0, - 2977, - 2975, - 2977, - ], - [ - 15, - "wit1pr02yrydlm0qd7jhtj0mp4355aj2g3gcy9smaq", - None, - 1824847, - 64, - 64, - 6, - 0, - 2862, - 2861, - 2862, - ], - [ - 16, - "wit15wd25cpstddkvxvfdzydcsnwym3d4mvwhjn2u2", - None, - 1856712, - 41, - 41, - 7, - 0, - 2830, - 2826, - 2830, - ], - [ - 17, - "wit1l3yps6rl2ct8tleh2v632mcwts5s4cuhrvzmmu", - None, - 1819644, - 52, - 52, - 7, - 0, - 2792, - 2785, - 2792, - ], - [ - 18, - "wit15k0huw65x8wkq3p06dmqxf3a46cdhkuljculsm", - None, - 1389040, - 30, - 30, - 7, - 0, - 2767, - 2762, - 2767, - ], - [ - 19, - "wit1nwgm7dz3m339h3uxhyflvr6yfutk7kvfy6auff", - None, - 1258429, - 22, - 22, - 8, - 0, - 2716, - 2709, - 2716, - ], - [ - 20, - "wit1w2hxy8h86p43wx9g722mx8ygs0dsdqlut3n7gw", - None, - 1393126, - 33, - 33, - 6, - 0, - 2700, - 2698, - 2700, - ], - [ - 21, - "wit16m7pxuajzs8jsgwqnukqk39v3qc8dxypqahqdj", - None, - 1259231, - 34, - 34, - 6, - 0, - 2644, - 2639, - 2644, - ], - [ - 22, - "wit15rv5eypwq54u6u3xc2ckd7vfyj53nnddmrmryt", - None, - 2048900, - 62, - 62, - 7, - 0, - 3021, - 3019, - 3021, - ], - [ - 23, - "wit133wnd4ueheeme4dxjjy052s73sjf8uslah70s4", - None, - 1071415, - 26, - 26, - 2, - 46955, - 340, - 340, - 29808, - ], - [ - 24, - "wit1k6vpqaajekgyx5whdp4ag7dp2h2627dcfjyngs", - None, - 1071457, - 13, - 13, - 2, - 0, - 441, - 441, - 441, - ], - [ - 25, - "wit18j3jsfn6y6lc43v4x5tn3lgk8vzlj37hx6esqy", - None, - 1071481, - 12, - 12, - 2, - 0, - 503, - 501, - 503, - ], - [ - 26, - "wit1aufqegyctt7h64eyyp2u6a68ultgkla5gnsxw8", - None, - 1071495, - 16, - 16, - 2, - 0, - 353, - 353, - 353, - ], - [ - 27, - "wit14ef2z0l3l9plkuuqvcsqnq2azc86c2v7udjfeg", - None, - 1071507, - 10, - 10, - 2, - 0, - 278, - 278, - 278, - ], - [ - 28, - "wit1dsxjndrkhpmgmd6nmt95nvmtqf667e5pmq2eqr", - None, - 1071527, - 11, - 11, - 2, - 0, - 164, - 164, - 164, - ], - [ - 29, - "wit1ewhsfsjz5gdern8kaz8x5yd9ph04lyg3ewxssc", - None, - 1071543, - 4, - 4, - 3, - 0, - 104, - 103, - 104, - ], - [ - 30, - "wit1g5n9m4s0lqa2am0edgfevnppu8y207yzjdhqag", - None, - 1071547, - 9, - 9, - 2, - 0, - 211, - 211, - 211, - ], - [ - 31, - "wit1r7gszcq5hu2xvxhpufgs56c0m47tuwtnl3qdpa", - None, - 1071554, - 8, - 8, - 2, - 0, - 261, - 261, - 261, - ], - [ - 32, - "wit10c45atl93vaudvpcmqtl8pqgt5lwzkk9r9mvrs", - None, - 1071563, - 7, - 7, - 3, - 0, - 224, - 224, - 224, - ], - [ - 33, - "wit1a82dxlj8cy3afpxna6kqgq2r9plraf9raqq35m", - None, - 1071421, - 27, - 27, - 2, - 0, - 1121, - 1121, - 1121, - ], - [ - 34, - "wit1gzntj0duaqjgexcjnwhgww9f564l7edx0khl6y", - None, - 1071434, - 22, - 22, - 2, - 0, - 348, - 348, - 348, - ], - [ - 35, - "wit1ccm40u8d8z6ps28tkx6f6ruh340l77psgwhf95", - None, - 1071451, - 17, - 17, - 2, - 0, - 361, - 360, - 361, - ], - [ - 36, - "wit1m6xt7zh3km2u9xzceqzaepkl5nj5qr4ss8zfed", - None, - 1071473, - 21, - 21, - 2, - 0, - 525, - 525, - 525, - ], - [ - 37, - "wit1zxg8m7t6rqs0mpptcmmd8fr5hxe3hu7e849xr8", - None, - 1071488, - 6, - 6, - 3, - 0, - 99, - 99, - 99, - ], - [ - 38, - "wit1m3u3hhs9fvqgt5a8d0zm7w7mhsytgz53pyf9hz", - None, - 1071761, - 14, - 14, - 2, - 0, - 210, - 209, - 210, - ], - [ - 39, - "wit1xe6j5kf8a20xvhqjf7jsej06k45pxhu99u6dza", - None, - 1071750, - 20, - 20, - 3, - 0, - 756, - 754, - 756, - ], - [ - 40, - "wit1kzmck6qyy503lme89lj02jxc3nuj45wg4l8r4c", - None, - 1071734, - 14, - 14, - 2, - 0, - 398, - 398, - 398, - ], - [ - 41, - "wit1e49dg9me24esfmz8gkvhhszr7dvasc77mmq6nj", - None, - 1071421, - 9, - 9, - 4, - 0, - 145, - 145, - 145, - ], - [ - 42, - "wit1astv06rz2m4pg9gaq5l79k34sh9w0j5nj2xe6k", - None, - 1071426, - 29, - 29, - 2, - 0, - 573, - 573, - 573, - ], - [ - 43, - "wit19ejd5fgeypn9d8f6r8st2jkdx89e3vmp4eu3zv", - None, - 1071425, - 21, - 21, - 2, - 0, - 473, - 473, - 473, - ], - [ - 44, - "wit1m5wsdsv7ard8va55tut5jk06nrx2gmxhgphqhl", - None, - 1071428, - 14, - 14, - 2, - 0, - 300, - 300, - 300, - ], - [ - 45, - "wit1ulra63922mnls2rvktaqh4hhrruama9eqjnkc8", - None, - 1071433, - 5, - 5, - 4, - 0, - 179, - 179, - 179, - ], - [ - 46, - "wit1n4gsyx57khmancx0cmzg2q975vvqayq55rlg0y", - None, - 1071436, - 21, - 21, - 2, - 0, - 509, - 498, - 509, - ], - [ - 47, - "wit1fz0wz7dcpanadsdlke75zgjlhqxuv2vusvte8f", - None, - 1071434, - 11, - 11, - 2, - 0, - 123, - 122, - 123, - ], - [ - 48, - "wit140uxs8r7asudphc7zzqc2ze8fk5ytkd6auxjf4", - None, - 1071440, - 7, - 7, - 2, - 0, - 151, - 151, - 151, - ], - [ - 49, - "wit1mts8na78rrdg9j405uh5t9lklkpsdfm8tfze95", - None, - 1071441, - 15, - 15, - 2, - 0, - 307, - 307, - 307, - ], - [ - 50, - "wit1k58yzyjse9frx5yx7xfg0g43mnuv5xw0y4venf", - None, - 1259154, - 35, - 35, - 3, - 0, - 1010, - 1010, - 1010, - ], - [ - 51, - "wit1epgja4fpkyupwlxjawnth39usajywqdh9cxxlx", - None, - 1071430, - 21, - 21, - 2, - 0, - 101, - 101, - 101, - ], - [ - 52, - "wit1gue84sf650hns8qsq4kn2x7awy29mrhdexdste", - None, - 1071439, - 31, - 31, - 2, - 0, - 819, - 818, - 819, - ], - ] - - sql = """ - INSERT INTO - addresses - VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """ - connection = sqlite3.connect(database) - cursor = connection.cursor() - cursor.executemany(sql, address_data) - connection.commit() - - -def insert_consensus_constants(database): - consensus_constants = [ - ["activity_period", 2000, None], - [ - "bootstrap_hash", - None, - "[666564676f6573627272727c2f3030312f3738392f3432382f6130312e676966]", - ], - [ - "bootstrapping_committee", - None, - "[wit1g0rkajsgwqux9rnmkfca5tz6djg0f87x7ms5qx,wit1cyrlc64hyu0rux7hclmg9rxwxpa0v9pevyaj2c,wit1asdpcspwysf0hg5kgwvgsp2h6g65y5kg9gj5dz,wit13l337znc5yuualnxfg9s2hu9txylntq5pyazty,wit17nnjuxmfuu92l6rxhque2qc3u2kvmx2fske4l9,wit1etherz02v4fvqty6jhdawefd0pl33qtevy7s4z,wit1drcpu0xc2akfcqn8r69vw70pj8fzjhjypdcfsq,wit1gxf0ca67vxtg27kkmgezg7dd84hwmzkxn7c62x,wit1hujx8v0y8rzqchmmagh8yw95r943cdddnegtgc,wit1yd97y52ezvhq4kzl6rph6d3v6e9yya3n0kwjyr,wit1fn5yxmgkphnnuu6347s2dlqpyrm4am280s6s9t,wit12khyjjk0s2hyuzyyhv5v2d5y5snws7l58z207g]", - ], - ["checkpoint_zero_timestamp", 1602666000, None], - ["checkpoints_period", 45, None], - ["collateral_age", 1000, None], - ["collateral_minimum", 1000000000, None], - ["epochs_with_minimum_difficulty", 2000, None], - ["extra_rounds", 3, None], - [ - "genesis_hash", - None, - "[6ca267d9accde3336739331d42d63509b799c6431e8d02b2d2cc9d3943d7ab02]", - ], - ["halving_period", 3500000, None], - ["initial_block_reward", 250000000000, None], - ["max_dr_weight", 80000, None], - ["max_vt_weight", 20000, None], - ["minimum_difficulty", 2000, None], - ["mining_backup_factor", 8, None], - ["mining_replication_factor", 3, None], - ["reputation_expire_alpha_diff", 20000, None], - ["reputation_issuance", 1, None], - ["reputation_issuance_stop", 1048576, None], - ["reputation_penalization_factor", 50, None], - ["superblock_committee_decreasing_period", 5, None], - ["superblock_committee_decreasing_step", 5, None], - ["superblock_period", 10, None], - ["superblock_signing_committee_size", 100, None], - ] - sql = """ - INSERT INTO - consensus_constants - VALUES - (?, ?, ?) - """ - connection = sqlite3.connect(database) - cursor = connection.cursor() - cursor.executemany(sql, consensus_constants) - connection.commit() - - -def insert_network_stats(database): - network_stats = [ - [ - "rollbacks", - None, - None, - [ - [1667618550, 1443390, 1443971, 582], - [1667577600, 1442480, 1443101, 622], - [1667403900, 1438620, 1442461, 3842], - ], - ], - ["epoch", None, None, 2024098], - [ - "miners", - None, - None, - { - "amount": 416061, - "top-100": [ - [3, 133], - [4, 130], - [5, 126], - [6, 122], - [7, 120], - [8, 118], - [9, 118], - [10, 117], - [11, 116], - [12, 111], - ], - }, - ], - [ - "data_request_solvers", - None, - None, - { - "amount": 574157, - "top-100": [ - [13, 3181], - [14, 2977], - [15, 2862], - [16, 2830], - [17, 2792], - [18, 2767], - [19, 2716], - [20, 2700], - [21, 2644], - [22, 2597], - ], - }, - ], - [ - "miners", - 1000000, - 1001000, - { - "23": 1, - "24": 1, - "25": 1, - "26": 1, - "27": 1, - "28": 1, - "29": 1, - "30": 1, - "31": 1, - "32": 1, - }, - ], - [ - "miners", - 1001000, - 1002000, - { - "33": 1, - "34": 1, - "35": 2, - "36": 1, - "37": 1, - "38": 1, - "26": 1, - "39": 1, - "40": 1, - "30": 1, - }, - ], - [ - "data_request_solvers", - 1000000, - 1001000, - { - "41": 1, - "42": 4, - "43": 2, - "44": 1, - "45": 9, - "46": 5, - "47": 1, - "34": 1, - "48": 1, - "49": 2, - }, - ], - [ - "data_request_solvers", - 1001000, - 1002000, - { - "50": 4, - "41": 9, - "42": 20, - "43": 10, - "51": 1, - "45": 16, - "46": 16, - "34": 7, - "52": 1, - "49": 7, - }, - ], - [ - "staking", - None, - None, - { - "ars": [ - 69270770065, - 242603004906, - 302440834321, - 384493409284, - 501020356631, - 595242122870, - 1015683336066, - 1141982577664, - 1330485899039, - ], - "trs": [ - 215951770049, - 301726789939, - 384776953771, - 492547412632, - 579258511220, - 807292242377, - 1100113261644, - 1216784921902, - 1424372511663, - ], - "percentiles": [90, 80, 70, 60, 50, 40, 30, 20, 10], - }, - ], - [ - "data_requests", - 1000000, - 1001000, - [ - 1066, - 1057, - 3909, - 0, - 5, - {"2": 5, "10": 813, "100": 248}, - {"1": 248, "500000": 5, "1000000": 813}, - {"1000000000": 5, "2500000000": 248, "5000000000": 813}, - ], - ], - [ - "data_requests", - 1001000, - 1002000, - [ - 1079, - 1073, - 4110, - 0, - 5, - {"2": 5, "10": 826, "100": 248}, - {"1": 248, "500000": 5, "1000000": 826}, - {"1000000000": 5, "2500000000": 248, "5000000000": 826}, - ], - ], - [ - "data_requests", - 2023000, - 2024000, - [ - 748, - 740, - 2478, - 77, - 1, - {"2": 1, "10": 500, "100": 247}, - {"1": 247, "500000": 1, "1000000": 500}, - {"1000000000": 1, "2500000000": 247, "5000000000": 500}, - ], - ], - [ - "data_requests", - 2024000, - 2025000, - [ - 1011, - 988, - 4012, - 97, - 1, - {"2": 1, "10": 764, "100": 246}, - {"1": 246, "500000": 1, "1000000": 764}, - {"1000000000": 1, "2500000000": 246, "5000000000": 764}, - ], - ], - ["lie_rate", 1000000, 1001000, [32940, 367, 310, 239]], - ["lie_rate", 1001000, 1002000, [33070, 271, 321, 220]], - ["lie_rate", 2023000, 2024000, [29702, 305, 544, 98]], - ["lie_rate", 2024000, 2025000, [32242, 654, 594, 135]], - ["burn_rate", 1000000, 1001000, [0, 0]], - ["burn_rate", 1001000, 1002000, [0, 0]], - ["burn_rate", 2023000, 2024000, [0, 543000000000]], - ["burn_rate", 2024000, 2025000, [0, 550500000000]], - ["value_transfers", 1000000, 1001000, [312]], - ["value_transfers", 1001000, 1002000, [266]], - ["value_transfers", 2023000, 2024000, [57]], - ["value_transfers", 2024000, 2025000, [98]], - ] - - sql = """ - INSERT INTO - network_stats - VALUES - (?, ?, ?, ?) - """ - connection = sqlite3.connect(database) - cursor = connection.cursor() - cursor.executemany( - sql, [[ns[0], ns[1], ns[2], json.dumps(ns[3])] for ns in network_stats] - ) - connection.commit() - - -def insert_pending_transaction(database): - connection = sqlite3.connect(database) - cursor = connection.cursor() - - pending_data_requests = [ - [1696016220, [27379], [1]], - [1696016340, [3, 14021], [1, 1]], - [1696016520, [3, 27334], [1, 1]], - [1696016580, [27871], [1]], - [1696016640, [27364, 27379], [1, 1]], - [1696016700, [3], [1]], - [1696016760, [13928], [1]], - [1696016880, [3], [1]], - [1696017000, [14011, 27379], [1, 1]], - ] - - sql = """ - INSERT INTO - data_request_mempool - VALUES - (?, ?, ?) - """ - cursor.executemany( - sql, - [ - [pdr[0], json.dumps(pdr[1]), json.dumps(pdr[2])] - for pdr in pending_data_requests - ], - ) - - pending_value_transfers = [ - [1696016160, [96, 120, 240, 240, 360, 360, 360], [1, 1, 2, 2, 3, 3, 3]], - [1696016640, [120], [1]], - [1696016700, [98], [1]], - [1696016760, [119], [1]], - [1696016820, [120], [1]], - [1696016880, [77], [1]], - [1696016940, [57], [1]], - [1696017000, [37], [1]], - ] - - sql = """ - INSERT INTO - value_transfer_mempool - VALUES - (?, ?, ?) - """ - cursor.executemany( - sql, - [ - [pvt[0], json.dumps(pvt[1]), json.dumps(pvt[2])] - for pvt in pending_value_transfers - ], - ) - - connection.commit() - - -def insert_wips(database): - wips = [ - [ - 1, - "WIP0008", - "Limit data request concurrency", - ["https://github.com/witnet/WIPs/blob/master/wip-0008.md"], - 192000, - None, - None, - None, - None, - ], - [ - 2, - "WIP0009-0011-0012", - "Adjust mining probability (WIP0009), improve superblock voting (WIP0011) and set minimum mining difficulty (WIP0012)", - [ - "https://github.com/witnet/WIPs/blob/master/wip-0009.md,https://github.com/witnet/WIPs/blob/master/wip-0011.md,https://github.com/witnet/WIPs/blob/master/wip-0012.md" - ], - 376320, - None, - None, - None, - None, - ], - [ - 3, - "THIRD_HARD_FORK", - "Set a maximum eligibility for data requests", - ["https://github.com/witnet/witnet-rust/pull/1957"], - 445440, - None, - None, - None, - None, - ], - [ - 4, - "WIP0014-0016", - "Activation of TAPI itself (WIP0014) and setting a minimum data request mining difficulty (WIP0016)", - [ - "https://github.com/witnet/WIPs/blob/master/wip-0014.md,https://github.com/witnet/WIPs/blob/master/wip-0016.md" - ], - 549141, - 522240, - 549120, - 0, - None, - ], - [ - 5, - "WIP0017-0018-0019", - "Add a median RADON reducer (WIP0017), modify the UnhandledIntercept RADON error (WIP0018) and add RNG functionality to Witnet (WIP0019)", - [ - "https://github.com/witnet/WIPs/blob/master/wip-0017.md,https://github.com/witnet/WIPs/blob/master/wip-0018.md,https://github.com/witnet/WIPs/blob/master/wip-0019.md" - ], - 683541, - 656640, - 683520, - 1, - None, - ], - [ - 6, - "WIP0020-0021", - "Add support HTTP-POST (WIP0020) and add an XML parsing operator (WIP0021)", - [ - "https://github.com/witnet/WIPs/blob/master/wip-0020.md,https://github.com/witnet/WIPs/blob/master/wip-0021.md" - ], - 1059861, - 1032960, - 1059840, - 2, - None, - ], - [ - 7, - "WIP0022 (defeated)", - "Set a data request reward collateral ratio", - ["https://github.com/witnet/WIPs/blob/master/wip-0022.md"], - None, - 1655120, - 1682000, - 3, - { - "bit": 3, - "urls": ["https://github.com/witnet/WIPs/blob/master/wip-0022.md"], - "rates": [ - { - "global_rate": 1.171875, - "periodic_rate": 31.5, - "relative_rate": 31.5, - }, - { - "global_rate": 2.392113095238095, - "periodic_rate": 32.800000000000004, - "relative_rate": 32.15, - }, - { - "global_rate": 3.616071428571429, - "periodic_rate": 32.9, - "relative_rate": 32.4, - }, - { - "global_rate": 4.769345238095238, - "periodic_rate": 31.0, - "relative_rate": 32.05, - }, - { - "global_rate": 6.045386904761905, - "periodic_rate": 34.300000000000004, - "relative_rate": 32.5, - }, - { - "global_rate": 7.3065476190476195, - "periodic_rate": 33.900000000000006, - "relative_rate": 32.733333333333334, - }, - { - "global_rate": 8.537946428571429, - "periodic_rate": 33.1, - "relative_rate": 32.785714285714285, - }, - { - "global_rate": 9.873511904761905, - "periodic_rate": 35.9, - "relative_rate": 33.175, - }, - { - "global_rate": 11.116071428571427, - "periodic_rate": 33.4, - "relative_rate": 33.2, - }, - { - "global_rate": 12.566964285714285, - "periodic_rate": 39.0, - "relative_rate": 33.78, - }, - { - "global_rate": 14.166666666666666, - "periodic_rate": 43.0, - "relative_rate": 34.61818181818182, - }, - { - "global_rate": 15.982142857142856, - "periodic_rate": 48.8, - "relative_rate": 35.8, - }, - { - "global_rate": 17.87202380952381, - "periodic_rate": 50.8, - "relative_rate": 36.95384615384615, - }, - { - "global_rate": 19.750744047619047, - "periodic_rate": 50.5, - "relative_rate": 37.92142857142857, - }, - { - "global_rate": 21.648065476190474, - "periodic_rate": 51.0, - "relative_rate": 38.79333333333334, - }, - { - "global_rate": 23.645833333333332, - "periodic_rate": 53.7, - "relative_rate": 39.725, - }, - { - "global_rate": 25.691964285714285, - "periodic_rate": 55.00000000000001, - "relative_rate": 40.62352941176471, - }, - { - "global_rate": 27.67857142857143, - "periodic_rate": 53.400000000000006, - "relative_rate": 41.333333333333336, - }, - { - "global_rate": 29.694940476190478, - "periodic_rate": 54.2, - "relative_rate": 42.01052631578948, - }, - { - "global_rate": 31.685267857142858, - "periodic_rate": 53.5, - "relative_rate": 42.585, - }, - { - "global_rate": 33.757440476190474, - "periodic_rate": 55.7, - "relative_rate": 43.20952380952381, - }, - { - "global_rate": 35.967261904761905, - "periodic_rate": 59.4, - "relative_rate": 43.945454545454545, - }, - { - "global_rate": 38.36309523809524, - "periodic_rate": 64.4, - "relative_rate": 44.83478260869565, - }, - { - "global_rate": 40.94122023809524, - "periodic_rate": 69.3, - "relative_rate": 45.85416666666667, - }, - { - "global_rate": 43.47470238095238, - "periodic_rate": 68.10000000000001, - "relative_rate": 46.744, - }, - { - "global_rate": 46.29092261904762, - "periodic_rate": 75.7, - "relative_rate": 47.857692307692304, - }, - { - "global_rate": 48.98065476190476, - "periodic_rate": 82.1590909090909, - "relative_rate": 48.98065476190476, - }, - ], - "title": "WIP0022 (defeated)", - "active": False, - "tapi_id": 7, - "finished": True, - "activated": False, - "stop_time": 1678356045, - "start_time": 1677146445, - "stop_epoch": 1682000, - "description": "Set a data request reward collateral ratio", - "start_epoch": 1655120, - "last_updated": 1695549179, - "current_epoch": 2064069, - "global_acceptance_rate": 48.98065476190476, - "relative_acceptance_rate": 48.98065476190476, - }, - ], - [ - 8, - "WIP0023 (defeated)", - "Burn slashed collateral", - ["https://github.com/witnet/WIPs/blob/master/wip-0023.md"], - None, - 1655120, - 1682000, - 4, - None, - ], - [ - 9, - "WIP0024 (defeated)", - "Improve the processing of numbers in oracle queries", - ["https://github.com/witnet/WIPs/blob/master/wip-0024.md"], - None, - 1655120, - 1682000, - 5, - None, - ], - [ - 10, - "WIP0025 (defeated)", - "Follow HTTP redirects in retrievals", - ["https://github.com/witnet/WIPs/blob/master/wip-0025.md"], - None, - 1655120, - 1682000, - 6, - None, - ], - [ - 11, - "WIP0026 (defeated)", - "Introduce a new EncodeReveal RADON error", - ["https://github.com/witnet/WIPs/blob/master/wip-0026.md"], - None, - 1655120, - 1682000, - 7, - None, - ], - [ - 12, - "WIP0027 (defeated)", - "Increase the age requirement for using transaction outputs as collateral", - ["https://github.com/witnet/WIPs/blob/master/wip-0027.md"], - None, - 1655120, - 1682000, - 8, - None, - ], - [ - 13, - "WIP0022", - "Set a data request reward collateral ratio", - ["https://github.com/witnet/WIPs/blob/master/wip-0022.md"], - 1708901, - 1682000, - 1708880, - 3, - { - "bit": 3, - "urls": ["https://github.com/witnet/WIPs/blob/master/wip-0022.md"], - "rates": [ - { - "global_rate": 3.0729166666666665, - "periodic_rate": 82.6, - "relative_rate": 82.6, - }, - { - "global_rate": 6.045386904761905, - "periodic_rate": 79.9, - "relative_rate": 81.25, - }, - { - "global_rate": 9.08110119047619, - "periodic_rate": 81.6, - "relative_rate": 81.36666666666666, - }, - { - "global_rate": 12.001488095238095, - "periodic_rate": 78.5, - "relative_rate": 80.65, - }, - { - "global_rate": 14.832589285714285, - "periodic_rate": 76.1, - "relative_rate": 79.74, - }, - { - "global_rate": 17.83110119047619, - "periodic_rate": 80.60000000000001, - "relative_rate": 79.88333333333333, - }, - { - "global_rate": 20.788690476190478, - "periodic_rate": 79.5, - "relative_rate": 79.82857142857142, - }, - { - "global_rate": 23.783482142857142, - "periodic_rate": 80.5, - "relative_rate": 79.9125, - }, - { - "global_rate": 26.6889880952381, - "periodic_rate": 78.10000000000001, - "relative_rate": 79.71111111111111, - }, - { - "global_rate": 29.70982142857143, - "periodic_rate": 81.2, - "relative_rate": 79.86, - }, - { - "global_rate": 32.641369047619044, - "periodic_rate": 78.8, - "relative_rate": 79.76363636363637, - }, - { - "global_rate": 35.61383928571429, - "periodic_rate": 79.9, - "relative_rate": 79.77499999999999, - }, - { - "global_rate": 38.64955357142857, - "periodic_rate": 81.6, - "relative_rate": 79.91538461538461, - }, - { - "global_rate": 41.75595238095238, - "periodic_rate": 83.5, - "relative_rate": 80.17142857142858, - }, - { - "global_rate": 44.851190476190474, - "periodic_rate": 83.2, - "relative_rate": 80.37333333333333, - }, - { - "global_rate": 47.97247023809524, - "periodic_rate": 83.89999999999999, - "relative_rate": 80.59375, - }, - { - "global_rate": 51.06026785714286, - "periodic_rate": 83.0, - "relative_rate": 80.73529411764706, - }, - { - "global_rate": 54.055059523809526, - "periodic_rate": 80.5, - "relative_rate": 80.72222222222221, - }, - { - "global_rate": 57.12797619047619, - "periodic_rate": 82.6, - "relative_rate": 80.82105263157895, - }, - { - "global_rate": 60.25297619047619, - "periodic_rate": 84.0, - "relative_rate": 80.97999999999999, - }, - { - "global_rate": 63.370535714285715, - "periodic_rate": 83.8, - "relative_rate": 81.11428571428571, - }, - { - "global_rate": 66.45833333333333, - "periodic_rate": 83.0, - "relative_rate": 81.2, - }, - { - "global_rate": 69.58333333333333, - "periodic_rate": 84.0, - "relative_rate": 81.32173913043478, - }, - { - "global_rate": 72.85714285714285, - "periodic_rate": 88.0, - "relative_rate": 81.6, - }, - { - "global_rate": 76.02306547619048, - "periodic_rate": 85.1, - "relative_rate": 81.74, - }, - { - "global_rate": 79.21875, - "periodic_rate": 85.9, - "relative_rate": 81.89999999999999, - }, - { - "global_rate": 82.14657738095238, - "periodic_rate": 89.43181818181817, - "relative_rate": 82.14657738095238, - }, - ], - "title": "WIP0022", - "active": False, - "tapi_id": 13, - "finished": True, - "activated": True, - "stop_time": 1679565645, - "start_time": 1678356045, - "stop_epoch": 1708880, - "description": "Set a data request reward collateral ratio", - "start_epoch": 1682000, - "last_updated": 1695549180, - "current_epoch": 2064069, - "global_acceptance_rate": 82.14657738095238, - "relative_acceptance_rate": 82.14657738095238, - }, - ], - [ - 14, - "WIP0023", - "Burn slashed collateral", - ["https://github.com/witnet/WIPs/blob/master/wip-0023.md"], - 1708901, - 1682000, - 1708880, - 4, - None, - ], - [ - 15, - "WIP0024", - "Improve the processing of numbers in oracle queries", - ["https://github.com/witnet/WIPs/blob/master/wip-0024.md"], - 1708901, - 1682000, - 1708880, - 5, - None, - ], - [ - 16, - "WIP0025", - "Follow HTTP redirects in retrievals", - ["https://github.com/witnet/WIPs/blob/master/wip-0025.md"], - 1708901, - 1682000, - 1708880, - 6, - None, - ], - [ - 17, - "WIP0026", - "Introduce a new EncodeReveal RADON error", - ["https://github.com/witnet/WIPs/blob/master/wip-0026.md"], - 1708901, - 1682000, - 1708880, - 7, - None, - ], - [ - 18, - "WIP0027", - "Increase the age requirement for using transaction outputs as collateral", - ["https://github.com/witnet/WIPs/blob/master/wip-0027.md"], - 1708901, - 1682000, - 1708880, - 8, - None, - ], - ] - sql = """ - INSERT INTO - wips - VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?) - """ - connection = sqlite3.connect(database) - cursor = connection.cursor() - cursor.executemany( - sql, - [ - [ - wip[0], - wip[1], - wip[2], - json.dumps(wip[3]), - wip[4], - wip[5], - wip[6], - wip[7], - json.dumps(wip[8]) if wip[8] else None, - ] - for wip in wips - ], - ) - connection.commit() - - -def get_epoch_data(config, epochs): - database = DatabaseManager(custom_types=["utxo", "filter"]) - - epoch_data = {} - hashes_seen = set() - - epoch_params = "epoch=%s OR " * len(epochs) - epoch_params = epoch_params[:-4] - - sql = f""" - SELECT - hash, - type, - epoch - FROM - hashes - WHERE - {epoch_params} - """ - data = database.sql_return_all(sql, parameters=epochs) - epoch_data["hash_data"] = [] - for d in data: - if d[0].hex() not in hashes_seen: - hashes_seen.add(d[0].hex()) - epoch_data["hash_data"].append([f"\\x{d[0].hex()}", d[1], d[2]]) - - sql = f""" - SELECT - block_hash, - value_transfer, - data_request, - commit, - reveal, - tally, - dr_weight, - vt_weight, - block_weight, - epoch, - tapi_signals, - confirmed, - reverted - FROM - blocks - WHERE - {epoch_params} - """ - data = database.sql_return_all(sql, parameters=epochs) - epoch_data["block_data"] = [ - [ - f"\\x{d[0].hex()}", - d[1], - d[2], - d[3], - d[4], - d[5], - d[6], - d[7], - d[8], - d[9], - d[10], - f"{d[11]}", - f"{d[12]}", - ] - for d in data - ] - - sql = f""" - SELECT - txn_hash, - miner, - output_addresses, - output_values, - epoch - FROM - mint_txns - WHERE - {epoch_params} - """ - data = database.sql_return_all(sql, parameters=epochs) - epoch_data["mint_data"] = [ - [ - f"\\x{d[0].hex()}", - d[1], - str(d[2]).replace("'", ""), - str(d[3]), - d[4], - ] - for d in data - ] - - sql = f""" - SELECT - txn_hash, - input_addresses, - input_values, - input_utxos, - output_addresses, - output_values, - timelocks, - weight, - epoch - FROM - value_transfer_txns - WHERE - {epoch_params} - """ - data = database.sql_return_all(sql, parameters=epochs) - epoch_data["value_transfer_data"] = [ - [ - f"\\x{d[0].hex()}", - str(d[1]).replace("'", ""), - str(d[2]), - str([f"\\x{u.transaction.hex()}:{u.idx}" for u in d[3]]) - .replace("'", "") - .replace("\\\\", "\\"), - str(d[4]).replace("'", ""), - str(d[5]), - str(d[6]), - d[7], - d[8], - ] - for d in data - ] - - sql = f""" - SELECT - txn_hash, - input_addresses, - input_values, - input_utxos, - output_address, - output_value, - witnesses, - witness_reward, - collateral, - consensus_percentage, - commit_and_reveal_fee, - weight, - kinds, - urls, - headers, - bodies, - scripts, - aggregate_filters, - aggregate_reducer, - tally_filters, - tally_reducer, - RAD_bytes_hash, - DRO_bytes_hash, - epoch - FROM - data_request_txns - WHERE - {epoch_params} - """ - data = database.sql_return_all( - sql, parameters=epochs, custom_types=["utxo", "filter"] - ) - epoch_data["data_request_data"] = [ - [ - f"\\x{d[0].hex()}", - str(d[1]).replace("'", ""), - str(d[2]), - str([f"\\x{u.transaction.hex()}:{u.idx}" for u in d[3]]) - .replace("'", "") - .replace("\\\\", "\\"), - d[4], - d[5], - d[6], - d[7], - d[8], - d[9], - d[10], - d[11], - str(d[12]).replace("{", "[").replace("}", "]"), - str(d[13]).replace("'", ""), - str(d[14]), - str([f"\\x{h.hex()}" for h in d[15]]) - .replace("'", "") - .replace("\\\\", "\\"), - str([f"\\x{s.hex()}" for s in d[16]]) - .replace("'", "") - .replace("\\\\", "\\"), - str([f"filter({f.type}, \\x{f.args.hex()})" for f in d[17]]) - .replace("'", "") - .replace("\\\\", "\\"), - str(d[18]), - str([f"filter({f.type}, \\x{f.args.hex()})" for f in d[19]]) - .replace("'", "") - .replace("\\\\", "\\"), - str(d[20]), - f"\\x{d[21].hex()}", - f"\\x{d[22].hex()}", - d[23], - ] - for d in data - ] - - # Also insert RAD and DRO bytes hashes into the hash table - for d in sorted(data, key=lambda epoch: epoch[23], reverse=True): - if d[21].hex() not in hashes_seen: - hashes_seen.add(d[21].hex()) - epoch_data["hash_data"].append( - [f"\\x{d[21].hex()}", "RAD_bytes_hash", d[23]] - ) - if d[22].hex() not in hashes_seen: - hashes_seen.add(d[22].hex()) - epoch_data["hash_data"].append( - [f"\\x{d[22].hex()}", "DRO_bytes_hash", d[23]] - ) - - sql = f""" - SELECT - txn_hash, - txn_address, - input_values, - input_utxos, - output_value, - data_request, - epoch - FROM - commit_txns - WHERE - {epoch_params} - """ - data = database.sql_return_all(sql, parameters=epochs) - epoch_data["commit_data"] = [ - [ - f"\\x{d[0].hex()}", - d[1], - str(d[2]), - str([f"\\x{u.transaction.hex()}:{u.idx}" for u in d[3]]) - .replace("'", "") - .replace("\\\\", "\\"), - d[4], - f"\\x{d[5].hex()}", - d[6], - ] - for d in data - ] - - sql = f""" - SELECT - txn_hash, - txn_address, - data_request, - result, - success, - epoch - FROM - reveal_txns - WHERE - {epoch_params} - """ - data = database.sql_return_all(sql, parameters=epochs) - epoch_data["reveal_data"] = [ - [ - f"\\x{d[0].hex()}", - d[1], - f"\\x{d[2].hex()}", - f"\\x{d[3].hex()}", - str(d[4]), - d[5], - ] - for d in data - ] - - sql = f""" - SELECT - txn_hash, - output_addresses, - output_values, - data_request, - error_addresses, - liar_addresses, - result, - success, - epoch - FROM - tally_txns - WHERE - {epoch_params} - """ - data = database.sql_return_all(sql, parameters=epochs) - epoch_data["tally_data"] = [ - [ - f"\\x{d[0].hex()}", - str(d[1]).replace("'", ""), - str(d[2]), - f"\\x{d[3].hex()}", - str(d[4]).replace("'", ""), - str(d[5]).replace("'", ""), - f"\\x{d[6].hex()}", - str(d[7]), - d[8], - ] - for d in data - ] - - return epoch_data - - -def insert_epoch_data(database, epoch_data): - connection = sqlite3.connect(database) - cursor = connection.cursor() - - sql = """ - INSERT INTO - hashes - VALUES - (?, ?, ?) - """ - cursor.executemany(sql, epoch_data["hash_data"]) - - sql = """ - INSERT INTO - blocks - VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """ - cursor.executemany(sql, epoch_data["block_data"]) - - sql = """ - INSERT INTO - mint_txns - VALUES - (?, ?, ?, ?, ?) - """ - cursor.executemany(sql, epoch_data["mint_data"]) - - sql = """ - INSERT INTO - value_transfer_txns - VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?) - """ - cursor.executemany(sql, epoch_data["value_transfer_data"]) - - sql = """ - INSERT INTO - data_request_txns - VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """ - cursor.executemany(sql, epoch_data["data_request_data"]) - - sql = """ - INSERT INTO - commit_txns - VALUES - (?, ?, ?, ?, ?, ?, ?) - """ - cursor.executemany(sql, epoch_data["commit_data"]) - - sql = """ - INSERT INTO - reveal_txns - VALUES - (?, ?, ?, ?, ?, ?) - """ - cursor.executemany(sql, epoch_data["reveal_data"]) - - sql = """ - INSERT INTO - tally_txns - VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?) - """ - cursor.executemany(sql, epoch_data["tally_data"]) - - connection.commit() - - -def main(): - parser = argparse.ArgumentParser( - prog="CreateMockupDatabase", - description="Create mockup database for running tests", - ) - parser.add_argument( - "--config-file", - type=str, - default="explorer.toml", - dest="config_file", - ) - parser.add_argument( - "--database", - type=str, - default="database.sqlite3", - dest="database", - ) - args = parser.parse_args() - - # fmt: off - epochs = [ - 753, 766, 30727, 31355, 31361, 48144, 135477, 135482, 135490, 135494, 135495, - 135500, 686551, 686556, 687204, 687209, 983037, 1004126, 1004130, 1059883, - 1059887, 1059888, 1791134, 1826734, 1859757, 1885263, 1926888, 1937617, 1059886, - 1955191, 1982909, 1998908, 1998909, 1998910, 1998911, 1998925, 1998926, 1998927, - 1998928, 1999122, 1999124, 1999125, 1999126, 2001244, 2001245, 2001246, 2001247, - 2001260, 2001261, 2001262, 2001263, 2002284, 2002285, 2002286, 2002287, 2002462, - 2002465, 2002482, 2002497, 2002517, 2002525, 2002529, 2002534, 2002537, 2002549, - 2002552, 2002557, 2002561, 2004897, 2004898, 2004899, 2004900, 2004938, 2004939, - 2004940, 2004941, 2005583, 2005584, 2005585, 2005586, 2005781, 2005782, 2005783, - 2005784, 2005793, 2011834, 2011835, 2011836, 2011837, 2011838, 2024094, 2024095, - 2024096, 2024097, 2024098, 2024099, 2024100 - ] - # fmt: on - - config = toml.load(args.config_file) - BlockchainConfig.config = config - - create_tables(args.database) - - epoch_data = get_epoch_data(config, epochs) - insert_epoch_data(args.database, epoch_data) - - insert_address_data(args.database) - - insert_consensus_constants(args.database) - - insert_network_stats(args.database) - - insert_pending_transaction(args.database) - - insert_wips(args.database) - - -if __name__ == "__main__": - main() diff --git a/mockups/data/database.sqlite3 b/mockups/data/database.sqlite3 index 47ad438c999d0db4bd9ef6650122a783423d8cb1..16202fba1026afde942a12b88962b8aa8a985f3a 100644 GIT binary patch literal 1728512 zcmeFa34kO=btYWZ_fbcW=F(`Snbrs)4U&2?BO@Xs5g-P&KoUBTG_n}PxQUF6?CS2S z>gqcB5(2t=Mj$Sm%U~NDWMg~1{>`!A-zT>51=wJlXpu;78x)ANV)j@%wx~52Pk;`qO?>IsZ*lXLs|T^HsOUEdmw+i-1MIB481) z2v`Ix0u}*_fJML}U=euY5!f}F1yP!Qe=7gq@+yBY_sQIAbGPPtvcHyH%sQE`WZsea z{>-JLA0K`7=(9&tBfm0IADI~b((v1cPY&-M`smQhhmH^a>)i)a7_LfBjWg_4a<0>-jmPwq&@SUbULXx2^H!zmTRKw z%UUC<6qLWZx}eCa7V#Wgsn11RHkM~&I(G7TgM&S(H=FPAV{Iw`|mledqK_B^!u(Bwp$1VEWcNR;VxZDpDHbA^&D zt7S)(R;S5A;IqZW1-Y`oShYCqtc;z!1GuLDCH?jEzf9iHN#%xwN~2hn+zSXPm5Vh} zU7V}Sg#s-Us?~-p$v`j6mgkfhJ9%4we_!go<~wENKxGXB{%Tth{n#ma?U3S`<~P z#$K>!aCxoyPF~YV<(h;F_?=~Qv5>@BUMwuk#y*C^ti$Iht1K59LD1mQsvLX4BYXDr z_WWbd*L(hP^2wc4o}5rAc?}Ou2;#+s={l1la#l zRZ2}hsZ9cc`=ll#e7Wwg7QDF1$1*y)_fZEd2LPe<(hN6hXDzDUtsLw5PNKB*PQK#tpmD==DAvUA=!Ckw0(lhBodS>!SCzT@! zmFd-Teb$K@a$$B_FRR(|Qek>&DTo=TMPEnD&Rn6$F^F#8x34GtLi3%xvXjb{2^A?w zv|OAOYQ>XrWw|W*+`{w%sRk8{t~jX5g$0k5AS?$D_Vh{fogD9^GM-Rz6lJnr47^3z zh!!iVv`{aKis`K4((2q?Pz_3p)!Nv}KNuYBO?}3ECl7a0Ih;_b`qQ;lwZIAuvb?xl zs5Ii~8so|-QNP|;s+Wu2LR67sCx2^bs3+ZTzLSSKsT^wJ!pcF3)|}#kB(mh_Xgb!5 zhq4ds3e;-{V)bsX$n+NT}$gLM^V%uCnP!SIc5ft_n=w zQBWy3w74?u>80gbO^u!W#lF7Y!gOKs@-_(w%jF4)sJ1*?j;eEoN@=O5WP?xJNYx1C{yn+-^qiWR1PLosE*01Q}l}Hk-0`=VNul<$_qrUD!~<5 z^c&G^tkIk|jEwZ8_nGhHWt~(mOQ^_3yr8F7RK4yOIMpjF71H36i$<|NJy(=$x?Edc zg6zM1Y^*0O&3E!ZCzS&U6*240*5Wc>;YCHgMY_!DWv@6rCl*(Wvz}jA=DM~3kz2`R zdeZ-#{zm$rCok=!a%nj1X+Nrv0^tM;OFX+$o`$f{hL&Jf$siMbn?(>@lrWwA0{bFk7b6dQG~>`yQ4J^qC& zuN)Y8^~ftnUOlO#nRk@S8;e$T3C)fNx6y&bB*GxY7}dwz3x98 zIy5-)MNQrQs!^yeu9ixTgTMX1 zDIFa7(#U5=zBD=3NoB0b%Th#`s?_RHG`G0C7)Prsk*bxKR!Uw(ON&c|*)nO6%TIjh z=+VK^+~~+?ZZh9VC7)2KR2{NXU5Sb%XCd-u3z$B!GJA~4Su*WStCgTuk_!hvb@wx$ zIWRggdc)|%WUiA+E}~56Dlj!KscDDS4*@==lpu1wB$#PM!Z-m(bC-N^g>)KSNY}Fy!F_z zflSGKCo`Q?G6|K%K$ljlD}}{IpjInhP+yU&K997+_;j!o)#sRs)#})fJ!gJyxG&Q; z+LxIe?W8iAP>B|2eN`(I7S%$%-jIQeOT~DuFy}8WEf$=Gr5J0=v>N-!b@jqfZ@>9Y zj&xEPNvOow%{*PR+^5aH}>_J#=>y#y}ft!-a9$m zNo6>pB3G-kqCiU1JSs2vWnB`g;ww^GP&Fa^Xf~pnEEUH7*jcP)`d9jw`d220I;jjL zRF)NY9I{#|PA_4)pDtIU90}|ML`fyxgfCGf5cr~$oAjfe{28kll`4k`V%UCL$kR`p}dHBvqmx1 z)fi!#uLOnJDi!q#9?Yr@HTJDbUh?p8?|tTbOJ72zr}4PI0wNJZG^{sF(2ASR4C@lUjfAP~X4yeY5XhpWWLe)04bi^(JIiMTIX^NX%CG zEGfr&84pC7EocoI)yiTdR?4g}J2w0GsWYjK!JaSme6i^q)I$4Bvrg)_7`a4rGdhPl< z(uD>%S-wo~hJ5%Rf->$zi#ocx7`a4t6 zJ*8cLXUe#v?fN@YyItF^zcU5fHSPL4Q>8t*U4Lgvv#Z+eiwb+}!BdPmx+L+$!I(?ea+uD>&-)8*~@J5w(mY}enJ zBI&Yr{hg_d4z%mOSEcc$&x*RH=az0Tft{heuW_O$EoP*>Bg&vvAh zxujixXZn|2?fPs-nwGJ4{hjGl^6mON)1Kto^>?Nx$+qk7Oe2zM*Wa1$W3*j=XIhSt zcKw~{Gltvscc!@*YS)i;q@x&Y*WZ~oVxV1rXL^VJcKziYX&Cz2^>?O9=xx{EnaZGN zqE|KbNjp;t7~lMB@|T)N$bZRyGyhNdf5?9&|M~o1W{#-^>4I{#Wwv%fB=K zU-CbZe{24S@;{h=W&U*jCHduiHDAof`4{D1kUx>XD}QVLS^1~sdEUuCCI6)Sp(e@y z&?i%SbBlmQz#?D~un1TLECLn*i-1MIB481)23Xe%0GXFF}~mR@>3}9c)5e}_LrOe^|wCqe3Z|5H*+r?I+i2-9~^wsu*$wA_mTX2 z^YOqtM{XZ=hyNuLW%duq!Oso9JAYtc&*<-G-qU|W-)FMV&HQWsYW54DWIq-Gi-1Mo zy90sSZtYv!)$_t-?M5MmW2g#3!5HOXNMlDcPdKjQYS*W+;=Xhp{3k-saXpXvI3kE} zoIv}e=aXp9|7;P9LY(n~F!lq_^92(D+{t|=r1B`X3@EBSmpE{8^F{1A5mAhUu@l1u zR5Qo**#HHx&$fugltr8bE)A5RN_$*{iZB|7l)+UFJ3X`twTvPs)-?8st70#XNazPK z;XYSh-|vFhms-TARGe_FxDKTbwN~)`4*bwD$2}&Q5L^&XG3gPXx{NxW=Dts~V%&2= z-|M*>#Qw5HOffHJxR;gyz>i(#63HZW0|poQhz4-erGiH|LF3qAai|nFJirmtVDTz`M7rrAUJpMJ014rYJNJMcQyWl~pi1;cbE>jTzC7}r7(9uFVN(Mpy z!yxw8Eno50gFe#% z|JWi%6uhscU?8e|Nr|I;?MX+lkfKFAiiqcmNC|Muh!UPlX=tE}IgJ#gpvMHaxxPir zjRWDwp>(CgT%ZA{bmT_DC(?<96Yy9-bSSz{5bjZr#1R}Be9g7cTDmIo6$Hek4xkFCuYEs2w+kmE4hdby8Q7N}eM#yLP<*03*KT%z8xR*m z&{qLQl6E2w&5Q!q(ePviln&&>fit&Hipr@fI=WyS|DE#yKVh zDKJKq<4QoUT?|cED9T;whp2^FL~9qIYRp(p6umPb_RJPB%s^-!7qQDjCq!E@ zq##vPFwUsQRm^3GnM3$th;3<35M$&<3Nr^Kv8NR6-2-CJXb}sj7X`ix36jbQ!IE-f z*V7Tkw+~qjBNspk*Yz>S!F@Z#jKX7picJeH8sert2V$Z{OkfHm+U3Gw0_gY<0pAQk zjA0rx5egR*u*)fTLZQ*H$Ty-W&`faZdQRHR8WSyIgaJ$L&G~; zFqFQIG0=Q$vGZ_Wj42EFD zgs{j79D;3WK`5otn9RT%jeF-o?1>xzlP$)Sc8SaV5UUQuloxxL7Ts8T4))}Q9`M5S zh#}%)W`ulTa&iO6k$@OtS;K+!qhRcRw1`O;ttZ$dhI!v7F%RQ_V_xKFg_I7)GZVE4zbK>$PJd#5VIsj-}nAJi2YTISi~GhqMtl0jv7mdW>}CSY+E%O0A0v4i52z( zL~-D{Sb;spCFTWJxL5};WqIj)LF~>JF^vVtmyEiYz#;`dY{js#)Dbbnohu+k(Qhu* zm=1&n8HWZQWS?Ob#X8JX&uI|*>LxLPnVESUYa@3erC9*5bO5R6+Q(cTu_%T>VeW;n zMP`Ia3^<0K@G#gS(ffK3`(G_$EOs$!0z3!c0VR^Y3$ewu5J?tdlI0Tf66Q7Eta~o# zxrDo(L=!?6WEoAr3dBpk37y59yOE-F*|TPk*PqFFp5mKgbvmxhie?8B*Xv> z989P&MndSyo|l2x%`IY~CINRKMaoqY3p*Z&m@gm!ggOp}4TeS-E6QaqwC*YRU%)^du~ z0&5uN_&}xq2E?Ab*~|!&sWf*BF!az>ffrMaVTa{gV!9;(71Wn8u)uss1(rTv;4#eg zh>EZtrv8_){}1IqfigXq*#Ezl|7!lP^M9HD)8rtI+mA)SB481)2v`Ix0u}*_fJML} zU=gqgSOhEr-<1fY2iC8|3w_@j3?}KGwUa1QYYeRZ>zdhse_sj3^KL_N_mg_^m!}4@ zU&ilrfBwDMFX#R^$8#Hb<1Ju676FTZMZh9p5wHkY1S|p;0gHe|z#{OU6oGp;t;Uqc z8lHFo90G;$ZVQx#jUE!!Y4<;{tq81-+?a=iGVtNX1iwdz!3_cCe^|xgk11eehw+}^F#Ho>S(mZWa9@NsN(9S2Tn4%E zgS`ET>gsfFtBD#;b8w@S2s+Nedk6eukznQ7VU17!81$_qj_qKLo|xUlTQdjK2kUD(RCLQDhf=NA#; z;6#YG62Pax1mIyV!ssBVf<$D3^l*zYAEt0byFjP|57}a=iHO7;Y ziB!kf(=XfryZaF(Cg3KFMC2(1?MeM5I{1MWF@|Ul0ru1@ge`$39kzHlG(;THDG<#G z`G2wPALjYMM@%IIJwYG_v>MGuOecg15Rr$p%&FA(wTKby=12Sy2kChcZ~#6K8sQ5h z;vpay6)|y>Vj3aT0)q4SCN==#sOZ4c5)JY>A#R*DFY|j^#3URUd@d0a2!R|B?A2|7Z4pWdAn%#q4LZ|2_NvWdA7p;p}f`-=F=(?0d35 zm;I^i+p<5LeM9y&*_UV6vJYh&+4<~DR%K6RAIRR5eQx%a>`mDq>t%1qUX#5tdoa5< zo6insdo%x%`RB~nGGER7b>=TKf13G3=A)SpWoTv( zoXNa2vy!PHOX+k*W**GkpSe48Tju7>(=&d?&0LqcI&(O4AhSD@%^=I>=r>2dG5YtT zUryi#;`U<^un1TLECLn*i-1MIB482tjv$bJ{h28|;Q!#67oz-uGxwu>tqCcfe$5$# zz)QdS3|yVluR8Nwl&?I4py%mF&)kCY6=$A_^5tg&l#iUj{^9iLGYC_j-Z*m|%JnnX zpjb&a zSvzfB+~!X&qAZ`TqAZ=Rpe&w7@bvWD=@8}Y=@+1!Ic;__7f#=Ya{Bb0DC5(&qSU7+ zQAVe4La9!}u9*%`8=mE9!}E(y!@8NCI_;wT{?iDmo<4ONfz;Cvo<4%|1*gYRo;;1P z>FMX6hKV%&z-hzz_nk&?_VoRyVINJO*f2cbx3Plq-VMX&JsSxAo_^lO9Ll>krcvIx zVfegbW1Mw*myEZv0?c4Hx8j>8wP*6Vels=1bo`v zFumYxn6@0-Fm0LG7(#i&#sJFeH%z;(TQ}``%DQRM(e)b2Yu63(*R0Q=eDb zP+qle+I3|8`6#bkH|-i%jU`QC_-k z@ZY!olvFD9GwV-6`P1v;D1U1GAj)^F??d^M>t=WQPpprkd~6-urry4uM)@|gk3IF{ zYmk=IkFB8>Qa`$8?ASlD2A)$tyf%;WEo-wV-<*WG|DiSXMe0p!r%=9e?E#dpUxT!# zUbl7+${$=qU!;Cu?N*epT|+OVUb6fOcvB=T)xOKIkg+b_g{kIltK7~5Q@};hSwJi_GEuFl^Z4f=$w~kE*t%5 z{y^q^`8fBH%yiDn-kD8}lF^rDzmR_~D%+1mz#?D~un1TLECLn*i-1ModlG^4W;zE1 z?bZ_c6g*cW*Gm++FbVn|GAM)$$zzcA16fNDpB|=6Upg*AvHRHgOOf2g&i(K)Md3W-+nb!>Hdh^A#;(o<^Pk{ErV3E@;Hj$JS>LnFO$r-3t-4A87@UK}K?k z?bCn&wr7HL3<~L7OamhG%KD_XF;zMUIWLjkz{4pP`i%HU*n{K~3N2?MLdF;Dq7JZw zox#Y8VEsr+fF6Rq7L`<5gIK*q4AyA}?Lf`~`n7rhf;md|iteg4-k03{ux1^nHlFL!t$A1iH^d zwj-F@704qMMi81VMy?EW5mK>mhzV@?2x*_5Gq0jgTr1Q2TZ|!LL4@oq*z9hSTsTOk z0)st8+yLyDN2U;@p)l!u%y^I{xdZ$R&;iK$gI)G^2pTyhw4;g=*HyL+} zH=2~8zXT_e)Gtn=_`i)~z%RhlBK1Dw9q=B3;@!qO;9c+=Nxc(JI;o$Br%3AmGVTCB zcPWbhYCHjcHiP2748RX0mCnBpW$L}nf6Pz&u?ScMECLn*i-1MIB481)2v`Ix0u}*_ zfJNZD0D=G3@_!M?YJl7b9R4^+TYzu_4m^QE-w~W6Tmjqy5OqNre-;n!9PsghZwq`o zyhtJI0EuFjHTM6J{2!o9|L8gP|3ArpEIILA&|J%cMZh9p5wHkY1S|p;0gHe|z#?D~ zun1TLECOu=(m%2`ju-hKUNZ*%-(EBC_8(Y4@tY2T7f_~tEC0Uc5A%opSOhEr76FTZMZh9p5wHkY1S|p;0gHe|z#{Ot5cu_$%^tD* z9Yo zKQ|5f!!7pz=$!*5+kyR91kR7ZD;^jf+<)v? z`Ug)Xk`&3hoT}=DhORZ`Y1@I!&G+7N{KPHeCyqbsj$6jJogTk-Wc-{D#B!=2YlZO> zw>>$R$DHC=07ao61^#_ziSjyuj*eNh&{ z?#}8q>dW&NtvcVRcd(HV-hS^^rR3)0x7~GOdyS=;db!yQoE|@aD_Zpc`;x3ihSmit ztNCJ4)^$}DFIur&)8$&DHdWD8rOS0WtuJ~>GL6V`1YQ=kEKbGxV%IF`ndw6PB82B= z%F#tpZ&ZsHty4gVU|qdvwQxR~xwu5sRCT6OzbG*|Jzdq)zsrSEJij1L-h21AQCnB$RbflVZyC-wjg>@JN;Bo=>eK!) zv_&r3hyQKT05TUJoLd)zC%CgN2Cq9bu>Y3B=YzKm!<5_pIeg}!{=xkR4y0doPXc9_ z$U?Im*aBknSQiA76CD%A1y}5?y}4yN*uElmJ}q3N>NU*=*=sIRt+`HJq*5|vUZhT~ zF6S<`)-}pIsTEbHb~LmW>pQE52eBQ|!-La~>NUG}M^()Wg?R?3T?D-CV!XZY>#{O4 zRhyYEODywUpO@5pIi9JObab)x(sX*Y-m15C(YbD5VE;`A&NpD1B0uFe{~cVrvNstj z>sKU0#XO_xTC?oiGE|z!x(t;aKGxP(D*9rLYGZg#gZeJipeAEeZu8&%ja)jJM$a^t z-1c*H`$x}~X0;D@Y1vHjAiBL9Th}yObHSORC_`OL>UE)_>NAVUaNB+^>74EoVrIGS zXSPp6+Xnbey%;mZ0$X_=R)&$8~-V{{j;~j|NkS_|Ns0iuJ(*Y zz#?D~un1TLECLn*i-1MIB481)2v`Jmh`@(hKKlyUFC)kCVhz9j0NECi15*$ZA-kt! zP8bHh9}3^)gQ?ykz5hkD4i$@ z-3X~TUFJtF4?-mU;~|ZuNzm!J4v&a~KYi}#YmEJWcj{zn^j#x2k7R~EGW4c_Hx9k1 z?-c`6Jzwd&yJuILvU&Gue_y(#A)RN=X}}Ks z4A9FnfMfoGW?9(e3sF@rUD!Uf$xI?R7m$Suo6*bjWpyFb!$no~?UB{?8)!?=QGegS z{^wqOK85&Z0}@5Q+xgdljT5^E_g{Hs`Vj~^T#Qi8>7;MY{@S;tXU-mfeEsrm_G7ZM#?&e^m)dLk+N?` z5dK~Jisdc@dxxv%l-uz~etjl;aqrOF7T7y3|969Z%PhX-0ch)VX1ZvHQ@N?y=`GXU zSorPq_XXxsbF-tzGXwkYKX$&6(Rrt2@tktE9m=lVe0kz1wSGe~N?-% zsdw9D0whCRkaZax^XO9coZH?~@R+#S4)#}ycr0K5qLq-C`EmvH~6R1lIjLdH5 zdcAURaR1)D=`%McU@Xm4YUck#TbgT*bZOuDY}WOedhwzU;*oCp1J2BsFE+*Q#8Xik z3%9I;+J#`;*KbK6-+T&e zy&|3S=M`Hz;+&IR`sCmDRq6PF<|}S2#S76FTZMZh9p5wHkY1S|p;0gHe|z#?D~_>Llw{>imL zyv6^-i&4D2j^b^zD1Q7!D1Ph#6mPu?xBvJgijTP{{`e4zKkA78pMK2x{{s^Hu?ScM zECLn*i-1MIB481)2v`Ix0u}*_z`qTFPquvaLyGTt&?FA}{<5(xl!q@Hi=|9$ztF)jP=oA!V6hy7RtECLn*i-1MIB481) z2v`Ix0u}*_fJML}@ShfeKidn||8zHsPvubjN&g7SeZwD1jeIRPJaS+($bEK1=8xtV zMt(MT&*<9dhx5Oj*_An&`{B%EnJ;HQkae>wsBAwL0gHe|z#?D~un1TLECSy>2(%1C zaU3wk1dElUJulEcC9x)+r1;MbR2XR?n5I$0X&5TUkC8jt^#dYfk9xj#_Od7J3njBRBRV zKR|YD2RX9|QN(9%;5w0TnbaX{VW_2K{ zTninL5b?P}*vVo=e$pcsE*pY4+`d)+vfxH{JG=kYN zi2X2>0?hlacHGFxx9Eft#zyeNR0v!J(v-tmi4LM6Vu+zZ$SfYl02$bj$kh=#gor4J zRD@7wzVGK+bQIG_XfBEHgezI-vw*}gDuAZY5werZSjWKESUv#@2MBQxF-{eQlyIJH z(TSvsXlc084I@Wu>1q*2s1hJ#nk3wHHL{;;V}bt0{C57)_T(t#<} z)0FxY05Ml<&v6{n^iafLMR=Sk2&Nw)i@J}9YL0TjHT8)rViNmuv_(e;fdZ`Dladqa zu|UTW3qXrO`~z_0h4{mPrB|ZO=xfNaN2JSz>wvdlq(vu&ylN_B5Cho|@+&b+1cVj? z$q6v1D0h)Ao@o}wJ}kNTlQ{;jz(+=St%o=HAp&M%-@(;V1%B+RzyW&>IE&Fy=xD-1 zivJaa!`BSs4rA1f0|#Osh;XPy$I+4nq2{rNhEu>1qA3F7(u{eGZb0Ho4@Vq~ZzPi! zk;HhwY=A)=yP-GOqT|OA`cpCjF~rzMAUenjaF#m2Y@s~ifuw?n5-mbM_Cb_~z(`^& zInoIST6C1ge8WvZJjPt6VowL!MdUmOG4WvH_FYB8fVi$76ZDvlTsMGpV&2nH$Z?>* zMTf==LaHgoDD3XsG2ub?ro(fP8 z0^$o_5VRY;0bo2d9ycZuV;xfjhPx2xzCdw;d4_Sc7vosb-c5W!hYC(2PiYTxLFC~| zO5qp~NHbV5#RSYm0i+5eg`rO(p8yNf{UnIuo@e*1?HYdJatL|Dtl~-x3o0-jC@Kl0 z2GfAT72qRaG8SGer58tzkFEoFGLSxm7SoEc|EKfcH04hx|9tnXWR??)fJML}U=gqg zSOhEr76FTZMZh9p5wHkY1fB>4(m%D&*#GwrKbp#ZAoJziaMm69+UVNw$1;!Q?lD&X z+-LJgM}9WD0)GH$d;yGy!0@A)T~EZ#Ee(r+MZh9p5wHkY1S|sI;|R3uLOMp)CK#k* zWPgez?4UF<)^rVHHw^BK!?Fy6yrMo#o*uF}!B!OzO$3uLRBI~tx9E6IEMT_t11E%m zEfk&)n^p*ee+Ywp%v|JyQc^IV2^b`WYs{0OZ(4korZfF;WfVcw>$!(gKla4*oX;R<6t+_e8oR|-cl*VG9EnEGP@V=L@j zzJ$fw4Ls$+xahb7HfY%K;errR62KtOU}1)NtZn}fW2Kb?lbPlbvS(@TOB%v5>tly3 z4C|iaQNUp{gE^1}Qo;Bt92jduW37V?chmk)ux(cG5cX6GLnkcC3fpoW>N0_3S{g0^ z5r^G31c$KVJK!cHVeG{olY+#dayRY&*rkh1TMV`~AQHittRu4%mowsXY_Wx9j^f^e z!uSg|sRv^>TpHjK;QGGVh`VY3CsM-U0S*{3ZbTGJ)C@K}SeIb{)I`F#2Q#+xV04Bx z4@Ob#Ntmcf%wZRY063fW|1f|>85U+ZWx#}}q${-KaLqvL(K9f`QVvUX2$2b4;4}ta zg}Rc#AArCOBigk8#|$n6XnFwKv%`Td01x54;F`2$5C*UWQ#q{YFoOaoh^>NMQhNj* zB}{3$Y5(V%qHoayFz&}NxjO{T5uqkymi@PfrAxMhmd-10I z-($X3Xfb?06ilU*lR!B<@E|2_C1D_? z_F*rE9TwsT!GXVqaE!f`5KntxDTaO4;jp)BIJ79ZF$lG3|MyMjYK-d;8@>swvrv z$x3k^F+$-1f+Kqz*?hls|3~(Am z*qrWi%`qGy(C{MQ%5B^KdF%w7#vx=C_IbyrW_khJ@Fj_bBZN}1RPa*)Ef}?prvTc+ zVh5fo+(_4^{T~VNf{?{<41%kR=R>kNgCl`}%Y{P>+QO#{j!PKD;0DtV{)5Mqh9nu6 zlX%nq4{sxeNr%Jr#C5SfpF8l;NSt6|$vOCB4%~R)QH4Il5QpoLnH?x|;I+iT+NS+q z;3C&!6tkl1b2u8H8SpK@PxwFhEP@mOE)K^nfwX=cUM|8@a71C4$qDAYP5VE(72O5N z^f@MK0Z=1J;3=fxF~A@0gHe|z#?D~un1TLECLn*i-1MIB482tPm92Tk-omG4orXG z&;Rv7Y+f=S-#PUlJo%kd2Zmdx{&D-Mq1LIdY&pdUt{QBf`p}PTKQ+)i^}%1?a*7ak z>Og<%)cdxd>T8|)uiH=cwobi$`>CGRsUO;YD&0Eu>TRcz_y7DAsr*;-|8M>S`N#6F z&M)P|{B!dZ2kplqU=gqgSOhEr76FTZMZh9p5wHkY1S|sI3kd9ms^GxErm6tJ98Nt5 z_ax6dwHIoF~xDspN!6a(g&M%tt{+b>f<_>@2pWO*MZ2L3NDe|5aP}ay0LejU z0on(f@BfFJ@BhDpyP#!OhqIAP~?bocsPCc)_XN=f3}&gU$EcA1S|p;0gHe|z#?D~un1TLECLn*i-1ModkBHuMh5^}z1c93ynMUHR&P`Q z=3rX|U=A7$fH}CS0Z3||s{lw2o@@V44xXz3FbB`00Kh4@0+@TAYyVFUp8Niv96a~^ z-yAgY|9kTPWV-W7`ENFVd=K@lZK_4UB481)2v`Ix0u}*_fJML}U=gqgSOoshM&POp z+IM8Y*#D1={9G#k<^1b&zmBtq*>~eW^JGabPu1#jPEVC(c^>s^sjD9O z`uqn{sVm3(ZfsE~&6LlnG<@k@)widP9PYcNr%9(#HdJQH(^HWy%9W{_R`cbkmK(d~ zP~Y9HI=I(NsZpA0*3~V>mh{YYp}zYS*Id!}+`cwbNyR2(P5ztJQg8X8mtA>z-!188 zMXBmDi?Tj5UvAx2SLRirrf)ddclTxkXUYk!`g~m$&uK+!_KkmX_syx)<(Ku{*rKQk zTFq7FXUg^3R7F>3=A%7NKG1h}I#p}n&n7Find!1=shTg9X6kjVr)o2+I@)(>-?1(A zn%9pU-QRcrmg8ryLyZ2amNi4HR@asO>-P1Xym-|n-Ttfg_T83FRduCNZ$ePfWm%kA zZT^7}>Gk<4IlQOu>AjtmW@@#DEGzI_pRbG|pV{s{Z@629BbS)Y?I5q0D>GF+C5x4U zoQh`R_^$CoyZUZQryw4t1*lq_QJr`1JuueC+UG;5<{Fi$CSybU@_p`5a;XssLvBVj ziuIMr>56ziWI?w(pwW0a~)Y`6TUDmak+EhW-3J)@55-{=Tkzif~zH+JOYRy6! zDu#6R$OI92CQrN!DRn3dBONH8B56U4>;w*SW}MnJ+;??4rI+hfIaQr+K=gZ`G}L!n zPpT=AQy5u=DFf8j)(-yhWrKZgUrNmvi?Xh(Xh*A3|2ICH+B491w5Ri&oYuWx+TY*j zrc>Jm^@c0^`fdWLwggYD>gxPdeWs)_kV=*5A9=-ZuHD(&e`9KWKAfMg*T8F~vPF;| ztb-AZ-xL6WZKVp?Kn??2$b77Gdmgo{ym1f9a<+7NYa~2Dga($W=r=wM|gd`}Z?(gY8ioR?&r`ekbQ;1|R9H}9Y zy1_%Ti{=*z6FdiB7ytSm(ih-g>Jbk~N|36<97lo_9&mnYZ@TZgo>Vfs%+yy}qBHo% zCjZ}&%mb>*? z*-t?)@Wa_!b}aKi_JK?(^Ea8_%RHKOGe7mjP4$+NMZh9p5wHkY1S|p;0gHe|z#{OU z2!Xx5$I{z0HAfEi-q<|4T}5-{*>l@8GvnjEw=}Oik4on7)xCGO37=OVbLgtx=WeaF zT^Vyl`&Q@C!(4v$Ru|C19BjAd?5pdh^5rr^;i9US0|vp4TFqDeORw#{zw_vM)h_#= z(tGlv6`M5o9qGL--JuidqH@`LdGFIZt87uY?AhPzbvwD+K-oceo37=OD|&BApI6bc z>(XAfd3KAMWo%!so1ECHcF6DQy(Yay$CBGT(pIo!4b5)KmCPl*N7GxiDx+h)SEoA_ zDkJ7Vr#58R0C!9MrUQ{9Q8+^5wo_UV5{Dv z*W9ChE>R-%T+(~p@HrX)WB=bbd}9i6|5kFJ&pFxO%AUx4D)ao&pB(wSk>4J9%Sd_T z=8RDr{}7dm~6RV-CY!)#P*P>RsnlYqDUfQDm%deNJ6A zW~v=%Q)_X8YR;+4^5>juGLf7+n!{6SZ%~}kolJOyP;&h^XB4*yP$1j&scW;Qs^6~K zWP}9B!F~O>=P<(&ssg;DnJq+HydeF!k9#5|H>wj9$dZ2Q=;+p}Eqn+Pd7~Cbpe)Zd z7a`i!8W|b5rnh(frbyOd)YJGM18eqM>Yqxa*Jyn}8E@Fr@@qP$cq1EW_Bh=sr z){jtwG(iiFP!GRM9~@!kB)a+tW#)$gpYqLL9>p&-{RlHxCba@{$+fStfzl}Ds>>yAGj_H zT&I>EK*;^I`>SxAFdB~<0Q0Hi311L%e-nHlA&P&@eOeGv$K9-9DwrB5T?0|8h8OrE zK+ZuV3~-Y96aNG_kESK@{{}`rn99E~_dxd3nZFzTar|LF76FTZMZh9p5wHkY1S|p; zf$wev)=ms$(pRq?UYj2p8cL@I9=Pp!jzgRqP`JmVXdIT*%6zpxe(Q-7&%5Dyci(qn z{MuHPqvNt1jZ4&#Z{WiBo|a;j(j&i+jL*!scsQ)t$3 zsz2e`^+DJbrU*>igcvIxKz4QZ(XLV)plE5aTb+(?|RO7+$dvL!n|?CuY=6|_yb9!TgbS<<3Yyt9~UyN zczl-y37~7mJ|iagBw{j@>TBXj4V3(Nd*j9Ud^~>Q`15WX$JP;Dj5nJ}o1oNmy>8?K zus1yb#?7lRU{91u$w8bw>;1oZIe%Oz(Z|!m2q91nVtz}lKzb$2c1o-T}bYm;(tT6QL7)TG>XMzZovE_r`Dg^ z1HoNuKyb~?dlI0XzyR%Unn$Nxu_+d@p|1j$|@vckPO` zbjqwCnFM#@k6#<}=A>JPMbMWXNOtP)V2T<7Ut_l651o1Q;)i`SSRD zqmHdiwQ*TPZ*6Xec^sWZ%w&1w)X-XbbTsXrYVkPQQdaYk zzE>}5S?rwX9}f@BIc4z0BVgga77HUy7FY)h&qhP%my(s%tVIAw0fCt^{-{@HG|U|r zoKVgqu7o8B46J2_ar;{9_QTEF(~jHU)|xZSVu#s7=~{C&kH^c6QV4ylVP(E5RZ)*) z#s)`^iybpBA34=u83H#qwzwH;a^rPy^DLNV#%o3F$I{WiQ3Sl?j(u;rV-RvGo40(| z6@$%tl8$>eXZFq+9MGGck{t|;_Dus;fAH;QBey3vl0F$!#>PbS=MH}EDn)XBa}iD0ILhIKp3WWpToLXvi2 znNDEumPcFsg6nP+-3h3CyHRwyG07*e+0PR^aH!ASz?j^&Gf5}TngtCQ=Ru(xeIJ*? z=9kb;%C0x?CV~!-9mg1Tx?tjaou6(9i3#TU0p%CO+?y~e{%-V}9kp3@>49KE_#Sfs z)CJ&T-TM@`^}9iYhXYsxIt0a-Nw&$hH_;V!Z-R7T(4b}2haI0TY*Mc~O^UG~&VIY{4A=x|j~2m<5S$AG6%$72xAh&H=*qH>?jlZe zc%wAkelA7kbf?HC0*njF5fnKoyP+IR_{{Ome%Y3Mcc*W_-^Q^&f!XJ5|M*?#6L*4B z(J_3C1Z?B~JmXzV_(8{L={8shW+=+U*;>Z-xQ#xUPSa5o7ghm zyNyTcOmN~cnC)RU>bRe{;BEjy0=Kmj=)+H|sXJPBr9fZ}1;a-W#B;Fe`UVLER>0 zVIF|M#OAjfROga;_!Mf@XX-`sOm}4aL+cToPF&N?`55mA+SHeonW>owN6eHIL)~JY zqw#0G+A!yn3(UJ&$2BGfL=qqX$6O38k8wQlpfWalRO=mQFnz#Ppm_$DB!>9p8evlG z;etrlRCT6eUgU86$j%?DOG9;R$Mf>h)PST9=+rNKYkK!+`XFB9(nINV zYIjpU#VutS;x(~RnXbwR@BQ=t)_dva4X^FLB$Ga9^bCon`jY0Y{O8<3-n{DeEv^-&q`UjklS?YBux?B_nb;?k}>;&FBJsU!Kf3&VH^gkqfvquOXbeRQGk zRcR=A02Jf-Whwg4PdDr)Afa7<2!hqpNco%~K{B%CUcY8zv zebMnQ<29XCw?1LR{N{_Z$__jlcH=+jiix4$Znn|qbio{NRCvOm9j~!n=mXqtm>c17 zyUGdScrOsaW>0o|i$y

=Dx74_CvppKko4j??iL-tB$LNOKpFG4OO7Hy(x)MdIVl z*8}6vy-m!)zUhf`#n76;_YXuiGLep&BH2yLW}rO}d7lPnKV1;<1lY$j znU|>WaEmV7pJ$MoyKZU^5*5j3G^H3;1lAQa=ZLtBM6 zk#6kcEw2kYppOtz9iMLOLyO^)v#&SjAurBCj9=aF#R9LZc8o#4!JV@&#a&-Ox@(=# z0x0X+3IwN{c;a>4F`FaWUC@<&_YrI8la5cf*JI{SxNuoMtN-lQ0q0qWpqU~aI)iQ# zGIy}Fz-_wy`rVB@n%Eul!0+ssF8#|r$bfTJ_}#ASsMmGyVpO;t;@MSy!}-J(1ytAh z<~*H?X{Z~=n7p^F0^bH8SX7DYdjV_$)Tj-*koRE_*qjNH#&shvy5o(dyc-YMFqlLS5!iR9BJcmn_LU1O09rbMbKd`r76?jD@P6+5f2U@k^Znm<1Xf;L@PxkqZz>he z^ZuXQ@NvBVZvz*6w^!Be@BiDXC-46^q~4IqzXwtO^0{BjP33yBKb5^T^HoIpcSk=t zx-@#($oogak>TNY58prZzlI(gdiLN~2VXzv4}5OmH3OpmulirxAM|~`?+5yx+WXhN zKiGR?&lh@L*YmXW7t^m#KRxv&kp6Z*_ubIHw(H8C7cMgdjOR#bJ0*>XqkK1r zJSJkuCX-VNzwi1MQO+UJLC9V0Q?7j? zg<>i~;Kj)CsDkpJgbr0ImO@GuI6?|dwd=$Vl(??q&K9EAZ4%XyqZytWxE7%sa;ih+ z29y#aC68T%*z~pHVT?15j$9deks?|-sXd0=I1+k=vB8umZkV!0>&Q_w=wuovODF?wOBPpPI*Jy6+@W_!KCK>HZ z9~$4#bA6$;A9^a_u5?2mhBeMfsEJRvaC%LPDAQbES`HDLRJ!N~PGU_$AAn(74?$o{ zjmKv!gr1UUi05N}VWb`N$j&2BcDE3Ha*Jrpl;cKJ#*70+MTACAOYTJ2ZwWq?rU9Wk z4k-S1WE5yudFU^WT}2@n3bqivdJ|Kr-5?YajrFtx78y~#@&XpQVTcHw8b$!@P|_Y3 zO34U8LSrWm@fwI7k0DcH3(+UFh)U%_T6`wZ6A*Y;djS~L5}l*tSRr65B$z`3ItMfW<;97l5b;&k=C|84V#oArTSy)lB-@6+8w# z;^1M8Ky;Fr!>wU@Ws7J)BN-zmtrtaZjJrb)VMoD4KzT?*jp67z(1!X<6UUE%h|qvM zf}lkZL%NmI@fOk050zpR53qjV(kQ}A;5!N?52<_-0~yTy1Of{}kWuZaFkl#@fkS+S zF}4-c!<(3bM>G?t`q~Ub5(8JF&@vJVya^dKxd(v{0yn~-QW3`>)-EQ30K?heI-(A> zh%)NnU4nB@2e?0_G{TlGsbi(6#6u$HJxoe2CPYu61y1Ck=Lv>KjNvRCh*>A5S8Q@h zL@Zoi`woPkM_^E(!-*SWYb?Qh>Wc^*Lt!p)nBhGore<_VhokqjMEGkH9J@wJ*fT16A4>Px!tI=_x3vtHO=0f_6o6hD&&|Igl?K`iKO`47?gxN!#)OFk}+ila;{+{=~?{~g)@9|oQ!|GaD4Y3`ed+z_d!~ehcv!CbL z`*jq?C3s%oNHc`eF3j>1b zfP6Ta5BWrw6Co(4&v6}##pC7Zgvar?@^MJU)6QzvRo_YpQ$b`o9y(X+recs3YL@O@wy##fLTL?L_z&YkSID@pZWl zauVvgNpo{Usa-XL#ArV4T=_U&@tAVq{?rQ}C89aXz4>%BNht5?7!@kUVXW3&F7)Hu?wiVo0Ty)oHtN(s^1+I*H5rd)vV|}gK(aKmTE^?C z#5$w#(hFF5Qclr)?BA_C_p2Vp-4`w0byQXpHT_^Z9~06bxbDIena&s%oHRJbGgvNW zGude7zYkaaSs%E_;Ov3wZoOI_pTThX9NvAQT-@&$RYuFvN(%3c7Pd7++JiPgk*V}M znNQZMpvv*Gzcx}~Wb^@tk3aV0r`;15KVL2`RR(x?3Z3OxUK2-Vi;;6O>MlpHudTea z9lW`&KbMsT-iOV)SoI{5;rztK&sA6M5kksiVK+w|dd|6*F}omx|GwzZ-6?ZHT>6a^m8xYVm1rIERhLOK5np zoPa{z(abCLyMh#?u%SaPFHT(YH1HIvwppCG!EDq&Y4O{e7JpI-MJRGMo(>m_$z<7= zyq627GuYU1VJcERGV!SzA&L$-Jz4b?E7R3n{F)8s9X$gjthuFO$;T(6fQG`SSj^05 zhhwxo&lzOzss?Kj2`qzT%b)IRR^?F56$NN-eO@RF4mD<^iR5@LPjw*$cPQJwq0)^0f8EYIcP{?&#g|{K zUHIUISE&Dg<@}xJ8|VJ|xx3Ca&;E_GuRPn@|C{@F@3+tVtuyzW+1>ll-mCY{?tW}{ zy}Q@`Ywee`ey{b;*7r95x8_65=QY04_)Cpv*S}c*>H47d#oABRCOcoX_@nsG!QF1} zDQ<5GMNU-|QfCTub&2E!kBX0qH=nYQAK?_$mY-PJoTIq{Y`OM!?0#{116J1uuPoOD zJmkoPLn&b5S=k@9#!UG`DiKY&f(E^{@(~Bn=_C-Z<-)Ey31FFzw%5F?T(dVpO!Srx zjlv1C^Kb+7`<*$J%Pv|ihYswF;ik8MP>gFa+d=s?ggwV>G}~+5S*|&Qk9(t~3Z*j} z6E-FH<$PujigoBX+hny4z??d%cF9TmA9{P}^d)81xq-Z^!vif1u| zYNVAx&Y8%ct7ZycuLdIi8Y@4P7h!!KBgYjE>UDi^F<&g2Uc=49?gTWD;pU@W>Gzbm5s{ zD2M56sCr(?sgrrN!B#ilY{hIaf8Wpg+ln8r-^=gy@suk8XR&ssridFbav?Cxpi`XN zbUG6V7IT|<0-h%xukLw?<+_b~emHa!Ks`c3IGu{ov4;o}fT;I7JDbNeE;XottRm}nB|Yaya_+kTZ>ED!F=}j% z7+E66rkt$fPQQ2Z>Nl0Ehw1{fMdyJ%2KNu$eZME(*q%U!fQ4RB157)rv*`@zko`=t zaiY;qf5Pf7Dpwb)e6+}kG`mNn2YULDvlnBJOh#fboD8BY7gN8|iBz$wlvGw~=e1Ef zCtv#yl&i1AX%{s1GV@&2YC@MSJDN|HGk6Yd$H96TeI-cP+4J!lJ>7M%<){-^KX{?l zpX9A7y*lz&2U!NDsorM{-(4T<8Le%+h2V)YaBzfi=tPE}j+QI?XtW4=S#4mz&QI*# zdZRTTZ_O|OAgn}zxJGLCoVYpaX1P{ghnHF}u$c;c(NiqW+4Gy{g9JHPt9u!${KW3T z4HkSd?Bj|S(iMVLd0;O9s(l3nZL*6Y==if0vT3~TP1pStT5Htzx{JtEwAer+UnP6( z+b$PT2iKQt0zYu7IzavbgKb!uNSbkshaKBlNhpcfFCMy3fom3n2J@~UI?1-)Ui15_ zHT@J8oLY~;a3MLevL%uTZv<}&&Rulwc;>@Nl2I=uWVjts9ox|DZmoI8_gOQV0}SP- zgx0}y)}0`eq(syOzd?WIeBlRVhhe^z1>id%i#cTnr_3ep1uaK!EL!bKj_iBOMI~9J z95QDj-YxK8*y%pX!6OuR=gx-oAR&e%>#rkDEQdjwlu4n!vplxw3(7@-!_{yNMhc6; z3$wKxh8F3sd;`jU9fL*6QT3NY)fdMV$I1egiGKn{k1hH=<)U~y9{|JD+wBsACn0Lf zTB3XCqEzXgGq?Xjj`x7YV80(x9t>rS)v-mNUoIK|1?~XkwxYx}9U;PS8}kJYr!QlS z@LB$O;a3V@$`_dwt1Li**yH}OMW0tLYJ(Fi@CD!+JM&uYFQEbGxobD;Oo`cUWzqGp zVH+aGY!1}H{toiw*rLxZ7lk*a5-b4&xgyn0@QTn0;5uT^#VwU14rCW30lu{N&uAnN zfNg=^dSD`)1~qv zC{`^ZmCgOsNI(m_sasC&Jbt2v<)R)Rw%J)9uV8AG9df~QP*>8PkWmO%rH(-AU^!kx zZ_hPg%2r{eO%&Y@D1&NIw+l50&LAk7ek`o1`=oAqx81WswSWV*yhtcQ&;~sWG1^&y z4UQkvez|C}r-N!SCr6MX2s!`aLQK^Y0qsaRYA)uAQq(a4T({%*Ob;t7K&L{f?D zS=6o5KwurA|2h-YzK+7KQ|!jW#i1_=1YkMz1aI|@pZ!j`=p6Uqx0Cuvh{_pn#Wq9~ zEV|x}`VARk^USawSO($CuW~8|9=q%_)f=8B>oW2`yIeG?F@8_>6Y8;@qMyfR%J2aWFDncZL8ee2HuvU^?ox7r_QzqEa}_3O32-~9R3+x)^u@t+@P9zI4| zPO;?#lHpW8s%?>+%1VT73MI?K-01mD>FO0^J>Ch8*i;b%v8 zK3w)twa}O&#k3ecKO$LVzyb0I_J-p}A&VfA(X{!2YN4!-W7ohjUWB@p*}YA`P^OSV z(!wN#NgRE=UZ@I3us2g!kUQXy<|EZYjzk*Y5ERmE z#oFIiEkq2Iu4ttbVloLzLCSrwM11?~B`uS=h-T*9z@Zbq7BElI}##W&3cM;l}1Y zW#O{Zx~E!*=ss{4k}D}s8@to7Uf>YjrtTGsB1i00I*AHKp!G<-6Yz%gt+r`Bqgn{{ zl=4vbA_!q0qQH=D9u=UXijZ;O-9^H*!ZYnlktk$FCB4$L-~OxBLh43zg(qzf0$|a? z>@`DQ2XkjNc+a_VIuNP?8af7*fhn7!eXTug|NGTKuHA!BW|5tohZ)*R0@Ru*?&IZo zdVorl#!PfoX7E@Lr}~K0E>^44ZQolh)OAjSwZ`_G{IaHN@zX0IgGC%4yV;8N`Anf3 zp3xhS4zR?$NZn3*E_A)5s(jtwfi>x~O9971?}XovFfWE{^~FpcSmV-#H$oK6I$^o?PhKdCgs(hhHhy+>XKo3UHQ|uM&Oc9(w+?E|E=#{^RvamX zl({LE6&wm6;kU(VWnb_s)c>36io#?m8XReQ^->n`&X+_W1(LWr!_pE7NOWe|@X5#> z=;ehdiquo+jB2gTLQc0T2r!G02wf8X-I1Ho<3K?g99|$GR!O|J8J!{7Nc9PD*n9PV zwOL4@DyMR+N4&^~ihE?Y6$^(0%knea`FypI?Ce~) zdF70Ft}OLd`?$#!j>NNwZl~&Xt?XZ5v&Rhx^iV`|h1#n2pH^@Gl-$b;H|v~^^?v#0 z#=H>v5f1>KX&uH2ju@bU5HDSjBfOTlC*XNV^a_RyP6=2~0RyboKeSnBpi&>B+&N(| zdXg-yua96uvjacgjN&b`^NHcTrA1~CzVdn;IJg=&{=;e^Se*!fM)3&w0R@}T0n>r4 zK!);fT&Mk|v_!*F#F0lp3nB!3k@gz*RgXwj?1jf%t)>~QqxiW7+H!<#WV6ByxVN$7 z87z-v2h;gS&`0*|Z-ScLU>o-ZI|l^wDjt*dykjU(ITjNO(tAe8u-jlPawW=)V+qzp z1T~&mU62G%pPq=JQQ$CY%oTtERxr&5>{I?bd_$V`Q*4f479(em2aH6w9z9q+A+(L) zjK*FviBJ@)u8dE^}fMEcUqS|L^ z!#@j|>E5R}59eZr^NX#Lq$z}vy&0>Zr|D?%rs{&go*~R>r$ZfFSJ0|Lat+7~XcsmH zM=CES^q4-b!E8)^B2Oo9%Pxc3cW)MgQqTs(6hLvjR4uXB5Nc!SF`XRyhzLtUz}C=S}B@h>)4G|YAuPO*P-duX)2xR)x%kwqx_h&wbo z7c5bYX6aAtS%ovcT!p%-eRQ)B`5Ksl43?LlE*zDhDA#b zQv5IF#E7ECk8T!%8@VDWf=db=We;~mDlBTp*;J+-s*F9|xNC=)Tq?KdF--Ihl4yKx zvk;D(rGhWUtH)#VK;g=$3kZ~voz7|v{^uRR7uY7a&=azcXs)pN&O54ws8pyZ!3Sxl zP{7EmeBy#G3^5+Jqzjwa+)IGaQ41MBXJPZxK{Tq@j}~(JfgFqjMvIZ$-LYn1bMY2X zATNNQr$7LWxK3UNU|mgtPjmuC^^a{9>Z>=GT!B0@HnZ!eUP(`g0{$&>R^Tb&41YP| zi^-q{(MWRgAaxxzzG<_N`3)Yl7_5|$v%;)p$IRMRVbNQf;XX*mHqR~J?q%)|>xc};xsw-kFC$^G4 z9JM?Qg0#p@$FaQ$AJ|qL9?PuF%YPNmtQrwQjnBnD)^DkOw#-bYwVgg;v(D+%=G&`V zQ^uedNs>$CiE|+7r?v%YWkj+LYJ^Bk`kAVq6@78!0EeXQiAIBd<3hC%0++Up@&VWB zj_iO?v--X;dPxLuEu>Usj>}NJtP?%1~5-GeK8XDf6eSSF*5BCL#-4sD%0-RST_U zK!1&Z97=>V89hbfb(yH5S^5K5UZc%&a!a}%sb4K-GJ24QYOk*r;(Nh!(g`^Gf++Z% z;_{%n+TX5@0RrNnBrHd8*9@UWPEr2~dugs{9BdX+@Cf}YHx!E^>6N?z2n;ptkPD#{ zn6?*Bx<(Jm@qCs&Qv7O%dtLkQtA%*`Bv&Mn77U>qctifXLZLU}px>czCl;}1OEPW? zb%d~^q^ekEoyM#>A*bYVud6Ny(qhRClfKf)Gq?RC`hfh>0re8$EQfM>;0-9ZiZI26 zhe^lhz3$9vPb?RLRuhUw3uQ;3f^=G#fJmmH)`KEcDw3`Mv=9c;kPl z7Fv2m=sYmfe{g3aA>iU>0v)((z!^co$sUOcVOabm7!8Jj^21*3b=5+7ygmMJs;5eT zq9R!@;6j-iwzY>{oaPN%i%P81Ol>3o+l)9RptAAX)k3f~H5ODv_xN308@5Tu3#909 zF@uEoP!~gXON?-VL;`pW=I~zDwFjz&=BkCrp!AcFG#rBfiPXdA#VXM$`YxDI7-hI) zkceQkSPIdFXq)$IFD@4X6fu+W60lWuW5l*aSVDM97D6v;oN|wp6gaC#gtkN;QpYJU z!&Xn4n_HVJ@$5Q3L~o-0v5QJfT(m$(Ow+P6f?&&QjPCX!Fw}IG$^la0J}=&r+W<%R4P2t*cH_`r8a6(!H*k_nidlb_b#{_ zaoWQ|LaX1GNV_GDUD-r-)aMZgzx<4rtN(kOE5en^@G;h_VzucYQ9N43j`KU4#*IqI zMI{gp^hb)}@kzuzG)?Ky_|45i;F4OB>pCm~iJUV?v!%p{o2EF)gHi{0>;I=Fp$D?L z$nF>{)K24zOvwU5+cI%c}Oq;D~fwsm)xj69@ytnPHnnb zh_n}zt4+uXmx@`&9ivG!FUD7{D3I`!;LJHd0TSCmzy%6zUQNIDuAPh5e1qv)QG|@? z?mU$xTAf7XG?9up??QyrqbS&Zh76HD7rqFEmcm&af$f^_+}xXFrD>aH3FO0cym4Zj zqEqQa^`=tJJo7a6IAfBo{o*01UKdi#de!`mY9W*_qD@M}%c>930mSF0YDPThob=O> zf8-a)!%^Wu;C|FlFq?9Yjwa2wR0|PB{C#Ff@E4&$<@BDEa*1HL_RDn&ra`U48r6@PKas3nIZZr2#Z8xQc#c7g=1zLuP6A z%Z2zAg)OlGun69pc0_&Pfp3D(R&%UO07#^@R!RzA0w}MQmcc4EL3^{2{IK>k{E9>5 zK{}cy(5NPy6xkj#Mm5xMqg3*uJTu8ua8q8fWSn4dJgfOJx z4Jdiq+&C`;MR62%RDkfSAYo@(W?FwxJ!`vyKhfyzPFt}!fZkxn)1 z&_adj3=8JYoGMBaVYlD<;mtxyYlm6ZB+sBLuos{m*4tj9%E5086JvW5y*z~HdFj{y z{V(r&svfrd zq)3j-Gg%nGiIH3wi(E=!T{_E5Bc2?xErKha4#yhnPuzm*N&Cl?rQcZ52UDb25U{Za zJdw5_Lm*yN4;XNPASGU;j{%_Jt&q#LaHs`zEpkqbM(yWSub;Zg41wwsUSoh@)I%kb zb;UvfUIu`rarmw)P*k-{1a$*1v51 zV(TT%KW_d<&4b4O)A;4a-HpBauhb7}e^h&a?S(sku=9cARsEy*ZwH5I*-1A*G(S5I zhZ*I_9T8>~1|UiBCqpnMS~U-@DlHu4u2M=R5Q>P$o-(-U2Dty=Fs(XS)Vo(NlG(&D zst9HW!Ioqr-74E+TF23GRf%`1cC4rr|GZ|uy}VcOIrOCrhJC6ufCMq5C+fHWW1$ss;F|k_5LN z^^J}%r^#{lps=VaaHbH~Xp$=|)|O6NPL_^!R!AcuVpvmXr|Ab4kPd_orI0#MNa0kZ zu#U-KoGalsK=;_CuJW5(I_){d(p+LaP6FWtX$U|<92nB%1MiPuChRyfg{TM>DhP2Q zl1BtyF#Rg~{r$HcR-HO>61G+m3Yw7#)N-nyw7&9GI<4G4k@9&A_@GpdtPrBF$G}aS z@Vly<;rHKsSaY?7H5_hY8S@zY>e%FQ!YXQI2k{^hXnG(i(kN)cB?TakGCLSOsI>D6 zV-3Im_QM_PJ=ufBQl5%q-56i*yZy(@B{7fClUkmO16%|SG~C7PY)FB|3K=N7KzlfZ zDsd{9pfR1kq6oLZV70a6kCjVuwJAF}8_=#nG}8gp?x2DrUa$ul;$O3jx1*CA%*Q5^ zu)Ibd1xI&l$v zk_SI($pir)4TAHYDQtPHZXY*j4>~(|JLWaUcLopWfY&hXSaHBw7v}ZjgCjH5eFtwZ z*Ay{gXLnneLq!Q3tF?(QxV5zCl6F!P7_Z_^gx=z=@NdfuHxE|qz zxMkAHr#F`HgkI5Xfek4b z8Ks+0KyYPLp(6K+j+TVQ)O^@$0GT&)>pJgzt2J}lon=DgL`9kkQLl?ItmBlGmRLjC zQfYT=My7>AjK&qJR!Q*8jK;g*Nw0`e_ucuHa%JSPrwW|&8{qt%V^Ilwu_*~XdI!m= zo}|?jP<{_2(AT&}Dfd<*uv+<)D(TfLzp-4IY`0bo<2y8}Ib?K__~Js` zO9zeyf4#6G85qE?YAw->de<8_JdKouCa#IW}O|rN(sVWmP;0tU=TXS+C5Y&`2xk zK2?7a%=yCPj;mLGeYvu3hwwP`2c}>hJF!H~K+xz{e9$V`IQcLyt4N7lWMS$3P+Xr2 z)#p_!-}X8yr>zOI-4`1*+NO9ymHI_2CzK8Ba~j;lPbfzmVvV$oGxvk1Lva;hq}QvG z*L^qtkQI}=1VBnyeAKuI*D_U+A-2R9?}#BU-W6n^xaa9zt5;fW6|(#20tCmbSv}y$AP~Ym(YdGMkpkCaMYqCcBaHmAk~DLS$~=`?_4y`GKq<3V}S0B_s&_la$K63qh#~~ABk~JnS`T33uOem;|B4_GOx4aMG1%`OJtxzRz)cw zDx8n^pWb`h4_Z0FN$o1ids`a1LOdI5hPjBeDX5_=wC92xf~Vvv;SQLV> zSHbDtn_q3kA~fO-B`bm&uTs2?=+(rb&*W(zCp^#`ngaoI1 z4_;-t1gG@ns(WZ;c0HO(Wf<==k(Wi6v5#NKCP0(x2Js7EAVRFo42FVXG?7j1wm$f? z<(d?iv`)xrfJNMh$_0bRkYYrFbO2>;&kENlt%ERx0n)UU9$<~`+_b_<5s zk1J96_-s?YNs|p^Ry-dKL`0xqD(%8-u;);r;t)bIV@H6~qc!)HCDs9HE`~qo_lxe6X#xBdvpEg>bLEE;p|8E-qZNx&inQs zs(p?6f9+E{Kl>=Sg8vVG)D?Y0X&?-{OhmllUBcp}F^8`RZnU$QD~(W5JIfM&0K7%n zfD~yiz#ZXt=`EF2hSx`I76Hr}p#Vv}V+aznz+Di(O-y%cE=k-lHa$jQ$`GJc_KTFf za?O`ioEt;E9m79^(j^Xai-`obz0x!d?PO;mK+va`${yqkycu5^WR;<7v)cbzErg~= z_GJnHk1X%L5jRC0pn#x-v&gF2pfxTWaK%BFsBQez4jDwVXnf>op$vQxwPXysNZM&K z?kZGn>TNJYSpj%)oWYS6GX8=*0F1zffQjDfDX;@UghuX2 zn)=_!pPW&yC*7#XI64=}d{(O{S9WhVm5^ajenoC_A998Wy~!xisHZJ)<;D3gpQb zm>`M*JsF)MTpBKynJmMF?m3smnnG>zYNIiyF%sY%_FHeQ76J(hohakx1bDQ*g){Na zC<|g2PbFT{Jx7U@{R^vt{hkI+P@>oPU#o?Hxm42tECrhKNbL*6Ljmx}a7%?MkX)rf zB>;9pfoYf0PSH5fORc+E$mv4XxLVg>&%_K9EoY2O;EPfTGx(=z9Iyw(?U?L zI)z}$#9^b+IUs9 zkj8RmNR|#Z>dYZbF#*Gc$I;r(1Z_qGu;tY`sNxN)n0_OE8NWeCbz@mA6g4JPiKObK zlJt>gTrfHvyafzl#to$ljD!|$!fAEUX;n+2XOaH>%|bD3>i+f$n5yUIm|*7dttJ&Y z26003qoli@pHD4l4_U=D5nGkFVAlFSHVaWJU>SlRW3GLvPk^N5qLh0Hv}ut*J)wxw zfg{W~zqV>#srg#VTGg%P=-}p1aI7~gxbaPHlk7|`Oo0A_>w{#CV*YZjv&W_$xvqIw!m=U=eZJ>MCE zH(o28Zct=q68J?aBh0A~a#yWCtQPVJ?6aBYWJaBP^R^^+C{bOMz>Izp3b2G&EU(fI zlgSyc_1~mpv|j1IH@p$X{lBdF#dEBkaYLn11!A*$NxSBjDHXkPh-sTkt zaN&@=JLyjK2T3quMBU7IWqz1-`-RmNfypMI8IEC&MW&c>vM>%nHzFBQh4f}K=A3Be zRV9yAA@Q1FZ`mjN!}jK3%cEk0lo6XpOl9sT-)D7 zd39Az2ct49K_;RX>$tQ-ME=YBBra*W%|lx?D`!ix0-gw&~#H4LO z$t*Je>?}uFN&tWg@MsLot48Ir?UJ?~b0~zf&vP(=Bb=KQ&A+9p(s%~1cwI4aqenbO z^{4aa3Gg}g8y~N3jlhJyN1~|Q(t((Xe&ly;uo>zQPZZMx-a9AH-Dz%z9l2H#vBA0K z-`gz2F6ppgm*6YZA|l217|VNIzyi)m251~erx@cmSC>ya&Hc?nB14SJ-Vk7;%{xXvB&a@D zAf}koAfl&BCk?Zg$68ol^BVWmU|oOYXrc70VTic7)T>D}GAvxDxVMY-Y07v|pKL|O z@;nKJs$$zb*lAhc1pkn22^^GI+at0knp^RlwJ*0>dZzphDaY-9Ax}vF2=jBvPt#`A zWXFOStb+{4svznz9Ku~K2_$g154lBK- zD@U14zGU_7dC_ED^TT*!GBcIMK% zo%XBS`>kJX{b1{C^Y1oi&2xCOiVA_9axaV)hF(ob-~j3>ZAPH5UX?g9dn z14~`Y+zAsQ`Z$^8$Y6iS)80$Wk%`bY=Fppc%^IVi7Ns`4h($*w*%BI1bt%#bgAjtM zI3F3-$!6yp)e{4tZx3M8*_vp5I^p=YMFe7=$%V6Hiw?_0gL2V<>4i1-2W_sWRgIeg zhrEjdnM>peGiAnhCC<_I901`!Cm1Gfi)nJD#Z(ZtFC)pjR7zs4@XZK9aS1>M#^Yqd zV)3=$5i*8I-$cS8yQ(9M4u{5%U1`7KucTITKjkKs5V1!2=s7Tc;Z(5!X-yJ`;Z=uM zR~!JNw%55JdKJE$qi1^CVb7vr0=b_UQuxvJdj^+gU@W#f8YUvsw8Q8hzE&f3Ff<^> zV)crICO6Gw!-@%f&CQ2hOBNb?X6ymrjN+1p_B45cw?`)u04wsu+G|2#h961){znhH zMw{ddv>OH^RN!lFKkQg98oMHq>rPF{MGs*p@|iet%oKPg2B>+GYY>^LJO!T7;=rQM z90WOg(nrIk==PZgk~ImRsW^QS!0YD$Lhmr>cmxITVcJ2;W5R`QwTBoKzJ)(9^svhr z91YfDVkw4!SQF4wHF4Srw=fKDKvZZ;^g<|oua&9_%Q+sQ~3B{}}(P>i{0k6jvExx>wDphO(I(r0-2A(!o2Nr$W>M3E$!K2W>{&BQ>0 zQ;@U-j95-`7F7rMYtkYI93&e^er(aFmy3!O;+#)e0R2YwU=KV@QOX2ImKTSfiBxz? zXAs>m<1x4-#4J{uIRB5osHc^S;sk{a={los(&njIjH#3(Qhz}v4Cne;H8F+kt`11X z&Z}`qb)xHJOQ-TopIR>J%%z_7er*}`3T^=jq>7N>y=TS_FgZmuk;QCgm8RSU-J9VS$1nN5(FKckP>1y#pq=UJ z!M7Sr6>#7M6R7y=OoR+lv!(ks5+IK6*rMNFF3O`JUIgC9!NOh>frGSzd&v<;bLSZh z&}eE7#TNyXev8%2ELMfUu|s>onUXxGGkPNtvGWg0i#-fV+aU`UVUD(6p6!V4f_{VQQ zU*1SOWrqgvJaX{_&Ze4SOn?;5_Oxdp%hU8MdrJ7}ybF)6-(Dk|fT83(LOY)66RKzG ziJ@-@yqTJkQi7ihU%`V1C5V7AZQIJsoBE{vgq;;?QI@c|c%C`g(Vg)G)!7VwJOQ2n#wb`2!X|K ziU`RJVKpUI{n(=4T-`pkMP7n*D5)N(D5YypW-eO1nZy|M;`Xr-H2dI;nB>9jY(%s$ z)@1uiA5$)B*lkEvT2ID>`Q5r8)1n|8YM(G4DKogZBMK*Qv<5Bpm|L5bxWJ@wY|(Ej z7lpcw^~{FGiJ?XT0Ov)Xl=+1=?$Ib9WM+&X6C%LHwUogyN^Zblt9 z^)lFq8v<9$QR4KweFs^02T02rzI~ksa%*n?qGldd@f#9 z2uZtFd4(5&qGeMD@A#tmN=6o4V|t|Uv`K0$XXr0l9x@<1oeXNE6awQk#i5-&90dyd zX$FD7zhT3-uQXrB$fBw@m^iq>%jZ1i6ml`H0)B=gVnx7#a}T&7Dy%|Ci~1yC1LS|6QK_7Zrc6dG5uT`FHjWXP>tJ`Td8Y`$zvj^Ypzh?7e62 z=F9GY_QUO4b^3p@^`_Q$HNV{ax#n%ny~d{-Z)sdx|7!gazu@2e=l>^9prWQQ02mn6 z2;v2K$Zr5SXJUCu@yq^C1SLiS*pl?}@gtuevGtWz7_;VIJX#3n&p|7d`*UeR!=u9t zP1)!2nI2GlC;lK^55+h}h+no=S>xG8jg5BWjEdRxka!F;GgWP=DpAbfnfL*UF#_nI zI2+?UB?ibISPub}*}An6rB(6n0}&|`a#$&k1;;q@r9^c=SOvib0f>~;-=9SY`PxI- zGW@>AyQ^D+2Njo!HkKbuui8=}B^BUM)iH&_hOWUQh62&#inT~*ZszFoJ&YRvc(V}k z%)ii(516=jY`unKlkMW#%3Rb~&gg{hOxnp*Y^9uGVy19YcGmTmRm7y+dzvxb*B@Pw z8Bqu~e49=OS0x8tc8QoXgDui!$QF{!N{EAqR0BW>pGVO$JZNv2a?l`OUh<+dcON1E z=Oug99?cQor`;nGbu?!7K3gq(7KKaq99Dc)zhiSn!lI>AHR<#M-BV%01$1I|wseRA zaPWSVCh3Sb1r*P~epQGyjRsZF{4O`BDm)LL8IlxpEaEX(`7?*+F)&%nmoy zud5b9@FBJB6FJjR;(QVUoaC19Pf{r@TEQV>k~5UQvVr}k2r+9y-_K~!_`fy_MXtv3 zffNDGcy@9#3IJ7$H&6-r^{Rh2j6aoO^ek(T@=}bNH@3X>Ge-+~h8bq%%#v}4t8^uL zIwx<&3n|C*AO9{^Q(mFWeAcIjPBK?#y=c9v+K=>V98?M=boHYjbG>OM)UBMnDzfMk zWM{@CB1BWgrhzoN7l9(pe>Ns(2ennVGG$+OcQj}9-`?C=xG>rc&dF8VLY{b0aHFR) zpNmm2%2B=*iYQXXlE5P>Db1{_V7t~@Fvov88;u$$t)L9H%g=eOVLi89gdot!yj!Js^th$?g;*wD8O-- z<_Tp8psrN1>s@Ku3e6}|8EDf?k>w&+^^N{d9U>x-E7W-m8?d2?$@L-jd2n0%XA1A0L`+%!3(b!jXDa~f2l4tvTJqICYb zYFm;j+^B69LNKLCocb$RX;^1g%%Be~R5qwG!6mz%3^xX4?8ib7aaSC=Tu!yWzgdWV ziUYtGMOf=NXvhc-TE_lC^E3)Qy-q24+W)gD9NS_xtcNa(EQdSQWMrQA*wBl?W1=j= zlThGUr^`xYMyKWcLl|+FiLfd>I1er2-|Hk8#u^hg1w)r7S#)p!I$4q2zc z(9=wlO?4S-57&=R$3@*Sb+OlY=4K&;r^-?;m*cfFq(NSnHII)Fok4&`KM^0|@3+MP zFi|~I-o4<=6>d}$ZRyh>C?3O6uyL^|K4_Og!rqSd2ZFySjg;U_AaCi55zKUD$NZ(9 zTlZW4Np(f1th9GFXAPlfbV1;*PRDqMZBVqV240+R2FgKJbQU%n4qlx;_(7#QD+)H- zWj8jnU+hH6M&P#sTH3&E*g9D9G!Wx=(z|9qYna<5|5@8(S?nDEvnuz|D&X@DEfpZfnM_3065nlH zaSx6I4DFCJF#shZw5tP+*+C=~Qm=C)gSb83l}v`uI4wfNu<`iI{r^An==A?r z?d|XW-QD@_h4#nV>-IOdKGnLv^@QeUn#*RZ@k@;t)qk)4uKM$8|E%_&+IZ(nJ3sR% z3g24~(=b$g)hH=hsYtofB}{iVLBe)|MNz{U{t5i2(JYVKtP!fLC@1wn+%}1dD|+tE zJ6?9jVcLbB>^n(dw49x(D2}^%5gt{(-owc)1#`Os{yFV9eB}s=k6SS9;*medlY{=I zd3fg?FTLk54MX`(m$Pow_)0P`*E-H0?K?pj-pNPNb)AqO(UJZ{Rbd0eMP3Z2CPa0@ z_oCkM(u2eF4&{SUsAoYIPs#yh@(>hH*v)_tpHxxeWdcGbz;9+RDQStKSQN@^I0W*6Uq01PUqR*%j`_OE--f-*L z-gM{T4_fsZZXSOt{=icdaF?d*Oy7|zhw39W2Aa!<(kRQOc3O41XB5w}eq9qo$CKlq zmD_oTrPDc-r8U6;(};OGDR*RAhocxmeAHCjJT#V;28mmob0^@dhH~&lPS2RHE5364 zj+5t zz;}s%6|OY~9vzmOZavHgJ`;#?roB!HP!(ehJ`#k40oC% zCDbJ3ad}v7y8ZA@E5-(5%h5{|BFLHeZ-@YfSwLR#jv=MMP{v?9b=vH-b0L_}BsL}g zDwSyb;G1TzS>;1B>?*G)*VL5ZT&1-fLg86dn&TQMFEMGXRg}ed6n$~VL)SV4`^yuq zpJ$rCX)@RxKx@9dx=w#DRc_I)3uw41cqSE>fgQOT#sWqXU`GN}$WlQwTPo}fif?i> zZNZu^tJb8zA(*%kq{axJP_~lBA)%$h0pPE03fL8b#k&_Dz@u%6)N;L`zkQuAt?u1R z(Uz~U*Fs9xmTl!_mT;7rncR_ELxbUnR5)tz48W5_d?-rE4!&-$c}KZsp%Mp|WQ+i7 z7mKPOgy*+4K_pZP5SZ^wS;JsqQHZAkv&a*cQ{UNM^PpUl=OcKLyNSy(Um_N|GF4ei ztprEN6qlV_IL2F|E{4`;L&?EqWa=-s*Sx)4(@6D*-oKDa!suxLD@E?KN*J*Hl9bCSE0U@%0Se-d^+8a!s4a@u8>Lc|p#ypxPpB z#QDxhtTyUo=`c=y6qJf64PGK#Omv3TAFX+Cc#Ab7gCLi=fX svj!BkPkrDM#Xbc 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


0dU16SD~Op?(Wnr zRMAy>vZ)>cm0OibZ(d9kGTiQo zTJ-Pw_62wUtK~pR-1qZFJOZeswIz#C;C2Zqc?he3+oFQ0Mq~}>lW2-X5?{J5)yYit z-tS%wkS%dO|B=QWlKpEJUQ$t1=NuWaWw%suW`2M7p~B#{P1QHiy{KTLa|sfdVpB zZ&k>kR;RG6pP^L6M~YT`+NH*B1Nj71IJ;iRVv$BBBHDunp9Du5WvR|e| ziMEP@QW>m0t!qKO3|J*Wk}78wqHDf76{KrUGCNjgsEKaHxlLe{I;d}`1lri{gn$v- z+-?@-;+X`-Q82A3{W+%ORZ71Rq{9SYS1J4|i|CSKR&Djp3YqsuE|)h-n63LdZv^=G zJ&aH2&^-&lXaCHv?|ka%_y0ai^~wD=J^lWkCjfeC5nxoHh)7Qy5RUq&k+>7oITf0{ zeiyN}E)1K7jgww=pBbRbSqk_ZIApg7p790s7|(|6ikEHoKoP472XGhpPbIJuSW@uy z*2D+i6v0MUpkz_41o~Wwrd2M`Fmg(19p_7q+Tc+-T=(EnI_Ur4Q91_d;8EW5^xjOFzVuB&iFo9mYj5l+5h}cciX=S#Y{2XG4I&?=LbT9j%pm}vTxly{xM#?4vUo~|F>2?-bfr1@W9F5i(Mf*FAvZi#Rz{dql0shw% zvMy2!M6T8muo;8Vje?v*%7g3Y~Gtn;8JXJ57 zi^}CQT@H;zr*W*SrMajjazhxfk#yi%%V4oEmpYXD$3KI1_^n?bM2sYKk|&)Onb2km z<7EQmBuWk%qG%T&jxw8IQ3X^(hOb5m&Jh}~l5ss~B$BB*TwN}f<$AjcL_VfeA+P(< zQTfm^a10?~+;4RGn0BnG^FLGbh}lDm7{-Tli20P~C`ZnGw=hG2 zctKx_F9Ot{yje__ik<7lK^w-vVTze_WTwH(w%bBe=tSYY(!An*GltzFET+}M6cCdo^>eU?$={Am?{#|`~=Tp~S@q6P?zVlUAlRgO4N#r*J ziz#0gvUdzBbtQXKU9Htffzicd^kTp)WGG-MAd)tOHc4Kkw+v5B?9fTscRVM!#7^2l zkfU`2RumvQr<00>!W&v_U{f^(A~FgvqB?|p$g&O44;a)^QA2G#SXhb>`1vXv(fsV- zcg{Awb;VEBeRVD(F)ETuKdMqV-;UuXg|t{4va+c`sIJut*@$7VE@|Ky{(r~s-}zMJ z|F`YkM*r{jM^63f$-i*?qsQLpAGbfgxMtu=_SPFE4UQpy2Hl9Uj7m76Z_l~=L4CN_uQ8HH@se_##K}ZP{ z1yd6sF*_Uh6_>OQGdo$V=Cb~SdI}+TSgFlXfN}l>SD)>NjS|;NV3(z(f zg(^B0*PV_wN|@K1?EQOl9NFtnJnAK5x|zK8%0IZgsTPcCQ;ALIG}b2)@dRcjLnF|e zfN)S6ciz)g5v6q;YK907=`NvR?^n&HF|l_O&B-Ux;A{!BJ?mQ$VaW z&nL>jaPCwhVBKie; zu`-kvH0s2m6fuKtl`3KC_QBa*kK~VJxm4oN-WbbGb?q0fjlOYtM)k#452$nx@dpav zL&(V zHF`zbGH6d&A<^ZBdJJ?ps4$5X%7oZV(IUQZFmIQ9WY&;oljN zMam%C%jWRO3=WX~#K9^hm`$ z;0tQB%2@%#_SnG@ENUe5+*R{{dUIFx3H z+iEO9h#=%S76U4gSTZ8;1Ozs!E$}s~y8TA`W4)<~o$uSOB5x(4$p265et75XZ=U*B z$3JrHm43VZakMjV^>yF!jQt_k8H$(Mq$a)T{;*=QZhv5~DSSc22T_xNfm;aznv|h? zRwClUvWO$WRR|@P9PAo-8qQ~6X?=cCA7e%b1rcemo+Y$j0ObIFA@ZVNELoLh*i{Cg zYX-4LA`!fR2!A*agZ8d&>Z92ok)!rPPRBmI^GCkz$~(U~?&{Y?1L1951A%-iBs$80 zdI*>!3WlFBQb^F?6i{d4%dVQLa8ZCJ!5oH1GJrb@F5>gzWvv%z?BA=BaQ#Qz)*8J* zxNUR>^pAqhgNaNqh6S^N2*-LLEm{k@JTT`VCaN-VqG=PMD=HRTpTNgIkmnD+x0*ga zc_e)Tuj$c`eB|=GN0;k&Klo7f$b%2hAI--fdT_a1)_q*q2G-Y zk#Ab=fmi?N>p%GZJ$nF;7TO7-PX!keSXH$IGes7Y$_B#^dK_FIj9Bc3hDH1V22E*2 zv&lat`4R4clpJH$_1}kb$u2Eq`y~FtXl&ecc(~rs%nO!edw*#Zc?&l1hAE`0;vN(g1Z}Cdc`_mUiNXi zq)6Y~%tGmVQm(fDdR3F<5x)KKEhx43qJJst?u=DEZ>`@fyWBT!@X!DI{VzXZ`#*Kz zZGGtdRP~K-jskRtf)xO6k#Q-=)*E+_jtajgWFNxkJe&L(!GQ!FqxHbalVM z!>~~aXyCwy>Cn)FN9pk0gGcE)GlNI*@(y8M4(AGIQNYLm@mQa!P>Jln)>gV7oZYL#vT?;}C_>06*H4;O?Oi>Ui z8GBF2L_Z7_7UTo|n?wd|6l#VCo42bv|3r>f8W6mzT>Pm!%GkDy!_Ia z1R{X4F7X|(Fy{vuQ4OWI+K9;*L;6*C#BszK&6j;og;Fl zRY8s>=-SBF3s{B_kqiLCC8$tfRkTY>jgqByGMyHFDx-4I6+`$Y1(Q*o;3g;-C8wG> zS`_~<_yf9zyAA8yEXZ*vh@Z-u`|DCu+y1r}H^<4p*Nzw3KC-6_+ zfrlp^Tnav+Iz<^KnxJA98Z$`Em|WzaFyrhzg@llm--y4we{a>dEyMCR0=|^kQwS+2 zK7?>F>EbY0qsl^tgn}#6QLHhln4!|ea);{>QCSRYc19tya@~Mvy~}@jAgxy)c92>Q zH#~B+?2p8SyniMA?wc>}#ZvFeHGGA`6aK$;=~VWR+MkCVNawe{ZUMz8-C`EXZpu?{1O_OpW)4)lMx)MdGG zqr#*DPRZ*!c_cj2iI5kCkmh$-j}hac09`7St6L+oscZ}A5Rk>3&83CQJrz{Mi-;Gf z`N4Gs_KNJjB_67(Az&Cb1hzGj7wo0*BPGNJ9BWkns<&?*I!ag282l`zw~Mrn<}^{F z=uu)P-gAEchrjQ;E_Bi9%RceJgWC^oj{udXX~F$GLK)|=lNluvSBG#;MLwCWFFC7B zE@{h?k`E#wLNk(N^fIY$^~xt}2B?cOWN@LvNwh(Hy3rZ3{qW_>mmhp=`sl4x1m3=s zTlb}`UCVMaKi8kjy5X}nz}ET0{jWTC{=RE};5+-&^y)ia|GGH*1OUwAN%w{LEz}*d zmCK%%gKM6OJT;|CuozT+tVt#XZ7O!6+)q4L7&Kj0G&YK4*88wXP-$UNqU8HhYf(pB z!V-y)C)~Be7a5u%3EAYxm$ldwNrg37>!A$lUWy7A;&j_A^&X(JU4&G1>EQ-#LIs7` zs$N_OH;!Q*L%lbo>Hs_0w2)yZaZC)GoOuWXtWwjBN_Mx%hu59-6>?s@k}rXdtM#>i z>Pybw_vDwp%Obk+$PH8^Go>`BA8$dB15rWb(kpKWf+#6iiH0PfXevgd_y+(IdQ+85 zt<3z=w_IFh>pkM~Abh?*+|eP`ru|&VGK5|l@k4?n;IW~N0yA6iHAkc8F9VGM7fd6N zkvd+#^n(m?SRDA5Q?QJb@nJMD29MIUWGJkJm^fW>IS#={Vv5{pXgGpiDcg{Oj_8KR zQuw#VS#2 zm~y8+QOg3lFw_CVa@pE-ZuBLB~J&i?B&zxTF3d*bJJ-@TK4f%N}M`n@-$ zlSwef1}HUd63Ie4bQrqOocGruo~OU%{@DI^-gPzk;k)j=H|`K9ZM1sz30jpWN|fL&CdR$;^E`py1J{@3&SSKsqJ z4&m-RSvHKK6cVgC3=~~SG>QloX!Y^{bi1~SS4r*@oqej#`Q4ln=qn~d&FeV+1@M#h>k@Kf<%%p-& zHvCw+R686zN*7`b9>s0Yl^)jX&{#52jojb+x99hteCd7{I^LkI$2SdVPWwmWDSWY& zzA&>e7-yl{q9z?qih*j^DrNxZ9#INMr~z@pFRqGB9;JVO&vUbAk=aDx=)zIn#uysd zR$j!86H@Vt&Y^HRA~s!PsRq+c`#k)XxG98bU)Z6rQD7+HZq|eqcF?~ea5i3&;Ulgf zpg}JxKE)^prJiey_PlB$d`Ms%VQy6uE!MDd$|rH*!{MrgV{0vQt&ou?pA)Z%L>zf* zR&~MtW-2(-wFXEIUq`}v+J{8qlrnjku&W9|cbjCDYi(3yUUx<2PO%<{BX-<>vbe90 zY1GNPyco7PtijL#`CyT^Mp&mo&%X~z3uT=@4#P?6{pMA3n_QvrhL4OOO~?jda%-&1 zXT&rTc3%kV&Db3g*i6~TY# zC|yi5c&+wTMHsx(t=TS6*l63Wdxl4Kus_42vlIFMvC}WvIsf0>UixMGV><)e z8Q9Lib_TXHu$_VJ416)pz_nLQyR`Q1P|d!xgC64DQ?-Hfg5A|-7%RSn&sG@15{f2N z3-G6zCs_bVgiI{-d_i9%zTsasU6O>?YaT3v_#^mm)Rdy{=Knk0x1o5pMa^O;EOXhx z8_26zRX&cSos%s}P8~S#`2FX<=c8ZJbQQU~uf6HXV^wDaMO)9gazeU>K>pjMe zNnzLh8OrX7%HD8n#ig!PG8Cb)SeAoF>6=LFRloZLek}<2i~hsUEaiBse%@TrSu{!Z zZ~qtPzvoI?-_)1pF-_?gE?Z9ovmC*kCnYe8YAT#FClw;7S=ix16u_?V=gccUgVeov zUMcu4t2%4F4HUPoFFm*@y;?CF)!Wy6#$HmQJISIi9r^$6$=w}4w?DQsu$_VJ3~XoM zi)jXa{QGC1{hxU0!HJ(d*QYxj4voewOWL-e_|GbvM>g=0{uW6VTpds=-OM!8w4-U; zxeT-6pokf${~p@E34U$8h;te8Y(IRXH0O3UwzKhBvaMb0yd+|+7+GunCUUvKa2J0! zVUfWZVW+q2l@a!yL}k9MZ!qK%_&LUe}%;JTslX z4%}5ZNke+c{Fa1MyWdDmUtY52q(&R0^KT>u5w%IP->iO||jSdU9TmoM`FlOX@6Uv}GP zPFBZXvKIdTy*JfanwGs1T^a_7)Nn}OfTHr0LT3rtNJeDg&V)wWmKC865*>upS|+IF zBqW8(CG>PXoVB83SG*ovnd_lY8--8nK4Z|yvm2Cz?tk(%?@=+Py>#X7-o1(>P)U|x z&VF}13T>#Is~2r3+UHFh1{BF;1jh?vMDqvthrkrl4@g})Wpp`SR;Si`qU9=C@55pT zj~_xvEU^a)G%m`hL$#)^SNFF$#d8qlzYg5$s2a}iYv4^HfRYujW|@3s}5lrdT1`V7OZZm z6}Hh%JAw}@(Q17~P~7=h3_tI?%YE?LULe?JQ#8Mt8?BP{Xv{cSWg1MF5iQ)X#2XK= z9AVhcaV#ATdXPZ11^+%3Zkh+H9kNlQVZBLq9(cx;Fq#I~{a0Umxj)YOHNu;*^5|i! zt4GT4yXN^(%u8xU69x52380V8==>-06@HU5I>Uv_0<_v!Sc>qiCmgjr&er>|wr3n? z>gvN)wHC+OV2WQ~<6rMQ-t^mA;n_<6<xy08f`%=x#3P9lQxIIbYUCf%C3 z=EubEYwnuVSLjpWEb~f7T@D*XaCcniaznwyO4H`P$3_ixvLYyW#W-{Omu$|)$Qz`s z*i0fRP`4^-&fxUmauiG;YHZJnJ+rD)(#%&Ye7(2c$3<)EOJqXqtS3fljNJe6S!;z~ zyQg~UT_69tANrvn?6H?CvkBQQB0v>9aLUn%Vbz#!ozu?=$*P3Y_muD>`L3FQG@Uj9 z5mKvdFk6zoH-dG(ct5-%`Yev*&dl4TgcdneU!^-w2anPv7K2CW5{t6sOlUbXM9R1h z65I@4eoKF8PG+5(4SzKxV0xSxem(=CBMlnDfo~87d zI3kZz`d4@>qZthXtxk5yM5k~`Yfef`f%9?R&KIK$rp@+8*Ad`TmRoi^@_UK;301l* za620>^x0VN4F4P|{*Ix~^V3bgu@TNHIh}a`#>^4M!TH3rgtHF?*WpqwRT~wS3 zGb%x&rFi5r45z(q@~>?@kczx zJfvv1+n~}W*Nu9PBvr~|(w0h@Q49F7d){Y>{Kz}s^GDy( z0|*+rp-qeSjI}%-)uF)}_AXO33$mUWtaL@8g#=@cdW|KlJ0d#H0SPMk-((|g`DL86 zQ7XOt@a7^G+j+iG|3gtp;=0C94eSsC8ZP&<;E@j%4Z74d)CZ5!H6#a*()G&*kFscM z4j!iKY7QQyYlIITWl?+>s1J21%ASX??t(OO2*Y&6^1(B+DDDiMnQmf+%I__@^6fw= z?dv*Zo3yspyTBHWyuPY==4bSF&OgxC?XNs?_uZWgSy=z2lpWo@B<_@b%M2b~>qb?6 z3NK%xIB7blPZ3EWMxxI{rA+s7#FV>TRy8+@G}im@lCIR%?FY|tw}uJ#(R5+1K$&Gs zxs$XX37JeiZcfWGZ^$ol)AqflbmnJd@=__d zsaUXo^3R{YzvKV!{KC%huiD%5%M0(vm9Kv=nj5bSfA)JivYuX~g`8&OuL!T#BjGYl zK*H0NMgox@fi&kpttxRRwItCfunhrvVp$`-_6^YpbbSC~7xL6>$@$ppTePKS-bg=` zo4sFHhr;);h!U!&)GfOaMFmd%^%O}zE`avQBLXMww^ds^c4E1~-f z2iNZU;4;Ho(H)sCvIfZJAsd#m85&QDXu@b3HQGd>L)g>_Cx(_@66bZ|qy@2crae_X zskjs^@GI-B&gGXlf)6j-mg27tFp?4mgScumvZz4TWAaTjRcp$(q4=qKuG4C{w9P!X zERrcFREaHA);KSSOSX&DOzx{QtJT9LsMM{UO32voSVgqA9Ej7_f4~P~RFxndx%1KC zq53`=Ylv5~(Sn>74ntIeGtLl{yQ*rMHOz-}*=G6#Y$j;GwlVT(#Ca>^peoWdB!x>I zB{=Kr;*}d6MC;A5#5m52Rf2cn;HSR(10dpm^al=J`6u4oXLw~&5iDhGK&6s>?8r=x z!V{Mea%L^FE7Dw?w(viRofu9}ahJ>slQ5p4VBH!^2TiCBDWv@l@94`cU7gqPjfh}~+*oYX-z3)2y z9ed}E{ENpPH}XH*`}a2~GMwGpdD|wR|D2zF>+yp-VEE+VT-vzI%Kr%wX)kRI#hX;)G|9>4)xv#clwdoNl)1Rv2>vs(G+D-Qd}4!!#_zAvi3k&n!boN{i{urtWO z+w_ek*Tj<9*D^>ROBAEV!0tm*J{j-)`PD$fJT>#5 z?v;xU(ij+NI;&G_idb@yPAOBf98pB#;FHhyUhx(ydp8)H;WGq7$YvQ%mlY9+V8N}h zo=eb~R{IYvp#pqAXCRU1$`jPBNryKRhuQqL{(-{Jr_j9jU2GMEHEzisL1r)2w{Myh zLq&6UJYgC+%2Hy^6LX=%&ndE=?Hu$2G2c)l7)2R?)^p>nRwFLVCCD!k@~JWu8|XZo zlIF?Ce>!3uhfcuAV;*IXy{oH%8lXnDx7I6Z`x4kz(N@)ZG|N$Cltws9vMRT0*kjb; zKj+j|xh|Am$i(k{x*y2bm;%r%R5{{^RQdF?XCYD1{(Uc*G>|9~ShQ@gGtG z>jqP9n{&YiAotP$VJ99E00qGy&-dQE8YpA9SdO=NqBadTE5hrhhA~fV zgB0+${?zE18ldW&bA0a|{Z<=Db-3I=5a_+XnA0SGpfST*;?j0`zbX{82%6ylvf?SWSq(m$RB(gA+gtrk(Iz_a^yW+hEzu(Eu#( zo4amZp?uqC(vPOUpG@)t z8{B}~k8WpRI|I*c23BVXlQl6+4mCCtx3N>3G z_P(tj2ocFa)8hXKF0hlZA>r+`jXz(B8f=L>V`!&$GPD8R$RZh-AtU#Q=DIRa-MvyS?HQ0Q@8VH`hl1?QX-TFIH(~vbfMB3Oo)| z+BC|aioh!VNt5Y%@yVRG1T=J6tEjIl+2u0iQpHrAkeq^+Wy#>c$DJQjm;yo7*uD3di3|FFC29G z|9c%`AkchMxO7OWZ))_oZ2f`aXN*75E9;L&OCI#I)&496X^%U}p zjHYfxFo?pMDAAFa)Zkgd9|*8=3UC5_I7u4D?*6;1S;iGZN9pEe@Utx9(V@>`;W*fb zj?%rO!J{0`JL(XmgP)~C3lAQpgHc;u)E!B*fK0*Og}??PS(C$Pr8JrCRcg(oTL2Q} zPP$a`D6-@89GeZ$r(Cd`M@gxX309Z~mI!Xge>R5W0p@Vs+Q|*T1md4uq-=pH+D2C~ z)s=1Nkb<)<*X|4|5jQMecwqnFrSxN^!(;HXT8~YvtxX09gWcv}fOBzX5-&jl#sOoFtk^RfQvXsT0+5{w@_sS0|rZChR&bd8@6hds zA1c!P@$GCpG#$PBeGfdoosADcyuI&5G8^mV3pXg9x-FjDPnXq_yS~CRGUYj^)R;LNbvF!zAZ$tiC=4Ukb+afLHAe$Q0pn%s?Y*6)7q9Y`oShT zYPG4LR^SPe4k4Y*N2o5a%oVX?Kr+HU6XF`Bk~ro9;fl#*Rz474L$iKVV_(t2}FRvfN*iRV zr6YmVfitIep%k#5%dOO0GmW_rJFkE%gUSUBD)GxR`I96soh+2>cwNY?nyTOvl$SEB zsdnFdilC-d!~`J8!Yv512pvo-NJMH>OV8MN&AzAaV`>a?)=B;Ez@kD#|Q` zFoQq??TT!Kf6z*L!|)zL7KoOciOOEM3Un#=;87^dbV=dS>;=1^=WJ@QU3u!89&vtL zyC-?-U6)__lBkh04@wZC2=bso;H_q}V4H?lLSQ zmLASZtUpL=JUJ`^luUHubW)AROM&Bc8_^_aIGe)I#{AKX; zd8z~Z7yiFR{=c{Prkyjd^FP}k+Zotw27c^4Ph32>A zk%NgYwd0lcGhNjA6f24*9GWiovM~RGP(nV9IE_YLNXct<;TI>1P-;E+(J~r2f)6j{ z1J&sYm&<*R1rS z=>h?cGwOgo#_w@NEV3Z8sw0CCFS)})Lffx!tXwOt3>M$DmA{C?^ zd-;LKZWKb_u8r+%%-e@P@V-a#FWR-S-tF_*dFAH~;1*%rn*W=A;c)NsM1o)NU+usC z)fcY(=(j(v1Sd)Q*qyx!Y^z63d1V~RBt*kWITSR5=OZux zGtcA^3&$*n+N2P?JZRRd!EQhNz(<~Vuzcu=Tgg1_nEjn(ex@w6vqv*?wN3$^8QA4Et5y|(Y880{hoN$JXvM`kxJWL0=8azrT z+c$WW1-@?hFkRVa_%Hzh8$3!^R2w|XLV$JnFdeLI@GyNT>quHtVN=!jRZe~M!j-`P zpV<8yJLihqpFZ<-Cw|#)w?AGuGw}4OAL`m-uYdAwPadnPt`~qHl}aC(uu(-~%SN+O zKFCF^3egYRXtG73X5(>~&*G?&6sv*X8e|rd8_PTS`7XjQFYqIHj2pr*xqZ^*_JhmM z-8nyalb8QixZ?UUKjMnT)2aA8G<3&Xkt z0n6Hw;1!=7%40BL6_Nn~G$@a0O)hrq6LXzZTEr^lO?d850;Nt2^s&i#<9VtM5!U@r z)>-@nREj7VVNmkML?zYXNzjHGEXy8@8VRJo(r?^ZB`vwKhGO%8&OhqQquJ`ZI{l_SD# z?8{=#QQ?XD(#TCf<`%PgBoS0= z5MU>L3e;{$KPxeI;?pOXWmk|gkA3+`}BpYPrmj0yetNK z*WP^P&b#jFdR}-fh%LA!c>2tpg2zRD87p<{_Z4K6#!KRLY8eqZz%`+k>@{VngBp%t zlcm25lh)g}NB7|y@$B7IHS`ed0@uNZ;KBxF_%I7z*wF73h1m`nw$YKY(TCSt2ff_J zQmH`%xqtGTFI&7vwriU_;DpYIJSb=zI=gMp6AhnR)xk=)0I}3 zCYKyq?JC%E-*J&*chUSrUy~Q($u;yGgE~fWJ1IAkW7)rVvS=9tIA)USoD(6ZIfjxL zWw4isu`{|HHCc3a-sHUl(^Dy&JL2bb|Fy^w_mXr4vE8Wix^6cjqOt0RJLuYzFFn}n z&b3!xdt-kI(SDDVPx_FCqd{8Hr&oZbhXJ5e(ni+$*0pwcchRTIow{;E_8b=<+H;9a z-e^5P-yiO9vw@h!hDP>-XBgP!Sj6Do?w}jukVbKr!HW(I)K&}gNNSWxz~{|;7aSe` z%!OE(yUT_7rare7W)89`SVl)1#S72#hr5Ld0BT_de34>JLjxDx0vtOw8wHGn&x6xt z3KxjyfNcS1Bj}B?O%|p#Es`xDaOWz~ER#b}LM{>T+D@eqPp`p)<98!FU&t8{aW=R- zGX>UA#6^mEEA%3P?5~lF9#V>#c727RNaoebT<=!gsN!V)<4m|=Vg9kNKiGkPQhNPp z{qH6R_Z-|4u^^f-CxVGoCkzhc?V?tKRt!BS6M0Y4aJR@tk|avatxl*+M6ft6LQ;rk z{xTNamdrMn-MBF%T~RD!r?}No%>k(9g|ulVQ6kbb!zVxtad`;{!f#`m15h#1;S-xn zK4cu?0W|q^3A-9R%5sShe3r!#KJ*(|%7-v7T?20LymYbl;8E<%Ei->hFQ2xTADX(Xmg*ag7-!6NXoDN z!GqhZOPT=DvRDWMP)0fDWTeUDX;HW6Z5c`Ym5ztun={1&)|8_fv6{b&N??vCeA>-xpsw z1dIJ`85IK*!$Ky$j9u3&-YxH-b-#ONkv{Xgr+p(&JLB5bA2>LD;1jBR--N&wV|%HCa-U01JGtk}9f7ld(`6lkrXSC30ZBFQYQub?<Bx2_InBZeUz{*G`fIq*lIE@eeK5F2lr=bn#%-hl$)+h`Yq;3TFT~m1KJ%?a zbLt}KFU*37BF|9X_N4{rdnkSJQdi9wJjw!;Jbak0!9IMLF3uS|%%aFUe3(uKVel|r z@oMlWX;(=bJj?=~KX{nKd6=$HIrv%ng4{-2`Sp%pm^o{puf?DKV;8RJ`5wQi=j#p= z$JN9eLSK|Kcf(7&ZU{wz0%5|Ya6kA+GNicGvWEK~9}Zz!c*285Z*qruphZunAR9_v zC%8|cxk7?fXb5G^+t`3-@0@A$4>p)@&D=AHNV~d*v`OnoB@0A+-L2<5oDc)7f)4+L|Is-h6Wqa zel{jt#`O{-Wg zLgdEz3Q1XvefRqwt0IOcNuI!{5r1YM&Q+!uVRu&k9JmugkIt8Vu+xq~y5p3PG6Pcj zU94u))eLR~KV0{D1LAe#Gnd}_@47n1a|^9 zdmOA(OPP9en<3&ttZ*L4y?m)(x?c6Ie|e9+@6pF*mmi-`9tJze9vQu_eE5-R`uOyL z#~*znojvsU6Gi#N6Ovt*L%{g&du%#TWOX5TKN!2#hokG&oR;7AMgQUT-hGd`T~*-x zW_eZJyj0Kp`h{yxKX`E3Qhjp&%};-@$6qo`$tea^OhPP6Aq(sFyux3YsJViQPC;5{ z6*5->C{LCM;ygUXQ2`i#Gk#c{GcWr0@qB;5?oe7$xYQM*dXp#w1^(M*I?xMp(W@HV zs_3dFlc5RfVw9n66Y-ih!_ZN>Iz{U22I|&JY9XpgZ?W)TH8d}&YL#@gMiVp(ZKnzV zJi)@KQGODxC2fsOgA2JhDJM9RGKI0;hMkkZY|o})>&HTIKA~(PhbdIIEBlWXAe?iX z=&WK_Q*LhNb$v)>zbMjovB7$`;YJ5;bXqLSzW0N~)#d-kcCPH4`B!^a{PH~fxcdDE z7gybxcn(S{5L(A_K*n!XjF} zw~cbf7vhK4o!1+0J--jZ)thg@(AAc z{=KCyUh?a9ykcLuzOp=(y^vNA|H@tH?*t8NHpv5FRu=Im%2@@|lSYW#^4HKi7?EIH zq>vR+drbO9DW8FiSZdTDaVByeW*G#UwGzgSIE!#|&Xuv`moaIS{+0A+lBQ}E=Eo)sQ(nl{XRW?7~PuX&8hNBXm^$8^$*X({#N z9z@0^PA(ZC$M}+;tFkCdhwh(&x@`H~}AH6(&wD{0QRihXEhdYSjXGa*TKZ(>b zu3$)L{h@Y(Tn!MdxDM+IT$dhuq zL|Xg)gmQm}i{N0Uu*J7&l){WRY+ytlO*qO>PAbwwuu;{-j0~^oe<|xaqJxJyoKey) zQyTm*i%fL*FkMx7_%K}(Ie3`EdEJNem9nnz0JlMMXrc`6s7&b;QTod`bSPoYLeZ2$f87u4NVy5J`&J4rbWUlu%>XZ~CB0LGv*{s&N z!GZCNEMSn<(z$AGnPsaqwqK$={KQS`bp5oMi4`r@L~Az03$nn-tEz^t`gPo5a52Oj-swgUw&tQV&y|S?;I9 z$FaHtBkC9SQ{UosT$1;-%O(j*4Mj0bYs}P&0fEb#29tO_Zq@70MHcmMv$KYww4^|c4Tt6vg5KGADt&*QwCn^KmDT&Tk#pu&36M2Dwx z@UnWWQWXv^5Fss;g{cZ zAC5+CKEl(UgWZ8V6l1&>RJa<@A;mD6BaY|@l^ROSJMD11xTH$Q6tl;y1ZK;$cU}C? zxSM-CJGPnHQ`5QKWaH5qoUq2UiMS0+7!j0JO7@vH1j|gdh%%QIX7mafZnmTA8)+k` z)Vj|Yb*FOKM+`2`92e)yZsOw5M3huFoKjHR(@Lp;>a=FrbQC8Bv>_UN1v@5~NaWa_ z-s|S4!1A7SM7h*LSWj$)fZd9UBN6Tnw81mj_x;zKl;yfx_jwD)>s4Y3sxAr`=9;{Q)neI{1T^UA z5_Tv2T`w<@jY7)phhMN&|1XFKh%0|^rM63V29MIw$p?>;bvUTO!$d%I2s5*^MjP6* zSOC@rf2jqaYVa^ibHAZqDm$Du!Qs4bm+j^?_4R^_jUtilhllzBuXkI0p@Z=YgQNCe z`n8Mv|M>3i&h4K#@eO;G-+ZzC5GKF;rZ73Jx^NwlP|Cl{MErPh7MxCP23fWHguo;rQ-y9IVZvVL1%1D*-i zD29dIAV}G)0aTmfst1F7B&f~moGU#HT~P|3M>*eMZSOYaVNn7`=FfID&O>wumJ zTm}~l7m4XWy37)nFm#j!pkU}IUH@<>0qBy%X+f|)lHH)N&~Jtl@3}bt*dIK&(`E>` z(yRK-pqb3Rcrq&F)Vcg9IvrAJ8I8t=A~XIOAI<JA>I zOF0LR(xuUZ*)9PCIs{Tu7_BUBl1;i6>z&&+aunK^^|I4vFV3I-z`<9zNuFWiI#m_} zI*cC0gtuIV3Ypdb{+U{|-_lEJtF$S|R3vJqui@x8>6%Y+3uI$%hDeC2?G?qinxrcI5jso~i$#Do zl%{fNp)fdjnC`?GJW99A2anR7=YvP-PQSsUEH1j?!}OJ$^(ydvxqe+QOEcE@|4;tb z#d-AqkIDK^{ny9t{^CpicXqD5^57Lt+$(q9eKqNOniKN+oM|gM=IH+Ke* zQFSB{i8(yh8gsZpGL=BGu-uLQCpG?2Fy>equkQ#x>~?V+m2Q}YFePkD-3CxS zNv8Pnge#__Fbt#b2JxDR#6p)cdYU{|8#y{E8ILao5TY|_NR-Eg3aXOKLm-`EUgS0) zEwnfMQ#K`06HZrpy`(jTbHHEPh?6;lXCW5tnrGxKyHSX!mpl)7D@uXji&MRJ;@|$# zR?6h^5*?}{Ai1$CsFe|B6BM*b*>Hy?_OX(eTkoRXXp)&XI-5d+scT~$zyIR=M_+Mp zml{#m#=88{mjo0-lry7Pue?zbi=To8hp;DkD3FIqNt}<8A(YXq_FKLTZ3bMeAw1y; z)}{cui=bk&E&e>)&-EwG_AwrQ?BREBG#fYR>)Dbw{p`9}6drjV=k_9A2owqYKs#6b z%P7+F)3hg`SQVI3&@8*O2zmMT4I5_Bj~F_N8j1GP&{0AT?GT1p z(n|(EO&2H({!)v!(cp*a&f~!kv+NTNewqajXy`C~wzVEq!M2L*Y5%snF3vxZ9ekyW z{D&W}e{6+HhN`64#G3;=Lo|(!)pRDAlcvgTu8bZtbNKM#uBs?fH`z=G9#S*>{|Kr* zhsU?J3|IP%c8qKc$n_y z89d4&=N>%FqL?*we!Gg};AdIf7=wrD!i>SAbm_$4QMwy>@F?AdJbV=Ux~n*DR4ZHW z@N~BLs(r=r%$F_l{}a2vv~%_^p8mO0??3rB{C4}}g*F2}{OG~mQ8s2neR-vEjCn3U z;Cn$lPX;}iHsJxD$ir#fWIT_H3JI2Rc!nhk%}VZOnvu+JKnse|()(hgHMsq7Cv0*f zG3*&-;K8NA?wA@pN_Vjh9%XUM4j<-lYOmeRHvC}@rzqWn54P2vXE?a8S)sPbZ~AFI zqZD`3&;4`Xckwbx%2zr0I+jvjzAd;i7C}9lF!7m@>e30DwFSwIIx8FgjC5paXmbmz z3TUoc(X+~ulyWwn20qhsxP8|<`P{zI@9W}rb!84id7TpqnP<#13RJ<8StNoL?mpsD zlGP{^!0;X4K(7e#M5>xaNi`oW>Zn6)LRP4;&6et#Z?gNgeI51Kr!QWHwR_dgSi7W2 z>6sja;YQRZCvdahKB2cQJcJ~ZDXMF+s@2o;0p zUI2A;5n5F3uiJCJ>*8ftyWeAtKLcw=f~82l_GPGq+CBBn2d{L5T)QWE>bsV$m{fNOZH+*Cosfa1*qB5Qi)g#0&fJ53 zZ-VTNl9<=pPUMs*ABGBWQvykhi^X0(f)3pBtG=K{?y65#x{b5Eg}c2o6us3|b_VMP zElM(jRe8J01ML}ihzk@TZ5le7{A`KhRL;m2{gFcq1T(rb49z4#0VWzfn$N1N1kyxc zO%_r}gGcGM;NVgEEx2Ag)H@Cc!2a>47Wx0~>3_T9=k~{T2DUS>oq_EPY-eCQ1KSzc z&cKUz20nJ;;H#r!u>Z!VKm5@<-uAYDv=Oe-0jLSw=mOA)2Xq{d%6cmJonbhtw$yx> z%B<=HPP*7cOfeTX@*U@UTO{k{t?!Y{`rt3ghXD9K)eWU6o_-FkOpy z@F-pNXYeTL;XKUYfUQW!cV)~MRAq0Y`q7L2!(F*%g$TO&H_zHPS+!C2zhr#z@)>vB z8}|Om&V|2x;R6@$KL7X6|M>a0pWi+A^XEQz?yJxKgR_79>^Gg=yZx`+{?P5OIr9(C z{N$N$IdlBf27;cH3XS?UCEQ=HzEi{>;g5KY9AZ-#qb=6K^>F z8^=F+{5y`{e(V>IJ$`It@a>Q73_QOx@Ydr8cL;H4Oo#6Dh;A2sPx5^_gOifoBLX(% z@0@%sr9!5lAt9)QjJjQi(}XC8TtPY7`?=LX>VqO5%|!a(W4^rYoHPUaL>V0{zB#&D z;Ta^R@^7eaQyS+ONr<$W`zv>Udo@r}cC;`i05guG%q&M#B`A%nRW}uo2BSI}bEFio z2ABba%5LLHvx)k4((LYC9|)JDWCsKyPW3x2MsvD^6wOB|jW+`3Y=bE+|r${f)?&o(cJ z7Y1e+pOUvL+6ob}Xq?5%foh_!HHM3KUT_p;C(3`Y)~n`6j{m}$P%#?+2yACWs#ZHR z(&$1;O$d7BRaa{A+fbuT94)(G9TbsSz z{_WE}q)ScC#ezg#;_yX@nnWJbE(*(Ou2+cUlUl?%o7$0%!C2R6+)_E5vM4QdVNs;!&0g6LL<`uo6!8hZ z#Qq*arCe;&qQHSq!+Ol#VL&^cwbM|{<{XbHS~a@AZJ8H)-@6(JCD>!k5VL}{<_w5) zW)$usbrbp%L|C~R7vqN7K6Z$Xx@Q7)CwZI}_3qRCKoy3S2j`tL@l{AZ>Wz@KNpQnj zS)NzNkpwdZSrzSVMz0uF%z+dO&v~@>v3{U(ob!{ITlnN2>2GGmC^SGBk&(w7V>B~r zHC}6{a33h01UPp(+8T59Je};d{Y&E;F!NUFV^qw}y*@t?)}SZ2^R`XW&}E$xltjQH z^<2>}$wTVABzD7(p+Gg5YcxA| z_I`Rb5b5XA%OSFf;6rqC?r5GG0I_T|GtXt-x}qG@Jm$NxfV!AX~L;Tp>*8X7FA7;7jh>g zRyFQjEvgaYFo`C;+ag2}-*&+4{V&%C8YJPl^OOBRnVYM|)E<-WcEC~cE(+(h?NYXR zIs+z}c)-?)qPd6^I&m6VOj^xz1j%Oi_pJs}Iw5Wuhl>*n907Nq&7Nq)!R8KZNEkb! zO6EdlO0+^xS6OB}qd1l3&F+8KFYI*QPKg8Hkn;;7I?D9+3KD>+VT(agvd5^q0{Ec4PvBR=e#SaY^cro-?3hsOI}vl^%%I^x7> zr_e+(shFxwm>|+40iW@JMQKcjCo=;7GHw%RenHR~MQF5lay1YH0mJXatEWujIIaO~ znB};fM$Dls-o&-+$~Ctc<|n9$M=Tw)71OBk-rM?tVzF{7FF8rsYl#2j5TrJW0p<`+ z8I3Q{Fd#8!@uZlI>46DDpcX9sw%Ge0`+>}jD}na3V=^ovMR{1J)FQ+;?h9`Kn2gae zuo;ChI<*l2!~gnxI-b|Nx32~Qro!?C9`b%10I(6Xfos5EtH91Q_l+L}D2$y5Y`ilg zf>!X0%*(wymjk6;Z0D5mDrn&rDVZZdKuQA`perIEuhdB%?;i*`ai0`3D>nMzBl7)?+vSgJWL_X zawxd99oGV5&c#Nj}_O?UtK^?|%FH@^4ozmO0(c-vX(jtt`_n#zo<$$)r1 z!A}6@#N2Le^K2SN_0C5-{{K$7bNp9!-{+S#e*p5`a}$uS=wN5bETZ3bQc=7bkJ~y7 z(!?xo$cD}{8Fj30=Y}uLUr8|BWWN5$SDtQys3V$(Cg85~KJ1nAg=j)3EEI*x2nkbvMDM0}?Ou>s52zKI}X$s>P3Rq$@W<|PEn z4*{9ip}z;?*cQC!q55@Q&AOgB9%d0kaRwmOW0CW$jOcm zhdHE&Ih+-|2to%srZ1M>U~GC%XI~GSz6=7kAHGrec)i>A*+iyn`(*~YYWuH#v;WdR zd+~DQ|0j1}yL0}R&wk|8zdrdZd*ACHwm+U_2Gl&h_NJOg8#OI8LkS*f4h;|0SyiN# ze!qHIRp@4_C8>>NxzHqBt5!9Sryn?Y!xCut z{$3kh?v`R4O|3nzn?k-x15= zXTuh`p2{fMz)xto=@)dif4WbKa2lm^4H`ZeS`g_ZE3=s;sdlS#t;wO}s}0D;B4@g| zpf3*|rHg6?kJ53l29MGO<%37*ZtlUObWz0MQ5GRhsUS5$&L%U>nNL}DtncM)3j+PP zrhhq4HC}6#@eFi<8m8t7WkTwpTE@+IRmwV&^CoIoH=miS ztSg$4-swQ7Jt#hH#+5RAf!eu5p5jcp`z2lt7mnd!~B4NwBAdkCtynM zsWErGZYP6dz&y2DI^HXPOH<|Sg^|g~!xExSLiDxCwSHBq^`xrBD1|AcM-ErR*RRw( znZd_^ERZG-t4~dGZ4}gfz$-nDgeiEbGo|M<25gyC^sRf5^`o%s$l^|NYIS$g@%z=` zf9T+AERx_5zpKBKv^T?@rqQ<6yVUt_>Q=vi=%!VpJqJsmI0!y4uHch2VMFfKmkitq zN!q1f$Md~NmeKR`{TSWN;cQ<0>>?M)u3AHk=`M^)7O{vXD7ObN6MgTI{?wSrvTK4p zq*c}#K^F5O1no7o$Hk@Yv^I#E1PHy zjbXUb9Ll39%LR>p@F?ABKX{aG4-OusOQ*SOkP*oNuHb$OcdzDo!Ic!x9qfq*6A@

GU#C6Zzbw#12bPsf*f+41$P#5Y=L3mShP7M}P`=kz-cUe}$|cU?`c+F$q#-oNq)C~^x~X>tM?of z&S6o|mAiZWtP&UsBBuaw$)`}!kYahwfjKSlKW796Rt8Fu&o<~*qf!vh2Tm)lhoAr_0%n5ZQip+$aR&Ic3HN!* zU+&38+^PW&_Df<5Kvh`GfLfvh(?mT9oQoI;pql6z9?lq2SQuP|h*2{Wai=hHmwLJC z_0%^vs%a{70AmRup|$_G*lT}QN29j)_Z)@-^Fo*Q8#_Gzn*H^a@N*l?w0lP8eGc| zs=pSX6%(`vOI6h_;D@lfA{-f;)#!TS%ppbM=>%K~B?%g=W-`seI(d&m2Ch=1Bbl`B zTDSqKbcb^x{j75#a1-R|SQRC+JP~;X^a_Fwtp)5-3Q4a`W^GCS|Lolhv?TX^7x;Vc zJT(u=V_CAsPU1LOkqBE>S5bV+?GSyS} z+8WW<*P}Z#J^!w*`agb;?*sA04OQQhEe-l$tz<~&WxHleeaWRF!0DA)aT1{Ku)^ZC zlmTc``2xzL>Ryy&h(cx&$t@ha3xfO$s2ZFr5(L#bIP)qyP?MCw<_uROg6QW1kW9G7 z1jHMV9{`ws6yq(xx{=Bk?CKz4*qSKEDt$q0E*@61NxS5S4N0ubE?Ax6_Jtq#)K>DQ zdC}tsldX4c-BF@mTHcD+N0rt35wb*Cxm$);A#evyiR>{()v<6!Fq`HfBtA&H+z`+w zSIvRc#G6rXA9N?M!0HjojzyAH8~+qiJDq~*ca$=E9x!|~ymA*v8=u3D54tZaf*Mo- zkzv3fgKBzPgsdQCRc?fy_ypWm$vx-UnJ9~Za^Y^NAb{YC0>O&quP7_oCNZq-Z7GvQpTO_ByqPJa`*%|smAHx#a4he1| zHkn}g3AQw_oaXlZc|g~wuE8Gc(igH-JBeUbtxSLAzi&cm<9Nl2){b@ouEA_ zkED1-knkej948Q=BuKx=2>LIboQ6PB?TpR(Nmx0q=Am-|Gx3h-w9x`YiPK1w00#A5 z!Ud87$Pwm`dDq|R(&$el5-QY)=rI=Q;-)p*;N~;D={1(=MBNSCbN{h7zjSnKP^P+H zf8q9*&re2?J86<40Ha{D#gP7Iohcrb#w8tqtcO&IS_In=J{o$&Nnid~=(D2nOFl3r z=c(>esd}>)+s&h?RNq z=6Gt7-FfdesLb>Vl)f}LL;Au_241B0plKF#Eh^bT*@sxtbIFi8r%*V9kqn1EhOS(# zJ=?4VJ%3uO7r(+9VWpY2T{Eqzy#P4G)xJft-MIrnlkju%Er0gdu@}C5E4Lwp-u``c zZxUNndl{h{y%V`n!5H5(lgbaA;~4Y9fyOFoJccEvH}2kmfhVDAyetPvhfU{V+bK@ks-h9u;^nD zuux*tbD8I8jf(Dd(A((6j#N=>>XvG!pk;IQrYbTNqSI0kDWTTmEXyNj{${9=q&hrN zgiNsaeI^YTJWur9kc6os*w)Rd#gW=*C#;Xu)r+6ao;`CN-;HJq^s=q?O4XX!A0os5 z^}jcd{lc+h(f{AK@sHPz{PKYx+W*OY$NaeZTHS#+$Q^k8uC4dYGveA85y(u5W-QxL z8O)#&6V3cFnJwB$pbS6+r!Qe zH~fp6)d#l8O+@Pa?r*#V8NY0Q+&r|Yi1OjBZ@;X(tcuqI9*>+iq($hnsa%l6q9}v3 zBG0B{141crrqoC20d&!P(L5|VpciQ!x?Dqfx$1egEpP8`d3hju+E!jBcy_7093`pu zURIJ~s-w|9AZ)4GI_gpcQf+`x>4npsWk;ec36(1fF*N)J6shPm^tw1EU6}Ua{BAZw z$0LgR5J9xZlJTO* z(X7fek3pJVW0a(N+b}C{{@nL%{U-7Ai^qP;=l9j9tQojJ&^>fN*a9mnDd>!{Vo3fB z#~==!5VT5X36+ZpwNiH&1cC@t)SBV8=IZQAUH|IEpMsb&y~`&3*ZOXh5(o+&z^|8m zM*>!YBU+cvOH_O7qIa3Nw}u<<%qV5{-}+kIVO*KE2zxVW2S#y%?Z7CNm0H_RRO!%D z#2ix|rtsE<#sp&{ITuo;pfonvM0a#dC=omixf9SDI)`#9Ix4AAY@1!K_xH?z1>*VO z#<4fQaA@n>=Hrw2>J1|NOk6r4(X&P&kB*J-sw(pT+Z?5qF ze_H?l(Eo75QwRS3{%>9X({GUE?~VL1UV3cn12(aXoA2|}Jr%4OWMA`vkSGiS;Y