Skip to content

app

Oracle

Source code in nebula/addons/blockchain/oracle/app.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
class Oracle:
    def __init__(self):
        # header file, required for interacting with chain code
        self.__contract_abi = dict()

        # stores gas expenses for experiments
        self.__gas_store = list()

        # stores timing records for experiments
        self.__time_store = list()

        # stores reputation records for experiments
        self.__reputation_store = list()

        # current (03.2024) average amount of WEI to pay for a unit of gas
        self.__gas_price_per_unit = 27.3

        # current (03.2024) average price in USD per WEI
        self.__price_USD_per_WEI = 0.00001971

        # static ip address of non-validator node (RPC)
        self.__blockchain_address = "http://172.25.0.104:8545"

        # executes RPC request to non-validator node until ready
        self.__ready = self.wait_for_blockchain()

        # creates an account from the primary key stored in the envs
        self.acc = self.__create_account()

        # create Web3 object for making transactions
        self.__web3 = self.__initialize_web3()

        # create a Web3 contract object from the compiled chaincode
        self.contract_obj = self.__compile_chaincode()

        # deploy the contract to the blockchain network
        self.__contract_address = self.deploy_chaincode()

        # update the contract object with the address
        self.contract_obj = self.__web3.eth.contract(
            abi=self.contract_obj.abi,
            bytecode=self.contract_obj.bytecode,
            address=self.contract_address,
        )

    @property
    def contract_abi(self):
        return self.__contract_abi

    @property
    def contract_address(self):
        return self.__contract_address

    @retry((Exception, requests.exceptions.HTTPError), tries=20, delay=10)
    def wait_for_blockchain(self) -> bool:
        """
        Executes REST post request for a selected RPC method to check if blockchain
        is up and running
        Returns: None

        """
        headers = {"Content-type": "application/json", "Accept": "application/json"}

        data = {"jsonrpc": "2.0", "method": "eth_accounts", "id": 1, "params": []}

        request = requests.post(url=self.__blockchain_address, json=data, headers=headers)

        # raise Exception if status is an error one
        request.raise_for_status()

        print("ORACLE: RPC node up and running", flush=True)

        return True

    def __initialize_web3(self):
        """
        Initializes Web3 object and configures it for PoA protocol
        Returns: Web3 object

        """

        # initialize Web3 object with ip of non-validator node
        web3 = Web3(Web3.HTTPProvider(self.__blockchain_address, request_kwargs={"timeout": 20}))  # 10

        # inject Proof-of-Authority settings to object
        web3.middleware_onion.inject(geth_poa_middleware, layer=0)

        # automatically sign transactions if available for execution
        web3.middleware_onion.add(construct_sign_and_send_raw_middleware(self.acc))

        # inject local account as default
        web3.eth.default_account = self.acc.address

        # return initialized object for executing transaction
        print(f"SUCCESS: Account created at {self.acc.address}")
        return web3

    def __compile_chaincode(self):
        """
        Compile raw chaincode and create Web3 contract object with it
        Returns: Web3 contract object

        """

        # open raw solidity file
        with open("reputation_system.sol") as file:
            simple_storage_file = file.read()

        # set compiler version
        install_solc("0.8.22")

        # compile solidity code
        compiled_sol = compile_standard(
            {
                "language": "Solidity",
                "sources": {"reputation_system.sol": {"content": simple_storage_file}},
                "settings": {
                    "evmVersion": "paris",
                    "outputSelection": {"*": {"*": ["abi", "metadata", "evm.bytecode", "evm.sourceMap"]}},
                    "optimizer": {"enabled": True, "runs": 1000},
                },
            },
            solc_version="0.8.22",
        )

        # store compiled code as json
        with open("compiled_code.json", "w") as file:
            json.dump(compiled_sol, file)

        # retrieve bytecode from the compiled contract
        contract_bytecode = compiled_sol["contracts"]["reputation_system.sol"]["ReputationSystem"]["evm"]["bytecode"][
            "object"
        ]

        # retrieve ABI from compiled contract
        self.__contract_abi = json.loads(
            compiled_sol["contracts"]["reputation_system.sol"]["ReputationSystem"]["metadata"]
        )["output"]["abi"]

        print("Oracle: Solidity files compiled and bytecode ready", flush=True)

        # return draft Web3 contract object
        return self.__web3.eth.contract(abi=self.__contract_abi, bytecode=contract_bytecode)

    @staticmethod
    def __create_account():
        """
        Retrieves the private key from the envs, set during docker build
        Returns: Web3 account object

        """

        # retrieve private key, set during ducker build
        private_key = os.environ.get("PRIVATE_KEY")

        # return Web3 account object
        return Account.from_key("0x" + private_key)

    @retry((Exception, requests.exceptions.HTTPError), tries=3, delay=4)
    def transfer_funds(self, address):
        """
        Creates transaction to blockchain network for assigning funds to Cores
        Args:
            address: public wallet address of Core to assign funds to

        Returns: Transaction receipt

        """

        # create raw transaction with all required parameters to change state of ledger
        raw_transaction = {
            "chainId": self.__web3.eth.chain_id,
            "from": self.acc.address,
            "value": self.__web3.to_wei("500", "ether"),
            "to": self.__web3.to_checksum_address(address),
            "nonce": self.__web3.eth.get_transaction_count(self.acc.address, "pending"),
            "gasPrice": self.__web3.to_wei(self.__gas_price_per_unit, "gwei"),
            "gas": self.__web3.to_wei("22000", "wei"),
        }

        # sign transaction with private key and execute it
        tx_receipt = self.__sign_and_deploy(raw_transaction)

        # return transaction receipt
        return f"SUCESS: {tx_receipt}"

    def __sign_and_deploy(self, trx_hash):
        """
        Signs a function call to the chain code with the primary key and awaits the receipt
        Args:
            trx_hash: Transformed dictionary of all properties relevant for call to chain code

        Returns: transaction receipt confirming the successful write to the ledger

        """

        # transaction is signed with private key
        signed_transaction = self.__web3.eth.account.sign_transaction(trx_hash, private_key=self.acc.key)

        # confirmation that transaction was passed from non-validator node to validator nodes
        executed_transaction = self.__web3.eth.send_raw_transaction(signed_transaction.rawTransaction)

        # non-validator node awaited the successful validation by validation nodes and returns receipt
        transaction_receipt = self.__web3.eth.wait_for_transaction_receipt(executed_transaction, timeout=20)  # 5

        # report used gas for experiment
        self.report_gas(transaction_receipt.gasUsed, 0)

        return transaction_receipt

    @retry(Exception, tries=20, delay=5)
    def deploy_chaincode(self):
        """
        Creates transaction to deploy chain code on the blockchain network by
        sending transaction to non-validator node
        Returns: address of chain code on the network

        """

        # create raw transaction with all properties to deploy contract
        raw_transaction = self.contract_obj.constructor().build_transaction({
            "chainId": self.__web3.eth.chain_id,
            "from": self.acc.address,
            "value": self.__web3.to_wei("3", "ether"),
            "gasPrice": self.__web3.to_wei(self.__gas_price_per_unit, "gwei"),
            "nonce": self.__web3.eth.get_transaction_count(self.acc.address, "pending"),
        })

        # sign transaction with private key and executes it
        tx_receipt = self.__sign_and_deploy(raw_transaction)

        # store the address received from the non-validator node
        contract_address = tx_receipt["contractAddress"]

        # returns contract address to provide to the cores later
        return contract_address

    def get_balance(self, addr):
        """
        Creates transaction to blockchain network to request balance for parameter address
        Args:
            addr: public wallet address of account

        Returns: current balance in ether (ETH)

        """

        # converts address type required for making a transaction
        cAddr = self.__web3.to_checksum_address(addr)

        # executes the transaction directly, no signing required
        balance = self.__web3.eth.get_balance(cAddr, "pending")

        # returns JSON response with ether balance to requesting core
        return {"address": cAddr, "balance_eth": self.__web3.from_wei(balance, "ether")}

    def report_gas(self, amount: int, aggregation_round: int) -> None:
        """
        Experiment method for collecting and reporting gas usage statistics
        Args:
            aggregation_round: Aggregation round of sender
            amount: Amount of gas spent in WEI

        Returns: None

        """

        # store the recorded gas for experiment
        self.__gas_store.append((amount, aggregation_round))

    def get_gas_report(self) -> Mapping[str, str]:
        """
        Experiment method for requesting the summed up records of reported gas usage
        Returns: JSON with name:value (WEI/USD) for every reported node

        """
        # sum up all reported costs
        total_wei = sum(record[0] for record in self.__gas_store)

        # convert sum in WEI to USD by computing with gas price USD per WEI
        total_usd = round(total_wei * self.__price_USD_per_WEI)

        return {"Sum (WEI)": total_wei, "Sum (USD)": f"{total_usd:,}"}

    @property
    def gas_store(self):
        """
        Experiment method for requesting the detailed records of the gas reports
        Returns: list of records of type: list[(node, timestamp, gas)]

        """
        return self.__gas_store

    def report_time(self, time_s: float, aggregation_round: int) -> None:
        """
        Experiment method for collecting and reporting time statistics
        Args:
            aggregation_round: Aggregation round of node
            method: Name of node which reports time
            time_s: Amount of time spend on method

        Returns: None

        """

        # store the recorded time for experiment
        self.__time_store.append((time_s, aggregation_round))

    def report_reputation(self, records: list, aggregation_round: int, sender: str) -> None:
        """
        Experiment method for collecting and reporting reputations statistics
        Args:
            aggregation_round: Current aggregation round of sender
            records: list of (name:reputation) records
            sender: node reporting its local view

        Returns: None

        """

        # store the recorded reputation for experiment
        self.__reputation_store.extend([(record[0], record[1], aggregation_round, sender) for record in records])

    @property
    def time_store(self) -> list:
        """
        Experiment method for requesting all records of nodes which reported timings
        Returns: JSON with method:(sum_time, n_calls) for every reported node

        """
        return self.__time_store

    @property
    def reputation_store(self) -> list:
        """
        Experiment method for requesting all records of reputations
        Returns: list with (name, reputation, timestamp)

        """
        return self.__reputation_store

    @property
    def ready(self) -> bool:
        """
        Returns true if the Oracle is ready itself and the chain code was deployed successfully
        Returns: True if ready False otherwise

        """
        return self.__ready

gas_store property

Experiment method for requesting the detailed records of the gas reports Returns: list of records of type: list[(node, timestamp, gas)]

ready: bool property

Returns true if the Oracle is ready itself and the chain code was deployed successfully Returns: True if ready False otherwise

reputation_store: list property

Experiment method for requesting all records of reputations Returns: list with (name, reputation, timestamp)

time_store: list property

Experiment method for requesting all records of nodes which reported timings Returns: JSON with method:(sum_time, n_calls) for every reported node

__compile_chaincode()

Compile raw chaincode and create Web3 contract object with it Returns: Web3 contract object

Source code in nebula/addons/blockchain/oracle/app.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def __compile_chaincode(self):
    """
    Compile raw chaincode and create Web3 contract object with it
    Returns: Web3 contract object

    """

    # open raw solidity file
    with open("reputation_system.sol") as file:
        simple_storage_file = file.read()

    # set compiler version
    install_solc("0.8.22")

    # compile solidity code
    compiled_sol = compile_standard(
        {
            "language": "Solidity",
            "sources": {"reputation_system.sol": {"content": simple_storage_file}},
            "settings": {
                "evmVersion": "paris",
                "outputSelection": {"*": {"*": ["abi", "metadata", "evm.bytecode", "evm.sourceMap"]}},
                "optimizer": {"enabled": True, "runs": 1000},
            },
        },
        solc_version="0.8.22",
    )

    # store compiled code as json
    with open("compiled_code.json", "w") as file:
        json.dump(compiled_sol, file)

    # retrieve bytecode from the compiled contract
    contract_bytecode = compiled_sol["contracts"]["reputation_system.sol"]["ReputationSystem"]["evm"]["bytecode"][
        "object"
    ]

    # retrieve ABI from compiled contract
    self.__contract_abi = json.loads(
        compiled_sol["contracts"]["reputation_system.sol"]["ReputationSystem"]["metadata"]
    )["output"]["abi"]

    print("Oracle: Solidity files compiled and bytecode ready", flush=True)

    # return draft Web3 contract object
    return self.__web3.eth.contract(abi=self.__contract_abi, bytecode=contract_bytecode)

__create_account() staticmethod

Retrieves the private key from the envs, set during docker build Returns: Web3 account object

Source code in nebula/addons/blockchain/oracle/app.py
174
175
176
177
178
179
180
181
182
183
184
185
186
@staticmethod
def __create_account():
    """
    Retrieves the private key from the envs, set during docker build
    Returns: Web3 account object

    """

    # retrieve private key, set during ducker build
    private_key = os.environ.get("PRIVATE_KEY")

    # return Web3 account object
    return Account.from_key("0x" + private_key)

__initialize_web3()

Initializes Web3 object and configures it for PoA protocol Returns: Web3 object

Source code in nebula/addons/blockchain/oracle/app.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def __initialize_web3(self):
    """
    Initializes Web3 object and configures it for PoA protocol
    Returns: Web3 object

    """

    # initialize Web3 object with ip of non-validator node
    web3 = Web3(Web3.HTTPProvider(self.__blockchain_address, request_kwargs={"timeout": 20}))  # 10

    # inject Proof-of-Authority settings to object
    web3.middleware_onion.inject(geth_poa_middleware, layer=0)

    # automatically sign transactions if available for execution
    web3.middleware_onion.add(construct_sign_and_send_raw_middleware(self.acc))

    # inject local account as default
    web3.eth.default_account = self.acc.address

    # return initialized object for executing transaction
    print(f"SUCCESS: Account created at {self.acc.address}")
    return web3

__sign_and_deploy(trx_hash)

Signs a function call to the chain code with the primary key and awaits the receipt Args: trx_hash: Transformed dictionary of all properties relevant for call to chain code

Returns: transaction receipt confirming the successful write to the ledger

Source code in nebula/addons/blockchain/oracle/app.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def __sign_and_deploy(self, trx_hash):
    """
    Signs a function call to the chain code with the primary key and awaits the receipt
    Args:
        trx_hash: Transformed dictionary of all properties relevant for call to chain code

    Returns: transaction receipt confirming the successful write to the ledger

    """

    # transaction is signed with private key
    signed_transaction = self.__web3.eth.account.sign_transaction(trx_hash, private_key=self.acc.key)

    # confirmation that transaction was passed from non-validator node to validator nodes
    executed_transaction = self.__web3.eth.send_raw_transaction(signed_transaction.rawTransaction)

    # non-validator node awaited the successful validation by validation nodes and returns receipt
    transaction_receipt = self.__web3.eth.wait_for_transaction_receipt(executed_transaction, timeout=20)  # 5

    # report used gas for experiment
    self.report_gas(transaction_receipt.gasUsed, 0)

    return transaction_receipt

deploy_chaincode()

Creates transaction to deploy chain code on the blockchain network by sending transaction to non-validator node Returns: address of chain code on the network

Source code in nebula/addons/blockchain/oracle/app.py
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
@retry(Exception, tries=20, delay=5)
def deploy_chaincode(self):
    """
    Creates transaction to deploy chain code on the blockchain network by
    sending transaction to non-validator node
    Returns: address of chain code on the network

    """

    # create raw transaction with all properties to deploy contract
    raw_transaction = self.contract_obj.constructor().build_transaction({
        "chainId": self.__web3.eth.chain_id,
        "from": self.acc.address,
        "value": self.__web3.to_wei("3", "ether"),
        "gasPrice": self.__web3.to_wei(self.__gas_price_per_unit, "gwei"),
        "nonce": self.__web3.eth.get_transaction_count(self.acc.address, "pending"),
    })

    # sign transaction with private key and executes it
    tx_receipt = self.__sign_and_deploy(raw_transaction)

    # store the address received from the non-validator node
    contract_address = tx_receipt["contractAddress"]

    # returns contract address to provide to the cores later
    return contract_address

get_balance(addr)

Creates transaction to blockchain network to request balance for parameter address Args: addr: public wallet address of account

Returns: current balance in ether (ETH)

Source code in nebula/addons/blockchain/oracle/app.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def get_balance(self, addr):
    """
    Creates transaction to blockchain network to request balance for parameter address
    Args:
        addr: public wallet address of account

    Returns: current balance in ether (ETH)

    """

    # converts address type required for making a transaction
    cAddr = self.__web3.to_checksum_address(addr)

    # executes the transaction directly, no signing required
    balance = self.__web3.eth.get_balance(cAddr, "pending")

    # returns JSON response with ether balance to requesting core
    return {"address": cAddr, "balance_eth": self.__web3.from_wei(balance, "ether")}

get_gas_report()

Experiment method for requesting the summed up records of reported gas usage Returns: JSON with name:value (WEI/USD) for every reported node

Source code in nebula/addons/blockchain/oracle/app.py
300
301
302
303
304
305
306
307
308
309
310
311
312
def get_gas_report(self) -> Mapping[str, str]:
    """
    Experiment method for requesting the summed up records of reported gas usage
    Returns: JSON with name:value (WEI/USD) for every reported node

    """
    # sum up all reported costs
    total_wei = sum(record[0] for record in self.__gas_store)

    # convert sum in WEI to USD by computing with gas price USD per WEI
    total_usd = round(total_wei * self.__price_USD_per_WEI)

    return {"Sum (WEI)": total_wei, "Sum (USD)": f"{total_usd:,}"}

report_gas(amount, aggregation_round)

Experiment method for collecting and reporting gas usage statistics Args: aggregation_round: Aggregation round of sender amount: Amount of gas spent in WEI

Returns: None

Source code in nebula/addons/blockchain/oracle/app.py
286
287
288
289
290
291
292
293
294
295
296
297
298
def report_gas(self, amount: int, aggregation_round: int) -> None:
    """
    Experiment method for collecting and reporting gas usage statistics
    Args:
        aggregation_round: Aggregation round of sender
        amount: Amount of gas spent in WEI

    Returns: None

    """

    # store the recorded gas for experiment
    self.__gas_store.append((amount, aggregation_round))

report_reputation(records, aggregation_round, sender)

Experiment method for collecting and reporting reputations statistics Args: aggregation_round: Current aggregation round of sender records: list of (name:reputation) records sender: node reporting its local view

Returns: None

Source code in nebula/addons/blockchain/oracle/app.py
338
339
340
341
342
343
344
345
346
347
348
349
350
351
def report_reputation(self, records: list, aggregation_round: int, sender: str) -> None:
    """
    Experiment method for collecting and reporting reputations statistics
    Args:
        aggregation_round: Current aggregation round of sender
        records: list of (name:reputation) records
        sender: node reporting its local view

    Returns: None

    """

    # store the recorded reputation for experiment
    self.__reputation_store.extend([(record[0], record[1], aggregation_round, sender) for record in records])

report_time(time_s, aggregation_round)

Experiment method for collecting and reporting time statistics Args: aggregation_round: Aggregation round of node method: Name of node which reports time time_s: Amount of time spend on method

Returns: None

Source code in nebula/addons/blockchain/oracle/app.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
def report_time(self, time_s: float, aggregation_round: int) -> None:
    """
    Experiment method for collecting and reporting time statistics
    Args:
        aggregation_round: Aggregation round of node
        method: Name of node which reports time
        time_s: Amount of time spend on method

    Returns: None

    """

    # store the recorded time for experiment
    self.__time_store.append((time_s, aggregation_round))

transfer_funds(address)

Creates transaction to blockchain network for assigning funds to Cores Args: address: public wallet address of Core to assign funds to

Returns: Transaction receipt

Source code in nebula/addons/blockchain/oracle/app.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
@retry((Exception, requests.exceptions.HTTPError), tries=3, delay=4)
def transfer_funds(self, address):
    """
    Creates transaction to blockchain network for assigning funds to Cores
    Args:
        address: public wallet address of Core to assign funds to

    Returns: Transaction receipt

    """

    # create raw transaction with all required parameters to change state of ledger
    raw_transaction = {
        "chainId": self.__web3.eth.chain_id,
        "from": self.acc.address,
        "value": self.__web3.to_wei("500", "ether"),
        "to": self.__web3.to_checksum_address(address),
        "nonce": self.__web3.eth.get_transaction_count(self.acc.address, "pending"),
        "gasPrice": self.__web3.to_wei(self.__gas_price_per_unit, "gwei"),
        "gas": self.__web3.to_wei("22000", "wei"),
    }

    # sign transaction with private key and execute it
    tx_receipt = self.__sign_and_deploy(raw_transaction)

    # return transaction receipt
    return f"SUCESS: {tx_receipt}"

wait_for_blockchain()

Executes REST post request for a selected RPC method to check if blockchain is up and running Returns: None

Source code in nebula/addons/blockchain/oracle/app.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
@retry((Exception, requests.exceptions.HTTPError), tries=20, delay=10)
def wait_for_blockchain(self) -> bool:
    """
    Executes REST post request for a selected RPC method to check if blockchain
    is up and running
    Returns: None

    """
    headers = {"Content-type": "application/json", "Accept": "application/json"}

    data = {"jsonrpc": "2.0", "method": "eth_accounts", "id": 1, "params": []}

    request = requests.post(url=self.__blockchain_address, json=data, headers=headers)

    # raise Exception if status is an error one
    request.raise_for_status()

    print("ORACLE: RPC node up and running", flush=True)

    return True

error_handler(func)

Adds default status and header to all REST responses used for Oracle

Source code in nebula/addons/blockchain/oracle/app.py
17
18
19
20
21
22
23
24
25
26
27
def error_handler(func):
    """Adds default status and header to all REST responses used for Oracle"""

    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs), 200, {"Content-Type": "application/json"}
        except Exception as e:
            return jsonify({"error": str(e)}), 500, {"Content-Type": "application/json"}

    return wrapper