Add Blockchain.submitTransactions

This commit is contained in:
Garrett Mills 2021-04-10 03:44:56 -05:00
parent 6441a5cad7
commit 3b7e72adab
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
4 changed files with 100 additions and 20 deletions

View File

@ -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<T extends FirebaseResourceItem> extends Iterable<T
return Application.getApplication().make<FirebaseUnit>(FirebaseUnit).ref(this.refName)
}
/** Get the next sequential ID. */
async getNextID(): Promise<number> {
return new Promise<number>((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<T | undefined> {
return new Promise<T | undefined>((res, rej) => {
this.ref().orderByChild('seqID')
@ -34,6 +39,7 @@ export class FirebaseResource<T extends FirebaseResourceItem> extends Iterable<T
})
}
/** Fetch an array of records in a range. */
async range(start: number, end: number): Promise<Collection<T>> {
return new Promise<Collection<T>>((res, rej) => {
this.ref().orderByChild('seqID')
@ -44,6 +50,7 @@ export class FirebaseResource<T extends FirebaseResourceItem> extends Iterable<T
})
}
/** Count the items in the collection. */
async count(): Promise<number> {
return new Promise<number>((res, rej) => {
this.ref().orderByChild('seqID')
@ -53,6 +60,21 @@ export class FirebaseResource<T extends FirebaseResourceItem> extends Iterable<T
})
}
/**
* Push a new item into the collection.
* @param item
*/
async push(item: T): Promise<T> {
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 = {}

View File

@ -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;

View File

@ -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;

View File

@ -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<boolean> {
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<Peer[]> {
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<Block>(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 (<BlockResource> this.app().make(BlockResource)).push(block)
return new Block(block)
}
public async submitTransactions(group: [TransactionResourceItem, TransactionResourceItem]) {
// Not sure yet
public async getGenesisBlock(): Promise<Block> {
return new Block({
uuid: '0000',
transactions: [],
lastBlockHash: '',
lastBlockUUID: '',
proof: '',
firebaseID: '',
seqID: -1,
})
}
public async getLastBlock(): Promise<Block | undefined> {
@ -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<string> {
const hashString = lastBlock.hash()
const privateKey = this.config.get("app.gpg.key.private")