From b020dd29c161d5783b0d94c3f532ec139a11f579 Mon Sep 17 00:00:00 2001 From: Greg Fitzgerald Date: Mon, 5 Oct 2020 18:49:38 -0600 Subject: [PATCH] Add Ristretto version of THEMIS (#566) * Add Ristretto version of THEMIS BN BPF instruction counts: CalculateAggregate: 83,511 SubmitProofDecryption: 33,755,027 Ristretto BPF instruction counts: CalculateAggregate: 13,049,558 SubmitProofDecryption: 13,149,232 * Fix CI script --- Cargo.lock | 78 +++- Cargo.toml | 6 +- ci/script.sh | 3 +- themis/{client => client_bn}/Cargo.lock | 6 +- themis/{client => client_bn}/Cargo.toml | 4 +- themis/{client => client_bn}/build.rs | 2 +- themis/{client => client_bn}/examples/tps.rs | 2 +- themis/{client => client_bn}/src/lib.rs | 6 +- .../tests/assert_instruction_count.rs | 4 +- themis/client_ristretto/Cargo.toml | 40 ++ themis/client_ristretto/build.rs | 12 + themis/client_ristretto/examples/tps.rs | 36 ++ themis/client_ristretto/src/lib.rs | 359 +++++++++++++++++ .../tests/assert_instruction_count.rs | 376 ++++++++++++++++++ themis/{program => program_bn}/Cargo.toml | 2 +- themis/{program => program_bn}/Xargo.toml | 0 themis/{program => program_bn}/program-id.md | 0 .../{program => program_bn}/src/entrypoint.rs | 0 themis/{program => program_bn}/src/error.rs | 0 .../src/instruction.rs | 0 themis/{program => program_bn}/src/lib.rs | 0 .../{program => program_bn}/src/processor.rs | 0 themis/{program => program_bn}/src/state.rs | 0 themis/program_ristretto/Cargo.toml | 34 ++ themis/program_ristretto/Xargo.toml | 2 + themis/program_ristretto/program-id.md | 1 + themis/program_ristretto/src/entrypoint.rs | 18 + themis/program_ristretto/src/error.rs | 41 ++ themis/program_ristretto/src/instruction.rs | 235 +++++++++++ themis/program_ristretto/src/lib.rs | 14 + themis/program_ristretto/src/processor.rs | 141 +++++++ themis/program_ristretto/src/state.rs | 348 ++++++++++++++++ 32 files changed, 1750 insertions(+), 20 deletions(-) rename themis/{client => client_bn}/Cargo.lock (99%) rename themis/{client => client_bn}/Cargo.toml (92%) rename themis/{client => client_bn}/build.rs (90%) rename themis/{client => client_bn}/examples/tps.rs (96%) rename themis/{client => client_bn}/src/lib.rs (98%) rename themis/{client => client_bn}/tests/assert_instruction_count.rs (99%) create mode 100644 themis/client_ristretto/Cargo.toml create mode 100644 themis/client_ristretto/build.rs create mode 100644 themis/client_ristretto/examples/tps.rs create mode 100644 themis/client_ristretto/src/lib.rs create mode 100644 themis/client_ristretto/tests/assert_instruction_count.rs rename themis/{program => program_bn}/Cargo.toml (97%) rename themis/{program => program_bn}/Xargo.toml (100%) rename themis/{program => program_bn}/program-id.md (100%) rename themis/{program => program_bn}/src/entrypoint.rs (100%) rename themis/{program => program_bn}/src/error.rs (100%) rename themis/{program => program_bn}/src/instruction.rs (100%) rename themis/{program => program_bn}/src/lib.rs (100%) rename themis/{program => program_bn}/src/processor.rs (100%) rename themis/{program => program_bn}/src/state.rs (100%) create mode 100644 themis/program_ristretto/Cargo.toml create mode 100644 themis/program_ristretto/Xargo.toml create mode 100644 themis/program_ristretto/program-id.md create mode 100644 themis/program_ristretto/src/entrypoint.rs create mode 100644 themis/program_ristretto/src/error.rs create mode 100644 themis/program_ristretto/src/instruction.rs create mode 100644 themis/program_ristretto/src/lib.rs create mode 100644 themis/program_ristretto/src/processor.rs create mode 100644 themis/program_ristretto/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 9940a43f68db0b..2f7c4d167562bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -558,6 +558,20 @@ dependencies = [ "subtle 2.2.3", ] +[[package]] +name = "curve25519-dalek" +version = "2.1.0" +source = "git+https://github.com/garious/curve25519-dalek?rev=60efef3553d6bf3d7f3b09b5f97acd54d72529ff#60efef3553d6bf3d7f3b09b5f97acd54d72529ff" +dependencies = [ + "borsh", + "byteorder", + "digest 0.8.1", + "rand_core", + "serde", + "subtle 2.2.3", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "2.1.0" @@ -663,7 +677,7 @@ version = "1.0.0-pre.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a8a37f4e8b35af971e6db5e3897e7a6344caa3f92f6544f88125a1f5f0035a" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "ed25519", "rand", "serde", @@ -691,6 +705,21 @@ dependencies = [ "sha2 0.9.1", ] +[[package]] +name = "elgamal_ristretto" +version = "0.2.4" +source = "git+https://github.com/garious/elgamal?rev=260763fb67c34debe3915b39d95b6e7b3e1461d0#260763fb67c34debe3915b39d95b6e7b3e1461d0" +dependencies = [ + "bincode", + "borsh", + "clear_on_drop", + "curve25519-dalek 2.1.0 (git+https://github.com/garious/curve25519-dalek?rev=60efef3553d6bf3d7f3b09b5f97acd54d72529ff)", + "rand_core", + "serde", + "sha2 0.8.2", + "zkp", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1364,6 +1393,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "merlin" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6feca46f4fa3443a01769d768727f10c10a20fdb65e52dc16a81f0c8269bb78" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + [[package]] name = "mime" version = "0.3.16" @@ -2386,7 +2427,7 @@ dependencies = [ "backtrace", "bytes 0.4.12", "cc", - "curve25519-dalek", + "curve25519-dalek 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "ed25519-dalek", "either", "failure", @@ -2553,7 +2594,7 @@ dependencies = [ "bv", "byteorder", "chrono", - "curve25519-dalek", + "curve25519-dalek 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "digest 0.9.0", "ed25519-dalek", "generic-array 0.14.3", @@ -2727,7 +2768,7 @@ dependencies = [ ] [[package]] -name = "spl-themis" +name = "spl-themis-bn" version = "0.1.0" dependencies = [ "bincode", @@ -2742,6 +2783,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "spl-themis-ristretto" +version = "0.1.0" +dependencies = [ + "bincode", + "borsh", + "curve25519-dalek 2.1.0 (git+https://github.com/garious/curve25519-dalek?rev=60efef3553d6bf3d7f3b09b5f97acd54d72529ff)", + "elgamal_ristretto", + "getrandom", + "num-derive", + "num-traits", + "rand", + "solana-sdk", + "thiserror", +] + [[package]] name = "spl-token" version = "2.0.5" @@ -3625,6 +3682,19 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zkp" +version = "0.7.0" +source = "git+https://github.com/garious/zkp?rev=ed9eba6de4214fe67466b662b0d8017c3645c3f8#ed9eba6de4214fe67466b662b0d8017c3645c3f8" +dependencies = [ + "curve25519-dalek 2.1.0 (git+https://github.com/garious/curve25519-dalek?rev=60efef3553d6bf3d7f3b09b5f97acd54d72529ff)", + "merlin", + "rand", + "serde", + "serde_derive", + "thiserror", +] + [[package]] name = "zstd" version = "0.5.3+zstd.1.4.5" diff --git a/Cargo.toml b/Cargo.toml index 42bff6a8f880a3..205f4c208d90ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,14 +3,16 @@ members = [ "utils/cgen", "utils/test-client", "memo/program", - "themis/program", + "themis/program_bn", + "themis/program_ristretto", "token-swap/program", "token/cli", "token/program", "token/program-v3", ] exclude = [ - "themis/client", + "themis/client_bn", + "themis/client_ristretto", "token/perf-monitor", ] diff --git a/ci/script.sh b/ci/script.sh index 2edf02c1e51bc3..7efb6817d1296e 100755 --- a/ci/script.sh +++ b/ci/script.sh @@ -67,7 +67,8 @@ done # Run SPL Token's performance monitor _ cargo test --manifest-path=token/perf-monitor/Cargo.toml -- --nocapture -_ cargo test --manifest-path=themis/client/Cargo.toml -- --nocapture +_ cargo test --manifest-path=themis/client_bn/Cargo.toml -- --nocapture +_ cargo test --manifest-path=themis/client_ristretto/Cargo.toml -- --nocapture # Test token js bindings diff --git a/themis/client/Cargo.lock b/themis/client_bn/Cargo.lock similarity index 99% rename from themis/client/Cargo.lock rename to themis/client_bn/Cargo.lock index 33397c741d9210..98ad1136a39d2f 100644 --- a/themis/client/Cargo.lock +++ b/themis/client_bn/Cargo.lock @@ -2330,7 +2330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] -name = "spl-themis" +name = "spl-themis-bn" version = "0.1.0" dependencies = [ "bincode", @@ -2346,7 +2346,7 @@ dependencies = [ ] [[package]] -name = "spl-themis-client" +name = "spl-themis-bn-client" version = "0.1.0" dependencies = [ "bincode", @@ -2362,7 +2362,7 @@ dependencies = [ "solana-runtime", "solana-sdk", "solana_rbpf", - "spl-themis", + "spl-themis-bn", "tarpc", "tokio 0.2.22", "url", diff --git a/themis/client/Cargo.toml b/themis/client_bn/Cargo.toml similarity index 92% rename from themis/client/Cargo.toml rename to themis/client_bn/Cargo.toml index 1d593ecf67d033..495a3af564a1a4 100644 --- a/themis/client/Cargo.toml +++ b/themis/client_bn/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "spl-themis-client" +name = "spl-themis-bn-client" version = "0.1.0" description = "SPL THEMIS client" authors = ["Solana Maintainers "] @@ -24,7 +24,7 @@ futures = "0.3" solana-banks-client = "1.3.14" solana-cli-config = "1.3.14" solana-sdk = "1.3.14" -spl-themis = { version = "0.1.0", path = "../program" } +spl-themis-bn = { version = "0.1.0", path = "../program_bn" } tarpc = { version = "0.21.1", features = ["full"] } tokio = "0.2" url = "2.1" diff --git a/themis/client/build.rs b/themis/client_bn/build.rs similarity index 90% rename from themis/client/build.rs rename to themis/client_bn/build.rs index 2cfe9107a7ab2d..31517e93d22365 100644 --- a/themis/client/build.rs +++ b/themis/client_bn/build.rs @@ -5,7 +5,7 @@ fn main() { Command::new(canonicalize("../../do.sh").unwrap()) .current_dir("../..") .arg("build") - .arg("themis/program") + .arg("themis/program_bn") .status() .expect("Failed to build themis program") .success(); diff --git a/themis/client/examples/tps.rs b/themis/client_bn/examples/tps.rs similarity index 96% rename from themis/client/examples/tps.rs rename to themis/client_bn/examples/tps.rs index af6a26b9110959..13f2220ecaa45c 100644 --- a/themis/client/examples/tps.rs +++ b/themis/client_bn/examples/tps.rs @@ -4,7 +4,7 @@ use bn::Fr; use solana_banks_client::start_tcp_client; use solana_cli_config::{Config, CONFIG_FILE}; use solana_sdk::signature::read_keypair_file; -use spl_themis_client::test_e2e; +use spl_themis_bn_client::test_e2e; use std::path::Path; use tokio::runtime::Runtime; use url::Url; diff --git a/themis/client/src/lib.rs b/themis/client_bn/src/lib.rs similarity index 98% rename from themis/client/src/lib.rs rename to themis/client_bn/src/lib.rs index 6c5a0727c038ce..c38a0ec72e2219 100644 --- a/themis/client/src/lib.rs +++ b/themis/client_bn/src/lib.rs @@ -12,7 +12,7 @@ use solana_sdk::{ system_instruction, transaction::Transaction, }; -use spl_themis::{ +use spl_themis_bn::{ instruction, state::generate_keys, // recover_scalar, User}, }; @@ -245,7 +245,7 @@ mod tests { instruction::InstructionError, program_error::ProgramError, }; - use spl_themis::processor::process_instruction; + use spl_themis_bn::processor::process_instruction; use std::{ collections::HashMap, sync::{Arc, RwLock}, @@ -337,7 +337,7 @@ mod tests { fn test_local_e2e_2ads() { let (genesis_config, sender_keypair) = create_genesis_config(sol_to_lamports(9_000_000.0)); let mut bank = Bank::new(&genesis_config); - bank.add_builtin_program("Themis", spl_themis::id(), process_instruction_native); + bank.add_builtin_program("Themis", spl_themis_bn::id(), process_instruction_native); let bank_forks = Arc::new(RwLock::new(BankForks::new(bank))); Runtime::new().unwrap().block_on(async { let transport = start_local_server(&bank_forks).await; diff --git a/themis/client/tests/assert_instruction_count.rs b/themis/client_bn/tests/assert_instruction_count.rs similarity index 99% rename from themis/client/tests/assert_instruction_count.rs rename to themis/client_bn/tests/assert_instruction_count.rs index 4d9e51f7ef00fd..12ea7ee224420c 100644 --- a/themis/client/tests/assert_instruction_count.rs +++ b/themis/client_bn/tests/assert_instruction_count.rs @@ -18,7 +18,7 @@ use solana_sdk::{ message::Message, pubkey::Pubkey, }; -use spl_themis::{ +use spl_themis_bn::{ instruction::ThemisInstruction, state::{generate_keys, /*recover_scalar,*/ Policies, User}, }; @@ -42,7 +42,7 @@ fn run_program( instruction_data: &[u8], ) -> Result { let mut program_account = SolanaAccount::default(); - program_account.data = load_program("spl_themis"); + program_account.data = load_program("spl_themis_bn"); let loader_id = bpf_loader::id(); let mut invoke_context = MockInvokeContext::default(); let executable = EbpfVm::::create_executable_from_elf( diff --git a/themis/client_ristretto/Cargo.toml b/themis/client_ristretto/Cargo.toml new file mode 100644 index 00000000000000..66f078db3b8bec --- /dev/null +++ b/themis/client_ristretto/Cargo.toml @@ -0,0 +1,40 @@ + +[package] +name = "spl-themis-ristretto-client" +version = "0.1.0" +description = "SPL THEMIS client" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2018" +exclude = ["js/**"] + +[features] +no-entrypoint = [] +skip-no-mangle = ["solana-sdk/skip-no-mangle"] +program = ["solana-sdk/program"] +default = ["solana-sdk/default"] + +[dependencies] +bincode = "1.3" +borsh = "0.7.1" +curve25519-dalek = {git = "https://github.com/garious/curve25519-dalek", rev = "60efef3553d6bf3d7f3b09b5f97acd54d72529ff", default-features = false, features = ["borsh"]} +elgamal_ristretto = { git = "https://github.com/garious/elgamal", rev = "260763fb67c34debe3915b39d95b6e7b3e1461d0" } +futures = "0.3" +solana-banks-client = "1.3.14" +solana-cli-config = "1.3.14" +solana-sdk = "1.3.14" +spl-themis-ristretto = { version = "0.1.0", path = "../program_ristretto" } +tarpc = { version = "0.21.1", features = ["full"] } +tokio = "0.2" +url = "2.1" + +[dev-dependencies] +separator = "0.4.1" +solana-banks-server = "1.3.14" +solana-bpf-loader-program = "1.3.14" +solana_rbpf = "=0.1.31" +solana-runtime = "1.3.14" + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/themis/client_ristretto/build.rs b/themis/client_ristretto/build.rs new file mode 100644 index 00000000000000..ded8ed74dd903c --- /dev/null +++ b/themis/client_ristretto/build.rs @@ -0,0 +1,12 @@ +use std::{fs::canonicalize, process::Command}; + +fn main() { + println!("cargo:warning=(not a warning) Building SPL Themis shared object"); + Command::new(canonicalize("../../do.sh").unwrap()) + .current_dir("../..") + .arg("build") + .arg("themis/program_ristretto") + .status() + .expect("Failed to build themis program") + .success(); +} diff --git a/themis/client_ristretto/examples/tps.rs b/themis/client_ristretto/examples/tps.rs new file mode 100644 index 00000000000000..e83219d8a9cd11 --- /dev/null +++ b/themis/client_ristretto/examples/tps.rs @@ -0,0 +1,36 @@ +//! Themis client + +use solana_banks_client::start_tcp_client; +use solana_cli_config::{Config, CONFIG_FILE}; +use solana_sdk::signature::read_keypair_file; +use spl_themis_ristretto_client::test_e2e; +use std::path::Path; +use tokio::runtime::Runtime; +use url::Url; + +fn main() { + let config_file = CONFIG_FILE.as_ref().unwrap(); + let config = if Path::new(&config_file).exists() { + Config::load(&config_file).unwrap() + } else { + Config::default() + }; + let rpc_banks_url = Config::compute_rpc_banks_url(&config.json_rpc_url); + let url = Url::parse(&rpc_banks_url).unwrap(); + let host_port = (url.host_str().unwrap(), url.port().unwrap()); + + Runtime::new().unwrap().block_on(async { + let mut banks_client = start_tcp_client(host_port).await.unwrap(); + let policies = vec![1u64.into(), 2u64.into()]; + let sender_keypair = read_keypair_file(&config.keypair_path).unwrap(); + test_e2e( + &mut banks_client, + sender_keypair, + policies, + 1_000, + 3u64.into(), + ) + .await + .unwrap(); + }); +} diff --git a/themis/client_ristretto/src/lib.rs b/themis/client_ristretto/src/lib.rs new file mode 100644 index 00000000000000..6d03d183b5b41a --- /dev/null +++ b/themis/client_ristretto/src/lib.rs @@ -0,0 +1,359 @@ +//! Themis client +use curve25519_dalek::{ + constants::RISTRETTO_BASEPOINT_POINT, ristretto::RistrettoPoint, scalar::Scalar, +}; +use elgamal_ristretto::{/*ciphertext::Ciphertext,*/ private::SecretKey, public::PublicKey}; +use futures::future::join_all; +use solana_banks_client::{BanksClient, BanksClientExt}; +use solana_sdk::{ + commitment_config::CommitmentLevel, + message::Message, + native_token::sol_to_lamports, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, +}; +use spl_themis_ristretto::{ + instruction, + state::generate_keys, // recover_scalar, User}, +}; +use std::{io, time::Instant}; +//use tarpc::context; + +/// For a single user, create interactions, calculate the aggregate, submit a proof, and verify it. +async fn run_user_workflow( + mut client: BanksClient, + sender_keypair: Keypair, + (_sk, pk): (SecretKey, PublicKey), + interactions: Vec<(RistrettoPoint, RistrettoPoint)>, + policies_pubkey: Pubkey, + _expected_scalar_aggregate: Scalar, +) -> io::Result { + let sender_pubkey = sender_keypair.pubkey(); + let mut num_transactions = 0; + + // Create the users account + let user_keypair = Keypair::new(); + let user_pubkey = user_keypair.pubkey(); + let ixs = + instruction::create_user_account(&sender_pubkey, &user_pubkey, sol_to_lamports(0.001), pk); + let msg = Message::new(&ixs, Some(&sender_keypair.pubkey())); + let recent_blockhash = client.get_recent_blockhash().await?; + let tx = Transaction::new(&[&sender_keypair, &user_keypair], msg, recent_blockhash); + let tx_size = bincode::serialize(&tx).unwrap().len(); + assert!( + tx_size <= 1200, + "transaction over 1200 bytes: {} bytes", + tx_size + ); + client + .process_transaction_with_commitment(tx, CommitmentLevel::Recent) + .await + .unwrap(); + num_transactions += 1; + + // Send one interaction at a time to stay under the BPF instruction limit + for (i, interaction) in interactions.into_iter().enumerate() { + let interactions = vec![(i as u8, interaction)]; + let ix = instruction::submit_interactions(&user_pubkey, &policies_pubkey, interactions); + let msg = Message::new(&[ix], Some(&sender_keypair.pubkey())); + let recent_blockhash = client.get_recent_blockhash().await?; + let tx = Transaction::new(&[&sender_keypair, &user_keypair], msg, recent_blockhash); + let tx_size = bincode::serialize(&tx).unwrap().len(); + assert!( + tx_size <= 1200, + "transaction over 1200 bytes: {} bytes", + tx_size + ); + client + .process_transaction_with_commitment(tx, CommitmentLevel::Recent) + .await + .unwrap(); + num_transactions += 1; + } + + //let user_account = client + // .get_account_with_commitment_and_context( + // context::current(), + // user_pubkey, + // CommitmentLevel::Recent, + // ) + // .await + // .unwrap() + // .unwrap(); + //let user = User::deserialize(&user_account.data).unwrap(); + //let ciphertext = Ciphertext { + // points: user.fetch_encrypted_aggregate(), + // pk, + //}; + + //let decrypted_aggregate = sk.decrypt(&ciphertext); + let decrypted_aggregate = RISTRETTO_BASEPOINT_POINT; + //let scalar_aggregate = recover_scalar(decrypted_aggregate, 16); + //assert_eq!(scalar_aggregate, expected_scalar_aggregate); + + //let ((announcement_g, announcement_ctx), response) = + // sk.prove_correct_decryption_no_Merlin(&ciphertext, &decrypted_aggregate).unwrap(); + let ((announcement_g, announcement_ctx), response) = ( + (RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_POINT), + 0u64.into(), + ); + //sk.prove_correct_decryption_no_Merlin(&ciphertext, &decrypted_aggregate).unwrap(); + + let ix = instruction::submit_proof_decryption( + &user_pubkey, + decrypted_aggregate, + announcement_g, + announcement_ctx, + response, + ); + let msg = Message::new(&[ix], Some(&sender_keypair.pubkey())); + let recent_blockhash = client.get_recent_blockhash().await?; + let tx = Transaction::new(&[&sender_keypair, &user_keypair], msg, recent_blockhash); + let tx_size = bincode::serialize(&tx).unwrap().len(); + assert!( + tx_size <= 1200, + "transaction over 1200 bytes: {} bytes", + tx_size + ); + client + .process_transaction_with_commitment(tx, CommitmentLevel::Recent) + .await + .unwrap(); + num_transactions += 1; + + //let user_account = client.get_account_with_commitment_and_context(context::current(), user_pubkey, CommitmentLevel::Recent).await.unwrap().unwrap(); + //let user = User::deserialize(&user_account.data).unwrap(); + //assert!(user.fetch_proof_verification()); + + Ok(num_transactions) +} + +pub async fn test_e2e( + client: &mut BanksClient, + sender_keypair: Keypair, + policies: Vec, + num_users: u64, + expected_scalar_aggregate: Scalar, +) -> io::Result<()> { + let sender_pubkey = sender_keypair.pubkey(); + let policies_keypair = Keypair::new(); + let policies_pubkey = policies_keypair.pubkey(); + let policies_len = policies.len(); + + // Create the policies account + let mut ixs = instruction::create_policies_account( + &sender_pubkey, + &policies_pubkey, + sol_to_lamports(0.01), + policies.len() as u8, + ); + let policies_slice: Vec<_> = policies + .iter() + .enumerate() + .map(|(i, x)| (i as u8, *x)) + .collect(); + ixs.push(instruction::store_policies( + &policies_pubkey, + policies_slice, + )); + + let msg = Message::new(&ixs, Some(&sender_keypair.pubkey())); + let recent_blockhash = client.get_recent_blockhash().await?; + let tx = Transaction::new(&[&sender_keypair, &policies_keypair], msg, recent_blockhash); + let tx_size = bincode::serialize(&tx).unwrap().len(); + assert!( + tx_size <= 1200, + "transaction over 1200 bytes: {} bytes", + tx_size + ); + client + .process_transaction_with_commitment(tx, CommitmentLevel::Recent) + .await + .unwrap(); + + // Send feepayer_keypairs some SOL + let feepayers: Vec<_> = (0..num_users).map(|_| Keypair::new()).collect(); + for feepayers in feepayers.chunks(20) { + println!("Seeding feepayer accounts..."); + let payments: Vec<_> = feepayers + .iter() + .map(|keypair| (keypair.pubkey(), sol_to_lamports(0.0011))) + .collect(); + let ixs = system_instruction::transfer_many(&sender_pubkey, &payments); + let msg = Message::new(&ixs, Some(&sender_keypair.pubkey())); + let recent_blockhash = client.get_recent_blockhash().await.unwrap(); + let tx = Transaction::new(&[&sender_keypair], msg, recent_blockhash); + let tx_size = bincode::serialize(&tx).unwrap().len(); + assert!( + tx_size <= 1200, + "transaction over 1200 bytes: {} bytes", + tx_size + ); + client + .process_transaction_with_commitment(tx, CommitmentLevel::Recent) + .await + .unwrap(); + } + + println!("Starting benchmark..."); + let now = Instant::now(); + + let (sk, pk) = generate_keys(); + let interactions: Vec<_> = (0..policies_len) + .map(|_| pk.encrypt(&RISTRETTO_BASEPOINT_POINT).points) + .collect(); + + let futures: Vec<_> = feepayers + .into_iter() + .map(move |feepayer_keypair| { + run_user_workflow( + client.clone(), + feepayer_keypair, + (sk.clone(), pk), + interactions.clone(), + policies_pubkey, + expected_scalar_aggregate, + ) + }) + .collect(); + let results = join_all(futures).await; + let elapsed = now.elapsed(); + println!("Benchmark complete."); + + let num_transactions = results + .into_iter() + .map(|result| result.unwrap()) + .sum::(); + println!( + "{} transactions in {:?} ({} TPS)", + num_transactions, + elapsed, + num_transactions as f64 / elapsed.as_secs_f64() + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use solana_banks_client::start_client; + use solana_banks_server::banks_server::start_local_server; + use solana_runtime::{bank::Bank, bank_forks::BankForks}; + use solana_sdk::{ + account::{Account, KeyedAccount}, + account_info::AccountInfo, + genesis_config::create_genesis_config, + instruction::InstructionError, + program_error::ProgramError, + }; + use spl_themis_ristretto::processor::process_instruction; + use std::{ + collections::HashMap, + sync::{Arc, RwLock}, + {cell::RefCell, rc::Rc}, + }; + use tokio::runtime::Runtime; + + fn to_instruction_error(error: ProgramError) -> InstructionError { + match error { + ProgramError::Custom(err) => InstructionError::Custom(err), + ProgramError::InvalidArgument => InstructionError::InvalidArgument, + ProgramError::InvalidInstructionData => InstructionError::InvalidInstructionData, + ProgramError::InvalidAccountData => InstructionError::InvalidAccountData, + ProgramError::AccountDataTooSmall => InstructionError::AccountDataTooSmall, + ProgramError::InsufficientFunds => InstructionError::InsufficientFunds, + ProgramError::IncorrectProgramId => InstructionError::IncorrectProgramId, + ProgramError::MissingRequiredSignature => InstructionError::MissingRequiredSignature, + ProgramError::AccountAlreadyInitialized => InstructionError::AccountAlreadyInitialized, + ProgramError::UninitializedAccount => InstructionError::UninitializedAccount, + ProgramError::NotEnoughAccountKeys => InstructionError::NotEnoughAccountKeys, + ProgramError::AccountBorrowFailed => InstructionError::AccountBorrowFailed, + ProgramError::MaxSeedLengthExceeded => InstructionError::MaxSeedLengthExceeded, + ProgramError::InvalidSeeds => InstructionError::InvalidSeeds, + } + } + + // Same as process_instruction, but but can be used as a builtin program. Handy for unit-testing. + pub fn process_instruction_native( + program_id: &Pubkey, + keyed_accounts: &[KeyedAccount], + input: &[u8], + ) -> Result<(), InstructionError> { + // Copy all the accounts into a HashMap to ensure there are no duplicates + let mut accounts: HashMap = keyed_accounts + .iter() + .map(|ka| (*ka.unsigned_key(), ka.account.borrow().clone())) + .collect(); + + // Create shared references to each account's lamports/data/owner + let account_refs: HashMap<_, _> = accounts + .iter_mut() + .map(|(key, account)| { + ( + *key, + ( + Rc::new(RefCell::new(&mut account.lamports)), + Rc::new(RefCell::new(&mut account.data[..])), + &account.owner, + ), + ) + }) + .collect(); + + // Create AccountInfos + let account_infos: Vec = keyed_accounts + .iter() + .map(|keyed_account| { + let key = keyed_account.unsigned_key(); + let (lamports, data, owner) = &account_refs[key]; + AccountInfo { + key, + is_signer: keyed_account.signer_key().is_some(), + is_writable: keyed_account.is_writable(), + lamports: lamports.clone(), + data: data.clone(), + owner, + executable: keyed_account.executable().unwrap(), + rent_epoch: keyed_account.rent_epoch().unwrap(), + } + }) + .collect(); + + // Execute the BPF entrypoint + process_instruction(program_id, &account_infos, input).map_err(to_instruction_error)?; + + // Commit changes to the KeyedAccounts + for keyed_account in keyed_accounts { + let mut account = keyed_account.account.borrow_mut(); + let key = keyed_account.unsigned_key(); + let (lamports, data, _owner) = &account_refs[key]; + account.lamports = **lamports.borrow(); + account.data = data.borrow().to_vec(); + } + + Ok(()) + } + + #[test] + fn test_local_e2e_2ads() { + let (genesis_config, sender_keypair) = create_genesis_config(sol_to_lamports(9_000_000.0)); + let mut bank = Bank::new(&genesis_config); + bank.add_builtin_program( + "Themis", + spl_themis_ristretto::id(), + process_instruction_native, + ); + let bank_forks = Arc::new(RwLock::new(BankForks::new(bank))); + Runtime::new().unwrap().block_on(async { + let transport = start_local_server(&bank_forks).await; + let mut banks_client = start_client(transport).await.unwrap(); + let policies = vec![1u64.into(), 2u64.into()]; + test_e2e(&mut banks_client, sender_keypair, policies, 10, 3u64.into()) + .await + .unwrap(); + }); + } +} diff --git a/themis/client_ristretto/tests/assert_instruction_count.rs b/themis/client_ristretto/tests/assert_instruction_count.rs new file mode 100644 index 00000000000000..581756701a1de6 --- /dev/null +++ b/themis/client_ristretto/tests/assert_instruction_count.rs @@ -0,0 +1,376 @@ +use borsh::BorshSerialize; +use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT; +use elgamal_ristretto::ciphertext::Ciphertext; +use separator::Separatable; +use solana_bpf_loader_program::{ + create_vm, + serialization::{deserialize_parameters, serialize_parameters}, +}; +use solana_rbpf::vm::{EbpfVm, InstructionMeter}; +use solana_runtime::process_instruction::{ + ComputeBudget, ComputeMeter, Executor, InvokeContext, Logger, ProcessInstruction, +}; +use solana_sdk::{ + account::{Account as SolanaAccount, KeyedAccount}, + bpf_loader, + entrypoint::SUCCESS, + instruction::{CompiledInstruction, InstructionError}, + message::Message, + pubkey::Pubkey, +}; +use spl_themis_ristretto::{ + instruction::ThemisInstruction, + state::{generate_keys, /*recover_scalar,*/ Policies, User}, +}; +use std::{cell::RefCell, fs::File, io::Read, path::PathBuf, rc::Rc, sync::Arc}; + +fn load_program(name: &str) -> Vec { + let mut path = PathBuf::new(); + path.push("../../target/bpfel-unknown-unknown/release"); + path.push(name); + path.set_extension("so"); + let mut file = File::open(path).unwrap(); + + let mut program = Vec::new(); + file.read_to_end(&mut program).unwrap(); + program +} + +fn run_program( + program_id: &Pubkey, + parameter_accounts: &[KeyedAccount], + instruction_data: &[u8], +) -> Result { + let mut program_account = SolanaAccount::default(); + program_account.data = load_program("spl_themis_ristretto"); + let loader_id = bpf_loader::id(); + let mut invoke_context = MockInvokeContext::default(); + let executable = EbpfVm::::create_executable_from_elf( + &&program_account.data, + None, + ) + .unwrap(); + let (mut vm, heap_region) = create_vm( + &loader_id, + executable.as_ref(), + parameter_accounts, + &mut invoke_context, + ) + .unwrap(); + let mut parameter_bytes = serialize_parameters( + &loader_id, + program_id, + parameter_accounts, + &instruction_data, + ) + .unwrap(); + assert_eq!( + SUCCESS, + vm.execute_program(parameter_bytes.as_mut_slice(), &[], &[heap_region]) + .unwrap() + ); + deserialize_parameters(&loader_id, parameter_accounts, ¶meter_bytes).unwrap(); + Ok(vm.get_total_instruction_count()) +} + +#[test] +fn assert_instruction_count() { + let program_id = Pubkey::new_rand(); + + // Create new policies + let policies_key = Pubkey::new_rand(); + let scalars = vec![1u64.into(), 2u64.into()]; + //let scalars = vec![ + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), //10 + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), // 2 * 10 + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), + // 1u64.into(), //10 + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), + // 2u64.into(), // 2 * 10 + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + // 0u64.into(), + //]; + let num_scalars = scalars.len(); + + let (sk, pk) = generate_keys(); + let encrypted_interactions: Vec<_> = (0..num_scalars) + .map(|i| (i as u8, pk.encrypt(&RISTRETTO_BASEPOINT_POINT).points)) + .collect(); + + let policies_account = SolanaAccount::new_ref( + 0, + Policies { + is_initialized: true, + num_scalars: num_scalars as u8, + scalars: scalars.clone(), + } + .try_to_vec() + .unwrap() + .len(), + &program_id, + ); + let instruction_data = ThemisInstruction::InitializePoliciesAccount { + num_scalars: num_scalars as u8, + } + .serialize() + .unwrap(); + let parameter_accounts = vec![KeyedAccount::new(&policies_key, false, &policies_account)]; + let initialize_policies_count = + run_program(&program_id, ¶meter_accounts[..], &instruction_data).unwrap(); + + // Create user account + let user_key = Pubkey::new_rand(); + let user_account = + SolanaAccount::new_ref(0, User::default().try_to_vec().unwrap().len(), &program_id); + let instruction_data = ThemisInstruction::InitializeUserAccount { public_key: pk } + .serialize() + .unwrap(); + let parameter_accounts = vec![KeyedAccount::new(&user_key, false, &user_account)]; + let initialize_user_count = + run_program(&program_id, ¶meter_accounts[..], &instruction_data).unwrap(); + + // Calculate Aggregate + let instruction_data = ThemisInstruction::SubmitInteractions { + encrypted_interactions, + } + .serialize() + .unwrap(); + let parameter_accounts = vec![ + KeyedAccount::new(&user_key, true, &user_account), + KeyedAccount::new(&policies_key, false, &policies_account), + ]; + let calculate_aggregate_count = + run_program(&program_id, ¶meter_accounts[..], &instruction_data).unwrap(); + + // Submit proof decryption + let user = User::deserialize(&user_account.try_borrow().unwrap().data).unwrap(); + let encrypted_point = user.fetch_encrypted_aggregate(); + let ciphertext = Ciphertext { + points: encrypted_point, + pk, + }; + + let decrypted_aggregate = sk.decrypt(&ciphertext); + //let scalar_aggregate = recover_scalar(decrypted_aggregate, 16); + //let expected_scalar_aggregate = 3u64.into(); + //assert_eq!(scalar_aggregate, expected_scalar_aggregate); + + let (announcement, response) = + sk.prove_correct_decryption_no_Merlin(&ciphertext, &decrypted_aggregate); + + let instruction_data = ThemisInstruction::SubmitProofDecryption { + plaintext: decrypted_aggregate, + announcement: Box::new(announcement), + response, + } + .serialize() + .unwrap(); + let parameter_accounts = vec![KeyedAccount::new(&user_key, true, &user_account)]; + let proof_decryption_count = + run_program(&program_id, ¶meter_accounts[..], &instruction_data).unwrap(); + + const BASELINE_NEW_POLICIES_COUNT: u64 = 80_000; // last known 75,796 @ 128, 4,675 @ 2 + const BASELINE_INITIALIZE_USER_COUNT: u64 = 22_000; // last known 19,868 + const BASELINE_CALCULATE_AGGREGATE_COUNT: u64 = 15_000_000; // last known 13,061,884 + const BASELINE_PROOF_DECRYPTION_COUNT: u64 = 60_000_000; // last known 13,167,140 + + println!("BPF instructions executed"); + println!( + " InitializePolicies({}): {} ({:?})", + num_scalars, + initialize_policies_count.separated_string(), + BASELINE_NEW_POLICIES_COUNT + ); + println!( + " InitializeUserAccount: {} ({:?})", + initialize_user_count.separated_string(), + BASELINE_INITIALIZE_USER_COUNT + ); + println!( + " CalculateAggregate: {} ({:?})", + calculate_aggregate_count.separated_string(), + BASELINE_CALCULATE_AGGREGATE_COUNT + ); + println!( + " SubmitProofDecryption: {} ({:?})", + proof_decryption_count.separated_string(), + BASELINE_PROOF_DECRYPTION_COUNT + ); + + assert!(initialize_policies_count <= BASELINE_NEW_POLICIES_COUNT); + assert!(initialize_user_count <= BASELINE_INITIALIZE_USER_COUNT); + assert!(calculate_aggregate_count <= BASELINE_CALCULATE_AGGREGATE_COUNT); + assert!(proof_decryption_count <= BASELINE_PROOF_DECRYPTION_COUNT); +} + +// Mock InvokeContext + +#[derive(Debug, Default)] +struct MockInvokeContext { + pub key: Pubkey, + pub logger: MockLogger, + pub compute_meter: MockComputeMeter, +} +impl InvokeContext for MockInvokeContext { + fn push(&mut self, _key: &Pubkey) -> Result<(), InstructionError> { + Ok(()) + } + fn pop(&mut self) {} + fn verify_and_update( + &mut self, + _message: &Message, + _instruction: &CompiledInstruction, + _accounts: &[Rc>], + ) -> Result<(), InstructionError> { + Ok(()) + } + fn get_caller(&self) -> Result<&Pubkey, InstructionError> { + Ok(&self.key) + } + fn get_programs(&self) -> &[(Pubkey, ProcessInstruction)] { + &[] + } + fn get_logger(&self) -> Rc> { + Rc::new(RefCell::new(self.logger.clone())) + } + fn is_cross_program_supported(&self) -> bool { + true + } + fn get_compute_budget(&self) -> ComputeBudget { + ComputeBudget { + max_invoke_depth: 10, + ..ComputeBudget::default() + } + } + fn get_compute_meter(&self) -> Rc> { + Rc::new(RefCell::new(self.compute_meter.clone())) + } + fn add_executor(&mut self, _pubkey: &Pubkey, _executor: Arc) {} + fn get_executor(&mut self, _pubkey: &Pubkey) -> Option> { + None + } + fn record_instruction(&self, _: &solana_sdk::instruction::Instruction) { + todo!() + } +} + +#[derive(Debug, Default, Clone)] +struct MockComputeMeter {} +impl ComputeMeter for MockComputeMeter { + fn consume(&mut self, _amount: u64) -> Result<(), InstructionError> { + Ok(()) + } + fn get_remaining(&self) -> u64 { + u64::MAX + } +} +#[derive(Debug, Default, Clone)] +struct MockLogger {} +impl Logger for MockLogger { + fn log_enabled(&self) -> bool { + true + } + fn log(&mut self, message: &str) { + println!("{}", message); + } +} + +struct TestInstructionMeter {} +impl InstructionMeter for TestInstructionMeter { + fn consume(&mut self, _amount: u64) {} + fn get_remaining(&self) -> u64 { + u64::MAX + } +} diff --git a/themis/program/Cargo.toml b/themis/program_bn/Cargo.toml similarity index 97% rename from themis/program/Cargo.toml rename to themis/program_bn/Cargo.toml index 592d079f092c99..4a5d8cd3770eb9 100644 --- a/themis/program/Cargo.toml +++ b/themis/program_bn/Cargo.toml @@ -2,7 +2,7 @@ # Note: This crate must be built using do.sh [package] -name = "spl-themis" +name = "spl-themis-bn" version = "0.1.0" description = "Solana Program Library THEMIS" authors = ["Solana Maintainers "] diff --git a/themis/program/Xargo.toml b/themis/program_bn/Xargo.toml similarity index 100% rename from themis/program/Xargo.toml rename to themis/program_bn/Xargo.toml diff --git a/themis/program/program-id.md b/themis/program_bn/program-id.md similarity index 100% rename from themis/program/program-id.md rename to themis/program_bn/program-id.md diff --git a/themis/program/src/entrypoint.rs b/themis/program_bn/src/entrypoint.rs similarity index 100% rename from themis/program/src/entrypoint.rs rename to themis/program_bn/src/entrypoint.rs diff --git a/themis/program/src/error.rs b/themis/program_bn/src/error.rs similarity index 100% rename from themis/program/src/error.rs rename to themis/program_bn/src/error.rs diff --git a/themis/program/src/instruction.rs b/themis/program_bn/src/instruction.rs similarity index 100% rename from themis/program/src/instruction.rs rename to themis/program_bn/src/instruction.rs diff --git a/themis/program/src/lib.rs b/themis/program_bn/src/lib.rs similarity index 100% rename from themis/program/src/lib.rs rename to themis/program_bn/src/lib.rs diff --git a/themis/program/src/processor.rs b/themis/program_bn/src/processor.rs similarity index 100% rename from themis/program/src/processor.rs rename to themis/program_bn/src/processor.rs diff --git a/themis/program/src/state.rs b/themis/program_bn/src/state.rs similarity index 100% rename from themis/program/src/state.rs rename to themis/program_bn/src/state.rs diff --git a/themis/program_ristretto/Cargo.toml b/themis/program_ristretto/Cargo.toml new file mode 100644 index 00000000000000..eefc743756a39b --- /dev/null +++ b/themis/program_ristretto/Cargo.toml @@ -0,0 +1,34 @@ + +# Note: This crate must be built using do.sh + +[package] +name = "spl-themis-ristretto" +version = "0.1.0" +description = "Solana Program Library THEMIS" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2018" +exclude = ["js/**"] + +[features] +no-entrypoint = [] +skip-no-mangle = ["solana-sdk/skip-no-mangle"] +program = ["solana-sdk/program"] +default = ["solana-sdk/default"] + +[dependencies] +bincode = "1.3" +borsh = "0.7.1" +curve25519-dalek = { git = "https://github.com/garious/curve25519-dalek", rev = "60efef3553d6bf3d7f3b09b5f97acd54d72529ff", features = ["borsh"] } +elgamal_ristretto = { git = "https://github.com/garious/elgamal", rev = "260763fb67c34debe3915b39d95b6e7b3e1461d0" } +getrandom = { version = "0.1.15", features = ["dummy"] } +num-derive = "0.3" +num-traits = "0.2" +rand = "0.7.0" +solana-sdk = { version = "1.3.14", default-features = false, optional = true } +thiserror = "1.0" + +[lib] +crate-type = ["cdylib", "lib"] + diff --git a/themis/program_ristretto/Xargo.toml b/themis/program_ristretto/Xargo.toml new file mode 100644 index 00000000000000..1744f098ae1f4a --- /dev/null +++ b/themis/program_ristretto/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/themis/program_ristretto/program-id.md b/themis/program_ristretto/program-id.md new file mode 100644 index 00000000000000..b10e1d03101c20 --- /dev/null +++ b/themis/program_ristretto/program-id.md @@ -0,0 +1 @@ +FHpg4MzrRSmTjNhPmEMbYhhe7sjrUFefe2hJg2m7fGvP diff --git a/themis/program_ristretto/src/entrypoint.rs b/themis/program_ristretto/src/entrypoint.rs new file mode 100644 index 00000000000000..c72f6ed2f419f8 --- /dev/null +++ b/themis/program_ristretto/src/entrypoint.rs @@ -0,0 +1,18 @@ +//! Program entrypoint + +#![cfg(feature = "program")] +#![cfg(not(feature = "no-entrypoint"))] + +use solana_sdk::{ + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, +}; + +entrypoint!(process_instruction); +fn process_instruction<'a>( + program_id: &Pubkey, + accounts: &'a [AccountInfo<'a>], + instruction_data: &[u8], +) -> ProgramResult { + crate::processor::process_instruction(program_id, accounts, instruction_data)?; + Ok(()) +} diff --git a/themis/program_ristretto/src/error.rs b/themis/program_ristretto/src/error.rs new file mode 100644 index 00000000000000..1215abe4d520f0 --- /dev/null +++ b/themis/program_ristretto/src/error.rs @@ -0,0 +1,41 @@ +//! Error types + +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use solana_sdk::program_error::PrintProgramError; +use solana_sdk::{decode_error::DecodeError, program_error::ProgramError}; +use thiserror::Error; + +/// Errors that may be returned by the Themis program. +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum ThemisError { + /// Invalid instruction + #[error("Invalid instruction")] + InvalidInstruction, + + /// Account already in use + #[error("Account in use")] + AccountInUse, +} +impl From for ProgramError { + fn from(e: ThemisError) -> Self { + ProgramError::Custom(e as u32) + } +} +impl DecodeError for ThemisError { + fn type_of() -> &'static str { + "ThemisError" + } +} + +impl PrintProgramError for ThemisError { + fn print(&self) + where + E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, + { + match self { + ThemisError::InvalidInstruction => println!("Error: Invalid instruction"), + ThemisError::AccountInUse => println!("Error: Account in use"), + } + } +} diff --git a/themis/program_ristretto/src/instruction.rs b/themis/program_ristretto/src/instruction.rs new file mode 100644 index 00000000000000..e20c8e09eb2b13 --- /dev/null +++ b/themis/program_ristretto/src/instruction.rs @@ -0,0 +1,235 @@ +//! Instruction types + +use crate::state::{Policies, User}; +use borsh::{BorshDeserialize, BorshSerialize}; +use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar}; +use elgamal_ristretto::public::PublicKey; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + system_instruction, +}; + +/// Instructions supported by the Themis program. +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] +pub enum ThemisInstruction { + /// Initialize a new user account + /// + /// The `InitializeUserAccount` instruction requires no signers and MUST be included within + /// the same Transaction as the system program's `CreateInstruction` that creates the account + /// being initialized. Otherwise another party can acquire ownership of the uninitialized account. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The account to initialize. + InitializeUserAccount { + /// Public key for all encrypted interations + public_key: PublicKey, + }, + + /// Initialize a new policies account + /// + /// The `InitializePoliciesAccount` instruction requires no signers and MUST be included within + /// the same Transaction as the system program's `CreateInstruction` that creates the account + /// being initialized. Otherwise another party can acquire ownership of the uninitialized account. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The account to initialize. + InitializePoliciesAccount { + /// Number of policies to be added + num_scalars: u8, + }, + + /// Store policies + /// + /// The `StorePolices` instruction is used to set individual policies. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable, signer]` The policies account. + StorePolicies { + /// Policies to be added + scalars: Vec<(u8, Scalar)>, + }, + + /// Calculate aggregate. The length of the `input` vector must equal the + /// number of policies. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable, signer]` The user account + /// 1. `[]` The policies account + SubmitInteractions { + /// Encrypted interactions + encrypted_interactions: Vec<(u8, (RistrettoPoint, RistrettoPoint))>, + }, + + /// Submit proof decryption + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable, signer]` The user account + SubmitProofDecryption { + /// plaintext + plaintext: RistrettoPoint, + + /// (announcement_g, announcement_ctx) + announcement: Box<(RistrettoPoint, RistrettoPoint)>, + + /// response + response: Scalar, + }, + + /// Request a payment + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable, signer]` The user account + RequestPayment { + /// Encrypted aggregate + encrypted_aggregate: Box<(RistrettoPoint, RistrettoPoint)>, + + /// Decrypted aggregate + decrypted_aggregate: RistrettoPoint, + + /// Proof correct decryption + proof_correct_decryption: RistrettoPoint, + }, +} + +impl ThemisInstruction { + pub fn serialize(&self) -> Result, ProgramError> { + self.try_to_vec() + .map_err(|_| ProgramError::AccountDataTooSmall) + } + + pub(crate) fn deserialize(data: &[u8]) -> Result { + Self::try_from_slice(&data).map_err(|_| ProgramError::InvalidInstructionData) + } +} + +/// Return an `InitializeUserAccount` instruction. +fn initialize_user_account(user_pubkey: &Pubkey, public_key: PublicKey) -> Instruction { + let data = ThemisInstruction::InitializeUserAccount { public_key }; + + let accounts = vec![AccountMeta::new(*user_pubkey, false)]; + + Instruction { + program_id: crate::id(), + accounts, + data: data.serialize().unwrap(), + } +} + +/// Return two instructions that create and initialize a user account. +pub fn create_user_account( + from: &Pubkey, + user_pubkey: &Pubkey, + lamports: u64, + public_key: PublicKey, +) -> Vec { + let space = User::default().try_to_vec().unwrap().len() as u64; + vec![ + system_instruction::create_account(from, user_pubkey, lamports, space, &crate::id()), + initialize_user_account(user_pubkey, public_key), + ] +} + +/// Return an `InitializePoliciesAccount` instruction. +fn initialize_policies_account(policies_pubkey: &Pubkey, num_scalars: u8) -> Instruction { + let data = ThemisInstruction::InitializePoliciesAccount { num_scalars }; + let accounts = vec![AccountMeta::new(*policies_pubkey, false)]; + Instruction { + program_id: crate::id(), + accounts, + data: data.serialize().unwrap(), + } +} + +/// Return two instructions that create and initialize a policies account. +pub fn create_policies_account( + from: &Pubkey, + policies_pubkey: &Pubkey, + lamports: u64, + num_scalars: u8, +) -> Vec { + let space = Policies::new(num_scalars).try_to_vec().unwrap().len() as u64; + vec![ + system_instruction::create_account(from, policies_pubkey, lamports, space, &crate::id()), + initialize_policies_account(policies_pubkey, num_scalars), + ] +} + +/// Return an `InitializePoliciesAccount` instruction. +pub fn store_policies(policies_pubkey: &Pubkey, scalars: Vec<(u8, Scalar)>) -> Instruction { + let data = ThemisInstruction::StorePolicies { scalars }; + let accounts = vec![AccountMeta::new(*policies_pubkey, true)]; + Instruction { + program_id: crate::id(), + accounts, + data: data.serialize().unwrap(), + } +} + +/// Return a `SubmitInteractions` instruction. +pub fn submit_interactions( + user_pubkey: &Pubkey, + policies_pubkey: &Pubkey, + encrypted_interactions: Vec<(u8, (RistrettoPoint, RistrettoPoint))>, +) -> Instruction { + let data = ThemisInstruction::SubmitInteractions { + encrypted_interactions, + }; + let accounts = vec![ + AccountMeta::new(*user_pubkey, true), + AccountMeta::new_readonly(*policies_pubkey, false), + ]; + Instruction { + program_id: crate::id(), + accounts, + data: data.serialize().unwrap(), + } +} + +/// Return a `SubmitProofDecryption` instruction. +pub fn submit_proof_decryption( + user_pubkey: &Pubkey, + plaintext: RistrettoPoint, + announcement_g: RistrettoPoint, + announcement_ctx: RistrettoPoint, + response: Scalar, +) -> Instruction { + let data = ThemisInstruction::SubmitProofDecryption { + plaintext, + announcement: Box::new((announcement_g, announcement_ctx)), + response, + }; + let accounts = vec![AccountMeta::new(*user_pubkey, true)]; + Instruction { + program_id: crate::id(), + accounts, + data: data.serialize().unwrap(), + } +} + +/// Return a `RequestPayment` instruction. +pub fn request_payment( + user_pubkey: &Pubkey, + encrypted_aggregate: (RistrettoPoint, RistrettoPoint), + decrypted_aggregate: RistrettoPoint, + proof_correct_decryption: RistrettoPoint, +) -> Instruction { + let data = ThemisInstruction::RequestPayment { + encrypted_aggregate: Box::new(encrypted_aggregate), + decrypted_aggregate, + proof_correct_decryption, + }; + let accounts = vec![AccountMeta::new(*user_pubkey, true)]; + Instruction { + program_id: crate::id(), + accounts, + data: data.serialize().unwrap(), + } +} diff --git a/themis/program_ristretto/src/lib.rs b/themis/program_ristretto/src/lib.rs new file mode 100644 index 00000000000000..1ff45c0e01a0bd --- /dev/null +++ b/themis/program_ristretto/src/lib.rs @@ -0,0 +1,14 @@ +//! An implementation of Brave's THEMIS for the Solana blockchain +#![forbid(unsafe_code)] + +pub mod entrypoint; +pub mod error; +pub mod instruction; +pub mod processor; +pub mod state; + +// Export current solana-sdk types for downstream users who may also be building with a different +// solana-sdk version +pub use solana_sdk; + +solana_sdk::declare_id!("FHpg4MzrRSmTjNhPmEMbYhhe7sjrUFefe2hJg2m7fGvP"); diff --git a/themis/program_ristretto/src/processor.rs b/themis/program_ristretto/src/processor.rs new file mode 100644 index 00000000000000..cd778e71c567a0 --- /dev/null +++ b/themis/program_ristretto/src/processor.rs @@ -0,0 +1,141 @@ +//! Themis program +use crate::{ + error::ThemisError, + instruction::ThemisInstruction, + state::{Policies, User}, +}; +use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar}; +use elgamal_ristretto::public::PublicKey; +use solana_sdk::{ + account_info::{next_account_info, AccountInfo}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +fn process_initialize_user_account( + user_info: &AccountInfo, + public_key: PublicKey, +) -> Result<(), ProgramError> { + // TODO: verify the program ID + if let Ok(user) = User::deserialize(&user_info.data.borrow()) { + if user.is_initialized { + return Err(ThemisError::AccountInUse.into()); + } + } + let user = User::new(public_key); + user.serialize(&mut user_info.data.borrow_mut()) +} + +fn process_initialize_policies_account( + num_scalars: u8, + policies_info: &AccountInfo, +) -> Result<(), ProgramError> { + if let Ok(policies) = Policies::deserialize(&policies_info.data.borrow()) { + if policies.is_initialized { + return Err(ThemisError::AccountInUse.into()); + } + } + let policies = Policies::new(num_scalars); + policies.serialize(&mut policies_info.data.borrow_mut()) +} + +fn process_store_policies( + scalars: Vec<(u8, Scalar)>, + policies_info: &AccountInfo, +) -> Result<(), ProgramError> { + let mut policies = Policies::deserialize(&policies_info.data.borrow())?; + for (i, scalar) in scalars { + policies.scalars[i as usize] = scalar; + } + policies.serialize(&mut policies_info.data.borrow_mut()) +} + +fn process_submit_interactions( + encrypted_interactions: &[(u8, (RistrettoPoint, RistrettoPoint))], + user_info: &AccountInfo, + policies_info: &AccountInfo, +) -> Result<(), ProgramError> { + let mut user = User::deserialize(&user_info.data.borrow())?; + let policies = Policies::deserialize(&policies_info.data.borrow())?; + user.submit_interactions(encrypted_interactions, &policies.scalars); + user.serialize(&mut user_info.data.borrow_mut()) +} + +fn process_submit_proof_decryption( + plaintext: RistrettoPoint, + announcement: (RistrettoPoint, RistrettoPoint), + response: Scalar, + user_info: &AccountInfo, +) -> Result<(), ProgramError> { + let mut user = User::deserialize(&user_info.data.borrow())?; + user.submit_proof_decryption(plaintext, announcement.0, announcement.1, response); + user.serialize(&mut user_info.data.borrow_mut()) +} + +fn process_request_payment( + encrypted_aggregate: (RistrettoPoint, RistrettoPoint), + decrypted_aggregate: RistrettoPoint, + proof_correct_decryption: RistrettoPoint, + user_info: &AccountInfo, +) -> Result<(), ProgramError> { + let mut user = User::deserialize(&user_info.data.borrow())?; + user.request_payment( + encrypted_aggregate, + decrypted_aggregate, + proof_correct_decryption, + ); + user.serialize(&mut user_info.data.borrow_mut()) +} + +/// Process the given transaction instruction +pub fn process_instruction<'a>( + _program_id: &Pubkey, + account_infos: &'a [AccountInfo<'a>], + input: &[u8], +) -> Result<(), ProgramError> { + let account_infos_iter = &mut account_infos.iter(); + let instruction = ThemisInstruction::deserialize(input)?; + + match instruction { + ThemisInstruction::InitializeUserAccount { public_key } => { + let user_info = next_account_info(account_infos_iter)?; + process_initialize_user_account(&user_info, public_key) + } + ThemisInstruction::InitializePoliciesAccount { num_scalars } => { + let policies_info = next_account_info(account_infos_iter)?; + process_initialize_policies_account(num_scalars, &policies_info) + } + ThemisInstruction::StorePolicies { scalars } => { + let policies_info = next_account_info(account_infos_iter)?; + process_store_policies(scalars, &policies_info) + } + ThemisInstruction::SubmitInteractions { + encrypted_interactions, + } => { + let user_info = next_account_info(account_infos_iter)?; + let policies_info = next_account_info(account_infos_iter)?; + process_submit_interactions(&encrypted_interactions, &user_info, &policies_info) + } + ThemisInstruction::SubmitProofDecryption { + plaintext, + announcement, + response, + } => { + let user_info = next_account_info(account_infos_iter)?; + process_submit_proof_decryption(plaintext, *announcement, response, &user_info) + } + ThemisInstruction::RequestPayment { + encrypted_aggregate, + decrypted_aggregate, + proof_correct_decryption, + } => { + let user_info = next_account_info(account_infos_iter)?; + process_request_payment( + *encrypted_aggregate, + decrypted_aggregate, + proof_correct_decryption, + &user_info, + ) + } + } +} diff --git a/themis/program_ristretto/src/state.rs b/themis/program_ristretto/src/state.rs new file mode 100644 index 00000000000000..67806772325884 --- /dev/null +++ b/themis/program_ristretto/src/state.rs @@ -0,0 +1,348 @@ +#![allow(missing_docs)] + +use borsh::{BorshDeserialize, BorshSerialize}; +use curve25519_dalek::{ + constants::RISTRETTO_BASEPOINT_POINT, ristretto::RistrettoPoint, scalar::Scalar, + traits::Identity, +}; +use elgamal_ristretto::{ciphertext::Ciphertext, private::SecretKey, public::PublicKey}; +use rand::thread_rng; +use solana_sdk::program_error::ProgramError; + +type Points = (RistrettoPoint, RistrettoPoint); + +#[derive(Default, BorshSerialize, BorshDeserialize)] +pub struct Policies { + pub is_initialized: bool, + pub num_scalars: u8, + pub scalars: Vec, +} + +impl Policies { + pub fn serialize(&self, mut data: &mut [u8]) -> Result<(), ProgramError> { + BorshSerialize::serialize(self, &mut data).map_err(|_| ProgramError::AccountDataTooSmall) + } + + pub fn deserialize(data: &[u8]) -> Result { + Self::try_from_slice(&data).map_err(|_| ProgramError::InvalidAccountData) + } + + pub fn new(num_scalars: u8) -> Self { + Self { + is_initialized: true, + num_scalars, + scalars: vec![Scalar::zero(); num_scalars as usize], + } + } + + /// Useful for testing + pub fn new_with_scalars(scalars: Vec) -> Self { + let mut policies = Self::new(scalars.len() as u8); + policies.scalars = scalars; + policies + } +} + +#[derive(BorshSerialize, BorshDeserialize)] +pub struct PaymentRequest { + pub encrypted_aggregate: Points, + pub decrypted_aggregate: RistrettoPoint, + pub proof_correct_decryption: RistrettoPoint, + pub valid: bool, +} + +impl PaymentRequest { + fn new( + encrypted_aggregate: Points, + decrypted_aggregate: RistrettoPoint, + proof_correct_decryption: RistrettoPoint, + valid: bool, + ) -> Self { + Self { + encrypted_aggregate, + decrypted_aggregate, + proof_correct_decryption, + valid, + } + } +} + +fn inner_product( + (mut aggregate_x, mut aggregate_y): Points, + ciphertexts: &[(u8, Points)], + scalars: &[Scalar], +) -> Points { + for &(i, (x, y)) in ciphertexts { + aggregate_x = x * scalars[i as usize] + aggregate_x; + aggregate_y = y * scalars[i as usize] + aggregate_y; + } + + (aggregate_x, aggregate_y) +} + +#[derive(BorshSerialize, BorshDeserialize)] +pub struct User { + encrypted_aggregate: Points, + public_key: PublicKey, + pub is_initialized: bool, + proof_verification: bool, + payment_requests: Vec, +} + +impl Default for User { + fn default() -> Self { + Self { + encrypted_aggregate: (RistrettoPoint::identity(), RistrettoPoint::identity()), + public_key: PublicKey::from(RistrettoPoint::identity()), + is_initialized: false, + proof_verification: false, + payment_requests: vec![], + } + } +} + +impl User { + pub fn serialize(&self, mut data: &mut [u8]) -> Result<(), ProgramError> { + BorshSerialize::serialize(self, &mut data).map_err(|_| ProgramError::AccountDataTooSmall) + } + + pub fn deserialize(data: &[u8]) -> Result { + Self::try_from_slice(&data).map_err(|_| ProgramError::InvalidAccountData) + } + + pub fn new(public_key: PublicKey) -> Self { + Self { + public_key, + ..Self::default() + } + } + + pub fn fetch_encrypted_aggregate(&self) -> Points { + self.encrypted_aggregate + } + + pub fn fetch_public_key(&self) -> PublicKey { + self.public_key + } + + pub fn fetch_proof_verification(&self) -> bool { + self.proof_verification + } + + pub fn submit_interactions( + &mut self, + interactions: &[(u8, Points)], + policies: &[Scalar], + ) -> bool { + self.encrypted_aggregate = inner_product(self.encrypted_aggregate, interactions, &policies); + true + } + + pub fn submit_proof_decryption( + &mut self, + plaintext: RistrettoPoint, + announcement_g: RistrettoPoint, + announcement_ctx: RistrettoPoint, + response: Scalar, + ) -> bool { + let client_pk = self.fetch_public_key(); + let ciphertext = Ciphertext { + points: self.fetch_encrypted_aggregate(), + pk: client_pk, + }; + self.proof_verification = client_pk.verify_correct_decryption_no_Merlin( + &((announcement_g, announcement_ctx), response), + &ciphertext, + &plaintext, + ); + true + } + + pub fn request_payment( + &mut self, + encrypted_aggregate: Points, + decrypted_aggregate: RistrettoPoint, + proof_correct_decryption: RistrettoPoint, + ) -> bool { + // TODO: implement proof verification + let proof_is_valid = true; + let payment_request = PaymentRequest::new( + encrypted_aggregate, + decrypted_aggregate, + proof_correct_decryption, + proof_is_valid, + ); + self.payment_requests.push(payment_request); + proof_is_valid + } +} + +pub fn generate_keys() -> (SecretKey, PublicKey) { + let mut csprng = thread_rng(); + let sk = SecretKey::new(&mut csprng); + let pk = PublicKey::from(&sk); + (sk, pk) +} + +pub fn recover_scalar(point: RistrettoPoint, k: u32) -> Scalar { + for i in 0..2u64.pow(k) { + let scalar = i.into(); + if RISTRETTO_BASEPOINT_POINT * scalar == point { + return scalar; + } + } + panic!("Encrypted scalar too long"); +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + fn test_policy_contract(policies: &[Scalar], expected_scalar_aggregate: Scalar) { + let (sk, pk) = generate_keys(); + let interactions: Vec<_> = (0..policies.len()) + .map(|i| (i as u8, pk.encrypt(&RISTRETTO_BASEPOINT_POINT).points)) + .collect(); + let mut user = User::new(pk); + + let tx_receipt = user.submit_interactions(&interactions, policies); + assert!(tx_receipt); + + let encrypted_point = user.fetch_encrypted_aggregate(); + let ciphertext = Ciphertext { + points: encrypted_point, + pk, + }; + + let decrypted_aggregate = sk.decrypt(&ciphertext); + let scalar_aggregate = recover_scalar(decrypted_aggregate, 16); + assert_eq!(scalar_aggregate, expected_scalar_aggregate); + + let ((announcement_g, announcement_ctx), response) = + sk.prove_correct_decryption_no_Merlin(&ciphertext, &decrypted_aggregate); + + let tx_receipt_proof = user.submit_proof_decryption( + decrypted_aggregate, + announcement_g, + announcement_ctx, + response, + ); + assert!(tx_receipt_proof); + + let proof_result = user.fetch_proof_verification(); + assert!(proof_result); + } + + #[test] + fn test_policy_contract_2ads() { + let policies = vec![1u64.into(), 2u64.into()]; + test_policy_contract(&policies, 3u64.into()); + } + + #[test] + fn test_policy_contract_128ads() { + let policies = vec![ + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), //10 + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), // 2 * 10 + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), + 1u64.into(), //10 + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), + 2u64.into(), // 2 * 10 + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + 0u64.into(), + ]; + test_policy_contract(&policies, 60u64.into()); + } +}