diff --git a/chainspacecontract/chainspacecontract/examples/bank_authenticated.py b/chainspacecontract/chainspacecontract/examples/bank_authenticated.py new file mode 100644 index 00000000..43872a68 --- /dev/null +++ b/chainspacecontract/chainspacecontract/examples/bank_authenticated.py @@ -0,0 +1,104 @@ +"""A smart contract that implements a simple, authenticated bank.""" + +############################################################################################# +# imports +############################################################################################# +from chainspacecontract import ChainspaceContract +from hashlib import sha256 +from petlib.ec import EcGroup, EcPt +from petlib.ecdsa import do_ecdsa_sign, do_ecdsa_verify +from petlib.bn import Bn +from binascii import hexlify, unhexlify +from json import dumps + +contract = ChainspaceContract('bank_authenticated') + + +############################################################################################# +# init +############################################################################################# +@contract.method('init') +def init(): + return { + 'outputs': ( + {'name': 'alice', 'balance': 10}, + {'name': 'bob', 'balance': 10} + ) + } + + +############################################################################################# +# contract method +# +# NOTE: all extra parameters (like secret_key) will be ingored by the framwork; they will +# not be sent anywhere +############################################################################################# +@contract.method('auth_transfer') +def auth_transfer(inputs, reference_inputs, parameters, sk): + from_account = inputs[0] + to_account = inputs[1] + + # compute outputs + from_account['balance'] -= parameters['amount'] + to_account['balance'] += parameters['amount'] + + # hash message to sign + hasher = sha256() + hasher.update(dumps(inputs).encode('utf8')) + hasher.update(dumps(reference_inputs).encode('utf8')) + hasher.update(dumps({'amount' : parameters["amount"]}).encode('utf8')) + + # sign message + G = EcGroup() + priv = Bn.from_hex(sk) + g = G.generator() + sig = do_ecdsa_sign(G, priv, hasher.digest()) + + # return + return { + 'outputs': (from_account, to_account), + 'extra_parameters' : { + 'signature' : {'r': Bn.hex(sig[0]), 's': Bn.hex(sig[1])} + } + } + + +############################################################################################# +# checker +############################################################################################# +@contract.checker('auth_transfer') +def auth_transfer_checker(inputs, reference_inputs, parameters, outputs, returns): + + # load public key + G = EcGroup() + pub = EcPt.from_binary( + unhexlify('03951d4b7141e99fe1e9d568ef0489db884e37615a6e5968665485a973'), + G + ) + + # hash message to verify signature + hasher = sha256() + hasher.update(dumps(inputs).encode('utf8')) + hasher.update(dumps(reference_inputs).encode('utf8')) + hasher.update(dumps({'amount' : parameters["amount"]}).encode('utf8')) + + # recompose signed digest + sig = ( + Bn.from_hex(parameters['signature']['r']), + Bn.from_hex(parameters['signature']['s']) + ) + + # verify signature + return do_ecdsa_verify(G, pub, sig, hasher.digest()) + + + +############################################################################################# +# main +############################################################################################# +if __name__ == '__main__': + contract.run() + + + +############################################################################################# diff --git a/chainspacecontract/chainspacecontract/test/contract.py b/chainspacecontract/chainspacecontract/test/contract.py index 016fba4a..2b4152f5 100644 --- a/chainspacecontract/chainspacecontract/test/contract.py +++ b/chainspacecontract/chainspacecontract/test/contract.py @@ -8,9 +8,12 @@ from chainspacecontract.examples.increment import contract as increment_contract from chainspacecontract.examples.increment_with_custom_checker import contract as increment_with_custom_checker_contract from chainspacecontract.examples.bank_unauthenticated import contract as bank_unauthenticated_contract +from chainspacecontract.examples.bank_authenticated import contract as bank_authenticated_contract +from chainspacecontract.examples import bank_authenticated class TestIncrement(unittest.TestCase): + """ def test_increment_checker_service(self): checker_service_process = Process(target=increment_contract.run_checker_service) checker_service_process.start() @@ -94,6 +97,46 @@ def test_bank_unauthenticated_checker_service(self): checker_service_process.terminate() checker_service_process.join() + """ + + + + ############################################################################################# + # test an authenticated bank transfer + ############################################################################################# + def test_bank_authenticated_checker_service(self): + checker_service_process = Process(target=bank_authenticated_contract.run_checker_service) + checker_service_process.start() + time.sleep(0.1) + + # NOTE: export public key + """ + G = EcGroup() + g = G.generator() + priv = G.order().random() + pub = priv * g + byte_string = pub.export() + print hexlify(byte_string) + print EcPt.from_binary(byte_string, G) == pub + """ + + response = requests.post('http://127.0.0.1:5000/auth_transfer', + json=bank_authenticated.auth_transfer( + [{'name': 'alice', 'balance': 10}, {'name': 'bob', 'balance': 10}], + None, + {'amount': 3}, + '83C72CF7E1BA9F120C5A45135A0FE3DA59D7771BB9C670B63134A8B0' + ) + ) + response_json = response.json() + self.assertTrue(response_json['success']) + + checker_service_process.terminate() + checker_service_process.join() + + + + if __name__ == '__main__': unittest.main() diff --git a/examples/bank_transfer_checker.py b/examples/bank_transfer_checker.py index 0ed74bde..11796917 100644 --- a/examples/bank_transfer_checker.py +++ b/examples/bank_transfer_checker.py @@ -23,13 +23,36 @@ def ccheck(V, msg): # checker # ------------------------------------------------------------------------------- def checker_function(T): - # check transfer's format - ccheck(len(T["referenceInputs"]) == 0, "Expect no references") + + + + if (T[u"contractID"] == 102): + + parameter = loads(T[u"parameters"][0]) + output = loads(T[u"outputs"][0]) + + if int(parameter["token"]) * 2 == int(output): + return {"status": "OK"} + else: + raise Exception("hey!") + + + if (T[u"contractID"] != 10): + return {"status": "OK"} + + + # retrieve inputs - from_account, to_account = T[u"inputs"] - amount = T[u"parameters"]["amount"] - from_account_new, to_account_new = T[u"outputs"] + from_account = loads(T[u"inputs"][0]) + to_account = loads(T[u"inputs"][1]) + amount = loads(T[u"parameters"][0])["amount"] + from_account_new = loads(T[u"outputs"][0]) + to_account_new = loads(T[u"outputs"][1]) + + + # check transfer's format + ccheck(len(T["referenceInputs"]) == 0, "Expect no references") # check positive amount ccheck(0 < amount, "Transfer should be positive") @@ -45,6 +68,7 @@ def checker_function(T): ccheck(from_account["amount"] - amount == from_account_new["amount"], "Incorrect new balance") ccheck(to_account["amount"] + amount == to_account_new["amount"], "Incorrect new balance") + # return return {"status": "OK"} @@ -66,11 +90,11 @@ def check(): try: return dumps(checker_function(loads(request.data))) except KeyError as e: - return dumps({"status": "Error", "message": e.args}) + return dumps({"status": "ERROR", "message": str(e)}) except Exception as e: - return dumps({"status": "Error", "message": e.args}) + return dumps({"status": "ERROR", "message": str(e)}) else: - return dumps({"status": "Error", "message":"Use POST method."}) + return dumps({"status": "ERROR", "message":"Use POST method."}) ################################################################################## diff --git a/examples/test_bank_transfer.py b/examples/test_bank_transfer.py index 417247b2..f8645192 100644 --- a/examples/test_bank_transfer.py +++ b/examples/test_bank_transfer.py @@ -17,38 +17,19 @@ # variables ################################################################################## # checker URL -checker_url = r"http://127.0.0.1:5001/bank/transfer" +checker_url = r"http://127.0.0.1:5001/bank/transfer" +node_url = r"http://127.0.0.1:3001/api/1.0/transaction/process" # old accounts (before money transfer) -Sally_account = {"accountId": "Sally", "amount": 10} -Alice_account = {"accountId": "Alice", "amount": 0} +Alice_account = {"accountId": "Alice", "amount": 0} +Sally_account = {"accountId": "Sally", "amount": 10} +ID_Alice_account = "826c1cc8ce6b59d78a6655fb7fdaf1dafffad55db4880f3d5a8c30c192f9312c" +ID_Sally_account = "99e0e2bac064280f66baebe1515fe31ca9fa59c9dca3a7e7ab406a49a8ada0d5" + # new accounts (after money transfer) -Sally_account_new = {"accountId": "Sally", "amount": 2} Alice_account_new = {"accountId": "Alice", "amount": 8} - -# example transfer -Example_transfer = { - "contractMethod" : checker_url, - "inputs" : [Sally_account, Alice_account], - "referenceInputs" : [], - "parameters" : {"amount":8}, - "outputs" : [Sally_account_new, Alice_account_new] -} -Example_invalid_transfer = { - "contractMethod" : checker_url, - "inputs" : [Sally_account, Alice_account], - "referenceInputs" : [], - "parameters" : {"amount":100}, - "outputs" : [Sally_account_new, Alice_account_new] -} -Example_malformed_transfer = { - "contractMethod" : checker_url, - # inputs are missing - "referenceInputs" : [], - "parameters" : {"amount":8}, - "outputs" : [Sally_account_new, Alice_account_new] -} +Sally_account_new = {"accountId": "Sally", "amount": 2} ################################################################################## @@ -72,101 +53,189 @@ def start_checker(app): # tests ################################################################################## # ------------------------------------------------------------------------------- -# test 1 -# try to validate a transaction (call the checker) at an hardcoded address +# test 2 +# check a bank tranfer with dependencies # ------------------------------------------------------------------------------- -def test_request(): - # run the checker - t = Thread(target=start_checker, args=(app_checker,)) - t.start() +def test_dependencies(): + # run checker and cspace + t1 = Thread(target=start_checker, args=(app_checker,)) + t1.start() try: - # test a valid transfer - r = requests.post(checker_url, data = dumps(Example_transfer)) - assert loads(r.text)["status"] == "OK" - # test a transfer with invalid amount - r = requests.post(checker_url, data = dumps(Example_invalid_transfer)) - assert loads(r.text)["status"] == "Error" + # ----------------------------------------------------------------------- + # create Alice's account + # ----------------------------------------------------------------------- + # set transaction + T1 = { + "contractID" : 5, + "inputIDs" : [], + "referenceInputIDs" : [], + "parameters" : [], + "returns" : [], + "outputIDs" : ["20"], + "dependencies" : [] + } + + # set store + store1 = [ + {"key": "20", "value": Alice_account} + ] + + # assemble + packet1 = {"transaction": T1, "store": store1}; + + + # ----------------------------------------------------------------------- + # create Sally's account + # ----------------------------------------------------------------------- + # set transaction + T2 = { + "contractID" : 5, + "inputIDs" : [], + "referenceInputIDs" : [], + "parameters" : [], + "returns" : [], + "outputIDs" : ["21"], + "dependencies" : [] + } + + # set store + store2 = [ + {"key": "21", "value": Sally_account} + ] + + # assemble + packet2 = {"transaction": T2, "store": store2}; + + + # ----------------------------------------------------------------------- + # make the transfer + # ----------------------------------------------------------------------- + # set transaction + T3 = { + "contractID" : 10, + "inputIDs" : [ID_Sally_account, ID_Alice_account], + "referenceInputIDs" : [], + "parameters" : [dumps({"amount":8})], + "returns" : [], + "outputIDs" : ["10", "11"], + "dependencies" : [dumps(packet1), dumps(packet2)] + } + + # set store + store3 = [ + {"key": ID_Sally_account, "value": Sally_account}, + {"key": ID_Alice_account, "value": Alice_account}, + {"key": "10", "value": Sally_account_new}, + {"key": "11", "value": Alice_account_new}, + ] - # test malformed transaction - r = requests.post(checker_url, data = dumps(Example_malformed_transfer)) - assert loads(r.text)["status"] == "Error" + # assemble + packet3 = {"transaction": T3, "store": store3}; - # get request - r = requests.get(checker_url) - assert loads(r.text)["status"] == "Error" + # sumbit the transaction to the ledger + r = requests.post(node_url, data = dumps(packet3)) + assert loads(r.text)["status"] == "OK" + finally: - t._Thread__stop() + t1._Thread__stop() # ------------------------------------------------------------------------------- -# test 2 -# final check: simulate a complete transfer +# test 3 +# check a transaction with dependencies & returns # ------------------------------------------------------------------------------- -def test_transaction(): +def test_dependencies_with_returns(): # run checker and cspace t1 = Thread(target=start_checker, args=(app_checker,)) t1.start() try: - """ - # add Alice's account to DB - r = requests.post(r"http://127.0.0.1:3001/api/1.0/debug_load", data = dumps(Sally_account)) - assert loads(r.text)["status"] == "OK" - ID1 = loads(r.text)["objectID"] - # add Sally's account to DB - r = requests.post(r"http://127.0.0.1:3001/api/1.0/debug_load", data = dumps(Alice_account)) - assert loads(r.text)["status"] == "OK" - ID2 = loads(r.text)["objectID"] + # ----------------------------------------------------------------------- + # variables + # ----------------------------------------------------------------------- + tokenX1 = "1" + tokenX2 = "2" + tokenX4 = "4" + + ID_tokenX1 = "cde1a2d429ba3e1096b7a004044a655aaef1cbb7829bde8821cadf1ca5c93802" - # get ID of output objects - # NOTE: hardcoded values; python H(x) does not return the same hash than Java... - ID3 = "6b88d14940c1294227c2be03fd0affd4fcb8af3a54165d3a51fc1a5d49aaabbd" - ID4 = "b1405ffcf16294c76367c056610d89ab7cd9da267ee3183cf471e523e91c386b" - """ + # ----------------------------------------------------------------------- + # create a token + # ----------------------------------------------------------------------- # set transaction - """ - T = { - "contractID" : 10, - "inputIDs" : [ID1, ID2], + T1 = { + "contractID" : 100, + "inputIDs" : [], "referenceInputIDs" : [], - "parameters" : dumps({"amount":8}), - "outputs" : [dumps(Sally_account_new), dumps(Alice_account_new)] + "parameters" : [], + "returns" : [], + "outputIDs" : ["20"], + "dependencies" : [] } - """ - ID1 = "7308c63e4ff99491af54005258e73bccb320edfa2a1a1aab293f051ca63ea64d" - ID2 = "3cdff76fc23e30ed617d6188170cf111c844b343f4f6ac7d2e0d6794814859e3" - ID3 = "0701d1f317e8fecd6ac59f71c662e33bd0aaef8a5784ba4a0bfceb5b48c01e41" - ID4 = "57435d7c4b49b34ec02c425af4ccdc8b92b2841d2956c5a22d86105f08c37f8d" + # set store + store1 = [ + {"key": "20", "value": tokenX1} + ] - T = { - "contractID" : 10, - "inputIDs" : [ID1, ID2], - "referenceInputIDs" : [], - "parameters" : dumps({"amount":8}), - "outputIDs" : [ID3, ID4] + # assemble + packet1 = {"transaction": T1, "store": store1}; + + + # ----------------------------------------------------------------------- + # read transaction: we use tokenX1 as reference to make a local return + # ----------------------------------------------------------------------- + # set transaction + T2 = { + "contractID" : 101, + "inputIDs" : [], + "referenceInputIDs" : [ID_tokenX1], + "parameters" : [], + "returns" : [dumps({"token" : tokenX2})], + "outputIDs" : [], + "dependencies" : [dumps(packet1)] } + # set store + store2 = [ + {"key": ID_tokenX1, "value": tokenX1} + ] + + # assemble + packet2 = {"transaction": T2, "store": store2}; + + + # ----------------------------------------------------------------------- + # get the local returns as param and outputs tokenX4 + # ----------------------------------------------------------------------- + # set transaction + T3 = { + "contractID" : 102, + "inputIDs" : [], + "referenceInputIDs" : [], + "parameters" : [], + "returns" : ["Hello!"], + "outputIDs" : ["3"], + "dependencies" : [dumps(packet2)] + } - # set key-value store - store = [ - {"key": ID1, "value": Sally_account}, - {"key": ID2, "value": Alice_account}, - {"key": ID3, "value": Sally_account_new}, - {"key": ID4, "value": Alice_account_new}, + # set store + store3 = [ + {"key": "3", "value": tokenX4}, ] - # pack transaction - packet = {"transaction": T, "store": store}; + # assemble + packet3 = {"transaction": T3, "store": store3}; # sumbit the transaction to the ledger - r = requests.post(r"http://127.0.0.1:3001/api/1.0/process_transaction", data = dumps(packet)) + r = requests.post(node_url, data = dumps(packet3)) assert loads(r.text)["status"] == "OK" + finally: t1._Thread__stop() diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Cache.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Cache.java index e86db8b9..903a14e5 100644 --- a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Cache.java +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Cache.java @@ -1,58 +1,11 @@ package uk.ac.ucl.cs.sec.chainspace; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; - /** * * */ -// TODO: use something more efficient that an ArrayList (tab, tree, linkedList) -class Cache extends ArrayList { - - // instance variables - private int cacheDepth; - - Cache(int cacheDepth) { - - this.cacheDepth = cacheDepth; - - } - - /** - * isInCache - * Verify whether the transaction has recenlty been processed. This could happens due to multiple broadcasts between - * nodes and shards. - */ - boolean isInCache(String input) throws NoSuchAlgorithmException { - - // compute digest - String digest = Utils.hash(input); - - // if the transaction is already in the cash, return true - // TODO: method toArray() as complexity O(n), making the binarySearch useless here - if( Arrays.binarySearch(this.toArray(), digest) != -1 ) {return true;} - - // otherwise update cache and return false - updateCache(digest); - return false; - - } - - /** - * updateCache - * Update the values in the cache (suppress the oldest entry and add the new one) - */ - private void updateCache(String digest) { +public interface Cache { - if (this.size() < this.cacheDepth) { - this.add(digest); - } - else { - this.remove(0); - this.add(digest); - } + boolean isInCache(String input); - } } diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Core.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Core.java index 98a73d51..30516a2e 100644 --- a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Core.java +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Core.java @@ -1,16 +1,8 @@ package uk.ac.ucl.cs.sec.chainspace; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.BasicResponseHandler; -import org.apache.http.impl.client.HttpClientBuilder; import org.json.JSONObject; -import org.json.simple.JSONArray; import java.io.IOException; -import java.security.NoSuchAlgorithmException; import java.sql.SQLException; @@ -25,10 +17,6 @@ class Core { private Cache cache; - // TODO: load cache depth from config - private static final int CACHE_DEPTH = 10; - - /** * Constructor * Runs a node service and init a database. @@ -36,10 +24,13 @@ class Core { Core(int nodeID) throws ClassNotFoundException, SQLException { // init cache - this.cache = new Cache(CACHE_DEPTH); + // here we are implementing a simple linear cash of complexity O(n). Any caching system implementing the Cache + // interface can be used instead. + this.cache = new SimpleCache(Main.CACHE_DEPTH); // init the database connection - this.databaseConnector = new DatabaseConnector(nodeID); + // here we're using SQLite as an example, but the core supports any extension of databaseConnector. + this.databaseConnector = new SQLiteConnector(nodeID); } @@ -48,104 +39,92 @@ class Core { * close * Gently shutdown the core */ - void close() throws SQLException { + void close() throws Exception { this.databaseConnector.close(); } /** - * debugLoad - * Debug method to quickly add an object to the node database. It returns the corresponding object ID. + * processTransaction + * This method processes a transaction object, call the checker, and store the outputs in the database if + * everything goes fine. */ - void debugLoad(String object) throws NoSuchAlgorithmException { + String[] processTransaction(String request) throws Exception { + + // get the transactions + Transaction transaction = TransactionPackager.makeTransaction(request); + TransactionForChecker transactionForChecker = TransactionPackager.makeFullTransaction(request); + + // recursively loop over dependencies + if (! Main.DEBUG_IGNORE_DEPENDENCIES) { + for (int i = 0; i < transaction.getDependencies().length; i++) { + + if (Main.VERBOSE) { System.out.println("\n[PROCESSING DEPENDENCY #" +i+ "]");} + // recusrively process the transaction + String[] returns = processTransaction(transaction.getDependencies()[i]); + // updates the parameters of the caller transaction + transactionForChecker.addParameters(returns); + if (Main.VERBOSE) { System.out.println("\n[END DEPENDENCY #" +i+ "]");} - // add object to the database - this.databaseConnector.saveObject("", object); + } + } + + // process top level transaction + return processTransactionHelper(transaction, transactionForChecker); } /** - * processTransaction - * This method processes a transaction object, call the checker, and store the outputs in the database if everything - * goes fine. + * processTransactionHelper + * Helper for processTransaction: executed on each recursion. */ - void processTransaction(Transaction transaction, Store store) - throws AbortTransactionException, SQLException, NoSuchAlgorithmException, IOException + private String[] processTransactionHelper(Transaction transaction, TransactionForChecker transactionForChecker) + throws Exception { // check if the transaction is in the cache (has recently been processed) - if (this.cache.isInCache(transaction.toJson())) { return; } - - // check transaction's integrity - if (!checkTransactionIntegrity(transaction, store)) { - throw new AbortTransactionException("Malformed transaction or key-value store."); - } - - // check input objects are active - // TODO: optimise database query (one query instead of looping) - for (int i = 0; i < transaction.getInputIDs().length; i++) { - if (this.databaseConnector.isObjectInactive(transaction.getInputIDs()[i])) { - throw new AbortTransactionException("Object " +transaction.getInputIDs()[i]+ " is inactive."); - } - } - - // check reference input objects are active - // TODO: optimise database query (one query instead of looping) - for (int i = 0; i < transaction.getReferenceInputIDs().length; i++) { - if (this.databaseConnector.isObjectInactive(transaction.getReferenceInputIDs()[i])) { - throw new AbortTransactionException("Object " +transaction.getReferenceInputIDs()[i]+ " is inactive."); + if (! Main.DEBUG_ALLOW_REPEAT) { + if (this.cache.isInCache(transaction.toJson())) { + throw new AbortTransactionException("This transaction as already been executed."); } } - - // assemble inputs objects for checker - String[] inputs = new String[transaction.getInputIDs().length]; - for (int i = 0; i < transaction.getInputIDs().length; i++) { - inputs[i] = store.getValueFromKey(transaction.getInputIDs()[i]); - } - - // assemble reference inputs objects for checker - String[] referenceInputs = new String[transaction.getReferenceInputIDs().length]; - for (int i = 0; i < transaction.getReferenceInputIDs().length; i++) { - referenceInputs[i] = store.getValueFromKey(transaction.getReferenceInputIDs()[i]); + // check input objects and reference inputs are active + if (this.databaseConnector.isInactive(transaction.getInputIDs())) { + throw new AbortTransactionException("At least one input object is inactive."); } - - // assemble output objects for checker - String[] outputs = new String[transaction.getOutputIDs().length]; - for (int i = 0; i < transaction.getOutputIDs().length; i++) { - outputs[i] = store.getValueFromKey(transaction.getOutputIDs()[i]); + if (this.databaseConnector.isInactive(transaction.getReferenceInputIDs())) { + throw new AbortTransactionException("At least one reference input is inactive."); } - - // call the checker - if (!callChecker(transaction, inputs, referenceInputs, outputs)) { - throw new AbortTransactionException("The checker declined the transaction."); + if (! Main.DEBUG_SKIP_CHECKER) { + callChecker(transactionForChecker); } - // check if objects are active - // This is the part where we call BFTSmart + /* + This is the part where we call BFTSmart. + */ // TODO: check that all inputs are active. // make input (consumed) objects inactive - // TODO: optimise database query (one query instead of looping) - for (int i = 0; i < transaction.getInputIDs().length; i++) { - this.databaseConnector.setObjectInactive(transaction.getInputIDs()[i]); + if (! Main.DEBUG_ALLOW_REPEAT) { + this.databaseConnector.setInactive(transaction.getInputIDs()); } // register new objects - // TODO: optimise database query (one query instead of looping) - for (String output : outputs) { - this.databaseConnector.saveObject(Utils.hash(transaction.toJson()), output); - } + this.databaseConnector.saveObject(transaction.getID(), transactionForChecker.getOutputs()); // update logs - this.databaseConnector.logTransaction(transaction.toJson()); + this.databaseConnector.logTransaction(transaction.getID(), transaction.toJson()); + + // pass out returns + return transaction.getReturns(); } @@ -154,102 +133,27 @@ void processTransaction(Transaction transaction, Store store) * callChecker * This method format a packet and call the checker in order to verify the transaction. */ - @SuppressWarnings("unchecked") // these warning are caused by a bug in org.json.simple.JSONArray - private boolean callChecker(Transaction transaction, String[] inputs, String[] referenceInputs, String[] outputs) - throws IOException + private void callChecker(TransactionForChecker transactionForChecker) + throws IOException, AbortTransactionException { // get checker URL - // TODO: at the moment the checker URL is hardcoded, this should be loaded from a config file + // TODO: This URL should be loaded from a config file (depending on the contractID) String checkerURL = "http://127.0.0.1:5001/bank/transfer"; - // create transaction in JSON for checker - JSONObject transactionForChecker = new JSONObject(); - // contract method - transactionForChecker.put("contractID", transaction.getContractID()); - // parameters - transactionForChecker.put("parameters", new JSONObject(transaction.getParameters())); - - // inputs - JSONArray inputsForChecker = new JSONArray(); - for (String input : inputs) { - inputsForChecker.add(new JSONObject(input)); - } - transactionForChecker.put("inputs", inputsForChecker); - - // reference inputs - JSONArray referenceInputsForChecker = new JSONArray(); - for (String referenceInput : referenceInputs) { - referenceInputsForChecker.add(new JSONObject(referenceInput)); - } - transactionForChecker.put("referenceInputs", referenceInputsForChecker); - - // outputs - JSONArray outputsForChecker = new JSONArray(); - for (Object output : outputs) { - outputsForChecker.add(new JSONObject(output.toString())); - } - transactionForChecker.put("outputs", outputsForChecker); - - // make post request - HttpClient httpClient = HttpClientBuilder.create().build(); - StringEntity postingString = new StringEntity(transactionForChecker.toString()); - HttpPost post = new HttpPost(checkerURL); - post.setEntity(postingString); - post.setHeader("Content-type", "application/json"); - - // get response - HttpResponse response = httpClient.execute(post); - String responseString = new BasicResponseHandler().handleResponse(response); + // call the checker + String responseString = Utils.makePostRequest(checkerURL, transactionForChecker.toJson()); JSONObject responseJson = new JSONObject(responseString); - // return - return responseJson.getString("status").equals("OK"); - - } - - - /** - * checkTransactionIntegrity - * Check the transaction's integrity. - */ - private boolean checkTransactionIntegrity(Transaction transaction, Store store) throws NoSuchAlgorithmException { - - // check transaction's and store's format - // all fields must be present. For instance, if a transaction has no parameters, and empty field should be sent - if (store.getArray() == null - || transaction.getInputIDs() == null - || transaction.getReferenceInputIDs() == null - || transaction.getOutputIDs() == null - || transaction.getParameters() == null ) - { - return false; - } - - - // check hashed of input objects - for (String inputID: transaction.getInputIDs()) { - if (! Utils.verifyHash(store.getValueFromKey(inputID), inputID)) { - return false; - } + // throw error if the checker declines the transaction + if (responseJson.getString("status").equalsIgnoreCase("ERROR")) { + throw new AbortTransactionException(responseJson.getString("message")); } - - // check hashed of reference input objects - for (String referenceInputID: transaction.getReferenceInputIDs()) { - if (! Utils.verifyHash(store.getValueFromKey(referenceInputID), referenceInputID)) { - return false; - } - } - - // check hashed of output objects - for (String outputID: transaction.getOutputIDs()) { - if (! Utils.verifyHash(store.getValueFromKey(outputID), outputID)) { - return false; - } + else if(! responseJson.getString("status").equalsIgnoreCase("OK")) { + throw new AbortTransactionException("The checker declined the transaction."); } - // otherwise, return true - return true; + if (Main.VERBOSE) { System.out.println("\nThe checker accepted the transaction!"); } } diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/DatabaseConnector.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/DatabaseConnector.java index 1628a228..aaf64668 100644 --- a/src/main/java/uk/ac/ucl/cs/sec/chainspace/DatabaseConnector.java +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/DatabaseConnector.java @@ -1,179 +1,50 @@ package uk.ac.ucl.cs.sec.chainspace; - import java.security.NoSuchAlgorithmException; -import java.sql.*; - /** * * */ -class DatabaseConnector { +abstract class DatabaseConnector { - // instance variables - private Connection connection; - /** - * constructor - * Initialise the database connection and create a table to store object (if it does not already exist). - */ - DatabaseConnector(int nodeID) throws SQLException, ClassNotFoundException { - - // create database connection - Class.forName("org.sqlite.JDBC"); - this.connection = DriverManager.getConnection("jdbc:sqlite:node" + nodeID + ".sqlite"); - Statement statement = connection.createStatement(); - - // create table to store objects - String sql = "CREATE TABLE IF NOT EXISTS data (" + - "object_id CHAR(32) NOT NULL UNIQUE," + - "object TEXT NOT NULL," + - "status INTEGER NOT NULL)"; - statement.executeUpdate(sql); - - // create table to store logs - sql = "CREATE TABLE IF NOT EXISTS logs (" + - "transaction_id CHAR(32) NOT NULL," + - "transactionJson TEXT NOT NULL)"; - statement.executeUpdate(sql); - - // create table to store logs head - sql = "CREATE TABLE IF NOT EXISTS head (" + - "ID INTEGER PRIMARY KEY," + - "digest CHAR(32) NOT NULL UNIQUE)"; - statement.executeUpdate(sql); - - // close statement - statement.close(); - } + abstract void saveObject(String transactionID, String[] objects) throws AbortTransactionException; + abstract boolean isInactive(String[] objectIDs) throws AbortTransactionException; - /** - * close - * Gently shut down the database connection - */ - void close() throws SQLException { - this.connection.close(); - } - + abstract void setInactive(String[] objectIDs) throws AbortTransactionException; - /** - * registerObject - * Debug method that blindly insert an object into the database if it does not already exist. - */ - void saveObject(String transactionID, String object) throws NoSuchAlgorithmException { - - String sql = "INSERT INTO data (object_id, object, status) VALUES (?, ?, 1)"; - PreparedStatement statement; - try { - statement = connection.prepareStatement(sql); - statement.setString(1, Utils.hash(transactionID +"|"+ object)); - statement.setString(2, object); - statement.executeUpdate(); - statement.close(); - } catch (SQLException ignored) {} // ignore: happens if object already exists + abstract void logTransaction(String transactionID, String transactionJson) throws Exception; - } + abstract void close() throws Exception; - /** - * getObject - * retrieve an a given object from the database. - */ - String getObject(String objectID) throws SQLException { - // prepare query - String sql = "SELECT object FROM data WHERE object_id = ? LIMIT 1"; - PreparedStatement statement = connection.prepareStatement(sql); - statement.setString(1, objectID); - ResultSet resultSet = statement.executeQuery(); - - // check if the object is in the database. - if (resultSet.isBeforeFirst()) { - return resultSet.getString("object"); - } - // if it's not, ask other shards - else { - return null; - } - } /** - * isObjectInactive - * Return true iff the object is in the database and is inactive. + * generateObjectID + * Create an object ID from the object and the trasaction that created it. */ - boolean isObjectInactive(String objectID) throws SQLException { - // prepare query - String sql = "SELECT status FROM data WHERE object_id = ? LIMIT 1"; - PreparedStatement statement = connection.prepareStatement(sql); - statement.setString(1, objectID); - ResultSet resultSet = statement.executeQuery(); - - // check if the object is in the database and if it is active. - // if the object status is unknown, return false - return resultSet.isBeforeFirst() && resultSet.getInt("status") == 0; + String generateObjectID(String transactionID, String object) throws NoSuchAlgorithmException { + return Utils.hash(transactionID + "|" + object); } + /** - * setObjectInactive - * Make object inactive (the object is now consumed). + * generateHead + * Create a new head from a new transaction and the previous head. */ - void setObjectInactive(String objectID) throws SQLException { - String sql = "UPDATE data SET status = 0 WHERE object_id = ?"; - PreparedStatement statement = connection.prepareStatement(sql); - statement.setString(1, objectID); - statement.executeUpdate(); - statement.close(); + String generateHead(String oldHead, String transactionJson) throws NoSuchAlgorithmException { + return Utils.hash(oldHead + "|" + transactionJson); } /** - * logTransaction - * Add transaction to the logs. + * generateHead + * Create a new head from a new transaction (should be used only for the first transaction). */ - // TODO: optimise requests (only one executeUpdate) - void logTransaction(String transactionJson) throws NoSuchAlgorithmException, SQLException { - - // add transaction to the logs - String sql = "INSERT INTO logs (transaction_id, transactionJson) VALUES (?, ?)"; - PreparedStatement statement; - statement = connection.prepareStatement(sql); - statement.setString(1, Utils.hash(transactionJson)); - statement.setString(2, transactionJson); - statement.executeUpdate(); - statement.close(); - - // update head - updateHead(transactionJson); - + String generateHead(String transactionJson) throws NoSuchAlgorithmException { + return Utils.hash(transactionJson); } - /** - * updateHead - * Update the logs' head. - */ - private void updateHead(String transactionJson) throws SQLException, NoSuchAlgorithmException { - - // get the previous head - String sql = "SELECT digest FROM head ORDER BY ID DESC LIMIT 1"; - PreparedStatement statement = connection.prepareStatement(sql); - ResultSet resultSet = statement.executeQuery(); - statement.close(); - - // insert new head - sql = "INSERT INTO head (ID, digest) VALUES (null, ?)"; - statement = connection.prepareStatement(sql); - - // check if there is at least one head in the table - if (resultSet.isBeforeFirst()) { - String newHead = Utils.hash( resultSet.getString("digest") + "|" + transactionJson); - statement.setString(1, newHead); - } - // if not, simply had a hash of the transaction - else { - statement.setString(1, Utils.hash(transactionJson)); - } - statement.executeUpdate(); - statement.close(); - } } diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Main.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Main.java index a727109d..226a40ed 100644 --- a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Main.java +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Main.java @@ -1,21 +1,88 @@ package uk.ac.ucl.cs.sec.chainspace; -import java.sql.SQLException; +/** + * + * + */ +public class Main { + // CONFIG: number of cores + static final int CORES = 2; -public class Main { + // CONFIG: verbose option prints out extensive comments on the console + static final boolean VERBOSE = false; + + // CONIFG: size of the cache (set to zero to disable cache) + static final int CACHE_DEPTH = 0; + + + /* + + DEBUG + + These are the debug options: + (1) DEBUG_ALLOW_REPEAT: when enabled, the system accepts multiple time the same trasaction. Object are not + uniques and are never set to 'inactive'. + + (2) DEBUG_SKIP_CHECKER: when enabled, the checker never called. This is equivalent of having a chcker that + returns always 'true'. + + (3) DEBUG_NO_BROADCAST: in normal operations, when receving a nex transaction, the first thing that the node + does is to boradcast that transaction to all other nodes. When this option is enabled, the node does not bradcast + the transaction and processes it by itself. This option is useful to limit the number of consols' print. + + (4) DEBUG_IGNORE_DEPENDENCIES: when enables, transaction's dependencis are ignored and only the top-level + transaction is executed (no cross-contract calls). + + + NOTE: All debug options sould be set to false when running in production environement. + + */ + static final boolean DEBUG_ALLOW_REPEAT = true; + static final boolean DEBUG_SKIP_CHECKER = false; + static final boolean DEBUG_NO_BROADCAST = true; + static final boolean DEBUG_IGNORE_DEPENDENCIES = false; + + /** + * main + * @param args not used + */ public static void main(String[] args) { + // verbose print + if (Main.VERBOSE) { Utils.printHeader("Starting Chainsapce..."); } + + + // run chainspace service + for (int i = 1; i <= CORES; i++) { + runNodeService(i); + } + + + // verbose print + if (Main.VERBOSE) { Utils.printLine(); } + + } + + + /** + * runNodeService + * Run a node service with a given node's ID. + * @param nodeID the node's ID + */ + private static void runNodeService(int nodeID) { + try { - // run chainspace service - new NodeService(1); - new NodeService(2); + // run a new node instance + new NodeService(nodeID); - } catch (SQLException | ClassNotFoundException e) { - e.printStackTrace(); + } + catch (Exception e) { + if (Main.VERBOSE) { Utils.printStacktrace(e); } + else { System.err.println("[ERROR] Node service #" +nodeID+ " failled to start."); } } } diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/NodeService.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/NodeService.java index a28d16ec..7bbdf08f 100644 --- a/src/main/java/uk/ac/ucl/cs/sec/chainspace/NodeService.java +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/NodeService.java @@ -1,18 +1,11 @@ package uk.ac.ucl.cs.sec.chainspace; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.HttpClientBuilder; import org.json.JSONObject; import spark.Request; import spark.Response; import spark.Service; - import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.sql.SQLException; @@ -47,6 +40,7 @@ class NodeService { printInitMessage(port); } + /** * finalize * Gently shut down the node's core when the garbage collector is called. @@ -57,6 +51,7 @@ protected void finalize() throws Throwable { this.core.close(); } + /** * routes for the web service */ @@ -66,99 +61,61 @@ private void addRoutes(Service service) { service.path("/api", () -> service.path("/1.0", () -> { // return node ID - service.get("/node_id", (request, response) -> new JSONObject().put("Node ID", nodeID).toString()); - - // debug : add an object to the database - service.post("/debug_load", this::debugLoadRequest); + service.get("/node/id", (request, response) -> new JSONObject().put("Node ID", nodeID).toString()); // process a transaction - service.post("/process_transaction", this::processTransactionRequest); + service.post("/transaction/process", this::processTransactionRequest); })); } - /** - * debugLoad - * Debug method to quickly add an object to the node database. It returns the corresponding object ID. - */ - private String debugLoadRequest(Request request, Response response) { - - // register objects & create response - JSONObject responseJson = new JSONObject(); - try { - // add object to db - core.debugLoad(request.body()); - - // create json response - responseJson.put("status", "OK"); - responseJson.put("objectID", Utils.hash(request.body())); - response.status(200); - } - catch (Exception e) { - // create json response - responseJson.put("status", "ERROR"); - responseJson.put("message", e.getMessage()); - response.status(500); - } - - // print request - printRequestDetails(request, responseJson.toString()); - - // send - response.type("application/json"); - return responseJson.toString(); - - } - - /** * processTransactionRequest * This method receives a json transaction, processes it, and responds with the transaction ID. */ private String processTransactionRequest(Request request, Response response) { - // get json request - JSONObject requestJson = new JSONObject(request.body()); + // verbose print + if (Main.VERBOSE) { Utils.printHeader("Incoming transaction"); } // broadcast transaction to other nodes - try { - broadcastTransaction(request.body()); - } catch (IOException e) { - e.printStackTrace(); - } // ignore failures + if (! Main.DEBUG_NO_BROADCAST) { + try { + + broadcastTransaction(request.body()); + + } catch (IOException ignored) { + // NOTE: this exception is ignored + if (Main.VERBOSE) { Utils.printStacktrace(ignored); } + } + } + // process the transaction & create response JSONObject responseJson = new JSONObject(); try { - // get the transaction and the key-value store as java objects - Transaction transaction; - Store store; - try { - transaction = Transaction.fromJson(requestJson.getJSONObject("transaction")); - store = Store.fromJson(requestJson.getJSONArray("store")); - } - catch (Exception e) { - throw new AbortTransactionException("Malformed transaction or key-value store."); - } - - // process the transaction - core.processTransaction(transaction, store); + // pass transaction to the core + String[] returns = this.core.processTransaction(request.body()); // create json response responseJson.put("status", "OK"); - responseJson.put("transactionID", transaction.getID()); + responseJson.put("returns", returns); response.status(200); + } catch (Exception e) { + // create json error response responseJson.put("status", "ERROR"); responseJson.put("message", e.getMessage()); response.status(400); - e.printStackTrace(); + // verbose print + if (Main.VERBOSE) { Utils.printStacktrace(e); } + } // print request @@ -167,6 +124,7 @@ private String processTransactionRequest(Request request, Response response) { // send response.type("application/json"); return responseJson.toString(); + } @@ -174,18 +132,17 @@ private String processTransactionRequest(Request request, Response response) { * broadcastTransaction * Broadcast the transaction to other nodes. */ - private void broadcastTransaction(String body) throws IOException { + private void broadcastTransaction(String data) throws IOException { // debug: avoid infinite loop - if (this.nodeID == 2) { return; } - - // make post request - HttpClient httpClient = HttpClientBuilder.create().build(); - StringEntity postingString = new StringEntity(body); - HttpPost post = new HttpPost("http://127.0.0.1:3002/api/1.0/process_transaction"); - post.setEntity(postingString); - post.setHeader("Content-type", "application/json"); - httpClient.execute(post); + // TODO: get the nodes ID and addresses from a config file + if (this.nodeID == 1) { + for (int i = 2; i <= Main.CORES; i++) { + String url = "http://127.0.0.1:300" + i + "/api/1.0/transaction/process"; + Utils.makePostRequest(url, data); + } + } + } @@ -194,7 +151,10 @@ private void broadcastTransaction(String body) throws IOException { * Print on the console an init message. */ private void printInitMessage(int port) { - System.out.println("\nNode service #" +nodeID+ " is running on port " +port+ " ..."); + + // print node info + System.out.println("\nNode service #" +nodeID+ " is running on port " +port); + } @@ -203,9 +163,13 @@ private void printInitMessage(int port) { * Print on the console some details about the incoming request. */ private void printRequestDetails(Request request, String response) { + + // print request summary System.out.println("\nNode service #" +nodeID+ " [POST] @" +request.url()+ " from " +request.ip()); System.out.println("\trequest content: " + request.body()); System.out.println("\tresponse content: " + response); + if (Main.VERBOSE) { Utils.printLine(); } + } } diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/SQLiteConnector.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/SQLiteConnector.java new file mode 100644 index 00000000..26de2efa --- /dev/null +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/SQLiteConnector.java @@ -0,0 +1,248 @@ +package uk.ac.ucl.cs.sec.chainspace; + + +import java.security.NoSuchAlgorithmException; +import java.sql.*; + + +/** + * + * + */ +class SQLiteConnector extends DatabaseConnector { + + // instance variables + private Connection connection; + + /** + * constructor + * Initialise the database connection and create a table to store object (if it does not already exist). + */ + SQLiteConnector(int nodeID) throws SQLException, ClassNotFoundException { + + // create database connection + Class.forName("org.sqlite.JDBC"); + this.connection = DriverManager.getConnection("jdbc:sqlite:node" + nodeID + ".sqlite"); + Statement statement = connection.createStatement(); + + if (! Main.DEBUG_ALLOW_REPEAT) { + // create table to store objects + String sql = "CREATE TABLE IF NOT EXISTS data (" + + "object_id CHAR(32) NOT NULL UNIQUE," + + "object TEXT NOT NULL," + + "status INTEGER NOT NULL)"; + statement.executeUpdate(sql); + + // create table to store logs + sql = "CREATE TABLE IF NOT EXISTS logs (" + + "transaction_id CHAR(32) NOT NULL UNIQUE," + + "transactionJson TEXT NOT NULL)"; + statement.executeUpdate(sql); + + // create table to store logs head + sql = "CREATE TABLE IF NOT EXISTS head (" + + "ID INTEGER PRIMARY KEY," + + "digest CHAR(32) NOT NULL UNIQUE)"; + statement.executeUpdate(sql); + } + else { + // removed all unique constraints for debug mode + // create table to store objects + String sql = "CREATE TABLE IF NOT EXISTS data (" + + "object_id CHAR(32) NOT NULL," + + "object TEXT NOT NULL," + + "status INTEGER NOT NULL)"; + statement.executeUpdate(sql); + + // create table to store logs + sql = "CREATE TABLE IF NOT EXISTS logs (" + + "transaction_id CHAR(32) NOT NULL," + + "transactionJson TEXT NOT NULL)"; + statement.executeUpdate(sql); + + // create table to store logs head + sql = "CREATE TABLE IF NOT EXISTS head (" + + "ID INTEGER PRIMARY KEY," + + "digest CHAR(32) NOT NULL)"; + statement.executeUpdate(sql); + } + + // close statement + statement.close(); + } + + + /** + * close + * Gently shut down the database connection + */ + public void close() throws SQLException { + this.connection.close(); + } + + + /** + * registerObject + * Debug method that blindly insert an object into the database if it does not already exist. + */ + // TODO: optimise requests (only one executeUpdate) + public void saveObject(String transactionID, String[] objects) throws AbortTransactionException { + + String sql = "INSERT INTO data (object_id, object, status) VALUES (?, ?, 1)"; + try (PreparedStatement statement = connection.prepareStatement(sql)) { + for (String object : objects) { + String objectID = this.generateObjectID(transactionID, object); + statement.setString(1, objectID); + statement.setString(2, object); + statement.executeUpdate(); + + if (Main.VERBOSE) { + System.out.println("\nNew object has been created:"); + System.out.println("\tID: " + objectID); + System.out.println("\tObject: " + object); + } + } + } catch (SQLException | NoSuchAlgorithmException e) { + throw new AbortTransactionException(e.getMessage()); + } + + } + + /** + * isObjectInactive + * Return true iff the object is in the database and is inactive. + */ + // TODO: optimise requests (only one executeQuery) + public boolean isInactive(String[] objectIDs) throws AbortTransactionException { + + // check all objects + String sql = "SELECT status FROM data WHERE object_id = ? LIMIT 1"; + try (PreparedStatement statement = connection.prepareStatement(sql)) { + for (String objectID : objectIDs) { + + // verbose print + if (Main.VERBOSE) { + System.out.println("\nChecking if object is active:"); + System.out.println("\tID: " + objectID); + } + + // execute query + statement.setString(1, objectID); + ResultSet resultSet = statement.executeQuery(); + + // check if the object is in the database and if it is active. + // if the object is unknown, skip + if (resultSet.isBeforeFirst() && resultSet.getInt("status") == 0) { + if (Main.VERBOSE) { + System.out.print("\tStatus: "); + System.err.println("FAILLED"); + } + return true; + } + if (Main.VERBOSE) { + System.out.println("\tStatus: OK"); + } + } + } catch (SQLException e) { + throw new AbortTransactionException(e.getMessage()); + } + + // if none is inactive, return false + return false; + + } + + /** + * setObjectInactive + * Make object inactive (the object is now consumed). + */ + // TODO: optimise requests (only one executeUpdate) + public void setInactive(String[] objectIDs) throws AbortTransactionException { + + // check all objects + + String sql = "UPDATE data SET status = 0 WHERE object_id = ?"; + try (PreparedStatement statement = connection.prepareStatement(sql)) { + for (String objectID : objectIDs) { + statement.setString(1, objectID); + statement.executeUpdate(); + + if (Main.VERBOSE) { + System.out.println("\nObject set inactive:"); + System.out.println("\tID: " + objectID); + } + + } + } catch (SQLException e) { + throw new AbortTransactionException(e.getMessage()); + } + + } + + + /** + * logTransaction + * Add transaction to the logs. + */ + // TODO: optimise requests (only one executeUpdate) + public void logTransaction(String transactionID, String transactionJson) + throws NoSuchAlgorithmException, SQLException + { + + // add transaction to the logs + String sql = "INSERT INTO logs (transaction_id, transactionJson) VALUES (?, ?)"; + PreparedStatement statement; + statement = connection.prepareStatement(sql); + statement.setString(1, transactionID); + statement.setString(2, transactionJson); + statement.executeUpdate(); + statement.close(); + + // verbose print + if ( Main.VERBOSE) { + System.out.println("\nTransaction added to logs:"); + System.out.println("\tID: "+transactionID); + System.out.println("\tTransaction: "+transactionJson); + } + + // update head + updateHead(transactionJson); + + } + + /** + * updateHead + * Update the logs' head. + */ + private void updateHead(String transactionJson) throws SQLException, NoSuchAlgorithmException { + + // verbose print + if ( Main.VERBOSE) { System.out.println("\nUpdating log's head:"); } + + // get the previous head + String sql = "SELECT digest FROM head ORDER BY ID DESC LIMIT 1"; + PreparedStatement statement = connection.prepareStatement(sql); + ResultSet resultSet = statement.executeQuery(); + + // insert new head + sql = "INSERT INTO head (ID, digest) VALUES (null, ?)"; + statement = connection.prepareStatement(sql); + + // check if there is at least one head in the table + String newHead; + if (resultSet.isBeforeFirst()) { + if ( Main.VERBOSE) { System.out.println("\tOld head: "+resultSet.getString("digest"));} + newHead = this.generateHead(resultSet.getString("digest"), transactionJson); + } + // if not, simply had a hash of the transaction + else { + if ( Main.VERBOSE) { System.out.println("\tOld head: NONE");} + newHead = this.generateHead(transactionJson); + } + statement.setString(1, newHead); + statement.executeUpdate(); + statement.close(); + if ( Main.VERBOSE) { System.out.println("\tNew head: "+newHead); } + + } +} diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/SimpleCache.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/SimpleCache.java new file mode 100644 index 00000000..badd937e --- /dev/null +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/SimpleCache.java @@ -0,0 +1,63 @@ +package uk.ac.ucl.cs.sec.chainspace; + +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * SimpleCache + * + * A simple and inefficient caching system. + */ +class SimpleCache extends ArrayList implements Cache { + + // instance variables + private int cacheDepth; + + SimpleCache(int cacheDepth) { + + this.cacheDepth = (cacheDepth < 0) ? 0 : cacheDepth; + + } + + /** + * isInCache + * Verify whether the transaction has recenlty been processed. This could happens due to multiple broadcasts between + * nodes and shards. + */ + public boolean isInCache(String input) { + + // compute digest + String digest; + try { + digest = Utils.hash(input); + } catch (NoSuchAlgorithmException e) { + digest = input; + } + + // if the transaction is already in the cash, return true + // NOTE: method toArray() as complexity O(n), making the binarySearch O(log n) useless here + if( Arrays.binarySearch(this.toArray(), digest) >= 0 ) {return true;} + + // otherwise update cache and return false + updateCache(digest); + return false; + + } + + /** + * updateCache + * Update the values in the cache (suppress the oldest entry and add the new one) + */ + private void updateCache(String digest) { + + if (this.size() < this.cacheDepth) { + this.add(digest); + } + else { + this.remove(0); + this.add(digest); + } + + } +} diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Store.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Store.java index 7fdf76e0..32226e36 100644 --- a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Store.java +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Store.java @@ -5,21 +5,29 @@ /** * - * + * Simple key-value store. */ class Store { + // instance variables private Pair[] array; + + /** + * constructor + * + */ private Store(Pair[] array) { + this.array = array; } - Pair[] getArray() { - return array; - } + /** + * getValueFromKey + * Get a value in the store from its associated key. + */ String getValueFromKey(String key) { for (int i = 0; i < this.getArray().length ; i++) { @@ -32,8 +40,19 @@ String getValueFromKey(String key) { } + /* + Getters + */ + Pair[] getArray() { + return array; + } + + /** + * fromJson + * Convert a JSONArray representing the sore into a proper Store Java object. + */ static Store fromJson(JSONArray jsonArray) { Pair[] pairArray = new Pair[jsonArray.length()]; @@ -41,20 +60,19 @@ static Store fromJson(JSONArray jsonArray) { for (int i = 0; i < jsonArray.length(); i++) { pairArray[i] = new Pair( jsonArray.getJSONObject(i).getString("key"), - jsonArray.getJSONObject(i).getJSONObject("value").toString() + jsonArray.getJSONObject(i).get("value").toString() ); } return new Store(pairArray); } - /* - String toJson() { - Gson gson = new GsonBuilder().create(); - return gson.toJson(this); - } - */ + + /** + * + * Elements of the key-value store + */ private static class Pair { String key, value; @@ -68,7 +86,6 @@ private static class Pair String getKey() { return key; } - String getValue() { return value; } diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Transaction.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Transaction.java index 7d6bf992..5bf8b9f7 100644 --- a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Transaction.java +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Transaction.java @@ -6,32 +6,72 @@ import java.security.NoSuchAlgorithmException; - +/** + * + * + */ class Transaction { - private int contractID; + private int contractID; private String[] inputIDs; private String[] referenceInputIDs; - private String parameters; + private String[] parameters; + private String[] returns; private String[] outputIDs; + private String[] dependencies; - Transaction(int contractID, String[] inputIDs, String[] referenceInputIDs, String parameters, String[] outputIDs) { - this.contractID = contractID; - this.inputIDs = inputIDs; - this.referenceInputIDs = referenceInputIDs; - this.parameters = parameters; - this.outputIDs = outputIDs; + /** + * constructor + */ + Transaction( + int contractID, + String[] inputIDs, + String[] referenceInputIDs, + String[] parameters, + String[] returns, + String[] outputIDs, + String[] dependencies + ) { + this.contractID = contractID; + this.inputIDs = inputIDs; + this.referenceInputIDs = referenceInputIDs; + this.parameters = parameters; + this.returns = returns; + this.outputIDs = outputIDs; + this.dependencies = dependencies; } + + /** + * fromJson + * Returns a transaction object from a json string representing it + */ static Transaction fromJson(JSONObject json) { Gson gson = new GsonBuilder().create(); return gson.fromJson(json.toString(), Transaction.class); } + /** + * toJson + * Returns a json string representing the transaction + */ String toJson() { Gson gson = new GsonBuilder().create(); return gson.toJson(this); } + /** + * getID + * Get the transaction's ID. + */ + String getID() throws NoSuchAlgorithmException { + return Utils.hash(this.toJson()); + } + + + /* + getters + */ + int getContractID() { return contractID; } @@ -44,15 +84,19 @@ String[] getReferenceInputIDs() { return referenceInputIDs; } - String getParameters() { + String[] getParameters() { return parameters; } + String[] getReturns() { + return returns; + } + String[] getOutputIDs() { return outputIDs; } - String getID() throws NoSuchAlgorithmException { - return Utils.hash(this.toJson()); + String[] getDependencies() { + return dependencies; } } diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/TransactionForChecker.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/TransactionForChecker.java new file mode 100644 index 00000000..dbf1fc72 --- /dev/null +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/TransactionForChecker.java @@ -0,0 +1,122 @@ +package uk.ac.ucl.cs.sec.chainspace; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.json.JSONObject; + +import java.security.NoSuchAlgorithmException; + +/** + * + * + */ +class TransactionForChecker { + private int contractID; + private String[] inputs; + private String[] referenceInputs; + private String[] parameters; + private String[] returns; + private String[] outputs; + private String[] dependencies; + + /** + * constructor + */ + TransactionForChecker( + int contractID, + String[] inputs, + String[] referenceInputs, + String[] parameters, + String[] returns, + String[] outputs, + String[] dependencies + ) { + this.contractID = contractID; + this.inputs = inputs; + this.referenceInputs = referenceInputs; + this.parameters = parameters; + this.returns = returns; + this.outputs = outputs; + this.dependencies = dependencies; + } + + + /** + * fromJson + * Returns a transaction object from a json string representing it + */ + static TransactionForChecker fromJson(JSONObject json) { + Gson gson = new GsonBuilder().create(); + return gson.fromJson(json.toString(), TransactionForChecker.class); + } + + /** + * toJson + * Returns a json string representing the transaction + */ + String toJson() { + Gson gson = new GsonBuilder().create(); + return gson.toJson(this); + } + + + /** + * getID + * Get the transaction's ID. + */ + String getID() throws NoSuchAlgorithmException { + return Utils.hash(this.toJson()); + } + + + + /** + * addParameters + * Add new parameters to the transaction. + */ + void addParameters(String[] newParameters) { + + String[] tmp = new String[this.parameters.length + newParameters.length]; + System.arraycopy(this.parameters, 0, tmp, 0, this.parameters.length); + System.arraycopy(newParameters, 0, tmp, this.parameters.length, newParameters.length); + this.parameters = tmp; + + } + + + /* + getters + */ + int getContractID() { + return contractID; + } + + String[] getInputs() { + + return inputs; + } + + String[] getReferenceInputs() { + return referenceInputs; + } + + String[] getParameters() { + + return parameters; + } + + String[] getReturns() { + + return returns; + } + + String[] getDependencies() { + + return dependencies; + } + + String[] getOutputs() { + + return outputs; + } +} diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/TransactionPackager.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/TransactionPackager.java new file mode 100644 index 00000000..aedcf258 --- /dev/null +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/TransactionPackager.java @@ -0,0 +1,142 @@ +package uk.ac.ucl.cs.sec.chainspace; + +import org.json.JSONObject; + +import java.security.NoSuchAlgorithmException; + +/** + * TransactionPackager + * + * This class provides a some utilisties to retrieve transactions objects from JSON strings (comming from the HTTP + * requests). This is a separate class, so if we decide to change the transaction's format in the future (change the + * key-value store, use tree structure, etc), the core is not modified: the core only requires the original transaction + * (the one with the IDs) and the packet to send to the checker. + */ +class TransactionPackager { + + /** + * makeTransaction + * Extract a transaction object from the json request. + */ + static Transaction makeTransaction(String request) throws AbortTransactionException { + // get json request + JSONObject requestJson = new JSONObject(request); + + // get the transaction and the key-value store as java objects + Transaction transaction; + try { + + // create java objects + transaction = Transaction.fromJson(requestJson.getJSONObject("transaction")); + + } + catch (Exception e) { + throw new AbortTransactionException("Malformed transaction."); + } + + // return transaction + return transaction; + } + + + /** + * makeFullTransaction + * convert a key-value store and a transaction into a fullTransaction java object + */ + static TransactionForChecker makeFullTransaction(String request) throws AbortTransactionException { + + // get json request + JSONObject requestJson = new JSONObject(request); + + // get the transaction and the key-value store as java objects + Transaction transaction; + Store store; + try { + + // create java objects + transaction = Transaction.fromJson(requestJson.getJSONObject("transaction")); + store = Store.fromJson(requestJson.getJSONArray("store")); + + // check transaction's integrity + if (! checkTransactionIntegrity(transaction, store)) { + throw new Exception(); + } + + } + catch (Exception e) { + throw new AbortTransactionException("Malformed transaction or key-value store."); + } + + // assemble inputs objects + String[] inputs = new String[transaction.getInputIDs().length]; + for (int i = 0; i < transaction.getInputIDs().length; i++) { + inputs[i] = store.getValueFromKey(transaction.getInputIDs()[i]); + } + + // assemble reference inputs objects + String[] referenceInputs = new String[transaction.getReferenceInputIDs().length]; + for (int i = 0; i < transaction.getReferenceInputIDs().length; i++) { + referenceInputs[i] = store.getValueFromKey(transaction.getReferenceInputIDs()[i]); + } + + // assemble output objects + String[] outputs = new String[transaction.getOutputIDs().length]; + for (int i = 0; i < transaction.getOutputIDs().length; i++) { + outputs[i] = store.getValueFromKey(transaction.getOutputIDs()[i]); + } + + // create full transaction + return new TransactionForChecker( + transaction.getContractID(), + inputs, + referenceInputs, + transaction.getParameters().clone(), + transaction.getReturns().clone(), + outputs, + transaction.getDependencies() + ); + + } + + + /** + * checkTransactionIntegrity + * Check the transaction's integrity. + */ + private static boolean checkTransactionIntegrity(Transaction transaction, Store store) + throws NoSuchAlgorithmException + { + + // check transaction's and store's format + // all fields must be present. For instance, if a transaction has no parameters, an empty array should be sent + if ( + store.getArray() == null + || transaction.getInputIDs() == null + || transaction.getReferenceInputIDs() == null + || transaction.getReturns() == null + || transaction.getParameters() == null + || transaction.getOutputIDs() == null + || transaction.getDependencies() == null + ){ + return false; + } + + + /* + TODO: for input and reference input objects, check that the ID in the store matche the value in the db. + + FOR (every transaction's input ID) + valueFromStore = getValueFromStore(ID) + valueFromDB = getObjectFromDB(ID) + IF ( hash(valueFromStore) == hash(valueFromDB) OR valueFromDB == NOT_FOUND ) + RETURN TRUE + ELSE + RETURN FALSE + + */ + + return true; + + } + +} diff --git a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Utils.java b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Utils.java index f0549e48..6b77d3b3 100644 --- a/src/main/java/uk/ac/ucl/cs/sec/chainspace/Utils.java +++ b/src/main/java/uk/ac/ucl/cs/sec/chainspace/Utils.java @@ -1,22 +1,34 @@ package uk.ac.ucl.cs.sec.chainspace; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.HttpClientBuilder; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** + * Utils * - * + * Some general purpose utilities. */ class Utils { + /** * hash * Compute the SHA-256 hash of the input string. * * @param input the string to hash * @return the input's SHA-256 digest - * @throws NoSuchAlgorithmException This exception should never happens since the algorithm is hardcoded. + * @throws NoSuchAlgorithmException This exception should never happens since the algorithm is hardcoded */ static String hash(String input) throws NoSuchAlgorithmException { @@ -35,7 +47,7 @@ static String hash(String input) throws NoSuchAlgorithmException { * @param object the hash image * @param hashedValue the digest * @return whether the digest matches the hash image - * @throws NoSuchAlgorithmException This exception should never happens since the algorithm is hardcoded. + * @throws NoSuchAlgorithmException This exception should never happens since the algorithm is hardcoded */ static boolean verifyHash(String object, String hashedValue) throws NoSuchAlgorithmException { @@ -44,4 +56,62 @@ static boolean verifyHash(String object, String hashedValue) throws NoSuchAlgori } + /** + * makePostRequest + * Make a simple post request + * + * @param url to url where to make the request + * @param postData the post data representing a json string + * @return the string response of the server + * @throws IOException general IO exception, thrown if anything goes bad + */ + static String makePostRequest(String url, String postData) throws IOException { + + // prepare post request + HttpClient httpClient = HttpClientBuilder.create().build(); + StringEntity postingString = new StringEntity(postData); + HttpPost post = new HttpPost(url); + post.setEntity(postingString); + post.setHeader("Content-type", "application/json"); + + // execute + HttpResponse response = httpClient.execute(post); + + // return string response + return new BasicResponseHandler().handleResponse(response); + + } + + + /** + * printHeader + * Nicely display a header to the console. + * @param title the title to print to the console + */ + static void printHeader(String title) { + System.out.println("\n----------------------------------------------------------------------------------"); + System.out.println("\t" + title); + System.out.println("----------------------------------------------------------------------------------"); + } + + /** + * printStacktrace + * Nicely print the exception's stack trace to the console. + * @param e the exception from with to print the stack trace + */ + static void printStacktrace(Exception e) { + System.out.println(); + e.printStackTrace(); + System.out.println(); + } + + /** + * printLine + * Draw a simple line to the console. + */ + static void printLine() { + System.out.println("\n----------------------------------------------------------------------------------"); + System.out.println("\n"); + } + } diff --git a/src/main/pom.xml b/src/main/pom.xml index f838de92..b2793444 100644 --- a/src/main/pom.xml +++ b/src/main/pom.xml @@ -61,6 +61,8 @@ spark-core 2.6.0 + +