Skip to content

Commit

Permalink
improve(API): Make gasFeeTotal more accurate in /limits and increase …
Browse files Browse the repository at this point in the history
…cache hit frequency (#1369)

* improve(API): Reduce stale-while-revalidate and gas price cache times

## `stale-while-revalidate`

We can reduce this cache time to 1s so that after the cached value is >1s old we can immediately start recomputing the limits value. This means in the best case we'll have as fresh gas cost data as possible.

## Gas price caching:

We should ideally use less stale gas price data. However, we don't want to increase the /limits response time.

We currently use the gas price to compute the gas cost so it makes sense to make the gas price cache time slightly longer or equal to the gas cost. This way if the gas cost cache is set, then we'll use the cached gas cost value. If its stale, then we'll fetch the gas price and hopefully hit the cache sometimes. This is why it doesn't make sense to set the gas price cache less than the gas cost cache time otherwise we'll very rarely hit the gas price cache.

* Separate native gas cost and op stack l1 gas cost calculation from tokenGasCost calculation

Willl allow us to use more customized caching times for different gas cost components that are expected to change on different time periods

* Use gas price cache for Linea as well

* fix: only cron cache gas prices for non Linea chains

* Update limits.ts

* Only pass in depositArgs for Linea

* add extra part to cache key

* Use sdk for helper methods

* Update limits.ts

* Fix gas-prices

* Use utils in gas-prices.ts to read data from cache

* add gas costs to cron job

* cache gas prices before cost

* remove promise.all

* Update _utils.ts

* cache op stack l1 costs for op chains only

* Test only cache gas prices

* debug

* Fix cron job

* Update cron-cache-gas-prices.ts

* fix promise nesting

* Update cron-cache-gas-prices.ts

* update cache times

* Update _utils.ts

* Update cron-cache-gas-prices.ts

* Add native gas cost caching

Keep cache warm

* Increase ttl of native gas cost, add gasFeeDetails to response

* sdk
  • Loading branch information
nicholaspai authored Jan 13, 2025
1 parent 5b9b48f commit 09d3771
Show file tree
Hide file tree
Showing 6 changed files with 426 additions and 157 deletions.
169 changes: 122 additions & 47 deletions api/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1970,94 +1970,169 @@ export function isContractCache(chainId: number, address: string) {
);
}

export function getCachedFillGasUsage(
export function getCachedNativeGasCost(
deposit: Parameters<typeof buildDepositForSimulation>[0],
gasPrice?: BigNumber,
overrides?: Partial<{
relayerAddress: string;
}>
) {
// We can use a long TTL since we are fetching only the native gas cost which should rarely change.
// Set this longer than the secondsPerUpdate value in the cron cache gas prices job.
const ttlPerChain = {
default: 10,
[CHAIN_IDs.ARBITRUM]: 10,
default: 120,
};

const cacheKey = buildInternalCacheKey(
"fillGasUsage",
"nativeGasCost",
deposit.destinationChainId,
deposit.outputToken
);
const ttl = ttlPerChain[deposit.destinationChainId] || ttlPerChain.default;
const fetchFn = async () => {
const relayerAddress =
overrides?.relayerAddress ??
sdk.constants.DEFAULT_SIMULATED_RELAYER_ADDRESS;
const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries(
deposit.destinationChainId,
overrides
);
const unsignedFillTxn =
await relayerFeeCalculatorQueries.getUnsignedTxFromDeposit(
buildDepositForSimulation(deposit),
relayerAddress
);
const voidSigner = new ethers.VoidSigner(
relayerAddress,
relayerFeeCalculatorQueries.provider
);
return voidSigner.estimateGas(unsignedFillTxn);
};

return makeCacheGetterAndSetter(
cacheKey,
ttlPerChain.default,
fetchFn,
(nativeGasCostFromCache) => {
return BigNumber.from(nativeGasCostFromCache);
}
);
}

export function getCachedOpStackL1DataFee(
deposit: Parameters<typeof buildDepositForSimulation>[0],
nativeGasCost: BigNumber,
overrides?: Partial<{
relayerAddress: string;
}>
) {
// The L1 data fee should change after each Ethereum block since its based on the L1 base fee.
// However, the L1 base fee should only change by 12.5% at most per block.
// We set this higher than the secondsPerUpdate value in the cron cache gas prices job which will update this
// more frequently.
const ttlPerChain = {
default: 60,
};

const cacheKey = buildInternalCacheKey(
"opStackL1DataFee",
deposit.destinationChainId,
deposit.outputToken // This should technically differ based on the output token since the L2 calldata
// size affects the L1 data fee and this calldata can differ based on the output token.
);
const fetchFn = async () => {
// We don't care about the gas token price or the token gas price, only the raw gas units. In the API
// we'll compute the gas price separately.
const markups = getGasMarkup(deposit.destinationChainId);
const gasCosts = await relayerFeeCalculatorQueries.getGasCosts(
buildDepositForSimulation(deposit),
overrides?.relayerAddress,
{
gasPrice,
// We want the fee multipliers if the gasPrice is undefined:
baseFeeMultiplier: markups.baseFeeMarkup,
priorityFeeMultiplier: markups.priorityFeeMarkup,
opStackL1GasCostMultiplier: sdk.utils.chainIsOPStack(
deposit.destinationChainId
)
? getGasMarkup(deposit.destinationChainId).opStackL1DataFeeMarkup
: undefined,
}
const { opStackL1DataFeeMarkup } = getGasMarkup(deposit.destinationChainId);
const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries(
deposit.destinationChainId,
overrides
);
return {
nativeGasCost: gasCosts.nativeGasCost,
tokenGasCost: gasCosts.tokenGasCost,
};
const unsignedTx =
await relayerFeeCalculatorQueries.getUnsignedTxFromDeposit(
buildDepositForSimulation(deposit),
overrides?.relayerAddress
);
const opStackL1GasCost =
await relayerFeeCalculatorQueries.getOpStackL1DataFee(
unsignedTx,
overrides?.relayerAddress,
{
opStackL2GasUnits: nativeGasCost, // Passed in here to avoid gas cost recomputation by the SDK
opStackL1DataFeeMultiplier: opStackL1DataFeeMarkup,
}
);
return opStackL1GasCost;
};

return getCachedValue(
return makeCacheGetterAndSetter(
cacheKey,
ttl,
ttlPerChain.default,
fetchFn,
(gasCosts: { nativeGasCost: BigNumber; tokenGasCost: BigNumber }) => {
return {
nativeGasCost: BigNumber.from(gasCosts.nativeGasCost),
tokenGasCost: BigNumber.from(gasCosts.tokenGasCost),
};
(l1DataFeeFromCache) => {
return BigNumber.from(l1DataFeeFromCache);
}
);
}

export function latestGasPriceCache(chainId: number) {
export function latestGasPriceCache(
chainId: number,
deposit?: Parameters<typeof buildDepositForSimulation>[0],
overrides?: Partial<{
relayerAddress: string;
}>
) {
// We set this higher than the secondsPerUpdate value in the cron cache gas prices job which will update this
// more frequently.
const ttlPerChain = {
default: 30,
[CHAIN_IDs.ARBITRUM]: 15,
};

return makeCacheGetterAndSetter(
buildInternalCacheKey("latestGasPriceCache", chainId),
ttlPerChain[chainId] || ttlPerChain.default,
async () => (await getMaxFeePerGas(chainId)).maxFeePerGas,
(bnFromCache) => BigNumber.from(bnFromCache)
// If deposit is defined, then the gas price will be dependent on the fill transaction derived from the deposit.
// Therefore, we technically should cache a different gas price per different types of deposit so we add
// an additional outputToken to the cache key to distinguish between gas prices dependent on deposit args
// for different output tokens, which should be the main factor affecting the fill gas cost.
buildInternalCacheKey(
`latestGasPriceCache${deposit ? `-${deposit.outputToken}` : ""}`,
chainId
),
ttlPerChain.default,
async () => await getMaxFeePerGas(chainId, deposit, overrides),
(gasPrice: sdk.gasPriceOracle.GasPriceEstimate) => {
return {
maxFeePerGas: BigNumber.from(gasPrice.maxFeePerGas),
maxPriorityFeePerGas: BigNumber.from(gasPrice.maxPriorityFeePerGas),
};
}
);
}

/**
* Resolve the current gas price for a given chain
* @param chainId The chain ID to resolve the gas price for
* @returns The gas price in the native currency of the chain
*/
export function getMaxFeePerGas(
chainId: number
export async function getMaxFeePerGas(
chainId: number,
deposit?: Parameters<typeof buildDepositForSimulation>[0],
overrides?: Partial<{
relayerAddress: string;
}>
): Promise<sdk.gasPriceOracle.GasPriceEstimate> {
if (deposit && deposit.destinationChainId !== chainId) {
throw new Error(
"Chain ID must match the destination chain ID of the deposit"
);
}
const {
baseFeeMarkup: baseFeeMultiplier,
priorityFeeMarkup: priorityFeeMultiplier,
} = getGasMarkup(chainId);
const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries(
chainId,
overrides
);
const unsignedFillTxn = deposit
? await relayerFeeCalculatorQueries.getUnsignedTxFromDeposit(
buildDepositForSimulation(deposit),
overrides?.relayerAddress
)
: undefined;
return sdk.gasPriceOracle.getGasPriceEstimate(getProvider(chainId), {
chainId,
unsignedTx: unsignedFillTxn,
baseFeeMultiplier,
priorityFeeMultiplier,
});
Expand Down
Loading

0 comments on commit 09d3771

Please sign in to comment.