From 086fee5b1e8fc72f12a8328380389f5d33355c60 Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 8 Oct 2025 15:24:00 +0100 Subject: [PATCH 1/3] feat(pricing): add `sign-session-key` pricing support and product-aware sorting - request `SIGN_SESSION_KEY` prices when fetching node data so the contract returns all four product columns - extend pricing context schema to accept `SIGN_SESSION_KEY` and re-sort nodes per product before slicing to threshold - route PKP auth through `SIGN_SESSION_KEY` pricing while keeping custom auth on `LIT_ACTION` - cover the new sort logic with a unit test to prove we now pick the cheapest validators for the requested product --- .../authAdapters/getPkpAuthContextAdapter.ts | 4 +- .../pricing/getNodesForRequest.ts | 2 + .../getMaxPricesForNodeProduct.spec.ts | 60 +++++++++++++++++++ .../getMaxPricesForNodeProduct.ts | 18 +++++- .../shared/managers/pricing-manager/schema.ts | 2 +- 5 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts diff --git a/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts b/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts index f95037769..89297a554 100644 --- a/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts +++ b/packages/auth/src/lib/AuthManager/authAdapters/getPkpAuthContextAdapter.ts @@ -147,9 +147,9 @@ export async function getPkpAuthContextAdapter( const nodeUrls = litClientCtx.getMaxPricesForNodeProduct({ nodePrices: respondingNodePrices, userMaxPrice: litClientCtx.getUserMaxPrice({ - product: 'LIT_ACTION', + product: 'SIGN_SESSION_KEY', }), - productId: PRODUCT_IDS['LIT_ACTION'], + productId: PRODUCT_IDS['SIGN_SESSION_KEY'], numRequiredNodes: threshold, }); diff --git a/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/apis/rawContractApis/pricing/getNodesForRequest.ts b/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/apis/rawContractApis/pricing/getNodesForRequest.ts index 8f39f5d5a..be3f7aa13 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/apis/rawContractApis/pricing/getNodesForRequest.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/LitChainClient/apis/rawContractApis/pricing/getNodesForRequest.ts @@ -15,11 +15,13 @@ import { * - DECRYPTION (0): Used for decryption operations * - SIGN (1): Used for signing operations * - LA (2): Used for Lit Actions execution + * - SIGN_SESSION_KEY (3): Used for sign session key operations */ export const PRODUCT_IDS = { DECRYPTION: 0n, // For decryption operations SIGN: 1n, // For signing operations LIT_ACTION: 2n, // For Lit Actions execution + SIGN_SESSION_KEY: 3n, // For sign session key operations } as const; // Schema for the request diff --git a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts new file mode 100644 index 000000000..2dcef396c --- /dev/null +++ b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts @@ -0,0 +1,60 @@ +import { PRODUCT_IDS } from '@lit-protocol/constants'; + +import { getMaxPricesForNodeProduct } from './getMaxPricesForNodeProduct'; + +describe('getMaxPricesForNodeProduct', () => { + it('uses the requested product column when ranking nodes', () => { + const nodePrices = [ + { + url: 'https://node-a', + prices: [80n, 5n, 9n, 30n], + }, + { + url: 'https://node-b', + prices: [70n, 4n, 8n, 10n], + }, + { + url: 'https://node-c', + prices: [60n, 3n, 7n, 20n], + }, + ]; + + // Log the incoming order to show the encryption column is already sorted lowest-first. + console.log( + 'incoming order', + nodePrices.map(({ url, prices }) => ({ + url, + decryptionPrice: prices[PRODUCT_IDS.DECRYPTION], + signPrice: prices[PRODUCT_IDS.SIGN], + litActionPrice: prices[PRODUCT_IDS.LIT_ACTION], + signSessionKeyPrice: prices[PRODUCT_IDS.SIGN_SESSION_KEY], + })) + ); + + // Call the helper exactly like the SDK does: ask for SIGN_SESSION_KEY prices, + // pass the raw price feed output, and cap the request at two nodes. + const result = getMaxPricesForNodeProduct({ + nodePrices, + userMaxPrice: 100n, + productId: PRODUCT_IDS.SIGN_SESSION_KEY, + numRequiredNodes: 2, + }); + + console.log( + 'selected nodes', + result.map(({ url, price }) => ({ url, price })) + ); + + // After sorting the nodes by the session-key column, the helper should + // return node-b (10) and node-c (20) even though the original array was + // ordered by the decryption price column. + expect(result).toHaveLength(2); + expect(result[0].url).toBe('https://node-b'); + expect(result[1].url).toBe('https://node-c'); + + // Base prices are taken from the SIGN_SESSION_KEY column (10 and 20) + // with the excess (100 - 30 = 70) split evenly. + expect(result[0].price).toBe(10n + 35n); + expect(result[1].price).toBe(20n + 35n); + }); +}); diff --git a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts index 5d90c4ed4..461b0c2d2 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts @@ -28,10 +28,24 @@ export function getMaxPricesForNodeProduct({ productId, numRequiredNodes, }: MaxPricesForNodes): { url: string; price: bigint }[] { + // Always evaluate pricing using the product-specific column so we truly pick + // the cheapest validators for that product (the upstream feed is sorted by + // prices[0]/decryption price only). + const sortedNodes = [...nodePrices].sort((a, b) => { + const priceA = a.prices[productId]; + const priceB = b.prices[productId]; + + if (priceA === priceB) { + return 0; + } + + return priceA < priceB ? -1 : 1; + }); + // If we don't need all nodes to service the request, only use the cheapest `n` of them const nodesToConsider = numRequiredNodes - ? nodePrices.slice(0, numRequiredNodes) - : nodePrices; + ? sortedNodes.slice(0, numRequiredNodes) + : sortedNodes; let totalBaseCost = 0n; diff --git a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/schema.ts b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/schema.ts index f00a567db..b2d8ce793 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/schema.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/schema.ts @@ -5,7 +5,7 @@ import { PRODUCT_ID_VALUES } from '@lit-protocol/constants'; export const PricingContextSchema = z .object({ - product: z.enum(['DECRYPTION', 'SIGN', 'LIT_ACTION']), + product: z.enum(['DECRYPTION', 'SIGN', 'LIT_ACTION', 'SIGN_SESSION_KEY']), userMaxPrice: z.bigint().optional(), nodePrices: z.array( z.object({ url: z.string(), prices: z.array(z.bigint()) }) From 12635fc6b09f6ae97d6535c0ce1d6c9782c0419e Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 8 Oct 2025 15:37:15 +0100 Subject: [PATCH 2/3] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Anson --- .../managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts index 2dcef396c..85cbc5493 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.spec.ts @@ -19,7 +19,7 @@ describe('getMaxPricesForNodeProduct', () => { }, ]; - // Log the incoming order to show the encryption column is already sorted lowest-first. + // Log the incoming order to show the decryption column is already sorted lowest-first. console.log( 'incoming order', nodePrices.map(({ url, prices }) => ({ From c53626dbdc2029b4159f8f695f809e9460fc720a Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 8 Oct 2025 17:11:52 +0100 Subject: [PATCH 3/3] docs(pricing): improve comments --- .../getMaxPricesForNodeProduct.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts index 461b0c2d2..3768ba3cd 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/pricing-manager/getMaxPricesForNodeProduct.ts @@ -15,6 +15,13 @@ export interface MaxPricesForNodes { * Ensures the total cost does not exceed userMaxPrice. * Operates in the order of lowest priced node to highest. * + * Example: + * - Selected nodes have SIGN_SESSION_KEY prices of 10 and 20. + * - `userMaxPrice` is 100. + * - Base total = 10 + 20 = 30. + * - Excess = 100 - 30 = 70. + * - Each node receives 70 / 2 = 35 extra budget, yielding 45 and 55. + * * @param nodePrices - An object where keys are node addresses and values are arrays of prices for different action types. * @param userMaxPrice - The maximum price the user is willing to pay to execute the request. * @param productId - The ID of the product to determine which price to consider. @@ -47,14 +54,14 @@ export function getMaxPricesForNodeProduct({ ? sortedNodes.slice(0, numRequiredNodes) : sortedNodes; + // Sum the unadjusted cost for the nodes we plan to use. let totalBaseCost = 0n; - - // Calculate the base total cost without adjustments for (const { prices } of nodesToConsider) { + // Example: base total accumulates 10 + 20 = 30 for the two cheapest nodes. totalBaseCost += prices[productId]; } - // Verify that we have a high enough userMaxPrice to fulfill the request + // Refuse to proceed if the caller's budget cannot even cover the base cost. if (totalBaseCost > userMaxPrice) { throw new MaxPriceTooLow( { @@ -72,13 +79,16 @@ export function getMaxPricesForNodeProduct({ * our request to fail if the price on some of the nodes is higher than we think it was, as long as it's not * drastically different than we expect it to be */ + // Any remaining budget is spread across the participating nodes to + // provide cushion for minor pricing fluctuations. Example: 100 - 30 = 70. const excessBalance = userMaxPrice - totalBaseCost; // Map matching the keys from `nodePrices`, but w/ the per-node maxPrice computed based on `userMaxPrice` const maxPricesPerNode: { url: string; price: bigint }[] = []; for (const { url, prices } of nodesToConsider) { - // For now, we'll distribute the remaining balance equally across nodes + // Distribute the remaining budget evenly across nodes to form the max price. + // Example: each node receives 70 / 2 = 35, becoming 10+35 and 20+35. maxPricesPerNode.push({ url, price: excessBalance