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

[token-2022] Add elgamal registry account #7341

Merged
merged 22 commits into from
Oct 25, 2024

Conversation

samkim-crypto
Copy link
Contributor

@samkim-crypto samkim-crypto commented Oct 8, 2024

Problem

Currently, it is not possible to use associated token accounts for confidential transfers because in order to initialize a confidential transfer account, the owner must initialize it with an ElGamal public key.

It would be nice to have a special program "registry" program where a user can store an ElGamal public key that it can use for any mint with confidential transfers enabled.

Summary of Changes

  • I created a simple ElGamal registry program where a user can store its ElGamal public key in a PDA account that is derived from the user's wallet address - ba60c3d. The program has two instructions. The CreateRegistry instruction derives the PDA, invokes the system program to create the account, and then initializes the registry account. The UpdateRegistry instruction checks the owner signature and updates the old ElGamal key with a new key. I believe the UpdateRegistry instruction does not need to check the PDA address, but please correct me if I am overlooking something 🙏 .
    • In the process, I refactored the proof extraction logic that lived in program-2022 to confidential-transfer-proof-extraction module since this will be shared by both program-2022 and ElGamal registry program - eef0666
  • I added a new ConfigureAccountWithRegistry instruction in the confidential transfer extension which is identical to the ConfigureAccount instruction, but skips the signature and zk proof verification - 3104459. I believe the token program itself just need to check the provided registry account is owned by the registry program and does not need to necessarily check the validity of the PDA address, but please correct me if I am overlooking something 🙏 . Another thing to note is the fact that on ConfigureAccountWithRegistry instruction, the program initializes the confidential transfer account with default maximum pending credits counter and also AeCiphertext::default() (all zero ciphertext). Unlike ElGamal ciphertexts, the all-zero symmetric ciphertext is not really valid, but I added this logic to simplify the program at the cost of increasing the complexity slightly on the client side (client can just interpret all zero ciphertext as encrypting 0). An alternative would be to add a valid zero-ciphertext field to the ElGamal registry account, but I thought this would make things a bit more complicated.
  • Added support for ConfigureAccountWithRegistry instruction in the token-client - 1a8dc29
  • Added tests to program-2022-test - 4862464

Copy link
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

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

Looks pretty good overall! Mostly small things, let me know what you think

Cargo.lock Outdated Show resolved Hide resolved
token/confidential-transfer/proof-extraction/Cargo.toml Outdated Show resolved Hide resolved
token/confidential-transfer/elgamal-registry/Cargo.toml Outdated Show resolved Hide resolved
token/confidential-transfer/proof-extraction/src/lib.rs Outdated Show resolved Hide resolved
Comment on lines 2902 to 2918
// configure account using ElGamal registry
let alice_account_keypair = Keypair::new();
let alice_token_account = alice_account_keypair.pubkey();
token
.create_auxiliary_token_account_with_extension_space(
&alice_account_keypair,
&alice_token_account,
vec![ExtensionType::ConfidentialTransferAccount],
)
.await
.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh gosh, this makes me realize something we haven't thought about yet. Should we make reallocate on token-2022 permissionless? Otherwise, it's not possible to configure someone else's account.

If we want to go with the safer change, we can have configure_with_registry optionally reallocate the account.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yeah completely missed that. It seems that if all accounts are to be rent-exempt, then there seems to be not much harm in making it permissionless? I'll think about it today and let's discuss tomorrow.

Copy link
Contributor

Choose a reason for hiding this comment

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

There is one slightly strange attack -- if we're competing in a trading environment, I could reallocate your token account to be as big as possible, which would count against the loaded data costs of your transaction, and try to make your transaction fail.

I'd probably go with special-casing this behavior and only reallocating for the confidential token account extension to be totally safe, but maybe I'm being overly cautious.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yes I forgot that there was a MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES field. Yeah in this case, it seems safest to just make reallocation be permissionless for confidential extensions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I added the option to reallocate when configuring confidential transfer accounts with registry.

@@ -1,2 +1,2 @@
[toolchain]
channel = "1.78.0"
channel = "1.79.0"
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this bumped intentionally?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, that accidentally got in. I will revert.

pub enum RegistryInstruction {
/// Initialize an ElGamal public key registry.
///
/// 0. `[writable, signer]` The funding account (must be a system account)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the other part of my comment got lost under the typo suggestion -- could we remove this and have the client function automatically transfer the rent-exempt lamports into the account so that the user does not need to pass a mutable signer to the program?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I updated the program so that the registry program just allocates and assigns ownership of the PDA account. Please take a look!

@samkim-crypto samkim-crypto force-pushed the elgamal-key branch 3 times, most recently from b96645c to 8ea238e Compare October 23, 2024 09:32
Copy link
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

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

Looking really close! Sorry, I did one last pass over the whole thing and found a couple more bits

Comment on lines 679 to 688
/// Data expected by
/// `ConfidentialTransferInstruction::ConfigureAccountWithRegistry`
#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
#[repr(C)]
pub struct ConfigureAccountWithRegistryInstructionData {
/// Reallocate token account if it is not large enough for the
/// `ConfidentialTransfer` extension.
pub reallocate_account: PodBool,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We can probably use the presence of the payer and system program to signal that the caller is ok with doing a reallocation, making the instruction data here unnecessary. What do you think?

Copy link
Contributor Author

@samkim-crypto samkim-crypto Oct 24, 2024

Choose a reason for hiding this comment

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

Yeah that is a good idea! I made the instruction constructor logic this way, but I missed having that logic in the program.


let elgamal_registry_account_seeds: &[&[_]] = &[
REGISTRY_ADDRESS_SEED,
&wallet_account_info.key.to_bytes(),
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: to_bytes() creates a new array of 32 bytes, so it's better to use as_ref()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point!

Comment on lines +156 to +159
current_extension_types.push(ExtensionType::ConfidentialTransferAccount);
let needed_account_len =
ExtensionType::try_calculate_account_len::<Account>(&current_extension_types)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can you add a comment saying that ExtensionType::try_calculate_account_len dedupes extension types, making this correct?


// reallocate
msg!(
"account needs realloc, +{:?} bytes",
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: is there a reason to use the Debug format string of {:?} instead of the normal one of {}? If not, let's go with the normal formatting

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I modeled the function over the reallocate processor logic and used debug mode for consistency with that function.

Btw, I considered refactoring the process_reallocate logic into sub-functions and recycling the logic for configuring confidential transfers, but I thought that would make things less readable. I can do the refactoring if you think that would be cleaner.

) -> ProgramResult {
let mut current_extension_types = {
let token_account = token_account_info.data.borrow();
let account = StateWithExtensions::<Account>::unpack(&token_account)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can you use PodStateWithExtensions::<PodAccount> instead for better CU usage?

Comment on lines 119 to 126
if reallocate {
let payer_info = next_account_info(account_info_iter)?;
let system_program_info = next_account_info(account_info_iter)?;
reallocate_for_configure_account_with_registry(
token_account_info,
payer_info,
system_program_info,
)?;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We can maybe make this processing logic more clever. Rather than always trying pull out the payer / system program info and then deciding to reallocate, the instruction can always try to reallocate, and only if extra lamports are needed, try to extract the payer and system program. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about this as well since that logic will be cleaner. The reason why I went with the current implementation is that if the user does not want to reallocate the account, then we would be consuming unnecessary compute units by unpacking the account and calculating the needed account length. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, that's a good point on the CUs... could you do a little test to see how many CUs the extra logic takes up? I'm hoping it's not too many, but there is a vector allocation, so who knows

Copy link
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

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

Looks good to me, thanks! We can do the CU calculation with a separate piece of work, this can go in first

@samkim-crypto samkim-crypto merged commit c59e2f3 into solana-labs:master Oct 25, 2024
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants