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

client: create snap sync fetcher to sync accounts #2107

Merged
merged 90 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
e8a4576
Add account fetcher base
scorbajio Aug 6, 2022
4b8f867
Add accountfetcher import
scorbajio Aug 7, 2022
aac8930
Add AccountFetcher as possible type for Synchronizer.fetcher
scorbajio Aug 8, 2022
4df7ffd
Place call to getAccountRange inside of fetcher
scorbajio Aug 8, 2022
1fb1851
Place call to getAccountRange() in accountfetcher and comment it out
scorbajio Aug 8, 2022
0a179e5
Add account fetcher base
scorbajio Aug 6, 2022
83efe1c
Add accountfetcher import
scorbajio Aug 7, 2022
0f65a77
add account fetcher getter setter in snapsync
g11tech Aug 9, 2022
3bb257b
Change order of importing accountfetcher in index file
scorbajio Aug 12, 2022
15a44b8
Change bytes parameter to be per task
scorbajio Aug 12, 2022
fcf0a91
Remove root and bytes from task inputs and make them fetcher variables
scorbajio Aug 12, 2022
7204fe5
Correct log message
scorbajio Aug 12, 2022
2c83c83
Add debug console log statement
scorbajio Aug 12, 2022
b13b75a
Fix linting issues
scorbajio Aug 12, 2022
72b9d97
Add account to mpt and check validity with root and proof
scorbajio Aug 12, 2022
df3db62
Set root of trie
scorbajio Aug 13, 2022
baac630
Add checks to fetcher.request()
scorbajio Aug 14, 2022
0cc51d2
client/snap: fix getAccountRange return type
jochem-brouwer Aug 14, 2022
4219dcb
client/snap: pass first proof
jochem-brouwer Aug 14, 2022
ec2272d
client/snap: add utility to convert slim account to a normal RLPd acc…
jochem-brouwer Aug 14, 2022
f4e2204
client/snap: implement account range db dump
jochem-brouwer Aug 17, 2022
e24cc3a
Update to use verifyRangeProof
scorbajio Aug 17, 2022
b616aa3
Correct some messages
scorbajio Aug 18, 2022
0645986
Update verifyProofRange input for first account hash to be fetcher or…
scorbajio Aug 20, 2022
875a186
Fix linting issues
scorbajio Aug 20, 2022
fae5bbd
Store accounts in store phase
scorbajio Aug 20, 2022
96d4544
Add logic for dividing hash ranges and adding them as tasks
scorbajio Aug 21, 2022
a8926f4
Increment count by 1 before next iteration
scorbajio Aug 21, 2022
4541027
client/snap: remove unnecessary account fetcher logic
jochem-brouwer Aug 21, 2022
34e4742
client/snap: correctly feed the right values to verifyRangeProof
jochem-brouwer Aug 21, 2022
a008ffb
lint fixes
g11tech Aug 22, 2022
c370391
small cleanup
g11tech Aug 23, 2022
70fd5e3
fix account fetcher with previous fixes
g11tech Sep 7, 2022
cda3040
overhaul and simplify the fetcher and add partial results handling
g11tech Sep 11, 2022
61fcf6a
cleanup comments
g11tech Sep 12, 2022
7b5b1f3
fix fetch spec tests
g11tech Sep 12, 2022
020c41e
Check if right range is missing using return value from verifyRangeProof
scorbajio Oct 21, 2022
593baa0
Add accountfetcher tests
scorbajio Oct 21, 2022
0dc19dd
Remove request test
scorbajio Oct 27, 2022
b288e16
Correct return type
scorbajio Nov 19, 2022
0e386c8
Add request test
scorbajio Nov 19, 2022
1f454b0
Add account fetcher base
scorbajio Aug 6, 2022
d49df23
Add accountfetcher import
scorbajio Aug 7, 2022
9060c61
Add AccountFetcher as possible type for Synchronizer.fetcher
scorbajio Aug 8, 2022
dd24731
Place call to getAccountRange inside of fetcher
scorbajio Aug 8, 2022
c549a8e
Place call to getAccountRange() in accountfetcher and comment it out
scorbajio Aug 8, 2022
126570d
Add account fetcher base
scorbajio Aug 6, 2022
02ddf55
Add accountfetcher import
scorbajio Aug 7, 2022
8179a33
add account fetcher getter setter in snapsync
g11tech Aug 9, 2022
3495a65
Change order of importing accountfetcher in index file
scorbajio Aug 12, 2022
c37507d
Change bytes parameter to be per task
scorbajio Aug 12, 2022
97f9b39
Remove root and bytes from task inputs and make them fetcher variables
scorbajio Aug 12, 2022
fee1dfa
Correct log message
scorbajio Aug 12, 2022
4f1f708
Add debug console log statement
scorbajio Aug 12, 2022
0081357
Fix linting issues
scorbajio Aug 12, 2022
c159ea2
Add account to mpt and check validity with root and proof
scorbajio Aug 12, 2022
0b3ab58
Set root of trie
scorbajio Aug 13, 2022
308922b
Add checks to fetcher.request()
scorbajio Aug 14, 2022
0f3520a
client/snap: fix getAccountRange return type
jochem-brouwer Aug 14, 2022
2340713
client/snap: pass first proof
jochem-brouwer Aug 14, 2022
4b9dbaa
client/snap: add utility to convert slim account to a normal RLPd acc…
jochem-brouwer Aug 14, 2022
89da73c
client/snap: implement account range db dump
jochem-brouwer Aug 17, 2022
311a6b4
Update to use verifyRangeProof
scorbajio Aug 17, 2022
8215fc5
Correct some messages
scorbajio Aug 18, 2022
3c2006c
Update verifyProofRange input for first account hash to be fetcher or…
scorbajio Aug 20, 2022
d5ef441
Fix linting issues
scorbajio Aug 20, 2022
2c5a3fd
Store accounts in store phase
scorbajio Aug 20, 2022
ec61f20
Add logic for dividing hash ranges and adding them as tasks
scorbajio Aug 21, 2022
08d2607
Increment count by 1 before next iteration
scorbajio Aug 21, 2022
1281ef6
client/snap: remove unnecessary account fetcher logic
jochem-brouwer Aug 21, 2022
690f193
client/snap: correctly feed the right values to verifyRangeProof
jochem-brouwer Aug 21, 2022
5c63390
lint fixes
g11tech Aug 22, 2022
42605ec
small cleanup
g11tech Aug 23, 2022
67d784e
fix account fetcher with previous fixes
g11tech Sep 7, 2022
3cc3130
overhaul and simplify the fetcher and add partial results handling
g11tech Sep 11, 2022
465c95a
cleanup comments
g11tech Sep 12, 2022
79d34e3
fix fetch spec tests
g11tech Sep 12, 2022
61f8400
Check if right range is missing using return value from verifyRangeProof
scorbajio Oct 21, 2022
baa491e
Add accountfetcher tests
scorbajio Oct 21, 2022
1c3980c
Remove request test
scorbajio Oct 27, 2022
da73d2e
Correct return type
scorbajio Nov 19, 2022
5a73cd3
Add request test
scorbajio Nov 19, 2022
8181265
Merge branch 'snap-client-fetchers' of github.com:scorbajio/ethereumj…
scorbajio Jan 27, 2023
f380d5a
Add test for proof verification
scorbajio Jan 28, 2023
5143038
Fix linting issues
scorbajio Jan 28, 2023
2d33b05
Fix test
scorbajio Feb 10, 2023
5b46855
Update comment
scorbajio Feb 10, 2023
ba1ed2a
Sync with master
scorbajio Feb 11, 2023
c6c44f6
Merge remote-tracking branch 'origin/master' into snap-client-fetchers
g11tech Feb 14, 2023
420d087
reduce diff
g11tech Feb 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/client/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ export interface ConfigOptions {
*/
skeletonSubchainMergeMinimum?: number

maxRangeBytes?: number

maxAccountRange?: bigint
/**
* The time after which synced state is downgraded to unsynced
*/
Expand Down Expand Up @@ -288,6 +291,9 @@ export class Config {
public static readonly SAFE_REORG_DISTANCE = 100
public static readonly SKELETON_FILL_CANONICAL_BACKSTEP = 100
public static readonly SKELETON_SUBCHAIN_MERGE_MINIMUM = 1000
public static readonly MAX_RANGE_BYTES = 50000
// This should get like 100 accounts in this range
public static readonly MAX_ACCOUNT_RANGE = BigInt(2) ** BigInt(256) / BigInt(1_000_000)
public static readonly SYNCED_STATE_REMOVAL_PERIOD = 60000

public readonly logger: Logger
Expand Down Expand Up @@ -320,6 +326,8 @@ export class Config {
public readonly safeReorgDistance: number
public readonly skeletonFillCanonicalBackStep: number
public readonly skeletonSubchainMergeMinimum: number
public readonly maxRangeBytes: number
public readonly maxAccountRange: bigint
public readonly syncedStateRemovalPeriod: number

public readonly disableBeaconSync: boolean
Expand Down Expand Up @@ -370,6 +378,8 @@ export class Config {
options.skeletonFillCanonicalBackStep ?? Config.SKELETON_FILL_CANONICAL_BACKSTEP
this.skeletonSubchainMergeMinimum =
options.skeletonSubchainMergeMinimum ?? Config.SKELETON_SUBCHAIN_MERGE_MINIMUM
this.maxRangeBytes = options.maxRangeBytes ?? Config.MAX_RANGE_BYTES
this.maxAccountRange = options.maxAccountRange ?? Config.MAX_ACCOUNT_RANGE
this.syncedStateRemovalPeriod =
options.syncedStateRemovalPeriod ?? Config.SYNCED_STATE_REMOVAL_PERIOD

Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/net/protocol/snapprotocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface SnapProtocolOptions extends ProtocolOptions {
convertSlimBody?: boolean
}

type AccountData = {
export type AccountData = {
hash: Buffer
body: AccountBodyBuffer
}
Expand Down Expand Up @@ -72,7 +72,7 @@ export interface SnapProtocolMethods {
) => Promise<{ reqId: bigint; accounts: AccountData[]; proof: Buffer[] }>
getStorageRanges: (opts: GetStorageRangesOpts) => Promise<{
reqId: bigint
slots: StorageData[]
slots: StorageData[][]
proof: Buffer[]
}>
getByteCodes: (opts: GetByteCodesOpts) => Promise<{ reqId: bigint; codes: Buffer[] }>
Expand Down
1 change: 1 addition & 0 deletions packages/client/lib/service/fullethereumservice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export class FullEthereumService extends EthereumService {
config: this.config,
chain: this.chain,
timeout: this.timeout,
convertSlimBody: true,
}),
]
if (this.config.lightserv) {
Expand Down
322 changes: 322 additions & 0 deletions packages/client/lib/sync/fetcher/accountfetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
import { Trie } from '@ethereumjs/trie'
import { accountBodyToRLP, bigIntToBuffer, bufferToBigInt, setLengthLeft } from '@ethereumjs/util'

import { LevelDB } from '../../execution/level'
import { short } from '../../util'

import { Fetcher } from './fetcher'

import type { Peer } from '../../net/peer'
import type { AccountData } from '../../net/protocol/snapprotocol'
import type { FetcherOptions } from './fetcher'
import type { Job } from './types'

type AccountDataResponse = AccountData[] & { completed?: boolean }

/**
* Implements an snap1 based account fetcher
* @memberof module:sync/fetcher
*/
export interface AccountFetcherOptions extends FetcherOptions {
/** Root hash of the account trie to serve */
root: Buffer

/** The origin to start account fetcher from */
first: bigint

/** Range to eventually fetch */
count?: bigint

/** Destroy fetcher once all tasks are done */
destroyWhenDone?: boolean
}

// root comes from block?
export type JobTask = {
/** The origin to start account fetcher from */
first: bigint
/** Range to eventually fetch */
count: bigint
}

export class AccountFetcher extends Fetcher<JobTask, AccountData[], AccountData> {
/**
* The stateRoot for the fetcher which sorts of pin it to a snapshot.
* This might eventually be removed as the snapshots are moving and not static
*/
root: Buffer

/** The origin to start account fetcher from (including), by default starts from 0 (0x0000...) */
first: bigint

/** The range to eventually, by default should be set at BigInt(2) ** BigInt(256) + BigInt(1) - first */
count: bigint

/**
* Create new block fetcher
*/
constructor(options: AccountFetcherOptions) {
super(options)

this.root = options.root
this.first = options.first
this.count = options.count ?? BigInt(2) ** BigInt(256) - this.first

const fullJob = { task: { first: this.first, count: this.count } } as Job<
JobTask,
AccountData[],
AccountData
>
const origin = this.getOrigin(fullJob)
const limit = this.getLimit(fullJob)

this.debug(
`Account fetcher instantiated root=${short(this.root)} origin=${short(origin)} limit=${short(
limit
)} destroyWhenDone=${this.destroyWhenDone}`
)
}

private async verifyRangeProof(
stateRoot: Buffer,
origin: Buffer,
{ accounts, proof }: { accounts: AccountData[]; proof: Buffer[] }
): Promise<boolean> {
this.debug(
`verifyRangeProof accounts:${accounts.length} first=${short(accounts[0].hash)} last=${short(
accounts[accounts.length - 1].hash
)}`
)

for (let i = 0; i < accounts.length - 1; i++) {
// ensure the range is monotonically increasing
if (accounts[i].hash.compare(accounts[i + 1].hash) === 1) {
throw Error(
`Account hashes not monotonically increasing: ${i} ${accounts[i].hash} vs ${i + 1} ${
accounts[i + 1].hash
}`
)
}
}

const trie = new Trie({ db: new LevelDB() })
const keys = accounts.map((acc: any) => acc.hash)
const values = accounts.map((acc: any) => accountBodyToRLP(acc.body))
// convert the request to the right values
return trie.verifyRangeProof(stateRoot, origin, keys[keys.length - 1], keys, values, <any>proof)
}

private getOrigin(job: Job<JobTask, AccountData[], AccountData>): Buffer {
const { task, partialResult } = job
const { first } = task
// Snap protocol will automatically pad it with 32 bytes left, so we don't need to worry
const origin = partialResult
? bigIntToBuffer(bufferToBigInt(partialResult[partialResult.length - 1].hash) + BigInt(1))
: bigIntToBuffer(first)
return setLengthLeft(origin, 32)
}

private getLimit(job: Job<JobTask, AccountData[], AccountData>): Buffer {
const { task } = job
const { first, count } = task
const limit = bigIntToBuffer(first + BigInt(count) - BigInt(1))
return setLengthLeft(limit, 32)
}

/**
* Request results from peer for the given job.
* Resolves with the raw result
* If `undefined` is returned, re-queue the job.
* @param job
* @param peer
*/
async request(
job: Job<JobTask, AccountData[], AccountData>
): Promise<AccountDataResponse | undefined> {
const { peer } = job
const origin = this.getOrigin(job)
const limit = this.getLimit(job)

const rangeResult = await peer!.snap!.getAccountRange({
root: this.root,
origin,
limit,
bytes: BigInt(this.config.maxRangeBytes),
})

const peerInfo = `id=${peer?.id.slice(0, 8)} address=${peer?.address}`

// eslint-disable-next-line eqeqeq
if (rangeResult === undefined) {
return undefined
} else {
// validate the proof
try {
// verifyRangeProof will also verify validate there are no missed states between origin and
Copy link
Contributor

@g11tech g11tech Sep 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jochem-brouwer @scorbajio pls review if my understanding of proof verification is correct here w.r.t origin that using origin in verifyRangeProof (see the definition of this private function) will validate that there are no missed accounts between origin and the first account recieved

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It /should/ throw if there are missing nodes, but we should probably test this (or it is already tested in trie)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way you are verifying makes sense to me. Could be useful to add some try-except blocks and log any errors in each function body.

// response data and returns a boolean indiciating if there are more accounts remaining to fetch
// in the specified range
const isMissingRightRange: boolean = await this.verifyRangeProof(
this.root,
origin,
rangeResult
)

// Check if there is any pending data to be synced to the right
let completed: boolean
if (isMissingRightRange) {
this.debug(
`Peer ${peerInfo} returned missing right range account=${rangeResult.accounts[
rangeResult.accounts.length - 1
].hash.toString('hex')} limit=${limit.toString('hex')}`
)
completed = false
} else {
completed = true
}
return Object.assign([], rangeResult.accounts, { completed })
} catch (err) {
throw Error(`InvalidAccountRange: ${err}`)
}
}
}

/**
* Process the reply for the given job.
* If the reply contains unexpected data, return `undefined`,
* this re-queues the job.
* @param job fetch job
* @param result result data
*/
process(
job: Job<JobTask, AccountData[], AccountData>,
result: AccountDataResponse
): AccountData[] | undefined {
const fullResult = (job.partialResult ?? []).concat(result)
job.partialResult = undefined
if (result.completed === true) {
return fullResult
} else {
// Save partial result to re-request missing items.
job.partialResult = fullResult
}
}

/**
* Store fetch result. Resolves once store operation is complete.
* @param result fetch result
*/
async store(result: AccountData[]): Promise<void> {
this.debug(`Stored ${result.length} accounts in account trie`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right now store doesn't do anything, as a separate helper class with be added in a followup PR to track and aid the fetcher

}

/**
* Generate list of tasks to fetch. Modifies `first` and `count` to indicate
* remaining items apart from the tasks it pushes in the queue
*
* Divides the full 256-bit range of hashes into ranges of @maxAccountRange
* size and turnes each range into a task for the fetcher
*/

tasks(first = this.first, count = this.count, maxTasks = this.config.maxFetcherJobs): JobTask[] {
const max = this.config.maxAccountRange
const tasks: JobTask[] = []
let debugStr = `origin=${short(setLengthLeft(bigIntToBuffer(first), 32))}`
let pushedCount = BigInt(0)
const startedWith = first

while (count >= BigInt(max) && tasks.length < maxTasks) {
tasks.push({ first, count: max })
first += BigInt(max)
count -= BigInt(max)
pushedCount += BigInt(max)
}
if (count > BigInt(0) && tasks.length < maxTasks) {
tasks.push({ first, count })
first += BigInt(count)
pushedCount += count
count = BigInt(0)
}

// If we started with where this.first was, i.e. there are no gaps and hence
// we can move this.first to where its now, and reduce count by pushedCount
if (startedWith === this.first) {
this.first = first
this.count = this.count - pushedCount
}

debugStr += ` limit=${short(
setLengthLeft(bigIntToBuffer(startedWith + pushedCount - BigInt(1)), 32)
)}`
this.debug(`Created new tasks num=${tasks.length} ${debugStr}`)
return tasks
}

nextTasks(): void {
if (this.in.length === 0 && this.count > BigInt(0)) {
const fullJob = { task: { first: this.first, count: this.count } } as Job<
JobTask,
AccountData[],
AccountData
>
const origin = this.getOrigin(fullJob)
const limit = this.getLimit(fullJob)

this.debug(`Fetcher pending with origin=${short(origin)} limit=${short(limit)}`)
const tasks = this.tasks()
for (const task of tasks) {
this.enqueueTask(task)
}
}
}

/**
* Clears all outstanding tasks from the fetcher
*/
clear() {
return
}

/**
* Returns an idle peer that can process a next job.
*/
peer(): Peer | undefined {
return this.pool.idle((peer) => 'snap' in peer)
}

processStoreError(
error: Error,
_task: JobTask
): { destroyFetcher: boolean; banPeer: boolean; stepBack: bigint } {
const stepBack = BigInt(0)
const destroyFetcher =
!(error.message as string).includes(`InvalidRangeProof`) &&
!(error.message as string).includes(`InvalidAccountRange`)
const banPeer = true
return { destroyFetcher, banPeer, stepBack }
}

/**
* Job log format helper.
* @param job
* @param withIndex pass true to additionally output job.index
*/
jobStr(job: Job<JobTask, AccountData[], AccountData>, withIndex = false) {
let str = ''
if (withIndex) {
str += `index=${job.index} `
}

const origin = this.getOrigin(job)
const limit = this.getLimit(job)

let partialResult
if (job.partialResult) {
partialResult = ` partialResults=${job.partialResult.length}`
} else {
partialResult = ''
}

str += `origin=${short(origin)} limit=${short(limit)}${partialResult}`
return str
}
}
Loading