2021-04-11 01:35:29 +00:00
|
|
|
import {Inject, Singleton} from "@extollo/di"
|
|
|
|
import {Application, Config, Logging, Unit} from "@extollo/lib"
|
|
|
|
import {FirebaseUnit} from "./FirebaseUnit"
|
2021-04-10 16:25:59 +00:00
|
|
|
import {
|
|
|
|
BlockEncounterTransaction,
|
|
|
|
BlockResourceItem,
|
|
|
|
BlockTransaction,
|
|
|
|
isBlockResourceItem
|
|
|
|
} from "../rtdb/BlockResource"
|
2021-04-11 01:35:29 +00:00
|
|
|
import {TransactionResourceItem} from "../rtdb/TransactionResource"
|
2021-04-10 08:30:17 +00:00
|
|
|
import * as openpgp from "openpgp"
|
2021-04-10 07:01:57 +00:00
|
|
|
import * as crypto from "crypto"
|
2021-04-10 16:25:59 +00:00
|
|
|
import axios from "axios"
|
2021-04-11 01:35:29 +00:00
|
|
|
import {collect, uuid_v4} from "@extollo/util"
|
2021-04-10 16:25:59 +00:00
|
|
|
import {ExposureResourceItem} from "../rtdb/ExposureResource"
|
2021-04-11 01:35:29 +00:00
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
/**
|
|
|
|
* Verify an OpenPGP signature based on an armored key.
|
|
|
|
* @param armoredKey
|
|
|
|
* @param armoredMessage
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
async function pgpVerify(armoredKey: string, armoredMessage: string) {
|
|
|
|
const [publicKeys, message] = await Promise.all([
|
|
|
|
openpgp.readKey({ armoredKey }),
|
|
|
|
openpgp.readMessage({ armoredMessage })
|
|
|
|
])
|
|
|
|
|
|
|
|
return !!(await (await openpgp.verify({ publicKeys, message })).signatures?.[0]?.verified)
|
|
|
|
}
|
2021-04-10 07:01:57 +00:00
|
|
|
|
2021-04-10 08:49:00 +00:00
|
|
|
/**
|
|
|
|
* Utility wrapper class for a block in the chain.
|
|
|
|
*/
|
2021-04-10 07:01:57 +00:00
|
|
|
export class Block implements BlockResourceItem {
|
2021-04-10 09:23:54 +00:00
|
|
|
uuid: string
|
|
|
|
transactions: BlockTransaction[]
|
|
|
|
timestamp: number
|
|
|
|
lastBlockHash: string
|
|
|
|
lastBlockUUID: string
|
|
|
|
proof: string
|
2021-04-10 16:25:59 +00:00
|
|
|
waitTime: number
|
2021-04-10 17:10:52 +00:00
|
|
|
peer: string
|
2021-04-10 12:26:42 +00:00
|
|
|
|
2021-04-10 09:12:48 +00:00
|
|
|
get config(): Config {
|
|
|
|
return Application.getApplication().make(Config)
|
|
|
|
}
|
2021-04-10 12:26:42 +00:00
|
|
|
|
2021-04-10 07:01:57 +00:00
|
|
|
constructor(rec: BlockResourceItem) {
|
2021-04-11 01:35:29 +00:00
|
|
|
this.uuid = rec.uuid || uuid_v4()
|
|
|
|
this.transactions = rec.transactions || []
|
|
|
|
this.lastBlockHash = rec.lastBlockHash || ''
|
|
|
|
this.lastBlockUUID = rec.lastBlockUUID || ''
|
2021-04-10 09:23:54 +00:00
|
|
|
this.proof = rec.proof
|
2021-04-11 01:35:29 +00:00
|
|
|
this.timestamp = rec.timestamp || (new Date).getTime()
|
2021-04-10 16:25:59 +00:00
|
|
|
this.waitTime = rec.waitTime
|
2021-04-10 17:10:52 +00:00
|
|
|
this.peer = rec.peer
|
2021-04-10 07:01:57 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 08:51:21 +00:00
|
|
|
/** Returns true if this is the genesis block. */
|
2021-04-10 09:12:48 +00:00
|
|
|
async isGenesis() {
|
|
|
|
// first block will be guaranteed uuid 0000
|
|
|
|
if (this.uuid !== '0000') {
|
2021-04-10 09:23:54 +00:00
|
|
|
return false
|
2021-04-10 09:12:48 +00:00
|
|
|
}
|
|
|
|
const proof = this.proof
|
|
|
|
const publicKey = this.config.get("app.gpg.key.public")
|
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
return pgpVerify(publicKey, proof)
|
2021-04-10 08:51:21 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 08:49:00 +00:00
|
|
|
/** Generate the hash for this block. */
|
2021-04-10 07:01:57 +00:00
|
|
|
hash() {
|
|
|
|
return crypto.createHash('sha256')
|
|
|
|
.update(this.toString(), 'utf-8')
|
|
|
|
.digest('hex')
|
|
|
|
}
|
|
|
|
|
2021-04-10 08:49:00 +00:00
|
|
|
/** Cast the Block's data to a plain object. */
|
2021-04-10 08:44:56 +00:00
|
|
|
toItem(): BlockResourceItem {
|
|
|
|
return {
|
|
|
|
uuid: this.uuid,
|
|
|
|
transactions: this.transactions,
|
|
|
|
lastBlockHash: this.lastBlockHash,
|
|
|
|
lastBlockUUID: this.lastBlockUUID,
|
|
|
|
proof: this.proof,
|
2021-04-10 08:58:38 +00:00
|
|
|
timestamp: this.timestamp,
|
2021-04-10 16:25:59 +00:00
|
|
|
waitTime: this.waitTime,
|
2021-04-10 17:10:52 +00:00
|
|
|
peer: this.peer,
|
2021-04-10 08:44:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-10 08:49:00 +00:00
|
|
|
/** Generate the deterministic hash-able string. */
|
2021-04-10 07:01:57 +00:00
|
|
|
toString() {
|
|
|
|
return [
|
|
|
|
this.uuid,
|
2021-04-10 16:25:59 +00:00
|
|
|
this.waitTime,
|
2021-04-10 14:34:04 +00:00
|
|
|
JSON.stringify(this.transactions || [], undefined, 0),
|
2021-04-10 07:01:57 +00:00
|
|
|
this.lastBlockHash,
|
|
|
|
this.lastBlockUUID,
|
|
|
|
].join('%')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-10 08:49:00 +00:00
|
|
|
/**
|
|
|
|
* Interface representing a federated peer.
|
|
|
|
*/
|
2021-04-10 07:01:57 +00:00
|
|
|
export interface Peer {
|
|
|
|
host: string,
|
|
|
|
name?: string,
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Blockchain Unit
|
|
|
|
* ---------------------------------------
|
|
|
|
* Main service for interacting with the contact blockchain.
|
|
|
|
*/
|
|
|
|
@Singleton()
|
|
|
|
export class Blockchain extends Unit {
|
2021-04-11 01:36:10 +00:00
|
|
|
private readonly MIN_WAIT_TIME = 10000
|
|
|
|
private readonly MAX_WAIT_TIME = 30000
|
|
|
|
private readonly PENALTY_INTERVAL = 5000
|
2021-04-10 17:10:52 +00:00
|
|
|
private readonly MAX_PEERS_PENALTY = 10
|
|
|
|
|
2021-04-10 07:01:57 +00:00
|
|
|
@Inject()
|
|
|
|
protected readonly logging!: Logging
|
|
|
|
|
|
|
|
@Inject()
|
|
|
|
protected readonly firebase!: FirebaseUnit
|
|
|
|
|
2021-04-10 08:30:17 +00:00
|
|
|
@Inject()
|
|
|
|
protected readonly config!: Config
|
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
/** The most-correct, approved chain according to this node. */
|
2021-04-11 01:35:29 +00:00
|
|
|
protected approvedChain: Block[] = []
|
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
/** The peers this node will communicate with. */
|
2021-04-11 01:35:29 +00:00
|
|
|
protected peers: Peer[] = []
|
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
/** If true, the writeback/refresh cycle will stop. */
|
2021-04-11 01:35:29 +00:00
|
|
|
protected breakForExit = false
|
|
|
|
|
2021-04-10 16:25:59 +00:00
|
|
|
/**
|
|
|
|
* Block transactions that will be attempted as part of this host's
|
|
|
|
* next block submission.
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected pendingTransactions: BlockTransaction[] = []
|
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
protected publicKey!: openpgp.Key
|
|
|
|
protected privateKey!: openpgp.Key
|
|
|
|
protected genesisProof!: string
|
2021-04-10 16:25:59 +00:00
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
protected nextWaitTime!: number
|
|
|
|
protected lastBlock!: Block
|
|
|
|
protected nextProof!: string
|
2021-04-10 16:25:59 +00:00
|
|
|
|
2021-04-10 18:00:29 +00:00
|
|
|
async up() {
|
2021-04-11 01:35:29 +00:00
|
|
|
this.logging.info('Generating OpenPGP assets...')
|
|
|
|
this.publicKey = await openpgp.readKey({
|
|
|
|
armoredKey: this.config.get("app.gpg.key.public")
|
|
|
|
})
|
|
|
|
|
|
|
|
this.privateKey = await openpgp.readKey({
|
|
|
|
armoredKey: this.config.get("app.gpg.key.private")
|
|
|
|
})
|
|
|
|
|
|
|
|
this.genesisProof = await openpgp.sign({
|
|
|
|
message: openpgp.Message.fromText('0000'),
|
|
|
|
privateKeys: this.privateKey,
|
|
|
|
})
|
|
|
|
|
|
|
|
this.logging.info('Performing initial load...')
|
|
|
|
await this.initialLoad()
|
|
|
|
|
|
|
|
this.logging.info('Contacting configured peers...')
|
2021-04-10 18:00:29 +00:00
|
|
|
const peers = this.config.get('server.peers')
|
2021-04-11 01:35:29 +00:00
|
|
|
await Promise.all(peers.map((host: string) => this.registerPeer({ host })))
|
|
|
|
|
|
|
|
this.logging.info('Performing initial writeback...')
|
|
|
|
await this.writeback()
|
|
|
|
}
|
|
|
|
|
|
|
|
async down() {
|
|
|
|
this.breakForExit = true
|
|
|
|
}
|
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
/** Load the initial data from the data sources into memory for fast access. */
|
2021-04-11 01:35:29 +00:00
|
|
|
async initialLoad() {
|
|
|
|
const [peers, chain] = await Promise.all([
|
|
|
|
this.firebase.ref('peers')
|
|
|
|
.once('value')
|
|
|
|
.then(val => val.val()),
|
|
|
|
|
|
|
|
this.firebase.ref('block')
|
|
|
|
.orderByKey()
|
|
|
|
.once('value')
|
|
|
|
.then(val => val.val())
|
|
|
|
])
|
|
|
|
|
|
|
|
this.logging.debug({peers, chain})
|
|
|
|
|
2021-04-11 01:38:25 +00:00
|
|
|
this.approvedChain = (chain || []).map((item: BlockResourceItem) => new Block(item))
|
2021-04-11 01:35:29 +00:00
|
|
|
this.peers = peers || []
|
2021-04-10 18:00:29 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 08:44:56 +00:00
|
|
|
/**
|
|
|
|
* Returns true if the given host is registered as a peer.
|
|
|
|
* @param host
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public hasPeer(host: string): boolean {
|
|
|
|
return this.peers.some(x => x.host === host)
|
2021-04-10 07:01:57 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 08:44:56 +00:00
|
|
|
/**
|
|
|
|
* Get a list of all registered peers.
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public getPeers(): Peer[] {
|
|
|
|
return this.peers
|
2021-04-10 16:25:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* From a peer, fetch the submission blockchain, if it is valid.
|
|
|
|
* @param peer
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public async getPeerSubmit(peer: Peer): Promise<Block[] | undefined> {
|
2021-04-10 16:25:59 +00:00
|
|
|
try {
|
2021-04-11 01:35:29 +00:00
|
|
|
const result = await axios.get(`${peer.host}api/v1/chain/submit`)
|
2021-04-10 16:25:59 +00:00
|
|
|
const blocks: unknown = result.data?.data?.records
|
2021-04-10 19:18:55 +00:00
|
|
|
if ( Array.isArray(blocks) && blocks.every(block => {
|
2021-04-11 01:35:29 +00:00
|
|
|
return isBlockResourceItem(block)
|
2021-04-10 19:18:55 +00:00
|
|
|
}) ) {
|
2021-04-10 16:25:59 +00:00
|
|
|
return blocks.map(x => new Block(x))
|
|
|
|
}
|
|
|
|
} catch (e) {
|
2021-04-10 19:18:55 +00:00
|
|
|
this.logging.error(e)
|
2021-04-10 16:25:59 +00:00
|
|
|
return undefined
|
|
|
|
}
|
2021-04-10 07:01:57 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 08:44:56 +00:00
|
|
|
/**
|
|
|
|
* Register a new host as a peer of this instance.
|
|
|
|
* @param peer
|
|
|
|
*/
|
2021-04-10 07:01:57 +00:00
|
|
|
public async registerPeer(peer: Peer) {
|
2021-04-11 01:35:29 +00:00
|
|
|
if ( !this.hasPeer(peer.host) ) {
|
2021-04-10 18:00:29 +00:00
|
|
|
this.logging.info(`Registering peer: ${peer.host}`)
|
|
|
|
const header = this.config.get('app.api_server_header')
|
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
this.peers.push(peer)
|
|
|
|
|
2021-04-10 18:00:29 +00:00
|
|
|
try {
|
|
|
|
await axios.post(`${peer.host}api/v1/peer`, {
|
|
|
|
host: this.getBaseURL(),
|
|
|
|
}, {
|
|
|
|
headers: {
|
2021-04-11 01:35:29 +00:00
|
|
|
[header]: this.getPeerToken(),
|
2021-04-10 19:18:55 +00:00
|
|
|
'content-type': 'application/json',
|
2021-04-10 18:00:29 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
} catch (e) {
|
|
|
|
this.logging.error(e)
|
|
|
|
}
|
2021-04-10 07:01:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-10 08:44:56 +00:00
|
|
|
/**
|
|
|
|
* Given an array of blocks in chain-order, validate the chain.
|
|
|
|
* @param chain
|
|
|
|
* @return boolean - true if the chain is valid
|
|
|
|
*/
|
2021-04-10 07:01:57 +00:00
|
|
|
public async validate(chain: Block[]) {
|
2021-04-10 07:51:22 +00:00
|
|
|
const blocks = collect<Block>(chain)
|
2021-04-10 14:34:04 +00:00
|
|
|
return (
|
|
|
|
await blocks.promiseMap(async (block, idx) => {
|
2021-04-11 02:08:19 +00:00
|
|
|
if ( await block.isGenesis() ) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2021-04-10 14:34:04 +00:00
|
|
|
const previous: Block | undefined = blocks.at(idx - 1)
|
|
|
|
if ( !previous ) {
|
|
|
|
this.logging.debug(`Chain is invalid: block ${idx} is missing previous ${idx - 1}.`)
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const pass = (
|
|
|
|
block.lastBlockUUID === previous.uuid
|
|
|
|
&& block.lastBlockHash === previous.hash()
|
|
|
|
)
|
|
|
|
|
|
|
|
if ( !pass ) {
|
|
|
|
this.logging.debug(`Chain is invalid: block ${idx} does not match previous.`)
|
|
|
|
this.logging.debug({
|
|
|
|
lastBlockUUID: block.lastBlockUUID,
|
|
|
|
computedLastUUID: previous.uuid,
|
|
|
|
lastBlockHash: block.lastBlockHash,
|
|
|
|
computedLastHash: previous.hash(),
|
|
|
|
})
|
2021-04-11 01:35:29 +00:00
|
|
|
|
|
|
|
return false
|
2021-04-10 14:34:04 +00:00
|
|
|
}
|
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
if ( !(await this.validateProofOfWork(block, previous)) ) {
|
|
|
|
this.logging.debug(`Chain is invalid: block ${idx} failed proof of work validation`)
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
2021-04-10 14:34:04 +00:00
|
|
|
})
|
|
|
|
).every(Boolean)
|
2021-04-10 07:01:57 +00:00
|
|
|
}
|
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
/**
|
|
|
|
* Perform the consensus algorithm among the peers of this node
|
|
|
|
* to push a block onto the chain.
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public async refresh() {
|
|
|
|
if ( this.breakForExit ) return;
|
|
|
|
this.logging.debug('Called refresh().')
|
2021-04-10 16:25:59 +00:00
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
const peers = this.getPeers()
|
2021-04-10 16:25:59 +00:00
|
|
|
const time_x_block: {[key: string]: Block} = {}
|
2021-04-10 17:10:52 +00:00
|
|
|
const time_x_blocks: {[key: string]: Block[]} = {}
|
2021-04-10 16:25:59 +00:00
|
|
|
const time_x_peer: {[key: string]: Peer | true} = {}
|
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
await Promise.all(peers.map(async peer => {
|
|
|
|
const blocks: Block[] | undefined = await this.getPeerSubmit(peer)
|
|
|
|
|
2021-04-10 16:25:59 +00:00
|
|
|
if ( blocks && await this.validate(blocks) ) {
|
2021-04-11 01:35:29 +00:00
|
|
|
const block = blocks.slice(-1)[0]
|
|
|
|
if ( !block ) return // TODO fixme
|
2021-04-10 16:25:59 +00:00
|
|
|
|
2021-04-10 17:10:52 +00:00
|
|
|
const penalty = blocks.slice(0, 10)
|
2021-04-11 01:35:29 +00:00
|
|
|
.map(block => block.peer === peer.host)
|
|
|
|
.filter(Boolean).length * this.PENALTY_INTERVAL
|
2021-04-10 17:10:52 +00:00
|
|
|
* (Math.min(peers.length, this.MAX_PEERS_PENALTY))
|
|
|
|
|
|
|
|
block.waitTime += penalty
|
|
|
|
|
2021-04-10 16:25:59 +00:00
|
|
|
time_x_block[block.waitTime] = block
|
2021-04-10 19:18:55 +00:00
|
|
|
time_x_blocks[block.waitTime] = blocks.reverse()
|
2021-04-10 16:25:59 +00:00
|
|
|
time_x_peer[block.waitTime] = peer
|
2021-04-10 19:18:55 +00:00
|
|
|
} else {
|
2021-04-11 01:35:29 +00:00
|
|
|
console.log('validation fail!')
|
2021-04-10 16:25:59 +00:00
|
|
|
}
|
2021-04-11 01:35:29 +00:00
|
|
|
}))
|
2021-04-10 16:25:59 +00:00
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
console.log(time_x_blocks, time_x_peer, time_x_block)
|
2021-04-10 16:25:59 +00:00
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
const submitBlock = this.getSubmitBlock()
|
|
|
|
if ( submitBlock ) {
|
|
|
|
time_x_block[submitBlock.waitTime] = submitBlock
|
|
|
|
time_x_peer[submitBlock.waitTime] = true
|
2021-04-10 16:25:59 +00:00
|
|
|
}
|
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
console.log('submit block', submitBlock)
|
|
|
|
|
2021-04-10 16:25:59 +00:00
|
|
|
const min = Math.min(...Object.keys(time_x_block).map(parseFloat))
|
|
|
|
const peer = time_x_peer[min]
|
2021-04-10 07:01:57 +00:00
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
console.log('peer?', peer)
|
|
|
|
|
2021-04-10 16:25:59 +00:00
|
|
|
if ( peer === true ) {
|
2021-04-11 01:35:29 +00:00
|
|
|
// Our version of the chain was accepted
|
|
|
|
this.approvedChain.push(submitBlock!)
|
2021-04-10 16:25:59 +00:00
|
|
|
this.pendingTransactions = []
|
2021-04-11 01:35:29 +00:00
|
|
|
} else if ( peer ) {
|
|
|
|
// A different server's chain was accepted
|
|
|
|
this.approvedChain = (time_x_blocks[min] || []).map(block => {
|
|
|
|
if (!block.transactions) {
|
|
|
|
block.transactions = []
|
2021-04-10 21:07:23 +00:00
|
|
|
}
|
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
return block
|
|
|
|
})
|
2021-04-10 16:25:59 +00:00
|
|
|
}
|
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
console.log('approved chain', this.approvedChain)
|
|
|
|
await this.writeback()
|
2021-04-10 16:25:59 +00:00
|
|
|
}
|
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
/**
|
|
|
|
* Get the current blockchain including the block submitted by this node.
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public getSubmitChain(): BlockResourceItem[] {
|
|
|
|
const submit = this.getSubmitBlock()
|
|
|
|
if ( !submit ) return this.approvedChain
|
|
|
|
else return [...this.approvedChain, submit]
|
2021-04-10 16:25:59 +00:00
|
|
|
}
|
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
/**
|
|
|
|
* Write the in-memory data back to persistent data stores.
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public async writeback() {
|
|
|
|
if ( this.breakForExit ) return;
|
|
|
|
this.logging.info('Generating initial proof-of-elapsed-time. This will take a second...')
|
|
|
|
this.nextWaitTime = this.random(this.MIN_WAIT_TIME, this.MAX_WAIT_TIME)
|
|
|
|
this.lastBlock = this.getLastBlock()
|
|
|
|
this.nextProof = await this.generateProofOfWork(this.lastBlock, this.nextWaitTime)
|
2021-04-10 16:25:59 +00:00
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
console.log('writeback approved chain', this.approvedChain)
|
2021-04-10 16:25:59 +00:00
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
await Promise.all([
|
|
|
|
this.firebase.ref('block').set(this.approvedChain.map(x => x.toItem())),
|
|
|
|
this.firebase.ref('peers').set(this.peers)
|
|
|
|
])
|
2021-04-10 07:01:57 +00:00
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
this.refresh()
|
|
|
|
}
|
2021-04-10 16:25:59 +00:00
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
/**
|
|
|
|
* Get the Block instance that we want to submit to the chain, if we have any transactions.
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public getSubmitBlock(): Block | undefined {
|
|
|
|
if ( !this.pendingTransactions?.length ) {
|
|
|
|
return
|
2021-04-10 16:25:59 +00:00
|
|
|
}
|
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
return new Block({
|
2021-04-10 08:58:38 +00:00
|
|
|
timestamp: (new Date).getTime(),
|
2021-04-10 08:44:56 +00:00
|
|
|
uuid: uuid_v4(),
|
2021-04-11 01:35:29 +00:00
|
|
|
transactions: this.pendingTransactions,
|
|
|
|
lastBlockHash: this.lastBlock.hash(),
|
|
|
|
lastBlockUUID: this.lastBlock.uuid,
|
|
|
|
proof: this.nextProof,
|
|
|
|
waitTime: this.nextWaitTime,
|
|
|
|
peer: this.getBaseURL(),
|
|
|
|
})
|
|
|
|
}
|
2021-04-10 08:44:56 +00:00
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
/**
|
|
|
|
* Submit a group of encounter transactions to be added to the chain.
|
|
|
|
* @param groups
|
|
|
|
*/
|
|
|
|
public submitTransactions(...groups: [TransactionResourceItem, TransactionResourceItem][]) {
|
|
|
|
groups.forEach(group => {
|
|
|
|
const txes = group.map(item => this.getEncounterTransaction(item))
|
|
|
|
this.pendingTransactions.push(...txes)
|
|
|
|
})
|
2021-04-10 14:01:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Submit the given exposure notifications onto the blockchain.
|
|
|
|
* @param exposures
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public submitExposures(...exposures: ExposureResourceItem[]) {
|
2021-04-10 16:25:59 +00:00
|
|
|
this.pendingTransactions.push(...exposures)
|
2021-04-10 07:01:57 +00:00
|
|
|
}
|
|
|
|
|
2021-04-11 02:04:08 +00:00
|
|
|
/**
|
|
|
|
* Get the peer-to-peer identifier token.
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public getPeerToken() {
|
|
|
|
return Buffer.from(this.genesisProof, 'utf-8')
|
|
|
|
.toString('base64')
|
2021-04-10 18:00:29 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 08:49:00 +00:00
|
|
|
/**
|
|
|
|
* Instantiate the genesis block of the entire chain.
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public getGenesisBlock(): Block {
|
2021-04-10 08:44:56 +00:00
|
|
|
return new Block({
|
2021-04-10 08:58:38 +00:00
|
|
|
timestamp: (new Date).getTime(),
|
2021-04-10 08:44:56 +00:00
|
|
|
uuid: '0000',
|
|
|
|
transactions: [],
|
|
|
|
lastBlockHash: '',
|
|
|
|
lastBlockUUID: '',
|
2021-04-11 01:35:29 +00:00
|
|
|
proof: this.genesisProof,
|
2021-04-10 08:44:56 +00:00
|
|
|
firebaseID: '',
|
2021-04-10 16:25:59 +00:00
|
|
|
waitTime: 0,
|
2021-04-10 17:10:52 +00:00
|
|
|
peer: this.getBaseURL(),
|
2021-04-10 08:44:56 +00:00
|
|
|
})
|
2021-04-10 07:01:57 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 08:49:00 +00:00
|
|
|
/**
|
2021-04-10 08:58:38 +00:00
|
|
|
* Get the last block in the blockchain, or push the genesis if one doesn't already exist.
|
2021-04-10 08:49:00 +00:00
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public getLastBlock(): Block {
|
|
|
|
if ( !this.approvedChain ) {
|
|
|
|
this.approvedChain = []
|
|
|
|
}
|
2021-04-10 08:58:38 +00:00
|
|
|
|
2021-04-11 01:35:29 +00:00
|
|
|
const rec = this.approvedChain.slice(-1)[0]
|
|
|
|
if (rec) return rec
|
|
|
|
|
|
|
|
const genesis = this.getGenesisBlock()
|
|
|
|
this.approvedChain.push(genesis)
|
|
|
|
return genesis
|
2021-04-10 07:01:57 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 12:36:14 +00:00
|
|
|
/**
|
|
|
|
* Get a list of all blocks in the chain, in order.
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
public read(): Promise<BlockResourceItem[]> {
|
|
|
|
return this.firebase.ref('block')
|
|
|
|
.once('value')
|
|
|
|
.then(snap => snap.val())
|
2021-04-10 12:36:14 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 08:49:00 +00:00
|
|
|
/**
|
|
|
|
* Given a client-submitted transaction, generate a block encounter transaction record.
|
|
|
|
* @param item
|
|
|
|
* @protected
|
|
|
|
*/
|
2021-04-10 08:44:56 +00:00
|
|
|
protected getEncounterTransaction(item: TransactionResourceItem): BlockEncounterTransaction {
|
|
|
|
return {
|
|
|
|
combinedHash: item.combinedHash,
|
|
|
|
timestamp: item.timestamp,
|
|
|
|
encodedGPSLocation: item.encodedGPSLocation,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-10 08:49:00 +00:00
|
|
|
/**
|
|
|
|
* Generate a proof of work string for the block that follows lastBlock.
|
|
|
|
* @param lastBlock
|
2021-04-10 16:25:59 +00:00
|
|
|
* @param waitTime
|
2021-04-10 08:49:00 +00:00
|
|
|
* @protected
|
|
|
|
*/
|
2021-04-10 16:25:59 +00:00
|
|
|
protected async generateProofOfWork(lastBlock: Block, waitTime: number): Promise<string> {
|
2021-04-10 08:30:17 +00:00
|
|
|
const hashString = lastBlock.hash()
|
|
|
|
const message = openpgp.Message.fromText(hashString)
|
|
|
|
|
2021-04-10 16:25:59 +00:00
|
|
|
await this.sleep(waitTime)
|
|
|
|
|
2021-04-10 08:30:17 +00:00
|
|
|
// Sign the hash using the server's private key
|
2021-04-11 01:35:29 +00:00
|
|
|
return openpgp.sign({
|
2021-04-10 08:30:17 +00:00
|
|
|
message,
|
2021-04-11 01:35:29 +00:00
|
|
|
privateKeys: this.privateKey,
|
|
|
|
})
|
2021-04-10 08:30:17 +00:00
|
|
|
}
|
2021-04-10 07:51:22 +00:00
|
|
|
|
2021-04-10 08:49:00 +00:00
|
|
|
/**
|
|
|
|
* Validate that the proof of work of currentBlock is accurate relative to lastBlock.
|
|
|
|
* @param currentBlock
|
|
|
|
* @param lastBlock
|
|
|
|
* @protected
|
|
|
|
*/
|
2021-04-11 01:35:29 +00:00
|
|
|
protected validateProofOfWork(currentBlock: Block, lastBlock: Block): Promise<boolean> {
|
2021-04-10 08:30:17 +00:00
|
|
|
const proof = lastBlock.proof
|
|
|
|
const publicKey = this.config.get("app.gpg.key.public")
|
2021-04-11 01:35:29 +00:00
|
|
|
return pgpVerify(publicKey, proof)
|
2021-04-10 07:51:22 +00:00
|
|
|
}
|
2021-04-10 16:25:59 +00:00
|
|
|
|
2021-04-10 17:10:52 +00:00
|
|
|
/**
|
|
|
|
* Get the base URL that identifies this peer.
|
|
|
|
* This should be the endpoint used to fetch the submitted blockchain.
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected getBaseURL(): string {
|
|
|
|
const base = this.config.get('server.base_url')
|
2021-04-10 18:00:29 +00:00
|
|
|
return `${base}${base.endsWith('/') ? '' : '/'}`
|
2021-04-10 17:10:52 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 16:25:59 +00:00
|
|
|
/** Sleep for (roughly) the given number of milliseconds. */
|
|
|
|
async sleep(ms: number) {
|
|
|
|
await new Promise<void>(res => {
|
|
|
|
setTimeout(res, ms)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a random number between two values.
|
|
|
|
* @param min
|
|
|
|
* @param max
|
|
|
|
*/
|
|
|
|
random(min: number, max: number): number {
|
|
|
|
return Math.floor(Math.random() * (Math.floor(max) - Math.ceil(min) + 1)) + Math.ceil(min);
|
|
|
|
}
|
2021-04-10 07:01:57 +00:00
|
|
|
}
|