Skip to content

Commit

Permalink
Handle empty dynamic contract registrations (#384)
Browse files Browse the repository at this point in the history
* Throw error in case of a bad merge

* Fix adding empty dynamic contracts register

* Raise error when an address is registered for multiple contracts

* Add warnings for address registered to multiple contracts and disallow overwrite

* Fix tests
  • Loading branch information
JonoPrest authored Dec 13, 2024
1 parent bc2801a commit 79f82fb
Show file tree
Hide file tree
Showing 6 changed files with 501 additions and 389 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,30 @@ type mapping = {
addressesByName: dict<Belt.Set.String.t>,
}

exception AddressRegisteredForMultipleContracts({address: Address.t, names: array<contractName>})

let addAddress = (map: mapping, ~name: string, ~address: Address.t) => {
switch map.nameByAddress->Utils.Dict.dangerouslyGetNonOption(address->Address.toString) {
| Some(currentName) if currentName != name =>
let logger = Logging.createChild(
~params={
"address": address->Address.toString,
"existingContract": currentName,
"newContract": name,
},
)
AddressRegisteredForMultipleContracts({
address,
names: [currentName, name],
})->ErrorHandling.mkLogAndRaise(~msg="Address registered for multiple contracts", ~logger)
| _ => ()
}
map.nameByAddress->Js.Dict.set(address->Address.toString, name)

let oldAddresses =
map.addressesByName->Utils.Dict.dangerouslyGetNonOption(name)->Belt.Option.getWithDefault(Belt.Set.String.empty)
map.addressesByName
->Utils.Dict.dangerouslyGetNonOption(name)
->Belt.Option.getWithDefault(Belt.Set.String.empty)
let newAddresses = oldAddresses->Belt.Set.String.add(address->Address.toString)
map.addressesByName->Js.Dict.set(name, newAddresses)
}
Expand Down
86 changes: 35 additions & 51 deletions codegenerator/cli/templates/static/codegen/src/EventProcessing.res
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,21 @@ let addToDynamicContractRegistrations = (
},
]

let dynamicContractRegistration = {
FetchState.dynamicContracts,
registeringEventBlockNumber,
registeringEventLogIndex,
registeringEventChain: eventItem.chain,
let registrations = switch dynamicContracts {
| [] => registrations
| dynamicContracts =>
let dynamicContractRegistration = {
FetchState.dynamicContracts,
registeringEventBlockNumber,
registeringEventLogIndex,
registeringEventChain: eventItem.chain,
}
[...registrations, dynamicContractRegistration]
}

{
unprocessedBatch,
registrations: [...registrations, dynamicContractRegistration],
registrations,
}
}

Expand Down Expand Up @@ -174,10 +179,7 @@ let runEventContractRegister = (

switch preRegisterLatestProcessedBlocks {
| Some(latestProcessedBlocks) =>
eventItem->updateEventSyncState(
~inMemoryStore,
~isPreRegisteringDynamicContracts=true,
)
eventItem->updateEventSyncState(~inMemoryStore, ~isPreRegisteringDynamicContracts=true)
latestProcessedBlocks :=
latestProcessedBlocks.contents->EventsProcessed.updateEventsProcessed(
~chain=eventItem.chain,
Expand All @@ -190,12 +192,7 @@ let runEventContractRegister = (
}
}

let runEventLoader = async (
~contextEnv,
~loader: Internal.loader,
~inMemoryStore,
~loadLayer,
) => {
let runEventLoader = async (~contextEnv, ~loader: Internal.loader, ~inMemoryStore, ~loadLayer) => {
switch await loader(contextEnv->ContextEnv.getLoaderArgs(~inMemoryStore, ~loadLayer)) {
| exception exn =>
exn
Expand All @@ -216,15 +213,15 @@ let convertFieldsToJson = (fields: dict<unknown>) => {
let value = fields->Js.Dict.unsafeGet(key)
// Skip `undefined` values and convert bigint fields to string
// There are not fields with nested bigints, so this is safe
new->Js.Dict.set(key, Js.typeof(value) === "bigint" ? value->Utils.magic->BigInt.toString->Utils.magic : value)
new->Js.Dict.set(
key,
Js.typeof(value) === "bigint" ? value->Utils.magic->BigInt.toString->Utils.magic : value,
)
}
new->(Utils.magic: dict<unknown> => Js.Json.t)
}

let addEventToRawEvents = (
eventItem: Internal.eventItem,
~inMemoryStore: InMemoryStore.t,
) => {
let addEventToRawEvents = (eventItem: Internal.eventItem, ~inMemoryStore: InMemoryStore.t) => {
let {
event,
eventName,
Expand All @@ -237,10 +234,12 @@ let addEventToRawEvents = (
let {block, transaction, params, logIndex, srcAddress} = event
let chainId = chain->ChainMap.Chain.toChainId
let eventId = EventUtils.packEventIndex(~logIndex, ~blockNumber)
let blockFields = block
let blockFields =
block
->(Utils.magic: Internal.eventBlock => dict<unknown>)
->convertFieldsToJson
let transactionFields = transaction
let transactionFields =
transaction
->(Utils.magic: Internal.eventTransaction => dict<unknown>)
->convertFieldsToJson

Expand Down Expand Up @@ -289,11 +288,11 @@ let runEventHandler = (
let timeBeforeHandler = Hrtime.makeTimer()

let loaderReturn = switch loader {
| Some(loader) =>
(await runEventLoader(~contextEnv, ~loader, ~inMemoryStore, ~loadLayer))->propogate
| None => (%raw(`undefined`): Internal.loaderReturn)
| Some(loader) =>
(await runEventLoader(~contextEnv, ~loader, ~inMemoryStore, ~loadLayer))->propogate
| None => (%raw(`undefined`): Internal.loaderReturn)
}

switch await handler(
contextEnv->ContextEnv.getHandlerArgs(
~loaderReturn,
Expand Down Expand Up @@ -349,10 +348,7 @@ let runHandler = async (
}

result->Result.map(() => {
eventItem->updateEventSyncState(
~inMemoryStore,
~isPreRegisteringDynamicContracts=false,
)
eventItem->updateEventSyncState(~inMemoryStore, ~isPreRegisteringDynamicContracts=false)

if config.enableRawEvents {
eventItem->addEventToRawEvents(~inMemoryStore)
Expand All @@ -365,10 +361,7 @@ let runHandler = async (
})
}

let addToUnprocessedBatch = (
eventItem: Internal.eventItem,
dynamicContractRegistrations,
) => {
let addToUnprocessedBatch = (eventItem: Internal.eventItem, dynamicContractRegistrations) => {
{
...dynamicContractRegistrations,
unprocessedBatch: [...dynamicContractRegistrations.unprocessedBatch, eventItem],
Expand Down Expand Up @@ -441,12 +434,7 @@ let rec registerDynamicContracts = (
}
}

let runLoaders = (
eventBatch: array<Internal.eventItem>,
~loadLayer,
~inMemoryStore,
~logger,
) => {
let runLoaders = (eventBatch: array<Internal.eventItem>, ~loadLayer, ~inMemoryStore, ~logger) => {
open ErrorHandling.ResultPropogateEnv
runAsyncEnv(async () => {
// We don't actually need loader returns,
Expand All @@ -457,13 +445,13 @@ let runLoaders = (
await eventBatch
->Array.keepMap(eventItem => {
switch eventItem {
| {loader: Some(loader)} => {
| {loader: Some(loader)} => {
let contextEnv = ContextEnv.make(~eventItem, ~logger)
runEventLoader(~contextEnv, ~loader, ~inMemoryStore, ~loadLayer)->Promise.thenResolve(
propogate,
)->Some
runEventLoader(~contextEnv, ~loader, ~inMemoryStore, ~loadLayer)
->Promise.thenResolve(propogate)
->Some
}
| _ => None
| _ => None
}
})
->Promise.all
Expand Down Expand Up @@ -560,11 +548,7 @@ let getDynamicContractRegistrations = (
->propogate

//We only preregister below the reorg threshold so it can be hardcoded as false
switch await Db.sql->IO.executeBatch(
~inMemoryStore,
~isInReorgThreshold=false,
~config,
) {
switch await Db.sql->IO.executeBatch(~inMemoryStore, ~isInReorgThreshold=false, ~config) {
| exception exn =>
exn->ErrorHandling.make(~msg="Failed writing batch to database", ~logger)->Error->propogate
| () => ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,14 @@ let addDynamicContractRegisters = (baseRegister, pendingDynamicContracts) => {
})
}

exception NextRegisterIsLessThanCurrent

let isRootRegister = registerType =>
switch registerType {
| RootRegister(_) => true
| DynamicContractRegister(_) => false
}

/**
If a fetchState register has caught up to its next regisered node. Merge them and recurse.
If no merging happens, None is returned
Expand All @@ -475,7 +483,30 @@ let rec pruneAndMergeNextRegistered = (register: register, ~isMerged=false) => {
| DynamicContractRegister({nextRegister})
if register.latestFetchedBlock.blockNumber <
nextRegister.latestFetchedBlock.blockNumber => merged
| DynamicContractRegister(_) =>
| DynamicContractRegister({nextRegister}) =>
if register.latestFetchedBlock.blockNumber > nextRegister.latestFetchedBlock.blockNumber {
let logger = Logging.createChild(
~params={
"context": "Merging Dynamic Contract Registers",
"currentRegister": {
"id": register->getRegisterId,
"latestFetchedBlock": register.latestFetchedBlock.blockNumber,
"addresses": register.contractAddressMapping->ContractAddressingMap.getAllAddresses,
},
"nextRegister": {
"id": nextRegister->getRegisterId,
"latestFetchedBlock": nextRegister.latestFetchedBlock.blockNumber,
"addresses": nextRegister.registerType->isRootRegister
? "Root"->Utils.magic
: nextRegister.contractAddressMapping->ContractAddressingMap.getAllAddresses,
},
},
)
NextRegisterIsLessThanCurrent->ErrorHandling.mkLogAndRaise(
~msg="Unexpected: Dynamic contract register latest fetched block is greater than next register when it should be equal",
~logger,
)
}
// Recursively look for other merges
register->mergeIntoNextRegistered->pruneAndMergeNextRegistered(~isMerged=true)
}
Expand Down Expand Up @@ -878,18 +909,55 @@ let isReadyForNextQuery = ({pendingDynamicContracts, baseRegister}: t, ~maxQueue
? baseRegister.fetchedEventQueue->Array.length < maxQueueSize
: true

let warnIfAttemptedAddressRegisterOnDifferentContracts = (
~contractAddress,
~contractName,
~existingContractName,
~chainId,
) => {
if existingContractName != contractName {
let logger = Logging.createChild(
~params={
"chainId": chainId,
"contractAddress": contractAddress->Address.toString,
"existingContractType": existingContractName,
"newContractType": contractName,
},
)
logger->Logging.childWarn(
`Contract address ${contractAddress->Address.toString} is already registered as contract ${existingContractName} and cannot also be registered as ${(contractName :> string)}`,
)
}
}

let rec checkBaseRegisterContainsRegisteredContract = (
register: register,
~contractName,
~contractAddress,
~chainId,
) => {
switch register.contractAddressMapping->ContractAddressingMap.getAddresses(contractName) {
| Some(addresses) if addresses->Belt.Set.String.has(contractAddress->Address.toString) => true
switch register.contractAddressMapping->ContractAddressingMap.getContractNameFromAddress(
~contractAddress,
) {
| Some(existingContractName) =>
if existingContractName != contractName {
warnIfAttemptedAddressRegisterOnDifferentContracts(
~contractAddress,
~contractName,
~existingContractName,
~chainId,
)
}
true
| _ =>
switch register.registerType {
| RootRegister(_) => false
| DynamicContractRegister({nextRegister}) =>
nextRegister->checkBaseRegisterContainsRegisteredContract(~contractName, ~contractAddress)
nextRegister->checkBaseRegisterContainsRegisteredContract(
~contractName,
~contractAddress,
~chainId,
)
}
}
}
Expand All @@ -898,12 +966,30 @@ let rec checkBaseRegisterContainsRegisteredContract = (
Recurses through registers and determines whether a contract has already been registered with
the given name and address
*/
let checkContainsRegisteredContractAddress = (self: t, ~contractName, ~contractAddress) => {
self.baseRegister->checkBaseRegisterContainsRegisteredContract(~contractName, ~contractAddress) ||
let checkContainsRegisteredContractAddress = (
self: t,
~contractName,
~contractAddress,
~chainId,
) => {
self.baseRegister->checkBaseRegisterContainsRegisteredContract(
~contractName,
~contractAddress,
~chainId,
) ||
self.pendingDynamicContracts->Array.some(({dynamicContracts}) =>
dynamicContracts->Array.some(dcr =>
dcr.contractAddress == contractAddress && (dcr.contractType :> string) == contractName
)
dynamicContracts->Array.some(dcr => {
let exists = dcr.contractAddress == contractAddress
if exists {
warnIfAttemptedAddressRegisterOnDifferentContracts(
~contractAddress,
~contractName,
~existingContractName=(dcr.contractType :> string),
~chainId,
)
}
exists
})
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,18 @@ let getLatestFullyFetchedBlock = ({partitions}: t) =>
})
->Option.getUnsafe

let checkContainsRegisteredContractAddress = ({partitions}: t, ~contractAddress, ~contractName) => {
partitions->Array.reduce(false, (accum, partition) => {
accum ||
partition->FetchState.checkContainsRegisteredContractAddress(~contractAddress, ~contractName)
let checkContainsRegisteredContractAddress = (
{partitions}: t,
~contractAddress,
~contractName,
~chainId,
) => {
partitions->Array.some(partition => {
partition->FetchState.checkContainsRegisteredContractAddress(
~contractAddress,
~contractName,
~chainId,
)
})
}

Expand Down
Loading

0 comments on commit 79f82fb

Please sign in to comment.