Skip to content

Commit

Permalink
Merge pull request #5 from push-protocol/block-validation
Browse files Browse the repository at this point in the history
Block validation
  • Loading branch information
akp111 authored Oct 24, 2024
2 parents 6ba69e5 + 7dc44df commit b1d989a
Show file tree
Hide file tree
Showing 18 changed files with 2,234 additions and 58 deletions.
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"typescript-eslint": "^8.0.1"
},
"dependencies": {
"@ethersproject/transactions": "^5.7.0",
"@klerick/nestjs-json-rpc": "^1.0.0",
"@nestjs/common": "^10.3.10",
"@nestjs/config": "^3.2.3",
Expand All @@ -63,16 +64,23 @@
"@nestjs/websockets": "^10.4.1",
"@prisma/client": "^5.19.1",
"@pushprotocol/node-core": "^0.0.8",
"@solana/web3.js": "^1.95.4",
"@types/bs58": "^4.0.4",
"bs58": "^6.0.0",
"crypto-js": "^4.2.0",
"ethers": "^5.7.2",
"google-protobuf": "^3.21.4",
"node-schedule": "^2.1.1",
"object-hash": "^3.0.0",
"prisma": "^5.19.1",
"reflect-metadata": "^0.2.2",
"starknet": "^6.11.0",
"swagger-ui-express": "^5.0.1",
"ts-luxon": "^5.0.7-beta.0",
"tweetnacl": "^1.0.3",
"typedi": "^0.10.0",
"util": "^0.12.5",
"winston": "^3.14.2",
"ws": "^8.18.0"
}
}
}
79 changes: 69 additions & 10 deletions src/modules/archive/archive-node.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,60 @@ import { Consumer, QItem } from '../../messaging/types/queue-types';
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { ObjectHasher } from '../../utils/objectHasher';

import { ValidatorContractState } from '../validator/validator-contract-state.service';
import { BlockUtil } from '../../utils/blockUtil';
import {
InputJsonValue,
InputJsonObject,
} from '@prisma/client/runtime/library';

type Transaction = {
ts?: bigint | number;
txn_hash: string;
block_hash: string;
category: string;
sender: string;
status: string;
from: string;
recipients: InputJsonValue;
data: Buffer;
data_as_json: InputJsonValue;
sig: string;
};
@Injectable()
export class ArchiveNodeService implements Consumer<QItem> {
constructor(private readonly prisma: PrismaService) {}
valContractState: ValidatorContractState = new ValidatorContractState();

async postConstruct() {
await this.valContractState.onModuleInit();
}
constructor(private readonly prisma: PrismaService) {
this.postConstruct()
}

public async accept(item: QItem): Promise<boolean> {
try {
// Deserialize the block data
const bytes = Uint8Array.from(Buffer.from(item.object, 'hex'));
const block = Block.deserializeBinary(bytes).toObject();

const deserializedBlock = Block.deserializeBinary(bytes);
const block = deserializedBlock.toObject();

// Block validation //
// validate the hash
const calculatedHash = BlockUtil.hashBlockAsHex(bytes);
if (calculatedHash != item.object_hash) {
throw new Error(
'received item hash= , ' +
item.object_hash +
'which differs from calculatedHash=, ' +
calculatedHash +
'ignoring the block because producer calculated the hash incorrectly',
);
}
// validate the signature
if (!(await this.validateBlock(deserializedBlock))) {
throw new Error('Block validation failed');
}
// Extract block hash from the block
const blockHash = this.getBlockHash(block);
if (await this.isBlockAlreadyStored(blockHash)) {
Expand All @@ -39,7 +82,6 @@ export class ArchiveNodeService implements Consumer<QItem> {
console.log('All transactions already exist, skipping block insert.');
return true;
}

// Insert block into the database
await this.prisma.block.create({ data: blockData });

Expand All @@ -54,6 +96,22 @@ export class ArchiveNodeService implements Consumer<QItem> {
}
}

private async validateBlock(block: Block) {
const validatorSet = new Set(this.valContractState.getAllNodesMap().keys());
const validationPerBlock = this.valContractState.contractCli.valPerBlock;
const validationRes = await BlockUtil.checkBlockFinalized(
block,
validatorSet,
validationPerBlock,
);
if (!validationRes.success) {
console.error('Error while block validation');
return false;
} else {
return true;
}
}

private getBlockHash(block: Block.AsObject): string {
// Generate a block hash using the ObjectHasher utility
return ObjectHasher.hashToSha256(block);
Expand All @@ -66,7 +124,9 @@ export class ArchiveNodeService implements Consumer<QItem> {
return block !== null;
}

private recursivelyConvertToJSON(obj: any): any {
private recursivelyConvertToJSON(
obj: Uint8Array | Array<unknown> | object,
): InputJsonObject | InputJsonValue {
if (obj instanceof Uint8Array) {
// Convert Uint8Array to a base64 string
return Buffer.from(obj).toString('base64');
Expand All @@ -77,11 +137,11 @@ export class ArchiveNodeService implements Consumer<QItem> {
}

if (obj !== null && typeof obj === 'object') {
const convertedObj: any = {};
const convertedObj: unknown = {};
for (const key in obj) {
convertedObj[key] = this.recursivelyConvertToJSON(obj[key]);
}
return convertedObj;
return convertedObj as InputJsonObject;
}

return obj;
Expand All @@ -92,7 +152,7 @@ export class ArchiveNodeService implements Consumer<QItem> {
txObjList: Block.AsObject['txobjList'],
blockHash: string,
blockTs: number,
): Promise<any[]> {
): Promise<Transaction[]> {
const transactionsData = [];

for (const txObj of txObjList) {
Expand Down Expand Up @@ -120,7 +180,6 @@ export class ArchiveNodeService implements Consumer<QItem> {
data_as_json: txObj,
sig: txObj.tx?.signature,
};

transactionsData.push(txData);
}

Expand Down
122 changes: 113 additions & 9 deletions src/utils/EthUtil.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
import { hashMessage } from '@ethersproject/hash'
import { recoverAddress } from '@ethersproject/transactions'
import { BytesLike, ethers, Wallet } from 'ethers'
import { verifyMessage } from 'ethers/lib/utils'
import { Logger } from 'winston'

import { BitUtil } from './bitUtil'
import { CaipAddr } from './chainUtil'
import { Check } from './check'
import { ObjectHasher } from './objectHasher'
import StrUtil from './strUtil'
import { WinstonUtil } from './winstonUtil'

/**
* Utitily class that allows
* - to sign objects with an eth private key
* - to check that signature later
*
* Ignores 'signature' properties
*/
export class EthUtil {
public static log: Logger = WinstonUtil.newLog(EthUtil)

// sign object
public static async create(wallet: Wallet, ...objectsToHash: any[]): Promise<string> {
const ethMessage = ObjectHasher.hashToSha256IgnoreSig(objectsToHash)
const sig = await wallet.signMessage(ethMessage)
return sig
}

static parseCaipAddress(addressinCAIP: string): CaipAddr | null {
if (StrUtil.isEmpty(addressinCAIP)) {
return null
Expand All @@ -22,14 +49,91 @@ export class EthUtil {
return null
}
}
}

// ex: eip155:5:0xD8634C39BBFd4033c0d3289C4515275102423681
export class CaipAddr {
// ex: eip155
namespace: string
// ex: 5
chainId: string | null
// ex: 0xD8634C39BBFd4033c0d3289C4515275102423681
addr: string
// check object
public static check(sig: string, targetWallet: string, ...objectsToHash: any[]): boolean {
const ethMessage = ObjectHasher.hashToSha256IgnoreSig(objectsToHash)
const verificationAddress = verifyMessage(ethMessage, sig)
if (targetWallet !== verificationAddress) {
return false
}
return true
}

public static isEthZero(addr: string) {
return '0x0000000000000000000000000000000000000000' === addr
}

static getMessageHashAsInContract(message: string): string {
return ethers.utils.keccak256(ethers.utils.arrayify(message))
}

static toBytes(value: BytesLike | number): Uint8Array {
return ethers.utils.arrayify(value)
}

// simple sign with a private key
static async signString(wallet: ethers.Signer, message: string): Promise<string> {
return await wallet.signMessage(this.toBytes(message))
}

// simple check signature's public key (via address)
public static async checkString(
message: string,
sig: string,
targetWallet: string
): Promise<boolean> {
const verificationAddress = verifyMessage(this.toBytes(message), sig)
console.log('verification address:', verificationAddress)
if (targetWallet !== verificationAddress) {
return false
}
return true
}

// https://ethereum.org/es/developers/tutorials/eip-1271-smart-contract-signatures/
// sign 'message hash'
public static async signForContract(wallet: ethers.Signer, message: string): Promise<string> {
const hash = this.getMessageHashAsInContract(message)
return await wallet.signMessage(this.toBytes(hash))
}

// check 'message hash'
public static async checkForContract(
message: string,
sig: string,
targetWallet: string
): Promise<boolean> {
const hash = this.getMessageHashAsInContract(message)
const verificationAddress = verifyMessage(ethers.utils.arrayify(hash), sig)
console.log('verification address:', verificationAddress)
if (targetWallet !== verificationAddress) {
return false
}
return true
}

// 0xAAAA == eip155:1:0xAAAAA
public static recoverAddressFromMsg(message: Uint8Array, signature: Uint8Array): string {
return recoverAddress(hashMessage(message), signature)
}

public static recoverAddress(hash: Uint8Array, signature: Uint8Array): string {
return recoverAddress(hash, signature)
}

public static ethHash(message: Uint8Array) {
return hashMessage(message)
}

public static async signBytes(wallet: Wallet, bytes: Uint8Array): Promise<Uint8Array> {
const sig = await wallet.signMessage(bytes)
Check.isTrue(sig.startsWith('0x'))
const sigNoPrefix = sig.slice(2)
const result = BitUtil.base16ToBytes(sigNoPrefix)
Check.isTrue(result != null && result.length > 0)
return result
}
}

export function Signed(target: Function) {}
36 changes: 36 additions & 0 deletions src/utils/arrayUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export class ArrayUtil {
public static isEmpty<T>(arr: T[] | Uint8Array | null): boolean {
if (arr == null) {
return true
}
if (typeof arr !== 'object') {
return false
}
return arr.length === 0
}

private static isArrayEmpty(array: Uint8Array | null): boolean {
return array == null || array.length === 0
}

public static hasMinSize(array: Uint8Array, minSize: number): boolean {
if (minSize === 0) {
return ArrayUtil.isArrayEmpty(array)
}
return array.length >= minSize
}

public static isEqual(arr0: Uint8Array, arr1: Uint8Array): boolean {
if (arr0 == null && arr1 == null) {
return true
}
if (arr0 == arr1) {
return true
}
if (arr0.length !== arr1.length) {
return false
}
return Buffer.from(arr0).equals(Buffer.from(arr1))
}
}

Loading

0 comments on commit b1d989a

Please sign in to comment.