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
677 changes: 673 additions & 4 deletions crates/core/src/rpc/surfnet_cheatcodes.rs

Large diffs are not rendered by default.

192 changes: 188 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,124 @@ 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.
///
/// Backs the `surfnet_setMint` cheatcode. `original` is the mint's current data:
/// its other extensions are kept, and a field `conf` omits keeps its current
/// value, which is null (auto-approve true) when the extension is first written.
pub fn build_confidential_mint_data(
original: &[u8],
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 base_len = spl_token_2022_interface::state::Mint::LEN;
let mint_len = StateWithExtensions::<spl_token_2022_interface::state::Mint>::unpack(original)
.and_then(|state| state.try_get_new_account_len::<ConfidentialTransferMint>())
.map_err(|e| format!("failed to size confidential mint: {e}"))?
.max(original.len());

let mut buffer = vec![0u8; mint_len];
buffer[base_len..original.len()].copy_from_slice(&original[base_len..]);
// `unpack_uninitialized` refuses a buffer whose account-type byte is already set.
if let Some(account_type) = buffer.get_mut(spl_token_2022_interface::state::Account::LEN) {
*account_type = 0;
}
Comment on lines +1487 to +1489

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Wrong mint marker offset When setMint adds confidential support to a Token-2022 mint that already has extensions, this clears byte 165 instead of the mint’s account-type byte at offset 82. The existing marker remains set, so unpack_uninitialized rejects the buffer and the update fails.

Suggested change
if let Some(account_type) = buffer.get_mut(spl_token_2022_interface::state::Account::LEN) {
*account_type = 0;
}
if let Some(account_type) = buffer.get_mut(spl_token_2022_interface::state::Mint::LEN) {
*account_type = 0;
}

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}"))?;

if state.get_extension::<ConfidentialTransferMint>().is_err() {
state
.init_extension::<ConfidentialTransferMint>(false)
.map_err(|e| format!("failed to init confidential mint extension: {e}"))?
.auto_approve_new_accounts = true.into();
}
let ct = state
.get_extension_mut::<ConfidentialTransferMint>()
.map_err(|e| format!("failed to read confidential mint extension: {e}"))?;
if let Some(authority) = authority {
ct.authority = authority.into();
}
if let Some(auto_approve) = conf.auto_approve_new_accounts {
ct.auto_approve_new_accounts = auto_approve.into();
}
if let Some(key) = auditor_elgamal_pubkey {
ct.auditor_elgamal_pubkey = PodElGamalPubkey::from(key).into();
}

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]
fn an_auditor_key_that_is_not_elgamal_material_is_refused() {
let base = spl_token_2022_interface::state::Mint {
is_initialized: true,
..Default::default()
};
let original = MintAccount::SplToken2022(base).pack_into_vec();

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

let auditor = PodElGamalPubkey::from(ElGamalKeypair::new_rand().pubkey_owned());
let conf = ConfidentialTransferMintUpdate {
auditor_elgamal_pubkey: Some(bs58::encode(bytes_of(&auditor)).into_string()),
..Default::default()
};
build_confidential_mint_data(&original, &base, &conf)
.expect("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 +1812,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,29 @@
// @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 keeps the current value, null when first written.
*/
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
* keeps the current value, null (no auditor) when first written.
*/
auditorElgamalPubkey?: string,
/**
* Whether new confidential accounts are approved on creation. Omitted keeps
* the current value, true when first written.
*/
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
Loading
Loading