From 7b0caceba76ba2ec284b9708b0484740ccbfbc81 Mon Sep 17 00:00:00 2001 From: Gabriel Martinez Rodriguez Date: Thu, 23 Jan 2025 14:53:14 +0100 Subject: [PATCH 1/6] feat(tandomnness): improve performance and reability --- apps/randomness/src/CustomGasEstimator.ts | 4 ++++ apps/randomness/src/DrandService.ts | 2 +- apps/randomness/src/index.ts | 27 ++++++++++++----------- contracts/src/randomness/Random.sol | 2 +- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/randomness/src/CustomGasEstimator.ts b/apps/randomness/src/CustomGasEstimator.ts index cecdac0524..60efea55b5 100644 --- a/apps/randomness/src/CustomGasEstimator.ts +++ b/apps/randomness/src/CustomGasEstimator.ts @@ -21,6 +21,10 @@ export class CustomGasEstimator extends DefaultGasLimitEstimator { return ok(100000n) } + if (transaction.functionName === "postDrand") { + return ok(220000n) + } + return this.simulateTransactionForGas(transactionManager, transaction) } } diff --git a/apps/randomness/src/DrandService.ts b/apps/randomness/src/DrandService.ts index 91f6f1a120..769d0154c7 100644 --- a/apps/randomness/src/DrandService.ts +++ b/apps/randomness/src/DrandService.ts @@ -38,7 +38,7 @@ export class DrandService { } const url = `${env.EVM_DRAND_URL}/rounds/${round}` - const response = await ResultAsync.fromPromise(fetchWithRetry(url, {}, 2, 500), unknownToError) + const response = await ResultAsync.fromPromise(fetchWithRetry(url, {}, 2, 1000), unknownToError) if (response.isErr()) { return err(DrandError.NetworkError) diff --git a/apps/randomness/src/index.ts b/apps/randomness/src/index.ts index a91e1b31fa..a3dbbe40ec 100644 --- a/apps/randomness/src/index.ts +++ b/apps/randomness/src/index.ts @@ -49,6 +49,10 @@ class RandomnessService { console.error(description) }) + // Start fetching Drand beacons for past rounds. Subsequently, beacons will be fetched at regular intervals + // to maintain synchronization with the Drand network + this.handleNewDrandBeacons() + // Synchronize the retrieval of new Drand beacons with the Drand network to request them as soon as they become available. const periodMs = Number(env.EVM_DRAND_PERIOD_SECONDS) * MS_IN_SECOND const drandGenesisTimestampMs = Number(env.EVM_DRAND_GENESIS_TIMESTAMP_SECONDS) * MS_IN_SECOND @@ -260,22 +264,19 @@ class RandomnessService { const randomnessToReveal = this.randomnessRepository.getRandomnessForBlockNumber(nextBlockNumber) - if (!randomnessToReveal) { - console.warn("Not found randomness to reveal with block number", nextBlockNumber) - return transactions - } - - const revealValueTransaction = this.transactionFactory.createRevealValueTransaction(randomnessToReveal) + if (randomnessToReveal) { + const revealValueTransaction = this.transactionFactory.createRevealValueTransaction(randomnessToReveal) - transactions.unshift(revealValueTransaction) + transactions.unshift(revealValueTransaction) - randomnessToReveal.addRevealTransactionIntentId(revealValueTransaction.intentId) + randomnessToReveal.addRevealTransactionIntentId(revealValueTransaction.intentId) - this.randomnessRepository.updateRandomness(randomnessToReveal).then((result) => { - if (result.isErr()) { - console.error("Failed to update randomness", result.error) - } - }) + this.randomnessRepository.updateRandomness(randomnessToReveal).then((result) => { + if (result.isErr()) { + console.error("Failed to update randomness", result.error) + } + }) + } transactions.push(...this.pendingPostDrandTransactions) diff --git a/contracts/src/randomness/Random.sol b/contracts/src/randomness/Random.sol index 3f3721542f..24375fff92 100644 --- a/contracts/src/randomness/Random.sol +++ b/contracts/src/randomness/Random.sol @@ -11,7 +11,7 @@ contract Random is RandomCommitment, Drand { * This is necessary, because whenever a Drand value is generated at time T, it is not possible to guarantee it * will be posted on a block with timestamp T (even if such a block exists) because of network delays. */ - uint256 public constant DRAND_DELAY_SECONDS = 2; + uint256 public constant DRAND_DELAY_SECONDS = 4; /** * The minimum amount of time (in seconds) that commitments to future Drand randomness must be made From 44e880c61ab4cd9826ff972097277e78cc1006fe Mon Sep 17 00:00:00 2001 From: Gabriel Martinez Rodriguez Date: Wed, 12 Feb 2025 13:04:40 +0100 Subject: [PATCH 2/6] chore(randomness): move drand logic from index to drand service --- apps/randomness/src/DrandService.ts | 124 +++++++++++++++++++++++++++- apps/randomness/src/env.ts | 2 +- apps/randomness/src/index.ts | 117 ++------------------------ 3 files changed, 131 insertions(+), 112 deletions(-) diff --git a/apps/randomness/src/DrandService.ts b/apps/randomness/src/DrandService.ts index 769d0154c7..8748c34526 100644 --- a/apps/randomness/src/DrandService.ts +++ b/apps/randomness/src/DrandService.ts @@ -1,7 +1,11 @@ -import { fetchWithRetry, nowInSeconds, unknownToError } from "@happy.tech/common" +import { fetchWithRetry, nowInSeconds, promiseWithResolvers, sleep, unknownToError } from "@happy.tech/common" +import type { Transaction } from "@happy.tech/txm" import { type Result, ResultAsync, err, ok } from "neverthrow" import type { Hex } from "viem" import { z } from "zod" +import { Drand } from "./Drand" +import type { DrandRepository } from "./DrandRepository" +import type { TransactionFactory } from "./TransactionFactory" import { env } from "./env" const drandBeaconSchema = z.object({ @@ -31,7 +35,39 @@ export enum DrandError { InvalidRound = "InvalidRound", } +const MS_IN_SECOND = 1000 + export class DrandService { + private readonly drandRepository: DrandRepository + private readonly transactionFactory: TransactionFactory + private pendingPostDrandTransactions: Transaction[] = [] + private getDrandBeaconLocked = false + private pendingGetDrandBeaconPromises: PromiseWithResolvers[] = [] + + constructor(drandRepository: DrandRepository, transactionFactory: TransactionFactory) { + this.drandRepository = drandRepository + this.transactionFactory = transactionFactory + } + + async start() { + // Synchronize the retrieval of new Drand beacons with the Drand network to request them as soon as they become available. + const periodMs = Number(env.EVM_DRAND_PERIOD_SECONDS) * MS_IN_SECOND + const drandGenesisTimestampMs = Number(env.EVM_DRAND_GENESIS_TIMESTAMP_SECONDS) * MS_IN_SECOND + const now = Date.now() + + // Calculates timestamp for the next Drand beacon: + // 1. Obtains the elapsed time since genesis: now - drandGenesisTimestampMs. + // 2. Divides this time by the period (periodMs) and rounds up to get the next round. + // 3. Converts the next round back to an absolute timestamp by multiplying by periodMs and adding drandGenesisTimestampMs. + const nextDrandBeaconTimestamp = + Math.ceil((now - drandGenesisTimestampMs) / periodMs) * periodMs + drandGenesisTimestampMs + await sleep(nextDrandBeaconTimestamp - now) + + setInterval(async () => { + await this.handleNewDrandBeacons() + }, periodMs) + } + async getDrandBeacon(round: bigint): Promise> { if (round <= 0n) { return err(DrandError.InvalidRound) @@ -73,4 +109,90 @@ export class DrandService { ) return BigInt(currentRound) } + + pullDrandTransactions() { + const returnTransactions = this.pendingPostDrandTransactions + this.pendingPostDrandTransactions = [] + return returnTransactions + } + + // Implements a mutex to ensure that only one instance of this function executes at a time. + // Calls made while the mutex is locked are queued as pending promises. + // When the current execution completes, the most recent pending promise is immediately resolved, + // allowing it to proceed without waiting for the next interval, while any other queued promises are rejected. + async handleNewDrandBeacons() { + if (this.getDrandBeaconLocked) { + const pending = promiseWithResolvers() + this.pendingGetDrandBeaconPromises.push(pending) + + try { + await pending.promise + } catch { + return + } + } + + this.getDrandBeaconLocked = true + try { + await this._handleNewDrandBeacons() + } catch (error) { + console.error("Error in handleNewDrandBeacons: ", error) + } + this.getDrandBeaconLocked = false + + this.pendingGetDrandBeaconPromises.pop()?.resolve() + this.pendingGetDrandBeaconPromises.forEach((p) => p.reject()) + } + + private async _handleNewDrandBeacons() { + const currentRound = this.currentRound() + const drandGaps = this.drandRepository.findRoundGapsInRange(currentRound - env.EVM_DRAND_MARGIN, currentRound) + + await Promise.all( + drandGaps.map(async (round) => { + let drandBeacon = await this.getDrandBeacon(round) + if (drandBeacon.isErr()) { + if (drandBeacon.error !== DrandError.TooEarly) { + console.error("Failed to get drand beacon", drandBeacon.error) + return + } + + await sleep(1000) + drandBeacon = await this.getDrandBeacon(round) + + if (drandBeacon.isErr()) { + console.error("Failed to get drand beacon", drandBeacon.error) + return + } + } + + const postDrandTransactionResult = this.transactionFactory.createPostDrandTransaction({ + round: round, + signature: drandBeacon.value.signature, + }) + + if (postDrandTransactionResult.isErr()) { + console.error("Failed to create post drand transaction", postDrandTransactionResult.error) + return + } + + const postDrandTransaction = postDrandTransactionResult.value + + const drand = Drand.create({ + round: round, + signature: drandBeacon.value.signature, + transactionIntentId: postDrandTransaction.intentId, + }) + + const drandSaved = await this.drandRepository.saveDrand(drand) + + if (drandSaved.isErr()) { + console.error("Failed to save drand", drandSaved.error) + return + } + + this.pendingPostDrandTransactions.push(postDrandTransaction) + }), + ) + } } diff --git a/apps/randomness/src/env.ts b/apps/randomness/src/env.ts index 3980e15b7c..41993c6e87 100644 --- a/apps/randomness/src/env.ts +++ b/apps/randomness/src/env.ts @@ -24,7 +24,7 @@ const envSchema = z.object({ EVM_DRAND_URL: z.string().trim(), EVM_DRAND_GENESIS_TIMESTAMP_SECONDS: z.string().transform((s) => BigInt(s)), EVM_DRAND_PERIOD_SECONDS: z.string().transform((s) => BigInt(s)), - EVM_DRAND_START_ROUND: z.string().transform((s) => BigInt(s)), + EVM_DRAND_MARGIN: z.string().transform((s) => BigInt(s)), }) const parsedEnv = envSchema.safeParse(process.env) diff --git a/apps/randomness/src/index.ts b/apps/randomness/src/index.ts index a3dbbe40ec..eda8585899 100644 --- a/apps/randomness/src/index.ts +++ b/apps/randomness/src/index.ts @@ -1,27 +1,20 @@ -import { promiseWithResolvers, sleep } from "@happy.tech/common" import { abis } from "@happy.tech/contracts/random/anvil" import { TransactionManager, TransactionStatus, TxmHookType } from "@happy.tech/txm" import type { LatestBlock, Transaction } from "@happy.tech/txm" import { CustomGasEstimator } from "./CustomGasEstimator.js" -import { Drand } from "./Drand" import { DrandRepository } from "./DrandRepository" -import { DrandError, DrandService } from "./DrandService" +import { DrandService } from "./DrandService" import { Randomness, RandomnessStatus } from "./Randomness.js" import { RandomnessRepository } from "./RandomnessRepository.js" import { TransactionFactory } from "./TransactionFactory.js" import { env } from "./env.js" -const MS_IN_SECOND = 1000 - class RandomnessService { private readonly randomnessRepository: RandomnessRepository private readonly drandRepository: DrandRepository private readonly txm: TransactionManager private readonly transactionFactory: TransactionFactory private readonly drandService: DrandService - private getDrandBeaconLocked = false - private pendingGetDrandBeaconPromises: PromiseWithResolvers[] = [] - private pendingPostDrandTransactions: Transaction[] = [] constructor() { this.randomnessRepository = new RandomnessRepository() @@ -37,7 +30,7 @@ class RandomnessService { }, }) this.transactionFactory = new TransactionFactory(this.txm, env.RANDOM_CONTRACT_ADDRESS, env.PRECOMMIT_DELAY) - this.drandService = new DrandService() + this.drandService = new DrandService(this.drandRepository, this.transactionFactory) } async start() { @@ -49,50 +42,7 @@ class RandomnessService { console.error(description) }) - // Start fetching Drand beacons for past rounds. Subsequently, beacons will be fetched at regular intervals - // to maintain synchronization with the Drand network - this.handleNewDrandBeacons() - - // Synchronize the retrieval of new Drand beacons with the Drand network to request them as soon as they become available. - const periodMs = Number(env.EVM_DRAND_PERIOD_SECONDS) * MS_IN_SECOND - const drandGenesisTimestampMs = Number(env.EVM_DRAND_GENESIS_TIMESTAMP_SECONDS) * MS_IN_SECOND - const now = Date.now() - - // Calculates timestamp for the next Drand beacon: - // 1. Obtains the elapsed time since genesis: now - drandGenesisTimestampMs. - // 2. Divides this time by the period (periodMs) and rounds up to get the next round. - // 3. Converts the next round back to an absolute timestamp by multiplying by periodMs and adding drandGenesisTimestampMs. - const nextDrandBeaconTimestamp = - Math.ceil((now - drandGenesisTimestampMs) / periodMs) * periodMs + drandGenesisTimestampMs - await sleep(nextDrandBeaconTimestamp - now) - - // Implements a mutex to ensure that only one instance of this function executes at a time. - // Calls made while the mutex is locked are queued as pending promises. - // When the current execution completes, the most recent pending promise is immediately resolved, - // allowing it to proceed without waiting for the next interval, while any other queued promises are rejected. - setInterval(async () => { - if (this.getDrandBeaconLocked) { - const pending = promiseWithResolvers() - this.pendingGetDrandBeaconPromises.push(pending) - - try { - await pending.promise - } catch { - return - } - } - - this.getDrandBeaconLocked = true - try { - await this.handleNewDrandBeacons() - } catch (error) { - console.error("Error in handleNewDrandBeacons: ", error) - } - this.getDrandBeaconLocked = false - - this.pendingGetDrandBeaconPromises.pop()?.resolve() - this.pendingGetDrandBeaconPromises.forEach((p) => p.reject()) - }, Number(env.EVM_DRAND_PERIOD_SECONDS) * MS_IN_SECOND) + this.drandService.start() } private onTransactionStatusChange(transaction: Transaction) { @@ -170,59 +120,6 @@ class RandomnessService { } } - private async handleNewDrandBeacons() { - const currentRound = this.drandService.currentRound() - const oldestDrand = this.drandRepository.getOldestDrandRound() ?? env.EVM_DRAND_START_ROUND - const drandGaps = this.drandRepository.findRoundGapsInRange(oldestDrand, currentRound) - - await Promise.all( - drandGaps.map(async (round) => { - let drandBeacon = await this.drandService.getDrandBeacon(round) - if (drandBeacon.isErr()) { - if (drandBeacon.error !== DrandError.TooEarly) { - console.error("Failed to get drand beacon", drandBeacon.error) - return - } - - await sleep(1000) - drandBeacon = await this.drandService.getDrandBeacon(round) - - if (drandBeacon.isErr()) { - console.error("Failed to get drand beacon", drandBeacon.error) - return - } - } - - const postDrandTransactionResult = this.transactionFactory.createPostDrandTransaction({ - round: round, - signature: drandBeacon.value.signature, - }) - - if (postDrandTransactionResult.isErr()) { - console.error("Failed to create post drand transaction", postDrandTransactionResult.error) - return - } - - const postDrandTransaction = postDrandTransactionResult.value - - const drand = Drand.create({ - round: round, - signature: drandBeacon.value.signature, - transactionIntentId: postDrandTransaction.intentId, - }) - - const drandSaved = await this.drandRepository.saveDrand(drand) - - if (drandSaved.isErr()) { - console.error("Failed to save drand", drandSaved.error) - return - } - - this.pendingPostDrandTransactions.push(postDrandTransaction) - }), - ) - } - private async onCollectTransactions(block: LatestBlock): Promise { const transactions: Transaction[] = [] @@ -278,9 +175,11 @@ class RandomnessService { }) } - transactions.push(...this.pendingPostDrandTransactions) + const drandTransactions = this.drandService.pullDrandTransactions() + + transactions.push(...drandTransactions) - this.pendingPostDrandTransactions.map(async (transaction) => { + drandTransactions.map(async (transaction) => { const drand = this.drandRepository.getDrandByTransactionIntentId(transaction.intentId) if (!drand) { @@ -294,8 +193,6 @@ class RandomnessService { }) }) - this.pendingPostDrandTransactions = [] - return transactions } } From bbf1d25c85e68f89439c098a9808e1d8aaa2ac3f Mon Sep 17 00:00:00 2001 From: Gabriel Martinez Rodriguez Date: Wed, 12 Feb 2025 13:07:48 +0100 Subject: [PATCH 3/6] chore(randomness): handle drand before interval --- apps/randomness/src/DrandService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/randomness/src/DrandService.ts b/apps/randomness/src/DrandService.ts index 8748c34526..108d1cd226 100644 --- a/apps/randomness/src/DrandService.ts +++ b/apps/randomness/src/DrandService.ts @@ -63,9 +63,9 @@ export class DrandService { Math.ceil((now - drandGenesisTimestampMs) / periodMs) * periodMs + drandGenesisTimestampMs await sleep(nextDrandBeaconTimestamp - now) - setInterval(async () => { - await this.handleNewDrandBeacons() - }, periodMs) + this.handleNewDrandBeacons() + + setInterval(this.handleNewDrandBeacons.bind(this), periodMs) } async getDrandBeacon(round: bigint): Promise> { From e0bc25324cdabe7df29211b0f9d7c06d78004288 Mon Sep 17 00:00:00 2001 From: Gabriel Martinez Rodriguez Date: Wed, 12 Feb 2025 13:11:32 +0100 Subject: [PATCH 4/6] chore(randomness): await for drand service --- apps/randomness/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/randomness/src/index.ts b/apps/randomness/src/index.ts index eda8585899..870db1bec2 100644 --- a/apps/randomness/src/index.ts +++ b/apps/randomness/src/index.ts @@ -42,7 +42,7 @@ class RandomnessService { console.error(description) }) - this.drandService.start() + await this.drandService.start() } private onTransactionStatusChange(transaction: Transaction) { From 7ef10f956660a9416c1599290e2394678d630d8d Mon Sep 17 00:00:00 2001 From: Gabriel Martinez Rodriguez Date: Fri, 14 Feb 2025 11:22:26 +0100 Subject: [PATCH 5/6] chore: changed gas limit --- apps/randomness/src/CustomGasEstimator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/randomness/src/CustomGasEstimator.ts b/apps/randomness/src/CustomGasEstimator.ts index 60efea55b5..d31e24cb64 100644 --- a/apps/randomness/src/CustomGasEstimator.ts +++ b/apps/randomness/src/CustomGasEstimator.ts @@ -18,11 +18,11 @@ export class CustomGasEstimator extends DefaultGasLimitEstimator { return ok(75000n) } if (transaction.functionName === "revealValue") { - return ok(100000n) + return ok(40000n) } if (transaction.functionName === "postDrand") { - return ok(220000n) + return ok(300000n) } return this.simulateTransactionForGas(transactionManager, transaction) From ca26b1757409e329735c97352ea2404067446c79 Mon Sep 17 00:00:00 2001 From: Gabriel Martinez Rodriguez Date: Fri, 14 Feb 2025 11:25:21 +0100 Subject: [PATCH 6/6] chore(randomness): added drand margin to .env.example and remove form local start script --- apps/randomness/.env.example | 2 +- apps/randomness/start-local-randomness.sh | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/randomness/.env.example b/apps/randomness/.env.example index 926a4ecd68..15a0eedc5a 100644 --- a/apps/randomness/.env.example +++ b/apps/randomness/.env.example @@ -8,7 +8,7 @@ CHAIN_ID=31337 RPC_URL=ws://127.0.0.1:8545 EVM_DRAND_GENESIS_TIMESTAMP_SECONDS=1727521075 EVM_DRAND_PERIOD_SECONDS=3 -EVM_DRAND_START_ROUND= +EVM_DRAND_MARGIN=10 EVM_DRAND_URL=https://api.drand.sh/v2/beacons/evmnet # The example value is the genesis timestamp of the HappyChain testnet HAPPY_GENESIS_TIMESTAMP_SECONDS=1723165536 diff --git a/apps/randomness/start-local-randomness.sh b/apps/randomness/start-local-randomness.sh index 21fcb549af..207b889af7 100755 --- a/apps/randomness/start-local-randomness.sh +++ b/apps/randomness/start-local-randomness.sh @@ -209,18 +209,8 @@ fi echo "Contracts deployed" -echo "Fetching Drand round..." - -round=$(curl -s $DRAND_URL | jq -r '.round') - -echo "Drand round: $round" - -echo "Setting environment variable $DRAND_ROUND_ENV_VAR to $round" -set_env_var .env $DRAND_ROUND_ENV_VAR $round false - make -C $SCRIPT_DIR/../../packages/txm build - empty_sqlite_db $TXM_DB_PATH echo $TXM_DB_PATH export TXM_DB_PATH=$TXM_DB_PATH