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