This repository includes all components needed to integrate 3 blockchains with API & SignService for each of them. Basic structure of repository:
corefolder includes common to all blockchains functionality: configuration loading, logging, HTTP server, database abstractions and corresponding tests.- each of blockchain folders (
monero,ripple,stellar) with chain-specific logic; - root folder also contains
Dockerfileper each needed container (6 required + several optional like testnet for Monero).
Core is responsible for setting up HTTP server and common among all blockchains environment. It also contains common abstract classes Wallet & Tx which are overridden for each of blockchains supported.
index.jsloads configuration, sets up logging, conntects to db and starts HTTP server with specified endpoints.index-api.jsprovides default API endpoints, not specific to any blockchain.index-sign.jsdoes the same for SignService.tests.jscontains core tests: config loading, data validation, server lifecycle, etc.test-chain.jscontains standard test suite for all blockchains and is being called from each blockchaintests.js.
Booting process can be described in following steps:
- Load configuration. Exit if failed, proceed if succeeded.
- Connect to Mongodb if config has
storeprefefrence. Exit if failed, proceed if succeeded. - Start server with provided wallet implementation & extra endpoints specifications.
- Gracefully shutdown on SIGHUP or other signals.
Below is contents of test-config.json which is used for tests of Monero API. All listed fields are required for API services. Other blockchains have very similar configuration files. Fields Wallet*** can be also set in SettingsUrl and environment variables under the same names.
{
"version": "0.0.1",
"chain": "monero",
"serviceName": "MoneroApiService",
"port": 3000,
"log": "debug",
"testnet": true,
"store": "mongodb://192.168.1.216:27017/lykke_test",
"node": "http://192.168.1.216:28081",
"assetId": "XMR_ASSET_ID",
"assetName": "Monero",
"assetOpKey": "monero",
"assetAccuracy": 12,
"refreshEach": 15000,
"SeedView": "3b7e393e48ccedc23e555f293b88b3a6662471e42b79071ed9fd7a6333cbd302",
"SeedSeed": "0569d304201515ac8f7af8276234b5c159514a2c97d2e17068bf0298adfe490a",
"SeedAddress": "9yhHFUUZeARW6ecyHJe2ZARrWEHnifGLQK8tvKZVccVYNoeRKQp8rfDXGzWaJuGT4m3diT8gHGww9B5vwW92m2k91iMJTPM",
"WalletAddress": "9zXxFrqsyjwGwFyWx3FQxM4Fe1XruVkNhP3FFHVuTBwtWU2dVoBaSbBFAF1GAwUgn82Xt1jqgQ8uFQffTAZnqe2L9ahmG7r",
"WalletViewKey": "516cfb79ce2265f4b407293ff7b1cb219f13fe10ee15d264f7772f98fe5f7208"
}
Configuration is loaded with retries as required from URL specified SettingsUrl ENV variable.
log.js in core folder is responsible for logging. It uses watson js logger with multiple outputs: file with name of [BLOCKCHAIN]-error.log for errors and stdout + azure table storage for all levels respective to logging level in configuration. When configuration is not loaded yet, output goes to default-error.log since we don't know blockchain name yet.
Mongodb is used for storage. DB URL is taken from store config preference. Standard db driver is used. store.js file contains db-related logic and corresponding abstractions as required. 2 collections are used: transactions for observed transactions, accounts for balances observing. Basic structure of records is following:
transactions:
{
"_id" : ObjectId("5a7f4d332d66cc54d35c6913"), // ID of transaction
"opid" : "OPERATION_ID", // operation id
"priority" : -1, // priority if any (extra parameter in POST /api/transactions/*)
"unlock" : -1, // unlock period if any (extra parameter in POST /api/transactions/*)
"operations" : [ // array of operations, that is payments or account creations (stellar-only)
{
"id" : '123', // id of operation (stellar only)
"from" : "SENDER_ADDRESS", // sender address, without paymentId
"sourcePaymentId" : null, // sender paymentId if any
"to" : "RECIEVER_ADDRESS", // receiver address, without paymentId
"paymentId" : "fcffc182c4643e5f", // receiver paymentId
"asset" : "monero", // asset code in blockchain terms
"amount" : 8000000000000, // amount as integer according to blockchain precision
"fee" : 0, // fee if any (0 for incoming transactions)
}
],
"hash" : "823ca0197f1c8eef55078fe84346b1fb2e17fee6ef8e868936481f3f81ad5898", // tx hash or id
"block" : 5023, // block when transaction was included in blockchain (or analog)
"timestamp" : 1518292268000, // last modification timestamp (time from blockchain)
"error" : null, // error if any, not used
"status" : "locked" // internal status: initial, sent, locked (incoming not mature enough outputs), confirmed, failed
}
accounts:
{
"_id" : "FULL_ADDRESS_WITH_PAYMENT_ID", // full account address, with paymentId
"paymentId" : "9c86de421dec37db", // paymentId of this account, that is unique identifier (random blockchain-specific string)
"balance" : 16000000000000, // account balance as integer according to blockchain precision
"block" : 5686 // last transaction block
}
paymentId is a unique string which identifies account transactions within one wallet. Monero uses 8-byte random, Stellar uses 14-byte random, Ripple uses 32-bit Integer random.
paymentId is included into _id, either implicitly (Monero, no special formatting required) or explicitly (by using simple format: "WALLETADDRESS+paymentId", where "+" is a separator which could be used in UI). Uniqueness of paymentId must be enforced externally through enforcing uniqueness of wallet address, that is _id. All cash-ins must be marked with corresponding paymentId:
- For Monero it's enough to send a payment to address equal to
_id, thus it doesn't contain any separators. - For Stellar & Ripple it's required to manually add a "memo" (Stellar) or "tag" (Ripple) to transaction in user's wallet application. This memo/tag must equal to a string following
+in address'_id.
Following indexes are enforced:
* hash unique & sparse index is enforced on transactions collection to ensure one tx per hash is recorded.
* opid unique & sparse index is enforced on transactions collection, for performance reasons.
* paymentId unique index is enforced on accounts collection, for performance reasons.
koa.js is used for middleware-style server implementation. All requests are logged (no body, only metod + path in INFO level & query + params in DEBUG level) & tagged with response time in X-Response-Time header. Request validation is standardized with koa-bouncer, thus whenever ValidationError exception is thrown by implementation, standard 400 response is generated by core's index.js as required. Other exceptions are caught and result in 500 error.
As a general rule, separate components are tested separately. core has tests for config loading, http server, db connectivity, etc. Each blockchain
has integration tests (API + SignService) at its folder. Ingegration test is mostly standard and can be found at core/tests-chain.js. Monero has separate test suite for native component (monero/xmr folder). Wherever a file named tests.js is placed, tests can be run run with npm test.
Each blockchain folder has following files:
api.jsis a node.js entry point for API service.sign.jsis a node.js entry point for SignService.wallet.jsis a specific blockchain implementation including transactions construction, updates, validation, etc.tests.jsfile contains tests.
Glossary:
- View wallet -- wallet which cannot sign transactions, it can only view incoming transfers and sometimes outgoing payments.
- Sign wallet -- wallet which can sign transactions, but doesn't see outputs or transactions because it's not connected to internet and doesn't have storage.
- Output -- generatly speaking public key of some specific amount of coins.
- Key image -- generaly speaking "signed" output which allows detection of whether output has been spent or not, prevents double spending in Monero.
Monero implentation is the most complicated of those 3 blockchains supported by this repository. It contains native C++ node.js module called xmr which is basically a modified wallet-cli v0.11.1.0 from Monero repo. Modification is done by extending tools::monero2 class by tools::XMRWallet class. To extend this class wallet2.cpp must be modified to have protected members instead of private. Thus wallet2.cpp must be compiled separately from libwallet and included in monero/xmr/wallet folder. libwallet is a shared library built by standard monero make (see Dockerfile-monero-sign for build process) required by native module at linking phase. Module building is done by standard npm install command through node-gyp.
Notable additions/changes of XMRWallet compared to standard tools::wallet2 include:
- Spend wallets don't store data in files.
- Modified refreshing logic to rescan spent outputs more often to support long-running view & spend wallets separation.
- Modified from standard file-based data formats for unsigned transactions, outputs, key images & signed transactions. Implementation still uses
boost::serialization, but addsgzip&base64encoding on top of custom data structures. Structures are custom to shift from exporting all outputs on each transaction to exporting only the ones which don't have key images yet. This dramatically decreases amounts of data.
Standard implementation:
- export all outputs from view wallet [~5 MB];
- import all outputs to spend wallet [~5 MB];
- export key images from spend wallet [~2 MB];
- import key images to view wallet [~2 MB];
- export unsigned transaction from view wallet [dozens of kilobytes];
- import & sign unsigned transaction in spend wallet [dozens of kilobytes];
- export signed transaction from spend wallet [dozens of kilobytes];
- import & submit signed transaction from view wallet [dozens of kilobytes].
Data amounts above are from testnet of several thousands blocks & for hundreds to small thousands of outputs, therefore one can easily imagine actual amounts of data in production.
XMRWallet implementation with view wallet somewhat in sync:
- export unsigned transaction & new outputs from view wallet [dozens of kilobytes];
- import new outputs & unsigned transaction to spend wallet [dozens of kilobytes];
- export new key images & signed transaction from spend wallet [dozens of kilobytes];
- import new key images & signed transaction to view wallet [dozens of kilobytes].
At last stage XMRWallet tries to submit transaction. If daemon accepts it, we're good and have all the key images without need of second round of syncing as opposed to standard implementation. If daemon declines transaction (with double spend error due to unavailability of some of key images when creating transaction), view wallet will return error. Next time mediator would try to create this transaction, view wallet would already have key images in sync, thus transaction would succeed.
There is also a precaution feature - full sync. Sometimes, if wallet is near empty or after sweep-all command (which merges thousands of outputs into smaller number of big outputs), wallet goes into sync required mode. It means that next time mediator would try to send a transaction, all outputs would be exported from view wallet instead of unsigned transaction. Mediator won't know this fact and would process transaction as usual.
- export all outputs from view wallet instead of unsigned transaction;
- import all outputs to spend wallet;
- export all key images from spend wallet;
- import all key images to view wallet.
At last stage, instead of submitting transaction, view wallet will return error. But next transaction would go fine since we have all key images in view wallet.
Bottomline:
- Optimized data formats:
- Transferring unrelated outputs / key images along with unsigned / signed transaction, to keep view wallet in sync.
- GZIP compression of data.
- Support of operations made outside of API/SignService.
- Keeping most of standard monero wallet logic, therefore ensuring ease of upgrades to future hard forks.
iartem/monero-testnet docker image is available for use at Docker Hub. It creates 3-node test network which simplifies testing. See monero/scripts.sh for scripts to generate wallets (nodes mine into them) and docker startup command.
Example API configuration:
{
// isalive
"version": "0.0.1",
"chain": "monero",
"serviceName": "MoneroApiService",
// port to bind to
"port": 3001,
// log level
"log": "debug",
// testnet flag
"testnet": true,
// mongodb url
"store": "mongodb://192.168.1.216:27017/lykke_test_monero",
// node url
"node": "http://192.168.1.216:28081",
// asset id to return in endpoints
"assetId": "XMR_ASSET_ID",
// isalive
"assetName": "Monero",
// asset key used in transactions (Stellar & Ripple)
"assetOpKey": "monero",
"assetAccuracy": 12,
"refreshEach": 15000,
"wallet": {
"view": "805578055208faca04c977f77efc02db9eda17f78e0f17b7475df1bf30f5bc04",
"address": "9ycSSr8QT2GL9GcWDJGW3jaGXnoNcN2PFLXprcqneVebFn2kGNPiLq8cxJrufqhCUq12rndThWegqiNbVzTK5YBFMf4rc8w"
}
}
Example SignService configuration:
{
"version": "0.0.1",
"chain": "monero",
"serviceName": "MoneroSignService",
"port": 5001,
"log": "debug",
"testnet": true,
"assetId": "XMR_ASSET_ID",
"assetName": "Monero",
"assetOpKey": "monero",
"assetAccuracy": 12,
"wallet": {
"view": "805578055208faca04c977f77efc02db9eda17f78e0f17b7475df1bf30f5bc04",
"address": "9ycSSr8QT2GL9GcWDJGW3jaGXnoNcN2PFLXprcqneVebFn2kGNPiLq8cxJrufqhCUq12rndThWegqiNbVzTK5YBFMf4rc8w"
}
}
These blockchains have very similar implementation:
- They support tagging transactions with some specific data.
- They require so called reserve to be held on any account to be able to make outgoing transactions and other operations from it.
Therefore, for reasons other than Monero, yet one wallet scheme is preferred for these blockchains as well.
Implementation for Stellar & Ripple is very similar to Monero implementation, with only one difference: Monero encodes paymentId into address, while Stellar & Ripple don't have such ability. For this reasons SignService returns addresses which look like WALLETADDRESS+PAYMENTID, where '+' is a separator.
Other than address encoding there is no differences from Monero.