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