-
Notifications
You must be signed in to change notification settings - Fork 781
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
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 4b8f867
Add accountfetcher import
scorbajio aac8930
Add AccountFetcher as possible type for Synchronizer.fetcher
scorbajio 4df7ffd
Place call to getAccountRange inside of fetcher
scorbajio 1fb1851
Place call to getAccountRange() in accountfetcher and comment it out
scorbajio 0a179e5
Add account fetcher base
scorbajio 83efe1c
Add accountfetcher import
scorbajio 0f65a77
add account fetcher getter setter in snapsync
g11tech 3bb257b
Change order of importing accountfetcher in index file
scorbajio 15a44b8
Change bytes parameter to be per task
scorbajio fcf0a91
Remove root and bytes from task inputs and make them fetcher variables
scorbajio 7204fe5
Correct log message
scorbajio 2c83c83
Add debug console log statement
scorbajio b13b75a
Fix linting issues
scorbajio 72b9d97
Add account to mpt and check validity with root and proof
scorbajio df3db62
Set root of trie
scorbajio baac630
Add checks to fetcher.request()
scorbajio 0cc51d2
client/snap: fix getAccountRange return type
jochem-brouwer 4219dcb
client/snap: pass first proof
jochem-brouwer ec2272d
client/snap: add utility to convert slim account to a normal RLPd acc…
jochem-brouwer f4e2204
client/snap: implement account range db dump
jochem-brouwer e24cc3a
Update to use verifyRangeProof
scorbajio b616aa3
Correct some messages
scorbajio 0645986
Update verifyProofRange input for first account hash to be fetcher or…
scorbajio 875a186
Fix linting issues
scorbajio fae5bbd
Store accounts in store phase
scorbajio 96d4544
Add logic for dividing hash ranges and adding them as tasks
scorbajio a8926f4
Increment count by 1 before next iteration
scorbajio 4541027
client/snap: remove unnecessary account fetcher logic
jochem-brouwer 34e4742
client/snap: correctly feed the right values to verifyRangeProof
jochem-brouwer a008ffb
lint fixes
g11tech c370391
small cleanup
g11tech 70fd5e3
fix account fetcher with previous fixes
g11tech cda3040
overhaul and simplify the fetcher and add partial results handling
g11tech 61fcf6a
cleanup comments
g11tech 7b5b1f3
fix fetch spec tests
g11tech 020c41e
Check if right range is missing using return value from verifyRangeProof
scorbajio 593baa0
Add accountfetcher tests
scorbajio 0dc19dd
Remove request test
scorbajio b288e16
Correct return type
scorbajio 0e386c8
Add request test
scorbajio 1f454b0
Add account fetcher base
scorbajio d49df23
Add accountfetcher import
scorbajio 9060c61
Add AccountFetcher as possible type for Synchronizer.fetcher
scorbajio dd24731
Place call to getAccountRange inside of fetcher
scorbajio c549a8e
Place call to getAccountRange() in accountfetcher and comment it out
scorbajio 126570d
Add account fetcher base
scorbajio 02ddf55
Add accountfetcher import
scorbajio 8179a33
add account fetcher getter setter in snapsync
g11tech 3495a65
Change order of importing accountfetcher in index file
scorbajio c37507d
Change bytes parameter to be per task
scorbajio 97f9b39
Remove root and bytes from task inputs and make them fetcher variables
scorbajio fee1dfa
Correct log message
scorbajio 4f1f708
Add debug console log statement
scorbajio 0081357
Fix linting issues
scorbajio c159ea2
Add account to mpt and check validity with root and proof
scorbajio 0b3ab58
Set root of trie
scorbajio 308922b
Add checks to fetcher.request()
scorbajio 0f3520a
client/snap: fix getAccountRange return type
jochem-brouwer 2340713
client/snap: pass first proof
jochem-brouwer 4b9dbaa
client/snap: add utility to convert slim account to a normal RLPd acc…
jochem-brouwer 89da73c
client/snap: implement account range db dump
jochem-brouwer 311a6b4
Update to use verifyRangeProof
scorbajio 8215fc5
Correct some messages
scorbajio 3c2006c
Update verifyProofRange input for first account hash to be fetcher or…
scorbajio d5ef441
Fix linting issues
scorbajio 2c5a3fd
Store accounts in store phase
scorbajio ec61f20
Add logic for dividing hash ranges and adding them as tasks
scorbajio 08d2607
Increment count by 1 before next iteration
scorbajio 1281ef6
client/snap: remove unnecessary account fetcher logic
jochem-brouwer 690f193
client/snap: correctly feed the right values to verifyRangeProof
jochem-brouwer 5c63390
lint fixes
g11tech 42605ec
small cleanup
g11tech 67d784e
fix account fetcher with previous fixes
g11tech 3cc3130
overhaul and simplify the fetcher and add partial results handling
g11tech 465c95a
cleanup comments
g11tech 79d34e3
fix fetch spec tests
g11tech 61f8400
Check if right range is missing using return value from verifyRangeProof
scorbajio baa491e
Add accountfetcher tests
scorbajio 1c3980c
Remove request test
scorbajio da73d2e
Correct return type
scorbajio 5a73cd3
Add request test
scorbajio 8181265
Merge branch 'snap-client-fetchers' of github.com:scorbajio/ethereumj…
scorbajio f380d5a
Add test for proof verification
scorbajio 5143038
Fix linting issues
scorbajio 2d33b05
Fix test
scorbajio 5b46855
Update comment
scorbajio ba1ed2a
Sync with master
scorbajio c6c44f6
Merge remote-tracking branch 'origin/master' into snap-client-fetchers
g11tech 420d087
reduce diff
g11tech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
// 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`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 nomissed
accounts between origin and the first account recievedThere was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.