Mise à jour effectuée, merci de nous signaler tout dysfonctionnement ! | Upgrade done, please let us know about any dysfunction!

Unverified Commit 11b6c684 authored by Éloïs's avatar Éloïs Committed by GitHub
Browse files

Merge pull request #1201 from duniter/txunlock1.6

[fix] #1200 Redefine unlock conditions
parents d930c305 c186ea8c
......@@ -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
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")
}
......
"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
}
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, [], [], [], [], [], "")
}
......
"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)
}
"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;
}
......
......@@ -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),
......
......@@ -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):