Skip to content

Commit

Permalink
split up validation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
0age committed Dec 8, 2024
1 parent f694bd8 commit 28de5d8
Show file tree
Hide file tree
Showing 8 changed files with 528 additions and 503 deletions.
504 changes: 1 addition & 503 deletions src/validation.ts

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions src/validation/allocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { getAddress } from 'viem/utils';
import { numberToHex } from 'viem/utils';
import { PGlite } from '@electric-sql/pglite';
import { getCompactDetails } from '../graphql';
import { getAllocatedBalance } from '../balance';
import { ValidationResult, CompactMessage } from './types';

// Helper to convert bigint to 32-byte hex string
function bigintToHex(value: bigint): string {
return numberToHex(value, { size: 32 }).slice(2);
}

export async function validateAllocation(
compact: CompactMessage,
chainId: string,
db: PGlite
): Promise<ValidationResult> {
try {
// Extract allocatorId from the compact id
const allocatorId =
(compact.id >> BigInt(160)) & ((BigInt(1) << BigInt(92)) - BigInt(1));

const response = await getCompactDetails({
allocator: process.env.ALLOCATOR_ADDRESS!,
sponsor: compact.sponsor,
lockId: compact.id.toString(),
chainId,
});

// Check withdrawal status
const resourceLock = response.account.resourceLocks.items[0];
if (!resourceLock) {
return { isValid: false, error: 'Resource lock not found' };
}

if (resourceLock.withdrawalStatus !== 0) {
return {
isValid: false,
error: 'Resource lock has forced withdrawals enabled',
};
}

// Verify allocatorId matches the one from GraphQL
const graphqlAllocatorId =
response.allocator.supportedChains.items[0]?.allocatorId;
if (!graphqlAllocatorId || BigInt(graphqlAllocatorId) !== allocatorId) {
return { isValid: false, error: 'Invalid allocator ID' };
}

// Calculate pending balance
const pendingBalance = response.accountDeltas.items.reduce(
(sum, delta) => sum + BigInt(delta.delta),
BigInt(0)
);

// Calculate allocatable balance
const resourceLockBalance = BigInt(resourceLock.balance);
const allocatableBalance =
resourceLockBalance > pendingBalance
? resourceLockBalance - pendingBalance
: BigInt(0);

// Get allocated balance from database with proper hex formatting
const allocatedBalance = await getAllocatedBalance(
db,
getAddress(compact.sponsor).toLowerCase(),
chainId,
bigintToHex(compact.id),
response.account.claims.items.map((item) => item.claimHash)
);

// Verify sufficient balance
const totalNeededBalance = allocatedBalance + BigInt(compact.amount);
if (allocatableBalance < totalNeededBalance) {
return {
isValid: false,
error: `Insufficient allocatable balance (have ${allocatableBalance}, need ${totalNeededBalance})`,
};
}

return { isValid: true };
} catch (error) {
return {
isValid: false,
error: `Allocation validation error: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
}
63 changes: 63 additions & 0 deletions src/validation/compact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { PGlite } from '@electric-sql/pglite';
import { ValidationResult, CompactMessage } from './types';
import { validateNonce } from './nonce';
import { validateStructure, validateExpiration } from './structure';
import { validateDomainAndId } from './domain';
import { validateAllocation } from './allocation';

export async function validateCompact(
compact: CompactMessage,
chainId: string,
db: PGlite
): Promise<ValidationResult> {
try {
// 1. Chain ID validation
const chainIdNum = parseInt(chainId);
if (
isNaN(chainIdNum) ||
chainIdNum <= 0 ||
chainIdNum.toString() !== chainId
) {
return { isValid: false, error: 'Invalid chain ID format' };
}

// 2. Structural Validation
const result = await validateStructure(compact);
if (!result.isValid) return result;

// 3. Nonce Validation (only if nonce is provided)
if (compact.nonce !== null) {
const nonceResult = await validateNonce(
compact.nonce,
compact.sponsor,
chainId,
db
);
if (!nonceResult.isValid) return nonceResult;
}

// 4. Expiration Validation
const expirationResult = validateExpiration(compact.expires);
if (!expirationResult.isValid) return expirationResult;

// 5. Domain and ID Validation
const domainResult = await validateDomainAndId(
compact.id,
compact.expires,
chainId,
process.env.ALLOCATOR_ADDRESS!
);
if (!domainResult.isValid) return domainResult;

// 6. Allocation Validation
const allocationResult = await validateAllocation(compact, chainId, db);
if (!allocationResult.isValid) return allocationResult;

return { isValid: true };
} catch (error) {
return {
isValid: false,
error: error instanceof Error ? error.message : 'Validation failed',
};
}
}
64 changes: 64 additions & 0 deletions src/validation/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ValidationResult } from './types';

export async function validateDomainAndId(
id: bigint,
expires: bigint,
chainId: string,
_allocatorAddress: string
): Promise<ValidationResult> {
try {
// Basic validation
if (id <= BigInt(0)) {
return { isValid: false, error: 'Invalid ID: must be positive' };
}

// Validate chainId format
const chainIdNum = parseInt(chainId);
if (
isNaN(chainIdNum) ||
chainIdNum <= 0 ||
chainIdNum.toString() !== chainId
) {
return { isValid: false, error: 'Invalid chain ID format' };
}

// For testing purposes, accept ID 1 as valid after basic validation
if (process.env.NODE_ENV === 'test' && id === BigInt(1)) {
return { isValid: true };
}

// Extract resetPeriod and allocatorId from the compact id
const resetPeriodIndex = Number((id >> BigInt(252)) & BigInt(0x7));

const resetPeriods = [
BigInt(1),
BigInt(15),
BigInt(60),
BigInt(600),
BigInt(3900),
BigInt(86400),
BigInt(612000),
BigInt(2592000),
];
const resetPeriod = resetPeriods[resetPeriodIndex];

// Ensure resetPeriod doesn't allow forced withdrawal before expiration
const now = BigInt(Math.floor(Date.now() / 1000));

if (now + resetPeriod < expires) {
return {
isValid: false,
error: 'Reset period would allow forced withdrawal before expiration',
};
}

return { isValid: true };
} catch (error) {
return {
isValid: false,
error: `Domain/ID validation error: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
}
6 changes: 6 additions & 0 deletions src/validation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './types';
export * from './nonce';
export * from './structure';
export * from './domain';
export * from './allocation';
export * from './compact';
Loading

0 comments on commit 28de5d8

Please sign in to comment.