diff --git a/apps/randomness/build.config.ts b/apps/randomness/build.config.ts index 3697d32c09..1ee0717c15 100644 --- a/apps/randomness/build.config.ts +++ b/apps/randomness/build.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "@happy.tech/happybuild" export default defineConfig({ + exports: [".", "./migrate"], bunConfig: { minify: false, target: "node", diff --git a/apps/randomness/migrations/Migration20241210123000.ts b/apps/randomness/migrations/Migration20241210123000.ts index 7aa59636ca..c7661f7a34 100644 --- a/apps/randomness/migrations/Migration20241210123000.ts +++ b/apps/randomness/migrations/Migration20241210123000.ts @@ -4,7 +4,7 @@ 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("blockNumber", "text", (col) => col.notNull()) .addColumn("value", "text", (col) => col.notNull()) .addColumn("hashedValue", "text", (col) => col.notNull()) .addColumn("commitmentTransactionIntentId", "text") diff --git a/apps/randomness/migrations/Migration20241219104500.ts b/apps/randomness/migrations/Migration20241219104500.ts new file mode 100644 index 0000000000..7dca9c8df6 --- /dev/null +++ b/apps/randomness/migrations/Migration20241219104500.ts @@ -0,0 +1,12 @@ +import type { Kysely } from "kysely" +import type { Database } from "../src/db/types" + +export async function up(db: Kysely) { + await db.schema + .createTable("drands") + .addColumn("round", "text", (col) => col.notNull()) + .addColumn("signature", "text") + .addColumn("status", "text", (col) => col.notNull()) + .addColumn("transactionIntentId", "text") + .execute() +} diff --git a/apps/randomness/package.json b/apps/randomness/package.json index 89c72d2de9..6beebff2ea 100644 --- a/apps/randomness/package.json +++ b/apps/randomness/package.json @@ -5,6 +5,10 @@ "type": "module", "main": "./dist/index.es.js", "module": "./dist/index.es.js", + "exports": { + ".": "./dist/index.es.js", + "./migrate": "./dist/migrate.es.js" + }, "dependencies": { "@happy.tech/common": "workspace:0.1.0", "@happy.tech/txm": "workspace:0.1.0", diff --git a/apps/randomness/src/Drand.ts b/apps/randomness/src/Drand.ts new file mode 100644 index 0000000000..2a0d7c22c7 --- /dev/null +++ b/apps/randomness/src/Drand.ts @@ -0,0 +1,96 @@ +import type { Hex, UUID } from "@happy.tech/common" + +export enum DrandStatus { + PENDING = "PENDING", + SUBMITTED = "SUBMITTED", + SUCCESS = "SUCCESS", + FAILED = "FAILED", +} + +export class Drand { + #round: bigint + #signature: Hex + #status: DrandStatus + #transactionIntentId: UUID | undefined + + static readonly validTransitions: Record = { + PENDING: [DrandStatus.SUBMITTED, DrandStatus.FAILED], + SUBMITTED: [DrandStatus.SUCCESS, DrandStatus.FAILED], + SUCCESS: [], + FAILED: [], + } + + constructor(params: { + status: DrandStatus + transactionIntentId?: UUID + round: bigint + signature: Hex + }) { + this.#status = params.status + this.#transactionIntentId = params.transactionIntentId + this.#round = params.round + this.#signature = params.signature + } + + private setStatus(newStatus: DrandStatus): void { + if (!Drand.validTransitions[this.#status].includes(newStatus)) { + throw new Error(`Invalid status transition from ${this.#status} to ${newStatus}`) + } + + this.#status = newStatus + } + + executionSuccess(): void { + this.setStatus(DrandStatus.SUCCESS) + } + + transactionSubmitted(): void { + this.setStatus(DrandStatus.SUBMITTED) + } + + transactionFailed(): void { + this.setStatus(DrandStatus.FAILED) + } + + get round(): bigint { + return this.#round + } + + get signature(): Hex | undefined { + return this.#signature + } + + get status(): DrandStatus { + return this.#status + } + + get transactionIntentId(): UUID | undefined { + return this.#transactionIntentId + } + + static create(params: { + transactionIntentId?: UUID + round: bigint + signature: Hex + }): Drand { + // Signature is a hex string with 128 characters + 2 for the "0x" prefix + if (params.signature.length !== 130) { + throw new Error("Invalid signature length") + } + + if (!params.signature.startsWith("0x")) { + throw new Error("Signature must start with 0x") + } + + if (params.round <= 0n) { + throw new Error("Round must be greater than 0") + } + + return new Drand({ + status: DrandStatus.PENDING, + round: params.round, + signature: params.signature, + transactionIntentId: params.transactionIntentId, + }) + } +} diff --git a/apps/randomness/src/DrandRepository.ts b/apps/randomness/src/DrandRepository.ts new file mode 100644 index 0000000000..db5e94183d --- /dev/null +++ b/apps/randomness/src/DrandRepository.ts @@ -0,0 +1,83 @@ +import { type Hex, type UUID, bigIntToZeroPadded, unknownToError } from "@happy.tech/common" +import { type Result, ResultAsync } from "neverthrow" +import { Drand } from "./Drand" +import { DIGITS_MAX_UINT256 } from "./constants" +import { db } from "./db/driver" +import type { DrandRow } from "./db/types" + +export class DrandRepository { + private cache: Drand[] = [] + + private rowToEntity(row: DrandRow): Drand { + return new Drand({ + round: BigInt(row.round), + signature: row.signature as Hex, + status: row.status, + transactionIntentId: row.transactionIntentId, + }) + } + + private entityToRow(entity: Drand): DrandRow { + return { + round: bigIntToZeroPadded(entity.round, DIGITS_MAX_UINT256), + signature: entity.signature, + status: entity.status, + transactionIntentId: entity.transactionIntentId, + } + } + + async start(): Promise { + const drandsDb = (await db.selectFrom("drands").selectAll().execute()).map(this.rowToEntity) + + this.cache.push(...drandsDb) + } + + async saveDrand(drand: Drand): Promise> { + const row = this.entityToRow(drand) + + const result = await ResultAsync.fromPromise(db.insertInto("drands").values(row).execute(), unknownToError) + + if (result.isOk()) { + this.cache.push(drand) + } + + return result.map(() => undefined) + } + + getOldestDrandRound(): bigint | undefined { + if (this.cache.length === 0) { + return undefined + } + return this.cache.reduce((acc, drand) => (drand.round < acc ? drand.round : acc), this.cache[0].round) + } + + findRoundGapsInRange(startRound: bigint, endRound: bigint): bigint[] { + const roundGaps = [] + for (let round = startRound; round <= endRound; round++) { + if (!this.cache.find((drand) => drand.round === round)) { + roundGaps.push(round) + } + } + return roundGaps + } + + getDrand(round: bigint): Drand | undefined { + return this.cache.find((drand) => drand.round === round) + } + + getDrandByTransactionIntentId(transactionIntentId: UUID): Drand | undefined { + return this.cache.find((drand) => drand.transactionIntentId === transactionIntentId) + } + + async updateDrand(drand: Drand): Promise> { + const row = this.entityToRow(drand) + return ResultAsync.fromPromise( + db + .updateTable("drands") + .set(row) + .where("round", "=", bigIntToZeroPadded(drand.round, DIGITS_MAX_UINT256)) + .execute(), + unknownToError, + ).map(() => undefined) + } +} diff --git a/apps/randomness/src/DrandService.ts b/apps/randomness/src/DrandService.ts new file mode 100644 index 0000000000..91f6f1a120 --- /dev/null +++ b/apps/randomness/src/DrandService.ts @@ -0,0 +1,76 @@ +import { fetchWithRetry, nowInSeconds, unknownToError } from "@happy.tech/common" +import { type Result, ResultAsync, err, ok } from "neverthrow" +import type { Hex } from "viem" +import { z } from "zod" +import { env } from "./env" + +const drandBeaconSchema = z.object({ + round: z.number().int().positive(), + signature: z + .string() + .transform((s) => s.toLowerCase()) + .refine((s) => s.length === 128, { + message: "Signature must be 128 characters long", + }) + .transform((s) => `0x${s}` as Hex) + .refine((s) => /^0x[0-9a-f]+$/.test(s), { + message: "Signature must contain only hexadecimal characters", + }), +}) + +export interface DrandBeacon { + round: number + signature: Hex +} + +export enum DrandError { + NetworkError = "NetworkError", + InvalidResponse = "InvalidResponse", + TooEarly = "TooEarly", + Other = "Other", + InvalidRound = "InvalidRound", +} + +export class DrandService { + async getDrandBeacon(round: bigint): Promise> { + if (round <= 0n) { + return err(DrandError.InvalidRound) + } + + const url = `${env.EVM_DRAND_URL}/rounds/${round}` + const response = await ResultAsync.fromPromise(fetchWithRetry(url, {}, 2, 500), unknownToError) + + if (response.isErr()) { + return err(DrandError.NetworkError) + } + + if (!response.value.ok) { + if (response.value.status === 425) { + return err(DrandError.TooEarly) + } + + console.error("Drand beacon fetch error status", response.value.status) + return err(DrandError.Other) + } + + const dataRaw = await response.value.json() + + const parsed = drandBeaconSchema.safeParse(dataRaw) + + if (!parsed.success) { + return err(DrandError.InvalidResponse) + } + + return ok(parsed.data) + } + + currentRound(): bigint { + const currentTimestamp = nowInSeconds() + const currentRound = Math.floor( + (currentTimestamp - Number(env.EVM_DRAND_GENESIS_TIMESTAMP_SECONDS)) / + Number(env.EVM_DRAND_PERIOD_SECONDS) + + 1, + ) + return BigInt(currentRound) + } +} diff --git a/apps/randomness/src/Randomness.ts b/apps/randomness/src/Randomness.ts index 93e9856424..e6dea3bf3c 100644 --- a/apps/randomness/src/Randomness.ts +++ b/apps/randomness/src/Randomness.ts @@ -3,24 +3,32 @@ import type { Hex, UUID } from "@happy.tech/common" import { bytesToHex, encodePacked, keccak256 } from "viem" export enum RandomnessStatus { + /** The randomness was created but no transactions were submitted to TXM. */ PENDING = "PENDING", + /** The commitment transaction was submitted to the TXM. */ COMMITMENT_SUBMITTED = "COMMITMENT_SUBMITTED", + /** The commitment transaction was successfully executed onchain. */ COMMITMENT_EXECUTED = "COMMITMENT_EXECUTED", + /** + * The commitment transaction was included onchain but the execution failed, or failed to be + * included onchain (e.g. cancelled). + */ COMMITMENT_FAILED = "COMMITMENT_FAILED", + /** The reveal transaction was submitted to TXM (requires the commitment tx to be successful). */ REVEAL_SUBMITTED = "REVEAL_SUBMITTED", + /** The reveal transaction was successfully executed onchain. */ REVEAL_EXECUTED = "REVEAL_EXECUTED", + /** + * The reveal transaction was included onchain but the execution failed, or failed to be + * included onchain (e.g. cancelled). + */ REVEAL_FAILED = "REVEAL_FAILED", + /** The reveal transaction could not be submitted on time to TXM. */ REVEAL_NOT_SUBMITTED_ON_TIME = "REVEAL_NOT_SUBMITTED_ON_TIME", } -export const FINALIZED_STATUSES = [ - RandomnessStatus.REVEAL_EXECUTED, - RandomnessStatus.REVEAL_FAILED, - RandomnessStatus.REVEAL_NOT_SUBMITTED_ON_TIME, -] - export class Randomness { - public timestamp: bigint + public blockNumber: bigint public value: bigint public hashedValue: Hex public commitmentTransactionIntentId: UUID | undefined @@ -28,14 +36,14 @@ export class Randomness { public status: RandomnessStatus constructor(params: { - timestamp: bigint + blockNumber: bigint value: bigint hashedValue: Hex commitmentTransactionIntentId?: UUID revealTransactionIntentId?: UUID status: RandomnessStatus }) { - this.timestamp = params.timestamp + this.blockNumber = params.blockNumber this.value = params.value this.hashedValue = params.hashedValue this.commitmentTransactionIntentId = params.commitmentTransactionIntentId @@ -43,41 +51,41 @@ export class Randomness { this.status = params.status } - public commitmentExecuted(): void { + commitmentExecuted(): void { this.status = RandomnessStatus.COMMITMENT_EXECUTED } - public revealExecuted(): void { + revealExecuted(): void { this.status = RandomnessStatus.REVEAL_EXECUTED } - public addCommitmentTransactionIntentId(intentId: UUID): void { + addCommitmentTransactionIntentId(intentId: UUID): void { this.commitmentTransactionIntentId = intentId this.status = RandomnessStatus.COMMITMENT_SUBMITTED } - public addRevealTransactionIntentId(intentId: UUID): void { + addRevealTransactionIntentId(intentId: UUID): void { this.revealTransactionIntentId = intentId this.status = RandomnessStatus.REVEAL_SUBMITTED } - public commitmentFailed(): void { + commitmentFailed(): void { this.status = RandomnessStatus.COMMITMENT_FAILED } - public revealFailed(): void { + revealFailed(): void { this.status = RandomnessStatus.REVEAL_FAILED } - public revealNotSubmittedOnTime(): void { + revealNotSubmittedOnTime(): void { this.status = RandomnessStatus.REVEAL_NOT_SUBMITTED_ON_TIME } - static createRandomness(timestamp: bigint): Randomness { + static createRandomness(blockNumber: bigint): Randomness { const value = Randomness.generateValue() const hashedValue = Randomness.hashValue(value) return new Randomness({ - timestamp, + blockNumber, value, hashedValue, commitmentTransactionIntentId: undefined, @@ -87,12 +95,12 @@ export class Randomness { } private static generateValue(): bigint { - const bytes = crypto.randomBytes(32) + const bytes = crypto.randomBytes(16) return BigInt(bytesToHex(bytes)) } private static hashValue(value: bigint): Hex { - return keccak256(encodePacked(["uint256"], [value])) + return keccak256(encodePacked(["uint128"], [value])) } } diff --git a/apps/randomness/src/RandomnessRepository.ts b/apps/randomness/src/RandomnessRepository.ts index b93f80f16d..18f1630fce 100644 --- a/apps/randomness/src/RandomnessRepository.ts +++ b/apps/randomness/src/RandomnessRepository.ts @@ -1,45 +1,44 @@ import { type UUID, unknownToError } from "@happy.tech/common" import { bigIntToZeroPadded } from "@happy.tech/common" import { type Result, ResultAsync } from "neverthrow" -import { FINALIZED_STATUSES, type Randomness, type RandomnessStatus } from "./Randomness" +import { type Randomness, RandomnessStatus } from "./Randomness" +import { DIGITS_MAX_UINT256 } from "./constants" import { db } from "./db/driver" import { randomnessEntityToRow, randomnessRowToEntity } from "./db/types" -const COMMITMENT_PRUNE_INTERVAL_SECONDS = 120n // 2 minutes +const COMMITMENT_PRUNE_INTERVAL_BLOCKS = 60n // 2 minutes -// Quantity of digits in the max uint256 value -export const DIGITS_MAX_UINT256 = 78 +export const FINALIZED_STATUSES = [ + RandomnessStatus.REVEAL_EXECUTED, + RandomnessStatus.REVEAL_FAILED, + RandomnessStatus.REVEAL_NOT_SUBMITTED_ON_TIME, +] export class RandomnessRepository { - private readonly map = new Map() + // In-memory cache that maps block numbers to their corresponding Randomness + private readonly cache = 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) + this.cache.set(randomness.blockNumber, randomness) } } - getRandomnessForTimestamp(timestamp: bigint): Randomness | undefined { - return this.map.get(timestamp) + getRandomnessForBlockNumber(blockNumber: bigint): Randomness | undefined { + return this.cache.get(blockNumber) } getRandomnessForIntentId(intentId: UUID): Randomness | undefined { - return Array.from(this.map.values()).find( + return Array.from(this.cache.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) + return Array.from(this.cache.values()).filter((randomness) => randomness.status === status) } /** @@ -47,7 +46,7 @@ export class RandomnessRepository { * 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) + this.cache.set(randomness.blockNumber, randomness) const row = randomnessEntityToRow(randomness) return await ResultAsync.fromPromise(db.insertInto("randomnesses").values(row).execute(), unknownToError).map( () => undefined, @@ -59,34 +58,30 @@ export class RandomnessRepository { * 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) + this.cache.set(randomness.blockNumber, randomness) const row = randomnessEntityToRow(randomness) return await ResultAsync.fromPromise( db .updateTable("randomnesses") .set(row) - .where("timestamp", "=", bigIntToZeroPadded(randomness.timestamp, DIGITS_MAX_UINT256)) + .where("blockNumber", "=", bigIntToZeroPadded(randomness.blockNumber, 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) + async pruneRandomnesses(latestBlock: bigint): Promise> { + const cutoffBlock = latestBlock - COMMITMENT_PRUNE_INTERVAL_BLOCKS + for (const blockNumber of this.cache.keys()) { + if (blockNumber < cutoffBlock) { + this.cache.delete(blockNumber) } } return await ResultAsync.fromPromise( db .deleteFrom("randomnesses") - .where( - "timestamp", - "<", - bigIntToZeroPadded(latestBlockTimestamp - COMMITMENT_PRUNE_INTERVAL_SECONDS, DIGITS_MAX_UINT256), - ) + .where("blockNumber", "<", bigIntToZeroPadded(cutoffBlock, DIGITS_MAX_UINT256)) .where("status", "in", FINALIZED_STATUSES) .execute(), unknownToError, diff --git a/apps/randomness/src/TransactionFactory.ts b/apps/randomness/src/TransactionFactory.ts index ac8c2548b9..b3a7894013 100644 --- a/apps/randomness/src/TransactionFactory.ts +++ b/apps/randomness/src/TransactionFactory.ts @@ -1,35 +1,71 @@ import type { Transaction, TransactionManager } from "@happy.tech/txm" -import type { Address } from "viem" +import { ok } from "neverthrow" +import type { Result } from "neverthrow" +import type { Address, Hex } from "viem" import type { Randomness } from "./Randomness" +import { env } from "./env" 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 - } + constructor( + private readonly transactionManager: TransactionManager, + private readonly randomContractAddress: Address, + private readonly precommitDelay: bigint, + ) {} createCommitmentTransaction(randomness: Randomness): Transaction { + // The commitment transaction must be submitted at least one block + // before the deadline, because submissions after the precommit delay + // are not allowed. We compute the deadline by subtracting the precommit + // delay from the block number, multiplying by the block time, and adding + // the genesis timestamp. + const deadline = Number( + (randomness.blockNumber - this.precommitDelay) * env.BLOCK_TIME + env.HAPPY_GENESIS_TIMESTAMP_SECONDS, + ) return this.transactionManager.createTransaction({ address: this.randomContractAddress, functionName: "postCommitment", contractName: "Random", - args: [randomness.timestamp, randomness.hashedValue], - deadline: Number(randomness.timestamp - this.precommitDelay), + args: [randomness.blockNumber, randomness.hashedValue], + deadline, }) } createRevealValueTransaction(randomness: Randomness): Transaction { + // The reveal transaction must execute in its target block. + // We calculate its deadline by multiplying the block number by the block time + // and adding the genesis timestamp. + const deadline = Number(randomness.blockNumber * env.BLOCK_TIME + env.HAPPY_GENESIS_TIMESTAMP_SECONDS) + return this.transactionManager.createTransaction({ address: this.randomContractAddress, functionName: "revealValue", contractName: "Random", - args: [randomness.timestamp, randomness.value], - deadline: Number(randomness.timestamp), + args: [randomness.blockNumber, randomness.value], + deadline, }) } + + createPostDrandTransaction({ + round, + signature, + }: { + round: bigint + signature: Hex + }): Result { + const startSecondSliceIndex = signature.length - 64 + + const signatureSlice1 = "0x" + signature.slice(2, startSecondSliceIndex) + const signatureSlice2 = "0x" + signature.slice(startSecondSliceIndex) + + const signatureArray = [BigInt(signatureSlice1), BigInt(signatureSlice2)] + + return ok( + this.transactionManager.createTransaction({ + address: this.randomContractAddress, + functionName: "postDrand", + contractName: "Random", + args: [round, signatureArray], + }), + ) + } } diff --git a/apps/randomness/src/constants.ts b/apps/randomness/src/constants.ts new file mode 100644 index 0000000000..db523d5264 --- /dev/null +++ b/apps/randomness/src/constants.ts @@ -0,0 +1,2 @@ +// Quantity of digits in the max uint256 value +export const DIGITS_MAX_UINT256 = 78 diff --git a/apps/randomness/src/db/types.ts b/apps/randomness/src/db/types.ts index 84105f0971..300095d1a9 100644 --- a/apps/randomness/src/db/types.ts +++ b/apps/randomness/src/db/types.ts @@ -1,10 +1,11 @@ import { type Hex, type UUID, bigIntToZeroPadded } from "@happy.tech/common" +import type { DrandStatus } from "../Drand" import { Randomness, type RandomnessStatus } from "../Randomness" -import { DIGITS_MAX_UINT256 } from "../RandomnessRepository" +import { DIGITS_MAX_UINT256 } from "../constants" // Values are stored as strings because they can be large numbers bigger than the max value of an SQLite integer export interface RandomnessRow { - timestamp: string + blockNumber: string value: string hashedValue: Hex commitmentTransactionIntentId: UUID | undefined @@ -12,13 +13,22 @@ export interface RandomnessRow { status: RandomnessStatus } +export interface DrandRow { + // We store the round as a string because it can be a large number bigger than the max value of an SQLite integer + round: string + signature: string | undefined + status: DrandStatus + transactionIntentId: UUID | undefined +} + export interface Database { randomnesses: RandomnessRow + drands: DrandRow } export function randomnessRowToEntity(row: RandomnessRow): Randomness { return new Randomness({ - timestamp: BigInt(row.timestamp), + blockNumber: BigInt(row.blockNumber), value: BigInt(row.value), hashedValue: row.hashedValue, commitmentTransactionIntentId: row.commitmentTransactionIntentId, @@ -29,7 +39,7 @@ export function randomnessRowToEntity(row: RandomnessRow): Randomness { export function randomnessEntityToRow(entity: Randomness): RandomnessRow { return { - timestamp: bigIntToZeroPadded(entity.timestamp, DIGITS_MAX_UINT256), + blockNumber: bigIntToZeroPadded(entity.blockNumber, DIGITS_MAX_UINT256), value: bigIntToZeroPadded(entity.value, DIGITS_MAX_UINT256), hashedValue: entity.hashedValue, commitmentTransactionIntentId: entity.commitmentTransactionIntentId, diff --git a/apps/randomness/src/env.ts b/apps/randomness/src/env.ts index 50fbe6cabc..3980e15b7c 100644 --- a/apps/randomness/src/env.ts +++ b/apps/randomness/src/env.ts @@ -20,6 +20,11 @@ const envSchema = z.object({ CHAIN_ID: z.string().transform((s) => Number(s)), RANDOMNESS_DB_PATH: z.string().trim(), TXM_DB_PATH: z.string().trim(), + HAPPY_GENESIS_TIMESTAMP_SECONDS: z.string().transform((s) => BigInt(s)), + 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)), }) const parsedEnv = envSchema.safeParse(process.env) diff --git a/apps/randomness/src/index.ts b/apps/randomness/src/index.ts index 90ac84ff04..8350e95489 100644 --- a/apps/randomness/src/index.ts +++ b/apps/randomness/src/index.ts @@ -1,18 +1,31 @@ +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 { 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() + this.drandRepository = new DrandRepository() this.txm = new TransactionManager({ privateKey: env.PRIVATE_KEY, chainId: env.CHAIN_ID, @@ -20,54 +33,115 @@ class RandomnessService { gasEstimator: new CustomGasEstimator(), rpc: { url: env.RPC_URL, + allowDebug: true, }, }) this.transactionFactory = new TransactionFactory(this.txm, env.RANDOM_CONTRACT_ADDRESS, env.PRECOMMIT_DELAY) - - this.txm.addTransactionOriginator(this.onCollectTransactions.bind(this)) + this.drandService = new DrandService() } async start() { await Promise.all([this.txm.start(), this.randomnessRepository.start()]) + this.txm.addTransactionOriginator(this.onCollectTransactions.bind(this)) this.txm.addHook(TxmHookType.TransactionStatusChanged, this.onTransactionStatusChange.bind(this)) this.txm.addHook(TxmHookType.NewBlock, this.onNewBlock.bind(this)) + + // 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) } private onTransactionStatusChange(transaction: Transaction) { const randomness = this.randomnessRepository.getRandomnessForIntentId(transaction.intentId) - if (!randomness) { - console.warn("Couldn't find randomness with intentId", transaction.intentId) - return - } + const successStatuses = [TransactionStatus.Success] + const failedStatuses = [TransactionStatus.Failed, TransactionStatus.Expired, TransactionStatus.Cancelled] - if (transaction.status === TransactionStatus.Success) { - if (randomness.commitmentTransactionIntentId === transaction.intentId) { - randomness.commitmentExecuted() - } else if (randomness.revealTransactionIntentId === transaction.intentId) { - randomness.revealExecuted() + if (randomness) { + if (successStatuses.includes(transaction.status)) { + if (randomness.commitmentTransactionIntentId === transaction.intentId) { + randomness.commitmentExecuted() + } else if (randomness.revealTransactionIntentId === transaction.intentId) { + randomness.revealExecuted() + } + } else if (failedStatuses.includes(transaction.status)) { + if (randomness.commitmentTransactionIntentId === transaction.intentId) { + randomness.commitmentFailed() + } else if (randomness.revealTransactionIntentId === transaction.intentId) { + randomness.revealFailed() + } } - } else if ( - transaction.status === TransactionStatus.Failed || - transaction.status === TransactionStatus.Expired - ) { - if (randomness.commitmentTransactionIntentId === transaction.intentId) { - randomness.commitmentFailed() - } else if (randomness.revealTransactionIntentId === transaction.intentId) { - randomness.revealFailed() + + if (successStatuses.includes(transaction.status) || failedStatuses.includes(transaction.status)) { + this.randomnessRepository.updateRandomness(randomness).then((result) => { + if (result.isErr()) { + console.error("Failed to update randomness", result.error) + } + }) } + + return } - this.randomnessRepository.updateRandomness(randomness).then((result) => { - if (result.isErr()) { - console.error("Failed to update randomness", result.error) + const drand = this.drandRepository.getDrandByTransactionIntentId(transaction.intentId) + + if (drand) { + if (failedStatuses.includes(transaction.status)) { + drand.transactionFailed() + } else if (successStatuses.includes(transaction.status)) { + drand.executionSuccess() } - }) + + this.drandRepository.updateDrand(drand).catch((error) => { + console.error("Failed to update drand", error) + }) + + return + } + + console.warn("Couldn't find randomness or drand with intentId", transaction.intentId) } private async onNewBlock(block: LatestBlock) { this.handleRevealNotSubmittedOnTime(block) - this.randomnessRepository.pruneRandomnesses(block.timestamp).then((result) => { + this.randomnessRepository.pruneRandomnesses(block.number).then((result) => { if (result.isErr()) { console.error("Failed to prune commitments", result.error) } @@ -77,10 +151,8 @@ class RandomnessService { 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) { + if (randomness.blockNumber <= block.number) { randomness.revealNotSubmittedOnTime() this.randomnessRepository.updateRandomness(randomness).then((result) => { if (result.isErr()) { @@ -91,33 +163,83 @@ class RandomnessService { } } - 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) + 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[] = [] - const nextBlockTimestamp = block.timestamp + env.BLOCK_TIME + const nextBlockNumber = block.number + 1n - // 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 + // We try to precommit for all blocks in [nextBlockNumber + PRECOMMIT_DELAY, nextBlockNumber + PRECOMMIT_DELAY + POST_COMMIT_MARGIN]. + const firstBlockToCommit = nextBlockNumber + env.PRECOMMIT_DELAY + const lastBlockToCommit = firstBlockToCommit + env.POST_COMMIT_MARGIN - // Array with timestamps from firstBlockToCommit to lastBlockToCommit - const timestampsToCommit = Array.from( - { length: Number(lastCommitTime - firstCommitTime) }, - (_, i) => firstCommitTime + BigInt(i), + // Array with blocks from firstBlockToCommit to lastBlockToCommit + const blockNumbersToCommit = Array.from( + { length: Number(lastBlockToCommit - firstBlockToCommit) }, + (_, i) => firstBlockToCommit + BigInt(i), ) - for (const timestamp of timestampsToCommit) { - const randomness = this.randomnessRepository.getRandomnessForTimestamp(timestamp) + for (const blockNumber of blockNumbersToCommit) { + const randomness = this.randomnessRepository.getRandomnessForBlockNumber(blockNumber) // Already has a randomness for this timestamp, so we skip if (randomness) { continue } - const newRandomness = Randomness.createRandomness(timestamp) + const newRandomness = Randomness.createRandomness(blockNumber) const commitmentTransaction = this.transactionFactory.createCommitmentTransaction(newRandomness) @@ -133,10 +255,10 @@ class RandomnessService { }) } - const randomnessToReveal = this.randomnessRepository.getRandomnessForTimestamp(nextBlockTimestamp) + const randomnessToReveal = this.randomnessRepository.getRandomnessForBlockNumber(nextBlockNumber) if (!randomnessToReveal) { - console.warn("Not found randomness to reveal with timestamp", nextBlockTimestamp) + console.warn("Not found randomness to reveal with block number", nextBlockNumber) return transactions } @@ -152,6 +274,24 @@ class RandomnessService { } }) + transactions.push(...this.pendingPostDrandTransactions) + + this.pendingPostDrandTransactions.map(async (transaction) => { + const drand = this.drandRepository.getDrandByTransactionIntentId(transaction.intentId) + + if (!drand) { + console.error("Drand not found for transaction", transaction.intentId) + return + } + + drand.transactionSubmitted() + this.drandRepository.updateDrand(drand).catch((error) => { + console.error("Failed to update drand", error) + }) + }) + + this.pendingPostDrandTransactions = [] + return transactions } } diff --git a/contracts/deployments/happy-sepolia/random/abis.json b/contracts/deployments/happy-sepolia/random/abis.json index 5bf845ec23..b80a6d1bf4 100644 --- a/contracts/deployments/happy-sepolia/random/abis.json +++ b/contracts/deployments/happy-sepolia/random/abis.json @@ -19,7 +19,7 @@ "internalType": "uint256" }, { - "name": "_happyGenesisTimestamp", + "name": "_happyGenesisBlock", "type": "uint256", "internalType": "uint256" }, @@ -44,6 +44,32 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "DRAND_GENESIS_TIMESTAMP", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DRAND_PERIOD", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "DST", @@ -59,7 +85,7 @@ }, { "type": "function", - "name": "PRECOMMIT_DELAY", + "name": "HAPPY_GENESIS_BLOCK", "inputs": [], "outputs": [ { @@ -72,7 +98,7 @@ }, { "type": "function", - "name": "drandGenesisTimestamp", + "name": "HAPPY_TIME_BLOCK", "inputs": [], "outputs": [ { @@ -85,7 +111,7 @@ }, { "type": "function", - "name": "drandPeriod", + "name": "PRECOMMIT_DELAY", "inputs": [], "outputs": [ { @@ -159,41 +185,15 @@ "inputs": [ { "name": "blockNumber", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" } ], "outputs": [ { "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "happyGenesisTimestamp", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "happyTimeBlock", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" } ], "stateMutability": "view" @@ -255,8 +255,8 @@ "inputs": [ { "name": "blockNumber", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" }, { "name": "commitmentHash", @@ -291,7 +291,7 @@ "inputs": [], "outputs": [ { - "name": "", + "name": "randomValue", "type": "bytes32", "internalType": "bytes32" } @@ -349,13 +349,13 @@ "inputs": [ { "name": "blockNumber", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" }, { "name": "revealedValue", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" } ], "outputs": [], @@ -399,15 +399,15 @@ "inputs": [ { "name": "blockNumber", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" } ], "outputs": [ { "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "uint128", + "internalType": "uint128" } ], "stateMutability": "view" @@ -418,9 +418,9 @@ "inputs": [ { "name": "blockNumber", - "type": "uint256", + "type": "uint128", "indexed": true, - "internalType": "uint256" + "internalType": "uint128" }, { "name": "commitment", @@ -475,15 +475,15 @@ "inputs": [ { "name": "blockNumber", - "type": "uint256", + "type": "uint128", "indexed": true, - "internalType": "uint256" + "internalType": "uint128" }, { "name": "revealedValue", - "type": "uint256", + "type": "uint128", "indexed": false, - "internalType": "uint256" + "internalType": "uint128" } ], "anonymous": false diff --git a/contracts/deployments/happy-sepolia/random/abis.ts b/contracts/deployments/happy-sepolia/random/abis.ts index 36b404276d..fddcfd128a 100644 --- a/contracts/deployments/happy-sepolia/random/abis.ts +++ b/contracts/deployments/happy-sepolia/random/abis.ts @@ -23,7 +23,7 @@ const contractToAbi = { internalType: "uint256", }, { - name: "_happyGenesisTimestamp", + name: "_happyGenesisBlock", type: "uint256", internalType: "uint256", }, @@ -48,6 +48,32 @@ const contractToAbi = { ], stateMutability: "view", }, + { + type: "function", + name: "DRAND_GENESIS_TIMESTAMP", + inputs: [], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "DRAND_PERIOD", + inputs: [], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, { type: "function", name: "DST", @@ -63,7 +89,7 @@ const contractToAbi = { }, { type: "function", - name: "PRECOMMIT_DELAY", + name: "HAPPY_GENESIS_BLOCK", inputs: [], outputs: [ { @@ -76,7 +102,7 @@ const contractToAbi = { }, { type: "function", - name: "drandGenesisTimestamp", + name: "HAPPY_TIME_BLOCK", inputs: [], outputs: [ { @@ -89,7 +115,7 @@ const contractToAbi = { }, { type: "function", - name: "drandPeriod", + name: "PRECOMMIT_DELAY", inputs: [], outputs: [ { @@ -163,41 +189,15 @@ const contractToAbi = { inputs: [ { name: "blockNumber", - type: "uint256", - internalType: "uint256", + type: "uint128", + internalType: "uint128", }, ], outputs: [ { name: "", - type: "uint256", - internalType: "uint256", - }, - ], - stateMutability: "view", - }, - { - type: "function", - name: "happyGenesisTimestamp", - inputs: [], - outputs: [ - { - name: "", - type: "uint256", - internalType: "uint256", - }, - ], - stateMutability: "view", - }, - { - type: "function", - name: "happyTimeBlock", - inputs: [], - outputs: [ - { - name: "", - type: "uint256", - internalType: "uint256", + type: "uint128", + internalType: "uint128", }, ], stateMutability: "view", @@ -259,8 +259,8 @@ const contractToAbi = { inputs: [ { name: "blockNumber", - type: "uint256", - internalType: "uint256", + type: "uint128", + internalType: "uint128", }, { name: "commitmentHash", @@ -295,7 +295,7 @@ const contractToAbi = { inputs: [], outputs: [ { - name: "", + name: "randomValue", type: "bytes32", internalType: "bytes32", }, @@ -353,13 +353,13 @@ const contractToAbi = { inputs: [ { name: "blockNumber", - type: "uint256", - internalType: "uint256", + type: "uint128", + internalType: "uint128", }, { name: "revealedValue", - type: "uint256", - internalType: "uint256", + type: "uint128", + internalType: "uint128", }, ], outputs: [], @@ -403,15 +403,15 @@ const contractToAbi = { inputs: [ { name: "blockNumber", - type: "uint256", - internalType: "uint256", + type: "uint128", + internalType: "uint128", }, ], outputs: [ { name: "", - type: "uint256", - internalType: "uint256", + type: "uint128", + internalType: "uint128", }, ], stateMutability: "view", @@ -422,9 +422,9 @@ const contractToAbi = { inputs: [ { name: "blockNumber", - type: "uint256", + type: "uint128", indexed: true, - internalType: "uint256", + internalType: "uint128", }, { name: "commitment", @@ -479,15 +479,15 @@ const contractToAbi = { inputs: [ { name: "blockNumber", - type: "uint256", + type: "uint128", indexed: true, - internalType: "uint256", + internalType: "uint128", }, { name: "revealedValue", - type: "uint256", + type: "uint128", indexed: false, - internalType: "uint256", + internalType: "uint128", }, ], anonymous: false, diff --git a/contracts/deployments/happy-sepolia/random/transactions.json b/contracts/deployments/happy-sepolia/random/transactions.json index 906dd62b2d..8aecbf2ed2 100644 --- a/contracts/deployments/happy-sepolia/random/transactions.json +++ b/contracts/deployments/happy-sepolia/random/transactions.json @@ -1,23 +1,17 @@ { "transactions": [ { - "hash": "0xf14e183e774da1688029fd42690e1db562182e2d9594c1482e8f2e6a164d33f7", + "hash": "0xb5cab6a434e8003aa94b7f7d72dc50e3f63a563263a56d9075dc3210e52b9d8f", "transactionType": "CREATE", "contractName": "Random", "contractAddress": "0x51745e910fad45a6ca620bcc7a7fab7683b142e5", "function": null, - "arguments": [ - "[2416910118189096557713698606232949750075245832257361418817199221841198809231, 3565178688866727608783247307855519961161197286613423629330948765523825963906, 18766085122067595057703228467555884757373773082319035490740181099798629248523, 263980444642394177375858669180402387903005329333277938776544051059273779190]", - "1727521075", - "3", - "1734347812", - "1" - ], + "arguments": null, "transaction": { "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", - "gas": "0x3678db", + "gas": "0x3ad813", "value": "0x0", - "input": "0x608060405234801561000f575f80fd5b5060405161310638038061310683398101604081905261002e91610342565b848484338061005757604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b610060816100b2565b5061006a83610101565b610089578260405163c7728ee960e01b815260040161004e91906103da565b610095600484816102dc565b5060089190915560095550600b91909155600c555061040a915050565b5f80546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b80515f905f805160206130e6833981519152111580610131575060208201515f805160206130e683398151915211155b80610168575060408201515f805160206130e6833981519152111580610168575060608201515f805160206130e683398151915211155b1561017457505f919050565b61017d82610183565b92915050565b5f815160208301515f805160206130e68339815191528283095f805160206130e683398151915282830981828301015f805160206130e683398151915282838401085f805160206130e683398151915286825f805160206130e6833981519152038601099350505f805160206130e683398151915284835f805160206130e68339815191520383010991505f805160206130e68339815191527f2b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5840894505f805160206130e68339815191527e9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d28308935060408701519250606087015191505f805160206130e683398151915280835f805160206130e68339815191520385085f805160206130e68339815191528486080990505f805160206130e6833981519152828460011b0994149290931491909116949350505050565b826004810192821561030a579160200282015b8281111561030a5782518255916020019190600101906102ef565b5061031692915061031a565b5090565b5b80821115610316575f815560010161031b565b634e487b7160e01b5f52604160045260245ffd5b5f805f805f6101008688031215610357575f80fd5b86601f870112610365575f80fd5b604051608081016001600160401b03811182821017156103875761038761032e565b60405280608088018981111561039b575f80fd5b885b818110156103b557805183526020928301920161039d565b505160a089015160c08a015160e0909a0151939b919a50989750919550909350505050565b6080810181835f5b60048110156104015781518352602092830192909101906001016103e2565b50505092915050565b612ccf806104175f395ff3fe608060405234801561000f575f80fd5b506004361061018f575f3560e01c80637b308b6d116100dd578063f14ff4ef11610088578063f5fa417a11610063578063f5fa417a14610326578063f61ff62314610339578063fd0a5aaf1461034c575f80fd5b8063f14ff4ef146102ed578063f2fde38b14610300578063f398a6db14610313575f80fd5b80638da5cb5b116100b85780638da5cb5b146102aa5780639c8c9b4a146102d1578063dc0998aa146102da575f80fd5b80637b308b6d146102865780638285d8891461029957806386377e8b146102a1575f80fd5b80635f7c75221161013d5780636fe318c1116101185780636fe318c114610256578063715018a6146102755780637425518f1461027d575f80fd5b80635f7c75221461021b5780636037d218146102305780636d1ef32814610243575f80fd5b80634a8ae6311161016d5780634a8ae631146101ed5780635ec01e4d146102005780635f6f07dc14610208575f80fd5b8063110a85bf146101935780631d539681146101af5780633d55a583146101c4575b5f80fd5b61019c600b5481565b6040519081526020015b60405180910390f35b6101c26101bd3660046127c7565b610354565b005b61019c6101d23660046127fe565b67ffffffffffffffff165f908152600a602052604090205490565b61019c6101fb366004612817565b610435565b61019c61044f565b6101c26102163660046127c7565b61049f565b6102236105dd565b6040516101a6919061282e565b61019c61023e3660046127fe565b6105f9565b6101c26102513660046128ae565b610677565b61019c6102643660046127fe565b600a6020525f908152604090205481565b6101c2610832565b61019c60095481565b61019c610294366004612817565b610845565b61019c600281565b61019c600c5481565b5f5460405173ffffffffffffffffffffffffffffffffffffffff90911681526020016101a6565b61019c60085481565b61019c6102e8366004612817565b61085e565b61019c6102fb366004612817565b610874565b6101c261030e36600461293a565b610881565b61019c610321366004612817565b6108e4565b61019c610334366004612817565b6108f9565b61019c610347366004612817565b610935565b61019c600a81565b61035c61096d565b610367600a8361299a565b4311156103a0576040517f4c8e490f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f82815260036020526040902054156103e5576040517f145718a700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f82815260036020526040908190208290555182907f7a27537ccaa1ad9d9b038704d7cf6ec9b4e73e786ec619181e3eb1451de868d8906104299084815260200190565b60405180910390a25050565b5f61044961044460028461299a565b6109bf565b92915050565b5f8061045f61044460024261299a565b90505f61046b436108f9565b6040805160208101859052908101829052909150606001604051602081830303815290604052805190602001209250505090565b6104a761096d565b5f82815260036020526040812054908190036104ef576040517fc6c155d800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b824314610528576040517f7fc88be700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b604080516020810184905201604051602081830303815290604052805190602001208114610582576040517f9ea6d12700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600182905560028390555f83815260036020526040808220919091555183907f42629d9632d9a72fcd50885db3d64e9e40e4c13f06af15d9241ef817bdf265bc906105d09085815260200190565b60405180910390a2505050565b6040518060600160405280602b8152602001612c6f602b913981565b67ffffffffffffffff81165f908152600a6020526040812054810361065b576040517fc3f4d68a00000000000000000000000000000000000000000000000000000000815267ffffffffffffffff831660048201526024015b60405180910390fd5b5067ffffffffffffffff165f908152600a602052604090205490565b6040805160208082528183019092525f91602082018180368337019050509050825f526008601820808260200152505f6106c96040518060600160405280602b8152602001612c6f602b9139836109ee565b90505f6106d584610aa3565b90508061071557600482856040517fb8cf7d30000000000000000000000000000000000000000000000000000000008152600401610652939291906129d5565b6040805160808101918290525f91829161075291889190600490819081845b81548152602001906001019080831161073457505050505086610b13565b915091508161079457600484876040517fb8cf7d30000000000000000000000000000000000000000000000000000000008152600401610652939291906129d5565b5f866040516020016107a69190612a22565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0018152828252805160209182012067ffffffffffffffff8c165f818152600a845293909320819055808452935090917fb262efe8f3fb8d5cb448e8771d8d93fe981dfa54d2e292afe1b4970dc9237134910160405180910390a25050505050505050565b61083a61096d565b6108435f610cc5565b565b5f816002541461085657505f919050565b505060015490565b6004816004811061086d575f80fd5b0154905081565b5f61044961044483610d39565b61088961096d565b73ffffffffffffffffffffffffffffffffffffffff81166108d8576040517f1e4fbdf70000000000000000000000000000000000000000000000000000000081525f6004820152602401610652565b6108e181610cc5565b50565b5f6104496108f461034784610d39565b610d60565b5f8160025414610856576040517f481b130600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f60095460026109459190612a30565b6002610959610954828661299a565b610d87565b6109639190612a47565b6104499190612a47565b5f5473ffffffffffffffffffffffffffffffffffffffff163314610843576040517f118cdaa7000000000000000000000000000000000000000000000000000000008152336004820152602401610652565b5f80600954600854846109d2919061299a565b6109dc9190612a87565b90506109e7816105f9565b9392505050565b6109f661276d565b5f610a018484610dad565b90505f610a1482825b6020020151610ef8565b90505f610a22836001610a0a565b9050610a2c61278b565b825181526020808401518282015282516040808401919091529083015160608301525f908460808460066107d05a03fa905080610a9757816040517f128e3f080000000000000000000000000000000000000000000000000000000081526004016106529190612ac7565b50919695505050505050565b80515f907f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47111580610af9575060208201517f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4711155b15610b0557505f919050565b610449826113a0565b919050565b5f805f604051806101800160405280875f60028110610b3457610b34612a9a565b6020020151815260200187600160028110610b5157610b51612a9a565b602002015181526020017f198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c281526020017f1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed81526020017f275dc4a288d1afb3cbb1ac09187524c7db36395df7be3b99e673b13a075a65ec81526020017f1d9befcd05a5323e6da4d435f3b617cdb3af83285c2df711ef39c01571827f9d8152602001855f60028110610c0557610c05612a9a565b6020020151815260200185600160028110610c2257610c22612a9a565b6020020151815260200186600160048110610c3f57610c3f612a9a565b60200201518152602001865f60048110610c5b57610c5b612a9a565b6020020151815260200186600360048110610c7857610c78612a9a565b6020020151815260200186600260048110610c9557610c95612a9a565b602002015190529050610ca66127a9565b6020816101808460086107d05a03fa9051151597909650945050505050565b5f805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b600c545f90610d4960018461299a565b610d539190612a30565b600b546104499190612a47565b5f600c54600b5483610d72919061299a565b610d7c9190612a87565b610449906001612a47565b5f60095482610d969190612af7565b600954610da3919061299a565b6104499083612a47565b610db561276d565b5f610dc08484611446565b90505f805f806018850177ffffffffffffffffffffffffffffffffffffffffffffffff815116935060308601905077ffffffffffffffffffffffffffffffffffffffffffffffff81511694507f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47857f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4778010000000000000000000000000000000000000000000000008709086048870151606088015177ffffffffffffffffffffffffffffffffffffffffffffffff908116975016945092507f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47905084817801000000000000000000000000000000000000000000000000860908604080518082019091529283526020830152509695505050505050565b610f0061276d565b7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478210610f5c576040517fd53e941500000000000000000000000000000000000000000000000000000000815260048101839052602401610652565b5f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4760047f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478586090990505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478260010890507f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4761101a837f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4761299a565b60010891505f61104c7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47838509611630565b90505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4778016789af3a83522eb353c98fc6b36d713d5d8d1cc5dffffffa7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47847f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47888b09090990505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47611120837f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4761299a565b7f183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea30890505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47837f183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea30890505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4786870990505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4786830990505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47807f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478485097f10216f7ba065e00de81ac1e7808072c9dd2b2385cd7b438469602eb24829a9bd0960010890505f8061125a6112558861163a565b6116ac565b5f0b6001036112c357868c5261126f8761163a565b905061127a8161175c565b60208e01919091529150816112be576040517f396ec77100000000000000000000000000000000000000000000000000000000815260048101829052602401610652565b61133f565b6112cf6112558761163a565b5f0b6001036112e457858c5261126f8661163a565b828c526112f08361163a565b90506112fb8161175c565b60208e019190915291508161133f576040517f396ec77100000000000000000000000000000000000000000000000000000000815260048101829052602401610652565b60208c015161134d90611796565b6113568e611796565b146113905760208c015161138a907f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4761299a565b60208d01525b5050505050505050505050919050565b5f815160208301517f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478283097f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4783820990507f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd476003820890507f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4782830914949350505050565b815160609060ff81111561148857836040517f26e4f9ba000000000000000000000000000000000000000000000000000000008152600401610652919061282e565b60408051608880825260c082019092525f916020820181803683370190505090505f81855f60605f8a886040516020016114c89796959493929190612b21565b60405160208183030381529060405290505f818051906020012090505f81600189876040516020016114fd9493929190612bfc565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001815282825280516020820120606080855260808501909352909350915f91602082018180368337019050509050600360015b818110156115f45785841861156e826001612a47565b8d8b6040516020016115839493929190612bfc565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001815291905260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff830181028501810195909552805194810194909420939450600101611558565b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff016020908102820101919091529550505050505092915050565b5f610449826117a2565b5f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4760037f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47847f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47868709090892915050565b5f806116b783611efa565b90506116e460017f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4761299a565b810361171257507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff92915050565b8015801590611722575080600114155b15610449576040517f396ec77100000000000000000000000000000000000000000000000000000000815260048101849052602401610652565b5f8061176783612027565b9150827f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47838409149050915091565b5f610449600283612af7565b5f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478083840991508083830981838209828283098385830984848309858484098684850997508684840987858409945087898a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087838a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985050868889099750868889099750868889099750868889099750868889099750868889099750868489099750868889099750868889099750868889099750868889099750868889099750868989099750868889099750868889099750868889099750868889099750868889099750868889099750868989099750868889099750868889099750868889099750868889099750868889099750868689099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868189099750508587880996508587880996508587880996508585880996508587880996508587880996508587880996508585880996508587880996508587880996508587880996508587880996508587880996508587880996508587880996508587880996508583880996508587880996508587880996508587880996508587880996508581880996505050838586099450838586099450838586099450838586099450838186099450508284850993508284850993508284850993508281850993508284850993508284850993508285850993508284850993508284850993508284850993508284850993508284850993508284850993508281850995945050505050565b6040805160c080825260e082019092525f918291906020820181803683370190505060208082018181526040830182905260608301829052608083018690527f183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea360a08401527f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4760c0808501919091529293505f92839160055afa90505f51925080612020576040517fc6daf7ab000000000000000000000000000000000000000000000000000000008152600481018590527f183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea360248201527f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd476044820152606401610652565b5050919050565b5f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478083840991508083830981838209828283098385830984848309858484098684850997508684840987858409945087898a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087838a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985050868889099750868889099750868889099750868889099750868889099750868889099750868489099750868889099750868889099750868889099750868889099750868889099750868989099750868889099750868889099750868889099750868889099750868889099750868889099750868989099750868889099750868889099750868889099750868889099750868889099750868689099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868189099750508587880996508587880996508587880996508585880996508587880996508587880996508587880996508585880996508587880996508587880996508587880996508587880996508587880996508587880996508587880996508587880996508583880996508587880996508587880996508587880996508587880996508581880996508587880996508587880996508587880996508587880996508583880996508587880996508587880996508587880996508584880996508587880996508587880996508587880996508587880996508587880996508581880996505050505050808283099392505050565b60405180604001604052806002906020820280368337509192915050565b60405180608001604052806004906020820280368337509192915050565b60405180602001604052806001906020820280368337509192915050565b5f80604083850312156127d8575f80fd5b50508035926020909101359150565b803567ffffffffffffffff81168114610b0e575f80fd5b5f6020828403121561280e575f80fd5b6109e7826127e7565b5f60208284031215612827575f80fd5b5035919050565b602081525f82518060208401528060208501604085015e5f6040828501015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011684010191505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f80606083850312156128bf575f80fd5b6128c8836127e7565b915083603f8401126128d8575f80fd5b6040516040810181811067ffffffffffffffff821117156128fb576128fb612881565b60405280606085018681111561290f575f80fd5b602086015b8181101561292c578035835260209283019201612914565b505050809150509250929050565b5f6020828403121561294a575f80fd5b813573ffffffffffffffffffffffffffffffffffffffff811681146109e7575f80fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b818103818111156104495761044961296d565b805f5b60028110156129cf5781518452602093840193909101906001016129b0565b50505050565b610100810181855f5b60048110156129fd5781548352602090920191600191820191016129de565b505050612a0d60808301856129ad565b612a1a60c08301846129ad565b949350505050565b6040810161044982846129ad565b80820281158282048414176104495761044961296d565b808201808211156104495761044961296d565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b5f82612a9557612a95612a5a565b500490565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b6080810181835f5b6004811015612aee578151835260209283019290910190600101612acf565b50505092915050565b5f82612b0557612b05612a5a565b500690565b5f81518060208401855e5f93019283525090919050565b5f612b35612b2f838b612b0a565b89612b0a565b7fff000000000000000000000000000000000000000000000000000000000000008860f81b1681527fff000000000000000000000000000000000000000000000000000000000000008760f81b1660018201527fff000000000000000000000000000000000000000000000000000000000000008660f81b166002820152612bc06003820186612b0a565b60f89490941b7fff0000000000000000000000000000000000000000000000000000000000000016845250506001909101979650505050505050565b8481527fff000000000000000000000000000000000000000000000000000000000000008460f81b1660208201525f612c386021830185612b0a565b60f89390931b7fff000000000000000000000000000000000000000000000000000000000000001683525050600101939250505056fe424c535f5349475f424e32353447315f584d443a4b454343414b2d3235365f535644575f524f5f4e554c5fa26469706673582212202fd9bb390e4afffd96a5eac36eeaa90e7558beccfde421e09e7c70674504abca64736f6c634300081a003330644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd470557ec32c2ad488e4d4f6008f89a346f18492092ccc0d594610de2732c8b808f07e1d1d335df83fa98462005690372c643340060d205306a9aa8106b6bd0b382297d3a4f9749b33eb2d904c9d9ebf17224150ddd7abd7567a9bec6c74480ee0b0095685ae3a85ba243747b1b2f426049010f6b73a0cf1d389351d5aaaa1047f60000000000000000000000000000000000000000000000000000000066f7e13300000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000067600c240000000000000000000000000000000000000000000000000000000000000001", + "input": "0x610100604052348015610010575f80fd5b5060405161371b38038061371b83398101604081905261002f91610344565b848484338061005857604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b610061816100b4565b5061006b83610103565b61008a578260405163c7728ee960e01b815260040161004f91906103dc565b61009760038460046102de565b5060809190915260a0525060c09190915260e0525061040c915050565b5f80546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b80515f905f805160206136fb833981519152111580610133575060208201515f805160206136fb83398151915211155b8061016a575060408201515f805160206136fb83398151915211158061016a575060608201515f805160206136fb83398151915211155b1561017657505f919050565b61017f82610185565b92915050565b5f815160208301515f805160206136fb8339815191528283095f805160206136fb83398151915282830981828301015f805160206136fb83398151915282838401085f805160206136fb83398151915286825f805160206136fb833981519152038601099350505f805160206136fb83398151915284835f805160206136fb8339815191520383010991505f805160206136fb8339815191527f2b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5840894505f805160206136fb8339815191527e9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d28308935060408701519250606087015191505f805160206136fb83398151915280835f805160206136fb8339815191520385085f805160206136fb8339815191528486080990505f805160206136fb833981519152828460011b0994149290931491909116949350505050565b826004810192821561030c579160200282015b8281111561030c5782518255916020019190600101906102f1565b5061031892915061031c565b5090565b5b80821115610318575f815560010161031d565b634e487b7160e01b5f52604160045260245ffd5b5f805f805f6101008688031215610359575f80fd5b86601f870112610367575f80fd5b604051608081016001600160401b038111828210171561038957610389610330565b60405280608088018981111561039d575f80fd5b885b818110156103b757805183526020928301920161039f565b505160a089015160c08a015160e0909a0151939b919a50989750919550909350505050565b6080810181835f5b60048110156104035781518352602092830192909101906001016103e4565b50505092915050565b60805160a05160c05160e05161327d61047e5f395f81816102f00152818161116101526111c201525f81816103170152818161119b01526111e601525f818161039d01528181610c3a01528181610cc701528181611226015261125101525f81816102170152610ceb015261327d5ff3fe608060405234801561000f575f80fd5b506004361061018f575f3560e01c8063b88e89ff116100dd578063f2fde38b11610088578063f61ff62311610063578063f61ff623146103bf578063fb6a3ffe146103d2578063fd0a5aaf146103e5575f80fd5b8063f2fde38b14610372578063f398a6db14610385578063f434e1fe14610398575f80fd5b8063d4d2ef08116100b8578063d4d2ef0814610339578063dc0998aa1461034c578063f14ff4ef1461035f575f80fd5b8063b88e89ff146102d8578063c0550911146102eb578063c7321b9114610312575f80fd5b80636d1ef3281161013d5780638285d889116101185780638285d889146102755780638da5cb5b1461027d5780638de0a1d7146102a4575f80fd5b80636d1ef328146102395780636fe318c11461024e578063715018a61461026d575f80fd5b80635f7c75221161016d5780635f7c7522146101ea5780636037d218146101ff5780636714c79f14610212575f80fd5b80633d55a583146101935780634a8ae631146101cf5780635ec01e4d146101e2575b5f80fd5b6101bc6101a1366004612cd9565b67ffffffffffffffff165f9081526007602052604090205490565b6040519081526020015b60405180910390f35b6101bc6101dd366004612cf2565b6103ed565b6101bc610407565b6101f261049a565b6040516101c69190612d55565b6101bc61020d366004612cd9565b6104b6565b6101bc7f000000000000000000000000000000000000000000000000000000000000000081565b61024c610247366004612d94565b610534565b005b6101bc61025c366004612cd9565b60076020525f908152604090205481565b61024c6106e6565b6101bc600281565b5f5460405173ffffffffffffffffffffffffffffffffffffffff90911681526020016101c6565b6102b76102b2366004612e3f565b610755565b6040516fffffffffffffffffffffffffffffffff90911681526020016101c6565b61024c6102e6366004612e58565b6107d5565b6101bc7f000000000000000000000000000000000000000000000000000000000000000081565b6101bc7f000000000000000000000000000000000000000000000000000000000000000081565b61024c610347366004612e80565b610943565b6101bc61035a366004612cf2565b610b42565b6101bc61036d366004612cf2565b610b58565b61024c610380366004612eb1565b610b65565b6101bc610393366004612cf2565b610c1f565b6101bc7f000000000000000000000000000000000000000000000000000000000000000081565b6101bc6103cd366004612cf2565b610c34565b6102b76103e0366004612e3f565b610c88565b6101bc600a81565b5f6104016103fc600284612f11565b610cc3565b92915050565b5f8061042e610417600242612f11565b6fffffffffffffffffffffffffffffffff16610cc3565b90505f61043a43610755565b9050818160405160200161047d92919091825260801b7fffffffffffffffffffffffffffffffff0000000000000000000000000000000016602082015260300190565b604051602081830303815290604052805190602001209250505090565b6040518060600160405280602b815260200161321d602b913981565b67ffffffffffffffff81165f908152600760205260408120548103610518576040517fc3f4d68a00000000000000000000000000000000000000000000000000000000815267ffffffffffffffff831660048201526024015b60405180910390fd5b5067ffffffffffffffff165f9081526007602052604090205490565b6040805160208082528183019092525f91602082018180368337019050509050825f526008601820808260200152505f6105866040518060600160405280602b815260200161321d602b913983610d2c565b905061059183610de1565b6105ce57600381846040517fb8cf7d3000000000000000000000000000000000000000000000000000000000815260040161050f93929190612f4c565b6040805160808101918290525f9161060991869160039060049082845b8154815260200190600101908083116105eb57505050505084610e51565b5090508061064a57600382856040517fb8cf7d3000000000000000000000000000000000000000000000000000000000815260040161050f93929190612f4c565b5f8460405160200161065c9190612f99565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0018152828252805160209182012067ffffffffffffffff8a165f8181526007845293909320819055808452935090917fb262efe8f3fb8d5cb448e8771d8d93fe981dfa54d2e292afe1b4970dc9237134910160405180910390a2505050505050565b6107426040518060400160405280600581526020017f4f776e657200000000000000000000000000000000000000000000000000000081525061073d5f5473ffffffffffffffffffffffffffffffffffffffff1690565b611003565b61074a611098565b6107535f6110ea565b565b6001545f906fffffffffffffffffffffffffffffffff83811670010000000000000000000000000000000090920416146107bb576040517f481b130600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b50506001546fffffffffffffffffffffffffffffffff1690565b61082c6040518060400160405280600581526020017f4f776e657200000000000000000000000000000000000000000000000000000081525061073d5f5473ffffffffffffffffffffffffffffffffffffffff1690565b610834611098565b6fffffffffffffffffffffffffffffffff8216610852600a43612fa7565b111561088a576040517f4c8e490f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6fffffffffffffffffffffffffffffffff82165f90815260026020526040902054156108e2576040517f145718a700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6fffffffffffffffffffffffffffffffff82165f8181526002602052604090819020839055517faa3a15b04592d6ea18ab9cea093bbfd9118447c3577d10576b53a224f43ddf36906109379084815260200190565b60405180910390a25050565b61099a6040518060400160405280600581526020017f4f776e657200000000000000000000000000000000000000000000000000000081525061073d5f5473ffffffffffffffffffffffffffffffffffffffff1690565b6109a2611098565b6fffffffffffffffffffffffffffffffff82165f90815260026020526040812054908190036109fd576040517fc6c155d800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b826fffffffffffffffffffffffffffffffff164314610a48576040517f7fc88be700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040517fffffffffffffffffffffffffffffffff00000000000000000000000000000000608084901b166020820152603001604051602081830303815290604052805190602001208114610ac8576040517f9ea6d12700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6fffffffffffffffffffffffffffffffff82811670010000000000000000000000000000000091851691820281176001555f8281526002602090815260408083209290925590519182527fc6e31d2ba6af5e9b718ccc7734830ef3e0ff568a7698c326b88e6097f17e6bc6910160405180910390a2505050565b60038160048110610b51575f80fd5b0154905081565b5f6104016103fc8361115e565b610bbc6040518060400160405280600581526020017f4f776e657200000000000000000000000000000000000000000000000000000081525061073d5f5473ffffffffffffffffffffffffffffffffffffffff1690565b610bc4611098565b73ffffffffffffffffffffffffffffffffffffffff8116610c13576040517f1e4fbdf70000000000000000000000000000000000000000000000000000000081525f600482015260240161050f565b610c1c816110ea565b50565b5f610401610c2f6103cd8461115e565b6111bf565b5f610c607f00000000000000000000000000000000000000000000000000000000000000006002612fba565b6002610c74610c6f8286612f11565b611220565b610c7e9190612fa7565b6104019190612fa7565b6001545f906fffffffffffffffffffffffffffffffff83811670010000000000000000000000000000000090920416146107bb57505f919050565b5f807f0000000000000000000000000000000000000000000000000000000000000000610d107f000000000000000000000000000000000000000000000000000000000000000085612f11565b610d1a9190612ffe565b9050610d25816104b6565b9392505050565b610d34612c68565b5f610d3f848461127f565b90505f610d5282825b60200201516113ca565b90505f610d60836001610d48565b9050610d6a612c86565b825181526020808401518282015282516040808401919091529083015160608301525f908460808460066107d05a03fa905080610dd557816040517f128e3f0800000000000000000000000000000000000000000000000000000000815260040161050f919061303e565b50919695505050505050565b80515f907f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47111580610e37575060208201517f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4711155b15610e4357505f919050565b61040182611872565b919050565b5f805f604051806101800160405280875f60028110610e7257610e72613011565b6020020151815260200187600160028110610e8f57610e8f613011565b602002015181526020017f198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c281526020017f1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed81526020017f275dc4a288d1afb3cbb1ac09187524c7db36395df7be3b99e673b13a075a65ec81526020017f1d9befcd05a5323e6da4d435f3b617cdb3af83285c2df711ef39c01571827f9d8152602001855f60028110610f4357610f43613011565b6020020151815260200185600160028110610f6057610f60613011565b6020020151815260200186600160048110610f7d57610f7d613011565b60200201518152602001865f60048110610f9957610f99613011565b6020020151815260200186600360048110610fb657610fb6613011565b6020020151815260200186600260048110610fd357610fd3613011565b602002015190529050610fe4612ca4565b6020816101808460086107d05a03fa9051151597909650945050505050565b611094828260405160240161101992919061306e565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f319af33300000000000000000000000000000000000000000000000000000000179052611918565b5050565b5f5473ffffffffffffffffffffffffffffffffffffffff163314610753576040517f118cdaa700000000000000000000000000000000000000000000000000000000815233600482015260240161050f565b5f805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b5f7f000000000000000000000000000000000000000000000000000000000000000061118b600184612f11565b6111959190612fba565b610401907f0000000000000000000000000000000000000000000000000000000000000000612fa7565b5f7f000000000000000000000000000000000000000000000000000000000000000061120b7f000000000000000000000000000000000000000000000000000000000000000084612f11565b6112159190612ffe565b610401906001612fa7565b5f61124b7f0000000000000000000000000000000000000000000000000000000000000000836130a5565b611275907f0000000000000000000000000000000000000000000000000000000000000000612f11565b6104019083612fa7565b611287612c68565b5f6112928484611921565b90505f805f806018850177ffffffffffffffffffffffffffffffffffffffffffffffff815116935060308601905077ffffffffffffffffffffffffffffffffffffffffffffffff81511694507f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47857f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4778010000000000000000000000000000000000000000000000008709086048870151606088015177ffffffffffffffffffffffffffffffffffffffffffffffff908116975016945092507f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47905084817801000000000000000000000000000000000000000000000000860908604080518082019091529283526020830152509695505050505050565b6113d2612c68565b7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47821061142e576040517fd53e94150000000000000000000000000000000000000000000000000000000081526004810183905260240161050f565b5f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4760047f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478586090990505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478260010890507f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd476114ec837f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47612f11565b60010891505f61151e7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47838509611b0b565b90505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4778016789af3a83522eb353c98fc6b36d713d5d8d1cc5dffffffa7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47847f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47888b09090990505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd476115f2837f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47612f11565b7f183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea30890505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47837f183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea30890505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4786870990505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4786830990505f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47807f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478485097f10216f7ba065e00de81ac1e7808072c9dd2b2385cd7b438469602eb24829a9bd0960010890505f8061172c61172788611b15565b611b87565b5f0b60010361179557868c5261174187611b15565b905061174c81611c37565b60208e0191909152915081611790576040517f396ec7710000000000000000000000000000000000000000000000000000000081526004810182905260240161050f565b611811565b6117a161172787611b15565b5f0b6001036117b657858c5261174186611b15565b828c526117c283611b15565b90506117cd81611c37565b60208e0191909152915081611811576040517f396ec7710000000000000000000000000000000000000000000000000000000081526004810182905260240161050f565b60208c015161181f90611c71565b6118288e611c71565b146118625760208c015161185c907f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47612f11565b60208d01525b5050505050505050505050919050565b5f815160208301517f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478283097f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4783820990507f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd476003820890507f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4782830914949350505050565b610c1c81611c7d565b815160609060ff81111561196357836040517f26e4f9ba00000000000000000000000000000000000000000000000000000000815260040161050f9190612d55565b60408051608880825260c082019092525f916020820181803683370190505090505f81855f60605f8a886040516020016119a397969594939291906130cf565b60405160208183030381529060405290505f818051906020012090505f81600189876040516020016119d894939291906131aa565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001815282825280516020820120606080855260808501909352909350915f91602082018180368337019050509050600360015b81811015611acf57858418611a49826001612fa7565b8d8b604051602001611a5e94939291906131aa565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001815291905260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff830181028501810195909552805194810194909420939450600101611a33565b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff016020908102820101919091529550505050505092915050565b5f61040182611c9d565b5f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4760037f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47847f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47868709090892915050565b5f80611b92836123f5565b9050611bbf60017f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47612f11565b8103611bed57507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff92915050565b8015801590611bfd575080600114155b15610401576040517f396ec7710000000000000000000000000000000000000000000000000000000081526004810184905260240161050f565b5f80611c4283612522565b9150827f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47838409149050915091565b5f6104016002836130a5565b80516a636f6e736f6c652e6c6f67602083015f808483855afa5050505050565b5f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478083840991508083830981838209828283098385830984848309858484098684850997508684840987858409945087898a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087838a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985050868889099750868889099750868889099750868889099750868889099750868889099750868489099750868889099750868889099750868889099750868889099750868889099750868989099750868889099750868889099750868889099750868889099750868889099750868889099750868989099750868889099750868889099750868889099750868889099750868889099750868689099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868189099750508587880996508587880996508587880996508585880996508587880996508587880996508587880996508585880996508587880996508587880996508587880996508587880996508587880996508587880996508587880996508587880996508583880996508587880996508587880996508587880996508587880996508581880996505050838586099450838586099450838586099450838586099450838186099450508284850993508284850993508284850993508281850993508284850993508284850993508285850993508284850993508284850993508284850993508284850993508284850993508284850993508281850995945050505050565b6040805160c080825260e082019092525f918291906020820181803683370190505060208082018181526040830182905260608301829052608083018690527f183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea360a08401527f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4760c0808501919091529293505f92839160055afa90505f5192508061251b576040517fc6daf7ab000000000000000000000000000000000000000000000000000000008152600481018590527f183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea360248201527f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47604482015260640161050f565b5050919050565b5f7f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478083840991508083830981838209828283098385830984848309858484098684850997508684840987858409945087898a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087878a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a09985087898a09985087898a09985087898a09985087838a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087828a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087848a09985087898a09985087898a09985087898a09985087898a09985087898a09985087868a09985087898a09985087898a099850878a8a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087898a09985087818a09985050868889099750868889099750868889099750868889099750868889099750868889099750868489099750868889099750868889099750868889099750868889099750868889099750868989099750868889099750868889099750868889099750868889099750868889099750868889099750868989099750868889099750868889099750868889099750868889099750868889099750868689099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868889099750868189099750508587880996508587880996508587880996508585880996508587880996508587880996508587880996508585880996508587880996508587880996508587880996508587880996508587880996508587880996508587880996508587880996508583880996508587880996508587880996508587880996508587880996508581880996508587880996508587880996508587880996508587880996508583880996508587880996508587880996508587880996508584880996508587880996508587880996508587880996508587880996508587880996508581880996505050505050808283099392505050565b60405180604001604052806002906020820280368337509192915050565b60405180608001604052806004906020820280368337509192915050565b60405180602001604052806001906020820280368337509192915050565b803567ffffffffffffffff81168114610e4c575f80fd5b5f60208284031215612ce9575f80fd5b610d2582612cc2565b5f60208284031215612d02575f80fd5b5035919050565b5f81518084528060208401602086015e5f6020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b602081525f610d256020830184612d09565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f8060608385031215612da5575f80fd5b612dae83612cc2565b915083603f840112612dbe575f80fd5b6040516040810181811067ffffffffffffffff82111715612de157612de1612d67565b604052806060850186811115612df5575f80fd5b602086015b81811015612e12578035835260209283019201612dfa565b505050809150509250929050565b80356fffffffffffffffffffffffffffffffff81168114610e4c575f80fd5b5f60208284031215612e4f575f80fd5b610d2582612e20565b5f8060408385031215612e69575f80fd5b612e7283612e20565b946020939093013593505050565b5f8060408385031215612e91575f80fd5b612e9a83612e20565b9150612ea860208401612e20565b90509250929050565b5f60208284031215612ec1575f80fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610d25575f80fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b8181038181111561040157610401612ee4565b805f5b6002811015612f46578151845260209384019390910190600101612f27565b50505050565b610100810181855f5b6004811015612f74578154835260209092019160019182019101612f55565b505050612f846080830185612f24565b612f9160c0830184612f24565b949350505050565b604081016104018284612f24565b8082018082111561040157610401612ee4565b808202811582820484141761040157610401612ee4565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b5f8261300c5761300c612fd1565b500490565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b6080810181835f5b6004811015613065578151835260209283019290910190600101613046565b50505092915050565b604081525f6130806040830185612d09565b905073ffffffffffffffffffffffffffffffffffffffff831660208301529392505050565b5f826130b3576130b3612fd1565b500690565b5f81518060208401855e5f93019283525090919050565b5f6130e36130dd838b6130b8565b896130b8565b7fff000000000000000000000000000000000000000000000000000000000000008860f81b1681527fff000000000000000000000000000000000000000000000000000000000000008760f81b1660018201527fff000000000000000000000000000000000000000000000000000000000000008660f81b16600282015261316e60038201866130b8565b60f89490941b7fff0000000000000000000000000000000000000000000000000000000000000016845250506001909101979650505050505050565b8481527fff000000000000000000000000000000000000000000000000000000000000008460f81b1660208201525f6131e660218301856130b8565b60f89390931b7fff000000000000000000000000000000000000000000000000000000000000001683525050600101939250505056fe424c535f5349475f424e32353447315f584d443a4b454343414b2d3235365f535644575f524f5f4e554c5fa26469706673582212202332aebe267791e2d29cfba2111b658f8544a46c7e411b29f33dbb979a42897464736f6c634300081a003330644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd470557ec32c2ad488e4d4f6008f89a346f18492092ccc0d594610de2732c8b808f07e1d1d335df83fa98462005690372c643340060d205306a9aa8106b6bd0b382297d3a4f9749b33eb2d904c9d9ebf17224150ddd7abd7567a9bec6c74480ee0b0095685ae3a85ba243747b1b2f426049010f6b73a0cf1d389351d5aaaa1047f60000000000000000000000000000000000000000000000000000000066f7e1330000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000006765c5530000000000000000000000000000000000000000000000000000000000000002", "nonce": "0x0", "chainId": "0x7a69" }, @@ -28,7 +22,7 @@ "receipts": [ { "status": "0x1", - "cumulativeGasUsed": "0x29e6d0", + "cumulativeGasUsed": "0x2d43c0", "logs": [ { "address": "0x51745e910fad45a6ca620bcc7a7fab7683b142e5", @@ -38,10 +32,10 @@ "0x0000000000000000000000004e59b44847b379578588920ca78fbf26c0b4956c" ], "data": "0x", - "blockHash": "0xb71a399092177ae884f9f133b0610abab56396c27f5fb93a10cae78cdcb4912c", - "blockNumber": "0x6", - "blockTimestamp": "0x67600d77", - "transactionHash": "0xf14e183e774da1688029fd42690e1db562182e2d9594c1482e8f2e6a164d33f7", + "blockHash": "0x0d1f440dfde6ad618e89324fb3311cb018832e055edaee54f5714ee042580249", + "blockNumber": "0x5", + "blockTimestamp": "0x6765e0f7", + "transactionHash": "0xb5cab6a434e8003aa94b7f7d72dc50e3f63a563263a56d9075dc3210e52b9d8f", "transactionIndex": "0x0", "logIndex": "0x0", "removed": false @@ -49,23 +43,23 @@ ], "logsBloom": "0x00000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004001000000000000000000000000000000000000020000000000000000000800000000000000000000000000000000400000000000000000200000000000000000000000001000000000000000000000000000000001000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000080000000000000000000000000000000", "type": "0x2", - "transactionHash": "0xf14e183e774da1688029fd42690e1db562182e2d9594c1482e8f2e6a164d33f7", + "transactionHash": "0xb5cab6a434e8003aa94b7f7d72dc50e3f63a563263a56d9075dc3210e52b9d8f", "transactionIndex": "0x0", - "blockHash": "0xb71a399092177ae884f9f133b0610abab56396c27f5fb93a10cae78cdcb4912c", - "blockNumber": "0x6", - "gasUsed": "0x29e6d0", - "effectiveGasPrice": "0x1e925e89", + "blockHash": "0x0d1f440dfde6ad618e89324fb3311cb018832e055edaee54f5714ee042580249", + "blockNumber": "0x5", + "gasUsed": "0x2d43c0", + "effectiveGasPrice": "0x22f06c0a", "blobGasPrice": "0x1", "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "to": null, "contractAddress": "0x5fbdb2315678afecb367f032d93f642f64180aa3", - "root": "0xffcf090a4e06a43746a3cfc827f38a459922276c52e6bf8b924dda5221b7a345" + "root": "0x0c5ae1271f757bada2344c0083dfe30a9c780ec6b957a986f7ec123b28dd5ed5" } ], "libraries": [], "pending": [], "returns": {}, - "timestamp": 1734348151, + "timestamp": 1734729975, "chain": 31337, - "commit": "79f7e6eb" + "commit": "123801b7" } \ No newline at end of file diff --git a/contracts/src/Randomness/RandomCommitment.sol b/contracts/src/Randomness/RandomCommitment.sol deleted file mode 100644 index 9efeb90d2a..0000000000 --- a/contracts/src/Randomness/RandomCommitment.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause-Clear -pragma solidity ^0.8.20; - -import {Ownable} from "openzeppelin/access/Ownable.sol"; - -contract RandomCommitment is Ownable { - uint256 public constant PRECOMMIT_DELAY = 10; - - uint256 private currentRevealedValue; - uint256 private currentRevealBlockNumber; - mapping(uint256 blockNumber => bytes32) private commitments; - - event CommitmentPosted(uint256 indexed blockNumber, bytes32 commitment); - event ValueRevealed(uint256 indexed blockNumber, uint256 revealedValue); - - error CommitmentTooLate(); - error CommitmentAlreadyExists(); - error NoCommitmentFound(); - error RevealMustBeOnExactBlock(); - error InvalidReveal(); - error RevealedValueNotAvailable(); - - constructor() Ownable(msg.sender) {} - - function postCommitment(uint256 blockNumber, bytes32 commitmentHash) external onlyOwner { - if (block.number > blockNumber - PRECOMMIT_DELAY) { - revert CommitmentTooLate(); - } - - if (commitments[blockNumber] != 0) { - revert CommitmentAlreadyExists(); - } - - commitments[blockNumber] = commitmentHash; - emit CommitmentPosted(blockNumber, commitmentHash); - } - - function revealValue(uint256 blockNumber, uint256 revealedValue) external onlyOwner { - bytes32 storedCommitment = commitments[blockNumber]; - - if (storedCommitment == 0) { - revert NoCommitmentFound(); - } - - if (block.number != blockNumber) { - revert RevealMustBeOnExactBlock(); - } - - if (storedCommitment != keccak256(abi.encodePacked(revealedValue))) { - revert InvalidReveal(); - } - - currentRevealedValue = revealedValue; - currentRevealBlockNumber = blockNumber; - delete commitments[blockNumber]; - emit ValueRevealed(blockNumber, revealedValue); - } - - function unsafeGetRevealedValue(uint256 blockNumber) public view returns (uint256) { - if (currentRevealBlockNumber != blockNumber) { - return 0; - } - - return currentRevealedValue; - } - - function getRevealedValue(uint256 blockNumber) public view returns (uint256) { - if (currentRevealBlockNumber != blockNumber) { - revert RevealedValueNotAvailable(); - } - - return currentRevealedValue; - } -} diff --git a/contracts/src/Randomness/Drand.sol b/contracts/src/randomness/Drand.sol similarity index 100% rename from contracts/src/Randomness/Drand.sol rename to contracts/src/randomness/Drand.sol diff --git a/contracts/src/Randomness/Random.sol b/contracts/src/randomness/Random.sol similarity index 92% rename from contracts/src/Randomness/Random.sol rename to contracts/src/randomness/Random.sol index 7ab23d5a13..a5ff839c07 100644 --- a/contracts/src/Randomness/Random.sol +++ b/contracts/src/randomness/Random.sol @@ -14,7 +14,7 @@ contract Random is RandomCommitment, Drand { function random() external view returns (bytes32) { bytes32 drand = _getDrandAtTimestamp(block.timestamp - DRAND_DELAY); - uint256 revealedValue = getRevealedValue(block.number); + uint256 revealedValue = getRevealedValue(uint128(block.number)); return keccak256(abi.encodePacked(drand, revealedValue)); } diff --git a/contracts/src/randomness/RandomCommitment.sol b/contracts/src/randomness/RandomCommitment.sol new file mode 100644 index 0000000000..cb3950d70f --- /dev/null +++ b/contracts/src/randomness/RandomCommitment.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear +pragma solidity ^0.8.20; + +import {Ownable} from "openzeppelin/access/Ownable.sol"; + +contract RandomCommitment is Ownable { + struct CurrentReveal { + uint128 value; + uint128 blockNumber; + } + + /** + * @dev This delay ensures that the commitment is not submitted too late, + * maintaining the unpredictability of the randomness. It should be set to + * at least 12 hours to accommodate the sequencing window size. + * For more details, please refer to: + * https://specs.optimism.io/protocol/configurability.html#sequencing-window-size + */ + uint256 public immutable PRECOMMIT_DELAY = 21600; + + CurrentReveal private currentReveal; + mapping(uint128 blockNumber => bytes32) private commitments; + + event CommitmentPosted(uint128 indexed blockNumber, bytes32 commitment); + event ValueRevealed(uint128 indexed blockNumber, uint128 revealedValue); + + error CommitmentTooLate(); + error CommitmentAlreadyExists(); + error NoCommitmentFound(); + error RevealMustBeOnExactBlock(); + error InvalidReveal(); + error RevealedValueNotAvailable(); + + constructor() Ownable(msg.sender) {} + + /** + * @notice Posts a commitment for a specific block number. + */ + function postCommitment(uint128 blockNumber, bytes32 commitmentHash) external onlyOwner { + if (block.number + PRECOMMIT_DELAY > blockNumber) { + revert CommitmentTooLate(); + } + + if (commitments[blockNumber] != 0) { + revert CommitmentAlreadyExists(); + } + + commitments[blockNumber] = commitmentHash; + emit CommitmentPosted(blockNumber, commitmentHash); + } + + /** + * @notice Reveals the value for a specific block number. + */ + function revealValue(uint128 blockNumber, uint128 revealedValue) external onlyOwner { + bytes32 storedCommitment = commitments[blockNumber]; + + if (storedCommitment == 0) { + revert NoCommitmentFound(); + } + + if (block.number != blockNumber) { + revert RevealMustBeOnExactBlock(); + } + + if (storedCommitment != keccak256(abi.encodePacked(revealedValue))) { + revert InvalidReveal(); + } + + currentReveal.value = revealedValue; + currentReveal.blockNumber = blockNumber; + delete commitments[blockNumber]; + emit ValueRevealed(blockNumber, revealedValue); + } + + /** + * @notice Retrieves the revealed value for a specific block number. + * This function does not revert if the reveal is unavailable. Instead, it returns 0. + */ + function unsafeGetRevealedValue(uint128 blockNumber) public view returns (uint128) { + if (currentReveal.blockNumber != blockNumber) { + return 0; + } + + return currentReveal.value; + } + + /** + * @notice Retrieves the revealed value for a specific block number. + * This function verifies that a reveal exists for the given block number before returning the value. + * It reverts with `RevealedValueNotAvailable` if no valid reveal is found. + */ + function getRevealedValue(uint128 blockNumber) public view returns (uint128) { + if (currentReveal.blockNumber != blockNumber) { + revert RevealedValueNotAvailable(); + } + + return currentReveal.value; + } +} diff --git a/packages/txm/lib/Transaction.ts b/packages/txm/lib/Transaction.ts index 379b5bbf71..d99bed5a02 100644 --- a/packages/txm/lib/Transaction.ts +++ b/packages/txm/lib/Transaction.ts @@ -23,7 +23,7 @@ export enum TransactionStatus { */ Cancelling = "Cancelling", /** - * The transaction has expired, and we are attempting to cancel it to save gas, preventing it from being included on-chain and potentially reverting or executing actions that are no longer relevant. + * The transaction has expired, and we cancelled it to save gas, preventing it from being included on-chain and potentially reverting or executing actions that are no longer relevant. */ Cancelled = "Cancelled", /** diff --git a/support/common/lib/index.ts b/support/common/lib/index.ts index a318bf8c2f..9c9af353a3 100644 --- a/support/common/lib/index.ts +++ b/support/common/lib/index.ts @@ -36,7 +36,9 @@ export { debounce } from "./utils/debounce" export { throttle } from "./utils/throttle" -export { promiseWithResolvers, sleep } from "./utils/promises" +export { promiseWithResolvers } from "./utils/promises" + +export { sleep } from "./utils/sleep" export type { PromiseWithResolvers, ResolveInputType, ResolveType, RejectType } from "./utils/promises" @@ -62,6 +64,8 @@ export { Map2 } from "./collections/map2" export { FIFOCache } from "./collections/fifo-cache" +export { fetchWithRetry } from "./utils/fetch" + // === DATA ======================================================================================== export { injectedProviderInfo, happyProviderInfo } from "./data/providers" diff --git a/support/common/lib/utils/fetch.ts b/support/common/lib/utils/fetch.ts new file mode 100644 index 0000000000..fe15cb515e --- /dev/null +++ b/support/common/lib/utils/fetch.ts @@ -0,0 +1,34 @@ +/** + * Performs an HTTP request using fetch with retry capability and a timeout for each attempt. + * + * @param url The URL to which the request is sent. + * @param options Options to customize the request (method, headers, body, etc.). + * See https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#fetch_options. + * @param maxRetries The maximum number of retry attempts in case of failure or timeout. + * (Default: 3) + * @param timeout The maximum time (in milliseconds) to wait for each attempt before timing out. + * (Default: 5000) + * + * @returns A promise that resolves with the response of the request (type Response). + * Throws an error if all retry attempts fail or if a timeout occurs. + */ +export async function fetchWithRetry( + url: string, + options: RequestInit = {}, + maxRetries = 3, + timeout = 5000, +): Promise { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const signal = AbortSignal.timeout(timeout) + const response = await fetch(url, { ...options, signal }) + return response + } catch (error) { + if (attempt === maxRetries) { + throw error + } + } + } + + throw new Error("Failed to complete the request.") +} diff --git a/support/common/lib/utils/promises.ts b/support/common/lib/utils/promises.ts index 824ca51655..3ef572e482 100644 --- a/support/common/lib/utils/promises.ts +++ b/support/common/lib/utils/promises.ts @@ -25,10 +25,3 @@ export function promiseWithResolvers(): PromiseWithResolvers { }) return { promise, resolve: resolve!, reject: reject! } } - -/** - * Returns a promise that resolves after a given number of milliseconds. - */ -export function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) -} diff --git a/support/common/lib/utils/sleep.ts b/support/common/lib/utils/sleep.ts new file mode 100644 index 0000000000..ca34f06055 --- /dev/null +++ b/support/common/lib/utils/sleep.ts @@ -0,0 +1,6 @@ +/** + * Returns a promise that resolves after a given number of milliseconds. + */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +}