diff --git a/app/lib/common-libs/constants.ts b/app/lib/common-libs/constants.ts index b9176d89a3cf765caf0f1f31924c7c7abd0e01e5..67e5f12c053daa6058dee571296c45aa5e18d076 100644 --- a/app/lib/common-libs/constants.ts +++ b/app/lib/common-libs/constants.ts @@ -7,16 +7,20 @@ const SIGNATURE = "[A-Za-z0-9+\\/=]{87,88}" const USER_ID = "[A-Za-z0-9_-]{2,100}" const INTEGER = "(0|[1-9]\\d{0,18})" const FINGERPRINT = "[A-F0-9]{64}" -const BLOCK_UID = INTEGER + "-" + FINGERPRINT const BLOCK_VERSION = "(10)" const TX_VERSION = "(10)" const DIVIDEND = "[1-9][0-9]{0,5}" const ZERO_OR_POSITIVE_INT = "0|[1-9][0-9]{0,18}" +const BLOCK_UID = "(" + ZERO_OR_POSITIVE_INT + ")-" + FINGERPRINT const RELATIVE_INTEGER = "(0|-?[1-9]\\d{0,18})" const FLOAT = "\\d+\.\\d+" const POSITIVE_INT = "[1-9][0-9]{0,18}" const TIMESTAMP = "[1-9][0-9]{0,18}" const BOOLEAN = "[01]" +const WS2PID = "[0-9a-f]{8}" +const SOFTWARE = "[a-z0-9]{2,15}" +const SOFT_VERSION = "[0-9a-z.-_]{2,15}" +const POW_PREFIX = "([1-9]|[1-9][0-9]|[1-8][0-9][0-9])" // 1-899 const SPECIAL_BLOCK = '0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855' const META_TS = "META:TS:" + BLOCK_UID const COMMENT = "[ a-zA-Z0-9-_:/;*\\[\\]()?!^\\+=@&~#{}|\\\\<>%.]{0,255}" @@ -68,7 +72,12 @@ export const CommonConstants = { INTEGER, BLOCKSTAMP: BLOCK_UID, FINGERPRINT, - TIMESTAMP + TIMESTAMP, + WS2PID, + SOFTWARE, + SOFT_VERSION, + POW_PREFIX, + ZERO_OR_POSITIVE_INT }, BLOCK_GENERATED_VERSION: 10, diff --git a/app/lib/common/package.ts b/app/lib/common/package.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f2b557374efed2c239a580a1e2723797562750e --- /dev/null +++ b/app/lib/common/package.ts @@ -0,0 +1,22 @@ + +export class Package { + + private json:{ version:string } + + private constructor() { + this.json = require('../../../package.json') + } + + get version() { + return this.json.version + } + + private static instance:Package + + static getInstance() { + if (!Package.instance) { + Package.instance = new Package() + } + return Package.instance + } +} \ No newline at end of file diff --git a/app/modules/ws2p/lib/WS2PCluster.ts b/app/modules/ws2p/lib/WS2PCluster.ts index a860036fd6d388157ebdeff39505e7558e9fea29..5543a640bb62425a8da21202ff419a30355418ac 100644 --- a/app/modules/ws2p/lib/WS2PCluster.ts +++ b/app/modules/ws2p/lib/WS2PCluster.ts @@ -14,6 +14,8 @@ import {Key, verify} from "../../../lib/common-libs/crypto/keyring" import {WS2PServerMessageHandler} from "./interface/WS2PServerMessageHandler" import {WS2PMessageHandler} from "./impl/WS2PMessageHandler" import { CommonConstants } from "../../../lib/common-libs/constants"; +import { Package } from "../../../lib/common/package"; +import { Constants } from "../../prover/lib/constants"; const es = require('event-stream') const nuuid = require('node-uuid') @@ -57,7 +59,7 @@ export class WS2PCluster { private memberkeysCache:{ [k:string]: number } = {} // A cache of the current HEAD for a given pubkey - private headsCache:{ [pubkey:string]: { blockstamp:string, message:string, sig:string } } = {} + private headsCache:{ [ws2pFullId:string]: { blockstamp:string, message:string, sig:string } } = {} // A buffer of "to be sent" heads private newHeads:{ message:string, sig:string }[] = [] @@ -75,19 +77,21 @@ export class WS2PCluster { async getKnownHeads(): Promise<WS2PHead[]> { const heads:WS2PHead[] = [] + const ws2pId = (this.server.conf.ws2p && this.server.conf.ws2p.uuid) || '000000' const localPub = this.server.conf.pair.pub - if (!this.headsCache[localPub]) { + const fullId = [localPub, ws2pId].join('-') + if (!this.headsCache[fullId]) { const current = await this.server.dal.getCurrentBlockOrNull() if (current) { const { sig, message } = this.sayHeadChangedTo(current.number, current.hash) const blockstamp = [current.number, current.hash].join('-') - this.headsCache[localPub] = { blockstamp, message, sig } + this.headsCache[fullId] = { blockstamp, message, sig } } } - for (const pubkey of Object.keys(this.headsCache)) { + for (const ws2pFullId of Object.keys(this.headsCache)) { heads.push({ - message: this.headsCache[pubkey].message, - sig: this.headsCache[pubkey].sig + message: this.headsCache[ws2pFullId].message, + sig: this.headsCache[ws2pFullId].sig }) } return heads @@ -101,20 +105,58 @@ export class WS2PCluster { if (!message) { throw "EMPTY_MESSAGE_FOR_HEAD" } - if (message.match(WS2PConstants.HEAD_REGEXP)) { + if (message.match(WS2PConstants.HEAD_V0_REGEXP)) { const [,, pub, blockstamp]:string[] = message.split(':') + const ws2pId = (this.server.conf.ws2p && this.server.conf.ws2p.uuid) || '000000' + const fullId = [pub, ws2pId].join('-') const sigOK = verify(message, sig, pub) if (sigOK) { // Already known? - if (!this.headsCache[pub] || this.headsCache[pub].blockstamp !== blockstamp) { + if (!this.headsCache[fullId] || this.headsCache[fullId].blockstamp !== blockstamp) { // More recent? - if (!this.headsCache[pub] || parseInt(this.headsCache[pub].blockstamp) < parseInt(blockstamp)) { + if (!this.headsCache[fullId] || parseInt(this.headsCache[fullId].blockstamp) < parseInt(blockstamp)) { // Check that issuer is a member and that the block exists const memberKey = await this.isMemberKey(pub) if (memberKey) { const exists = await this.existsBlock(blockstamp) if (exists) { - this.headsCache[pub] = { blockstamp, message, sig } + this.headsCache[fullId] = { blockstamp, message, sig } + this.newHeads.push({message, sig}) + added.push({message, sig}) + // Cancel a pending "heads" to be spread + if (this.headsTimeout) { + clearTimeout(this.headsTimeout) + } + // Reprogram it a few moments later + this.headsTimeout = setTimeout(async () => { + const heads = this.newHeads.splice(0, this.newHeads.length) + if (heads.length) { + await this.spreadNewHeads(heads) + } + }, WS2PConstants.HEADS_SPREAD_TIMEOUT) + } + } + } + } + } else { + throw "HEAD_MESSAGE_WRONGLY_SIGNED" + } + } + else if (message.match(WS2PConstants.HEAD_V1_REGEXP)) { + const [,,, pub, blockstamp, software, ws2pId, softVersion, prefix]:string[] = message.split(':') + const sigOK = verify(message, sig, pub) + const fullId = [pub, ws2pId].join('-') + if (sigOK) { + // Already known? + if (!this.headsCache[fullId] || this.headsCache[fullId].blockstamp !== blockstamp) { + // More recent? + if (!this.headsCache[fullId] || parseInt(this.headsCache[fullId].blockstamp) < parseInt(blockstamp)) { + // Check that issuer is a member and that the block exists + const memberKey = await this.isMemberKey(pub) + if (memberKey) { + const exists = await this.existsBlock(blockstamp) + if (exists) { + this.headsCache[fullId] = { blockstamp, message, sig } this.newHeads.push({message, sig}) added.push({message, sig}) // Cancel a pending "heads" to be spread @@ -352,7 +394,11 @@ export class WS2PCluster { private sayHeadChangedTo(number:number, hash:string) { const key = new Key(this.server.conf.pair.pub, this.server.conf.pair.sec) const pub = key.publicKey - const message = `WS2P:HEAD:${pub}:${number}-${hash}` + const software = 'duniter' + const softVersion = Package.getInstance().version + const ws2pId = (this.server.conf.ws2p && this.server.conf.ws2p.uuid) || '00000000' + const prefix = this.server.conf.prefix || Constants.DEFAULT_PEER_ID + const message = `WS2P:HEAD:1:${pub}:${number}-${hash}:${ws2pId}:${software}:${softVersion}:${prefix}` const sig = key.signSync(message) return { sig, message, pub } } diff --git a/app/modules/ws2p/lib/constants.ts b/app/modules/ws2p/lib/constants.ts index 8fd9b790b34ff9845dbccd5b7c198b4a057d6e99..f196b27d36e5dc33e40f7290c41653132fb6c8af 100644 --- a/app/modules/ws2p/lib/constants.ts +++ b/app/modules/ws2p/lib/constants.ts @@ -19,7 +19,19 @@ export const WS2PConstants = { BAN_DURATION_IN_SECONDS: 120, ERROR_RECALL_DURATION_IN_SECONDS: 60, - HEAD_REGEXP: new RegExp('^WS2P:HEAD:' + CommonConstants.FORMATS.PUBKEY + ':' + CommonConstants.FORMATS.BLOCKSTAMP + '$'), + HEAD_V0_REGEXP: new RegExp('^WS2P:HEAD:' + + CommonConstants.FORMATS.PUBKEY + ':' + + CommonConstants.FORMATS.BLOCKSTAMP + + '$'), + + HEAD_V1_REGEXP: new RegExp('^WS2P:HEAD:1:' + + '(' + CommonConstants.FORMATS.PUBKEY + '):' + + '(' + CommonConstants.FORMATS.BLOCKSTAMP + '):' + + '(' + CommonConstants.FORMATS.WS2PID + '):' + + '(' + CommonConstants.FORMATS.SOFTWARE + '):' + + '(' + CommonConstants.FORMATS.SOFT_VERSION + '):' + + '(' + CommonConstants.FORMATS.POW_PREFIX + ')' + + '$'), HEADS_SPREAD_TIMEOUT: 100 // Wait 100ms before sending a bunch of signed heads } \ No newline at end of file diff --git a/test/fast/modules/ws2p/ws2p_regexp.ts b/test/fast/modules/ws2p/ws2p_regexp.ts new file mode 100644 index 0000000000000000000000000000000000000000..1dff5363df765c0139d0f36e1e46e55d0bb502c7 --- /dev/null +++ b/test/fast/modules/ws2p/ws2p_regexp.ts @@ -0,0 +1,47 @@ +import * as assert from 'assert' +import { WS2PConstants } from '../../../../app/modules/ws2p/lib/constants'; + +describe('WS2P Regexp', () => { + + it('should match correctly HEADv0 regexps', () => { + assert.deepEqual('WRONG_VALUE'.match(WS2PConstants.HEAD_V0_REGEXP), null) + assert.deepEqual('WS2P'.match(WS2PConstants.HEAD_V0_REGEXP), null) + assert.deepEqual('WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:63957#00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E'.match(WS2PConstants.HEAD_V0_REGEXP), null) + assert.deepEqual('WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:63957-'.match(WS2PConstants.HEAD_V0_REGEXP), null) + assert.deepEqual('WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:00-00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E'.match(WS2PConstants.HEAD_V0_REGEXP), null) + assert.deepEqual( + Array.from('WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:0-00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E'.match(WS2PConstants.HEAD_V0_REGEXP) || { length: 0 }), + [ + 'WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:0-00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E', + '0' + ] + ) + assert.deepEqual( + Array.from('WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:63957-00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E'.match(WS2PConstants.HEAD_V0_REGEXP) || { length: 0 }), + ['WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:63957-00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E', '63957'] + ) + }) + + it('should match correctly HEADv1 regexps', () => { + assert.deepEqual('WRONG_VALUE'.match(WS2PConstants.HEAD_V0_REGEXP), null) + assert.deepEqual('WS2P'.match(WS2PConstants.HEAD_V0_REGEXP), null) + assert.deepEqual('WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:63957#00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E'.match(WS2PConstants.HEAD_V1_REGEXP), null) + assert.deepEqual('WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:63957-'.match(WS2PConstants.HEAD_V1_REGEXP), null) + assert.deepEqual('WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:00-00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E'.match(WS2PConstants.HEAD_V1_REGEXP), null) + assert.deepEqual('WS2P:HEAD:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:0-00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E'.match(WS2PConstants.HEAD_V1_REGEXP), null) + assert.deepEqual( + Array.from('WS2P:HEAD:1:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:63957-00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E:abcdef01:duniter:1.6.8:899'.match(WS2PConstants.HEAD_V1_REGEXP) || { length: 0 }), + [ + 'WS2P:HEAD:1:3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj:63957-00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E:abcdef01:duniter:1.6.8:899', + '3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj', + '63957-00003DC30A5218974ED1BBA3DD8593F43A2C7CDD3EBD17B785FD5191DBB1657E', + '63957', + 'abcdef01', + 'duniter', + '1.6.8', + '899', + '899' + ] + ) + }) +})