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

Withdrawal address validation #9

Merged
merged 11 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
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.3",
"main": "dist/index.js",
"name": "@stakewise/v3-deposit-data-parser",
"description": "v3-deposit-data-parser",
Expand Down
6 changes: 3 additions & 3 deletions src/parser/getDepositData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ 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(await getDepositData(data)).toEqual(result)
dfkadyr marked this conversation as resolved.
Show resolved Hide resolved
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,
getOperatorAddress,
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 getOperatorAddress({ 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/getOperatorAddress.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 getOperatorAddress from './getOperatorAddress'
import type { GetOperatorAddressInput } from './getOperatorAddress'


type MockGetEigenPods = jest.MockedFunction<typeof getEigenPods>

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

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

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

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

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

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

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

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

(getEigenPods as MockGetEigenPods).mockResolvedValue([])

const errorText = new ParserError(ErrorTypes.EIGEN_PODS_EMPTY)
await expect(getOperatorAddress(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 GetOperatorAddressInput

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

const errorText = new ParserError(ErrorTypes.INVALID_WITHDRAW_ADDRESS)
await expect(getOperatorAddress(input)).rejects.toThrow(errorText)
})
})
35 changes: 35 additions & 0 deletions src/parser/helpers/getOperatorAddress.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 GetOperatorAddressInput = {
vaultAddress: string
withdrawalAddress?: string
network: SupportedNetworks
}

const getOperatorAddress = async (values: GetOperatorAddressInput): 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 || eigenPods.length === 0) {
Cast0001 marked this conversation as resolved.
Show resolved Hide resolved
throw new ParserError(ErrorTypes.EIGEN_PODS_EMPTY)
}

const operatorAddress = eigenPods.find((eigenPod) => eigenPod.address === withdrawalAddress)
Cast0001 marked this conversation as resolved.
Show resolved Hide resolved

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

return operatorAddress.address
}


export default getOperatorAddress
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 getOperatorAddress } from './getOperatorAddress'
export { default as ParserError, ErrorMessages, ErrorTypes } from './errors'
export { default as getWithdrawalCredentials } from './getWithdrawalCredentials'
38 changes: 38 additions & 0 deletions src/parser/helpers/requests/checkIsRestakeVault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { urls } from './urls'
import { SupportedNetworks } from '../../types'


const checkIsRestakeVault = async (vaultId: string, network: SupportedNetworks) => {
try {
const response = await fetch(urls[network], {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
query: `query Vault($vaultId: ID!) { vault(id: $vaultId) { isRestake }}`,
variables: {
vaultId: vaultId.toLowerCase(),
},
}),
})

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?.vault?.isRestake
}
catch (error) {
console.error('Error fetching isRestake:', error)
}
}


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


type EigenPods = { address: string }[]

const getEigenPods = async (vaultId: string, network: SupportedNetworks)=> {
try {
const response = await fetch(urls[network], {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
query: `query EigenPods($vaultId: ID!) { eigenPods(where: { vault: $vaultId }) { address }}`,
variables: {
vaultId: vaultId.toLowerCase(),
},
}),
})

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)
}
Cast0001 marked this conversation as resolved.
Show resolved Hide resolved

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


export default getEigenPods
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',
}
8 changes: 4 additions & 4 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ export const depositDataParser = async (input: ParserInput) => {
const pubkeySet = new Set<string>()
const treeLeaves: Uint8Array[] = []

parsedFile.forEach((item: FileItem, index) => {
const { pubkey, signature } = item
await Promise.all(parsedFile.map(async (item: FileItem, index) => {
Cast0001 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -41,7 +41,7 @@ export const depositDataParser = async (input: ParserInput) => {
})
}
}
})
}))

if (pubkeySet.size !== parsedFile?.length) {
throw new ParserError(ErrorTypes.DUPLICATE_PUBLIC_KEYS)
Expand Down
1 change: 1 addition & 0 deletions src/parser/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type FileItem = {
amount: number
pubkey: string
signature: string
withdrawal_address?: string
}

export type DepositDataFile = FileItem[]
Expand Down
Loading