Skip to content

Commit

Permalink
fix: recursive delegation querying
Browse files Browse the repository at this point in the history
  • Loading branch information
marthendalnunes committed Dec 6, 2024
1 parent 9d81f1f commit 6853666
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 96 deletions.
102 changes: 20 additions & 82 deletions apps/api-delegations/src/db/actions/delegations/get-delegation-db.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,30 @@
import type { DelegationWithMetadata } from 'universal-types';
import type { GetDelegationParams } from '../../../validation.js';
import { db } from '../../index.js';
import { ROOT_AUTHORITY } from 'universal-data';
import {
MAX_DELEGATION_DEPTH,
buildAuthConfig,
replaceAuthKeys,
} from './utils.js';

function addAuthorityDelegationAtLeaf(
root: DelegationWithMetadata,
newAuthorityDelegation: DelegationWithMetadata,
): DelegationWithMetadata {
// If we've reached a leaf where authorityDelegation is null, insert the new delegation here.
if (root.authorityDelegation === null) {
return {
...root,
authorityDelegation: newAuthorityDelegation,
};
}

// Otherwise, recurse down one level and update that subtree
return {
...root,
authorityDelegation: addAuthorityDelegationAtLeaf(
root.authorityDelegation,
newAuthorityDelegation,
),
};
}
type GetDelegationDbReturnType = Promise<DelegationWithMetadata | undefined>;

export async function getDelegationDb({
hash,
}: GetDelegationParams): Promise<DelegationWithMetadata | undefined> {
return db.transaction(async (tx) => {
// Fetch the top-level (initial) delegation
const topDelegationDb = await tx.query.delegations.findFirst({
where: (delegations, { eq }) => eq(delegations.hash, hash),
with: { caveats: true },
});

if (!topDelegationDb) {
return undefined;
}

// Initialize our top-level delegation with authorityDelegation = null
let delegation: DelegationWithMetadata = {
...topDelegationDb,
authorityDelegation: null,
};

// If authority is the root authority, no further chaining is needed
if (delegation.authority === ROOT_AUTHORITY) {
return delegation;
}

// Check for delegation loops
if (delegation.hash === delegation.authority) {
throw new Error('Delegation loop detected');
}

// Walk down the chain of authority delegations until we hit the root
let currentAuthority = delegation.authority;

// TODO: replace loop with query using recursive CTE
while (currentAuthority !== ROOT_AUTHORITY) {
const nextDelegationDb = await tx.query.delegations.findFirst({
where: (delegations, { eq }) => eq(delegations.hash, currentAuthority),
with: { caveats: true },
});

if (!nextDelegationDb) {
// If we can't find the next delegation in the chain, stop and return what we have so far
break;
}

// Check for delegation loops
if (nextDelegationDb.hash === nextDelegationDb.authority) {
throw new Error('Delegation loop detected');
}

const nextDelegation: DelegationWithMetadata = {
...nextDelegationDb,
authorityDelegation: null,
};

// Insert this delegation at the leaf of our current chain
delegation = addAuthorityDelegationAtLeaf(delegation, nextDelegation);
}: GetDelegationParams): GetDelegationDbReturnType {
const delegation = await db.query.delegations.findFirst({
where: (delegations, { eq }) => eq(delegations.hash, hash),
with: {
// Supports MAX_DELEGATION_DEPTH levels of parent recursion
caveats: true,
auth: buildAuthConfig(MAX_DELEGATION_DEPTH),
},
});

// Move on to the next authority in the chain
currentAuthority = nextDelegation.authority;
}
if (!delegation) {
return;
}

return delegation;
});
// Recursively update auth for authorityDelegation
return replaceAuthKeys(delegation);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,27 @@ import { and } from 'drizzle-orm';
import type { Address } from 'viem';
import { db } from '../../index.js';
import { sqlLower } from '../../utils.js';
import {
MAX_DELEGATION_DEPTH,
buildAuthConfig,
replaceAuthKeys,
} from './utils.js';
import type { DelegationWithMetadata } from 'universal-types';

type GetDelegationsCollectionDbReturnType = {
delegate: DelegationWithMetadata[];
delegator: DelegationWithMetadata[];
};

export async function getDelegationsCollectionDb({
address,
chainId,
type,
}: { address: Address; chainId: number; type: string }) {
}: {
address: Address;
chainId: number;
type: string;
}): Promise<GetDelegationsCollectionDbReturnType> {
const lowercasedAddress = address.toLowerCase();

const [delegate, delegator] = await db.transaction(async (tx) =>
Expand All @@ -21,6 +36,7 @@ export async function getDelegationsCollectionDb({
),
with: {
caveats: true,
auth: buildAuthConfig(MAX_DELEGATION_DEPTH),
},
}),
tx.query.delegations.findMany({
Expand All @@ -32,13 +48,14 @@ export async function getDelegationsCollectionDb({
),
with: {
caveats: true,
auth: buildAuthConfig(MAX_DELEGATION_DEPTH),
},
}),
]),
);

return {
delegate,
delegator,
delegate: delegate.map(replaceAuthKeys),
delegator: delegator.map(replaceAuthKeys),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { db } from '../../index.js';
import { delegations } from '../../schema.js';
import { sqlLower } from '../../utils.js';
import type { DelegationWithMetadata } from 'universal-types';
import {
MAX_DELEGATION_DEPTH,
buildAuthConfig,
replaceAuthKeys,
} from './utils.js';

export type GetDelegationsDbReturnType = DelegationWithMetadata[] | undefined;

Expand Down Expand Up @@ -32,12 +37,9 @@ export async function getDelegationsDb({
where: (_, { and }) => and(...conditions),
with: {
caveats: true,
auth: buildAuthConfig(MAX_DELEGATION_DEPTH),
},
});

// TODO: add authorityDelegation to each delegation once the auxiliary chaining table is live
return delegationsDb.map((delegationDb) => ({
...delegationDb,
authorityDelegation: null,
}));
return delegationsDb.map(replaceAuthKeys);
}
35 changes: 35 additions & 0 deletions apps/api-delegations/src/db/actions/delegations/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { DelegationWithMetadata } from 'universal-types';

export type ReplaceAuthKeysParams = Omit<
DelegationWithMetadata,
'authorityDelegation'
> & {
auth: ReplaceAuthKeysParams | null;
};

export function replaceAuthKeys(
delegation: ReplaceAuthKeysParams,
): DelegationWithMetadata {
const { auth, ...rest } = delegation;

return {
...rest,
authorityDelegation: auth ? replaceAuthKeys(auth) : null,
};
}

export const MAX_DELEGATION_DEPTH = 5;

export type AuthConfig = true | { with: { caveats: true; auth: AuthConfig } };

export function buildAuthConfig(depth: number): AuthConfig {
if (depth === 0) {
return true; // at the bottom level, 'auth' is just 'true'
}
return {
with: {
caveats: true,
auth: buildAuthConfig(depth - 1),
},
};
}
7 changes: 6 additions & 1 deletion apps/api-delegations/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ export const delegations = pgTable('delegations', {
});

// Relations
export const delegationsRelations = relations(delegations, ({ many }) => ({
export const delegationsRelations = relations(delegations, ({ one, many }) => ({
caveats: many(caveats),
// Uses auth instead of authorityDelegation due to Drizzle's query column length limit
auth: one(delegations, {
fields: [delegations.authority],
references: [delegations.hash],
}),
}));

// ----------------------------------------------
Expand Down
6 changes: 1 addition & 5 deletions packages/universal-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
"description": "Shared types for the Universal stack.",
"version": "0.0.0",
"license": "MIT",
"files": [
"src/**/*.ts",
"!src/**/*.test.ts",
"!src/**/*.test-d.ts"
],
"files": ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.test-d.ts"],
"sideEffects": false,
"type": "module",
"exports": {
Expand Down

0 comments on commit 6853666

Please sign in to comment.