From c186ea8ce9b4a8a2d34cf26c3dce8fa570c28a2e Mon Sep 17 00:00:00 2001 From: cgeek <cem.moreau@gmail.com> Date: Tue, 14 Nov 2017 13:39:21 +0100 Subject: [PATCH] [fix] #1200 Redefine unlock conditions (cherry picked from commit 8eb68e6) --- .gitignore | 8 +- app/lib/common-libs/parsers/transaction.ts | 6 +- app/lib/common-libs/txunlock.ts | 91 ++++++++++++--- app/lib/dto/TransactionDTO.ts | 53 +++++++++ app/lib/indexer.ts | 53 ++------- app/lib/rules/global_rules.ts | 76 ++++++++----- app/lib/rules/local_rules.ts | 34 +----- doc/Protocol.md | 107 +++++++++++++++--- test/fast/modules/common/grammar-test.js | 76 ------------- test/fast/modules/common/grammar.ts | 84 ++++++++++++++ test/integration/crosschain-test.js | 4 +- test/integration/transactions-csv-cltv-sig.ts | 76 +++++++++++++ test/integration/transactions-test.js | 4 +- 13 files changed, 451 insertions(+), 221 deletions(-) delete mode 100644 test/fast/modules/common/grammar-test.js create mode 100644 test/fast/modules/common/grammar.ts create mode 100644 test/integration/transactions-csv-cltv-sig.ts diff --git a/.gitignore b/.gitignore index 8d7e9c280..db4b1103d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,11 @@ test/blockchain/lib/*.d.ts /server.d.ts app/**/*.js* app/**/*.d.ts -test/integration/*.js* +test/integration/revoked_pubkey_replay.js +test/integration/server-shutdown.js +test/integration/transactions-csv-cltv-sig.js +test/integration/ws2p*js +test/integration/*.js.map test/integration/*.d.ts test/integration/membership_chainability.js* test/integration/membership_chainability.d.ts @@ -58,3 +62,5 @@ test/fast/proxies*.js* test/fast/proxies*.d.ts test/fast/modules/ws2p/*.js* test/fast/modules/ws2p/*.d.ts +test/fast/modules/common/grammar.js* +test/fast/modules/common/grammar.d.ts diff --git a/app/lib/common-libs/parsers/transaction.ts b/app/lib/common-libs/parsers/transaction.ts index 6fe526908..e7c96e93e 100644 --- a/app/lib/common-libs/parsers/transaction.ts +++ b/app/lib/common-libs/parsers/transaction.ts @@ -1,7 +1,7 @@ -import {CommonConstants} from "../../../lib/common-libs/constants" +import {CommonConstants} from "../constants" import {GenericParser} from "./GenericParser" import {rawer} from "../../../lib/common-libs/index" -import { unlock } from '../txunlock'; +import {checkGrammar} from '../txunlock'; export class TransactionParser extends GenericParser { @@ -106,7 +106,7 @@ function extractOutputs(raw:string) { for (const line of lines) { if (line.match(CommonConstants.TRANSACTION.TARGET)) { outputs.push(line); - const unlocked = unlock(line.split(':')[2], [], {}) + const unlocked = checkGrammar(line.split(':')[2]) if (unlocked === null) { throw Error("Wrong output format") } diff --git a/app/lib/common-libs/txunlock.ts b/app/lib/common-libs/txunlock.ts index f01c2bafe..f37f88a2b 100644 --- a/app/lib/common-libs/txunlock.ts +++ b/app/lib/common-libs/txunlock.ts @@ -1,8 +1,8 @@ -"use strict"; import {hashf} from "../common" +import {evalParams} from "../rules/global_rules" +import {TxSignatureResult} from "../dto/TransactionDTO" -let Parser = require("jison").Parser; -let buid = require('../../../app/lib/common-libs/buid').Buid +let Parser = require("jison").Parser let grammar = { "lex": { @@ -42,36 +42,91 @@ let grammar = { [ "( e )", "$$ = $2;" ] ] } -}; +} -export function unlock(conditionsStr:string, executions:any, metadata:any): boolean|null { +export interface UnlockMetadata { + currentTime?:number + elapsedTime?:number +} - let parser = new Parser(grammar); +export function unlock(conditionsStr:string, unlockParams:string[], sigResult:TxSignatureResult, metadata?:UnlockMetadata): boolean|null { + + const params = evalParams(unlockParams, conditionsStr, sigResult) + let parser = new Parser(grammar) + let nbFunctions = 0 parser.yy = { i: 0, sig: function (pubkey:string) { - let sigParam = executions[this.i++]; - return (sigParam && pubkey === sigParam.pubkey && sigParam.sigOK) || false; + // Counting functions + nbFunctions++ + // Make the test + let success = false + let i = 0 + while (!success && i < params.length) { + const p = params[i] + success = p.successful && p.funcName === 'SIG' && p.parameter === pubkey + i++ + } + return success }, xHx: function(hash:string) { - let xhxParam = executions[this.i++]; - if (xhxParam === undefined) { - xhxParam = "" + // Counting functions + nbFunctions++ + // Make the test + let success = false + let i = 0 + while (!success && i < params.length) { + const p = params[i] + success = p.successful && p.funcName === 'XHX' && hashf(p.parameter) === hash + i++ } - return hashf(xhxParam) === hash; + return success }, cltv: function(deadline:string) { - return metadata.currentTime && metadata.currentTime >= parseInt(deadline); + // Counting functions + nbFunctions++ + // Make the test + return metadata && metadata.currentTime && metadata.currentTime >= parseInt(deadline) }, csv: function(amountToWait:string) { - return metadata.elapsedTime && metadata.elapsedTime >= parseInt(amountToWait); + // Counting functions + nbFunctions++ + // Make the test + return metadata && metadata.elapsedTime && metadata.elapsedTime >= parseInt(amountToWait) } - }; + } + + try { + const areAllValidParameters = params.reduce((success, p) => success && !!(p.successful), true) + if (!areAllValidParameters) { + throw "All parameters must be successful" + } + const unlocked = parser.parse(conditionsStr) + if (unlockParams.length > nbFunctions) { + throw "There must be at most as much params as function calls" + } + return unlocked + } catch(e) { + return null + } +} + +export function checkGrammar(conditionsStr:string): boolean|null { + + let parser = new Parser(grammar); + + parser.yy = { + i: 0, + sig: () => true, + xHx: () => true, + cltv: () => true, + csv: () => true + } try { - return parser.parse(conditionsStr); + return parser.parse(conditionsStr) } catch(e) { - return null; + return null } -} \ No newline at end of file +} diff --git a/app/lib/dto/TransactionDTO.ts b/app/lib/dto/TransactionDTO.ts index 15e65a4a2..b70cfbaca 100644 --- a/app/lib/dto/TransactionDTO.ts +++ b/app/lib/dto/TransactionDTO.ts @@ -1,5 +1,6 @@ import {hashf} from "../common" import {Cloneable} from "./Cloneable" +import {verify} from "../common-libs/crypto/keyring" export interface BaseDTO { base: number @@ -25,6 +26,32 @@ export class OutputDTO implements BaseDTO { ) {} } +export interface TxSignatureResult { + sigs:{ + k:string + ok:boolean + }[] +} + +export class TxSignatureResultImpl implements TxSignatureResult { + + // The signature results + public sigs:{ + k:string + ok:boolean + }[] + + constructor(issuers:string[]) { + this.sigs = issuers.map(k => { + return { k, ok: false } + }) + } + + get allMatching() { + return this.sigs.reduce((ok, s) => ok && s.ok, true) + } +} + export class TransactionDTO implements Cloneable { clone(): any { @@ -197,6 +224,24 @@ export class TransactionDTO implements Cloneable { } } + getTransactionSigResult() { + const sigResult = new TxSignatureResultImpl(this.issuers.slice()) + let i = 0 + const raw = this.getRawTxNoSig() + let matching = true + while (matching && i < this.signatures.length) { + const sig = this.signatures[i] + const pub = this.issuers[i] + sigResult.sigs[i].ok = matching = verify(raw, sig, pub) + i++ + } + return sigResult + } + + checkSignatures() { + return this.getTransactionSigResult().allMatching + } + static fromJSONObject(obj:any, currency:string = "") { return new TransactionDTO( obj.version || 10, @@ -276,6 +321,14 @@ export class TransactionDTO implements Cloneable { } } + static unlock2params(unlock:string) { + const match = unlock.match(/^\d+:(.*)$/) + if (match) { + return match[1].split(' ') + } + return [] + } + static mock() { return new TransactionDTO(1, "", 0, "", "", 0, [], [], [], [], [], "") } diff --git a/app/lib/indexer.ts b/app/lib/indexer.ts index 7b9c17140..ddb1cfa99 100644 --- a/app/lib/indexer.ts +++ b/app/lib/indexer.ts @@ -1,4 +1,3 @@ -"use strict"; import {BlockDTO} from "./dto/BlockDTO" import {ConfDTO, CurrencyConfDTO} from "./dto/ConfDTO" import {IdentityDTO} from "./dto/IdentityDTO" @@ -6,11 +5,11 @@ import {RevocationDTO} from "./dto/RevocationDTO" import {CertificationDTO} from "./dto/CertificationDTO" import {TransactionDTO} from "./dto/TransactionDTO" import {DBHead} from "./db/DBHead" -import {LOCAL_RULES_HELPERS} from "./rules/local_rules" import {verify} from "./common-libs/crypto/keyring" import {rawer, txunlock} from "./common-libs/index" import {CommonConstants} from "./common-libs/constants" import {MembershipDTO} from "./dto/MembershipDTO" +import {UnlockMetadata} from "./common-libs/txunlock" const _ = require('underscore'); @@ -1777,7 +1776,7 @@ export class Indexer { reduce: reduce, reduceBy: reduceBy, getMaxBlockSize: (HEAD: DBHead) => Math.max(500, Math.ceil(1.1 * HEAD.avgBlockSize)), - checkPeopleAreNotOudistanced: checkPeopleAreNotOudistanced + checkPeopleAreNotOudistanced } } @@ -1979,44 +1978,14 @@ async function checkCertificationIsValid (block: BlockDTO, cert: CindexEntry, fi function txSourceUnlock(ENTRY:SindexEntry, source:SindexEntry, HEAD: DBHead) { const tx = ENTRY.txObj; - let sigResults = LOCAL_RULES_HELPERS.getSigResult(tx) - let unlocksForCondition = []; - let unlocksMetadata: any = {}; - let unlockValues = ENTRY.unlock; - if (source.conditions) { - if (unlockValues) { - // Evaluate unlock values - let sp = unlockValues.split(' '); - for (const func of sp) { - const match = func.match(/\((.+)\)/) - let param = match && match[1]; - if (param && func.match(/SIG/)) { - let pubkey = tx.issuers[parseInt(param)]; - if (!pubkey) { - return false; - } - unlocksForCondition.push({ - pubkey: pubkey, - sigOK: sigResults.sigs[pubkey] && sigResults.sigs[pubkey].matching || false - }); - } else { - // XHX - unlocksForCondition.push(param); - } - } - } - - if (source.conditions.match(/CLTV/)) { - unlocksMetadata.currentTime = HEAD.medianTime; - } - - if (source.conditions.match(/CSV/)) { - unlocksMetadata.elapsedTime = HEAD.medianTime - source.written_time; - } - - if (txunlock(source.conditions, unlocksForCondition, unlocksMetadata)) { - return true; - } + const unlockParams:string[] = TransactionDTO.unlock2params(ENTRY.unlock || '') + const unlocksMetadata:UnlockMetadata = {} + const sigResult = TransactionDTO.fromJSONObject(tx).getTransactionSigResult() + if (source.conditions.match(/CLTV/)) { + unlocksMetadata.currentTime = HEAD.medianTime; + } + if (source.conditions.match(/CSV/)) { + unlocksMetadata.elapsedTime = HEAD.medianTime - source.written_time; } - return false; + return txunlock(source.conditions, unlockParams, sigResult, unlocksMetadata) } diff --git a/app/lib/rules/global_rules.ts b/app/lib/rules/global_rules.ts index 665b087f9..761dcf3dc 100644 --- a/app/lib/rules/global_rules.ts +++ b/app/lib/rules/global_rules.ts @@ -1,17 +1,16 @@ -"use strict"; import {ConfDTO} from "../dto/ConfDTO" import {FileDAL} from "../dal/fileDAL" import {DBBlock} from "../db/DBBlock" -import {TransactionDTO} from "../dto/TransactionDTO" -import * as local_rules from "./local_rules" +import {TransactionDTO, TxSignatureResult} from "../dto/TransactionDTO" import {BlockDTO} from "../dto/BlockDTO" import {verify} from "../common-libs/crypto/keyring" import {rawer, txunlock} from "../common-libs/index" import {CommonConstants} from "../common-libs/constants" import {IdentityDTO} from "../dto/IdentityDTO" +import {hashf} from "../common" +import {Indexer} from "../indexer" -const _ = require('underscore'); -const indexer = require('../indexer').Indexer +const _ = require('underscore') const constants = CommonConstants @@ -23,6 +22,40 @@ let logger = { // TODO: all the global rules should be replaced by index rule someday +export interface ParamEval { + successful:boolean + funcName:string + parameter:string +} + +export function evalParams(params:string[], conditions = '', sigResult:TxSignatureResult): ParamEval[] { + const res:ParamEval[] = [] + const issuers = sigResult.sigs.map(s => s.k) + for (const func of params) { + if (func.match(/^SIG/)) { + const param = (func.match(/^SIG\((.*)\)$/) as string[])[1] + const index = parseInt(param) + const sigEntry = !isNaN(index) && index < issuers.length && sigResult.sigs[index] + const signatory:{ k:string, ok:boolean } = sigEntry || { k: '', ok: false } + res.push({ + funcName: 'SIG', + parameter: signatory.k, + successful: signatory.ok + }) + } + else if (func.match(/^XHX/)) { + const password = (func.match(/^XHX\((.*)\)$/) as string[])[1] + const hash = hashf(password) + res.push({ + funcName: 'XHX', + parameter: password, + successful: conditions.indexOf('XHX(' + hash + ')') !== -1 + }) + } + } + return res +} + export const GLOBAL_RULES_FUNCTIONS = { checkIdentitiesAreWritable: async (block:{ identities:string[], version: number }, conf:ConfDTO, dal:FileDAL) => { @@ -90,31 +123,10 @@ export const GLOBAL_RULES_FUNCTIONS = { if (block.medianTime - dbSrc.written_time < tx.locktime) { throw constants.ERRORS.LOCKTIME_PREVENT; } - let sigResults = local_rules.LOCAL_RULES_HELPERS.getSigResult(tx); - let unlocksForCondition = []; + let unlockValues = unlocks[k] + let unlocksForCondition:string[] = (unlockValues || '').split(' ') let unlocksMetadata:any = {}; - let unlockValues = unlocks[k]; if (dbSrc.conditions) { - if (unlockValues) { - // Evaluate unlock values - let sp = unlockValues.split(' '); - for (const func of sp) { - let param = func.match(/\((.+)\)/)[1]; - if (func.match(/^SIG/)) { - let pubkey = tx.issuers[parseInt(param)]; - if (!pubkey) { - logger.warn('Source ' + [src.amount, src.base, src.type, src.identifier, src.pos].join(':') + ' unlock fail (unreferenced signatory)'); - throw constants.ERRORS.WRONG_UNLOCKER; - } - unlocksForCondition.push({ - pubkey: pubkey, - sigOK: sigResults.sigs[pubkey] && sigResults.sigs[pubkey].matching || false - }); - } else if (func.match(/^XHX/)) { - unlocksForCondition.push(param); - } - } - } if (dbSrc.conditions.match(/CLTV/)) { unlocksMetadata.currentTime = block.medianTime; @@ -124,14 +136,18 @@ export const GLOBAL_RULES_FUNCTIONS = { unlocksMetadata.elapsedTime = block.medianTime - dbSrc.written_time; } + const sigs = tx.getTransactionSigResult() + try { - if (!txunlock(dbSrc.conditions, unlocksForCondition, unlocksMetadata)) { + if (!txunlock(dbSrc.conditions, unlocksForCondition, sigs, unlocksMetadata)) { throw Error('Locked'); } } catch (e) { logger.warn('Source ' + [src.amount, src.base, src.type, src.identifier, src.pos].join(':') + ' unlock fail'); throw constants.ERRORS.WRONG_UNLOCKER; } + } else { + throw Error("Source with no conditions") } } let sumOfOutputs = outputs.reduce(function(p, output) { @@ -167,7 +183,7 @@ export const GLOBAL_RULES_HELPERS = { return Promise.resolve(false); } try { - return indexer.DUP_HELPERS.checkPeopleAreNotOudistanced([member], newLinks, newcomers, conf, dal); + return Indexer.DUP_HELPERS.checkPeopleAreNotOudistanced([member], newLinks, newcomers, conf, dal); } catch (e) { return true; } diff --git a/app/lib/rules/local_rules.ts b/app/lib/rules/local_rules.ts index b2cfb00c3..41d19f163 100644 --- a/app/lib/rules/local_rules.ts +++ b/app/lib/rules/local_rules.ts @@ -374,9 +374,8 @@ export const LOCAL_RULES_FUNCTIONS = { const txs = block.transactions // Check rule against each transaction for (const tx of txs) { - let sigResult = getSigResult(tx); - if (!sigResult.matching) { - throw Error('Signature from a transaction must match'); + if (!tx.checkSignatures()) { + throw Error('Signature from a transaction must match') } } return true; @@ -387,33 +386,6 @@ function checkSingleMembershipSignature(ms:any) { return verify(ms.getRaw(), ms.signature, ms.issuer); } -function getSigResult(tx:any) { - let sigResult:any = { sigs: {}, matching: true }; - let json:any = { "version": tx.version, "currency": tx.currency, "blockstamp": tx.blockstamp, "locktime": tx.locktime, "inputs": [], "outputs": [], "issuers": tx.issuers, "signatures": [], "comment": tx.comment, unlocks: [] }; - tx.inputs.forEach(function (input:any) { - json.inputs.push(input.raw); - }); - tx.outputs.forEach(function (output:any) { - json.outputs.push(output.raw); - }); - json.unlocks = tx.unlocks; - let i = 0; - let signaturesMatching = true; - const raw = tx.getRawTxNoSig() - while (signaturesMatching && i < tx.signatures.length) { - const sig = tx.signatures[i]; - const pub = tx.issuers[i]; - signaturesMatching = verify(raw, sig, pub); - sigResult.sigs[pub] = { - matching: signaturesMatching, - index: i - }; - i++; - } - sigResult.matching = signaturesMatching; - return sigResult; -} - function checkBunchOfTransactions(transactions:TransactionDTO[], done:any = undefined){ const block:any = { transactions }; return (async () => { @@ -439,8 +411,6 @@ export const LOCAL_RULES_HELPERS = { checkSingleMembershipSignature: checkSingleMembershipSignature, - getSigResult: getSigResult, - checkBunchOfTransactions: checkBunchOfTransactions, checkSingleTransactionLocally: (tx:any, done:any = undefined) => checkBunchOfTransactions([tx], done), diff --git a/doc/Protocol.md b/doc/Protocol.md index ee06c1e47..b657553f4 100644 --- a/doc/Protocol.md +++ b/doc/Protocol.md @@ -475,14 +475,28 @@ It follows a machine-readable BNF grammar composed of #### Condition matching -Each `Unlock` of TX2 refers to an input of TX2 through `IN_INDEX`, input itself refering to an `Output` of TX1 through `T_HASH` reference and `T_INDEX`. +Considering a transaction `TX` consuming some money: each unlock line `UNLOCK` of TX tries to unlock the input `INPUT = TX.Inputs[IN_INDEX]` refering to an ouput `Output` whose value is: -* An output contains `F` functions in its conditions (read from left to right) -* An unlock contains `P` parameters (or less, min. is zero), each separated by a space (read from left to right) +If `INPUT.TYPE = 'T'`: -A function of TX1 at position `f` returns TRUE if parameter at position `p` resolves the function. Otherwise it returns FALSE. + Output = TRANSACTIONS_IN_BLOCKCHAIN_OR_IN_LOCAL_BLOCK[T_HASH].OUPUTS[T_INDEX] -The condition of an `Output` is unlocked if, evaluated globally with `(`, `)`, `&&`, and `||`, the condition returns TRUE. +If `INPUT.TYPE = 'D'`: + + Output = BLOCKCHAIN[BLOCK_ID].Dividend[PUBLIC_KEY] + +Let's name: + +* `CONDITIONS` the conditions of a given output `Output` +* `NB_FUNCTIONS` the number of functions present in `CONDITIONS` (read from left to right) +* `NB_PARAMETERS` the number of parameters present in `UNLOCK`, each separated by a space (read from left to right) + +Then, an `UNLOCK` line is considered *successful* if: + +* `Output` exists +* `NB_PARAMETERS <= NB_FUNCTIONS` +* Each `UNLOCK` parameter returns TRUE +* `CONDITIONS` evaluated globally with `(`, `)`, `&&`, `||` and its locking functions returns TRUE ##### Example 1 @@ -524,18 +538,35 @@ Unlocks: Because `SIG(1)` refers to the signature `DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo`, considering that signature `DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo` is good over TX2. -#### Unlocking functions +#### Locking and unlocking functions -These functions may be present under both `Unlocks` and `Outputs` fields. +The *locking* functions are present under `Outputs` field. -* When present under `Outputs`, these functions define the *necessary conditions* to spend each output. -* When present under `Unlocks`, these functions define the *sufficient proofs* that each input can be spent. +The *unlocking* functions are present under `Unlocks` field. ##### SIG function +###### Definition + +Lock: + + SIG(PUBKEY_A) + +Unlock: + + SIG(INDEX) + +###### Condition + +Lock `SIG(PUBKEY_A)` returns TRUE if it exists a parameter `SIG(INDEX)` returning TRUE where `Issuers[INDEX] == PUBKEY_A`. + +Unlock `SIG(INDEX)` returns TRUE if `Signatures[INDEX]` is a valid valid signature of TX against `Issuers[INDEX]` public key. + +###### Description + This function is a control over the signature. -* in an `Output` of TX1, `SIG(PUBKEY_A)` requires from a future transaction TX2 unlocking the output to give as parameter a valid signature of TX2 by `PUBKEY_A` +* in an `Output` of a transaction TX1, `SIG(PUBKEY_A)` requires from a future transaction TX2 unlocking the output to give as parameter a valid signature of TX2 by `PUBKEY_A` * if TX2 does not give `SIG(INDEX)` parameter as [matching parameter](#condition-matching), the condition fails * if TX2's `Issuers[INDEX]` does not equal `PUBKEY_A`, the condition fails * if TX2's `SIG(INDEX)` does not return TRUE, the condition fails @@ -578,6 +609,24 @@ The necessary condition `SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g)` is m ##### XHX function +###### Definition + +Lock: + + XHX(HASH) + +Unlock: + + XHX(PASSWORD) + +###### Condition + +`XHX(HASH)` returns true if it exists a parameter `XHX(PASSWORD)` where `SHA256(PASSWORD) = HASH`. + +`XHX(PASSWORD)` returns true if it exists a locking function `XHX(HASH)` where `SHA256(PASSWORD) = HASH` in the conditions. + +###### Description + This function is a password control. So if we have, in TX1: @@ -608,9 +657,9 @@ Where: * `6991C993631BED4733972ED7538E41CCC33660F554E3C51963E2A0AC4D6453D3` is the hash of TX1. -The necessary condition `XHX(8AFC8DF633FC158F9DB4864ABED696C1AA0FE5D617A7B5F7AB8DE7CA2EFCD4CB)` is matched here if `XHX(1872767826647264) = 8AFC8DF633FC158F9DB4864ABED696C1AA0FE5D617A7B5F7AB8DE7CA2EFCD4CB`. +Then `XHX(PASSWORD)` returns TRUE if it exists `XHX(HASH)` in the output's conditions of TX1, with the relation `SHA256(PASSWORD) = HASH`. -`XHX(1872767826647264)` is to be evaluated as `SHA256(1872767826647264)`. +Here `XHX(1872767826647264)` returns TRUE if it exists `XHX(8AFC8DF633FC158F9DB4864ABED696C1AA0FE5D617A7B5F7AB8DE7CA2EFCD4CB)` in the output's conditions of TX1, as it matches the link `SHA256(1872767826647264) = 8AFC8DF633FC158F9DB4864ABED696C1AA0FE5D617A7B5F7AB8DE7CA2EFCD4CB`. #### Example 1 @@ -710,6 +759,20 @@ Signatures (fakes here): ##### CLTV function +###### Definition + +Lock: + + CLVT(TIMESTAMP) + +Unlock: no unlocking function. + +###### Condition + +`CLVT(TIMESTAMP)` returns true if and only if the transaction including block's `MedianTime >= TIMESTAMP`. + +###### Description + This function locks an output in the future, which will be unlocked at a given date. So if we have, in TX1: @@ -722,12 +785,28 @@ Outputs 25:2:CLTV(1489677041) ``` -Then the `25` units can be spent *exclusively* in a block whose `MedianTime >= 1489677041` +So here, the `25` units can be spent *exclusively* in a block whose `MedianTime >= 1489677041` `CLTV`'s parameter must be an integer with a length between `1` and `10` chars. ##### CSV function +###### Definition + +Lock: + + CSV(DELAY) + +Unlock: no unlocking function. + +We define `TxTime` as the `MedianTime` of the block referenced by the transaction's `Blockstamp` field. + +###### Condition + +`CSV(DELAY)` returns true if and only if the transaction including block's `MedianTime - TxTime >= DELAY`. + +###### Description + This function locks an output in the future, which will be unlocked after the given amount of time has elapsed. So if we have, in TX1: @@ -742,8 +821,6 @@ Outputs 25:2:CSV(3600) ``` -We define `TxTime` as the `MedianTime` of block `204-00003E2B8A35370BA5A7064598F628A62D4E9EC1936BE8651CE9A85F2E06981B`. - Then the `25` units can be spent *exclusively* in a block whose `MedianTime - TxTime >= 3600`. `CSV`'s parameter must be an integer with a length between `1` and `8` chars. diff --git a/test/fast/modules/common/grammar-test.js b/test/fast/modules/common/grammar-test.js deleted file mode 100644 index 49979860e..000000000 --- a/test/fast/modules/common/grammar-test.js +++ /dev/null @@ -1,76 +0,0 @@ -"use strict"; - -const unlock = require('../../../../app/lib/common-libs').txunlock -const should = require('should'); -const assert = require('assert'); - -describe('Grammar', () => { - - let k1 = "HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd"; - let k2 = "GgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd"; - let Ha = "CA978112CA1BBDCAFAC231B39A23DC4DA786EFF8147C4E72B9807785AFEE48BB"; - let Hz = "594E519AE499312B29433B7DD8A97FF068DEFCBA9755B6D5D00E84C524D67B06"; - - it('SIG should work', () => { - - unlock('SIG(' + k1 + ')', [{ pubkey: k1, sigOK: true }]).should.equal(true); - unlock('SIG(' + k1 + ')', [{ pubkey: k1, sigOK: false }]).should.equal(false); - unlock('SIG(' + k1 + ')', [{ pubkey: k2, sigOK: false }]).should.equal(false); - unlock('SIG(' + k1 + ')', [{ pubkey: k2, sigOK: true }]).should.equal(false); - }); - - it('XHX should work', () => { - - unlock('XHX(' + Ha + ')', ['a']).should.equal(true); - unlock('XHX(' + Hz + ')', ['z']).should.equal(true); - unlock('XHX(' + Hz + ')', ['a']).should.equal(false); - unlock('XHX(' + Ha + ')', ['z']).should.equal(false); - }); - - it('&& keywork should work', () => { - - unlock('SIG(' + k1 + ') && SIG(' + k2 + ')', [{ pubkey: k1, sigOK: true }, { pubkey: k1, sigOK: true }]).should.equal(false); - unlock('SIG(' + k1 + ') && SIG(' + k2 + ')', [{ pubkey: k1, sigOK: true }, { pubkey: k2, sigOK: false }]).should.equal(false); - unlock('SIG(' + k1 + ') && SIG(' + k2 + ')', [{ pubkey: k1, sigOK: true }, { pubkey: k2, sigOK: true }]).should.equal(true); - }); - - it('|| keywork should work', () => { - - unlock('SIG(' + k1 + ') || SIG(' + k2 + ')', [{ pubkey: k1, sigOK: true }, { pubkey: k2, sigOK: true }]).should.equal(true); - unlock('SIG(' + k1 + ') || SIG(' + k2 + ')', [{ pubkey: k1, sigOK: false }, { pubkey: k2, sigOK: true }]).should.equal(true); - unlock('SIG(' + k1 + ') || SIG(' + k2 + ')', [{ pubkey: k1, sigOK: true }, { pubkey: k2, sigOK: false }]).should.equal(true); - unlock('SIG(' + k1 + ') || SIG(' + k2 + ')', [{ pubkey: k1, sigOK: false }, { pubkey: k2, sigOK: false }]).should.equal(false); - }); - - it('|| && keyworks combined should work', () => { - - unlock('SIG(' + k1 + ') || (SIG(' + k1 + ') && SIG(' + k2 + '))', [{ pubkey: k1, sigOK: true },{ pubkey: k1, sigOK: true },{ pubkey: k2, sigOK: true }]).should.equal(true); - unlock('SIG(' + k2 + ') || (SIG(' + k1 + ') && SIG(' + k2 + '))', [{ pubkey: k2, sigOK: false },{ pubkey: k1, sigOK: true },{ pubkey: k2, sigOK: false }]).should.equal(false); - }); - - it('SIG XHX functions combined should work', () => { - - unlock('SIG(' + k1 + ') && XHX(' + Ha + ')', [{ pubkey: k1, sigOK: true },'a']).should.equal(true); - unlock('SIG(' + k1 + ') && XHX(' + Ha + ')', [{ pubkey: k1, sigOK: true },'z']).should.equal(false); - unlock('SIG(' + k1 + ') || XHX(' + Ha + ')', [{ pubkey: k1, sigOK: true },'a']).should.equal(true); - unlock('SIG(' + k1 + ') || XHX(' + Ha + ')', [{ pubkey: k1, sigOK: true },'z']).should.equal(true); - unlock('SIG(' + k1 + ') || XHX(' + Ha + ')', [{ pubkey: k1, sigOK: false },'z']).should.equal(false); - unlock('SIG(' + k1 + ') || XHX(' + Ha + ')', [{ pubkey: k1, sigOK: false },'a']).should.equal(true); - }); - - it('SIG, XHX, &&, || words combined should work', () => { - - unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', [{ pubkey: k1, sigOK: true },'a','z']).should.equal(true); - unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', [{ pubkey: k1, sigOK: true },'a','a']).should.equal(true); - unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', [{ pubkey: k1, sigOK: true },'z','a']).should.equal(false); - unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', [{ pubkey: k1, sigOK: false },'a','a']).should.equal(false); - unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', [{ pubkey: k1, sigOK: false },'a','z']).should.equal(true); - unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', [{ pubkey: k1, sigOK: false },'z','z']).should.equal(true); - unlock('(SIG(EA7Dsw39ShZg4SpURsrgMaMqrweJPUFPYHwZA8e92e3D) || XHX(03AC674216F3E15C761EE1A5E255F067953623C8B388B4459E13F978D7C846F4))', [{ pubkey: k1, sigOK: false },'1234']).should.equal(true); - }); - - it('Wrong syntax should return `null`', () => { - - assert.equal(unlock('XHX(03AC674216F3E15C761EE1A5E255F067953623C8B388B4459E13F978D7C846F4))', []), null) - }); -}); diff --git a/test/fast/modules/common/grammar.ts b/test/fast/modules/common/grammar.ts new file mode 100644 index 000000000..a72a0c974 --- /dev/null +++ b/test/fast/modules/common/grammar.ts @@ -0,0 +1,84 @@ +import {unlock} from "../../../../app/lib/common-libs/txunlock" + +const assert = require('assert') + +describe('Grammar', () => { + + let k1 = "HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd" + let k2 = "GgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd" + let k3 = "IgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd" + let Ha = "CA978112CA1BBDCAFAC231B39A23DC4DA786EFF8147C4E72B9807785AFEE48BB" + let Hz = "594E519AE499312B29433B7DD8A97FF068DEFCBA9755B6D5D00E84C524D67B06" + + it('SIG should work', () => { + assert.equal(unlock('SIG(' + k1 + ')', ['SIG(0)'], { sigs: [{ k:k1, ok:true }] }), true) + assert.equal(unlock('SIG(' + k1 + ')', ['SIG(0)'], { sigs: [{ k:k1, ok:false }] }), null) + assert.equal(unlock('SIG(' + k1 + ')', ['SIG(0)'], { sigs: [{ k:k2, ok:true }] }), false) + assert.equal(unlock('SIG(' + k2 + ')', ['SIG(0)'], { sigs: [{ k:k2, ok:true }] }), true) + }) + + it('SIG should work', () => { + assert.equal(unlock('SIG(' + k1 + ')', ['SIG(0)'], { sigs: [{ k:k1, ok:true }] }), true) + assert.equal(unlock('SIG(' + k1 + ')', ['SIG(0)'], { sigs: [{ k:k1, ok:false }] }), null) + assert.equal(unlock('SIG(' + k1 + ')', ['SIG(0)'], { sigs: [{ k:k2, ok:true }] }), false) + assert.equal(unlock('SIG(' + k2 + ')', ['SIG(0)'], { sigs: [{ k:k2, ok:true }] }), true) + }) + + it('XHX should work', () => { + assert.equal(unlock('XHX(' + Ha + ')', ['XHX(a)'], { sigs: []}), true) + assert.equal(unlock('XHX(' + Hz + ')', ['XHX(z)'], { sigs: []}), true) + assert.equal(unlock('XHX(' + Hz + ')', ['XHX(a)'], { sigs: []}), null) + assert.equal(unlock('XHX(' + Ha + ')', ['XHX(z)'], { sigs: []}), null) + }) + + it('&& keywork should work', () => { + assert.equal(unlock('SIG(' + k1 + ') && SIG(' + k2 + ')', ['SIG(0)', 'SIG(0)'], { sigs: [{ k:k1, ok:true }, { k:k1, ok:true }] }), false) + assert.equal(unlock('SIG(' + k1 + ') && SIG(' + k2 + ')', ['SIG(0)', 'SIG(0)'], { sigs: [{ k:k1, ok:true }, { k:k2, ok:true }] }), false) + assert.equal(unlock('SIG(' + k1 + ') && SIG(' + k2 + ')', ['SIG(0)', 'SIG(1)'], { sigs: [{ k:k1, ok:true }, { k:k3, ok:true }] }), false) + assert.equal(unlock('SIG(' + k1 + ') && SIG(' + k2 + ')', ['SIG(0)', 'SIG(1)'], { sigs: [{ k:k1, ok:true }, { k:k2, ok:true }] }), true) + }) + + it('|| keywork should work', () => { + assert.equal(unlock('SIG(' + k1 + ') || SIG(' + k2 + ')', ['SIG(0)'], { sigs: [{ k:k1, ok:true }] }), true) + assert.equal(unlock('SIG(' + k1 + ') || SIG(' + k2 + ')', ['SIG(0)'], { sigs: [{ k:k2, ok:true }] }), true) + assert.equal(unlock('SIG(' + k1 + ') || SIG(' + k2 + ')', ['SIG(0)', 'SIG(1)'], { sigs: [{ k:k1, ok:true }, { k:k2, ok:true }] }), true) + assert.equal(unlock('SIG(' + k1 + ') || SIG(' + k2 + ')', ['SIG(0)', 'SIG(1)'], { sigs: [{ k:k1, ok:false }, { k:k2, ok:false }] }), null) + }) + + it('|| && keyworks combined should work', () => { + assert.equal(unlock('SIG(' + k1 + ') || (SIG(' + k1 + ') && SIG(' + k2 + '))', ['SIG(0)','SIG(0)','SIG(1)'], { sigs: [{ k:k1, ok:true }, { k:k2, ok:true }] }), true) + assert.equal(unlock('SIG(' + k2 + ') || (SIG(' + k1 + ') && SIG(' + k2 + '))', ['SIG(0)','SIG(0)','SIG(1)'], { sigs: [{ k:k1, ok:true }, { k:k1, ok:false }, { k:k2, ok:true }] }), null) + }) + + it('SIG XHX functions combined should work', () => { + assert.equal(unlock('SIG(' + k1 + ') && XHX(' + Ha + ')', ['SIG(0)', 'XHX(a)'], { sigs: [{ k:k1, ok:true }] }), true) + assert.equal(unlock('SIG(' + k1 + ') && XHX(' + Ha + ')', ['SIG(0)', 'XHX(z)'], { sigs: [{ k:k1, ok:true }] }), null) + assert.equal(unlock('SIG(' + k1 + ') || XHX(' + Ha + ')', ['SIG(0)', 'XHX(a)'], { sigs: [{ k:k1, ok:true }] }), true) + assert.equal(unlock('SIG(' + k1 + ') || XHX(' + Ha + ')', ['SIG(0)', 'XHX(z)'], { sigs: [{ k:k1, ok:true }] }), null) + assert.equal(unlock('SIG(' + k1 + ') || XHX(' + Ha + ')', ['SIG(0)'], { sigs: [{ k:k1, ok:true }] }), true) + assert.equal(unlock('SIG(' + k1 + ') || XHX(' + Ha + ')', ['SIG(0)', 'XHX(z)'], { sigs: [{ k:k1, ok:false }] }), null) + assert.equal(unlock('SIG(' + k1 + ') || XHX(' + Ha + ')', ['SIG(0)', 'XHX(a)'], { sigs: [{ k:k1, ok:false }] }), null) + assert.equal(unlock('SIG(' + k1 + ') || XHX(' + Ha + ')', ['SIG(0)', 'XHX(a)'], { sigs: [{ k:k2, ok:true }] }), true) + }) + + it('SIG, XHX, &&, || words combined should work', () => { + assert.equal(unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', ['SIG(0)', 'XHX(a)', 'XHX(z)'], { sigs: [{ k:k1, ok:true }] }), true) + assert.equal(unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', ['SIG(0)', 'XHX(a)', 'XHX(a)'], { sigs: [{ k:k1, ok:true }] }), true) + assert.equal(unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', ['SIG(0)', 'XHX(z)', 'XHX(a)'], { sigs: [{ k:k1, ok:true }] }), true) // The order does not matter + assert.equal(unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', ['SIG(0)', 'XHX(a)', 'XHX(a)'], { sigs: [{ k:k1, ok:false }] }), null) // H(z) is false + assert.equal(unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', ['SIG(0)', 'XHX(a)', 'XHX(z)'], { sigs: [{ k:k2, ok:true }] }), true) // H(z) is true + assert.equal(unlock('SIG(' + k1 + ') && XHX(' + Ha + ') || XHX(' + Hz + ')', ['SIG(0)', 'XHX(z)', 'XHX(z)'], { sigs: [{ k:k2, ok:true }] }), true) // H(z) is true + assert.equal(unlock('(SIG(EA7Dsw39ShZg4SpURsrgMaMqrweJPUFPYHwZA8e92e3D) || XHX(03AC674216F3E15C761EE1A5E255F067953623C8B388B4459E13F978D7C846F4))', ['SIG(0)', 'XHX(1234)'], { sigs: [{ k:k1, ok:true }] }), true) + }) + + it('CSV+CLTV+1of2SIG', () => { + assert.equal(unlock('(SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd) || SIG(2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc)) && (CSV(10) || CLTV(1500000000))', ['SIG(0)'], { sigs: [{ k:'2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc', ok:true }] }, {"currentTime":1499999999,"elapsedTime":9}), false) + assert.equal(unlock('(SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd) || SIG(2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc)) && (CSV(10) || CLTV(1500000000))', ['SIG(0)'], { sigs: [{ k:'2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc', ok:true }] }, {"currentTime":1499999999,"elapsedTime":10}), true) + assert.equal(unlock('(SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd) || SIG(2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc)) && (CSV(10) || CLTV(1500000000))', ['SIG(0)'], { sigs: [{ k:'2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc', ok:true }] }, {"currentTime":1499999999,"elapsedTime":9}), false) + assert.equal(unlock('(SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd) || SIG(2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc)) && (CSV(10) || CLTV(1500000000))', ['SIG(0)'], { sigs: [{ k:'2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc', ok:true }] }, {"currentTime":1500000000,"elapsedTime":9}), true) + }) + + it('Wrong syntax should return `null`', () => { + assert.equal(unlock('XHX(03AC674216F3E15C761EE1A5E255F067953623C8B388B4459E13F978D7C846F4))', [], { sigs: [] }), null) + }) +}) diff --git a/test/integration/crosschain-test.js b/test/integration/crosschain-test.js index a0e8f32ae..c00e626e5 100644 --- a/test/integration/crosschain-test.js +++ b/test/integration/crosschain-test.js @@ -316,13 +316,13 @@ describe("Crosschain transactions", function() { // 1. toc secretely chooses X password let btx1 = yield tocB.prepareUTX(btx0, ['SIG(0)'], [{ qty: 120, base: 0, lock: '(XHX(8AFC8DF633FC158F9DB4864ABED696C1AA0FE5D617A7B5F7AB8DE7CA2EFCD4CB) && SIG(' + ticB.pub + ')) || (SIG(' + tocB.pub + ') && SIG(' + ticB.pub + '))' }], { comment: 'BETA toc to tic', blockstamp: blockstampB }); // 2. toc makes a rollback transaction from tx1, signed by both parties (through internet): toc and tic - let btx2 = yield tocB.prepareMTX(btx1, ticB, ['XHX(0) SIG(1) SIG(0) SIG(1)'], [{ qty: 120, base: 0, lock: 'SIG(' + tocB.pub + ')' }], { comment: 'money back to tocB in 48h', locktime: 3, blockstamp: blockstampB }); // N.B.: locktime should be like 48h in real world + let btx2 = yield tocB.prepareMTX(btx1, ticB, ['SIG(0) SIG(1)'], [{ qty: 120, base: 0, lock: 'SIG(' + tocB.pub + ')' }], { comment: 'money back to tocB in 48h', locktime: 3, blockstamp: blockstampB }); // N.B.: locktime should be like 48h in real world // TICM side (META) // 3. tic generates a transaction based on H(X) given by toc (through internet) let mtx3 = yield ticM.prepareUTX(mtx0, ['SIG(0)'], [{ qty: 120, base: 0, lock: '(XHX(8AFC8DF633FC158F9DB4864ABED696C1AA0FE5D617A7B5F7AB8DE7CA2EFCD4CB) && SIG(' + tocM.pub + ')) || (SIG(' + ticM.pub + ') && SIG(' + tocM.pub + '))' }], { comment: 'META tic to toc', blockstamp: blockstampM }); // 4. tic makes a rollback transaction from tx1, signed by both parties: toc and tic - let mtx4 = yield ticM.prepareMTX(mtx3, tocM, ['XHX(0) SIG(1) SIG(0) SIG(1)'], [{ qty: 120, base: 0, lock: 'SIG(' + ticM.pub + ')' }], { comment: 'money back to ticM', locktime: 2, blockstamp: blockstampM }); // N.B.: locktime should be like 24h in real world + let mtx4 = yield ticM.prepareMTX(mtx3, tocM, ['SIG(0) SIG(1)'], [{ qty: 120, base: 0, lock: 'SIG(' + ticM.pub + ')' }], { comment: 'money back to ticM', locktime: 2, blockstamp: blockstampM }); // N.B.: locktime should be like 24h in real world // We submit TX1 to the network & write it yield tocB.sendTX(btx1); diff --git a/test/integration/transactions-csv-cltv-sig.ts b/test/integration/transactions-csv-cltv-sig.ts new file mode 100644 index 000000000..4146d97d5 --- /dev/null +++ b/test/integration/transactions-csv-cltv-sig.ts @@ -0,0 +1,76 @@ +import {simpleNodeWith2Users, TestingServer} from "./tools/toolbox" +import {hashf} from "../../app/lib/common" +import {Buid} from "../../app/lib/common-libs/buid" + +describe("Transaction: CSV+CLTV+1of2SIG", function() { + + const now = 1500000000 + const DONT_WAIT_FOR_BLOCKCHAIN_CHANGE = true + let s1:TestingServer + let cat:any + let tac:any + let txHash:string + + const conf = { + nbCores: 1, + sigQty: 1, + udTime0: now, + medianTimeBlocks: 1 // MedianTime(t) = Time(t-1) + } + + before(async () => { + const res1 = await simpleNodeWith2Users(conf) + s1 = res1.s1 + cat = res1.cat + tac = res1.tac + await s1.commit({ time: now }) + await s1.commit({ time: now }) + }) + + it('cat should have 100 units', async () => { + const sources = await s1.get('/tx/sources/' + cat.pub) + sources.should.have.property('sources').length(1) + }) + + it('cat should be able to make a CSV+CLTV+1of2SIG tx', async () => { + const current = await s1.dal.getCurrentBlockOrNull() + const tx = await cat.makeTX([{ + src: '100:0:D:HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd:1', + unlock: 'SIG(0)' + }], [{ + qty: 100, + base: 0, + lock: '(SIG(' + cat.pub + ') || SIG(' + tac.pub + ')) && (CSV(10) || CLTV(' + now + '))' + }], { + blockstamp: Buid.format.buid(current) + }) + txHash = hashf(tx) + await cat.sendTX(tx) + const block = await s1.commit({ time: now }, DONT_WAIT_FOR_BLOCKCHAIN_CHANGE) + block.should.have.property('transactions').length(1) + await s1.waitToHaveBlock(2) + }) + + it('tac should be able to consume the tx after 10s', async () => { + await s1.commit({ time: now + 10 }) + await s1.commit({ time: now + 10 }) + const current = await s1.dal.getCurrentBlockOrNull() + const tx = await tac.makeTX([{ + src: '100:0:T:' + txHash + ':0', + unlock: 'SIG(0)' + }], [{ + qty: 100, + base: 0, + lock: 'SIG(' + tac.pub + ')' + }], { + blockstamp: Buid.format.buid(current) + }) + await tac.sendTX(tx) + const block = await s1.commit(null, DONT_WAIT_FOR_BLOCKCHAIN_CHANGE) + block.should.have.property('transactions').length(1) + }) + + after(async () => { + await s1.closeCluster() + }) +}) diff --git a/test/integration/transactions-test.js b/test/integration/transactions-test.js index ee5cfa156..5592ecf47 100644 --- a/test/integration/transactions-test.js +++ b/test/integration/transactions-test.js @@ -193,8 +193,8 @@ describe("Testing transactions", function() { (yield s1.get('/tx/sources/DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV')).should.have.property('sources').length(4); // As well as tic let tx3 = yield tic.prepareUTX(tx2, ['XHX(1872767826647264) SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + toc.pub + ')' }], { comment: 'wrong', blockstamp: [current.number, current.hash].join('-') }); let tx4 = yield toc.prepareUTX(tx2, ['XHX(1872767826647264) SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + toc.pub + ')' }], { comment: 'ok', blockstamp: [current.number, current.hash].join('-') }); - let tx5 = yield tic.prepareMTX(tx2, toc, ['XHX(1872767826647264) SIG(1) SIG(0) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789) XHX(123456789)'], [{ qty: 1200, base: 0, lock: 'SIG(' + toc.pub + ')' }], { comment: 'multi OK', blockstamp: [current.number, current.hash].join('-') }); - let tx6 = yield toc.prepareMTX(tx2, tic, ['XHX(1872767826647264) SIG(1) SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + toc.pub + ')' }], { comment: 'multi WRONG', blockstamp: [current.number, current.hash].join('-') }); + let tx5 = yield tic.prepareMTX(tx2, toc, ['XHX(1872767826647264) SIG(1) SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + toc.pub + ')' }], { comment: 'multi OK', blockstamp: [current.number, current.hash].join('-') }); + let tx6 = yield toc.prepareMTX(tx2, tic, ['XHX(1872767826647264) SIG(1) SIG(0) SIG(0) SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + toc.pub + ')' }], { comment: 'multi WRONG', blockstamp: [current.number, current.hash].join('-') }); // nLocktime let tx7 = yield tic.prepareMTX(tx2, toc, ['XHX(1872767826647264) SIG(1) SIG(0)'], [{ qty: 1200, base: 0, lock: 'SIG(' + toc.pub + ')' }], { comment: 'wrong locktime', locktime: 100, blockstamp: [current.number, current.hash].join('-') }); yield unit.shouldFail(toc.sendTX(tx3), 'Wrong unlocker in transaction'); -- GitLab