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

1310 modify gasprices storage to support multi asset fees #1412

Merged
9 changes: 9 additions & 0 deletions .changeset/fuzzy-poems-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@penumbra-zone/storage': major
'@penumbra-zone/services': minor
'@penumbra-zone/query': minor
'@penumbra-zone/types': minor
'@penumbra-zone/wasm': minor
---

Modify GasPrices storage to support multi-asset fees
17 changes: 12 additions & 5 deletions packages/query/src/block-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { processActionDutchAuctionEnd } from './helpers/process-action-dutch-auc
import { processActionDutchAuctionSchedule } from './helpers/process-action-dutch-auction-schedule';
import { processActionDutchAuctionWithdraw } from './helpers/process-action-dutch-auction-withdraw';
import { RootQuerier } from './root-querier';
import { GasPrices } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/fee/v1/fee_pb';

declare global {
// eslint-disable-next-line no-var
Expand Down Expand Up @@ -162,12 +163,18 @@ export class BlockProcessor implements BlockProcessorInterface {
await this.indexedDb.saveFmdParams(compactBlock.fmdParameters);
}
if (compactBlock.gasPrices) {
// TODO #1310 pre-populate assetId for native GasPrices using stakingTokenAssetId
await this.indexedDb.saveGasPrices(compactBlock.gasPrices);
await this.indexedDb.saveGasPrices(
new GasPrices({
assetId: this.stakingAssetId,
...compactBlock.gasPrices,
}),
);
}
if (compactBlock.altGasPrices.length) {
for (const gasPrice of compactBlock.altGasPrices) {
await this.indexedDb.saveGasPrices(gasPrice);
}
Comment on lines +166 to +176
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this suggests that the RPC node is returning information in compact blocks about two alternative gas fees, but with zero gas prices. Manual testing confirmed that all other token denominations result in a "GasPrices not available" error.

Screenshot 2024-07-04 at 1 15 13 AM

}
// if (compactBlock.altGasPrices) {
// TODO #1310 save altGasPrices to indexed-db
// }

// wasm view server scan
// - decrypts new notes
Expand Down
3 changes: 2 additions & 1 deletion packages/services/src/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export interface IndexedDbMock {
constants?: Mock;
getAppParams?: Mock;
getAssetsMetadata?: Mock;
getGasPrices?: Mock;
getNativeGasPrices?: Mock;
getAltGasPrices?: Mock;
getFmdParams?: Mock;
getFullSyncHeight?: Mock;
getNotesForVoting?: Mock;
Expand Down
7 changes: 4 additions & 3 deletions packages/services/src/view-service/gas-prices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ describe('GasPrices request handler', () => {
vi.resetAllMocks();

mockIndexedDb = {
getGasPrices: vi.fn(),
getNativeGasPrices: vi.fn(),
getAltGasPrices: vi.fn(),
};
mockServices = {
getWalletServices: vi.fn(() =>
Expand All @@ -40,15 +41,15 @@ describe('GasPrices request handler', () => {
});

test('should successfully get gas prices when idb has them', async () => {
mockIndexedDb.getGasPrices?.mockResolvedValue(testData);
mockIndexedDb.getNativeGasPrices?.mockResolvedValue(testData);
const gasPricesResponse = new GasPricesResponse(
await gasPrices(new GasPricesRequest(), mockCtx),
);
expect(gasPricesResponse.gasPrices?.equals(testData)).toBeTruthy();
});

test('should fail to get gas prices when idb has none', async () => {
mockIndexedDb.getGasPrices?.mockResolvedValue(undefined);
mockIndexedDb.getNativeGasPrices?.mockResolvedValue(undefined);
await expect(gasPrices(new GasPricesRequest(), mockCtx)).rejects.toThrow(
'Gas prices is not available',
);
Expand Down
5 changes: 3 additions & 2 deletions packages/services/src/view-service/gas-prices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import { Code, ConnectError } from '@connectrpc/connect';
export const gasPrices: Impl['gasPrices'] = async (_, ctx) => {
const services = await ctx.values.get(servicesCtx)();
const { indexedDb } = await services.getWalletServices();
const gasPrices = await indexedDb.getGasPrices();
const gasPrices = await indexedDb.getNativeGasPrices();
const altGasPRices = await indexedDb.getAltGasPrices();
if (!gasPrices) throw new ConnectError('Gas prices is not available', Code.NotFound);

return {
gasPrices,
// TODO #1310 add altGasPrices
altGasPRices,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('TransactionPlanner request handler', () => {
mockIndexedDb = {
getFmdParams: vi.fn(),
getAppParams: vi.fn(),
getGasPrices: vi.fn(),
getNativeGasPrices: vi.fn(),
constants: vi.fn(),
stakingTokenAssetId: vi.fn(),
hasStakingAssetBalance: vi.fn(),
Expand Down Expand Up @@ -92,7 +92,7 @@ describe('TransactionPlanner request handler', () => {
}),
}),
);
mockIndexedDb.getGasPrices?.mockResolvedValueOnce(
mockIndexedDb.getNativeGasPrices?.mockResolvedValueOnce(
new GasPrices({
verificationPrice: 22n,
executionPrice: 222n,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const transactionPlanner: Impl['transactionPlanner'] = async (req, ctx) =
const { indexedDb } = await services.getWalletServices();

// Query IndexedDB directly to check for the existence of staking token
const nativeToken = await indexedDb.hasStakingAssetBalance();
const nativeToken = await indexedDb.hasStakingAssetBalance(req.source);

// Initialize the gas fee token using the native staking token's asset ID
// If there is no native token balance, extract and use an alternate gas fee token
Expand All @@ -28,7 +28,7 @@ export const transactionPlanner: Impl['transactionPlanner'] = async (req, ctx) =
if (!chainId) throw new ConnectError('ChainId not available', Code.FailedPrecondition);

// Wasm planner needs the presence of gas prices in the db to work
const gasPrices = await indexedDb.getGasPrices();
const gasPrices = await indexedDb.getNativeGasPrices();
if (!gasPrices) throw new ConnectError('Gas prices is not available', Code.FailedPrecondition);

const idbConstants = indexedDb.constants();
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/src/indexed-db/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
* The version number for the IndexedDB schema. This version number is used to manage
* database upgrades and ensure that the correct schema version is applied.
*/
export const IDB_VERSION = 43;
export const IDB_VERSION = 44;
27 changes: 18 additions & 9 deletions packages/storage/src/indexed-db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class IndexedDb implements IndexedDbInterface {
db.createObjectStore('SWAPS', {
keyPath: 'swapCommitment.inner',
}).createIndex('nullifier', 'nullifier.inner');
db.createObjectStore('GAS_PRICES');
db.createObjectStore('GAS_PRICES', { keyPath: 'assetId.inner' });
db.createObjectStore('POSITIONS', { keyPath: 'id.inner' });
db.createObjectStore('EPOCHS', { autoIncrement: true });
db.createObjectStore('VALIDATOR_INFOS');
Expand Down Expand Up @@ -385,21 +385,26 @@ export class IndexedDb implements IndexedDbInterface {
return SwapRecord.fromJson(json);
}

// TODO #1310 'getGasPrices()' should be renamed to 'getNativeGasPrice()'
async getGasPrices(): Promise<GasPrices | undefined> {
// TODO #1310 use this.stakingTokenAssetId as the key for the query
const jsonGasPrices = await this.db.get('GAS_PRICES', 'gas_prices');
async getNativeGasPrices(): Promise<GasPrices | undefined> {
const jsonGasPrices = await this.db.get(
'GAS_PRICES',
uint8ArrayToBase64(this.stakingTokenAssetId.inner),
);
if (!jsonGasPrices) return undefined;
return GasPrices.fromJson(jsonGasPrices);
}

// TODO #1310 implement getAltGasPrices()
async getAltGasPrices(): Promise<GasPrices[]> {
const allGasPrices = await this.db.getAll('GAS_PRICES');
return allGasPrices
.map(gp => GasPrices.fromJson(gp))
.filter(gp => !gp.assetId?.equals(this.stakingTokenAssetId));
}

async saveGasPrices(value: PartialMessage<GasPrices>): Promise<void> {
await this.u.update({
table: 'GAS_PRICES',
value: new GasPrices(value).toJson() as Jsonified<GasPrices>,
key: 'gas_prices',
});
}

Expand Down Expand Up @@ -815,7 +820,7 @@ export class IndexedDb implements IndexedDbInterface {
};
}

async hasStakingAssetBalance(): Promise<boolean> {
async hasStakingAssetBalance(addressIndex: AddressIndex | undefined): Promise<boolean> {
const spendableUMNotes = await this.db.getAllFromIndex(
'SPENDABLE_NOTES',
'assetId',
Expand All @@ -824,7 +829,11 @@ export class IndexedDb implements IndexedDbInterface {

return spendableUMNotes.some(note => {
const umNote = SpendableNoteRecord.fromJson(note);
return umNote.heightSpent === 0n && !isZero(getAmountFromRecord(umNote));
return (
umNote.heightSpent === 0n &&
!isZero(getAmountFromRecord(umNote)) &&
umNote.addressIndex?.equals(addressIndex)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice filtering for address index here

);
});
}
}
3 changes: 2 additions & 1 deletion packages/storage/src/indexed-db/indexed-db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,13 +393,14 @@ describe('IndexedDb', () => {
const db = await IndexedDb.initialize({ ...generateInitialProps() });

const gasPrices = new GasPrices({
assetId: db.stakingTokenAssetId,
blockSpacePrice: 0n,
compactBlockSpacePrice: 0n,
verificationPrice: 0n,
executionPrice: 0n,
});
await db.saveGasPrices(gasPrices);
const savedPrices = await db.getGasPrices();
const savedPrices = await db.getNativeGasPrices();

expect(gasPrices.equals(savedPrices)).toBeTruthy();
});
Expand Down
9 changes: 4 additions & 5 deletions packages/types/src/indexed-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ export interface IndexedDbInterface {
getSwapByNullifier(nullifier: Nullifier): Promise<SwapRecord | undefined>;
saveSwap(note: SwapRecord): Promise<void>;
getSwapByCommitment(commitment: StateCommitment): Promise<SwapRecord | undefined>;
getGasPrices(): Promise<GasPrices | undefined>;
// TODO #1310 add getAltGasPrices()
getNativeGasPrices(): Promise<GasPrices | undefined>;
getAltGasPrices(): Promise<GasPrices[]>;
saveGasPrices(value: PartialMessage<GasPrices>): Promise<void>;
getNotesForVoting(
addressIndex: AddressIndex | undefined,
Expand Down Expand Up @@ -146,7 +146,7 @@ export interface IndexedDbInterface {
auctionId: AuctionId,
): Promise<{ input: Value; output: Value } | undefined>;

hasStakingAssetBalance(): Promise<boolean>;
hasStakingAssetBalance(addressIndex: AddressIndex | undefined): Promise<boolean>;
}

export interface PenumbraDb extends DBSchema {
Expand Down Expand Up @@ -231,9 +231,8 @@ export interface PenumbraDb extends DBSchema {
nullifier: Jsonified<Required<SwapRecord>['nullifier']['inner']>; // base64
};
};
// TODO #1310 use the assetId as key
GAS_PRICES: {
key: 'gas_prices';
key: Jsonified<Required<GasPrices>['assetId']['inner']>; // base64
value: Jsonified<GasPrices>;
};
POSITIONS: {
Expand Down
44 changes: 16 additions & 28 deletions packages/wasm/crate/src/planner.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
use crate::metadata::customize_symbol_inner;
use crate::note_record::SpendableNoteRecord;
use crate::storage::{IndexedDBStorage, OutstandingReserves};
use crate::utils;
use crate::{error::WasmResult, swap_record::SwapRecord};
use std::collections::BTreeMap;
use std::mem;

use anyhow::anyhow;
use ark_ff::UniformRand;
use decaf377::{Fq, Fr};
use penumbra_asset::asset::{Id, Metadata};
use penumbra_asset::{Value, STAKING_TOKEN_ASSET_ID};
use penumbra_asset::Value;
use penumbra_auction::auction::dutch::actions::ActionDutchAuctionWithdrawPlan;
use penumbra_auction::auction::dutch::{
ActionDutchAuctionEnd, ActionDutchAuctionSchedule, DutchAuctionDescription,
Expand Down Expand Up @@ -38,11 +36,15 @@ use penumbra_transaction::ActionList;
use penumbra_transaction::{plan::MemoPlan, ActionPlan, TransactionParameters};
use prost::Message;
use rand_core::{OsRng, RngCore};
use std::collections::BTreeMap;
use std::mem;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;

use crate::metadata::customize_symbol_inner;
use crate::note_record::SpendableNoteRecord;
use crate::storage::{IndexedDBStorage, OutstandingReserves};
use crate::utils;
use crate::{error::WasmResult, swap_record::SwapRecord};

/// Prioritize notes to spend to release value of a specific transaction.
///
/// Various logic is possible for note selection. Currently, this method
Expand Down Expand Up @@ -181,13 +183,15 @@ pub async fn plan_transaction(

let chain_id: String = app_parameters.chain_id;

// Decode the gas fee token into an `Id` type
let fee_asset_id: Id = Id::decode(gas_fee_token)?;

// Request information about current gas prices
// TODO #1310 GasPrices record may not exist for alternative fee assets
let mut gas_prices: GasPrices = {
let gas_prices: GasPrices = {
let gas_prices: penumbra_proto::core::component::fee::v1::GasPrices =
serde_wasm_bindgen::from_value(
storage
.get_gas_prices()
.get_gas_prices_by_asset_id(fee_asset_id)
.await?
.ok_or_else(|| anyhow!("GasPrices not available"))?,
)?;
Expand All @@ -202,28 +206,12 @@ pub async fn plan_transaction(
}
};

// Decode the gas fee token into an `Id` type
let alt_gas: Id = Id::decode(gas_fee_token)?;

// Check if the decoded gas fee token is different from the staking token asset ID.
// If the gas fee token is different, use the alternative gas fee token with a 10x
// multiplier.
if alt_gas != *STAKING_TOKEN_ASSET_ID {
gas_prices = GasPrices {
asset_id: alt_gas,
block_space_price: gas_prices.block_space_price * 10,
compact_block_space_price: gas_prices.compact_block_space_price * 10,
verification_price: gas_prices.verification_price * 10,
execution_price: gas_prices.execution_price * 10,
};
Comment on lines -213 to -217
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gas fee token price multiplier will instead be determined by governance proposals, rather than arbitrarily setting a 10x multiplier.

};

let mut transaction_parameters = TransactionParameters {
chain_id,
expiry_height,
..Default::default()
};
transaction_parameters.fee.0.asset_id = alt_gas;
transaction_parameters.fee.0.asset_id = fee_asset_id;

let mut actions_list = ActionList::default();

Expand Down
9 changes: 5 additions & 4 deletions packages/wasm/crate/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,14 +323,15 @@ impl IndexedDBStorage {
.transpose()?)
}

// TODO #1310 should be changed to get GasPrices by assetId
pub async fn get_gas_prices(&self) -> WasmResult<Option<JsValue>> {
pub async fn get_gas_prices_by_asset_id(&self, asset_id: Id) -> WasmResult<Option<JsValue>> {
let tx = self
.db
.transaction_on_one(&self.constants.tables.gas_prices)?;
let store = tx.object_store(&self.constants.tables.gas_prices)?;

Ok(store.get_owned("gas_prices")?.await?)
Ok(store
.get_owned(byte_array_to_base64(&asset_id.to_proto().inner))?
.await?)
// TODO GasPrices is missing domain type impl, requiring this
// .map(serde_wasm_bindgen::from_value)
// .transpose()?)
Expand Down Expand Up @@ -387,7 +388,7 @@ impl IndexedDBStorage {
}
}

fn byte_array_to_base64(byte_array: &Vec<u8>) -> String {
pub fn byte_array_to_base64(byte_array: &Vec<u8>) -> String {
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, byte_array)
}

Expand Down
5 changes: 4 additions & 1 deletion packages/wasm/crate/tests/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod tests {
use wasm_bindgen_test::*;

use penumbra_wasm::planner::plan_transaction;
use penumbra_wasm::storage::byte_array_to_base64;
use penumbra_wasm::{
build::build_action,
keys::load_proving_key,
Expand Down Expand Up @@ -372,7 +373,9 @@ mod tests {
serde_wasm_bindgen::to_value(&"last_forgotten").unwrap();
let fmd_json_key: JsValue = serde_wasm_bindgen::to_value(&"params").unwrap();
let app_json_key: JsValue = serde_wasm_bindgen::to_value(&"params").unwrap();
let gas_json_key: JsValue = serde_wasm_bindgen::to_value(&"gas_prices").unwrap();
let gas_json_key: JsValue = JsValue::from_str(&byte_array_to_base64(
&STAKING_TOKEN_ASSET_ID.to_proto().inner,
));

store_note.put_val(&spendable_note_json).unwrap();
store_tree_commitments
Expand Down
Empty file modified scripts/pack-public.sh
100644 → 100755
Empty file.
Loading