diff --git a/src/app/FirebaseResource.ts b/src/app/FirebaseResource.ts index 1a8992e..688067e 100644 --- a/src/app/FirebaseResource.ts +++ b/src/app/FirebaseResource.ts @@ -4,6 +4,9 @@ import {FirebaseUnit, RTDBRef} from "./units/FirebaseUnit" import * as firebase from "firebase-admin" import {Application} from "@extollo/lib"; +/** + * Base interface for an item in a Firebase RTDB collection. + */ export interface FirebaseResourceItem { firebaseID: string; seqID: number; @@ -17,15 +20,17 @@ export class FirebaseResource extends Iterable(FirebaseUnit).ref(this.refName) } + /** Get the next sequential ID. */ async getNextID(): Promise { return new Promise((res, rej) => { this.ref().orderByChild('seqID') .on('value', snapshot => { - res(this.resolveObject(snapshot.val()).reverse()?.[0]?.seqID || 1) + res((this.resolveObject(snapshot.val()).reverse()?.[0]?.seqID || 0) + 1) }, rej) }) } + /** Get the record at the ith index. */ async at(i: number): Promise { return new Promise((res, rej) => { this.ref().orderByChild('seqID') @@ -34,6 +39,7 @@ export class FirebaseResource extends Iterable> { return new Promise>((res, rej) => { this.ref().orderByChild('seqID') @@ -44,6 +50,7 @@ export class FirebaseResource extends Iterable { return new Promise((res, rej) => { this.ref().orderByChild('seqID') @@ -53,6 +60,21 @@ export class FirebaseResource extends Iterable { + item.seqID = await this.getNextID() + await this.ref().push(item) + return item + } + + /** + * Given the value of a realtime-database snapshot, resolve it to an array of T. + * @param snapshot + * @protected + */ protected resolveObject(snapshot: any | null | undefined) { if ( !snapshot ) snapshot = {} diff --git a/src/app/rtdb/BlockResource.ts b/src/app/rtdb/BlockResource.ts index e7ad719..2a26900 100644 --- a/src/app/rtdb/BlockResource.ts +++ b/src/app/rtdb/BlockResource.ts @@ -3,17 +3,24 @@ import {Injectable} from "@extollo/di" import {RTDBRef} from "../units/FirebaseUnit" import {AsyncCollection} from "@extollo/util" +/** + * A block transaction representing an encounter between two clients. + */ export interface BlockEncounterTransaction { combinedHash: string; timestamp: number; encodedGPSLocation: string; } +/** + * A block transaction representing an infected client. + */ export interface BlockInfectionTransaction { clientID: string; timestamp: number; } +/** Union type of all possible block transactions. */ export type BlockTransaction = BlockInfectionTransaction | BlockEncounterTransaction export function isBlockEncounterTransaction(what: any): what is BlockEncounterTransaction { @@ -37,11 +44,11 @@ export function isBlockTransaction(what: any): what is BlockTransaction { return isBlockEncounterTransaction(what) || isBlockInfectionTransaction(what) } +/** + * Interface representing a single block in the chain. + */ export interface BlockResourceItem extends FirebaseResourceItem { uuid: string; - combinedHash: string; - timestamp: number; - encodedGPSLocation: string; transactions: BlockTransaction[]; lastBlockHash: string; lastBlockUUID: string; diff --git a/src/app/rtdb/TransactionResource.ts b/src/app/rtdb/TransactionResource.ts index c172992..9420a04 100644 --- a/src/app/rtdb/TransactionResource.ts +++ b/src/app/rtdb/TransactionResource.ts @@ -3,6 +3,9 @@ import {Injectable} from "@extollo/di" import {RTDBRef} from "../units/FirebaseUnit" import {AsyncCollection} from "@extollo/util"; +/** + * Interface representing a client-submitted encounter transaction. + */ export interface TransactionResourceItem extends FirebaseResourceItem { combinedHash: string; timestamp: number; diff --git a/src/app/units/Blockchain.ts b/src/app/units/Blockchain.ts index cb99a6b..8bda38a 100644 --- a/src/app/units/Blockchain.ts +++ b/src/app/units/Blockchain.ts @@ -1,7 +1,7 @@ import {Singleton, Inject} from "@extollo/di" import {Unit, Logging, Config} from "@extollo/lib" import {FirebaseUnit} from "./FirebaseUnit" -import {BlockResource, BlockResourceItem, BlockTransaction} from "../rtdb/BlockResource" +import {BlockEncounterTransaction, BlockResource, BlockResourceItem, BlockTransaction} from "../rtdb/BlockResource" import {TransactionResourceItem} from "../rtdb/TransactionResource" import * as openpgp from "openpgp" import * as crypto from "crypto" @@ -11,9 +11,6 @@ export class Block implements BlockResourceItem { firebaseID: string; seqID: number; uuid: string; - combinedHash: string; - timestamp: number; - encodedGPSLocation: string; transactions: BlockTransaction[]; lastBlockHash: string; lastBlockUUID: string; @@ -23,9 +20,6 @@ export class Block implements BlockResourceItem { this.firebaseID = rec.firebaseID; this.seqID = rec.seqID this.uuid = rec.uuid - this.combinedHash = rec.combinedHash - this.timestamp = rec.timestamp - this.encodedGPSLocation = rec.encodedGPSLocation this.transactions = rec.transactions this.lastBlockHash = rec.lastBlockHash this.lastBlockUUID = rec.lastBlockUUID @@ -38,12 +32,21 @@ export class Block implements BlockResourceItem { .digest('hex') } + toItem(): BlockResourceItem { + return { + seqID: this.seqID, + firebaseID: this.firebaseID, + uuid: this.uuid, + transactions: this.transactions, + lastBlockHash: this.lastBlockHash, + lastBlockUUID: this.lastBlockUUID, + proof: this.proof, + } + } + toString() { return [ this.uuid, - this.combinedHash, - this.timestamp.toString(), - this.encodedGPSLocation, JSON.stringify(this.transactions), this.lastBlockHash, this.lastBlockUUID, @@ -72,22 +75,38 @@ export class Blockchain extends Unit { @Inject() protected readonly config!: Config + /** + * Returns true if the given host is registered as a peer. + * @param host + */ public async hasPeer(host: string): Promise { const peers = await this.getPeers() return peers.some(peer => peer.host.toLowerCase() === host.toLowerCase()) } + /** + * Get a list of all registered peers. + */ public async getPeers(): Promise { const data = await this.firebase.ref('peers').once('value') return (data.val() as Peer[]) || [] } + /** + * Register a new host as a peer of this instance. + * @param peer + */ public async registerPeer(peer: Peer) { if ( !(await this.hasPeer(peer.host)) ) { await this.firebase.ref('peers').push().set(peer) } } + /** + * Given an array of blocks in chain-order, validate the chain. + * @param chain + * @return boolean - true if the chain is valid + */ public async validate(chain: Block[]) { const blocks = collect(chain) return blocks.every((block: Block, idx: number) => { @@ -101,14 +120,35 @@ export class Blockchain extends Unit { } - public async submitBlock(block: BlockResourceItem, afterBlock: Block, proofToken: string) { - const newBlock = new Block(block) - newBlock.lastBlockHash = afterBlock.hash() - newBlock.lastBlockUUID = afterBlock.uuid + public async submitTransactions(group: [TransactionResourceItem, TransactionResourceItem]) { + let lastBlock = await this.getLastBlock() + if ( !lastBlock ) await this.getGenesisBlock() + + const block: BlockResourceItem = { + uuid: uuid_v4(), + transactions: group.map(item => this.getEncounterTransaction(item)), + lastBlockHash: lastBlock!.hash(), + lastBlockUUID: lastBlock!.uuid, + proof: await this.generateProofOfWork(lastBlock!), + + firebaseID: '', + seqID: -1, + } + + await ( this.app().make(BlockResource)).push(block) + return new Block(block) } - public async submitTransactions(group: [TransactionResourceItem, TransactionResourceItem]) { - // Not sure yet + public async getGenesisBlock(): Promise { + return new Block({ + uuid: '0000', + transactions: [], + lastBlockHash: '', + lastBlockUUID: '', + proof: '', + firebaseID: '', + seqID: -1, + }) } public async getLastBlock(): Promise { @@ -124,6 +164,14 @@ export class Blockchain extends Unit { } + protected getEncounterTransaction(item: TransactionResourceItem): BlockEncounterTransaction { + return { + combinedHash: item.combinedHash, + timestamp: item.timestamp, + encodedGPSLocation: item.encodedGPSLocation, + } + } + protected async generateProofOfWork(lastBlock: Block): Promise { const hashString = lastBlock.hash() const privateKey = this.config.get("app.gpg.key.private")