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

Mising operations of Aristo: handle sidechain in memory #2260

Closed
jangko opened this issue May 31, 2024 · 1 comment · Fixed by #2405
Closed

Mising operations of Aristo: handle sidechain in memory #2260

jangko opened this issue May 31, 2024 · 1 comment · Fixed by #2405
Assignees
Labels
bug Something isn't working

Comments

@jangko
Copy link
Contributor

jangko commented May 31, 2024

Copied from discord discussion:

One of the test case in test_blockchain_json doing something like this:

nimbus-eth1/tests/fixtures/eth_tests/BlockchainTests/TransitionTests/bcHomesteadToDao/DaoTransactions.json
(several other failing test cases also doing similar things)

Genesis-D1-D2-D3-D4-D5X-H1-H2-H3-H4-H5X-D5-D6-D7-D8-...-D14

where D is default chain, and H is side chain. Block with a 'X' is an invalid block. When executing block H1, there is an error message like this because H1 request for genesis stateRoot but cannot find it anymore.

Error: unhandled exception: AccountLedger.init(): RootNotFound(Aristo, ctx=ctx/newColFn(), error=GenericError)

comments from @arnetheduck :
This is something we should be able to support with the pure in-memory option - ie as long as D4 is the finalization point, it's supported, but that also means that only D4 has been written to disk.

so it sounds like it's a set of operations that we need are not captured in the current API design, but for the sake of argument, here's how it can be done (based on what I've seen in persistBlocks at least:

when processing D1, we open transaction T1
then for D2..D15, we open T1.2..T1.15 - ie we never commit T1 - this is the state in which persistBlocks is iterating over the blocks we give it
in this state, we can still process forks because we've not committed T1 and we have all the "diff" layers in memory

Let's say that D4 is finalized. A naive way to implement this is:
roll back all transactions including T1
re-play D1..D4 and commit that
re-play D5..D15 and the remaining branches without committing them

then when D8 is finalized, repeat - this time, H1 is not replayed, because it forks off a finalized section of the graph

@jangko jangko added the bug Something isn't working label Jun 4, 2024
@jangko jangko self-assigned this Jun 7, 2024
@arnetheduck
Copy link
Member

arnetheduck commented Jun 19, 2024

Based on what we have today in the codebase, here's a sketch of a design that should be correct, albeit inefficient - it's useful as a starting point however and it is based on the idea of doing exactly what persistBlocks is doing today, but incrementally.

In short, we would move the local local variables of persistBlocks to a state object - let's call it ForkedChainDb:

type ForkedChainDb = object
  dbtx: ...
  coredb: CoreDbRef
  
  blocks: Table[Hash256, EthBlock]
  head: Hash256
  safeHead: Hash256
  heads: seq[Hash256]
  base: Hash256

This object would keep track of what blocks have been processed "so far" and obviously would have appropriate lists of blocks, parent relationships and so on - the fields in this object are for illustration only.

We'd have two operations - addBlock and updateHead (that correspond to the EL json-rpc api) - addBlock would do exactly what persistBlocks does when it moves on to the "next" block in the loop - it would call processBlock and so on. If a block fails to apply, it can simply drop roll back the database transaction and replay the blocks that it already has (insanely inefficient, but it's fine for now since these are already-validated blocks).

updateHead is where we would perform the persist and commit call, to save progress to the database - it would receive the current and finalized heads, and if the head has moved forward "enough", it would replay the old blocks again, but this time it would commit the progress to the database.

proc addBlock(chain: ForkedChain,blk: EthBlock): Result =
  if chain.dbtx == nil: chain.coredb.createTransaction()
  if processBlock(blk, ...).iserr:
    chain.dbtx.rollback()
    # recreate in-memory state
    for blk in chain.blocks: addBlock(blk)
    return

  chain.blocks.add blk
  heads.add(rlpHash(blk))
  heads.del(blk.parentRoot) # approximately

proc updateHead(chain: FC, head: Hash256, finalized: Hash256) =
  chain.head = head
  chain.finalized = finalized

  let newbase = chain.blockat(min(chain.finalized.blocknubmer, head.blockNumber - 128))))
  if chain.base != newbase:
    chain.dbtx.rollback()
    # Replay the blocks from our local database
    persistBlocks(chain.blocks[chain.base..newbase])
    chain.blocks.del(chain.base..newBase)
    # recreate the in-memory state
    for blk in chain.blocks: chain.addblock(blk)
    chain.base = newBase

Of course, the above is a sketch, but this would be enough to correctly follow the chain and respond to most json-rpc queries (which would call ForkedChainDb and not CoreDb)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants