From 9b317c4ae4582e6e09e7482bcb159cf913f3c144 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Sun, 30 Jul 2023 22:36:03 +0200 Subject: [PATCH 1/6] Query IsIncentivized --- drand_contract.ts | 15 ++++++++++++++- loop.ts | 18 ++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/drand_contract.ts b/drand_contract.ts index 70b940c..93a9deb 100644 --- a/drand_contract.ts +++ b/drand_contract.ts @@ -1,6 +1,6 @@ import { MsgExecuteContract } from "npm:cosmjs-types/cosmwasm/wasm/v1/tx.js"; -import { CosmWasmClient, MsgExecuteContractEncodeObject, toUtf8 } from "./deps.ts"; +import { assert, CosmWasmClient, MsgExecuteContractEncodeObject, toUtf8 } from "./deps.ts"; export function makeAddBeaconMessage( senderAddress: string, @@ -33,3 +33,16 @@ export async function queryIsAllowListed( }); return listed; } + +export async function queryIsIncentivized( + client: CosmWasmClient, + contractAddress: string, + rounds: number[], + botAddress: string, +): Promise { + const { incentivized } = await client.queryContractSmart(contractAddress, { + is_incentivized: { rounds, sender: botAddress }, + }); + assert(Array.isArray(incentivized)); + return incentivized; +} diff --git a/loop.ts b/loop.ts index 73701eb..530a678 100644 --- a/loop.ts +++ b/loop.ts @@ -11,7 +11,7 @@ import { SignerData, SigningCosmWasmClient, } from "./deps.ts"; -import { makeAddBeaconMessage } from "./drand_contract.ts"; +import { makeAddBeaconMessage, queryIsIncentivized } from "./drand_contract.ts"; import { ibcPacketsSent } from "./ibc.ts"; interface Capture { @@ -40,12 +40,18 @@ export async function loop( }: Capture, beacon: RandomnessBeacon, ): Promise { - const baseText = `➘ #${beacon.round} received after ${publishedSince(beacon.round)}ms`; - if (!isMyGroup(botAddress, beacon.round)) { - console.log(`${baseText}. Skipping.`); + console.log(`➘ #${beacon.round} received after ${publishedSince(beacon.round)}ms`); + + const isIncentivized = await queryIsIncentivized( + client, + drandAddress, + [beacon.round], + botAddress, + ); + + if (!isIncentivized) { + console.log(`Skipping.`); return false; - } else { - console.log(`${baseText}. Submitting.`); } const broadcastTime = Date.now() / 1000; From bb2ef15ceabbb83e4990f5c09734c8bff6ce356f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Sun, 30 Jul 2023 22:37:06 +0200 Subject: [PATCH 2/6] Remove local eligible logic --- group.ts | 17 ---------------- group_test.ts | 54 +-------------------------------------------------- loop.ts | 1 - 3 files changed, 1 insertion(+), 71 deletions(-) diff --git a/group.ts b/group.ts index 8fd5ef4..49d8b18 100644 --- a/group.ts +++ b/group.ts @@ -5,20 +5,3 @@ export function group(address: string): "A" | "B" { if (hash % 2 == 0) return "A"; else return "B"; } - -/** - * All rounds not ending on 0 are skipped. Rounds divisible by 20 to to group B and the others go to group A. - */ -export function eligibleGroup(round: number): "A" | "B" | null { - if (!round) throw new Error("Round is falsy"); - if (!Number.isInteger(round)) throw new Error("Round value not an Integer"); - - if (round % 10 != 0) return null; - - if (round % 20 == 0) return "B"; - else return "A"; -} - -export function isMyGroup(address: string, round: number): boolean { - return eligibleGroup(round) == group(address); -} diff --git a/group_test.ts b/group_test.ts index f346f87..910d45f 100644 --- a/group_test.ts +++ b/group_test.ts @@ -1,5 +1,5 @@ import { assertEquals } from "https://deno.land/std@0.177.0/testing/asserts.ts"; -import { eligibleGroup, group } from "./group.ts"; +import { group } from "./group.ts"; Deno.test("group works", () => { assertEquals(group("nois1ffy2rz96sjxzm2ezwkmvyeupktp7elt6w3xckt"), "B"); @@ -9,55 +9,3 @@ Deno.test("group works", () => { assertEquals(group("nois1rw47dxvhw3ahdlcznvwpcz43cdq8l0832eg6re"), "A"); assertEquals(group("nois12a8yv4ndgnkygujj7cmmkfz2j9wjanezldwye0"), "B"); }); - -Deno.test("eligibleGroup works", () => { - assertEquals(eligibleGroup(1), null); - assertEquals(eligibleGroup(2), null); - assertEquals(eligibleGroup(3), null); - assertEquals(eligibleGroup(4), null); - assertEquals(eligibleGroup(5), null); - assertEquals(eligibleGroup(6), null); - assertEquals(eligibleGroup(7), null); - assertEquals(eligibleGroup(8), null); - assertEquals(eligibleGroup(9), null); - assertEquals(eligibleGroup(10), "A"); - assertEquals(eligibleGroup(11), null); - assertEquals(eligibleGroup(12), null); - assertEquals(eligibleGroup(13), null); - assertEquals(eligibleGroup(14), null); - assertEquals(eligibleGroup(15), null); - assertEquals(eligibleGroup(16), null); - assertEquals(eligibleGroup(17), null); - assertEquals(eligibleGroup(18), null); - assertEquals(eligibleGroup(19), null); - assertEquals(eligibleGroup(20), "B"); - assertEquals(eligibleGroup(21), null); - assertEquals(eligibleGroup(22), null); - assertEquals(eligibleGroup(23), null); - assertEquals(eligibleGroup(24), null); - assertEquals(eligibleGroup(25), null); - assertEquals(eligibleGroup(26), null); - assertEquals(eligibleGroup(27), null); - assertEquals(eligibleGroup(28), null); - assertEquals(eligibleGroup(29), null); - assertEquals(eligibleGroup(30), "A"); - assertEquals(eligibleGroup(31), null); - - assertEquals(eligibleGroup(111765), null); - assertEquals(eligibleGroup(111766), null); - assertEquals(eligibleGroup(111767), null); - assertEquals(eligibleGroup(111768), null); - assertEquals(eligibleGroup(111769), null); - assertEquals(eligibleGroup(111770), "A"); - assertEquals(eligibleGroup(111771), null); - assertEquals(eligibleGroup(111772), null); - assertEquals(eligibleGroup(111773), null); - assertEquals(eligibleGroup(111774), null); - assertEquals(eligibleGroup(111775), null); - assertEquals(eligibleGroup(111776), null); - assertEquals(eligibleGroup(111777), null); - assertEquals(eligibleGroup(111778), null); - assertEquals(eligibleGroup(111779), null); - assertEquals(eligibleGroup(111780), "B"); - assertEquals(eligibleGroup(111781), null); -}); diff --git a/loop.ts b/loop.ts index 530a678..520b33c 100644 --- a/loop.ts +++ b/loop.ts @@ -1,6 +1,5 @@ import { TxRaw } from "npm:cosmjs-types/cosmos/tx/v1beta1/tx.js"; import { publishedSince, timeOfRound } from "./drand.ts"; -import { isMyGroup } from "./group.ts"; import { assertIsDeliverTxSuccess, calculateFee, From 038c77283f5ffc9a3dcb8e1441444259445b80ff Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Sun, 30 Jul 2023 23:17:28 +0200 Subject: [PATCH 3/6] Let timeOfRound return time in ms --- drand.ts | 6 +++--- loop.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drand.ts b/drand.ts index 518c292..3472e41 100644 --- a/drand.ts +++ b/drand.ts @@ -22,16 +22,16 @@ export const drandUrls = [ const DRAND_GENESIS = 1677685200; const DRAND_ROUND_LENGTH = 3; -// Time of round in seconds. +// Time of round in milliseconds. // // See TimeOfRound implementation: https://github.com/drand/drand/blob/eb36ba81e3f28c966f95bcd602f60e7ff8ef4c35/chain/time.go#L30-L33 export function timeOfRound(round: number): number { - return (DRAND_GENESIS + (round - 1) * DRAND_ROUND_LENGTH); + return (DRAND_GENESIS + (round - 1) * DRAND_ROUND_LENGTH) * 1000; } /** * Time between publishing and now in milliseconds */ export function publishedSince(round: number): number { - return Date.now() - timeOfRound(round) * 1000; + return Date.now() - timeOfRound(round); } diff --git a/loop.ts b/loop.ts index 520b33c..b0a965f 100644 --- a/loop.ts +++ b/loop.ts @@ -105,7 +105,7 @@ export async function loop( console.info( `✔ #${beacon.round} committed (Points: ${points}; Payout: ${payout}; Gas: ${result.gasUsed}/${result.gasWanted}; Jobs processed: ${jobs}; Transaction: ${result.transactionHash})`, ); - const publishTime = timeOfRound(beacon.round); + const publishTime = timeOfRound(beacon.round) / 1000; const { block } = await client.forceGetTmClient().block(result.height); const commitTime = block.header.time.getTime() / 1000; // seconds with fractional part const diff = commitTime - publishTime; From 65bf2cc700b020a347de3ac415bf9511f6ae8344 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Sun, 30 Jul 2023 23:20:09 +0200 Subject: [PATCH 4/6] Fix mutability of nextSignData --- main.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/main.ts b/main.ts index e6b0b09..396f70f 100644 --- a/main.ts +++ b/main.ts @@ -32,7 +32,11 @@ function printableCoin(coin: Coin): string { } } -let nextSignData: SignerData = { +type Mutable = { + -readonly [Key in keyof Type]: Type[Key]; +}; + +let nextSignData: Mutable = { chainId: "", accountNumber: NaN, sequence: NaN, From 2b0437d102e81d9a6866f47adf7c971a429a57ce Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 31 Jul 2023 00:23:02 +0200 Subject: [PATCH 5/6] Call queryIsIncentivized right before beacon comes in --- drand.ts | 7 +++++++ drand_contract.ts | 1 + loop.ts | 20 ++++++++++++-------- main.ts | 21 +++++++++++++++++++-- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/drand.ts b/drand.ts index 3472e41..03a0c53 100644 --- a/drand.ts +++ b/drand.ts @@ -35,3 +35,10 @@ export function timeOfRound(round: number): number { export function publishedSince(round: number): number { return Date.now() - timeOfRound(round); } + +/** + * Time between now and publishing in milliseconds + */ +export function publishedIn(round: number): number { + return -publishedSince(round); +} diff --git a/drand_contract.ts b/drand_contract.ts index 93a9deb..4bbc02b 100644 --- a/drand_contract.ts +++ b/drand_contract.ts @@ -43,6 +43,7 @@ export async function queryIsIncentivized( const { incentivized } = await client.queryContractSmart(contractAddress, { is_incentivized: { rounds, sender: botAddress }, }); + // console.log(`#${rounds[0]} incentivized query returned at ${publishedSince(rounds[0])}ms`) assert(Array.isArray(incentivized)); return incentivized; } diff --git a/loop.ts b/loop.ts index b0a965f..45e129c 100644 --- a/loop.ts +++ b/loop.ts @@ -10,7 +10,7 @@ import { SignerData, SigningCosmWasmClient, } from "./deps.ts"; -import { makeAddBeaconMessage, queryIsIncentivized } from "./drand_contract.ts"; +import { makeAddBeaconMessage } from "./drand_contract.ts"; import { ibcPacketsSent } from "./ibc.ts"; interface Capture { @@ -23,6 +23,7 @@ interface Capture { gasPrice: string; userAgent: string; getNextSignData: () => SignerData; + incentivizedRounds: Map>; } export async function loop( @@ -36,29 +37,32 @@ export async function loop( gasPrice, userAgent, getNextSignData, + incentivizedRounds, }: Capture, beacon: RandomnessBeacon, ): Promise { console.log(`➘ #${beacon.round} received after ${publishedSince(beacon.round)}ms`); - const isIncentivized = await queryIsIncentivized( - client, - drandAddress, - [beacon.round], - botAddress, - ); - + // We don't have evidence that this round is incentivized. This is no guarantee it did not + // get incentivized in the meantime, but we prefer to skip than risk the gas. + const isIncentivized = await incentivizedRounds.get(beacon.round); if (!isIncentivized) { console.log(`Skipping.`); return false; } + // Use this log to ensure awaiting the isIncentivized query does not slow us down. + console.log(`♪ #${beacon.round} ready for signing after ${publishedSince(beacon.round)}ms`); + const broadcastTime = Date.now() / 1000; const msg = makeAddBeaconMessage(botAddress, drandAddress, beacon); const fee = calculateFee(gasLimitAddBeacon, gasPrice); const memo = `Add round: ${beacon.round} (${userAgent})`; const signData = getNextSignData(); // Do this the manual way to save one query const signed = await client.sign(botAddress, [msg], fee, memo, signData); + + // console.log(`♫ #${beacon.round} signed after ${publishedSince(beacon.round)}ms`); + const tx = Uint8Array.from(TxRaw.encode(signed).finish()); const p1 = client.broadcastTx(tx); diff --git a/main.ts b/main.ts index 396f70f..119154d 100644 --- a/main.ts +++ b/main.ts @@ -1,4 +1,4 @@ -import { drandOptions, drandUrls } from "./drand.ts"; +import { drandOptions, drandUrls, publishedIn } from "./drand.ts"; import { group } from "./group.ts"; import { assert, @@ -16,7 +16,7 @@ import { } from "./deps.ts"; import { BeaconCache } from "./cache.ts"; import { loop } from "./loop.ts"; -import { queryIsAllowListed } from "./drand_contract.ts"; +import { queryIsAllowListed, queryIsIncentivized } from "./drand_contract.ts"; // Constants const gasLimitRegister = 200_000; @@ -123,6 +123,8 @@ if (import.meta.main) { // Initialize local sign data await resetSignData(); + const incentivizedRounds = new Map>(); + const fastestNodeClient = new FastestNodeClient(drandUrls, drandOptions); fastestNodeClient.start(); const cache = new BeaconCache(fastestNodeClient, 200 /* 10 min of beacons */); @@ -130,6 +132,20 @@ if (import.meta.main) { for await (const beacon of watch(fastestNodeClient, abortController)) { cache.add(beacon.round, beacon.signature); + setTimeout(() => { + // This is called 100ms after publishing time (might be some ms later) + // From here we have ~300ms until the beacon comes in which should be + // enough for the query to finish. In case the query is not yet done, + // we can wait for the promise to be resolved. + // console.log(`Now : ${new Date().toISOString()}\nPublish time: ${new Date(timeOfRound(round)).toISOString()}`); + const round = beacon.round + 1; + const promise = queryIsIncentivized(client, config.contract, [round], botAddress).then( + (incentivized) => !!incentivized[0], + (_err) => false, + ); + incentivizedRounds.set(round, promise); + }, publishedIn(beacon.round + 1) + 100); + const didSubmit = await loop({ client, broadcaster2, @@ -140,6 +156,7 @@ if (import.meta.main) { botAddress, drandAddress: config.contract, userAgent, + incentivizedRounds, }, beacon); if (didSubmit) { From 5cb419b7f2b5d4555127605d757baafa126c1e7c Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 31 Jul 2023 00:30:33 +0200 Subject: [PATCH 6/6] Add variable names n and m --- main.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/main.ts b/main.ts index 119154d..3e39a3d 100644 --- a/main.ts +++ b/main.ts @@ -130,7 +130,10 @@ if (import.meta.main) { const cache = new BeaconCache(fastestNodeClient, 200 /* 10 min of beacons */); const abortController = new AbortController(); for await (const beacon of watch(fastestNodeClient, abortController)) { - cache.add(beacon.round, beacon.signature); + const n = beacon.round; // n is the round we just received and process now + const m = n + 1; // m := n+1 refers to the next round in this current loop + + cache.add(n, beacon.signature); setTimeout(() => { // This is called 100ms after publishing time (might be some ms later) @@ -138,13 +141,12 @@ if (import.meta.main) { // enough for the query to finish. In case the query is not yet done, // we can wait for the promise to be resolved. // console.log(`Now : ${new Date().toISOString()}\nPublish time: ${new Date(timeOfRound(round)).toISOString()}`); - const round = beacon.round + 1; - const promise = queryIsIncentivized(client, config.contract, [round], botAddress).then( + const promise = queryIsIncentivized(client, config.contract, [m], botAddress).then( (incentivized) => !!incentivized[0], (_err) => false, ); - incentivizedRounds.set(round, promise); - }, publishedIn(beacon.round + 1) + 100); + incentivizedRounds.set(m, promise); + }, publishedIn(m) + 100); const didSubmit = await loop({ client,