diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 62d802f38..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,63 +0,0 @@ -# Docker Compose for single sidetree bitcoin node - -version: '3.4' -services: - sidetree-bitcore: - image: sidetree-bitcore:latest - build: - context: . - dockerfile: docker/sidetree-bitcore/Dockerfile - ports: - - 3001:3001 - volumes: - - bitcoin-data:/app/sidetree-bitcore/data - environment: - BITCOIN_NETWORK: testnet - - sidetree-bitcoin: - image: sidetree-bitcoin:latest - build: - context: . - dockerfile: docker/sidetree-bitcoin/Dockerfile - ports: - - 3002:3002 - environment: - MONGODB_CONNECTION_STRING: "mongodb://root:example@mongo:27017/" - depends_on: - - mongo - - - sidetree-core: - image: sidetree-core:latest - build: - context: . - dockerfile: docker/sidetree-core/Dockerfile - ports: - - 3000:3000 - environment: - MONGODB_CONNECTION_STRING: "mongodb://root:example@mongo:27017/" - BLOCKCHAIN_SERVICE_URI: "http://sidetree-bitcoin:3002" - depends_on: - - mongo - - sidetree-bitcoin - - mongo: - image: mongo - restart: always - ports: - - 27017:27017 - environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: example - - mongo-express: - image: mongo-express - restart: always - ports: - - 8081:8081 - environment: - ME_CONFIG_MONGODB_ADMINUSERNAME: root - ME_CONFIG_MONGODB_ADMINPASSWORD: example - -volumes: - bitcoin-data: diff --git a/docs/bitcoin.md b/docs/bitcoin.md new file mode 100644 index 000000000..d40d02283 --- /dev/null +++ b/docs/bitcoin.md @@ -0,0 +1,31 @@ +# Bitcoin Blockchain Service Reference Implementation + + +## Value Time Lock + +### Protocol parameters + +| Protocol parameters | Description | +| ------------------------------------ | ---------------------------------------- | +| minimumValueTimeLockDurationInBlocks | TODO | +| maximumValueTimeLockDurationInBlocks | TODO | + +### Configuration parameters +* valueTimeLockUpdateEnabled + +This parameter controls whether the value time lock is actively being renewed and if the funds will be returned to wallet in case of `valueTimeLockAmountInBitcoins` being set to zero. When this parameter is set to `false`, parameters `valueTimeLockAmountInBitcoins`, `valueTimeLockPollPeriodInSeconds` and `valueTimeLockTransactionFeesAmountInBitcoins` will be ignored. + +* valueTimeLockAmountInBitcoins + +The desired fund locked to write larger operation batches. Set to 0 will causes existing locked fund (if exists) to be released back to wallet upon lock expiry. + +* valueTimeLockPollPeriodInSeconds + +The polling duration between checks to see if the value time lock needs to be re-locked or released back to wallet. + +* valueTimeLockTransactionFeesAmountInBitcoins + +The fund allocated for transaction fees for subsequent re-locking of the initial value time lock. + +> Developer's note: +This allotted amount is locked together with value time lock for simplicity of re-lock implementation. If this allotted amount is depleted due to subsequent re-locks, the remaining locked amount will be released back to wallet, and a new lock will be created with this allotted amount added to it again. diff --git a/docs/implementation.md b/docs/core.md similarity index 88% rename from docs/implementation.md rename to docs/core.md index 09854ddc3..2a50a6c9b 100644 --- a/docs/implementation.md +++ b/docs/core.md @@ -1,4 +1,4 @@ -# Sidetree Node.js Implementation Document +# Sidetree Core Node.js Implementation Document This document focuses on the Node.js implementation of the Sidetree protocol. @@ -82,6 +82,7 @@ The orchestration layer requires implementation of following interfaces per prot - `IRequestHandler` - Handles REST API requests. +## Core Service REST API ### REST API HTTP Response status codes @@ -94,9 +95,7 @@ The orchestration layer requires implementation of following interfaces per prot | 500 | Server error. | - -## Core Serivce REST API -The Core Service REST API impliments the [Sidetree REST API](https://identity.foundation/sidetree/api/), in addition it also exposes the following version API. +The Core Service REST API implements the [Sidetree REST API](https://identity.foundation/sidetree/api/), in addition it also exposes the following version API. ### Fetch the current service versions. Fetches the current version of the core and the dependent services. The service implementation defines the versioning scheme and its interpretation. @@ -698,141 +697,6 @@ HTTP/1.1 200 OK } ``` - -## CAS REST API -The CAS (content addressable storage) REST API interface aims to abstract the underlying Sidetree storage away from the main protocol logic. This allows the CAS to be updated or even replaced if needed without affecting the core protocol logic. Conversely, the interface also allows the protocol logic to be implemented in an entirely different language while interfacing with the same CAS. - -All hashes used in the API are encoded multihash as specified by the Sidetree protocol. - -### Read content -Read the content of a given address and return it in the response body as octet-stream. - -#### Request path -``` -GET /?max-size= -``` - -#### Request query parameters -- `max-size` - - Required. - - If the content exceeds the specified maximum allowed size, `HTTP 400 Bad Request` with `content_exceeds_maximum_allowed_size` as the value for the `code` parameter in a JSON body is returned. - - -#### Request example -``` -GET /QmWd5PH6vyRH5kMdzZRPBnf952dbR4av3Bd7B2wBqMaAcf -``` -#### Response headers -| Name | Value | -| --------------------- | ---------------------- | -| ```Content-Type``` | ```application/octet-stream``` | - -#### Response example - Resoucre not found - -```http -HTTP/1.1 404 Not Found -``` - -#### Response example - Content exceeds maximum allowed size - -```http -HTTP/1.1 400 Bad Request - -{ - "code": "content_exceeds_maximum_allowed_size" -} -``` - -#### Response example - Content not a file - -```http -HTTP/1.1 400 Bad Request - -{ - "code": "content_not_a_file" -} -``` - -#### Response example - Content hash is invalid - -```http -HTTP/1.1 400 Bad Request - -{ - "code": "content_hash_invalid" -} -``` - -### Write content -Write content to CAS. - -#### Request path -``` -POST / -``` - -#### Request headers -| Name | Value | -| --------------------- | ---------------------- | -| ```Content-Type``` | ```application/octet-stream``` | - -#### Response headers -| Name | Value | -| --------------------- | ---------------------- | -| ```Content-Type``` | ```application/json``` | - -#### Response body schema -```json -{ - "hash": "Hash of data written to CAS" -} -``` - -#### Response body example -```json -{ - "hash": "QmWd5PH6vyRH5kMdzZRPBnf952dbR4av3Bd7B2wBqMaAcf" -} -``` - -### Fetch the current service version -Fetches the current version of the service. The service implementation defines the versioning scheme and its interpretation. - -Returns the service _name_ and _version_ of the CAS service. - -#### Request path -``` -GET /version -``` - -#### Request headers -None. - -#### Request example -``` -GET /version -``` - -#### Response body schema -```json -{ - "name": "A string representing the name of the service", - "version": "A string representing the version of currently running service." -} -``` - -#### Response example -```http -HTTP/1.1 200 OK - -{ - "name": "ipfs", - "version": "1.0.0" -} -``` - ## Frequently Asked Questions ### Why is the signature not verified before a request is queued and written to the blockchain? End users are expected to use a "user agent" for making requests which should almost always generate the right signature, diff --git a/docs/docker.md b/docs/docker.md deleted file mode 100644 index 29356bf51..000000000 --- a/docs/docker.md +++ /dev/null @@ -1,33 +0,0 @@ -# Sidetree Docker Images - -## Overview -The Sidetree components can be build an operated as dockerized containers. To run a sidetree installation, you'll need the following images: - -- *sidetree-bitcore* -- Bitcore Node with Sidetree Bitcore Extension -- *sidetree-bitcoin* -- Generic Blockchain Interface with Bitcoin Implementation -- *sidetree-core* -- Sidetree Core Interface -- *mongo* -- *ipfs* - -## Environment -You'll need to have Docker Environment setup. To interact with the `docker-compose.yaml` you'll also need `docker-compose`. - -## Build - -To build all containers locally run - - docker-compose build - -in the root directory of this repository. - -## Run via compose - - docker-compose up {"service"} - -## Container Configuration -Containers are (mostly) configured via environment variables. These are: - -### sidetree-bitcore -- BITCOIN_NETWORK: {testnet, livenet} -- BITCOIN_PRIVATE_KEY_WIF: "Private Key in XX Format" -- BITCOIN_FEE: "Tx Fee in Satoshi" diff --git a/lib/bitcoin/BitcoinBlockDataIterator.ts b/lib/bitcoin/BitcoinBlockDataIterator.ts index 35010eb82..4d1608704 100644 --- a/lib/bitcoin/BitcoinBlockDataIterator.ts +++ b/lib/bitcoin/BitcoinBlockDataIterator.ts @@ -1,5 +1,5 @@ -import BitcoinFileReader from './BitcoinFileReader'; import BitcoinBlockModel from './models/BitcoinBlockModel'; +import BitcoinFileReader from './BitcoinFileReader'; import BitcoinRawDataParser from './BitcoinRawDataParser'; /** diff --git a/lib/bitcoin/BitcoinClient.ts b/lib/bitcoin/BitcoinClient.ts index ac49295cd..cc95cf4d7 100644 --- a/lib/bitcoin/BitcoinClient.ts +++ b/lib/bitcoin/BitcoinClient.ts @@ -1,16 +1,16 @@ import * as httpStatus from 'http-status'; +import { Address, Block, Networks, PrivateKey, Script, Transaction, Unit, crypto } from 'bitcore-lib'; +import nodeFetch, { FetchError, RequestInit, Response } from 'node-fetch'; import BitcoinBlockModel from './models/BitcoinBlockModel'; -import BitcoinSidetreeTransactionModel from './models/BitcoinSidetreeTransactionModel'; import BitcoinInputModel from './models/BitcoinInputModel'; +import BitcoinSidetreeTransactionModel from './models/BitcoinSidetreeTransactionModel'; import BitcoinLockTransactionModel from './models/BitcoinLockTransactionModel'; import BitcoinOutputModel from './models/BitcoinOutputModel'; import BitcoinTransactionModel from './models/BitcoinTransactionModel'; import BitcoinWallet from './BitcoinWallet'; import IBitcoinWallet from './interfaces/IBitcoinWallet'; -import nodeFetch, { FetchError, RequestInit, Response } from 'node-fetch'; -import ReadableStream from '../common/ReadableStream'; -import { Address, Block, Networks, PrivateKey, Script, Transaction, Unit, crypto } from 'bitcore-lib'; import { IBlockInfo } from './BitcoinProcessor'; +import ReadableStream from '../common/ReadableStream'; /** * Structure (internal to this class) to store the transaction information @@ -119,7 +119,7 @@ export default class BitcoinClient { * @param bitcoinLockTransaction The transaction object. */ public async broadcastLockTransaction (bitcoinLockTransaction: BitcoinLockTransactionModel): Promise { - const transactionHash = this.broadcastTransactionRpc(bitcoinLockTransaction.serializedTransactionObject); + const transactionHash = await this.broadcastTransactionRpc(bitcoinLockTransaction.serializedTransactionObject); console.info(`Broadcasted lock transaction: ${transactionHash}`); return transactionHash; @@ -590,8 +590,8 @@ export default class BitcoinClient { previousFreezeDurationInBlocks: number, newFreezeDurationInBlocks: number): Promise<[Transaction, Script]> { - // tslint:disable-next-line: max-line-length - console.info(`Creating a freeze transaction with freeze time in blocks: ${newFreezeDurationInBlocks} from previously frozen transaction with id: ${previousFreezeTransaction.id}`); + // eslint-disable-next-line max-len + console.info(`Creating a freeze transaction with freeze time of ${newFreezeDurationInBlocks} blocks, from previously frozen transaction with id: ${previousFreezeTransaction.id}`); const freezeScript = BitcoinClient.createFreezeScript(newFreezeDurationInBlocks, this.bitcoinWallet.getAddress()); const payToScriptHashOutput = Script.buildScriptHashOut(freezeScript); @@ -611,7 +611,7 @@ export default class BitcoinClient { previousFreezeTransaction: BitcoreTransactionWrapper, previousFreezeDurationInBlocks: number): Promise { - // tslint:disable-next-line: max-line-length + // eslint-disable-next-line max-len console.info(`Creating a transaction to return (to the wallet) the previously frozen amount from transaction with id: ${previousFreezeTransaction.id} which was frozen for block duration: ${previousFreezeDurationInBlocks}`); return this.createSpendTransactionFromFrozenTransaction( diff --git a/lib/bitcoin/BitcoinFileReader.ts b/lib/bitcoin/BitcoinFileReader.ts index 2b70e2193..193721c4d 100644 --- a/lib/bitcoin/BitcoinFileReader.ts +++ b/lib/bitcoin/BitcoinFileReader.ts @@ -1,7 +1,7 @@ +import * as fs from 'fs'; import ErrorCode from './ErrorCode'; import IBitcoinFileReader from './interfaces/IBitcoinFileReader'; import SidetreeError from '../common/SidetreeError'; -import * as fs from 'fs'; /** * concrete implementation of BitcoinFileReader diff --git a/lib/bitcoin/BitcoinProcessor.ts b/lib/bitcoin/BitcoinProcessor.ts index 77693540d..6d4eefab2 100644 --- a/lib/bitcoin/BitcoinProcessor.ts +++ b/lib/bitcoin/BitcoinProcessor.ts @@ -1,5 +1,6 @@ -import BitcoinBlockModel from './models/BitcoinBlockModel'; +import * as timeSpan from 'time-span'; import BitcoinBlockDataIterator from './BitcoinBlockDataIterator'; +import BitcoinBlockModel from './models/BitcoinBlockModel'; import BitcoinClient from './BitcoinClient'; import BitcoinServiceStateModel from './models/BitcoinServiceStateModel'; import BitcoinTransactionModel from './models/BitcoinTransactionModel'; @@ -18,8 +19,8 @@ import RequestError from './RequestError'; import ResponseStatus from '../common/enums/ResponseStatus'; import ServiceInfoProvider from '../common/ServiceInfoProvider'; import ServiceVersionModel from '../common/models/ServiceVersionModel'; -import SidetreeError from '../common/SidetreeError'; import SharedErrorCode from '../common/SharedErrorCode'; +import SidetreeError from '../common/SidetreeError'; import SidetreeTransactionParser from './SidetreeTransactionParser'; import SpendingMonitor from './SpendingMonitor'; import TransactionFeeModel from '../common/models/TransactionFeeModel'; @@ -29,8 +30,6 @@ import ValueTimeLockModel from '../common/models/ValueTimeLockModel'; import VersionManager from './VersionManager'; import VersionModel from '../common/models/VersionModel'; -import timeSpan = require('time-span'); - /** * Object representing a blockchain time and hash */ @@ -148,15 +147,16 @@ export default class BitcoinProcessor { const valueTimeLockTransactionFeesInBtc = config.valueTimeLockTransactionFeesAmountInBitcoins === 0 ? 0 : config.valueTimeLockTransactionFeesAmountInBitcoins || 0.25; - this.lockMonitor = - new LockMonitor( - this.bitcoinClient, - this.mongoDbLockTransactionStore, - this.lockResolver, - config.valueTimeLockPollPeriodInSeconds || 10 * 60, - BitcoinClient.convertBtcToSatoshis(config.valueTimeLockAmountInBitcoins), // Desired lock amount in satoshis - BitcoinClient.convertBtcToSatoshis(valueTimeLockTransactionFeesInBtc), // Txn Fees amount in satoshis - ProtocolParameters.maximumValueTimeLockDurationInBlocks); // Desired lock duration in blocks + this.lockMonitor = new LockMonitor( + this.bitcoinClient, + this.mongoDbLockTransactionStore, + this.lockResolver, + config.valueTimeLockPollPeriodInSeconds, + config.valueTimeLockUpdateEnabled, + BitcoinClient.convertBtcToSatoshis(config.valueTimeLockAmountInBitcoins), // Desired lock amount in satoshis + BitcoinClient.convertBtcToSatoshis(valueTimeLockTransactionFeesInBtc), // Txn Fees amount in satoshis + ProtocolParameters.maximumValueTimeLockDurationInBlocks // Desired lock duration in blocks + ); } /** @@ -194,7 +194,8 @@ export default class BitcoinProcessor { // NOTE: important to this initialization after we have processed all the blocks // this is because that the lock monitor needs the normalized fee calculator to // have all the data. - await this.lockMonitor.initialize(); + await this.lockMonitor.startPeriodicProcessing(); + void this.periodicPoll(); } @@ -563,11 +564,11 @@ export default class BitcoinProcessor { /** * Gets the lock information which is currently held by this node. It throws an RequestError if none exist. */ - public getActiveValueTimeLockForThisNode (): ValueTimeLockModel { + public async getActiveValueTimeLockForThisNode (): Promise { let currentLock: ValueTimeLockModel | undefined; try { - currentLock = this.lockMonitor.getCurrentValueTimeLock(); + currentLock = await this.lockMonitor.getCurrentValueTimeLock(); } catch (e) { if (e instanceof SidetreeError && e.code === ErrorCode.LockMonitorCurrentValueTimeLockInPendingState) { diff --git a/lib/bitcoin/BitcoinRawDataParser.ts b/lib/bitcoin/BitcoinRawDataParser.ts index 820f663ae..57228780c 100644 --- a/lib/bitcoin/BitcoinRawDataParser.ts +++ b/lib/bitcoin/BitcoinRawDataParser.ts @@ -1,8 +1,8 @@ import BitcoinBlockModel from './models/BitcoinBlockModel'; import BitcoinClient from './BitcoinClient'; +import { Block } from 'bitcore-lib'; import ErrorCode from './ErrorCode'; import SidetreeError from '../common/SidetreeError'; -import { Block } from 'bitcore-lib'; /** * Parser for raw bitcoin block data diff --git a/lib/bitcoin/BitcoinWallet.ts b/lib/bitcoin/BitcoinWallet.ts index cad5f78b0..c1610e1d4 100644 --- a/lib/bitcoin/BitcoinWallet.ts +++ b/lib/bitcoin/BitcoinWallet.ts @@ -1,7 +1,7 @@ -import IBitcoinWallet from './interfaces/IBitcoinWallet'; +import { Address, PrivateKey, Script, Transaction } from 'bitcore-lib'; import ErrorCode from './ErrorCode'; +import IBitcoinWallet from './interfaces/IBitcoinWallet'; import SidetreeError from '../common/SidetreeError'; -import { Address, PrivateKey, Script, Transaction } from 'bitcore-lib'; /** * Represents a bitcoin wallet. diff --git a/lib/bitcoin/IBitcoinConfig.ts b/lib/bitcoin/IBitcoinConfig.ts index ac97792c6..05edbb8cd 100644 --- a/lib/bitcoin/IBitcoinConfig.ts +++ b/lib/bitcoin/IBitcoinConfig.ts @@ -11,17 +11,18 @@ export default interface IBitcoinConfig { bitcoinRpcUsername: string | undefined; bitcoinRpcPassword: string | undefined; bitcoinWalletOrImportString: IBitcoinWallet | string; + databaseName: string; defaultTransactionFeeInSatoshisPerKB: number | undefined; - lowBalanceNoticeInDays: number | undefined; - sidetreeTransactionPrefix: string; genesisBlockNumber: number; + lowBalanceNoticeInDays: number | undefined; mongoDbConnectionString: string; - databaseName: string; requestTimeoutInMilliseconds: number | undefined; requestMaxRetries: number | undefined; - transactionPollPeriodInSeconds: number | undefined; sidetreeTransactionFeeMarkupPercentage: number; + sidetreeTransactionPrefix: string; + transactionPollPeriodInSeconds: number | undefined; + valueTimeLockUpdateEnabled: boolean; valueTimeLockAmountInBitcoins: number; - valueTimeLockPollPeriodInSeconds: number | undefined; + valueTimeLockPollPeriodInSeconds: number; valueTimeLockTransactionFeesAmountInBitcoins: number | undefined; } diff --git a/lib/bitcoin/MongoDbBlockMetadataStore.ts b/lib/bitcoin/MongoDbBlockMetadataStore.ts index e56fe5e98..ae8623088 100644 --- a/lib/bitcoin/MongoDbBlockMetadataStore.ts +++ b/lib/bitcoin/MongoDbBlockMetadataStore.ts @@ -1,7 +1,7 @@ +import { Collection, Cursor } from 'mongodb'; import BlockMetadata from './models/BlockMetadata'; import IBlockMetadataStore from './interfaces/IBlockMetadataStore'; import MongoDbStore from '../common/MongoDbStore'; -import { Collection, Cursor } from 'mongodb'; /** * Implementation of IBlockMetadataStore using MongoDB database. @@ -108,7 +108,7 @@ export default class MongoDbBlockMetadataStore extends MongoDbStore implements I } const exponentiallySpacedBlocks = await this.collection!.find( - { height : { $in : heightOfBlocksToReturn } }, + { height: { $in: heightOfBlocksToReturn } }, MongoDbBlockMetadataStore.optionToExcludeIdField ).toArray(); exponentiallySpacedBlocks.sort((a, b) => b.height - a.height); // Sort in height descending order. diff --git a/lib/bitcoin/SpendingMonitor.ts b/lib/bitcoin/SpendingMonitor.ts index 3d8f3fcc0..969be71d1 100644 --- a/lib/bitcoin/SpendingMonitor.ts +++ b/lib/bitcoin/SpendingMonitor.ts @@ -69,7 +69,7 @@ export default class SpendingMonitor { const allTxnsSinceStartingBlock = await this.transactionStore.getTransactionsLaterThan(startingBlockFirstTxnNumber - 1, undefined); - // tslint:disable-next-line: max-line-length + // eslint-disable-next-line max-len console.info(`SpendingMonitor: total number of transactions from the transaction store starting from block: ${startingBlockHeight} are: ${allTxnsSinceStartingBlock.length}`); // Since the transactions from the store include transactions written by ALL the nodes in the network, @@ -84,7 +84,7 @@ export default class SpendingMonitor { const totalFeePlusCurrentFee = totalFeeForRelatedTxns + currentFeeInSatoshis; if (totalFeePlusCurrentFee > this.bitcoinFeeSpendingCutoffInSatoshis) { - // tslint:disable-next-line: max-line-length + // eslint-disable-next-line max-len console.error(`Current fee (in satoshis): ${currentFeeInSatoshis} + total fees (${totalFeeForRelatedTxns}) since block number: ${startingBlockHeight} is greater than the spending cap: ${this.bitcoinFeeSpendingCutoffInSatoshis}`); return false; } diff --git a/lib/bitcoin/lock/LockIdentifierSerializer.ts b/lib/bitcoin/lock/LockIdentifierSerializer.ts index e1a6b9531..7313ea464 100644 --- a/lib/bitcoin/lock/LockIdentifierSerializer.ts +++ b/lib/bitcoin/lock/LockIdentifierSerializer.ts @@ -1,7 +1,7 @@ -import base64url from 'base64url'; import ErrorCode from '../ErrorCode'; import LockIdentifierModel from '../models/LockIdentifierModel'; import SidetreeError from '../../common/SidetreeError'; +import base64url from 'base64url'; /** * Encapsulates functionality to serialize and deserialize a lock identifier. diff --git a/lib/bitcoin/lock/LockMonitor.ts b/lib/bitcoin/lock/LockMonitor.ts index 77d7747ad..cd64e32ac 100644 --- a/lib/bitcoin/lock/LockMonitor.ts +++ b/lib/bitcoin/lock/LockMonitor.ts @@ -4,6 +4,7 @@ import ErrorCode from '../ErrorCode'; import LockIdentifier from '../models/LockIdentifierModel'; import LockIdentifierSerializer from './LockIdentifierSerializer'; import LockResolver from './LockResolver'; +import LogColor from '../../common/LogColor'; import MongoDbLockTransactionStore from './MongoDbLockTransactionStore'; import SavedLockModel from '../models/SavedLockedModel'; import SavedLockType from '../enums/SavedLockType'; @@ -31,17 +32,19 @@ interface LockState { * Encapsulates functionality to monitor and create/remove amount locks on bitcoin. */ export default class LockMonitor { - private initialized: boolean; - private periodicPollTimeoutId: NodeJS.Timeout | undefined; - private currentLockState: LockState; - + /** + * Constructor for LockMonitor. + * @param valueTimeLockUpdateEnabled When this parameter is set to `false`, parameters `lockPeriodInBlocks`, + * `transactionFeesAmountInSatoshis` and `desiredLockAmountInSatoshis` will be ignored. + */ constructor ( private bitcoinClient: BitcoinClient, private lockTransactionStore: MongoDbLockTransactionStore, private lockResolver: LockResolver, private pollPeriodInSeconds: number, + private valueTimeLockUpdateEnabled: boolean, private desiredLockAmountInSatoshis: number, private transactionFeesAmountInSatoshis: number, private lockPeriodInBlocks: number) { @@ -53,34 +56,21 @@ export default class LockMonitor { if (!Number.isInteger(transactionFeesAmountInSatoshis)) { throw new SidetreeError(ErrorCode.LockMonitorTransactionFeesAmountIsNotWholeNumber, `${transactionFeesAmountInSatoshis}`); } - - this.currentLockState = { - activeValueTimeLock: undefined, - latestSavedLockInfo: undefined, - status: LockStatus.None - }; - - this.initialized = false; } /** - * Initializes this object by performing the periodic poll tasks. + * Starts the periodic reading and updating of lock status. */ - public async initialize (): Promise { - this.currentLockState = await this.getCurrentLockState(); - + public async startPeriodicProcessing (): Promise { await this.periodicPoll(); - this.initialized = true; } /** * Gets the current lock information if exist; undefined otherwise. Throws an error * if the lock information is not confirmed on the blockchain. */ - public getCurrentValueTimeLock (): ValueTimeLockModel | undefined { - - // Make a copy of the state so in case it gets changed between now and the function return - const currentLockState = Object.assign({}, this.currentLockState); + public async getCurrentValueTimeLock (): Promise { + const currentLockState = await this.getCurrentLockState(); // If there's no lock then return undefined if (currentLockState.status === LockStatus.None) { @@ -109,12 +99,6 @@ export default class LockMonitor { } catch (e) { const message = `An error occurred during periodic poll: ${SidetreeError.stringify(e)}`; console.error(message); - - // Rethrow if the error is in the initialization phase. We don't want to continue with the - // service during initialization. - if (!this.initialized) { - throw e; - } } finally { this.periodicPollTimeoutId = setTimeout(this.periodicPoll.bind(this), 1000 * this.pollPeriodInSeconds); } @@ -123,45 +107,46 @@ export default class LockMonitor { } private async handlePeriodicPolling (): Promise { + const currentLockState = await this.getCurrentLockState(); + console.info(`Refreshed the in-memory value time lock state.`); - // If the current lock is in pending state then we cannot do anything and need to just return. - if (this.currentLockState.status === LockStatus.Pending) { - console.info(`The current lock status is in pending state; going to skip rest of the routine.`); + // If lock update is disabled, then no further action needs to be taken. + if (this.valueTimeLockUpdateEnabled === false) { + console.info(`Value time lock update is disabled, will not attempt to update the value time lock.`); + return; + } - // But refresh the lock state before returning so that the next polling has the new value. - this.currentLockState = await this.getCurrentLockState(); + // If the current lock is in pending state then we cannot do anything other than rebroadcast the transaction again. + if (currentLockState.status === LockStatus.Pending) { + console.info(`The current lock status is in pending state, rebroadcast the transaction again in case the transaction is lost in the previous broadcast.`); + await this.rebroadcastTransaction(currentLockState.latestSavedLockInfo!); return; } // Now that we are not pending, check what do we have to do about the lock next. - const validCurrentLockExist = this.currentLockState.status === LockStatus.Confirmed; + const validCurrentLockExist = currentLockState.status === LockStatus.Confirmed; const lockRequired = this.desiredLockAmountInSatoshis > 0; - let currentLockUpdated = false; - if (lockRequired && !validCurrentLockExist) { await this.handleCreatingNewLock(this.desiredLockAmountInSatoshis); - currentLockUpdated = true; } if (lockRequired && validCurrentLockExist) { // The routine will true only if there were any changes made to the lock - currentLockUpdated = - await this.handleExistingLockRenewal( - this.currentLockState.activeValueTimeLock!, - this.currentLockState.latestSavedLockInfo!, - this.desiredLockAmountInSatoshis); + await this.handleExistingLockRenewal( + currentLockState.activeValueTimeLock!, + currentLockState.latestSavedLockInfo!, + this.desiredLockAmountInSatoshis + ); } if (!lockRequired && validCurrentLockExist) { - currentLockUpdated = - await this.handleReleaseExistingLock( - this.currentLockState.activeValueTimeLock!, - this.desiredLockAmountInSatoshis); - } + console.info(LogColor.lightBlue(`Value time lock no longer needed.`)); - if (currentLockUpdated) { - this.currentLockState = await this.getCurrentLockState(); + await this.handleReleaseExistingLock( + currentLockState.activeValueTimeLock!, + this.desiredLockAmountInSatoshis + ); } } @@ -184,9 +169,6 @@ export default class LockMonitor { // if it is not as we don't want to do anything until last lock information is at least // broadcasted. if (!(await this.isTransactionBroadcasted(lastSavedLock.transactionId))) { - - await this.rebroadcastTransaction(lastSavedLock); - return { activeValueTimeLock: undefined, latestSavedLockInfo: lastSavedLock, @@ -284,7 +266,8 @@ export default class LockMonitor { `Lock amount: ${totalLockAmount}; Wallet balance: ${walletBalance}`); } - console.info(`Going to create a new lock for amount: ${totalLockAmount} satoshis. Current wallet balance: ${walletBalance}`); + console.info(LogColor.lightBlue(`Current wallet balance: ${LogColor.green(walletBalance)}`)); + console.info(LogColor.lightBlue(`Creating a new lock for amount: ${LogColor.green(totalLockAmount)} satoshis.`)); const lockTransaction = await this.bitcoinClient.createLockTransaction(totalLockAmount, this.lockPeriodInBlocks); @@ -306,15 +289,15 @@ export default class LockMonitor { desiredLockAmountInSatoshis: number): Promise { // Just return if we haven't reached the unlock block yet - if (! (await this.isUnlockTimeReached(currentValueTimeLock.unlockTransactionTime))) { + if (!(await this.isUnlockTimeReached(currentValueTimeLock.unlockTransactionTime))) { return false; } - // If the desired lock amount is different from prevoius then just return the amount to + // If the desired lock amount is different from previous then just return the amount to // the wallet and let the next poll iteration start a new lock. if (latestSavedLockInfo.desiredLockAmountInSatoshis !== desiredLockAmountInSatoshis) { - // tslint:disable-next-line: max-line-length - console.info(`Current desired lock amount ${desiredLockAmountInSatoshis} satoshis is different from the previous desired lock amount ${latestSavedLockInfo.desiredLockAmountInSatoshis} satoshis. Going to release the lock.`); + console.info(LogColor.lightBlue(`Current desired lock amount ${LogColor.green(desiredLockAmountInSatoshis)} satoshis is different from the previous `) + + LogColor.lightBlue(`desired lock amount ${LogColor.green(latestSavedLockInfo.desiredLockAmountInSatoshis)} satoshis. Going to release the lock.`)); await this.releaseLock(currentValueTimeLock, desiredLockAmountInSatoshis); return true; @@ -322,14 +305,13 @@ export default class LockMonitor { // If we have gotten to here then we need to try renew. try { - await this.renewLock(currentValueTimeLock, desiredLockAmountInSatoshis); } catch (e) { // If there is not enough balance for the relock then just release the lock. Let the next // iteration of the polling to try and create a new lock. if (e instanceof SidetreeError && e.code === ErrorCode.LockMonitorNotEnoughBalanceForRelock) { - console.warn(`There is not enough balance for relocking so going to release the lock. Error: ${e.message}`); + console.warn(LogColor.yellow(`There is not enough balance for relocking so going to release the lock. Error: ${e.message}`)); await this.releaseLock(currentValueTimeLock, desiredLockAmountInSatoshis); } else { // This is an unexpected error at this point ... rethrow as this is needed to be investigated. @@ -350,13 +332,17 @@ export default class LockMonitor { */ private async handleReleaseExistingLock (currentValueTimeLock: ValueTimeLockModel, desiredLockAmountInSatoshis: number): Promise { - // Don't continue unless the current locktime model is actually reached - if (! (await this.isUnlockTimeReached(currentValueTimeLock.unlockTransactionTime))) { + // Don't continue unless the current lock time model is actually reached + if (!(await this.isUnlockTimeReached(currentValueTimeLock.unlockTransactionTime))) { return false; } + console.info(LogColor.lightBlue(`Value time lock no longer needed and unlock time reached, releasing lock...`)); + await this.releaseLock(currentValueTimeLock, desiredLockAmountInSatoshis); + console.info(LogColor.lightBlue(`Value time lock released.`)); + return true; } @@ -375,7 +361,7 @@ export default class LockMonitor { if (currentValueTimeLock.amountLocked - relockTransaction.transactionFee < desiredLockAmountInSatoshis) { throw new SidetreeError( ErrorCode.LockMonitorNotEnoughBalanceForRelock, - // tslint:disable-next-line: max-line-length + // eslint-disable-next-line max-len `The current locked amount (${currentValueTimeLock.amountLocked} satoshis) minus the relocking fee (${relockTransaction.transactionFee} satoshis) is causing the relock amount to go below the desired lock amount: ${desiredLockAmountInSatoshis}`); } diff --git a/lib/bitcoin/lock/LockResolver.ts b/lib/bitcoin/lock/LockResolver.ts index c1ac03c8f..cbbc705e3 100644 --- a/lib/bitcoin/lock/LockResolver.ts +++ b/lib/bitcoin/lock/LockResolver.ts @@ -1,13 +1,13 @@ import BitcoinClient from '../BitcoinClient'; -import BitcoinTransactionModel from '../models/BitcoinTransactionModel'; import BitcoinOutputModel from '../models/BitcoinOutputModel'; +import BitcoinTransactionModel from '../models/BitcoinTransactionModel'; import ErrorCode from '../ErrorCode'; import LockIdentifierModel from '../models/LockIdentifierModel'; import LockIdentifierSerializer from './LockIdentifierSerializer'; +import { Script } from 'bitcore-lib'; import SidetreeError from '../../common/SidetreeError'; import ValueTimeLockModel from '../../common/models/ValueTimeLockModel'; import VersionManager from '../VersionManager'; -import { Script } from 'bitcore-lib'; /** Structure (internal for this class) to hold the redeem script verification results */ interface LockScriptVerifyResult { @@ -88,9 +88,11 @@ export default class LockResolver { const unlockAtBlock = lockStartBlock + scriptVerifyResult.lockDurationInBlocks!; if (!this.isLockDurationValid(lockStartBlock, unlockAtBlock)) { - throw new SidetreeError(ErrorCode.LockResolverDurationIsInvalid, - // tslint:disable-next-line: max-line-length - `Lock start block: ${lockStartBlock}. Unlock block: ${unlockAtBlock}. Allowed range: [${this.minimumLockDurationInBlocks} - ${this.maximumLockDurationInBlocks}.]`); + throw new SidetreeError( + ErrorCode.LockResolverDurationIsInvalid, + // eslint-disable-next-line max-len + `Lock start block: ${lockStartBlock}. Unlock block: ${unlockAtBlock}. Allowed range: [${this.minimumLockDurationInBlocks} - ${this.maximumLockDurationInBlocks}.]` + ); } const normalizedFee = this.versionManager.getFeeCalculator(lockStartBlock).getNormalizedFee(lockStartBlock); diff --git a/lib/bitcoin/lock/MongoDbLockTransactionStore.ts b/lib/bitcoin/lock/MongoDbLockTransactionStore.ts index 6b1ff595c..8274114bb 100644 --- a/lib/bitcoin/lock/MongoDbLockTransactionStore.ts +++ b/lib/bitcoin/lock/MongoDbLockTransactionStore.ts @@ -1,5 +1,5 @@ -import SavedLockModel from './../models/SavedLockedModel'; import { Collection, Db, Long, MongoClient } from 'mongodb'; +import SavedLockModel from './../models/SavedLockedModel'; /** * Encapsulates functionality to store the bitcoin lock information to Db. @@ -39,7 +39,7 @@ export default class MongoDbLockTransactionStore { */ public async addLock (bitcoinLock: SavedLockModel): Promise { const lockInMongoDb = { - desiredLockAmountInSatoshis : bitcoinLock.desiredLockAmountInSatoshis, + desiredLockAmountInSatoshis: bitcoinLock.desiredLockAmountInSatoshis, transactionId: bitcoinLock.transactionId, rawTransaction: bitcoinLock.rawTransaction, redeemScriptAsHex: bitcoinLock.redeemScriptAsHex, diff --git a/tests/bitcoin/BitcoinClient.spec.ts b/tests/bitcoin/BitcoinClient.spec.ts index e852bb792..741a7a72b 100644 --- a/tests/bitcoin/BitcoinClient.spec.ts +++ b/tests/bitcoin/BitcoinClient.spec.ts @@ -1,13 +1,13 @@ import * as httpStatus from 'http-status'; import * as nodeFetchPackage from 'node-fetch'; -import BitcoinDataGenerator from './BitcoinDataGenerator'; +import { Address, PrivateKey, Script, Transaction } from 'bitcore-lib'; import BitcoinClient from '../../lib/bitcoin/BitcoinClient'; +import BitcoinDataGenerator from './BitcoinDataGenerator'; import BitcoinLockTransactionModel from '../../lib/bitcoin/models/BitcoinLockTransactionModel'; import BitcoinTransactionModel from '../../lib/bitcoin/models/BitcoinTransactionModel'; import BitcoinWallet from '../../lib/bitcoin/BitcoinWallet'; import IBitcoinWallet from '../../lib/bitcoin/interfaces/IBitcoinWallet'; import ReadableStream from '../../lib/common/ReadableStream'; -import { Address, PrivateKey, Script, Transaction } from 'bitcore-lib'; describe('BitcoinClient', async () => { diff --git a/tests/bitcoin/BitcoinDataGenerator.ts b/tests/bitcoin/BitcoinDataGenerator.ts index 182d79d46..283c61090 100644 --- a/tests/bitcoin/BitcoinDataGenerator.ts +++ b/tests/bitcoin/BitcoinDataGenerator.ts @@ -1,6 +1,6 @@ +import { PrivateKey, Transaction } from 'bitcore-lib'; import BitcoinBlockModel from '../../lib/bitcoin/models/BitcoinBlockModel'; import BitcoinClient from '../../lib/bitcoin/BitcoinClient'; -import { PrivateKey, Transaction } from 'bitcore-lib'; /** * Encapsulates the functions that help with generating the test data for the Bitcoin blockchain. diff --git a/tests/bitcoin/BitcoinProcessor.spec.ts b/tests/bitcoin/BitcoinProcessor.spec.ts index 07b31943d..719b6333a 100644 --- a/tests/bitcoin/BitcoinProcessor.spec.ts +++ b/tests/bitcoin/BitcoinProcessor.spec.ts @@ -1,9 +1,9 @@ import * as fs from 'fs'; import * as httpStatus from 'http-status'; +import BitcoinProcessor, { IBlockInfo } from '../../lib/bitcoin/BitcoinProcessor'; import BitcoinBlockModel from '../../lib/bitcoin/models/BitcoinBlockModel'; import BitcoinClient from '../../lib/bitcoin/BitcoinClient'; import BitcoinDataGenerator from './BitcoinDataGenerator'; -import BitcoinProcessor, { IBlockInfo } from '../../lib/bitcoin/BitcoinProcessor'; import BitcoinRawDataParser from '../../lib/bitcoin/BitcoinRawDataParser'; import BitcoinTransactionModel from '../../lib/bitcoin/models/BitcoinTransactionModel'; import BlockMetadata from '../../lib/bitcoin/models/BlockMetadata'; @@ -58,6 +58,7 @@ describe('BitcoinProcessor', () => { sidetreeTransactionPrefix: 'sidetree:', sidetreeTransactionFeeMarkupPercentage: 0, transactionPollPeriodInSeconds: 60, + valueTimeLockUpdateEnabled: true, valueTimeLockPollPeriodInSeconds: 60, valueTimeLockAmountInBitcoins: 1, valueTimeLockTransactionFeesAmountInBitcoins: undefined @@ -90,7 +91,7 @@ describe('BitcoinProcessor', () => { transactionStoreInitializeSpy = spyOn(bitcoinProcessor['transactionStore'], 'initialize'); bitcoinClientInitializeSpy = spyOn(bitcoinProcessor['bitcoinClient'], 'initialize'); mongoLockTxnStoreSpy = spyOn(bitcoinProcessor['mongoDbLockTransactionStore'], 'initialize'); - lockMonitorSpy = spyOn(bitcoinProcessor['lockMonitor'], 'initialize'); + lockMonitorSpy = spyOn(bitcoinProcessor['lockMonitor']!, 'startPeriodicProcessing'); blockMetadataStoreAddSpy = spyOn(bitcoinProcessor['blockMetadataStore'], 'add'); blockMetadataStoreGetLastSpy = spyOn(bitcoinProcessor['blockMetadataStore'], 'getLast'); @@ -154,6 +155,7 @@ describe('BitcoinProcessor', () => { requestMaxRetries: undefined, transactionPollPeriodInSeconds: undefined, sidetreeTransactionFeeMarkupPercentage: 0, + valueTimeLockUpdateEnabled: true, valueTimeLockPollPeriodInSeconds: 60, valueTimeLockAmountInBitcoins: 1, valueTimeLockTransactionFeesAmountInBitcoins: undefined @@ -863,9 +865,9 @@ describe('BitcoinProcessor', () => { it('should process as intended', async () => { const processSidetreeTransactionsInBlockSpy = spyOn(bitcoinProcessor, 'processSidetreeTransactionsInBlock' as any); const blockData: any[] = [ - { hash: 'abc', height: 2, previousHash: 'def', transactions: [ { outputs: [{ satoshis: 100 }, { satoshis: 5000000000 }, { satoshis: 50 }] }] }, - { hash: 'def', height: 1, previousHash: 'out of range', transactions: [ { outputs: [{ satoshis: 5000000000 }] }] }, - { hash: 'ghi', height: 4, previousHash: 'out of range', transactions: [ { outputs: [{ satoshis: 5000000000 }] }] } + { hash: 'abc', height: 2, previousHash: 'def', transactions: [{ outputs: [{ satoshis: 100 }, { satoshis: 5000000000 }, { satoshis: 50 }] }] }, + { hash: 'def', height: 1, previousHash: 'out of range', transactions: [{ outputs: [{ satoshis: 5000000000 }] }] }, + { hash: 'ghi', height: 4, previousHash: 'out of range', transactions: [{ outputs: [{ satoshis: 5000000000 }] }] } ]; const notYetValidatedBlocks: Map = new Map(); const startingHeight = 2; @@ -1291,7 +1293,7 @@ describe('BitcoinProcessor', () => { let getSidetreeTxnCallIndex = 0; spyOn(bitcoinProcessor as any, 'getSidetreeTransactionModelIfExist').and.callFake(() => { - let retValue: TransactionModel | undefined = undefined; + let retValue: TransactionModel | undefined; if (getSidetreeTxnCallIndex < mockSidetreeTxnModels.length) { retValue = mockSidetreeTxnModels[getSidetreeTxnCallIndex]; @@ -1523,7 +1525,7 @@ describe('BitcoinProcessor', () => { }); describe('getActiveValueTimeLockForThisNode', () => { - it('should return the value-time-lock from the lockmonitor', () => { + it('should return the value-time-lock from the lock monitor', async () => { const mockValueTimeLock: ValueTimeLockModel = { amountLocked: 1000, identifier: 'lock identifier', @@ -1533,17 +1535,17 @@ describe('BitcoinProcessor', () => { lockTransactionTime: 1220 }; - spyOn(bitcoinProcessor['lockMonitor'], 'getCurrentValueTimeLock').and.returnValue(mockValueTimeLock); + spyOn(bitcoinProcessor['lockMonitor']!, 'getCurrentValueTimeLock').and.returnValue(Promise.resolve(mockValueTimeLock)); - const actual = bitcoinProcessor.getActiveValueTimeLockForThisNode(); + const actual = await bitcoinProcessor.getActiveValueTimeLockForThisNode(); expect(actual).toEqual(mockValueTimeLock); }); - it('should throw not-found error if the lock monitor returns undefined.', () => { - spyOn(bitcoinProcessor['lockMonitor'], 'getCurrentValueTimeLock').and.returnValue(undefined); + it('should throw not-found error if the lock monitor returns undefined.', async () => { + spyOn(bitcoinProcessor['lockMonitor']!, 'getCurrentValueTimeLock').and.returnValue(Promise.resolve(undefined)); try { - bitcoinProcessor.getActiveValueTimeLockForThisNode(); + await bitcoinProcessor.getActiveValueTimeLockForThisNode(); fail('Expected exception is not thrown'); } catch (e) { const expectedError = new RequestError(ResponseStatus.NotFound, SharedErrorCode.ValueTimeLockNotFound); @@ -1551,13 +1553,13 @@ describe('BitcoinProcessor', () => { } }); - it('should throw pending-state exception if the lock monitor throws pending-state error', () => { - spyOn(bitcoinProcessor['lockMonitor'], 'getCurrentValueTimeLock').and.callFake(() => { + it('should throw pending-state exception if the lock monitor throws pending-state error', async () => { + spyOn(bitcoinProcessor['lockMonitor']!, 'getCurrentValueTimeLock').and.callFake(() => { throw new SidetreeError(ErrorCode.LockMonitorCurrentValueTimeLockInPendingState); }); try { - bitcoinProcessor.getActiveValueTimeLockForThisNode(); + await bitcoinProcessor.getActiveValueTimeLockForThisNode(); fail('Expected exception is not thrown'); } catch (e) { const expectedError = new RequestError(ResponseStatus.NotFound, ErrorCode.ValueTimeLockInPendingState); @@ -1565,11 +1567,11 @@ describe('BitcoinProcessor', () => { } }); - it('should bubble up any other errors.', () => { - spyOn(bitcoinProcessor['lockMonitor'], 'getCurrentValueTimeLock').and.throwError('no lock found.'); + it('should bubble up any other errors.', async () => { + spyOn(bitcoinProcessor['lockMonitor']!, 'getCurrentValueTimeLock').and.throwError('no lock found.'); try { - bitcoinProcessor.getActiveValueTimeLockForThisNode(); + await bitcoinProcessor.getActiveValueTimeLockForThisNode(); fail('Expected exception is not thrown'); } catch (e) { const expectedError = new RequestError(ResponseStatus.ServerError); diff --git a/tests/bitcoin/BitcoinRawDataParser.spec.ts b/tests/bitcoin/BitcoinRawDataParser.spec.ts index 3dc254936..62b017b67 100644 --- a/tests/bitcoin/BitcoinRawDataParser.spec.ts +++ b/tests/bitcoin/BitcoinRawDataParser.spec.ts @@ -1,7 +1,7 @@ +import * as fs from 'fs'; import BitcoinRawDataParser from '../../lib/bitcoin/BitcoinRawDataParser'; import ErrorCode from '../../lib/bitcoin/ErrorCode'; import SidetreeError from '../../lib/common/SidetreeError'; -import * as fs from 'fs'; describe('BitcoinRawDataParser', () => { describe('parseRawDataFile', () => { diff --git a/tests/bitcoin/SidetreeTransactionParser.spec.ts b/tests/bitcoin/SidetreeTransactionParser.spec.ts index 3917330e2..bdcabfbd7 100644 --- a/tests/bitcoin/SidetreeTransactionParser.spec.ts +++ b/tests/bitcoin/SidetreeTransactionParser.spec.ts @@ -158,7 +158,7 @@ describe('SidetreeTransactionParser', () => { }); it('should return undefined if no valid sidetree transaction exist', async (done) => { - const mockOutput: BitcoinOutputModel = { satoshis: 0, scriptAsmAsString: 'some random data' }; + const mockOutput: BitcoinOutputModel = { satoshis: 0, scriptAsmAsString: 'some random data' }; const actual = sidetreeTxnParser['getSidetreeDataFromOutputIfExist'](mockOutput, sidetreeTransactionPrefix); expect(actual).not.toBeDefined(); diff --git a/tests/bitcoin/SpendingMonitor.spec.ts b/tests/bitcoin/SpendingMonitor.spec.ts index 8342308bd..a6f212f5a 100644 --- a/tests/bitcoin/SpendingMonitor.spec.ts +++ b/tests/bitcoin/SpendingMonitor.spec.ts @@ -22,7 +22,7 @@ describe('SpendingMonitor', () => { describe('constructor', () => { it('should throw if the cutoff period is not in the correct range.', async (done) => { try { - // tslint:disable-next-line: no-unused-expression + // eslint-disable-next-line no-new new SpendingMonitor(0, bitcoinFeeSpendingCutoffInSatoshis, new MockTransactionStore()); fail('Expected exception not thrown'); } catch (e) { @@ -33,7 +33,7 @@ describe('SpendingMonitor', () => { it('should throw if the cutoff amount is not in the correct range.', async (done) => { try { - // tslint:disable-next-line: no-unused-expression + // eslint-disable-next-line no-new new SpendingMonitor(1, 0, new MockTransactionStore()); fail('Expected exception not thrown'); } catch (e) { diff --git a/tests/bitcoin/lock/LockMonitor.spec.ts b/tests/bitcoin/lock/LockMonitor.spec.ts index 967979a49..6e12c4e4a 100644 --- a/tests/bitcoin/lock/LockMonitor.spec.ts +++ b/tests/bitcoin/lock/LockMonitor.spec.ts @@ -8,8 +8,8 @@ import LockIdentifierSerializer from '../../../lib/bitcoin/lock/LockIdentifierSe import LockMonitor from '../../../lib/bitcoin/lock/LockMonitor'; import LockResolver from '../../../lib/bitcoin/lock/LockResolver'; import MongoDbLockTransactionStore from '../../../lib/bitcoin/lock/MongoDbLockTransactionStore'; -import SavedLockedModel from '../../../lib/bitcoin/models/SavedLockedModel'; import SavedLockType from '../../../lib/bitcoin/enums/SavedLockType'; +import SavedLockedModel from '../../../lib/bitcoin/models/SavedLockedModel'; import SidetreeError from '../../../lib/common/SidetreeError'; import ValueTimeLockModel from '../../../lib/common/models/ValueTimeLockModel'; import VersionManager from '../../../lib/bitcoin/VersionManager'; @@ -38,41 +38,30 @@ describe('LockMonitor', () => { let lockMonitor: LockMonitor; beforeEach(() => { - lockMonitor = new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 60, 1200, 100, 2000); - lockMonitor['initialized'] = true; + lockMonitor = new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 60, true, 1200, 100, 2000); }); describe('constructor', () => { it('should throw if the desired lock amount is not a whole number', () => { JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( - () => new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 10, 1000.34, 25, 1234), + () => new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 10, true, 1000.34, 25, 1234), ErrorCode.LockMonitorDesiredLockAmountIsNotWholeNumber); }); it('should throw if the txn fees amount is not a whole number', () => { JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( - () => new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 10, 1000, 1234.56, 45), + () => new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 10, true, 1000, 1234.56, 45), ErrorCode.LockMonitorTransactionFeesAmountIsNotWholeNumber); }); - - it('should set the initialized flag to false', () => { - const monitor = new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 10, 1000, 1200, 45); - expect(monitor['initialized']).toBeFalsy(); - }); }); - describe('initialize', () => { + describe('startPeriodicProcessing', () => { it('should call the periodic poll function', async () => { - const mockLockInfo = createLockState(undefined, undefined, 'none'); - const resolveSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockLockInfo)); const pollSpy = spyOn(lockMonitor as any, 'periodicPoll').and.returnValue(Promise.resolve()); - lockMonitor['initialized'] = false; - await lockMonitor.initialize(); + await lockMonitor.startPeriodicProcessing(); - expect(resolveSpy).toHaveBeenCalledBefore(pollSpy); expect(pollSpy).toHaveBeenCalled(); - expect(lockMonitor['initialized']).toBeTruthy(); }); }); @@ -81,10 +70,10 @@ describe('LockMonitor', () => { const clearTimeoutSpy = spyOn(global, 'clearTimeout').and.returnValue(); const handlePollingSpy = spyOn(lockMonitor as any, 'handlePeriodicPolling').and.returnValue(Promise.resolve()); - const setTimeoutOutput: NodeJS.Timeout = 12344 as any; + const setTimeoutOutput = 12344 as any; const setTimeoutSpy = spyOn(global, 'setTimeout').and.returnValue(setTimeoutOutput as any); - const mockPeriodicPollTimeoutId: NodeJS.Timeout = 98765 as any; + const mockPeriodicPollTimeoutId = 98765 as any; lockMonitor['periodicPollTimeoutId'] = mockPeriodicPollTimeoutId; await lockMonitor['periodicPoll'](); @@ -95,25 +84,6 @@ describe('LockMonitor', () => { expect(lockMonitor['periodicPollTimeoutId']).toEqual(setTimeoutOutput); }); - it('should rethrow if the initialize flag is not true', async (done) => { - const mockErrorCode = 'error during initialization'; - spyOn(lockMonitor as any, 'handlePeriodicPolling').and.callFake(() => { - throw new SidetreeError(mockErrorCode); - }); - - const setTimeoutSpy = spyOn(global, 'setTimeout').and.returnValue(123456 as any); - - lockMonitor['periodicPollTimeoutId'] = undefined; - lockMonitor['initialized'] = false; - - await JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrownAsync( - () => lockMonitor['periodicPoll'](), - mockErrorCode); - - expect(setTimeoutSpy).toHaveBeenCalled(); - done(); - }); - it('should call setTimeout() at the end of the execution even if an exception is thrown.', async () => { const handlePollingSpy = spyOn(lockMonitor as any, 'handlePeriodicPolling').and.throwError('unhandled exception'); @@ -121,7 +91,6 @@ describe('LockMonitor', () => { const setTimeoutSpy = spyOn(global, 'setTimeout').and.returnValue(setTimeoutOutput as any); lockMonitor['periodicPollTimeoutId'] = undefined; - lockMonitor['initialized'] = true; await lockMonitor['periodicPoll'](); expect(handlePollingSpy).toHaveBeenCalled(); @@ -130,22 +99,24 @@ describe('LockMonitor', () => { }); describe('getCurrentValueTimeLock', () => { - it('should return undefined if there is no current lock', () => { - lockMonitor['currentLockState'] = createLockState(undefined, undefined, 'none'); + it('should return undefined if there is no current lock', async () => { + const mockCurrentLockState = createLockState(undefined, undefined, 'none'); + spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockState)); - const actual = lockMonitor.getCurrentValueTimeLock(); + const actual = await lockMonitor.getCurrentValueTimeLock(); expect(actual).toBeUndefined(); }); it('should throw if the current lock status is pending', () => { - lockMonitor['currentLockState'] = createLockState(undefined, undefined, 'pending'); + const mockCurrentLockState = createLockState(undefined, undefined, 'pending'); + spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockState)); - JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( + JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrownAsync( () => lockMonitor.getCurrentValueTimeLock(), ErrorCode.LockMonitorCurrentValueTimeLockInPendingState); }); - it('should return the current value time lock', () => { + it('should return the current value time lock', async () => { const mockCurrentValueLock: ValueTimeLockModel = { amountLocked: 300, identifier: 'identifier', @@ -155,15 +126,16 @@ describe('LockMonitor', () => { lockTransactionTime: 1220 }; - lockMonitor['currentLockState'] = createLockState(undefined, mockCurrentValueLock, 'confirmed'); + const mockCurrentLockState = createLockState(undefined, mockCurrentValueLock, 'confirmed'); + spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(mockCurrentLockState); - const actual = lockMonitor.getCurrentValueTimeLock(); + const actual = await lockMonitor.getCurrentValueTimeLock(); expect(actual).toEqual(mockCurrentValueLock); }); }); describe('handlePeriodicPolling', () => { - it('should only update the lock state if the current lock status is pending.', async () => { + it('should only refresh the lock state if lock update is disabled.', async () => { const mockCurrentValueLock: ValueTimeLockModel = { amountLocked: 300, identifier: 'identifier', @@ -173,15 +145,14 @@ describe('LockMonitor', () => { lockTransactionTime: 1220 }; - const mockCurrentLockInfo = createLockState(undefined, mockCurrentValueLock, 'pending'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; + const mockCurrentLockInfo = createLockState(undefined, mockCurrentValueLock, 'confirmed'); - const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState'); + const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(mockCurrentLockInfo); const createNewLockSpy = spyOn(lockMonitor as any, 'handleCreatingNewLock'); const existingLockSpy = spyOn(lockMonitor as any, 'handleExistingLockRenewal'); const releaseLockSpy = spyOn(lockMonitor as any, 'handleReleaseExistingLock'); - lockMonitor['desiredLockAmountInSatoshis'] = 1000; + lockMonitor['valueTimeLockUpdateEnabled'] = false; await lockMonitor['handlePeriodicPolling'](); expect(resolveCurrentLockSpy).toHaveBeenCalled(); @@ -190,11 +161,31 @@ describe('LockMonitor', () => { expect(releaseLockSpy).not.toHaveBeenCalled(); }); + + it('should rebroadcast if the last lock transaction is still pending.', async () => { + const rebroadcastTransactionSpy = spyOn(lockMonitor as any, 'rebroadcastTransaction').and.returnValue(Promise.resolve()); + const mockCurrentValueLock: ValueTimeLockModel = { + amountLocked: 300, + identifier: 'identifier', + owner: 'owner', + unlockTransactionTime: 12323, + normalizedFee: 100, + lockTransactionTime: 1220 + }; + + const mockCurrentLockInfo = createLockState(undefined, mockCurrentValueLock, 'pending'); + const getCurrentLockStateSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(mockCurrentLockInfo); + + await lockMonitor['handlePeriodicPolling'](); + + expect(getCurrentLockStateSpy).toHaveBeenCalled(); + expect(rebroadcastTransactionSpy).toHaveBeenCalled(); + }); + it('should not do anything if a lock is not required and none exist.', async () => { const mockCurrentLockInfo = createLockState(undefined, undefined, 'none'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; + const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(mockCurrentLockInfo); - const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState'); const createNewLockSpy = spyOn(lockMonitor as any, 'handleCreatingNewLock'); const existingLockSpy = spyOn(lockMonitor as any, 'handleExistingLockRenewal'); const releaseLockSpy = spyOn(lockMonitor as any, 'handleReleaseExistingLock'); @@ -202,15 +193,14 @@ describe('LockMonitor', () => { lockMonitor['desiredLockAmountInSatoshis'] = 0; await lockMonitor['handlePeriodicPolling'](); + expect(resolveCurrentLockSpy).toHaveBeenCalled(); expect(createNewLockSpy).not.toHaveBeenCalled(); expect(existingLockSpy).not.toHaveBeenCalled(); expect(releaseLockSpy).not.toHaveBeenCalled(); - expect(resolveCurrentLockSpy).not.toHaveBeenCalled(); }); it('should call the new lock routine if a lock is required but does not exist.', async () => { const mockCurrentLockInfo = createLockState(undefined, undefined, 'none'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); @@ -257,7 +247,6 @@ describe('LockMonitor', () => { }; const mockCurrentLockInfo = createLockState(mockSavedLock, mockCurrentValueLock, 'confirmed'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); @@ -295,7 +284,6 @@ describe('LockMonitor', () => { }; const mockCurrentLockInfo = createLockState(mockSavedLock, mockCurrentValueLock, 'confirmed'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); @@ -306,10 +294,10 @@ describe('LockMonitor', () => { lockMonitor['desiredLockAmountInSatoshis'] = 50; await lockMonitor['handlePeriodicPolling'](); + expect(resolveCurrentLockSpy).toHaveBeenCalled(); expect(createNewLockSpy).not.toHaveBeenCalled(); expect(existingLockSpy).toHaveBeenCalled(); expect(releaseLockSpy).not.toHaveBeenCalled(); - expect(resolveCurrentLockSpy).not.toHaveBeenCalled(); }); it('should call the release lock routine if a lock is not required but one does exist.', async () => { @@ -333,7 +321,6 @@ describe('LockMonitor', () => { }; const mockCurrentLockInfo = createLockState(mockSavedLock, mockCurrentValueLock, 'confirmed'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); @@ -350,7 +337,7 @@ describe('LockMonitor', () => { expect(resolveCurrentLockSpy).toHaveBeenCalled(); }); - it('should not resolve current lock if the the release lock routine returns false.', async () => { + it('should always refresh the lock state even if the the release lock routine returns false.', async () => { const mockSavedLock: SavedLockedModel = { createTimestamp: 1212, @@ -371,9 +358,7 @@ describe('LockMonitor', () => { }; const mockCurrentLockInfo = createLockState(mockSavedLock, mockCurrentValueLock, 'confirmed'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; - - const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); + const getCurrentLockStateSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); const createNewLockSpy = spyOn(lockMonitor as any, 'handleCreatingNewLock'); const existingLockSpy = spyOn(lockMonitor as any, 'handleExistingLockRenewal'); @@ -381,11 +366,11 @@ describe('LockMonitor', () => { lockMonitor['desiredLockAmountInSatoshis'] = 0; await lockMonitor['handlePeriodicPolling'](); - + + expect(getCurrentLockStateSpy).toHaveBeenCalled(); expect(createNewLockSpy).not.toHaveBeenCalled(); expect(existingLockSpy).not.toHaveBeenCalled(); expect(releaseLockSpy).toHaveBeenCalled(); - expect(resolveCurrentLockSpy).not.toHaveBeenCalled(); }); }); @@ -404,8 +389,7 @@ describe('LockMonitor', () => { expect(resolveLockSpy).not.toHaveBeenCalled(); }); - it('should rebroadcast if the last lock transaction is not yet broadcasted.', async () => { - const rebroadcastSpy = spyOn(lockMonitor as any, 'rebroadcastTransaction').and.returnValue(Promise.resolve()); + it('should return lock status as pending if the last lock transaction is not yet broadcasted.', async () => { const resolveLockSpy = spyOn(lockResolver, 'resolveLockIdentifierAndThrowOnError'); const mockLastLock: SavedLockedModel = { @@ -425,7 +409,6 @@ describe('LockMonitor', () => { const actual = await lockMonitor['getCurrentLockState'](); expect(actual).toEqual(expected); - expect(rebroadcastSpy).toHaveBeenCalled(); expect(resolveLockSpy).not.toHaveBeenCalled(); }); diff --git a/tests/bitcoin/lock/LockResolver.spec.ts b/tests/bitcoin/lock/LockResolver.spec.ts index ae2b28e72..fabfae9c8 100644 --- a/tests/bitcoin/lock/LockResolver.spec.ts +++ b/tests/bitcoin/lock/LockResolver.spec.ts @@ -1,7 +1,9 @@ +import { Address, Networks, PrivateKey, Script, crypto } from 'bitcore-lib'; import BitcoinClient from '../../../lib/bitcoin/BitcoinClient'; import BitcoinOutputModel from '../../../lib/bitcoin/models/BitcoinOutputModel'; import BitcoinTransactionModel from '../../../lib/bitcoin/models/BitcoinTransactionModel'; import ErrorCode from '../../../lib/bitcoin/ErrorCode'; +import { IBlockInfo } from '../../../lib/bitcoin/BitcoinProcessor'; import JasmineSidetreeErrorValidator from '../../JasmineSidetreeErrorValidator'; import LockIdentifierModel from '../../../lib/bitcoin/models/LockIdentifierModel'; import LockIdentifierSerializer from '../../../lib/bitcoin/lock/LockIdentifierSerializer'; @@ -9,8 +11,6 @@ import LockResolver from '../../../lib/bitcoin/lock/LockResolver'; import ValueTimeLockModel from '../../../lib/common/models/ValueTimeLockModel'; import VersionManager from '../../../lib/bitcoin/VersionManager'; import VersionModel from '../../../lib/common/models/VersionModel'; -import { Address, Networks, PrivateKey, Script, crypto } from 'bitcore-lib'; -import { IBlockInfo } from '../../../lib/bitcoin/BitcoinProcessor'; function createValidLockRedeemScript (lockDurationInBlocks: number, targetWalletAddress: Address): Script { const lockDurationInBlocksBuffer = Buffer.alloc(3);