Add realtime subscription and debug endpoint for pushing exposure notifications to chain

This commit is contained in:
Garrett Mills 2021-04-10 09:01:13 -05:00
parent 69c441ba56
commit d8cae0f559
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
9 changed files with 172 additions and 2 deletions

View File

@ -3,12 +3,14 @@ import {CommandLine} from "@extollo/cli"
import {FirebaseUnit} from "./app/units/FirebaseUnit" import {FirebaseUnit} from "./app/units/FirebaseUnit"
import {Blockchain} from "./app/units/Blockchain" import {Blockchain} from "./app/units/Blockchain"
import {Transaction} from "./app/units/rtdb/Transaction" import {Transaction} from "./app/units/rtdb/Transaction"
import {Exposure} from "./app/units/rtdb/Exposure"
export const Units = [ export const Units = [
Config, Config,
FirebaseUnit, FirebaseUnit,
Blockchain, Blockchain,
Transaction, Transaction,
Exposure,
Files, Files,
CommandLine, CommandLine,
Controllers, Controllers,

View File

@ -1,5 +1,5 @@
import {Inject, Injectable} from "@extollo/di" import {Inject, Injectable} from "@extollo/di"
import {Collection, Iterable} from "@extollo/util" import {collect, Collection, Iterable} from "@extollo/util"
import {FirebaseUnit, RTDBRef} from "./units/FirebaseUnit" import {FirebaseUnit, RTDBRef} from "./units/FirebaseUnit"
import * as firebase from "firebase-admin" import * as firebase from "firebase-admin"
import {Application} from "@extollo/lib" import {Application} from "@extollo/lib"

View File

@ -3,6 +3,7 @@ import {Injectable, Inject} from "@extollo/di"
import {TransactionResource, TransactionResourceItem} from "../../../rtdb/TransactionResource" import {TransactionResource, TransactionResourceItem} from "../../../rtdb/TransactionResource"
import {many, one} from "@extollo/util" import {many, one} from "@extollo/util"
import {Blockchain as BlockchainService} from "../../../units/Blockchain" import {Blockchain as BlockchainService} from "../../../units/Blockchain"
import {ExposureResource, ExposureResourceItem} from "../../../rtdb/ExposureResource";
/** /**
* Blockchain Controller * Blockchain Controller
@ -42,4 +43,19 @@ export class Blockchain extends Controller {
await (<TransactionResource> this.make(TransactionResource)).push(item) await (<TransactionResource> this.make(TransactionResource)).push(item)
return one(item) return one(item)
} }
/**
* Post a new exposure notification to the blockchain. This is only intended for testing.
*/
public async postExposure() {
const item: ExposureResourceItem = {
firebaseID: '',
seqID: -1,
clientID: String(this.request.input('clientID')),
timestamp: parseInt(String(this.request.input('timestamp'))),
}
await (<ExposureResource> this.make(ExposureResource)).push(item)
return one(item)
}
} }

View File

@ -1,6 +1,6 @@
import {error, Middleware} from "@extollo/lib" import {error, Middleware} from "@extollo/lib"
import {Injectable} from "@extollo/di" import {Injectable} from "@extollo/di"
import {HTTPStatus} from "@extollo/util"; import {HTTPStatus} from "@extollo/util"
/** /**
* ValidateEncounterTransaction Middleware * ValidateEncounterTransaction Middleware

View File

@ -0,0 +1,25 @@
import {error, Middleware} from "@extollo/lib"
import {Injectable} from "@extollo/di"
import {HTTPStatus} from "@extollo/util"
/**
* ValidateExposureTransaction Middleware
* --------------------------------------------
* Errors out the request if it is missing any fields required to create
* a new exposure notification on the blockchain.
*/
@Injectable()
export class ValidateExposureTransaction extends Middleware {
public async apply() {
const required: string[] = [
'clientID',
'timestamp',
]
for ( const field of required ) {
if ( !this.request.input(field) ) {
return error(`Missing required field: ${field}`, HTTPStatus.BAD_REQUEST, 'json')
}
}
}
}

View File

@ -5,5 +5,9 @@ Route.group('/api/v1', () => {
.pre('DebugOnly') .pre('DebugOnly')
.pre('api:ValidateEncounterTransaction') .pre('api:ValidateEncounterTransaction')
Route.post('/exposure', 'api:Blockchain.postExposure')
.pre('DebugOnly')
.pre('api:ValidateExposureTransaction')
Route.get('/chain', 'api:Blockchain.readBlockchain') Route.get('/chain', 'api:Blockchain.readBlockchain')
}) })

View File

@ -0,0 +1,24 @@
import {FirebaseResource, FirebaseResourceItem} from "../FirebaseResource"
import {Injectable} from "@extollo/di"
import {RTDBRef} from "../units/FirebaseUnit"
import {AsyncCollection} from "@extollo/util"
/**
* Interface representing a client-submitted encounter transaction.
*/
export interface ExposureResourceItem extends FirebaseResourceItem {
clientID: string; // the exposed client's ID - used as one half of the hashes
timestamp: number; // the unix-time in milliseconds when the interaction occurred
}
/**
* A Firebase realtime-database resource for managing exposure transactions.
*/
@Injectable()
export class ExposureResource extends FirebaseResource<ExposureResourceItem> {
public static collect(): AsyncCollection<ExposureResourceItem> {
return new AsyncCollection<ExposureResourceItem>(new ExposureResource())
}
protected refName: RTDBRef = 'exposure'
}

View File

@ -6,6 +6,7 @@ import { TransactionResourceItem } from "../rtdb/TransactionResource"
import * as openpgp from "openpgp" import * as openpgp from "openpgp"
import * as crypto from "crypto" import * as crypto from "crypto"
import { collect, uuid_v4 } from "@extollo/util" import { collect, uuid_v4 } from "@extollo/util"
import {ExposureResourceItem} from "../rtdb/ExposureResource";
/** /**
* Utility wrapper class for a block in the chain. * Utility wrapper class for a block in the chain.
@ -177,6 +178,32 @@ export class Blockchain extends Unit {
return new Block(block) return new Block(block)
} }
/**
* Submit the given exposure notifications onto the blockchain.
* @param exposures
*/
public async submitExposures(...exposures: ExposureResourceItem[]) {
const lastBlock = await this.getLastBlock()
this.logging.verbose('Last block:')
this.logging.verbose(lastBlock)
const block: BlockResourceItem = {
timestamp: (new Date).getTime(),
uuid: uuid_v4(),
transactions: exposures,
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)
}
/** /**
* Instantiate the genesis block of the entire chain. * Instantiate the genesis block of the entire chain.
*/ */

View File

@ -0,0 +1,72 @@
import { FirebaseUnit } from "../FirebaseUnit"
import { Singleton, Inject } from "@extollo/di"
import { Unit, Logging } from "@extollo/lib"
import { Blockchain } from "../Blockchain"
import { ExposureResource, ExposureResourceItem } from "../../rtdb/ExposureResource"
/**
* Exposure Unit
* ---------------------------------------
* This unit listens for exposure notifications created on the realtime database.
* When new ones come through, it validates them, and pushes them onto this
* server's blockchain.
*/
@Singleton()
export class Exposure extends Unit {
/** True if currently processing transactions. */
private processing: boolean = false
@Inject()
protected readonly firebase!: FirebaseUnit
@Inject()
protected readonly blockchain!: Blockchain
@Inject()
protected readonly logging!: Logging
/** Claim the right to process transactions. Returns true if the right was granted. */
claim() {
if ( !this.processing ) {
this.processing = true
return true
}
return false
}
/** Release the right to claim transactions. */
release() {
this.processing = false
}
/**
* Subscribe to the transactions reference and wait for new transactions to be added.
*/
public async up() {
this.firebase.ref('exposure').on('child_added', async (snapshot) => {
this.logging.debug('Received child_added event for exposures reference.')
if ( !this.claim() ) return
await this.firebase.trylock('block')
const exposure: ExposureResourceItem = snapshot.val()
// Push the exposure transactions onto the chain
await this.blockchain.submitExposures(exposure)
if ( snapshot.key )
await (<ExposureResource> this.make(ExposureResource)).ref().child(snapshot.key).remove()
this.release()
await this.firebase.unlock('block')
})
}
/**
* Release listeners and resources before shutdown.
*/
public async down() {
// Release all subscriptions before shutdown
this.firebase.ref("transaction").off()
}
}