mirror of
				https://github.com/hackku21/loc-chain-backend.git
				synced 2025-06-13 12:53:59 +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/lib": "^0.1.5", | ||||
|     "@extollo/util": "^0.3.3", | ||||
|     "axios": "^0.21.1", | ||||
|     "bcrypt": "^5.0.1", | ||||
|     "copyfiles": "^2.4.1", | ||||
|     "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. | ||||
|      */ | ||||
|  | ||||
| @ -13,8 +13,12 @@ Route.group('/api/v1', () => { | ||||
|         .pre('DebugOnly') | ||||
| 
 | ||||
|     Route.get('/chain', 'api:Blockchain.readBlockchain') | ||||
| 
 | ||||
|     Route.get('/check', 'api:Blockchain.check') | ||||
|         .pre('api:FirebaseUserOnly') | ||||
| 
 | ||||
|     Route.get('/check-debug', 'api:Blockchain.check') | ||||
|         .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
 | ||||
|     proof: string;  // the generated proof-of-work string
 | ||||
|     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 { Unit, Logging, Config, Application } from "@extollo/lib" | ||||
| 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 * as openpgp from "openpgp" | ||||
| import * as crypto from "crypto" | ||||
| import axios from "axios" | ||||
| 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. | ||||
| @ -20,6 +28,7 @@ export class Block implements BlockResourceItem { | ||||
|     lastBlockHash: string | ||||
|     lastBlockUUID: string | ||||
|     proof: string | ||||
|     waitTime: number | ||||
| 
 | ||||
|     get config(): Config { | ||||
|         return Application.getApplication().make(Config) | ||||
| @ -34,6 +43,7 @@ export class Block implements BlockResourceItem { | ||||
|         this.lastBlockUUID = rec.lastBlockUUID | ||||
|         this.proof = rec.proof | ||||
|         this.timestamp = rec.timestamp | ||||
|         this.waitTime = rec.waitTime | ||||
|     } | ||||
| 
 | ||||
|     /** Returns true if this is the genesis block. */ | ||||
| @ -75,6 +85,7 @@ export class Block implements BlockResourceItem { | ||||
|             lastBlockUUID: this.lastBlockUUID, | ||||
|             proof: this.proof, | ||||
|             timestamp: this.timestamp, | ||||
|             waitTime: this.waitTime, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -82,6 +93,7 @@ export class Block implements BlockResourceItem { | ||||
|     toString() { | ||||
|         return [ | ||||
|             this.uuid, | ||||
|             this.waitTime, | ||||
|             JSON.stringify(this.transactions || [], undefined, 0), | ||||
|             this.lastBlockHash, | ||||
|             this.lastBlockUUID, | ||||
| @ -113,6 +125,17 @@ export class Blockchain extends Unit { | ||||
|     @Inject() | ||||
|     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. | ||||
|      * @param host | ||||
| @ -126,8 +149,23 @@ export class Blockchain extends Unit { | ||||
|      * 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[]) || [] | ||||
|         return PeerResource.collect().all() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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) { | ||||
|         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; | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|                 const pass = ( | ||||
|                     block.lastBlockUUID === previous.uuid | ||||
|                     && block.lastBlockHash === previous.hash() | ||||
| @ -187,7 +230,88 @@ export class Blockchain extends Unit { | ||||
|     } | ||||
| 
 | ||||
|     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 | ||||
|      */ | ||||
|     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(lastBlock) | ||||
| @ -213,7 +346,7 @@ export class Blockchain extends Unit { | ||||
|         } | ||||
| 
 | ||||
|         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 | ||||
|      */ | ||||
|     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(lastBlock) | ||||
| @ -239,7 +379,7 @@ export class Blockchain extends Unit { | ||||
|         } | ||||
| 
 | ||||
|         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: '', | ||||
|             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. | ||||
|      * @param lastBlock | ||||
|      * @param waitTime | ||||
|      * @protected | ||||
|      */ | ||||
|     protected async generateProofOfWork(lastBlock: Block): Promise<string> { | ||||
|     protected async generateProofOfWork(lastBlock: Block, waitTime: number): Promise<string> { | ||||
|         const hashString = lastBlock.hash() | ||||
|         const privateKey = this.config.get("app.gpg.key.private") | ||||
|         const message = openpgp.Message.fromText(hashString) | ||||
| 
 | ||||
|         await this.sleep(waitTime) | ||||
| 
 | ||||
|         // Sign the hash using the server's private key
 | ||||
|         return (await openpgp.sign({ | ||||
|             message, | ||||
| @ -338,4 +482,20 @@ export class Blockchain extends Unit { | ||||
| 
 | ||||
|         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