@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
116 lines • 6.66 kB
JavaScript
import { byteArrayEquals } from "@chainsafe/ssz";
import { ssz } from "@lodestar/types";
import { Eth1Error, Eth1ErrorCode } from "./errors.js";
import { getDepositsWithProofs } from "./utils/deposits.js";
import { getEth1DataForBlocks } from "./utils/eth1Data.js";
import { assertConsecutiveDeposits } from "./utils/eth1DepositEvent.js";
export class Eth1DepositsCache {
constructor(opts, config, db) {
this.config = config;
this.db = db;
this.unsafeAllowDepositDataOverwrite = opts.unsafeAllowDepositDataOverwrite ?? false;
}
/**
* Returns a list of `Deposit` objects, within the given deposit index `range`.
*
* The `depositCount` is used to generate the proofs for the `Deposits`. For example, if we
* have 100 proofs, but the Ethereum Consensus chain only acknowledges 50 of them, we must produce our
* proofs with respect to a tree size of 50.
*/
async get(indexRange, eth1Data) {
const depositEvents = await this.db.depositEvent.values(indexRange);
const depositRootTree = await this.db.depositDataRoot.getDepositRootTree();
return getDepositsWithProofs(depositEvents, depositRootTree, eth1Data);
}
/**
* Add log to cache
* This function enforces that `logs` are imported one-by-one with consecutive indexes
*/
async add(depositEvents) {
assertConsecutiveDeposits(depositEvents);
const lastLog = await this.db.depositEvent.lastValue();
const firstEvent = depositEvents[0];
// Check, validate and skip if we got any deposit events already present in DB
// This can happen if the remote eth1/EL resets its head in these four scenarios:
// 1. Remote eth1/EL resynced/restarted from head behind its previous head pre-merge
// 2. In a post merge scenario, Lodestar restarted from finalized state from DB which
// generally is a few epochs behind the last synced head. This causes eth1 tracker to reset
// and refetch the deposits as the lodestar syncs further along (Post merge there is 1-1
// correspondence between EL and CL blocks)
// 3. The EL reorged beyond the eth1 follow distance.
//
// While 1. & 2. are benign and we handle them below by checking if the duplicate log fetched
// is same as one written in DB. Refer to this issue for some data dump of how this happens
// https://github.com/ChainSafe/lodestar/issues/3674
//
// If the duplicate log fetched is not same as written in DB then its probablu scenario 3.
// which would be a catastrophic event for the network (or we messed up real bad!!!).
//
// So we provide for a way to overwrite this log without deleting full db via
// --unsafeAllowDepositDataOverwrite cli flag which will just overwrite the previous tracker data
// if any. This option as indicated by its name is unsafe and to be only used if you know what
// you are doing.
if (lastLog !== null && firstEvent !== undefined) {
const newIndex = firstEvent.index;
const lastLogIndex = lastLog.index;
if (!this.unsafeAllowDepositDataOverwrite && firstEvent.index <= lastLog.index) {
// lastLogIndex - newIndex + 1 events are duplicate since this is a consecutive log
// as asserted by assertConsecutiveDeposits. Splice those events out from depositEvents.
const skipEvents = depositEvents.splice(0, lastLogIndex - newIndex + 1);
// After splicing skipEvents will contain duplicate events to be checked and validated
// and rest of the remaining events in depositEvents could be safely written to DB and
// move the tracker along.
for (const depositEvent of skipEvents) {
const prevDBSerializedEvent = await this.db.depositEvent.getBinary(depositEvent.index);
if (!prevDBSerializedEvent) {
throw new Eth1Error({ code: Eth1ErrorCode.MISSING_DEPOSIT_LOG, newIndex, lastLogIndex });
}
const serializedEvent = ssz.phase0.DepositEvent.serialize(depositEvent);
if (!byteArrayEquals(prevDBSerializedEvent, serializedEvent)) {
throw new Eth1Error({ code: Eth1ErrorCode.DUPLICATE_DISTINCT_LOG, newIndex, lastLogIndex });
}
}
}
else if (newIndex > lastLogIndex + 1) {
// deposit events need to be consective, the way we fetch our tracker. If the deposit event
// is not consecutive it means either our tracker, or the corresponding eth1/EL
// node or the database has messed up. All these failures are critical and the tracker
// shouldn't proceed without the resolution of this error.
throw new Eth1Error({ code: Eth1ErrorCode.NON_CONSECUTIVE_LOGS, newIndex, lastLogIndex });
}
}
const depositRoots = depositEvents.map((depositEvent) => ({
index: depositEvent.index,
root: ssz.phase0.DepositData.hashTreeRoot(depositEvent.depositData),
}));
// Store events after verifying that data is consecutive
// depositDataRoot will throw if adding non consecutive roots
await this.db.depositDataRoot.batchPutValues(depositRoots);
await this.db.depositEvent.batchPutValues(depositEvents);
}
/**
* Appends partial eth1 data (depositRoot, depositCount) in a block range (inclusive)
* Returned array is sequential and ascending in blockNumber
* @param fromBlock
* @param toBlock
*/
async getEth1DataForBlocks(blocks, lastProcessedDepositBlockNumber) {
const highestBlock = blocks.at(-1)?.blockNumber;
return getEth1DataForBlocks(blocks, this.db.depositEvent.valuesStream({ lte: highestBlock, reverse: true }), await this.db.depositDataRoot.getDepositRootTree(), lastProcessedDepositBlockNumber);
}
/**
* Returns the highest blockNumber stored in DB if any
*/
async getHighestDepositEventBlockNumber() {
const latestEvent = await this.db.depositEvent.lastValue();
return latestEvent?.blockNumber || null;
}
/**
* Returns the lowest blockNumber stored in DB if any
*/
async getLowestDepositEventBlockNumber() {
const firstEvent = await this.db.depositEvent.firstValue();
return firstEvent?.blockNumber || null;
}
}
//# sourceMappingURL=eth1DepositsCache.js.map