mirror of
https://github.com/hackku21/loc-chain-backend.git
synced 2024-10-27 20:34:03 +00:00
Add proof-of-elapsed-time based consensus algorithm
This commit is contained in:
parent
fbc3711560
commit
9e4164632c
@ -12,6 +12,7 @@
|
|||||||
"@extollo/di": "^0.4.5",
|
"@extollo/di": "^0.4.5",
|
||||||
"@extollo/lib": "^0.1.5",
|
"@extollo/lib": "^0.1.5",
|
||||||
"@extollo/util": "^0.3.3",
|
"@extollo/util": "^0.3.3",
|
||||||
|
"axios": "^0.21.1",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"firebase-admin": "^9.6.0",
|
"firebase-admin": "^9.6.0",
|
||||||
|
1596
pnpm-lock.yaml
1596
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,18 @@ export class Blockchain extends Controller {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the version of the blockchain held by this host, including the host's
|
||||||
|
* most recent submission, that has NOT been accepted yet.
|
||||||
|
*/
|
||||||
|
public async readBlockchainSubmission() {
|
||||||
|
return many((await this.blockchain.getSubmitChain()).map(x => {
|
||||||
|
// @ts-ignore
|
||||||
|
delete x.firebaseID
|
||||||
|
return x
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the current blockchain is valid.
|
* Determine whether the current blockchain is valid.
|
||||||
*/
|
*/
|
||||||
|
@ -13,8 +13,12 @@ Route.group('/api/v1', () => {
|
|||||||
.pre('DebugOnly')
|
.pre('DebugOnly')
|
||||||
|
|
||||||
Route.get('/chain', 'api:Blockchain.readBlockchain')
|
Route.get('/chain', 'api:Blockchain.readBlockchain')
|
||||||
|
|
||||||
Route.get('/check', 'api:Blockchain.check')
|
Route.get('/check', 'api:Blockchain.check')
|
||||||
.pre('api:FirebaseUserOnly')
|
.pre('api:FirebaseUserOnly')
|
||||||
|
|
||||||
Route.get('/check-debug', 'api:Blockchain.check')
|
Route.get('/check-debug', 'api:Blockchain.check')
|
||||||
.pre('DebugOnly')
|
.pre('DebugOnly')
|
||||||
|
|
||||||
|
Route.get('/chain/submit', 'api:Blockchain.readBlockchainSubmission')
|
||||||
})
|
})
|
||||||
|
@ -57,6 +57,23 @@ export interface BlockResourceItem extends FirebaseResourceItem {
|
|||||||
lastBlockUUID: string; // the UUID of the previous block
|
lastBlockUUID: string; // the UUID of the previous block
|
||||||
proof: string; // the generated proof-of-work string
|
proof: string; // the generated proof-of-work string
|
||||||
timestamp: number; // millisecond unix timestamp when this block was created
|
timestamp: number; // millisecond unix timestamp when this block was created
|
||||||
|
waitTime: number; // number of milliseconds between last block and this one
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given item is a valid BlockResourceItem.
|
||||||
|
* @param what
|
||||||
|
*/
|
||||||
|
export function isBlockResourceItem(what: any): what is BlockResourceItem {
|
||||||
|
return (
|
||||||
|
typeof what?.uuid === 'string'
|
||||||
|
&& Array.isArray(what?.transactions)
|
||||||
|
&& typeof what?.lastBlockHash === 'string'
|
||||||
|
&& typeof what?.lastBlockUUID === 'string'
|
||||||
|
&& typeof what?.proof === 'string'
|
||||||
|
&& typeof what?.timestamp === 'number'
|
||||||
|
&& typeof what?.waitTime === 'number'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
24
src/app/rtdb/PeerResource.ts
Normal file
24
src/app/rtdb/PeerResource.ts
Normal 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 peer of this node.
|
||||||
|
*/
|
||||||
|
export interface PeerResourceItem extends FirebaseResourceItem {
|
||||||
|
host: string,
|
||||||
|
name?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Firebase realtime-database resource for managing blockchain peers.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class PeerResource extends FirebaseResource<PeerResourceItem> {
|
||||||
|
public static collect(): AsyncCollection<PeerResourceItem> {
|
||||||
|
return new AsyncCollection<PeerResourceItem>(new PeerResource())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected refName: RTDBRef = 'peers'
|
||||||
|
}
|
@ -1,12 +1,20 @@
|
|||||||
import { Singleton, Inject } from "@extollo/di"
|
import { Singleton, Inject } from "@extollo/di"
|
||||||
import { Unit, Logging, Config, Application } from "@extollo/lib"
|
import { Unit, Logging, Config, Application } from "@extollo/lib"
|
||||||
import { FirebaseUnit } from "./FirebaseUnit"
|
import { FirebaseUnit } from "./FirebaseUnit"
|
||||||
import { BlockEncounterTransaction, BlockResource, BlockResourceItem, BlockTransaction } from "../rtdb/BlockResource"
|
import {
|
||||||
|
BlockEncounterTransaction,
|
||||||
|
BlockResource,
|
||||||
|
BlockResourceItem,
|
||||||
|
BlockTransaction,
|
||||||
|
isBlockResourceItem
|
||||||
|
} from "../rtdb/BlockResource"
|
||||||
import { TransactionResourceItem } from "../rtdb/TransactionResource"
|
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 axios from "axios"
|
||||||
import { collect, uuid_v4 } from "@extollo/util"
|
import { collect, uuid_v4 } from "@extollo/util"
|
||||||
import {ExposureResourceItem} from "../rtdb/ExposureResource";
|
import {ExposureResourceItem} from "../rtdb/ExposureResource"
|
||||||
|
import {PeerResource} from "../rtdb/PeerResource"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility wrapper class for a block in the chain.
|
* Utility wrapper class for a block in the chain.
|
||||||
@ -20,6 +28,7 @@ export class Block implements BlockResourceItem {
|
|||||||
lastBlockHash: string
|
lastBlockHash: string
|
||||||
lastBlockUUID: string
|
lastBlockUUID: string
|
||||||
proof: string
|
proof: string
|
||||||
|
waitTime: number
|
||||||
|
|
||||||
get config(): Config {
|
get config(): Config {
|
||||||
return Application.getApplication().make(Config)
|
return Application.getApplication().make(Config)
|
||||||
@ -34,6 +43,7 @@ export class Block implements BlockResourceItem {
|
|||||||
this.lastBlockUUID = rec.lastBlockUUID
|
this.lastBlockUUID = rec.lastBlockUUID
|
||||||
this.proof = rec.proof
|
this.proof = rec.proof
|
||||||
this.timestamp = rec.timestamp
|
this.timestamp = rec.timestamp
|
||||||
|
this.waitTime = rec.waitTime
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns true if this is the genesis block. */
|
/** Returns true if this is the genesis block. */
|
||||||
@ -75,6 +85,7 @@ export class Block implements BlockResourceItem {
|
|||||||
lastBlockUUID: this.lastBlockUUID,
|
lastBlockUUID: this.lastBlockUUID,
|
||||||
proof: this.proof,
|
proof: this.proof,
|
||||||
timestamp: this.timestamp,
|
timestamp: this.timestamp,
|
||||||
|
waitTime: this.waitTime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +93,7 @@ export class Block implements BlockResourceItem {
|
|||||||
toString() {
|
toString() {
|
||||||
return [
|
return [
|
||||||
this.uuid,
|
this.uuid,
|
||||||
|
this.waitTime,
|
||||||
JSON.stringify(this.transactions || [], undefined, 0),
|
JSON.stringify(this.transactions || [], undefined, 0),
|
||||||
this.lastBlockHash,
|
this.lastBlockHash,
|
||||||
this.lastBlockUUID,
|
this.lastBlockUUID,
|
||||||
@ -113,6 +125,17 @@ export class Blockchain extends Unit {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly config!: Config
|
protected readonly config!: Config
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block transactions that will be attempted as part of this host's
|
||||||
|
* next block submission.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected pendingTransactions: BlockTransaction[] = []
|
||||||
|
|
||||||
|
protected pendingSubmit?: Block
|
||||||
|
|
||||||
|
protected isSubmitting: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given host is registered as a peer.
|
* Returns true if the given host is registered as a peer.
|
||||||
* @param host
|
* @param host
|
||||||
@ -126,8 +149,23 @@ export class Blockchain extends Unit {
|
|||||||
* Get a list of all registered peers.
|
* Get a list of all registered peers.
|
||||||
*/
|
*/
|
||||||
public async getPeers(): Promise<Peer[]> {
|
public async getPeers(): Promise<Peer[]> {
|
||||||
const data = await this.firebase.ref('peers').once('value')
|
return PeerResource.collect().all()
|
||||||
return (data.val() as Peer[]) || []
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From a peer, fetch the submission blockchain, if it is valid.
|
||||||
|
* @param peer
|
||||||
|
*/
|
||||||
|
public async getPeerSubmit(peer: Peer): Promise<Block[] | undefined> {
|
||||||
|
try {
|
||||||
|
const result = await axios.get(peer.host)
|
||||||
|
const blocks: unknown = result.data?.data?.records
|
||||||
|
if ( Array.isArray(blocks) && blocks.every(isBlockResourceItem) ) {
|
||||||
|
return blocks.map(x => new Block(x))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,7 +174,14 @@ export class Blockchain extends Unit {
|
|||||||
*/
|
*/
|
||||||
public async registerPeer(peer: Peer) {
|
public async registerPeer(peer: Peer) {
|
||||||
if (!(await this.hasPeer(peer.host))) {
|
if (!(await this.hasPeer(peer.host))) {
|
||||||
await this.firebase.ref('peers').push().set(peer)
|
await (<PeerResource> this.make(PeerResource)).push({
|
||||||
|
firebaseID: '',
|
||||||
|
seqID: -1,
|
||||||
|
name: peer.name,
|
||||||
|
host: peer.host,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,8 +209,6 @@ export class Blockchain extends Unit {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const pass = (
|
const pass = (
|
||||||
block.lastBlockUUID === previous.uuid
|
block.lastBlockUUID === previous.uuid
|
||||||
&& block.lastBlockHash === previous.hash()
|
&& block.lastBlockHash === previous.hash()
|
||||||
@ -187,7 +230,88 @@ export class Blockchain extends Unit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async refresh() {
|
public async refresh() {
|
||||||
|
if ( this.isSubmitting ) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
this.isSubmitting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const validSeqID = (await this.read()).reverse()[0]?.seqID
|
||||||
|
|
||||||
|
const peers = await this.getPeers()
|
||||||
|
const time_x_block: {[key: string]: Block} = {}
|
||||||
|
const time_x_peer: {[key: string]: Peer | true} = {}
|
||||||
|
|
||||||
|
for ( const peer of peers ) {
|
||||||
|
const blocks: Block[] | undefined = await this.getPeerSubmit(peer)
|
||||||
|
if ( blocks && await this.validate(blocks) ) {
|
||||||
|
const block = blocks.reverse()[0]
|
||||||
|
if ( !block || block.seqID === validSeqID || !block.seqID ) continue
|
||||||
|
|
||||||
|
time_x_block[block.waitTime] = block
|
||||||
|
time_x_peer[block.waitTime] = peer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.pendingTransactions.length && !this.pendingSubmit ) {
|
||||||
|
await this.attemptSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.pendingSubmit ) {
|
||||||
|
time_x_block[this.pendingSubmit.waitTime] = this.pendingSubmit
|
||||||
|
time_x_peer[this.pendingSubmit.waitTime] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = Math.min(...Object.keys(time_x_block).map(parseFloat))
|
||||||
|
const block = time_x_block[min]
|
||||||
|
const peer = time_x_peer[min]
|
||||||
|
|
||||||
|
await (<BlockResource>this.app().make(BlockResource)).push(block)
|
||||||
|
if ( peer === true ) {
|
||||||
|
this.pendingSubmit = undefined
|
||||||
|
this.pendingTransactions = []
|
||||||
|
} else {
|
||||||
|
this.pendingSubmit = undefined
|
||||||
|
await this.attemptSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSubmitting = false
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSubmitChain(): Promise<BlockResourceItem[]> {
|
||||||
|
const blocks = await this.read()
|
||||||
|
const submit = await this.attemptSubmit()
|
||||||
|
if ( submit ) {
|
||||||
|
submit.seqID = blocks.length > 0 ? collect<BlockResourceItem>(blocks).max('seqID') + 1 : 0
|
||||||
|
blocks.push(submit.toItem())
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
public async attemptSubmit() {
|
||||||
|
if ( !this.pendingSubmit && this.pendingTransactions.length ) {
|
||||||
|
const lastBlock = await this.getLastBlock()
|
||||||
|
const waitTime = this.random(3000, 5000)
|
||||||
|
const proof = await this.generateProofOfWork(lastBlock, waitTime)
|
||||||
|
|
||||||
|
const block: BlockResourceItem = {
|
||||||
|
timestamp: (new Date).getTime(),
|
||||||
|
uuid: uuid_v4(),
|
||||||
|
transactions: this.pendingTransactions,
|
||||||
|
lastBlockHash: lastBlock!.hash(),
|
||||||
|
lastBlockUUID: lastBlock!.uuid,
|
||||||
|
proof,
|
||||||
|
waitTime,
|
||||||
|
|
||||||
|
firebaseID: '',
|
||||||
|
seqID: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingSubmit = new Block(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pendingSubmit
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -195,7 +319,16 @@ export class Blockchain extends Unit {
|
|||||||
* @param group
|
* @param group
|
||||||
*/
|
*/
|
||||||
public async submitTransactions(group: [TransactionResourceItem, TransactionResourceItem]) {
|
public async submitTransactions(group: [TransactionResourceItem, TransactionResourceItem]) {
|
||||||
const lastBlock = await this.getLastBlock()
|
const txes = group.map(item => this.getEncounterTransaction(item))
|
||||||
|
|
||||||
|
if ( this.pendingSubmit ) {
|
||||||
|
this.pendingSubmit.transactions.push(...txes)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingTransactions.push(...txes)
|
||||||
|
this.refresh()
|
||||||
|
|
||||||
|
/*const lastBlock = await this.getLastBlock()
|
||||||
|
|
||||||
this.logging.verbose('Last block:')
|
this.logging.verbose('Last block:')
|
||||||
this.logging.verbose(lastBlock)
|
this.logging.verbose(lastBlock)
|
||||||
@ -213,7 +346,7 @@ export class Blockchain extends Unit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await (<BlockResource>this.app().make(BlockResource)).push(block)
|
await (<BlockResource>this.app().make(BlockResource)).push(block)
|
||||||
return new Block(block)
|
return new Block(block)*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -221,7 +354,14 @@ export class Blockchain extends Unit {
|
|||||||
* @param exposures
|
* @param exposures
|
||||||
*/
|
*/
|
||||||
public async submitExposures(...exposures: ExposureResourceItem[]) {
|
public async submitExposures(...exposures: ExposureResourceItem[]) {
|
||||||
const lastBlock = await this.getLastBlock()
|
if ( this.pendingSubmit ) {
|
||||||
|
this.pendingSubmit.transactions.push(...exposures)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingTransactions.push(...exposures)
|
||||||
|
this.refresh()
|
||||||
|
|
||||||
|
/*const lastBlock = await this.getLastBlock()
|
||||||
|
|
||||||
this.logging.verbose('Last block:')
|
this.logging.verbose('Last block:')
|
||||||
this.logging.verbose(lastBlock)
|
this.logging.verbose(lastBlock)
|
||||||
@ -239,7 +379,7 @@ export class Blockchain extends Unit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await (<BlockResource>this.app().make(BlockResource)).push(block)
|
await (<BlockResource>this.app().make(BlockResource)).push(block)
|
||||||
return new Block(block)
|
return new Block(block)*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -263,6 +403,7 @@ export class Blockchain extends Unit {
|
|||||||
})),
|
})),
|
||||||
firebaseID: '',
|
firebaseID: '',
|
||||||
seqID: -1,
|
seqID: -1,
|
||||||
|
waitTime: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,13 +442,16 @@ export class Blockchain extends Unit {
|
|||||||
/**
|
/**
|
||||||
* Generate a proof of work string for the block that follows lastBlock.
|
* Generate a proof of work string for the block that follows lastBlock.
|
||||||
* @param lastBlock
|
* @param lastBlock
|
||||||
|
* @param waitTime
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected async generateProofOfWork(lastBlock: Block): Promise<string> {
|
protected async generateProofOfWork(lastBlock: Block, waitTime: number): Promise<string> {
|
||||||
const hashString = lastBlock.hash()
|
const hashString = lastBlock.hash()
|
||||||
const privateKey = this.config.get("app.gpg.key.private")
|
const privateKey = this.config.get("app.gpg.key.private")
|
||||||
const message = openpgp.Message.fromText(hashString)
|
const message = openpgp.Message.fromText(hashString)
|
||||||
|
|
||||||
|
await this.sleep(waitTime)
|
||||||
|
|
||||||
// Sign the hash using the server's private key
|
// Sign the hash using the server's private key
|
||||||
return (await openpgp.sign({
|
return (await openpgp.sign({
|
||||||
message,
|
message,
|
||||||
@ -338,4 +482,20 @@ export class Blockchain extends Unit {
|
|||||||
|
|
||||||
return !!(await result.signatures?.[0]?.verified)
|
return !!(await result.signatures?.[0]?.verified)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user