From c215c165e5e8a0a44ce052a79325bf2c1283635d Mon Sep 17 00:00:00 2001
From: cgeek <cem.moreau@gmail.com>
Date: Thu, 16 Mar 2017 18:20:45 +0100
Subject: [PATCH] [enh] #894 + #895 Add CSV + CLTV functions

---
 app/lib/constants.js                  |  8 ++--
 app/lib/dup/indexer.js                | 18 ++++++--
 app/lib/rules/global_rules.js         | 15 +++++--
 app/lib/ucp/txunlock.js               | 13 +++++-
 doc/Protocol.md                       | 58 +++++++++++++++++++++-----
 test/integration/transactions-cltv.js | 59 +++++++++++++++++++++++++++
 test/integration/transactions-csv.js  | 59 +++++++++++++++++++++++++++
 7 files changed, 209 insertions(+), 21 deletions(-)
 create mode 100644 test/integration/transactions-cltv.js
 create mode 100644 test/integration/transactions-csv.js

diff --git a/app/lib/constants.js b/app/lib/constants.js
index 9af6ffbfa..4a452cd3e 100644
--- a/app/lib/constants.js
+++ b/app/lib/constants.js
@@ -10,6 +10,8 @@ const POSITIVE_INT = "[1-9][0-9]{0,18}";
 const DIVIDEND     = "[1-9][0-9]{0,5}";
 const ZERO_OR_POSITIVE_INT = "0|[1-9][0-9]{0,18}";
 const INTEGER      = "(0|[1-9]\\d{0,18})";
+const CLTV_INTEGER = "([0-9]{1,10})";
+const CSV_INTEGER  = "([0-9]{1,8})";
 const XUNLOCK      = "[a-zA-Z0-9]{1,64}";
 const RELATIVE_INTEGER = "(0|-?[1-9]\\d{0,18})";
 const FLOAT        = "\\d+\.\\d+";
@@ -19,9 +21,8 @@ const TX_VERSION   = "(10)";
 const SIGNATURE    = "[A-Za-z0-9+\\/=]{87,88}";
 const FINGERPRINT  = "[A-F0-9]{64}";
 const COMMENT      = "[ a-zA-Z0-9-_:/;*\\[\\]()?!^\\+=@&~#{}|\\\\<>%.]{0,255}";
-const CONDITIONS   = "(&&|\\|\\|| |[()]|(SIG\\([0-9a-zA-Z]{43,44}\\)|(XHX\\([A-F0-9]{64}\\))))*";
-//const CONDITIONS   = "(&&|\|\|| |[()]|(SIG\\(\\da-zA-Z\\))|(XHX\\(" + FINGERPRINT + "\\)))*";
 const UNLOCK       = "(SIG\\(" + INTEGER + "\\)|XHX\\(" + XUNLOCK + "\\))";
+const CONDITIONS   = "(&&|\\|\\|| |[()]|(SIG\\([0-9a-zA-Z]{43,44}\\)|(XHX\\([A-F0-9]{64}\\)|CLTV\\(" + CLTV_INTEGER + "\\)|CSV\\(" + CSV_INTEGER + "\\))))*";
 const BLOCK_UID    = INTEGER + "-" + FINGERPRINT;
 const META_TS      = "META:TS:" + BLOCK_UID;
 
@@ -208,7 +209,8 @@ module.exports = {
     BLOCKSTAMP:find('Blockstamp: (' + BLOCK_UID + ')'),
     COMMENT: find("Comment: (" + COMMENT + ")"),
     LOCKTIME:find("Locktime: (" + INTEGER + ")"),
-    INLINE_COMMENT: exact(COMMENT)
+    INLINE_COMMENT: exact(COMMENT),
+    OUTPUT_CONDITION: exact(CONDITIONS)
   },
   PEER: {
     BLOCK: find("Block: (" + INTEGER + "-" + FINGERPRINT + ")"),
diff --git a/app/lib/dup/indexer.js b/app/lib/dup/indexer.js
index cccce2bac..eb824f157 100644
--- a/app/lib/dup/indexer.js
+++ b/app/lib/dup/indexer.js
@@ -722,7 +722,7 @@ const indexer = module.exports = {
 
     // BR_G47
     yield _.where(sindex, { op: constants.IDX_UPDATE }).map((ENTRY) => co(function*() {
-      let source = _.filter(sindex, (src) => src.identifier == ENTRY.identifier && src.pos == ENTRY.pos && src.conditions)[0];
+      let source = _.filter(sindex, (src) => src.identifier == ENTRY.identifier && src.pos == ENTRY.pos && src.conditions && src.op === constants.IDX_CREATE)[0];
       if (!source) {
         const reducable = yield dal.sindexDAL.sqlFind({
           identifier: ENTRY.identifier,
@@ -733,7 +733,7 @@ const indexer = module.exports = {
         source = reduce(reducable);
       }
       ENTRY.conditions = source.conditions;
-      ENTRY.isLocked = !txSourceUnlock(ENTRY, source);
+      ENTRY.isLocked = !txSourceUnlock(ENTRY, source, HEAD);
     }));
 
     // BR_G48
@@ -1732,10 +1732,11 @@ function checkCertificationIsValid (block, cert, findIdtyFunc, conf, dal) {
   });
 }
 
-function txSourceUnlock(ENTRY, source) {
+function txSourceUnlock(ENTRY, source, HEAD) {
   const tx = ENTRY.txObj;
   let sigResults = require('../rules/local_rules').HELPERS.getSigResult(tx, 'a');
   let unlocksForCondition = [];
+  let unlocksMetadata = {};
   let unlockValues = ENTRY.unlock;
   if (source.conditions) {
     if (unlockValues) {
@@ -1758,7 +1759,16 @@ function txSourceUnlock(ENTRY, source) {
         }
       }
     }
-    if (unlock(source.conditions, unlocksForCondition)) {
+
+    if (source.conditions.match(/CLTV/)) {
+      unlocksMetadata.currentTime = HEAD.medianTime;
+    }
+
+    if (source.conditions.match(/CSV/)) {
+      unlocksMetadata.elapsedTime = HEAD.medianTime - parseInt(source.written_time);
+    }
+
+    if (unlock(source.conditions, unlocksForCondition, unlocksMetadata)) {
       return true;
     }
   }
diff --git a/app/lib/rules/global_rules.js b/app/lib/rules/global_rules.js
index 4dc3c55eb..6ba691b29 100644
--- a/app/lib/rules/global_rules.js
+++ b/app/lib/rules/global_rules.js
@@ -84,6 +84,7 @@ rules.FUNCTIONS = {
         }
         let sigResults = local_rules.HELPERS.getSigResult(tx);
         let unlocksForCondition = [];
+        let unlocksMetadata = {};
         let unlockValues = unlocks[k];
         if (dbSrc.conditions) {
           if (unlockValues) {
@@ -101,14 +102,22 @@ rules.FUNCTIONS = {
                   pubkey: pubkey,
                   sigOK: sigResults.sigs[pubkey] && sigResults.sigs[pubkey].matching || false
                 });
-              } else {
-                // XHX
+              } else if (func.match(/^XHX/)) {
                 unlocksForCondition.push(param);
               }
             }
           }
+
+          if (dbSrc.conditions.match(/CLTV/)) {
+            unlocksMetadata.currentTime = block.medianTime;
+          }
+
+          if (dbSrc.conditions.match(/CSV/)) {
+            unlocksMetadata.elapsedTime = block.medianTime - dbSrc.written_time;
+          }
+
           try {
-            if (!unlock(dbSrc.conditions, unlocksForCondition)) {
+            if (!unlock(dbSrc.conditions, unlocksForCondition, unlocksMetadata)) {
               throw Error('Locked');
             }
           } catch (e) {
diff --git a/app/lib/ucp/txunlock.js b/app/lib/ucp/txunlock.js
index beacb2a85..cab1219e4 100644
--- a/app/lib/ucp/txunlock.js
+++ b/app/lib/ucp/txunlock.js
@@ -12,8 +12,11 @@ let grammar = {
       ["\\(",                     "return '(';"],
       ["\\)",                     "return ')';"],
       ["[0-9A-Za-z]{40,64}",      "return 'PARAMETER';"],
+      ["[0-9]{1,10}",             "return 'PARAMETER';"],
       ["SIG",                     "return 'SIG';"],
       ["XHX",                     "return 'XHX';"],
+      ["CLTV",                    "return 'CLTV';"],
+      ["CSV",                     "return 'CSV';"],
       ["$",                       "return 'EOF';"]
     ]
   },
@@ -32,6 +35,8 @@ let grammar = {
       [ "e OR e",  "$$ = $1 || $3;" ],
       [ "SIG ( e )","$$ = yy.sig($3);"],
       [ "XHX ( e )","$$ = yy.xHx($3);"],
+      [ "CLTV ( e )","$$ = yy.cltv($3);"],
+      [ "CSV ( e )","$$ = yy.csv($3);"],
       [ "PARAMETER", "$$ = $1;" ],
       [ "( e )",   "$$ = $2;" ]
     ]
@@ -40,7 +45,7 @@ let grammar = {
 
 let logger = require('../logger')('unlock');
 
-module.exports = function unlock(conditionsStr, executions) {
+module.exports = function unlock(conditionsStr, executions, metadata) {
 
   let parser = new Parser(grammar);
 
@@ -53,6 +58,12 @@ module.exports = function unlock(conditionsStr, executions) {
     xHx: function(hash) {
       let xhxParam = executions[this.i++];
       return ucp.format.hashf(xhxParam) === hash;
+    },
+    cltv: function(deadline) {
+      return metadata.currentTime && metadata.currentTime >= parseInt(deadline);
+    },
+    csv: function(amountToWait) {
+      return metadata.elapsedTime && metadata.elapsedTime >= parseInt(amountToWait);
     }
   };
 
diff --git a/doc/Protocol.md b/doc/Protocol.md
index 4a12171e8..e1430e7fc 100644
--- a/doc/Protocol.md
+++ b/doc/Protocol.md
@@ -437,8 +437,8 @@ If no values are provided, the valid input condition is an empty string.
 It follows a machine-readable BNF grammar composed of
 
 * `(` and `)` characters
-* `AND` and `OR` operators
-* `SIG(PUBLIC_KEY)`, `XHX(INTEGER)` functions
+* `&&` and `||` operators
+* `SIG(PUBLIC_KEY)`, `XHX(SHA256_HASH)`, `CLTV(INTEGER)`, `CSV(INTEGER)` functions
 * ` ` space
 
 **An empty condition or a condition fully composed of spaces is considered an invalid output condition**.
@@ -446,8 +446,10 @@ It follows a machine-readable BNF grammar composed of
 ##### Output condition examples
 
 * `SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd)`
-* `(SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd) AND XHX(309BC5E644F797F53E5A2065EAF38A173437F2E6))`
-* `(SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd) OR (SIG(DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV) AND XHX(309BC5E644F797F53E5A2065EAF38A173437F2E6)))`
+* `(SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd) && XHX(309BC5E644F797F53E5A2065EAF38A173437F2E6))`
+* `(SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd) && CSV(3600))`
+* `(SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd) && (CLTV(1489677041) || CSV(3600)))`
+* `(SIG(HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd) || (SIG(DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV) && XHX(309BC5E644F797F53E5A2065EAF38A173437F2E6)))`
 
 #### Condition matching
 
@@ -492,14 +494,14 @@ Is resolved by TX2:
 
 Because `SIG(1)` refers to the signature `DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo`, considering that signature `DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo` is good over TX2.
 
-#### SIG and XHX functions
+#### Unlocking functions
 
-These functions are present under both `Unlocks` and `Outputs` fields.
+These functions may be present under both `Unlocks` and `Outputs` fields.
 
 * 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.
 
-##### SIG example
+##### SIG function
 
 This function is a control over the signature.
 
@@ -540,7 +542,7 @@ The necessary condition `SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g)` is m
 * `Issuers[0] = BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g`
 
 
-##### XHX example
+##### XHX function
 
 This function is a password control.
 
@@ -653,7 +655,7 @@ Key `HsLShA`,  `CYYjHs` and `9WYHTa` sending 235 coins to key `BYfWYF` using 4 s
     Outputs:
     120:2:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g)
     146:2:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx)
-    49:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i) OR XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85))
+    49:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i) || XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85))
     Comment: -----@@@----- (why not this comment?)
 
 Signatures (fakes here):
@@ -662,6 +664,42 @@ Signatures (fakes here):
     2D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX
     2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk
 
+##### CLTV function
+
+This function locks an ouput in the future, which will be unlocked at a given date.
+
+So if we have, in TX1:
+
+    Version: 10
+    Type: Transaction
+    [...]
+	Outputs
+    25:2:CLTV(1489677041)
+
+Then 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
+
+This function locks an ouput in the future, which will be unlocked after the given amount of time has elapsed.
+
+So if we have, in TX1:
+
+    Version: 10
+    Type: Transaction
+    Currency: beta_brousouf
+    Blockstamp: 204-00003E2B8A35370BA5A7064598F628A62D4E9EC1936BE8651CE9A85F2E06981B
+    [...]
+	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.
+
 #### Compact format
 
 A transaction may be described with a more compact format, to be used in a [Block](#block) document. The general format is:
@@ -715,7 +753,7 @@ Here is an example compacting [example 3](#example-3) from above:
     5:SIG(2)
     120:2:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g)
     146:2:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx)
-    49:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i) OR XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85))
+    49:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i) || XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85))
     -----@@@----- (why not this comment?)
     42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r
     2D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX
diff --git a/test/integration/transactions-cltv.js b/test/integration/transactions-cltv.js
new file mode 100644
index 000000000..f2a955c4e
--- /dev/null
+++ b/test/integration/transactions-cltv.js
@@ -0,0 +1,59 @@
+"use strict";
+
+const co = require('co');
+const _ = require('underscore');
+const should = require('should');
+const assert = require('assert');
+const constants = require('../../app/lib/constants');
+const bma       = require('duniter-bma').duniter.methods.bma;
+const toolbox   = require('./tools/toolbox');
+const node   = require('./tools/node');
+const unit   = require('./tools/unit');
+const http   = require('./tools/http');
+
+const now = 1480000000;
+
+const conf = {
+  dt: 1000,
+  ud0: 200,
+  udTime0: now - 1, // So we have a UD right on block#1
+  medianTimeBlocks: 1 // Easy: medianTime(b) = time(b-1)
+};
+
+let s1, cat, tac
+
+describe("Transactions: CLTV", function() {
+
+  before(() => co(function*() {
+    const res = yield toolbox.simpleNodeWith2Users(conf);
+    s1 = res.s1;
+    cat = res.cat;
+    tac = res.tac;
+    yield s1.commit({ time: now });
+    yield s1.commit({ time: now + 1 });
+  }));
+
+  it('it should exist block#1 with UD of 200', () => s1.expect('/blockchain/block/1', (block) => {
+    should.exists(block);
+    assert.equal(block.number, 1);
+    assert.equal(block.dividend, 200);
+  }));
+
+  it('with SIG and XHX', () => co(function *() {
+    let tx1 = yield cat.prepareITX(200, tac);
+    yield unit.shouldNotFail(cat.sendTX(tx1));
+    yield s1.commit({ time: now + 19 }); // TODO: why not in the same block?
+    let current = yield s1.get('/blockchain/current');
+    let tx2 = yield tac.prepareUTX(tx1, ['SIG(0)'], [{ qty: 200, base: 0, lock: 'SIG(' + cat.pub + ') && CLTV(1480000022)' }], {
+      comment: 'must wait until time 1480000022',
+      blockstamp: [current.number, current.hash].join('-')
+    });
+    yield unit.shouldNotFail(cat.sendTX(tx2));
+    yield s1.commit({ time: now + 21 }); // TODO: why not in the same block?
+    let tx3 = yield cat.prepareITX(200, tac);
+    yield unit.shouldFail(cat.sendTX(tx3), 'Wrong unlocker in transaction');
+    yield s1.commit({ time: now + 22 });
+    yield unit.shouldNotFail(cat.sendTX(tx3)); // Because next block will have medianTime = 22
+    yield s1.commit({ time: now + 22 });
+  }));
+});
diff --git a/test/integration/transactions-csv.js b/test/integration/transactions-csv.js
new file mode 100644
index 000000000..7a176270f
--- /dev/null
+++ b/test/integration/transactions-csv.js
@@ -0,0 +1,59 @@
+"use strict";
+
+const co = require('co');
+const _ = require('underscore');
+const should = require('should');
+const assert = require('assert');
+const constants = require('../../app/lib/constants');
+const bma       = require('duniter-bma').duniter.methods.bma;
+const toolbox   = require('./tools/toolbox');
+const node   = require('./tools/node');
+const unit   = require('./tools/unit');
+const http   = require('./tools/http');
+
+const now = 1480000000;
+
+const conf = {
+  dt: 1000,
+  ud0: 200,
+  udTime0: now - 1, // So we have a UD right on block#1
+  medianTimeBlocks: 1 // Easy: medianTime(b) = time(b-1)
+};
+
+let s1, cat, tac
+
+describe("Transactions: CSV", function() {
+
+  before(() => co(function*() {
+    const res = yield toolbox.simpleNodeWith2Users(conf);
+    s1 = res.s1;
+    cat = res.cat;
+    tac = res.tac;
+    yield s1.commit({ time: now });
+    yield s1.commit({ time: now + 1 });
+  }));
+
+  it('it should exist block#1 with UD of 200', () => s1.expect('/blockchain/block/1', (block) => {
+    should.exists(block);
+    assert.equal(block.number, 1);
+    assert.equal(block.dividend, 200);
+  }));
+
+  it('with SIG and XHX', () => co(function *() {
+    let tx1 = yield cat.prepareITX(200, tac);
+    yield unit.shouldNotFail(cat.sendTX(tx1));
+    yield s1.commit({ time: now + 19 }); // TODO: why not in the same block?
+    let current = yield s1.get('/blockchain/current');
+    let tx2 = yield tac.prepareUTX(tx1, ['SIG(0)'], [{ qty: 200, base: 0, lock: 'SIG(' + cat.pub + ') && CSV(20)' }], {
+      comment: 'must wait 20 seconds',
+      blockstamp: [current.number, current.hash].join('-')
+    });
+    yield unit.shouldNotFail(cat.sendTX(tx2));
+    yield s1.commit({ time: now + 38 }); // TODO: why not in the same block?
+    let tx3 = yield cat.prepareITX(200, tac);
+    yield unit.shouldFail(cat.sendTX(tx3), 'Wrong unlocker in transaction');
+    yield s1.commit({ time: now + 39 });
+    yield unit.shouldNotFail(cat.sendTX(tx3)); // Because next block will have medianTime = 39
+    yield s1.commit({ time: now + 39 });
+  }));
+});
-- 
GitLab