Commit 097d1d7a authored by Cédric Moreau's avatar Cédric Moreau

[enh] Protocol: add BR_G44.2 - allowing certification replay before expiry

parent 2bc114cd
......@@ -146,6 +146,7 @@ module.exports = {
STEPMAX: 3,
SIGDELAY: 3600 * 24 * 365 * 5,
SIGPERIOD: 0, // Instant
SIGREPLAY: 0, // Instant
MSPERIOD: 0, // Instant
SIGSTOCK: 40,
SIGWINDOW: 3600 * 24 * 7, // a week
......
......@@ -396,6 +396,11 @@ export class FileDAL {
return this.blockDAL.getCountOfBlocksIssuedBy(issuer)
}
/**
* Find all the blocks in the blockchain whose number is between [start ; end]
* @param start Lower number bound (included).
* @param end Higher number bound (included).
*/
async getBlocksBetween (start:number, end:number) {
start = Math.max(0, start)
end= Math.max(0, end)
......@@ -878,8 +883,8 @@ export class FileDAL {
.value();
}
existsNonReplayableLink(from:string, to:string) {
return this.cindexDAL.existsNonReplayableLink(from, to)
existsNonReplayableLink(from:string, to:string, medianTime: number, version: number) {
return this.cindexDAL.existsNonReplayableLink(from, to, medianTime, version)
}
async getSource(identifier:string, pos:number, isDividend: boolean): Promise<SimpleTxInput | null> {
......@@ -919,10 +924,13 @@ export class FileDAL {
return (ms && ms.leaving) || false;
}
async existsCert(cert:any) {
async existsCert(cert: DBCert, current: DBBlock|null) {
const existing = await this.certDAL.existsGivenCert(cert);
if (existing) return existing;
const existsLink = await this.cindexDAL.existsNonReplayableLink(cert.from, cert.to);
if (!current) {
return false
}
const existsLink = await this.cindexDAL.existsNonReplayableLink(cert.from, cert.to, current.medianTime, current.version)
return !!existsLink;
}
......
......@@ -15,7 +15,7 @@ export interface CIndexDAO extends ReduceableDAO<CindexEntry> {
findByReceiverAndExpiredOn(pub: string, expired_on: number): Promise<CindexEntry[]>
existsNonReplayableLink(issuer:string, receiver:string): Promise<boolean>
existsNonReplayableLink(issuer:string, receiver:string, medianTime: number, version: number): Promise<boolean>
getReceiversAbove(minsig: number): Promise<string[]>
......
......@@ -88,7 +88,7 @@ export class LevelDBCindex extends LevelDBTable<LevelDBCindexEntry> implements C
issuers = Underscore.uniq(issuers)
await Promise.all(issuers.map(async issuer => {
const entry = await this.get(issuer)
const fullEntries = reduceBy(entry.issued, ['issuer', 'receiver', 'created_on'])
const fullEntries = reduceBy(entry.issued, ['issuer', 'receiver'])
const toRemove: string[] = []
// We remember the maximum value of expired_on, for efficient trimming search
fullEntries
......@@ -190,10 +190,10 @@ export class LevelDBCindex extends LevelDBTable<LevelDBCindexEntry> implements C
}
}
async existsNonReplayableLink(issuer: string, receiver: string): Promise<boolean> {
async existsNonReplayableLink(issuer: string, receiver: string, medianTime: number, version: number): Promise<boolean> {
const entries = await this.findByIssuer(issuer)
const reduced = Indexer.DUP_HELPERS.reduceBy(entries, ['issuer', 'receiver', 'created_on'])
return reduced.filter(e => e.receiver === receiver && !e.expired_on).length > 0
const reduced = Indexer.DUP_HELPERS.reduceBy(entries, ['issuer', 'receiver'])
return reduced.filter(e => e.receiver === receiver && (version <= 10 || e.replayable_on >= medianTime)).length > 0
}
async findByIssuer(issuer: string): Promise<CindexEntry[]> {
......@@ -219,7 +219,7 @@ export class LevelDBCindex extends LevelDBTable<LevelDBCindexEntry> implements C
async findExpiresOnLteNotExpiredYet(medianTime: number): Promise<CindexEntry[]> {
const issuers: string[] = Underscore.uniq((await this.indexForExpiresOn.findAllValues({ lte: LevelDBCindex.trimExpiredOnKey(medianTime) })).reduce(reduceConcat, []))
return (await Promise.all(issuers.map(async issuer => {
const fullEntries = Indexer.DUP_HELPERS.reduceBy((await this.get(issuer)).issued, ['issuer', 'receiver', 'created_on'])
const fullEntries = Indexer.DUP_HELPERS.reduceBy((await this.get(issuer)).issued, ['issuer', 'receiver'])
return fullEntries.filter(e => e.expires_on <= medianTime && !e.expired_on)
}))).reduce(reduceConcat, [])
}
......@@ -229,21 +229,21 @@ export class LevelDBCindex extends LevelDBTable<LevelDBCindexEntry> implements C
}
async getValidLinksFrom(issuer: string): Promise<CindexEntry[]> {
const fullEntries = Indexer.DUP_HELPERS.reduceBy(((await this.getOrNull(issuer)) || { issued: [] }).issued, ['issuer', 'receiver', 'created_on'])
const fullEntries = Indexer.DUP_HELPERS.reduceBy(((await this.getOrNull(issuer)) || { issued: [] }).issued, ['issuer', 'receiver'])
return fullEntries.filter(e => !e.expired_on)
}
async getValidLinksTo(receiver: string): Promise<CindexEntry[]> {
const issuers: string[] = ((await this.getOrNull(receiver)) || { issued: [], received: [] }).received
return (await Promise.all(issuers.map(async issuer => {
const fullEntries = Indexer.DUP_HELPERS.reduceBy((await this.get(issuer)).issued, ['issuer', 'receiver', 'created_on'])
const fullEntries = Indexer.DUP_HELPERS.reduceBy((await this.get(issuer)).issued, ['issuer', 'receiver'])
return fullEntries.filter(e => e.receiver === receiver && !e.expired_on)
}))).reduce(reduceConcat, [])
}
async reducablesFrom(from: string): Promise<FullCindexEntry[]> {
const entries = ((await this.getOrNull(from)) || { issued: [], received: [] }).issued
return Indexer.DUP_HELPERS.reduceBy(entries, ['issuer', 'receiver', 'created_on'])
return Indexer.DUP_HELPERS.reduceBy(entries, ['issuer', 'receiver'])
}
trimExpiredCerts(belowNumber: number): Promise<void> {
......
......@@ -280,7 +280,8 @@ export class BlockDTO implements Cloneable {
udReevalTime0: parseInt(sp[18]),
dtReeval: parseInt(sp[19]),
// New parameter, defaults to msWindow
msPeriod: parseInt(sp[9])
msPeriod: parseInt(sp[9]),
sigReplay: parseInt(sp[9]),
}
}
......
......@@ -44,6 +44,7 @@ export interface CurrencyConfDTO {
dt: number
ud0: number
sigPeriod: number
sigReplay: number
sigStock: number
sigWindow: number
sigValidity: number
......@@ -132,6 +133,7 @@ export class ConfDTO implements StorageDTO, CurrencyConfDTO, KeypairConfDTO, Net
public udReevalTime0: number,
public stepMax: number,
public sigPeriod: number,
public sigReplay: number,
public msPeriod: number,
public sigValidity: number,
public msValidity: number,
......@@ -195,7 +197,7 @@ export class ConfDTO implements StorageDTO, CurrencyConfDTO, KeypairConfDTO, Net
) {}
static mock() {
return new ConfDTO("", "", [], [], 0, 3600 * 1000, constants.PROOF_OF_WORK.DEFAULT.CPU, 1, constants.PROOF_OF_WORK.DEFAULT.PREFIX, 0, 0, constants.CONTRACT.DEFAULT.C, constants.CONTRACT.DEFAULT.DT, constants.CONTRACT.DEFAULT.DT_REEVAL, 0, constants.CONTRACT.DEFAULT.UD0, 0, 0, constants.CONTRACT.DEFAULT.STEPMAX, constants.CONTRACT.DEFAULT.SIGPERIOD, 0, constants.CONTRACT.DEFAULT.SIGVALIDITY, constants.CONTRACT.DEFAULT.MSVALIDITY, constants.CONTRACT.DEFAULT.SIGQTY, constants.CONTRACT.DEFAULT.SIGSTOCK, constants.CONTRACT.DEFAULT.X_PERCENT, constants.CONTRACT.DEFAULT.PERCENTROT, constants.CONTRACT.DEFAULT.POWDELAY, constants.CONTRACT.DEFAULT.AVGGENTIME, constants.CONTRACT.DEFAULT.MEDIANTIMEBLOCKS, false, 3000, false, constants.BRANCHES.DEFAULT_WINDOW_SIZE, constants.CONTRACT.DEFAULT.IDTYWINDOW, constants.CONTRACT.DEFAULT.MSWINDOW, constants.CONTRACT.DEFAULT.SIGWINDOW, 0, { pub:'', sec:'' }, null, "", "", 0, "", "", "", "", 0, "", "", null, false, "", true, true, false, 100, new ProxiesConf(), undefined)
return new ConfDTO("", "", [], [], 0, 3600 * 1000, constants.PROOF_OF_WORK.DEFAULT.CPU, 1, constants.PROOF_OF_WORK.DEFAULT.PREFIX, 0, 0, constants.CONTRACT.DEFAULT.C, constants.CONTRACT.DEFAULT.DT, constants.CONTRACT.DEFAULT.DT_REEVAL, 0, constants.CONTRACT.DEFAULT.UD0, 0, 0, constants.CONTRACT.DEFAULT.STEPMAX, constants.CONTRACT.DEFAULT.SIGPERIOD, constants.CONTRACT.DEFAULT.SIGREPLAY, 0, constants.CONTRACT.DEFAULT.SIGVALIDITY, constants.CONTRACT.DEFAULT.MSVALIDITY, constants.CONTRACT.DEFAULT.SIGQTY, constants.CONTRACT.DEFAULT.SIGSTOCK, constants.CONTRACT.DEFAULT.X_PERCENT, constants.CONTRACT.DEFAULT.PERCENTROT, constants.CONTRACT.DEFAULT.POWDELAY, constants.CONTRACT.DEFAULT.AVGGENTIME, constants.CONTRACT.DEFAULT.MEDIANTIMEBLOCKS, false, 3000, false, constants.BRANCHES.DEFAULT_WINDOW_SIZE, constants.CONTRACT.DEFAULT.IDTYWINDOW, constants.CONTRACT.DEFAULT.MSWINDOW, constants.CONTRACT.DEFAULT.SIGWINDOW, 0, { pub:'', sec:'' }, null, "", "", 0, "", "", "", "", 0, "", "", null, false, "", true, true, false, 100, new ProxiesConf(), undefined)
}
static defaultConf() {
......@@ -211,6 +213,7 @@ export class ConfDTO implements StorageDTO, CurrencyConfDTO, KeypairConfDTO, Net
"ud0": constants.CONTRACT.DEFAULT.UD0,
"stepMax": constants.CONTRACT.DEFAULT.STEPMAX,
"sigPeriod": constants.CONTRACT.DEFAULT.SIGPERIOD,
"sigReplay": constants.CONTRACT.DEFAULT.SIGREPLAY,
"sigValidity": constants.CONTRACT.DEFAULT.SIGVALIDITY,
"msValidity": constants.CONTRACT.DEFAULT.MSVALIDITY,
"sigQty": constants.CONTRACT.DEFAULT.SIGQTY,
......
......@@ -121,6 +121,7 @@ export interface CindexEntry extends IndexEntry {
created_on: number,
sig: string,
chainable_on: number,
replayable_on: number,
expires_on: number,
expired_on: number,
from_wid: null, // <-These 2 fields are useless
......@@ -133,6 +134,7 @@ export interface CindexEntry extends IndexEntry {
toNewcomer?: boolean,
toLeaver?: boolean,
isReplay?: boolean,
isReplayable?: boolean,
sigOK?: boolean,
created_on_ref?: { medianTime: number },
}
......@@ -145,6 +147,7 @@ export interface FullCindexEntry {
chainable_on: number
expires_on: number
expired_on: number
replayable_on: number
}
export interface SindexEntry extends IndexEntry {
......@@ -258,6 +261,7 @@ export class Indexer {
msValidity:number,
msPeriod:number,
sigPeriod:number,
sigReplay:number,
sigStock:number
}): IndexEntry[] {
......@@ -473,6 +477,7 @@ export class Indexer {
unchainables: 0,
sig: cert.sig,
chainable_on: block.medianTime + conf.sigPeriod,
replayable_on: block.medianTime + conf.sigReplay,
expires_on: conf.sigValidity,
expired_on: 0,
from_wid: null,
......@@ -970,10 +975,17 @@ export class Indexer {
ENTRY.toLeaver = !!(reduce(await dal.mindexDAL.reducable(ENTRY.receiver)).leaving)
}))
// BR_G44
// BR_G44 + 44.2
await Promise.all(cindex.map(async (ENTRY: CindexEntry) => {
const reducable = await dal.cindexDAL.findByIssuerAndReceiver(ENTRY.issuer, ENTRY.receiver)
ENTRY.isReplay = count(reducable) > 0 && reduce(reducable).expired_on === 0
if (HEAD.number > 0 && HEAD_1.version > 10) {
ENTRY.isReplayable = count(reducable) === 0 || reduce(reducable).replayable_on < HEAD_1.medianTime
}
else {
// v10 blocks do not allow certification replay
ENTRY.isReplayable = false
}
}))
// BR_G45
......@@ -1477,7 +1489,7 @@ export class Indexer {
// BR_G71
static ruleCertificationReplay(cindex: CindexEntry[]) {
for (const ENTRY of cindex) {
if (ENTRY.isReplay) return false;
if (ENTRY.isReplay && !ENTRY.isReplayable) return false
}
return true
}
......
......@@ -29,6 +29,7 @@ module.exports = {
config: {
onLoading: async (conf:ConfDTO, program: ProgramOptions) => {
conf.msPeriod = conf.msWindow
conf.sigReplay = conf.msPeriod
conf.switchOnHeadAdvance = CommonConstants.SWITCH_ON_BRANCH_AHEAD_BY_X_BLOCKS
// Transactions storage
......@@ -43,6 +44,7 @@ module.exports = {
},
beforeSave: async (conf:ConfDTO) => {
conf.msPeriod = conf.msWindow
conf.sigReplay = conf.msPeriod
conf.switchOnHeadAdvance = CommonConstants.SWITCH_ON_BRANCH_AHEAD_BY_X_BLOCKS
if (!conf.storage) {
conf.storage = {
......
......@@ -410,7 +410,7 @@ export class BlockGenerator {
}
}
// Already exists a link not replayable yet?
let exists = await this.dal.existsNonReplayableLink(cert.from, cert.to);
let exists = await this.dal.existsNonReplayableLink(cert.from, cert.to, current.medianTime, current.version)
if (exists) {
throw 'It already exists a similar certification written, which is not replayable yet';
}
......@@ -756,7 +756,7 @@ class NextBlockGenerator implements BlockGeneratorInterface {
let exists = false;
if (current) {
// Already exists a link not replayable yet?
exists = await this.dal.existsNonReplayableLink(cert.from, cert.to);
exists = await this.dal.existsNonReplayableLink(cert.from, cert.to, current.medianTime, current.version)
}
if (!exists) {
// Already exists a link not chainable yet?
......
......@@ -227,7 +227,7 @@ export class IdentityService extends FIFOService {
written_hash: null,
block: cert.block_number
}
let existingCert = await this.dal.existsCert(mCert);
let existingCert = await this.dal.existsCert(mCert, current)
if (!existingCert) {
if (!(await this.dal.certDAL.getSandboxForKey(cert.from).acceptNewSandBoxEntry(mCert, this.conf.pair && this.conf.pair.pub))) {
throw constants.ERRORS.SANDBOX_FOR_CERT_IS_FULL;
......
......@@ -1137,6 +1137,7 @@ udTime0 | Time of first UD.
udReevalTime0 | Time of first reevaluation of the UD.
sigPeriod | Minimum delay between 2 certifications of a same issuer, in seconds. Must be positive or zero.
msPeriod | Minimum delay between 2 memberships of a same issuer, in seconds. Must be positive or zero.
sigReplay | Minimum delay between 2 certifications of a same issuer to a same receiver, in seconds. Equals to `msPeriod`.
sigStock | Maximum quantity of active certifications made by member.
sigWindow | Maximum delay a certification can wait before being expired for non-writing.
sigValidity | Maximum age of an active signature (in seconds)
......@@ -1391,6 +1392,7 @@ Each certification produces 1 new entry:
sig = SIGNATURE
expires_on = MedianTime + sigValidity
chainable_on = MedianTime + sigPeriod
replayable_on = MedianTime + sigReplay
expired_on = 0
)
......@@ -2274,6 +2276,25 @@ If `count(reducable) == 0`:
Else:
ENTRY.isReplay = reduce(reducable).expired_on == 0
####### BR_G44.2 - ENTRY.isReplayable
If `HEAD.number > 0 && HEAD~1.version > 10` :
reducable = GLOBAL_CINDEX[issuer=ENTRY.issuer,receiver=ENTRY.receiver,expired_on=0]
If `count(reducable) == 0`:
ENTRY.isReplayable = true
Else:
ENTRY.isReplayable = reduce(reducable).replayable_on < HEAD~1.medianTime
Else:
ENTRY.isReplayable = false
EndIf
####### BR_G45 - ENTRY.sigOK
......@@ -2519,7 +2540,7 @@ Rule:
Rule:
ENTRY.isReplay == false
ENTRY.isReplay == false || ENTRY.isReplayable == true
###### BR_G72 - Certification signature
......
......@@ -103,7 +103,8 @@ describe("v1.0 Local Index", function(){
// We don't care about these in this test
msPeriod: 0,
sigPeriod: 0,
sigStock: 0
sigStock: 0,
sigReplay: 0,
});
});
......
// Source file from duniter: Crypto-currency software to manage libre currency such as Ğ1
// Copyright (C) 2018 Cedric Moreau <cem.moreau@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
import {assertEqual, writeBasicTestWithConfAnd2Users} from "../tools/test-framework"
import {assertThrows} from "../../unit-tools"
import {reduce} from "../../../app/lib/indexer"
describe('Certification replay', () => writeBasicTestWithConfAnd2Users({
sigReplay: 3,
sigPeriod: 0,
sigValidity: 10,
}, (test) => {
const now = 1500000000
test('should be able to init with 2 blocks', async (s1, cat, tac) => {
await cat.createIdentity()
await tac.createIdentity()
await cat.cert(tac)
await tac.cert(cat)
await cat.join()
await tac.join()
await s1.commit({ time: now, version: 10 })
await s1.commit({ time: now })
})
test('should exist only 1 valid link from cat replyable at t + 3', async (s1, cat) => {
const reducableFromCat = await s1.dal.cindexDAL.reducablesFrom(cat.pub)
assertEqual(reducableFromCat.length, 1)
assertEqual(reduce(reducableFromCat).chainable_on, now) // No delay between certifications
assertEqual(reduce(reducableFromCat).replayable_on, now + 3) // Replay of a particular certification <-- What we want to test
assertEqual(reduce(reducableFromCat).expires_on, now + 10) // The expiration date of the certification **INITIALLY**
})
test('should reject a replay from cat', async (s1, cat, tac) => {
await assertThrows(cat.cert(tac), '{\n "ucode": 1004,\n "message": "Already up-to-date"\n}')
})
test('should accept replay if time has passed enought', async (s1, cat, tac) => {
await s1.commit({ time: now + 4 })
await s1.commit({ time: now + 4 })
await cat.cert(tac)
const b = await s1.commit({ time: now + 4 })
assertEqual(b.certifications.length, 1)
})
test('should exist only 2 CINDEX entries from cat', async (s1, cat) => {
const validLinksFromCat = await s1.dal.cindexDAL.findByIssuer(cat.pub)
assertEqual(validLinksFromCat.length, 2)
})
test('should exist only 1 valid link from cat', async (s1, cat) => {
const reducableFromCat = await s1.dal.cindexDAL.getValidLinksFrom(cat.pub)
assertEqual(reducableFromCat.length, 1)
assertEqual(reduce(reducableFromCat).chainable_on, now + 4)
assertEqual(reduce(reducableFromCat).replayable_on, now + 4 + 3) // Replayable date should have changed!
assertEqual(reduce(reducableFromCat).expires_on, now + 4 + 10) // The expiration date should have changed! (this is the interest of a replay)
})
}))
import {catUser, NewTestingServer, tacUser, TestingServer, tocUser} from "./toolbox"
import {TestUser} from "./TestUser"
import * as assert from 'assert'
import {Underscore} from "../../../app/lib/common-libs/underscore"
export function writeBasicTestWith2Users(writeTests: (
test: (
......@@ -9,16 +10,30 @@ export function writeBasicTestWith2Users(writeTests: (
) => void
) => void) {
writeBasicTestWithConfAnd2Users({}, writeTests)
}
export function writeBasicTestWithConfAnd2Users(conf: {
}, writeTests: (
test: (
testTitle: string,
fn: (server: TestingServer, cat: TestUser, tac: TestUser, toc: TestUser) => Promise<void>
) => void
) => void) {
let s1:TestingServer, cat:TestUser, tac:TestUser, toc:TestUser
const configuration = Underscore.extend({
medianTimeBlocks: 1,
pair: {
pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd',
sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP'
}
}, conf)
before(async () => {
s1 = NewTestingServer({
medianTimeBlocks: 1,
pair: {
pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd',
sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP'
}
})
s1 = NewTestingServer(configuration)
cat = catUser(s1)
tac = tacUser(s1)
toc = tocUser(s1)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment