diff --git a/apps/randomness/.env.example b/apps/randomness/.env.example index 405dd94aaa..926a4ecd68 100644 --- a/apps/randomness/.env.example +++ b/apps/randomness/.env.example @@ -1,12 +1,16 @@ PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 RANDOM_CONTRACT_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 PRECOMMIT_DELAY=10 +# Seconds before the PRECOMMIT_DELAY when we make the commitment POST_COMMIT_MARGIN=10 -TIME_BLOCK=2 +BLOCK_TIME=2 +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_URL=https://api.drand.sh/v2/beacons/evmnet +# The example value is the genesis timestamp of the HappyChain testnet HAPPY_GENESIS_TIMESTAMP_SECONDS=1723165536 -RANDOMNESS_DB_PATH= \ No newline at end of file +RANDOMNESS_DB_PATH= +TXM_DB_PATH= diff --git a/apps/randomness/CHANGELOG.md b/apps/randomness/CHANGELOG.md index f3c9f9be60..8b2bfb6ad1 100644 --- a/apps/randomness/CHANGELOG.md +++ b/apps/randomness/CHANGELOG.md @@ -11,4 +11,4 @@ - Updated dependencies [fe5b333] - @happy.tech/common@0.1.0 - @happy.tech/txm@0.1.0 - - @happy.tech/contracts@0.1.0 + - @happy.tech/contracts@0.1.0 \ No newline at end of file diff --git a/apps/randomness/Makefile b/apps/randomness/Makefile index 4c81fc87df..1434629bcd 100644 --- a/apps/randomness/Makefile +++ b/apps/randomness/Makefile @@ -5,9 +5,14 @@ include ../../makefiles/formatting.mk include ../../makefiles/bundling.mk include ../../makefiles/help.mk +start: ## Starts the randomness service + node --env-file=.env dist/index.es.js +PHONY: start + + migrate: ## Runs pending migrations - node --env-file .env dist/migrate.es.js -.PHONY: migrate + tsx --env-file=.env src/migrate.ts +PHONY: migrate link-anvil: rm src/ABI/random.ts @@ -17,4 +22,4 @@ link-anvil: link-happy-sepolia: rm src/ABI/random.ts ln -s ../../../../contracts/deployments/happy-sepolia/random/abis.ts src/ABI/random.ts -.PHONY: link-happy-sepolia +.PHONY: link-happy-sepolia \ No newline at end of file diff --git a/apps/randomness/build.config.ts b/apps/randomness/build.config.ts index 5ee6b0dda0..3697d32c09 100644 --- a/apps/randomness/build.config.ts +++ b/apps/randomness/build.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from "@happy.tech/happybuild" export default defineConfig({ bunConfig: { - entrypoints: ["./src/index.ts", "./src/migrate.ts"], minify: false, target: "node", external: ["better-sqlite3"], diff --git a/apps/randomness/migrations/Migration20241209145000.js b/apps/randomness/migrations/Migration20241209145000.js deleted file mode 100644 index 136b7f2795..0000000000 --- a/apps/randomness/migrations/Migration20241209145000.js +++ /dev/null @@ -1,10 +0,0 @@ -// The 'value' column is defined as text because the data size exceeds the capacity of an integer field -export async function up(db) { - await db.schema - .createTable("commitments") - .addColumn("timestamp", "integer", (col) => col.notNull()) - .addColumn("value", "text", (col) => col.notNull()) - .addColumn("commitment", "text", (col) => col.notNull()) - .addColumn("transactionIntentId", "text", (col) => col.notNull()) - .execute() -} diff --git a/apps/randomness/migrations/Migration20241210123000.ts b/apps/randomness/migrations/Migration20241210123000.ts new file mode 100644 index 0000000000..7aa59636ca --- /dev/null +++ b/apps/randomness/migrations/Migration20241210123000.ts @@ -0,0 +1,14 @@ +import type { Kysely } from "kysely" +import type { Database } from "../src/db/types" + +export async function up(db: Kysely) { + await db.schema + .createTable("randomnesses") + .addColumn("timestamp", "text", (col) => col.notNull()) + .addColumn("value", "text", (col) => col.notNull()) + .addColumn("hashedValue", "text", (col) => col.notNull()) + .addColumn("commitmentTransactionIntentId", "text") + .addColumn("revealTransactionIntentId", "text") + .addColumn("status", "text", (col) => col.notNull()) + .execute() +} diff --git a/apps/randomness/package.json b/apps/randomness/package.json index 67df5123f3..89c72d2de9 100644 --- a/apps/randomness/package.json +++ b/apps/randomness/package.json @@ -6,9 +6,9 @@ "main": "./dist/index.es.js", "module": "./dist/index.es.js", "dependencies": { - "@happy.tech/contracts": "workspace:0.1.0", "@happy.tech/common": "workspace:0.1.0", "@happy.tech/txm": "workspace:0.1.0", + "@happy.tech/contracts": "workspace:0.1.0", "better-sqlite3": "^11.7.0", "kysely": "^0.27.5", "neverthrow": "^8.1.0", @@ -16,7 +16,8 @@ "zod": "^3.23.8" }, "devDependencies": { - "@happy.tech/configs": "workspace:", + "@happy.tech/configs": "workspace:0.1.0", + "@happy.tech/happybuild": "workspace:0.1.0", "typescript": "^5.6.2" } } diff --git a/apps/randomness/src/ABI/random.ts b/apps/randomness/src/ABI/random.ts deleted file mode 120000 index 5cc08678aa..0000000000 --- a/apps/randomness/src/ABI/random.ts +++ /dev/null @@ -1 +0,0 @@ -../../../../contracts/deployments/anvil/random/abis.ts \ No newline at end of file diff --git a/apps/randomness/src/CommitmentManager.ts b/apps/randomness/src/CommitmentManager.ts deleted file mode 100644 index ee07f897bd..0000000000 --- a/apps/randomness/src/CommitmentManager.ts +++ /dev/null @@ -1,80 +0,0 @@ -import crypto from "node:crypto" -import { type UUID, bigIntToZeroPadded, unknownToError } from "@happy.tech/common" -import { type Result, ResultAsync } from "neverthrow" -import type { Hex } from "viem" -import { encodePacked, keccak256 } from "viem" -import { db } from "./db/driver" -import { DIGITS_MAX_UINT256, commitmentInfoToDb, dbToCommitmentInfo } from "./db/types" - -export interface CommitmentInfo { - timestamp: bigint - value: bigint - commitment: Hex - transactionIntentId: UUID -} - -const COMMITMENT_PRUNE_INTERVAL_SECONDS = 120n // 2 minutes - -export class CommitmentManager { - private readonly map = new Map() - - async start(): Promise { - const commitmentsDb = (await db.selectFrom("commitments").selectAll().execute()).map(dbToCommitmentInfo) - for (const commitment of commitmentsDb) { - this.map.set(commitment.timestamp, commitment) - } - } - - generateCommitment(): Omit { - const value = this.generateRandomness() - const commitment = this.hashValue(value) - const commitmentObject = { value, commitment } - return commitmentObject - } - - setCommitmentForTimestamp(commitment: CommitmentInfo): void { - this.map.set(commitment.timestamp, commitment) - } - - async saveCommitment(commitment: CommitmentInfo): Promise> { - const result = await ResultAsync.fromPromise( - db.insertInto("commitments").values(commitmentInfoToDb(commitment)).execute(), - unknownToError, - ) - - return result.map(() => undefined) - } - - getCommitmentForTimestamp(timestamp: bigint): CommitmentInfo | undefined { - return this.map.get(timestamp) - } - - async pruneCommitments(latestBlockTimestamp: bigint): Promise> { - return ResultAsync.fromPromise( - db - .deleteFrom("commitments") - .where( - "timestamp", - "<", - bigIntToZeroPadded(latestBlockTimestamp - COMMITMENT_PRUNE_INTERVAL_SECONDS, DIGITS_MAX_UINT256), - ) - .execute(), - unknownToError, - ).map(() => undefined) - } - - private generateRandomness(): bigint { - const bytes = crypto.randomBytes(32) - let hex = "0x" - - for (const byte of bytes) { - hex += byte.toString(16).padStart(2, "0") - } - - return BigInt(hex) - } - - private hashValue(value: bigint): Hex { - return keccak256(encodePacked(["uint256"], [value])) - } -} diff --git a/apps/randomness/src/Factories/CommitmentTransactionFactory.ts b/apps/randomness/src/Factories/CommitmentTransactionFactory.ts deleted file mode 100644 index 206bf8dd4c..0000000000 --- a/apps/randomness/src/Factories/CommitmentTransactionFactory.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Transaction, TransactionManager } from "@happy.tech/txm" -import type { Address, Hex } from "viem" - -export class CommitmentTransactionFactory { - private readonly transactionManager: TransactionManager - private readonly randomContractAddress: Address - private readonly precommitDelay: bigint - - constructor(transactionManager: TransactionManager, randomContractAddress: Address, precommitDelay: bigint) { - this.transactionManager = transactionManager - this.randomContractAddress = randomContractAddress - this.precommitDelay = precommitDelay - } - - create(timestamp: bigint, commitment: Hex): Transaction { - return this.transactionManager.createTransaction({ - address: this.randomContractAddress, - functionName: "postCommitment", - contractName: "Random", - args: [timestamp, commitment], - deadline: Number(timestamp - this.precommitDelay), - }) - } -} diff --git a/apps/randomness/src/Factories/RevealValueTransactionFactory.ts b/apps/randomness/src/Factories/RevealValueTransactionFactory.ts deleted file mode 100644 index a2a69d607c..0000000000 --- a/apps/randomness/src/Factories/RevealValueTransactionFactory.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Transaction, TransactionManager } from "@happy.tech/txm" -import type { Address } from "viem" - -export class RevealValueTransactionFactory { - private readonly transactionManager: TransactionManager - private readonly randomContractAddress: Address - - constructor(transactionManager: TransactionManager, randomContractAddress: Address) { - this.transactionManager = transactionManager - this.randomContractAddress = randomContractAddress - } - - create(timestamp: bigint, revealedValue: bigint): Transaction { - return this.transactionManager.createTransaction({ - address: this.randomContractAddress, - functionName: "revealValue", - contractName: "Random", - args: [timestamp, revealedValue], - deadline: Number(timestamp), - }) - } -} diff --git a/apps/randomness/src/Randomness.ts b/apps/randomness/src/Randomness.ts new file mode 100644 index 0000000000..bee98c2874 --- /dev/null +++ b/apps/randomness/src/Randomness.ts @@ -0,0 +1,92 @@ +import crypto from "node:crypto" +import type { Hex, UUID } from "@happy.tech/common" +import { bytesToHex, encodePacked, keccak256 } from "viem" + +export enum RandomnessStatus { + PENDING = "PENDING", + COMMITMENT_SUBMITTED = "COMMITMENT_SUBMITTED", + COMMITMENT_EXECUTED = "COMMITMENT_EXECUTED", + COMMITMENT_FAILED = "COMMITMENT_FAILED", + REVEAL_SUBMITTED = "REVEAL_SUBMITTED", + REVEAL_EXECUTED = "REVEAL_EXECUTED", + REVEAL_FAILED = "REVEAL_FAILED", + REVEAL_NOT_SUBMITTED_ON_TIME = "REVEAL_NOT_SUBMITTED_ON_TIME", +} + +export class Randomness { + public timestamp: bigint + public value: bigint + public hashedValue: Hex + public commitmentTransactionIntentId: UUID | undefined + public revealTransactionIntentId: UUID | undefined + public status: RandomnessStatus + + constructor(params: { + timestamp: bigint + value: bigint + hashedValue: Hex + commitmentTransactionIntentId?: UUID + revealTransactionIntentId?: UUID + status: RandomnessStatus + }) { + this.timestamp = params.timestamp + this.value = params.value + this.hashedValue = params.hashedValue + this.commitmentTransactionIntentId = params.commitmentTransactionIntentId + this.revealTransactionIntentId = params.revealTransactionIntentId + this.status = params.status + } + + public commitmentExecuted(): void { + this.status = RandomnessStatus.COMMITMENT_EXECUTED + } + + public revealExecuted(): void { + this.status = RandomnessStatus.REVEAL_EXECUTED + } + + public addCommitmentTransactionIntentId(intentId: UUID): void { + this.commitmentTransactionIntentId = intentId + this.status = RandomnessStatus.COMMITMENT_SUBMITTED + } + + public addRevealTransactionIntentId(intentId: UUID): void { + this.revealTransactionIntentId = intentId + this.status = RandomnessStatus.REVEAL_SUBMITTED + } + + public commitmentFailed(): void { + this.status = RandomnessStatus.COMMITMENT_FAILED + } + + public revealFailed(): void { + this.status = RandomnessStatus.REVEAL_FAILED + } + + public revealNotSubmittedOnTime(): void { + this.status = RandomnessStatus.REVEAL_NOT_SUBMITTED_ON_TIME + } + + static createRandomness(timestamp: bigint): Randomness { + const value = Randomness.generateValue() + const hashedValue = Randomness.hashValue(value) + return new Randomness({ + timestamp, + value, + hashedValue, + commitmentTransactionIntentId: undefined, + revealTransactionIntentId: undefined, + status: RandomnessStatus.PENDING, + }) + } + + private static generateValue(): bigint { + const bytes = crypto.randomBytes(32) + + return BigInt(bytesToHex(bytes)) + } + + private static hashValue(value: bigint): Hex { + return keccak256(encodePacked(["uint256"], [value])) + } +} diff --git a/apps/randomness/src/RandomnessRepository.ts b/apps/randomness/src/RandomnessRepository.ts new file mode 100644 index 0000000000..b74b0fb157 --- /dev/null +++ b/apps/randomness/src/RandomnessRepository.ts @@ -0,0 +1,94 @@ +import { type UUID, unknownToError } from "@happy.tech/common" +import { bigIntToZeroPadded } from "@happy.tech/common" +import { type Result, ResultAsync } from "neverthrow" +import type { Randomness, RandomnessStatus } from "./Randomness" +import { db } from "./db/driver" +import { randomnessEntityToRow, randomnessRowToEntity } from "./db/types" + +const COMMITMENT_PRUNE_INTERVAL_SECONDS = 120n // 2 minutes + +// Quantity of digits in the max uint256 value +export const DIGITS_MAX_UINT256 = 78 + +export class RandomnessRepository { + private readonly map = new Map() + + async start(): Promise { + const randomnessesDb = (await db.selectFrom("randomnesses").selectAll().execute()).map(randomnessRowToEntity) + for (const randomness of randomnessesDb) { + this.map.set(randomness.timestamp, randomness) + } + } + + getRandomnessForTimestamp(timestamp: bigint): Randomness | undefined { + return this.map.get(timestamp) + } + + getRandomnessForIntentId(intentId: UUID): Randomness | undefined { + return Array.from(this.map.values()).find( + (randomness) => + randomness.commitmentTransactionIntentId === intentId || + randomness.revealTransactionIntentId === intentId, + ) + } + + getRandomnessInTimeRange(start: bigint, end: bigint): Randomness[] { + return Array.from(this.map.values()).filter( + (randomness) => randomness.timestamp >= start && randomness.timestamp <= end, + ) + } + + getRandomnessInStatus(status: RandomnessStatus): Randomness[] { + return Array.from(this.map.values()).filter((randomness) => randomness.status === status) + } + + /** + * Save a randomness to the database + * Even if the operation fails, the randomness will be saved in a in-memory cache + */ + async saveRandomness(randomness: Randomness): Promise> { + this.map.set(randomness.timestamp, randomness) + const row = randomnessEntityToRow(randomness) + return await ResultAsync.fromPromise(db.insertInto("randomnesses").values(row).execute(), unknownToError).map( + () => undefined, + ) + } + + /** + * Update a randomness in the database + * Even if the operation fails, the randomness will be updated in a in-memory cache + */ + async updateRandomness(randomness: Randomness): Promise> { + this.map.set(randomness.timestamp, randomness) + const row = randomnessEntityToRow(randomness) + return await ResultAsync.fromPromise( + db + .updateTable("randomnesses") + .set(row) + .where("timestamp", "=", bigIntToZeroPadded(randomness.timestamp, DIGITS_MAX_UINT256)) + .execute(), + unknownToError, + ).map(() => undefined) + } + + async pruneRandomnesses(latestBlockTimestamp: bigint): Promise> { + const cutoffTimestamp = latestBlockTimestamp - BigInt(COMMITMENT_PRUNE_INTERVAL_SECONDS) + for (const timestamp of this.map.keys()) { + if (timestamp < cutoffTimestamp) { + this.map.delete(timestamp) + } + } + + return await ResultAsync.fromPromise( + db + .deleteFrom("randomnesses") + .where( + "timestamp", + "<", + bigIntToZeroPadded(latestBlockTimestamp - COMMITMENT_PRUNE_INTERVAL_SECONDS, DIGITS_MAX_UINT256), + ) + .execute(), + unknownToError, + ).map(() => undefined) + } +} diff --git a/apps/randomness/src/TransactionFactory.ts b/apps/randomness/src/TransactionFactory.ts new file mode 100644 index 0000000000..ac8c2548b9 --- /dev/null +++ b/apps/randomness/src/TransactionFactory.ts @@ -0,0 +1,35 @@ +import type { Transaction, TransactionManager } from "@happy.tech/txm" +import type { Address } from "viem" +import type { Randomness } from "./Randomness" + +export class TransactionFactory { + private readonly transactionManager: TransactionManager + private readonly randomContractAddress: Address + private readonly precommitDelay: bigint + + constructor(transactionManager: TransactionManager, randomContractAddress: Address, precommitDelay: bigint) { + this.transactionManager = transactionManager + this.randomContractAddress = randomContractAddress + this.precommitDelay = precommitDelay + } + + createCommitmentTransaction(randomness: Randomness): Transaction { + return this.transactionManager.createTransaction({ + address: this.randomContractAddress, + functionName: "postCommitment", + contractName: "Random", + args: [randomness.timestamp, randomness.hashedValue], + deadline: Number(randomness.timestamp - this.precommitDelay), + }) + } + + createRevealValueTransaction(randomness: Randomness): Transaction { + return this.transactionManager.createTransaction({ + address: this.randomContractAddress, + functionName: "revealValue", + contractName: "Random", + args: [randomness.timestamp, randomness.value], + deadline: Number(randomness.timestamp), + }) + } +} diff --git a/apps/randomness/src/db/types.ts b/apps/randomness/src/db/types.ts index 1fe2ac6808..84105f0971 100644 --- a/apps/randomness/src/db/types.ts +++ b/apps/randomness/src/db/types.ts @@ -1,36 +1,39 @@ -import type { Hex, UUID } from "@happy.tech/common" -import { bigIntToZeroPadded } from "@happy.tech/common" -import type { CommitmentInfo } from "../CommitmentManager" +import { type Hex, type UUID, bigIntToZeroPadded } from "@happy.tech/common" +import { Randomness, type RandomnessStatus } from "../Randomness" +import { DIGITS_MAX_UINT256 } from "../RandomnessRepository" // Values are stored as strings because they can be large numbers bigger than the max value of an SQLite integer -export interface CommitmentInfoTable { +export interface RandomnessRow { timestamp: string value: string - commitment: Hex - transactionIntentId: UUID + hashedValue: Hex + commitmentTransactionIntentId: UUID | undefined + revealTransactionIntentId: UUID | undefined + status: RandomnessStatus } export interface Database { - commitments: CommitmentInfoTable + randomnesses: RandomnessRow } -// Quantity of digits in the max uint256 value -export const DIGITS_MAX_UINT256 = 78 - -export function commitmentInfoToDb(commitmentInfo: CommitmentInfo): CommitmentInfoTable { - return { - timestamp: bigIntToZeroPadded(commitmentInfo.timestamp, DIGITS_MAX_UINT256), - value: bigIntToZeroPadded(commitmentInfo.value, DIGITS_MAX_UINT256), - commitment: commitmentInfo.commitment, - transactionIntentId: commitmentInfo.transactionIntentId, - } +export function randomnessRowToEntity(row: RandomnessRow): Randomness { + return new Randomness({ + timestamp: BigInt(row.timestamp), + value: BigInt(row.value), + hashedValue: row.hashedValue, + commitmentTransactionIntentId: row.commitmentTransactionIntentId, + revealTransactionIntentId: row.revealTransactionIntentId, + status: row.status, + }) } -export function dbToCommitmentInfo(db: CommitmentInfoTable): CommitmentInfo { +export function randomnessEntityToRow(entity: Randomness): RandomnessRow { return { - timestamp: BigInt(db.timestamp), - value: BigInt(db.value), - commitment: db.commitment, - transactionIntentId: db.transactionIntentId, + timestamp: bigIntToZeroPadded(entity.timestamp, DIGITS_MAX_UINT256), + value: bigIntToZeroPadded(entity.value, DIGITS_MAX_UINT256), + hashedValue: entity.hashedValue, + commitmentTransactionIntentId: entity.commitmentTransactionIntentId, + revealTransactionIntentId: entity.revealTransactionIntentId, + status: entity.status, } } diff --git a/apps/randomness/src/env.ts b/apps/randomness/src/env.ts index 5e00c4f362..50fbe6cabc 100644 --- a/apps/randomness/src/env.ts +++ b/apps/randomness/src/env.ts @@ -12,13 +12,14 @@ const envSchema = z.object({ .string() .trim() .transform((s) => BigInt(s)), - TIME_BLOCK: z + BLOCK_TIME: z .string() .trim() .transform((s) => BigInt(s)), RPC_URL: z.string().trim(), - CHAIN_ID: z.number().int().positive(), + CHAIN_ID: z.string().transform((s) => Number(s)), RANDOMNESS_DB_PATH: z.string().trim(), + TXM_DB_PATH: z.string().trim(), }) const parsedEnv = envSchema.safeParse(process.env) diff --git a/apps/randomness/src/index.ts b/apps/randomness/src/index.ts index d2d32e1d85..30640d8fea 100644 --- a/apps/randomness/src/index.ts +++ b/apps/randomness/src/index.ts @@ -1,19 +1,18 @@ import { abis } from "@happy.tech/contracts/random/anvil" -import { TransactionManager, TransactionStatus } from "@happy.tech/txm" +import { TransactionManager, TransactionStatus, TxmHookType } from "@happy.tech/txm" import type { LatestBlock, Transaction } from "@happy.tech/txm" -import { CommitmentManager } from "./CommitmentManager.js" import { CustomGasEstimator } from "./CustomGasEstimator.js" -import { CommitmentTransactionFactory } from "./Factories/CommitmentTransactionFactory.js" -import { RevealValueTransactionFactory } from "./Factories/RevealValueTransactionFactory.js" +import { Randomness, RandomnessStatus } from "./Randomness.js" +import { RandomnessRepository } from "./RandomnessRepository.js" +import { TransactionFactory } from "./TransactionFactory.js" import { env } from "./env.js" class RandomnessService { - private readonly commitmentManager: CommitmentManager + private readonly randomnessRepository: RandomnessRepository private readonly txm: TransactionManager - private readonly commitmentTransactionFactory: CommitmentTransactionFactory - private readonly revealValueTransactionFactory: RevealValueTransactionFactory + private readonly transactionFactory: TransactionFactory constructor() { - this.commitmentManager = new CommitmentManager() + this.randomnessRepository = new RandomnessRepository() this.txm = new TransactionManager({ privateKey: env.PRIVATE_KEY, chainId: env.CHAIN_ID, @@ -23,65 +22,128 @@ class RandomnessService { url: env.RPC_URL, }, }) - this.commitmentTransactionFactory = new CommitmentTransactionFactory( - this.txm, - env.RANDOM_CONTRACT_ADDRESS, - env.PRECOMMIT_DELAY, - ) - this.revealValueTransactionFactory = new RevealValueTransactionFactory(this.txm, env.RANDOM_CONTRACT_ADDRESS) + this.transactionFactory = new TransactionFactory(this.txm, env.RANDOM_CONTRACT_ADDRESS, env.PRECOMMIT_DELAY) this.txm.addTransactionOriginator(this.onCollectTransactions.bind(this)) } async start() { - await Promise.all([this.txm.start(), this.commitmentManager.start()]) + await Promise.all([this.txm.start(), this.randomnessRepository.start()]) + this.txm.addHook(this.onTransactionStatusChange.bind(this), TxmHookType.TransactionStatusChanged) + } + + private onTransactionStatusChange(payload: { transaction: Transaction }) { + const randomness = this.randomnessRepository.getRandomnessForIntentId(payload.transaction.intentId) + + if (!randomness) { + console.warn("Couldn't find randomness with intentId", payload.transaction.intentId) + return + } + + if (payload.transaction.status === TransactionStatus.Success) { + if (randomness.commitmentTransactionIntentId === payload.transaction.intentId) { + randomness.commitmentExecuted() + } else if (randomness.revealTransactionIntentId === payload.transaction.intentId) { + randomness.revealExecuted() + } + } else if ( + payload.transaction.status === TransactionStatus.Failed || + payload.transaction.status === TransactionStatus.Expired + ) { + if (randomness.commitmentTransactionIntentId === payload.transaction.intentId) { + randomness.commitmentFailed() + } else if (randomness.revealTransactionIntentId === payload.transaction.intentId) { + randomness.revealFailed() + } + } + + this.randomnessRepository.updateRandomness(randomness).then((result) => { + if (result.isErr()) { + console.error("Failed to update randomness", result.error) + } + }) + } + + private async handleRevealNotSubmittedOnTime(block: LatestBlock) { + const randomnesses = this.randomnessRepository.getRandomnessInStatus(RandomnessStatus.COMMITMENT_EXECUTED) + + const nextBlockTimestamp = block.timestamp + env.BLOCK_TIME + + for (const randomness of randomnesses) { + if (randomness.timestamp < nextBlockTimestamp) { + randomness.revealNotSubmittedOnTime() + this.randomnessRepository.updateRandomness(randomness).then((result) => { + if (result.isErr()) { + console.error("Failed to update randomness", result.error) + } + }) + } + } } private async onCollectTransactions(block: LatestBlock): Promise { + // TODO: Move to a on new block hook when we have it - https://linear.app/happychain/issue/HAPPY-257/create-onnewblock-hook + this.handleRevealNotSubmittedOnTime(block) + const transactions: Transaction[] = [] - // We try to commit the randomness POST_COMMIT_MARGIN to be safe that the transaction is included before the PRECOMMIT_DELAY - const commitmentTimestamp = block.timestamp + env.PRECOMMIT_DELAY + env.POST_COMMIT_MARGIN - const partialCommitment = this.commitmentManager.generateCommitment() + const nextBlockTimestamp = block.timestamp + env.BLOCK_TIME + + // We try to precommit for all blocks in [nextBlockTimestamp + PRECOMMIT_DELAY, nextBlockTimestamp + PRECOMMIT_DELAY + POST_COMMIT_MARGIN]. + const firstCommitTime = nextBlockTimestamp + env.PRECOMMIT_DELAY + const lastCommitTime = firstCommitTime + env.POST_COMMIT_MARGIN - const commitmentTransaction = this.commitmentTransactionFactory.create( - commitmentTimestamp, - partialCommitment.commitment, + // Array with timestamps from firstBlockToCommit to lastBlockToCommit + const timestampsToCommit = Array.from( + { length: Number(lastCommitTime - firstCommitTime) }, + (_, i) => firstCommitTime + BigInt(i), ) - transactions.push(commitmentTransaction) + for (const timestamp of timestampsToCommit) { + const randomness = this.randomnessRepository.getRandomnessForTimestamp(timestamp) + + // Already has a randomness for this timestamp, so we skip + if (randomness) { + continue + } + + const newRandomness = Randomness.createRandomness(timestamp) + + const commitmentTransaction = this.transactionFactory.createCommitmentTransaction(newRandomness) + + transactions.push(commitmentTransaction) + + newRandomness.addCommitmentTransactionIntentId(commitmentTransaction.intentId) - const commitment = { - ...partialCommitment, - transactionIntentId: commitmentTransaction.intentId, - timestamp: commitmentTimestamp, + // We don't await for saving the commitment, because we dont want to block the transaction collection + this.randomnessRepository.saveRandomness(newRandomness).then((result) => { + if (result.isErr()) { + console.error("Failed to save commitment", result.error) + } + }) } - this.commitmentManager.setCommitmentForTimestamp(commitment) + const randomnessToReveal = this.randomnessRepository.getRandomnessForTimestamp(nextBlockTimestamp) - // We don't await for saving the commitment, because we dont want to block the transaction collection - this.commitmentManager.saveCommitment(commitment).then((result) => { - if (result.isErr()) { - console.error("Failed to save commitment", result.error) - } - }) + if (!randomnessToReveal) { + console.warn("Not found randomness to reveal with timestamp", nextBlockTimestamp) + return transactions + } - const revealValueCommitment = this.commitmentManager.getCommitmentForTimestamp(block.timestamp + env.TIME_BLOCK) + const revealValueTransaction = this.transactionFactory.createRevealValueTransaction(randomnessToReveal) - if (revealValueCommitment) { - const transaction = await this.txm.getTransaction(revealValueCommitment.transactionIntentId) + transactions.unshift(revealValueTransaction) - if (transaction?.status === TransactionStatus.Success) { - const revealValueTransaction = this.revealValueTransactionFactory.create( - block.timestamp + env.TIME_BLOCK, - revealValueCommitment.value, - ) - transactions.push(revealValueTransaction) + randomnessToReveal.addRevealTransactionIntentId(revealValueTransaction.intentId) + + this.randomnessRepository.updateRandomness(randomnessToReveal).then((result) => { + if (result.isErr()) { + console.error("Failed to update randomness", result.error) } - } + }) - // We don't await for pruning, because we dont want to block the transaction collection - this.commitmentManager.pruneCommitments(block.timestamp).then((result) => { + // We don't await for pruning, because we don't want to block the transaction collection + this.randomnessRepository.pruneRandomnesses(block.timestamp).then((result) => { if (result.isErr()) { console.error("Failed to prune commitments", result.error) } diff --git a/apps/randomness/src/migrate.ts b/apps/randomness/src/migrate.ts index 481def3d94..c69b1f3af4 100644 --- a/apps/randomness/src/migrate.ts +++ b/apps/randomness/src/migrate.ts @@ -1,15 +1,18 @@ import { promises as fs } from "node:fs" import path from "node:path" +import { fileURLToPath } from "node:url" import { FileMigrationProvider, Migrator } from "kysely" import { db } from "./db/driver" +const dirname = path.dirname(fileURLToPath(import.meta.url)) + async function migrateToLatest() { const migrator = new Migrator({ db, provider: new FileMigrationProvider({ fs, path, - migrationFolder: path.join(__dirname, "../migrations"), + migrationFolder: path.join(dirname, "../migrations"), }), }) diff --git a/bun.lockb b/bun.lockb index de01746fda..064d89247c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/txm/CHANGELOG.md b/packages/txm/CHANGELOG.md index 2e53335708..d125a1e44e 100644 --- a/packages/txm/CHANGELOG.md +++ b/packages/txm/CHANGELOG.md @@ -11,4 +11,4 @@ - Updated dependencies [fe5b333] - @happy.tech/configs@0.1.0 - @happy.tech/common@0.1.0 - - @happy.tech/contracts@0.1.0 + - @happy.tech/contracts@0.1.0 \ No newline at end of file diff --git a/packages/txm/Makefile b/packages/txm/Makefile index 1e62eb7c87..15e5273063 100644 --- a/packages/txm/Makefile +++ b/packages/txm/Makefile @@ -4,5 +4,5 @@ include ../../makefiles/bundling.mk include ../../makefiles/help.mk migrate: ## Runs pending migrations - node dist/migrate.es.js + tsx lib/migrate.ts .PHONY: migrate diff --git a/packages/txm/lib/migrate.ts b/packages/txm/lib/migrate.ts index 481def3d94..c69b1f3af4 100644 --- a/packages/txm/lib/migrate.ts +++ b/packages/txm/lib/migrate.ts @@ -1,15 +1,18 @@ import { promises as fs } from "node:fs" import path from "node:path" +import { fileURLToPath } from "node:url" import { FileMigrationProvider, Migrator } from "kysely" import { db } from "./db/driver" +const dirname = path.dirname(fileURLToPath(import.meta.url)) + async function migrateToLatest() { const migrator = new Migrator({ db, provider: new FileMigrationProvider({ fs, path, - migrationFolder: path.join(__dirname, "../migrations"), + migrationFolder: path.join(dirname, "../migrations"), }), }) diff --git a/packages/txm/migrations/Migration20241111163800.js b/packages/txm/migrations/Migration20241111163800.ts similarity index 83% rename from packages/txm/migrations/Migration20241111163800.js rename to packages/txm/migrations/Migration20241111163800.ts index 93fdd41a95..068afd2da0 100644 --- a/packages/txm/migrations/Migration20241111163800.js +++ b/packages/txm/migrations/Migration20241111163800.ts @@ -1,4 +1,7 @@ -export async function up(db) { +import type { Kysely } from "kysely" +import type { Database } from "../lib/db/types" + +export async function up(db: Kysely) { await db.schema .createTable("transaction") .addColumn("intentId", "text", (col) => col.notNull()) diff --git a/packages/txm/migrations/Migration20241111223000.js b/packages/txm/migrations/Migration20241111223000.ts similarity index 76% rename from packages/txm/migrations/Migration20241111223000.js rename to packages/txm/migrations/Migration20241111223000.ts index 97e24c6ba7..0a8766c8a9 100644 --- a/packages/txm/migrations/Migration20241111223000.js +++ b/packages/txm/migrations/Migration20241111223000.ts @@ -1,9 +1,12 @@ +import type { Kysely } from "kysely" +import type { Database } from "../lib/db/types" + /* SQLite does not have native time types. The SQL interface allows arbitrary type names including "DATE" and "DATETIME", but this is invalid in this API, and results in "NUMERIC" affinity instead of "INTEGER" affinity, which is the one we want here */ -export async function up(db) { +export async function up(db: Kysely) { await db.schema.alterTable("transaction").addColumn("createdAt", "integer").execute() await db.schema.alterTable("transaction").addColumn("updatedAt", "integer").execute() diff --git a/packages/txm/migrations/Migration20241205104400.js b/packages/txm/migrations/Migration20241205104400.ts similarity index 50% rename from packages/txm/migrations/Migration20241205104400.js rename to packages/txm/migrations/Migration20241205104400.ts index d40dadef5f..ca6208919e 100644 --- a/packages/txm/migrations/Migration20241205104400.js +++ b/packages/txm/migrations/Migration20241205104400.ts @@ -1,4 +1,7 @@ -export async function up(db) { +import type { Kysely } from "kysely" +import type { Database } from "../lib/db/types" + +export async function up(db: Kysely) { await db.schema .alterTable("transaction") .addColumn("from", "text", (col) => col.notNull()) diff --git a/packages/txm/package.json b/packages/txm/package.json index 6997a72348..dc62c63e67 100644 --- a/packages/txm/package.json +++ b/packages/txm/package.json @@ -1,7 +1,7 @@ { "name": "@happy.tech/txm", - "private": true, "version": "0.1.0", + "private": true, "type": "module", "main": "./dist/index.es.js", "module": "./dist/index.es.js", @@ -10,10 +10,6 @@ ".": { "default": "./dist/index.es.js", "types": "./dist/index.es.d.ts" - }, - "./migrations": { - "default": "./dist/migrate.es.js", - "types": "./dist/migrate.es.d.ts" } }, "dependencies": {