Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
546 changes: 542 additions & 4 deletions crates/core/src/rpc/surfnet_cheatcodes.rs

Large diffs are not rendered by default.

187 changes: 183 additions & 4 deletions crates/core/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{collections::HashSet, vec};
use std::{collections::HashSet, str::FromStr, vec};

use agave_reserved_account_keys::ReservedAccountKeys;
use base64::{Engine, prelude::BASE64_STANDARD};
Expand Down Expand Up @@ -51,15 +51,17 @@ use solana_zk_sdk_pod::encryption::{
use spl_token_2022_interface::extension::{
BaseStateWithExtensions, BaseStateWithExtensionsMut, ExtensionType, StateWithExtensions,
StateWithExtensionsMut,
confidential_transfer::{ConfidentialTransferAccount, PENDING_BALANCE_LO_BIT_LENGTH},
confidential_transfer::{
ConfidentialTransferAccount, ConfidentialTransferMint, PENDING_BALANCE_LO_BIT_LENGTH,
},
confidential_transfer_fee::ConfidentialTransferFeeAmount,
interest_bearing_mint::InterestBearingConfig,
scaled_ui_amount::ScaledUiAmountConfig,
transfer_fee::TransferFeeConfig,
};
use surfpool_types::types::{
ConfidentialBalanceKeys, ConfidentialTransferAccountUpdate, DeriveConfidentialKeysResponse,
GetConfidentialBalanceResponse,
ConfidentialBalanceKeys, ConfidentialTransferAccountUpdate, ConfidentialTransferMintUpdate,
DeriveConfidentialKeysResponse, GetConfidentialBalanceResponse,
};
use txtx_addon_kit::indexmap::IndexMap;

Expand Down Expand Up @@ -1444,6 +1446,119 @@ pub fn build_confidential_token_account_data(
Ok(buffer)
}

/// Build the raw account data for a Token-2022 mint that carries the
/// confidential-transfer mint extension.
///
/// Test-only helper backing the `surfnet_setMint` cheatcode: it writes the
/// extension directly, bypassing the on-chain
/// `ConfidentialTransferInitializeMint`. An absent `authority` or
/// `auditor_elgamal_pubkey` is written as the extension's null value.
Comment on lines +1449 to +1455

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

heavy comments here

pub fn build_confidential_mint_data(
base: &spl_token_2022_interface::state::Mint,
conf: &ConfidentialTransferMintUpdate,
) -> Result<Vec<u8>, String> {
let authority = conf
.authority
.as_deref()
.map(Pubkey::from_str)
.transpose()
.map_err(|e| format!("authority: {e}"))?;

let auditor_elgamal_pubkey = conf
.auditor_elgamal_pubkey
.as_deref()
.map(|key| {
let bytes = decode_confidential_key(key, 32)
.map_err(|e| format!("auditorElgamalPubkey: {e}"))?;
ElGamalPubkey::try_from(bytes.as_slice())
.map_err(|e| format!("auditorElgamalPubkey: invalid ElGamal public key ({e})"))
})
.transpose()?;

let extension_types = [ExtensionType::ConfidentialTransferMint];
let mint_len =
ExtensionType::try_calculate_account_len::<spl_token_2022_interface::state::Mint>(
&extension_types,
)
.map_err(|e| format!("failed to size confidential mint: {e}"))?;

let mut buffer = vec![0u8; mint_len];
let mut state =
StateWithExtensionsMut::<spl_token_2022_interface::state::Mint>::unpack_uninitialized(
&mut buffer,
)
.map_err(|e| format!("failed to init confidential mint buffer: {e}"))?;

state.base = *base;
state.pack_base();
state
.init_account_type()
.map_err(|e| format!("failed to set account type: {e}"))?;

{
let ct = state
.init_extension::<ConfidentialTransferMint>(false)
.map_err(|e| format!("failed to init confidential mint extension: {e}"))?;
ct.authority = authority.map(Into::into).unwrap_or_default();
ct.auto_approve_new_accounts = conf.auto_approve_new_accounts.unwrap_or(true).into();
ct.auditor_elgamal_pubkey = auditor_elgamal_pubkey
.map(|key| PodElGamalPubkey::from(key).into())
.unwrap_or_default();
}

drop(state);
Ok(buffer)
}

#[cfg(test)]
mod confidential_mint_tests {
use solana_zk_sdk::encryption::elgamal::ElGamalKeypair;

use super::*;

/// The auditor key has to be real ElGamal material: a well-formed 32-byte
/// string that is not a curve point is refused just like a short one, so a
/// mint never ships an auditor key no client can encrypt to.
#[test]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

comments here too long

fn an_auditor_key_that_is_not_elgamal_material_is_refused() {
let base = spl_token_2022_interface::state::Mint {
is_initialized: true,
..Default::default()
};

for auditor in [
bs58::encode([7u8; 16]).into_string(),
bs58::encode([0xffu8; 32]).into_string(),
] {
let error = build_confidential_mint_data(
&base,
&ConfidentialTransferMintUpdate {
auditor_elgamal_pubkey: Some(auditor),
..Default::default()
},
)
.unwrap_err();
assert!(error.starts_with("auditorElgamalPubkey:"), "got: {error}");
}

let auditor = ElGamalKeypair::new_rand();
assert!(
build_confidential_mint_data(
&base,
&ConfidentialTransferMintUpdate {
auditor_elgamal_pubkey: Some(
bs58::encode(bytes_of(&PodElGamalPubkey::from(auditor.pubkey_owned())))
.into_string()
),
..Default::default()
},
)
.is_ok(),
"a real ElGamal pubkey is accepted"
);
}
}

/// Decrypt the confidential balances held on a Token-2022 token account.
///
/// Backs the `surfnet_getConfidentialBalance` cheatcode, the read half of the
Expand Down Expand Up @@ -1692,12 +1807,76 @@ impl MintAccount {
}
}

pub fn new(token_program_id: &Pubkey) -> Self {
if token_program_id == &spl_token_2022_interface::id() {
Self::SplToken2022(spl_token_2022_interface::state::Mint {
is_initialized: true,
..Default::default()
})
} else {
Self::SplToken(spl_token_interface::state::Mint {
is_initialized: true,
..Default::default()
})
}
}

pub fn decimals(&self) -> u8 {
match self {
Self::SplToken2022(mint) => mint.decimals,
Self::SplToken(mint) => mint.decimals,
}
}

pub fn set_decimals(&mut self, decimals: u8) {
match self {
Self::SplToken2022(mint) => mint.decimals = decimals,
Self::SplToken(mint) => mint.decimals = decimals,
}
}

pub fn set_supply(&mut self, supply: u64) {
match self {
Self::SplToken2022(mint) => mint.supply = supply,
Self::SplToken(mint) => mint.supply = supply,
}
}

pub fn set_mint_authority(&mut self, mint_authority: COption<Pubkey>) {
match self {
Self::SplToken2022(mint) => mint.mint_authority = mint_authority,
Self::SplToken(mint) => mint.mint_authority = mint_authority,
}
}

pub fn pack_into_vec(&self) -> Vec<u8> {
match self {
Self::SplToken2022(mint) => {
let mut dst = [0u8; spl_token_2022_interface::state::Mint::LEN];
mint.pack_into_slice(&mut dst);
dst.to_vec()
}
Self::SplToken(mint) => {
let mut dst = [0u8; spl_token_interface::state::Mint::LEN];
mint.pack_into_slice(&mut dst);
dst.to_vec()
}
}
}

pub fn pack_into_preserving_extensions(&self, original: &[u8]) -> SurfpoolResult<Vec<u8>> {
let base_len = spl_token_interface::state::Mint::LEN;
if original.len() < base_len {
return Err(SurfpoolError::unpack_mint_account());
}

let mut data = original.to_vec();
match self {
Self::SplToken2022(mint) => mint.pack_into_slice(&mut data[..base_len]),
Self::SplToken(mint) => mint.pack_into_slice(&mut data[..base_len]),
}
Ok(data)
}
}

pub struct GeyserAccountUpdate {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// @generated by ts-rs from the Rust types in crates/types.
// Do not edit; run `npm run generate:kit-types` in crates/sdk-node instead.
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Writes the Token-2022 `ConfidentialTransferMint` extension on a mint created
* via `surfnet_setMint`.
*
* This is a test-only cheatcode: it writes the extension directly, bypassing
* the real on-chain `ConfidentialTransferInitializeMint` instruction, so a mint
* with a chosen auditor can be created without forking one from mainnet.
*/
export type ConfidentialTransferMintUpdate = {
/**
* The authority that approves new confidential accounts and updates this
* config (base58). Omitted leaves it null.
*/
authority?: string,
/**
* The auditor's ElGamal public key (base58 or base64, 32 bytes), which every
* confidential transfer on this mint also encrypts its amount to. Omitted
* leaves it null, i.e. no auditor.
*/
auditorElgamalPubkey?: string,
/**
* Whether new confidential accounts are approved on creation (default true).
Comment on lines +1 to +26

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

why all these comments?

*/
autoApproveNewAccounts?: boolean, };
24 changes: 24 additions & 0 deletions crates/sdk-node/surfpool-sdk/kit/generated/MintUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// @generated by ts-rs from the Rust types in crates/types.
// Do not edit; run `npm run generate:kit-types` in crates/sdk-node instead.
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfidentialTransferMintUpdate } from "./ConfidentialTransferMintUpdate.js";

export type MintUpdate = {
/**
* providing this value sets the number of decimals of the mint
*/
decimals?: number,
/**
* providing this value sets the mint authority: a base58 pubkey, or the
* literal string "null" to clear the authority
*/
mintAuthority?: string,
/**
* providing this value sets the total supply of the mint
*/
supply?: number | bigint,
/**
* providing this value writes the Token-2022 confidential-transfer mint
* extension (Token-2022 only)
*/
confidential?: ConfidentialTransferMintUpdate, };
2 changes: 2 additions & 0 deletions crates/sdk-node/surfpool-sdk/kit/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ export type * from "./AccountUpdate.js";
export type * from "./CheatcodeControlConfig.js";
export type * from "./ConfidentialBalanceKeys.js";
export type * from "./ConfidentialTransferAccountUpdate.js";
export type * from "./ConfidentialTransferMintUpdate.js";
export type * from "./DeriveConfidentialKeysResponse.js";
export type * from "./ExportSnapshotConfig.js";
export type * from "./ExportSnapshotFilter.js";
export type * from "./ExportSnapshotScope.js";
export type * from "./GetConfidentialBalanceResponse.js";
export type * from "./GetStreamedAccountsResponse.js";
export type * from "./GetSurfnetInfoResponse.js";
export type * from "./MintUpdate.js";
export type * from "./OfflineAccountConfig.js";
export type * from "./OverrideInstance.js";
export type * from "./ParsedAccount.js";
Expand Down
1 change: 1 addition & 0 deletions crates/sdk-node/surfpool-sdk/kit/generated/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const SURFNET_CHEATCODE_METHODS = [
"surfnet_resetNetwork",
"surfnet_resumeClock",
"surfnet_setAccount",
"surfnet_setMint",
"surfnet_setProgramAuthority",
"surfnet_setSupply",
"surfnet_setTokenAccount",
Expand Down
5 changes: 5 additions & 0 deletions crates/sdk-node/surfpool-sdk/kit/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
GetConfidentialBalanceResponse,
GetStreamedAccountsResponse,
GetSurfnetInfoResponse,
MintUpdate,
OfflineAccountConfig,
ResetAccountConfig,
RpcProfileResultConfig,
Expand Down Expand Up @@ -111,6 +112,9 @@ export type SurfnetDisableCheatcodeApi = {
export type SurfnetSetAccountApi = {
setAccount(pubkey: Address, update: AccountUpdate): null;
};
export type SurfnetSetMintApi = {
setMint(mint: Address, update: MintUpdate, tokenProgram?: Address): null;
};
export type SurfnetSetTokenAccountApi = {
setTokenAccount(owner: Address, mint: Address, update: TokenAccountUpdate, tokenProgram?: Address): null;
};
Expand Down Expand Up @@ -217,6 +221,7 @@ export type SurfnetCheatcodesApi = SurfnetCloneProgramAccountApi &
SurfnetResetNetworkApi &
SurfnetResumeClockApi &
SurfnetSetAccountApi &
SurfnetSetMintApi &
SurfnetSetProgramAuthorityApi &
SurfnetSetSupplyApi &
SurfnetSetTokenAccountApi &
Expand Down
50 changes: 49 additions & 1 deletion crates/types/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,53 @@ pub struct ConfidentialTransferAccountUpdate {
pub maximum_pending_balance_credit_counter: Option<u64>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(
feature = "ts-bindings",
derive(ts_rs::TS),
ts(export, optional_fields)
)]
pub struct MintUpdate {
/// providing this value sets the number of decimals of the mint
pub decimals: Option<u8>,
/// providing this value sets the mint authority: a base58 pubkey, or the
/// literal string "null" to clear the authority
#[cfg_attr(feature = "ts-bindings", ts(optional, type = "string"))]
pub mint_authority: Option<SetSomeAccount>,
/// providing this value sets the total supply of the mint
#[cfg_attr(feature = "ts-bindings", ts(optional, type = "number | bigint"))]
pub supply: Option<u64>,
/// providing this value writes the Token-2022 confidential-transfer mint
/// extension (Token-2022 only)
pub confidential: Option<ConfidentialTransferMintUpdate>,
}

/// Writes the Token-2022 `ConfidentialTransferMint` extension on a mint created
/// via `surfnet_setMint`.
///
/// This is a test-only cheatcode: it writes the extension directly, bypassing
/// the real on-chain `ConfidentialTransferInitializeMint` instruction, so a mint
/// with a chosen auditor can be created without forking one from mainnet.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(
feature = "ts-bindings",
derive(ts_rs::TS),
ts(export, optional_fields)
)]
pub struct ConfidentialTransferMintUpdate {
/// The authority that approves new confidential accounts and updates this
/// config (base58). Omitted leaves it null.
pub authority: Option<String>,
/// The auditor's ElGamal public key (base58 or base64, 32 bytes), which every
/// confidential transfer on this mint also encrypts its amount to. Omitted
/// leaves it null, i.e. no auditor.
pub auditor_elgamal_pubkey: Option<String>,
/// Whether new confidential accounts are approved on creation (default true).
pub auto_approve_new_accounts: Option<bool>,
}

/// The owner's confidential-transfer secrets, passed to
/// `surfnet_getConfidentialBalance` so it can decrypt the account.
///
Expand Down Expand Up @@ -1783,7 +1830,7 @@ pub enum CheatcodeFilter {
/// `surfpool-core/src/rpc/surfnet_cheatcodes.rs` asserts it matches the
/// methods actually registered by the `SurfnetCheatcodes` trait, so adding,
/// removing, or renaming a cheatcode without updating this list fails CI.
pub const SURFNET_CHEATCODE_METHODS: [&str; 28] = [
pub const SURFNET_CHEATCODE_METHODS: [&str; 29] = [
"surfnet_cloneProgramAccount",
"surfnet_deriveConfidentialKeys",
"surfnet_disableCheatcode",
Expand All @@ -1805,6 +1852,7 @@ pub const SURFNET_CHEATCODE_METHODS: [&str; 28] = [
"surfnet_resetNetwork",
"surfnet_resumeClock",
"surfnet_setAccount",
"surfnet_setMint",
"surfnet_setProgramAuthority",
"surfnet_setSupply",
"surfnet_setTokenAccount",
Expand Down
Loading