edgecase_datafeed 220 2021-04-30 This is the date at the time of creation of this datafeed article. A checkpoint article containing a hash of this datafeed article may be created on this date or at a later date. 215 10 2021-04-10 bitcoin fe062b308ad6254b71ae4af5bbe8ec485105ebb9ae9cf1b905b115f596dd827a 678607 1HtwyqFWNVDoSEVqZwjBRRAV2oEsi8aQXr 13MfGs39pR5aEK4iKdoLjVYXKwi6Y3uyPq 1Hdv9WprSk5ugh12TpsLvEt6tfdSmnz1SG
Blockchain_Application_Development_Strategy stjohn_piano 2021-04-30 yes Problem Summary: While developing blockchain software, how can we cheaply find and fix errors in logic code that executes near expensive blockchain calls? Problem Description: 1) A blockchain application will create blockchain transactions. These transactions, once they settle, are irreversible. They are essentially "signed API calls" to a blockchain. If and when they settle, they will permanently alter that blockchain's state. A blockchain can be thought of as a write-only external API. 2) Inside a blockchain application, there will be important functions (e.g. "createWithdrawal") that do a lot of work - designing, creating, storing, and broadcasting a transaction. 3) Developing new code that will fit into this work sequence is necessary, but also difficult to test. Naively, the entire work sequence is run in order to test the new section of code. Mistakes are expensive and irreversible. 4) Calls to databases can also be expensive. An incorrect database call can create messy data that is very time-consuming to rectify. 5) Calls to external APIs can also be expensive. They may count against a paid monthly limit. And, similarly to database calls, an incorrect API call can create messy data on the company account on the external service that is very time-consuming to rectify. Development Strategy: - We treat all blockchain transactions, database calls, and API calls as "external calls". This list should probably also include "calls to internal APIs or other internal processes", because errors that occur across multiple processes are painful to diagnose, and because clean-up is likely to be time-consuming. - An external call occurs in its own function, which can be tested separately. This is called an "edge function". All edge functions should have timeouts and failure handling. -- Output data can be recorded, and later used as input data when testing other functions. - A function that calls an edge function is called a "blend function". It may call multiple edge functions and / or various logic functions. -- Almost no logic takes place within a blend function. We permit only assignments, function calls, error-handling decisions, and logging actions. -- Even a single syntax error can be expensive to locate and fix if external calls occur during the function. This is why we avoid logic in blend functions (unless the edge functions only retrieve data). It's possible to mock the edge functions, but this can be time-consuming. Putting all logic into separate logic functions for unit-testing is the cheapest way to detect, locate, and fix errors within the logic code, especially during continuous application development. - A function that only performs logic operations is called a "logic function". A function that only calls other logic functions is also a logic function. - Unit tests can be written for logic functions. - End-to-end / behavioural tests can be written for blend functions. -- Mock functions can be written and substituted for any edge functions. Mock functions will return output data that was previously recorded. - Edge functions are tested manually, using a cmd-line tool (logging at DEBUG level). -- The output data can be stored for later use as test input to other functions. -- For database edge functions, it's also possible (although time-consuming) to write tests that spin up a new database, populate it with data, run the edge function, and check that the data in the database is now in the expected final state. -- For blockchain edge functions, it's also possible (although time-consuming) to write tests that spin up a tiny standalone version of the relevant blockchain, populate it with data by executing transactions, run the edge function, and check that the data in the blockchain is now in the expected final state. -- Edge functions that solely retrieve data can be tested with unit tests. These are database SELECT functions, external API GET functions, and blockchain loadTransaction / loadBlock functions. Tactics: - Use argument validation. For crucial functions, this should be very strict. - Use named arguments where possible, not positional ones. This is very helpful when you later add or subtract arguments from functions. - Each module (i.e. file) has its own named logger. This is expensive to set up but extraordinarily helpful for diagnosing problems. - Ideally, loggers are namespaced by filename and path (e.g. they contain something similar to "[appName/chainOps/foo.js]" somewhere in the line) and contain the relevant function name and line number. They are configurable by parent modules (i.e. a parent module can set a child module's log level). Options: Colorised output, an initial timestamp. Log level names (e.g. DEBUG, INFO) should all be padded to the same length. Optionally, the logger logs to a file, with a rolling window (e.g. 30 days). If multiple processes are logging to the same file, use a uniqueID in each log line to differentiate between each output stream. Here's a couple example log lines: 2021-02-02 17:58:11 DEBUG [datafeed_explorer/__init__.py: 38 (setup)] Setup complete. 2021-02-02 17:58:41 INFO [datafeed_explorer/explorer.py: 42 (handle_request)] [request 55397015] New request received. - All external functions return the same thing: A 'result' object containing 'err' and 'msg' properties and various other data properties. -- The data properties depend on the function. They should always be present, even if empty. -- The 'err' property should always be present, even if empty. -- The 'msg' property contains a message string that describes the result of the function. This is helpful for logging. It can be an empty string. -- The 'err' property is an object that contains 'code' and 'msg' properties. --- The 'code' property contains the error code, which permits complex error handling in caller functions. If this is 0, then no error occurred. Always set it to a non-zero value at the start of a function, so that the function only completes successfully if certain conditions are explicitly achieved. --- The err.msg property contains an optional error message, which is helpful for logging and for throwing informative Errors. - At first, we handle errors by throwing them. They are caught and reported in try / catch clauses. -- Over time, check for common errors and return them within the result object. The caller function can then select an appropriate action to take in response to the error. Let's do an example. Here is a NodeJS function called createWithdrawal. This is new code, written for this article as an illustration. It has not been tested. Most of the called functions are not shown. createWithdrawal.js nodejs yes let logger = new Logger({name:'chainOps:withdraw.js', level:'error', timestamp:true}); logger.setLevel('debug'); // tmp: for recording purposes. let log = logger.info; let deb = logger.debug; let jd = JSON.stringify; function lj(foo) { log(jd(foo, null, 2)); } function dj(foo) { deb(jd(foo, null, 2)); } let v = require('validate.js'); // createWithdrawal is a blend function. async function createWithdrawal(args) { try { let err, msg; let name = 'createWithdrawal'; deb(`Entering function: ${name}`) deb(`Args: ${args}`); // Validation functions throw errors if their argument isn't valid. v.validateNArgs({args, n:1}); let {coinSymbol, addresses, transferIDs} = args; v.validateString(coinSymbol); // E.g. "BTC". for (let address of addresses) { v.validateAddress({coinSymbol, address}); } // transferIDs contains the database IDs of these withdrawals. v.validateArray(transferIDs); for (let transferID of transferIDs) { v.validateInteger(transferID); } let finalMsg = ''; let finalErr = {'code': 1, 'msg': '[unknown error]'}; // We set finalErr to contain an error by default. Success must be explicitly toggled. // generateBlockchainTx is a edge function (a blockchain one). let tx; ({err, msg, tx} = await generateBlockchainTx({coinSymbol, addresses})); // Design note: The tx object should contain various properties, including the coinSymbol and the txHex. dj({tx}); // Here, we record the tx data, which can later be used for testing other functions. if (err.code) { logger.error(jd(err)); if (err.code == 1) { // Insufficient funds. let recipient = 'walletSupport'; let report = '[detailed description of problem]'; logger.error(report); ({err, msg} = await sendNotification({recipient, report})); if (err.code) throw Error(err.msg); return {err:finalErr, msg:finalMsg}; } else { throw Error(err.msg); } } // Generate a log message. // This is a simple operation (i.e. create a text string that includes data from an array), but nonetheless we put it within its own logic function, so that it can be unit-tested separately. // This function is shown below the current function. // createLogString4 is a logic function. let logString4; ({err, msg, logString4} = createLogString4({transferIDs})); if (err.code) throw Error(err.msg); log(logString4); // calculateTotalWithdrawAmount is a blend function. ({err, msg, totalWithdrawAmount} = calculateTotalWithdrawAmount({dbConnection, transferIDs}); if (err.code) throw Error(err.msg); lj({totalWithdrawAmount}); // getDBConnection is an edge function. let dbConnection; ({err, msg, dbConnection} = await getDBConnection({})); if (err.code) { finalMsg = `Failed to open dbConnection: ${err.msg}`; deb(finalMsg); // In this case, we return this error in the result object. // The caller function can then handle this particular error in a specific way. return {err:finalErr, msg:finalMsg}; } // beginTransaction is an edge function (or rather, an edge method). ({err, msg} = await dbConnection.beginTransaction()); if (err.code) throw Error(err.msg); // storeBlockchainTx is a (database) edge function. ({err, msg} = await storeBlockchainTx({dbConnection, tx})); if (err.code) throw Error(err.msg); // broadcastBlockchainTransaction is a (blockchain) edge function. let txID; ({err, msg, txID} = await broadcastBlockchainTransaction({tx})); if (err.code) throw Error(err.msg); dj({txID}); // storeBlockchainTxID is a (database) edge function. ({err, msg} = await storeBlockchainTxID({dbConnection, tx, txID}); if (err.code) throw Error(err.msg); // commitTransaction is an edge function (or rather, an edge method). ({err, msg} = await dbConnection.commitTransaction()); if (err.code) throw Error(err.msg); // Explicitly toggle success. finalErr = {'code':0, 'msg':''}; // The creation of this finalMsg value should probably be moved into its own logString5 function, so that it can be easily unit-tested. finalMsg = `${name}: ${coinSymbol} withdrawal transaction created and broadcast. TxID: ${txID}`; return {err:finalErr, msg:finalMsg}; } catch(mainErr) { logger.error(mainErr); ({err, msg} = await dbConnection.rollback()); if (err.code) throw Error(err.msg); ({err, msg} = await dbConnection.release()); if (err.code) throw Error(err.msg); throw Error(mainErr); } function createLogString4({transferIDs}) { let x = transferIDs.join(', '); let result = `Transfer IDs: [${x}]`; return {err:0, msg:'', result}; } // This is a blend function, but it only retrieves data, so we can unit-test it, and it can include logic. async function calculateTotalWithdrawAmount({dbConnection, transferIDs}) { let ids = transferIDs.join(', '); let sql = "SELECT SUM(amount) AS total FROM withdraw WHERE id IN (${ids})"; ({err, msg, result} = await dbConnection.query(sql)); if (err.code) throw Error(err.msg); let totalWithdrawAmount = result.rows[0].total; return {err:0, msg:'', totalWithdrawAmount}; }
iQIcBAABCgAGBQJgjD0FAAoJEC8RP+HmG9MXEAUQAJbGfwh8s+I0x2FDGZJnfewy gY5LWEdMyPOHRZ58D3ZPBbua0Wk+siveEWMv0YlbHm+RkYEG8W1CuL7tOcZc6ZnB hlA9Ksmr6YmvqrOeTH4jB6IfnN5iHnsWOQqHZqKDi19E5uRSs/H9VLBeZR3ssUKO DzQ0YvgDiBawALtfyTeSqpLowDmeAnLdehsm4pN34nh3R05vXq4ZCDpcmeRoDDmc VoxRuV7PfnimX5ndViwFX5rQZ/+PeLRgA95UvUm7iNsEsVOKcg5fOVz4QiJ3JdvR 92Tl6DgS7w41kpp4CS6FUaOd8kSThAU7WQGEmdlqwD9VHFMsFL+I6nJlz+LxafxR xbpA5p0tHD+SxSNJTX+Vny7VLo9I/wi5G46FP9P9rtEPtwMYDidkWQNmZJXvlAtI Ru3VkPBFUR7gtuyq1rBIBr8GgK+4FlY+zA1PAgURU/bd3fhDKLs7WWyzwJdc2uGI TkOHEQosT8uy4df61YDFREBZDIIFcWjUU0HcYh+sxF8YrQQbqEv0XrkRHpkni+Tp Imd+qxiAEYlWw/cFrXPaTXcnspoS2LqlZC5/9iQYH2GIT0917vJZfvsfWFqayJeO ifLGmUnPaAeEap8LGFPFpGoUzEqez1zeqAbiERcp8zQX7UZu8CPIg6lSW/dfm40d NkwEBsh4Ut/s5USSDwzE =Nfid
iQIcBAABCgAGBQJgjD0nAAoJECL1OzZgiBhw544P/2AdllMKdhhqzXFHgZfDTEOP gn763uN2nLx5vb7V0jFaBgfcVvKP1+BEkLLsjaz6qS0vaWkUTGzxoQ/zKXIE8ZbW cdzuqUEiMjd9qVsQeyfWKnMgfInRDWYkGh5ASDylLvThugEodHk+tolF9geh7fK3 GsuU2Qy6qMYBaMoSADrWp2gujw5ziQPQYUueIdR6ko5O/VzTP9PjMdtN+11x1LgC voEM54sZGDKfAhSYPf6IvBeMHHrEM48tbsLjg4JlxBv7Ie5vXx7pbls2crUDoGb5 cMZttT+3MiwBMZKTHCBb6gTx8KcmKjF75G6RQ7kmd7MKEbKPaPcKIvj6/Hwc/enP FDoCLu5habXf2vhr5RdJUxWq+kRBznhiQAIeGMy6+AIHhPtXKMwXLCQ5y9epamWv LacmEJZXNUR0520J65kAMw4CQR8G2weQCKPkdgwcBKTlfCTGknXmo7c4JKFSD67N SPkISr2PnzFG9SD/5Z6u9O5xys3XB8AwbMIDd/ilTi3YjexPJk39iFCXY2ddBGIP vhvuF8R3kumSoclnxv32DyKmXpiDC4wOho9h/ATIWyte7KOmDgy+X+K7nkdznyeD yNGsyAe6rjzhzJ+wd27cgb9n60m9J1mEDxc+t4ZjlNz9boHTwiKXO9w/ByAHzXMx cvIaM4WE79sFLn3DdhGV =+ed/