Skip to content

Commit

Permalink
Withdrawal address validation (#9)
Browse files Browse the repository at this point in the history
* add withdrawal address checkers, validate restake vault

* 1.8.0

* add getOperatorAddress tests

* 1.8.1

* update README

* 1.8.2

* 1.8.3

* improve logic, add gqlRequest helper

* 1.8.4

* code style improves

* 1.8.5
  • Loading branch information
dfkadyr authored Jul 9, 2024
1 parent 90fb256 commit f9754f1
Show file tree
Hide file tree
Showing 16 changed files with 246 additions and 25 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
- **Signature Verification:** Leverages BLS (Boneh-Lynn-Shacham) signatures to authenticate deposit data against given public keys.
- **Progress Callbacks:** Provides real-time feedback on processing progress, suitable for applications processing large datasets.
- **Error Handling:** Implements comprehensive error handling, with custom error types and callback functions for robust error management.

- **Withdrawal Address Verification:** The verification is carried out to confirm that the Withdrawal Address is included in the deposit data and that it matches any of the eigen pods addresses that we request directly for the restake vault.
## Installation and Setup
```bash
npm i @stakewise/v3-deposit-data-parser
Expand Down Expand Up @@ -76,9 +76,11 @@ self.addEventListener('message', async (event) => {
| Type | Message |
|------------|---------|
| `EMPTY_FILE` | Deposit data file is empty
| `EIGEN_PODS_EMPTY` | No Eigen pods in the Vault
| `INVALID_JSON_FORMAT` | Deposit data file must be in JSON format
| `MERKLE_TREE_GENERATION_ERROR` | Failed to generate the Merkle tree
| `INVALID_PUBLIC_KEY_FORMAT` | Failed to parse deposit data public key
| `INVALID_WITHDRAW_ADDRESS` | The withdrawal addresses don’t match Eigen pods
| `MISSING_FIELDS` | Failed to verify the deposit data public keys. Missing fields: {fields}
| `DUPLICATE_PUBLIC_KEYS` | Failed to verify the deposit data public keys. All the entries must be unique.
| `INVALID_SIGNATURE` | Failed to verify the deposit data signatures. Please make sure the file is generated for the {network} network.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.7.0",
"version": "1.8.5",
"main": "dist/index.js",
"name": "@stakewise/v3-deposit-data-parser",
"description": "v3-deposit-data-parser",
Expand Down
5 changes: 2 additions & 3 deletions src/parser/getDepositData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@ describe('getDepositData',() => {
const networks: SupportedNetworks[] = [ 'holesky', 'mainnet', 'gnosis', 'chiado' ]

networks.forEach(network => {
it(`processes valid amount with "${network}" network`, () => {
it(`processes valid amount with "${network}" network`, async () => {
const data: DepositDataInput = { ...mockInput, network }
const amount = getAmount(network)
const result = getDepositData(data)
const result = await getDepositData(data)

expect(getDepositData(data)).toEqual(result)
expect(result.amount).toEqual(amount)
})
})
Expand Down
24 changes: 20 additions & 4 deletions src/parser/getDepositData.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import { DepositData, SupportedNetworks } from './types'
import { getWithdrawalCredentials, prefix0x, ParserError, ErrorTypes, getBytes, getAmount } from './helpers'
import {
prefix0x,
ErrorTypes,
ParserError,
requests,
getBytes,
getAmount,
getEigenPodAddress,
getWithdrawalCredentials,
} from './helpers'


export type DepositDataInput = {
pubkey: string
vaultAddress: string
withdrawalAddress?: string
network: SupportedNetworks
}

const getDepositData = (values: DepositDataInput): DepositData => {
const { pubkey, vaultAddress, network } = values
const getDepositData = async (values: DepositDataInput): Promise<DepositData> => {
const { pubkey, vaultAddress, withdrawalAddress, network } = values

const isRestakeVault = await requests.checkIsRestakeVault(vaultAddress, network)

const withdrawalCredentialAddress = isRestakeVault
? await getEigenPodAddress({ vaultAddress, withdrawalAddress, network })
: vaultAddress

try {
const withdrawalCredentials = getWithdrawalCredentials(vaultAddress)
const withdrawalCredentials = getWithdrawalCredentials(withdrawalCredentialAddress)

const depositData = {
amount: getAmount(network),
Expand Down
8 changes: 6 additions & 2 deletions src/parser/helpers/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@ type DynamicValues = Record<string, any>

export enum ErrorTypes {
EMPTY_FILE = 'EMPTY_FILE',
EIGEN_PODS_EMPTY = 'EIGEN_PODS_EMPTY',
MISSING_FIELDS = 'MISSING_FIELDS',
INVALID_SIGNATURE = 'INVALID_SIGNATURE',
INVALID_JSON_FORMAT = 'INVALID_JSON_FORMAT',
DUPLICATE_PUBLIC_KEYS = 'DUPLICATE_PUBLIC_KEYS',
INVALID_WITHDRAW_ADDRESS = 'INVALID_WITHDRAW_ADDRESS',
INVALID_PUBLIC_KEY_FORMAT = 'INVALID_PUBLIC_KEY_FORMAT',
MERKLE_TREE_GENERATION_ERROR = 'MERKLE_TREE_GENERATION_ERROR',
}

export const ErrorMessages: Record<ErrorTypes, string> = {
[ErrorTypes.EMPTY_FILE]: 'Deposit data file is empty.',
[ErrorTypes.EIGEN_PODS_EMPTY]: 'No Eigen pods in the Vault',
[ErrorTypes.INVALID_JSON_FORMAT]: 'Deposit data file must be in JSON format.',
[ErrorTypes.DUPLICATE_PUBLIC_KEYS]: 'Failed to verify the deposit data public keys. All the entries must be unique.',
[ErrorTypes.INVALID_PUBLIC_KEY_FORMAT]: 'Failed to parse deposit data public key',
[ErrorTypes.MERKLE_TREE_GENERATION_ERROR]: 'Failed to generate the Merkle tree',
[ErrorTypes.INVALID_PUBLIC_KEY_FORMAT]: 'Failed to parse deposit data public key',
[ErrorTypes.INVALID_WITHDRAW_ADDRESS]: `The withdrawal addresses don’t match Eigen pods`,
[ErrorTypes.MISSING_FIELDS]: 'Failed to verify the deposit data public keys. Missing fields: {fields}',
[ErrorTypes.DUPLICATE_PUBLIC_KEYS]: 'Failed to verify the deposit data public keys. All the entries must be unique.',
[ErrorTypes.INVALID_SIGNATURE]: `
Failed to verify the deposit data signatures. Please make sure the file is generated for the {network} network.
`,
Expand Down
68 changes: 68 additions & 0 deletions src/parser/helpers/getEigenPodAddress.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { getEigenPods } from './requests'
import ParserError, { ErrorTypes } from './errors'
import getEigenPodAddress from './getEigenPodAddress'
import type { GetEigenPodAddressInput } from './getEigenPodAddress'


type MockGetEigenPods = jest.MockedFunction<typeof getEigenPods>

jest.mock('./requests/getEigenPods')

describe('getEigenPodAddress', () => {
afterEach(() => {
jest.clearAllMocks()
})

it('should return the correct operator address for valid input', async () => {
const input: GetEigenPodAddressInput = {
vaultAddress: '0xe05d8895e8b3ba51ce4f89b337c621889d3f38bf',
withdrawalAddress: '0x6B8c2EBf69aE6c7ae583F219D464637Bd1b6bFa3',
network: 'holesky',
};

(getEigenPods as MockGetEigenPods).mockResolvedValue([
{ address: '0x6B8c2EBf69aE6c7ae583F219D464637Bd1b6bFa3' },
])

const result = await getEigenPodAddress(input)
expect(result).toBe('0x6B8c2EBf69aE6c7ae583F219D464637Bd1b6bFa3')
})

it('should throw an error if withdrawalAddress is missing', async () => {
const input = {
vaultAddress: '0xe05d8895e8b3ba51ce4f89b337c621889d3f38bf',
network: 'holesky',
} as GetEigenPodAddressInput

const errorText = new ParserError(ErrorTypes.MISSING_FIELDS, { fields: [ 'withdrawal_address' ] })
await expect(getEigenPodAddress(input)).rejects.toThrow(errorText)
})

it('should throw an error if eigenPods is empty', async () => {
const input = {
vaultAddress: '0xe05d8895e8b3ba51ce4f89b337c621889d3f38bf',
withdrawalAddress: '0x6B8c2EBf69aE6c7ae583F219D464637Bd1b6bFa3',
network: 'holesky',
} as GetEigenPodAddressInput

(getEigenPods as MockGetEigenPods).mockResolvedValue([])

const errorText = new ParserError(ErrorTypes.EIGEN_PODS_EMPTY)
await expect(getEigenPodAddress(input)).rejects.toThrow(errorText)
})

it('should throw an error if operator address is not found in eigenPods', async () => {
const input = {
vaultAddress: '0xe05d8895e8b3ba51ce4f89b337c621889d3f38bf',
withdrawalAddress: '0x6B8c2EBf69aE6c7ae583F219D464637Bd1b6bFa3',
network: 'holesky',
} as GetEigenPodAddressInput

(getEigenPods as MockGetEigenPods).mockResolvedValue([
{ address: '0xe05d8895e8b3ba51ce4f89b337c621889d3f38bf' },
])

const errorText = new ParserError(ErrorTypes.INVALID_WITHDRAW_ADDRESS)
await expect(getEigenPodAddress(input)).rejects.toThrow(errorText)
})
})
35 changes: 35 additions & 0 deletions src/parser/helpers/getEigenPodAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getEigenPods } from './requests'
import { SupportedNetworks } from '../types'
import ParserError, { ErrorTypes } from './errors'


export type GetEigenPodAddressInput = {
vaultAddress: string
withdrawalAddress?: string
network: SupportedNetworks
}

const getEigenPodAddress = async (values: GetEigenPodAddressInput): Promise<string> => {
const { vaultAddress, withdrawalAddress, network } = values

if (!withdrawalAddress) {
throw new ParserError(ErrorTypes.MISSING_FIELDS, { fields: [ 'withdrawal_address' ] })
}

const eigenPods = await getEigenPods(vaultAddress, network)

if (!eigenPods?.length) {
throw new ParserError(ErrorTypes.EIGEN_PODS_EMPTY)
}

const eigenPod = eigenPods.find((eigenPod) => eigenPod.address === withdrawalAddress)

if (!eigenPod) {
throw new ParserError(ErrorTypes.INVALID_WITHDRAW_ADDRESS)
}

return eigenPod.address
}


export default getEigenPodAddress
2 changes: 2 additions & 0 deletions src/parser/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export { getBytes } from './getBytes'
export * as requests from './requests'
export { default as prefix0x } from './prefix0x'
export { default as mockData } from './mockData'
export { default as getAmount } from './getAmount'
export { default as containers } from './containers'
export { default as computeDomain } from './computeDomain'
export { default as getForkVersion } from './getForkVersion'
export { default as getEigenPodAddress } from './getEigenPodAddress'
export { default as ParserError, ErrorMessages, ErrorTypes } from './errors'
export { default as getWithdrawalCredentials } from './getWithdrawalCredentials'
20 changes: 20 additions & 0 deletions src/parser/helpers/requests/checkIsRestakeVault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import gqlRequest from './gqlRequest'
import { SupportedNetworks } from '../../types'


const checkIsRestakeVault = async (vaultId: string, network: SupportedNetworks) => {
const query = `query Vault($vaultId: ID!) { vault(id: $vaultId) { isRestake }}`
const variables = { vaultId: vaultId.toLowerCase() }

try {
const data = await gqlRequest({ query, variables }, network)

return data?.vault?.isRestake
}
catch (error) {
console.error('Error fetching isRestake:', error)
}
}


export default checkIsRestakeVault
22 changes: 22 additions & 0 deletions src/parser/helpers/requests/getEigenPods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import gqlRequest from './gqlRequest'
import { SupportedNetworks } from '../../types'


type EigenPods = { address: string }[]

const getEigenPods = async (vaultId: string, network: SupportedNetworks) => {
const query = `query EigenPods($vaultId: ID!) { eigenPods(where: { vault: $vaultId }) { address }}`
const variables = { vaultId: vaultId.toLowerCase() }

try {
const data = await gqlRequest({ query, variables }, network)

return data?.eigenPods as EigenPods
}
catch (error) {
console.error('Error fetching EigenPods:', error)
}
}


export default getEigenPods
42 changes: 42 additions & 0 deletions src/parser/helpers/requests/gqlRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { urls } from './urls'
import { SupportedNetworks } from '../../types'


type GqlRequestOptions = {
query: string
variables?: Record<string, any>
}

const gqlRequest = async (options: GqlRequestOptions, network: SupportedNetworks) => {
try {
const response = await fetch(urls[network], {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
query: options.query,
variables: options.variables,
}),
})

if (response?.status !== 200) {
throw new Error(`API request failed: ${response?.url}`)
}

const result = await response.json()

if (result?.errors) {
throw new Error(result.errors[0].message)
}

return result?.data
}
catch (error) {
console.error('Error in gqlRequest:', error)
throw error
}
}


export default gqlRequest
2 changes: 2 additions & 0 deletions src/parser/helpers/requests/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as getEigenPods } from './getEigenPods'
export { default as checkIsRestakeVault } from './checkIsRestakeVault'
9 changes: 9 additions & 0 deletions src/parser/helpers/requests/urls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { SupportedNetworks } from 'parser/types'


export const urls: Record<SupportedNetworks, string> = {
'holesky': 'https://holesky-graph.stakewise.io/subgraphs/name/stakewise/stakewise',
'mainnet': 'https://mainnet-graph.stakewise.io/subgraphs/name/stakewise/stakewise',
'gnosis': 'https://graph-gno.stakewise.io/subgraphs/name/stakewise/stakewise',
'chiado': 'https://chiado-graph.stakewise.io/subgraphs/name/stakewise/stakewise',
}
23 changes: 11 additions & 12 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { ParserError, ErrorTypes } from './helpers'
import initBls from './initBls'
import getTreeLeaf from './getTreeLeaf'
import validateJson from './validateJson'
import type { ParserInput } from './types'
import getPostMessage from './getPostMessage'
import getDepositData from './getDepositData'
import validateFields from './validateFields'
import verifySignature from './verifySignature'
import type { FileItem, ParserInput } from './types'


export const depositDataParser = async (input: ParserInput) => {
Expand All @@ -19,12 +19,13 @@ export const depositDataParser = async (input: ParserInput) => {
const pubkeySet = new Set<string>()
const treeLeaves: Uint8Array[] = []

parsedFile.forEach((item: FileItem, index) => {
const { pubkey, signature } = item
for (let index = 0; index < parsedFile.length; index++) {
const item = parsedFile[index]
const { pubkey, signature, withdrawal_address } = item

validateFields({ item })

const depositData = getDepositData({ pubkey, vaultAddress, network })
const depositData = await getDepositData({ pubkey, vaultAddress, withdrawalAddress: withdrawal_address, network })

verifySignature({ bls, pubkey, signature, depositData, network })

Expand All @@ -33,15 +34,13 @@ export const depositDataParser = async (input: ParserInput) => {
pubkeySet.add(pubkey)
treeLeaves.push(treeLeaf)

if (parsedFile.length > 1000) {
if (typeof onProgress === 'function') {
onProgress({
total: parsedFile.length,
value: index + 1,
})
}
if (parsedFile.length > 1000 && typeof onProgress === 'function') {
onProgress({
total: parsedFile.length,
value: index + 1,
})
}
})
}

if (pubkeySet.size !== parsedFile?.length) {
throw new ParserError(ErrorTypes.DUPLICATE_PUBLIC_KEYS)
Expand Down
Loading

0 comments on commit f9754f1

Please sign in to comment.