Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use timestamps in election creation #343

Merged
merged 7 commits into from
Feb 29, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
"@ethersproject/units": "^5.7.0",
"@ethersproject/wallet": "^5.7.0",
"@size-limit/file": "^8.2.4",
"@vocdoni/proto": "1.15.4",
"@vocdoni/proto": "1.15.5",
"axios": "0.27.2",
"blake2b": "^2.1.4",
"iso-language-codes": "^1.1.0",
Expand Down
6 changes: 3 additions & 3 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ export abstract class API {

private static isVochainError(error: string): never {
switch (true) {
case error.includes('starts at height') && error.includes('current height is'):
case error.includes('starts at') && error.includes('current'):
throw new ErrElectionNotStarted(error);
case error.includes('finished at height') && error.includes('current height is'):
case error.includes('finished at') && error.includes('current'):
throw new ErrElectionFinished(error);
case error.includes('current state: ENDED'):
throw new ErrElectionFinished(error);
default:
throw error;
throw new ErrAPI(error);
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/api/election.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ export abstract class ElectionAPI extends API {
*
* @param {string} url API endpoint URL
* @param {number} maxCensusSize
* @param {number} electionBlocks
* @param {number} electionDuration
* @param {boolean} encryptedVotes
* @param {boolean} anonymousVotes
* @param {number} maxVoteOverwrite
Expand All @@ -520,15 +520,15 @@ export abstract class ElectionAPI extends API {
public static price(
url: string,
maxCensusSize: number,
electionBlocks: number,
electionDuration: number,
encryptedVotes: boolean,
anonymousVotes: boolean,
maxVoteOverwrite: number
): Promise<IElectionCalculatePriceResponse> {
return axios
.post<IElectionCalculatePriceResponse>(url + ElectionAPIMethods.PRICE, {
maxCensusSize,
electionBlocks,
electionDuration,
encryptedVotes,
anonymousVotes,
maxVoteOverwrite,
Expand Down
21 changes: 1 addition & 20 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,26 +561,7 @@ export class VocdoniSDKClient {
key: ElectionCreationSteps.GET_DATA_PIN,
};

const blocks = {
actual: chainData.height,
start: 0,
end: 0,
};
if (election.startDate) {
blocks.start = await this.chainService.dateToBlock(election.startDate);
}
blocks.end = await this.chainService.dateToBlock(election.endDate);
yield {
key: ElectionCreationSteps.ESTIMATE_BLOCK_TIMES,
};

const electionTxData = ElectionCore.generateNewElectionTransaction(
election,
cid,
blocks,
account.address,
account.nonce
);
const electionTxData = ElectionCore.generateNewElectionTransaction(election, cid, account.address, account.nonce);
yield {
key: ElectionCreationSteps.GENERATE_TX,
};
Expand Down
204 changes: 13 additions & 191 deletions src/core/election.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { AllElectionStatus, CensusType, ElectionStatus, UnpublishedElection } fr
import { TransactionCore } from './transaction';
import { Buffer } from 'buffer';
import { strip0x } from '../util/common';
import { ChainCosts, ChainData } from '../services';
import { ChainCosts } from '../services';
import { TxMessage } from '../util/constants';

export abstract class ElectionCore extends TransactionCore {
private static readonly VOCHAIN_BLOCK_TIME_IN_SECONDS = 12;
private static readonly VOCHAIN_BLOCK_TIME_IN_SECONDS = 10;

/**
* Cannot be constructed.
Expand Down Expand Up @@ -71,11 +71,10 @@ export abstract class ElectionCore extends TransactionCore {
public static generateNewElectionTransaction(
election: UnpublishedElection,
cid: string,
blocks: { actual: number; start: number; end: number },
address: string,
nonce: number
): { tx: Uint8Array; metadata: string; message: string } {
const txData = this.prepareElectionData(election, cid, blocks, address, nonce);
const txData = this.prepareElectionData(election, cid, address, nonce);

const newProcess = NewProcessTx.fromPartial({
txtype: TxType.NEW_PROCESS,
Expand All @@ -93,18 +92,23 @@ export abstract class ElectionCore extends TransactionCore {
private static prepareElectionData(
election: UnpublishedElection,
cid: string,
blocks: { actual: number; start: number; end: number },
address: string,
nonce: number
): { metadata: string; electionData: object } {
let startTime = election.startDate ? Math.floor(election.startDate.getTime() / 1000) : 0;
// If the start date is less than the current time plus the block time, set it to begin immediately
// This is to prevent the transaction to be rejected by the node
if (startTime !== 0 && startTime < Math.floor(Date.now() / 1000) + this.VOCHAIN_BLOCK_TIME_IN_SECONDS * 5) {
startTime = 0;
}
return {
metadata: Buffer.from(JSON.stringify(election.generateMetadata()), 'utf8').toString('base64'),
electionData: {
nonce: nonce,
process: {
entityId: Uint8Array.from(Buffer.from(address, 'hex')),
startBlock: election.startDate ? blocks.start : 0,
blockCount: blocks.end - (election.startDate ? blocks.start : blocks.actual),
startTime,
duration: election.duration,
censusRoot: Uint8Array.from(Buffer.from(election.census.censusId, 'hex')),
censusURI: election.census.censusURI,
status: ProcessStatus.READY,
Expand Down Expand Up @@ -139,19 +143,12 @@ export abstract class ElectionCore extends TransactionCore {
}
}

public static estimateElectionBlocks(election: UnpublishedElection, chainData: ChainData): number {
return (
this.estimateBlockAtDateTime(election.endDate, chainData) -
this.estimateBlockAtDateTime(election.startDate ?? new Date(), chainData)
);
}

public static estimateElectionCost(election: UnpublishedElection, costs: ChainCosts, chainData: ChainData): number {
public static estimateElectionCost(election: UnpublishedElection, costs: ChainCosts): number {
if (!election.maxCensusSize) {
throw new Error('Could not estimate cost because maxCensusSize is not set');
}

const electionBlocks = this.estimateElectionBlocks(election, chainData);
const electionBlocks = Math.floor(election.duration / this.VOCHAIN_BLOCK_TIME_IN_SECONDS);

if (electionBlocks <= 0) {
throw new Error('Could not estimate cost because of negative election blocks size');
Expand Down Expand Up @@ -188,179 +185,4 @@ export abstract class ElectionCore extends TransactionCore {

return costs.basePrice + sizePrice + durationPrice + encryptedPrice + anonymousPrice + overwritePrice;
}

/**
* Returns the DateTime at which the given block number is expected to be mined
*
* @param blockNumber The desired block number
* @param chainData The block status information from the chain to use for the estimation
*/
// @ts-ignore
private static estimateDateAtBlock(blockNumber: number, chainData: ChainData): Date {
if (!blockNumber) return null;

const blocksPerM = 60 / this.VOCHAIN_BLOCK_TIME_IN_SECONDS;
const blocksPer10m = 10 * blocksPerM;
const blocksPerH = blocksPerM * 60;
const blocksPer6h = 6 * blocksPerH;
const blocksPerDay = 24 * blocksPerH;

// Diff between the last mined block and the given one
const blockDiff = Math.abs(blockNumber - chainData.height);
let averageBlockTime = this.VOCHAIN_BLOCK_TIME_IN_SECONDS * 1000;
let weightA: number, weightB: number;

// chainData.blockTime => [1m, 10m, 1h, 6h, 24h]
if (blockDiff > blocksPerDay) {
if (chainData.blockTime[4] > 0) averageBlockTime = chainData.blockTime[4];
else if (chainData.blockTime[3] > 0) averageBlockTime = chainData.blockTime[3];
else if (chainData.blockTime[2] > 0) averageBlockTime = chainData.blockTime[2];
else if (chainData.blockTime[1] > 0) averageBlockTime = chainData.blockTime[1];
else if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
} else if (blockDiff > blocksPer6h) {
// blocksPer6h <= blockDiff < blocksPerDay
const pivot = (blockDiff - blocksPer6h) / blocksPerH;
weightB = pivot / (24 - 6); // 0..1
weightA = 1 - weightB;

if (chainData.blockTime[4] > 0 && chainData.blockTime[3] > 0) {
averageBlockTime = weightA * chainData.blockTime[3] + weightB * chainData.blockTime[4];
} else if (chainData.blockTime[3] > 0) averageBlockTime = chainData.blockTime[3];
else if (chainData.blockTime[2] > 0) averageBlockTime = chainData.blockTime[2];
else if (chainData.blockTime[1] > 0) averageBlockTime = chainData.blockTime[1];
else if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
} else if (blockDiff > blocksPerH) {
// blocksPerH <= blockDiff < blocksPer6h
const pivot = (blockDiff - blocksPerH) / blocksPerH;
weightB = pivot / (6 - 1); // 0..1
weightA = 1 - weightB;

if (chainData.blockTime[3] > 0 && chainData.blockTime[2] > 0) {
averageBlockTime = weightA * chainData.blockTime[2] + weightB * chainData.blockTime[3];
} else if (chainData.blockTime[2] > 0) averageBlockTime = chainData.blockTime[2];
else if (chainData.blockTime[1] > 0) averageBlockTime = chainData.blockTime[1];
else if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
} else if (blockDiff > blocksPer10m) {
// blocksPer10m <= blockDiff < blocksPerH
const pivot = (blockDiff - blocksPer10m) / blocksPerM;
weightB = pivot / (60 - 10); // 0..1
weightA = 1 - weightB;

if (chainData.blockTime[2] > 0 && chainData.blockTime[1] > 0) {
averageBlockTime = weightA * chainData.blockTime[1] + weightB * chainData.blockTime[2];
} else if (chainData.blockTime[1] > 0) averageBlockTime = chainData.blockTime[1];
else if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
} else if (blockDiff > blocksPerM) {
// blocksPerM <= blockDiff < blocksPer10m
const pivot = (blockDiff - blocksPerM) / blocksPerM;
weightB = pivot / (10 - 1); // 0..1
weightA = 1 - weightB;

if (chainData.blockTime[1] > 0 && chainData.blockTime[0] > 0) {
averageBlockTime = weightA * chainData.blockTime[0] + weightB * chainData.blockTime[1];
} else if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
} else {
if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
}

const targetTimestamp = chainData.blockTimestamp * 1000 + (blockNumber - chainData.height) * averageBlockTime;
return new Date(targetTimestamp);
}

/**
* Returns the block number that is expected to be current at the given date and time
*
* @param dateTime The desired date time
* @param chainData The block status information from the chain to use for the estimation
*/
private static estimateBlockAtDateTime(dateTime: Date, chainData: ChainData): number {
if (typeof dateTime == 'number') dateTime = new Date(dateTime);
else if (!(dateTime instanceof Date)) return null;

const outliers = (arr: number[]): number[] => {
const values = arr.concat();
values.sort((a, b) => a - b);

const q1 = values[Math.floor(values.length / 4)];
const q3 = values[Math.ceil(values.length * (3 / 4))];
const iqr = q3 - q1;
const maxValue = q3 + iqr * 1.5;
const minValue = q1 - iqr * 1.5;

return values.filter((x) => x <= maxValue && x >= minValue);
};

chainData.blockTime = chainData.blockTime.map((part) => {
return outliers(chainData.blockTime).includes(part) ? part : 0;
});

let averageBlockTime = this.VOCHAIN_BLOCK_TIME_IN_SECONDS * 1000;
let weightA: number, weightB: number;

// Diff between the last mined block and the given date
const dateDiff = Math.abs(dateTime.getTime() - chainData.blockTimestamp * 1000);

// chainData.blockTime => [1m, 10m, 1h, 6h, 24h]

if (dateDiff >= 1000 * 60 * 60 * 24) {
if (chainData.blockTime[4] > 0) averageBlockTime = chainData.blockTime[4];
else if (chainData.blockTime[3] > 0) averageBlockTime = chainData.blockTime[3];
else if (chainData.blockTime[2] > 0) averageBlockTime = chainData.blockTime[2];
else if (chainData.blockTime[1] > 0) averageBlockTime = chainData.blockTime[1];
else if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
} else if (dateDiff >= 1000 * 60 * 60 * 6) {
// 1000 * 60 * 60 * 6 <= dateDiff < 1000 * 60 * 60 * 24
if (chainData.blockTime[4] > 0 && chainData.blockTime[3] > 0) {
const pivot = (dateDiff - 1000 * 60 * 60 * 6) / (1000 * 60 * 60);
weightB = pivot / (24 - 6); // 0..1
weightA = 1 - weightB;

averageBlockTime = weightA * chainData.blockTime[3] + weightB * chainData.blockTime[4];
} else if (chainData.blockTime[3] > 0) averageBlockTime = chainData.blockTime[3];
else if (chainData.blockTime[2] > 0) averageBlockTime = chainData.blockTime[2];
else if (chainData.blockTime[1] > 0) averageBlockTime = chainData.blockTime[1];
else if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
} else if (dateDiff >= 1000 * 60 * 60) {
// 1000 * 60 * 60 <= dateDiff < 1000 * 60 * 60 * 6
if (chainData.blockTime[3] > 0 && chainData.blockTime[2] > 0) {
const pivot = (dateDiff - 1000 * 60 * 60) / (1000 * 60 * 60);
weightB = pivot / (6 - 1); // 0..1
weightA = 1 - weightB;

averageBlockTime = weightA * chainData.blockTime[2] + weightB * chainData.blockTime[3];
} else if (chainData.blockTime[2] > 0) averageBlockTime = chainData.blockTime[2];
else if (chainData.blockTime[1] > 0) averageBlockTime = chainData.blockTime[1];
else if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
} else if (dateDiff >= 1000 * 60 * 10) {
// 1000 * 60 * 10 <= dateDiff < 1000 * 60 * 60
if (chainData.blockTime[2] > 0 && chainData.blockTime[1] > 0) {
const pivot = (dateDiff - 1000 * 60 * 10) / (1000 * 60);
weightB = pivot / (60 - 10); // 0..1
weightA = 1 - weightB;

averageBlockTime = weightA * chainData.blockTime[1] + weightB * chainData.blockTime[2];
} else if (chainData.blockTime[1] > 0) averageBlockTime = chainData.blockTime[1];
else if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
} else if (dateDiff >= 1000 * 60) {
// 1000 * 60 <= dateDiff < 1000 * 60 * 6
const pivot = (dateDiff - 1000 * 60) / (1000 * 60);
weightB = pivot / (10 - 1); // 0..1
weightA = 1 - weightB;

if (chainData.blockTime[1] > 0 && chainData.blockTime[0] > 0) {
averageBlockTime = weightA * chainData.blockTime[0] + weightB * chainData.blockTime[1];
} else if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
} else {
if (chainData.blockTime[0] > 0) averageBlockTime = chainData.blockTime[0];
}

const estimatedBlockDiff = dateDiff / averageBlockTime;
const estimatedBlock =
dateTime.getTime() < chainData.blockTimestamp * 1000
? chainData.height - Math.ceil(estimatedBlockDiff)
: chainData.height + Math.floor(estimatedBlockDiff);

if (estimatedBlock < 0) return 0;
return estimatedBlock;
}
}
29 changes: 11 additions & 18 deletions src/services/election.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export enum ElectionCreationSteps {
CENSUS_CREATED = 'census-created',
GET_ACCOUNT_DATA = 'get-account-data',
GET_DATA_PIN = 'get-data-pin',
ESTIMATE_BLOCK_TIMES = 'estimate-block-times',
GENERATE_TX = 'generate-tx',
SIGN_TX = 'sign-tx',
CREATING = 'creating',
Expand All @@ -53,7 +52,6 @@ export type ElectionCreationStepValue =
| { key: ElectionCreationSteps.CENSUS_CREATED }
| { key: ElectionCreationSteps.GET_ACCOUNT_DATA }
| { key: ElectionCreationSteps.GET_DATA_PIN }
| { key: ElectionCreationSteps.ESTIMATE_BLOCK_TIMES }
| { key: ElectionCreationSteps.GENERATE_TX }
| { key: ElectionCreationSteps.SIGN_TX }
| { key: ElectionCreationSteps.CREATING; txHash: string }
Expand Down Expand Up @@ -326,8 +324,9 @@ export class ElectionService extends Service implements ElectionServicePropertie
*/
estimateElectionCost(election: UnpublishedElection): Promise<number> {
invariant(this.chainService, 'No chain service set');
return Promise.all([this.chainService.fetchChainCosts(), this.chainService.fetchChainData()])
.then(([chainCosts, chainData]) => ElectionCore.estimateElectionCost(election, chainCosts, chainData))
return this.chainService
.fetchChainCosts()
.then((chainCosts) => ElectionCore.estimateElectionCost(election, chainCosts))
.then((cost) => Math.trunc(cost));
}

Expand All @@ -337,20 +336,14 @@ export class ElectionService extends Service implements ElectionServicePropertie
* @returns {Promise<number>} The cost in tokens.
*/
calculateElectionCost(election: UnpublishedElection): Promise<number> {
invariant(this.chainService, 'No chain service set');
invariant(this.url, 'No URL set');
return this.chainService
.fetchChainData()
.then((chainData) =>
ElectionAPI.price(
this.url,
election.maxCensusSize,
ElectionCore.estimateElectionBlocks(election, chainData),
election.electionType.secretUntilTheEnd,
election.electionType.anonymous,
election.voteType.maxVoteOverwrites
)
)
.then((cost) => cost.price);
return ElectionAPI.price(
this.url,
election.maxCensusSize,
election.duration,
election.electionType.secretUntilTheEnd,
election.electionType.anonymous,
election.voteType.maxVoteOverwrites
).then((cost) => cost.price);
}
}
Loading
Loading