From 023cce4194a25372a594b803696d7871c275ec49 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Thu, 16 Mar 2023 07:35:20 +0900 Subject: [PATCH] [zk-token-sdk] Add option to create proof context state in the proof verification program (#29996) * extend verifiable trait * add PodBool * implement ZkProofData trait * add proof context program to zk-token-proof program * update tests for close account * add close account instruction * reorganize tests * complete tests * clean up and add docs * clean up pod * add proof program state * update tests * move proof program tests as separate module * clippy * cargo sort * cargo fmt * re-organize visibility * add context state description * update maintainer reference * change `VerifyProofData` and `ProofContextState` to pod * add tests for mixing proof types * add tests for self owned context state accounts * cargo fmt * remove unnecessary scoping and add comments on scopes * re-organize proof instructions * clippy * update zk-token-proof-test to 1.16.0 * upgrade spl-token-2022 to 0.6.1 * reoganize proof type * cargo lock * remove ZkProofContext trait (cherry picked from commit 2d58bb287d57f1e19e58f024c7d6766c86626049) # Conflicts: # Cargo.lock # Cargo.toml # programs/bpf/Cargo.lock # zk-token-sdk/src/instruction/transfer.rs --- Cargo.lock | 17 + Cargo.toml | 300 +++++++ programs/bpf/Cargo.lock | 6 + programs/zk-token-proof-tests/Cargo.toml | 15 + .../tests/process_transaction.rs | 788 ++++++++++++++++++ programs/zk-token-proof/src/lib.rs | 129 ++- zk-token-sdk/src/instruction/close_account.rs | 60 +- zk-token-sdk/src/instruction/mod.rs | 34 +- .../src/instruction/pubkey_validity.rs | 49 +- zk-token-sdk/src/instruction/transfer.rs | 91 +- .../src/instruction/transfer_with_fee.rs | 119 +-- zk-token-sdk/src/instruction/withdraw.rs | 60 +- .../src/instruction/withdraw_withheld.rs | 81 +- zk-token-sdk/src/lib.rs | 1 + zk-token-sdk/src/zk_token_elgamal/pod.rs | 23 +- .../src/zk_token_proof_instruction.rs | 215 ++++- zk-token-sdk/src/zk_token_proof_state.rs | 72 ++ 17 files changed, 1823 insertions(+), 237 deletions(-) create mode 100644 programs/zk-token-proof-tests/Cargo.toml create mode 100644 programs/zk-token-proof-tests/tests/process_transaction.rs create mode 100644 zk-token-sdk/src/zk_token_proof_state.rs diff --git a/Cargo.lock b/Cargo.lock index 34c05106cdbdd1..69ef6bd6f75778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6538,6 +6538,17 @@ dependencies = [ "solana-zk-token-sdk 1.14.18", ] +[[package]] +name = "solana-zk-token-proof-program-tests" +version = "1.16.0" +dependencies = [ + "bytemuck", + "solana-program-runtime", + "solana-program-test", + "solana-sdk 1.16.0", + "solana-zk-token-sdk 1.16.0", +] + [[package]] name = "solana-zk-token-sdk" version = "1.14.16" @@ -6680,9 +6691,15 @@ dependencies = [ [[package]] name = "spl-token-2022" +<<<<<<< HEAD version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0edb869dbe159b018f17fb9bfa67118c30f232d7f54a73742bc96794dff77ed8" +======= +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0043b590232c400bad5ee9eb983ced003d15163c4c5d56b090ac6d9a57457b47" +>>>>>>> 2d58bb287 ([zk-token-sdk] Add option to create proof context state in the proof verification program (#29996)) dependencies = [ "arrayref", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 9aad90aeb4f175..f0950266c313de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,12 @@ members = [ "programs/stake", "programs/vote", "programs/zk-token-proof", +<<<<<<< HEAD +======= + "programs/zk-token-proof-tests", + "pubsub-client", + "quic-client", +>>>>>>> 2d58bb287 ([zk-token-sdk] Add option to create proof context state in the proof verification program (#29996)) "rayon-threadlimit", "rbpf-cli", "remote-wallet", @@ -94,3 +100,297 @@ exclude = [ # This prevents a Travis CI error when building for Windows. resolver = "2" +<<<<<<< HEAD +======= + +[workspace.package] +version = "1.16.0" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana" +homepage = "https://solanalabs.com/" +license = "Apache-2.0" +edition = "2021" + +[workspace.dependencies] +aes-gcm-siv = "0.10.3" +ahash = "0.7.6" +anyhow = "1.0.58" +ark-bn254 = "0.3.0" +ark-ec = "0.3.0" +ark-ff = "0.3.0" +array-bytes = "=1.4.1" +arrayref = "0.3.6" +assert_cmd = "2.0" +assert_matches = "1.5.0" +async-mutex = "1.4.0" +async-trait = "0.1.57" +atty = "0.2.11" +backoff = "0.4.0" +base64 = "0.13.0" +bincode = "1.3.3" +bitflags = "1.3.1" +blake3 = "1.3.1" +block-buffer = "0.9.0" +borsh = "0.9.3" +borsh-derive = "0.9.1" +bs58 = "0.4.0" +bv = "0.11.1" +byte-unit = "4.0.14" +bytecount = "0.6.3" +bytemuck = "1.11.0" +byteorder = "1.4.3" +bytes = "1.2" +bzip2 = "0.4.4" +caps = "0.5.4" +cargo_metadata = "0.15.0" +cc = "1.0.79" +chrono = { version = "0.4.23", default-features = false } +chrono-humanize = "0.2.1" +cipher = "0.4" +clap = "2.33.1" +console = "0.15.0" +console_error_panic_hook = "0.1.7" +console_log = "0.2.0" +const_format = "0.2.26" +core_affinity = "0.5.10" +criterion-stats = "0.3.0" +crossbeam-channel = "0.5.6" +csv = "1.1.6" +ctrlc = "3.2.3" +curve25519-dalek = "3.2.1" +dashmap = "4.0.2" +derivation-path = { version = "0.2.0", default-features = false } +dialoguer = "0.10.2" +digest = "0.10.1" +dir-diff = "0.3.2" +dirs-next = "2.0.0" +dlopen = "0.1.8" +dlopen_derive = "0.1.4" +eager = "0.1.0" +ed25519-dalek = "=1.0.1" +ed25519-dalek-bip32 = "0.2.0" +either = "1.7.0" +enum_dispatch = "0.3.8" +enum-iterator = "1.2.0" +env_logger = "0.9.3" +etcd-client = "0.8.1" +fast-math = "0.1" +fd-lock = "3.0.6" +flate2 = "1.0.25" +fnv = "1.0.7" +fs_extra = "1.2.0" +futures = "0.3.24" +futures-util = "0.3.26" +gag = "1.0.0" +generic-array = { version = "0.14.6", default-features = false } +gethostname = "0.2.3" +getrandom = "0.1.14" +goauth = "0.13.1" +hashbrown = "0.12" +hex = "0.4.3" +hidapi = { version = "1.4.1", default-features = false } +histogram = "0.6.9" +hmac = "0.12.1" +http = "0.2.8" +humantime = "2.0.1" +hyper = "0.14.20" +hyper-proxy = "0.9.1" +im = "15.1.0" +index_list = "0.2.7" +indexmap = "1.9.1" +indicatif = "0.17.1" +Inflector = "0.11.4" +itertools = "0.10.5" +jemallocator = { package = "tikv-jemallocator", version = "0.4.1", features = ["unprefixed_malloc_on_supported_platforms"] } +js-sys = "0.3.59" +json5 = "0.4.1" +jsonrpc-core = "18.0.0" +jsonrpc-core-client = "18.0.0" +jsonrpc-derive = "18.0.0" +jsonrpc-http-server = "18.0.0" +jsonrpc-ipc-server = "18.0.0" +jsonrpc-pubsub = "18.0.0" +jsonrpc-server-utils = "18.0.0" +lazy_static = "1.4.0" +libc = "0.2.131" +libloading = "0.7.4" +libsecp256k1 = "0.6.0" +log = "0.4.17" +lru = "0.7.7" +lz4 = "1.24.0" +matches = "0.1.9" +memmap2 = "0.5.8" +memoffset = "0.8" +merlin = "3" +min-max-heap = "1.3.0" +modular-bitfield = "0.11.2" +nix = "0.25.0" +num-bigint = "0.4.3" +num_cpus = "1.13.1" +num_enum = "0.5.7" +num-derive = "0.3" +num-traits = "0.2" +once_cell = "1.13.0" +openssl = "0.10" +ouroboros = "0.15.0" +parking_lot = "0.12" +pbkdf2 = { version = "0.11.0", default-features = false } +pem = "1.1.1" +percentage = "0.1.0" +pickledb = { version = "0.5.1", default-features = false } +pkcs8 = "0.8.0" +predicates = "2.1" +pretty-hex = "0.3.0" +proc-macro2 = "1.0.19" +proptest = "1.0" +prost = "0.11.6" +prost-types = "0.11.6" +protobuf-src = "1.0.5" +qstring = "0.7.2" +quinn = "0.9.3" +quinn-proto = "0.9.2" +quinn-udp = "0.3.2" +quote = "1.0" +rand = "0.7.0" +rand_chacha = "0.2.2" +rand_core = "0.6.4" +raptorq = "1.7.0" +rayon = "1.5.3" +rcgen = "0.10.0" +reed-solomon-erasure = "6.0.0" +regex = "1.6.0" +rolling-file = "0.2.0" +reqwest = { version = "0.11.12", default-features = false } +rpassword = "7.2" +rustc_version = "0.4" +rustls = { version = "0.20.6", default-features = false } +rustversion = "1.0.11" +scopeguard = "1.1.0" +semver = "1.0.16" +serde = "1.0.152" +serde_bytes = "0.11.9" +serde_derive = "1.0.103" +serde_json = "1.0.83" +serde_yaml = "0.9.13" +serial_test = "0.9.0" +serde_with = { version = "2.2.0", default-features = false } +sha2 = "0.10.6" +sha3 = "0.10.4" +signal-hook = "0.3.14" +smpl_jwt = "0.7.1" +socket2 = "0.4.7" +soketto = "0.7" +solana_rbpf = "=0.2.40" +solana-account-decoder = { path = "account-decoder", version = "=1.16.0" } +solana-address-lookup-table-program = { path = "programs/address-lookup-table", version = "=1.16.0" } +solana-banks-client = { path = "banks-client", version = "=1.16.0" } +solana-banks-interface = { path = "banks-interface", version = "=1.16.0" } +solana-banks-server = { path = "banks-server", version = "=1.16.0" } +solana-bench-tps = { path = "bench-tps", version = "=1.16.0" } +solana-bloom = { path = "bloom", version = "=1.16.0" } +solana-bpf-loader-program = { path = "programs/bpf_loader", version = "=1.16.0" } +solana-bucket-map = { path = "bucket_map", version = "=1.16.0" } +solana-connection-cache = { path = "connection-cache", version = "=1.16.0", default-features = false } +solana-clap-utils = { path = "clap-utils", version = "=1.16.0" } +solana-clap-v3-utils = { path = "clap-v3-utils", version = "=1.16.0" } +solana-cli = { path = "cli", version = "=1.16.0" } +solana-cli-config = { path = "cli-config", version = "=1.16.0" } +solana-cli-output = { path = "cli-output", version = "=1.16.0" } +solana-client = { path = "client", version = "=1.16.0" } +solana-compute-budget-program = { path = "programs/compute-budget", version = "=1.16.0" } +solana-config-program = { path = "programs/config", version = "=1.16.0" } +solana-core = { path = "core", version = "=1.16.0" } +solana-download-utils = { path = "download-utils", version = "=1.16.0" } +solana-entry = { path = "entry", version = "=1.16.0" } +solana-faucet = { path = "faucet", version = "=1.16.0" } +solana-frozen-abi = { path = "frozen-abi", version = "=1.16.0" } +solana-frozen-abi-macro = { path = "frozen-abi/macro", version = "=1.16.0" } +solana-genesis = { path = "genesis", version = "=1.16.0" } +solana-genesis-utils = { path = "genesis-utils", version = "=1.16.0" } +solana-geyser-plugin-interface = { path = "geyser-plugin-interface", version = "=1.16.0" } +solana-geyser-plugin-manager = { path = "geyser-plugin-manager", version = "=1.16.0" } +solana-gossip = { path = "gossip", version = "=1.16.0" } +solana-ledger = { path = "ledger", version = "=1.16.0" } +solana-local-cluster = { path = "local-cluster", version = "=1.16.0" } +solana-logger = { path = "logger", version = "=1.16.0" } +solana-measure = { path = "measure", version = "=1.16.0" } +solana-merkle-tree = { path = "merkle-tree", version = "=1.16.0" } +solana-metrics = { path = "metrics", version = "=1.16.0" } +solana-net-utils = { path = "net-utils", version = "=1.16.0" } +solana-notifier = { path = "notifier", version = "=1.16.0" } +solana-perf = { path = "perf", version = "=1.16.0" } +solana-poh = { path = "poh", version = "=1.16.0" } +solana-program = { path = "sdk/program", version = "=1.16.0" } +solana-program-runtime = { path = "program-runtime", version = "=1.16.0" } +solana-program-test = { path = "program-test", version = "=1.16.0" } +solana-pubsub-client = { path = "pubsub-client", version = "=1.16.0" } +solana-quic-client = { path = "quic-client", version = "=1.16.0" } +solana-rayon-threadlimit = { path = "rayon-threadlimit", version = "=1.16.0" } +solana-remote-wallet = { path = "remote-wallet", version = "=1.16.0", default-features = false } +solana-rpc = { path = "rpc", version = "=1.16.0" } +solana-rpc-client = { path = "rpc-client", version = "=1.16.0", default-features = false } +solana-rpc-client-api = { path = "rpc-client-api", version = "=1.16.0" } +solana-rpc-client-nonce-utils = { path = "rpc-client-nonce-utils", version = "=1.16.0" } +solana-runtime = { path = "runtime", version = "=1.16.0" } +solana-sdk = { path = "sdk", version = "=1.16.0" } +solana-sdk-macro = { path = "sdk/macro", version = "=1.16.0" } +solana-send-transaction-service = { path = "send-transaction-service", version = "=1.16.0" } +solana-stake-program = { path = "programs/stake", version = "=1.16.0" } +solana-storage-bigtable = { path = "storage-bigtable", version = "=1.16.0" } +solana-storage-proto = { path = "storage-proto", version = "=1.16.0" } +solana-streamer = { path = "streamer", version = "=1.16.0" } +solana-sys-tuner = { path = "sys-tuner", version = "=1.16.0" } +solana-test-validator = { path = "test-validator", version = "=1.16.0" } +solana-thin-client = { path = "thin-client", version = "=1.16.0" } +solana-tpu-client = { path = "tpu-client", version = "=1.16.0", default-features = false } +solana-transaction-status = { path = "transaction-status", version = "=1.16.0" } +solana-udp-client = { path = "udp-client", version = "=1.16.0" } +solana-version = { path = "version", version = "=1.16.0" } +solana-vote-program = { path = "programs/vote", version = "=1.16.0" } +solana-zk-token-proof-program = { path = "programs/zk-token-proof", version = "=1.16.0" } +solana-zk-token-sdk = { path = "zk-token-sdk", version = "=1.16.0" } +spl-associated-token-account = "=1.1.3" +spl-instruction-padding = "0.1" +spl-memo = "=3.0.1" +spl-token = "=3.5.0" +spl-token-2022 = "=0.6.1" +static_assertions = "1.1.0" +stream-cancel = "0.8.1" +strum = "0.24" +strum_macros = "0.24" +subtle = "2.4.1" +symlink = "0.1.0" +syn = "1.0" +sys-info = "0.9.1" +sysctl = "0.4.4" +systemstat = "0.2.3" +tar = "0.4.38" +tarpc = "0.29.0" +tempfile = "3.3.0" +test-case = "2.2.2" +thiserror = "1.0.31" +tiny-bip39 = "0.8.2" +tokio = "~1.14.1" +tokio-serde = "0.8" +tokio-stream = "0.1.9" +tokio-tungstenite = "0.17.2" +tokio-util = "0.6" +tonic = "0.8.3" +tonic-build = "0.8.4" +trees = "0.4.2" +tungstenite = "0.17.2" +unix_socket2 = "0.5.4" +uriparse = "0.6.4" +url = "2.2.2" +users = "0.10.0" +wasm-bindgen = "0.2" +winapi = "0.3.8" +winreg = "0.10" +x509-parser = "0.14.0" +zeroize = { version = "1.3", default-features = false } +zstd = "0.11.2" + +# for details, see https://github.com/solana-labs/crossbeam/commit/fd279d707025f0e60951e429bf778b4813d1b6bf +[patch.crates-io] +crossbeam-epoch = { git = "https://github.com/solana-labs/crossbeam", rev = "fd279d707025f0e60951e429bf778b4813d1b6bf" } +>>>>>>> 2d58bb287 ([zk-token-sdk] Add option to create proof context state in the proof verification program (#29996)) diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index a6f7d8ec5ba175..fbdebe2ed7950e 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -5965,9 +5965,15 @@ dependencies = [ [[package]] name = "spl-token-2022" +<<<<<<< HEAD:programs/bpf/Cargo.lock version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0edb869dbe159b018f17fb9bfa67118c30f232d7f54a73742bc96794dff77ed8" +======= +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0043b590232c400bad5ee9eb983ced003d15163c4c5d56b090ac6d9a57457b47" +>>>>>>> 2d58bb287 ([zk-token-sdk] Add option to create proof context state in the proof verification program (#29996)):programs/sbf/Cargo.lock dependencies = [ "arrayref", "bytemuck", diff --git a/programs/zk-token-proof-tests/Cargo.toml b/programs/zk-token-proof-tests/Cargo.toml new file mode 100644 index 00000000000000..8ddb5656a73b87 --- /dev/null +++ b/programs/zk-token-proof-tests/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "solana-zk-token-proof-program-tests" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana" +version = "1.16.0" +license = "Apache-2.0" +edition = "2021" +publish = false + +[dev-dependencies] +bytemuck = { version = "1.11.0", features = ["derive"] } +solana-program-runtime = { path = "../../program-runtime", version = "=1.16.0" } +solana-program-test = { path = "../../program-test", version = "=1.16.0" } +solana-sdk = { path = "../../sdk", version = "=1.16.0" } +solana-zk-token-sdk = { path = "../../zk-token-sdk", version = "=1.16.0" } diff --git a/programs/zk-token-proof-tests/tests/process_transaction.rs b/programs/zk-token-proof-tests/tests/process_transaction.rs new file mode 100644 index 00000000000000..555295e4e9a6ed --- /dev/null +++ b/programs/zk-token-proof-tests/tests/process_transaction.rs @@ -0,0 +1,788 @@ +use { + bytemuck::Pod, + solana_program_test::*, + solana_sdk::{ + instruction::InstructionError, + signature::Signer, + signer::keypair::Keypair, + system_instruction, + transaction::{Transaction, TransactionError}, + }, + solana_zk_token_sdk::{ + encryption::elgamal::ElGamalKeypair, instruction::*, zk_token_proof_instruction::*, + zk_token_proof_program, zk_token_proof_state::ProofContextState, + }, + std::mem::size_of, +}; + +const VERIFY_INSTRUCTION_TYPES: [ProofInstruction; 6] = [ + ProofInstruction::VerifyCloseAccount, + ProofInstruction::VerifyWithdraw, + ProofInstruction::VerifyWithdrawWithheldTokens, + ProofInstruction::VerifyTransfer, + ProofInstruction::VerifyTransferWithFee, + ProofInstruction::VerifyPubkeyValidity, +]; + +#[tokio::test] +async fn test_close_account() { + let elgamal_keypair = ElGamalKeypair::new_rand(); + + let zero_ciphertext = elgamal_keypair.public.encrypt(0_u64); + let success_proof_data = CloseAccountData::new(&elgamal_keypair, &zero_ciphertext).unwrap(); + + let incorrect_keypair = ElGamalKeypair { + public: ElGamalKeypair::new_rand().public, + secret: ElGamalKeypair::new_rand().secret, + }; + let fail_proof_data = CloseAccountData::new(&incorrect_keypair, &zero_ciphertext).unwrap(); + + test_verify_proof_without_context( + ProofInstruction::VerifyCloseAccount, + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_verify_proof_with_context( + ProofInstruction::VerifyCloseAccount, + size_of::>(), + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_close_context_state( + ProofInstruction::VerifyCloseAccount, + size_of::>(), + &success_proof_data, + ) + .await; +} + +#[tokio::test] +async fn test_withdraw_withheld_tokens() { + let elgamal_keypair = ElGamalKeypair::new_rand(); + let destination_keypair = ElGamalKeypair::new_rand(); + + let amount: u64 = 0; + let withdraw_withheld_authority_ciphertext = elgamal_keypair.public.encrypt(amount); + + let success_proof_data = WithdrawWithheldTokensData::new( + &elgamal_keypair, + &destination_keypair.public, + &withdraw_withheld_authority_ciphertext, + amount, + ) + .unwrap(); + + let incorrect_keypair = ElGamalKeypair { + public: ElGamalKeypair::new_rand().public, + secret: ElGamalKeypair::new_rand().secret, + }; + let fail_proof_data = WithdrawWithheldTokensData::new( + &incorrect_keypair, + &destination_keypair.public, + &withdraw_withheld_authority_ciphertext, + amount, + ) + .unwrap(); + + test_verify_proof_without_context( + ProofInstruction::VerifyWithdrawWithheldTokens, + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_verify_proof_with_context( + ProofInstruction::VerifyWithdrawWithheldTokens, + size_of::>(), + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_close_context_state( + ProofInstruction::VerifyWithdrawWithheldTokens, + size_of::>(), + &success_proof_data, + ) + .await; +} + +#[tokio::test] +async fn test_transfer() { + let source_keypair = ElGamalKeypair::new_rand(); + let dest_pubkey = ElGamalKeypair::new_rand().public; + let auditor_pubkey = ElGamalKeypair::new_rand().public; + + let spendable_balance: u64 = 0; + let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance); + + let transfer_amount: u64 = 0; + + let success_proof_data = TransferData::new( + transfer_amount, + (spendable_balance, &spendable_ciphertext), + &source_keypair, + (&dest_pubkey, &auditor_pubkey), + ) + .unwrap(); + + let incorrect_keypair = ElGamalKeypair { + public: ElGamalKeypair::new_rand().public, + secret: ElGamalKeypair::new_rand().secret, + }; + + let fail_proof_data = TransferData::new( + transfer_amount, + (spendable_balance, &spendable_ciphertext), + &incorrect_keypair, + (&dest_pubkey, &auditor_pubkey), + ) + .unwrap(); + + test_verify_proof_without_context( + ProofInstruction::VerifyTransfer, + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_verify_proof_with_context( + ProofInstruction::VerifyTransfer, + size_of::>(), + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_close_context_state( + ProofInstruction::VerifyTransfer, + size_of::>(), + &success_proof_data, + ) + .await; +} + +#[tokio::test] +async fn test_transfer_with_fee() { + let source_keypair = ElGamalKeypair::new_rand(); + let destination_pubkey = ElGamalKeypair::new_rand().public; + let auditor_pubkey = ElGamalKeypair::new_rand().public; + let withdraw_withheld_authority_pubkey = ElGamalKeypair::new_rand().public; + + let spendable_balance: u64 = 120; + let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance); + + let transfer_amount: u64 = 0; + + let fee_parameters = FeeParameters { + fee_rate_basis_points: 400, + maximum_fee: 3, + }; + + let success_proof_data = TransferWithFeeData::new( + transfer_amount, + (spendable_balance, &spendable_ciphertext), + &source_keypair, + (&destination_pubkey, &auditor_pubkey), + fee_parameters, + &withdraw_withheld_authority_pubkey, + ) + .unwrap(); + + let incorrect_keypair = ElGamalKeypair { + public: ElGamalKeypair::new_rand().public, + secret: ElGamalKeypair::new_rand().secret, + }; + + let fail_proof_data = TransferWithFeeData::new( + transfer_amount, + (spendable_balance, &spendable_ciphertext), + &incorrect_keypair, + (&destination_pubkey, &auditor_pubkey), + fee_parameters, + &withdraw_withheld_authority_pubkey, + ) + .unwrap(); + + test_verify_proof_without_context( + ProofInstruction::VerifyTransferWithFee, + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_verify_proof_with_context( + ProofInstruction::VerifyTransferWithFee, + size_of::>(), + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_close_context_state( + ProofInstruction::VerifyTransferWithFee, + size_of::>(), + &success_proof_data, + ) + .await; +} + +#[tokio::test] +async fn test_withdraw() { + let elgamal_keypair = ElGamalKeypair::new_rand(); + + let current_balance: u64 = 77; + let current_ciphertext = elgamal_keypair.public.encrypt(current_balance); + let withdraw_amount: u64 = 55; + + let success_proof_data = WithdrawData::new( + withdraw_amount, + &elgamal_keypair, + current_balance, + ¤t_ciphertext, + ) + .unwrap(); + + let incorrect_keypair = ElGamalKeypair { + public: ElGamalKeypair::new_rand().public, + secret: ElGamalKeypair::new_rand().secret, + }; + let fail_proof_data = WithdrawData::new( + withdraw_amount, + &incorrect_keypair, + current_balance, + ¤t_ciphertext, + ) + .unwrap(); + + test_verify_proof_without_context( + ProofInstruction::VerifyWithdraw, + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_verify_proof_with_context( + ProofInstruction::VerifyWithdraw, + size_of::>(), + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_close_context_state( + ProofInstruction::VerifyWithdraw, + size_of::>(), + &success_proof_data, + ) + .await; +} + +#[tokio::test] +async fn test_pubkey_validity() { + let elgamal_keypair = ElGamalKeypair::new_rand(); + + let success_proof_data = PubkeyValidityData::new(&elgamal_keypair).unwrap(); + + let incorrect_keypair = ElGamalKeypair { + public: ElGamalKeypair::new_rand().public, + secret: ElGamalKeypair::new_rand().secret, + }; + + let fail_proof_data = PubkeyValidityData::new(&incorrect_keypair).unwrap(); + + test_verify_proof_without_context( + ProofInstruction::VerifyPubkeyValidity, + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_verify_proof_with_context( + ProofInstruction::VerifyPubkeyValidity, + size_of::>(), + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_close_context_state( + ProofInstruction::VerifyPubkeyValidity, + size_of::>(), + &success_proof_data, + ) + .await; +} + +async fn test_verify_proof_without_context( + proof_instruction: ProofInstruction, + success_proof_data: &T, + fail_proof_data: &T, +) where + T: Pod + ZkProofData, + U: Pod, +{ + let mut context = ProgramTest::default().start_with_context().await; + + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + + // verify a valid proof (wihtout creating a context account) + let instructions = vec![proof_instruction.encode_verify_proof(None, success_proof_data)]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + client.process_transaction(transaction).await.unwrap(); + + // try to verify an invalid proof (without creating a context account) + let instructions = vec![proof_instruction.encode_verify_proof(None, fail_proof_data)]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + let err = client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + err, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData) + ); + + // try to verify a valid proof, but with a wrong proof type + for wrong_instruction_type in VERIFY_INSTRUCTION_TYPES { + if proof_instruction == wrong_instruction_type { + continue; + } + + let instruction = + vec![wrong_instruction_type.encode_verify_proof(None, success_proof_data)]; + let transaction = Transaction::new_signed_with_payer( + &instruction, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + let err = client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + err, + TransactionError::InstructionError(0, InstructionError::InvalidInstructionData) + ); + } +} + +async fn test_verify_proof_with_context( + instruction_type: ProofInstruction, + space: usize, + success_proof_data: &T, + fail_proof_data: &T, +) where + T: Pod + ZkProofData, + U: Pod, +{ + let mut context = ProgramTest::default().start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + + let context_state_account = Keypair::new(); + let context_state_authority = Keypair::new(); + + let context_state_info = ContextStateInfo { + context_state_account: &context_state_account.pubkey(), + context_state_authority: &context_state_authority.pubkey(), + }; + + // try to create proof context state with an invalid proof + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account.pubkey(), + rent.minimum_balance(space), + space as u64, + &zk_token_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), fail_proof_data), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account], + recent_blockhash, + ); + let err = client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + err, + TransactionError::InstructionError(1, InstructionError::InvalidInstructionData) + ); + + // try to create proof context state with incorrect account data length + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account.pubkey(), + rent.minimum_balance(space), + (space.checked_sub(1).unwrap()) as u64, + &zk_token_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), success_proof_data), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account], + recent_blockhash, + ); + let err = client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + err, + TransactionError::InstructionError(1, InstructionError::InvalidAccountData) + ); + + // try to create proof context state with insufficient rent + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account.pubkey(), + rent.minimum_balance(space).checked_sub(1).unwrap(), + space as u64, + &zk_token_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), success_proof_data), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account], + recent_blockhash, + ); + let err = client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + err, + TransactionError::InsufficientFundsForRent { account_index: 1 }, + ); + + // try to create proof context state with an invalid `ProofType` + for wrong_instruction_type in VERIFY_INSTRUCTION_TYPES { + if instruction_type == wrong_instruction_type { + continue; + } + + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account.pubkey(), + rent.minimum_balance(space), + space as u64, + &zk_token_proof_program::id(), + ), + wrong_instruction_type + .encode_verify_proof(Some(context_state_info), success_proof_data), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account], + recent_blockhash, + ); + let err = client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + err, + TransactionError::InstructionError(1, InstructionError::InvalidInstructionData) + ); + } + + // successfully create a proof context state + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account.pubkey(), + rent.minimum_balance(space), + space as u64, + &zk_token_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), success_proof_data), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account], + recent_blockhash, + ); + client.process_transaction(transaction).await.unwrap(); + + // try overwriting the context state + let instructions = + vec![instruction_type.encode_verify_proof(Some(context_state_info), success_proof_data)]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + let err = client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + err, + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized) + ); + + // self-owned context state account + let context_state_account_and_authority = Keypair::new(); + let context_state_info = ContextStateInfo { + context_state_account: &context_state_account_and_authority.pubkey(), + context_state_authority: &context_state_account_and_authority.pubkey(), + }; + + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account_and_authority.pubkey(), + rent.minimum_balance(space), + space as u64, + &zk_token_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), success_proof_data), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account_and_authority], + recent_blockhash, + ); + client.process_transaction(transaction).await.unwrap(); +} + +async fn test_close_context_state( + instruction_type: ProofInstruction, + space: usize, + success_proof_data: &T, +) where + T: Pod + ZkProofData, + U: Pod, +{ + let mut context = ProgramTest::default().start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + + let context_state_account = Keypair::new(); + let context_state_authority = Keypair::new(); + + let context_state_info = ContextStateInfo { + context_state_account: &context_state_account.pubkey(), + context_state_authority: &context_state_authority.pubkey(), + }; + + let destination_account = Keypair::new(); + + // create a proof context state + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account.pubkey(), + rent.minimum_balance(space), + space as u64, + &zk_token_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), success_proof_data), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account], + recent_blockhash, + ); + client.process_transaction(transaction).await.unwrap(); + + // try to close context state with incorrect authority + let incorrect_authority = Keypair::new(); + let instruction = close_context_state( + ContextStateInfo { + context_state_account: &context_state_account.pubkey(), + context_state_authority: &incorrect_authority.pubkey(), + }, + &destination_account.pubkey(), + ); + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + &[payer, &incorrect_authority], + recent_blockhash, + ); + let err = client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + err, + TransactionError::InstructionError(0, InstructionError::InvalidAccountOwner) + ); + + // successfully close proof context state + let instruction = close_context_state( + ContextStateInfo { + context_state_account: &context_state_account.pubkey(), + context_state_authority: &context_state_authority.pubkey(), + }, + &destination_account.pubkey(), + ); + let transaction = Transaction::new_signed_with_payer( + &[instruction.clone()], + Some(&payer.pubkey()), + &[payer, &context_state_authority], + recent_blockhash, + ); + client.process_transaction(transaction).await.unwrap(); + + // create and close proof context in a single transaction + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account.pubkey(), + 0_u64, // do not deposit rent + space as u64, + &zk_token_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), success_proof_data), + close_context_state( + ContextStateInfo { + context_state_account: &context_state_account.pubkey(), + context_state_authority: &context_state_authority.pubkey(), + }, + &destination_account.pubkey(), + ), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account, &context_state_authority], + recent_blockhash, + ); + client.process_transaction(transaction).await.unwrap(); + + // close proof context state with owner as destination + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account.pubkey(), + 0_u64, + space as u64, + &zk_token_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), success_proof_data), + close_context_state( + ContextStateInfo { + context_state_account: &context_state_account.pubkey(), + context_state_authority: &context_state_authority.pubkey(), + }, + &context_state_authority.pubkey(), + ), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account, &context_state_authority], + recent_blockhash, + ); + client.process_transaction(transaction).await.unwrap(); + + // try close account with itself as destination + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account.pubkey(), + 0_u64, + space as u64, + &zk_token_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), success_proof_data), + close_context_state( + ContextStateInfo { + context_state_account: &context_state_account.pubkey(), + context_state_authority: &context_state_authority.pubkey(), + }, + &context_state_account.pubkey(), + ), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account, &context_state_authority], + recent_blockhash, + ); + let err = client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + err, + TransactionError::InstructionError(2, InstructionError::InvalidInstructionData) + ); + + // close self-owned proof context accounts + let context_state_account_and_authority = Keypair::new(); + let context_state_info = ContextStateInfo { + context_state_account: &context_state_account_and_authority.pubkey(), + context_state_authority: &context_state_account_and_authority.pubkey(), + }; + + let instructions = vec![ + system_instruction::create_account( + &payer.pubkey(), + &context_state_account_and_authority.pubkey(), + 0_u64, + space as u64, + &zk_token_proof_program::id(), + ), + instruction_type.encode_verify_proof(Some(context_state_info), success_proof_data), + close_context_state(context_state_info, &context_state_account.pubkey()), + ]; + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &context_state_account_and_authority], + recent_blockhash, + ); + client.process_transaction(transaction).await.unwrap(); +} diff --git a/programs/zk-token-proof/src/lib.rs b/programs/zk-token-proof/src/lib.rs index 77d40064435a96..0ffd26598b0dd0 100644 --- a/programs/zk-token-proof/src/lib.rs +++ b/programs/zk-token-proof/src/lib.rs @@ -3,26 +3,114 @@ use { bytemuck::Pod, solana_program_runtime::{ic_msg, invoke_context::InvokeContext}, - solana_sdk::instruction::{InstructionError, TRANSACTION_LEVEL_STACK_HEIGHT}, - solana_zk_token_sdk::zk_token_proof_instruction::*, + solana_sdk::{ + instruction::{InstructionError, TRANSACTION_LEVEL_STACK_HEIGHT}, + system_program, + }, + solana_zk_token_sdk::{ + zk_token_proof_instruction::*, + zk_token_proof_program::id, + zk_token_proof_state::{ProofContextState, ProofContextStateMeta}, + }, std::result::Result, }; -fn verify(invoke_context: &mut InvokeContext) -> Result<(), InstructionError> { +fn process_verify_proof(invoke_context: &mut InvokeContext) -> Result<(), InstructionError> +where + T: Pod + ZkProofData, + U: Pod, +{ let transaction_context = &invoke_context.transaction_context; let instruction_context = transaction_context.get_current_instruction_context()?; let instruction_data = instruction_context.get_instruction_data(); - let instruction = ProofInstruction::decode_data::(instruction_data); - - let proof = instruction.ok_or_else(|| { + let proof_data = ProofInstruction::proof_data::(instruction_data).ok_or_else(|| { ic_msg!(invoke_context, "invalid proof data"); InstructionError::InvalidInstructionData })?; - proof.verify().map_err(|err| { - ic_msg!(invoke_context, "proof verification failed: {:?}", err); + proof_data.verify_proof().map_err(|err| { + ic_msg!(invoke_context, "proof_verification failed: {:?}", err); InstructionError::InvalidInstructionData - }) + })?; + + // create context state if accounts are provided with the instruction + if instruction_context.get_number_of_instruction_accounts() > 0 { + let context_state_authority = *instruction_context + .try_borrow_instruction_account(transaction_context, 1)? + .get_key(); + + let mut proof_context_account = + instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + + if *proof_context_account.get_owner() != id() { + return Err(InstructionError::InvalidAccountOwner); + } + + let proof_context_state_meta = + ProofContextStateMeta::try_from_bytes(proof_context_account.get_data())?; + + if proof_context_state_meta.proof_type != ProofType::Uninitialized.into() { + return Err(InstructionError::AccountAlreadyInitialized); + } + + let context_state_data = ProofContextState::encode( + &context_state_authority, + T::PROOF_TYPE, + proof_data.context_data(), + ); + + if proof_context_account.get_data().len() != context_state_data.len() { + return Err(InstructionError::InvalidAccountData); + } + + proof_context_account.set_data(context_state_data)?; + } + + Ok(()) +} + +fn process_close_proof_context(invoke_context: &mut InvokeContext) -> Result<(), InstructionError> { + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + + let owner_pubkey = { + let owner_account = + instruction_context.try_borrow_instruction_account(transaction_context, 2)?; + + if !owner_account.is_signer() { + return Err(InstructionError::MissingRequiredSignature); + } + *owner_account.get_key() + }; // done with `owner_account`, so drop it to prevent a potential double borrow + + let proof_context_account_pubkey = *instruction_context + .try_borrow_instruction_account(transaction_context, 0)? + .get_key(); + let destination_account_pubkey = *instruction_context + .try_borrow_instruction_account(transaction_context, 1)? + .get_key(); + if proof_context_account_pubkey == destination_account_pubkey { + return Err(InstructionError::InvalidInstructionData); + } + + let mut proof_context_account = + instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + let proof_context_state_meta = + ProofContextStateMeta::try_from_bytes(proof_context_account.get_data())?; + let expected_owner_pubkey = proof_context_state_meta.context_state_authority; + + if owner_pubkey != expected_owner_pubkey { + return Err(InstructionError::InvalidAccountOwner); + } + + let mut destination_account = + instruction_context.try_borrow_instruction_account(transaction_context, 1)?; + destination_account.checked_add_lamports(proof_context_account.get_lamports())?; + proof_context_account.set_lamports(0)?; + proof_context_account.set_data_length(0)?; + proof_context_account.set_owner(system_program::id().as_ref())?; + + Ok(()) } pub fn process_instruction( @@ -44,32 +132,39 @@ pub fn process_instruction( let transaction_context = &invoke_context.transaction_context; let instruction_context = transaction_context.get_current_instruction_context()?; let instruction_data = instruction_context.get_instruction_data(); - let instruction = ProofInstruction::decode_type(instruction_data); + let instruction = ProofInstruction::instruction_type(instruction_data) + .ok_or(InstructionError::InvalidInstructionData)?; - match instruction.ok_or(InstructionError::InvalidInstructionData)? { + match instruction { + ProofInstruction::CloseContextState => { + ic_msg!(invoke_context, "CloseContextState"); + process_close_proof_context(invoke_context) + } ProofInstruction::VerifyCloseAccount => { ic_msg!(invoke_context, "VerifyCloseAccount"); - verify::(invoke_context) + process_verify_proof::(invoke_context) } ProofInstruction::VerifyWithdraw => { ic_msg!(invoke_context, "VerifyWithdraw"); - verify::(invoke_context) + process_verify_proof::(invoke_context) } ProofInstruction::VerifyWithdrawWithheldTokens => { ic_msg!(invoke_context, "VerifyWithdrawWithheldTokens"); - verify::(invoke_context) + process_verify_proof::( + invoke_context, + ) } ProofInstruction::VerifyTransfer => { ic_msg!(invoke_context, "VerifyTransfer"); - verify::(invoke_context) + process_verify_proof::(invoke_context) } ProofInstruction::VerifyTransferWithFee => { ic_msg!(invoke_context, "VerifyTransferWithFee"); - verify::(invoke_context) + process_verify_proof::(invoke_context) } ProofInstruction::VerifyPubkeyValidity => { ic_msg!(invoke_context, "VerifyPubkeyValidity"); - verify::(invoke_context) + process_verify_proof::(invoke_context) } } } diff --git a/zk-token-sdk/src/instruction/close_account.rs b/zk-token-sdk/src/instruction/close_account.rs index 5874266f423257..c745e400e3d9c8 100644 --- a/zk-token-sdk/src/instruction/close_account.rs +++ b/zk-token-sdk/src/instruction/close_account.rs @@ -1,19 +1,21 @@ -use { - crate::zk_token_elgamal::pod, - bytemuck::{Pod, Zeroable}, -}; #[cfg(not(target_os = "solana"))] use { crate::{ encryption::elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, errors::ProofError, - instruction::Verifiable, sigma_proofs::zero_balance_proof::ZeroBalanceProof, transcript::TranscriptProtocol, }, merlin::Transcript, std::convert::TryInto, }; +use { + crate::{ + instruction::{ProofType, ZkProofData}, + zk_token_elgamal::pod, + }, + bytemuck::{Pod, Zeroable}, +}; /// This struct includes the cryptographic proof *and* the account data information needed to verify /// the proof @@ -25,14 +27,21 @@ use { #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct CloseAccountData { + /// The context data for the close account proof + pub context: CloseAccountProofContext, + + /// Proof that the source account available balance is zero + pub proof: CloseAccountProof, // 96 bytes +} + +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct CloseAccountProofContext { /// The source account ElGamal pubkey pub pubkey: pod::ElGamalPubkey, // 32 bytes /// The source account available balance in encrypted form pub ciphertext: pod::ElGamalCiphertext, // 64 bytes - - /// Proof that the source account available balance is zero - pub proof: CloseAccountProof, // 96 bytes } #[cfg(not(target_os = "solana"))] @@ -44,25 +53,32 @@ impl CloseAccountData { let pod_pubkey = pod::ElGamalPubkey((&keypair.public).to_bytes()); let pod_ciphertext = pod::ElGamalCiphertext(ciphertext.to_bytes()); - let mut transcript = CloseAccountProof::transcript_new(&pod_pubkey, &pod_ciphertext); + let context = CloseAccountProofContext { + pubkey: pod_pubkey, + ciphertext: pod_ciphertext, + }; + let mut transcript = CloseAccountProof::transcript_new(&pod_pubkey, &pod_ciphertext); let proof = CloseAccountProof::new(keypair, ciphertext, &mut transcript); - Ok(CloseAccountData { - pubkey: pod_pubkey, - ciphertext: pod_ciphertext, - proof, - }) + Ok(CloseAccountData { context, proof }) } } -#[cfg(not(target_os = "solana"))] -impl Verifiable for CloseAccountData { - fn verify(&self) -> Result<(), ProofError> { - let mut transcript = CloseAccountProof::transcript_new(&self.pubkey, &self.ciphertext); +impl ZkProofData for CloseAccountData { + const PROOF_TYPE: ProofType = ProofType::CloseAccount; + + fn context_data(&self) -> &CloseAccountProofContext { + &self.context + } + + #[cfg(not(target_os = "solana"))] + fn verify_proof(&self) -> Result<(), ProofError> { + let mut transcript = + CloseAccountProof::transcript_new(&self.context.pubkey, &self.context.ciphertext); - let pubkey = self.pubkey.try_into()?; - let ciphertext = self.ciphertext.try_into()?; + let pubkey = self.context.pubkey.try_into()?; + let ciphertext = self.context.ciphertext.try_into()?; self.proof.verify(&pubkey, &ciphertext, &mut transcript) } } @@ -127,11 +143,11 @@ mod test { // general case: encryption of 0 let ciphertext = keypair.public.encrypt(0_u64); let close_account_data = CloseAccountData::new(&keypair, &ciphertext).unwrap(); - assert!(close_account_data.verify().is_ok()); + assert!(close_account_data.verify_proof().is_ok()); // general case: encryption of > 0 let ciphertext = keypair.public.encrypt(1_u64); let close_account_data = CloseAccountData::new(&keypair, &ciphertext).unwrap(); - assert!(close_account_data.verify().is_err()); + assert!(close_account_data.verify_proof().is_err()); } } diff --git a/zk-token-sdk/src/instruction/mod.rs b/zk-token-sdk/src/instruction/mod.rs index 3d5e44a5e01ee9..f257ee961a1a4e 100644 --- a/zk-token-sdk/src/instruction/mod.rs +++ b/zk-token-sdk/src/instruction/mod.rs @@ -5,6 +5,7 @@ pub mod transfer_with_fee; pub mod withdraw; pub mod withdraw_withheld; +use num_derive::{FromPrimitive, ToPrimitive}; #[cfg(not(target_os = "solana"))] use { crate::{ @@ -17,14 +18,35 @@ use { curve25519_dalek::scalar::Scalar, }; pub use { - close_account::CloseAccountData, pubkey_validity::PubkeyValidityData, transfer::TransferData, - transfer_with_fee::TransferWithFeeData, withdraw::WithdrawData, - withdraw_withheld::WithdrawWithheldTokensData, + bytemuck::Pod, + close_account::{CloseAccountData, CloseAccountProofContext}, + pubkey_validity::{PubkeyValidityData, PubkeyValidityProofContext}, + transfer::{TransferData, TransferProofContext}, + transfer_with_fee::{FeeParameters, TransferWithFeeData, TransferWithFeeProofContext}, + withdraw::{WithdrawData, WithdrawProofContext}, + withdraw_withheld::{WithdrawWithheldTokensData, WithdrawWithheldTokensProofContext}, }; -#[cfg(not(target_os = "solana"))] -pub trait Verifiable { - fn verify(&self) -> Result<(), ProofError>; +#[derive(Clone, Copy, Debug, FromPrimitive, ToPrimitive, PartialEq, Eq)] +#[repr(u8)] +pub enum ProofType { + /// Empty proof type used to distinguish if a proof context account is initialized + Uninitialized, + CloseAccount, + Withdraw, + WithdrawWithheldTokens, + Transfer, + TransferWithFee, + PubkeyValidity, +} + +pub trait ZkProofData { + const PROOF_TYPE: ProofType; + + fn context_data(&self) -> &T; + + #[cfg(not(target_os = "solana"))] + fn verify_proof(&self) -> Result<(), ProofError>; } #[cfg(not(target_os = "solana"))] diff --git a/zk-token-sdk/src/instruction/pubkey_validity.rs b/zk-token-sdk/src/instruction/pubkey_validity.rs index 3fc5a6288fb446..83f0461a1733f3 100644 --- a/zk-token-sdk/src/instruction/pubkey_validity.rs +++ b/zk-token-sdk/src/instruction/pubkey_validity.rs @@ -1,19 +1,21 @@ -use { - crate::zk_token_elgamal::pod, - bytemuck::{Pod, Zeroable}, -}; #[cfg(not(target_os = "solana"))] use { crate::{ encryption::elgamal::{ElGamalKeypair, ElGamalPubkey}, errors::ProofError, - instruction::Verifiable, sigma_proofs::pubkey_proof::PubkeySigmaProof, transcript::TranscriptProtocol, }, merlin::Transcript, std::convert::TryInto, }; +use { + crate::{ + instruction::{ProofType, ZkProofData}, + zk_token_elgamal::pod, + }, + bytemuck::{Pod, Zeroable}, +}; /// This struct includes the cryptographic proof *and* the account data information needed to /// verify the proof @@ -24,34 +26,45 @@ use { #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct PubkeyValidityData { - /// The public key to be proved - pub pubkey: pod::ElGamalPubkey, + /// The context data for the public key validity proof + pub context: PubkeyValidityProofContext, /// Proof that the public key is well-formed pub proof: PubkeyValidityProof, // 64 bytes } +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct PubkeyValidityProofContext { + /// The public key to be proved + pub pubkey: pod::ElGamalPubkey, // 32 bytes +} + #[cfg(not(target_os = "solana"))] impl PubkeyValidityData { pub fn new(keypair: &ElGamalKeypair) -> Result { let pod_pubkey = pod::ElGamalPubkey(keypair.public.to_bytes()); - let mut transcript = PubkeyValidityProof::transcript_new(&pod_pubkey); + let context = PubkeyValidityProofContext { pubkey: pod_pubkey }; + let mut transcript = PubkeyValidityProof::transcript_new(&pod_pubkey); let proof = PubkeyValidityProof::new(keypair, &mut transcript); - Ok(PubkeyValidityData { - pubkey: pod_pubkey, - proof, - }) + Ok(PubkeyValidityData { context, proof }) } } -#[cfg(not(target_os = "solana"))] -impl Verifiable for PubkeyValidityData { - fn verify(&self) -> Result<(), ProofError> { - let mut transcript = PubkeyValidityProof::transcript_new(&self.pubkey); - let pubkey = self.pubkey.try_into()?; +impl ZkProofData for PubkeyValidityData { + const PROOF_TYPE: ProofType = ProofType::PubkeyValidity; + + fn context_data(&self) -> &PubkeyValidityProofContext { + &self.context + } + + #[cfg(not(target_os = "solana"))] + fn verify_proof(&self) -> Result<(), ProofError> { + let mut transcript = PubkeyValidityProof::transcript_new(&self.context.pubkey); + let pubkey = self.context.pubkey.try_into()?; self.proof.verify(&pubkey, &mut transcript) } } @@ -100,6 +113,6 @@ mod test { let keypair = ElGamalKeypair::new_rand(); let pubkey_validity_data = PubkeyValidityData::new(&keypair).unwrap(); - assert!(pubkey_validity_data.verify().is_ok()); + assert!(pubkey_validity_data.verify_proof().is_ok()); } } diff --git a/zk-token-sdk/src/instruction/transfer.rs b/zk-token-sdk/src/instruction/transfer.rs index bb55ed9d37d592..67e4d6926fb3b4 100644 --- a/zk-token-sdk/src/instruction/transfer.rs +++ b/zk-token-sdk/src/instruction/transfer.rs @@ -1,7 +1,3 @@ -use { - crate::zk_token_elgamal::pod, - bytemuck::{Pod, Zeroable}, -}; #[cfg(not(target_os = "solana"))] use { crate::{ @@ -12,7 +8,7 @@ use { pedersen::{Pedersen, PedersenCommitment, PedersenOpening}, }, errors::ProofError, - instruction::{combine_lo_hi_ciphertexts, split_u64, Role, Verifiable}, + instruction::{combine_lo_hi_ciphertexts, split_u64, Role}, range_proof::RangeProof, sigma_proofs::{ equality_proof::CtxtCommEqualityProof, validity_proof::AggregatedValidityProof, @@ -23,6 +19,13 @@ use { merlin::Transcript, std::convert::TryInto, }; +use { + crate::{ + instruction::{ProofType, ZkProofData}, + zk_token_elgamal::pod, + }, + bytemuck::{Pod, Zeroable}, +}; #[cfg(not(target_os = "solana"))] const TRANSFER_SOURCE_AMOUNT_BITS: usize = 64; @@ -42,6 +45,7 @@ lazy_static::lazy_static! { #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct TransferData { +<<<<<<< HEAD /// Group encryption of the low 32 bits of the transfer amount pub ciphertext_lo: pod::TransferAmountEncryption, @@ -53,11 +57,31 @@ pub struct TransferData { /// The final spendable ciphertext after the transfer pub new_source_ciphertext: pod::ElGamalCiphertext, +======= + /// The context data for the transfer proof + pub context: TransferProofContext, +>>>>>>> 2d58bb287 ([zk-token-sdk] Add option to create proof context state in the proof verification program (#29996)) /// Zero-knowledge proofs for Transfer pub proof: TransferProof, } +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct TransferProofContext { + /// Group encryption of the low 16 bits of the transfer amount + pub ciphertext_lo: pod::TransferAmountEncryption, // 128 bytes + + /// Group encryption of the high 48 bits of the transfer amount + pub ciphertext_hi: pod::TransferAmountEncryption, // 128 bytes + + /// The public encryption keys associated with the transfer: source, dest, and auditor + pub transfer_pubkeys: pod::TransferPubkeys, // 96 bytes + + /// The final spendable ciphertext after the transfer + pub new_source_ciphertext: pod::ElGamalCiphertext, // 64 bytes +} + #[cfg(not(target_os = "solana"))] impl TransferData { #[allow(clippy::too_many_arguments)] @@ -116,6 +140,13 @@ impl TransferData { let pod_ciphertext_hi: pod::TransferAmountEncryption = ciphertext_hi.into(); let pod_new_source_ciphertext: pod::ElGamalCiphertext = new_source_ciphertext.into(); + let context = TransferProofContext { + ciphertext_lo: pod_ciphertext_lo, + ciphertext_hi: pod_ciphertext_hi, + transfer_pubkeys: pod_transfer_pubkeys, + new_source_ciphertext: pod_new_source_ciphertext, + }; + let mut transcript = TransferProof::transcript_new( &pod_transfer_pubkeys, &pod_ciphertext_lo, @@ -133,18 +164,12 @@ impl TransferData { &mut transcript, ); - Ok(Self { - ciphertext_lo: pod_ciphertext_lo, - ciphertext_hi: pod_ciphertext_hi, - transfer_pubkeys: pod_transfer_pubkeys, - new_source_ciphertext: pod_new_source_ciphertext, - proof, - }) + Ok(Self { context, proof }) } /// Extracts the lo ciphertexts associated with a transfer data fn ciphertext_lo(&self, role: Role) -> Result { - let ciphertext_lo: TransferAmountEncryption = self.ciphertext_lo.try_into()?; + let ciphertext_lo: TransferAmountEncryption = self.context.ciphertext_lo.try_into()?; let handle_lo = match role { Role::Source => Some(ciphertext_lo.source_handle), @@ -165,7 +190,7 @@ impl TransferData { /// Extracts the lo ciphertexts associated with a transfer data fn ciphertext_hi(&self, role: Role) -> Result { - let ciphertext_hi: TransferAmountEncryption = self.ciphertext_hi.try_into()?; + let ciphertext_hi: TransferAmountEncryption = self.context.ciphertext_hi.try_into()?; let handle_hi = match role { Role::Source => Some(ciphertext_hi.source_handle), @@ -201,21 +226,27 @@ impl TransferData { } } -#[cfg(not(target_os = "solana"))] -impl Verifiable for TransferData { - fn verify(&self) -> Result<(), ProofError> { +impl ZkProofData for TransferData { + const PROOF_TYPE: ProofType = ProofType::Transfer; + + fn context_data(&self) -> &TransferProofContext { + &self.context + } + + #[cfg(not(target_os = "solana"))] + fn verify_proof(&self) -> Result<(), ProofError> { // generate transcript and append all public inputs let mut transcript = TransferProof::transcript_new( - &self.transfer_pubkeys, - &self.ciphertext_lo, - &self.ciphertext_hi, - &self.new_source_ciphertext, + &self.context.transfer_pubkeys, + &self.context.ciphertext_lo, + &self.context.ciphertext_hi, + &self.context.new_source_ciphertext, ); - let ciphertext_lo = self.ciphertext_lo.try_into()?; - let ciphertext_hi = self.ciphertext_hi.try_into()?; - let transfer_pubkeys = self.transfer_pubkeys.try_into()?; - let new_spendable_ciphertext = self.new_source_ciphertext.try_into()?; + let ciphertext_lo = self.context.ciphertext_lo.try_into()?; + let ciphertext_hi = self.context.ciphertext_hi.try_into()?; + let transfer_pubkeys = self.context.transfer_pubkeys.try_into()?; + let new_spendable_ciphertext = self.context.new_source_ciphertext.try_into()?; self.proof.verify( &ciphertext_lo, @@ -541,7 +572,7 @@ mod test { ) .unwrap(); - assert!(transfer_data.verify().is_ok()); + assert!(transfer_data.verify_proof().is_ok()); // Case 2: transfer max amount @@ -562,7 +593,7 @@ mod test { ) .unwrap(); - assert!(transfer_data.verify().is_ok()); + assert!(transfer_data.verify_proof().is_ok()); // Case 3: general success case @@ -582,7 +613,7 @@ mod test { ) .unwrap(); - assert!(transfer_data.verify().is_ok()); + assert!(transfer_data.verify_proof().is_ok()); // Case 4: invalid destination or auditor pubkey let spendable_balance: u64 = 0; @@ -602,7 +633,7 @@ mod test { ) .unwrap(); - assert!(transfer_data.verify().is_err()); + assert!(transfer_data.verify_proof().is_err()); // auditor pubkey invalid let dest_pk = ElGamalKeypair::new_rand().public; @@ -616,7 +647,7 @@ mod test { ) .unwrap(); - assert!(transfer_data.verify().is_err()); + assert!(transfer_data.verify_proof().is_err()); } #[test] diff --git a/zk-token-sdk/src/instruction/transfer_with_fee.rs b/zk-token-sdk/src/instruction/transfer_with_fee.rs index 7fbb603e2ec333..a6bdee776b54aa 100644 --- a/zk-token-sdk/src/instruction/transfer_with_fee.rs +++ b/zk-token-sdk/src/instruction/transfer_with_fee.rs @@ -1,7 +1,3 @@ -use { - crate::zk_token_elgamal::pod, - bytemuck::{Pod, Zeroable}, -}; #[cfg(not(target_os = "solana"))] use { crate::{ @@ -14,7 +10,7 @@ use { errors::ProofError, instruction::{ combine_lo_hi_ciphertexts, combine_lo_hi_commitments, combine_lo_hi_openings, - combine_lo_hi_u64, split_u64, transfer::TransferAmountEncryption, Role, Verifiable, + combine_lo_hi_u64, split_u64, transfer::TransferAmountEncryption, Role, }, range_proof::RangeProof, sigma_proofs::{ @@ -29,6 +25,13 @@ use { std::convert::TryInto, subtle::{ConditionallySelectable, ConstantTimeGreater}, }; +use { + crate::{ + instruction::{ProofType, ZkProofData}, + zk_token_elgamal::pod, + }, + bytemuck::{Pod, Zeroable}, +}; #[cfg(not(target_os = "solana"))] const MAX_FEE_BASIS_POINTS: u64 = 10_000; @@ -61,29 +64,36 @@ lazy_static::lazy_static! { #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct TransferWithFeeData { + /// The context data for the transfer with fee proof + pub context: TransferWithFeeProofContext, + + // transfer fee proof + pub proof: TransferWithFeeProof, +} + +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct TransferWithFeeProofContext { /// Group encryption of the low 16 bites of the transfer amount - pub ciphertext_lo: pod::TransferAmountEncryption, + pub ciphertext_lo: pod::TransferAmountEncryption, // 128 bytes /// Group encryption of the high 48 bits of the transfer amount - pub ciphertext_hi: pod::TransferAmountEncryption, + pub ciphertext_hi: pod::TransferAmountEncryption, // 128 bytes /// The public encryption keys associated with the transfer: source, dest, and auditor - pub transfer_with_fee_pubkeys: pod::TransferWithFeePubkeys, + pub transfer_with_fee_pubkeys: pod::TransferWithFeePubkeys, // 128 bytes /// The final spendable ciphertext after the transfer, - pub new_source_ciphertext: pod::ElGamalCiphertext, + pub new_source_ciphertext: pod::ElGamalCiphertext, // 64 bytes // transfer fee encryption of the low 16 bits of the transfer fee amount - pub fee_ciphertext_lo: pod::FeeEncryption, + pub fee_ciphertext_lo: pod::FeeEncryption, // 96 bytes // transfer fee encryption of the hi 32 bits of the transfer fee amount - pub fee_ciphertext_hi: pod::FeeEncryption, + pub fee_ciphertext_hi: pod::FeeEncryption, // 96 bytes // fee parameters - pub fee_parameters: pod::FeeParameters, - - // transfer fee proof - pub proof: TransferWithFeeProof, + pub fee_parameters: pod::FeeParameters, // 10 bytes } #[cfg(not(target_os = "solana"))] @@ -173,6 +183,16 @@ impl TransferWithFeeData { let pod_fee_ciphertext_lo: pod::FeeEncryption = fee_ciphertext_lo.to_pod(); let pod_fee_ciphertext_hi: pod::FeeEncryption = fee_ciphertext_hi.to_pod(); + let context = TransferWithFeeProofContext { + ciphertext_lo: pod_ciphertext_lo, + ciphertext_hi: pod_ciphertext_hi, + transfer_with_fee_pubkeys: pod_transfer_with_fee_pubkeys, + new_source_ciphertext: pod_new_source_ciphertext, + fee_ciphertext_lo: pod_fee_ciphertext_lo, + fee_ciphertext_hi: pod_fee_ciphertext_hi, + fee_parameters: fee_parameters.into(), + }; + let mut transcript = TransferWithFeeProof::transcript_new( &pod_transfer_with_fee_pubkeys, &pod_ciphertext_lo, @@ -196,21 +216,12 @@ impl TransferWithFeeData { &mut transcript, ); - Ok(Self { - ciphertext_lo: pod_ciphertext_lo, - ciphertext_hi: pod_ciphertext_hi, - transfer_with_fee_pubkeys: pod_transfer_with_fee_pubkeys, - new_source_ciphertext: pod_new_source_ciphertext, - fee_ciphertext_lo: pod_fee_ciphertext_lo, - fee_ciphertext_hi: pod_fee_ciphertext_hi, - fee_parameters: fee_parameters.into(), - proof, - }) + Ok(Self { context, proof }) } /// Extracts the lo ciphertexts associated with a transfer-with-fee data fn ciphertext_lo(&self, role: Role) -> Result { - let ciphertext_lo: TransferAmountEncryption = self.ciphertext_lo.try_into()?; + let ciphertext_lo: TransferAmountEncryption = self.context.ciphertext_lo.try_into()?; let handle_lo = match role { Role::Source => Some(ciphertext_lo.source_handle), @@ -231,7 +242,7 @@ impl TransferWithFeeData { /// Extracts the lo ciphertexts associated with a transfer-with-fee data fn ciphertext_hi(&self, role: Role) -> Result { - let ciphertext_hi: TransferAmountEncryption = self.ciphertext_hi.try_into()?; + let ciphertext_hi: TransferAmountEncryption = self.context.ciphertext_hi.try_into()?; let handle_hi = match role { Role::Source => Some(ciphertext_hi.source_handle), @@ -252,7 +263,7 @@ impl TransferWithFeeData { /// Extracts the lo fee ciphertexts associated with a transfer_with_fee data fn fee_ciphertext_lo(&self, role: Role) -> Result { - let fee_ciphertext_lo: FeeEncryption = self.fee_ciphertext_lo.try_into()?; + let fee_ciphertext_lo: FeeEncryption = self.context.fee_ciphertext_lo.try_into()?; let fee_handle_lo = match role { Role::Source => None, @@ -275,7 +286,7 @@ impl TransferWithFeeData { /// Extracts the hi fee ciphertexts associated with a transfer_with_fee data fn fee_ciphertext_hi(&self, role: Role) -> Result { - let fee_ciphertext_hi: FeeEncryption = self.fee_ciphertext_hi.try_into()?; + let fee_ciphertext_hi: FeeEncryption = self.context.fee_ciphertext_hi.try_into()?; let fee_handle_hi = match role { Role::Source => None, @@ -329,26 +340,32 @@ impl TransferWithFeeData { } } -#[cfg(not(target_os = "solana"))] -impl Verifiable for TransferWithFeeData { - fn verify(&self) -> Result<(), ProofError> { +impl ZkProofData for TransferWithFeeData { + const PROOF_TYPE: ProofType = ProofType::TransferWithFee; + + fn context_data(&self) -> &TransferWithFeeProofContext { + &self.context + } + + #[cfg(not(target_os = "solana"))] + fn verify_proof(&self) -> Result<(), ProofError> { let mut transcript = TransferWithFeeProof::transcript_new( - &self.transfer_with_fee_pubkeys, - &self.ciphertext_lo, - &self.ciphertext_hi, - &self.new_source_ciphertext, - &self.fee_ciphertext_lo, - &self.fee_ciphertext_hi, + &self.context.transfer_with_fee_pubkeys, + &self.context.ciphertext_lo, + &self.context.ciphertext_hi, + &self.context.new_source_ciphertext, + &self.context.fee_ciphertext_lo, + &self.context.fee_ciphertext_hi, ); - let ciphertext_lo = self.ciphertext_lo.try_into()?; - let ciphertext_hi = self.ciphertext_hi.try_into()?; - let pubkeys_transfer_with_fee = self.transfer_with_fee_pubkeys.try_into()?; - let new_source_ciphertext = self.new_source_ciphertext.try_into()?; + let ciphertext_lo = self.context.ciphertext_lo.try_into()?; + let ciphertext_hi = self.context.ciphertext_hi.try_into()?; + let pubkeys_transfer_with_fee = self.context.transfer_with_fee_pubkeys.try_into()?; + let new_source_ciphertext = self.context.new_source_ciphertext.try_into()?; - let fee_ciphertext_lo = self.fee_ciphertext_lo.try_into()?; - let fee_ciphertext_hi = self.fee_ciphertext_hi.try_into()?; - let fee_parameters = self.fee_parameters.into(); + let fee_ciphertext_lo = self.context.fee_ciphertext_lo.try_into()?; + let fee_ciphertext_hi = self.context.fee_ciphertext_hi.try_into()?; + let fee_parameters = self.context.fee_parameters.into(); self.proof.verify( &ciphertext_lo, @@ -886,7 +903,7 @@ mod test { ) .unwrap(); - assert!(fee_data.verify().is_ok()); + assert!(fee_data.verify_proof().is_ok()); // Case 2: transfer max amount let spendable_balance: u64 = u64::max_value(); @@ -910,7 +927,7 @@ mod test { ) .unwrap(); - assert!(fee_data.verify().is_ok()); + assert!(fee_data.verify_proof().is_ok()); // Case 3: general success case let spendable_balance: u64 = 120; @@ -933,7 +950,7 @@ mod test { ) .unwrap(); - assert!(fee_data.verify().is_ok()); + assert!(fee_data.verify_proof().is_ok()); // Case 4: invalid destination, auditor, or withdraw authority pubkeys let spendable_balance: u64 = 120; @@ -961,7 +978,7 @@ mod test { ) .unwrap(); - assert!(fee_data.verify().is_err()); + assert!(fee_data.verify_proof().is_err()); // auditor pubkey invalid let destination_pubkey: ElGamalPubkey = ElGamalKeypair::new_rand().public; @@ -978,7 +995,7 @@ mod test { ) .unwrap(); - assert!(fee_data.verify().is_err()); + assert!(fee_data.verify_proof().is_err()); // withdraw authority invalid let destination_pubkey: ElGamalPubkey = ElGamalKeypair::new_rand().public; @@ -995,6 +1012,6 @@ mod test { ) .unwrap(); - assert!(fee_data.verify().is_err()); + assert!(fee_data.verify_proof().is_err()); } } diff --git a/zk-token-sdk/src/instruction/withdraw.rs b/zk-token-sdk/src/instruction/withdraw.rs index 5b84fe840bc616..340ea94c96d619 100644 --- a/zk-token-sdk/src/instruction/withdraw.rs +++ b/zk-token-sdk/src/instruction/withdraw.rs @@ -1,7 +1,3 @@ -use { - crate::zk_token_elgamal::pod, - bytemuck::{Pod, Zeroable}, -}; #[cfg(not(target_os = "solana"))] use { crate::{ @@ -10,7 +6,6 @@ use { pedersen::{Pedersen, PedersenCommitment}, }, errors::ProofError, - instruction::Verifiable, range_proof::RangeProof, sigma_proofs::equality_proof::CtxtCommEqualityProof, transcript::TranscriptProtocol, @@ -18,6 +13,13 @@ use { merlin::Transcript, std::convert::TryInto, }; +use { + crate::{ + instruction::{ProofType, ZkProofData}, + zk_token_elgamal::pod, + }, + bytemuck::{Pod, Zeroable}, +}; #[cfg(not(target_os = "solana"))] const WITHDRAW_AMOUNT_BIT_LENGTH: usize = 64; @@ -32,15 +34,22 @@ const WITHDRAW_AMOUNT_BIT_LENGTH: usize = 64; #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct WithdrawData { + /// The context data for the withdraw proof + pub context: WithdrawProofContext, // 128 bytes + + /// Range proof + pub proof: WithdrawProof, // 736 bytes +} + +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct WithdrawProofContext { /// The source account ElGamal pubkey pub pubkey: pod::ElGamalPubkey, // 32 bytes /// The source account available balance *after* the withdraw (encrypted by /// `source_pk` pub final_ciphertext: pod::ElGamalCiphertext, // 64 bytes - - /// Range proof - pub proof: WithdrawProof, // 736 bytes } #[cfg(not(target_os = "solana"))] @@ -64,24 +73,33 @@ impl WithdrawData { let pod_pubkey = pod::ElGamalPubkey((&keypair.public).to_bytes()); let pod_final_ciphertext: pod::ElGamalCiphertext = final_ciphertext.into(); - let mut transcript = WithdrawProof::transcript_new(&pod_pubkey, &pod_final_ciphertext); - let proof = WithdrawProof::new(keypair, final_balance, &final_ciphertext, &mut transcript); - Ok(Self { + let context = WithdrawProofContext { pubkey: pod_pubkey, final_ciphertext: pod_final_ciphertext, - proof, - }) + }; + + let mut transcript = WithdrawProof::transcript_new(&pod_pubkey, &pod_final_ciphertext); + let proof = WithdrawProof::new(keypair, final_balance, &final_ciphertext, &mut transcript); + + Ok(Self { context, proof }) } } -#[cfg(not(target_os = "solana"))] -impl Verifiable for WithdrawData { - fn verify(&self) -> Result<(), ProofError> { - let mut transcript = WithdrawProof::transcript_new(&self.pubkey, &self.final_ciphertext); +impl ZkProofData for WithdrawData { + const PROOF_TYPE: ProofType = ProofType::Withdraw; + + fn context_data(&self) -> &WithdrawProofContext { + &self.context + } + + #[cfg(not(target_os = "solana"))] + fn verify_proof(&self) -> Result<(), ProofError> { + let mut transcript = + WithdrawProof::transcript_new(&self.context.pubkey, &self.context.final_ciphertext); - let elgamal_pubkey = self.pubkey.try_into()?; - let final_balance_ciphertext = self.final_ciphertext.try_into()?; + let elgamal_pubkey = self.context.pubkey.try_into()?; + let final_balance_ciphertext = self.context.final_ciphertext.try_into()?; self.proof .verify(&elgamal_pubkey, &final_balance_ciphertext, &mut transcript) } @@ -200,7 +218,7 @@ mod test { ¤t_ciphertext, ) .unwrap(); - assert!(data.verify().is_ok()); + assert!(data.verify_proof().is_ok()); // generate and verify proof with wrong balance let wrong_balance: u64 = 99; @@ -211,6 +229,6 @@ mod test { ¤t_ciphertext, ) .unwrap(); - assert!(data.verify().is_err()); + assert!(data.verify_proof().is_err()); } } diff --git a/zk-token-sdk/src/instruction/withdraw_withheld.rs b/zk-token-sdk/src/instruction/withdraw_withheld.rs index e0e5363b4a0792..58049031e1fd3e 100644 --- a/zk-token-sdk/src/instruction/withdraw_withheld.rs +++ b/zk-token-sdk/src/instruction/withdraw_withheld.rs @@ -1,7 +1,3 @@ -use { - crate::zk_token_elgamal::pod, - bytemuck::{Pod, Zeroable}, -}; #[cfg(not(target_os = "solana"))] use { crate::{ @@ -10,13 +6,19 @@ use { pedersen::PedersenOpening, }, errors::ProofError, - instruction::Verifiable, sigma_proofs::equality_proof::CtxtCtxtEqualityProof, transcript::TranscriptProtocol, }, merlin::Transcript, std::convert::TryInto, }; +use { + crate::{ + instruction::{ProofType, ZkProofData}, + zk_token_elgamal::pod, + }, + bytemuck::{Pod, Zeroable}, +}; /// This struct includes the cryptographic proof *and* the account data information needed to verify /// the proof @@ -28,15 +30,21 @@ use { #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct WithdrawWithheldTokensData { - pub withdraw_withheld_authority_pubkey: pod::ElGamalPubkey, + pub context: WithdrawWithheldTokensProofContext, - pub destination_pubkey: pod::ElGamalPubkey, + pub proof: WithdrawWithheldTokensProof, +} - pub withdraw_withheld_authority_ciphertext: pod::ElGamalCiphertext, +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct WithdrawWithheldTokensProofContext { + pub withdraw_withheld_authority_pubkey: pod::ElGamalPubkey, // 32 bytes - pub destination_ciphertext: pod::ElGamalCiphertext, + pub destination_pubkey: pod::ElGamalPubkey, // 32 bytes - pub proof: WithdrawWithheldTokensProof, + pub withdraw_withheld_authority_ciphertext: pod::ElGamalCiphertext, // 64 bytes + + pub destination_ciphertext: pod::ElGamalCiphertext, // 64 bytes } #[cfg(not(target_os = "solana"))] @@ -58,6 +66,13 @@ impl WithdrawWithheldTokensData { pod::ElGamalCiphertext(withdraw_withheld_authority_ciphertext.to_bytes()); let pod_destination_ciphertext = pod::ElGamalCiphertext(destination_ciphertext.to_bytes()); + let context = WithdrawWithheldTokensProofContext { + withdraw_withheld_authority_pubkey: pod_withdraw_withheld_authority_pubkey, + destination_pubkey: pod_destination_pubkey, + withdraw_withheld_authority_ciphertext: pod_withdraw_withheld_authority_ciphertext, + destination_ciphertext: pod_destination_ciphertext, + }; + let mut transcript = WithdrawWithheldTokensProof::transcript_new( &pod_withdraw_withheld_authority_pubkey, &pod_destination_pubkey, @@ -74,32 +89,34 @@ impl WithdrawWithheldTokensData { &mut transcript, ); - Ok(Self { - withdraw_withheld_authority_pubkey: pod_withdraw_withheld_authority_pubkey, - destination_pubkey: pod_destination_pubkey, - withdraw_withheld_authority_ciphertext: pod_withdraw_withheld_authority_ciphertext, - destination_ciphertext: pod_destination_ciphertext, - proof, - }) + Ok(Self { context, proof }) } } -#[cfg(not(target_os = "solana"))] -impl Verifiable for WithdrawWithheldTokensData { - fn verify(&self) -> Result<(), ProofError> { +impl ZkProofData for WithdrawWithheldTokensData { + const PROOF_TYPE: ProofType = ProofType::WithdrawWithheldTokens; + + fn context_data(&self) -> &WithdrawWithheldTokensProofContext { + &self.context + } + + #[cfg(not(target_os = "solana"))] + fn verify_proof(&self) -> Result<(), ProofError> { let mut transcript = WithdrawWithheldTokensProof::transcript_new( - &self.withdraw_withheld_authority_pubkey, - &self.destination_pubkey, - &self.withdraw_withheld_authority_ciphertext, - &self.destination_ciphertext, + &self.context.withdraw_withheld_authority_pubkey, + &self.context.destination_pubkey, + &self.context.withdraw_withheld_authority_ciphertext, + &self.context.destination_ciphertext, ); let withdraw_withheld_authority_pubkey = - self.withdraw_withheld_authority_pubkey.try_into()?; - let destination_pubkey = self.destination_pubkey.try_into()?; - let withdraw_withheld_authority_ciphertext = - self.withdraw_withheld_authority_ciphertext.try_into()?; - let destination_ciphertext = self.destination_ciphertext.try_into()?; + self.context.withdraw_withheld_authority_pubkey.try_into()?; + let destination_pubkey = self.context.destination_pubkey.try_into()?; + let withdraw_withheld_authority_ciphertext = self + .context + .withdraw_withheld_authority_ciphertext + .try_into()?; + let destination_ciphertext = self.context.destination_ciphertext.try_into()?; self.proof.verify( &withdraw_withheld_authority_pubkey, @@ -210,7 +227,7 @@ mod test { ) .unwrap(); - assert!(withdraw_withheld_tokens_data.verify().is_ok()); + assert!(withdraw_withheld_tokens_data.verify_proof().is_ok()); let amount: u64 = 55; let withdraw_withheld_authority_ciphertext = @@ -224,7 +241,7 @@ mod test { ) .unwrap(); - assert!(withdraw_withheld_tokens_data.verify().is_ok()); + assert!(withdraw_withheld_tokens_data.verify_proof().is_ok()); let amount = u64::max_value(); let withdraw_withheld_authority_ciphertext = @@ -238,6 +255,6 @@ mod test { ) .unwrap(); - assert!(withdraw_withheld_tokens_data.verify().is_ok()); + assert!(withdraw_withheld_tokens_data.verify_proof().is_ok()); } } diff --git a/zk-token-sdk/src/lib.rs b/zk-token-sdk/src/lib.rs index c7a0391eaab8c8..14ce5b4380af47 100644 --- a/zk-token-sdk/src/lib.rs +++ b/zk-token-sdk/src/lib.rs @@ -37,3 +37,4 @@ pub mod instruction; pub mod zk_token_elgamal; pub mod zk_token_proof_instruction; pub mod zk_token_proof_program; +pub mod zk_token_proof_state; diff --git a/zk-token-sdk/src/zk_token_elgamal/pod.rs b/zk-token-sdk/src/zk_token_elgamal/pod.rs index 2658e3a1446357..fd728bc6a7caab 100644 --- a/zk-token-sdk/src/zk_token_elgamal/pod.rs +++ b/zk-token-sdk/src/zk_token_elgamal/pod.rs @@ -1,5 +1,10 @@ pub use bytemuck::{Pod, Zeroable}; -use std::fmt; +use { + crate::zk_token_proof_instruction::ProofType, + num_traits::{FromPrimitive, ToPrimitive}, + solana_program::instruction::InstructionError, + std::fmt, +}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Pod, Zeroable)] #[repr(transparent)] @@ -29,6 +34,22 @@ impl From for u64 { } } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Pod, Zeroable)] +#[repr(transparent)] +pub struct PodProofType(u8); +impl From for PodProofType { + fn from(proof_type: ProofType) -> Self { + Self(ToPrimitive::to_u8(&proof_type).unwrap()) + } +} +impl TryFrom for ProofType { + type Error = InstructionError; + + fn try_from(pod: PodProofType) -> Result { + FromPrimitive::from_u8(pod.0).ok_or(Self::Error::InvalidAccountData) + } +} + #[derive(Clone, Copy, Pod, Zeroable, PartialEq, Eq)] #[repr(transparent)] pub struct CompressedRistretto(pub [u8; 32]); diff --git a/zk-token-sdk/src/zk_token_proof_instruction.rs b/zk-token-sdk/src/zk_token_proof_instruction.rs index 867e0f783c901a..21d1dae975562d 100644 --- a/zk-token-sdk/src/zk_token_proof_instruction.rs +++ b/zk-token-sdk/src/zk_token_proof_instruction.rs @@ -1,18 +1,41 @@ ///! Instructions provided by the ZkToken Proof program pub use crate::instruction::*; use { - bytemuck::{bytes_of, Pod}, + bytemuck::bytes_of, num_derive::{FromPrimitive, ToPrimitive}, num_traits::{FromPrimitive, ToPrimitive}, - solana_program::instruction::Instruction, + solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }, }; #[derive(Clone, Copy, Debug, FromPrimitive, ToPrimitive, PartialEq, Eq)] #[repr(u8)] pub enum ProofInstruction { - /// Verify a `CloseAccountData` struct + /// Close a zero-knowledge proof context state. /// /// Accounts expected by this instruction: + /// 0. `[writable]` The proof context account to close + /// 1. `[writable]` The destination account for lamports + /// 2. `[signer]` The context account's owner + /// + /// Data expected by this instruction: + /// None + /// + CloseContextState, + + /// Verify a close account zero-knowledge proof. + /// + /// This instruction can be configured to optionally create a proof context state account. + /// + /// Accounts expected by this instruction: + /// + /// * Creating a proof context account + /// 0. `[writable]` The proof context account + /// 1. `[]` The proof context account owner + /// + /// * Otherwise /// None /// /// Data expected by this instruction: @@ -20,9 +43,17 @@ pub enum ProofInstruction { /// VerifyCloseAccount, - /// Verify a `WithdrawData` struct + /// Verify a withdraw zero-knowledge proof. + /// + /// This instruction can be configured to optionally create a proof context state account. /// /// Accounts expected by this instruction: + /// + /// * Creating a proof context account + /// 0. `[writable]` The proof context account + /// 1. `[]` The proof context account owner + /// + /// * Otherwise /// None /// /// Data expected by this instruction: @@ -30,9 +61,17 @@ pub enum ProofInstruction { /// VerifyWithdraw, - /// Verify a `WithdrawWithheldTokensData` struct + /// Verify a withdraw withheld tokens zero-knowledge proof. + /// + /// This instruction can be configured to optionally create a proof context state account. /// /// Accounts expected by this instruction: + /// + /// * Creating a proof context account + /// 0. `[writable]` The proof context account + /// 1. `[]` The proof context account owner + /// + /// * Otherwise /// None /// /// Data expected by this instruction: @@ -40,9 +79,17 @@ pub enum ProofInstruction { /// VerifyWithdrawWithheldTokens, - /// Verify a `TransferData` struct + /// Verify a transfer zero-knowledge proof. + /// + /// This instruction can be configured to optionally create a proof context state account. /// /// Accounts expected by this instruction: + /// + /// * Creating a proof context account + /// 0. `[writable]` The proof context account + /// 1. `[]` The proof context account owner + /// + /// * Otherwise /// None /// /// Data expected by this instruction: @@ -50,9 +97,17 @@ pub enum ProofInstruction { /// VerifyTransfer, - /// Verify a `TransferWithFeeData` struct + /// Verify a transfer with fee zero-knowledge proof. + /// + /// This instruction can be configured to optionally create a proof context state account. /// /// Accounts expected by this instruction: + /// + /// * Creating a proof context account + /// 0. `[writable]` The proof context account + /// 1. `[]` The proof context account owner + /// + /// * Otherwise /// None /// /// Data expected by this instruction: @@ -60,9 +115,17 @@ pub enum ProofInstruction { /// VerifyTransferWithFee, - /// Verify a `PubkeyValidityData` struct + /// Verify a pubkey validity zero-knowledge proof. + /// + /// This instruction can be configured to optionally create a proof context state account. /// /// Accounts expected by this instruction: + /// + /// * Creating a proof context account + /// 0. `[writable]` The proof context account + /// 1. `[]` The proof context account owner + /// + /// * Otherwise /// None /// /// Data expected by this instruction: @@ -71,50 +134,124 @@ pub enum ProofInstruction { VerifyPubkeyValidity, } -impl ProofInstruction { - pub fn encode(&self, proof: &T) -> Instruction { - let mut data = vec![ToPrimitive::to_u8(self).unwrap()]; - data.extend_from_slice(bytes_of(proof)); - Instruction { - program_id: crate::zk_token_proof_program::id(), - accounts: vec![], - data, - } - } +/// Pubkeys associated with a context state account to be used as parameters to functions. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ContextStateInfo<'a> { + pub context_state_account: &'a Pubkey, + pub context_state_authority: &'a Pubkey, +} - pub fn decode_type(input: &[u8]) -> Option { - input.first().and_then(|x| FromPrimitive::from_u8(*x)) - } +/// Create a `CloseContextState` instruction. +pub fn close_context_state( + context_state_info: ContextStateInfo, + destination_account: &Pubkey, +) -> Instruction { + let accounts = vec![ + AccountMeta::new(*context_state_info.context_state_account, false), + AccountMeta::new(*destination_account, false), + AccountMeta::new_readonly(*context_state_info.context_state_authority, true), + ]; - pub fn decode_data(input: &[u8]) -> Option<&T> { - if input.is_empty() { - None - } else { - bytemuck::try_from_bytes(&input[1..]).ok() - } + let data = vec![ToPrimitive::to_u8(&ProofInstruction::CloseContextState).unwrap()]; + + Instruction { + program_id: crate::zk_token_proof_program::id(), + accounts, + data, } } -pub fn verify_close_account(proof_data: &CloseAccountData) -> Instruction { - ProofInstruction::VerifyCloseAccount.encode(proof_data) +/// Create a `VerifyCloseAccount` instruction. +pub fn verify_close_account( + context_state_info: Option, + proof_data: &CloseAccountData, +) -> Instruction { + ProofInstruction::VerifyCloseAccount.encode_verify_proof(context_state_info, proof_data) } -pub fn verify_withdraw(proof_data: &WithdrawData) -> Instruction { - ProofInstruction::VerifyWithdraw.encode(proof_data) +/// Create a `VerifyWithdraw` instruction. +pub fn verify_withdraw( + context_state_info: Option, + proof_data: &WithdrawData, +) -> Instruction { + ProofInstruction::VerifyWithdraw.encode_verify_proof(context_state_info, proof_data) } -pub fn verify_withdraw_withheld_tokens(proof_data: &WithdrawWithheldTokensData) -> Instruction { - ProofInstruction::VerifyWithdrawWithheldTokens.encode(proof_data) +/// Create a `VerifyWithdrawWithheldTokens` instruction. +pub fn verify_withdraw_withheld_tokens( + context_state_info: Option, + proof_data: &WithdrawWithheldTokensData, +) -> Instruction { + ProofInstruction::VerifyWithdrawWithheldTokens + .encode_verify_proof(context_state_info, proof_data) } -pub fn verify_transfer(proof_data: &TransferData) -> Instruction { - ProofInstruction::VerifyTransfer.encode(proof_data) +/// Create a `VerifyTransfer` instruction. +pub fn verify_transfer( + context_state_info: Option, + proof_data: &TransferData, +) -> Instruction { + ProofInstruction::VerifyTransfer.encode_verify_proof(context_state_info, proof_data) } -pub fn verify_transfer_with_fee(proof_data: &TransferWithFeeData) -> Instruction { - ProofInstruction::VerifyTransferWithFee.encode(proof_data) +/// Create a `VerifyTransferWithFee` instruction. +pub fn verify_transfer_with_fee( + context_state_info: Option, + proof_data: &TransferWithFeeData, +) -> Instruction { + ProofInstruction::VerifyTransferWithFee.encode_verify_proof(context_state_info, proof_data) } -pub fn verify_pubkey_validity(proof_data: &PubkeyValidityData) -> Instruction { - ProofInstruction::VerifyPubkeyValidity.encode(proof_data) +/// Create a `VerifyPubkeyValidity` instruction. +pub fn verify_pubkey_validity( + context_state_info: Option, + proof_data: &PubkeyValidityData, +) -> Instruction { + ProofInstruction::VerifyPubkeyValidity.encode_verify_proof(context_state_info, proof_data) +} + +impl ProofInstruction { + pub fn encode_verify_proof( + &self, + context_state_info: Option, + proof_data: &T, + ) -> Instruction + where + T: Pod + ZkProofData, + U: Pod, + { + let accounts = if let Some(context_state_info) = context_state_info { + vec![ + AccountMeta::new(*context_state_info.context_state_account, false), + AccountMeta::new_readonly(*context_state_info.context_state_authority, false), + ] + } else { + vec![] + }; + + let mut data = vec![ToPrimitive::to_u8(self).unwrap()]; + data.extend_from_slice(bytes_of(proof_data)); + + Instruction { + program_id: crate::zk_token_proof_program::id(), + accounts, + data, + } + } + + pub fn instruction_type(input: &[u8]) -> Option { + input + .first() + .and_then(|instruction| FromPrimitive::from_u8(*instruction)) + } + + pub fn proof_data(input: &[u8]) -> Option<&T> + where + T: Pod + ZkProofData, + U: Pod, + { + input + .get(1..) + .and_then(|data| bytemuck::try_from_bytes(data).ok()) + } } diff --git a/zk-token-sdk/src/zk_token_proof_state.rs b/zk-token-sdk/src/zk_token_proof_state.rs new file mode 100644 index 00000000000000..d95aa4f11ec1c3 --- /dev/null +++ b/zk-token-sdk/src/zk_token_proof_state.rs @@ -0,0 +1,72 @@ +use { + crate::{zk_token_elgamal::pod::PodProofType, zk_token_proof_instruction::ProofType}, + bytemuck::{bytes_of, Pod, Zeroable}, + num_traits::ToPrimitive, + solana_program::{ + instruction::{InstructionError, InstructionError::InvalidAccountData}, + pubkey::Pubkey, + }, + std::mem::size_of, +}; + +/// The proof context account state +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(C)] +pub struct ProofContextState { + /// The proof context authority that can close the account + pub context_state_authority: Pubkey, + /// The proof type for the context data + pub proof_type: PodProofType, + /// The proof context data + pub proof_context: T, +} + +// `bytemuck::Pod` cannot be derived for generic structs unless the struct is marked +// `repr(packed)`, which may cause unnecessary complications when referencing its fields. Directly +// mark `ProofContextState` as `Zeroable` and `Pod` since since none of its fields has an alignment +// requirement greater than 1 and therefore, guaranteed to be `packed`. +unsafe impl Zeroable for ProofContextState {} +unsafe impl Pod for ProofContextState {} + +impl ProofContextState { + pub fn encode( + context_state_authority: &Pubkey, + proof_type: ProofType, + proof_context: &T, + ) -> Vec { + let mut buf = Vec::with_capacity(size_of::()); + buf.extend_from_slice(context_state_authority.as_ref()); + buf.push(ToPrimitive::to_u8(&proof_type).unwrap()); + buf.extend_from_slice(bytes_of(proof_context)); + buf + } + + /// Interpret a slice as a `ProofContextState`. + /// + /// This function requires a generic parameter. To access only the generic-independent fields + /// in `ProofContextState` without a generic parameter, use + /// `ProofContextStateMeta::try_from_bytes` instead. + pub fn try_from_bytes(input: &[u8]) -> Result<&Self, InstructionError> { + bytemuck::try_from_bytes(input).map_err(|_| InvalidAccountData) + } +} + +/// The `ProofContextState` without the proof context itself. This struct exists to facilitate the +/// decoding of generic-independent fields in `ProofContextState`. +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct ProofContextStateMeta { + /// The proof context authority that can close the account + pub context_state_authority: Pubkey, + /// The proof type for the context data + pub proof_type: PodProofType, +} + +impl ProofContextStateMeta { + pub fn try_from_bytes(input: &[u8]) -> Result<&Self, InstructionError> { + input + .get(..size_of::()) + .and_then(|data| bytemuck::try_from_bytes(data).ok()) + .ok_or(InvalidAccountData) + } +}