-
Notifications
You must be signed in to change notification settings - Fork 249
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
feat: implement light client verification methods #1116
Open
austinabell
wants to merge
34
commits into
near:master
Choose a base branch
from
austinabell:light_client
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
a537493
feat: implement light client verification methods
austinabell 39b4575
chore: docs
austinabell 879a7ab
refactor: move light-client APIs to own crate, migrate sha impl
austinabell 17f0cf3
chore: changeset
austinabell f47b4fa
chore: add docs to validation steps
austinabell f8612ff
refactor: switch function params to be an object rather than positional
austinabell b280d8e
chore: revert naj test changes
austinabell 4e81fae
test: adds back light client block verification test
austinabell 09f957f
chore: lint
austinabell 8ad77ae
test: add back execution proof verification in NAJ test
austinabell 153dfa7
test: add execution proof test vectors
austinabell 59b4f14
chore: address comments before refactoring
austinabell 68601e5
refactor: move Enum class to types
austinabell 73644e2
refactor: move light client logic into separate files
austinabell ab94d18
refactor: move borsh utils to own file
austinabell e15fad7
Merge branch 'master' into light_client
austinabell 206df23
chore: remove todo from updated types
austinabell de393fb
chore: update changeset
austinabell daca5d2
test: move execution verification providers check into accounts
austinabell f509b3f
Merge branch 'master' into light_client
austinabell 0dde04f
Merge branch 'master' into light_client
austinabell 29c9ea6
fix: bug with bp signature validation
austinabell fe45242
refactor: move block hash generation to after trivial validation
austinabell 20bff2e
chore: lint fix
austinabell 0559e23
fix: bn comparison bug
austinabell d867179
fix: execution test parameter format from changes
austinabell 8731572
Merge branch 'master' into light_client
austinabell f766671
fix: changes based on review comments
austinabell c5d1378
Merge branch 'master' into light_client
austinabell a8e19d8
chore: empty commit to re-trigger flaky CI
austinabell e7c26ce
Merge branch 'master' into light_client
vikinatora d97a801
fix: pnpm-lock.yaml
vikinatora 35c12da
chore: lock package version
vikinatora 6aed942
Merge remote-tracking branch 'upstream/master' into light_client
vikinatora 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
"@near-js/light-client": patch | ||
"near-api-js": patch | ||
"@near-js/types": patch | ||
--- | ||
|
||
Implement light client block and execution validation |
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,8 @@ | ||
# @near-js/light-client | ||
|
||
NEAR light client verification utilities. Based on the [Light Client spec](https://github.com/near/NEPs/blob/master/specs/ChainSpec/LightClient.md) | ||
|
||
# License | ||
|
||
This repository is distributed under the terms of both the MIT license and the Apache License (Version 2.0). | ||
See [LICENSE](https://github.com/near/near-api-js/blob/master/LICENSE) and [LICENSE-APACHE](https://github.com/near/near-api-js/blob/master/LICENSE-APACHE) for details. |
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,5 @@ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
collectCoverage: true | ||
}; |
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,36 @@ | ||
{ | ||
"name": "@near-js/light-client", | ||
"version": "0.0.1", | ||
"description": "TypeScript API for NEAR light client verification", | ||
"main": "lib/index.js", | ||
"scripts": { | ||
"build": "pnpm compile", | ||
"compile": "tsc -p tsconfig.json", | ||
"lint:js": "eslint -c ../../.eslintrc.js.yml test/**/*.js --no-eslintrc", | ||
"lint:js:fix": "eslint -c ../../.eslintrc.js.yml test/**/*.js --no-eslintrc --fix", | ||
"lint:ts": "eslint -c ../../.eslintrc.ts.yml src/**/*.ts --no-eslintrc", | ||
"lint:ts:fix": "eslint -c ../../.eslintrc.ts.yml src/**/*.ts --no-eslintrc --fix", | ||
"test": "jest test" | ||
}, | ||
"keywords": [], | ||
"author": "", | ||
"license": "ISC", | ||
"dependencies": { | ||
"@near-js/crypto": "workspace:*", | ||
"@near-js/types": "workspace:*", | ||
"bn.js": "5.2.1", | ||
"bs58": "4.0.0", | ||
"borsh": "0.7.0", | ||
"js-sha256": "0.9.0" | ||
}, | ||
"devDependencies": { | ||
"@near-js/providers": "workspace:*", | ||
"@types/node": "18.11.18", | ||
"jest": "26.0.1", | ||
"ts-jest": "26.5.6", | ||
"typescript": "4.9.4" | ||
}, | ||
"files": [ | ||
"lib" | ||
] | ||
} |
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,162 @@ | ||
import { computeBlockHash, combineHash } from './utils'; | ||
import { | ||
SCHEMA, | ||
BorshApprovalInner, | ||
BorshValidatorStakeView, | ||
BorshValidatorStakeViewV1, | ||
BorshValidatorStakeViewWrapper, | ||
} from './borsh'; | ||
import bs58 from 'bs58'; | ||
import { sha256 } from 'js-sha256'; | ||
import { | ||
LightClientBlockLiteView, | ||
NextLightClientBlockResponse, | ||
ValidatorStakeView, | ||
} from '@near-js/types'; | ||
import { PublicKey } from '@near-js/crypto'; | ||
import BN from 'bn.js'; | ||
import { serialize } from 'borsh'; | ||
|
||
export interface ValidateLightClientBlockParams { | ||
lastKnownBlock: LightClientBlockLiteView; | ||
currentBlockProducers: ValidatorStakeView[]; | ||
newBlock: NextLightClientBlockResponse; | ||
} | ||
|
||
/** | ||
* Validates a light client block response from the RPC against the last known block and block | ||
* producer set. | ||
* | ||
* @param lastKnownBlock The last light client block retrieved. This must be the block at the epoch before newBlock. | ||
* @param currentBlockProducers The block producer set for the epoch of the last known block. | ||
* @param newBlock The new block to validate. | ||
*/ | ||
export function validateLightClientBlock({ | ||
lastKnownBlock, | ||
currentBlockProducers, | ||
newBlock, | ||
}: ValidateLightClientBlockParams) { | ||
// Numbers for each step references the spec: | ||
// https://github.com/near/NEPs/blob/c7d72138117ed0ab86629a27d1f84e9cce80848f/specs/ChainSpec/LightClient.md | ||
// (1) Verify that the block height is greater than the last known block. | ||
if (newBlock.inner_lite.height <= lastKnownBlock.inner_lite.height) { | ||
throw new Error( | ||
'New block must be at least the height of the last known block' | ||
); | ||
} | ||
|
||
// (2) Verify that the new block is in the same epoch or in the next epoch known to the last | ||
// known block. | ||
if ( | ||
newBlock.inner_lite.epoch_id !== lastKnownBlock.inner_lite.epoch_id && | ||
newBlock.inner_lite.epoch_id !== lastKnownBlock.inner_lite.next_epoch_id | ||
) { | ||
throw new Error( | ||
'New block must either be in the same epoch or the next epoch from the last known block' | ||
); | ||
} | ||
|
||
const blockProducers: ValidatorStakeView[] = currentBlockProducers; | ||
if (newBlock.approvals_after_next.length < blockProducers.length) { | ||
throw new Error( | ||
'Number of approvals for next epoch must be at least the number of current block producers' | ||
); | ||
} | ||
|
||
// (4) and (5) | ||
// (4) `approvals_after_next` contains valid signatures on the block producer approval messages. | ||
// (5) The signatures present represent more than 2/3 of the total stake. | ||
const totalStake = new BN(0); | ||
const approvedStake = new BN(0); | ||
|
||
const currentBlockHash = computeBlockHash(newBlock); | ||
const nextBlockHash = combineHash( | ||
bs58.decode(newBlock.next_block_inner_hash), | ||
currentBlockHash | ||
); | ||
|
||
for (let i = 0; i < blockProducers.length; i++) { | ||
const approval = newBlock.approvals_after_next[i]; | ||
const stake = blockProducers[i].stake; | ||
|
||
totalStake.iadd(new BN(stake)); | ||
|
||
if (approval === null) { | ||
continue; | ||
} | ||
|
||
approvedStake.iadd(new BN(stake)); | ||
|
||
const publicKey = PublicKey.fromString(blockProducers[i].public_key); | ||
const signature = bs58.decode(approval.split(':')[1]); | ||
|
||
const approvalEndorsement = serialize( | ||
SCHEMA, | ||
new BorshApprovalInner({ endorsement: nextBlockHash }) | ||
); | ||
|
||
const approvalHeight: BN = new BN(newBlock.inner_lite.height + 2); | ||
const approvalHeightLe = approvalHeight.toArrayLike(Buffer, 'le', 8); | ||
const approvalMessage = new Uint8Array([ | ||
...approvalEndorsement, | ||
...approvalHeightLe, | ||
]); | ||
|
||
if (!publicKey.verify(approvalMessage, signature)) { | ||
throw new Error( | ||
`Invalid approval message signature for validator ${blockProducers[i].account_id}` | ||
); | ||
} | ||
} | ||
|
||
// (5) Calculates the 2/3 threshold and checks that the approved stake accumulated above | ||
// exceeds it. | ||
const threshold = totalStake.mul(new BN(2)).div(new BN(3)); | ||
if (approvedStake.lte(threshold)) { | ||
throw new Error('Approved stake does not exceed the 2/3 threshold'); | ||
} | ||
|
||
// (6) Verify that if the new block is in the next epoch, the hash of the next block producers | ||
// equals the `next_bp_hash` provided in that block. | ||
if ( | ||
newBlock.inner_lite.epoch_id === lastKnownBlock.inner_lite.next_epoch_id | ||
) { | ||
// (3) If the block is in a new epoch, then `next_bps` must be present. | ||
if (!newBlock.next_bps) { | ||
throw new Error( | ||
'New block must include next block producers if a new epoch starts' | ||
); | ||
} | ||
|
||
const bpsHash = hashBlockProducers(newBlock.next_bps); | ||
|
||
if (!bpsHash.equals(bs58.decode(newBlock.inner_lite.next_bp_hash))) { | ||
throw new Error('Next block producers hash doesn\'t match'); | ||
} | ||
} | ||
} | ||
|
||
function hashBlockProducers(bps: ValidatorStakeView[]): Buffer { | ||
const borshBps: BorshValidatorStakeView[] = bps.map((bp) => { | ||
if (bp.validator_stake_struct_version) { | ||
if (bp.validator_stake_struct_version !== 'V1') { | ||
throw new Error( | ||
'Only version 1 of the validator stake struct is supported' | ||
); | ||
} | ||
} | ||
return new BorshValidatorStakeView({ | ||
v1: new BorshValidatorStakeViewV1({ | ||
account_id: bp.account_id, | ||
public_key: PublicKey.fromString(bp.public_key), | ||
stake: bp.stake, | ||
}), | ||
}); | ||
}); | ||
const serializedBps = serialize( | ||
SCHEMA, | ||
// NOTE: just wrapping because borsh-js requires this type to be in the schema for some reason | ||
new BorshValidatorStakeViewWrapper({ bps: borshBps }) | ||
); | ||
return Buffer.from(sha256.array(serializedBps)); | ||
} |
Oops, something went wrong.
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.
The python implementation checks for strict equality. The spec uses a zip of the two arrays. Should we also assert the lengths are equal?
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.
That implementation is wrong. The spec says, and in practice, there are blocks with more block producers than approvals because in the case of validator rotations, both the old and new bps must be included.
I can find and give a block where this is the case if needed, but I don't have an example off-hand.
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.
Gotcha, I trust your judgment here. Thanks!
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.
References, for posterity, to remove the trust aspect for others 😄 : https://github.com/near/NEPs/blob/master/specs/ChainSpec/LightClient.md#signature-verification. Also, line in rainbow bridge implementation, which is used in production https://github.com/aurora-is-near/rainbow-bridge/blob/33ca808b45cb5e9cf2e27f741b0f6e42d97c276b/contracts/eth/nearbridge/contracts/NearBridge.sol#L212 (python client just used for tests)
Also, anyone would be able to switch this to strict equal and see that it fails on recent blocks.