From 36a1534c7ea6ad5458e58e7ee6e1433fd0d759b3 Mon Sep 17 00:00:00 2001 From: ananas-block Date: Thu, 13 Jun 2024 01:18:02 +0100 Subject: [PATCH 1/4] test_deposit_delegate mint to works refactor: allow delegate mix its own undelegated accounts with delegated accounts deposit functional tests work claiming works, delegation doesnt work undelegate works with 1 delegate --- Cargo.lock | 33 +- ...oy5p9uQUpPAtFoJHwcYhts2Xu2ArWNJUgYyji.json | 1 + ...pnXzSKgGpEGHuxMRFtTz6Gt6hozVqu8e5fiqt.json | 1 + ...MugpKq9XCGzx81UtTBuytByW8arm9EaBVpD5k.json | 2 +- ...z75wSdqy9bpeWacqLWrqAwWBfqh4iSYtejiwK.json | 1 - ...Lffn32a2EnsmvFFmGK4ZZgqYrDkAxpGXc1ogj.json | 1 + .../src/escrow_with_compressed_pda/sdk.rs | 12 +- .../token-escrow/src/escrow_with_pda/sdk.rs | 10 +- .../programs/token-escrow/tests/test.rs | 38 +- .../token-escrow/tests/test_compressed_pda.rs | 23 +- forester/src/nullifier/address/processor.rs | 11 +- forester/src/nullifier/state/processor.rs | 27 +- forester/src/rollover/operations.rs | 21 +- forester/tests/interop_address_test.rs | 2 + forester/tests/interop_nullify_test.rs | 2 + forester/tests/rollover_test.rs | 9 +- forester/tests/test_utils.rs | 2 + .../src/idl/light_compressed_token.ts | 96 +- .../src/idls/light_compressed_token.ts | 96 +- js/stateless.js/src/idls/light_registry.ts | 1010 ++++++++++++-- .../src/instructions/register_program.rs | 2 +- programs/compressed-token/src/burn.rs | 6 +- programs/compressed-token/src/delegation.rs | 10 +- programs/compressed-token/src/freeze.rs | 6 +- .../compressed-token/src/process_transfer.rs | 171 ++- programs/registry/Cargo.toml | 11 +- .../account_compression_cpi/access_control.rs | 0 .../initialize_tree_and_queue.rs | 91 ++ .../src/account_compression_cpi/mod.rs | 6 + .../src/account_compression_cpi/nullify.rs | 64 + .../register_program.rs | 27 + .../rollover_state_tree.rs | 89 ++ .../src/account_compression_cpi/sdk.rs | 242 ++++ .../update_address_tree.rs | 73 + .../src/decentralization_and_contention.rs | 284 ++++ .../src/delegate/delegate_instruction.rs | 87 ++ programs/registry/src/delegate/deposit.rs | 1222 +++++++++++++++++ .../src/delegate/deposit_instruction.rs | 121 ++ programs/registry/src/delegate/mod.rs | 24 + programs/registry/src/delegate/process_cpi.rs | 286 ++++ .../registry/src/delegate/process_delegate.rs | 532 +++++++ programs/registry/src/delegate/state.rs | 181 +++ programs/registry/src/delegate/traits.rs | 37 + programs/registry/src/epoch/claim_forester.rs | 432 ++++++ .../src/epoch/claim_forester_instruction.rs | 132 ++ .../src/epoch/finalize_registration.rs | 14 + programs/registry/src/epoch/mod.rs | 7 + programs/registry/src/epoch/register_epoch.rs | 509 +++++++ programs/registry/src/epoch/report_work.rs | 51 + programs/registry/src/epoch/sync_delegate.rs | 1161 ++++++++++++++++ .../src/epoch/sync_delegate_instruction.rs | 133 ++ programs/registry/src/errors.rs | 31 + programs/registry/src/forester.rs | 57 - programs/registry/src/forester/mod.rs | 1 + programs/registry/src/forester/state.rs | 98 ++ programs/registry/src/lib.rs | 676 +++++---- .../src/protocol_config/initialize.rs | 26 + programs/registry/src/protocol_config/mint.rs | 125 ++ programs/registry/src/protocol_config/mod.rs | 4 + .../registry/src/protocol_config/state.rs | 351 +++++ .../registry/src/protocol_config/update.rs | 17 + programs/registry/src/sdk.rs | 851 +++++++++--- programs/registry/src/utils.rs | 48 + .../src/invoke_cpi/process_cpi_context.rs | 3 + .../tests/address_merkle_tree_tests.rs | 63 +- .../compressed-token-test/tests/test.rs | 143 +- test-programs/e2e-test/Cargo.toml | 1 + test-programs/e2e-test/tests/test.rs | 69 +- test-programs/registry-test/Cargo.toml | 2 + test-programs/registry-test/tests/tests.rs | 1208 +++++++++++++++- test-programs/system-cpi-test/src/sdk.rs | 2 +- test-programs/system-cpi-test/tests/test.rs | 9 +- .../tests/test_program_owned_trees.rs | 21 +- test-programs/system-test/tests/test.rs | 6 +- test-utils/src/address_tree_rollover.rs | 5 + test-utils/src/assert_epoch.rs | 146 ++ test-utils/src/assert_token_tx.rs | 3 +- test-utils/src/e2e_test_env.rs | 446 +++++- test-utils/src/forester_epoch.rs | 507 +++++++ test-utils/src/indexer/test_indexer.rs | 6 +- test-utils/src/lib.rs | 45 + test-utils/src/registry.rs | 1042 +++++++++++++- test-utils/src/rpc/rpc_connection.rs | 10 + test-utils/src/rpc/solana_rpc.rs | 12 + test-utils/src/rpc/test_rpc.rs | 131 ++ test-utils/src/spl.rs | 75 +- test-utils/src/test_env.rs | 459 ++++++- test-utils/src/test_forester.rs | 109 +- 88 files changed, 12945 insertions(+), 1272 deletions(-) create mode 100644 cli/accounts/epoch_pda_C388ZXSoy5p9uQUpPAtFoJHwcYhts2Xu2ArWNJUgYyji.json create mode 100644 cli/accounts/forester_epoch_pda_5Tpw5vUpnXzSKgGpEGHuxMRFtTz6Gt6hozVqu8e5fiqt.json delete mode 100644 cli/accounts/registered_forester_epoch_pda_DFiGEbaz75wSdqy9bpeWacqLWrqAwWBfqh4iSYtejiwK.json create mode 100644 cli/accounts/registered_forester_pda_BqDEaVeLffn32a2EnsmvFFmGK4ZZgqYrDkAxpGXc1ogj.json create mode 100644 programs/registry/src/account_compression_cpi/access_control.rs create mode 100644 programs/registry/src/account_compression_cpi/initialize_tree_and_queue.rs create mode 100644 programs/registry/src/account_compression_cpi/mod.rs create mode 100644 programs/registry/src/account_compression_cpi/nullify.rs create mode 100644 programs/registry/src/account_compression_cpi/register_program.rs create mode 100644 programs/registry/src/account_compression_cpi/rollover_state_tree.rs create mode 100644 programs/registry/src/account_compression_cpi/sdk.rs create mode 100644 programs/registry/src/account_compression_cpi/update_address_tree.rs create mode 100644 programs/registry/src/decentralization_and_contention.rs create mode 100644 programs/registry/src/delegate/delegate_instruction.rs create mode 100644 programs/registry/src/delegate/deposit.rs create mode 100644 programs/registry/src/delegate/deposit_instruction.rs create mode 100644 programs/registry/src/delegate/mod.rs create mode 100644 programs/registry/src/delegate/process_cpi.rs create mode 100644 programs/registry/src/delegate/process_delegate.rs create mode 100644 programs/registry/src/delegate/state.rs create mode 100644 programs/registry/src/delegate/traits.rs create mode 100644 programs/registry/src/epoch/claim_forester.rs create mode 100644 programs/registry/src/epoch/claim_forester_instruction.rs create mode 100644 programs/registry/src/epoch/finalize_registration.rs create mode 100644 programs/registry/src/epoch/mod.rs create mode 100644 programs/registry/src/epoch/register_epoch.rs create mode 100644 programs/registry/src/epoch/report_work.rs create mode 100644 programs/registry/src/epoch/sync_delegate.rs create mode 100644 programs/registry/src/epoch/sync_delegate_instruction.rs create mode 100644 programs/registry/src/errors.rs delete mode 100644 programs/registry/src/forester.rs create mode 100644 programs/registry/src/forester/mod.rs create mode 100644 programs/registry/src/forester/state.rs create mode 100644 programs/registry/src/protocol_config/initialize.rs create mode 100644 programs/registry/src/protocol_config/mint.rs create mode 100644 programs/registry/src/protocol_config/mod.rs create mode 100644 programs/registry/src/protocol_config/state.rs create mode 100644 programs/registry/src/protocol_config/update.rs create mode 100644 programs/registry/src/utils.rs create mode 100644 test-utils/src/assert_epoch.rs create mode 100644 test-utils/src/forester_epoch.rs diff --git a/Cargo.lock b/Cargo.lock index e1ee92ce57..e9b4b0bd13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -899,11 +899,11 @@ dependencies = [ [[package]] name = "borsh" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" dependencies = [ - "borsh-derive 1.4.0", + "borsh-derive 1.5.1", "cfg_aliases", ] @@ -935,9 +935,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", "proc-macro-crate 3.1.0", @@ -1130,9 +1130,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" @@ -1790,6 +1790,7 @@ dependencies = [ "light-indexed-merkle-tree", "light-merkle-tree-reference", "light-prover-client", + "light-registry", "light-system-program", "light-test-utils", "light-utils", @@ -3096,11 +3097,18 @@ dependencies = [ "account-compression", "aligned-sized", "anchor-lang", + "anchor-spl", "bytemuck", + "light-compressed-token", "light-hasher", "light-heap", "light-macros", + "light-system-program", + "light-utils", "log", + "num-bigint 0.4.6", + "num-traits", + "rand 0.8.5", "solana-program-test", "solana-sdk", "tokio", @@ -3600,9 +3608,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -4408,6 +4416,7 @@ dependencies = [ "light-verifier", "num-bigint 0.4.6", "num-traits", + "rand 0.8.5", "reqwest 0.11.26", "serde_json", "solana-cli-output", @@ -5204,7 +5213,7 @@ name = "solana-banks-client" version = "1.18.11" source = "git+https://github.com/lightprotocol/agave?branch=v1.18.11-enforce-cpi-tracking#a24d3c07e25de696b4922ddde3e69877e8e9fa27" dependencies = [ - "borsh 1.4.0", + "borsh 1.5.1", "futures", "solana-banks-interface", "solana-program", @@ -5584,7 +5593,7 @@ dependencies = [ "blake3", "borsh 0.10.3", "borsh 0.9.3", - "borsh 1.4.0", + "borsh 1.5.1", "bs58 0.4.0", "bv", "bytemuck", @@ -5899,7 +5908,7 @@ dependencies = [ "base64 0.21.7", "bincode", "bitflags 2.5.0", - "borsh 1.4.0", + "borsh 1.5.1", "bs58 0.4.0", "bytemuck", "byteorder", diff --git a/cli/accounts/epoch_pda_C388ZXSoy5p9uQUpPAtFoJHwcYhts2Xu2ArWNJUgYyji.json b/cli/accounts/epoch_pda_C388ZXSoy5p9uQUpPAtFoJHwcYhts2Xu2ArWNJUgYyji.json new file mode 100644 index 0000000000..e8bc38c845 --- /dev/null +++ b/cli/accounts/epoch_pda_C388ZXSoy5p9uQUpPAtFoJHwcYhts2Xu2ArWNJUgYyji.json @@ -0,0 +1 @@ +{"pubkey":"C388ZXSoy5p9uQUpPAtFoJHwcYhts2Xu2ArWNJUgYyji","account":{"lamports":1781760,"data":["QuAuAqeJeGsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMqaOwAAAAACAAAAAAAAAADKmjsAAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA=","base64"],"owner":"7Z9Yuy3HkBCc2Wf3xzMGnz6qpV4n7ciwcoEMGKqhAnj1","executable":false,"rentEpoch":18446744073709551615,"space":128}} \ No newline at end of file diff --git a/cli/accounts/forester_epoch_pda_5Tpw5vUpnXzSKgGpEGHuxMRFtTz6Gt6hozVqu8e5fiqt.json b/cli/accounts/forester_epoch_pda_5Tpw5vUpnXzSKgGpEGHuxMRFtTz6Gt6hozVqu8e5fiqt.json new file mode 100644 index 0000000000..9e0febb636 --- /dev/null +++ b/cli/accounts/forester_epoch_pda_5Tpw5vUpnXzSKgGpEGHuxMRFtTz6Gt6hozVqu8e5fiqt.json @@ -0,0 +1 @@ +{"pubkey":"5Tpw5vUpnXzSKgGpEGHuxMRFtTz6Gt6hozVqu8e5fiqt","account":{"lamports":2345520,"data":["HXXTjWOP+nLMuMIVdedaPnWzorVHJCIvMcPXWnMDRUrSS6K/PzOqzAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADKmjsAAAAAAgAAAAAAAAAAypo7AAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAA=","base64"],"owner":"7Z9Yuy3HkBCc2Wf3xzMGnz6qpV4n7ciwcoEMGKqhAnj1","executable":false,"rentEpoch":18446744073709551615,"space":209}} \ No newline at end of file diff --git a/cli/accounts/governance_authority_pda_8KEKiyAMugpKq9XCGzx81UtTBuytByW8arm9EaBVpD5k.json b/cli/accounts/governance_authority_pda_8KEKiyAMugpKq9XCGzx81UtTBuytByW8arm9EaBVpD5k.json index 54df3c4124..38f2cf6648 100644 --- a/cli/accounts/governance_authority_pda_8KEKiyAMugpKq9XCGzx81UtTBuytByW8arm9EaBVpD5k.json +++ b/cli/accounts/governance_authority_pda_8KEKiyAMugpKq9XCGzx81UtTBuytByW8arm9EaBVpD5k.json @@ -1 +1 @@ -{"pubkey":"8KEKiyAMugpKq9XCGzx81UtTBuytByW8arm9EaBVpD5k","account":{"lamports":1670400,"data":["92V2ansKL5ECY+L7WEJcIRnY07lwy9TuaZBIebD9aqhznpq8Pv+mUf0AAAAAAAAAAP//////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==","base64"],"owner":"7Z9Yuy3HkBCc2Wf3xzMGnz6qpV4n7ciwcoEMGKqhAnj1","executable":false,"rentEpoch":18446744073709551615,"space":112}} \ No newline at end of file +{"pubkey":"8KEKiyAMugpKq9XCGzx81UtTBuytByW8arm9EaBVpD5k","account":{"lamports":1844400,"data":["YLDvkgH+Y5ICY+L7WEJcIRnY07lwy9TuaZBIebD9aqhznpq8Pv+mUf0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADKmjsAAAAAAgAAAAAAAAAAypo7AAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","base64"],"owner":"7Z9Yuy3HkBCc2Wf3xzMGnz6qpV4n7ciwcoEMGKqhAnj1","executable":false,"rentEpoch":18446744073709551615,"space":137}} \ No newline at end of file diff --git a/cli/accounts/registered_forester_epoch_pda_DFiGEbaz75wSdqy9bpeWacqLWrqAwWBfqh4iSYtejiwK.json b/cli/accounts/registered_forester_epoch_pda_DFiGEbaz75wSdqy9bpeWacqLWrqAwWBfqh4iSYtejiwK.json deleted file mode 100644 index c631a3db01..0000000000 --- a/cli/accounts/registered_forester_epoch_pda_DFiGEbaz75wSdqy9bpeWacqLWrqAwWBfqh4iSYtejiwK.json +++ /dev/null @@ -1 +0,0 @@ -{"pubkey":"DFiGEbaz75wSdqy9bpeWacqLWrqAwWBfqh4iSYtejiwK","account":{"lamports":1336320,"data":["cYUIcLQlc8/MuMIVdedaPnWzorVHJCIvMcPXWnMDRUrSS6K/PzOqzAAAAAAAAAAAAAAAAAAAAAD//////////w==","base64"],"owner":"7Z9Yuy3HkBCc2Wf3xzMGnz6qpV4n7ciwcoEMGKqhAnj1","executable":false,"rentEpoch":18446744073709551615,"space":64}} \ No newline at end of file diff --git a/cli/accounts/registered_forester_pda_BqDEaVeLffn32a2EnsmvFFmGK4ZZgqYrDkAxpGXc1ogj.json b/cli/accounts/registered_forester_pda_BqDEaVeLffn32a2EnsmvFFmGK4ZZgqYrDkAxpGXc1ogj.json new file mode 100644 index 0000000000..3e416707e4 --- /dev/null +++ b/cli/accounts/registered_forester_pda_BqDEaVeLffn32a2EnsmvFFmGK4ZZgqYrDkAxpGXc1ogj.json @@ -0,0 +1 @@ +{"pubkey":"BqDEaVeLffn32a2EnsmvFFmGK4ZZgqYrDkAxpGXc1ogj","account":{"lamports":1670400,"data":["BesVNs5W82rMuMIVdedaPnWzorVHJCIvMcPXWnMDRUrSS6K/PzOqzAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==","base64"],"owner":"7Z9Yuy3HkBCc2Wf3xzMGnz6qpV4n7ciwcoEMGKqhAnj1","executable":false,"rentEpoch":18446744073709551615,"space":112}} \ No newline at end of file diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/sdk.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/sdk.rs index efa9532b6d..362f7f1142 100644 --- a/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/sdk.rs +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/sdk.rs @@ -11,7 +11,9 @@ use light_system_program::{ invoke::processor::CompressedProof, sdk::{ address::{add_and_get_remaining_account_indices, pack_new_address_params}, - compressed_account::{pack_merkle_context, CompressedAccount, MerkleContext}, + compressed_account::{ + pack_merkle_context, CompressedAccountWithMerkleContext, MerkleContext, + }, CompressedCpiContext, }, NewAddressParams, @@ -22,13 +24,12 @@ use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; pub struct CreateCompressedPdaEscrowInstructionInputs<'a> { pub lock_up_time: u64, pub signer: &'a Pubkey, - pub input_merkle_context: &'a [MerkleContext], pub output_compressed_account_merkle_tree_pubkeys: &'a [Pubkey], pub output_compressed_accounts: &'a [TokenTransferOutputData], pub root_indices: &'a [u16], pub proof: &'a Option, pub input_token_data: &'a [light_compressed_token::token_data::TokenData], - pub input_compressed_accounts: &'a [CompressedAccount], + pub input_compressed_accounts: &'a [CompressedAccountWithMerkleContext], pub mint: &'a Pubkey, pub new_address_params: NewAddressParams, pub cpi_context_account: &'a Pubkey, @@ -42,7 +43,6 @@ pub fn create_escrow_instruction( let (mut remaining_accounts, inputs) = create_inputs_and_remaining_accounts_checked( input_params.input_token_data, input_params.input_compressed_accounts, - input_params.input_merkle_context, None, input_params.output_compressed_accounts, input_params.root_indices, @@ -139,7 +139,7 @@ pub struct CreateCompressedPdaWithdrawalInstructionInputs<'a> { pub root_indices: &'a [u16], pub proof: &'a Option, pub input_token_data: &'a [light_compressed_token::token_data::TokenData], - pub input_compressed_accounts: &'a [CompressedAccount], + pub input_compressed_accounts: &'a [CompressedAccountWithMerkleContext], pub mint: &'a Pubkey, pub old_lock_up_time: u64, pub new_lock_up_time: u64, @@ -155,7 +155,7 @@ pub fn create_withdrawal_instruction( let (mut remaining_accounts, inputs) = create_inputs_and_remaining_accounts_checked( input_params.input_token_data, input_params.input_compressed_accounts, - &[input_params.input_token_escrow_merkle_context], + // &[input_params.input_token_escrow_merkle_context], None, input_params.output_compressed_accounts, input_params.root_indices, diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs index 5593ddc8c8..8aadd7c149 100644 --- a/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs @@ -15,7 +15,7 @@ use light_system_program::{ invoke::processor::CompressedProof, sdk::{ address::add_and_get_remaining_account_indices, - compressed_account::{CompressedAccount, MerkleContext}, + compressed_account::CompressedAccountWithMerkleContext, }, }; use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; @@ -26,13 +26,13 @@ use crate::escrow_with_compressed_pda::sdk::get_token_owner_pda; pub struct CreateEscrowInstructionInputs<'a> { pub lock_up_time: u64, pub signer: &'a Pubkey, - pub input_merkle_context: &'a [MerkleContext], + // pub input_merkle_context: &'a [MerkleContext], pub output_compressed_account_merkle_tree_pubkeys: &'a [Pubkey], pub output_compressed_accounts: &'a [TokenTransferOutputData], pub root_indices: &'a [u16], pub proof: &'a Option, pub input_token_data: &'a [light_compressed_token::token_data::TokenData], - pub input_compressed_accounts: &'a [CompressedAccount], + pub input_compressed_accounts: &'a [CompressedAccountWithMerkleContext], pub mint: &'a Pubkey, } @@ -50,7 +50,7 @@ pub fn create_escrow_instruction( let (mut remaining_accounts, inputs) = create_inputs_and_remaining_accounts_checked( input_params.input_token_data, input_params.input_compressed_accounts, - input_params.input_merkle_context, + // input_params.input_mesrkle_context, None, input_params.output_compressed_accounts, input_params.root_indices, @@ -122,7 +122,7 @@ pub fn create_withdrawal_escrow_instruction( let (mut remaining_accounts, inputs) = create_inputs_and_remaining_accounts( input_params.input_token_data, input_params.input_compressed_accounts, - input_params.input_merkle_context, + // input_params.input_merkle_context, None, input_params.output_compressed_accounts, input_params.root_indices, diff --git a/examples/token-escrow/programs/token-escrow/tests/test.rs b/examples/token-escrow/programs/token-escrow/tests/test.rs index fe8e243922..e71b7bf1e5 100644 --- a/examples/token-escrow/programs/token-escrow/tests/test.rs +++ b/examples/token-escrow/programs/token-escrow/tests/test.rs @@ -11,7 +11,7 @@ // release compressed tokens use light_hasher::Poseidon; -use light_system_program::sdk::{compressed_account::MerkleContext, event::PublicTransactionEvent}; +use light_system_program::sdk::event::PublicTransactionEvent; use light_test_utils::airdrop_lamports; use light_test_utils::indexer::{Indexer, TestIndexer}; use light_test_utils::spl::{create_mint_helper, mint_tokens_helper}; @@ -244,14 +244,14 @@ pub async fn perform_escrow( input_token_data: &[input_compressed_token_account_data.token_data], lock_up_time: *lock_up_time, signer: &payer_pubkey, - input_merkle_context: &[MerkleContext { - leaf_index: compressed_input_account_with_context - .merkle_context - .leaf_index, - merkle_tree_pubkey: env.merkle_tree_pubkey, - nullifier_queue_pubkey: env.nullifier_queue_pubkey, - queue_index: None, - }], + // input_merkle_context: &[MerkleContext { + // leaf_index: compressed_input_account_with_context + // .merkle_context + // .leaf_index, + // merkle_tree_pubkey: env.merkle_tree_pubkey, + // nullifier_queue_pubkey: env.nullifier_queue_pubkey, + // queue_index: None, + // }], output_compressed_account_merkle_tree_pubkeys: &[ env.merkle_tree_pubkey, env.merkle_tree_pubkey, @@ -260,7 +260,7 @@ pub async fn perform_escrow( root_indices: &rpc_result.root_indices, proof: &Some(rpc_result.proof), mint: &input_compressed_token_account_data.token_data.mint, - input_compressed_accounts: &[compressed_input_account_with_context.compressed_account], + input_compressed_accounts: &[compressed_input_account_with_context], }; create_escrow_instruction(create_ix_inputs, *escrow_amount) } @@ -401,14 +401,14 @@ pub async fn perform_withdrawal( input_token_data: &[escrow_token_data_with_context.token_data], lock_up_time: 0, signer: &payer_pubkey, - input_merkle_context: &[MerkleContext { - leaf_index: compressed_input_account_with_context - .merkle_context - .leaf_index, - merkle_tree_pubkey: env.merkle_tree_pubkey, - nullifier_queue_pubkey: env.nullifier_queue_pubkey, - queue_index: None, - }], + // input_merkle_context: &[MerkleContext { + // leaf_index: compressed_input_account_with_context + // .merkle_context + // .leaf_index, + // merkle_tree_pubkey: env.merkle_tree_pubkey, + // nullifier_queue_pubkey: env.nullifier_queue_pubkey, + // queue_index: None, + // }], output_compressed_account_merkle_tree_pubkeys: &[ env.merkle_tree_pubkey, env.merkle_tree_pubkey, @@ -417,7 +417,7 @@ pub async fn perform_withdrawal( root_indices: &rpc_result.root_indices, proof: &Some(rpc_result.proof), mint: &escrow_token_data_with_context.token_data.mint, - input_compressed_accounts: &[compressed_input_account_with_context.compressed_account], + input_compressed_accounts: &[compressed_input_account_with_context], }; create_withdrawal_escrow_instruction(create_ix_inputs, *withdrawal_amount) diff --git a/examples/token-escrow/programs/token-escrow/tests/test_compressed_pda.rs b/examples/token-escrow/programs/token-escrow/tests/test_compressed_pda.rs index fde4e7e88a..efe30e0878 100644 --- a/examples/token-escrow/programs/token-escrow/tests/test_compressed_pda.rs +++ b/examples/token-escrow/programs/token-escrow/tests/test_compressed_pda.rs @@ -108,8 +108,7 @@ async fn test_escrow_with_compressed_pda() { let rpc_error = RpcError::TransactionError(transaction_error); assert!(matches!(result, Err(error) if error.to_string() == rpc_error.to_string())); - rpc.warp_to_slot(lock_up_time + 1).unwrap(); - + rpc.warp_to_slot(lockup_end + 1).unwrap(); perform_withdrawal_with_event( &mut rpc, &mut test_indexer, @@ -251,14 +250,14 @@ async fn create_escrow_ix( input_token_data: &[input_compressed_token_account_data.token_data], lock_up_time, signer: &payer_pubkey, - input_merkle_context: &[MerkleContext { - leaf_index: compressed_input_account_with_context - .merkle_context - .leaf_index, - merkle_tree_pubkey: env.merkle_tree_pubkey, - nullifier_queue_pubkey: env.nullifier_queue_pubkey, - queue_index: None, - }], + // input_merkle_context: &[MerkleContext { + // leaf_index: compressed_input_account_with_context + // .merkle_context + // .leaf_index, + // merkle_tree_pubkey: env.merkle_tree_pubkey, + // nullifier_queue_pubkey: env.nullifier_queue_pubkey, + // queue_index: None, + // }], output_compressed_account_merkle_tree_pubkeys: &[ env.merkle_tree_pubkey, env.merkle_tree_pubkey, @@ -269,7 +268,7 @@ async fn create_escrow_ix( mint: &input_compressed_token_account_data.token_data.mint, new_address_params, cpi_context_account: &env.cpi_context_account_pubkey, - input_compressed_accounts: &[compressed_input_account_with_context.compressed_account], + input_compressed_accounts: &[compressed_input_account_with_context], }; let instruction = create_escrow_instruction(create_ix_inputs.clone(), escrow_amount); (payer_pubkey, instruction) @@ -482,7 +481,7 @@ pub async fn perform_withdrawal( old_lock_up_time, new_lock_up_time, address: compressed_escrow_pda.compressed_account.address.unwrap(), - input_compressed_accounts: &[compressed_escrow_pda.compressed_account], + input_compressed_accounts: &[compressed_escrow_pda], }; create_withdrawal_instruction(create_withdrawal_ix_inputs.clone(), escrow_amount) } diff --git a/forester/src/nullifier/address/processor.rs b/forester/src/nullifier/address/processor.rs index dd4fef8728..9d4c9ea225 100644 --- a/forester/src/nullifier/address/processor.rs +++ b/forester/src/nullifier/address/processor.rs @@ -8,7 +8,8 @@ use crate::{ForesterConfig, RpcPool}; use account_compression::utils::constants::{ ADDRESS_MERKLE_TREE_CHANGELOG, ADDRESS_MERKLE_TREE_INDEXED_CHANGELOG, }; -use light_registry::sdk::{ + +use light_registry::account_compression_cpi::sdk::{ create_update_address_merkle_tree_instruction, UpdateAddressMerkleTreeInstructionInputs, }; use light_test_utils::indexer::Indexer; @@ -268,8 +269,8 @@ pub async fn update_merkle_tree( ) -> Result { let start = Instant::now(); - let update_ix = - create_update_address_merkle_tree_instruction(UpdateAddressMerkleTreeInstructionInputs { + let update_ix = create_update_address_merkle_tree_instruction( + UpdateAddressMerkleTreeInstructionInputs { authority: config.payer_keypair.pubkey(), address_merkle_tree: tree_data.tree_pubkey, address_queue: tree_data.queue_pubkey, @@ -283,7 +284,9 @@ pub async fn update_merkle_tree( indexed_changelog_index: ((account_data.proof.root_seq - 1) % ADDRESS_MERKLE_TREE_INDEXED_CHANGELOG) as u16, - }); + }, + 0, // TODO: add correct epoch + ); // Prepare the instructions let instructions = vec![ diff --git a/forester/src/nullifier/state/processor.rs b/forester/src/nullifier/state/processor.rs index 23f375511d..0edbc78324 100644 --- a/forester/src/nullifier/state/processor.rs +++ b/forester/src/nullifier/state/processor.rs @@ -5,7 +5,9 @@ use crate::operations::fetch_state_queue_data; use crate::tree_sync::TreeData; use crate::{ForesterConfig, RpcPool}; use account_compression::utils::constants::STATE_MERKLE_TREE_CHANGELOG; -use light_registry::sdk::{create_nullify_instruction, CreateNullifyInstructionInputs}; +use light_registry::account_compression_cpi::sdk::{ + create_nullify_instruction, CreateNullifyInstructionInputs, +}; use light_test_utils::indexer::Indexer; use light_test_utils::rpc::rpc_connection::RpcConnection; use log::{debug, error, info, warn}; @@ -276,16 +278,19 @@ async fn nullify_state( let change_log_index = root_seq % STATE_MERKLE_TREE_CHANGELOG; debug!("change_log_index: {:?}", change_log_index); - let ix = create_nullify_instruction(CreateNullifyInstructionInputs { - nullifier_queue: tree_data.queue_pubkey, - merkle_tree: tree_data.tree_pubkey, - change_log_indices: vec![change_log_index], - leaves_queue_indices: vec![leaves_queue_index], - indices: vec![leaf_index], - proofs: vec![proof], - authority: payer.pubkey(), - derivation: Pubkey::from_str(&config.external_services.derivation).unwrap(), - }); + let ix = create_nullify_instruction( + CreateNullifyInstructionInputs { + nullifier_queue: tree_data.queue_pubkey, + merkle_tree: tree_data.tree_pubkey, + change_log_indices: vec![change_log_index], + leaves_queue_indices: vec![leaves_queue_index], + indices: vec![leaf_index], + proofs: vec![proof], + authority: payer.pubkey(), + derivation: Pubkey::from_str(&config.external_services.derivation).unwrap(), + }, + 0, + ); let instructions = vec![ ComputeBudgetInstruction::set_compute_unit_limit(config.cu_limit), diff --git a/forester/src/rollover/operations.rs b/forester/src/rollover/operations.rs index a999763148..f0ea7ee020 100644 --- a/forester/src/rollover/operations.rs +++ b/forester/src/rollover/operations.rs @@ -2,6 +2,10 @@ use std::ops::DerefMut; use std::sync::Arc; use anchor_lang::{system_program, InstructionData, ToAccountMetas}; +use light_registry::account_compression_cpi::sdk::{ + create_rollover_address_merkle_tree_instruction, create_rollover_state_merkle_tree_instruction, + CreateRolloverMerkleTreeInstructionInputs, +}; use log::info; use solana_sdk::instruction::Instruction; use solana_sdk::pubkey::Pubkey; @@ -19,10 +23,10 @@ use account_compression::{ }; use light_hasher::Poseidon; use light_merkle_tree_reference::MerkleTree; -use light_registry::sdk::{ - create_rollover_address_merkle_tree_instruction, create_rollover_state_merkle_tree_instruction, - CreateRolloverMerkleTreeInstructionInputs, -}; +// use light_registry::sdk::{ +// create_rollover_address_merkle_tree_instruction, create_rollover_state_merkle_tree_instruction, +// CreateRolloverMerkleTreeInstructionInputs, +// }; use light_test_utils::address_merkle_tree_config::{ get_address_bundle_config, get_state_bundle_config, }; @@ -320,6 +324,7 @@ pub async fn create_rollover_address_merkle_tree_instructions( old_queue: *nullifier_queue_pubkey, old_merkle_tree: *merkle_tree_pubkey, }, + 0, // TODO: make epoch dynamic ); vec![ create_nullifier_queue_instruction, @@ -368,14 +373,16 @@ pub async fn create_rollover_state_merkle_tree_instructions( &account_compression::ID, Some(new_state_merkle_tree_keypair), ); - let instruction = - create_rollover_state_merkle_tree_instruction(CreateRolloverMerkleTreeInstructionInputs { + let instruction = create_rollover_state_merkle_tree_instruction( + CreateRolloverMerkleTreeInstructionInputs { authority: *authority, new_queue: new_nullifier_queue_keypair.pubkey(), new_merkle_tree: new_state_merkle_tree_keypair.pubkey(), old_queue: *nullifier_queue_pubkey, old_merkle_tree: *merkle_tree_pubkey, - }); + }, + 0, // TODO: make epoch dynamic + ); vec![ create_nullifier_queue_instruction, create_state_merkle_tree_instruction, diff --git a/forester/tests/interop_address_test.rs b/forester/tests/interop_address_test.rs index 0688054d87..3b907beb90 100644 --- a/forester/tests/interop_address_test.rs +++ b/forester/tests/interop_address_test.rs @@ -121,5 +121,7 @@ fn general_action_config() -> GeneralActionConfig { create_state_mt: None, create_address_mt: None, rollover: None, + add_forester: None, + disable_epochs: true, } } diff --git a/forester/tests/interop_nullify_test.rs b/forester/tests/interop_nullify_test.rs index 44c03ba058..5469d47700 100644 --- a/forester/tests/interop_nullify_test.rs +++ b/forester/tests/interop_nullify_test.rs @@ -144,5 +144,7 @@ fn general_action_config() -> GeneralActionConfig { create_state_mt: None, create_address_mt: None, rollover: None, + add_forester: None, + disable_epochs: true, } } diff --git a/forester/tests/rollover_test.rs b/forester/tests/rollover_test.rs index c342d6d19d..1d61565ad1 100644 --- a/forester/tests/rollover_test.rs +++ b/forester/tests/rollover_test.rs @@ -70,7 +70,9 @@ async fn test_address_tree_rollover() { info!("test_address_tree_rollover: created address"); // rollover address Merkle tree - env.rollover_address_merkle_tree_and_queue(0).await.unwrap(); + env.rollover_address_merkle_tree_and_queue(0, &env_accounts.forester, env.epoch) + .await + .unwrap(); info!("test_address_tree_rollover: rollover address tree"); } @@ -130,6 +132,9 @@ async fn test_state_tree_rollover() { for i in 0..5 { env.compress_sol_deterministic(&payer_keypair, LAMPORTS_PER_SOL, Some(i)) .await; - env.rollover_state_merkle_tree_and_queue(i).await.unwrap(); + // rollover address Merkle tree + env.rollover_state_merkle_tree_and_queue(i, &env_accounts.forester, env.epoch) + .await + .unwrap(); } } diff --git a/forester/tests/test_utils.rs b/forester/tests/test_utils.rs index e00a0812aa..ad329aa2a1 100644 --- a/forester/tests/test_utils.rs +++ b/forester/tests/test_utils.rs @@ -68,6 +68,8 @@ pub fn general_action_config() -> GeneralActionConfig { nullify_compressed_accounts: Some(1.0), empty_address_queue: Some(1.0), rollover: None, + add_forester: None, + disable_epochs: true, } } diff --git a/js/compressed-token/src/idl/light_compressed_token.ts b/js/compressed-token/src/idl/light_compressed_token.ts index 12551555f2..7c4c3404b2 100644 --- a/js/compressed-token/src/idl/light_compressed_token.ts +++ b/js/compressed-token/src/idl/light_compressed_token.ts @@ -5,10 +5,9 @@ export type LightCompressedToken = { { name: 'createTokenPool'; docs: [ - 'This instruction expects a mint account to be created in a separate', - 'token program instruction with token authority as mint authority. This', - 'instruction creates a token pool account for that mint owned by token', - 'authority.', + 'This instruction creates a token pool for a given mint. Every spl mint', + 'can have one token pool. When a token is compressed the compressed', + 'tokens are transferrred to the token pool.', ]; accounts: [ { @@ -51,7 +50,11 @@ export type LightCompressedToken = { 'Mints tokens from an spl token mint to a list of compressed accounts.', 'Minted tokens are transferred to a pool account owned by the compressed', 'token program. The instruction creates one compressed output account for', - 'every amount and pubkey input pair one output compressed account.', + 'every amount and pubkey input pair one output compressed account. A', + 'constant amount of lamports can be transferred to each output account to', + 'enable. A use case to add lamports to a compressed token account is to', + 'prevent spam. This is the only way to add lamports to a compressed token', + 'account.', ]; accounts: [ { @@ -159,6 +162,16 @@ export type LightCompressedToken = { }, { name: 'transfer'; + docs: [ + 'Transfers compressed tokens from one account to another. All accounts', + 'must be of the same mint. Additional spl tokens can be compressed or', + 'decompressed. In one transaction only compression or decompression is', + 'possible. Lamports can be transferred along side tokens. If output token', + 'accounts specify less lamports than inputs the remaining lamports are', + 'transferred to an output compressed account. Signer must owner or', + 'delegate. If a delegated token account is transferred the delegate is', + 'not preserved.', + ]; accounts: [ { name: 'feePayer'; @@ -247,6 +260,14 @@ export type LightCompressedToken = { }, { name: 'approve'; + docs: [ + 'Delegates an amount to a delegate. A compressed token account is either', + 'completely delegated or not. Prior delegates are not preserved. Cannot', + 'be called by a delegate.', + 'The instruction creates two output accounts:', + '1. one account with delegated amount', + '2. one account with remaining(change) amount', + ]; accounts: [ { name: 'feePayer'; @@ -317,6 +338,10 @@ export type LightCompressedToken = { }, { name: 'revoke'; + docs: [ + 'Revokes a delegation. The instruction merges all inptus into one output', + 'account. Cannot be called by a delegate. Delegates are not preserved.', + ]; accounts: [ { name: 'feePayer'; @@ -387,6 +412,10 @@ export type LightCompressedToken = { }, { name: 'freeze'; + docs: [ + 'Freezes compressed token accounts. Inputs must not be frozen. Creates as', + 'many outputs as inputs. Balances and delegates are preserved.', + ]; accounts: [ { name: 'feePayer'; @@ -457,6 +486,10 @@ export type LightCompressedToken = { }, { name: 'thaw'; + docs: [ + 'Thaws frozen compressed token accounts. Inputs must be frozen. Creates', + 'as many outputs as inputs. Balances and delegates are preserved.', + ]; accounts: [ { name: 'feePayer'; @@ -527,6 +560,11 @@ export type LightCompressedToken = { }, { name: 'burn'; + docs: [ + 'Burns compressed tokens and spl tokens from the pool account. Delegates', + 'can burn tokens. The output compressed token account remains delegated.', + 'Creates one output compressed token account.', + ]; accounts: [ { name: 'feePayer'; @@ -1496,10 +1534,9 @@ export const IDL: LightCompressedToken = { { name: 'createTokenPool', docs: [ - 'This instruction expects a mint account to be created in a separate', - 'token program instruction with token authority as mint authority. This', - 'instruction creates a token pool account for that mint owned by token', - 'authority.', + 'This instruction creates a token pool for a given mint. Every spl mint', + 'can have one token pool. When a token is compressed the compressed', + 'tokens are transferrred to the token pool.', ], accounts: [ { @@ -1542,7 +1579,11 @@ export const IDL: LightCompressedToken = { 'Mints tokens from an spl token mint to a list of compressed accounts.', 'Minted tokens are transferred to a pool account owned by the compressed', 'token program. The instruction creates one compressed output account for', - 'every amount and pubkey input pair one output compressed account.', + 'every amount and pubkey input pair one output compressed account. A', + 'constant amount of lamports can be transferred to each output account to', + 'enable. A use case to add lamports to a compressed token account is to', + 'prevent spam. This is the only way to add lamports to a compressed token', + 'account.', ], accounts: [ { @@ -1650,6 +1691,16 @@ export const IDL: LightCompressedToken = { }, { name: 'transfer', + docs: [ + 'Transfers compressed tokens from one account to another. All accounts', + 'must be of the same mint. Additional spl tokens can be compressed or', + 'decompressed. In one transaction only compression or decompression is', + 'possible. Lamports can be transferred along side tokens. If output token', + 'accounts specify less lamports than inputs the remaining lamports are', + 'transferred to an output compressed account. Signer must owner or', + 'delegate. If a delegated token account is transferred the delegate is', + 'not preserved.', + ], accounts: [ { name: 'feePayer', @@ -1738,6 +1789,14 @@ export const IDL: LightCompressedToken = { }, { name: 'approve', + docs: [ + 'Delegates an amount to a delegate. A compressed token account is either', + 'completely delegated or not. Prior delegates are not preserved. Cannot', + 'be called by a delegate.', + 'The instruction creates two output accounts:', + '1. one account with delegated amount', + '2. one account with remaining(change) amount', + ], accounts: [ { name: 'feePayer', @@ -1808,6 +1867,10 @@ export const IDL: LightCompressedToken = { }, { name: 'revoke', + docs: [ + 'Revokes a delegation. The instruction merges all inptus into one output', + 'account. Cannot be called by a delegate. Delegates are not preserved.', + ], accounts: [ { name: 'feePayer', @@ -1878,6 +1941,10 @@ export const IDL: LightCompressedToken = { }, { name: 'freeze', + docs: [ + 'Freezes compressed token accounts. Inputs must not be frozen. Creates as', + 'many outputs as inputs. Balances and delegates are preserved.', + ], accounts: [ { name: 'feePayer', @@ -1948,6 +2015,10 @@ export const IDL: LightCompressedToken = { }, { name: 'thaw', + docs: [ + 'Thaws frozen compressed token accounts. Inputs must be frozen. Creates', + 'as many outputs as inputs. Balances and delegates are preserved.', + ], accounts: [ { name: 'feePayer', @@ -2018,6 +2089,11 @@ export const IDL: LightCompressedToken = { }, { name: 'burn', + docs: [ + 'Burns compressed tokens and spl tokens from the pool account. Delegates', + 'can burn tokens. The output compressed token account remains delegated.', + 'Creates one output compressed token account.', + ], accounts: [ { name: 'feePayer', diff --git a/js/stateless.js/src/idls/light_compressed_token.ts b/js/stateless.js/src/idls/light_compressed_token.ts index 12551555f2..7c4c3404b2 100644 --- a/js/stateless.js/src/idls/light_compressed_token.ts +++ b/js/stateless.js/src/idls/light_compressed_token.ts @@ -5,10 +5,9 @@ export type LightCompressedToken = { { name: 'createTokenPool'; docs: [ - 'This instruction expects a mint account to be created in a separate', - 'token program instruction with token authority as mint authority. This', - 'instruction creates a token pool account for that mint owned by token', - 'authority.', + 'This instruction creates a token pool for a given mint. Every spl mint', + 'can have one token pool. When a token is compressed the compressed', + 'tokens are transferrred to the token pool.', ]; accounts: [ { @@ -51,7 +50,11 @@ export type LightCompressedToken = { 'Mints tokens from an spl token mint to a list of compressed accounts.', 'Minted tokens are transferred to a pool account owned by the compressed', 'token program. The instruction creates one compressed output account for', - 'every amount and pubkey input pair one output compressed account.', + 'every amount and pubkey input pair one output compressed account. A', + 'constant amount of lamports can be transferred to each output account to', + 'enable. A use case to add lamports to a compressed token account is to', + 'prevent spam. This is the only way to add lamports to a compressed token', + 'account.', ]; accounts: [ { @@ -159,6 +162,16 @@ export type LightCompressedToken = { }, { name: 'transfer'; + docs: [ + 'Transfers compressed tokens from one account to another. All accounts', + 'must be of the same mint. Additional spl tokens can be compressed or', + 'decompressed. In one transaction only compression or decompression is', + 'possible. Lamports can be transferred along side tokens. If output token', + 'accounts specify less lamports than inputs the remaining lamports are', + 'transferred to an output compressed account. Signer must owner or', + 'delegate. If a delegated token account is transferred the delegate is', + 'not preserved.', + ]; accounts: [ { name: 'feePayer'; @@ -247,6 +260,14 @@ export type LightCompressedToken = { }, { name: 'approve'; + docs: [ + 'Delegates an amount to a delegate. A compressed token account is either', + 'completely delegated or not. Prior delegates are not preserved. Cannot', + 'be called by a delegate.', + 'The instruction creates two output accounts:', + '1. one account with delegated amount', + '2. one account with remaining(change) amount', + ]; accounts: [ { name: 'feePayer'; @@ -317,6 +338,10 @@ export type LightCompressedToken = { }, { name: 'revoke'; + docs: [ + 'Revokes a delegation. The instruction merges all inptus into one output', + 'account. Cannot be called by a delegate. Delegates are not preserved.', + ]; accounts: [ { name: 'feePayer'; @@ -387,6 +412,10 @@ export type LightCompressedToken = { }, { name: 'freeze'; + docs: [ + 'Freezes compressed token accounts. Inputs must not be frozen. Creates as', + 'many outputs as inputs. Balances and delegates are preserved.', + ]; accounts: [ { name: 'feePayer'; @@ -457,6 +486,10 @@ export type LightCompressedToken = { }, { name: 'thaw'; + docs: [ + 'Thaws frozen compressed token accounts. Inputs must be frozen. Creates', + 'as many outputs as inputs. Balances and delegates are preserved.', + ]; accounts: [ { name: 'feePayer'; @@ -527,6 +560,11 @@ export type LightCompressedToken = { }, { name: 'burn'; + docs: [ + 'Burns compressed tokens and spl tokens from the pool account. Delegates', + 'can burn tokens. The output compressed token account remains delegated.', + 'Creates one output compressed token account.', + ]; accounts: [ { name: 'feePayer'; @@ -1496,10 +1534,9 @@ export const IDL: LightCompressedToken = { { name: 'createTokenPool', docs: [ - 'This instruction expects a mint account to be created in a separate', - 'token program instruction with token authority as mint authority. This', - 'instruction creates a token pool account for that mint owned by token', - 'authority.', + 'This instruction creates a token pool for a given mint. Every spl mint', + 'can have one token pool. When a token is compressed the compressed', + 'tokens are transferrred to the token pool.', ], accounts: [ { @@ -1542,7 +1579,11 @@ export const IDL: LightCompressedToken = { 'Mints tokens from an spl token mint to a list of compressed accounts.', 'Minted tokens are transferred to a pool account owned by the compressed', 'token program. The instruction creates one compressed output account for', - 'every amount and pubkey input pair one output compressed account.', + 'every amount and pubkey input pair one output compressed account. A', + 'constant amount of lamports can be transferred to each output account to', + 'enable. A use case to add lamports to a compressed token account is to', + 'prevent spam. This is the only way to add lamports to a compressed token', + 'account.', ], accounts: [ { @@ -1650,6 +1691,16 @@ export const IDL: LightCompressedToken = { }, { name: 'transfer', + docs: [ + 'Transfers compressed tokens from one account to another. All accounts', + 'must be of the same mint. Additional spl tokens can be compressed or', + 'decompressed. In one transaction only compression or decompression is', + 'possible. Lamports can be transferred along side tokens. If output token', + 'accounts specify less lamports than inputs the remaining lamports are', + 'transferred to an output compressed account. Signer must owner or', + 'delegate. If a delegated token account is transferred the delegate is', + 'not preserved.', + ], accounts: [ { name: 'feePayer', @@ -1738,6 +1789,14 @@ export const IDL: LightCompressedToken = { }, { name: 'approve', + docs: [ + 'Delegates an amount to a delegate. A compressed token account is either', + 'completely delegated or not. Prior delegates are not preserved. Cannot', + 'be called by a delegate.', + 'The instruction creates two output accounts:', + '1. one account with delegated amount', + '2. one account with remaining(change) amount', + ], accounts: [ { name: 'feePayer', @@ -1808,6 +1867,10 @@ export const IDL: LightCompressedToken = { }, { name: 'revoke', + docs: [ + 'Revokes a delegation. The instruction merges all inptus into one output', + 'account. Cannot be called by a delegate. Delegates are not preserved.', + ], accounts: [ { name: 'feePayer', @@ -1878,6 +1941,10 @@ export const IDL: LightCompressedToken = { }, { name: 'freeze', + docs: [ + 'Freezes compressed token accounts. Inputs must not be frozen. Creates as', + 'many outputs as inputs. Balances and delegates are preserved.', + ], accounts: [ { name: 'feePayer', @@ -1948,6 +2015,10 @@ export const IDL: LightCompressedToken = { }, { name: 'thaw', + docs: [ + 'Thaws frozen compressed token accounts. Inputs must be frozen. Creates', + 'as many outputs as inputs. Balances and delegates are preserved.', + ], accounts: [ { name: 'feePayer', @@ -2018,6 +2089,11 @@ export const IDL: LightCompressedToken = { }, { name: 'burn', + docs: [ + 'Burns compressed tokens and spl tokens from the pool account. Delegates', + 'can burn tokens. The output compressed token account remains delegated.', + 'Creates one output compressed token account.', + ], accounts: [ { name: 'feePayer', diff --git a/js/stateless.js/src/idls/light_registry.ts b/js/stateless.js/src/idls/light_registry.ts index 82ae54e15d..727a3393d9 100644 --- a/js/stateless.js/src/idls/light_registry.ts +++ b/js/stateless.js/src/idls/light_registry.ts @@ -30,19 +30,15 @@ export type LightRegistry = { ]; args: [ { - name: 'authority'; - type: 'publicKey'; + name: 'bump'; + type: 'u8'; }, { - name: 'rewards'; + name: 'protocolConfig'; type: { - vec: 'u64'; + defined: 'ProtocolConfig'; }; }, - { - name: 'bump'; - type: 'u8'; - }, ]; }, { @@ -58,6 +54,11 @@ export type LightRegistry = { isMut: true; isSigner: false; }, + { + name: 'newAuthority'; + isMut: false; + isSigner: true; + }, ]; args: [ { @@ -105,13 +106,13 @@ export type LightRegistry = { }, { name: 'registeredProgramPda'; - isMut: false; + isMut: true; isSigner: false; }, { name: 'programToBeRegistered'; isMut: false; - isSigner: false; + isSigner: true; }, ]; args: [ @@ -410,7 +411,7 @@ export type LightRegistry = { name: 'registerForester'; accounts: [ { - name: 'foresterEpochPda'; + name: 'foresterPda'; isMut: true; isSigner: false; }, @@ -439,7 +440,118 @@ export type LightRegistry = { name: 'authority'; type: 'publicKey'; }, + { + name: 'config'; + type: { + defined: 'ForesterConfig'; + }; + }, + ]; + }, + { + name: 'updateForester'; + accounts: [ + { + name: 'foresterPda'; + isMut: true; + isSigner: false; + }, + { + name: 'authority'; + isMut: false; + isSigner: true; + }, + { + name: 'newAuthority'; + isMut: false; + isSigner: true; + isOptional: true; + }, + ]; + args: [ + { + name: 'config'; + type: { + defined: 'ForesterConfig'; + }; + }, + ]; + }, + { + name: 'registerForesterEpoch'; + docs: [ + 'Registers the forester for the epoch.', + '1. Only the forester can register herself for the epoch.', + '2. Protocol config is copied.', + '3. Epoch account is created if needed.', + ]; + accounts: [ + { + name: 'authority'; + isMut: true; + isSigner: true; + }, + { + name: 'foresterPda'; + isMut: false; + isSigner: false; + }, + { + name: 'foresterEpochPda'; + isMut: true; + isSigner: false; + }, + { + name: 'protocolConfig'; + isMut: false; + isSigner: false; + }, + { + name: 'epochPda'; + isMut: true; + isSigner: false; + }, + { + name: 'systemProgram'; + isMut: false; + isSigner: false; + }, + ]; + args: [ + { + name: 'epoch'; + type: 'u64'; + }, + ]; + }, + { + name: 'finalizeRegistration'; + docs: [ + 'This transaction can be included as additional instruction in the first', + 'work instructions during the active phase.', + 'Registration Period must be over.', + 'TODO: introduce grace period between registration and before', + "active phase starts, do I really need it or isn't it clear who gets the", + 'first slot the first sign up?', + ]; + accounts: [ + { + name: 'authority'; + isMut: true; + isSigner: true; + }, + { + name: 'foresterEpochPda'; + isMut: true; + isSigner: false; + }, + { + name: 'epochPda'; + isMut: false; + isSigner: false; + }, ]; + args: []; }, { name: 'updateForesterEpochPda'; @@ -462,6 +574,27 @@ export type LightRegistry = { }, ]; }, + { + name: 'reportWork'; + accounts: [ + { + name: 'authority'; + isMut: false; + isSigner: true; + }, + { + name: 'foresterEpochPda'; + isMut: true; + isSigner: false; + }, + { + name: 'epochPda'; + isMut: true; + isSigner: false; + }, + ]; + args: []; + }, { name: 'initializeAddressMerkleTree'; accounts: [ @@ -601,31 +734,34 @@ export type LightRegistry = { ]; accounts: [ { - name: 'foresterEpoch'; + name: 'epochPda'; + docs: ['Is used for tallying and rewards calculation']; type: { kind: 'struct'; fields: [ { - name: 'authority'; - type: 'publicKey'; + name: 'epoch'; + type: 'u64'; }, { - name: 'counter'; - type: 'u64'; + name: 'protocolConfig'; + type: { + defined: 'ProtocolConfig'; + }; }, { - name: 'epochStart'; + name: 'totalWork'; type: 'u64'; }, { - name: 'epochEnd'; + name: 'registeredStake'; type: 'u64'; }, ]; }; }, { - name: 'lightGovernanceAuthority'; + name: 'foresterEpochPda'; type: { kind: 'struct'; fields: [ @@ -634,102 +770,371 @@ export type LightRegistry = { type: 'publicKey'; }, { - name: 'bump'; - type: 'u8'; + name: 'config'; + type: { + defined: 'ForesterConfig'; + }; }, { name: 'epoch'; type: 'u64'; }, { - name: 'epochLength'; + name: 'stakeWeight'; + type: 'u64'; + }, + { + name: 'workCounter'; + type: 'u64'; + }, + { + name: 'hasReportedWork'; + docs: [ + 'Work can be reported in an extra round to earn extra performance based', + 'rewards. // TODO: make sure that performance based rewards can only be', + 'claimed if work has been reported', + ]; + type: 'bool'; + }, + { + name: 'foresterIndex'; + docs: [ + 'Start index of the range that determines when the forester is eligible to perform work.', + 'End index is forester_start_index + stake_weight', + ]; + type: 'u64'; + }, + { + name: 'epochActivePhaseStartSlot'; type: 'u64'; }, { - name: 'padding'; + name: 'totalEpochStateWeight'; + docs: [ + 'Total epoch state weight is registered stake of the epoch account after', + 'registration is concluded and active epoch period starts.', + ]; type: { - array: ['u8', 7]; + option: 'u64'; }; }, { - name: 'rewards'; + name: 'protocolConfig'; type: { - vec: 'u64'; + defined: 'ProtocolConfig'; }; }, + { + name: 'finalizeCounter'; + docs: [ + 'Incremented every time finalize registration is called.', + ]; + type: 'u64'; + }, ]; }; }, - ]; - errors: [ - { - code: 6000; - name: 'InvalidForester'; - msg: 'InvalidForester'; - }, - ]; -}; - -export const IDL: LightRegistry = { - version: '0.4.1', - name: 'light_registry', - constants: [ { - name: 'AUTHORITY_PDA_SEED', - type: 'bytes', - value: '[97, 117, 116, 104, 111, 114, 105, 116, 121]', + name: 'protocolConfigPda'; + type: { + kind: 'struct'; + fields: [ + { + name: 'authority'; + type: 'publicKey'; + }, + { + name: 'bump'; + type: 'u8'; + }, + { + name: 'config'; + type: { + defined: 'ProtocolConfig'; + }; + }, + ]; + }; }, - ], - instructions: [ { - name: 'initializeGovernanceAuthority', - accounts: [ - { - name: 'authority', - isMut: true, - isSigner: true, - }, - { - name: 'authorityPda', - isMut: true, - isSigner: false, - }, - { - name: 'systemProgram', - isMut: false, - isSigner: false, - }, - ], - args: [ - { - name: 'authority', - type: 'publicKey', - }, - { - name: 'rewards', - type: { - vec: 'u64', + name: 'foresterAccount'; + type: { + kind: 'struct'; + fields: [ + { + name: 'authority'; + type: 'publicKey'; }, - }, - { - name: 'bump', - type: 'u8', - }, - ], + { + name: 'config'; + type: { + defined: 'ForesterConfig'; + }; + }, + { + name: 'activeStakeWeight'; + type: 'u64'; + }, + { + name: 'pendingStakeWeight'; + docs: [ + 'Pending stake which will get active once the next epoch starts.', + ]; + type: 'u64'; + }, + { + name: 'currentEpoch'; + type: 'u64'; + }, + { + name: 'lastCompressedForesterEpochPdaHash'; + docs: [ + 'Link to previous compressed forester epoch account hash.', + ]; + type: { + array: ['u8', 32]; + }; + }, + { + name: 'lastRegisteredEpoch'; + type: 'u64'; + }, + ]; + }; }, + ]; + types: [ { - name: 'updateGovernanceAuthority', - accounts: [ - { - name: 'authority', - isMut: true, - isSigner: true, - }, + name: 'ProtocolConfig'; + docs: [ + 'Epoch Phases:', + '1. Registration', + '2. Active', + '3. Report Work', + '4. Post (Epoch has ended, and rewards can be claimed.)', + '- There is always an active phase in progress, registration and report work', + 'phases run in parallel to a currently active phase.', + ]; + type: { + kind: 'struct'; + fields: [ + { + name: 'genesisSlot'; + docs: [ + 'Solana slot when the protocol starts operating.', + ]; + type: 'u64'; + }, + { + name: 'epochReward'; + docs: ['Total rewards per epoch.']; + type: 'u64'; + }, + { + name: 'baseReward'; + docs: [ + 'Base reward for foresters, the difference between epoch reward and base', + 'reward distributed based on performance.', + ]; + type: 'u64'; + }, + { + name: 'minStake'; + docs: [ + 'Minimum stake required for a forester to register to an epoch.', + ]; + type: 'u64'; + }, + { + name: 'slotLength'; + docs: [ + 'Light protocol slot length. (Naming is confusing for Solana slot.)', + 'TODO: rename to epoch_length (registration + active phase length)', + ]; + type: 'u64'; + }, + { + name: 'registrationPhaseLength'; + docs: ['Foresters can register for this phase.']; + type: 'u64'; + }, + { + name: 'activePhaseLength'; + docs: ['Foresters can perform work in this phase.']; + type: 'u64'; + }, + { + name: 'reportWorkPhaseLength'; + docs: [ + 'Foresters can report work to receive performance based rewards in this', + 'phase.', + 'TODO: enforce report work == registration phase length so that', + 'epoch in report work phase is registration epoch - 1', + ]; + type: 'u64'; + }, + { + name: 'mint'; + type: 'publicKey'; + }, + ]; + }; + }, + { + name: 'ForesterConfig'; + type: { + kind: 'struct'; + fields: [ + { + name: 'fee'; + docs: ['Fee in percentage points.']; + type: 'u64'; + }, + ]; + }; + }, + { + name: 'EpochState'; + type: { + kind: 'enum'; + variants: [ + { + name: 'Registration'; + }, + { + name: 'Active'; + }, + { + name: 'ReportWork'; + }, + { + name: 'Post'; + }, + { + name: 'Pre'; + }, + ]; + }; + }, + ]; + errors: [ + { + code: 6000; + name: 'InvalidForester'; + msg: 'InvalidForester'; + }, + { + code: 6001; + name: 'NotInReportWorkPhase'; + }, + { + code: 6002; + name: 'StakeAccountAlreadySynced'; + }, + { + code: 6003; + name: 'EpochEnded'; + }, + { + code: 6004; + name: 'ForresterNotEligible'; + }, + { + code: 6005; + name: 'NotInRegistrationPeriod'; + }, + { + code: 6006; + name: 'StakeInsuffient'; + }, + { + code: 6007; + name: 'ForesterAlreadyRegistered'; + }, + { + code: 6008; + name: 'InvalidEpochAccount'; + }, + { + code: 6009; + name: 'InvalidEpoch'; + }, + { + code: 6010; + name: 'EpochStillInProgress'; + }, + { + code: 6011; + name: 'NotInActivePhase'; + }, + { + code: 6012; + name: 'ForesterAlreadyReportedWork'; + }, + ]; +}; + +export const IDL: LightRegistry = { + version: '0.4.1', + name: 'light_registry', + constants: [ + { + name: 'AUTHORITY_PDA_SEED', + type: 'bytes', + value: '[97, 117, 116, 104, 111, 114, 105, 116, 121]', + }, + ], + instructions: [ + { + name: 'initializeGovernanceAuthority', + accounts: [ + { + name: 'authority', + isMut: true, + isSigner: true, + }, { name: 'authorityPda', isMut: true, isSigner: false, }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'bump', + type: 'u8', + }, + { + name: 'protocolConfig', + type: { + defined: 'ProtocolConfig', + }, + }, + ], + }, + { + name: 'updateGovernanceAuthority', + accounts: [ + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'authorityPda', + isMut: true, + isSigner: false, + }, + { + name: 'newAuthority', + isMut: false, + isSigner: true, + }, ], args: [ { @@ -777,13 +1182,13 @@ export const IDL: LightRegistry = { }, { name: 'registeredProgramPda', - isMut: false, + isMut: true, isSigner: false, }, { name: 'programToBeRegistered', isMut: false, - isSigner: false, + isSigner: true, }, ], args: [ @@ -1082,7 +1487,7 @@ export const IDL: LightRegistry = { name: 'registerForester', accounts: [ { - name: 'foresterEpochPda', + name: 'foresterPda', isMut: true, isSigner: false, }, @@ -1111,7 +1516,118 @@ export const IDL: LightRegistry = { name: 'authority', type: 'publicKey', }, + { + name: 'config', + type: { + defined: 'ForesterConfig', + }, + }, + ], + }, + { + name: 'updateForester', + accounts: [ + { + name: 'foresterPda', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'newAuthority', + isMut: false, + isSigner: true, + isOptional: true, + }, + ], + args: [ + { + name: 'config', + type: { + defined: 'ForesterConfig', + }, + }, + ], + }, + { + name: 'registerForesterEpoch', + docs: [ + 'Registers the forester for the epoch.', + '1. Only the forester can register herself for the epoch.', + '2. Protocol config is copied.', + '3. Epoch account is created if needed.', + ], + accounts: [ + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'foresterPda', + isMut: false, + isSigner: false, + }, + { + name: 'foresterEpochPda', + isMut: true, + isSigner: false, + }, + { + name: 'protocolConfig', + isMut: false, + isSigner: false, + }, + { + name: 'epochPda', + isMut: true, + isSigner: false, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'epoch', + type: 'u64', + }, + ], + }, + { + name: 'finalizeRegistration', + docs: [ + 'This transaction can be included as additional instruction in the first', + 'work instructions during the active phase.', + 'Registration Period must be over.', + 'TODO: introduce grace period between registration and before', + "active phase starts, do I really need it or isn't it clear who gets the", + 'first slot the first sign up?', + ], + accounts: [ + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'foresterEpochPda', + isMut: true, + isSigner: false, + }, + { + name: 'epochPda', + isMut: false, + isSigner: false, + }, ], + args: [], }, { name: 'updateForesterEpochPda', @@ -1134,6 +1650,27 @@ export const IDL: LightRegistry = { }, ], }, + { + name: 'reportWork', + accounts: [ + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'foresterEpochPda', + isMut: true, + isSigner: false, + }, + { + name: 'epochPda', + isMut: true, + isSigner: false, + }, + ], + args: [], + }, { name: 'initializeAddressMerkleTree', accounts: [ @@ -1273,7 +1810,34 @@ export const IDL: LightRegistry = { ], accounts: [ { - name: 'foresterEpoch', + name: 'epochPda', + docs: ['Is used for tallying and rewards calculation'], + type: { + kind: 'struct', + fields: [ + { + name: 'epoch', + type: 'u64', + }, + { + name: 'protocolConfig', + type: { + defined: 'ProtocolConfig', + }, + }, + { + name: 'totalWork', + type: 'u64', + }, + { + name: 'registeredStake', + type: 'u64', + }, + ], + }, + }, + { + name: 'foresterEpochPda', type: { kind: 'struct', fields: [ @@ -1282,22 +1846,72 @@ export const IDL: LightRegistry = { type: 'publicKey', }, { - name: 'counter', + name: 'config', + type: { + defined: 'ForesterConfig', + }, + }, + { + name: 'epoch', type: 'u64', }, { - name: 'epochStart', + name: 'stakeWeight', type: 'u64', }, { - name: 'epochEnd', + name: 'workCounter', + type: 'u64', + }, + { + name: 'hasReportedWork', + docs: [ + 'Work can be reported in an extra round to earn extra performance based', + 'rewards. // TODO: make sure that performance based rewards can only be', + 'claimed if work has been reported', + ], + type: 'bool', + }, + { + name: 'foresterIndex', + docs: [ + 'Start index of the range that determines when the forester is eligible to perform work.', + 'End index is forester_start_index + stake_weight', + ], + type: 'u64', + }, + { + name: 'epochActivePhaseStartSlot', + type: 'u64', + }, + { + name: 'totalEpochStateWeight', + docs: [ + 'Total epoch state weight is registered stake of the epoch account after', + 'registration is concluded and active epoch period starts.', + ], + type: { + option: 'u64', + }, + }, + { + name: 'protocolConfig', + type: { + defined: 'ProtocolConfig', + }, + }, + { + name: 'finalizeCounter', + docs: [ + 'Incremented every time finalize registration is called.', + ], type: 'u64', }, ], }, }, { - name: 'lightGovernanceAuthority', + name: 'protocolConfigPda', type: { kind: 'struct', fields: [ @@ -1310,25 +1924,171 @@ export const IDL: LightRegistry = { type: 'u8', }, { - name: 'epoch', - type: 'u64', + name: 'config', + type: { + defined: 'ProtocolConfig', + }, }, + ], + }, + }, + { + name: 'foresterAccount', + type: { + kind: 'struct', + fields: [ { - name: 'epochLength', - type: 'u64', + name: 'authority', + type: 'publicKey', }, { - name: 'padding', + name: 'config', type: { - array: ['u8', 7], + defined: 'ForesterConfig', }, }, { - name: 'rewards', + name: 'activeStakeWeight', + type: 'u64', + }, + { + name: 'pendingStakeWeight', + docs: [ + 'Pending stake which will get active once the next epoch starts.', + ], + type: 'u64', + }, + { + name: 'currentEpoch', + type: 'u64', + }, + { + name: 'lastCompressedForesterEpochPdaHash', + docs: [ + 'Link to previous compressed forester epoch account hash.', + ], type: { - vec: 'u64', + array: ['u8', 32], }, }, + { + name: 'lastRegisteredEpoch', + type: 'u64', + }, + ], + }, + }, + ], + types: [ + { + name: 'ProtocolConfig', + docs: [ + 'Epoch Phases:', + '1. Registration', + '2. Active', + '3. Report Work', + '4. Post (Epoch has ended, and rewards can be claimed.)', + '- There is always an active phase in progress, registration and report work', + 'phases run in parallel to a currently active phase.', + ], + type: { + kind: 'struct', + fields: [ + { + name: 'genesisSlot', + docs: [ + 'Solana slot when the protocol starts operating.', + ], + type: 'u64', + }, + { + name: 'epochReward', + docs: ['Total rewards per epoch.'], + type: 'u64', + }, + { + name: 'baseReward', + docs: [ + 'Base reward for foresters, the difference between epoch reward and base', + 'reward distributed based on performance.', + ], + type: 'u64', + }, + { + name: 'minStake', + docs: [ + 'Minimum stake required for a forester to register to an epoch.', + ], + type: 'u64', + }, + { + name: 'slotLength', + docs: [ + 'Light protocol slot length. (Naming is confusing for Solana slot.)', + 'TODO: rename to epoch_length (registration + active phase length)', + ], + type: 'u64', + }, + { + name: 'registrationPhaseLength', + docs: ['Foresters can register for this phase.'], + type: 'u64', + }, + { + name: 'activePhaseLength', + docs: ['Foresters can perform work in this phase.'], + type: 'u64', + }, + { + name: 'reportWorkPhaseLength', + docs: [ + 'Foresters can report work to receive performance based rewards in this', + 'phase.', + 'TODO: enforce report work == registration phase length so that', + 'epoch in report work phase is registration epoch - 1', + ], + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + ], + }, + }, + { + name: 'ForesterConfig', + type: { + kind: 'struct', + fields: [ + { + name: 'fee', + docs: ['Fee in percentage points.'], + type: 'u64', + }, + ], + }, + }, + { + name: 'EpochState', + type: { + kind: 'enum', + variants: [ + { + name: 'Registration', + }, + { + name: 'Active', + }, + { + name: 'ReportWork', + }, + { + name: 'Post', + }, + { + name: 'Pre', + }, ], }, }, @@ -1339,5 +2099,53 @@ export const IDL: LightRegistry = { name: 'InvalidForester', msg: 'InvalidForester', }, + { + code: 6001, + name: 'NotInReportWorkPhase', + }, + { + code: 6002, + name: 'StakeAccountAlreadySynced', + }, + { + code: 6003, + name: 'EpochEnded', + }, + { + code: 6004, + name: 'ForresterNotEligible', + }, + { + code: 6005, + name: 'NotInRegistrationPeriod', + }, + { + code: 6006, + name: 'StakeInsuffient', + }, + { + code: 6007, + name: 'ForesterAlreadyRegistered', + }, + { + code: 6008, + name: 'InvalidEpochAccount', + }, + { + code: 6009, + name: 'InvalidEpoch', + }, + { + code: 6010, + name: 'EpochStillInProgress', + }, + { + code: 6011, + name: 'NotInActivePhase', + }, + { + code: 6012, + name: 'ForesterAlreadyReportedWork', + }, ], }; diff --git a/programs/account-compression/src/instructions/register_program.rs b/programs/account-compression/src/instructions/register_program.rs index 45d7e2ec9e..16dc6f243d 100644 --- a/programs/account-compression/src/instructions/register_program.rs +++ b/programs/account-compression/src/instructions/register_program.rs @@ -14,7 +14,7 @@ pub struct RegisteredProgram { #[derive(Accounts)] pub struct RegisterProgramToGroup<'info> { /// CHECK: Signer is checked according to authority pda in instruction. - #[account(mut, constraint= authority.key() == group_authority_pda.authority @AccountCompressionErrorCode::InvalidAuthority)] + #[account( mut, constraint= authority.key() == group_authority_pda.authority @AccountCompressionErrorCode::InvalidAuthority)] pub authority: Signer<'info>, pub program_to_be_registered: Signer<'info>, #[account( diff --git a/programs/compressed-token/src/burn.rs b/programs/compressed-token/src/burn.rs index ec65d235ac..3776560df0 100644 --- a/programs/compressed-token/src/burn.rs +++ b/programs/compressed-token/src/burn.rs @@ -172,7 +172,7 @@ pub mod sdk { use anchor_lang::{AnchorSerialize, InstructionData, ToAccountMetas}; use light_system_program::{ invoke::processor::CompressedProof, - sdk::compressed_account::{CompressedAccount, MerkleContext}, + sdk::compressed_account::CompressedAccountWithMerkleContext, }; use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; @@ -196,8 +196,7 @@ pub mod sdk { pub root_indices: Vec, pub proof: CompressedProof, pub input_token_data: Vec, - pub input_compressed_accounts: Vec, - pub input_merkle_contexts: Vec, + pub input_compressed_accounts: Vec, pub change_account_merkle_tree: Pubkey, pub mint: Pubkey, pub burn_amount: u64, @@ -212,7 +211,6 @@ pub mod sdk { &[inputs.change_account_merkle_tree], &inputs.input_token_data, &inputs.input_compressed_accounts, - &inputs.input_merkle_contexts, &inputs.root_indices, &Vec::new(), ); diff --git a/programs/compressed-token/src/delegation.rs b/programs/compressed-token/src/delegation.rs index f083f3d0c8..6e15488b57 100644 --- a/programs/compressed-token/src/delegation.rs +++ b/programs/compressed-token/src/delegation.rs @@ -247,7 +247,7 @@ pub mod sdk { use anchor_lang::{AnchorSerialize, InstructionData, ToAccountMetas}; use light_system_program::{ invoke::processor::CompressedProof, - sdk::compressed_account::{CompressedAccount, MerkleContext}, + sdk::compressed_account::CompressedAccountWithMerkleContext, }; use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; @@ -269,8 +269,7 @@ pub mod sdk { pub root_indices: Vec, pub proof: CompressedProof, pub input_token_data: Vec, - pub input_compressed_accounts: Vec, - pub input_merkle_contexts: Vec, + pub input_compressed_accounts: Vec, pub mint: Pubkey, pub delegated_amount: u64, pub delegate_lamports: Option, @@ -290,7 +289,6 @@ pub mod sdk { ], &inputs.input_token_data, &inputs.input_compressed_accounts, - &inputs.input_merkle_contexts, &inputs.root_indices, &Vec::new(), ); @@ -358,8 +356,7 @@ pub mod sdk { pub root_indices: Vec, pub proof: CompressedProof, pub input_token_data: Vec, - pub input_compressed_accounts: Vec, - pub input_merkle_contexts: Vec, + pub input_compressed_accounts: Vec, pub mint: Pubkey, pub output_account_merkle_tree: Pubkey, } @@ -372,7 +369,6 @@ pub mod sdk { &[inputs.output_account_merkle_tree], &inputs.input_token_data, &inputs.input_compressed_accounts, - &inputs.input_merkle_contexts, &inputs.root_indices, &Vec::new(), ); diff --git a/programs/compressed-token/src/freeze.rs b/programs/compressed-token/src/freeze.rs index ba4ae11110..54e43bb204 100644 --- a/programs/compressed-token/src/freeze.rs +++ b/programs/compressed-token/src/freeze.rs @@ -183,7 +183,7 @@ pub mod sdk { use anchor_lang::{AnchorSerialize, InstructionData, ToAccountMetas}; use light_system_program::{ invoke::processor::CompressedProof, - sdk::compressed_account::{CompressedAccount, MerkleContext}, + sdk::compressed_account::CompressedAccountWithMerkleContext, }; use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; @@ -202,8 +202,7 @@ pub mod sdk { pub root_indices: Vec, pub proof: CompressedProof, pub input_token_data: Vec, - pub input_compressed_accounts: Vec, - pub input_merkle_contexts: Vec, + pub input_compressed_accounts: Vec, pub outputs_merkle_tree: Pubkey, } @@ -215,7 +214,6 @@ pub mod sdk { &[inputs.outputs_merkle_tree], &inputs.input_token_data, &inputs.input_compressed_accounts, - &inputs.input_merkle_contexts, &inputs.root_indices, &Vec::new(), ); diff --git a/programs/compressed-token/src/process_transfer.rs b/programs/compressed-token/src/process_transfer.rs index 63599ea521..aa425e7a40 100644 --- a/programs/compressed-token/src/process_transfer.rs +++ b/programs/compressed-token/src/process_transfer.rs @@ -85,10 +85,10 @@ pub fn process_transfer<'a, 'b, 'c, 'info: 'b + 'c>( let mut vec = vec![false; inputs.output_compressed_accounts.len()]; if let Some(index) = delegated_transfer.delegate_change_account_index { vec[index as usize] = true; + (Some(vec), Some(ctx.accounts.authority.key())) } else { - return err!(crate::ErrorCode::InvalidDelegateIndex); + (None, None) } - (Some(vec), Some(ctx.accounts.authority.key())) } else { (None, None) }; @@ -270,13 +270,14 @@ pub fn add_token_data_to_input_compressed_accounts( input_token_data: &[TokenData], hashed_mint: &[u8; 32], ) -> Result<()> { - let hashed_owner = hash_to_bn254_field_size_be(&input_token_data[0].owner.to_bytes()) - .unwrap() - .0; for (i, compressed_account_with_context) in input_compressed_accounts_with_merkle_context .iter_mut() .enumerate() { + // TODO: get from vector + let hashed_owner = hash_to_bn254_field_size_be(&input_token_data[i].owner.to_bytes()) + .unwrap() + .0; let mut data = Vec::new(); input_token_data[i].serialize(&mut data)?; let amount = input_token_data[i].amount.to_le_bytes(); @@ -487,12 +488,15 @@ pub fn get_input_compressed_accounts_with_merkle_context_and_check_signer = Vec::with_capacity(input_token_data_with_context.len()); - let owner = if let Some(signer_is_delegate) = signer_is_delegate { - signer_is_delegate.owner - } else { - *signer - }; + for input_token_data in input_token_data_with_context.iter() { + let owner = if input_token_data.delegate_index.is_none() { + *signer + } else if let Some(signer_is_delegate) = signer_is_delegate { + signer_is_delegate.owner + } else { + *signer + }; // This is a check for convenience to throw a meaningful error. // The actual security results from the proof verification. if signer_is_delegate.is_some() @@ -510,10 +514,11 @@ pub fn get_input_compressed_accounts_with_merkle_context_and_check_signer, input_token_data: &[TokenData], - input_compressed_accounts: &[CompressedAccount], + input_compressed_accounts: &[CompressedAccountWithMerkleContext], mint: Pubkey, delegate: Option, is_compress: bool, @@ -629,7 +633,6 @@ pub mod transfer_sdk { let (remaining_accounts, mut inputs_struct) = create_inputs_and_remaining_accounts( input_token_data, input_compressed_accounts, - input_merkle_context, delegate, output_compressed_accounts, root_indices, @@ -691,8 +694,7 @@ pub mod transfer_sdk { #[allow(clippy::too_many_arguments)] pub fn create_inputs_and_remaining_accounts_checked( input_token_data: &[TokenData], - input_compressed_accounts: &[CompressedAccount], - input_merkle_context: &[MerkleContext], + input_compressed_accounts: &[CompressedAccountWithMerkleContext], owner_if_delegate_is_signer: Option, output_compressed_accounts: &[TokenTransferOutputData], root_indices: &[u16], @@ -724,7 +726,6 @@ pub mod transfer_sdk { create_inputs_and_remaining_accounts( input_token_data, input_compressed_accounts, - input_merkle_context, owner_if_delegate_is_signer, output_compressed_accounts, root_indices, @@ -741,8 +742,8 @@ pub mod transfer_sdk { #[allow(clippy::too_many_arguments)] pub fn create_inputs_and_remaining_accounts( input_token_data: &[TokenData], - input_compressed_accounts: &[CompressedAccount], - input_merkle_context: &[MerkleContext], + input_compressed_accounts: &[CompressedAccountWithMerkleContext], + // input_merkle_context: &[MerkleContext], delegate: Option, output_compressed_accounts: &[TokenTransferOutputData], root_indices: &[u16], @@ -781,7 +782,6 @@ pub mod transfer_sdk { additonal_accounts.as_slice(), input_token_data, input_compressed_accounts, - input_merkle_context, root_indices, output_compressed_accounts, ); @@ -809,11 +809,11 @@ pub mod transfer_sdk { (remaining_accounts, inputs_struct) } + // TODO: move to light sdk pub fn create_input_output_and_remaining_accounts( additional_accounts: &[Pubkey], input_token_data: &[TokenData], - input_compressed_accounts: &[CompressedAccount], - input_merkle_context: &[MerkleContext], + input_compressed_accounts: &[CompressedAccountWithMerkleContext], root_indices: &[u16], output_compressed_accounts: &[TokenTransferOutputData], ) -> ( @@ -835,27 +835,77 @@ pub mod transfer_sdk { } let mut input_token_data_with_context: Vec = Vec::new(); - for (i, token_data) in input_token_data.iter().enumerate() { - match remaining_accounts.get(&input_merkle_context[i].merkle_tree_pubkey) { + create_input_token_accounts( + input_token_data, + &mut remaining_accounts, + input_compressed_accounts, + &mut index, + root_indices, + &mut input_token_data_with_context, + ); + + let mut _output_compressed_accounts: Vec = + Vec::with_capacity(output_compressed_accounts.len()); + for (i, mt) in output_compressed_accounts.iter().enumerate() { + match remaining_accounts.get(&mt.merkle_tree) { Some(_) => {} None => { - remaining_accounts.insert(input_merkle_context[i].merkle_tree_pubkey, index); + remaining_accounts.insert(mt.merkle_tree, index); index += 1; } }; + _output_compressed_accounts.push(PackedTokenTransferOutputData { + owner: output_compressed_accounts[i].owner, + amount: output_compressed_accounts[i].amount, + lamports: output_compressed_accounts[i].lamports, + merkle_tree_index: *remaining_accounts.get(&mt.merkle_tree).unwrap() as u8, + }); + } + ( + remaining_accounts, + input_token_data_with_context, + _output_compressed_accounts, + ) + } + + pub fn create_input_token_accounts( + input_token_data: &[TokenData], + remaining_accounts: &mut HashMap, + input_compressed_accounts: &[CompressedAccountWithMerkleContext], + index: &mut usize, + root_indices: &[u16], + input_token_data_with_context: &mut Vec, + ) { + for (i, token_data) in input_token_data.iter().enumerate() { + match remaining_accounts.get( + &input_compressed_accounts[i] + .merkle_context + .merkle_tree_pubkey, + ) { + Some(_) => {} + None => { + remaining_accounts.insert( + input_compressed_accounts[i] + .merkle_context + .merkle_tree_pubkey, + *index, + ); + *index += 1; + } + }; let delegate_index = match token_data.delegate { Some(delegate) => match remaining_accounts.get(&delegate) { Some(delegate_index) => Some(*delegate_index as u8), None => { - remaining_accounts.insert(delegate, index); - index += 1; - Some((index - 1) as u8) + remaining_accounts.insert(delegate, *index); + *index += 1; + Some((*index - 1) as u8) } }, None => None, }; - let lamports = if input_compressed_accounts[i].lamports != 0 { - Some(input_compressed_accounts[i].lamports) + let lamports = if input_compressed_accounts[i].compressed_account.lamports != 0 { + Some(input_compressed_accounts[i].compressed_account.lamports) } else { None }; @@ -864,54 +914,47 @@ pub mod transfer_sdk { delegate_index, merkle_context: PackedMerkleContext { merkle_tree_pubkey_index: *remaining_accounts - .get(&input_merkle_context[i].merkle_tree_pubkey) + .get( + &input_compressed_accounts[i] + .merkle_context + .merkle_tree_pubkey, + ) .unwrap() as u8, nullifier_queue_pubkey_index: 0, - leaf_index: input_merkle_context[i].leaf_index, + leaf_index: input_compressed_accounts[i].merkle_context.leaf_index, queue_index: None, }, root_index: root_indices[i], lamports, }; input_token_data_with_context.push(token_data_with_context); - } - for (i, _) in input_token_data.iter().enumerate() { - match remaining_accounts.get(&input_merkle_context[i].nullifier_queue_pubkey) { + + match remaining_accounts.get( + &input_compressed_accounts[i] + .merkle_context + .nullifier_queue_pubkey, + ) { Some(_) => {} None => { - remaining_accounts - .insert(input_merkle_context[i].nullifier_queue_pubkey, index); - index += 1; + remaining_accounts.insert( + input_compressed_accounts[i] + .merkle_context + .nullifier_queue_pubkey, + *index, + ); + *index += 1; } }; input_token_data_with_context[i] .merkle_context .nullifier_queue_pubkey_index = *remaining_accounts - .get(&input_merkle_context[i].nullifier_queue_pubkey) + .get( + &input_compressed_accounts[i] + .merkle_context + .nullifier_queue_pubkey, + ) .unwrap() as u8; } - let mut _output_compressed_accounts: Vec = - Vec::with_capacity(output_compressed_accounts.len()); - for (i, mt) in output_compressed_accounts.iter().enumerate() { - match remaining_accounts.get(&mt.merkle_tree) { - Some(_) => {} - None => { - remaining_accounts.insert(mt.merkle_tree, index); - index += 1; - } - }; - _output_compressed_accounts.push(PackedTokenTransferOutputData { - owner: output_compressed_accounts[i].owner, - amount: output_compressed_accounts[i].amount, - lamports: output_compressed_accounts[i].lamports, - merkle_tree_index: *remaining_accounts.get(&mt.merkle_tree).unwrap() as u8, - }); - } - ( - remaining_accounts, - input_token_data_with_context, - _output_compressed_accounts, - ) } pub fn to_account_metas(remaining_accounts: HashMap) -> Vec { diff --git a/programs/registry/Cargo.toml b/programs/registry/Cargo.toml index 90cc56fc5f..d4f9dcca36 100644 --- a/programs/registry/Cargo.toml +++ b/programs/registry/Cargo.toml @@ -20,16 +20,22 @@ mem-profiling = [] default = ["custom-heap", "mem-profiling"] test-sbf = [] bench-sbf = [] +sdk = [] [dependencies] aligned-sized = { version = "0.2.1", path = "../../macros/aligned-sized" } light-macros= { version = "0.4.1", path = "../../macros/light" } -anchor-lang = { workspace = true } +anchor-lang = { workspace = true , features = ["init-if-needed"]} +anchor-spl = { workspace = true } bytemuck = "1.16" light-hasher = { version = "0.2.1", path = "../../merkle-tree/hasher" } light-heap = { version = "0.2.1", path = "../../heap", optional = true } account-compression = { version = "0.4.1", path = "../account-compression", features = ["cpi"] } - +light-system-program = { version = "0.4.1", path = "../system", features = ["cpi"] } +light-compressed-token = { version = "0.4.1", path = "../compressed-token", features = ["cpi"] } +light-utils = { version = "0.2.1", path = "../../utils" } +num-bigint = "0.4.5" +num-traits = "0.2.19" [target.'cfg(not(target_os = "solana"))'.dependencies] solana-sdk = { workspace = true } log = "0.4" @@ -39,3 +45,4 @@ solana-program-test = { workspace = true } solana-sdk = { workspace = true } tokio = "1.38.0" light-macros= { version = "0.4.1", path = "../../macros/light" } +rand = "0.8.5" diff --git a/programs/registry/src/account_compression_cpi/access_control.rs b/programs/registry/src/account_compression_cpi/access_control.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/programs/registry/src/account_compression_cpi/initialize_tree_and_queue.rs b/programs/registry/src/account_compression_cpi/initialize_tree_and_queue.rs new file mode 100644 index 0000000000..7ea4eb284f --- /dev/null +++ b/programs/registry/src/account_compression_cpi/initialize_tree_and_queue.rs @@ -0,0 +1,91 @@ +use account_compression::{ + program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED, AddressMerkleTreeConfig, + AddressQueueConfig, NullifierQueueConfig, StateMerkleTreeConfig, +}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct InitializeMerkleTreeAndQueue<'info> { + /// Anyone can create new trees just the fees cannot be set arbitrarily. + #[account(mut)] + pub authority: Signer<'info>, + /// CHECK: + #[account(mut)] + pub merkle_tree: AccountInfo<'info>, + /// CHECK: + #[account(mut)] + pub queue: AccountInfo<'info>, + /// CHECK: + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: + #[account(mut)] + #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] + pub cpi_authority: AccountInfo<'info>, + pub account_compression_program: Program<'info, AccountCompression>, +} + +pub fn process_initialize_state_merkle_tree( + ctx: Context, + bump: u8, + index: u64, // TODO: replace with counter from pda + program_owner: Option, + merkle_tree_config: StateMerkleTreeConfig, // TODO: check config with protocol config + queue_config: NullifierQueueConfig, + additional_rent: u64, +) -> Result<()> { + let bump = &[bump]; + let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; + let signer_seeds = &[&seeds[..]]; + let accounts = account_compression::cpi::accounts::InitializeStateMerkleTreeAndNullifierQueue { + authority: ctx.accounts.cpi_authority.to_account_info(), + merkle_tree: ctx.accounts.merkle_tree.to_account_info(), + nullifier_queue: ctx.accounts.queue.to_account_info(), + registered_program_pda: Some(ctx.accounts.registered_program_pda.clone()), + }; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.account_compression_program.to_account_info(), + accounts, + signer_seeds, + ); + + account_compression::cpi::initialize_state_merkle_tree_and_nullifier_queue( + cpi_ctx, + index, + program_owner, + merkle_tree_config, + queue_config, + additional_rent, + ) +} + +pub fn process_initialize_address_merkle_tree( + ctx: Context, + bump: u8, + index: u64, // TODO: replace with counter from pda + program_owner: Option, + merkle_tree_config: AddressMerkleTreeConfig, // TODO: check config with protocol config + queue_config: AddressQueueConfig, +) -> Result<()> { + let bump = &[bump]; + let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; + let signer_seeds = &[&seeds[..]]; + let accounts = account_compression::cpi::accounts::InitializeAddressMerkleTreeAndQueue { + authority: ctx.accounts.cpi_authority.to_account_info(), + merkle_tree: ctx.accounts.merkle_tree.to_account_info(), + queue: ctx.accounts.queue.to_account_info(), + registered_program_pda: Some(ctx.accounts.registered_program_pda.clone()), + }; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.account_compression_program.to_account_info(), + accounts, + signer_seeds, + ); + + account_compression::cpi::initialize_address_merkle_tree_and_queue( + cpi_ctx, + index, + program_owner, + merkle_tree_config, + queue_config, + ) +} diff --git a/programs/registry/src/account_compression_cpi/mod.rs b/programs/registry/src/account_compression_cpi/mod.rs new file mode 100644 index 0000000000..355189dbae --- /dev/null +++ b/programs/registry/src/account_compression_cpi/mod.rs @@ -0,0 +1,6 @@ +pub mod initialize_tree_and_queue; +pub mod nullify; +pub mod register_program; +pub mod rollover_state_tree; +pub mod sdk; +pub mod update_address_tree; diff --git a/programs/registry/src/account_compression_cpi/nullify.rs b/programs/registry/src/account_compression_cpi/nullify.rs new file mode 100644 index 0000000000..722ce97fd5 --- /dev/null +++ b/programs/registry/src/account_compression_cpi/nullify.rs @@ -0,0 +1,64 @@ +use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; +use anchor_lang::prelude::*; + +use crate::epoch::register_epoch::ForesterEpochPda; + +#[derive(Accounts)] +pub struct NullifyLeaves<'info> { + /// CHECK: + #[account(mut)] + pub registered_forester_pda: Account<'info, ForesterEpochPda>, + /// CHECK: unchecked for now logic that regulates forester access is yet to be added. + pub authority: Signer<'info>, + /// CHECK: + #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] + pub cpi_authority: AccountInfo<'info>, + /// CHECK: + #[account( + seeds = [&crate::ID.to_bytes()], bump, seeds::program = &account_compression::ID, + )] + pub registered_program_pda: + Account<'info, account_compression::instructions::register_program::RegisteredProgram>, + pub account_compression_program: Program<'info, AccountCompression>, + /// CHECK: when emitting event. + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: in account compression program + #[account(mut)] + pub merkle_tree: AccountInfo<'info>, + /// CHECK: in account compression program + #[account(mut)] + pub nullifier_queue: AccountInfo<'info>, +} + +pub fn process_nullify( + ctx: Context, + bump: u8, + change_log_indices: Vec, + leaves_queue_indices: Vec, + indices: Vec, + proofs: Vec>, +) -> Result<()> { + let bump = &[bump]; + let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; + let signer_seeds = &[&seeds[..]]; + let accounts = account_compression::cpi::accounts::NullifyLeaves { + authority: ctx.accounts.cpi_authority.to_account_info(), + registered_program_pda: Some(ctx.accounts.registered_program_pda.to_account_info()), + log_wrapper: ctx.accounts.log_wrapper.to_account_info(), + merkle_tree: ctx.accounts.merkle_tree.to_account_info(), + nullifier_queue: ctx.accounts.nullifier_queue.to_account_info(), + }; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.account_compression_program.to_account_info(), + accounts, + signer_seeds, + ); + + account_compression::cpi::nullify_leaves( + cpi_ctx, + change_log_indices, + leaves_queue_indices, + indices, + proofs, + ) +} diff --git a/programs/registry/src/account_compression_cpi/register_program.rs b/programs/registry/src/account_compression_cpi/register_program.rs new file mode 100644 index 0000000000..32c774538a --- /dev/null +++ b/programs/registry/src/account_compression_cpi/register_program.rs @@ -0,0 +1,27 @@ +use account_compression::{ + program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED, GroupAuthority, +}; +use anchor_lang::prelude::*; + +use crate::{protocol_config::state::ProtocolConfigPda, AUTHORITY_PDA_SEED}; + +#[derive(Accounts)] +pub struct RegisteredProgram<'info> { + #[account(mut, constraint = authority.key() == authority_pda.authority)] + pub authority: Signer<'info>, + /// CHECK: + #[account(mut, seeds = [AUTHORITY_PDA_SEED], bump)] + pub authority_pda: Account<'info, ProtocolConfigPda>, + /// CHECK: this is + #[account(mut, seeds = [CPI_AUTHORITY_PDA_SEED], bump)] + pub cpi_authority: AccountInfo<'info>, + #[account(mut)] + pub group_pda: Account<'info, GroupAuthority>, + pub account_compression_program: Program<'info, AccountCompression>, + pub system_program: Program<'info, System>, + /// CHECK: + #[account(mut)] + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: is checked in the account compression program. + pub program_to_be_registered: Signer<'info>, +} diff --git a/programs/registry/src/account_compression_cpi/rollover_state_tree.rs b/programs/registry/src/account_compression_cpi/rollover_state_tree.rs new file mode 100644 index 0000000000..5347330a85 --- /dev/null +++ b/programs/registry/src/account_compression_cpi/rollover_state_tree.rs @@ -0,0 +1,89 @@ +use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; +use anchor_lang::prelude::*; + +use crate::epoch::register_epoch::ForesterEpochPda; + +#[derive(Accounts)] +pub struct RolloverMerkleTreeAndQueue<'info> { + /// CHECK: + #[account(mut)] + pub registered_forester_pda: Account<'info, ForesterEpochPda>, + /// CHECK: unchecked for now logic that regulates forester access is yet to be added. + #[account(mut)] + pub authority: Signer<'info>, + /// CHECK: + #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] + pub cpi_authority: AccountInfo<'info>, + /// CHECK: + #[account( + seeds = [&crate::ID.to_bytes()], bump, seeds::program = &account_compression::ID, + )] + pub registered_program_pda: + Account<'info, account_compression::instructions::register_program::RegisteredProgram>, + pub account_compression_program: Program<'info, AccountCompression>, + /// CHECK: + #[account(zero)] + pub new_merkle_tree: AccountInfo<'info>, + /// CHECK: + #[account(zero)] + pub new_queue: AccountInfo<'info>, + /// CHECK: + #[account(mut)] + pub old_merkle_tree: AccountInfo<'info>, + /// CHECK: + #[account(mut)] + pub old_queue: AccountInfo<'info>, +} + +pub fn process_rollover_address_merkle_tree_and_queue( + ctx: Context, + bump: u8, +) -> Result<()> { + let bump = &[bump]; + + let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; + let signer_seeds = &[&seeds[..]]; + + let accounts = account_compression::cpi::accounts::RolloverAddressMerkleTreeAndQueue { + fee_payer: ctx.accounts.authority.to_account_info(), + authority: ctx.accounts.cpi_authority.to_account_info(), + registered_program_pda: Some(ctx.accounts.registered_program_pda.to_account_info()), + new_address_merkle_tree: ctx.accounts.new_merkle_tree.to_account_info(), + new_queue: ctx.accounts.new_queue.to_account_info(), + old_address_merkle_tree: ctx.accounts.old_merkle_tree.to_account_info(), + old_queue: ctx.accounts.old_queue.to_account_info(), + }; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.account_compression_program.to_account_info(), + accounts, + signer_seeds, + ); + + account_compression::cpi::rollover_address_merkle_tree_and_queue(cpi_ctx) +} +pub fn process_rollover_state_merkle_tree_and_queue( + ctx: Context, + bump: u8, +) -> Result<()> { + let bump = &[bump]; + + let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; + let signer_seeds = &[&seeds[..]]; + + let accounts = account_compression::cpi::accounts::RolloverStateMerkleTreeAndNullifierQueue { + fee_payer: ctx.accounts.authority.to_account_info(), + authority: ctx.accounts.cpi_authority.to_account_info(), + registered_program_pda: Some(ctx.accounts.registered_program_pda.to_account_info()), + new_state_merkle_tree: ctx.accounts.new_merkle_tree.to_account_info(), + new_nullifier_queue: ctx.accounts.new_queue.to_account_info(), + old_state_merkle_tree: ctx.accounts.old_merkle_tree.to_account_info(), + old_nullifier_queue: ctx.accounts.old_queue.to_account_info(), + }; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.account_compression_program.to_account_info(), + accounts, + signer_seeds, + ); + + account_compression::cpi::rollover_state_merkle_tree_and_nullifier_queue(cpi_ctx) +} diff --git a/programs/registry/src/account_compression_cpi/sdk.rs b/programs/registry/src/account_compression_cpi/sdk.rs new file mode 100644 index 0000000000..462c0b5d95 --- /dev/null +++ b/programs/registry/src/account_compression_cpi/sdk.rs @@ -0,0 +1,242 @@ +#![cfg(not(target_os = "solana"))] +use crate::{ + sdk::NOOP_PROGRAM_ID, + utils::{get_cpi_authority_pda, get_forester_epoch_pda_address, get_forester_pda_address}, +}; +use account_compression::{ + AddressMerkleTreeConfig, AddressQueueConfig, NullifierQueueConfig, StateMerkleTreeConfig, +}; +use anchor_lang::prelude::*; +use anchor_lang::InstructionData; +use solana_sdk::instruction::Instruction; + +pub struct CreateNullifyInstructionInputs { + pub authority: Pubkey, + pub nullifier_queue: Pubkey, + pub merkle_tree: Pubkey, + pub change_log_indices: Vec, + pub leaves_queue_indices: Vec, + pub indices: Vec, + pub proofs: Vec>, + pub derivation: Pubkey, +} + +pub fn create_nullify_instruction( + inputs: CreateNullifyInstructionInputs, + epoch: u64, +) -> Instruction { + let register_program_pda = get_registered_program_pda(&crate::ID); + let (forester_pda, _) = get_forester_pda_address(&inputs.derivation); + let registered_forester_pda = get_forester_epoch_pda_address(&forester_pda, epoch).0; + log::info!("registered_forester_pda: {:?}", registered_forester_pda); + let (cpi_authority, bump) = get_cpi_authority_pda(); + let instruction_data = crate::instruction::Nullify { + bump, + change_log_indices: inputs.change_log_indices, + leaves_queue_indices: inputs.leaves_queue_indices, + indices: inputs.indices, + proofs: inputs.proofs, + }; + + let accounts = crate::accounts::NullifyLeaves { + authority: inputs.authority, + registered_forester_pda, + registered_program_pda: register_program_pda, + nullifier_queue: inputs.nullifier_queue, + merkle_tree: inputs.merkle_tree, + log_wrapper: NOOP_PROGRAM_ID, + cpi_authority, + account_compression_program: account_compression::ID, + }; + Instruction { + program_id: crate::ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction_data.data(), + } +} + +pub fn get_registered_program_pda(program_id: &Pubkey) -> Pubkey { + Pubkey::find_program_address( + &[program_id.to_bytes().as_slice()], + &account_compression::ID, + ) + .0 +} + +pub struct CreateRolloverMerkleTreeInstructionInputs { + pub authority: Pubkey, + pub new_queue: Pubkey, + pub new_merkle_tree: Pubkey, + pub old_queue: Pubkey, + pub old_merkle_tree: Pubkey, +} + +pub fn create_rollover_address_merkle_tree_instruction( + inputs: CreateRolloverMerkleTreeInstructionInputs, + epoch: u64, +) -> Instruction { + let (_, bump) = get_cpi_authority_pda(); + + let instruction_data = crate::instruction::RolloverAddressMerkleTreeAndQueue { bump }; + create_rollover_instruction(instruction_data.data(), inputs, epoch) +} + +pub fn create_rollover_state_merkle_tree_instruction( + inputs: CreateRolloverMerkleTreeInstructionInputs, + epoch: u64, +) -> Instruction { + let (_, bump) = get_cpi_authority_pda(); + + let instruction_data = crate::instruction::RolloverStateMerkleTreeAndQueue { bump }; + create_rollover_instruction(instruction_data.data(), inputs, epoch) +} + +pub fn create_rollover_instruction( + data: Vec, + inputs: CreateRolloverMerkleTreeInstructionInputs, + epoch: u64, +) -> Instruction { + let (cpi_authority, _) = get_cpi_authority_pda(); + let registered_program_pda = get_registered_program_pda(&crate::ID); + let (forester_pda, _) = get_forester_pda_address(&inputs.authority); + let registered_forester_pda = get_forester_epoch_pda_address(&forester_pda, epoch).0; + let accounts = crate::accounts::RolloverMerkleTreeAndQueue { + account_compression_program: account_compression::ID, + registered_forester_pda, + cpi_authority, + authority: inputs.authority, + registered_program_pda, + new_merkle_tree: inputs.new_merkle_tree, + new_queue: inputs.new_queue, + old_merkle_tree: inputs.old_merkle_tree, + old_queue: inputs.old_queue, + }; + + Instruction { + program_id: crate::ID, + accounts: accounts.to_account_metas(Some(true)), + data, + } +} + +pub struct UpdateAddressMerkleTreeInstructionInputs { + pub authority: Pubkey, + pub address_merkle_tree: Pubkey, + pub address_queue: Pubkey, + pub changelog_index: u16, + pub indexed_changelog_index: u16, + pub value: u16, + pub low_address_index: u64, + pub low_address_value: [u8; 32], + pub low_address_next_index: u64, + pub low_address_next_value: [u8; 32], + pub low_address_proof: [[u8; 32]; 16], +} + +pub fn create_update_address_merkle_tree_instruction( + instructions: UpdateAddressMerkleTreeInstructionInputs, + epoch: u64, +) -> Instruction { + let register_program_pda = get_registered_program_pda(&crate::ID); + let (forester_pda, _) = get_forester_pda_address(&instructions.authority); + let registered_forester_pda = get_forester_epoch_pda_address(&forester_pda, epoch).0; + + let (cpi_authority, bump) = get_cpi_authority_pda(); + let instruction_data = crate::instruction::UpdateAddressMerkleTree { + bump, + changelog_index: instructions.changelog_index, + indexed_changelog_index: instructions.indexed_changelog_index, + value: instructions.value, + low_address_index: instructions.low_address_index, + low_address_value: instructions.low_address_value, + low_address_next_index: instructions.low_address_next_index, + low_address_next_value: instructions.low_address_next_value, + low_address_proof: instructions.low_address_proof, + }; + + let accounts = crate::accounts::UpdateAddressMerkleTree { + authority: instructions.authority, + registered_forester_pda, + registered_program_pda: register_program_pda, + merkle_tree: instructions.address_merkle_tree, + queue: instructions.address_queue, + log_wrapper: NOOP_PROGRAM_ID, + cpi_authority, + account_compression_program: account_compression::ID, + }; + Instruction { + program_id: crate::ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction_data.data(), + } +} + +pub fn create_initialize_address_merkle_tree_and_queue_instruction( + index: u64, + payer: Pubkey, + program_owner: Option, + merkle_tree_pubkey: Pubkey, + queue_pubkey: Pubkey, + address_merkle_tree_config: AddressMerkleTreeConfig, + address_queue_config: AddressQueueConfig, +) -> Instruction { + let register_program_pda = get_registered_program_pda(&crate::ID); + let (cpi_authority, bump) = get_cpi_authority_pda(); + + let instruction_data = crate::instruction::InitializeAddressMerkleTree { + bump, + index, + program_owner, + merkle_tree_config: address_merkle_tree_config, + queue_config: address_queue_config, + }; + let accounts = crate::accounts::InitializeMerkleTreeAndQueue { + authority: payer, + registered_program_pda: register_program_pda, + merkle_tree: merkle_tree_pubkey, + queue: queue_pubkey, + cpi_authority, + account_compression_program: account_compression::ID, + }; + Instruction { + program_id: crate::ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction_data.data(), + } +} + +pub fn create_initialize_merkle_tree_instruction( + payer: Pubkey, + merkle_tree_pubkey: Pubkey, + nullifier_queue_pubkey: Pubkey, + state_merkle_tree_config: StateMerkleTreeConfig, + nullifier_queue_config: NullifierQueueConfig, + program_owner: Option, + index: u64, + additional_rent: u64, +) -> Instruction { + let register_program_pda = get_registered_program_pda(&crate::ID); + let (cpi_authority, bump) = get_cpi_authority_pda(); + + let instruction_data = crate::instruction::InitializeStateMerkleTree { + bump, + index, + program_owner, + merkle_tree_config: state_merkle_tree_config, + queue_config: nullifier_queue_config, + additional_rent, + }; + let accounts = crate::accounts::InitializeMerkleTreeAndQueue { + authority: payer, + registered_program_pda: register_program_pda, + merkle_tree: merkle_tree_pubkey, + queue: nullifier_queue_pubkey, + cpi_authority, + account_compression_program: account_compression::ID, + }; + Instruction { + program_id: crate::ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction_data.data(), + } +} diff --git a/programs/registry/src/account_compression_cpi/update_address_tree.rs b/programs/registry/src/account_compression_cpi/update_address_tree.rs new file mode 100644 index 0000000000..64b5fde0da --- /dev/null +++ b/programs/registry/src/account_compression_cpi/update_address_tree.rs @@ -0,0 +1,73 @@ +use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; +use anchor_lang::prelude::*; + +use crate::epoch::register_epoch::ForesterEpochPda; + +#[derive(Accounts)] +pub struct UpdateAddressMerkleTree<'info> { + /// CHECK: + #[account(mut)] + pub registered_forester_pda: Account<'info, ForesterEpochPda>, + /// CHECK: unchecked for now logic that regulates forester access is yet to be added. + pub authority: Signer<'info>, + /// CHECK: + #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] + pub cpi_authority: AccountInfo<'info>, + /// CHECK: + #[account( + seeds = [&crate::ID.to_bytes()], bump, seeds::program = &account_compression::ID, + )] + pub registered_program_pda: + Account<'info, account_compression::instructions::register_program::RegisteredProgram>, + pub account_compression_program: Program<'info, AccountCompression>, + /// CHECK: in account compression program + #[account(mut)] + pub queue: AccountInfo<'info>, + /// CHECK: in account compression program + #[account(mut)] + pub merkle_tree: AccountInfo<'info>, + /// CHECK: when emitting event. + pub log_wrapper: UncheckedAccount<'info>, +} + +pub fn process_update_address_merkle_tree( + ctx: Context, + bump: u8, + changelog_index: u16, + indexed_changelog_index: u16, + value: u16, + low_address_index: u64, + low_address_value: [u8; 32], + low_address_next_index: u64, + low_address_next_value: [u8; 32], + low_address_proof: [[u8; 32]; 16], +) -> Result<()> { + let bump = &[bump]; + let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; + let signer_seeds = &[&seeds[..]]; + + let accounts = account_compression::cpi::accounts::UpdateAddressMerkleTree { + authority: ctx.accounts.cpi_authority.to_account_info(), + registered_program_pda: Some(ctx.accounts.registered_program_pda.to_account_info()), + log_wrapper: ctx.accounts.log_wrapper.to_account_info(), + queue: ctx.accounts.queue.to_account_info(), + merkle_tree: ctx.accounts.merkle_tree.to_account_info(), + }; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.account_compression_program.to_account_info(), + accounts, + signer_seeds, + ); + + account_compression::cpi::update_address_merkle_tree( + cpi_ctx, + changelog_index, + indexed_changelog_index, + value, + low_address_index, + low_address_value, + low_address_next_index, + low_address_next_value, + low_address_proof, + ) +} diff --git a/programs/registry/src/decentralization_and_contention.rs b/programs/registry/src/decentralization_and_contention.rs new file mode 100644 index 0000000000..9bd5780280 --- /dev/null +++ b/programs/registry/src/decentralization_and_contention.rs @@ -0,0 +1,284 @@ +use crate::errors::RegistryError; +use crate::protocol_config::state::ProtocolConfig; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::pubkey::Pubkey; +use light_hasher::Hasher; +use light_utils::hash_to_bn254_field_size_be; + +/// Simulate work and slots for testing purposes. +/// 1. Check eligibility of forester +pub fn simulate_work_and_slots_instruction( + forester_epoch_pda: &mut ForesterEpochPda, + num_work: u64, + queue_pubkey: &Pubkey, + current_slot: &u64, +) -> Result<()> { + forester_epoch_pda + .protocol_config + .is_active_phase(*current_slot, forester_epoch_pda.epoch)?; + forester_epoch_pda.check_eligibility(*current_slot, queue_pubkey)?; + forester_epoch_pda.work_counter += num_work; + Ok(()) +} + +pub struct MockCompressedTokenAccount { + pub balance: u64, +} + +pub struct MockSplTokenAccount { + pub balance: u64, +} + +#[cfg(test)] +mod tests { + use super::*; + use solana_sdk::{signature::Keypair, signer::Signer}; + use std::collections::HashMap; + + /// Scenario: + /// 1. Protocol config setup + /// 2. User stakes 1000 tokens + /// 3. Forester creates a stake account + /// 4. User delegates 1000 tokens to forester + /// 5. Forester registers for epoch + /// 6. Forester performs some actions + /// 7. Tally results + /// 8. Forester withdraws rewards + /// 9. User syncs stake account + #[test] + fn staking_scenario() { + let protocol_config = ProtocolConfig { + genesis_slot: 20, + registration_phase_length: 1, + active_phase_length: 7, + report_work_phase_length: 2, + epoch_reward: 100_000, + base_reward: 50_000, + min_stake: 0, + slot_length: 1, + mint: Pubkey::new_unique(), + }; + let mut current_solana_slot = protocol_config.genesis_slot; + + // ---------------------------------------------------------------------------------------- + // 2. User stakes 1000 tokens + // - token transfer from compressed token account to compressed token staking account + // - the compressed token staking account stores all staked tokens + // - staked tokens are not automatically delegated + // - user_delegate_account stake_weight is increased by the added stake amount + let mut user_delegate_account = DelegateAccount::default(); + let user_token_balance = 1000; + let mut user_stake_token_account = MockCompressedTokenAccount { balance: 0 }; + let mut user_token_account = MockCompressedTokenAccount { balance: 1000 }; + stake_instruction( + &mut user_delegate_account, + user_token_balance, + &mut user_token_account, + &mut user_stake_token_account, + ) + .unwrap(); + + // ---------------------------------------------------------------------------------------- + // 3. Forester creates a stake and token accounts + // - forester_token_pool_account is intermediate storage for staking rewards that have not been synced to the user token staking accounts yet + // - is owned by a pda derived from the forester pubkey + // - forester_fee_token_account recipient for forester fees + // - forester_pda + let forester_pubkey = Pubkey::new_unique(); + let forester_delegate_account_pubkey = Pubkey::new_unique(); + let mut forester_pda = ForesterAccount { + forester_config: ForesterConfig { + forester_pubkey, + fee: 10, + }, + ..ForesterAccount::default() + }; + // Forester fee rewards go to this compressed token account. + let mut forester_fee_token_account = MockCompressedTokenAccount { balance: 0 }; + // Is an spl token account since it will need to be accessed by many parties. + // Compressed token accounts are not well suited to be pool accounts. + let mut forester_token_pool_account = MockSplTokenAccount { balance: 0 }; + + // ---------------------------------------------------------------------------------------- + // 4. User delegates 1000 tokens to forester + // - delegated stake is not active until the next epoch + // - this is enforced by adding the delegated stake to forester_pda pending stake weight + // - forester_pda pending stake weight is synced to active stake weight once the epoch changes + // - delegated stake is stored in the user_delegate_account delegated_stake_weight + delegate_instruction( + &protocol_config, + &mut user_delegate_account, + &forester_delegate_account_pubkey, + &mut forester_pda, + user_token_balance, + current_solana_slot, + true, + ) + .unwrap(); + assert_eq!( + forester_pda.pending_undelegated_stake_weight, + user_delegate_account.delegated_stake_weight + ); + assert_eq!( + forester_delegate_account_pubkey, + user_delegate_account + .delegate_forester_delegate_account + .unwrap() + ); + // ---------------------------------------------------------------------------------------- + // Registration phase starts (epoch 1) + // Active phase starts (epoch 1) + // (We need to start in epoch 1, because nobody can register before epoch 0) + current_solana_slot = 20 + protocol_config.active_phase_length; + // ---------------------------------------------------------------------------------------- + // 5. Forester registers for epoch and initializes epoch account if + // needed + // - epoch account is initialized if not already initialized + // - forester epoch account is initialized with values from forester + // stake account and epoch account + let mut epoch_pda = EpochPda::default(); + let mut forester_epoch_pda = ForesterEpochPda::default(); + register_for_epoch_instruction( + &protocol_config, + &mut forester_pda, + &mut forester_epoch_pda, + &mut epoch_pda, + current_solana_slot, + ) + .unwrap(); + assert_eq!(forester_pda.pending_undelegated_stake_weight, 0); + assert_eq!(forester_epoch_pda.stake_weight, user_token_balance); + assert_eq!(epoch_pda.registered_stake, user_token_balance); + assert_eq!(forester_epoch_pda.epoch_start_slot, 28); + assert_eq!(forester_epoch_pda.epoch, 1); + // ---------------------------------------------------------------------------------------- + // Registration phase ends (epoch 1) + // Active phase starts (epoch 1) + current_solana_slot += protocol_config.registration_phase_length; + assert!(protocol_config + .is_registration_phase(current_solana_slot) + .is_err()); + // ---------------------------------------------------------------------------------------- + // 6. Forester performs some actions until epoch ends + set_total_registered_stake_instruction(&mut forester_epoch_pda, &epoch_pda); + simulate_work_and_slots_instruction( + &mut forester_epoch_pda, + protocol_config.active_phase_length - 1, + &Pubkey::new_unique(), + ¤t_solana_slot, + ) + .unwrap(); + + // ---------------------------------------------------------------------------------------- + // Active phase ends (epoch 1) + // Active phase starts (epoch 2) + current_solana_slot += protocol_config.active_phase_length; + assert!(protocol_config + .is_active_phase(current_solana_slot, 1) + .is_err()); + assert!(protocol_config + .is_active_phase(current_solana_slot, 2) + .is_ok()); + assert!(protocol_config + .is_report_work_phase(current_solana_slot, 1) + .is_ok()); + // ---------------------------------------------------------------------------------------- + // 7. Report work from active epoch phase + report_work_instruction(&mut forester_epoch_pda, &mut epoch_pda, current_solana_slot) + .unwrap(); + + // ---------------------------------------------------------------------------------------- + // Report work phase ends (epoch 1) + current_solana_slot += protocol_config.report_work_phase_length; + assert!(protocol_config + .is_report_work_phase(current_solana_slot, 1) + .is_err()); + assert!(protocol_config + .is_post_epoch(current_solana_slot, 1) + .is_ok()); + // ---------------------------------------------------------------------------------------- + // 8. Forester claim rewards (post epoch) + let compressed_forester_epoch_pda = forester_claim_rewards_instruction( + &mut forester_fee_token_account, + &mut forester_token_pool_account, + &mut forester_pda, + &mut forester_epoch_pda, + &mut epoch_pda, + current_solana_slot, + ) + .unwrap(); + let forester_fee = 10_000; + assert_eq!(forester_fee_token_account.balance, forester_fee); + assert_eq!( + forester_token_pool_account.balance, + protocol_config.epoch_reward - forester_fee + ); + + // ---------------------------------------------------------------------------------------- + // 9. User syncs stake account and syncs stake token account + let hashed_forester_pubkey = hash_to_bn254_field_size_be(&forester_pubkey.to_bytes()) + .unwrap() + .0; + let compressed_forester_epoch_pda_input_account = CompressedForesterEpochAccountInput { + rewards_earned: compressed_forester_epoch_pda.rewards_earned, + epoch: compressed_forester_epoch_pda.epoch, + stake_weight: compressed_forester_epoch_pda.stake_weight, + }; + sync_delegate_account_instruction( + &mut user_delegate_account, + vec![compressed_forester_epoch_pda_input_account], + hashed_forester_pubkey, + compressed_forester_epoch_pda.previous_hash, + ) + .unwrap(); + + assert_eq!( + user_delegate_account.delegated_stake_weight, + user_token_balance + compressed_forester_epoch_pda.rewards_earned + ); + assert_eq!(user_delegate_account.pending_token_amount, 90_000); + + sync_token_account_instruction( + &mut forester_token_pool_account, + &mut user_stake_token_account, + &mut user_delegate_account, + ); + assert_eq!(user_stake_token_account.balance, 1000 + 90_000); + assert_eq!(forester_token_pool_account.balance, 0); + assert_eq!(user_delegate_account.pending_token_amount, 0); + + // ---------------------------------------------------------------------------------------- + // 10. User undelegates and unstakes + let mut recipient_token_account = MockCompressedTokenAccount { balance: 0 }; + let unstake_amount = 100; + undelegate_instruction( + &protocol_config, + &mut user_delegate_account, + &mut forester_pda, + unstake_amount, + current_solana_slot, + ) + .unwrap(); + assert_eq!( + user_delegate_account.pending_undelegated_stake_weight, + unstake_amount + ); + assert_eq!( + forester_pda.active_stake_weight, + 900 + protocol_config.epoch_reward - forester_fee + ); + + unstake_instruction( + &mut user_delegate_account, + &mut user_stake_token_account, + &mut recipient_token_account, + protocol_config, + unstake_amount, + current_solana_slot, + ) + .unwrap(); + assert_eq!(user_delegate_account.delegated_stake_weight, 90900); + assert_eq!(user_stake_token_account.balance, 90900); + assert_eq!(recipient_token_account.balance, unstake_amount); + } +} diff --git a/programs/registry/src/delegate/delegate_instruction.rs b/programs/registry/src/delegate/delegate_instruction.rs new file mode 100644 index 0000000000..9884c62f77 --- /dev/null +++ b/programs/registry/src/delegate/delegate_instruction.rs @@ -0,0 +1,87 @@ +use crate::{protocol_config::state::ProtocolConfigPda, ForesterAccount}; + +use super::traits::{CompressedCpiContextTrait, SignerAccounts, SystemProgramAccounts}; +use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; +use anchor_lang::prelude::*; +use light_system_program::program::LightSystemProgram; + +#[derive(Accounts)] +pub struct DelegatetOrUndelegateInstruction<'info> { + /// Fee payer needs to be mutable to pay rollover and protocol fees. + #[account(mut)] + pub fee_payer: Signer<'info>, + pub authority: Signer<'info>, + /// CHECK: + #[account( + seeds = [CPI_AUTHORITY_PDA_SEED], bump + )] + pub cpi_authority: AccountInfo<'info>, + pub protocol_config: Account<'info, ProtocolConfigPda>, + #[account(mut)] + pub forester_pda: Account<'info, ForesterAccount>, + /// CHECK: + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: checked in emit_event.rs. + pub noop_program: AccountInfo<'info>, + /// CHECK: + pub account_compression_authority: AccountInfo<'info>, + /// CHECK: + pub account_compression_program: Program<'info, AccountCompression>, + /// CHECK: checked in cpi_signer_check. + pub invoking_program: AccountInfo<'info>, + /// CHECK: + pub system_program: AccountInfo<'info>, + // /// CHECK: + // #[account(mut)] + // pub cpi_context_account: AccountInfo<'info>, + pub self_program: Program<'info, crate::program::LightRegistry>, + pub light_system_program: Program<'info, LightSystemProgram>, +} + +impl<'info> SystemProgramAccounts<'info> for DelegatetOrUndelegateInstruction<'info> { + fn get_registered_program_pda(&self) -> AccountInfo<'info> { + self.registered_program_pda.to_account_info() + } + fn get_noop_program(&self) -> AccountInfo<'info> { + self.noop_program.to_account_info() + } + fn get_account_compression_authority(&self) -> AccountInfo<'info> { + self.account_compression_authority.to_account_info() + } + fn get_account_compression_program(&self) -> AccountInfo<'info> { + self.account_compression_program.to_account_info() + } + fn get_system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + fn get_sol_pool_pda(&self) -> Option> { + None + } + fn get_decompression_recipient(&self) -> Option> { + None + } + fn get_light_system_program(&self) -> AccountInfo<'info> { + self.light_system_program.to_account_info() + } + fn get_self_program(&self) -> AccountInfo<'info> { + self.invoking_program.to_account_info() + } +} + +impl<'info> SignerAccounts<'info> for DelegatetOrUndelegateInstruction<'info> { + fn get_fee_payer(&self) -> AccountInfo<'info> { + self.fee_payer.to_account_info() + } + fn get_authority(&self) -> AccountInfo<'info> { + self.authority.to_account_info() + } + fn get_cpi_authority_pda(&self) -> AccountInfo<'info> { + self.cpi_authority.to_account_info() + } +} + +impl<'info> CompressedCpiContextTrait<'info> for DelegatetOrUndelegateInstruction<'info> { + fn get_cpi_context(&self) -> Option> { + None + } +} diff --git a/programs/registry/src/delegate/deposit.rs b/programs/registry/src/delegate/deposit.rs new file mode 100644 index 0000000000..bd2ca92d1c --- /dev/null +++ b/programs/registry/src/delegate/deposit.rs @@ -0,0 +1,1222 @@ +use crate::errors::RegistryError; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::pubkey::Pubkey; +use light_compressed_token::{ + process_transfer::{InputTokenDataWithContext, PackedTokenTransferOutputData}, + TokenData, +}; +use light_hasher::{errors::HasherError, DataHasher, Poseidon}; +use light_system_program::sdk::compressed_account::{CompressedAccount, MerkleContext}; +use light_system_program::{ + invoke::processor::CompressedProof, + sdk::{ + compressed_account::{ + CompressedAccountData, PackedCompressedAccountWithMerkleContext, PackedMerkleContext, + }, + CompressedCpiContext, + }, + OutputCompressedAccountWithPackedContext, +}; +use light_utils::hash_to_bn254_field_size_be; + +use super::{ + deposit_instruction::DepositOrWithdrawInstruction, + get_escrow_token_authority, + process_cpi::{cpi_compressed_token_transfer, cpi_light_system_program}, + state::{DelegateAccount, InputDelegateAccount}, + DELEGATE_ACCOUNT_DISCRIMINATOR, ESCROW_TOKEN_ACCOUNT_SEED, +}; + +pub struct DepositCompressedAccounts { + pub output_token_accounts: Vec, + pub input_delegate_pda: Option, + pub output_delegate_pda: OutputCompressedAccountWithPackedContext, +} + +pub fn process_deposit_or_withdrawal<'a, 'b, 'c, 'info: 'b + 'c, const IS_DEPOSIT: bool>( + ctx: Context<'a, 'b, 'c, 'info, DepositOrWithdrawInstruction<'info>>, + salt: u64, + proof: CompressedProof, + cpi_context: CompressedCpiContext, + delegate_account: Option, + deposit_amount: u64, + mut input_compressed_token_accounts: Vec, + input_escrow_token_account: Option, + escrow_token_account_merkle_tree_index: u8, + change_compressed_account_merkle_tree_index: u8, + output_delegate_compressed_account_merkle_tree_index: u8, +) -> Result<()> { + // if !IS_DEPOSIT { + // let slot = Clock::get()?.slot; + // let epoch = ctx.accounts.protocol_config.config.get_current_epoch(slot); + // delegate_account + // .as_ref() + // .unwrap() + // .delegate_account + // .sync_pending_stake_weight(epoch); + // } + let mint = &ctx.accounts.protocol_config.config.mint; + let slot = Clock::get()?.slot; + let epoch = ctx.accounts.protocol_config.config.get_current_epoch(slot); + let compressed_accounts = deposit_or_withdraw::( + &ctx.accounts.authority.key(), + &ctx.accounts.escrow_token_authority.key(), + mint, + delegate_account, + deposit_amount, + &input_compressed_token_accounts, + &input_escrow_token_account, + escrow_token_account_merkle_tree_index, + change_compressed_account_merkle_tree_index, + output_delegate_compressed_account_merkle_tree_index, + epoch, + )?; + + if let Some(input_escrow_token_account) = input_escrow_token_account { + input_compressed_token_accounts.push(input_escrow_token_account); + } + let system_cpi_context = CompressedCpiContext { + set_context: true, + ..cpi_context + }; + cpi_light_system_program( + &ctx, + None, + Some(system_cpi_context), + compressed_accounts.input_delegate_pda, + compressed_accounts.output_delegate_pda, + ctx.remaining_accounts.to_vec(), + )?; + let owner = ctx.accounts.authority.key(); + let (_, bump) = get_escrow_token_authority(&owner, salt); + let bump = &[bump]; + let salt_bytes = salt.to_le_bytes(); + let seeds = [ + ESCROW_TOKEN_ACCOUNT_SEED, + owner.as_ref(), + salt_bytes.as_slice(), + bump, + ]; + let mut cpi_context = cpi_context; + cpi_context.first_set_context = false; + cpi_compressed_token_transfer( + &ctx, + Some(proof), + None, + false, + salt, + cpi_context, + mint, + input_compressed_token_accounts, + compressed_accounts.output_token_accounts, + &owner, + ctx.accounts.escrow_token_authority.to_account_info(), + seeds, + ctx.remaining_accounts.to_vec(), + ) +} + +// TODO: assert that escrow token account and delegate account sums match, all +// stakeweight or delegated tokens need to be in the escrow account +// TODO: require the token account to be last synced in the current epoch +// TODO: throw if inputs have a delegate +/// Deposit to a DelegateAccount +/// 1. Deposit compressed tokens to DelegatePda +/// inputs: InputTokenData, deposit_amount +/// create two outputs, escrow compressed account and change account +/// compressed escrow account is owned by pda derived from authority +pub fn deposit_or_withdraw( + authority: &Pubkey, + escrow_token_authority: &Pubkey, + // get from ProtocolConfig + mint: &Pubkey, + // If None create new delegate account + delegate_account: Option, + deposit_amount: u64, + input_compressed_token_accounts: &[InputTokenDataWithContext], + // Input escrow token account is linked as its hash is part of the + // DelegateAccount. + input_escrow_token_account: &Option, + escrow_token_account_merkle_tree_index: u8, + change_compressed_account_merkle_tree_index: u8, + output_delegate_compressed_account_merkle_tree_index: u8, + epoch: u64, +) -> Result { + if delegate_account.is_some() && input_escrow_token_account.is_none() + || delegate_account.is_none() && input_escrow_token_account.is_some() + { + msg!("Delegate account and escrow token account must be provided together"); + return Err(RegistryError::InputEscrowTokenHashNotProvided.into()); + } + if !IS_DEPOSIT && input_escrow_token_account.is_none() { + msg!("An input compressed escrow token account is required for withdrawal"); + return Err(RegistryError::InputEscrowTokenHashNotProvided.into()); + } + let hashed_owner = hash_to_bn254_field_size_be(authority.as_ref()).unwrap().0; + let hashed_mint = hash_to_bn254_field_size_be(mint.as_ref()).unwrap().0; + let hashed_escrow_token_authority = + hash_to_bn254_field_size_be(escrow_token_authority.as_ref()) + .unwrap() + .0; + + let sum_inputs = if IS_DEPOSIT { + let sum_inputs = input_compressed_token_accounts + .iter() + .map(|x| x.amount) + .sum::(); + if sum_inputs != deposit_amount { + msg!( + "Deposit amount does not match sum of input token accounts: {} != {}", + deposit_amount, + sum_inputs + ); + return Err(RegistryError::DepositAmountNotEqualInputAmount.into()); + } + sum_inputs + } else { + 0 + }; + + let output_escrow_token_account = update_escrow_compressed_token_account::( + escrow_token_authority, + input_escrow_token_account, + deposit_amount, + escrow_token_account_merkle_tree_index, + )?; + let input_escrow_token_account_hash = + if let Some(input_escrow_token_account) = input_escrow_token_account.as_ref() { + Some( + hash_input_token_data_with_context( + &hashed_mint, + &hashed_owner, + input_escrow_token_account.amount, + ) + .map_err(ProgramError::from)?, + ) + } else { + None + }; + let output_bytes = output_escrow_token_account.amount.to_le_bytes(); + let output_compressed_token_hash = TokenData::hash_with_hashed_values::( + &hashed_mint, + &hashed_escrow_token_authority, + &output_bytes, + &None, + ) + .map_err(ProgramError::from)?; + + let mut output_token_accounts = Vec::new(); + output_token_accounts.push(output_escrow_token_account); + + if deposit_amount != sum_inputs { + let change_compressed_token_account = + create_change_output_compressed_token_account::( + input_compressed_token_accounts, + deposit_amount, + authority, + change_compressed_account_merkle_tree_index, + )?; + output_token_accounts.push(change_compressed_token_account); + } + // TODO: create a close account instruction + let (input_delegate_pda, output_delegate_pda) = update_delegate_compressed_account::( + delegate_account, + authority, + input_escrow_token_account_hash, + output_compressed_token_hash, + deposit_amount, + output_delegate_compressed_account_merkle_tree_index, + epoch, + )?; + Ok(DepositCompressedAccounts { + input_delegate_pda, + output_delegate_pda, + output_token_accounts, + }) +} + +#[derive(Clone, Debug, PartialEq, AnchorDeserialize, AnchorSerialize)] +pub struct InputDelegateAccountWithPackedContext { + pub root_index: u16, + pub merkle_context: PackedMerkleContext, + pub delegate_account: InputDelegateAccount, +} +#[derive(Clone, Debug, PartialEq, AnchorDeserialize, AnchorSerialize)] +pub struct InputDelegateAccountWithContext { + pub root_index: u16, + pub merkle_context: MerkleContext, + pub delegate_account: InputDelegateAccount, +} + +#[derive(Clone, Copy, Debug, PartialEq, AnchorDeserialize, AnchorSerialize)] +pub struct DelegateAccountWithPackedContext { + pub root_index: u16, + pub merkle_context: PackedMerkleContext, + pub delegate_account: DelegateAccount, + pub output_merkle_tree_index: u8, +} + +#[derive(Clone, Copy, Debug, PartialEq, AnchorDeserialize, AnchorSerialize)] +pub struct DelegateAccountWithContext { + pub merkle_context: MerkleContext, + pub delegate_account: DelegateAccount, + pub output_merkle_tree_index: Pubkey, +} + +pub fn hash_input_token_data_with_context( + mint: &[u8; 32], + hashed_owner: &[u8; 32], + amount: u64, +) -> std::result::Result<[u8; 32], HasherError> { + let amount_bytes = amount.to_le_bytes(); + TokenData::hash_with_hashed_values::(mint, hashed_owner, &amount_bytes, &None) +} + +fn update_delegate_compressed_account( + input_delegate_account: Option, + authority: &Pubkey, + input_escrow_token_account_hash: Option<[u8; 32]>, + output_escrow_token_account_hash: [u8; 32], + deposit_amount: u64, + merkle_tree_index: u8, + epoch: u64, +) -> Result<( + Option, + OutputCompressedAccountWithPackedContext, +)> { + let (input_account, mut delegate_account) = if let Some(input) = input_delegate_account { + let input_escrow_token_account_hash = + if let Some(input_escrow_token_account_hash) = input_escrow_token_account_hash { + Ok(input_escrow_token_account_hash) + } else { + err!(RegistryError::InputEscrowTokenHashNotProvided) + }?; + let (mut delegate_account, input_account) = + create_input_delegate_account(authority, input_escrow_token_account_hash, input)?; + delegate_account.escrow_token_account_hash = output_escrow_token_account_hash; + (Some(input_account), delegate_account) + } else { + ( + None, + DelegateAccount { + owner: *authority, + escrow_token_account_hash: output_escrow_token_account_hash, + last_sync_epoch: epoch, + ..Default::default() + }, + ) + }; + if IS_DEPOSIT { + delegate_account.stake_weight = delegate_account + .stake_weight + .checked_add(deposit_amount) + .ok_or(RegistryError::ComputeEscrowAmountFailed)?; + } else { + delegate_account.stake_weight = delegate_account + .stake_weight + .checked_sub(deposit_amount) + .ok_or(RegistryError::ComputeEscrowAmountFailed)?; + } + + let output_account: CompressedAccount = + create_delegate_compressed_account::(&delegate_account)?; + let output_account_with_merkle_context = OutputCompressedAccountWithPackedContext { + compressed_account: output_account, + merkle_tree_index, + }; + Ok((input_account, output_account_with_merkle_context)) +} + +pub fn create_input_delegate_account( + authority: &Pubkey, + input_escrow_token_account_hash: [u8; 32], + input: InputDelegateAccountWithPackedContext, +) -> Result<(DelegateAccount, PackedCompressedAccountWithMerkleContext)> { + let delegate_account = DelegateAccount { + owner: *authority, + escrow_token_account_hash: input_escrow_token_account_hash, + delegate_forester_delegate_account: input + .delegate_account + .delegate_forester_delegate_account, + delegated_stake_weight: input.delegate_account.delegated_stake_weight, + stake_weight: input.delegate_account.stake_weight, + pending_epoch: input.delegate_account.pending_epoch, + pending_undelegated_stake_weight: input.delegate_account.pending_undelegated_stake_weight, + last_sync_epoch: input.delegate_account.last_sync_epoch, + pending_token_amount: input.delegate_account.pending_token_amount, + pending_synced_stake_weight: input.delegate_account.pending_synced_stake_weight, + pending_delegated_stake_weight: input.delegate_account.pending_delegated_stake_weight, + }; + let input_account = create_compressed_delegate_account( + delegate_account, + input.merkle_context, + input.root_index, + )?; + Ok((delegate_account, input_account)) +} + +pub fn create_compressed_delegate_account( + delegate_account: DelegateAccount, + merkle_context: PackedMerkleContext, + root_index: u16, +) -> Result { + let compressed_account = create_delegate_compressed_account::(&delegate_account)?; + let input_account = PackedCompressedAccountWithMerkleContext { + merkle_context, + root_index, + compressed_account, + }; + Ok(input_account) +} + +pub fn create_delegate_compressed_account( + delegate_account: &DelegateAccount, +) -> std::result::Result { + let data = if IS_INPUT { + Vec::new() + } else { + let mut data = Vec::with_capacity(DelegateAccount::LEN); + + DelegateAccount::serialize(delegate_account, &mut data).unwrap(); + data + }; + let data_hash = delegate_account + .hash::() + .map_err(ProgramError::from)?; + let data = CompressedAccountData { + discriminator: DELEGATE_ACCOUNT_DISCRIMINATOR, + data_hash, + data, + }; + let output_account = CompressedAccount { + owner: crate::ID, + lamports: 0, + address: None, + data: Some(data), + }; + Ok(output_account) +} + +pub fn update_escrow_compressed_token_account( + escrow_token_authority: &Pubkey, + input_escrow_token_account: &Option, + amount: u64, + merkle_tree_index: u8, +) -> Result { + let mut output_amount = if let Some(input_escrow_token_account) = input_escrow_token_account { + input_escrow_token_account.amount + } else { + 0 + }; + if IS_DEPOSIT { + output_amount = output_amount + .checked_add(amount) + .ok_or(RegistryError::ComputeEscrowAmountFailed)?; + } else { + output_amount = output_amount + .checked_sub(amount) + .ok_or(RegistryError::ComputeEscrowAmountFailed)?; + } + Ok(PackedTokenTransferOutputData { + amount: output_amount, + owner: *escrow_token_authority, + lamports: None, + merkle_tree_index, + }) +} + +fn create_change_output_compressed_token_account( + input_token_data_with_context: &[InputTokenDataWithContext], + deposit_amount: u64, + owner: &Pubkey, + merkle_tree_index: u8, +) -> Result { + let input_sum = input_token_data_with_context + .iter() + .map(|account| account.amount) + .sum::(); + let change_amount = if IS_DEPOSIT { + match input_sum.checked_sub(deposit_amount) { + Some(change_amount) => Ok(change_amount), + None => err!(RegistryError::ArithmeticUnderflow), + }? + } else { + match input_sum.checked_add(deposit_amount) { + Some(change_amount) => Ok(change_amount), + None => err!(RegistryError::ArithmeticUnderflow), + }? + }; + Ok(PackedTokenTransferOutputData { + amount: change_amount, + owner: *owner, + lamports: None, + merkle_tree_index, + }) +} + +#[cfg(test)] +mod tests { + + use light_compressed_token::token_data::AccountState; + + use super::*; + + fn get_input_token_data_with_context_test_data() -> Vec { + vec![ + InputTokenDataWithContext { + amount: 100, + delegate_index: Some(1), + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: 0, + nullifier_queue_pubkey_index: 0, + leaf_index: 0, + queue_index: None, + }, + root_index: 0, + lamports: Some(50), + }, + InputTokenDataWithContext { + amount: 50, + delegate_index: Some(2), + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: 0, + nullifier_queue_pubkey_index: 0, + leaf_index: 0, + queue_index: None, + }, + root_index: 0, + lamports: Some(25), + }, + ] + } + + #[test] + fn test_create_change_output_compressed_token_account_pass() { + let input_token_data_with_context = get_input_token_data_with_context_test_data(); + let deposit_amount = 120; + let owner = Pubkey::default(); + let merkle_tree_index = 0; + + let output = create_change_output_compressed_token_account::( + &input_token_data_with_context, + deposit_amount, + &owner, + merkle_tree_index, + ) + .unwrap(); + + assert_eq!(output.amount, 30); + assert_eq!(output.owner, owner); + assert_eq!(output.merkle_tree_index, merkle_tree_index); + assert_eq!(output.lamports, None); + } + + #[test] + fn test_create_change_output_compressed_token_account_fail() { + let input_token_data_with_context = get_input_token_data_with_context_test_data(); + let deposit_amount = 200; + let owner = Pubkey::default(); + let merkle_tree_index = 0; + + let res = create_change_output_compressed_token_account::( + &input_token_data_with_context, + deposit_amount, + &owner, + merkle_tree_index, + ); + assert!(matches!( + res, + Err(error) if error == RegistryError::ArithmeticUnderflow.into() + )); + } + + fn get_input_escrow_token_account(amount: u64) -> Option { + Some(InputTokenDataWithContext { + amount, + delegate_index: Some(1), + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: 0, + nullifier_queue_pubkey_index: 0, + leaf_index: 0, + queue_index: None, + }, + root_index: 0, + lamports: Some(50), + }) + } + + #[test] + fn test_update_escrow_compressed_token_account_deposit_pass() { + let escrow_token_authority = Pubkey::default(); + let input_escrow_token_account = get_input_escrow_token_account(100); + let amount = 50; + let merkle_tree_index = 0; + + let output = update_escrow_compressed_token_account::( + &escrow_token_authority, + &input_escrow_token_account, + amount, + merkle_tree_index, + ) + .unwrap(); + + assert_eq!(output.amount, 150); + assert_eq!(output.owner, escrow_token_authority); + assert_eq!(output.merkle_tree_index, merkle_tree_index); + assert_eq!(output.lamports, None); + } + + #[test] + fn test_update_escrow_compressed_token_account_withdraw_pass() { + let escrow_token_authority = Pubkey::default(); + let input_escrow_token_account = get_input_escrow_token_account(100); + let amount = 50; + let merkle_tree_index = 0; + + let output = update_escrow_compressed_token_account::( + &escrow_token_authority, + &input_escrow_token_account, + amount, + merkle_tree_index, + ) + .unwrap(); + + assert_eq!(output.amount, 50); + assert_eq!(output.owner, escrow_token_authority); + assert_eq!(output.merkle_tree_index, merkle_tree_index); + assert_eq!(output.lamports, None); + } + + #[test] + fn test_update_escrow_compressed_token_account_withdraw_fail() { + let escrow_token_authority = Pubkey::default(); + let input_escrow_token_account = get_input_escrow_token_account(50); + let amount = 100; + let merkle_tree_index = 0; + + let res = update_escrow_compressed_token_account::( + &escrow_token_authority, + &input_escrow_token_account, + amount, + merkle_tree_index, + ); + assert!(matches!( + res, + Err(error) if error == RegistryError::ComputeEscrowAmountFailed.into() + )); + } + + #[test] + fn test_update_escrow_compressed_token_account_deposit_fail() { + let escrow_token_authority = Pubkey::default(); + let input_escrow_token_account = get_input_escrow_token_account(u64::MAX); + let amount = 1; + let merkle_tree_index = 0; + + let res = update_escrow_compressed_token_account::( + &escrow_token_authority, + &input_escrow_token_account, + amount, + merkle_tree_index, + ); + assert!(matches!( + res, + Err(error) if error == RegistryError::ComputeEscrowAmountFailed.into() + )); + } + + fn get_test_delegate_account() -> DelegateAccount { + DelegateAccount { + owner: Pubkey::new_unique(), + delegate_forester_delegate_account: Some(Pubkey::new_unique()), + delegated_stake_weight: 100, + stake_weight: 200, + pending_undelegated_stake_weight: 50, + pending_epoch: 1, + last_sync_epoch: 0, + pending_token_amount: 25, + escrow_token_account_hash: [0u8; 32], + pending_synced_stake_weight: 0, + pending_delegated_stake_weight: 0, + } + } + + #[test] + fn test_create_delegate_compressed_account_pass() { + let delegate_account = get_test_delegate_account(); + + let result = create_delegate_compressed_account::(&delegate_account); + + assert!(result.is_ok()); + + let compressed_account = result.unwrap(); + assert_eq!(compressed_account.owner, crate::ID); + assert_eq!(compressed_account.lamports, 0); + assert!(compressed_account.address.is_none()); + assert!(compressed_account.data.is_some()); + + let data = compressed_account.data.unwrap(); + assert_eq!(data.discriminator, DELEGATE_ACCOUNT_DISCRIMINATOR); + assert_eq!(data.data_hash, delegate_account.hash::().unwrap()); + + let mut serialized_data = Vec::with_capacity(DelegateAccount::LEN); + DelegateAccount::serialize(&delegate_account, &mut serialized_data).unwrap(); + assert_eq!(data.data, serialized_data); + } + fn get_test_input_delegate_account_with_context() -> InputDelegateAccountWithPackedContext { + InputDelegateAccountWithPackedContext { + delegate_account: InputDelegateAccount { + delegate_forester_delegate_account: Some(Pubkey::new_unique()), + delegated_stake_weight: 100, + stake_weight: 100, + pending_undelegated_stake_weight: 50, + pending_epoch: 1, + last_sync_epoch: 10, + pending_token_amount: 25, + pending_synced_stake_weight: 0, + pending_delegated_stake_weight: 0, + }, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: 1, + nullifier_queue_pubkey_index: 2, + leaf_index: 3, + queue_index: None, + }, + root_index: 4, + } + } + #[test] + fn test_create_input_delegate_account_pass() { + let authority = Pubkey::new_unique(); + let input_escrow_token_account_hash = [1u8; 32]; + let input = get_test_input_delegate_account_with_context(); + + let result = create_input_delegate_account( + &authority, + input_escrow_token_account_hash, + input.clone(), + ); + + assert!(result.is_ok()); + + let (delegate_account, input_account) = result.unwrap(); + assert_eq!(delegate_account.owner, authority); + assert_eq!( + delegate_account.escrow_token_account_hash, + input_escrow_token_account_hash + ); + assert_eq!( + delegate_account.delegate_forester_delegate_account, + input.delegate_account.delegate_forester_delegate_account + ); + assert_eq!( + delegate_account.delegated_stake_weight, + input.delegate_account.delegated_stake_weight + ); + assert_eq!( + delegate_account.stake_weight, + input.delegate_account.stake_weight + ); + assert_eq!( + delegate_account.pending_epoch, + input.delegate_account.pending_epoch + ); + assert_eq!( + delegate_account.pending_undelegated_stake_weight, + input.delegate_account.pending_undelegated_stake_weight + ); + assert_eq!( + delegate_account.last_sync_epoch, + input.delegate_account.last_sync_epoch + ); + assert_eq!( + delegate_account.pending_token_amount, + input.delegate_account.pending_token_amount + ); + + let compressed_account = input_account.compressed_account; + assert_eq!(compressed_account.owner, crate::ID); + assert_eq!(compressed_account.lamports, 0); + assert!(compressed_account.address.is_none()); + assert!(compressed_account.data.is_some()); + + let data = compressed_account.data.unwrap(); + assert_eq!(data.discriminator, DELEGATE_ACCOUNT_DISCRIMINATOR); + assert_eq!(data.data_hash, delegate_account.hash::().unwrap()); + + assert_eq!(input_account.merkle_context, input.merkle_context); + assert_eq!(input_account.root_index, input.root_index); + } + + #[test] + fn test_update_delegate_compressed_account_pass() { + let authority = Pubkey::new_unique(); + let input_escrow_token_account_hash = Some([1u8; 32]); + let output_escrow_token_account_hash = [2u8; 32]; + let input = Some(get_test_input_delegate_account_with_context()); + let deposit_amount = 100; + let merkle_tree_index = 11; + + let result = update_delegate_compressed_account::( + input.clone(), + &authority, + input_escrow_token_account_hash, + output_escrow_token_account_hash, + deposit_amount, + merkle_tree_index, + 0, + ); + + assert!(result.is_ok()); + + let (input_account, output_account_with_merkle_context) = result.unwrap(); + if let Some(input_account) = input_account.as_ref() { + assert_eq!(input_account.root_index, 4); + assert_eq!(input_account.merkle_context.merkle_tree_pubkey_index, 1); + assert_eq!(input_account.merkle_context.nullifier_queue_pubkey_index, 2); + assert_eq!(input_account.merkle_context.leaf_index, 3); + assert_eq!(input_account.merkle_context.queue_index, None); + let input_data = input_account.compressed_account.data.as_ref().unwrap(); + assert!(input_data.data.is_empty()); + } + + assert_eq!( + output_account_with_merkle_context.merkle_tree_index, + merkle_tree_index + ); + let output_account = output_account_with_merkle_context + .compressed_account + .clone(); + assert_eq!(output_account.owner, crate::ID); + assert_eq!(output_account.lamports, 0); + assert!(output_account.address.is_none()); + assert!(output_account.data.is_some()); + + let data = output_account.data.unwrap(); + assert_eq!(data.discriminator, DELEGATE_ACCOUNT_DISCRIMINATOR); + assert_eq!( + data.data_hash, + output_account_with_merkle_context + .compressed_account + .data + .as_ref() + .unwrap() + .data_hash + ); + + let output_delegate_account = DelegateAccount { + owner: authority, + escrow_token_account_hash: output_escrow_token_account_hash, + delegate_forester_delegate_account: input + .clone() + .unwrap() + .delegate_account + .delegate_forester_delegate_account, + delegated_stake_weight: input + .clone() + .unwrap() + .delegate_account + .delegated_stake_weight, + stake_weight: input.clone().unwrap().delegate_account.stake_weight + deposit_amount, + pending_undelegated_stake_weight: input + .clone() + .unwrap() + .delegate_account + .pending_undelegated_stake_weight, + pending_epoch: input.clone().unwrap().delegate_account.pending_epoch, + last_sync_epoch: input.clone().unwrap().delegate_account.last_sync_epoch, + pending_token_amount: input.clone().unwrap().delegate_account.pending_token_amount, + pending_synced_stake_weight: input + .clone() + .unwrap() + .delegate_account + .pending_synced_stake_weight, + pending_delegated_stake_weight: input + .clone() + .unwrap() + .delegate_account + .pending_delegated_stake_weight, + }; + // let mut serialized_data = Vec::with_capacity(DelegateAccount::LEN); + let output_des = DelegateAccount::deserialize(&mut &data.data[..]).unwrap(); + assert_eq!(output_delegate_account, output_des); + } + + fn get_test_input_token_data_with_context() -> InputTokenDataWithContext { + InputTokenDataWithContext { + amount: 100, + delegate_index: None, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: 1, + nullifier_queue_pubkey_index: 2, + leaf_index: 3, + queue_index: None, + }, + root_index: 5, + lamports: Some(50), + } + } + + #[test] + fn test_deposit_with_delegate_account() { + let authority = Pubkey::new_unique(); + let escrow_token_authority = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let delegate_account = Some(get_test_input_delegate_account_with_context()); + let deposit_amount = 100; + let input_compressed_token_accounts = vec![get_test_input_token_data_with_context()]; + let input_escrow_token_account = Some(get_test_input_token_data_with_context()); + assert_eq!( + input_escrow_token_account.as_ref().unwrap().amount, + delegate_account + .as_ref() + .unwrap() + .delegate_account + .stake_weight + ); + let escrow_token_account_merkle_tree_index = 0; + let change_compressed_account_merkle_tree_index = 1; + let output_delegate_compressed_account_merkle_tree_index = 2; + + let result = deposit_or_withdraw::( + &authority, + &escrow_token_authority, + &mint, + delegate_account.clone(), + deposit_amount, + &input_compressed_token_accounts, + &input_escrow_token_account, + escrow_token_account_merkle_tree_index, + change_compressed_account_merkle_tree_index, + output_delegate_compressed_account_merkle_tree_index, + 0, + ); + + assert!(result.is_ok()); + assert_deposit_or_withdraw_result::( + result.unwrap(), + mint, + authority, + escrow_token_authority, + delegate_account, + deposit_amount, + output_delegate_compressed_account_merkle_tree_index, + &input_compressed_token_accounts, + input_escrow_token_account, + ); + } + + #[test] + fn test_deposit_without_delegate_account() { + let authority = Pubkey::new_unique(); + let escrow_token_authority = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let delegate_account = None; + let deposit_amount = 100; + let input_compressed_token_accounts = vec![get_test_input_token_data_with_context()]; + let input_escrow_token_account = None; + let escrow_token_account_merkle_tree_index = 0; + let change_compressed_account_merkle_tree_index = 1; + let output_delegate_compressed_account_merkle_tree_index = 2; + + let result = deposit_or_withdraw::( + &authority, + &escrow_token_authority, + &mint, + delegate_account.clone(), + deposit_amount, + &input_compressed_token_accounts, + &input_escrow_token_account, + escrow_token_account_merkle_tree_index, + change_compressed_account_merkle_tree_index, + output_delegate_compressed_account_merkle_tree_index, + 0, + ); + + assert!(result.is_ok()); + assert_deposit_or_withdraw_result::( + result.unwrap(), + mint, + authority, + escrow_token_authority, + delegate_account, + deposit_amount, + output_delegate_compressed_account_merkle_tree_index, + &input_compressed_token_accounts, + input_escrow_token_account, + ); + } + + #[test] + fn test_withdraw_with_delegate_account() { + // Partial withdrawal + { + let authority = Pubkey::new_unique(); + let escrow_token_authority = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let delegate_account = Some(get_test_input_delegate_account_with_context()); + let withdraw_amount = 100; + let input_compressed_token_accounts = vec![]; + let input_escrow_token_account = Some(get_test_input_token_data_with_context()); + let escrow_token_account_merkle_tree_index = 0; + let change_compressed_account_merkle_tree_index = 1; + let output_delegate_compressed_account_merkle_tree_index = 2; + + let result = deposit_or_withdraw::( + &authority, + &escrow_token_authority, + &mint, + delegate_account.clone(), + withdraw_amount, + &input_compressed_token_accounts, + &input_escrow_token_account, + escrow_token_account_merkle_tree_index, + change_compressed_account_merkle_tree_index, + output_delegate_compressed_account_merkle_tree_index, + 0, + ); + + assert!(result.is_ok()); + assert_deposit_or_withdraw_result::( + result.unwrap(), + mint, + authority, + escrow_token_authority, + delegate_account, + withdraw_amount, + output_delegate_compressed_account_merkle_tree_index, + &input_compressed_token_accounts, + input_escrow_token_account, + ); + } + // Full withdrawal + { + let authority = Pubkey::new_unique(); + let escrow_token_authority = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let delegate_account = Some(get_test_input_delegate_account_with_context()); + let withdraw_amount = delegate_account + .as_ref() + .unwrap() + .delegate_account + .stake_weight; + let input_compressed_token_accounts = vec![]; + let input_escrow_token_account = Some(get_test_input_token_data_with_context()); + let escrow_token_account_merkle_tree_index = 0; + let change_compressed_account_merkle_tree_index = 1; + let output_delegate_compressed_account_merkle_tree_index = 2; + + let result = deposit_or_withdraw::( + &authority, + &escrow_token_authority, + &mint, + delegate_account.clone(), + withdraw_amount, + &input_compressed_token_accounts, + &input_escrow_token_account, + escrow_token_account_merkle_tree_index, + change_compressed_account_merkle_tree_index, + output_delegate_compressed_account_merkle_tree_index, + 0, + ); + + assert!(result.is_ok()); + assert_deposit_or_withdraw_result::( + result.unwrap(), + mint, + authority, + escrow_token_authority, + delegate_account, + withdraw_amount, + output_delegate_compressed_account_merkle_tree_index, + &input_compressed_token_accounts, + input_escrow_token_account, + ); + } + } + + #[test] + fn test_withdraw_without_input_compressed_() { + let authority = Pubkey::new_unique(); + let escrow_token_authority = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let delegate_account = Some(get_test_input_delegate_account_with_context()); + let withdraw_amount = 100; + let input_compressed_token_accounts = vec![get_test_input_token_data_with_context()]; + let input_escrow_token_account = None; + let escrow_token_account_merkle_tree_index = 0; + let change_compressed_account_merkle_tree_index = 1; + let output_delegate_compressed_account_merkle_tree_index = 2; + + let result = deposit_or_withdraw::( + &authority, + &escrow_token_authority, + &mint, + delegate_account.clone(), + withdraw_amount, + &input_compressed_token_accounts, + &input_escrow_token_account, + escrow_token_account_merkle_tree_index, + change_compressed_account_merkle_tree_index, + output_delegate_compressed_account_merkle_tree_index, + 0, + ); + + assert!(matches!( + result, + Err(error) if error == RegistryError::InputEscrowTokenHashNotProvided.into() + )); + } + + fn assert_deposit_or_withdraw_result( + result: DepositCompressedAccounts, + mint: Pubkey, + authority: Pubkey, + escrow_token_authority: Pubkey, + delegate_account: Option, + amount: u64, + output_delegate_compressed_account_merkle_tree_index: u8, + input_compressed_token_accounts: &Vec, + input_token_escrow_account: Option, + ) { + let input_sum = input_compressed_token_accounts + .iter() + .map(|x| x.amount) + .sum::(); + let input_escrow_amount = + if let Some(input_token_escrow_account) = input_token_escrow_account.as_ref() { + input_token_escrow_account.amount + } else { + 0 + }; + + let expected_escrow_amount = if IS_DEPOSIT { + input_escrow_amount + amount + } else { + input_escrow_amount - amount + }; + + let input_token_data = TokenData { + mint, + owner: authority, + amount: input_escrow_amount, + delegate: None, + state: AccountState::Initialized, + }; + let output_escrow_token_data = TokenData { + mint, + owner: escrow_token_authority, + amount: expected_escrow_amount, + delegate: None, + state: AccountState::Initialized, + }; + + if IS_DEPOSIT { + assert_eq!(result.output_token_accounts.len(), 1); + assert_eq!(result.output_token_accounts[0].amount, input_sum - amount); + assert_eq!(result.output_token_accounts[0].owner, authority); + assert_eq!(result.output_token_accounts[0].merkle_tree_index, 1); + } else { + assert_eq!(result.output_token_accounts.len(), 1); + assert_eq!(result.output_token_accounts[0].amount, input_sum + amount); + assert_eq!(result.output_token_accounts[0].owner, authority); + assert_eq!(result.output_token_accounts[0].merkle_tree_index, 1); + } + + assert_eq!( + result.output_token_accounts[1].amount, + output_escrow_token_data.amount + ); + assert_eq!( + result.output_token_accounts[1].owner, + escrow_token_authority + ); + + let expected_output_delegate_pda = if let Some(delegate_account) = delegate_account.as_ref() + { + let expected_input_delegate_pda = Some(PackedCompressedAccountWithMerkleContext { + compressed_account: create_delegate_compressed_account::(&DelegateAccount { + owner: authority, + escrow_token_account_hash: input_token_data.hash::().unwrap(), + delegate_forester_delegate_account: delegate_account + .delegate_account + .delegate_forester_delegate_account, + delegated_stake_weight: delegate_account + .delegate_account + .delegated_stake_weight, + stake_weight: delegate_account.delegate_account.stake_weight, + pending_epoch: delegate_account.delegate_account.pending_epoch, + pending_undelegated_stake_weight: delegate_account + .delegate_account + .pending_undelegated_stake_weight, + last_sync_epoch: delegate_account.delegate_account.last_sync_epoch, + pending_token_amount: delegate_account.delegate_account.pending_token_amount, + pending_synced_stake_weight: delegate_account + .delegate_account + .pending_synced_stake_weight, + pending_delegated_stake_weight: delegate_account + .delegate_account + .pending_delegated_stake_weight, + }) + .unwrap(), + merkle_context: delegate_account.merkle_context, + root_index: 4, + }); + assert_eq!(result.input_delegate_pda, expected_input_delegate_pda); + let stake_weight = if IS_DEPOSIT { + delegate_account.delegate_account.stake_weight + amount + } else { + delegate_account.delegate_account.stake_weight - amount + }; + assert_eq!(stake_weight, expected_escrow_amount); + + OutputCompressedAccountWithPackedContext { + compressed_account: create_delegate_compressed_account::(&DelegateAccount { + owner: authority, + escrow_token_account_hash: output_escrow_token_data.hash::().unwrap(), + delegate_forester_delegate_account: delegate_account + .delegate_account + .delegate_forester_delegate_account, + delegated_stake_weight: delegate_account + .delegate_account + .delegated_stake_weight, + stake_weight, + pending_epoch: delegate_account.delegate_account.pending_epoch, + pending_undelegated_stake_weight: delegate_account + .delegate_account + .pending_undelegated_stake_weight, + last_sync_epoch: delegate_account.delegate_account.last_sync_epoch, + pending_token_amount: delegate_account.delegate_account.pending_token_amount, + pending_synced_stake_weight: delegate_account + .delegate_account + .pending_synced_stake_weight, + pending_delegated_stake_weight: delegate_account + .delegate_account + .pending_delegated_stake_weight, + }) + .unwrap(), + merkle_tree_index: output_delegate_compressed_account_merkle_tree_index, + } + } else { + assert_eq!(amount, expected_escrow_amount); + OutputCompressedAccountWithPackedContext { + compressed_account: create_delegate_compressed_account::(&DelegateAccount { + owner: authority, + escrow_token_account_hash: output_escrow_token_data.hash::().unwrap(), + delegate_forester_delegate_account: None, + delegated_stake_weight: 0, + stake_weight: amount, + pending_epoch: 0, + pending_undelegated_stake_weight: 0, + last_sync_epoch: 0, + pending_token_amount: 0, + pending_synced_stake_weight: 0, + pending_delegated_stake_weight: 0, + }) + .unwrap(), + merkle_tree_index: output_delegate_compressed_account_merkle_tree_index, + } + }; + assert_eq!(result.output_delegate_pda, expected_output_delegate_pda); + } +} diff --git a/programs/registry/src/delegate/deposit_instruction.rs b/programs/registry/src/delegate/deposit_instruction.rs new file mode 100644 index 0000000000..bcc3167c41 --- /dev/null +++ b/programs/registry/src/delegate/deposit_instruction.rs @@ -0,0 +1,121 @@ +use crate::protocol_config::state::ProtocolConfigPda; + +use super::{ + traits::{ + CompressedCpiContextTrait, CompressedTokenProgramAccounts, SignerAccounts, + SystemProgramAccounts, + }, + ESCROW_TOKEN_ACCOUNT_SEED, +}; +use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; +use anchor_lang::prelude::*; +use light_compressed_token::program::LightCompressedToken; +use light_system_program::program::LightSystemProgram; + +#[derive(Accounts)] +#[instruction(salt: u64)] +pub struct DepositOrWithdrawInstruction<'info> { + /// Fee payer needs to be mutable to pay rollover and protocol fees. + #[account(mut)] + pub fee_payer: Signer<'info>, + pub authority: Signer<'info>, + /// CHECK: + #[account( + seeds = [ESCROW_TOKEN_ACCOUNT_SEED, authority.key().as_ref(), salt.to_le_bytes().as_slice()], bump + )] + pub escrow_token_authority: AccountInfo<'info>, + /// CHECK: + #[account( + seeds = [CPI_AUTHORITY_PDA_SEED], bump + )] + pub cpi_authority: AccountInfo<'info>, + pub protocol_config: Account<'info, ProtocolConfigPda>, + /// CHECK: + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: checked in emit_event.rs. + pub noop_program: AccountInfo<'info>, + /// CHECK: + pub account_compression_authority: AccountInfo<'info>, + /// CHECK: + pub account_compression_program: Program<'info, AccountCompression>, + /// CHECK: checked in cpi_signer_check. + pub invoking_program: AccountInfo<'info>, + /// CHECK: + pub system_program: AccountInfo<'info>, + /// CHECK: + #[account(mut)] + pub cpi_context_account: AccountInfo<'info>, + pub self_program: Program<'info, crate::program::LightRegistry>, + /// CHECK: + pub token_cpi_authority_pda: AccountInfo<'info>, + pub light_system_program: Program<'info, LightSystemProgram>, + pub compressed_token_program: Program<'info, LightCompressedToken>, +} + +impl<'info> SystemProgramAccounts<'info> for DepositOrWithdrawInstruction<'info> { + fn get_registered_program_pda(&self) -> AccountInfo<'info> { + self.registered_program_pda.to_account_info() + } + fn get_noop_program(&self) -> AccountInfo<'info> { + self.noop_program.to_account_info() + } + fn get_account_compression_authority(&self) -> AccountInfo<'info> { + self.account_compression_authority.to_account_info() + } + fn get_account_compression_program(&self) -> AccountInfo<'info> { + self.account_compression_program.to_account_info() + } + fn get_system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + fn get_sol_pool_pda(&self) -> Option> { + None + } + fn get_decompression_recipient(&self) -> Option> { + None + } + fn get_light_system_program(&self) -> AccountInfo<'info> { + self.light_system_program.to_account_info() + } + fn get_self_program(&self) -> AccountInfo<'info> { + self.self_program.to_account_info() + } +} + +impl<'info> SignerAccounts<'info> for DepositOrWithdrawInstruction<'info> { + fn get_fee_payer(&self) -> AccountInfo<'info> { + self.fee_payer.to_account_info() + } + fn get_authority(&self) -> AccountInfo<'info> { + self.authority.to_account_info() + } + fn get_cpi_authority_pda(&self) -> AccountInfo<'info> { + self.cpi_authority.to_account_info() + } +} + +impl<'info> CompressedTokenProgramAccounts<'info> for DepositOrWithdrawInstruction<'info> { + fn get_token_cpi_authority_pda(&self) -> AccountInfo<'info> { + self.token_cpi_authority_pda.to_account_info() + } + fn get_compressed_token_program(&self) -> AccountInfo<'info> { + self.compressed_token_program.to_account_info() + } + fn get_escrow_authority_pda(&self) -> AccountInfo<'info> { + self.escrow_token_authority.to_account_info() + } + fn get_token_pool_pda(&self) -> AccountInfo<'info> { + unimplemented!("escrow authority not implemented for DepositOrWithdrawInstruction"); + } + fn get_spl_token_program(&self) -> AccountInfo<'info> { + unimplemented!("escrow authority not implemented for DepositOrWithdrawInstruction"); + } + fn get_compress_or_decompress_token_account(&self) -> Option> { + None + } +} +impl<'info> CompressedCpiContextTrait<'info> for DepositOrWithdrawInstruction<'info> { + fn get_cpi_context(&self) -> Option> { + Some(self.cpi_context_account.to_account_info()) + } +} diff --git a/programs/registry/src/delegate/mod.rs b/programs/registry/src/delegate/mod.rs new file mode 100644 index 0000000000..049ead21c8 --- /dev/null +++ b/programs/registry/src/delegate/mod.rs @@ -0,0 +1,24 @@ +pub mod delegate_instruction; +pub mod deposit; +pub mod deposit_instruction; +pub mod process_cpi; +pub mod process_delegate; +pub mod state; +// TODO: move into cpi dir +pub mod traits; +use anchor_lang::solana_program::pubkey::Pubkey; + +pub const ESCROW_TOKEN_ACCOUNT_SEED: &[u8] = b"ESCROW_TOKEN_ACCOUNT_SEED"; +pub const DELEGATE_ACCOUNT_DISCRIMINATOR: [u8; 8] = [1, 0, 0, 0, 0, 0, 0, 0]; +pub const FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR: [u8; 8] = [2, 0, 0, 0, 0, 0, 0, 0]; + +pub fn get_escrow_token_authority(authority: &Pubkey, salt: u64) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[ + ESCROW_TOKEN_ACCOUNT_SEED, + authority.as_ref(), + salt.to_le_bytes().as_slice(), + ], + &crate::ID, + ) +} diff --git a/programs/registry/src/delegate/process_cpi.rs b/programs/registry/src/delegate/process_cpi.rs new file mode 100644 index 0000000000..c90b2a95ee --- /dev/null +++ b/programs/registry/src/delegate/process_cpi.rs @@ -0,0 +1,286 @@ +use super::{ + get_escrow_token_authority, + traits::{ + CompressedCpiContextTrait, CompressedTokenProgramAccounts, MintToAccounts, SignerAccounts, + SystemProgramAccounts, + }, + ESCROW_TOKEN_ACCOUNT_SEED, +}; +use account_compression::utils::constants::CPI_AUTHORITY_PDA_SEED; +use anchor_lang::{prelude::*, Bumps}; +use light_compressed_token::process_transfer::{ + CompressedTokenInstructionDataTransfer, DelegatedTransfer, InputTokenDataWithContext, + PackedTokenTransferOutputData, +}; +use light_system_program::{ + invoke::processor::CompressedProof, + sdk::{compressed_account::PackedCompressedAccountWithMerkleContext, CompressedCpiContext}, + InstructionDataInvokeCpi, OutputCompressedAccountWithPackedContext, +}; + +#[inline(never)] +pub fn cpi_light_system_program< + 'a, + 'b, + 'c, + 'info, + C: SignerAccounts<'info> + SystemProgramAccounts<'info> + CompressedCpiContextTrait<'info> + Bumps, +>( + ctx: &Context<'a, 'b, 'c, 'info, C>, + proof: Option, + cpi_context: Option, + input_pda: Option, + output_pda: OutputCompressedAccountWithPackedContext, + remaining_accounts: Vec>, +) -> Result<()> { + // let cpi_context = if let Some(mut cpi_context) = cpi_context { + // cpi_context.set_context = true; + // Some(cpi_context) + // } else { + // None + // }; + let bump = &[BUMP_CPI_AUTHORITY]; + let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; + let signer_seeds = &[&seeds[..]]; + let input_compressed_accounts_with_merkle_context = if let Some(input_pda) = input_pda { + vec![input_pda] + } else { + vec![] + }; + let inputs_struct = light_system_program::invoke_cpi::instruction::InstructionDataInvokeCpi { + relay_fee: None, + input_compressed_accounts_with_merkle_context, + output_compressed_accounts: vec![output_pda], + proof, + new_address_params: Vec::new(), + compress_or_decompress_lamports: None, + is_compress: false, + signer_seeds: seeds.iter().map(|seed| seed.to_vec()).collect(), + cpi_context, + }; + let mut inputs = Vec::new(); + InstructionDataInvokeCpi::serialize(&inputs_struct, &mut inputs).map_err(ProgramError::from)?; + + let cpi_accounts = light_system_program::cpi::accounts::InvokeCpiInstruction { + fee_payer: ctx.accounts.get_fee_payer(), + authority: ctx.accounts.get_cpi_authority_pda(), + registered_program_pda: ctx.accounts.get_registered_program_pda(), + noop_program: ctx.accounts.get_noop_program(), + account_compression_authority: ctx.accounts.get_account_compression_authority(), + account_compression_program: ctx.accounts.get_account_compression_program(), + invoking_program: ctx.accounts.get_self_program(), + system_program: ctx.accounts.get_system_program(), + sol_pool_pda: None, + decompression_recipient: None, + cpi_context_account: ctx.accounts.get_cpi_context(), + }; + let mut cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.get_light_system_program(), + cpi_accounts, + signer_seeds, + ); + + cpi_ctx.remaining_accounts = remaining_accounts.to_vec(); + + light_system_program::cpi::invoke_cpi(cpi_ctx, inputs)?; + Ok(()) +} + +#[inline(never)] +pub fn cpi_compressed_token_transfer< + 'info, + C: SignerAccounts<'info> + + SystemProgramAccounts<'info> + + CompressedTokenProgramAccounts<'info> + + CompressedCpiContextTrait<'info> + + Bumps, + const SEED_LEN: usize, +>( + ctx: &Context<'_, '_, '_, 'info, C>, + proof: Option, + compression_amount: Option, + is_compress: bool, + salt: u64, + mut cpi_context: CompressedCpiContext, + mint: &Pubkey, + input_token_data_with_context: Vec, + output_compressed_accounts: Vec, + owner: &Pubkey, + authority: AccountInfo<'info>, + seeds: [&[u8]; SEED_LEN], + mut remaining_accounts: Vec>, +) -> Result<()> { + cpi_context.cpi_context_account_index = remaining_accounts.len() as u8; + let inputs_struct = CompressedTokenInstructionDataTransfer { + proof, + mint: *mint, + delegated_transfer: Some(DelegatedTransfer { + owner: *owner, + delegate_change_account_index: None, + }), + input_token_data_with_context, + output_compressed_accounts, + is_compress, + compress_or_decompress_amount: compression_amount, + cpi_context: Some(cpi_context), + lamports_change_account_merkle_tree_index: None, + }; + + let mut inputs = Vec::new(); + CompressedTokenInstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + // let authority = ctx.accounts.get_escrow_authority_pda(); + let (token_pool_pda, token_program, compress_or_decompress_token_account) = + if compression_amount.is_some() { + ( + Some(ctx.accounts.get_token_pool_pda()), + Some(ctx.accounts.get_spl_token_program()), + ctx.accounts.get_compress_or_decompress_token_account(), + ) + } else { + (None, None, None) + }; + let cpi_accounts = light_compressed_token::cpi::accounts::TransferInstruction { + fee_payer: ctx.accounts.get_fee_payer(), + authority, + registered_program_pda: ctx.accounts.get_registered_program_pda(), + noop_program: ctx.accounts.get_noop_program(), + account_compression_authority: ctx.accounts.get_account_compression_authority(), + account_compression_program: ctx.accounts.get_account_compression_program(), + self_program: ctx.accounts.get_compressed_token_program(), + cpi_authority_pda: ctx.accounts.get_token_cpi_authority_pda(), + light_system_program: ctx.accounts.get_light_system_program(), + token_pool_pda, + compress_or_decompress_token_account, + token_program, + system_program: ctx.accounts.get_system_program(), + }; + let signer_seeds = &[&seeds[..]]; + let mut cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.get_compressed_token_program(), + cpi_accounts, + signer_seeds, + ); + remaining_accounts.push(ctx.accounts.get_cpi_context().unwrap()); + cpi_ctx.remaining_accounts = remaining_accounts; + light_compressed_token::cpi::transfer(cpi_ctx, inputs) +} + +#[inline(never)] +pub fn cpi_compressed_token_mint_to< + 'a, + 'b, + 'c, + 'info, + C: SignerAccounts<'info> + + SystemProgramAccounts<'info> + + CompressedTokenProgramAccounts<'info> + + MintToAccounts<'info> + + Bumps, + const SEED_LEN: usize, +>( + ctx: &Context<'a, 'b, 'c, 'info, C>, + recipients: Vec, + amounts: Vec, + seeds: [&[u8]; SEED_LEN], + merkle_tree: AccountInfo<'info>, +) -> Result<()> { + let signer_seeds = &[&seeds[..]]; + let cpi_accounts = light_compressed_token::cpi::accounts::MintToInstruction { + fee_payer: ctx.accounts.get_fee_payer(), + authority: ctx.accounts.get_cpi_authority_pda(), + registered_program_pda: ctx.accounts.get_registered_program_pda(), + noop_program: ctx.accounts.get_noop_program(), + account_compression_authority: ctx.accounts.get_account_compression_authority(), + account_compression_program: ctx.accounts.get_account_compression_program(), + self_program: ctx.accounts.get_compressed_token_program(), + cpi_authority_pda: ctx.accounts.get_token_cpi_authority_pda(), + light_system_program: ctx.accounts.get_light_system_program(), + token_pool_pda: ctx.accounts.get_token_pool_pda(), + token_program: ctx.accounts.get_spl_token_program(), + system_program: ctx.accounts.get_system_program(), + mint: ctx.accounts.get_mint(), + merkle_tree, + sol_pool_pda: None, + }; + + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.get_compressed_token_program(), + cpi_accounts, + signer_seeds, + ); + + light_compressed_token::cpi::mint_to(cpi_ctx, recipients, amounts, None) +} + +pub const BUMP_CPI_AUTHORITY: u8 = 254; +/// Get static cpi signer seeds +pub fn get_cpi_signer_seeds() -> [&'static [u8]; 2] { + let bump: &[u8; 1] = &[BUMP_CPI_AUTHORITY]; + let seeds: [&'static [u8]; 2] = [CPI_AUTHORITY_PDA_SEED, bump]; + seeds +} + +#[inline(never)] +pub fn mint_spl_to_pool_pda< + 'info, + C: SignerAccounts<'info> + + SystemProgramAccounts<'info> + + CompressedTokenProgramAccounts<'info> + + CompressedCpiContextTrait<'info> + + MintToAccounts<'info> + + Bumps, + const SEED_LEN: usize, +>( + ctx: &Context<'_, '_, '_, 'info, C>, + mint_amount: u64, + recipient: AccountInfo<'info>, + seeds: [&[u8]; SEED_LEN], +) -> Result<()> { + let cpi_accounts = anchor_spl::token::MintTo { + mint: ctx.accounts.get_mint(), + to: recipient, + authority: ctx.accounts.get_cpi_authority_pda(), + }; + let signer_seeds = &[&seeds[..]]; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.get_spl_token_program(), + cpi_accounts, + signer_seeds, + ); + + anchor_spl::token::mint_to(cpi_ctx, mint_amount)?; + Ok(()) +} + +#[inline(never)] +pub fn approve_spl_token< + 'info, + C: SignerAccounts<'info> + + SystemProgramAccounts<'info> + + CompressedTokenProgramAccounts<'info> + + CompressedCpiContextTrait<'info> + + Bumps, + const SEED_LEN: usize, +>( + ctx: &Context<'_, '_, '_, 'info, C>, + amount: u64, + recipient: AccountInfo<'info>, + delegate: AccountInfo<'info>, + seeds: [&[u8]; SEED_LEN], +) -> Result<()> { + let cpi_accounts = anchor_spl::token::Approve { + to: recipient, + authority: ctx.accounts.get_cpi_authority_pda(), + delegate, + }; + let signer_seeds = &[&seeds[..]]; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.get_spl_token_program(), + cpi_accounts, + signer_seeds, + ); + + anchor_spl::token::approve(cpi_ctx, amount)?; + Ok(()) +} diff --git a/programs/registry/src/delegate/process_delegate.rs b/programs/registry/src/delegate/process_delegate.rs new file mode 100644 index 0000000000..e5bf7a37dc --- /dev/null +++ b/programs/registry/src/delegate/process_delegate.rs @@ -0,0 +1,532 @@ +use anchor_lang::prelude::*; +use light_system_program::{ + invoke::processor::CompressedProof, + sdk::compressed_account::{CompressedAccount, PackedCompressedAccountWithMerkleContext}, + OutputCompressedAccountWithPackedContext, +}; + +use crate::{errors::RegistryError, protocol_config::state::ProtocolConfig, ForesterAccount}; + +use super::{ + delegate_instruction::DelegatetOrUndelegateInstruction, + deposit::{ + create_compressed_delegate_account, create_delegate_compressed_account, + DelegateAccountWithPackedContext, + }, + process_cpi::cpi_light_system_program, +}; + +// TODO: double check that we provide the possibility to pass a different output tree in all instructions +pub fn process_delegate_or_undelegate<'a, 'b, 'c, 'info: 'b + 'c, const IS_DELEGATE: bool>( + ctx: Context<'a, 'b, 'c, 'info, DelegatetOrUndelegateInstruction<'info>>, + proof: CompressedProof, + delegate_account: DelegateAccountWithPackedContext, + delegate_amount: u64, + no_sync: bool, +) -> Result<()> { + let slot = Clock::get()?.slot; + let (input_delegate_pda, output_delegate_pda) = delegate_or_undelegate::( + &ctx.accounts.authority.key(), + &ctx.accounts.protocol_config.config, + delegate_account, + &ctx.accounts.forester_pda.key(), + &mut ctx.accounts.forester_pda, + delegate_amount, + slot, + no_sync, + )?; + + cpi_light_system_program( + &ctx, + Some(proof), + None, + Some(input_delegate_pda), + output_delegate_pda, + ctx.remaining_accounts.to_vec(), + ) +} + +pub fn delegate_or_undelegate( + authority: &Pubkey, + protocol_config: &ProtocolConfig, + delegate_account: DelegateAccountWithPackedContext, + forester_pda_pubkey: &Pubkey, + forester_pda: &mut ForesterAccount, + delegate_amount: u64, + current_slot: u64, + no_sync: bool, +) -> Result<( + PackedCompressedAccountWithMerkleContext, + OutputCompressedAccountWithPackedContext, +)> { + if !no_sync { + forester_pda.sync(current_slot, protocol_config)?; + } + if *authority != delegate_account.delegate_account.owner { + return err!(RegistryError::InvalidAuthority); + } + // check that delegate account is synced to last claimed (completed) epoch + if forester_pda.last_claimed_epoch != delegate_account.delegate_account.last_sync_epoch + && delegate_account + .delegate_account + .delegate_forester_delegate_account + .is_some() + { + msg!( + "Not synced to last forester claimed epoch {}, last synced epoch {} ", + forester_pda.last_claimed_epoch, + delegate_account.delegate_account.last_sync_epoch + ); + return err!(RegistryError::DelegateAccountNotSynced); + } + if let Some(forester_pubkey) = delegate_account + .delegate_account + .delegate_forester_delegate_account + { + if forester_pubkey != *forester_pda_pubkey { + return err!(RegistryError::InvalidForester); + } + } + let epoch = forester_pda.last_registered_epoch; // protocol_config.get_current_epoch(current_slot); + + // check that is not delegated to a different forester + if delegate_account.delegate_account.delegated_stake_weight > 0 + && delegate_account + .delegate_account + .delegate_forester_delegate_account + .is_some() + && *forester_pda_pubkey + != delegate_account + .delegate_account + .delegate_forester_delegate_account + .unwrap() + { + return err!(RegistryError::AlreadyDelegated); + } + // modify forester pda + if IS_DELEGATE { + forester_pda.pending_undelegated_stake_weight = forester_pda + .pending_undelegated_stake_weight + .checked_add(delegate_amount) + .ok_or(RegistryError::ComputeEscrowAmountFailed)?; + } else { + msg!( + "forester pda active stake weight: {}", + forester_pda.active_stake_weight + ); + msg!("undelegate amount {}", delegate_amount); + forester_pda.active_stake_weight = forester_pda + .active_stake_weight + .checked_sub(delegate_amount) + .ok_or(RegistryError::ComputeEscrowAmountFailed)?; + } + + // modify delegate account + let delegate_account_mod = { + let mut delegate_account = delegate_account.delegate_account; + delegate_account.sync_pending_stake_weight(epoch); + if IS_DELEGATE { + // add delegated stake weight to pending_delegated_stake_weight + // remove delegated stake weight from stake_weight + delegate_account.pending_delegated_stake_weight = delegate_account + .pending_delegated_stake_weight + .checked_add(delegate_amount) + .ok_or(RegistryError::ComputeEscrowAmountFailed)?; + delegate_account.stake_weight = delegate_account + .stake_weight + .checked_sub(delegate_amount) + .ok_or(RegistryError::ComputeEscrowAmountFailed)?; + delegate_account.delegate_forester_delegate_account = Some(*forester_pda_pubkey); + delegate_account.pending_epoch = epoch; + } else { + // remove delegated stake weight from delegated_stake_weight + // add delegated stake weight to pending_undelegated_stake_weight + delegate_account.delegated_stake_weight = delegate_account + .delegated_stake_weight + .checked_sub(delegate_amount) + .ok_or(RegistryError::ComputeEscrowAmountFailed)?; + delegate_account.pending_undelegated_stake_weight = delegate_account + .pending_undelegated_stake_weight + .checked_add(delegate_amount) + .ok_or(RegistryError::ComputeEscrowAmountFailed)?; + delegate_account.pending_epoch = epoch; + if delegate_account.delegated_stake_weight == 0 { + delegate_account.delegate_forester_delegate_account = None; + } + } + delegate_account + }; + let input_delegate_compressed_account = create_compressed_delegate_account( + delegate_account.delegate_account, + delegate_account.merkle_context, + delegate_account.root_index, + )?; + let output_account: CompressedAccount = + create_delegate_compressed_account::(&delegate_account_mod)?; + let output_delegate_compressed_account = OutputCompressedAccountWithPackedContext { + compressed_account: output_account, + merkle_tree_index: delegate_account.output_merkle_tree_index, + }; + // let output_delegate_compressed_account = update_delegate_compressed_account::( + // *delegate_account, + // delegate_amount, + // delegate_account.output_merkle_tree_index, + // epoch, + // forester_pda_pubkey, + // )?; + + Ok(( + input_delegate_compressed_account, + output_delegate_compressed_account, + )) +} + +/// Creates an updated delegate account. +/// Delegate(IS_DELEGATE): +/// - increase delegated_stake_weight +/// - decrease stake_weight +/// Undelegate(Not(IS_DELEGATE)): +/// - decrease delegated_stake_weight +/// - increase pending_undelegated_stake_weight +fn update_delegate_compressed_account( + input_delegate_account: DelegateAccountWithPackedContext, + delegate_amount: u64, + merkle_tree_index: u8, + epoch: u64, + forester_pda_pubkey: &Pubkey, +) -> Result { + let output_account: CompressedAccount = + create_delegate_compressed_account::(&input_delegate_account.delegate_account)?; + let output_account_with_merkle_context = OutputCompressedAccountWithPackedContext { + compressed_account: output_account, + merkle_tree_index, + }; + Ok(output_account_with_merkle_context) +} + +#[cfg(test)] +mod tests { + use crate::delegate::state::DelegateAccount; + + use super::*; + use anchor_lang::solana_program::pubkey::Pubkey; + use light_hasher::{DataHasher, Poseidon}; + use light_system_program::sdk::compressed_account::PackedMerkleContext; + + fn get_test_delegate_account_with_context() -> DelegateAccountWithPackedContext { + DelegateAccountWithPackedContext { + root_index: 4, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: 1, + nullifier_queue_pubkey_index: 2, + leaf_index: 3, + queue_index: None, + }, + delegate_account: DelegateAccount { + owner: Pubkey::new_unique(), + delegate_forester_delegate_account: Some(Pubkey::new_unique()), + delegated_stake_weight: 100, + stake_weight: 200, + pending_delegated_stake_weight: 0, + pending_undelegated_stake_weight: 50, + pending_epoch: 1, + last_sync_epoch: 11, + pending_token_amount: 25, + escrow_token_account_hash: [1u8; 32], + pending_synced_stake_weight: 0, + }, + output_merkle_tree_index: 6, + } + } + + #[test] + fn test_update_delegate_compressed_account_delegate_pass() { + let input_delegate_account = get_test_delegate_account_with_context(); + let delegate_amount = 50; + let merkle_tree_index = 1; + let epoch = 10; + let forester_pda_pubkey = Pubkey::new_unique(); + + let result = update_delegate_compressed_account::( + input_delegate_account.clone(), + delegate_amount, + merkle_tree_index, + epoch, + &forester_pda_pubkey, + ); + + assert!(result.is_ok()); + + let expected_delegate_account = DelegateAccount { + delegated_stake_weight: input_delegate_account + .delegate_account + .delegated_stake_weight + + delegate_amount, + delegate_forester_delegate_account: Some(forester_pda_pubkey), + stake_weight: input_delegate_account.delegate_account.stake_weight - delegate_amount, + ..input_delegate_account.delegate_account + }; + + let output = result.unwrap(); + assert_eq!(output.merkle_tree_index, merkle_tree_index); + let deserialized_delegate_account = DelegateAccount::deserialize( + &mut &output.compressed_account.data.as_ref().unwrap().data[..], + ) + .unwrap(); + assert_eq!(deserialized_delegate_account, expected_delegate_account); + assert_eq!( + output.compressed_account.data.unwrap().data_hash, + expected_delegate_account.hash::().unwrap() + ); + } + + #[test] + fn test_update_delegate_compressed_account_delegate_fail() { + let input_delegate_account = get_test_delegate_account_with_context(); + let delegate_amount = u64::MAX; + let merkle_tree_index = 1; + let epoch = 10; + let forester_pda_pubkey = Pubkey::new_unique(); + + let result = update_delegate_compressed_account::( + input_delegate_account.clone(), + delegate_amount, + merkle_tree_index, + epoch, + &forester_pda_pubkey, + ); + + assert!(result.is_err()); + } + + #[test] + fn test_update_delegate_compressed_account_undelegate_pass() { + let input_delegate_account = get_test_delegate_account_with_context(); + let delegate_amount = 50; + let merkle_tree_index = 1; + let epoch = 10; + let forester_pda_pubkey = Pubkey::new_unique(); + + let result = update_delegate_compressed_account::( + input_delegate_account.clone(), + delegate_amount, + merkle_tree_index, + epoch, + &forester_pda_pubkey, + ); + + assert!(result.is_ok()); + + let expected_delegate_account = DelegateAccount { + delegated_stake_weight: input_delegate_account + .delegate_account + .delegated_stake_weight + - delegate_amount, + pending_undelegated_stake_weight: input_delegate_account + .delegate_account + .pending_undelegated_stake_weight + + delegate_amount, + pending_epoch: epoch, + ..input_delegate_account.delegate_account + }; + + let output = result.unwrap(); + assert_eq!(output.merkle_tree_index, merkle_tree_index); + let deserialized_delegate_account = DelegateAccount::deserialize( + &mut &output.compressed_account.data.as_ref().unwrap().data[..], + ) + .unwrap(); + assert_eq!(deserialized_delegate_account, expected_delegate_account); + assert_eq!( + output.compressed_account.data.unwrap().data_hash, + expected_delegate_account.hash::().unwrap() + ); + } + + #[test] + fn test_update_delegate_compressed_account_undelegate_fail() { + let input_delegate_account = get_test_delegate_account_with_context(); + let delegate_amount = u64::MAX; + let merkle_tree_index = 1; + let epoch = 10; + let forester_pda_pubkey = Pubkey::new_unique(); + + let result = update_delegate_compressed_account::( + input_delegate_account.clone(), + delegate_amount, + merkle_tree_index, + epoch, + &forester_pda_pubkey, + ); + + assert!(result.is_err()); + } + + fn get_test_forester_account() -> ForesterAccount { + ForesterAccount { + active_stake_weight: 200, + pending_undelegated_stake_weight: 50, + ..Default::default() + } + } + + #[test] + fn test_delegate_or_undelegate_delegate_pass() { + let protocol_config = ProtocolConfig { + ..Default::default() + }; + let mut forester_pda = get_test_forester_account(); + let delegate_account = get_test_delegate_account_with_context(); + let authority = delegate_account.delegate_account.owner; + let forester_pda_pubkey = delegate_account + .delegate_account + .delegate_forester_delegate_account + .unwrap(); + let delegate_amount = 50; + let current_slot = 10; + let no_sync = true; + + let result = delegate_or_undelegate::( + &authority, + &protocol_config, + delegate_account, + &forester_pda_pubkey, + &mut forester_pda, + delegate_amount, + current_slot, + no_sync, + ); + + let (input_delegate_pda, output_delegate_pda) = result.unwrap(); + assert_eq!(input_delegate_pda.compressed_account.owner, crate::ID); + assert_eq!(output_delegate_pda.compressed_account.owner, crate::ID); + + let expected_delegate_account = DelegateAccount { + delegated_stake_weight: delegate_account.delegate_account.delegated_stake_weight + + delegate_amount, + stake_weight: delegate_account.delegate_account.stake_weight - delegate_amount, + ..delegate_account.delegate_account + }; + + let deserialized_delegate_account = DelegateAccount::deserialize( + &mut &output_delegate_pda + .compressed_account + .data + .as_ref() + .unwrap() + .data[..], + ) + .unwrap(); + assert_eq!(deserialized_delegate_account, expected_delegate_account); + } + + #[test] + fn test_delegate_or_undelegate_undelegate_pass() { + let protocol_config = ProtocolConfig { + ..Default::default() + }; + + let mut forester_pda = get_test_forester_account(); + let delegate_account = get_test_delegate_account_with_context(); + let authority = delegate_account.delegate_account.owner; + let forester_pda_pubkey = delegate_account + .delegate_account + .delegate_forester_delegate_account + .unwrap(); + let delegate_amount = 50; + let current_slot = 10; + let no_sync = true; + + let result = delegate_or_undelegate::( + &authority, + &protocol_config, + delegate_account, + &forester_pda_pubkey, + &mut forester_pda, + delegate_amount, + current_slot, + no_sync, + ) + .unwrap(); + + let (input_delegate_pda, output_delegate_pda) = result; + assert_eq!(input_delegate_pda.compressed_account.owner, crate::ID); + assert_eq!(output_delegate_pda.compressed_account.owner, crate::ID); + + let expected_delegate_account = DelegateAccount { + delegated_stake_weight: delegate_account.delegate_account.delegated_stake_weight + - delegate_amount, + pending_undelegated_stake_weight: delegate_account + .delegate_account + .pending_undelegated_stake_weight + + delegate_amount, + pending_epoch: protocol_config.get_current_epoch(current_slot), + ..delegate_account.delegate_account + }; + + let deserialized_delegate_account = DelegateAccount::deserialize( + &mut &output_delegate_pda + .compressed_account + .data + .as_ref() + .unwrap() + .data[..], + ) + .unwrap(); + assert_eq!(deserialized_delegate_account, expected_delegate_account); + } + + #[test] + fn test_delegate_or_undelegate_undelegate_fail() { + let authority = Pubkey::new_unique(); + let protocol_config = ProtocolConfig { + ..Default::default() + }; + let forester_pda_pubkey = Pubkey::new_unique(); + let mut forester_pda = get_test_forester_account(); + let delegate_account = get_test_delegate_account_with_context(); + let delegate_amount = u64::MAX; + let current_slot = 10; + let no_sync = true; + + let result = delegate_or_undelegate::( + &authority, + &protocol_config, + delegate_account, + &forester_pda_pubkey, + &mut forester_pda, + delegate_amount, + current_slot, + no_sync, + ); + + assert!(matches!(result, Err(error) if error == RegistryError::InvalidAuthority.into())); + } + + #[test] + fn test_delegate_or_undelegate_delegate_fail() { + let protocol_config = ProtocolConfig { + ..Default::default() + }; + let forester_pda_pubkey = Pubkey::new_unique(); + let mut forester_pda = get_test_forester_account(); + let delegate_account = get_test_delegate_account_with_context(); + let authority = delegate_account.delegate_account.owner; + let delegate_amount = u64::MAX; + let current_slot = 10; + let no_sync = true; + + let result = delegate_or_undelegate::( + &authority, + &protocol_config, + delegate_account, + &forester_pda_pubkey, + &mut forester_pda, + delegate_amount, + current_slot, + no_sync, + ); + + assert!(matches!(result, Err(error) if error == RegistryError::AlreadyDelegated.into())); + } +} diff --git a/programs/registry/src/delegate/state.rs b/programs/registry/src/delegate/state.rs new file mode 100644 index 0000000000..d415f2f345 --- /dev/null +++ b/programs/registry/src/delegate/state.rs @@ -0,0 +1,181 @@ +use crate::protocol_config::state::ProtocolConfig; +use aligned_sized::aligned_sized; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::pubkey::Pubkey; +use light_hasher::{errors::HasherError, DataHasher, Hasher}; +use light_utils::hash_to_bn254_field_size_be; + +/// Instruction data input verion of DelegateAccount The following fields are +/// missing since these are computed onchain: +/// 1. owner +/// 2. escrow_token_account_hash +/// -> we save 64 bytes in instructiond data +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub struct InputDelegateAccount { + pub delegate_forester_delegate_account: Option, + /// Stake weight that is delegated to a forester. + /// Newly delegated stake is not active until the next epoch. + pub delegated_stake_weight: u64, + /// undelgated stake is stake that is not yet delegated to a forester + pub stake_weight: u64, + /// When delegating stake is pending until the next epoch + pub pending_delegated_stake_weight: u64, + /// When undelegating stake is pending until the next epoch + pub pending_undelegated_stake_weight: u64, + pub pending_synced_stake_weight: u64, + pub pending_epoch: u64, + pub last_sync_epoch: u64, + /// Pending token amount are rewards that are not yet claimed to the stake + /// compressed token account. + pub pending_token_amount: u64, +} + +impl From for InputDelegateAccount { + fn from(delegate_account: DelegateAccount) -> Self { + InputDelegateAccount { + delegate_forester_delegate_account: delegate_account.delegate_forester_delegate_account, + delegated_stake_weight: delegate_account.delegated_stake_weight, + stake_weight: delegate_account.stake_weight, + pending_undelegated_stake_weight: delegate_account.pending_undelegated_stake_weight, + pending_epoch: delegate_account.pending_epoch, + last_sync_epoch: delegate_account.last_sync_epoch, + pending_token_amount: delegate_account.pending_token_amount, + pending_synced_stake_weight: delegate_account.pending_synced_stake_weight, + pending_delegated_stake_weight: delegate_account.pending_delegated_stake_weight, + } + } +} + +#[aligned_sized] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub struct DelegateAccount { + pub owner: Pubkey, + pub delegate_forester_delegate_account: Option, + /// Stake weight that is delegated to a forester. + /// Newly delegated stake is not active until the next epoch. + pub delegated_stake_weight: u64, + /// newly delegated stakeweight becomes active after the next epoch + pub pending_delegated_stake_weight: u64, + /// undelgated stake is stake that is not yet delegated to a forester + pub stake_weight: u64, + /// Buffer variable to account for the lag of one epoch for rewards to reach + /// to registration account + pub pending_synced_stake_weight: u64, + /// When undelegating stake is pending until the next epoch + pub pending_undelegated_stake_weight: u64, + pub pending_epoch: u64, + pub last_sync_epoch: u64, + /// Pending token amount are rewards that are not yet claimed to the stake + /// compressed token account. + pub pending_token_amount: u64, + pub escrow_token_account_hash: [u8; 32], +} + +pub trait CompressedAccountTrait { + fn get_owner(&self) -> Pubkey; +} +impl CompressedAccountTrait for DelegateAccount { + fn get_owner(&self) -> Pubkey { + self.owner + } +} + +// TODO: pass in hashed owner +impl DataHasher for DelegateAccount { + fn hash(&self) -> std::result::Result<[u8; 32], HasherError> { + let hashed_owner = hash_to_bn254_field_size_be(self.owner.as_ref()).unwrap().0; + let hashed_delegate_forester_delegate_account = + if let Some(delegate_forester_delegate_account) = + self.delegate_forester_delegate_account + { + hash_to_bn254_field_size_be(delegate_forester_delegate_account.as_ref()) + .unwrap() + .0 + } else { + [0u8; 32] + }; + H::hashv(&[ + hashed_owner.as_slice(), + hashed_delegate_forester_delegate_account.as_slice(), + &self.delegated_stake_weight.to_le_bytes(), + &self.pending_synced_stake_weight.to_le_bytes(), + &self.stake_weight.to_le_bytes(), + &self.pending_undelegated_stake_weight.to_le_bytes(), + ]) + } +} + +impl DelegateAccount { + pub fn sync_pending_stake_weight(&mut self, current_epoch: u64) { + msg!("sync_pending_stake_weight current_epoch: {}", current_epoch); + msg!( + "sync_pending_stake_weight pending_epoch: {}", + self.pending_epoch + ); + if current_epoch > self.pending_epoch { + self.stake_weight += self.pending_undelegated_stake_weight; + self.pending_undelegated_stake_weight = 0; + // last sync epoch is only relevant for syncing the delegate account with the forester rewards + // self.last_sync_epoch = current_epoch; + self.delegated_stake_weight += self.pending_delegated_stake_weight; + self.pending_delegated_stake_weight = 0; + // self.pending_epoch = 0; + } + } +} + +// pub fn undelegate( +// protocol_config: &ProtocolConfig, +// delegate_account: &mut DelegateAccount, +// forester_pda: &mut ForesterAccount, +// amount: u64, +// current_slot: u64, +// ) -> Result<()> { +// forester_pda.sync(current_slot, protocol_config)?; +// forester_pda.active_stake_weight -= amount; +// delegate_account.delegated_stake_weight -= amount; +// delegate_account.pending_undelegated_stake_weight += amount; +// delegate_account.pending_epoch = protocol_config.get_current_epoch(current_slot); +// if delegate_account.delegated_stake_weight == 0 { +// delegate_account.delegate_forester_delegate_account = None; +// } +// Ok(()) +// } + +// TODO: we need a drastically improved compressed token transfer sdk +// pub fn withdraw_instruction( +// delegate_account: &mut DelegateAccount, +// delegate_token_account: &mut AccountInfo, +// recipient_token_account: &mut AccountInfo, +// protocol_config: ProtocolConfig, +// amount: u64, +// current_slot: u64, +// ) -> Result<()> { +// withdraw(delegate_account, protocol_config, amount, current_slot); +// // transfer tokens +// // TODO: add compressed token transfer +// // delegate_token_account.balance -= amount; +// // recipient_token_account.balance += amount; +// Ok(()) +// } +/** + * User flow: + * 1. Deposit compressed tokens to DelegatePda + * - inputs: InputTokenData, deposit_amount + * - create two outputs, escrow compressed account and change account + * - compressed escrow account is owned by pda derived from authority + * - + */ +#[allow(unused)] +fn withdraw( + delegate_account: &mut DelegateAccount, + protocol_config: ProtocolConfig, + amount: u64, + current_slot: u64, +) { + let current_epoch = protocol_config.get_current_epoch(current_slot); + delegate_account.sync_pending_stake_weight(current_epoch); + // reduce stake weight + // only non delegated stake can be unstaked + delegate_account.stake_weight -= amount; +} diff --git a/programs/registry/src/delegate/traits.rs b/programs/registry/src/delegate/traits.rs new file mode 100644 index 0000000000..3003ec6258 --- /dev/null +++ b/programs/registry/src/delegate/traits.rs @@ -0,0 +1,37 @@ +use anchor_lang::prelude::*; + +pub trait SystemProgramAccounts<'info> { + fn get_registered_program_pda(&self) -> AccountInfo<'info>; + fn get_noop_program(&self) -> AccountInfo<'info>; + fn get_account_compression_authority(&self) -> AccountInfo<'info>; + fn get_account_compression_program(&self) -> AccountInfo<'info>; + fn get_system_program(&self) -> AccountInfo<'info>; + fn get_sol_pool_pda(&self) -> Option>; + fn get_decompression_recipient(&self) -> Option>; + fn get_light_system_program(&self) -> AccountInfo<'info>; + fn get_self_program(&self) -> AccountInfo<'info>; +} + +pub trait CompressedCpiContextTrait<'info> { + fn get_cpi_context(&self) -> Option>; +} + +pub trait CompressedTokenProgramAccounts<'info> { + fn get_token_cpi_authority_pda(&self) -> AccountInfo<'info>; + fn get_compressed_token_program(&self) -> AccountInfo<'info>; + fn get_escrow_authority_pda(&self) -> AccountInfo<'info>; + fn get_token_pool_pda(&self) -> AccountInfo<'info>; + fn get_spl_token_program(&self) -> AccountInfo<'info>; + fn get_compress_or_decompress_token_account(&self) -> Option>; +} + +pub trait SignerAccounts<'info> { + fn get_fee_payer(&self) -> AccountInfo<'info>; + fn get_authority(&self) -> AccountInfo<'info>; + fn get_cpi_authority_pda(&self) -> AccountInfo<'info>; +} + +// TODO: create macros and include all accounts which are required for mint to +pub trait MintToAccounts<'info> { + fn get_mint(&self) -> AccountInfo<'info>; +} diff --git a/programs/registry/src/epoch/claim_forester.rs b/programs/registry/src/epoch/claim_forester.rs new file mode 100644 index 0000000000..0f59e35131 --- /dev/null +++ b/programs/registry/src/epoch/claim_forester.rs @@ -0,0 +1,432 @@ +use crate::{ + delegate::{ + process_cpi::{ + cpi_compressed_token_mint_to, cpi_light_system_program, mint_spl_to_pool_pda, + }, + state::CompressedAccountTrait, + FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, + }, + errors::RegistryError, + forester::state::ForesterAccount, +}; +use aligned_sized::aligned_sized; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::pubkey::Pubkey; +use light_compressed_token::process_transfer::get_cpi_signer_seeds; +use light_hasher::Hasher; +use light_system_program::{ + sdk::compressed_account::{CompressedAccount, CompressedAccountData}, + OutputCompressedAccountWithPackedContext, +}; +use light_utils::hash_to_bn254_field_size_be; + +use super::{ + claim_forester_instruction::ClaimForesterInstruction, + register_epoch::{EpochPda, ForesterEpochPda}, +}; +// TODO: add reimbursement for opening the epoch account (close an one epoch account to open a new one of X epochs ago) +/// Forester claim rewards: +/// 1. Transfer forester fees to foresters compressed token account +/// 2. Transfer rewards to foresters token account +/// 3. compress forester epoch account +/// 4. close forester epoch account (in instruction struct) +/// 5. (skipped) if all stake has claimed close epoch account +pub fn process_forester_claim_rewards<'info>( + ctx: Context<'_, '_, '_, 'info, ClaimForesterInstruction<'info>>, +) -> Result<()> { + let forester_pda_pubkey = ctx.accounts.forester_pda.key(); + + let current_slot = Clock::get()?.slot; + let (epoch_results_compressed_account, fee, net_reward) = forester_claim_rewards( + &mut ctx.accounts.forester_pda, + &ctx.accounts.forester_epoch_pda, + &ctx.accounts.epoch_pda, + current_slot, + &forester_pda_pubkey, + 0, + )?; + // Mint netrewards to forester pool. These rewards can be claimed by the + // delegates. + mint_spl_to_pool_pda( + &ctx, + net_reward, + ctx.accounts.forester_token_pool.to_account_info(), + get_cpi_signer_seeds(), + )?; + + cpi_light_system_program( + &ctx, + None, + None, + None, + epoch_results_compressed_account, + vec![ctx.accounts.output_merkle_tree.to_account_info()], + )?; + // Mint forester fee + cpi_compressed_token_mint_to( + &ctx, + vec![ctx.accounts.forester_pda.config.fee_recipient], + vec![fee], + get_cpi_signer_seeds(), + ctx.accounts.output_merkle_tree.to_account_info(), + ) +} + +#[aligned_sized] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub struct CompressedForesterEpochAccount { + pub rewards_earned: u64, + pub epoch: u64, + pub stake_weight: u64, + pub previous_hash: [u8; 32], + pub forester_pda_pubkey: Pubkey, +} +impl CompressedAccountTrait for CompressedForesterEpochAccount { + fn get_owner(&self) -> Pubkey { + self.forester_pda_pubkey + } +} + +impl CompressedForesterEpochAccount { + pub fn hash(&self, hashed_forester_pubkey: [u8; 32]) -> Result<[u8; 32]> { + let hash = light_hasher::poseidon::Poseidon::hashv(&[ + hashed_forester_pubkey.as_slice(), + self.previous_hash.as_slice(), + &self.rewards_earned.to_le_bytes(), + &self.epoch.to_le_bytes(), + &self.stake_weight.to_le_bytes(), + ]) + .map_err(ProgramError::from)?; + Ok(hash) + } + + pub fn get_reward(&self, stake: u64) -> Result { + Ok(self + .rewards_earned + .checked_mul(stake) + .ok_or(RegistryError::ArithmeticOverflow)? + .checked_div(self.stake_weight) + .ok_or(RegistryError::ArithmeticUnderflow)?) + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub struct CompressedForesterEpochAccountInput { + pub rewards_earned: u64, + pub epoch: u64, + pub stake_weight: u64, +} + +impl CompressedForesterEpochAccountInput { + pub fn into_compressed_forester_epoch_pda( + self, + previous_hash: [u8; 32], + forester_pda_pubkey: Pubkey, + ) -> CompressedForesterEpochAccount { + CompressedForesterEpochAccount { + rewards_earned: self.rewards_earned, + epoch: self.epoch, + stake_weight: self.stake_weight, + previous_hash, + forester_pda_pubkey, + } + } +} + +pub fn serialize_compressed_forester_epoch_account( + epoch_results_compressed_account: CompressedForesterEpochAccount, + merkle_tree_index: u8, + hashed_forester_pubkey: [u8; 32], +) -> Result { + let data_hash = epoch_results_compressed_account.hash(hashed_forester_pubkey)?; + let mut data = Vec::with_capacity(CompressedForesterEpochAccount::LEN); + epoch_results_compressed_account.serialize(&mut data)?; + let compressed_account = CompressedAccount { + owner: crate::ID, + lamports: 0, + address: None, + data: Some(CompressedAccountData { + discriminator: FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, + data_hash, + data, + }), + }; + let epoch_results_compressed_account = OutputCompressedAccountWithPackedContext { + compressed_account, + merkle_tree_index, + }; + Ok(epoch_results_compressed_account) +} + +/// Instruction checks that: +/// 1. epoch and forester epoch are in the same epoch +/// 2. forester account and forester epoch pda are related +pub fn forester_claim_rewards( + forester_pda: &mut ForesterAccount, + forester_epoch_pda: &ForesterEpochPda, + epoch_pda: &EpochPda, + current_slot: u64, + forester_pda_pubkey: &Pubkey, + merkle_tree_index: u8, +) -> Result<(OutputCompressedAccountWithPackedContext, u64, u64)> { + epoch_pda + .protocol_config + .is_post_epoch(current_slot, forester_epoch_pda.epoch)?; + + let total_stake_weight = epoch_pda.registered_stake; + let total_tally = epoch_pda.total_work; + let forester_stake_weight = forester_epoch_pda.stake_weight; + let forester_tally = forester_epoch_pda.work_counter; + msg!("epoch_pda: {:?}", epoch_pda); + msg!("forester_epoch_pda: {:?}", forester_epoch_pda); + let reward = epoch_pda.protocol_config.get_rewards( + total_stake_weight, + total_tally, + forester_stake_weight, + forester_tally, + ); + msg!("reward: {}", reward); + let fee = reward + .checked_mul(forester_pda.config.fee) + .ok_or(RegistryError::ArithmeticOverflow)? + .checked_div(100) + .ok_or(RegistryError::ArithmeticUnderflow)?; + msg!("fee: {}", fee); + let net_reward = reward + .checked_sub(fee) + .ok_or(RegistryError::ArithmeticUnderflow)?; + msg!("net_reward: {}", net_reward); + // Increase the active deleagted stake weight by the net_reward + forester_pda.active_stake_weight = forester_pda + .active_stake_weight + .checked_add(net_reward) + .ok_or(RegistryError::ArithmeticOverflow)?; + let epoch_results_compressed_account = CompressedForesterEpochAccount { + rewards_earned: net_reward, + epoch: forester_epoch_pda.epoch, + stake_weight: forester_epoch_pda.stake_weight, + previous_hash: forester_pda.last_compressed_forester_epoch_pda_hash, + forester_pda_pubkey: *forester_pda_pubkey, + }; + let hashed_forester_pda_pubkey = hash_to_bn254_field_size_be(forester_pda_pubkey.as_ref()) + .unwrap() + .0; + let epoch_results_compressed_account = serialize_compressed_forester_epoch_account( + epoch_results_compressed_account, + merkle_tree_index, + hashed_forester_pda_pubkey, + )?; + forester_pda.last_compressed_forester_epoch_pda_hash = epoch_results_compressed_account + .compressed_account + .data + .as_ref() + .unwrap() + .data_hash; + forester_pda.last_claimed_epoch = forester_epoch_pda.epoch; + Ok((epoch_results_compressed_account, fee, net_reward)) +} + +#[cfg(test)] +mod tests { + use crate::{protocol_config::state::ProtocolConfig, ForesterConfig}; + + use super::*; + use anchor_lang::solana_program::pubkey::Pubkey; + + fn get_test_data() -> (CompressedForesterEpochAccount, u64, u8, [u8; 32]) { + let test_epoch_account = CompressedForesterEpochAccount { + rewards_earned: 1000, + epoch: 1, + stake_weight: 100, + previous_hash: [0; 32], + forester_pda_pubkey: Pubkey::default(), + }; + let fee = 10; + let merkle_tree_index = 1; + let hashed_forester_pubkey = [0; 32]; + + ( + test_epoch_account, + fee, + merkle_tree_index, + hashed_forester_pubkey, + ) + } + + #[test] + fn test_serialize_compressed_forester_epoch_account() { + let (epoch_account, _fee, merkle_tree_index, hashed_forester_pubkey) = get_test_data(); + + let result = serialize_compressed_forester_epoch_account( + epoch_account, + merkle_tree_index, + hashed_forester_pubkey, + ); + + assert!(result.is_ok()); + let serialized_account = result.unwrap(); + + let expected_compressed_account = CompressedAccount { + owner: crate::ID, + lamports: 0, + address: None, + data: Some(CompressedAccountData { + discriminator: FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, + data_hash: epoch_account.hash(hashed_forester_pubkey).unwrap(), + data: epoch_account.try_to_vec().unwrap(), + }), + }; + + let expected_output_compressed_account_with_packed_context = + OutputCompressedAccountWithPackedContext { + compressed_account: expected_compressed_account, + merkle_tree_index, + }; + + assert_eq!( + expected_output_compressed_account_with_packed_context, + serialized_account + ); + } + fn get_test_forester_claim_rewards_test_data( + ) -> (ForesterAccount, ForesterEpochPda, EpochPda, u64, Pubkey, u8) { + let active_stake = 100; + let forester_pda = ForesterAccount { + active_stake_weight: active_stake, + config: ForesterConfig { + fee: 5, + fee_recipient: Pubkey::default(), + }, + last_compressed_forester_epoch_pda_hash: [2; 32], + ..Default::default() + }; + + let forester_epoch_pda = ForesterEpochPda { + epoch: 1, + stake_weight: active_stake, + work_counter: 100, + ..Default::default() + }; + + let epoch_pda = EpochPda { + registered_stake: active_stake, + total_work: 100, + protocol_config: ProtocolConfig { + active_phase_length: 40, + genesis_slot: 0, + registration_phase_length: 10, + report_work_phase_length: 10, + base_reward: 20, + slot_length: 1, + epoch_reward: 100, + ..Default::default() + }, + epoch: 1, + ..Default::default() + }; + + let current_slot = 100; + let forester_pda_pubkey = Pubkey::default(); + let merkle_tree_index = 1; + + ( + forester_pda, + forester_epoch_pda, + epoch_pda, + current_slot, + forester_pda_pubkey, + merkle_tree_index, + ) + } + #[test] + fn test_forester_claim_rewards_failing() { + let ( + mut forester_pda, + forester_epoch_pda, + epoch_pda, + _, + forester_pda_pubkey, + merkle_tree_index, + ) = get_test_forester_claim_rewards_test_data(); + // Set current slot so that the epoch is still ongoing + let current_slot = epoch_pda.protocol_config.genesis_slot + + epoch_pda.protocol_config.registration_phase_length + + epoch_pda.protocol_config.active_phase_length + + 1; + let result = forester_claim_rewards( + &mut forester_pda, + &forester_epoch_pda, + &epoch_pda, + current_slot, + &forester_pda_pubkey, + merkle_tree_index, + ); + assert!(matches!(result, Err(error) if error == RegistryError::InvalidEpoch.into())); + } + + #[test] + fn test_forester_claim_rewards() { + let ( + mut forester_pda, + forester_epoch_pda, + epoch_pda, + current_slot, + forester_pda_pubkey, + merkle_tree_index, + ) = get_test_forester_claim_rewards_test_data(); + let mut pre_forester_pda = forester_pda.clone(); + let result = forester_claim_rewards( + &mut forester_pda, + &forester_epoch_pda, + &epoch_pda, + current_slot, + &forester_pda_pubkey, + merkle_tree_index, + ); + + assert!(result.is_ok()); + let (compressed_account, fee, net_reward) = result.unwrap(); + + let expected_compressed_account = CompressedAccount { + owner: crate::ID, + lamports: 0, + address: None, + data: Some(CompressedAccountData { + discriminator: FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, + data_hash: forester_pda.last_compressed_forester_epoch_pda_hash, + data: CompressedForesterEpochAccount { + rewards_earned: net_reward, + epoch: forester_epoch_pda.epoch, + stake_weight: forester_epoch_pda.stake_weight, + previous_hash: pre_forester_pda.last_compressed_forester_epoch_pda_hash, + forester_pda_pubkey, + } + .try_to_vec() + .unwrap(), + }), + }; + + let expected_output_compressed_account_with_packed_context = + OutputCompressedAccountWithPackedContext { + compressed_account: expected_compressed_account, + merkle_tree_index, + }; + + assert_eq!( + compressed_account, + expected_output_compressed_account_with_packed_context, + ); + pre_forester_pda.active_stake_weight += net_reward; + pre_forester_pda.last_compressed_forester_epoch_pda_hash = + expected_output_compressed_account_with_packed_context + .compressed_account + .data + .as_ref() + .unwrap() + .data_hash; + + assert_eq!(fee, 5); // 100 * 0.05 + assert_eq!(net_reward, 95); // 100 - 5 + assert_eq!(forester_pda, pre_forester_pda); + } +} diff --git a/programs/registry/src/epoch/claim_forester_instruction.rs b/programs/registry/src/epoch/claim_forester_instruction.rs new file mode 100644 index 0000000000..9f41da208e --- /dev/null +++ b/programs/registry/src/epoch/claim_forester_instruction.rs @@ -0,0 +1,132 @@ +use crate::delegate::traits::MintToAccounts; +use crate::delegate::traits::{ + CompressedCpiContextTrait, CompressedTokenProgramAccounts, SignerAccounts, + SystemProgramAccounts, +}; +use crate::errors::RegistryError; +use crate::{EpochPda, ForesterAccount, ForesterEpochPda, FORESTER_EPOCH_SEED}; +use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, Token, TokenAccount}; +use light_compressed_token::program::LightCompressedToken; +use light_compressed_token::POOL_SEED; +use light_system_program::program::LightSystemProgram; + +#[derive(Accounts)] +pub struct ClaimForesterInstruction<'info> { + /// Fee payer needs to be mutable to pay rollover and protocol fees. + #[account(mut)] + pub fee_payer: Signer<'info>, + pub authority: Signer<'info>, + /// CHECK: + #[account( + seeds = [CPI_AUTHORITY_PDA_SEED], bump + )] + pub cpi_authority: AccountInfo<'info>, + /// CHECK: + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: checked in emit_event.rs. + pub noop_program: AccountInfo<'info>, + /// CHECK: + pub account_compression_authority: AccountInfo<'info>, + /// CHECK: + pub account_compression_program: Program<'info, AccountCompression>, + /// CHECK: checked in cpi_signer_check. + pub invoking_program: AccountInfo<'info>, + /// CHECK: + pub system_program: AccountInfo<'info>, + pub self_program: Program<'info, crate::program::LightRegistry>, + /// CHECK: + pub token_cpi_authority_pda: AccountInfo<'info>, + pub light_system_program: Program<'info, LightSystemProgram>, + pub compressed_token_program: Program<'info, LightCompressedToken>, + pub spl_token_program: Program<'info, Token>, + #[account(mut)] + pub forester_token_pool: Account<'info, TokenAccount>, + #[account(mut)] + pub forester_pda: Account<'info, ForesterAccount>, + #[account(mut, seeds=[FORESTER_EPOCH_SEED, forester_pda.key().as_ref(), epoch_pda.epoch.to_le_bytes().as_ref()], bump ,close=fee_payer)] + pub forester_epoch_pda: Account<'info, ForesterEpochPda>, + #[account(mut)] + pub epoch_pda: Account<'info, EpochPda>, + #[account(mut, constraint= epoch_pda.protocol_config.mint == mint.key() @RegistryError::InvalidMint)] + pub mint: Account<'info, Mint>, + /// CHECK: (checked in different program) + #[account(mut)] + pub output_merkle_tree: AccountInfo<'info>, + #[account(mut, seeds= [POOL_SEED, mint.key().as_ref()], bump, seeds::program= compressed_token_program)] + pub compression_token_pool: Account<'info, TokenAccount>, +} + +impl<'info> SystemProgramAccounts<'info> for ClaimForesterInstruction<'info> { + fn get_registered_program_pda(&self) -> AccountInfo<'info> { + self.registered_program_pda.to_account_info() + } + fn get_noop_program(&self) -> AccountInfo<'info> { + self.noop_program.to_account_info() + } + fn get_account_compression_authority(&self) -> AccountInfo<'info> { + self.account_compression_authority.to_account_info() + } + fn get_account_compression_program(&self) -> AccountInfo<'info> { + self.account_compression_program.to_account_info() + } + fn get_system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + fn get_sol_pool_pda(&self) -> Option> { + None + } + fn get_decompression_recipient(&self) -> Option> { + None + } + fn get_light_system_program(&self) -> AccountInfo<'info> { + self.light_system_program.to_account_info() + } + fn get_self_program(&self) -> AccountInfo<'info> { + self.invoking_program.to_account_info() + } +} + +impl<'info> SignerAccounts<'info> for ClaimForesterInstruction<'info> { + fn get_fee_payer(&self) -> AccountInfo<'info> { + self.fee_payer.to_account_info() + } + fn get_authority(&self) -> AccountInfo<'info> { + self.authority.to_account_info() + } + fn get_cpi_authority_pda(&self) -> AccountInfo<'info> { + self.cpi_authority.to_account_info() + } +} + +impl<'info> CompressedTokenProgramAccounts<'info> for ClaimForesterInstruction<'info> { + fn get_token_cpi_authority_pda(&self) -> AccountInfo<'info> { + self.token_cpi_authority_pda.to_account_info() + } + fn get_compressed_token_program(&self) -> AccountInfo<'info> { + self.compressed_token_program.to_account_info() + } + fn get_escrow_authority_pda(&self) -> AccountInfo<'info> { + unimplemented!("escrow authority not implemented for ClaimForesterInstruction"); + } + fn get_spl_token_program(&self) -> AccountInfo<'info> { + self.spl_token_program.to_account_info() + } + fn get_token_pool_pda(&self) -> AccountInfo<'info> { + self.compression_token_pool.to_account_info() + } + fn get_compress_or_decompress_token_account(&self) -> Option> { + None + } +} +impl<'info> CompressedCpiContextTrait<'info> for ClaimForesterInstruction<'info> { + fn get_cpi_context(&self) -> Option> { + None + } +} +impl<'info> MintToAccounts<'info> for ClaimForesterInstruction<'info> { + fn get_mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } +} diff --git a/programs/registry/src/epoch/finalize_registration.rs b/programs/registry/src/epoch/finalize_registration.rs new file mode 100644 index 0000000000..e0c3ca4937 --- /dev/null +++ b/programs/registry/src/epoch/finalize_registration.rs @@ -0,0 +1,14 @@ +use anchor_lang::prelude::*; + +use crate::{EpochPda, ForesterEpochPda}; + +#[derive(Accounts)] +pub struct FinalizeRegistration<'info> { + #[account(mut)] + pub authority: Signer<'info>, + /// CHECK: + #[account(mut,has_one = authority)] + pub forester_epoch_pda: Account<'info, ForesterEpochPda>, + /// CHECK: TODO: check that this is the correct epoch account + pub epoch_pda: Account<'info, EpochPda>, +} diff --git a/programs/registry/src/epoch/mod.rs b/programs/registry/src/epoch/mod.rs new file mode 100644 index 0000000000..4f167f90c1 --- /dev/null +++ b/programs/registry/src/epoch/mod.rs @@ -0,0 +1,7 @@ +pub mod claim_forester; +pub mod claim_forester_instruction; +pub mod finalize_registration; +pub mod register_epoch; +pub mod report_work; +pub mod sync_delegate; +pub mod sync_delegate_instruction; diff --git a/programs/registry/src/epoch/register_epoch.rs b/programs/registry/src/epoch/register_epoch.rs new file mode 100644 index 0000000000..07ca8eaab9 --- /dev/null +++ b/programs/registry/src/epoch/register_epoch.rs @@ -0,0 +1,509 @@ +use crate::errors::RegistryError; +use crate::forester::state::{ForesterAccount, ForesterConfig}; +use crate::protocol_config::state::{ProtocolConfig, ProtocolConfigPda}; +use aligned_sized::aligned_sized; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::pubkey::Pubkey; + +/// Is used for tallying and rewards calculation +#[account] +#[aligned_sized(anchor)] +#[derive(Debug, Default, PartialEq, Eq)] +pub struct EpochPda { + pub epoch: u64, + pub protocol_config: ProtocolConfig, + pub total_work: u64, + pub registered_stake: u64, +} + +#[aligned_sized(anchor)] +#[account] +#[derive(Debug, Default, PartialEq, Eq)] +pub struct ForesterEpochPda { + pub authority: Pubkey, + pub config: ForesterConfig, + pub epoch: u64, + pub stake_weight: u64, + pub work_counter: u64, + /// Work can be reported in an extra round to earn extra performance based + /// rewards. // TODO: make sure that performance based rewards can only be + /// claimed if work has been reported + pub has_reported_work: bool, + /// Start index of the range that determines when the forester is eligible to perform work. + /// End index is forester_start_index + stake_weight + pub forester_index: u64, + pub epoch_active_phase_start_slot: u64, + /// Total epoch state weight is registered stake of the epoch account after + /// registration is concluded and active epoch period starts. + pub total_epoch_state_weight: Option, + pub protocol_config: ProtocolConfig, + /// Incremented every time finalize registration is called. + pub finalize_counter: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ForesterSlot { + pub slot: u64, + pub start_solana_slot: u64, + pub end_solana_slot: u64, + pub forester_index: u64, +} + +impl ForesterEpochPda { + pub fn get_current_slot(&self, current_slot: u64) -> Result { + if current_slot + >= self.epoch_active_phase_start_slot + self.protocol_config.active_phase_length + { + return err!(RegistryError::EpochEnded); + } + + let epoch_progres = match current_slot.checked_sub(self.epoch_active_phase_start_slot) { + Some(epoch_progres) => epoch_progres, + None => return err!(RegistryError::EpochEnded), + }; + Ok(epoch_progres / self.protocol_config.slot_length) + } + + // TODO: add function that returns all light slots with start and end solana slots for a given epoch + pub fn get_eligible_forester_index( + current_light_slot: u64, + pubkey: &Pubkey, + total_epoch_state_weight: u64, + ) -> Result { + // Domain separation using the pubkey and current_light_slot. + let mut hasher = anchor_lang::solana_program::hash::Hasher::default(); + hasher.hashv(&[ + pubkey.to_bytes().as_slice(), + ¤t_light_slot.to_le_bytes(), + ]); + let hash_value = u64::from_be_bytes(hasher.result().to_bytes()[0..8].try_into().unwrap()); + let forester_slot = hash_value % total_epoch_state_weight; + Ok(forester_slot) + } + + pub fn is_eligible(&self, forester_slot: u64) -> bool { + forester_slot >= self.forester_index + && forester_slot < self.forester_index + self.stake_weight + } + + /// Check forester account is: + /// - of correct epoch + /// - eligible to perform work in the current slot + pub fn check_eligibility(&self, current_slot: u64, pubkey: &Pubkey) -> Result<()> { + self.protocol_config + .is_active_phase(current_slot, self.epoch)?; + let current_light_slot = self.get_current_slot(current_slot)?; + let forester_slot = Self::get_eligible_forester_index( + current_light_slot, + pubkey, + self.total_epoch_state_weight.unwrap(), + )?; + if self.is_eligible(forester_slot) { + Ok(()) + } else { + err!(RegistryError::ForresterNotEligible) + } + } + + /// Checks forester: + /// - signer + /// - eligibility + /// - increments work counter. + pub fn check_forester( + forester_epoch_pda: &mut ForesterEpochPda, + authority: &Pubkey, + queue_pubkey: &Pubkey, + current_solana_slot: u64, + ) -> Result<()> { + if forester_epoch_pda.authority != *authority { + msg!( + "Invalid forester: forester_epoch_pda authority {} != provided {}", + forester_epoch_pda.authority, + authority + ); + return err!(RegistryError::InvalidForester); + } + // let current_slot = forester_epoch_pda.get_current_slot(current_solana_slot)?; + forester_epoch_pda.check_eligibility(current_solana_slot, queue_pubkey)?; + // TODO: check eligibility + forester_epoch_pda.work_counter += 1; + Ok(()) + } + + pub fn check_forester_in_program( + forester_epoch_pda: &mut ForesterEpochPda, + authority: &Pubkey, + queue_pubkey: &Pubkey, + ) -> Result<()> { + let current_solana_slot = anchor_lang::solana_program::sysvar::clock::Clock::get()?.slot; + Self::check_forester( + forester_epoch_pda, + authority, + queue_pubkey, + current_solana_slot, + ) + } +} + +/// This instruction needs to be executed once once the active period starts. +pub fn set_total_registered_stake_instruction( + forester_epoch_pda: &mut ForesterEpochPda, + epoch_pda: &EpochPda, +) { + forester_epoch_pda.total_epoch_state_weight = Some(epoch_pda.registered_stake); +} +// TODO: move to constants +pub const FORESTER_EPOCH_SEED: &[u8] = b"forester_epoch"; +pub const EPOCH_SEED: &[u8] = b"epoch"; + +#[derive(Accounts)] +pub struct UpdateForesterEpochPda<'info> { + #[account(address = forester_epoch_pda.authority)] + pub signer: Signer<'info>, + /// CHECK: + #[account(mut)] + pub forester_epoch_pda: Account<'info, ForesterEpochPda>, +} + +#[derive(Accounts)] +#[instruction(current_epoch: u64)] +pub struct RegisterForesterEpoch<'info> { + #[account(mut)] + pub authority: Signer<'info>, + #[account(mut, has_one = authority)] + pub forester_pda: Account<'info, ForesterAccount>, + /// CHECK: + #[account(init, seeds = [FORESTER_EPOCH_SEED, forester_pda.key().to_bytes().as_slice(), current_epoch.to_le_bytes().as_slice()], bump, space =ForesterEpochPda::LEN , payer = authority)] + pub forester_epoch_pda: Account<'info, ForesterEpochPda>, + pub protocol_config: Account<'info, ProtocolConfigPda>, + /// CHECK: TODO: check that this is the correct epoch account + #[account(init_if_needed, seeds = [EPOCH_SEED, current_epoch.to_le_bytes().as_slice()], bump, space =EpochPda::LEN, payer = authority)] + pub epoch_pda: Account<'info, EpochPda>, + system_program: Program<'info, System>, +} + +/// Register Forester for epoch: +/// 1. initialize epoch account if not initialized +/// 2. check that forester has enough stake +/// 3. check that forester has not already registered for the epoch +/// 4. check that we are in the registration period +/// 5. sync pending stake to active stake if stake hasn't been synced yet +/// 6. Initialize forester epoch account. +/// 7. Add forester active stake to epoch registered stake. +/// +/// Epoch account: +/// - should only be created in epoch registration period +/// - should only be created once +/// - contains the protocol config to set the protocol config for that epoch +/// (changes to protocol config take effect with next epoch) +/// - collectes the active stake of registered foresters +/// +/// Forester Epoch Account: +/// - should only be created in epoch registration period +/// - should only be created once per epoch per forester +#[inline(never)] +pub fn register_for_epoch_instruction( + authority: &Pubkey, + forester_pda: &mut ForesterAccount, + forester_epoch_pda: &mut ForesterEpochPda, + epoch_pda: &mut EpochPda, + current_slot: u64, +) -> Result<()> { + msg!("epoch_pda.protocol config: {:?}", epoch_pda.protocol_config); + if forester_pda.active_stake_weight < epoch_pda.protocol_config.min_stake { + return err!(RegistryError::StakeInsuffient); + } + if forester_pda.last_registered_epoch == epoch_pda.epoch && epoch_pda.epoch != 0 { + msg!( + "forester_pda.last_registered_epoch: {}", + forester_pda.last_registered_epoch + ); + msg!("epoch_pda.epoch: {}", epoch_pda.epoch); + // With onchain implementation this error will not be necessary for the pda will be derived from the epoch + // from the forester pubkey. + return err!(RegistryError::ForesterAlreadyRegistered); + } + msg!("epoch_pda.epoch: {}", epoch_pda.epoch); + // Check whether we are in a epoch registration phase and which epoch we are in + let current_epoch_start_slot = epoch_pda + .protocol_config + .is_registration_phase(current_slot)?; + msg!("current_epoch_start_slot: {}", current_epoch_start_slot); + // Sync pending stake to active stake if stake hasn't been synced yet. + forester_pda.sync(current_slot, &epoch_pda.protocol_config)?; + forester_pda.last_registered_epoch = epoch_pda.epoch; + msg!("synced forester account stake"); + msg!("register for epoch with forester_pda: {:?}", forester_pda); + msg!("stake: {}", forester_pda.active_stake_weight); + // Add forester active stake to epoch registered stake. + // // Initialize forester epoch account. + let initialized_forester_epoch_pda = ForesterEpochPda { + authority: *authority, + config: forester_pda.config, + epoch: epoch_pda.epoch, + stake_weight: forester_pda.active_stake_weight, + work_counter: 0, + has_reported_work: false, + epoch_active_phase_start_slot: current_epoch_start_slot, + forester_index: epoch_pda.registered_stake, + total_epoch_state_weight: None, + protocol_config: epoch_pda.protocol_config, + finalize_counter: 0, + }; + forester_epoch_pda.clone_from(&initialized_forester_epoch_pda); + epoch_pda.registered_stake += forester_pda.active_stake_weight; + + Ok(()) +} + +#[cfg(test)] +mod test { + use solana_sdk::signature::{Keypair, Signer}; + use std::collections::HashMap; + + use super::*; + + fn setup_forester_epoch_pda( + forester_start_range: u64, + forester_stake_weight: u64, + active_phase_length: u64, + slot_length: u64, + epoch_active_phase_start_slot: u64, + total_epoch_state_weight: u64, + ) -> ForesterEpochPda { + ForesterEpochPda { + authority: Pubkey::new_unique(), + config: ForesterConfig::default(), + epoch: 0, + stake_weight: forester_stake_weight, + work_counter: 0, + has_reported_work: false, + forester_index: forester_start_range, + epoch_active_phase_start_slot, + total_epoch_state_weight: Some(total_epoch_state_weight), + finalize_counter: 0, + protocol_config: ProtocolConfig { + genesis_slot: 0, + registration_phase_length: 1, + active_phase_length, + report_work_phase_length: 2, + epoch_reward: 100_000, + base_reward: 50_000, + min_stake: 0, + slot_length, + mint: Pubkey::new_unique(), + forester_registration_guarded: false, + }, + } + } + + // Instead of index I use stake weight to get the period + #[test] + fn test_eligibility_check_within_epoch() { + let mut eligible = HashMap::::new(); + let slot_length = 20; + let num_foresters = 5; + let epoch_active_phase_start_slot = 10; + let epoch_len = 2000; + let queue_pubkey = Keypair::new().pubkey(); + let mut total_stake_weight = 0; + for forester_index in 0..num_foresters { + let forester_stake_weight = 10_000 * (forester_index + 1); + total_stake_weight += forester_stake_weight; + } + let mut current_total_stake_weight = 0; + for forester_index in 0..num_foresters { + let forester_stake_weight = 10_000 * (forester_index + 1); + let account = setup_forester_epoch_pda( + current_total_stake_weight, + forester_stake_weight, + epoch_len, + slot_length, + epoch_active_phase_start_slot, + total_stake_weight, + ); + current_total_stake_weight += forester_stake_weight; + + // Check eligibility within and outside the epoch + for i in 0..epoch_len { + let index = account.check_eligibility(i, &queue_pubkey); + if index.is_ok() { + match eligible.get_mut(&(forester_index as u8)) { + Some((_, count)) => { + *count += 1; + } + None => { + eligible.insert(forester_index as u8, (forester_stake_weight, 1)); + } + }; + } + } + } + println!("stats --------------------------------"); + for (forester_index, num_eligible_slots) in eligible.iter() { + println!("forester_index = {:?}", forester_index); + println!("num_eligible_slots = {:?}", num_eligible_slots); + } + + let sum = eligible.values().map(|x| x.1).sum::(); + let total_slots: u64 = epoch_len - epoch_active_phase_start_slot; + assert_eq!(sum, total_slots); + } + + #[test] + fn test_onchain_epoch() { + let registration_phase_length = 1; + let active_phase_length = 7; + let report_work_phase_length = 2; + let protocol_config = ProtocolConfig { + genesis_slot: 20, + registration_phase_length, + active_phase_length, + report_work_phase_length, + epoch_reward: 100_000, + base_reward: 50_000, + min_stake: 0, + slot_length: 1, + mint: Pubkey::new_unique(), + forester_registration_guarded: false, + }; + // Diagram of epochs 0 and 1. + // Registration 0 starts at genesis slot. + // |---- Registration 0 ----|------------------ Active 0 ------|---- Report Work 0 ----|---- Post 0 ---- + // |-- Registration 1 --|------------------ Active 1 ----------------- + + let mut current_slot = protocol_config.genesis_slot; + for epoch in 0..1000 { + if epoch == 0 { + for _ in 0..protocol_config.registration_phase_length { + assert!(protocol_config.is_registration_phase(current_slot).is_ok()); + + assert!(protocol_config + .is_active_phase(current_slot, epoch) + .is_err()); + assert!(protocol_config.is_post_epoch(current_slot, epoch).is_err()); + + assert!(protocol_config + .is_report_work_phase(current_slot, epoch) + .is_err()); + + current_slot += 1; + } + } + + for i in 0..protocol_config.active_phase_length { + assert!(protocol_config.is_active_phase(current_slot, epoch).is_ok()); + if protocol_config.active_phase_length.saturating_sub(i) + <= protocol_config.registration_phase_length + { + assert!(protocol_config.is_registration_phase(current_slot).is_ok()); + } else { + assert!(protocol_config.is_registration_phase(current_slot).is_err()); + } + if epoch == 0 { + assert!(protocol_config.is_post_epoch(current_slot, epoch).is_err()); + } else { + assert!(protocol_config + .is_post_epoch(current_slot, epoch - 1) + .is_ok()); + } + if epoch == 0 { + assert!(protocol_config + .is_report_work_phase(current_slot, epoch) + .is_err()); + } else if i < protocol_config.report_work_phase_length { + assert!(protocol_config + .is_report_work_phase(current_slot, epoch - 1) + .is_ok()); + } else { + assert!(protocol_config + .is_report_work_phase(current_slot, epoch - 1) + .is_err()); + } + assert!(protocol_config + .is_report_work_phase(current_slot, epoch) + .is_err()); + current_slot += 1; + } + } + } + + // TODO: remove + #[test] + fn test_offchain_epoch() { + let registration_phase_length = 1; + let active_phase_length = 7; + let report_work_phase_length = 2; + let protocol_config = ProtocolConfig { + genesis_slot: 20, + registration_phase_length, + active_phase_length, + report_work_phase_length, + epoch_reward: 100_000, + base_reward: 50_000, + min_stake: 0, + slot_length: 1, + mint: Pubkey::new_unique(), + forester_registration_guarded: false, + }; + // Diagram of epochs 0 and 1. + // Registration 0 starts at genesis slot. + // |---- Registration 0 ----|------------------ Active 0 ------|---- Report Work 0 ----|---- Post 0 ---- + // |-- Registration 1 --|------------------ Active 1 ----------------- + + let mut current_slot = protocol_config.genesis_slot; + for epoch in 0..1000 { + if epoch == 0 { + for _ in 0..protocol_config.registration_phase_length { + assert!(protocol_config.is_registration_phase(current_slot).is_ok()); + + assert!(protocol_config + .is_active_phase(current_slot, epoch) + .is_err()); + assert!(protocol_config.is_post_epoch(current_slot, epoch).is_err()); + + assert!(protocol_config + .is_report_work_phase(current_slot, epoch) + .is_err()); + + current_slot += 1; + } + } + + for i in 0..protocol_config.active_phase_length { + assert!(protocol_config.is_active_phase(current_slot, epoch).is_ok()); + if protocol_config.active_phase_length.saturating_sub(i) + <= protocol_config.registration_phase_length + { + assert!(protocol_config.is_registration_phase(current_slot).is_ok()); + } else { + assert!(protocol_config.is_registration_phase(current_slot).is_err()); + } + if epoch == 0 { + assert!(protocol_config.is_post_epoch(current_slot, epoch).is_err()); + } else { + assert!(protocol_config + .is_post_epoch(current_slot, epoch - 1) + .is_ok()); + } + if epoch == 0 { + assert!(protocol_config + .is_report_work_phase(current_slot, epoch) + .is_err()); + } else if i < protocol_config.report_work_phase_length { + assert!(protocol_config + .is_report_work_phase(current_slot, epoch - 1) + .is_ok()); + } else { + assert!(protocol_config + .is_report_work_phase(current_slot, epoch - 1) + .is_err()); + } + assert!(protocol_config + .is_report_work_phase(current_slot, epoch) + .is_err()); + current_slot += 1; + } + } + } +} diff --git a/programs/registry/src/epoch/report_work.rs b/programs/registry/src/epoch/report_work.rs new file mode 100644 index 0000000000..a5734bf740 --- /dev/null +++ b/programs/registry/src/epoch/report_work.rs @@ -0,0 +1,51 @@ +use crate::errors::RegistryError; + +use super::register_epoch::{EpochPda, ForesterEpochPda}; +use anchor_lang::prelude::*; +/// Report work: +/// - work is reported so that performance based rewards can be calculated after +/// the report work phase ends +/// 1. Check that we are in the report work phase +/// 2. Check that forester has registered for the epoch +/// 3. Check that forester has not already reported work +/// 4. Add work to total work +/// +/// Considerations: +/// - we could remove this phase: +/// -> con: we would have no performance based rewards +/// -> pro: reduced complexity +/// 1. Design possibilities even without a separate phase: +/// - we could introduce a separate reward just per work performed (uncapped, +/// for weighted cap we need this round, hardcoded cap would work without +/// this round) +/// - reward could be in sol, or light tokens +pub fn report_work_instruction( + forester_epoch_pda: &mut ForesterEpochPda, + epoch_pda: &mut EpochPda, + current_slot: u64, +) -> Result<()> { + epoch_pda + .protocol_config + .is_report_work_phase(current_slot, epoch_pda.epoch)?; + + if forester_epoch_pda.epoch != epoch_pda.epoch { + return err!(RegistryError::InvalidEpochAccount); + } + if forester_epoch_pda.has_reported_work { + return err!(RegistryError::ForesterAlreadyRegistered); + } + + forester_epoch_pda.has_reported_work = true; + epoch_pda.total_work += forester_epoch_pda.work_counter; + Ok(()) +} + +#[derive(Accounts)] +pub struct ReportWork<'info> { + authority: Signer<'info>, + // TODO: rename forester_epoch_pda to forester_epoch_pda + #[account(mut, has_one = authority)] + pub forester_epoch_pda: Account<'info, ForesterEpochPda>, + #[account(mut)] + pub epoch_pda: Account<'info, EpochPda>, +} diff --git a/programs/registry/src/epoch/sync_delegate.rs b/programs/registry/src/epoch/sync_delegate.rs new file mode 100644 index 0000000000..5ed6e2a784 --- /dev/null +++ b/programs/registry/src/epoch/sync_delegate.rs @@ -0,0 +1,1161 @@ +use crate::delegate::deposit::{ + create_compressed_delegate_account, create_delegate_compressed_account, + update_escrow_compressed_token_account, DelegateAccountWithPackedContext, +}; +use crate::delegate::process_cpi::{ + approve_spl_token, cpi_compressed_token_transfer, get_cpi_signer_seeds, +}; +use crate::delegate::{ + get_escrow_token_authority, ESCROW_TOKEN_ACCOUNT_SEED, + FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, +}; +use crate::delegate::{process_cpi::cpi_light_system_program, state::DelegateAccount}; +use crate::errors::RegistryError; +use anchor_lang::prelude::*; +use light_compressed_token::process_transfer::{ + InputTokenDataWithContext, PackedTokenTransferOutputData, +}; +use light_system_program::invoke::processor::CompressedProof; +use light_system_program::sdk::compressed_account::{ + CompressedAccount, CompressedAccountData, PackedCompressedAccountWithMerkleContext, + PackedMerkleContext, +}; +use light_system_program::sdk::CompressedCpiContext; +use light_system_program::OutputCompressedAccountWithPackedContext; +use light_utils::hash_to_bn254_field_size_be; + +use super::claim_forester::CompressedForesterEpochAccountInput; +use super::sync_delegate_instruction::SyncDelegateInstruction; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Copy, PartialEq)] +pub struct SyncDelegateTokenAccount { + pub salt: u64, + pub cpi_context: CompressedCpiContext, +} + +/// THIS IS INSECURE +/// TODO: make secure by checking inclusion of the last compressed forester epoch pda +// 392 bytes + 576 accounts + 64 signature = 1032 bytes -> 200 bytes for 8 +// CompressedForesterEpochAccountInput compressed accounts +pub fn process_sync_delegate_account<'info>( + ctx: Context<'_, '_, '_, 'info, SyncDelegateInstruction<'info>>, + delegate_account: DelegateAccountWithPackedContext, // 155 bytes + previous_hash: [u8; 32], // 32 bytes + forester_pda_pubkey: Pubkey, // 32 bytes + compressed_forester_epoch_pdas: Vec, // 4 bytes + last_account_root_index: u16, // 2 bytes + last_account_merkle_context: PackedMerkleContext, // 7 bytes + inclusion_proof: CompressedProof, // 128 bytes + sync_delegate_token_account: Option, // 12 bytes + input_escrow_token_account: Option, // 20 bytes + output_token_account_merkle_tree_index: u8, +) -> Result<()> { + let authority = ctx.accounts.authority.key(); + let escrow_authority = if let Some(authority) = ctx.accounts.escrow_token_authority.as_ref() { + Some(authority.key()) + } else { + None + }; + let slot = Clock::get()?.slot; + let epoch = ctx.accounts.protocol_config.config.get_current_epoch(slot); + let ( + input_delegate_compressed_account, + // TODO: we need readonly accounts just add a bool to input accounts context and don't nullify, skip in sum checks + _input_readonly_compressed_forester_epoch_account, + output_account_with_merkle_context, + output_token_escrow_account, + ) = sync_delegate_account_and_create_compressed_accounts( + authority, + delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + last_account_merkle_context, + last_account_root_index, + &input_escrow_token_account, + &escrow_authority, + output_token_account_merkle_tree_index, + epoch, + )?; + + let cpi_context = if let Some(sync_delegate_token_account) = sync_delegate_token_account { + let (salt, mut cpi_context) = ( + sync_delegate_token_account.salt, + CompressedCpiContext { + first_set_context: sync_delegate_token_account.cpi_context.first_set_context, + set_context: true, + ..sync_delegate_token_account.cpi_context + }, + ); + let input_escrow_token_account = + if let Some(input_escrow_token_account) = input_escrow_token_account { + Ok(input_escrow_token_account) + } else { + err!(RegistryError::InvalidAuthority) + }?; + msg!( + "input_escrow_token_account: {:?}", + input_escrow_token_account + ); + let output_token_escrow_account = + if let Some(output_token_escrow_account) = output_token_escrow_account { + Ok(output_token_escrow_account) + } else { + err!(RegistryError::InvalidAuthority) + }?; + msg!( + "output_token_escrow_account: {:?}", + output_token_escrow_account + ); + let amount_diff = output_token_escrow_account.amount - input_escrow_token_account.amount; + + let cpi_signer = ctx + .accounts + .escrow_token_authority + .as_ref() + .unwrap() + .to_account_info(); + msg!("cpi_signer: {:?}", cpi_signer.key()); + msg!( + "forester_token_pool {:?}", + ctx.accounts.forester_token_pool.as_ref().unwrap().key() + ); + approve_spl_token( + &ctx, + amount_diff, + ctx.accounts + .forester_token_pool + .as_ref() + .unwrap() + .to_account_info(), + cpi_signer.to_account_info(), + get_cpi_signer_seeds(), + )?; + + let owner = ctx.accounts.authority.key(); + + let mint = ctx.accounts.protocol_config.config.mint; + let (_, bump) = get_escrow_token_authority(&owner, salt); + let bump = &[bump]; + let salt_bytes = salt.to_le_bytes(); + let seeds = [ + ESCROW_TOKEN_ACCOUNT_SEED, + owner.as_ref(), + salt_bytes.as_slice(), + bump, + ]; + cpi_compressed_token_transfer( + &ctx, + None, + Some(amount_diff), + true, + salt, + cpi_context, + &mint, + vec![input_escrow_token_account], + vec![output_token_escrow_account], + &owner, + cpi_signer, + seeds, + ctx.remaining_accounts.to_vec(), + )?; + cpi_context.set_context = sync_delegate_token_account.cpi_context.set_context; + cpi_context.first_set_context = false; + Some(cpi_context) + } else { + None + }; + cpi_light_system_program( + &ctx, + Some(inclusion_proof), + cpi_context, + Some(input_delegate_compressed_account), // TODO: add readonly account + output_account_with_merkle_context, + ctx.remaining_accounts.to_vec(), + ) +} + +fn sync_delegate_account_and_create_compressed_accounts( + authority: Pubkey, + mut delegate_account: DelegateAccountWithPackedContext, + compressed_forester_epoch_pdas: Vec, + previous_hash: [u8; 32], + forester_pda_pubkey: Pubkey, + last_account_merkle_context: PackedMerkleContext, + last_account_root_index: u16, + input_escrow_token_account: &Option, + escrow_token_authority: &Option, + merkle_tree_index: u8, + epoch: u64, +) -> Result<( + PackedCompressedAccountWithMerkleContext, + PackedCompressedAccountWithMerkleContext, + OutputCompressedAccountWithPackedContext, + Option, +)> { + if authority != delegate_account.delegate_account.owner { + return err!(RegistryError::InvalidAuthority); + } + + let input_delegate_compressed_account = create_compressed_delegate_account( + delegate_account.delegate_account, + delegate_account.merkle_context, + delegate_account.root_index, + )?; + let epoch = compressed_forester_epoch_pdas.last().unwrap().epoch; + + let last_forester_pda_hash = sync_delegate_account( + &mut delegate_account.delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + )?; + // could also take epoch from last compressed forester epoch pda + delegate_account + .delegate_account + .sync_pending_stake_weight(epoch); + let input_readonly_compressed_forester_epoch_account = create_compressed_forester_epoch_account( + last_forester_pda_hash, + last_account_merkle_context, + last_account_root_index, + ); + + let output_escrow_account = if input_escrow_token_account.is_some() { + let amount = delegate_account.delegate_account.pending_token_amount; + msg!("pending token amount: {:?}", amount); + delegate_account.delegate_account.pending_token_amount = 0; + Some(update_escrow_compressed_token_account::( + &escrow_token_authority.unwrap(), + input_escrow_token_account, + amount, + merkle_tree_index, + )?) + } else { + None + }; + + let output_account: CompressedAccount = + create_delegate_compressed_account::(&delegate_account.delegate_account)?; + let output_account_with_merkle_context = OutputCompressedAccountWithPackedContext { + compressed_account: output_account, + merkle_tree_index: delegate_account.output_merkle_tree_index, + }; + + Ok(( + input_delegate_compressed_account, + input_readonly_compressed_forester_epoch_account, + output_account_with_merkle_context, + output_escrow_account, + )) +} + +// TODO: test that hash is equivalent to manually instantiated account +fn create_compressed_forester_epoch_account( + last_forester_pda_hash: [u8; 32], + last_account_merkle_context: PackedMerkleContext, + last_account_root_index: u16, +) -> PackedCompressedAccountWithMerkleContext { + let data = CompressedAccountData { + discriminator: FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, + data_hash: last_forester_pda_hash, + data: Vec::new(), + }; + let readonly_input_account = CompressedAccount { + owner: crate::ID, + lamports: 0, + address: None, + data: Some(data), + }; + PackedCompressedAccountWithMerkleContext { + compressed_account: readonly_input_account, + merkle_context: last_account_merkle_context, + root_index: last_account_root_index, + } +} + +/** + * Issue: + * - I can get into a situation where the registered epoch has been created, + * delegate has claimed, + * the claimed stake is not part of the current registered epoch yet + * since it wasn't calculated until after registration has concluded + * -> delegate has more stake than she should + * + * reproduces: + * cargo test-sbf -p registry-test -- --test test_init --nocapture > output.txt 2>&1 + * + * Ideas: + * - don't allow claiming of the last epoch (leads to weird edge cases) + * - have an extra field which holds the stake of the last epoch (doesn't count into active stake yet) + * - this seems to be more manageable but is also ugly + * + * + * There are too many unknowns: + * - what do I do if someone undelegates? (needs to sync completely first,then full undelegate should work, + * what about partial? -> we need to record the stake at the beginning of the last epoch that was synced and all subsequent epochs + * is this really an issue? this can only happen for one epoch because of the overlap of phases of different epochs -> lag between claiming + * - registration for epoch requires epoch pda which is always up to date of the latest claimed reward -> its really just 1 epoch reward + * + * -> I need a thorough offchain test to mock these scenarios + * + * This might take too much time, I need to: + * - sleep + * - switch to contention and make that prod ready first + * + */ + +/// Sync Delegate Account: +/// - syncs the virtual balance of accumulated stake rewards to the stake +/// account +/// - it does not sync the token stake account balance +/// - the token stake account balance must be fully synced to perform any +/// actions that move delegated stake +/// 1. input a vector of compressed forester epoch accounts +/// 2. Check that epoch of first compressed forester epoch account is less than +/// DelegateAccount.last_sync_epoch +/// 3. iterate over all compressed forester epoch accounts, increase +/// Account.stake_weight by rewards_earned in every step +/// 4. set DelegateAccount.last_sync_epoch to the epoch of the last compressed +/// forester epoch account +/// 5. prove inclusion of last hash in State merkle tree (outside of this function) +pub fn sync_delegate_account( + delegate_account: &mut DelegateAccount, + // pending_synced_stake_weight: &mut u64, + // mut pending_synced_stake_weight: Vec<(u64, u64)>, // (stake_weight, epoch) + compressed_forester_epoch_pdas: Vec, + // epoch_synced: Vec, + // add last synced epoch to compressed_forester_epoch_pdas (probably need to add this to forester epoch pdas too) + // keep rewards of none synced epochs in a vector, its active stake but not synced yet, + // TODO: ensure that this pending stake can also be undelegated + mut previous_hash: [u8; 32], + forester_pubkey: Pubkey, +) -> Result<[u8; 32]> { + let last_sync_epoch = delegate_account.last_sync_epoch; + if compressed_forester_epoch_pdas[0].epoch <= last_sync_epoch && last_sync_epoch != 0 { + return err!(RegistryError::StakeAccountAlreadySynced); + } + let hashed_forester_pubkey = hash_to_bn254_field_size_be(forester_pubkey.as_ref()) + .ok_or(RegistryError::HashToFieldError)? + .0; + // let mut epoch_rewards = vec![]; + // let mut last_stake_weight = delegate_account.delegated_stake_weight; + let mut last_epoch = delegate_account.last_sync_epoch; + for (i, compressed_forester_epoch_pda) in compressed_forester_epoch_pdas.iter().enumerate() { + delegate_account.sync_pending_stake_weight(compressed_forester_epoch_pda.epoch); + + // Forester pubkey is not hashed thus we use a random value and hash offchain + let compressed_forester_epoch_pda = compressed_forester_epoch_pda + .into_compressed_forester_epoch_pda(previous_hash, crate::ID); + previous_hash = compressed_forester_epoch_pda.hash(hashed_forester_pubkey)?; + msg!( + "delegate_account.delegated_stake_weight: {:?}", + delegate_account.delegated_stake_weight + ); + msg!( + "delegate_account.pending_undelegated_stake_weight {:?}", + delegate_account.pending_undelegated_stake_weight + ); + msg!( + "stake from last epoch {:?}", + delegate_account.delegated_stake_weight + + delegate_account.pending_undelegated_stake_weight + - delegate_account.pending_synced_stake_weight, + ); + let pending_synced_stake_weight = if compressed_forester_epoch_pda.epoch - last_epoch == 1 { + delegate_account.pending_synced_stake_weight + } else { + 0 + }; + msg!( + "pending_synced_stake_weight: {:?}", + pending_synced_stake_weight + ); + // TODO: double check that this doesn't become an issue when undelegating + let get_delegate_epoch_reward = compressed_forester_epoch_pda.get_reward( + delegate_account.delegated_stake_weight + + delegate_account.pending_undelegated_stake_weight + - pending_synced_stake_weight, + )?; + msg!( + "compressed_forester_epoch_pda: {:?}", + compressed_forester_epoch_pda + ); + msg!("epoch reward: {:?}", get_delegate_epoch_reward); + delegate_account.delegated_stake_weight = delegate_account + .delegated_stake_weight + .checked_add(get_delegate_epoch_reward) + .ok_or(RegistryError::ArithmeticOverflow)?; + // Tokens are already minted and can be synced to the delegate account + delegate_account.pending_token_amount = delegate_account + .pending_token_amount + .checked_add(get_delegate_epoch_reward) + .ok_or(RegistryError::ArithmeticOverflow)?; + // the registry account is always one epoch behind -> we need cache the + // last epoch reward and subtract it when calculating the reward for the + // next epoch + delegate_account.pending_synced_stake_weight = get_delegate_epoch_reward; + last_epoch = compressed_forester_epoch_pda.epoch; + // last_stake_weight = compressed_forester_epoch_pda.stake_weight; + // Return the last forester epoch account hash. + // We need to prove inclusion of the last hash in the State merkle tree. + if i == compressed_forester_epoch_pdas.len() - 1 { + let last_delegate_account = compressed_forester_epoch_pda; + delegate_account.last_sync_epoch = last_delegate_account.epoch; + println!( + "final pending_synced_stake_weight: {:?}", + delegate_account.pending_synced_stake_weight + ); + return Ok(previous_hash); + } + } + // This error is unreachable since the loop returns after the last iteration. + err!(RegistryError::StakeAccountSyncError) +} + +#[cfg(test)] +mod tests { + use core::num; + + use crate::{ + delegate::DELEGATE_ACCOUNT_DISCRIMINATOR, + epoch::claim_forester::CompressedForesterEpochAccount, + }; + + use super::*; + use light_hasher::{DataHasher, Poseidon}; + use light_system_program::sdk::compressed_account::{ + CompressedAccount, CompressedAccountData, PackedCompressedAccountWithMerkleContext, + PackedMerkleContext, + }; + + fn get_test_data() -> ( + [u8; 32], + PackedMerkleContext, + u16, + PackedCompressedAccountWithMerkleContext, + ) { + let last_forester_pda_hash = [1; 32]; + let last_account_merkle_context = PackedMerkleContext { + merkle_tree_pubkey_index: 1, + nullifier_queue_pubkey_index: 2, + leaf_index: 1234, + queue_index: None, + }; + let last_account_root_index = 5; + + let expected_data = CompressedAccountData { + discriminator: FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, + data_hash: last_forester_pda_hash, + data: Vec::new(), + }; + let expected_account = CompressedAccount { + owner: crate::ID, + lamports: 0, + address: None, + data: Some(expected_data), + }; + let expected_output = PackedCompressedAccountWithMerkleContext { + compressed_account: expected_account, + merkle_context: last_account_merkle_context.clone(), + root_index: last_account_root_index, + }; + + ( + last_forester_pda_hash, + last_account_merkle_context, + last_account_root_index, + expected_output, + ) + } + + #[test] + fn test_create_compressed_forester_epoch_account_passing() { + let ( + last_forester_pda_hash, + last_account_merkle_context, + last_account_root_index, + expected_output, + ) = get_test_data(); + + let output = create_compressed_forester_epoch_account( + last_forester_pda_hash, + last_account_merkle_context, + last_account_root_index, + ); + + assert_eq!(output, expected_output); + let authority = Pubkey::new_unique(); + assert_eq!( + output + .compressed_account + .hash::(&authority, &output.merkle_context.leaf_index) + .unwrap(), + expected_output + .compressed_account + .hash::(&authority, &expected_output.merkle_context.leaf_index) + .unwrap(), + ); + } + + #[test] + fn test_create_compressed_forester_epoch_account_failing() { + let ( + last_forester_pda_hash, + last_account_merkle_context, + last_account_root_index, + mut expected_output, + ) = get_test_data(); + expected_output + .compressed_account + .data + .as_mut() + .unwrap() + .data_hash = [2; 32]; + let output = create_compressed_forester_epoch_account( + last_forester_pda_hash, + last_account_merkle_context, + last_account_root_index, + ); + assert_ne!(output, expected_output); + } + + fn get_test_data_sync() -> ( + DelegateAccount, + Vec, + [u8; 32], + Pubkey, + DelegateAccount, + ) { + let initial_delegate_account = DelegateAccount { + owner: Pubkey::new_unique(), + delegate_forester_delegate_account: None, + delegated_stake_weight: 100, + ..Default::default() + }; + + let compressed_forester_epoch_pdas = vec![ + CompressedForesterEpochAccountInput { + rewards_earned: 10, + epoch: 0, + stake_weight: 100, + }, + // Registry account for epoch 1 is inited when epoch 0 is still ongoing + // -> rewards_earned are not yet calculated thus not included in stake_weight + CompressedForesterEpochAccountInput { + rewards_earned: 20, + epoch: 1, + stake_weight: 100, + }, + CompressedForesterEpochAccountInput { + rewards_earned: 30, + epoch: 2, + stake_weight: 110, + }, + ]; + + let previous_hash = [0; 32]; + let forester_pda_pubkey = Pubkey::new_unique(); + let mut expected_delegate_account = initial_delegate_account.clone(); + expected_delegate_account.delegated_stake_weight += 10 + 20 + 30; + expected_delegate_account.pending_token_amount += 10 + 20 + 30; + expected_delegate_account.last_sync_epoch = 2; + expected_delegate_account.pending_synced_stake_weight = 30; + ( + initial_delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + expected_delegate_account, + ) + } + + #[test] + fn test_sync_delegate_account_passing() { + let ( + mut delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + expected_delegate_account, + ) = get_test_data_sync(); + + let result = sync_delegate_account( + &mut delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + ); + + assert!(result.is_ok()); + assert_eq!(delegate_account, expected_delegate_account); + } + fn get_test_data_sync_inconcistent() -> ( + DelegateAccount, + Vec, + [u8; 32], + Pubkey, + DelegateAccount, + ) { + let initial_delegate_account = DelegateAccount { + owner: Pubkey::new_unique(), + delegate_forester_delegate_account: None, + delegated_stake_weight: 1000000, + ..Default::default() + }; + let rewards = 990000; + + let compressed_forester_epoch_pdas = vec![ + CompressedForesterEpochAccountInput { + rewards_earned: rewards, + epoch: 0, + stake_weight: 1000000, + }, + CompressedForesterEpochAccountInput { + rewards_earned: rewards, + epoch: 1, + stake_weight: 1000000, + }, + CompressedForesterEpochAccountInput { + rewards_earned: rewards, + epoch: 2, + stake_weight: 1000000 + 990000, + }, + // CompressedForesterEpochAccountInput { + // rewards_earned: 990000, + // epoch: 3, + // stake_weight: 1990000 + 2 * 990000, + // }, + ]; + + let previous_hash = [0; 32]; + let forester_pda_pubkey = Pubkey::new_unique(); + let mut expected_delegate_account = initial_delegate_account.clone(); + expected_delegate_account.delegated_stake_weight += 3 * rewards; + expected_delegate_account.pending_token_amount += 3 * rewards; + expected_delegate_account.last_sync_epoch = 2; + expected_delegate_account.pending_synced_stake_weight = rewards; + ( + initial_delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + expected_delegate_account, + ) + } + #[test] + fn test_sync_delegate_account_inconsistent_updates_passing() { + let ( + mut delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + expected_delegate_account, + ) = get_test_data_sync_inconcistent(); + let result = sync_delegate_account( + &mut delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + ); + + assert!(result.is_ok()); + assert_eq!(delegate_account, expected_delegate_account); + } + + fn get_test_data_sync_skipped_epoch() -> ( + DelegateAccount, + Vec, + [u8; 32], + Pubkey, + DelegateAccount, + ) { + let initial_delegate_account = DelegateAccount { + owner: Pubkey::new_unique(), + delegate_forester_delegate_account: None, + delegated_stake_weight: 1000000, + ..Default::default() + }; + let rewards = 990000; + + let compressed_forester_epoch_pdas = vec![ + CompressedForesterEpochAccountInput { + rewards_earned: rewards, + epoch: 0, + stake_weight: 1000000, + }, + // CompressedForesterEpochAccountInput { + // rewards_earned: rewards, + // epoch: 1, + // stake_weight: 1000000, + // }, + CompressedForesterEpochAccountInput { + rewards_earned: rewards, + epoch: 2, + stake_weight: 1000000 + 990000, + }, + // CompressedForesterEpochAccountInput { + // rewards_earned: 990000, + // epoch: 3, + // stake_weight: 1990000 + 2 * 990000, + // }, + ]; + + let previous_hash = [0; 32]; + let forester_pda_pubkey = Pubkey::new_unique(); + let mut expected_delegate_account = initial_delegate_account.clone(); + expected_delegate_account.delegated_stake_weight += 2 * rewards; + expected_delegate_account.pending_token_amount += 2 * rewards; + expected_delegate_account.last_sync_epoch = 2; + expected_delegate_account.pending_synced_stake_weight = rewards; + ( + initial_delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + expected_delegate_account, + ) + } + + #[test] + fn test_sync_delegate_account_skipped_updates_passing() { + let ( + mut delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + expected_delegate_account, + ) = get_test_data_sync_skipped_epoch(); + let result = sync_delegate_account( + &mut delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + ); + + assert!(result.is_ok()); + assert_eq!(delegate_account, expected_delegate_account); + } + + fn get_rnd_test_data() -> ( + DelegateAccount, + Vec, + [u8; 32], + Pubkey, + DelegateAccount, + ) { + let initial_delegate_account = DelegateAccount { + owner: Pubkey::new_unique(), + delegate_forester_delegate_account: None, + delegated_stake_weight: 1000000, + ..Default::default() + }; + let rewards = 990000; + let num_iter = 2; + use rand::SeedableRng; + let mut rng = rand::rngs::StdRng::seed_from_u64(0); + // let mut compressed_forester_epoch_pdas = vec![]; + let mut expected_rewards = 0; + + // for i in 0..num_iter { + + // } + + let compressed_forester_epoch_pdas = vec![ + CompressedForesterEpochAccountInput { + rewards_earned: rewards, + epoch: 0, + stake_weight: 1000000, + }, + // CompressedForesterEpochAccountInput { + // rewards_earned: rewards, + // epoch: 1, + // stake_weight: 1000000, + // }, + CompressedForesterEpochAccountInput { + rewards_earned: rewards, + epoch: 2, + stake_weight: 1000000 + 990000, + }, + // CompressedForesterEpochAccountInput { + // rewards_earned: 990000, + // epoch: 3, + // stake_weight: 1990000 + 2 * 990000, + // }, + ]; + + let previous_hash = [0; 32]; + let forester_pda_pubkey = Pubkey::new_unique(); + let mut expected_delegate_account = initial_delegate_account.clone(); + expected_delegate_account.delegated_stake_weight += 2 * rewards; + expected_delegate_account.pending_token_amount += 2 * rewards; + expected_delegate_account.last_sync_epoch = 2; + expected_delegate_account.pending_synced_stake_weight = rewards; + ( + initial_delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + expected_delegate_account, + ) + } + + #[test] + fn test_sync_delegate_account_undelegate_passing() { + let ( + mut delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + expected_delegate_account, + ) = get_test_data_sync_inconcistent(); + // undelegate 50% in epoch 1 -> for the last epoch reward should only be 50% + let undelegate = delegate_account.delegated_stake_weight / 2; + + delegate_account.pending_undelegated_stake_weight += undelegate; + delegate_account.delegated_stake_weight -= undelegate; + delegate_account.pending_epoch = 0; + // delegate_account.delegated_stake_weight -= undelegate; + + // expected_delegate_account.delegated_stake_weight -= undelegate; + // expected_delegate_account.pending_token_amount -= undelegate; + // expected_delegate_account.pending_synced_stake_weight = undelegate; + + let result = sync_delegate_account( + &mut delegate_account, + compressed_forester_epoch_pdas.clone(), + previous_hash, + forester_pda_pubkey, + ); + + assert!(result.is_ok()); + println!( + "{:?}", + 3 * compressed_forester_epoch_pdas[0].rewards_earned + - delegate_account.delegated_stake_weight + ); + assert_eq!(delegate_account, expected_delegate_account); + } + + #[test] + fn test_sync_delegate_account_failing() { + let ( + mut delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + mut expected_delegate_account, + ) = get_test_data_sync(); + + // Modify expected_delegate_account to be incorrect for the failing test + expected_delegate_account.delegated_stake_weight -= 10; + + let result = sync_delegate_account( + &mut delegate_account, + compressed_forester_epoch_pdas, + previous_hash, + forester_pda_pubkey, + ); + assert!(result.is_ok()); + assert_ne!(delegate_account, expected_delegate_account); + } + + #[test] + fn test_sync_delegate_account_and_create_compressed_accounts_no_token_sync_passing() { + let authority = Pubkey::new_unique(); + let mut delegate_account = DelegateAccountWithPackedContext { + root_index: 11, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: 1, + nullifier_queue_pubkey_index: 2, + leaf_index: 1234, + queue_index: None, + }, + delegate_account: DelegateAccount { + owner: authority, + delegate_forester_delegate_account: None, + delegated_stake_weight: 100, + ..Default::default() + }, + output_merkle_tree_index: 1, + }; + let epoch = 0; + let compressed_forester_epoch_pdas = vec![CompressedForesterEpochAccountInput { + rewards_earned: 10, + epoch, + stake_weight: 100, + }]; + let previous_hash = [1; 32]; + let forester_pda_pubkey = Pubkey::new_unique(); + let last_account_merkle_context = PackedMerkleContext { + merkle_tree_pubkey_index: 1, + nullifier_queue_pubkey_index: 2, + leaf_index: 1234, + queue_index: None, + }; + let last_account_root_index = 5; + let escrow_token_authority = Pubkey::new_unique(); + let merkle_tree_index = 1; + let result = sync_delegate_account_and_create_compressed_accounts( + authority, + delegate_account, + compressed_forester_epoch_pdas.clone(), + previous_hash, + forester_pda_pubkey, + last_account_merkle_context, + last_account_root_index, + &None, + &Some(escrow_token_authority), + merkle_tree_index, + epoch, + ); + assert!(result.is_ok()); + let ( + input_delegate_compressed_account, + input_readonly_compressed_forester_epoch_account, + output_account_with_merkle_context, + output_escrow_account, + ) = result.unwrap(); + // delegate_account.delegate_account.pending_token_amount += 10; + assert_eq!( + input_delegate_compressed_account.merkle_context, + delegate_account.merkle_context + ); + assert_eq!( + input_delegate_compressed_account.root_index, + delegate_account.root_index + ); + let data = CompressedAccountData { + discriminator: DELEGATE_ACCOUNT_DISCRIMINATOR, + data_hash: delegate_account + .delegate_account + .hash::() + .unwrap(), + data: Vec::new(), + }; + assert_eq!( + input_delegate_compressed_account + .compressed_account + .data + .unwrap(), + data + ); + + let mut output_delegate_account = delegate_account.delegate_account.clone(); + let sum = compressed_forester_epoch_pdas + .iter() + .map(|x| x.rewards_earned) + .sum::(); + output_delegate_account.delegated_stake_weight += sum; + output_delegate_account.pending_token_amount += sum; + output_delegate_account.pending_synced_stake_weight += compressed_forester_epoch_pdas + .last() + .unwrap() + .rewards_earned; + let mut data = Vec::new(); + output_delegate_account.serialize(&mut data).unwrap(); + + let deserlized = DelegateAccount::deserialize_reader( + &mut &output_account_with_merkle_context + .compressed_account + .data + .as_ref() + .unwrap() + .data[..], + ) + .unwrap(); + assert_eq!(output_delegate_account, deserlized); + let data = CompressedAccountData { + discriminator: DELEGATE_ACCOUNT_DISCRIMINATOR, + data_hash: output_delegate_account.hash::().unwrap(), + data, + }; + assert_eq!( + output_account_with_merkle_context + .compressed_account + .data + .unwrap(), + data + ); + assert_eq!(output_escrow_account, None); + + let ref_compressed_forester_epoch_account = CompressedForesterEpochAccount { + rewards_earned: compressed_forester_epoch_pdas[0].rewards_earned, + epoch: compressed_forester_epoch_pdas[0].epoch, + stake_weight: compressed_forester_epoch_pdas[0].stake_weight, + previous_hash, + forester_pda_pubkey, + }; + let hashed_forester_pubkey = hash_to_bn254_field_size_be(forester_pda_pubkey.as_ref()) + .unwrap() + .0; + + let data = CompressedAccountData { + discriminator: FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, + data_hash: ref_compressed_forester_epoch_account + .hash(hashed_forester_pubkey) + .unwrap(), + data: Vec::new(), + }; + assert_eq!( + input_readonly_compressed_forester_epoch_account + .compressed_account + .data + .unwrap(), + data + ); + } + + #[test] + fn test_sync_delegate_account_and_create_compressed_accounts_with_token_sync_passing() { + let authority = Pubkey::new_unique(); + let delegate_account = DelegateAccountWithPackedContext { + root_index: 11, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: 1, + nullifier_queue_pubkey_index: 2, + leaf_index: 1234, + queue_index: None, + }, + delegate_account: DelegateAccount { + owner: authority, + delegate_forester_delegate_account: None, + delegated_stake_weight: 100, + ..Default::default() + }, + output_merkle_tree_index: 1, + }; + let epoch = 0; + let compressed_forester_epoch_pdas = vec![CompressedForesterEpochAccountInput { + rewards_earned: 10, + epoch, + stake_weight: 100, + }]; + let previous_hash = [1; 32]; + let forester_pda_pubkey = Pubkey::new_unique(); + let last_account_merkle_context = PackedMerkleContext { + merkle_tree_pubkey_index: 1, + nullifier_queue_pubkey_index: 2, + leaf_index: 1234, + queue_index: None, + }; + let input_escrow_token_account = InputTokenDataWithContext { + amount: 100, + lamports: None, + delegate_index: None, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: 1, + nullifier_queue_pubkey_index: 2, + leaf_index: 1234, + queue_index: None, + }, + root_index: 5, + }; + let last_account_root_index = 5; + let escrow_token_authority = Pubkey::new_unique(); + let merkle_tree_index = 1; + let result = sync_delegate_account_and_create_compressed_accounts( + authority, + delegate_account, + compressed_forester_epoch_pdas.clone(), + previous_hash, + forester_pda_pubkey, + last_account_merkle_context, + last_account_root_index, + &Some(input_escrow_token_account), + &Some(escrow_token_authority), + merkle_tree_index, + epoch, + ); + + assert!(result.is_ok()); + let ( + input_delegate_compressed_account, + input_readonly_compressed_forester_epoch_account, + output_account_with_merkle_context, + output_escrow_account, + ) = result.unwrap(); + assert_eq!( + input_delegate_compressed_account.merkle_context, + delegate_account.merkle_context + ); + assert_eq!( + input_delegate_compressed_account.root_index, + delegate_account.root_index + ); + let data = CompressedAccountData { + discriminator: DELEGATE_ACCOUNT_DISCRIMINATOR, + data_hash: delegate_account + .delegate_account + .hash::() + .unwrap(), + data: Vec::new(), + }; + assert_eq!( + input_delegate_compressed_account + .compressed_account + .data + .unwrap(), + data + ); + + let mut output_delegate_account = delegate_account.delegate_account.clone(); + let sum = compressed_forester_epoch_pdas + .iter() + .map(|x| x.rewards_earned) + .sum::(); + output_delegate_account.delegated_stake_weight += sum; + output_delegate_account.pending_synced_stake_weight = compressed_forester_epoch_pdas + .last() + .unwrap() + .rewards_earned; + output_delegate_account.pending_token_amount = 0; + let mut data = Vec::new(); + output_delegate_account.serialize(&mut data).unwrap(); + + let data = CompressedAccountData { + discriminator: DELEGATE_ACCOUNT_DISCRIMINATOR, + data_hash: output_delegate_account.hash::().unwrap(), + data, + }; + assert_eq!( + output_account_with_merkle_context + .compressed_account + .data + .unwrap(), + data + ); + + let ref_compressed_forester_epoch_account = CompressedForesterEpochAccount { + rewards_earned: compressed_forester_epoch_pdas[0].rewards_earned, + epoch: compressed_forester_epoch_pdas[0].epoch, + stake_weight: compressed_forester_epoch_pdas[0].stake_weight, + previous_hash, + forester_pda_pubkey, + }; + let hashed_forester_pubkey = hash_to_bn254_field_size_be(forester_pda_pubkey.as_ref()) + .unwrap() + .0; + + let data = CompressedAccountData { + discriminator: FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, + data_hash: ref_compressed_forester_epoch_account + .hash(hashed_forester_pubkey) + .unwrap(), + data: Vec::new(), + }; + assert_eq!( + input_readonly_compressed_forester_epoch_account + .compressed_account + .data + .unwrap(), + data + ); + let expected_output_escrow_account = PackedTokenTransferOutputData { + owner: escrow_token_authority, + lamports: None, + amount: output_delegate_account.delegated_stake_weight, + merkle_tree_index, + }; + assert_eq!(output_escrow_account, Some(expected_output_escrow_account)); + } +} diff --git a/programs/registry/src/epoch/sync_delegate_instruction.rs b/programs/registry/src/epoch/sync_delegate_instruction.rs new file mode 100644 index 0000000000..cd73a2cf5d --- /dev/null +++ b/programs/registry/src/epoch/sync_delegate_instruction.rs @@ -0,0 +1,133 @@ +use crate::delegate::traits::{CompressedCpiContextTrait, CompressedTokenProgramAccounts}; +use crate::protocol_config::state::ProtocolConfigPda; + +use crate::delegate::{ + traits::{SignerAccounts, SystemProgramAccounts}, + ESCROW_TOKEN_ACCOUNT_SEED, +}; +use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; +use anchor_lang::prelude::*; +use anchor_spl::token::{Token, TokenAccount}; +use light_compressed_token::program::LightCompressedToken; +use light_system_program::program::LightSystemProgram; + +#[derive(Accounts)] +#[instruction(salt: u64)] +pub struct SyncDelegateInstruction<'info> { + /// Fee payer needs to be mutable to pay rollover and protocol fees. + #[account(mut)] + pub fee_payer: Signer<'info>, + pub authority: Signer<'info>, + /// CHECK: + #[account( + seeds = [ESCROW_TOKEN_ACCOUNT_SEED,authority.key().as_ref(), salt.to_le_bytes().as_slice()], bump + )] + pub escrow_token_authority: Option>, + /// CHECK: + #[account( + seeds = [CPI_AUTHORITY_PDA_SEED], bump + )] + pub cpi_authority: AccountInfo<'info>, + pub protocol_config: Account<'info, ProtocolConfigPda>, + /// CHECK: + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: checked in emit_event.rs. + pub noop_program: AccountInfo<'info>, + /// CHECK: + pub account_compression_authority: AccountInfo<'info>, + /// CHECK: + pub account_compression_program: Program<'info, AccountCompression>, + /// CHECK: + pub system_program: AccountInfo<'info>, + pub self_program: Program<'info, crate::program::LightRegistry>, + pub light_system_program: Program<'info, LightSystemProgram>, + /// CHECK: + pub cpi_context_account: Option>, + pub compressed_token_program: Option>, + /// CHECK: + pub token_cpi_authority_pda: Option>, + #[account(mut)] + pub forester_token_pool: Option>, + #[account(mut)] + pub spl_token_pool: Option>, + pub spl_token_program: Option>, +} + +impl<'info> SystemProgramAccounts<'info> for SyncDelegateInstruction<'info> { + fn get_registered_program_pda(&self) -> AccountInfo<'info> { + self.registered_program_pda.to_account_info() + } + fn get_noop_program(&self) -> AccountInfo<'info> { + self.noop_program.to_account_info() + } + fn get_account_compression_authority(&self) -> AccountInfo<'info> { + self.account_compression_authority.to_account_info() + } + fn get_account_compression_program(&self) -> AccountInfo<'info> { + self.account_compression_program.to_account_info() + } + fn get_system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + fn get_sol_pool_pda(&self) -> Option> { + None + } + fn get_decompression_recipient(&self) -> Option> { + None + } + fn get_light_system_program(&self) -> AccountInfo<'info> { + self.light_system_program.to_account_info() + } + fn get_self_program(&self) -> AccountInfo<'info> { + self.self_program.to_account_info() + } +} + +impl<'info> SignerAccounts<'info> for SyncDelegateInstruction<'info> { + fn get_fee_payer(&self) -> AccountInfo<'info> { + self.fee_payer.to_account_info() + } + fn get_authority(&self) -> AccountInfo<'info> { + self.authority.to_account_info() + } + fn get_cpi_authority_pda(&self) -> AccountInfo<'info> { + self.cpi_authority.to_account_info() + } +} +impl<'info> CompressedCpiContextTrait<'info> for SyncDelegateInstruction<'info> { + fn get_cpi_context(&self) -> Option> { + Some(self.cpi_context_account.as_ref().unwrap().to_account_info()) + } +} + +impl<'info> CompressedTokenProgramAccounts<'info> for SyncDelegateInstruction<'info> { + fn get_token_cpi_authority_pda(&self) -> AccountInfo<'info> { + self.token_cpi_authority_pda + .as_ref() + .unwrap() + .to_account_info() + } + fn get_compressed_token_program(&self) -> AccountInfo<'info> { + self.compressed_token_program + .as_ref() + .unwrap() + .to_account_info() + } + fn get_escrow_authority_pda(&self) -> AccountInfo<'info> { + self.escrow_token_authority + .as_ref() + .unwrap() + .to_account_info() + } + fn get_token_pool_pda(&self) -> AccountInfo<'info> { + self.spl_token_pool.as_ref().unwrap().to_account_info() + } + fn get_spl_token_program(&self) -> AccountInfo<'info> { + self.spl_token_program.as_ref().unwrap().to_account_info() + } + fn get_compress_or_decompress_token_account(&self) -> Option> { + self.forester_token_pool + .as_ref() + .map(|account| account.to_account_info()) + } +} diff --git a/programs/registry/src/errors.rs b/programs/registry/src/errors.rs new file mode 100644 index 0000000000..8539fee7a5 --- /dev/null +++ b/programs/registry/src/errors.rs @@ -0,0 +1,31 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum RegistryError { + #[msg("InvalidForester")] + InvalidForester, + NotInReportWorkPhase, + StakeAccountAlreadySynced, + EpochEnded, + ForresterNotEligible, + NotInRegistrationPeriod, + StakeInsuffient, + ForesterAlreadyRegistered, + InvalidEpochAccount, + InvalidEpoch, + EpochStillInProgress, + NotInActivePhase, + ForesterAlreadyReportedWork, + ComputeEscrowAmountFailed, + InputEscrowTokenHashNotProvided, + ArithmeticUnderflow, + ArithmeticOverflow, + AlreadyDelegated, + InvalidAuthority, + InvalidMint, + HashToFieldError, + StakeAccountSyncError, + DepositAmountNotEqualInputAmount, + InvalidProtocolConfigUpdate, + DelegateAccountNotSynced, +} diff --git a/programs/registry/src/forester.rs b/programs/registry/src/forester.rs deleted file mode 100644 index b60b40954d..0000000000 --- a/programs/registry/src/forester.rs +++ /dev/null @@ -1,57 +0,0 @@ -use anchor_lang::prelude::*; - -use crate::{LightGovernanceAuthority, RegistryError}; -use aligned_sized::aligned_sized; - -pub const FORESTER_EPOCH_SEED: &[u8] = b"forester_epoch"; - -#[aligned_sized(anchor)] -#[account] -#[derive(PartialEq, Debug)] -pub struct ForesterEpoch { - pub authority: Pubkey, - pub counter: u64, - pub epoch_start: u64, - pub epoch_end: u64, -} - -#[derive(Accounts)] -#[instruction(bump: u8, authority: Pubkey)] -pub struct RegisterForester<'info> { - /// CHECK: - #[account(init, seeds = [FORESTER_EPOCH_SEED, authority.to_bytes().as_slice()], bump, space =ForesterEpoch::LEN , payer = signer)] - pub forester_epoch_pda: Account<'info, ForesterEpoch>, - #[account(mut, address = authority_pda.authority)] - pub signer: Signer<'info>, - pub authority_pda: Account<'info, LightGovernanceAuthority>, - system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct UpdateForesterEpochPda<'info> { - #[account(address = forester_epoch_pda.authority)] - pub signer: Signer<'info>, - /// CHECK: - #[account(mut)] - pub forester_epoch_pda: Account<'info, ForesterEpoch>, -} - -pub fn check_forester(forester_epoch_pda: &mut ForesterEpoch, authority: &Pubkey) -> Result<()> { - if forester_epoch_pda.authority != *authority { - msg!( - "Invalid forester: forester_epoch_pda authority {} != provided {}", - forester_epoch_pda.authority, - authority - ); - return err!(RegistryError::InvalidForester); - } - forester_epoch_pda.counter += 1; - Ok(()) -} - -pub fn get_forester_epoch_pda_address(authority: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[FORESTER_EPOCH_SEED, authority.to_bytes().as_slice()], - &crate::ID, - ) -} diff --git a/programs/registry/src/forester/mod.rs b/programs/registry/src/forester/mod.rs new file mode 100644 index 0000000000..266c62acc7 --- /dev/null +++ b/programs/registry/src/forester/mod.rs @@ -0,0 +1 @@ +pub mod state; diff --git a/programs/registry/src/forester/state.rs b/programs/registry/src/forester/state.rs new file mode 100644 index 0000000000..f90ce1e724 --- /dev/null +++ b/programs/registry/src/forester/state.rs @@ -0,0 +1,98 @@ +use crate::protocol_config::state::ProtocolConfig; +use account_compression::utils::constants::CPI_AUTHORITY_PDA_SEED; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::pubkey::Pubkey; +use anchor_spl::token::{Mint, Token, TokenAccount}; + +use crate::protocol_config::state::ProtocolConfigPda; +use aligned_sized::aligned_sized; + +#[aligned_sized(anchor)] +#[account] +#[derive(Debug, Default, Copy, PartialEq, Eq)] +pub struct ForesterAccount { + pub authority: Pubkey, + pub config: ForesterConfig, + pub active_stake_weight: u64, + /// Pending stake which will get active once the next epoch starts. + pub pending_undelegated_stake_weight: u64, + pub current_epoch: u64, + /// Link to previous compressed forester epoch account hash. + pub last_compressed_forester_epoch_pda_hash: [u8; 32], + pub last_registered_epoch: u64, + pub last_claimed_epoch: u64, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] +pub struct ForesterConfig { + /// Fee in percentage points. + pub fee: u64, + pub fee_recipient: Pubkey, +} + +impl ForesterAccount { + /// If current epoch changed, move pending stake to active stake and update + /// current epoch field + pub fn sync(&mut self, current_slot: u64, protocol_config: &ProtocolConfig) -> Result<()> { + // get last registration epoch, stake sync treats the registration phase + // of an epoch like the next active epoch + let current_epoch = protocol_config.get_current_epoch( + current_slot.saturating_sub(protocol_config.registration_phase_length), + ); + // msg!("current_epoch: {}", current_epoch); + // msg!("self.current_epoch: {}", self.current_epoch); + // If the current epoch is greater than the last registered epoch, or next epoch is in registration phase + if current_epoch > self.current_epoch + || protocol_config.is_registration_phase(current_slot).is_ok() + { + // msg!("self pending stake weight: {}", self.pending_undelegated_stake_weight); + // msg!("self active stake weight: {}", self.active_stake_weight); + self.current_epoch = current_epoch; + self.active_stake_weight += self.pending_undelegated_stake_weight; + self.pending_undelegated_stake_weight = 0; + // msg!("self pending stake weight: {}", self.pending_undelegated_stake_weight); + // msg!("self active stake weight: {}", self.active_stake_weight); + } + Ok(()) + } +} + +pub const FORESTER_SEED: &[u8] = b"forester"; +pub const FORESTER_TOKEN_POOL_SEED: &[u8] = b"forester_token_pool"; +#[derive(Accounts)] +pub struct RegisterForester<'info> { + /// CHECK: + #[account(init, seeds = [FORESTER_SEED, authority.key().to_bytes().as_slice()], bump, space =ForesterAccount::LEN , payer = signer)] + pub forester_pda: Account<'info, ForesterAccount>, + #[account( + init, + seeds = [ + FORESTER_TOKEN_POOL_SEED, authority.key().to_bytes().as_slice(), + ], + bump, + payer = signer, + token::mint = mint, + token::authority = cpi_authority_pda, + )] + pub token_pool_pda: Account<'info, TokenAccount>, + #[account(mut)] + pub signer: Signer<'info>, + pub authority: Signer<'info>, + /// CHECK: + #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] + pub cpi_authority_pda: AccountInfo<'info>, + pub protocol_config_pda: Account<'info, ProtocolConfigPda>, + #[account(constraint = mint.key() == protocol_config_pda.config.mint)] + pub mint: Account<'info, Mint>, + system_program: Program<'info, System>, + token_program: Program<'info, Token>, +} + +#[derive(Accounts)] +pub struct UpdateForester<'info> { + /// CHECK: + #[account(mut, has_one = authority)] + pub forester_pda: Account<'info, ForesterAccount>, + pub authority: Signer<'info>, + pub new_authority: Option>, +} diff --git a/programs/registry/src/lib.rs b/programs/registry/src/lib.rs index 7c26fe6693..34db55a60b 100644 --- a/programs/registry/src/lib.rs +++ b/programs/registry/src/lib.rs @@ -1,57 +1,110 @@ #![allow(clippy::too_many_arguments)] use account_compression::utils::constants::CPI_AUTHORITY_PDA_SEED; -use account_compression::{program::AccountCompression, state::GroupAuthority}; use account_compression::{AddressMerkleTreeConfig, AddressQueueConfig}; use account_compression::{NullifierQueueConfig, StateMerkleTreeConfig}; use anchor_lang::prelude::*; +pub mod account_compression_cpi; +pub mod errors; +pub use crate::epoch::{ + claim_forester_instruction::*, finalize_registration::*, register_epoch::*, report_work::*, + sync_delegate::process_sync_delegate_account, sync_delegate_instruction::*, +}; +pub use account_compression_cpi::{ + initialize_tree_and_queue::*, nullify::*, register_program::*, rollover_state_tree::*, + update_address_tree::*, +}; + +pub use protocol_config::{initialize::*, mint::*, update::*}; +pub mod delegate; +pub mod epoch; pub mod forester; -pub use forester::*; +pub mod protocol_config; +pub mod utils; +use anchor_lang::solana_program::pubkey::Pubkey; +use delegate::deposit::{process_deposit_or_withdrawal, InputDelegateAccountWithPackedContext}; +use delegate::process_delegate::process_delegate_or_undelegate; +pub use delegate::{delegate_instruction::*, deposit_instruction::*}; +use delegate::{ + deposit::DelegateAccountWithPackedContext, + process_cpi::{cpi_compressed_token_mint_to, get_cpi_signer_seeds}, +}; +use epoch::claim_forester::process_forester_claim_rewards; +use epoch::{ + claim_forester::CompressedForesterEpochAccountInput, sync_delegate::SyncDelegateTokenAccount, +}; +pub use forester::state::*; +use light_compressed_token::process_transfer::InputTokenDataWithContext; +use light_system_program::sdk::compressed_account::PackedMerkleContext; +use light_system_program::{invoke::processor::CompressedProof, sdk::CompressedCpiContext}; +use protocol_config::state::ProtocolConfig; + #[cfg(not(target_os = "solana"))] pub mod sdk; - declare_id!("7Z9Yuy3HkBCc2Wf3xzMGnz6qpV4n7ciwcoEMGKqhAnj1"); -#[error_code] -pub enum RegistryError { - #[msg("InvalidForester")] - InvalidForester, -} - -#[constant] -pub const AUTHORITY_PDA_SEED: &[u8] = b"authority"; - #[program] pub mod light_registry { - use anchor_lang::solana_program::pubkey::Pubkey; - use super::*; pub fn initialize_governance_authority( ctx: Context, - authority: Pubkey, - rewards: Vec, bump: u8, + protocol_config: ProtocolConfig, ) -> Result<()> { - ctx.accounts.authority_pda.authority = authority; + if protocol_config.mint != ctx.accounts.mint.key() + || ctx.accounts.mint.mint_authority.unwrap() != ctx.accounts.cpi_authority.key() + { + return err!(errors::RegistryError::InvalidMint); + } + ctx.accounts.authority_pda.authority = ctx.accounts.authority.key(); ctx.accounts.authority_pda.bump = bump; - ctx.accounts.authority_pda.rewards = rewards; - ctx.accounts.authority_pda.epoch = 0; - ctx.accounts.authority_pda.epoch_length = u64::MAX; + ctx.accounts.authority_pda.config = protocol_config; + msg!("mint: {:?}", ctx.accounts.mint.key()); Ok(()) } + // TODO: rename to update_protocol_config pub fn update_governance_authority( ctx: Context, - bump: u8, + _bump: u8, new_authority: Pubkey, + new_config: ProtocolConfig, ) -> Result<()> { ctx.accounts.authority_pda.authority = new_authority; - ctx.accounts.authority_pda.bump = bump; + // ctx.accounts.authority_pda.bump = bump; + // mint cannot be updated + if ctx.accounts.authority_pda.config.mint != new_config.mint { + return err!(errors::RegistryError::InvalidMint); + } + // forester registration guarded can only be disabled + if !ctx + .accounts + .authority_pda + .config + .forester_registration_guarded + && new_config.forester_registration_guarded + { + return err!(errors::RegistryError::InvalidProtocolConfigUpdate); + } Ok(()) } + pub fn mint<'info>( + ctx: Context<'_, '_, '_, 'info, Mint<'info>>, + amounts: Vec, + recipients: Vec, + ) -> Result<()> { + cpi_compressed_token_mint_to( + &ctx, + recipients, + amounts, + get_cpi_signer_seeds(), + ctx.accounts.merkle_tree.to_account_info(), + ) + } + pub fn register_system_program(ctx: Context, bump: u8) -> Result<()> { let bump = &[bump]; let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; @@ -83,28 +136,14 @@ pub mod light_registry { indices: Vec, proofs: Vec>, ) -> Result<()> { - check_forester( + ForesterEpochPda::check_forester_in_program( &mut ctx.accounts.registered_forester_pda, &ctx.accounts.authority.key(), + &ctx.accounts.nullifier_queue.key(), )?; - let bump = &[bump]; - let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; - let signer_seeds = &[&seeds[..]]; - let accounts = account_compression::cpi::accounts::NullifyLeaves { - authority: ctx.accounts.cpi_authority.to_account_info(), - registered_program_pda: Some(ctx.accounts.registered_program_pda.to_account_info()), - log_wrapper: ctx.accounts.log_wrapper.to_account_info(), - merkle_tree: ctx.accounts.merkle_tree.to_account_info(), - nullifier_queue: ctx.accounts.nullifier_queue.to_account_info(), - }; - let cpi_ctx = CpiContext::new_with_signer( - ctx.accounts.account_compression_program.to_account_info(), - accounts, - signer_seeds, - ); - - account_compression::cpi::nullify_leaves( - cpi_ctx, + process_nullify( + ctx, + bump, change_log_indices, leaves_queue_indices, indices, @@ -125,29 +164,14 @@ pub mod light_registry { low_address_next_value: [u8; 32], low_address_proof: [[u8; 32]; 16], ) -> Result<()> { - check_forester( + ForesterEpochPda::check_forester_in_program( &mut ctx.accounts.registered_forester_pda, &ctx.accounts.authority.key(), + &ctx.accounts.queue.key(), )?; - let bump = &[bump]; - let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; - let signer_seeds = &[&seeds[..]]; - - let accounts = account_compression::cpi::accounts::UpdateAddressMerkleTree { - authority: ctx.accounts.cpi_authority.to_account_info(), - registered_program_pda: Some(ctx.accounts.registered_program_pda.to_account_info()), - log_wrapper: ctx.accounts.log_wrapper.to_account_info(), - queue: ctx.accounts.queue.to_account_info(), - merkle_tree: ctx.accounts.merkle_tree.to_account_info(), - }; - let cpi_ctx = CpiContext::new_with_signer( - ctx.accounts.account_compression_program.to_account_info(), - accounts, - signer_seeds, - ); - - account_compression::cpi::update_address_merkle_tree( - cpi_ctx, + process_update_address_merkle_tree( + ctx, + bump, changelog_index, indexed_changelog_index, value, @@ -163,81 +187,133 @@ pub mod light_registry { ctx: Context, bump: u8, ) -> Result<()> { - check_forester( + ForesterEpochPda::check_forester_in_program( &mut ctx.accounts.registered_forester_pda, &ctx.accounts.authority.key(), + &ctx.accounts.old_queue.key(), )?; - let bump = &[bump]; - - let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; - let signer_seeds = &[&seeds[..]]; - - let accounts = account_compression::cpi::accounts::RolloverAddressMerkleTreeAndQueue { - fee_payer: ctx.accounts.authority.to_account_info(), - authority: ctx.accounts.cpi_authority.to_account_info(), - registered_program_pda: Some(ctx.accounts.registered_program_pda.to_account_info()), - new_address_merkle_tree: ctx.accounts.new_merkle_tree.to_account_info(), - new_queue: ctx.accounts.new_queue.to_account_info(), - old_address_merkle_tree: ctx.accounts.old_merkle_tree.to_account_info(), - old_queue: ctx.accounts.old_queue.to_account_info(), - }; - let cpi_ctx = CpiContext::new_with_signer( - ctx.accounts.account_compression_program.to_account_info(), - accounts, - signer_seeds, - ); - - account_compression::cpi::rollover_address_merkle_tree_and_queue(cpi_ctx) + process_rollover_address_merkle_tree_and_queue(ctx, bump) } pub fn rollover_state_merkle_tree_and_queue( ctx: Context, bump: u8, ) -> Result<()> { - check_forester( + // TODO: specificy how to forest rolled over queues + ForesterEpochPda::check_forester_in_program( &mut ctx.accounts.registered_forester_pda, &ctx.accounts.authority.key(), + &ctx.accounts.old_queue.key(), )?; - let bump = &[bump]; - - let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; - let signer_seeds = &[&seeds[..]]; - - let accounts = - account_compression::cpi::accounts::RolloverStateMerkleTreeAndNullifierQueue { - fee_payer: ctx.accounts.authority.to_account_info(), - authority: ctx.accounts.cpi_authority.to_account_info(), - registered_program_pda: Some(ctx.accounts.registered_program_pda.to_account_info()), - new_state_merkle_tree: ctx.accounts.new_merkle_tree.to_account_info(), - new_nullifier_queue: ctx.accounts.new_queue.to_account_info(), - old_state_merkle_tree: ctx.accounts.old_merkle_tree.to_account_info(), - old_nullifier_queue: ctx.accounts.old_queue.to_account_info(), - }; - let cpi_ctx = CpiContext::new_with_signer( - ctx.accounts.account_compression_program.to_account_info(), - accounts, - signer_seeds, - ); - - account_compression::cpi::rollover_state_merkle_tree_and_nullifier_queue(cpi_ctx) + process_rollover_state_merkle_tree_and_queue(ctx, bump) } pub fn register_forester( ctx: Context, _bump: u8, - authority: Pubkey, + config: ForesterConfig, ) -> Result<()> { - ctx.accounts.forester_epoch_pda.authority = authority; - ctx.accounts.forester_epoch_pda.epoch_start = 0; - ctx.accounts.forester_epoch_pda.epoch_end = u64::MAX; + if ctx.accounts.protocol_config_pda.authority != ctx.accounts.signer.key() + && ctx + .accounts + .protocol_config_pda + .config + .forester_registration_guarded + { + return err!(errors::RegistryError::InvalidAuthority); + } + ctx.accounts.forester_pda.authority = ctx.accounts.authority.key(); + ctx.accounts.forester_pda.config = config; + // // TODO: remove once delegating is implemented + // if ctx + // .accounts + // .protocol_config_pda + // .config + // .forester_registration_guarded + // { + // ctx.accounts.forester_pda.active_stake_weight = 1; + // } msg!( "registered forester: {:?}", - ctx.accounts.forester_epoch_pda.authority + ctx.accounts.forester_pda.authority ); + msg!("registered forester pda: {:?}", ctx.accounts.forester_pda); + Ok(()) + } + + pub fn update_forester(ctx: Context, config: ForesterConfig) -> Result<()> { + if let Some(authority) = ctx.accounts.new_authority.as_ref() { + ctx.accounts.forester_pda.authority = authority.key(); + } + ctx.accounts.forester_pda.config = config; msg!( - "registered forester pda: {:?}", - ctx.accounts.forester_epoch_pda + "updated forester: {:?}", + ctx.accounts.forester_pda.authority ); + msg!("updated forester pda: {:?}", ctx.accounts.forester_pda); + Ok(()) + } + + /// Registers the forester for the epoch. + /// 1. Only the forester can register herself for the epoch. + /// 2. Protocol config is copied. + /// 3. Epoch account is created if needed. + pub fn register_forester_epoch<'info>( + ctx: Context<'_, '_, '_, 'info, RegisterForesterEpoch<'info>>, + epoch: u64, + ) -> Result<()> { + let protocol_config = ctx.accounts.protocol_config.config; + let current_solana_slot = anchor_lang::solana_program::clock::Clock::get()?.slot; + // Init epoch account if not initialized + let current_epoch = protocol_config.get_current_epoch(current_solana_slot); + // TODO: check that epoch is in registration phase + if current_epoch != epoch { + return err!(errors::RegistryError::InvalidEpoch); + } + // Only init if not initialized + if ctx.accounts.epoch_pda.registered_stake == 0 { + (*ctx.accounts.epoch_pda).clone_from(&EpochPda { + epoch: current_epoch, + protocol_config: ctx.accounts.protocol_config.config, + total_work: 0, + registered_stake: 0, + }); + } + register_for_epoch_instruction( + &ctx.accounts.authority.key(), + &mut ctx.accounts.forester_pda, + &mut ctx.accounts.forester_epoch_pda, + &mut ctx.accounts.epoch_pda, + current_solana_slot, + )?; + Ok(()) + } + + /// This transaction can be included as additional instruction in the first + /// work instructions during the active phase. + /// Registration Period must be over. + /// TODO: introduce grace period between registration and before + /// active phase starts, do I really need it or isn't it clear who gets the + /// first slot the first sign up? + pub fn finalize_registration<'info>( + ctx: Context<'_, '_, '_, 'info, FinalizeRegistration<'info>>, + ) -> Result<()> { + let current_solana_slot = anchor_lang::solana_program::clock::Clock::get()?.slot; + let current_epoch = ctx + .accounts + .epoch_pda + .protocol_config + .get_current_active_epoch(current_solana_slot)?; + if current_epoch != ctx.accounts.epoch_pda.epoch + || ctx.accounts.epoch_pda.epoch != ctx.accounts.forester_epoch_pda.epoch + { + return err!(errors::RegistryError::InvalidEpoch); + } + ctx.accounts.forester_epoch_pda.total_epoch_state_weight = + Some(ctx.accounts.epoch_pda.registered_stake); + ctx.accounts.forester_epoch_pda.finalize_counter += 1; + // TODO: add limit for finalize counter to throw if exceeded + // Is a safeguard so that noone can block parallelism Ok(()) } @@ -249,32 +325,36 @@ pub mod light_registry { Ok(()) } + pub fn report_work<'info>(ctx: Context<'_, '_, '_, 'info, ReportWork<'info>>) -> Result<()> { + let current_solana_slot = anchor_lang::solana_program::clock::Clock::get()?.slot; + ctx.accounts + .epoch_pda + .protocol_config + .is_report_work_phase(current_solana_slot, ctx.accounts.epoch_pda.epoch)?; + // TODO: unify epoch security checks + if ctx.accounts.epoch_pda.epoch != ctx.accounts.forester_epoch_pda.epoch { + return err!(errors::RegistryError::InvalidEpoch); + } + if ctx.accounts.forester_epoch_pda.has_reported_work { + return err!(errors::RegistryError::ForesterAlreadyReportedWork); + } + ctx.accounts.epoch_pda.total_work += ctx.accounts.forester_epoch_pda.work_counter; + ctx.accounts.forester_epoch_pda.has_reported_work = true; + Ok(()) + } + #[allow(clippy::too_many_arguments)] pub fn initialize_address_merkle_tree( - ctx: Context, + ctx: Context, bump: u8, index: u64, // TODO: replace with counter from pda program_owner: Option, merkle_tree_config: AddressMerkleTreeConfig, // TODO: check config with protocol config queue_config: AddressQueueConfig, ) -> Result<()> { - let bump = &[bump]; - let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; - let signer_seeds = &[&seeds[..]]; - let accounts = account_compression::cpi::accounts::InitializeAddressMerkleTreeAndQueue { - authority: ctx.accounts.cpi_authority.to_account_info(), - merkle_tree: ctx.accounts.merkle_tree.to_account_info(), - queue: ctx.accounts.queue.to_account_info(), - registered_program_pda: Some(ctx.accounts.registered_program_pda.clone()), - }; - let cpi_ctx = CpiContext::new_with_signer( - ctx.accounts.account_compression_program.to_account_info(), - accounts, - signer_seeds, - ); - - account_compression::cpi::initialize_address_merkle_tree_and_queue( - cpi_ctx, + process_initialize_address_merkle_tree( + ctx, + bump, index, program_owner, merkle_tree_config, @@ -284,7 +364,7 @@ pub mod light_registry { #[allow(clippy::too_many_arguments)] pub fn initialize_state_merkle_tree( - ctx: Context, + ctx: Context, bump: u8, index: u64, // TODO: replace with counter from pda program_owner: Option, @@ -292,24 +372,9 @@ pub mod light_registry { queue_config: NullifierQueueConfig, additional_rent: u64, ) -> Result<()> { - let bump = &[bump]; - let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; - let signer_seeds = &[&seeds[..]]; - let accounts = - account_compression::cpi::accounts::InitializeStateMerkleTreeAndNullifierQueue { - authority: ctx.accounts.cpi_authority.to_account_info(), - merkle_tree: ctx.accounts.merkle_tree.to_account_info(), - nullifier_queue: ctx.accounts.queue.to_account_info(), - registered_program_pda: Some(ctx.accounts.registered_program_pda.clone()), - }; - let cpi_ctx = CpiContext::new_with_signer( - ctx.accounts.account_compression_program.to_account_info(), - accounts, - signer_seeds, - ); - - account_compression::cpi::initialize_state_merkle_tree_and_nullifier_queue( - cpi_ctx, + process_initialize_state_merkle_tree( + ctx, + bump, index, program_owner, merkle_tree_config, @@ -318,6 +383,128 @@ pub mod light_registry { ) } + pub fn deposit<'info>( + ctx: Context<'_, '_, '_, 'info, DepositOrWithdrawInstruction<'info>>, + salt: u64, + delegate_account: Option, + deposit_amount: u64, + input_compressed_token_accounts: Vec, + input_escrow_token_account: Option, + escrow_token_account_merkle_tree_index: u8, + change_compressed_account_merkle_tree_index: u8, + output_delegate_compressed_account_merkle_tree_index: u8, + proof: CompressedProof, + cpi_context: CompressedCpiContext, + ) -> Result<()> { + process_deposit_or_withdrawal::( + ctx, + salt, + proof, + cpi_context, + delegate_account, + deposit_amount, + input_compressed_token_accounts, + input_escrow_token_account, + escrow_token_account_merkle_tree_index, + change_compressed_account_merkle_tree_index, + output_delegate_compressed_account_merkle_tree_index, + ) + } + + pub fn withdrawal<'info>( + ctx: Context<'_, '_, '_, 'info, DepositOrWithdrawInstruction<'info>>, + salt: u64, + proof: CompressedProof, + cpi_context: CompressedCpiContext, + delegate_account: InputDelegateAccountWithPackedContext, + withdrawal_amount: u64, + input_escrow_token_account: InputTokenDataWithContext, + escrow_token_account_merkle_tree_index: u8, + change_compressed_account_merkle_tree_index: u8, + output_delegate_compressed_account_merkle_tree_index: u8, + ) -> Result<()> { + process_deposit_or_withdrawal::( + ctx, + salt, + proof, + cpi_context, + Some(delegate_account), + withdrawal_amount, + Vec::new(), + Some(input_escrow_token_account), + escrow_token_account_merkle_tree_index, + change_compressed_account_merkle_tree_index, + output_delegate_compressed_account_merkle_tree_index, + ) + } + + pub fn delegate<'info>( + ctx: Context<'_, '_, '_, 'info, DelegatetOrUndelegateInstruction<'info>>, + proof: CompressedProof, + delegate_account: DelegateAccountWithPackedContext, + delegate_amount: u64, + no_sync: bool, + ) -> Result<()> { + process_delegate_or_undelegate::( + ctx, + proof, + delegate_account, + delegate_amount, + no_sync, + ) + } + + pub fn undelegate<'info>( + ctx: Context<'_, '_, '_, 'info, DelegatetOrUndelegateInstruction<'info>>, + proof: CompressedProof, + delegate_account: DelegateAccountWithPackedContext, + delegate_amount: u64, + no_sync: bool, + ) -> Result<()> { + process_delegate_or_undelegate::( + ctx, + proof, + delegate_account, + delegate_amount, + no_sync, + ) + } + + pub fn claim_forester_rewards<'info>( + ctx: Context<'_, '_, '_, 'info, ClaimForesterInstruction<'info>>, + ) -> Result<()> { + process_forester_claim_rewards(ctx) + } + + pub fn sync_delegate<'info>( + ctx: Context<'_, '_, '_, 'info, SyncDelegateInstruction<'info>>, + _salt: u64, + delegate_account: DelegateAccountWithPackedContext, + previous_hash: [u8; 32], + forester_pda_pubkey: Pubkey, + compressed_forester_epoch_pdas: Vec, + last_account_root_index: u16, + last_account_merkle_context: PackedMerkleContext, + inclusion_proof: CompressedProof, + sync_delegate_token_account: Option, + input_escrow_token_account: Option, + output_token_account_merkle_tree_index: u8, + ) -> Result<()> { + process_sync_delegate_account( + ctx, + delegate_account, + previous_hash, + forester_pda_pubkey, + compressed_forester_epoch_pdas, + last_account_root_index, + last_account_merkle_context, + inclusion_proof, + sync_delegate_token_account, + input_escrow_token_account, + output_token_account_merkle_tree_index, + ) + } + // TODO: update rewards field // signer is light governance authority @@ -338,197 +525,8 @@ pub mod light_registry { // signer is registered relayer // update the relayer signer pubkey in the pda - // TODO: add rollover Merkle tree with rewards - // signer is registered relayer - // cpi to account compression program rollover Merkle tree - // increment points in registered relayer account - // TODO: add rollover lookup table with rewards // signer is registered relayer // cpi to account compression program rollover lookup table // increment points in registered relayer account - - // TODO: add nullify compressed_account with rewards - // signer is registered relayer - // cpi to account compression program nullify compressed_account - // increment points in registered relayer account -} - -#[derive(Accounts)] -pub struct InitializeAddressMerkleTreeAndQueue<'info> { - /// Anyone can create new trees just the fees cannot be set arbitrarily. - #[account(mut)] - pub authority: Signer<'info>, - /// CHECK: - #[account(mut)] - pub merkle_tree: AccountInfo<'info>, - /// CHECK: - #[account(mut)] - pub queue: AccountInfo<'info>, - /// CHECK: - pub registered_program_pda: AccountInfo<'info>, - /// CHECK: - #[account(mut)] - #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] - cpi_authority: AccountInfo<'info>, - account_compression_program: Program<'info, AccountCompression>, -} - -#[derive(Accounts)] -pub struct InitializeStateMerkleTreeAndQueue<'info> { - /// Anyone can create new trees just the fees cannot be set arbitrarily. - #[account(mut)] - pub authority: Signer<'info>, - /// CHECK: - #[account(mut)] - pub merkle_tree: AccountInfo<'info>, - /// CHECK: - #[account(mut)] - pub queue: AccountInfo<'info>, - /// CHECK: - pub registered_program_pda: AccountInfo<'info>, - /// CHECK: - #[account(mut)] - #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] - cpi_authority: AccountInfo<'info>, - account_compression_program: Program<'info, AccountCompression>, -} - -#[derive(Debug)] -#[account] -pub struct LightGovernanceAuthority { - pub authority: Pubkey, - pub bump: u8, - pub epoch: u64, - pub epoch_length: u64, - pub _padding: [u8; 7], - pub rewards: Vec, // initializing with storage for 8 u64s TODO: add instruction to resize -} - -#[derive(Accounts)] -#[instruction(bump: u8)] -pub struct InitializeAuthority<'info> { - // TODO: add check that this is upgrade authority - #[account(mut)] - authority: Signer<'info>, - /// CHECK: - #[account(init, seeds = [AUTHORITY_PDA_SEED], bump, space = 8 + 32 + 8 + 8 * 8, payer = authority)] - authority_pda: Account<'info, LightGovernanceAuthority>, - system_program: Program<'info, System>, -} - -#[derive(Accounts)] -#[instruction(bump: u8)] -pub struct UpdateAuthority<'info> { - #[account(mut, constraint = authority.key() == authority_pda.authority)] - authority: Signer<'info>, - /// CHECK: - #[account(mut, seeds = [AUTHORITY_PDA_SEED], bump)] - authority_pda: Account<'info, LightGovernanceAuthority>, -} - -#[derive(Accounts)] -pub struct RegisteredProgram<'info> { - #[account(mut, constraint = authority.key() == authority_pda.authority)] - authority: Signer<'info>, - /// CHECK: - #[account(mut, seeds = [AUTHORITY_PDA_SEED], bump)] - authority_pda: Account<'info, LightGovernanceAuthority>, - /// CHECK: this is - #[account(mut, seeds = [CPI_AUTHORITY_PDA_SEED], bump)] - cpi_authority: AccountInfo<'info>, - #[account(mut)] - group_pda: Account<'info, GroupAuthority>, - account_compression_program: Program<'info, AccountCompression>, - system_program: Program<'info, System>, - /// CHECK: - registered_program_pda: AccountInfo<'info>, - /// CHECK: is checked in the account compression program. - program_to_be_registered: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct NullifyLeaves<'info> { - /// CHECK: - #[account(mut)] - pub registered_forester_pda: Account<'info, ForesterEpoch>, - /// CHECK: unchecked for now logic that regulates forester access is yet to be added. - pub authority: Signer<'info>, - /// CHECK: - #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] - cpi_authority: AccountInfo<'info>, - /// CHECK: - #[account( - seeds = [&crate::ID.to_bytes()], bump, seeds::program = &account_compression::ID, - )] - pub registered_program_pda: - Account<'info, account_compression::instructions::register_program::RegisteredProgram>, - pub account_compression_program: Program<'info, AccountCompression>, - /// CHECK: when emitting event. - pub log_wrapper: UncheckedAccount<'info>, - /// CHECK: in account compression program - #[account(mut)] - pub merkle_tree: AccountInfo<'info>, - /// CHECK: in account compression program - #[account(mut)] - pub nullifier_queue: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct RolloverMerkleTreeAndQueue<'info> { - /// CHECK: - #[account(mut)] - pub registered_forester_pda: Account<'info, ForesterEpoch>, - /// CHECK: unchecked for now logic that regulates forester access is yet to be added. - #[account(mut)] - pub authority: Signer<'info>, - /// CHECK: - #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] - cpi_authority: AccountInfo<'info>, - /// CHECK: - #[account( - seeds = [&crate::ID.to_bytes()], bump, seeds::program = &account_compression::ID, - )] - pub registered_program_pda: - Account<'info, account_compression::instructions::register_program::RegisteredProgram>, - pub account_compression_program: Program<'info, AccountCompression>, - /// CHECK: - #[account(zero)] - pub new_merkle_tree: AccountInfo<'info>, - /// CHECK: - #[account(zero)] - pub new_queue: AccountInfo<'info>, - /// CHECK: - #[account(mut)] - pub old_merkle_tree: AccountInfo<'info>, - /// CHECK: - #[account(mut)] - pub old_queue: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct UpdateAddressMerkleTree<'info> { - /// CHECK: - #[account(mut)] - pub registered_forester_pda: Account<'info, ForesterEpoch>, - /// CHECK: unchecked for now logic that regulates forester access is yet to be added. - pub authority: Signer<'info>, - /// CHECK: - #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] - cpi_authority: AccountInfo<'info>, - /// CHECK: - #[account( - seeds = [&crate::ID.to_bytes()], bump, seeds::program = &account_compression::ID, - )] - pub registered_program_pda: - Account<'info, account_compression::instructions::register_program::RegisteredProgram>, - pub account_compression_program: Program<'info, AccountCompression>, - /// CHECK: in account compression program - #[account(mut)] - pub queue: AccountInfo<'info>, - /// CHECK: in account compression program - #[account(mut)] - pub merkle_tree: AccountInfo<'info>, - /// CHECK: when emitting event. - pub log_wrapper: UncheckedAccount<'info>, } diff --git a/programs/registry/src/protocol_config/initialize.rs b/programs/registry/src/protocol_config/initialize.rs new file mode 100644 index 0000000000..89b9f78e8e --- /dev/null +++ b/programs/registry/src/protocol_config/initialize.rs @@ -0,0 +1,26 @@ +use crate::protocol_config::state::ProtocolConfigPda; +use account_compression::utils::constants::CPI_AUTHORITY_PDA_SEED; +use anchor_lang::prelude::*; +use anchor_spl::token::Mint; + +#[constant] +pub const AUTHORITY_PDA_SEED: &[u8] = b"authority"; + +#[derive(Accounts)] +#[instruction(bump: u8)] +pub struct InitializeAuthority<'info> { + // TODO: add check that this is upgrade authority + #[account(mut)] + pub authority: Signer<'info>, + /// CHECK: + #[account(init, seeds = [AUTHORITY_PDA_SEED], bump, space = ProtocolConfigPda::LEN, payer = authority)] + pub authority_pda: Account<'info, ProtocolConfigPda>, + pub system_program: Program<'info, System>, + pub mint: Account<'info, Mint>, + /// CHECK: + #[account( + seeds = [CPI_AUTHORITY_PDA_SEED], + bump, + )] + pub cpi_authority: AccountInfo<'info>, +} diff --git a/programs/registry/src/protocol_config/mint.rs b/programs/registry/src/protocol_config/mint.rs new file mode 100644 index 0000000000..0fd1f591f1 --- /dev/null +++ b/programs/registry/src/protocol_config/mint.rs @@ -0,0 +1,125 @@ +use account_compression::utils::constants::CPI_AUTHORITY_PDA_SEED; +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint as SplMint, Token, TokenAccount}; +use light_compressed_token::program::LightCompressedToken; +use light_macros::pubkey; +use light_system_program::program::LightSystemProgram; + +use crate::{ + delegate::traits::{ + CompressedTokenProgramAccounts, MintToAccounts, SignerAccounts, SystemProgramAccounts, + }, + AUTHORITY_PDA_SEED, +}; + +use super::state::ProtocolConfigPda; + +pub const MINT: Pubkey = pubkey!("2bpg7jkqKDUSxB8dGh3SB4BC2b7JhbgY9cvYzpLP1PcZ"); + +#[derive(Accounts)] +pub struct Mint<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + pub authority: Signer<'info>, + /// CHECK: + #[account(mut, seeds = [AUTHORITY_PDA_SEED], bump, has_one = authority)] + pub protocol_config_pda: Account<'info, ProtocolConfigPda>, + #[account(mut)] + pub mint: Account<'info, SplMint>, + /// CHECK: + #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] + pub cpi_authority: AccountInfo<'info>, + /// CHECK: + pub token_cpi_authority_pda: AccountInfo<'info>, + pub compressed_token_program: Program<'info, LightCompressedToken>, + /// CHECK: this account is checked implictly since a mint to from a mint + /// account to a token account of a different mint will fail + #[account(mut)] + pub token_pool_pda: Account<'info, TokenAccount>, + pub token_program: Program<'info, Token>, + pub light_system_program: Program<'info, LightSystemProgram>, + /// CHECK: (different program) checked in account compression program + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: (different program) checked in system and account compression + /// programs + pub noop_program: AccountInfo<'info>, + /// CHECK: this account in account compression program + pub account_compression_authority: UncheckedAccount<'info>, + /// CHECK: this account in account compression program + pub account_compression_program: + Program<'info, account_compression::program::AccountCompression>, + /// CHECK: (different program) will be checked by the system program + #[account(mut)] + pub merkle_tree: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, +} + +impl<'info> SystemProgramAccounts<'info> for Mint<'info> { + fn get_registered_program_pda(&self) -> AccountInfo<'info> { + self.registered_program_pda.to_account_info() + } + fn get_noop_program(&self) -> AccountInfo<'info> { + self.noop_program.to_account_info() + } + fn get_account_compression_authority(&self) -> AccountInfo<'info> { + self.account_compression_authority.to_account_info() + } + fn get_account_compression_program(&self) -> AccountInfo<'info> { + self.account_compression_program.to_account_info() + } + fn get_system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + fn get_sol_pool_pda(&self) -> Option> { + None + } + fn get_decompression_recipient(&self) -> Option> { + None + } + fn get_light_system_program(&self) -> AccountInfo<'info> { + self.light_system_program.to_account_info() + } + fn get_self_program(&self) -> AccountInfo<'info> { + self.light_system_program.to_account_info() + } +} + +impl<'info> SignerAccounts<'info> for Mint<'info> { + fn get_fee_payer(&self) -> AccountInfo<'info> { + self.fee_payer.to_account_info() + } + fn get_authority(&self) -> AccountInfo<'info> { + self.authority.to_account_info() + } + fn get_cpi_authority_pda(&self) -> AccountInfo<'info> { + self.cpi_authority.to_account_info() + } +} + +impl<'info> CompressedTokenProgramAccounts<'info> for Mint<'info> { + fn get_token_cpi_authority_pda(&self) -> AccountInfo<'info> { + self.token_cpi_authority_pda.to_account_info() + } + fn get_compressed_token_program(&self) -> AccountInfo<'info> { + self.compressed_token_program.to_account_info() + } + fn get_escrow_authority_pda(&self) -> AccountInfo<'info> { + unimplemented!("escrow authority not implemented"); + // self.cpi_authority.to_account_info() + } + fn get_spl_token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + fn get_token_pool_pda(&self) -> AccountInfo<'info> { + self.token_pool_pda.to_account_info() + } + fn get_compress_or_decompress_token_account(&self) -> Option> { + None + } +} + +impl<'info> MintToAccounts<'info> for Mint<'info> { + fn get_mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } +} diff --git a/programs/registry/src/protocol_config/mod.rs b/programs/registry/src/protocol_config/mod.rs new file mode 100644 index 0000000000..6dab4ad1cb --- /dev/null +++ b/programs/registry/src/protocol_config/mod.rs @@ -0,0 +1,4 @@ +pub mod initialize; +pub mod mint; +pub mod state; +pub mod update; diff --git a/programs/registry/src/protocol_config/state.rs b/programs/registry/src/protocol_config/state.rs new file mode 100644 index 0000000000..5807d75a7e --- /dev/null +++ b/programs/registry/src/protocol_config/state.rs @@ -0,0 +1,351 @@ +use crate::{errors::RegistryError, MINT}; +use aligned_sized::aligned_sized; +use anchor_lang::prelude::*; + +#[aligned_sized(anchor)] +#[derive(Debug)] +#[account] +pub struct ProtocolConfigPda { + pub authority: Pubkey, + pub bump: u8, + pub config: ProtocolConfig, +} + +// TODO: replace epoch reward with inflation curve. +/// Epoch Phases: +/// 1. Registration +/// 2. Active +/// 3. Report Work +/// 4. Post (Epoch has ended, and rewards can be claimed.) +/// - There is always an active phase in progress, registration and report work +/// phases run in parallel to a currently active phase. +#[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub struct ProtocolConfig { + /// Solana slot when the protocol starts operating. + pub genesis_slot: u64, + /// Total rewards per epoch. + pub epoch_reward: u64, + /// Base reward for foresters, the difference between epoch reward and base + /// reward distributed based on performance. + pub base_reward: u64, + /// Minimum stake required for a forester to register to an epoch. + pub min_stake: u64, + /// Light protocol slot length. (Naming is confusing for Solana slot.) + /// TODO: rename to epoch_length (registration + active phase length) + pub slot_length: u64, + /// Foresters can register for this phase. + pub registration_phase_length: u64, + /// Foresters can perform work in this phase. + pub active_phase_length: u64, + /// Foresters can report work to receive performance based rewards in this + /// phase. + /// TODO: enforce report work == registration phase length so that + /// epoch in report work phase is registration epoch - 1 + pub report_work_phase_length: u64, + pub mint: Pubkey, + pub forester_registration_guarded: bool, +} + +impl Default for ProtocolConfig { + fn default() -> Self { + Self { + genesis_slot: 0, + epoch_reward: 1_000_000, + base_reward: 500_000, + min_stake: 0, + slot_length: 10, + registration_phase_length: 100, + active_phase_length: 1000, + report_work_phase_length: 100, + mint: MINT, + forester_registration_guarded: true, + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub enum EpochState { + Registration, + Active, + ReportWork, + Post, + #[default] + Pre, +} + +impl ProtocolConfig { + /// Current epoch including registration phase Only use to get registration + /// phase. + pub fn get_current_epoch(&self, slot: u64) -> u64 { + (slot.saturating_sub(self.genesis_slot)) / self.active_phase_length + } + pub fn get_current_active_epoch(&self, slot: u64) -> Result { + // msg!("slot: {}", slot); + // msg!("genesis_slot: {}", self.genesis_slot); + // msg!( + // "registration_phase_length: {}", + // self.registration_phase_length + // ); + let slot = match slot.checked_sub(self.genesis_slot + self.registration_phase_length) { + Some(slot) => slot, + None => return err!(RegistryError::EpochEnded), + }; + Ok(slot / self.active_phase_length) + } + + pub fn get_current_epoch_progress(&self, slot: u64) -> u64 { + (slot.saturating_sub(self.genesis_slot)) % self.active_phase_length + } + + pub fn get_current_active_epoch_progress(&self, slot: u64) -> u64 { + (slot.saturating_sub(self.genesis_slot + self.registration_phase_length)) + % self.active_phase_length + } + + /// In the last part of the active phase the registration phase starts. + pub fn is_registration_phase(&self, slot: u64) -> Result { + let current_epoch = self.get_current_epoch(slot); + let current_epoch_progress = self.get_current_epoch_progress(slot); + if current_epoch_progress >= self.registration_phase_length { + return err!(RegistryError::NotInRegistrationPeriod); + } + Ok((current_epoch) * self.active_phase_length + + self.genesis_slot + + self.registration_phase_length) + } + + pub fn is_active_phase(&self, slot: u64, epoch: u64) -> Result<()> { + if self.get_current_active_epoch(slot)? != epoch { + return err!(RegistryError::NotInActivePhase); + } + Ok(()) + } + + pub fn is_report_work_phase(&self, slot: u64, epoch: u64) -> Result<()> { + self.is_active_phase(slot, epoch + 1)?; + let current_epoch_progress = self.get_current_active_epoch_progress(slot); + if current_epoch_progress >= self.report_work_phase_length { + return err!(RegistryError::NotInReportWorkPhase); + } + Ok(()) + } + + pub fn is_post_epoch(&self, slot: u64, epoch: u64) -> Result<()> { + if self.get_current_active_epoch(slot)? == epoch { + return err!(RegistryError::InvalidEpoch); + } + Ok(()) + } + + /// Rewards: + /// 1. foresters should be incentivezed to performe work + /// 2. foresters contention is mitigated by giving time slots + /// 2.1 foresters with more stake receive more timeslots to perform work + /// 3. rewards + /// 3.1 base rewards 50% of the total rewards - distributed based on relative stake + /// 3.2 remainging 50% relative amount of work performed + pub fn get_rewards( + &self, + total_stake_weight: u64, + total_tally: u64, + forester_stake_weight: u64, + forester_tally: u64, + ) -> u64 { + let total_merit_reward = self.epoch_reward - self.base_reward; + let merit_reward = total_merit_reward * forester_tally / total_tally; + let stake_reward = self.base_reward * forester_stake_weight / total_stake_weight; + merit_reward + stake_reward + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_zero_values() { + let protocol_config = ProtocolConfig { + genesis_slot: 0, + registration_phase_length: 1, + active_phase_length: 7, + report_work_phase_length: 2, + epoch_reward: 100_000, + base_reward: 50_000, + min_stake: 0, + slot_length: 1, + mint: Pubkey::new_unique(), + forester_registration_guarded: true, + }; + + assert_eq!(protocol_config.get_rewards(100_000, 20_000, 0, 0), 0); + assert_eq!( + protocol_config.get_rewards(100_000, 20_000, 10_000, 0), + 5_000 + ); + assert_eq!( + protocol_config.get_rewards(100_000, 20_000, 0, 10_000), + 25_000 + ); + } + + #[test] + fn test_equal_stake_and_tally() { + let protocol_config = ProtocolConfig { + genesis_slot: 0, + registration_phase_length: 1, + active_phase_length: 7, + report_work_phase_length: 2, + epoch_reward: 100_000, + base_reward: 50_000, + min_stake: 0, + slot_length: 1, + mint: Pubkey::new_unique(), + forester_registration_guarded: true, + }; + + let total_stake_weight = 100_000; + let total_tally = 20_000; + + assert_eq!( + protocol_config.get_rewards(total_stake_weight, total_tally, 100_000, 20_000), + 100_000 + ); + } + + #[test] + fn test_single_forester() { + let protocol_config = ProtocolConfig { + genesis_slot: 0, + registration_phase_length: 1, + active_phase_length: 7, + report_work_phase_length: 2, + epoch_reward: 100_000, + base_reward: 50_000, + min_stake: 0, + slot_length: 1, + mint: Pubkey::new_unique(), + forester_registration_guarded: true, + }; + + let total_stake_weight = 10_000; + let total_tally = 10_000; + let forester_stake_weight = 10_000; + let forester_tally = 10_000; + + let reward = protocol_config.get_rewards( + total_stake_weight, + total_tally, + forester_stake_weight, + forester_tally, + ); + let expected_reward = 100_000; + assert_eq!(reward, expected_reward); + } + + #[test] + fn test_proportional_distribution() { + let protocol_config = ProtocolConfig { + genesis_slot: 0, + registration_phase_length: 1, + active_phase_length: 7, + report_work_phase_length: 2, + epoch_reward: 100_000, + base_reward: 50_000, + min_stake: 0, + slot_length: 1, + mint: Pubkey::new_unique(), + forester_registration_guarded: true, + }; + + let total_stake_weight = 100_000; + let total_tally = 20_000; + + let forester_stake_weight = 10_000; + let forester_tally = 5_000; + + let reward = protocol_config.get_rewards( + total_stake_weight, + total_tally, + forester_stake_weight, + forester_tally, + ); + let expected_reward = 17_500; + assert_eq!(reward, expected_reward); + } + + #[test] + fn reward_calculation() { + let protocol_config = ProtocolConfig { + genesis_slot: 0, + registration_phase_length: 1, + active_phase_length: 7, + report_work_phase_length: 2, + epoch_reward: 100_000, + base_reward: 50_000, + min_stake: 0, + slot_length: 1, + mint: Pubkey::new_unique(), + forester_registration_guarded: true, + }; + let total_stake_weight = 100_000; + let total_tally = 20_000; + { + let forester_stake_weight = 10_000; + let forester_tally = 10_000; + + let reward = protocol_config.get_rewards( + total_stake_weight, + total_tally, + forester_stake_weight, + forester_tally, + ); + assert_eq!(reward, 30_000); + let forester_stake_weight = 90_000; + let forester_tally = 10_000; + + let reward = protocol_config.get_rewards( + total_stake_weight, + total_tally, + forester_stake_weight, + forester_tally, + ); + assert_eq!(reward, 70_000); + } + // Forester performs max and receives max + { + let forester_stake_weight = 20_000; + let forester_tally = 10_000; + + let reward = protocol_config.get_rewards( + total_stake_weight, + total_tally, + forester_stake_weight, + forester_tally, + ); + assert_eq!(reward, 35_000); + let forester_stake_weight = 80_000; + let forester_tally = 10_000; + + let reward = protocol_config.get_rewards( + total_stake_weight, + total_tally, + forester_stake_weight, + forester_tally, + ); + assert_eq!(reward, 65_000); + } + // forester performs less -> receives less + { + let forester_stake_weight = 20_000; + let forester_tally = 0; + + let reward = protocol_config.get_rewards( + total_stake_weight, + total_tally, + forester_stake_weight, + forester_tally, + ); + assert_eq!(reward, 10_000); + } + } +} diff --git a/programs/registry/src/protocol_config/update.rs b/programs/registry/src/protocol_config/update.rs new file mode 100644 index 0000000000..6052563c46 --- /dev/null +++ b/programs/registry/src/protocol_config/update.rs @@ -0,0 +1,17 @@ +use anchor_lang::prelude::*; + +use crate::AUTHORITY_PDA_SEED; + +use super::state::ProtocolConfigPda; + +#[derive(Accounts)] +#[instruction(bump: u8)] +pub struct UpdateAuthority<'info> { + #[account(mut, constraint = authority.key() == authority_pda.authority)] + pub authority: Signer<'info>, + /// CHECK: + // TODO: rename to protocol config pda + #[account(mut, seeds = [AUTHORITY_PDA_SEED], bump)] + pub authority_pda: Account<'info, ProtocolConfigPda>, + pub new_authority: Signer<'info>, +} diff --git a/programs/registry/src/sdk.rs b/programs/registry/src/sdk.rs index 726a7b2d1a..877cd05583 100644 --- a/programs/registry/src/sdk.rs +++ b/programs/registry/src/sdk.rs @@ -1,11 +1,48 @@ #![cfg(not(target_os = "solana"))] -use crate::get_forester_epoch_pda_address; -use account_compression::{ - self, utils::constants::GROUP_AUTHORITY_SEED, AddressMerkleTreeConfig, AddressQueueConfig, - NullifierQueueConfig, StateMerkleTreeConfig, ID, +use std::collections::HashMap; + +use crate::{ + delegate::{ + deposit::{ + DelegateAccountWithContext, DelegateAccountWithPackedContext, + InputDelegateAccountWithPackedContext, + }, + get_escrow_token_authority, + }, + epoch::{ + claim_forester::CompressedForesterEpochAccountInput, + sync_delegate::SyncDelegateTokenAccount, + }, + protocol_config::state::ProtocolConfig, + utils::{ + get_cpi_authority_pda, get_epoch_pda_address, get_forester_epoch_pda_address, + get_forester_pda_address, get_forester_token_pool_pda, get_protocol_config_pda_address, + }, + ForesterConfig, MINT, }; +use account_compression::{self, ID}; use anchor_lang::{system_program, InstructionData, ToAccountMetas}; +use light_compressed_token::{ + get_token_pool_pda, + process_transfer::{ + transfer_sdk::{ + create_input_output_and_remaining_accounts, create_input_token_accounts, + to_account_metas, + }, + InputTokenDataWithContext, + }, + TokenData, +}; use light_macros::pubkey; +use light_system_program::{ + invoke::processor::CompressedProof, + sdk::{ + compressed_account::{ + pack_merkle_context, CompressedAccountWithMerkleContext, MerkleContext, + }, + CompressedCpiContext, + }, +}; use solana_sdk::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, @@ -36,11 +73,13 @@ pub fn create_initialize_group_authority_instruction( pub fn create_update_authority_instruction( signer_pubkey: Pubkey, new_authority: Pubkey, + new_protocol_config: ProtocolConfig, ) -> Instruction { - let authority_pda = get_governance_authority_pda(); + let authority_pda = get_protocol_config_pda_address(); let update_authority_ix = crate::instruction::UpdateGovernanceAuthority { - bump: authority_pda.1, + _bump: authority_pda.1, new_authority, + new_config: new_protocol_config, }; // update with new authority @@ -67,76 +106,69 @@ pub fn create_register_program_instruction( let register_program_ix = crate::instruction::RegisterSystemProgram { bump: cpi_authority_pda.1, }; + let register_program_accounts = crate::accounts::RegisteredProgram { + authority: signer_pubkey, + program_to_be_registered: program_id_to_be_registered, + registered_program_pda, + authority_pda: authority_pda.0, + group_pda: group_account, + cpi_authority: cpi_authority_pda.0, + account_compression_program: ID, + system_program: system_program::ID, + }; let instruction = Instruction { program_id: crate::ID, - accounts: vec![ - AccountMeta::new(signer_pubkey, true), - AccountMeta::new(authority_pda.0, false), - AccountMeta::new(cpi_authority_pda.0, false), - AccountMeta::new(group_account, false), - AccountMeta::new_readonly(ID, false), - AccountMeta::new_readonly(system_program::ID, false), - AccountMeta::new(registered_program_pda, false), - AccountMeta::new(program_id_to_be_registered, true), - ], + accounts: register_program_accounts.to_account_metas(Some(true)), data: register_program_ix.data(), }; (instruction, registered_program_pda) } -pub fn get_governance_authority_pda() -> (Pubkey, u8) { - Pubkey::find_program_address(&[crate::AUTHORITY_PDA_SEED], &crate::ID) -} - -pub fn get_cpi_authority_pda() -> (Pubkey, u8) { - Pubkey::find_program_address(&[crate::CPI_AUTHORITY_PDA_SEED], &crate::ID) -} - pub fn create_initialize_governance_authority_instruction( signer_pubkey: Pubkey, - authority: Pubkey, + protocol_config: ProtocolConfig, ) -> Instruction { - let authority_pda = get_governance_authority_pda(); + let authority_pda = get_protocol_config_pda_address(); let ix = crate::instruction::InitializeGovernanceAuthority { bump: authority_pda.1, - authority, - rewards: vec![], + protocol_config, + }; + let cpi_authority_pda = get_cpi_authority_pda().0; + + let accounts = crate::accounts::InitializeAuthority { + authority_pda: authority_pda.0, + authority: signer_pubkey, + system_program: system_program::ID, + mint: protocol_config.mint, + cpi_authority: cpi_authority_pda, }; - Instruction { program_id: crate::ID, - accounts: vec![ - AccountMeta::new(signer_pubkey, true), - AccountMeta::new(authority_pda.0, false), - AccountMeta::new_readonly(system_program::ID, false), - ], + accounts: accounts.to_account_metas(Some(true)), data: ix.data(), } } -pub fn get_group_pda(seed: Pubkey) -> Pubkey { - Pubkey::find_program_address( - &[GROUP_AUTHORITY_SEED, seed.to_bytes().as_slice()], - &account_compression::ID, - ) - .0 -} pub fn create_register_forester_instruction( governance_authority: &Pubkey, forester_authority: &Pubkey, + config: ForesterConfig, ) -> Instruction { - let (forester_epoch_pda, _bump) = get_forester_epoch_pda_address(forester_authority); - let instruction_data = crate::instruction::RegisterForester { - _bump: 0, - authority: *forester_authority, - }; - let (authority_pda, _) = get_governance_authority_pda(); + let (forester_pda, _bump) = get_forester_pda_address(forester_authority); + let instruction_data = crate::instruction::RegisterForester { _bump: 0, config }; + let (protocol_config_pda, _) = get_protocol_config_pda_address(); + let token_pool_pda = get_forester_token_pool_pda(forester_authority); let accounts = crate::accounts::RegisterForester { - forester_epoch_pda, + forester_pda, signer: *governance_authority, - authority_pda, + protocol_config_pda, system_program: solana_sdk::system_program::id(), + authority: *forester_authority, + token_pool_pda, + mint: MINT, + cpi_authority_pda: get_cpi_authority_pda().0, + token_program: anchor_spl::token::ID, }; Instruction { program_id: crate::ID, @@ -145,11 +177,11 @@ pub fn create_register_forester_instruction( } } -pub fn create_update_forester_instruction( +pub fn create_update_forester_epoch_pda_instruction( forester_authority: &Pubkey, new_authority: &Pubkey, ) -> Instruction { - let (forester_epoch_pda, _bump) = get_forester_epoch_pda_address(forester_authority); + let (forester_epoch_pda, _bump) = get_forester_pda_address(forester_authority); let instruction_data = crate::instruction::UpdateForesterEpochPda { authority: *new_authority, }; @@ -164,39 +196,59 @@ pub fn create_update_forester_instruction( } } -pub struct CreateNullifyInstructionInputs { - pub authority: Pubkey, - pub nullifier_queue: Pubkey, - pub merkle_tree: Pubkey, - pub change_log_indices: Vec, - pub leaves_queue_indices: Vec, - pub indices: Vec, - pub proofs: Vec>, - pub derivation: Pubkey, +pub fn create_update_forester_pda_instruction( + forester_authority: &Pubkey, + new_authority: Option, + config: ForesterConfig, +) -> Instruction { + let (forester_pda, _) = get_forester_pda_address(forester_authority); + let instruction_data = crate::instruction::UpdateForester { config }; + let accounts = crate::accounts::UpdateForester { + forester_pda, + authority: *forester_authority, + new_authority, + }; + Instruction { + program_id: crate::ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction_data.data(), + } } -pub fn create_nullify_instruction(inputs: CreateNullifyInstructionInputs) -> Instruction { - let register_program_pda = get_registered_program_pda(&crate::ID); - let registered_forester_pda = get_forester_epoch_pda_address(&inputs.derivation).0; - log::info!("registered_forester_pda: {:?}", registered_forester_pda); - let (cpi_authority, bump) = get_cpi_authority_pda(); - let instruction_data = crate::instruction::Nullify { - bump, - change_log_indices: inputs.change_log_indices, - leaves_queue_indices: inputs.leaves_queue_indices, - indices: inputs.indices, - proofs: inputs.proofs, +pub fn create_register_forester_epoch_pda_instruction( + authority: &Pubkey, + epoch: u64, +) -> Instruction { + let (forester_pda, _) = get_forester_pda_address(authority); + let (forester_epoch_pda, _bump) = get_forester_epoch_pda_address(&forester_pda, epoch); + + let epoch_pda = get_epoch_pda_address(epoch); + let protocol_config_pda = get_protocol_config_pda_address().0; + let instruction_data = crate::instruction::RegisterForesterEpoch { epoch }; + let accounts = crate::accounts::RegisterForesterEpoch { + forester_epoch_pda, + forester_pda, + authority: *authority, + epoch_pda, + protocol_config: protocol_config_pda, + system_program: solana_sdk::system_program::id(), }; + Instruction { + program_id: crate::ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction_data.data(), + } +} - let accounts = crate::accounts::NullifyLeaves { - authority: inputs.authority, - registered_forester_pda, - registered_program_pda: register_program_pda, - nullifier_queue: inputs.nullifier_queue, - merkle_tree: inputs.merkle_tree, - log_wrapper: NOOP_PROGRAM_ID, - cpi_authority, - account_compression_program: account_compression::ID, +pub fn create_finalize_registration_instruction(authority: &Pubkey, epoch: u64) -> Instruction { + let forester_pda = get_forester_pda_address(authority).0; + let (forester_epoch_pda, _bump) = get_forester_epoch_pda_address(&forester_pda, epoch); + let epoch_pda = get_epoch_pda_address(epoch); + let instruction_data = crate::instruction::FinalizeRegistration {}; + let accounts = crate::accounts::FinalizeRegistration { + forester_epoch_pda, + authority: *authority, + epoch_pda, }; Instruction { program_id: crate::ID, @@ -205,142 +257,402 @@ pub fn create_nullify_instruction(inputs: CreateNullifyInstructionInputs) -> Ins } } -pub fn get_registered_program_pda(program_id: &Pubkey) -> Pubkey { - Pubkey::find_program_address( - &[program_id.to_bytes().as_slice()], - &account_compression::ID, - ) - .0 +pub fn create_report_work_instruction(authority: &Pubkey, epoch: u64) -> Instruction { + let (forester_pda, _) = get_forester_pda_address(authority); + let (forester_epoch_pda, _bump) = get_forester_epoch_pda_address(&forester_pda, epoch); + let epoch_pda = get_epoch_pda_address(epoch); + let instruction_data = crate::instruction::ReportWork {}; + let accounts = crate::accounts::ReportWork { + authority: *authority, + forester_epoch_pda, + epoch_pda, + }; + Instruction { + program_id: crate::ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction_data.data(), + } +} + +pub struct StandardCompressedTokenProgramAccounts { + pub token_cpi_authority_pda: Pubkey, + pub compressed_token_program: Pubkey, + pub token_pool_pda: Pubkey, + pub token_program: Pubkey, + pub light_system_program: Pubkey, + pub registered_program_pda: Pubkey, + pub noop_program: Pubkey, + pub account_compression_authority: Pubkey, + pub account_compression_program: Pubkey, + pub system_program: Pubkey, } -pub struct CreateRolloverMerkleTreeInstructionInputs { - pub authority: Pubkey, - pub new_queue: Pubkey, - pub new_merkle_tree: Pubkey, - pub old_queue: Pubkey, - pub old_merkle_tree: Pubkey, +pub fn get_standard_compressed_token_program_accounts( + mint: Pubkey, +) -> StandardCompressedTokenProgramAccounts { + StandardCompressedTokenProgramAccounts { + token_cpi_authority_pda: light_compressed_token::process_transfer::get_cpi_authority_pda() + .0, + compressed_token_program: light_compressed_token::ID, + token_pool_pda: light_compressed_token::get_token_pool_pda(&mint), + token_program: anchor_spl::token::ID, + light_system_program: light_system_program::ID, + registered_program_pda: light_system_program::utils::get_registered_program_pda( + &light_system_program::ID, + ), + noop_program: NOOP_PROGRAM_ID, + account_compression_authority: light_system_program::utils::get_cpi_authority_pda( + &light_system_program::ID, + ), + account_compression_program: account_compression::ID, + system_program: system_program::ID, + } } -pub fn create_rollover_address_merkle_tree_instruction( - inputs: CreateRolloverMerkleTreeInstructionInputs, +pub fn create_mint_to_instruction( + mint: &Pubkey, + authority: &Pubkey, + recipient: &Pubkey, + amount: u64, + merkle_tree: &Pubkey, ) -> Instruction { - let (_, bump) = crate::sdk::get_cpi_authority_pda(); + let protocol_config_pda = get_protocol_config_pda_address().0; + let (cpi_authority_pda, _) = get_cpi_authority_pda(); + let instruction_data = crate::instruction::Mint { + amounts: vec![amount], + recipients: vec![*recipient], + }; + let standard_accounts = get_standard_compressed_token_program_accounts(*mint); + let accounts = crate::accounts::Mint { + fee_payer: *authority, + authority: *authority, + protocol_config_pda, + mint: *mint, + merkle_tree: *merkle_tree, + cpi_authority: cpi_authority_pda, + token_cpi_authority_pda: standard_accounts.token_cpi_authority_pda, + compressed_token_program: standard_accounts.compressed_token_program, + token_pool_pda: standard_accounts.token_pool_pda, + token_program: standard_accounts.token_program, + light_system_program: standard_accounts.light_system_program, + registered_program_pda: standard_accounts.registered_program_pda, + noop_program: standard_accounts.noop_program, + account_compression_authority: standard_accounts.account_compression_authority, + account_compression_program: standard_accounts.account_compression_program, + system_program: standard_accounts.system_program, + }; + Instruction { + program_id: crate::ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction_data.data(), + } +} - let instruction_data = crate::instruction::RolloverAddressMerkleTreeAndQueue { bump }; - create_rollover_instruction(instruction_data.data(), inputs) +pub struct StandardRegistryAccounts { + pub protocol_config_pda: Pubkey, + pub cpi_authority_pda: Pubkey, + pub self_program: Pubkey, } -pub fn create_rollover_state_merkle_tree_instruction( - inputs: CreateRolloverMerkleTreeInstructionInputs, -) -> Instruction { - let (_, bump) = crate::sdk::get_cpi_authority_pda(); +pub fn get_standard_registry_accounts() -> StandardRegistryAccounts { + StandardRegistryAccounts { + protocol_config_pda: get_protocol_config_pda_address().0, + cpi_authority_pda: get_cpi_authority_pda().0, + self_program: crate::ID, + } +} - let instruction_data = crate::instruction::RolloverStateMerkleTreeAndQueue { bump }; - create_rollover_instruction(instruction_data.data(), inputs) +#[derive(Debug, Clone)] +pub struct CreateDepositInstructionInputs { + pub sender: Pubkey, + pub cpi_context_account: Pubkey, + pub salt: u64, + pub delegate_account: Option, + pub amount: u64, + pub input_token_data: Vec, + pub input_compressed_accounts: Vec, + pub input_escrow_token_account: Option<(TokenData, CompressedAccountWithMerkleContext)>, + pub escrow_token_account_merkle_tree: Pubkey, + pub change_compressed_account_merkle_tree: Pubkey, + pub output_delegate_compressed_account_merkle_tree: Pubkey, + pub proof: CompressedProof, + pub root_indices: Vec, } -pub fn create_rollover_instruction( - data: Vec, - inputs: CreateRolloverMerkleTreeInstructionInputs, +pub fn get_index_and_add_to_remaining_accounts( + remaining_accounts: &mut HashMap, + account: &Pubkey, +) -> usize { + let index = remaining_accounts.len(); + + match remaining_accounts.get(account) { + Some(index) => *index, + None => { + remaining_accounts.insert(*account, index); + index + } + } +} + +/// Accounts in proof order: +/// 1. input pda (if some) +/// 2. input token accounts[..] +/// 3. input escrow token account (if some) +/// +/// 3 types of input accounts +/// 1. input_token_data +/// 2. +// input_escrow_token_account is expected to be the last account in the proof +pub fn create_deposit_instruction( + inputs: CreateDepositInstructionInputs, ) -> Instruction { - let (cpi_authority, _) = crate::sdk::get_cpi_authority_pda(); - let registered_program_pda = get_registered_program_pda(&crate::ID); - let registered_forester_pda = get_forester_epoch_pda_address(&inputs.authority).0; - let accounts = crate::accounts::RolloverMerkleTreeAndQueue { - account_compression_program: account_compression::ID, - registered_forester_pda, - cpi_authority, - authority: inputs.authority, - registered_program_pda, - new_merkle_tree: inputs.new_merkle_tree, - new_queue: inputs.new_queue, - old_merkle_tree: inputs.old_merkle_tree, - old_queue: inputs.old_queue, + let root_indices_range_input_accounts = 0..inputs.input_compressed_accounts.len(); + + let (mut remaining_accounts, input_compressed_token_accounts, _) = + create_input_output_and_remaining_accounts( + &[], + &inputs.input_token_data, + inputs.input_compressed_accounts.as_slice(), + &inputs.root_indices[root_indices_range_input_accounts], + &[], // outputs are created onchain + ); + let input_escrow_token_account = + if let Some((token_data, compressed_account)) = inputs.input_escrow_token_account { + let mut index = remaining_accounts.len(); + let mut input_token_data_with_context: Vec = Vec::new(); + create_input_token_accounts( + &[token_data], + &mut remaining_accounts, + &[compressed_account], + &mut index, + &inputs.root_indices[inputs.input_compressed_accounts.len() + ..inputs.input_compressed_accounts.len() + 1], + &mut input_token_data_with_context, + ); + Some(input_token_data_with_context[0].clone()) + } else { + None + }; + let escrow_token_account_merkle_tree_index = get_index_and_add_to_remaining_accounts( + &mut remaining_accounts, + &inputs.escrow_token_account_merkle_tree, + ) as u8; + let change_compressed_account_merkle_tree_index = get_index_and_add_to_remaining_accounts( + &mut remaining_accounts, + &inputs.change_compressed_account_merkle_tree, + ) as u8; + let output_delegate_compressed_account_merkle_tree_index = + get_index_and_add_to_remaining_accounts( + &mut remaining_accounts, + &inputs.output_delegate_compressed_account_merkle_tree, + ) as u8; + let cpi_context_account_index = get_index_and_add_to_remaining_accounts( + &mut remaining_accounts, + &inputs.cpi_context_account, + ) as u8; + let delegate_account = if let Some(delegate_account) = inputs.delegate_account { + let packed_merkle_context = + pack_merkle_context(&[delegate_account.merkle_context], &mut remaining_accounts); + + Some(InputDelegateAccountWithPackedContext { + delegate_account: delegate_account.delegate_account.into(), + merkle_context: packed_merkle_context[0], + root_index: inputs.root_indices[inputs.root_indices.len() - 1], + }) + } else { + None }; - + let instruction_data = if IS_DEPOSIT { + crate::instruction::Deposit { + salt: inputs.salt, + delegate_account, + deposit_amount: inputs.amount, + input_compressed_token_accounts, + input_escrow_token_account, + escrow_token_account_merkle_tree_index, + change_compressed_account_merkle_tree_index, + output_delegate_compressed_account_merkle_tree_index, + proof: inputs.proof, + cpi_context: CompressedCpiContext { + set_context: false, + first_set_context: true, + cpi_context_account_index, + }, + } + .data() + } else { + let delegate_account = if let Some(delegate_account) = delegate_account { + delegate_account + } else { + panic!("delegate account is required for withdrawal"); + }; + let input_escrow_token_account = + if let Some(input_escrow_token_account) = input_escrow_token_account { + input_escrow_token_account + } else { + panic!("input escrow token account is required for withdrawal"); + }; + crate::instruction::Withdrawal { + salt: inputs.salt, + delegate_account, + withdrawal_amount: inputs.amount, + input_escrow_token_account, + escrow_token_account_merkle_tree_index, + change_compressed_account_merkle_tree_index, + output_delegate_compressed_account_merkle_tree_index, + proof: inputs.proof, + cpi_context: CompressedCpiContext { + set_context: false, + first_set_context: true, + cpi_context_account_index, + }, + } + .data() + }; + let standard_accounts = get_standard_compressed_token_program_accounts(MINT); + let (cpi_authority_pda, _) = get_cpi_authority_pda(); + let standard_registry_accounts = get_standard_registry_accounts(); + + let escrow_token_authority = get_escrow_token_authority(&inputs.sender, inputs.salt).0; + let accounts = crate::accounts::DepositOrWithdrawInstruction { + fee_payer: inputs.sender, + authority: inputs.sender, + cpi_authority: cpi_authority_pda, + token_cpi_authority_pda: standard_accounts.token_cpi_authority_pda, + compressed_token_program: standard_accounts.compressed_token_program, + light_system_program: standard_accounts.light_system_program, + registered_program_pda: standard_accounts.registered_program_pda, + noop_program: standard_accounts.noop_program, + account_compression_authority: standard_accounts.account_compression_authority, + account_compression_program: standard_accounts.account_compression_program, + system_program: standard_accounts.system_program, + cpi_context_account: inputs.cpi_context_account, + invoking_program: standard_registry_accounts.self_program, + escrow_token_authority, + protocol_config: standard_registry_accounts.protocol_config_pda, + self_program: standard_registry_accounts.self_program, + }; + let remaining_accounts = to_account_metas(remaining_accounts); Instruction { program_id: crate::ID, - accounts: accounts.to_account_metas(Some(true)), - data, + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + data: instruction_data, } } -pub struct UpdateAddressMerkleTreeInstructionInputs { - pub authority: Pubkey, - pub address_merkle_tree: Pubkey, - pub address_queue: Pubkey, - pub changelog_index: u16, - pub indexed_changelog_index: u16, - pub value: u16, - pub low_address_index: u64, - pub low_address_value: [u8; 32], - pub low_address_next_index: u64, - pub low_address_next_value: [u8; 32], - pub low_address_proof: [[u8; 32]; 16], +#[derive(Debug, Clone)] +pub struct CreateDelegateInstructionInputs { + pub sender: Pubkey, + pub delegate_account: DelegateAccountWithContext, + pub amount: u64, + pub output_delegate_compressed_account_merkle_tree: Pubkey, + pub proof: CompressedProof, + pub root_index: u16, + pub no_sync: bool, + pub forester_pda: Pubkey, } -pub fn create_update_address_merkle_tree_instruction( - instructions: UpdateAddressMerkleTreeInstructionInputs, +pub fn create_delegate_instruction( + inputs: CreateDelegateInstructionInputs, ) -> Instruction { - let register_program_pda = get_registered_program_pda(&crate::ID); - let registered_forester_pda = get_forester_epoch_pda_address(&instructions.authority).0; - - let (cpi_authority, bump) = get_cpi_authority_pda(); - let instruction_data = crate::instruction::UpdateAddressMerkleTree { - bump, - changelog_index: instructions.changelog_index, - indexed_changelog_index: instructions.indexed_changelog_index, - value: instructions.value, - low_address_index: instructions.low_address_index, - low_address_value: instructions.low_address_value, - low_address_next_index: instructions.low_address_next_index, - low_address_next_value: instructions.low_address_next_value, - low_address_proof: instructions.low_address_proof, + let mut remaining_accounts = HashMap::new(); + let output_merkle_tree_index = get_index_and_add_to_remaining_accounts( + &mut remaining_accounts, + &inputs.output_delegate_compressed_account_merkle_tree, + ) as u8; + + let packed_merkle_context = pack_merkle_context( + &[inputs.delegate_account.merkle_context], + &mut remaining_accounts, + ); + + let delegate_account = DelegateAccountWithPackedContext { + delegate_account: inputs.delegate_account.delegate_account, + merkle_context: packed_merkle_context[0], + root_index: inputs.root_index, + output_merkle_tree_index, }; - - let accounts = crate::accounts::UpdateAddressMerkleTree { - authority: instructions.authority, - registered_forester_pda, - registered_program_pda: register_program_pda, - merkle_tree: instructions.address_merkle_tree, - queue: instructions.address_queue, - log_wrapper: NOOP_PROGRAM_ID, - cpi_authority, - account_compression_program: account_compression::ID, + let instruction_data = if IS_DELEGATE { + crate::instruction::Delegate { + delegate_account, + delegate_amount: inputs.amount, + proof: inputs.proof, + no_sync: inputs.no_sync, + } + .data() + } else { + crate::instruction::Undelegate { + delegate_account, + delegate_amount: inputs.amount, + proof: inputs.proof, + no_sync: inputs.no_sync, + } + .data() }; + let standard_accounts = get_standard_compressed_token_program_accounts(MINT); + let (cpi_authority_pda, _) = get_cpi_authority_pda(); + let standard_registry_accounts = get_standard_registry_accounts(); + + let accounts = crate::accounts::DelegatetOrUndelegateInstruction { + fee_payer: inputs.sender, + authority: inputs.sender, + cpi_authority: cpi_authority_pda, + light_system_program: standard_accounts.light_system_program, + registered_program_pda: standard_accounts.registered_program_pda, + noop_program: standard_accounts.noop_program, + account_compression_authority: standard_accounts.account_compression_authority, + account_compression_program: standard_accounts.account_compression_program, + system_program: standard_accounts.system_program, + invoking_program: standard_registry_accounts.self_program, + protocol_config: standard_registry_accounts.protocol_config_pda, + self_program: standard_registry_accounts.self_program, + forester_pda: inputs.forester_pda, + }; + let remaining_accounts = to_account_metas(remaining_accounts); Instruction { program_id: crate::ID, - accounts: accounts.to_account_metas(Some(true)), - data: instruction_data.data(), + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + data: instruction_data, } } -pub fn create_initialize_address_merkle_tree_and_queue_instruction( - index: u64, - payer: Pubkey, - program_owner: Option, - merkle_tree_pubkey: Pubkey, - queue_pubkey: Pubkey, - address_merkle_tree_config: AddressMerkleTreeConfig, - address_queue_config: AddressQueueConfig, +pub fn create_forester_claim_instruction( + forester_pubkey: Pubkey, + epoch: u64, + output_merkle_tree: Pubkey, ) -> Instruction { - let register_program_pda = get_registered_program_pda(&crate::ID); - let (cpi_authority, bump) = crate::sdk::get_cpi_authority_pda(); - - let instruction_data = crate::instruction::InitializeAddressMerkleTree { - bump, - index, - program_owner, - merkle_tree_config: address_merkle_tree_config, - queue_config: address_queue_config, - }; - let accounts = crate::accounts::InitializeAddressMerkleTreeAndQueue { - authority: payer, - registered_program_pda: register_program_pda, - merkle_tree: merkle_tree_pubkey, - queue: queue_pubkey, - cpi_authority, - account_compression_program: account_compression::ID, + let instruction_data = crate::instruction::ClaimForesterRewards {}; + + let standard_accounts = get_standard_compressed_token_program_accounts(MINT); + let (cpi_authority_pda, _) = get_cpi_authority_pda(); + let standard_registry_accounts = get_standard_registry_accounts(); + + let forester_pda = get_forester_pda_address(&forester_pubkey).0; + let forester_epoch_pda = get_forester_epoch_pda_address(&forester_pda, epoch).0; + let forester_token_pool = get_forester_token_pool_pda(&forester_pubkey); + let epoch_pda = get_epoch_pda_address(epoch); + let accounts = crate::accounts::ClaimForesterInstruction { + fee_payer: forester_pubkey, + authority: forester_pubkey, + cpi_authority: cpi_authority_pda, + token_cpi_authority_pda: standard_accounts.token_cpi_authority_pda, + compressed_token_program: standard_accounts.compressed_token_program, + light_system_program: standard_accounts.light_system_program, + registered_program_pda: standard_accounts.registered_program_pda, + noop_program: standard_accounts.noop_program, + account_compression_authority: standard_accounts.account_compression_authority, + account_compression_program: standard_accounts.account_compression_program, + system_program: standard_accounts.system_program, + invoking_program: standard_registry_accounts.self_program, + self_program: standard_registry_accounts.self_program, + forester_token_pool, + forester_epoch_pda, + forester_pda, + spl_token_program: anchor_spl::token::ID, + epoch_pda, + mint: MINT, + output_merkle_tree, + compression_token_pool: get_token_pool_pda(&MINT), }; Instruction { program_id: crate::ID, @@ -349,38 +661,149 @@ pub fn create_initialize_address_merkle_tree_and_queue_instruction( } } -pub fn create_initialize_merkle_tree_instruction( - payer: Pubkey, - merkle_tree_pubkey: Pubkey, - nullifier_queue_pubkey: Pubkey, - state_merkle_tree_config: StateMerkleTreeConfig, - nullifier_queue_config: NullifierQueueConfig, - program_owner: Option, - index: u64, - additional_rent: u64, +#[derive(Debug, Clone)] +pub struct CreateSyncDelegateInstructionInputs { + pub sender: Pubkey, + pub cpi_context_account: Pubkey, + pub salt: u64, + pub delegate_account: DelegateAccountWithContext, + // pub input_compressed_accounts: Vec, + pub input_escrow_token_account: Option<(TokenData, CompressedAccountWithMerkleContext)>, + pub output_token_account_merkle_tree: Pubkey, + // pub change_compressed_account_merkle_tree: Pubkey, + pub output_delegate_compressed_account_merkle_tree: Pubkey, + pub proof: CompressedProof, + pub root_indices: Vec, + pub forester_pubkey: Pubkey, + pub previous_hash: [u8; 32], + pub compressed_forester_epoch_pdas: Vec, + pub sync_delegate_token_account: bool, + pub last_account_merkle_context: MerkleContext, + pub last_account_root_index: u16, +} + +pub fn create_sync_delegate_instruction( + inputs: CreateSyncDelegateInstructionInputs, ) -> Instruction { - let register_program_pda = get_registered_program_pda(&crate::ID); - let (cpi_authority, bump) = crate::sdk::get_cpi_authority_pda(); - - let instruction_data = crate::instruction::InitializeStateMerkleTree { - bump, - index, - program_owner, - merkle_tree_config: state_merkle_tree_config, - queue_config: nullifier_queue_config, - additional_rent, + let mut remaining_accounts = HashMap::new(); + + let output_merkle_tree_index = get_index_and_add_to_remaining_accounts( + &mut remaining_accounts, + &inputs.output_delegate_compressed_account_merkle_tree, + ) as u8; + let delegate_account = { + let delegate_account = inputs.delegate_account; + let packed_merkle_context = + pack_merkle_context(&[delegate_account.merkle_context], &mut remaining_accounts); + DelegateAccountWithPackedContext { + delegate_account: delegate_account.delegate_account.into(), + merkle_context: packed_merkle_context[0], + root_index: inputs.root_indices[0], + output_merkle_tree_index, + } }; - let accounts = crate::accounts::InitializeAddressMerkleTreeAndQueue { - authority: payer, - registered_program_pda: register_program_pda, - merkle_tree: merkle_tree_pubkey, - queue: nullifier_queue_pubkey, - cpi_authority, - account_compression_program: account_compression::ID, + let last_account_merkle_context = pack_merkle_context( + &[inputs.last_account_merkle_context], + &mut remaining_accounts, + )[0]; + + let sync_delegate_token_account = SyncDelegateTokenAccount { + salt: inputs.salt, + cpi_context: CompressedCpiContext { + set_context: false, + first_set_context: true, + cpi_context_account_index: get_index_and_add_to_remaining_accounts( + &mut remaining_accounts, + &inputs.cpi_context_account, + ) as u8, + }, }; + + let ( + escrow_token_authority, + cpi_context_account, + compressed_token_program, + forester_token_pool, + token_cpi_authority_pda, + sync_delegate_token_account, + input_escrow_token_account, + spl_token_pool, + spl_token_program, + output_token_account_merkle_tree_index, + ) = if let Some((token_data, compressed_account)) = inputs.input_escrow_token_account { + let mut index = remaining_accounts.len(); + let mut input_token_data_with_context: Vec = Vec::new(); + create_input_token_accounts( + &[token_data], + &mut remaining_accounts, + &[compressed_account], + &mut index, + &[inputs.root_indices[1]], + &mut input_token_data_with_context, + ); + let output_token_account_merkle_tree_index = get_index_and_add_to_remaining_accounts( + &mut remaining_accounts, + &inputs.output_token_account_merkle_tree, + ) as u8; + let standard_accounts = get_standard_compressed_token_program_accounts(MINT); + ( + Some(get_escrow_token_authority(&inputs.sender, inputs.salt).0), + Some(inputs.cpi_context_account), + Some(standard_accounts.compressed_token_program), + Some(get_forester_token_pool_pda(&inputs.forester_pubkey)), + Some(standard_accounts.token_cpi_authority_pda), + Some(sync_delegate_token_account), + Some(input_token_data_with_context[0].clone()), + Some(get_token_pool_pda(&MINT)), + Some(anchor_spl::token::ID), + output_token_account_merkle_tree_index, + ) + } else { + (None, None, None, None, None, None, None, None, None, 0) + }; + let forester_pda_pubkey = get_forester_pda_address(&inputs.forester_pubkey).0; + let instruction_data = crate::instruction::SyncDelegate { + _salt: inputs.salt, + input_escrow_token_account, + delegate_account, + forester_pda_pubkey, + previous_hash: inputs.previous_hash, + compressed_forester_epoch_pdas: inputs.compressed_forester_epoch_pdas, + last_account_merkle_context, + last_account_root_index: inputs.last_account_root_index, + output_token_account_merkle_tree_index, + inclusion_proof: inputs.proof, + sync_delegate_token_account, + }; + + let standard_accounts = get_standard_compressed_token_program_accounts(MINT); + let (cpi_authority_pda, _) = get_cpi_authority_pda(); + let standard_registry_accounts = get_standard_registry_accounts(); + + let accounts = crate::accounts::SyncDelegateInstruction { + fee_payer: inputs.sender, + authority: inputs.sender, + cpi_authority: cpi_authority_pda, + light_system_program: standard_accounts.light_system_program, + registered_program_pda: standard_accounts.registered_program_pda, + noop_program: standard_accounts.noop_program, + account_compression_authority: standard_accounts.account_compression_authority, + account_compression_program: standard_accounts.account_compression_program, + system_program: standard_accounts.system_program, + self_program: standard_registry_accounts.self_program, + protocol_config: standard_registry_accounts.protocol_config_pda, + escrow_token_authority, + cpi_context_account, + token_cpi_authority_pda, + compressed_token_program, + forester_token_pool, + spl_token_pool, + spl_token_program, + }; + let remaining_accounts = to_account_metas(remaining_accounts); Instruction { program_id: crate::ID, - accounts: accounts.to_account_metas(Some(true)), + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), data: instruction_data.data(), } } diff --git a/programs/registry/src/utils.rs b/programs/registry/src/utils.rs new file mode 100644 index 0000000000..45b3c4e536 --- /dev/null +++ b/programs/registry/src/utils.rs @@ -0,0 +1,48 @@ +use account_compression::utils::constants::GROUP_AUTHORITY_SEED; +use anchor_lang::solana_program::pubkey::Pubkey; + +use crate::{ + AUTHORITY_PDA_SEED, EPOCH_SEED, FORESTER_EPOCH_SEED, FORESTER_SEED, FORESTER_TOKEN_POOL_SEED, +}; + +pub fn get_group_pda(seed: Pubkey) -> Pubkey { + Pubkey::find_program_address( + &[GROUP_AUTHORITY_SEED, seed.to_bytes().as_slice()], + &account_compression::ID, + ) + .0 +} + +pub fn get_protocol_config_pda_address() -> (Pubkey, u8) { + Pubkey::find_program_address(&[AUTHORITY_PDA_SEED], &crate::ID) +} + +pub fn get_cpi_authority_pda() -> (Pubkey, u8) { + Pubkey::find_program_address(&[crate::CPI_AUTHORITY_PDA_SEED], &crate::ID) +} + +pub fn get_forester_epoch_pda_address(forester_pda_address: &Pubkey, epoch: u64) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[ + FORESTER_EPOCH_SEED, + forester_pda_address.to_bytes().as_slice(), + epoch.to_le_bytes().as_slice(), + ], + &crate::ID, + ) +} + +pub fn get_forester_pda_address(authority: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[FORESTER_SEED, authority.to_bytes().as_slice()], + &crate::ID, + ) +} + +pub fn get_epoch_pda_address(epoch: u64) -> Pubkey { + Pubkey::find_program_address(&[EPOCH_SEED, epoch.to_le_bytes().as_slice()], &crate::ID).0 +} + +pub fn get_forester_token_pool_pda(authority: &Pubkey) -> Pubkey { + Pubkey::find_program_address(&[FORESTER_TOKEN_POOL_SEED, authority.as_ref()], &crate::ID).0 +} diff --git a/programs/system/src/invoke_cpi/process_cpi_context.rs b/programs/system/src/invoke_cpi/process_cpi_context.rs index 836d812716..f0b22e20d7 100644 --- a/programs/system/src/invoke_cpi/process_cpi_context.rs +++ b/programs/system/src/invoke_cpi/process_cpi_context.rs @@ -75,6 +75,7 @@ pub fn process_cpi_context<'info>( msg!("cpi context account : {:?}", cpi_context_account); msg!("fee payer : {:?}", fee_payer); msg!("cpi context : {:?}", cpi_context); + msg!("Either fee payer mismatch or first set context is true but set context is false"); return err!(SystemProgramError::CpiContextFeePayerMismatch); } inputs.combine(&cpi_context_account.context); @@ -114,6 +115,8 @@ pub fn set_cpi_context( cpi_context_account.context.push(inputs); } else { msg!(" {} != {}", fee_payer, cpi_context_account.fee_payer); + msg!("cpi context account : {:?}", cpi_context_account); + msg!("cpi context : {:?}", inputs.cpi_context); return err!(SystemProgramError::CpiContextFeePayerMismatch); } Ok(()) diff --git a/test-programs/account-compression-test/tests/address_merkle_tree_tests.rs b/test-programs/account-compression-test/tests/address_merkle_tree_tests.rs index dff37e115c..dfe58fb50b 100644 --- a/test-programs/account-compression-test/tests/address_merkle_tree_tests.rs +++ b/test-programs/account-compression-test/tests/address_merkle_tree_tests.rs @@ -85,9 +85,15 @@ async fn address_queue_and_tree_functional( assert!(address_queue.contains(&address2, None).unwrap()); // CHECK: 3 inserts two addresses into the address Merkle tree - empty_address_queue_test(&payer, &mut context, &mut address_merkle_tree_bundle, true) - .await - .unwrap(); + empty_address_queue_test( + &payer, + &mut context, + &mut address_merkle_tree_bundle, + true, + 0, + ) + .await + .unwrap(); let address3 = 20_u32.to_biguint().unwrap(); let addresses: Vec<[u8; 32]> = vec![bigint_to_be_bytes_array(&address3).unwrap()]; @@ -108,9 +114,15 @@ async fn address_queue_and_tree_functional( .unwrap() .unwrap(); // CHECK: 4 insert third address which is inbetween the first two addresses - empty_address_queue_test(&payer, &mut context, &mut address_merkle_tree_bundle, true) - .await - .unwrap(); + empty_address_queue_test( + &payer, + &mut context, + &mut address_merkle_tree_bundle, + true, + 0, + ) + .await + .unwrap(); } #[tokio::test] @@ -545,9 +557,15 @@ async fn update_address_merkle_tree_failing_tests( ) .await .unwrap(); - empty_address_queue_test(&payer, &mut context, &mut address_merkle_tree_bundle, true) - .await - .unwrap(); + empty_address_queue_test( + &payer, + &mut context, + &mut address_merkle_tree_bundle, + true, + 0, + ) + .await + .unwrap(); // CHECK: 1 cannot insert the same address twice let result = insert_addresses( &mut context, @@ -643,6 +661,7 @@ async fn update_address_merkle_tree_failing_tests( None, None, true, + 0, ) .await .unwrap_err(); @@ -663,6 +682,7 @@ async fn update_address_merkle_tree_failing_tests( None, None, true, + 0, ) .await .unwrap_err(); @@ -682,6 +702,7 @@ async fn update_address_merkle_tree_failing_tests( None, None, true, + 0, ) .await; assert_rpc_error( @@ -707,6 +728,7 @@ async fn update_address_merkle_tree_failing_tests( None, None, true, + 0, ) .await; assert_rpc_error( @@ -732,6 +754,7 @@ async fn update_address_merkle_tree_failing_tests( None, None, true, + 0, ) .await; assert_rpc_error( @@ -757,6 +780,7 @@ async fn update_address_merkle_tree_failing_tests( None, None, true, + 0, ) .await; assert_rpc_error( @@ -783,6 +807,7 @@ async fn update_address_merkle_tree_failing_tests( None, None, true, + 0, ) .await; assert_rpc_error( @@ -820,6 +845,7 @@ async fn update_address_merkle_tree_failing_tests( Some(invalid_changelog_index_low as u16), None, true, + 0, ) .await; assert_rpc_error( @@ -845,6 +871,7 @@ async fn update_address_merkle_tree_failing_tests( Some(invalid_changelog_index_high as u16), None, true, + 0, ) .await; assert_rpc_error( @@ -874,6 +901,7 @@ async fn update_address_merkle_tree_failing_tests( None, Some(invalid_indexed_changelog_index_high as u16), true, + 0, ) .await; assert_rpc_error( @@ -900,6 +928,7 @@ async fn update_address_merkle_tree_failing_tests( None, None, true, + 0, ) .await; assert_rpc_error( @@ -926,6 +955,7 @@ async fn update_address_merkle_tree_failing_tests( Some(changelog_index as u16), Some(indexed_changelog_index as u16), true, + 0, ) .await; assert_rpc_error( @@ -966,6 +996,7 @@ async fn update_address_merkle_tree_failing_tests( Some(changelog_index as u16), None, true, + 0, ) .await; assert_rpc_error( @@ -1031,9 +1062,15 @@ async fn update_address_merkle_tree_wrap_around( ) .await .unwrap(); - empty_address_queue_test(&payer, &mut context, &mut address_merkle_tree_bundle, true) - .await - .unwrap(); + empty_address_queue_test( + &payer, + &mut context, + &mut address_merkle_tree_bundle, + true, + 0, + ) + .await + .unwrap(); } insert_addresses( @@ -1068,6 +1105,7 @@ async fn update_address_merkle_tree_wrap_around( None, None, true, + 0, ) .await; assert_rpc_error( @@ -1459,6 +1497,7 @@ pub async fn test_with_invalid_low_element( None, None, true, + 0, ) .await; assert_rpc_error(error_invalid_low_element, 0, expected_error).unwrap(); diff --git a/test-programs/compressed-token-test/tests/test.rs b/test-programs/compressed-token-test/tests/test.rs index 5faed2a735..4d5d68a223 100644 --- a/test-programs/compressed-token-test/tests/test.rs +++ b/test-programs/compressed-token-test/tests/test.rs @@ -1086,17 +1086,14 @@ async fn test_approve_failing() { let inputs = CreateApproveInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: sender.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint, @@ -1133,17 +1130,14 @@ async fn test_approve_failing() { let inputs = CreateApproveInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: sender.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint, @@ -1184,17 +1178,14 @@ async fn test_approve_failing() { let inputs = CreateApproveInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: sender.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint, @@ -1224,17 +1215,14 @@ async fn test_approve_failing() { let inputs = CreateApproveInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: sender.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint: invalid_mint.pubkey(), @@ -1267,17 +1255,14 @@ async fn test_approve_failing() { let inputs = CreateApproveInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: sender.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint, @@ -1496,17 +1481,14 @@ async fn test_revoke_failing() { let inputs = CreateRevokeInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: sender.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint, @@ -1532,17 +1514,14 @@ async fn test_revoke_failing() { let inputs = CreateRevokeInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: sender.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint, @@ -1577,17 +1556,14 @@ async fn test_revoke_failing() { let inputs = CreateRevokeInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: sender.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint: invalid_mint.pubkey(), @@ -2175,17 +2151,14 @@ async fn test_failing_freeze() { let inputs = CreateInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: invalid_authority.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), outputs_merkle_tree, @@ -2214,17 +2187,14 @@ async fn test_failing_freeze() { let inputs = CreateInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: payer.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), outputs_merkle_tree: invalid_merkle_tree.pubkey(), @@ -2255,17 +2225,14 @@ async fn test_failing_freeze() { let inputs = CreateInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: payer.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), outputs_merkle_tree, @@ -2321,17 +2288,14 @@ async fn test_failing_freeze() { let inputs = CreateInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: payer.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), outputs_merkle_tree, @@ -2437,17 +2401,14 @@ async fn test_failing_thaw() { let inputs = CreateInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: invalid_authority.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), outputs_merkle_tree, @@ -2476,17 +2437,14 @@ async fn test_failing_thaw() { let inputs = CreateInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: payer.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), outputs_merkle_tree: invalid_merkle_tree.pubkey(), @@ -2517,17 +2475,14 @@ async fn test_failing_thaw() { let inputs = CreateInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: payer.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), outputs_merkle_tree, @@ -2574,17 +2529,14 @@ async fn test_failing_thaw() { let inputs = CreateInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: payer.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), + input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), outputs_merkle_tree, @@ -2880,10 +2832,6 @@ pub async fn failing_compress_decompress( let instruction = create_transfer_instruction( &rpc.get_payer().pubkey(), &payer.pubkey(), - &input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect::>(), &[change_out_compressed_account], &root_indices, &proof, @@ -2894,7 +2842,7 @@ pub async fn failing_compress_decompress( .as_slice(), &input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), *mint, @@ -3318,18 +3266,19 @@ async fn perform_transfer_failing_test( } else { input_compressed_account_token_data[0].mint }; + // WARNING THIS MIGHT BE WRONG DOUBLE CHECK + let mut input_compressed_accounts = input_compressed_accounts.to_vec(); + input_compressed_accounts.iter_mut().for_each(|x| { + x.merkle_context = MerkleContext { + merkle_tree_pubkey: *merkle_tree_pubkey, + nullifier_queue_pubkey: *nullifier_queue_pubkey, + queue_index: None, + leaf_index: x.merkle_context.leaf_index, + }; + }); let instruction = create_transfer_instruction( &payer.pubkey(), &payer.pubkey(), - &input_compressed_accounts - .iter() - .map(|x| MerkleContext { - merkle_tree_pubkey: *merkle_tree_pubkey, - nullifier_queue_pubkey: *nullifier_queue_pubkey, - leaf_index: x.merkle_context.leaf_index, - queue_index: None, - }) - .collect::>(), &[ change_token_transfer_output, transfer_recipient_token_transfer_output, @@ -3337,11 +3286,7 @@ async fn perform_transfer_failing_test( root_indices, proof, input_compressed_account_token_data.as_slice(), - &input_compressed_accounts - .iter() - .map(|x| &x.compressed_account) - .cloned() - .collect::>(), + &input_compressed_accounts, mint, None, false, diff --git a/test-programs/e2e-test/Cargo.toml b/test-programs/e2e-test/Cargo.toml index bbfd0d297a..c8623c7da3 100644 --- a/test-programs/e2e-test/Cargo.toml +++ b/test-programs/e2e-test/Cargo.toml @@ -20,6 +20,7 @@ default = ["custom-heap"] [dependencies] anchor-lang = { workspace = true } light-compressed-token = { path = "../../programs/compressed-token" , features = ["cpi"]} +light-registry = { path = "../../programs/registry" , features = ["cpi"]} light-system-program = { path = "../../programs/system" , features = ["cpi"]} account-compression = { path = "../../programs/account-compression" , features = ["cpi"] } light-hasher = {path = "../../merkle-tree/hasher"} diff --git a/test-programs/e2e-test/tests/test.rs b/test-programs/e2e-test/tests/test.rs index 83e2b3f943..60e9656004 100644 --- a/test-programs/e2e-test/tests/test.rs +++ b/test-programs/e2e-test/tests/test.rs @@ -1,41 +1,72 @@ #![cfg(feature = "test-sbf")] +use light_registry::protocol_config::state::ProtocolConfig; use light_test_utils::e2e_test_env::{E2ETestEnv, GeneralActionConfig, KeypairActionConfig}; use light_test_utils::indexer::TestIndexer; use light_test_utils::rpc::ProgramTestRpcConnection; -use light_test_utils::test_env::setup_test_programs_with_accounts; +use light_test_utils::test_env::{ + set_env_with_delegate_and_forester, setup_test_programs_with_accounts_with_protocol_config, +}; #[tokio::test] async fn test_10_all() { - let (rpc, env_accounts) = setup_test_programs_with_accounts(None).await; + let protocol_config = ProtocolConfig { + genesis_slot: 0, + slot_length: 100, + registration_phase_length: 100, + active_phase_length: 200, + report_work_phase_length: 100, + ..ProtocolConfig::default() + }; + // let (rpc, env_accounts) = + // setup_test_programs_with_accounts_with_protocol_config(None, protocol_config, true).await; - let indexer: TestIndexer = TestIndexer::init_from_env( - &env_accounts.forester.insecure_clone(), - &env_accounts, - KeypairActionConfig::all_default().inclusion(), - KeypairActionConfig::all_default().non_inclusion(), - ) - .await; - - let mut env = - E2ETestEnv::>::new( - rpc, - indexer, - &env_accounts, - KeypairActionConfig::all_default(), - GeneralActionConfig::default(), + // let indexer: TestIndexer = TestIndexer::init_from_env( + // &env_accounts.forester.insecure_clone(), + // &env_accounts, + // KeypairActionConfig::all_default().inclusion(), + // KeypairActionConfig::all_default().non_inclusion(), + // ) + // .await; + let (mut e2e_env, _delegate_keypair, _env, _tree_accounts, _registered_epoch) = + set_env_with_delegate_and_forester( + None, + Some(KeypairActionConfig::all_default()), + Some(GeneralActionConfig::default()), 10, None, ) .await; - env.execute_rounds().await; + + // let mut env = + // E2ETestEnv::>::new( + // rpc, + // indexer, + // &env_accounts, + // KeypairActionConfig::all_default(), + // GeneralActionConfig::default(), + // 10, + // None, + // ) + // .await; + e2e_env.execute_rounds().await; + println!("stats {:?}", e2e_env.stats); } // cargo test-sbf -p e2e-test -- --nocapture --ignored --test test_10000_all > output.txt 2>&1 #[ignore] #[tokio::test] async fn test_10000_all() { - let (rpc, env_accounts) = setup_test_programs_with_accounts(None).await; + let protocol_config = ProtocolConfig { + genesis_slot: 0, + slot_length: 10, + registration_phase_length: 100, + active_phase_length: 200, + report_work_phase_length: 100, + ..ProtocolConfig::default() + }; + let (rpc, env_accounts) = + setup_test_programs_with_accounts_with_protocol_config(None, protocol_config, true).await; let indexer: TestIndexer = TestIndexer::init_from_env( &env_accounts.forester.insecure_clone(), diff --git a/test-programs/registry-test/Cargo.toml b/test-programs/registry-test/Cargo.toml index c1d0f03930..90c687c35f 100644 --- a/test-programs/registry-test/Cargo.toml +++ b/test-programs/registry-test/Cargo.toml @@ -20,6 +20,7 @@ default = ["custom-heap"] [dependencies] + [dev-dependencies] solana-program-test = { workspace = true } light-test-utils = { version = "0.2.1", path = "../../test-utils" } @@ -43,3 +44,4 @@ light-verifier = {path = "../../circuit-lib/verifier"} solana-cli-output = { workspace = true } serde_json = "1.0.114" solana-sdk = { workspace = true } +rand = "0.8.5" \ No newline at end of file diff --git a/test-programs/registry-test/tests/tests.rs b/test-programs/registry-test/tests/tests.rs index 3251bebb61..fbde6b9e3a 100644 --- a/test-programs/registry-test/tests/tests.rs +++ b/test-programs/registry-test/tests/tests.rs @@ -1,20 +1,49 @@ #![cfg(feature = "test-sbf")] use anchor_lang::{InstructionData, ToAccountMetas}; -use light_registry::{ - get_forester_epoch_pda_address, - sdk::{ - create_nullify_instruction, create_update_address_merkle_tree_instruction, - get_governance_authority_pda, CreateNullifyInstructionInputs, - UpdateAddressMerkleTreeInstructionInputs, - }, - ForesterEpoch, LightGovernanceAuthority, RegistryError, +use light_registry::account_compression_cpi::sdk::{ + create_nullify_instruction, create_update_address_merkle_tree_instruction, + CreateNullifyInstructionInputs, UpdateAddressMerkleTreeInstructionInputs, +}; +use light_registry::delegate::get_escrow_token_authority; +use light_registry::delegate::state::DelegateAccount; +use light_registry::epoch::claim_forester::CompressedForesterEpochAccount; +use light_registry::errors::RegistryError; +use light_registry::protocol_config::state::{ProtocolConfig, ProtocolConfigPda}; +use light_registry::sdk::{ + create_finalize_registration_instruction, create_report_work_instruction, +}; +use light_registry::utils::{ + get_forester_epoch_pda_address, get_forester_pda_address, get_forester_token_pool_pda, + get_protocol_config_pda_address, +}; +use light_registry::{ForesterAccount, ForesterConfig, ForesterEpochPda, MINT}; +use light_test_utils::assert_epoch::{ + assert_epoch_pda, assert_finalized_epoch_registration, assert_registered_forester_pda, + assert_report_work, fetch_epoch_and_forester_pdas, +}; +use light_test_utils::e2e_test_env::{init_program_test_env, TestForester}; +use light_test_utils::forester_epoch::{get_epoch_phases, Epoch, Forester, TreeAccounts, TreeType}; +use light_test_utils::indexer::{Indexer, TestIndexer}; + +use light_test_utils::registry::{ + delegate_test, deposit_test, forester_claim_test, mint_standard_tokens, sync_delegate_test, + undelegate_test, withdraw_test, DelegateInputs, DepositInputs, SyncDelegateInputs, + UndelegateInputs, WithdrawInputs, +}; +use light_test_utils::rpc::solana_rpc::SolanaRpcUrl; +use light_test_utils::rpc::ProgramTestRpcConnection; +use light_test_utils::test_env::{ + create_delegate, deposit_to_delegate_account_helper, set_env_with_delegate_and_forester, + setup_accounts_devnet, setup_test_programs_with_accounts_with_protocol_config, EnvAccounts, + STANDARD_TOKEN_MINT_KEYPAIR, }; +use light_test_utils::test_forester::{empty_address_queue_test, nullify_compressed_accounts}; +use light_test_utils::{get_custom_compressed_account, spl}; use light_test_utils::{ registry::{ create_rollover_address_merkle_tree_instructions, create_rollover_state_merkle_tree_instructions, register_test_forester, - update_test_forester, }, rpc::{errors::assert_rpc_error, rpc_connection::RpcConnection, SolanaRpcConnection}, test_env::{ @@ -22,7 +51,8 @@ use light_test_utils::{ setup_test_programs_with_accounts, }, }; -use light_test_utils::{rpc::solana_rpc::SolanaRpcUrl, test_env::setup_accounts_devnet}; +use rand::Rng; +use solana_sdk::program_pack::Pack; use solana_sdk::{ instruction::Instruction, native_token::LAMPORTS_PER_SOL, @@ -30,7 +60,6 @@ use solana_sdk::{ signature::{read_keypair_file, Keypair}, signer::Signer, }; -use std::str::FromStr; #[tokio::test] async fn test_register_program() { @@ -46,35 +75,1069 @@ async fn test_register_program() { .unwrap(); } +/// Functional tests: +/// 1. create delegate account and deposit +/// 2. deposit into existing account +/// 3. withdrawal +#[tokio::test] +async fn test_deposit() { + let token_mint_keypair = Keypair::from_bytes(STANDARD_TOKEN_MINT_KEYPAIR.as_slice()).unwrap(); + + let protocol_config = ProtocolConfig { + mint: token_mint_keypair.pubkey(), + ..Default::default() + }; + let (mut rpc, env) = + setup_test_programs_with_accounts_with_protocol_config(None, protocol_config, false).await; + let delegate_keypair = Keypair::new(); + rpc.airdrop_lamports(&delegate_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let mut e2e_env = init_program_test_env(rpc, &env).await; + + mint_standard_tokens::>( + &mut e2e_env.rpc, + &mut e2e_env.indexer, + &env.governance_authority, + &delegate_keypair.pubkey(), + 1_000_000_000, + &env.merkle_tree_pubkey, + ) + .await + .unwrap(); + // 1. Functional create account and deposit + { + let token_accounts = e2e_env + .indexer + .get_compressed_token_accounts_by_owner(&delegate_keypair.pubkey()); + let deposit_amount = 1_000_000; + let escrow_pda_authority = get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0; + // approve amount is expected to equal deposit amount + spl::approve_test( + &delegate_keypair, + &mut e2e_env.rpc, + &mut e2e_env.indexer, + token_accounts, + deposit_amount, + None, + &escrow_pda_authority, + &env.merkle_tree_pubkey, + &env.merkle_tree_pubkey, + None, + ) + .await; + let token_accounts = e2e_env + .indexer + .get_compressed_token_accounts_by_owner(&delegate_keypair.pubkey()) + .iter() + .filter(|a| a.token_data.delegate.is_some()) + .cloned() + .collect::>(); + + let deposit_inputs = DepositInputs { + sender: &delegate_keypair, + amount: deposit_amount, + delegate_account: None, + input_token_data: token_accounts, + input_escrow_token_account: None, + epoch: 0, + }; + deposit_test(&mut e2e_env.rpc, &mut e2e_env.indexer, deposit_inputs) + .await + .unwrap(); + } + // 2. Functional deposit into existing account + { + println!("\n\n fetching accounts for 2nd deposit \n\n"); + let escrow_pda_authority = get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0; + + let escrow_token_accounts = e2e_env + .indexer + .get_compressed_token_accounts_by_owner(&escrow_pda_authority); + + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + + let deposit_amount = 1_000_000; + let token_accounts = e2e_env + .indexer + .get_compressed_token_accounts_by_owner(&delegate_keypair.pubkey()); + // approve amount is expected to equal deposit amount + spl::approve_test( + &delegate_keypair, + &mut e2e_env.rpc, + &mut e2e_env.indexer, + token_accounts, + deposit_amount, + None, + &escrow_pda_authority, + &env.merkle_tree_pubkey, + &env.merkle_tree_pubkey, + None, + ) + .await; + let token_accounts = e2e_env + .indexer + .get_compressed_token_accounts_by_owner(&delegate_keypair.pubkey()) + .iter() + .filter(|a| a.token_data.delegate.is_some()) + .cloned() + .collect::>(); + let deposit_inputs = DepositInputs { + sender: &delegate_keypair, + amount: deposit_amount, + delegate_account: delegate_account[0].clone(), + input_token_data: token_accounts, + input_escrow_token_account: Some(escrow_token_accounts[0].clone()), + epoch: 0, + }; + deposit_test(&mut e2e_env.rpc, &mut e2e_env.indexer, deposit_inputs) + .await + .unwrap(); + } + // 3. Functional withdrawal + { + println!("\n\n fetching accounts for withdrawal \n\n"); + let escrow_pda_authority = get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0; + + let escrow_token_accounts = e2e_env + .indexer + .get_compressed_token_accounts_by_owner(&escrow_pda_authority); + + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + let inputs = WithdrawInputs { + sender: &delegate_keypair, + amount: 99999, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + input_escrow_token_account: escrow_token_accounts[0].clone(), + }; + withdraw_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) + .await + .unwrap(); + } +} + +#[tokio::test] +async fn test_delegate() { + let token_mint_keypair = Keypair::from_bytes(STANDARD_TOKEN_MINT_KEYPAIR.as_slice()).unwrap(); + + let protocol_config = ProtocolConfig { + mint: token_mint_keypair.pubkey(), + ..Default::default() + }; + let (mut rpc, env) = + setup_test_programs_with_accounts_with_protocol_config(None, protocol_config, true).await; + let delegate_keypair = Keypair::new(); + rpc.airdrop_lamports(&delegate_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let mut e2e_env = init_program_test_env(rpc, &env).await; + + mint_standard_tokens::>( + &mut e2e_env.rpc, + &mut e2e_env.indexer, + &env.governance_authority, + &delegate_keypair.pubkey(), + 1_000_000_000, + &env.merkle_tree_pubkey, + ) + .await + .unwrap(); + let forester_pda = env.registered_forester_pda; + let deposit_amount = 1_000_000; + deposit_to_delegate_account_helper( + &mut e2e_env, + &delegate_keypair, + deposit_amount, + &env, + 0, + None, + None, + ) + .await; + // delegate to forester + { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + let inputs = DelegateInputs { + sender: &delegate_keypair, + amount: deposit_amount, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + forester_pda, + no_sync: false, + output_merkle_tree: env.merkle_tree_pubkey, + }; + delegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) + .await + .unwrap(); + } + let current_slot = e2e_env.rpc.get_slot().await.unwrap(); + e2e_env + .rpc + .warp_to_slot(current_slot + protocol_config.active_phase_length) + .unwrap(); + // undelegate from forester + { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + let inputs = UndelegateInputs { + sender: &delegate_keypair, + amount: deposit_amount - 1, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + forester_pda, + no_sync: false, + output_merkle_tree: env.merkle_tree_pubkey, + }; + undelegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) + .await + .unwrap(); + } + // undelegate from forester + { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + let inputs = UndelegateInputs { + sender: &delegate_keypair, + amount: 1, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + forester_pda, + no_sync: false, + output_merkle_tree: env.merkle_tree_pubkey, + }; + undelegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) + .await + .unwrap(); + } +} + +use anchor_lang::AccountDeserialize; +use rand::SeedableRng; + +// TODO: (doesn't work) add multiple foresters with their own delegates (do it here) +// - have a vector of foresters and their delegates -> this way we can also easily skip epochs +// TODO: (done) have foresters skip epochs +// TODO: make sure that delegates can always undelegate and withdraw regardless of forester actions +// TODO: add check for empty accounts sync delegate +// TODO: add readonly account and use it +// TODO: add inflation curve +// TODO: add timelocked stake accounts +// TODO: add a test where the stake percentage of one delegate stays constant while others change so that we can assert the rewards +#[tokio::test] +async fn test_e2e() { + let (mut e2e_env, delegate_keypair, env, tree_accounts, registered_epoch) = + set_env_with_delegate_and_forester(None, None, None, 0, None).await; + let mut previous_hash = [0u8; 32]; + // let current_epoch = registered_epoch.clone(); + let mut phases = registered_epoch.phases.clone(); + let num_epochs = 40; + let mut completed_epochs = 0; + let add_delegates = true; + let pre_mint_account = e2e_env.rpc.get_account(MINT).await.unwrap().unwrap(); + let pre_token_balance = spl_token::state::Mint::unpack(&pre_mint_account.data) + .unwrap() + .supply; + let mut rng = rand::rngs::ThreadRng::default(); + let seed = rng.gen::(); + let mut rng_from_seed = rand::rngs::StdRng::seed_from_u64(seed); + // let mut counter = 0; + let forester_keypair = env.forester.insecure_clone(); + let delegates = vec![(delegate_keypair, 0)]; + let mut epoch = registered_epoch.epoch; + + // Forester keypair, delegates, active_epoch, registered_epoch + let mut foresters: Vec<(Keypair, Vec<(Keypair, i32)>, Option, Option)> = + vec![(forester_keypair, delegates, Some(registered_epoch), None)]; + // adding a second forester with stake it will not be registered until next epoch + // env fails with account not found when registering the second forester + // { + // println!("adding second forester"); + // println!("epoch {}", epoch); + // let forester = TestForester { + // keypair: Keypair::new(), + // forester: Forester::default(), + // is_registered: None, + // }; + // let forester_config = ForesterConfig { + // fee: rng_from_seed.gen_range(0..=100), + // fee_recipient: forester.keypair.pubkey(), + // }; + // register_test_forester( + // &mut e2e_env.rpc, + // &env.governance_authority, + // &forester.keypair, + // forester_config, + // ) + // .await + // .unwrap(); + + // let forester_pda = get_forester_pda_address(&forester.keypair.pubkey()).0; + // // TODO: investigate why + 1 + // let delgate_keypair = + // create_delegate(&mut e2e_env, &env, 1_000_000, forester_pda, epoch + 1, None).await; + // foresters.push((forester.keypair, vec![(delgate_keypair, 0)], None, None)); + // } + + // let pre_forester_two_balance = e2e_env + // .indexer + // .get_compressed_token_balance(&foresters[1].0.pubkey(), &MINT); + let mut num_mint_tos = 0; + for i in 1..=num_epochs { + // Prints + { + println!( + "-------------------------------\n\n epoch: {} \n\n -------------------------------", + i + ); + let registered_forester_is_some = foresters.iter().any(|f| f.2.is_some()); + if !registered_forester_is_some { + println!("no registered forester skipping epoch"); + // continue; + } else { + completed_epochs += 1; + } + + let current_mint_account = e2e_env.rpc.get_account(MINT).await.unwrap().unwrap(); + let current_token_balance = + spl_token::state::Mint::unpack(¤t_mint_account.data.as_slice()) + .unwrap() + .supply; + println!("current_token_balance: {}", current_token_balance); + let escrow_authority = get_escrow_token_authority(&foresters[0].1[0].0.pubkey(), 0).0; + let delegate_balance = e2e_env + .indexer + .get_compressed_token_balance(&escrow_authority, &MINT); + println!("delegate_balance: {}", delegate_balance); + let forester_pda_pubkey = get_forester_pda_address(&foresters[0].0.pubkey()).0; + let forester_pda_account = e2e_env.rpc.get_account(forester_pda_pubkey).await.unwrap(); + if let Some(account) = forester_pda_account { + let forester_pda = + ForesterAccount::try_deserialize(&mut account.data.as_slice()).unwrap(); + println!("forester_pda: {:?}", forester_pda); + } + let forester_epoch_pda_pubkey = + get_forester_epoch_pda_address(&foresters[0].0.pubkey(), epoch).0; + + let forester_epoch_pda = e2e_env + .rpc + .get_account(forester_epoch_pda_pubkey) + .await + .unwrap(); + if let Some(account) = forester_epoch_pda { + let forester_epoch_pda = + ForesterEpochPda::try_deserialize(&mut account.data.as_slice()).unwrap(); + println!("forester_epoch_pda: {:?}", forester_epoch_pda); + } + let forester_token_pool = get_forester_token_pool_pda(&foresters[0].0.pubkey()); + let forester_token_pool_account = + e2e_env.rpc.get_account(forester_token_pool).await.unwrap(); + if let Some(account) = forester_token_pool_account { + let forester_token_pool_balance = + spl_token::state::Account::unpack(&account.data.as_slice()) + .unwrap() + .amount; + println!( + "forester_token_pool_balance: {}", + forester_token_pool_balance + ); + } + + println!("\n\n\n"); + } + for (forester_keypair, _, epoch, _) in foresters.iter() { + // find next slot + let current_slot = e2e_env.rpc.get_slot().await.unwrap(); + + if let Some(epoch) = epoch { + let treeschedule = epoch + .merkle_trees + .iter() + .find(|t| t.tree_pubkey.tree_type == TreeType::State) + .unwrap(); + let next_eligible_light_slot = treeschedule + .slots + .iter() + .find(|s| s.is_some() && s.as_ref().unwrap().start_solana_slot > current_slot) + .unwrap(); + e2e_env + .rpc + .warp_to_slot(next_eligible_light_slot.as_ref().unwrap().start_solana_slot) + .unwrap(); + // create work 1 item in nullifier queue + perform_work(&mut e2e_env, &forester_keypair, &env, epoch.epoch).await; + } + } + // advance epoch to report work and next registration phase + e2e_env + .rpc + .warp_to_slot(phases.report_work.start - 1) + .unwrap(); + let protocol_config = e2e_env + .rpc + .get_anchor_account::(&env.governance_authority_pda) + .await + .unwrap() + .unwrap() + .config; + + // register for next epoch + for (forester_keypair, _, _, next_epoch) in foresters.iter_mut() { + let register_or_not = rng_from_seed.gen_bool(0.6); + if register_or_not { + let next_registered_epoch = + Epoch::register(&mut e2e_env.rpc, &protocol_config, &forester_keypair) + .await + .unwrap(); + assert!(next_registered_epoch.is_some()); + let next_registered_epoch = next_registered_epoch.unwrap(); + let forester_pda_pubkey = get_forester_pda_address(&forester_keypair.pubkey()).0; + let forester_pda = e2e_env + .rpc + .get_anchor_account::(&forester_pda_pubkey) + .await + .unwrap() + .unwrap(); + let expected_stake = forester_pda.active_stake_weight; + assert_epoch_pda( + &mut e2e_env.rpc, + next_registered_epoch.epoch, + expected_stake, + ) + .await; + assert_registered_forester_pda( + &mut e2e_env.rpc, + &next_registered_epoch.forester_epoch_pda, + &forester_keypair.pubkey(), + next_registered_epoch.epoch, + ) + .await; + *next_epoch = Some(next_registered_epoch); + } else { + *next_epoch = None; + } + } + // // // check that we can still forest the last epoch + // perform_work(&mut e2e_env, &forester_keypair, &env, current_epoch.epoch).await; + + e2e_env.rpc.warp_to_slot(phases.report_work.start).unwrap(); + // report work + for (forester_keypair, _, current_epoch, _) in foresters.iter_mut() { + if let Some(current_epoch) = current_epoch { + let (pre_forester_epoch_pda, pre_epoch_pda) = fetch_epoch_and_forester_pdas( + &mut e2e_env.rpc, + ¤t_epoch.forester_epoch_pda, + ¤t_epoch.epoch_pda, + ) + .await; + let ix = + create_report_work_instruction(&forester_keypair.pubkey(), current_epoch.epoch); + e2e_env + .rpc + .create_and_send_transaction( + &[ix], + &forester_keypair.pubkey(), + &[&forester_keypair], + ) + .await + .unwrap(); + assert_report_work( + &mut e2e_env.rpc, + ¤t_epoch.forester_epoch_pda, + ¤t_epoch.epoch_pda, + pre_forester_epoch_pda, + pre_epoch_pda, + ) + .await; + } + } + for (forester_keypair, _, current_epoch, next_registered_epoch) in foresters.iter_mut() { + if let Some(next_registered_epoch) = next_registered_epoch { + let ix = create_finalize_registration_instruction( + &env.forester.pubkey(), + next_registered_epoch.epoch, + ); + + println!("epoch: {}", epoch); + println!("next_registered_epoch: {:?}", next_registered_epoch.epoch); + e2e_env + .rpc + .create_and_send_transaction(&[ix], &env.forester.pubkey(), &[&env.forester]) + .await + .unwrap(); + next_registered_epoch + .fetch_account_and_add_trees_with_schedule( + &mut e2e_env.rpc, + tree_accounts.clone(), + ) + .await + .unwrap(); + } + if let Some(current_epoch) = current_epoch { + forester_claim_test( + &mut e2e_env.rpc, + &mut e2e_env.indexer, + &forester_keypair, + current_epoch.epoch, + env.merkle_tree_pubkey, + ) + .await + .unwrap(); + } + // switch to next epoch + *current_epoch = next_registered_epoch.clone(); + epoch += 1; + } + let forester_keypair = foresters[0].0.insecure_clone(); + // delegates only delegate to the first forester + for (index, (delegate_keypair, counter)) in foresters[0].1.iter_mut().enumerate() { + let sync = rng_from_seed.gen_bool(0.3); + let sync_tokens = rng_from_seed.gen_bool(0.1); + + if i > 0 && sync || num_epochs - 1 == i || *counter == 4 { + println!("\n\n--------------------------------------\n\n"); + println!("syncing balance of delegate {}", index); + println!("forester index: {}", index); + println!("\n\n--------------------------------------\n\n"); + let forester_pda_pubkey = get_forester_pda_address(&forester_keypair.pubkey()).0; + + let compressed_epoch_pda = + get_custom_compressed_account::<_, _, CompressedForesterEpochAccount>( + &mut e2e_env.indexer, + &forester_pda_pubkey, + &light_registry::ID, + ); + println!("compressed_epoch_pda: {:?}", compressed_epoch_pda); + + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + println!("delegate_account: {:?}", delegate_account); + + let deserialized_delegate = + delegate_account[0].as_ref().unwrap().deserialized_account; + if deserialized_delegate.last_sync_epoch >= epoch { + println!("delegate {} already synced", index); + continue; + } + + let escrow_authority = get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0; + let escrow = e2e_env + .indexer + .get_compressed_token_accounts_by_owner(&escrow_authority); + println!("escrow: {:?}", escrow); + let inputs = SyncDelegateInputs { + sender: &delegate_keypair, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + compressed_forester_epoch_pdas: compressed_epoch_pda, + forester: forester_keypair.pubkey(), + output_merkle_tree: env.merkle_tree_pubkey, + sync_delegate_token_account: sync_tokens, + previous_hash, + input_escrow_token_account: Some(escrow[0].clone()), + }; + sync_delegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) + .await + .unwrap(); + let forester_pda: ForesterAccount = e2e_env + .rpc + .get_anchor_account::(&forester_pda_pubkey) + .await + .unwrap() + .unwrap(); + previous_hash = forester_pda.last_compressed_forester_epoch_pda_hash; + *counter = 0; + // undelegate after syncing + { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + let max_amount = delegate_account[0] + .as_ref() + .unwrap() + .deserialized_account + .delegated_stake_weight; + if max_amount == 0 { + println!("epoch {}", i); + println!( + "delegate {} has no stake -----------------------------------------", + index + ); + println!( + "delegate account {:?}", + delegate_account[0].as_ref().unwrap().deserialized_account + ); + continue; + } + let amount = rng_from_seed.gen_range(1..=max_amount); + println!( + "delegate {} start undelegating {} -----------------------------------------", + index, amount + ); + println!("delegate_account: {:?}", delegate_account); + let inputs = UndelegateInputs { + sender: delegate_keypair, + amount, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + forester_pda: env.registered_forester_pda, + no_sync: false, + output_merkle_tree: env.merkle_tree_pubkey, + }; + undelegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) + .await + .unwrap(); + println!( + "delegate {} undelegated {} -----------------------------------------", + index, amount + ); + } + // // delegate + { + create_delegate( + &mut e2e_env, + &env, + 1_000_000, + env.registered_forester_pda, + epoch, + Some(delegate_keypair.insecure_clone()), + ) + .await; + num_mint_tos += 1; + } + } else { + *counter += 1; + } + } + let num_add_delegates = if add_delegates { + rng_from_seed.gen_range(0..3) + } else { + 0 + }; + for _ in 0..num_add_delegates { + let deposit_amount = rng_from_seed.gen_range(1_000_000..1_000_000_000); + let delegate_keypair = create_delegate( + &mut e2e_env, + &env, + deposit_amount, + env.registered_forester_pda, + epoch, + None, + ) + .await; + foresters[0].1.push((delegate_keypair, 0)); + } + println!( + "added {} delegates -----------------------------------------", + num_add_delegates + ); + + let forester_token_pool = get_forester_token_pool_pda(&foresters[0].0.pubkey()); + let forester_token_pool_account = + e2e_env.rpc.get_account(forester_token_pool).await.unwrap(); + if let Some(account) = forester_token_pool_account { + let forester_token_pool_balance = + spl_token::state::Account::unpack(&account.data.as_slice()) + .unwrap() + .amount; + println!("epoch: {}", i); + println!( + "forester_token_pool_balance: {}", + forester_token_pool_balance + ); + if forester_token_pool_balance == 990000 || forester_token_pool_balance == 0 { + println!( + "forester_token_pool_balance: {}", + forester_token_pool_balance + ); + // print delegate account for every delegate + for (delegate_keypair, _) in foresters[0].1.iter() { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + println!("delegate_account: {:?}", delegate_account); + } + // print all epoch accounts + let forester_pda_pubkey = get_forester_pda_address(&foresters[0].0.pubkey()).0; + + let compressed_epoch_pda = + get_custom_compressed_account::<_, _, CompressedForesterEpochAccount>( + &mut e2e_env.indexer, + &forester_pda_pubkey, + &light_registry::ID, + ); + println!("compressed_epoch_pda: {:?}", compressed_epoch_pda); + // print allforester accounts + let forester_pda: ForesterAccount = e2e_env + .rpc + .get_anchor_account::(&forester_pda_pubkey) + .await + .unwrap() + .unwrap(); + println!("forester_pda: {:?}", forester_pda); + } + } + // // current_epoch = next_registered_epoch; + // for forester in foresters.iter_mut() { + // if let Some(next_registered_epoch) = &forester.3 { + // phases = next_registered_epoch.phases.clone(); + // } + // } + phases = get_epoch_phases(&protocol_config, epoch); + } + let post_mint_account = e2e_env.rpc.get_account(MINT).await.unwrap().unwrap(); + let post_token_balance = spl_token::state::Mint::unpack(&post_mint_account.data) + .unwrap() + .supply; + let expected_amount_minted = completed_epochs * e2e_env.protocol_config.epoch_reward; + + let forester_keypair = foresters[0].0.insecure_clone(); + for (index, (delegate_keypair, _)) in foresters[0].1.iter_mut().enumerate() { + println!("\n\n--------------------------------------\n\n"); + println!("syncing balance of delegate {}", index); + println!("\n\n--------------------------------------\n\n"); + let forester_pda_pubkey = get_forester_pda_address(&forester_keypair.pubkey()).0; + + let compressed_epoch_pda = + get_custom_compressed_account::<_, _, CompressedForesterEpochAccount>( + &mut e2e_env.indexer, + &forester_pda_pubkey, + &light_registry::ID, + ); + println!("compressed_epoch_pda: {:?}", compressed_epoch_pda); + + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + println!("delegate_account: {:?}", delegate_account); + + let deserialized_delegate = delegate_account[0].as_ref().unwrap().deserialized_account; + if deserialized_delegate.last_sync_epoch >= epoch { + println!("delegate {} already synced", index); + continue; + } + + let escrow_authority = get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0; + let escrow = e2e_env + .indexer + .get_compressed_token_accounts_by_owner(&escrow_authority); + println!("escrow: {:?}", escrow); + let inputs = SyncDelegateInputs { + sender: &delegate_keypair, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + compressed_forester_epoch_pdas: compressed_epoch_pda, + forester: forester_keypair.pubkey(), + output_merkle_tree: env.merkle_tree_pubkey, + sync_delegate_token_account: true, + previous_hash, + input_escrow_token_account: Some(escrow[0].clone()), + }; + sync_delegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) + .await + .unwrap(); + } + let forester_token_pool = get_forester_token_pool_pda(&foresters[0].0.pubkey()); + let forester_token_pool_account = e2e_env.rpc.get_account(forester_token_pool).await.unwrap(); + if let Some(account) = forester_token_pool_account { + let forester_token_pool_balance = + spl_token::state::Account::unpack(&account.data.as_slice()) + .unwrap() + .amount; + println!( + "forester_token_pool_balance: {}", + forester_token_pool_balance + ); + } + println!("completed epochs: {}", completed_epochs); + + assert_eq!( + post_token_balance, + pre_token_balance * (foresters[0].1.len() as u64 + num_mint_tos) + expected_amount_minted + ); + // let forester_two_balance = e2e_env + // .indexer + // .get_compressed_token_balance(&foresters[1].0.pubkey(), &MINT); + // println!("forester_two_balance: {}", forester_two_balance); + // println!("pre_forester_two_balance: {}", pre_forester_two_balance); + // assert!(forester_two_balance > pre_forester_two_balance); +} + +pub async fn perform_work( + e2e_env: &mut light_test_utils::e2e_test_env::E2ETestEnv< + ProgramTestRpcConnection, + TestIndexer, + >, + forester_keypair: &Keypair, + _env: &EnvAccounts, + epoch: u64, +) { + // create work 1 item in address and nullifier queue each + + e2e_env.create_address(None).await; + e2e_env + .compress_sol_deterministic(&forester_keypair, 1_000_000, None) + .await; + e2e_env + .transfer_sol_deterministic(&forester_keypair, &Pubkey::new_unique(), None) + .await + .unwrap(); + + println!("performed transactions -----------------------------------------"); + // perform 1 work + nullify_compressed_accounts( + &mut e2e_env.rpc, + &forester_keypair, + &mut e2e_env.indexer.state_merkle_trees[0], + epoch, + ) + .await; + // empty_address_queue_test( + // &forester_keypair, + // &mut e2e_env.rpc, + // &mut e2e_env.indexer.address_merkle_trees[0], + // false, + // epoch, + // ) + // .await + // .unwrap(); +} + /// Test: /// 1. SUCCESS: Register a forester /// 2. SUCCESS: Update forester authority +/// 3. SUCESS: Register forester for epoch #[tokio::test] async fn test_register_and_update_forester_pda() { - let (mut rpc, env) = setup_test_programs_with_accounts(None).await; + // TODO: add setup test programs wrapper that allows for non default protocol config + let token_mint_keypair = Keypair::from_bytes(STANDARD_TOKEN_MINT_KEYPAIR.as_slice()).unwrap(); + + let protocol_config = ProtocolConfig { + mint: token_mint_keypair.pubkey(), + ..Default::default() + }; + let (mut rpc, env) = + setup_test_programs_with_accounts_with_protocol_config(None, protocol_config, false).await; let forester_keypair = Keypair::new(); rpc.airdrop_lamports(&forester_keypair.pubkey(), 1_000_000_000) .await .unwrap(); + println!("rpc.air -----------------------------------------"); + let config = ForesterConfig { + fee: 1, + fee_recipient: Pubkey::new_unique(), + }; // 1. SUCCESS: Register a forester register_test_forester( &mut rpc, &env.governance_authority, - &forester_keypair.pubkey(), + &forester_keypair, + config, ) .await .unwrap(); + println!("registered _test_forester -----------------------------------------"); + + // // 2. SUCCESS: Update forester authority + // let new_forester_keypair = Keypair::new(); + // rpc.airdrop_lamports(&new_forester_keypair.pubkey(), 1_000_000_000) + // .await + // .unwrap(); + // let config = ForesterConfig { fee: 2 }; + + // update_test_forester( + // &mut rpc, + // &forester_keypair, + // Some(&new_forester_keypair), + // config, + // ) + // .await + // .unwrap(); + let protocol_config = rpc + .get_anchor_account::(&env.governance_authority_pda) + .await + .unwrap() + .unwrap() + .config; + + // 3. SUCCESS: register forester for epoch + let tree_accounts = vec![ + TreeAccounts { + tree_type: TreeType::State, + merkle_tree: env.merkle_tree_pubkey, + queue: env.nullifier_queue_pubkey, + is_rolledover: false, + }, + TreeAccounts { + tree_type: TreeType::Address, + merkle_tree: env.address_merkle_tree_pubkey, + queue: env.address_merkle_tree_queue_pubkey, + is_rolledover: false, + }, + ]; - // 2. SUCCESS: Update forester authority - let new_forester_keypair = Keypair::new(); - rpc.airdrop_lamports(&new_forester_keypair.pubkey(), 1_000_000_000) + let registered_epoch = Epoch::register(&mut rpc, &protocol_config, &forester_keypair) .await .unwrap(); + assert!(registered_epoch.is_some()); + let mut registered_epoch = registered_epoch.unwrap(); + let forester_epoch_pda = rpc + .get_anchor_account::(®istered_epoch.forester_epoch_pda) + .await + .unwrap() + .unwrap(); + assert!(forester_epoch_pda.total_epoch_state_weight.is_none()); + assert_eq!(forester_epoch_pda.epoch, 0); + let epoch = 0; + let expected_stake = 1; + assert_epoch_pda(&mut rpc, epoch, expected_stake).await; + assert_registered_forester_pda( + &mut rpc, + ®istered_epoch.forester_epoch_pda, + &forester_keypair.pubkey(), + epoch, + ) + .await; - update_test_forester(&mut rpc, &forester_keypair, &new_forester_keypair.pubkey()) + // advance epoch to active phase + rpc.warp_to_slot(registered_epoch.phases.active.start) + .unwrap(); + // finalize registration + { + registered_epoch + .fetch_account_and_add_trees_with_schedule(&mut rpc, tree_accounts) + .await + .unwrap(); + let ix = create_finalize_registration_instruction( + &forester_keypair.pubkey(), + registered_epoch.epoch, + ); + rpc.create_and_send_transaction(&[ix], &forester_keypair.pubkey(), &[&forester_keypair]) + .await + .unwrap(); + assert_finalized_epoch_registration( + &mut rpc, + ®istered_epoch.forester_epoch_pda, + ®istered_epoch.epoch_pda, + ) + .await; + } + // TODO: write an e2e test with multiple foresters - essentially integrate into e2e tests and make every round a slot + // 1. create multiple foresters + // 2. register them etc. + // 3. iterate over every light slot + // 3.1. give a random number of work items + // 3.2. check for every forester who is eligible + + // create work 1 item in address and nullifier queue each + let (mut state_merkle_tree_bundle, mut address_merkle_tree, mut rpc) = { + let mut e2e_env = init_program_test_env(rpc, &env).await; + e2e_env.create_address(None).await; + e2e_env + .compress_sol_deterministic(&forester_keypair, 1_000_000, None) + .await; + e2e_env + .transfer_sol_deterministic(&forester_keypair, &Pubkey::new_unique(), None) + .await + .unwrap(); + + ( + e2e_env.indexer.state_merkle_trees[0].clone(), + e2e_env.indexer.address_merkle_trees[0].clone(), + e2e_env.rpc, + ) + }; + println!("performed transactions -----------------------------------------"); + // perform 1 work + nullify_compressed_accounts( + &mut rpc, + &forester_keypair, + &mut state_merkle_tree_bundle, + epoch, + ) + .await; + empty_address_queue_test( + &forester_keypair, + &mut rpc, + &mut address_merkle_tree, + false, + epoch, + ) + .await + .unwrap(); + + // advance epoch to report work and next registration phase + rpc.warp_to_slot( + registered_epoch.phases.report_work.start - protocol_config.registration_phase_length, + ) + .unwrap(); + // register for next epoch + let next_registered_epoch = Epoch::register(&mut rpc, &protocol_config, &forester_keypair) .await .unwrap(); + assert!(next_registered_epoch.is_some()); + let next_registered_epoch = next_registered_epoch.unwrap(); + assert_eq!(next_registered_epoch.epoch, 1); + assert_epoch_pda(&mut rpc, next_registered_epoch.epoch, expected_stake).await; + assert_registered_forester_pda( + &mut rpc, + &next_registered_epoch.forester_epoch_pda, + &forester_keypair.pubkey(), + next_registered_epoch.epoch, + ) + .await; + // TODO: check that we can still forest the last epoch + rpc.warp_to_slot(registered_epoch.phases.report_work.start) + .unwrap(); + // report work + { + let (pre_forester_epoch_pda, pre_epoch_pda) = fetch_epoch_and_forester_pdas( + &mut rpc, + ®istered_epoch.forester_epoch_pda, + ®istered_epoch.epoch_pda, + ) + .await; + let ix = create_report_work_instruction(&forester_keypair.pubkey(), registered_epoch.epoch); + rpc.create_and_send_transaction(&[ix], &forester_keypair.pubkey(), &[&forester_keypair]) + .await + .unwrap(); + assert_report_work( + &mut rpc, + ®istered_epoch.forester_epoch_pda, + ®istered_epoch.epoch_pda, + pre_forester_epoch_pda, + pre_epoch_pda, + ) + .await; + } + + // TODO: test claim once implemented + // advance to post epoch phase } +// TODO: test edge cases first and last slot of every phase +// TODO: add failing tests, perform actions in invalid phases, pass unrelated epoch and forester pda /// Test: /// 1. FAIL: Register a forester with invalid authority @@ -89,13 +1152,16 @@ async fn failing_test_forester() { let payer = rpc.get_payer().insecure_clone(); // 1. FAIL: Register a forester with invalid authority { - let result = register_test_forester(&mut rpc, &payer, &Keypair::new().pubkey()).await; + let result = + register_test_forester(&mut rpc, &payer, &Keypair::new(), ForesterConfig::default()) + .await; let expected_error_code = anchor_lang::error::ErrorCode::ConstraintAddress as u32; assert_rpc_error(result, 0, expected_error_code).unwrap(); } // 2. FAIL: Update forester authority with invalid authority { - let forester_epoch_pda = get_forester_epoch_pda_address(&env.forester.pubkey()).0; + let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); + let forester_epoch_pda = get_forester_epoch_pda_address(&forester_pda, 0).0; let instruction_data = light_registry::instruction::UpdateForesterEpochPda { authority: Keypair::new().pubkey(), }; @@ -116,7 +1182,8 @@ async fn failing_test_forester() { } // 3. FAIL: Nullify with invalid authority { - let expected_error_code = RegistryError::InvalidForester as u32 + 6000; + let expected_error_code = + light_registry::errors::RegistryError::InvalidForester as u32 + 6000; let inputs = CreateNullifyInstructionInputs { authority: payer.pubkey(), nullifier_queue: env.nullifier_queue_pubkey, @@ -127,9 +1194,10 @@ async fn failing_test_forester() { proofs: vec![vec![[0u8; 32]; 26]], derivation: payer.pubkey(), }; - let mut ix = create_nullify_instruction(inputs); + let mut ix = create_nullify_instruction(inputs, 0); + let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); // Swap the derived forester pda with an initialized but invalid one. - ix.accounts[0].pubkey = get_forester_epoch_pda_address(&env.forester.pubkey()).0; + ix.accounts[0].pubkey = get_forester_epoch_pda_address(&forester_pda, 0).0; let result = rpc .create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) .await; @@ -137,7 +1205,8 @@ async fn failing_test_forester() { } // 4 FAIL: update address Merkle tree failed { - let expected_error_code = RegistryError::InvalidForester as u32 + 6000; + let expected_error_code = + light_registry::errors::RegistryError::InvalidForester as u32 + 6000; let authority = rpc.get_payer().insecure_clone(); let mut instruction = create_update_address_merkle_tree_instruction( UpdateAddressMerkleTreeInstructionInputs { @@ -153,9 +1222,11 @@ async fn failing_test_forester() { low_address_next_value: [0u8; 32], low_address_proof: [[0u8; 32]; 16], }, + 0, ); + let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); // Swap the derived forester pda with an initialized but invalid one. - instruction.accounts[0].pubkey = get_forester_epoch_pda_address(&env.forester.pubkey()).0; + instruction.accounts[0].pubkey = get_forester_epoch_pda_address(&forester_pda, 0).0; let result = rpc .create_and_send_transaction(&[instruction], &authority.pubkey(), &[&authority]) @@ -175,11 +1246,12 @@ async fn failing_test_forester() { &new_merkle_tree_keypair, &env.address_merkle_tree_pubkey, &env.address_merkle_tree_queue_pubkey, + 0, // TODO: adapt epoch ) .await; + let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); // Swap the derived forester pda with an initialized but invalid one. - instructions[2].accounts[0].pubkey = - get_forester_epoch_pda_address(&env.forester.pubkey()).0; + instructions[2].accounts[0].pubkey = get_forester_epoch_pda_address(&forester_pda, 0).0; let result = rpc .create_and_send_transaction( @@ -205,11 +1277,12 @@ async fn failing_test_forester() { &env.merkle_tree_pubkey, &env.nullifier_queue_pubkey, &new_cpi_context.pubkey(), + 0, // TODO: adapt epoch ) .await; + let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); // Swap the derived forester pda with an initialized but invalid one. - instructions[2].accounts[0].pubkey = - get_forester_epoch_pda_address(&env.forester.pubkey()).0; + instructions[2].accounts[0].pubkey = get_forester_epoch_pda_address(&forester_pda, 0).0; let result = rpc .create_and_send_transaction( @@ -226,38 +1299,43 @@ async fn failing_test_forester() { } } -// cargo test-sbf -p registry-test -- --test update_registry_governance_on_testnet update_forester_on_testnet --ignored --nocapture -#[ignore] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn update_forester_on_testnet() { - let env_accounts = get_test_env_accounts(); - let mut rpc = SolanaRpcConnection::new(SolanaRpcUrl::ZKTestnet, None); - rpc.airdrop_lamports(&env_accounts.forester.pubkey(), LAMPORTS_PER_SOL * 100) - .await - .unwrap(); - let forester_epoch_account = - Pubkey::from_str("DFiGEbaz75wSdqy9bpeWacqLWrqAwWBfqh4iSYtejiwK").unwrap(); - let forester_epoch = rpc - .get_anchor_account::(&env_accounts.registered_forester_epoch_pda) - .await - .unwrap() - .unwrap(); - println!("ForesterEpoch: {:?}", forester_epoch); - assert_eq!(forester_epoch.authority, env_accounts.forester.pubkey()); - - let updated_keypair = read_keypair_file("../../target/forester-keypair.json").unwrap(); - println!("updated keypair: {:?}", updated_keypair.pubkey()); - update_test_forester(&mut rpc, &env_accounts.forester, &updated_keypair.pubkey()) - .await - .unwrap(); - let forester_epoch = rpc - .get_anchor_account::(&env_accounts.registered_forester_epoch_pda) - .await - .unwrap() - .unwrap(); - println!("ForesterEpoch: {:?}", forester_epoch_account); - assert_eq!(forester_epoch.authority, updated_keypair.pubkey()); -} +// // cargo test-sbf -p registry-test -- --test update_registry_governance_on_testnet update_forester_on_testnet --ignored --nocapture +// #[ignore] +// #[tokio::test(flavor = "multi_thread", worker_threads = 1)] +// async fn update_forester_on_testnet() { +// let env_accounts = get_test_env_accounts(); +// let mut rpc = SolanaRpcConnection::new(SolanaRpcUrl::Devnet, None); +// // rpc.airdrop_lamports(&env_accounts.forester.pubkey(), LAMPORTS_PER_SOL * 100) +// // .await +// // .unwrap(); +// let forester_pubkey = Pubkey::from_str("8KEKiyAMugpKq9XCGzx81UtTBuytByW8arm9EaBVpD5k").unwrap(); +// // let forester_account_pubkey = get_forester_pda_address(forester_pubkey).0; +// let forester_epoch = rpc +// .get_anchor_account::(&forester_pubkey) +// .await +// .unwrap() +// .unwrap(); +// println!("ForesterEpoch: {:?}", forester_epoch); +// assert_eq!(forester_epoch.authority, env_accounts.forester.pubkey()); +// panic!(""); +// let updated_keypair = read_keypair_file("../../target/forester-keypair.json").unwrap(); +// println!("updated keypair: {:?}", updated_keypair.pubkey()); +// update_test_forester( +// &mut rpc, +// &env_accounts.forester, +// Some(&updated_keypair), +// ForesterConfig::default(), +// ) +// .await +// .unwrap(); +// let forester_epoch = rpc +// .get_anchor_account::(&env_accounts.registered_forester_pda) +// .await +// .unwrap() +// .unwrap(); +// println!("ForesterEpoch: {:?}", forester_epoch); +// assert_eq!(forester_epoch.authority, updated_keypair.pubkey()); +// } #[ignore] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -271,7 +1349,7 @@ async fn update_registry_governance_on_testnet() { .await .unwrap(); let governance_authority = rpc - .get_anchor_account::(&env_accounts.governance_authority_pda) + .get_anchor_account::(&env_accounts.governance_authority_pda) .await .unwrap() .unwrap(); @@ -284,14 +1362,16 @@ async fn update_registry_governance_on_testnet() { let updated_keypair = read_keypair_file("../../target/governance-authority-keypair.json").unwrap(); println!("updated keypair: {:?}", updated_keypair.pubkey()); - let (_, bump) = get_governance_authority_pda(); + let (_, bump) = get_protocol_config_pda_address(); let instruction = light_registry::instruction::UpdateGovernanceAuthority { new_authority: updated_keypair.pubkey(), - bump, + _bump: bump, + new_config: ProtocolConfig::default(), }; let accounts = light_registry::accounts::UpdateAuthority { authority_pda: env_accounts.governance_authority_pda, authority: env_accounts.governance_authority.pubkey(), + new_authority: updated_keypair.pubkey(), }; let ix = Instruction { program_id: light_registry::ID, @@ -308,7 +1388,7 @@ async fn update_registry_governance_on_testnet() { .unwrap(); println!("signature: {:?}", signature); let governance_authority = rpc - .get_anchor_account::(&env_accounts.governance_authority_pda) + .get_anchor_account::(&env_accounts.governance_authority_pda) .await .unwrap() .unwrap(); diff --git a/test-programs/system-cpi-test/src/sdk.rs b/test-programs/system-cpi-test/src/sdk.rs index b011b21d21..a601b91a0c 100644 --- a/test-programs/system-cpi-test/src/sdk.rs +++ b/test-programs/system-cpi-test/src/sdk.rs @@ -10,13 +10,13 @@ use anchor_lang::{InstructionData, ToAccountMetas}; use light_compressed_token::{ get_token_pool_pda, process_transfer::transfer_sdk::to_account_metas, }; -use light_registry::sdk::get_registered_program_pda; use light_system_program::{ invoke::processor::CompressedProof, sdk::{ address::pack_new_address_params, compressed_account::PackedCompressedAccountWithMerkleContext, }, + utils::get_registered_program_pda, NewAddressParams, }; use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; diff --git a/test-programs/system-cpi-test/tests/test.rs b/test-programs/system-cpi-test/tests/test.rs index 25eaeb75c0..7e5f210a7a 100644 --- a/test-programs/system-cpi-test/tests/test.rs +++ b/test-programs/system-cpi-test/tests/test.rs @@ -472,7 +472,8 @@ async fn test_create_pda_in_program_owned_merkle_trees() { registered_program_pda: env.registered_program_pda, registered_registry_program_pda: env.registered_registry_program_pda, forester: env.forester.insecure_clone(), - registered_forester_epoch_pda: env.registered_forester_epoch_pda, + registered_forester_pda: env.registered_forester_pda, + forester_epoch: env.forester_epoch.clone(), }; perform_create_pda_failing( @@ -515,7 +516,8 @@ async fn test_create_pda_in_program_owned_merkle_trees() { registered_program_pda: env.registered_program_pda, registered_registry_program_pda: env.registered_registry_program_pda, forester: env.forester.insecure_clone(), - registered_forester_epoch_pda: env.registered_forester_epoch_pda, + registered_forester_pda: env.registered_forester_pda, + forester_epoch: env.forester_epoch.clone(), }; perform_create_pda_failing( &mut test_indexer, @@ -568,7 +570,8 @@ async fn test_create_pda_in_program_owned_merkle_trees() { registered_program_pda: env.registered_program_pda, registered_registry_program_pda: env.registered_registry_program_pda, forester: env.forester.insecure_clone(), - registered_forester_epoch_pda: env.registered_forester_epoch_pda, + registered_forester_pda: env.registered_forester_pda, + forester_epoch: env.forester_epoch.clone(), }; let seed = [4u8; 32]; let data = [5u8; 31]; diff --git a/test-programs/system-cpi-test/tests/test_program_owned_trees.rs b/test-programs/system-cpi-test/tests/test_program_owned_trees.rs index 73f04a79aa..10563d1176 100644 --- a/test-programs/system-cpi-test/tests/test_program_owned_trees.rs +++ b/test-programs/system-cpi-test/tests/test_program_owned_trees.rs @@ -9,10 +9,12 @@ use account_compression::{ use anchor_lang::{system_program, InstructionData, ToAccountMetas}; use light_compressed_token::mint_sdk::create_mint_to_instruction; use light_hasher::Poseidon; -use light_registry::get_forester_epoch_pda_address; -use light_registry::sdk::{ - create_nullify_instruction, get_cpi_authority_pda, get_registered_program_pda, - CreateNullifyInstructionInputs, + +use light_registry::account_compression_cpi::sdk::{ + create_nullify_instruction, get_registered_program_pda, CreateNullifyInstructionInputs, +}; +use light_registry::utils::{ + get_cpi_authority_pda, get_forester_epoch_pda_address, get_forester_pda_address, }; use light_test_utils::get_concurrent_merkle_tree; use light_test_utils::rpc::errors::{assert_rpc_error, RpcError}; @@ -325,7 +327,8 @@ async fn test_invalid_registered_program() { let new_queue_keypair = Keypair::new(); let (cpi_authority, bump) = get_cpi_authority_pda(); let registered_program_pda = get_registered_program_pda(&light_registry::ID); - let registered_forester_pda = get_forester_epoch_pda_address(&env.forester.pubkey()).0; + let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); + let registered_forester_pda = get_forester_epoch_pda_address(&forester_pda, 0).0; let instruction_data = light_registry::instruction::RolloverStateMerkleTreeAndQueue { bump }; let accounts = light_registry::accounts::RolloverMerkleTreeAndQueue { @@ -393,7 +396,8 @@ async fn test_invalid_registered_program() { let registered_program_pda = get_registered_program_pda(&light_registry::ID); let instruction_data = light_registry::instruction::RolloverAddressMerkleTreeAndQueue { bump }; - let registered_forester_pda = get_forester_epoch_pda_address(&env.forester.pubkey()).0; + let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); + let registered_forester_pda = get_forester_epoch_pda_address(&forester_pda, 0).0; let accounts = light_registry::accounts::RolloverMerkleTreeAndQueue { account_compression_program: account_compression::ID, @@ -468,7 +472,7 @@ async fn test_invalid_registered_program() { proofs: vec![vec![[0u8; 32]; 26]], derivation: env.forester.pubkey(), }; - let ix = create_nullify_instruction(inputs); + let ix = create_nullify_instruction(inputs, 0); let result = rpc .create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) @@ -481,7 +485,8 @@ async fn test_invalid_registered_program() { // 8. update address with invalid group { let register_program_pda = get_registered_program_pda(&light_registry::ID); - let registered_forester_pda = get_forester_epoch_pda_address(&env.forester.pubkey()).0; + let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); + let registered_forester_pda = get_forester_epoch_pda_address(&forester_pda, 0).0; let (cpi_authority, bump) = get_cpi_authority_pda(); let instruction_data = light_registry::instruction::UpdateAddressMerkleTree { bump, diff --git a/test-programs/system-test/tests/test.rs b/test-programs/system-test/tests/test.rs index 6a5a8cf9c5..69c44bdf93 100644 --- a/test-programs/system-test/tests/test.rs +++ b/test-programs/system-test/tests/test.rs @@ -1485,10 +1485,12 @@ async fn regenerate_accounts() { "registered_registry_program_pda", env.registered_registry_program_pda, ), + ("registered_forester_pda", env.registered_forester_pda), ( - "registered_forester_epoch_pda", - env.registered_forester_epoch_pda, + "forester_epoch_pda", + env.forester_epoch.as_ref().unwrap().forester_epoch_pda, ), + ("epoch_pda", env.forester_epoch.as_ref().unwrap().epoch_pda), ]; for (name, pubkey) in pubkeys { diff --git a/test-utils/src/address_tree_rollover.rs b/test-utils/src/address_tree_rollover.rs index 13ec99e012..c151e58e92 100644 --- a/test-utils/src/address_tree_rollover.rs +++ b/test-utils/src/address_tree_rollover.rs @@ -283,6 +283,7 @@ pub async fn perform_address_merkle_tree_roll_over_forester( new_address_merkle_tree_keypair: &Keypair, old_merkle_tree_pubkey: &Pubkey, old_queue_pubkey: &Pubkey, + epoch: u64, ) -> Result { let instructions = create_rollover_address_merkle_tree_instructions( context, @@ -291,6 +292,7 @@ pub async fn perform_address_merkle_tree_roll_over_forester( new_address_merkle_tree_keypair, old_merkle_tree_pubkey, old_queue_pubkey, + epoch, ) .await; let blockhash = context.get_latest_blockhash().await.unwrap(); @@ -303,6 +305,7 @@ pub async fn perform_address_merkle_tree_roll_over_forester( context.process_transaction(transaction).await } +#[allow(clippy::too_many_arguments)] pub async fn perform_state_merkle_tree_roll_over_forester( payer: &Keypair, context: &mut R, @@ -311,6 +314,7 @@ pub async fn perform_state_merkle_tree_roll_over_forester( cpi_context: &Keypair, old_merkle_tree_pubkey: &Pubkey, old_queue_pubkey: &Pubkey, + epoch: u64, ) -> Result<(solana_sdk::signature::Signature, Slot), RpcError> { let instructions = create_rollover_state_merkle_tree_instructions( context, @@ -320,6 +324,7 @@ pub async fn perform_state_merkle_tree_roll_over_forester( old_merkle_tree_pubkey, old_queue_pubkey, &cpi_context.pubkey(), + epoch, ) .await; let blockhash = context.get_latest_blockhash().await.unwrap(); diff --git a/test-utils/src/assert_epoch.rs b/test-utils/src/assert_epoch.rs new file mode 100644 index 0000000000..35948af647 --- /dev/null +++ b/test-utils/src/assert_epoch.rs @@ -0,0 +1,146 @@ +use crate::rpc::rpc_connection::RpcConnection; +use light_registry::{ + protocol_config::state::ProtocolConfigPda, + utils::{get_epoch_pda_address, get_forester_pda_address, get_protocol_config_pda_address}, + EpochPda, ForesterAccount, ForesterEpochPda, +}; +use solana_sdk::pubkey::Pubkey; + +pub async fn assert_finalized_epoch_registration( + rpc: &mut R, + forester_epoch_pda_pubkey: &Pubkey, + epoch_pda_pubkey: &Pubkey, +) { + let epoch_pda = rpc + .get_anchor_account::(epoch_pda_pubkey) + .await + .unwrap() + .unwrap(); + let expected_total_epoch_stake_weight = epoch_pda.registered_stake; + let forester_epoch_pda = rpc + .get_anchor_account::(forester_epoch_pda_pubkey) + .await + .unwrap() + .unwrap(); + assert!(forester_epoch_pda.total_epoch_state_weight.is_some()); + assert_eq!( + forester_epoch_pda.total_epoch_state_weight.unwrap(), + expected_total_epoch_stake_weight + ); +} + +pub async fn assert_epoch_pda( + rpc: &mut R, + epoch: u64, + expected_registered_stake: u64, +) { + let epoch_pda_pubkey = get_epoch_pda_address(epoch); + let epoch_pda = rpc + .get_anchor_account::(&epoch_pda_pubkey) + .await + .unwrap() + .unwrap(); + println!("epoch_pda: {:?}", epoch_pda); + let protocol_config_pda_pubkey = get_protocol_config_pda_address().0; + let protocol_config_pda = rpc + .get_anchor_account::(&protocol_config_pda_pubkey) + .await + .unwrap() + .unwrap(); + assert_eq!(epoch_pda.registered_stake, expected_registered_stake); + assert_eq!(epoch_pda.total_work, 0); + assert_eq!(epoch_pda.protocol_config, protocol_config_pda.config); + assert_eq!(epoch_pda.epoch, epoch); +} +/// Helper function to fetch the forester epoch and epoch account to assert diff +/// after transaction. +pub async fn fetch_epoch_and_forester_pdas( + rpc: &mut R, + forester_epoch_pda: &Pubkey, + epoch_pda: &Pubkey, +) -> (ForesterEpochPda, EpochPda) { + let forester_epoch_pda = rpc + .get_anchor_account::(forester_epoch_pda) + .await + .unwrap() + .unwrap(); + println!("forester_epoch_pda: {:?}", forester_epoch_pda); + let epoch_pda = rpc + .get_anchor_account::(epoch_pda) + .await + .unwrap() + .unwrap(); + println!("epoch_pda: {:?}", epoch_pda); + + (forester_epoch_pda, epoch_pda) +} + +/// Asserts: +/// 1. ForesterEpochPda has reported work +/// 2. EpochPda has updated total work by forester work counter +pub async fn assert_report_work( + rpc: &mut R, + forester_epoch_pda_pubkey: &Pubkey, + epoch_pda_pubkey: &Pubkey, + mut pre_forester_epoch_pda: ForesterEpochPda, + mut pre_epoch_pda: EpochPda, +) { + let forester_epoch_pda = rpc + .get_anchor_account::(forester_epoch_pda_pubkey) + .await + .unwrap() + .unwrap(); + pre_forester_epoch_pda.has_reported_work = true; + assert_eq!(forester_epoch_pda, pre_forester_epoch_pda); + let epoch_pda = rpc + .get_anchor_account::(epoch_pda_pubkey) + .await + .unwrap() + .unwrap(); + pre_epoch_pda.total_work += forester_epoch_pda.work_counter; + assert_eq!(epoch_pda, pre_epoch_pda); +} + +/// Asserts the correct creation of a ForesterEpochPda. +pub async fn assert_registered_forester_pda( + rpc: &mut R, + forester_epoch_pda_pubkey: &Pubkey, + forester_derivation_pubkey: &Pubkey, + epoch: u64, +) { + let (forester_pda_pubkey, _) = get_forester_pda_address(forester_derivation_pubkey); + + let epoch_pda_pubkey = get_epoch_pda_address(epoch); + let epoch_pda = rpc + .get_anchor_account::(&epoch_pda_pubkey) + .await + .unwrap() + .unwrap(); + let forester_pda = rpc + .get_anchor_account::(&forester_pda_pubkey) + .await + .unwrap() + .unwrap(); + let epoch_active_phase_start_slot = epoch_pda.protocol_config.genesis_slot + + epoch_pda.protocol_config.registration_phase_length + + epoch_pda.epoch * epoch_pda.protocol_config.active_phase_length; + let expected_forester_epoch_pda = ForesterEpochPda { + authority: forester_pda.authority, + config: forester_pda.config, + epoch: epoch_pda.epoch, + stake_weight: forester_pda.active_stake_weight, + work_counter: 0, + has_reported_work: false, + forester_index: epoch_pda.registered_stake - forester_pda.active_stake_weight, + total_epoch_state_weight: None, + epoch_active_phase_start_slot, + protocol_config: epoch_pda.protocol_config, + finalize_counter: 0, + }; + let forester_epoch_pda = rpc + .get_anchor_account::(forester_epoch_pda_pubkey) + .await + .unwrap() + .unwrap(); + assert_eq!(forester_epoch_pda, expected_forester_epoch_pda); +} diff --git a/test-utils/src/assert_token_tx.rs b/test-utils/src/assert_token_tx.rs index 632b22e0cc..22c88de6b2 100644 --- a/test-utils/src/assert_token_tx.rs +++ b/test-utils/src/assert_token_tx.rs @@ -227,13 +227,14 @@ pub async fn assert_create_mint( authority: &Pubkey, mint: &Pubkey, pool: &Pubkey, + mint_authority: &Pubkey, ) { let mint_account: spl_token::state::Mint = spl_token::state::Mint::unpack(&context.get_account(*mint).await.unwrap().unwrap().data) .unwrap(); assert_eq!(mint_account.supply, 0); assert_eq!(mint_account.decimals, 2); - assert_eq!(mint_account.mint_authority.unwrap(), *authority); + assert_eq!(mint_account.mint_authority.unwrap(), *mint_authority); assert_eq!(mint_account.freeze_authority, Some(*authority).into()); assert!(mint_account.is_initialized); let mint_account: spl_token::state::Account = diff --git a/test-utils/src/e2e_test_env.rs b/test-utils/src/e2e_test_env.rs index 045657961b..ab88b6fbeb 100644 --- a/test-utils/src/e2e_test_env.rs +++ b/test-utils/src/e2e_test_env.rs @@ -69,6 +69,10 @@ // refactor all tests to work with that so that we can run all tests with a test validator and concurrency use light_compressed_token::token_data::AccountState; +use light_registry::protocol_config::state::{ProtocolConfig, ProtocolConfigPda}; +use light_registry::sdk::create_finalize_registration_instruction; +use light_registry::utils::get_protocol_config_pda_address; +use light_registry::ForesterConfig; use log::info; use num_bigint::{BigUint, RandBigInt}; use num_traits::Num; @@ -103,12 +107,18 @@ use crate::address_tree_rollover::{ assert_rolled_over_address_merkle_tree_and_queue, perform_address_merkle_tree_roll_over_forester, perform_state_merkle_tree_roll_over_forester, }; +use crate::assert_epoch::{ + assert_finalized_epoch_registration, assert_report_work, fetch_epoch_and_forester_pdas, +}; +use crate::forester_epoch::{Epoch, Forester, TreeAccounts, TreeType}; use crate::indexer::{ AddressMerkleTreeAccounts, AddressMerkleTreeBundle, Indexer, StateMerkleTreeAccounts, - StateMerkleTreeBundle, TokenDataWithContext, + StateMerkleTreeBundle, TestIndexer, TokenDataWithContext, }; +use crate::registry::register_test_forester; use crate::rpc::errors::RpcError; use crate::rpc::rpc_connection::RpcConnection; +use crate::rpc::ProgramTestRpcConnection; use crate::spl::{ approve_test, burn_test, compress_test, compressed_transfer_test, create_mint_helper, create_token_account, decompress_test, freeze_test, mint_tokens_helper, revoke_test, thaw_test, @@ -151,6 +161,10 @@ pub struct Stats { pub spl_burned: u64, pub spl_frozen: u64, pub spl_thawed: u64, + pub registered_foresters: u64, + pub created_foresters: u64, + pub work_reported: u64, + pub finalized_registrations: u64, } impl Stats { @@ -178,14 +192,50 @@ impl Stats { println!("Spl burned {}", self.spl_burned); println!("Spl frozen {}", self.spl_frozen); println!("Spl thawed {}", self.spl_thawed); + println!("Registered foresters {}", self.registered_foresters); + println!("Created foresters {}", self.created_foresters); + println!("Work reported {}", self.work_reported); + println!("Finalized registrations {}", self.finalized_registrations); } } +pub async fn init_program_test_env( + rpc: ProgramTestRpcConnection, + env_accounts: &EnvAccounts, +) -> E2ETestEnv> { + let indexer: TestIndexer = TestIndexer::init_from_env( + &env_accounts.forester.insecure_clone(), + env_accounts, + KeypairActionConfig::all_default().inclusion(), + KeypairActionConfig::all_default().non_inclusion(), + ) + .await; + + E2ETestEnv::>::new( + rpc, + indexer, + env_accounts, + KeypairActionConfig::all_default(), + GeneralActionConfig::default(), + 10, + None, + ) + .await +} + +#[derive(Debug, PartialEq)] +pub struct TestForester { + pub keypair: Keypair, + pub forester: Forester, + pub is_registered: Option, +} pub struct E2ETestEnv> { pub payer: Keypair, + pub governance_keypair: Keypair, pub indexer: I, pub users: Vec, pub mints: Vec, + pub foresters: Vec, pub rpc: R, pub keypair_action_config: KeypairActionConfig, pub general_action_config: GeneralActionConfig, @@ -193,6 +243,13 @@ pub struct E2ETestEnv> { pub rounds: u64, pub rng: StdRng, pub stats: Stats, + pub epoch: u64, + pub slot: u64, + /// Forester struct is reused but not used for foresting here + /// Epoch config keeps track of the ongong epochs. + pub epoch_config: Forester, + pub protocol_config: ProtocolConfig, + pub registration_epoch: u64, } impl> E2ETestEnv @@ -237,6 +294,32 @@ where vec![user.keypair.pubkey()], ) .await; + let protocol_config_pda_address = get_protocol_config_pda_address().0; + let protocol_config = rpc + .get_anchor_account::(&protocol_config_pda_address) + .await + .unwrap() + .unwrap() + .config; + // TODO: add clear test env enum + // register foresters is only compatible with ProgramTest environment + let (foresters, epoch_config) = + if let Some(registered_epoch) = env_accounts.forester_epoch.as_ref() { + let _forester = Forester { + registration: registered_epoch.clone(), + active: registered_epoch.clone(), + ..Default::default() + }; + // Forester epoch account is assumed to exist (is inited with test program deployment) + let forester = TestForester { + keypair: env_accounts.forester.insecure_clone(), + forester: _forester.clone(), + is_registered: Some(0), + }; + (vec![forester], _forester) + } else { + (Vec::::new(), Forester::default()) + }; Self { payer, indexer, @@ -247,8 +330,15 @@ where round: 0, rounds, rng, - mints: vec![], + mints: vec![mint], stats: Stats::default(), + foresters, + registration_epoch: 0, + epoch: 0, + slot: 0, + epoch_config, + protocol_config, + governance_keypair: env_accounts.governance_authority.insecure_clone(), } } @@ -330,10 +420,25 @@ where .nullify_compressed_accounts .unwrap_or_default(), ) { - let payer = self.indexer.get_payer().insecure_clone(); for state_tree_bundle in self.indexer.get_state_merkle_trees_mut().iter_mut() { println!("\n --------------------------------------------------\n\t\t NULLIFYING LEAVES\n --------------------------------------------------"); - nullify_compressed_accounts(&mut self.rpc, &payer, state_tree_bundle).await; + // find forester which is eligible this slot for this tree + if let Some(payer) = Self::get_eligible_forester_for_queue( + &state_tree_bundle.accounts.nullifier_queue, + &self.foresters, + self.slot, + ) { + // TODO: add newly addeded trees to foresters + nullify_compressed_accounts( + &mut self.rpc, + &payer, + state_tree_bundle, + self.epoch, + ) + .await; + } else { + println!("No forester found for nullifier queue"); + }; } } @@ -342,13 +447,30 @@ where .empty_address_queue .unwrap_or_default(), ) { - let payer = self.indexer.get_payer().insecure_clone(); for address_merkle_tree_bundle in self.indexer.get_address_merkle_trees_mut().iter_mut() { - println!("\n --------------------------------------------------\n\t\t Empty Address Queue\n --------------------------------------------------"); - empty_address_queue_test(&payer, &mut self.rpc, address_merkle_tree_bundle, false) + // find forester which is eligible this slot for this tree + if let Some(payer) = Self::get_eligible_forester_for_queue( + &address_merkle_tree_bundle.accounts.queue, + &self.foresters, + self.slot, + ) { + println!("\n --------------------------------------------------\n\t\t Empty Address Queue\n --------------------------------------------------"); + println!("epoch {}", self.epoch); + println!("forester {}", payer.pubkey()); + // TODO: add newly addeded trees to foresters + empty_address_queue_test( + &payer, + &mut self.rpc, + address_merkle_tree_bundle, + false, + self.epoch, + ) .await .unwrap(); + } else { + println!("No forester found for address queue"); + }; } } @@ -366,11 +488,19 @@ where && is_read_for_rollover { println!("\n --------------------------------------------------\n\t\t Rollover State Merkle Tree\n --------------------------------------------------"); - - self.rollover_state_merkle_tree_and_queue(index) - .await - .unwrap(); - self.stats.rolledover_state_trees += 1; + // find forester which is eligible this slot for this tree + if let Some(payer) = Self::get_eligible_forester_for_queue( + &self.indexer.get_state_merkle_trees()[index] + .accounts + .nullifier_queue, + &self.foresters, + self.slot, + ) { + self.rollover_state_merkle_tree_and_queue(index, &payer, self.epoch) + .await + .unwrap(); + self.stats.rolledover_state_trees += 1; + } } } @@ -387,11 +517,241 @@ where .gen_bool(self.general_action_config.rollover.unwrap_or_default()) && is_read_for_rollover { - println!("\n --------------------------------------------------\n\t\t Rollover Address Merkle Tree\n --------------------------------------------------"); - self.rollover_address_merkle_tree_and_queue(index) - .await - .unwrap(); - self.stats.rolledover_address_trees += 1; + // find forester which is eligible this slot for this tree + if let Some(payer) = Self::get_eligible_forester_for_queue( + &self.indexer.get_address_merkle_trees()[index] + .accounts + .queue, + &self.foresters, + self.slot, + ) { + println!("\n --------------------------------------------------\n\t\t Rollover Address Merkle Tree\n --------------------------------------------------"); + self.rollover_address_merkle_tree_and_queue(index, &payer, self.epoch) + .await + .unwrap(); + self.stats.rolledover_address_trees += 1; + } + } + } + + if self + .rng + .gen_bool(self.general_action_config.add_forester.unwrap_or_default()) + { + println!("\n --------------------------------------------------\n\t\t Add Forester\n --------------------------------------------------"); + let forester = TestForester { + keypair: Keypair::new(), + forester: Forester::default(), + is_registered: None, + }; + let forester_config = ForesterConfig { + fee: self.rng.gen_range(0..=100), + fee_recipient: forester.keypair.pubkey(), + }; + register_test_forester( + &mut self.rpc, + &self.governance_keypair, + &forester.keypair, + forester_config, + ) + .await + .unwrap(); + self.foresters.push(forester); + self.stats.created_foresters += 1; + } + + // advance to next light slot and perform forester epoch actions + if !self.general_action_config.disable_epochs { + println!("\n --------------------------------------------------\n\t\t Start Epoch Actions \n --------------------------------------------------"); + + let current_solana_slot = self.rpc.get_slot().await.unwrap(); + let current_light_slot = self + .protocol_config + .get_current_active_epoch_progress(current_solana_slot) + / self.protocol_config.slot_length; + // If slot didn't change, advance to next slot + // if current_light_slot != self.slot { + let new_slot = current_solana_slot + self.protocol_config.slot_length; + println!("advanced slot from {} to {}", self.slot, current_light_slot); + println!("solana slot from {} to {}", current_solana_slot, new_slot); + self.rpc.warp_to_slot(new_slot).unwrap(); + + self.slot = current_light_slot + 1; + + let current_solana_slot = self.rpc.get_slot().await.unwrap(); + // need to detect whether new registration phase started + let current_registration_epoch = + self.protocol_config.get_current_epoch(current_solana_slot); + // If reached new registration phase register all foresters + if current_registration_epoch != self.registration_epoch { + println!("\n --------------------------------------------------\n\t\t Register Foresters for new Epoch \n --------------------------------------------------"); + + self.registration_epoch = current_registration_epoch; + println!("new register epoch {}", self.registration_epoch); + println!("num foresters {}", self.foresters.len()); + for forester in self.foresters.iter_mut() { + println!( + "registered forester {} for epoch {}", + forester.keypair.pubkey(), + self.registration_epoch + ); + + let registered_epoch = + Epoch::register(&mut self.rpc, &self.protocol_config, &forester.keypair) + .await + .unwrap() + .unwrap(); + println!("registered_epoch {:?}", registered_epoch.phases); + forester.forester.registration = registered_epoch; + if forester.is_registered.is_none() { + forester.is_registered = Some(self.registration_epoch); + } + self.stats.registered_foresters += 1; + } + } + + let current_active_epoch = self + .protocol_config + .get_current_active_epoch(current_solana_slot) + .unwrap(); + // If reached new active epoch + // 1. move epoch in every forester to report work epoch + // 2. report work for every forester + // 3. finalize registration for every forester + #[allow(clippy::comparison_chain)] + if current_active_epoch > self.epoch { + self.slot = current_light_slot; + self.epoch = current_active_epoch; + // 1. move epoch in every forester to report work epoch + for forester in self.foresters.iter_mut() { + if forester.is_registered.is_none() { + continue; + } + forester.forester.switch_to_report_work(); + } + println!("\n --------------------------------------------------\n\t\t Report Work \n --------------------------------------------------"); + + // 2. report work for every forester + for forester in self.foresters.iter_mut() { + if forester.is_registered.is_none() { + continue; + } + println!("report work for forester {}", forester.keypair.pubkey()); + println!( + "forester.forester.report_work.forester_epoch_pda {}", + forester.forester.report_work.forester_epoch_pda + ); + println!( + "forester.forester.report_work.epoch_pda {}", + forester.forester.report_work.epoch_pda + ); + + let (pre_forester_epoch_pda, pre_epoch_pda) = fetch_epoch_and_forester_pdas( + &mut self.rpc, + &forester.forester.report_work.forester_epoch_pda, + &forester.forester.report_work.epoch_pda, + ) + .await; + forester + .forester + .report_work(&mut self.rpc, &forester.keypair) + .await + .unwrap(); + println!("reported work"); + assert_report_work( + &mut self.rpc, + &forester.forester.report_work.forester_epoch_pda, + &forester.forester.report_work.epoch_pda, + pre_forester_epoch_pda, + pre_epoch_pda, + ) + .await; + self.stats.work_reported += 1; + } + + // 3. finalize registration for every forester + println!("\n --------------------------------------------------\n\t\t Finalize Registration \n --------------------------------------------------"); + + // 3.1 get tree accounts + // TODO: use TreeAccounts in TestIndexer + let mut tree_accounts = self + .indexer + .get_state_merkle_trees() + .iter() + .map(|state_merkle_tree_bundle| TreeAccounts { + tree_type: TreeType::State, + merkle_tree: state_merkle_tree_bundle.accounts.merkle_tree, + queue: state_merkle_tree_bundle.accounts.nullifier_queue, + is_rolledover: false, + }) + .collect::>(); + self.indexer.get_address_merkle_trees().iter().for_each( + |address_merkle_tree_bundle| { + tree_accounts.push(TreeAccounts { + tree_type: TreeType::Address, + merkle_tree: address_merkle_tree_bundle.accounts.merkle_tree, + queue: address_merkle_tree_bundle.accounts.queue, + is_rolledover: false, + }); + }, + ); + // 3.2 finalize registration for every forester + for forester in self.foresters.iter_mut() { + if forester.is_registered.is_none() { + continue; + } + println!( + "registered forester {} for epoch {}", + forester.keypair.pubkey(), + self.epoch + ); + println!( + "forester.forester registration epoch {:?}", + forester.forester.registration.epoch + ); + println!( + "forester.forester active epoch {:?}", + forester.forester.active.epoch + ); + println!( + "forester.forester report_work epoch {:?}", + forester.forester.report_work.epoch + ); + + forester + .forester + .active + .fetch_account_and_add_trees_with_schedule( + &mut self.rpc, + tree_accounts.clone(), + ) + .await + .unwrap(); + let ix = create_finalize_registration_instruction( + &forester.keypair.pubkey(), + forester.forester.active.epoch, + ); + self.rpc + .create_and_send_transaction( + &[ix], + &forester.keypair.pubkey(), + &[&forester.keypair], + ) + .await + .unwrap(); + assert_finalized_epoch_registration( + &mut self.rpc, + &forester.forester.active.forester_epoch_pda, + &forester.forester.active.epoch_pda, + ) + .await; + self.stats.finalized_registrations += 1; + } + } else if current_active_epoch < self.epoch { + panic!( + "current_active_epoch {} is less than self.epoch {}", + current_active_epoch, self.epoch + ); } } } @@ -691,6 +1051,26 @@ where } } + pub fn get_eligible_forester_for_queue( + queue_pubkey: &Pubkey, + foresters: &[TestForester], + light_slot: u64, + ) -> Option { + for f in foresters.iter() { + let tree = f + .forester + .active + .merkle_trees + .iter() + .find(|mt| mt.tree_pubkey.queue == *queue_pubkey); + if let Some(tree) = tree { + if tree.is_eligible(light_slot) { + return Some(f.keypair.insecure_clone()); + } + } + } + None + } pub async fn transfer_sol_deterministic( &mut self, from: &Keypair, @@ -1342,7 +1722,7 @@ where &mut self.indexer, &mt_pubkeys[0], &self.payer, - &mint, + &self.mints[0], vec![Self::safe_gen_range(&mut self.rng, 100_000..1_000_000, 100_000); 1], vec![*user; 1], ) @@ -1409,6 +1789,8 @@ where pub async fn rollover_state_merkle_tree_and_queue( &mut self, index: usize, + payer: &Keypair, + epoch: u64, ) -> Result<(), RpcError> { let bundle = self.indexer.get_state_merkle_trees()[index].accounts; let new_nullifier_queue_keypair = Keypair::new(); @@ -1421,13 +1803,14 @@ where .await .unwrap(); let rollover_signature_and_slot = perform_state_merkle_tree_roll_over_forester( - self.indexer.get_payer(), + payer, &mut self.rpc, &new_nullifier_queue_keypair, &new_merkle_tree_keypair, &new_cpi_signature_keypair, &bundle.merkle_tree, &bundle.nullifier_queue, + epoch, ) .await .unwrap(); @@ -1471,6 +1854,8 @@ where pub async fn rollover_address_merkle_tree_and_queue( &mut self, index: usize, + payer: &Keypair, + epoch: u64, ) -> Result<(), RpcError> { let bundle = self.indexer.get_address_merkle_trees()[index].accounts; let new_nullifier_queue_keypair = Keypair::new(); @@ -1482,12 +1867,13 @@ where .unwrap(); println!("prior balance {}", fee_payer_balance); perform_address_merkle_tree_roll_over_forester( - self.indexer.get_payer(), + payer, &mut self.rpc, &new_nullifier_queue_keypair, &new_merkle_tree_keypair, &bundle.merkle_tree, &bundle.queue, + epoch, ) .await?; assert_rolled_over_address_merkle_tree_and_queue( @@ -1851,6 +2237,10 @@ pub struct GeneralActionConfig { pub nullify_compressed_accounts: Option, pub empty_address_queue: Option, pub rollover: Option, + pub add_forester: Option, + /// TODO: add this + /// Creates one infinte epoch + pub disable_epochs: bool, } impl Default for GeneralActionConfig { fn default() -> Self { @@ -1861,6 +2251,8 @@ impl Default for GeneralActionConfig { nullify_compressed_accounts: Some(0.2), empty_address_queue: Some(0.2), rollover: None, + add_forester: None, + disable_epochs: false, } } } @@ -1874,16 +2266,20 @@ impl GeneralActionConfig { nullify_compressed_accounts: None, empty_address_queue: None, rollover: None, + add_forester: None, + disable_epochs: false, } } pub fn test_with_rollover() -> Self { Self { add_keypair: Some(0.3), - create_state_mt: None, - create_address_mt: None, - nullify_compressed_accounts: None, - empty_address_queue: None, - rollover: None, + create_state_mt: Some(1.0), + create_address_mt: Some(1.0), + nullify_compressed_accounts: Some(0.2), + empty_address_queue: Some(0.2), + rollover: Some(0.5), + add_forester: None, + disable_epochs: false, } } } diff --git a/test-utils/src/forester_epoch.rs b/test-utils/src/forester_epoch.rs new file mode 100644 index 0000000000..84f331368b --- /dev/null +++ b/test-utils/src/forester_epoch.rs @@ -0,0 +1,507 @@ +// TODO: move into separate forester utils crate +use anchor_lang::{ + prelude::borsh, solana_program::pubkey::Pubkey, AnchorDeserialize, AnchorSerialize, +}; + +use light_registry::{ + protocol_config::state::{EpochState, ProtocolConfig}, + sdk::{create_register_forester_epoch_pda_instruction, create_report_work_instruction}, + utils::{get_epoch_pda_address, get_forester_epoch_pda_address, get_forester_pda_address}, + EpochPda, ForesterEpochPda, ForesterSlot, +}; +use solana_sdk::signature::{Keypair, Signature, Signer}; + +use crate::rpc::{errors::RpcError, rpc_connection::RpcConnection}; + +// What does the forester need to know? +// What are my public keys (current epoch account, last epoch account, known Merkle trees) +// 1. The current epoch +// 2. When does the next registration start +// 3. When is my turn. + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct Forester { + pub registration: Epoch, + pub active: Epoch, + pub report_work: Epoch, +} + +impl Forester { + pub fn switch_to_report_work(&mut self) { + self.report_work = self.active.clone(); + self.active = self.registration.clone(); + } + + pub async fn report_work( + &mut self, + rpc: &mut impl RpcConnection, + forester_keypair: &Keypair, + ) -> Result { + let ix = create_report_work_instruction(&forester_keypair.pubkey(), self.report_work.epoch); + rpc.create_and_send_transaction(&[ix], &forester_keypair.pubkey(), &[forester_keypair]) + .await + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TreeAccounts { + pub merkle_tree: Pubkey, + pub queue: Pubkey, + // TODO: evaluate whether we need + pub is_rolledover: bool, + pub tree_type: TreeType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TreeType { + Address, + State, +} + +pub fn get_schedule_for_queue( + mut start_solana_slot: u64, + queue_pubkey: &Pubkey, + protocol_config: &ProtocolConfig, + total_epoch_state_weight: u64, +) -> Vec> { + let mut vec = Vec::new(); + let start_slot = 0; + // TODO: enforce that active_phase_length is a multiple of slot_length + let end_slot = start_slot + (protocol_config.active_phase_length / protocol_config.slot_length); + for i in start_slot..end_slot { + let forester_index = ForesterEpochPda::get_eligible_forester_index( + start_slot, + queue_pubkey, + total_epoch_state_weight, + ) + .unwrap(); + vec.push(Some(ForesterSlot { + slot: i, + start_solana_slot, + end_solana_slot: start_solana_slot + protocol_config.slot_length, + forester_index, + })); + start_solana_slot += protocol_config.slot_length; + } + vec +} + +pub fn get_schedule_for_forester_in_queue( + start_solana_slot: u64, + queue_pubkey: &Pubkey, + total_epoch_state_weight: u64, + forester_epoch_pda: &ForesterEpochPda, +) -> Vec> { + let mut slots = get_schedule_for_queue( + start_solana_slot, + queue_pubkey, + &forester_epoch_pda.protocol_config, + total_epoch_state_weight, + ); + slots.iter_mut().for_each(|x| { + // TODO: remove unwrap + if forester_epoch_pda.is_eligible(x.as_ref().unwrap().forester_index) { + } else { + *x = None; + } + }); + slots +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TreeForesterSchedule { + pub tree_pubkey: TreeAccounts, + /// Vec with the slots that the forester is eligible to perform work. + /// Non eligible slots are None. + pub slots: Vec>, +} + +impl TreeForesterSchedule { + pub fn new(tree_pubkey: TreeAccounts) -> Self { + Self { + tree_pubkey, + slots: Vec::new(), + } + } + + pub fn new_with_schedule( + tree_pubkey: TreeAccounts, + solana_slot: u64, + forester_epoch_pda: &ForesterEpochPda, + ) -> Self { + let mut _self = Self { + tree_pubkey, + slots: Vec::new(), + }; + _self.slots = get_schedule_for_forester_in_queue( + solana_slot, + &_self.tree_pubkey.queue, + forester_epoch_pda.total_epoch_state_weight.unwrap(), + forester_epoch_pda, + ); + _self + } + + pub fn is_eligible(&self, forester_slot: u64) -> bool { + self.slots[forester_slot as usize].is_some() + } +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default, PartialEq, Eq)] +pub struct EpochPhases { + pub registration: Phase, + pub active: Phase, + pub report_work: Phase, + pub post: Phase, +} + +impl EpochPhases { + pub fn get_current_phase(&self, current_slot: u64) -> Phase { + if current_slot >= self.registration.start && current_slot <= self.registration.end { + self.registration.clone() + } else if current_slot >= self.active.start && current_slot <= self.active.end { + self.active.clone() + } else if current_slot >= self.report_work.start && current_slot <= self.report_work.end { + self.report_work.clone() + } else { + self.post.clone() + } + } + pub fn get_current_epoch_state(&self, current_slot: u64) -> EpochState { + if current_slot >= self.registration.start && current_slot <= self.registration.end { + EpochState::Registration + } else if current_slot >= self.active.start && current_slot <= self.active.end { + EpochState::Active + } else if current_slot >= self.report_work.start && current_slot <= self.report_work.end { + EpochState::ReportWork + } else { + EpochState::Post + } + } +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default, PartialEq, Eq)] +pub struct Phase { + pub start: u64, + pub end: u64, +} + +pub fn get_epoch_phases(protocol_config: &ProtocolConfig, epoch: u64) -> EpochPhases { + let epoch_start_slot = protocol_config + .genesis_slot + .saturating_add(epoch.saturating_mul(protocol_config.active_phase_length)); + + let registration_start = epoch_start_slot; + let registration_end = registration_start + .saturating_add(protocol_config.registration_phase_length) + .saturating_sub(1); + + let active_start = registration_end.saturating_add(1); + let active_end = active_start + .saturating_add(protocol_config.active_phase_length) + .saturating_sub(1); + + let report_work_start = active_end.saturating_add(1); + let report_work_end = report_work_start + .saturating_add(protocol_config.report_work_phase_length) + .saturating_sub(1); + + let post_start = report_work_end.saturating_add(1); + let post_end = u64::MAX; + + EpochPhases { + registration: Phase { + start: registration_start, + end: registration_end, + }, + active: Phase { + start: active_start, + end: active_end, + }, + report_work: Phase { + start: report_work_start, + end: report_work_end, + }, + post: Phase { + start: post_start, + end: post_end, + }, + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Epoch { + pub epoch: u64, + pub epoch_pda: Pubkey, + pub forester_epoch_pda: Pubkey, + pub phases: EpochPhases, + pub state: EpochState, + pub merkle_trees: Vec, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default, PartialEq, Eq)] +pub struct EpochRegistration { + pub epoch: u64, + pub slots_until_registration_starts: u64, + pub slots_until_registration_ends: u64, +} + +impl Epoch { + /// returns slots until next epoch and that epoch + /// registration is open if + pub async fn slots_until_next_epoch_registration( + rpc: &mut R, + protocol_config: &ProtocolConfig, + ) -> Result { + let current_solana_slot = rpc.get_slot().await?; + println!("current_solana_slot {:?}", current_solana_slot); + println!( + "protocol_config {:?}", + protocol_config.registration_phase_length + ); + println!("protocol_config {:?}", protocol_config.active_phase_length); + + let mut epoch = protocol_config.get_current_epoch(current_solana_slot); + let registration_start_slot = + protocol_config.genesis_slot + epoch * protocol_config.active_phase_length; + println!("registration_start_slot {:?}", registration_start_slot); + let registration_end_slot = + registration_start_slot + protocol_config.registration_phase_length; + if current_solana_slot > registration_end_slot { + epoch += 1; + } + let next_registration_start_slot = + protocol_config.genesis_slot + epoch * protocol_config.active_phase_length; + let next_registration_end_slot = + next_registration_start_slot + protocol_config.registration_phase_length; + println!( + "next_registration_start_slot {:?}", + next_registration_start_slot + ); + println!( + "next_registration_end_slot {:?}", + next_registration_end_slot + ); + println!("curent_solana_slot {:?}", current_solana_slot); + let slots_until_registration_ends = + next_registration_end_slot.saturating_sub(current_solana_slot); + let slots_until_registration_starts = + next_registration_start_slot.saturating_sub(current_solana_slot); + Ok(EpochRegistration { + epoch, + slots_until_registration_starts, + slots_until_registration_ends, + }) + } + + /// creates forester account and fetches epoch account + pub async fn register( + rpc: &mut R, + protocol_config: &ProtocolConfig, + authority: &Keypair, + ) -> Result, RpcError> { + let epoch_registration = + Self::slots_until_next_epoch_registration(rpc, protocol_config).await?; + if epoch_registration.slots_until_registration_starts > 0 + || epoch_registration.slots_until_registration_ends == 0 + { + return Ok(None); + } + + let instruction = create_register_forester_epoch_pda_instruction( + &authority.pubkey(), + epoch_registration.epoch, + ); + let signature = rpc + .create_and_send_transaction(&[instruction], &authority.pubkey(), &[authority]) + .await?; + rpc.confirm_transaction(signature).await?; + let epoch_pda_pubkey = get_epoch_pda_address(epoch_registration.epoch); + let epoch_pda = rpc + .get_anchor_account::(&epoch_pda_pubkey) + .await? + .unwrap(); + let forester_pda = get_forester_pda_address(&authority.pubkey()).0; + let forester_epoch_pda_pubkey = + get_forester_epoch_pda_address(&forester_pda, epoch_registration.epoch).0; + + let phases = get_epoch_phases(protocol_config, epoch_pda.epoch); + Ok(Some(Self { + // epoch: epoch_registration.epoch, + epoch_pda: epoch_pda_pubkey, + forester_epoch_pda: forester_epoch_pda_pubkey, + merkle_trees: Vec::new(), + epoch: epoch_pda.epoch, + state: phases.get_current_epoch_state(rpc.get_slot().await?), + phases, + })) + } + // TODO: implement + /// forester account and epoch account already exist + /// -> fetch accounts and init + pub fn fetch_registered() {} + + pub async fn fetch_account_and_add_trees_with_schedule( + &mut self, + rpc: &mut R, + trees: Vec, + ) -> Result<(), RpcError> { + let current_solana_slot = rpc.get_slot().await?; + + if self.phases.active.end < current_solana_slot + || self.phases.active.start > current_solana_slot + { + println!("current_solana_slot {:?}", current_solana_slot); + println!("registration phase {:?}", self.phases.registration); + println!("active phase {:?}", self.phases.active); + // return Err(RpcError::EpochNotActive); + panic!("TODO: throw epoch not active error"); + } + let epoch_pda = rpc + .get_anchor_account::(&self.epoch_pda) + .await? + .unwrap(); + let mut forester_epoch_pda = rpc + .get_anchor_account::(&self.forester_epoch_pda) + .await? + .unwrap(); + // IF active phase has started and total_epoch_state_weight is not set, set it now to + if forester_epoch_pda.total_epoch_state_weight.is_none() { + forester_epoch_pda.total_epoch_state_weight = Some(epoch_pda.registered_stake); + } + self.add_trees_with_schedule(&forester_epoch_pda, trees, current_solana_slot); + Ok(()) + } + /// Internal function to init Epoch struct with registered account + /// 1. calculate epoch phases + /// 2. set current epoch state + /// 3. derive tree schedule for all input trees + pub fn add_trees_with_schedule( + &mut self, + forester_epoch_pda: &ForesterEpochPda, + trees: Vec, + current_solana_slot: u64, + ) { + // let state = self.phases.get_current_epoch_state(current_solana_slot); + // TODO: add epoch state to sync schedule + for tree in trees { + let tree_schedule = TreeForesterSchedule::new_with_schedule( + tree, + current_solana_slot, + forester_epoch_pda, + ); + self.merkle_trees.push(tree_schedule); + } + } + + pub fn update_state(&mut self, current_solana_slot: u64) -> EpochState { + let current_state = self.phases.get_current_epoch_state(current_solana_slot); + if current_state != self.state { + self.state = current_state.clone(); + } + current_state + } + + /// execute active phase test: + /// (multi thread) + /// - iterate over all trees, check whether eligible and empty queues + /// forester: + /// - start a new thread per tree + /// - this thread will sleep when it is not elibile and wake up with + /// some buffer time prior to the start of the slot + /// - threads shut down when the active phase ends + pub fn execute_active_phase() {} + + /// report work phase: + /// (single thread) + /// - free Merkle tree memory + /// - execute report work tx (single thread) + pub fn execute_report_work_phase() {} + /// post phase: + /// (single thread) + /// - claim rewards + /// - close forester epoch account + pub fn execute_post_phase() {} +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_epoch_phases() { + let config = ProtocolConfig { + genesis_slot: 200, + epoch_reward: 0, + base_reward: 0, + min_stake: 0, + slot_length: 10, + registration_phase_length: 100, + active_phase_length: 1000, + report_work_phase_length: 100, + mint: Pubkey::default(), + forester_registration_guarded: false, + }; + + let epoch = 1; + let phases = get_epoch_phases(&config, epoch); + + assert_eq!(phases.registration.start, 1200); + assert_eq!(phases.registration.end, 1299); + + assert_eq!(phases.active.start, 1300); + assert_eq!(phases.active.end, 2299); + + assert_eq!(phases.report_work.start, 2300); + assert_eq!(phases.report_work.end, 2399); + + assert_eq!(phases.post.start, 2400); + assert_eq!(phases.post.end, u64::MAX); + } + + #[test] + fn test_get_schedule_for_queue() { + let protocol_config = ProtocolConfig { + genesis_slot: 0, + epoch_reward: 1000, + base_reward: 500, + min_stake: 100, + slot_length: 10, + registration_phase_length: 100, + active_phase_length: 1000, + report_work_phase_length: 100, + mint: Pubkey::new_unique(), + forester_registration_guarded: false, + }; + + let total_epoch_state_weight = 500; + let queue_pubkey = Pubkey::new_unique(); + let start_solana_slot = 0; + + let schedule = get_schedule_for_queue( + start_solana_slot, + &queue_pubkey, + &protocol_config, + total_epoch_state_weight, + ); + + assert_eq!( + schedule.len(), + (protocol_config.active_phase_length / protocol_config.slot_length) as usize + ); + + for (i, slot_option) in schedule.iter().enumerate() { + let slot = slot_option.as_ref().unwrap(); + assert_eq!(slot.slot, i as u64); + assert_eq!( + slot.start_solana_slot, + start_solana_slot + (i as u64 * protocol_config.slot_length) + ); + assert_eq!( + slot.end_solana_slot, + slot.start_solana_slot + protocol_config.slot_length + ); + assert!(slot.forester_index < total_epoch_state_weight); + } + } +} diff --git a/test-utils/src/indexer/test_indexer.rs b/test-utils/src/indexer/test_indexer.rs index a53d8450c4..68faf51044 100644 --- a/test-utils/src/indexer/test_indexer.rs +++ b/test-utils/src/indexer/test_indexer.rs @@ -1,4 +1,3 @@ -use light_registry::sdk::get_registered_program_pda; use log::{debug, info, warn}; use num_bigint::BigUint; use solana_sdk::bs58; @@ -947,10 +946,7 @@ impl TestIndexer { pub fn get_compressed_token_balance(&self, owner: &Pubkey, mint: &Pubkey) -> u64 { self.token_compressed_accounts .iter() - .filter(|x| { - x.compressed_account.compressed_account.owner == *owner - && x.token_data.mint == *mint - }) + .filter(|x| x.token_data.owner == *owner && x.token_data.mint == *mint) .map(|x| x.token_data.amount) .sum() } diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 9cb36392b4..933e9d39dd 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -1,10 +1,13 @@ use crate::rpc::errors::RpcError; use crate::rpc::rpc_connection::RpcConnection; use anchor_lang::solana_program::{pubkey::Pubkey, system_instruction}; +use indexer::Indexer; use light_concurrent_merkle_tree::copy::ConcurrentMerkleTreeCopy; use light_hash_set::HashSet; use light_hasher::Hasher; use light_indexed_merkle_tree::copy::IndexedMerkleTreeCopy; +use light_registry::delegate::state::CompressedAccountTrait; +use light_system_program::sdk::compressed_account::CompressedAccountWithMerkleContext; use num_traits::{CheckedAdd, CheckedSub, ToBytes, Unsigned}; use solana_sdk::{ account::Account, @@ -20,11 +23,13 @@ pub mod address_merkle_tree_config; pub mod address_tree_rollover; pub mod assert_address_merkle_tree; pub mod assert_compressed_tx; +pub mod assert_epoch; pub mod assert_merkle_tree; pub mod assert_queue; pub mod assert_rollover; pub mod assert_token_tx; pub mod e2e_test_env; +pub mod forester_epoch; #[allow(unused)] pub mod indexer; pub mod registry; @@ -185,3 +190,43 @@ pub fn assert_custom_error_or_program_error( Ok(()) } + +#[derive(Debug, Clone)] +pub struct FetchedAccount { + pub deserialized_account: T, + pub comporessed_account: CompressedAccountWithMerkleContext, +} + +pub fn get_custom_compressed_account< + R: RpcConnection, + I: Indexer, + T: anchor_lang::AnchorDeserialize + CompressedAccountTrait, +>( + indexer: &mut I, + owner: &Pubkey, + program_owner: &Pubkey, +) -> Vec>> { + let accounts = indexer.get_compressed_accounts_by_owner(program_owner); + let find_account_of_owner = accounts + .iter() + .map(|a| { + if let Some(data) = &a.compressed_account.data { + let deserialized_account = T::deserialize_reader(&mut &data.data[..]); + if deserialized_account.is_ok() + && deserialized_account.as_ref().unwrap().get_owner() == *owner + { + Some(FetchedAccount:: { + deserialized_account: deserialized_account.unwrap(), + comporessed_account: a.clone(), + }) + } else { + None + } + } else { + None + } + }) + .filter(|a| a.is_some()) + .collect::>(); + find_account_of_owner +} diff --git a/test-utils/src/registry.rs b/test-utils/src/registry.rs index ffe13cd922..3cbbf8f291 100644 --- a/test-utils/src/registry.rs +++ b/test-utils/src/registry.rs @@ -1,47 +1,76 @@ use crate::address_merkle_tree_config::{get_address_bundle_config, get_state_bundle_config}; -use crate::indexer::{AddressMerkleTreeAccounts, StateMerkleTreeAccounts}; +use crate::indexer::{ + AddressMerkleTreeAccounts, Indexer, StateMerkleTreeAccounts, TokenDataWithContext, +}; use crate::rpc::rpc_connection::RpcConnection; +use crate::FetchedAccount; use crate::{create_account_instruction, rpc::errors::RpcError}; use account_compression::{ AddressMerkleTreeConfig, AddressQueueConfig, NullifierQueueConfig, QueueAccount, StateMerkleTreeConfig, }; + +use anchor_lang::AnchorDeserialize; +use light_compressed_token::TokenData; +use light_hasher::{DataHasher, Poseidon}; +use light_registry::account_compression_cpi::sdk::{ + create_rollover_state_merkle_tree_instruction, CreateRolloverMerkleTreeInstructionInputs, +}; +use light_registry::delegate::deposit::DelegateAccountWithContext; +use light_registry::delegate::get_escrow_token_authority; +use light_registry::delegate::state::DelegateAccount; +use light_registry::epoch::claim_forester::{ + CompressedForesterEpochAccount, CompressedForesterEpochAccountInput, +}; +use light_registry::protocol_config::state::ProtocolConfigPda; use light_registry::sdk::{ - create_rollover_address_merkle_tree_instruction, create_rollover_state_merkle_tree_instruction, - CreateRolloverMerkleTreeInstructionInputs, + create_delegate_instruction, create_deposit_instruction, create_forester_claim_instruction, + create_register_forester_instruction, create_sync_delegate_instruction, + create_update_forester_pda_instruction, CreateDelegateInstructionInputs, + CreateDepositInstructionInputs, CreateSyncDelegateInstructionInputs, }; -use light_registry::{ - get_forester_epoch_pda_address, - sdk::{create_register_forester_instruction, create_update_forester_instruction}, - ForesterEpoch, +use light_registry::utils::{ + get_forester_epoch_pda_address, get_forester_pda_address, get_forester_token_pool_pda, + get_protocol_config_pda_address, }; +use light_registry::{ForesterAccount, ForesterConfig, ForesterEpochPda}; +use light_system_program::sdk::compressed_account::CompressedAccountWithMerkleContext; +use light_system_program::sdk::event::PublicTransactionEvent; +use solana_sdk::account::Account; +use solana_sdk::program_pack::Pack; +use solana_sdk::signature::Signature; use solana_sdk::{ instruction::Instruction, pubkey::Pubkey, signature::{Keypair, Signer}, }; +/// Creates and asserts forester account creation. pub async fn register_test_forester( rpc: &mut R, governance_authority: &Keypair, - forester_authority: &Pubkey, + forester_authority: &Keypair, + config: ForesterConfig, ) -> Result<(), RpcError> { - let ix = - create_register_forester_instruction(&governance_authority.pubkey(), forester_authority); + let ix = create_register_forester_instruction( + &governance_authority.pubkey(), + &forester_authority.pubkey(), + config, + ); rpc.create_and_send_transaction( &[ix], &governance_authority.pubkey(), - &[governance_authority], + &[governance_authority, forester_authority], ) .await?; assert_registered_forester( rpc, - forester_authority, - ForesterEpoch { - authority: *forester_authority, - counter: 0, - epoch_start: 0, - epoch_end: u64::MAX, + &forester_authority.pubkey(), + ForesterAccount { + authority: forester_authority.pubkey(), + config, + active_stake_weight: 0, + ..Default::default() }, ) .await @@ -50,30 +79,46 @@ pub async fn register_test_forester( pub async fn update_test_forester( rpc: &mut R, forester_authority: &Keypair, - new_forester_authority: &Pubkey, + new_forester_authority: Option<&Keypair>, + config: ForesterConfig, ) -> Result<(), RpcError> { let mut pre_account_state = rpc - .get_anchor_account::( - &get_forester_epoch_pda_address(&forester_authority.pubkey()).0, + .get_anchor_account::( + &get_forester_pda_address(&forester_authority.pubkey()).0, ) .await? .unwrap(); - let ix = - create_update_forester_instruction(&forester_authority.pubkey(), new_forester_authority); - rpc.create_and_send_transaction(&[ix], &forester_authority.pubkey(), &[forester_authority]) + let (signers, new_forester_authority) = if let Some(new_authority) = new_forester_authority { + pre_account_state.authority = new_authority.pubkey(); + + ( + vec![forester_authority, &new_authority], + Some(new_authority.pubkey()), + ) + } else { + (vec![forester_authority], None) + }; + let ix = create_update_forester_pda_instruction( + &forester_authority.pubkey(), + new_forester_authority, + config, + ); + + rpc.create_and_send_transaction(&[ix], &forester_authority.pubkey(), &signers) .await?; - pre_account_state.authority = *new_forester_authority; + + pre_account_state.config = config; assert_registered_forester(rpc, &forester_authority.pubkey(), pre_account_state).await } pub async fn assert_registered_forester( rpc: &mut R, forester: &Pubkey, - expected_account: ForesterEpoch, + expected_account: ForesterAccount, ) -> Result<(), RpcError> { - let pda = get_forester_epoch_pda_address(forester).0; + let pda = get_forester_pda_address(forester).0; let account_data = rpc - .get_anchor_account::(&pda) + .get_anchor_account::(&pda) .await? .unwrap(); if account_data != expected_account { @@ -164,6 +209,7 @@ pub async fn create_rollover_address_merkle_tree_instructions( new_address_merkle_tree_keypair: &Keypair, merkle_tree_pubkey: &Pubkey, nullifier_queue_pubkey: &Pubkey, + epoch: u64, ) -> Vec { let (merkle_tree_config, queue_config) = get_address_bundle_config( rpc, @@ -194,14 +240,14 @@ pub async fn create_rollover_address_merkle_tree_instructions( &account_compression::ID, Some(new_address_merkle_tree_keypair), ); - let instruction = create_rollover_address_merkle_tree_instruction( + let instruction = light_registry::account_compression_cpi::sdk::create_rollover_address_merkle_tree_instruction( CreateRolloverMerkleTreeInstructionInputs { authority: *authority, new_queue: new_nullifier_queue_keypair.pubkey(), new_merkle_tree: new_address_merkle_tree_keypair.pubkey(), old_queue: *nullifier_queue_pubkey, old_merkle_tree: *merkle_tree_pubkey, - }, + },epoch ); vec![ create_nullifier_queue_instruction, @@ -217,6 +263,7 @@ pub async fn perform_state_merkle_tree_roll_over( new_state_merkle_tree_keypair: &Keypair, merkle_tree_pubkey: &Pubkey, nullifier_queue_pubkey: &Pubkey, + epoch: u64, ) -> Result<(), RpcError> { let instructions = create_rollover_address_merkle_tree_instructions( rpc, @@ -225,6 +272,7 @@ pub async fn perform_state_merkle_tree_roll_over( new_state_merkle_tree_keypair, merkle_tree_pubkey, nullifier_queue_pubkey, + epoch, ) .await; rpc.create_and_send_transaction( @@ -239,7 +287,7 @@ pub async fn perform_state_merkle_tree_roll_over( .await?; Ok(()) } - +#[allow(clippy::too_many_arguments)] pub async fn create_rollover_state_merkle_tree_instructions( rpc: &mut R, authority: &Pubkey, @@ -248,6 +296,7 @@ pub async fn create_rollover_state_merkle_tree_instructions( merkle_tree_pubkey: &Pubkey, nullifier_queue_pubkey: &Pubkey, cpi_context: &Pubkey, + epoch: u64, ) -> Vec { let (merkle_tree_config, queue_config) = get_state_bundle_config( rpc, @@ -275,17 +324,944 @@ pub async fn create_rollover_state_merkle_tree_instructions( &account_compression::ID, Some(new_state_merkle_tree_keypair), ); - let instruction = - create_rollover_state_merkle_tree_instruction(CreateRolloverMerkleTreeInstructionInputs { + let instruction = create_rollover_state_merkle_tree_instruction( + CreateRolloverMerkleTreeInstructionInputs { authority: *authority, new_queue: new_nullifier_queue_keypair.pubkey(), new_merkle_tree: new_state_merkle_tree_keypair.pubkey(), old_queue: *nullifier_queue_pubkey, old_merkle_tree: *merkle_tree_pubkey, - }); + }, + epoch, + ); vec![ create_nullifier_queue_instruction, create_state_merkle_tree_instruction, instruction, ] } + +pub async fn mint_standard_tokens>( + rpc: &mut R, + indexer: &mut I, + authority: &Keypair, + recipient: &Pubkey, + amount: u64, + merkle_tree: &Pubkey, +) -> Result { + let protocol_config_pda = get_protocol_config_pda_address().0; + let protocol_config = rpc + .get_anchor_account::(&protocol_config_pda) + .await? + .unwrap(); + let mint = protocol_config.config.mint; + let ix = light_registry::sdk::create_mint_to_instruction( + &mint, + &authority.pubkey(), + recipient, + amount, + merkle_tree, + ); + + let (event, signature, _) = rpc + .create_and_send_transaction_with_event::( + &[ix], + &authority.pubkey(), + &[authority], + None, + ) + .await? + .unwrap(); + indexer.add_event_and_compressed_accounts(&event); + Ok(signature) +} + +pub struct DepositInputs<'a> { + pub sender: &'a Keypair, + pub amount: u64, + pub delegate_account: Option>, + pub input_token_data: Vec, + pub input_escrow_token_account: Option, + pub epoch: u64, +} + +pub struct WithdrawInputs<'a> { + pub sender: &'a Keypair, + pub amount: u64, + pub delegate_account: FetchedAccount, + pub input_escrow_token_account: TokenDataWithContext, +} + +pub async fn deposit_test<'a, R: RpcConnection, I: Indexer>( + rpc: &mut R, + indexer: &mut I, + inputs: DepositInputs<'a>, +) -> Result { + deposit_or_withdraw_test::(rpc, indexer, inputs).await +} + +pub async fn withdraw_test<'a, R: RpcConnection, I: Indexer>( + rpc: &mut R, + indexer: &mut I, + inputs: WithdrawInputs<'a>, +) -> Result { + let inputs = DepositInputs { + sender: inputs.sender, + amount: inputs.amount, + delegate_account: Some(inputs.delegate_account), + input_token_data: Vec::new(), + input_escrow_token_account: Some(inputs.input_escrow_token_account), + epoch: 0, + }; + deposit_or_withdraw_test::(rpc, indexer, inputs).await +} +pub async fn deposit_or_withdraw_test< + 'a, + R: RpcConnection, + I: Indexer, + const IS_DEPOSIT: bool, +>( + rpc: &mut R, + indexer: &mut I, + inputs: DepositInputs<'a>, +) -> Result { + let mut input_compressed_accounts = Vec::new(); + + inputs.input_token_data.iter().for_each(|t| { + input_compressed_accounts.push(t.compressed_account.clone()); + }); + + if let Some(escrow_token_account) = inputs.input_escrow_token_account.as_ref() { + input_compressed_accounts.push(escrow_token_account.compressed_account.clone()); + } + let first_mt = if let Some(token_data_with_context) = inputs.input_escrow_token_account.as_ref() + { + token_data_with_context + .compressed_account + .merkle_context + .merkle_tree_pubkey + } else { + inputs.input_token_data[0] + .compressed_account + .merkle_context + .merkle_tree_pubkey + }; + + if let Some(delegate_account) = inputs.delegate_account.as_ref() { + input_compressed_accounts.push(delegate_account.comporessed_account.clone()); + }; + + let cpi_context_account = indexer.get_state_merkle_tree_accounts(&[first_mt])[0].cpi_context; + + let input_hashes = input_compressed_accounts + .iter() + .map(|a| a.hash().unwrap()) + .collect::>(); + let proof_rpc_result = indexer + .create_proof_for_compressed_accounts( + Some(&input_hashes), + Some( + &input_compressed_accounts + .iter() + .map(|a| a.merkle_context.merkle_tree_pubkey) + .collect::>(), + ), + None, + None, + rpc, + ) + .await; + let delegate_account = if let Some(input_pda) = inputs.delegate_account { + let input_delegate_compressed_account = DelegateAccountWithContext { + delegate_account: input_pda.deserialized_account, + merkle_context: input_pda.comporessed_account.merkle_context, + output_merkle_tree_index: input_pda + .comporessed_account + .merkle_context + .merkle_tree_pubkey, + }; + Some(input_delegate_compressed_account) + } else { + None + }; + let input_token_data = inputs + .input_token_data + .iter() + .map(|t| t.token_data) + .collect::>(); + let input_compressed_accounts = inputs + .input_token_data + .iter() + .map(|t| t.compressed_account.clone()) + .collect::>(); + let input_escrow_token_account = inputs + .input_escrow_token_account + .map(|t| (t.token_data, t.compressed_account)); + let create_deposit_instruction_inputs = CreateDepositInstructionInputs { + sender: inputs.sender.pubkey(), + cpi_context_account, + salt: 0, + delegate_account, + amount: inputs.amount, + input_token_data, + input_compressed_accounts, + input_escrow_token_account, + escrow_token_account_merkle_tree: first_mt, + change_compressed_account_merkle_tree: first_mt, + output_delegate_compressed_account_merkle_tree: first_mt, + proof: proof_rpc_result.proof, + root_indices: proof_rpc_result.root_indices, + }; + let ix = create_deposit_instruction::(create_deposit_instruction_inputs.clone()); + + let (event, signature, _) = rpc + .create_and_send_transaction_with_event::( + &[ix], + &inputs.sender.pubkey(), + &[inputs.sender], + None, + ) + .await? + .unwrap(); + let (created_output_accounts, created_output_token_accounts) = + indexer.add_event_and_compressed_accounts(&event); + assert_deposit_or_withdrawal::( + created_output_accounts, + created_output_token_accounts, + create_deposit_instruction_inputs, + inputs.epoch, + ); + Ok(signature) +} + +/// Expecting: +/// 1. pda account with stake weight add asigned deposit amount +/// 2. escrow account with add assigned deposit amount +/// 3. change account inputs sub deposit amount +pub fn assert_deposit_or_withdrawal( + created_output_accounts: Vec, + created_output_token_accounts: Vec, + inputs: CreateDepositInstructionInputs, + epoch: u64, +) { + let escrow_authority_pda = get_escrow_token_authority(&inputs.sender, inputs.salt).0; + + let expected_escrow_token_data = + if let Some((mut escrow_token_data, _)) = inputs.input_escrow_token_account.clone() { + assert_eq!( + escrow_token_data.owner, escrow_authority_pda, + "input owner mismatch" + ); + if IS_DEPOSIT { + escrow_token_data.amount += inputs.amount; + } else { + escrow_token_data.amount -= inputs.amount; + } + escrow_token_data + } else { + TokenData { + owner: escrow_authority_pda, + amount: inputs.amount, + mint: inputs.input_token_data[0].mint, + delegate: None, + state: light_compressed_token::token_data::AccountState::Initialized, + } + }; + let output_escrow_token_data = created_output_token_accounts[0].token_data; + assert_eq!(output_escrow_token_data, expected_escrow_token_data); + + let expected_delegate_account = if let Some(mut input_pda) = inputs.delegate_account.clone() { + input_pda.delegate_account.escrow_token_account_hash = + output_escrow_token_data.hash::().unwrap(); + if IS_DEPOSIT { + input_pda.delegate_account.stake_weight += inputs.amount; + } else { + input_pda.delegate_account.stake_weight -= inputs.amount; + } + // input_pda.delegate_account.last_sync_epoch = epoch; + input_pda.delegate_account + } else { + DelegateAccount { + owner: inputs.sender, + stake_weight: inputs.amount, + pending_undelegated_stake_weight: 0, + pending_epoch: 0, + delegated_stake_weight: 0, + delegate_forester_delegate_account: None, + last_sync_epoch: epoch, + pending_token_amount: 0, + escrow_token_account_hash: output_escrow_token_data.hash::().unwrap(), + pending_synced_stake_weight: 0, + pending_delegated_stake_weight: 0, + } + }; + let output_delegate_account = DelegateAccount::deserialize_reader( + &mut &created_output_accounts[0] + .compressed_account + .data + .as_ref() + .unwrap() + .data[..], + ) + .unwrap(); + println!("assert epoch {}", epoch); + assert_eq!(output_delegate_account, expected_delegate_account); +} + +pub struct DelegateInputs<'a> { + pub sender: &'a Keypair, + pub amount: u64, + pub delegate_account: FetchedAccount, + pub forester_pda: Pubkey, + pub no_sync: bool, + pub output_merkle_tree: Pubkey, +} + +pub struct UndelegateInputs<'a> { + pub sender: &'a Keypair, + pub amount: u64, + pub delegate_account: FetchedAccount, + pub forester_pda: Pubkey, + pub no_sync: bool, + pub output_merkle_tree: Pubkey, +} + +pub async fn delegate_test<'a, R: RpcConnection, I: Indexer>( + rpc: &mut R, + indexer: &mut I, + inputs: DelegateInputs<'a>, +) -> Result { + delegate_or_undelegate_test::(rpc, indexer, inputs).await +} + +pub async fn undelegate_test<'a, R: RpcConnection, I: Indexer>( + rpc: &mut R, + indexer: &mut I, + inputs: UndelegateInputs<'a>, +) -> Result { + let inputs = DelegateInputs { + sender: inputs.sender, + amount: inputs.amount, + delegate_account: inputs.delegate_account, + forester_pda: inputs.forester_pda, + no_sync: inputs.no_sync, + output_merkle_tree: inputs.output_merkle_tree, + }; + delegate_or_undelegate_test::(rpc, indexer, inputs).await +} +pub async fn delegate_or_undelegate_test< + 'a, + R: RpcConnection, + I: Indexer, + const IS_DEPOSIT: bool, +>( + rpc: &mut R, + indexer: &mut I, + inputs: DelegateInputs<'a>, +) -> Result { + let input_compressed_accounts = vec![inputs.delegate_account.comporessed_account.clone()]; + + let input_hashes = input_compressed_accounts + .iter() + .map(|a| a.hash().unwrap()) + .collect::>(); + let proof_rpc_result = indexer + .create_proof_for_compressed_accounts( + Some(&input_hashes), + Some( + &input_compressed_accounts + .iter() + .map(|a| a.merkle_context.merkle_tree_pubkey) + .collect::>(), + ), + None, + None, + rpc, + ) + .await; + let delegate_account = DelegateAccountWithContext { + delegate_account: inputs.delegate_account.deserialized_account, + merkle_context: inputs.delegate_account.comporessed_account.merkle_context, + output_merkle_tree_index: inputs + .delegate_account + .comporessed_account + .merkle_context + .merkle_tree_pubkey, + }; + + let create_deposit_instruction_inputs = CreateDelegateInstructionInputs { + sender: inputs.sender.pubkey(), + delegate_account, + amount: inputs.amount, + output_delegate_compressed_account_merkle_tree: inputs.output_merkle_tree, + proof: proof_rpc_result.proof, + forester_pda: inputs.forester_pda, + root_index: proof_rpc_result.root_indices[0], + no_sync: inputs.no_sync, + }; + let ix = create_delegate_instruction::(create_deposit_instruction_inputs.clone()); + println!("trying to fetch forester pda"); + let pre_forester_pda = rpc + .get_anchor_account::(&inputs.forester_pda) + .await + .unwrap() + .unwrap(); + + let (event, signature, _) = rpc + .create_and_send_transaction_with_event::( + &[ix], + &inputs.sender.pubkey(), + &[inputs.sender], + None, + ) + .await? + .unwrap(); + let (created_output_accounts, _) = indexer.add_event_and_compressed_accounts(&event); + assert_delegate_or_undelegate::( + rpc, + created_output_accounts, + create_deposit_instruction_inputs, + pre_forester_pda, + ) + .await; + Ok(signature) +} + +pub async fn assert_delegate_or_undelegate( + rpc: &mut R, + created_output_accounts: Vec, + inputs: CreateDelegateInstructionInputs, + pre_forester_pda: ForesterAccount, +) { + let protocol_config_pda = get_protocol_config_pda_address().0; + let protocol_config: ProtocolConfigPda = rpc + .get_anchor_account::(&protocol_config_pda) + .await + .unwrap() + .unwrap(); + let current_slot = rpc.get_slot().await.unwrap(); + + let forester_pda: ForesterAccount = rpc + .get_anchor_account::(&inputs.forester_pda) + .await + .unwrap() + .unwrap(); + { + let expected_forester_pda = { + let mut input_pda = pre_forester_pda.clone(); + if !inputs.no_sync { + input_pda + .sync(current_slot, &protocol_config.config) + .unwrap(); + } + if IS_DEPOSIT { + input_pda.pending_undelegated_stake_weight += inputs.amount; + } else { + input_pda.active_stake_weight -= inputs.amount; + } + input_pda + }; + assert_eq!(forester_pda, expected_forester_pda); + } + let current_epoch = forester_pda.last_registered_epoch; + + let expected_delegate_account = { + let mut input_pda = inputs.delegate_account.clone(); + if current_epoch > input_pda.delegate_account.pending_epoch { + input_pda.delegate_account.stake_weight += + input_pda.delegate_account.pending_undelegated_stake_weight; + input_pda.delegate_account.pending_undelegated_stake_weight = 0; + // last sync epoch is only relevant for syncing the delegate account with the forester rewards + // self.last_sync_epoch = current_epoch; + input_pda.delegate_account.delegated_stake_weight += + input_pda.delegate_account.pending_delegated_stake_weight; + input_pda.delegate_account.pending_delegated_stake_weight = 0; + // self.pending_epoch = 0; + } + input_pda.delegate_account.pending_epoch = current_epoch; + // input_pda.delegate_account.last_sync_epoch = current_epoch; + if IS_DEPOSIT { + input_pda.delegate_account.stake_weight -= inputs.amount; + input_pda.delegate_account.pending_delegated_stake_weight += inputs.amount; + } else { + input_pda.delegate_account.delegated_stake_weight -= inputs.amount; + input_pda.delegate_account.pending_undelegated_stake_weight += inputs.amount; + } + if input_pda.delegate_account.delegated_stake_weight != 0 + || input_pda.delegate_account.pending_delegated_stake_weight != 0 + { + input_pda + .delegate_account + .delegate_forester_delegate_account = Some(inputs.forester_pda); + } else { + input_pda + .delegate_account + .delegate_forester_delegate_account = None; + } + input_pda.delegate_account + }; + let output_delegate_account = DelegateAccount::deserialize_reader( + &mut &created_output_accounts[0] + .compressed_account + .data + .as_ref() + .unwrap() + .data[..], + ) + .unwrap(); + assert_eq!(output_delegate_account, expected_delegate_account); +} + +pub struct ClaimForesterInputs<'a> { + pub sender: &'a Keypair, + pub amount: u64, + pub delegate_account: FetchedAccount, + pub forester_pda: Pubkey, + pub no_sync: bool, + pub output_merkle_tree: Pubkey, +} + +pub async fn forester_claim_test<'a, R: RpcConnection, I: Indexer>( + rpc: &mut R, + indexer: &mut I, + forester: &Keypair, + epoch: u64, + output_merkle_tree: Pubkey, +) -> Result { + let ix = create_forester_claim_instruction(forester.pubkey(), epoch, output_merkle_tree); + let forester_pda = get_forester_pda_address(&forester.pubkey()).0; + let pre_forester_pda = rpc + .get_anchor_account::(&forester_pda) + .await + .unwrap() + .unwrap(); + let forester_epoch_pda_pubkey = get_forester_epoch_pda_address(&forester_pda, epoch).0; + let pre_forester_epoch_pda = rpc + .get_anchor_account::(&forester_epoch_pda_pubkey) + .await + .unwrap() + .unwrap(); + + let token_pool = get_forester_token_pool_pda(&forester.pubkey()); + let pre_token_pool_account = rpc.get_account(token_pool).await.unwrap().unwrap(); + let (event, signature, _) = rpc + .create_and_send_transaction_with_events::( + &[ix], + &forester.pubkey(), + &[forester], + None, + ) + .await? + .unwrap(); + println!("events {:?}", event); + let (created_output_accounts, _) = indexer.add_event_and_compressed_accounts(&event[0]); + let (_, created_token_accounts) = indexer.add_event_and_compressed_accounts(&event[1]); + // currently we only get the first of two public transaction events + assert_forester_claim::( + rpc, + created_output_accounts, + created_token_accounts, + forester, + epoch, + pre_forester_pda, + pre_token_pool_account, + pre_forester_epoch_pda, + ) + .await; + Ok(signature) +} + +pub async fn assert_forester_claim( + rpc: &mut R, + created_output_accounts: Vec, + created_token_accounts: Vec, + forester: &Keypair, + epoch: u64, + pre_forester_pda: ForesterAccount, + pre_token_pool_account: Account, + pre_forester_epoch_pda: ForesterEpochPda, +) { + let forester_pda_pubkey = get_forester_pda_address(&forester.pubkey()).0; + // assert compressed account + let rewards = { + let deserialized_compressed_epoch_account = + CompressedForesterEpochAccount::deserialize_reader( + &mut &created_output_accounts[0] + .compressed_account + .data + .as_ref() + .unwrap() + .data[..], + ) + .unwrap(); + let expected_compressed_epoch_account = CompressedForesterEpochAccount { + rewards_earned: deserialized_compressed_epoch_account.rewards_earned, + epoch, + stake_weight: pre_forester_epoch_pda.stake_weight, // this doesn't have to be true since the active stake weight can change in registration phase + previous_hash: pre_forester_pda.last_compressed_forester_epoch_pda_hash, + forester_pda_pubkey, + }; + println!( + "deserialized_compressed_epoch_account {:?}", + deserialized_compressed_epoch_account + ); + assert!(expected_compressed_epoch_account.rewards_earned > 0); + assert_eq!( + deserialized_compressed_epoch_account, + expected_compressed_epoch_account + ); + deserialized_compressed_epoch_account.rewards_earned + }; + // assert token pool update + let mint = { + let pre_amount = spl_token::state::Account::unpack(&pre_token_pool_account.data) + .unwrap() + .amount; + let token_pool_pda_pubkey = get_forester_token_pool_pda(&forester.pubkey()); + let post_account = rpc + .get_account(token_pool_pda_pubkey) + .await + .unwrap() + .unwrap(); + let unpacked_post_account = spl_token::state::Account::unpack(&post_account.data).unwrap(); + assert_eq!((unpacked_post_account.amount - pre_amount), rewards); + unpacked_post_account.mint + }; + + let forester_pda = rpc + .get_anchor_account::(&forester_pda_pubkey) + .await + .unwrap() + .unwrap(); + { + let expected_forester_pda = { + let mut input_pda = pre_forester_pda.clone(); + + input_pda.last_compressed_forester_epoch_pda_hash = created_output_accounts[0] + .compressed_account + .data + .as_ref() + .unwrap() + .data_hash; + input_pda.active_stake_weight += rewards; + input_pda.last_claimed_epoch = epoch; + input_pda + }; + assert_eq!(forester_pda, expected_forester_pda); + } + let forester_epoch_pda_pubkey = get_forester_epoch_pda_address(&forester_pda_pubkey, epoch).0; + let forester_epoch_pda = rpc + .get_anchor_account::(&forester_epoch_pda_pubkey) + .await + .unwrap(); + assert!( + forester_epoch_pda.is_none(), + "Forester epoch pda should be closed." + ); + // assert forester fee compressed token account + { + let token_data = created_token_accounts[0].token_data; + let forester_fee = pre_forester_pda.config.fee as f64 / 100.0; + let forester_rewards = rewards as f64 / (1.0 - forester_fee) * forester_fee; + let expected_token_data = TokenData { + owner: forester.pubkey(), + amount: forester_rewards as u64, + mint, + delegate: None, + state: light_compressed_token::token_data::AccountState::Initialized, + }; + assert_eq!(token_data, expected_token_data); + } +} + +pub struct SyncDelegateInputs<'a> { + pub sender: &'a Keypair, + // pub amount: u64, + pub delegate_account: FetchedAccount, + pub forester: Pubkey, + pub output_merkle_tree: Pubkey, + pub input_escrow_token_account: Option, + pub compressed_forester_epoch_pdas: Vec>>, + // TODO: remove and get from epoch - 1 of compressed_forester_epoch_pdas + pub previous_hash: [u8; 32], + pub sync_delegate_token_account: bool, + // pub last_account_merkle_context: MerkleContext, +} + +pub async fn sync_delegate_test<'a, R: RpcConnection, I: Indexer>( + rpc: &mut R, + indexer: &mut I, + inputs: SyncDelegateInputs<'a>, +) -> Result { + let mut input_compressed_accounts = vec![inputs.delegate_account.comporessed_account.clone()]; + let input_escrow_token_account = + if let Some(escrow_token_account) = inputs.input_escrow_token_account { + input_compressed_accounts.push(escrow_token_account.compressed_account.clone()); + Some(( + escrow_token_account.token_data, + escrow_token_account.compressed_account, + )) + } else { + None + }; + + let input_hashes = input_compressed_accounts + .iter() + .map(|a| a.hash().unwrap()) + .collect::>(); + println!("input_hashes {:?}", input_hashes); + let proof_rpc_result = indexer + .create_proof_for_compressed_accounts( + Some(&input_hashes), + Some( + &input_compressed_accounts + .iter() + .map(|a| a.merkle_context.merkle_tree_pubkey) + .collect::>(), + ), + None, + None, + rpc, + ) + .await; + let delegate_account = DelegateAccountWithContext { + delegate_account: inputs.delegate_account.deserialized_account, + merkle_context: inputs.delegate_account.comporessed_account.merkle_context, + output_merkle_tree_index: inputs + .delegate_account + .comporessed_account + .merkle_context + .merkle_tree_pubkey, + }; + let first_mt = inputs + .delegate_account + .comporessed_account + .merkle_context + .merkle_tree_pubkey; + + let cpi_context_account = indexer.get_state_merkle_tree_accounts(&[first_mt])[0].cpi_context; + + let mut compressed_forester_epoch_pdas = inputs + .compressed_forester_epoch_pdas + .iter() + .filter(|a| a.is_some()) + .map(|a| { + let x = a.as_ref().unwrap().deserialized_account; + + CompressedForesterEpochAccountInput { + rewards_earned: x.rewards_earned, + epoch: x.epoch, + stake_weight: x.stake_weight, + } + }) + .filter(|a| { + if inputs.delegate_account.deserialized_account.last_sync_epoch == 0 { + true + } else { + a.epoch > inputs.delegate_account.deserialized_account.last_sync_epoch + } + }) + .collect::>(); + compressed_forester_epoch_pdas.reverse(); + println!( + "compressed_forester_epoch_pdas {:?}", + compressed_forester_epoch_pdas.len() + ); + if compressed_forester_epoch_pdas.is_empty() { + return Ok(Signature::default()); + } + let last_account = inputs + .compressed_forester_epoch_pdas + .iter() + .filter(|a| a.is_some()) + .last() + .unwrap() + .as_ref() + .unwrap() + .comporessed_account + .clone(); + let create_instruction_inputs = CreateSyncDelegateInstructionInputs { + sender: inputs.sender.pubkey(), + delegate_account, + output_delegate_compressed_account_merkle_tree: inputs.output_merkle_tree, + proof: proof_rpc_result.proof, + salt: 0, + cpi_context_account, + output_token_account_merkle_tree: inputs.output_merkle_tree, + root_indices: proof_rpc_result.root_indices, + input_escrow_token_account, + forester_pubkey: inputs.forester, + previous_hash: inputs.previous_hash, + compressed_forester_epoch_pdas: compressed_forester_epoch_pdas.clone(), + last_account_root_index: 0, //TODO: add once we verify the proof onchain + sync_delegate_token_account: inputs.sync_delegate_token_account, + last_account_merkle_context: last_account.merkle_context, + }; + let ix = create_sync_delegate_instruction(create_instruction_inputs.clone()); + println!("delegate_account {:?}", delegate_account); + + let token_pool = get_forester_token_pool_pda(&inputs.forester); + let pre_token_pool_account = rpc.get_account(token_pool).await.unwrap().unwrap(); + let (event, signature, _) = rpc + .create_and_send_transaction_with_events::( + &[ix], + &inputs.sender.pubkey(), + &[inputs.sender], + None, + ) + .await? + .unwrap(); + let (created_output_accounts, created_token_accounts) = + indexer.add_event_and_compressed_accounts(&event[0]); + println!("created_output_accounts {:?}", created_output_accounts); + println!("created_token_accounts {:?}", created_token_accounts); + assert_sync_delegate::( + rpc, + created_output_accounts, + created_token_accounts, + create_instruction_inputs, + pre_token_pool_account, + compressed_forester_epoch_pdas, + ) + .await; + Ok(signature) +} + +/// Expecting: +/// 1. update compressed token escrow account +/// 2. update delegate account +/// 3. updated token pool account +pub async fn assert_sync_delegate( + rpc: &mut R, + created_output_accounts: Vec, + created_output_token_account: Vec, + inputs: CreateSyncDelegateInstructionInputs, + pre_token_pool_account: Account, + input_compressed_epochs: Vec, +) { + let rewards = if let Some((token_data, _input_escrow_token_account)) = + inputs.input_escrow_token_account + { + let actual_amount = created_output_token_account[0].token_data.amount - token_data.amount; + println!("actual_amount {:?}", actual_amount); + println!( + "created_output_token_account {:?}", + created_output_token_account + ); + println!("input_compressed_epochs {:?}", input_compressed_epochs); + // account holds all stakeweight should get all rewards + if inputs + .delegate_account + .delegate_account + .delegated_stake_weight + == input_compressed_epochs[0].stake_weight + { + let sum_rewards = input_compressed_epochs + .iter() + .map(|a| a.rewards_earned) + .sum::(); + // TODO: can I check that users cannot invoke if there is nothing to claim? + // assert_eq!(sum_rewards, actual_amount); + // assert!(0 < actual_amount); + assert!(actual_amount <= sum_rewards); + } else { + let sum_rewards = input_compressed_epochs + .iter() + .map(|a| a.rewards_earned) + .sum::(); + // assert!(0 < actual_amount); + // assert_eq!(sum_rewards, actual_amount); + assert!(actual_amount <= sum_rewards); + } + Some(actual_amount) + } else { + None + }; + + // assert token pool update + if let Some(rewards) = rewards { + let pre_amount = spl_token::state::Account::unpack(&pre_token_pool_account.data) + .unwrap() + .amount; + let token_pool_pda_pubkey = get_forester_token_pool_pda(&inputs.forester_pubkey); + let post_account = rpc + .get_account(token_pool_pda_pubkey) + .await + .unwrap() + .unwrap(); + let unpacked_post_account = spl_token::state::Account::unpack(&post_account.data).unwrap(); + assert_eq!((pre_amount - unpacked_post_account.amount), rewards); + } + + let updated_delegate_account = created_output_accounts[0].clone(); + let deserialized_delegate_account = DelegateAccount::deserialize_reader( + &mut &updated_delegate_account + .compressed_account + .data + .as_ref() + .unwrap() + .data[..], + ) + .unwrap(); + let epoch = input_compressed_epochs.last().unwrap().epoch; + println!("\n\n epoch {:?} \n\n", epoch); + let expected_delegate_account = if let Some(rewards) = rewards { + let expected_delegate_account = { + let mut input_pda = inputs.delegate_account.delegate_account.clone(); + if epoch > input_pda.pending_epoch { + input_pda.stake_weight += input_pda.pending_undelegated_stake_weight; + input_pda.pending_undelegated_stake_weight = 0; + + input_pda.delegated_stake_weight += input_pda.pending_delegated_stake_weight; + input_pda.pending_delegated_stake_weight = 0; + input_pda.pending_token_amount = 0; + } + input_pda.delegated_stake_weight += rewards; + // pending epoch doesnt change because it is just responsible for + // syncing delegated + // input_pda.pending_epoch = 0; + // input_pda.pending_epoch = epoch; + input_pda.last_sync_epoch = epoch; + input_pda.pending_synced_stake_weight = + deserialized_delegate_account.pending_synced_stake_weight; + + input_pda + }; + expected_delegate_account + } else { + let expected_delegate_account = { + let mut input_pda = inputs.delegate_account.delegate_account.clone(); + input_pda.stake_weight += input_pda.pending_undelegated_stake_weight; + input_pda.pending_undelegated_stake_weight = 0; + let sum_rewards = input_compressed_epochs + .iter() + .map(|a| a.rewards_earned) + .sum::(); + input_pda.delegated_stake_weight += + sum_rewards + input_pda.pending_delegated_stake_weight; + input_pda.pending_delegated_stake_weight = 0; + input_pda.pending_token_amount += sum_rewards; + // input_pda.pending_epoch = epoch; + input_pda.last_sync_epoch = epoch; + input_pda.pending_synced_stake_weight = + deserialized_delegate_account.pending_synced_stake_weight; + // input_compressed_epochs + // .iter() + // .last() + // .unwrap() + // .rewards_earned; + + input_pda + }; + expected_delegate_account + }; + let actual_amount = deserialized_delegate_account.pending_synced_stake_weight; + let last_epoch_reward = input_compressed_epochs + .iter() + .last() + .unwrap() + .rewards_earned; + // assert!(0 < actual_amount); + assert!(actual_amount <= last_epoch_reward); + assert_eq!(deserialized_delegate_account, expected_delegate_account); +} diff --git a/test-utils/src/rpc/rpc_connection.rs b/test-utils/src/rpc/rpc_connection.rs index 99587836b7..73af885ad7 100644 --- a/test-utils/src/rpc/rpc_connection.rs +++ b/test-utils/src/rpc/rpc_connection.rs @@ -35,6 +35,16 @@ pub trait RpcConnection: Clone + Send + Sync + Debug + 'static { where T: AnchorDeserialize + Send + Debug; + fn create_and_send_transaction_with_events( + &mut self, + instruction: &[Instruction], + authority: &Pubkey, + signers: &[&Keypair], + transaction_params: Option, + ) -> impl std::future::Future, Signature, Slot)>, RpcError>> + Send + where + T: AnchorDeserialize + Send + Debug; + fn create_and_send_transaction<'a>( &'a mut self, instruction: &'a [Instruction], diff --git a/test-utils/src/rpc/solana_rpc.rs b/test-utils/src/rpc/solana_rpc.rs index 92d0c3f35d..fddc939d38 100644 --- a/test-utils/src/rpc/solana_rpc.rs +++ b/test-utils/src/rpc/solana_rpc.rs @@ -135,6 +135,18 @@ impl RpcConnection for SolanaRpcConnection { let client = RpcClient::new_with_commitment(url, commitment_config); Self { client, payer } } + async fn create_and_send_transaction_with_events( + &mut self, + _instructions: &[Instruction], + _payer: &Pubkey, + _signers: &[&Keypair], + _transaction_params: Option, + ) -> Result, Signature, u64)>, RpcError> + where + T: AnchorDeserialize + Debug, + { + unimplemented!("create_and_send_transaction_with_events") + } async fn create_and_send_transaction_with_event( &mut self, diff --git a/test-utils/src/rpc/test_rpc.rs b/test-utils/src/rpc/test_rpc.rs index 6df0ce9f69..1a094f72f3 100644 --- a/test-utils/src/rpc/test_rpc.rs +++ b/test-utils/src/rpc/test_rpc.rs @@ -182,6 +182,137 @@ impl RpcConnection for ProgramTestRpcConnection { Ok(result) } + async fn create_and_send_transaction_with_events( + &mut self, + instruction: &[Instruction], + payer: &Pubkey, + signers: &[&Keypair], + transaction_params: Option, + ) -> Result, Signature, Slot)>, RpcError> + where + T: AnchorDeserialize, + { + let pre_balance = self + .context + .banks_client + .get_account(*payer) + .await? + .unwrap() + .lamports; + + let transaction = Transaction::new_signed_with_payer( + instruction, + Some(payer), + signers, + self.context.get_new_latest_blockhash().await?, + ); + + let signature = transaction.signatures[0]; + // Simulate the transaction. Currently, in banks-client/server, only + // simulations are able to track CPIs. Therefore, simulating is the + // only way to retrieve the event. + let simulation_result = self + .context + .banks_client + .simulate_transaction(transaction.clone()) + .await?; + // Handle an error nested in the simulation result. + if let Some(Err(e)) = simulation_result.result { + let error = match e { + TransactionError::InstructionError(_, _) => RpcError::TransactionError(e), + _ => RpcError::from(BanksClientError::TransactionError(e)), + }; + return Err(error); + } + + // Retrieve the event. + // let event = simulation_result + // .simulation_details + // .and_then(|details| details.inner_instructions) + // .and_then(|instructions| { + // instructions.iter().flatten().find_map(|inner_instruction| { + // T::try_from_slice(inner_instruction.instruction.data.as_slice()).ok() + // }) + // }); + let events: Vec = simulation_result + .simulation_details + .and_then(|details| details.inner_instructions) + .map(|instructions| { + instructions + .iter() + .flatten() + .filter_map(|inner_instruction| { + T::try_from_slice(inner_instruction.instruction.data.as_slice()).ok() + }) + .collect() + }) + .unwrap_or_default(); + // If transaction was successful, execute it. + if let Some(Ok(())) = simulation_result.result { + let result = self + .context + .banks_client + .process_transaction(transaction) + .await; + if let Err(e) = result { + let error = RpcError::from(e); + return Err(error); + } + } + + // assert correct rollover fee and network_fee distribution + if let Some(transaction_params) = transaction_params { + let mut deduped_signers = signers.to_vec(); + deduped_signers.dedup(); + let post_balance = self.get_account(*payer).await?.unwrap().lamports; + + // a network_fee is charged if there are input compressed accounts or new addresses + let mut network_fee: i64 = 0; + if transaction_params.num_input_compressed_accounts != 0 { + network_fee += transaction_params.fee_config.network_fee as i64; + } + if transaction_params.num_new_addresses != 0 { + network_fee += transaction_params.fee_config.address_network_fee as i64; + } + let expected_post_balance = pre_balance as i64 + - i64::from(transaction_params.num_new_addresses) + * transaction_params.fee_config.address_queue_rollover as i64 + - i64::from(transaction_params.num_output_compressed_accounts) + * transaction_params.fee_config.state_merkle_tree_rollover as i64 + - transaction_params.compress + - transaction_params.fee_config.solana_network_fee * deduped_signers.len() as i64 + - network_fee; + + if post_balance as i64 != expected_post_balance { + println!("transaction_params: {:?}", transaction_params); + println!("pre_balance: {}", pre_balance); + println!("post_balance: {}", post_balance); + println!("expected post_balance: {}", expected_post_balance); + println!( + "diff post_balance: {}", + post_balance as i64 - expected_post_balance + ); + println!( + "rollover fee: {}", + transaction_params.fee_config.state_merkle_tree_rollover + ); + println!( + "address_network_fee: {}", + transaction_params.fee_config.address_network_fee + ); + println!("network_fee: {}", network_fee); + println!("num signers {}", deduped_signers.len()); + return Err(RpcError::from(BanksClientError::TransactionError( + TransactionError::InstructionError(0, InstructionError::Custom(11111)), + ))); + } + } + + let slot = self.context.banks_client.get_root_slot().await?; + let result = Some((events, signature, slot)); + Ok(result) + } + async fn confirm_transaction(&mut self, _transaction: Signature) -> Result { Ok(true) } diff --git a/test-utils/src/spl.rs b/test-utils/src/spl.rs index 3cfdd9b30a..d0b7c93c1c 100644 --- a/test-utils/src/spl.rs +++ b/test-utils/src/spl.rs @@ -166,20 +166,43 @@ pub async fn create_token_pool( } pub async fn create_mint_helper(rpc: &mut R, payer: &Keypair) -> Pubkey { + let mint: Keypair = Keypair::new(); + + create_mint_helper_with_keypair(rpc, payer, &mint, &None).await +} + +pub async fn create_mint_helper_with_keypair( + rpc: &mut R, + payer: &Keypair, + mint: &Keypair, + mint_authority: &Option, +) -> Pubkey { let payer_pubkey = payer.pubkey(); let rent = rpc .get_minimum_balance_for_rent_exemption(Mint::LEN) .await .unwrap(); - let mint = Keypair::new(); - let (instructions, pool) = - create_initialize_mint_instructions(&payer_pubkey, &payer_pubkey, rent, 2, &mint); + let (instructions, pool) = create_initialize_mint_instructions( + &payer_pubkey, + &payer_pubkey, + rent, + 2, + mint, + mint_authority, + ); - rpc.create_and_send_transaction(&instructions, &payer_pubkey, &[payer, &mint]) + rpc.create_and_send_transaction(&instructions, &payer_pubkey, &[payer, mint]) .await .unwrap(); - assert_create_mint(rpc, &payer_pubkey, &mint.pubkey(), &pool).await; + assert_create_mint( + rpc, + &payer_pubkey, + &mint.pubkey(), + &pool, + &mint_authority.unwrap_or(payer_pubkey), + ) + .await; mint.pubkey() } @@ -207,15 +230,17 @@ pub fn create_initialize_mint_instructions( rent: u64, decimals: u8, mint_keypair: &Keypair, + mint_authority: &Option, ) -> ([Instruction; 4], Pubkey) { let account_create_ix = create_account_instruction(payer, Mint::LEN, rent, &spl_token::ID, Some(mint_keypair)); let mint_pubkey = mint_keypair.pubkey(); + let mint_authority = mint_authority.unwrap_or(*authority); let create_mint_instruction = initialize_mint( &spl_token::ID, &mint_keypair.pubkey(), - authority, + &mint_authority, Some(authority), decimals, ) @@ -371,14 +396,13 @@ pub async fn compressed_transfer_test>( let instruction = create_transfer_instruction( &payer.pubkey(), &from.pubkey(), // authority - &input_merkle_tree_context, &output_compressed_accounts, &proof_rpc_result.root_indices, &Some(proof_rpc_result.proof), &input_compressed_account_token_data, // input_token_data &input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), *mint, @@ -504,13 +528,9 @@ pub async fn decompress_test>( let mint = input_compressed_accounts[0].token_data.mint; let instruction = create_transfer_instruction( &rpc.get_payer().pubkey(), - &payer.pubkey(), // authority - &input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect::>(), // input_compressed_account_merkle_tree_pubkeys + &payer.pubkey(), // authority &[change_out_compressed_account], // output_compressed_accounts - &proof_rpc_result.root_indices, // root_indices + &proof_rpc_result.root_indices, // root_indices &Some(proof_rpc_result.proof), input_compressed_accounts .iter() @@ -519,7 +539,7 @@ pub async fn decompress_test>( .as_slice(), // input_token_data &input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint, // mint @@ -616,7 +636,6 @@ pub async fn compress_test>( let instruction = create_transfer_instruction( &rpc.get_payer().pubkey(), &payer.pubkey(), // authority - &Vec::new(), // input_compressed_account_merkle_tree_pubkeys &[output_compressed_account], // output_compressed_accounts &Vec::new(), // root_indices &None, @@ -732,17 +751,13 @@ pub async fn approve_test>( let inputs = CreateApproveInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: authority.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint, @@ -891,17 +906,13 @@ pub async fn revoke_test>( let inputs = CreateRevokeInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: authority.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), mint, @@ -1041,17 +1052,13 @@ pub async fn freeze_or_thaw_test>(), outputs_merkle_tree: *outputs_merkle_tree, @@ -1324,17 +1331,13 @@ pub async fn create_burn_test_instruction>( let inputs = CreateBurnInstructionInputs { fee_payer: rpc.get_payer().pubkey(), authority: authority.pubkey(), - input_merkle_contexts: input_compressed_accounts - .iter() - .map(|x| x.compressed_account.merkle_context) - .collect(), input_token_data: input_compressed_accounts .iter() .map(|x| x.token_data) .collect(), input_compressed_accounts: input_compressed_accounts .iter() - .map(|x| &x.compressed_account.compressed_account) + .map(|x| &x.compressed_account) .cloned() .collect::>(), change_account_merkle_tree: *change_account_merkle_tree, diff --git a/test-utils/src/test_env.rs b/test-utils/src/test_env.rs index 0e8767a539..940b724a8f 100644 --- a/test-utils/src/test_env.rs +++ b/test-utils/src/test_env.rs @@ -1,13 +1,21 @@ -use std::cmp; - use crate::assert_address_merkle_tree::assert_address_merkle_tree_initialized; +use crate::assert_epoch::{ + assert_epoch_pda, assert_finalized_epoch_registration, assert_registered_forester_pda, +}; use crate::assert_queue::assert_address_queue_initialized; -use crate::create_account_instruction; -use crate::registry::register_test_forester; +use crate::e2e_test_env::{E2ETestEnv, GeneralActionConfig, KeypairActionConfig, TestForester}; +use crate::forester_epoch::{Epoch, Forester, TreeAccounts, TreeType}; +use crate::indexer::{Indexer, TestIndexer, TokenDataWithContext}; +use crate::registry::{ + delegate_test, deposit_test, mint_standard_tokens, register_test_forester, DelegateInputs, + DepositInputs, +}; use crate::rpc::rpc_connection::RpcConnection; use crate::rpc::solana_rpc::SolanaRpcUrl; use crate::rpc::test_rpc::ProgramTestRpcConnection; use crate::rpc::SolanaRpcConnection; +use crate::spl::{approve_test, create_mint_helper_with_keypair}; +use crate::{create_account_instruction, get_custom_compressed_account, FetchedAccount}; use account_compression::sdk::create_initialize_address_merkle_tree_and_queue_instruction; use account_compression::utils::constants::GROUP_AUTHORITY_SEED; use account_compression::{ @@ -18,18 +26,25 @@ use account_compression::{NullifierQueueConfig, StateMerkleTreeConfig}; use anchor_lang::{system_program, InstructionData, ToAccountMetas}; use light_hasher::Poseidon; use light_macros::pubkey; -use light_registry::get_forester_epoch_pda_address; +use light_registry::delegate::get_escrow_token_authority; +use light_registry::delegate::state::DelegateAccount; +use light_registry::protocol_config::state::ProtocolConfig; use light_registry::sdk::{ - create_initialize_governance_authority_instruction, + create_finalize_registration_instruction, create_initialize_governance_authority_instruction, create_initialize_group_authority_instruction, create_register_program_instruction, - get_cpi_authority_pda, get_governance_authority_pda, get_group_pda, }; +use light_registry::utils::{ + get_cpi_authority_pda, get_epoch_pda_address, get_forester_pda_address, get_group_pda, + get_protocol_config_pda_address, +}; +use light_registry::{ForesterAccount, ForesterConfig, ForesterEpochPda}; use light_system_program::utils::get_registered_program_pda; use solana_program_test::{ProgramTest, ProgramTestContext}; use solana_sdk::{ pubkey::Pubkey, signature::Keypair, signature::Signer, system_instruction, transaction::Transaction, }; +use std::cmp; pub const CPI_CONTEXT_ACCOUNT_RENT: u64 = 143487360; // lamports of the cpi context account pub const NOOP_PROGRAM_ID: Pubkey = pubkey!("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"); @@ -57,6 +72,7 @@ pub async fn setup_test_programs( program_test.set_compute_max_units(1_400_000u64); program_test.start_with_context().await } + #[derive(Debug)] pub struct EnvAccounts { pub merkle_tree_pubkey: Pubkey, @@ -70,7 +86,8 @@ pub struct EnvAccounts { pub address_merkle_tree_pubkey: Pubkey, pub address_merkle_tree_queue_pubkey: Pubkey, pub cpi_context_account_pubkey: Pubkey, - pub registered_forester_epoch_pda: Pubkey, + pub registered_forester_pda: Pubkey, + pub forester_epoch: Option, } // Hardcoded keypairs for deterministic pubkeys for testing @@ -144,6 +161,13 @@ pub const FORESTER_TEST_KEYPAIR: [u8; 64] = [ 204, ]; +pub const STANDARD_TOKEN_MINT_KEYPAIR: [u8; 64] = [ + 158, 84, 243, 82, 179, 195, 150, 115, 253, 116, 105, 208, 172, 232, 56, 204, 78, 134, 49, 115, + 154, 29, 215, 9, 15, 210, 232, 206, 164, 0, 143, 230, 23, 199, 87, 14, 203, 181, 205, 0, 91, + 12, 187, 82, 112, 64, 102, 81, 107, 38, 243, 254, 236, 101, 20, 225, 114, 9, 216, 84, 233, 71, + 60, 166, +]; + /// Setup test programs with accounts /// deploys: /// 1. light program @@ -156,9 +180,35 @@ pub const FORESTER_TEST_KEYPAIR: [u8; 64] = [ /// 6. creates and initializes group authority /// 7. registers the light_system_program program with the group authority /// 8. initializes Merkle tree owned by - +/// Note: +/// - registers a forester +/// - advances to the active phase slot 2 +/// - active phase doesn't end pub async fn setup_test_programs_with_accounts( additional_programs: Option>, +) -> (ProgramTestRpcConnection, EnvAccounts) { + let token_mint_keypair = Keypair::from_bytes(STANDARD_TOKEN_MINT_KEYPAIR.as_slice()).unwrap(); + + setup_test_programs_with_accounts_with_protocol_config( + additional_programs, + ProtocolConfig { + // Init with an active epoch which doesn't end + active_phase_length: 1_000_000_000, + slot_length: 1_000_000_000, + genesis_slot: 0, + registration_phase_length: 2, + mint: token_mint_keypair.pubkey(), + ..Default::default() + }, + true, + ) + .await +} + +pub async fn setup_test_programs_with_accounts_with_protocol_config( + additional_programs: Option>, + protocol_config: ProtocolConfig, + register_forester_and_advance_to_active_phase: bool, ) -> (ProgramTestRpcConnection, EnvAccounts) { use crate::airdrop_lamports; @@ -170,26 +220,44 @@ pub async fn setup_test_programs_with_accounts( airdrop_lamports(&mut context, &forester.pubkey(), 10_000_000_000) .await .unwrap(); - let env_accounts = initialize_accounts(&mut context, &payer, &forester).await; + let env_accounts = initialize_accounts( + &mut context, + &payer, + &forester, + protocol_config, + register_forester_and_advance_to_active_phase, + ) + .await; (context, env_accounts) } pub async fn setup_accounts_devnet(payer: &Keypair, forester: &Keypair) -> EnvAccounts { let mut rpc = SolanaRpcConnection::new(SolanaRpcUrl::Devnet, None); - initialize_accounts(&mut rpc, payer, forester).await + initialize_accounts(&mut rpc, payer, forester, ProtocolConfig::default(), false).await } pub async fn initialize_accounts( context: &mut R, payer: &Keypair, forester: &Keypair, + protocol_config: ProtocolConfig, + register_forester_and_advance_to_active_phase: bool, ) -> EnvAccounts { let cpi_authority_pda = get_cpi_authority_pda(); - let authority_pda = get_governance_authority_pda(); + let authority_pda = get_protocol_config_pda_address(); + let token_mint_keypair = Keypair::from_bytes(STANDARD_TOKEN_MINT_KEYPAIR.as_slice()).unwrap(); + println!("mint keypair: {:?}", token_mint_keypair.pubkey()); + create_mint_helper_with_keypair( + context, + payer, + &token_mint_keypair, + &Some(cpi_authority_pda.0), + ) + .await; let instruction = - create_initialize_governance_authority_instruction(payer.pubkey(), payer.pubkey()); + create_initialize_governance_authority_instruction(payer.pubkey(), protocol_config); context .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) .await @@ -207,9 +275,19 @@ pub async fn initialize_accounts( assert_eq!(gov_authority.authority, payer.pubkey()); println!("forester: {:?}", forester.pubkey()); - register_test_forester(context, payer, &forester.pubkey()) - .await - .unwrap(); + register_test_forester( + context, + payer, + &forester, + ForesterConfig { + fee: 1, + fee_recipient: forester.pubkey(), + }, + ) + .await + .unwrap(); + println!("Registered register_test_forester "); + let system_program_id_test_keypair = Keypair::from_bytes(&SYSTEM_PROGRAM_ID_TEST_KEYPAIR).unwrap(); register_program_with_registry_program( @@ -224,7 +302,7 @@ pub async fn initialize_accounts( register_program_with_registry_program(context, payer, &group_pda, ®istry_id_test_keypair) .await .unwrap(); - + println!("Registered system program"); let merkle_tree_keypair = Keypair::from_bytes(&MERKLE_TREE_TEST_KEYPAIR).unwrap(); let merkle_tree_pubkey = merkle_tree_keypair.pubkey(); let nullifier_queue_keypair = Keypair::from_bytes(&NULLIFIER_QUEUE_TEST_KEYPAIR).unwrap(); @@ -266,6 +344,42 @@ pub async fn initialize_accounts( init_cpi_context_account(context, &merkle_tree_pubkey, &cpi_signature_keypair, payer).await; let registered_system_program_pda = get_registered_program_pda(&light_system_program::ID); let registered_registry_program_pda = get_registered_program_pda(&light_registry::ID); + let forester_epoch = if register_forester_and_advance_to_active_phase { + let mut registered_epoch = Epoch::register(context, &protocol_config, forester) + .await + .unwrap() + .unwrap(); + context + .warp_to_slot(registered_epoch.phases.active.start) + .unwrap(); + let tree_accounts = vec![ + TreeAccounts { + tree_type: TreeType::State, + merkle_tree: merkle_tree_pubkey, + queue: nullifier_queue_pubkey, + is_rolledover: false, + }, + TreeAccounts { + tree_type: TreeType::Address, + merkle_tree: address_merkle_tree_keypair.pubkey(), + queue: address_merkle_tree_queue_keypair.pubkey(), + is_rolledover: false, + }, + ]; + + registered_epoch + .fetch_account_and_add_trees_with_schedule(context, tree_accounts.clone()) + .await + .unwrap(); + let ix = create_finalize_registration_instruction(&forester.pubkey(), 0); + context + .create_and_send_transaction(&[ix], &forester.pubkey(), &[forester]) + .await + .unwrap(); + Some(registered_epoch) + } else { + None + }; EnvAccounts { merkle_tree_pubkey, nullifier_queue_pubkey, @@ -278,10 +392,166 @@ pub async fn initialize_accounts( address_merkle_tree_queue_pubkey: address_merkle_tree_queue_keypair.pubkey(), cpi_context_account_pubkey: cpi_signature_keypair.pubkey(), registered_registry_program_pda, - registered_forester_epoch_pda: get_forester_epoch_pda_address(&forester.pubkey()).0, + registered_forester_pda: get_forester_pda_address(&forester.pubkey()).0, + forester_epoch, } } +pub async fn set_env_with_delegate_and_forester( + protocol_config: Option, + keypair_config: Option, + general_config: Option, + rounds: u64, + seed: Option, +) -> ( + crate::e2e_test_env::E2ETestEnv< + ProgramTestRpcConnection, + TestIndexer, + >, + Keypair, + EnvAccounts, + Vec, + Epoch, +) { + let protocol_config = protocol_config.unwrap_or_default(); + let (rpc, env) = + setup_test_programs_with_accounts_with_protocol_config(None, protocol_config, false).await; + let indexer: TestIndexer = TestIndexer::init_from_env( + &env.forester.insecure_clone(), + &env, + KeypairActionConfig::all_default().inclusion(), + KeypairActionConfig::all_default().non_inclusion(), + ) + .await; + let mut e2e_env = + E2ETestEnv::>::new( + rpc, + indexer, + &env, + keypair_config.unwrap_or(KeypairActionConfig::all_default()), + general_config.unwrap_or_default(), + rounds, + seed, + ) + .await; + let delegate_keypair = create_delegate( + &mut e2e_env, + &env, + 1_000_000, + env.registered_forester_pda, + 0, + None, + ) + .await; + + // TODO: remove + let tree_accounts = vec![ + TreeAccounts { + tree_type: TreeType::State, + merkle_tree: env.merkle_tree_pubkey, + queue: env.nullifier_queue_pubkey, + is_rolledover: false, + }, + TreeAccounts { + tree_type: TreeType::Address, + merkle_tree: env.address_merkle_tree_pubkey, + queue: env.address_merkle_tree_queue_pubkey, + is_rolledover: false, + }, + ]; + let slot = protocol_config.genesis_slot + protocol_config.active_phase_length; + e2e_env.rpc.warp_to_slot(slot).unwrap(); + + let registered_epoch = Epoch::register(&mut e2e_env.rpc, &protocol_config, &env.forester) + .await + .unwrap(); + assert!(registered_epoch.is_some()); + let mut registered_epoch = registered_epoch.unwrap(); + let forester_epoch_pda: ForesterEpochPda = e2e_env + .rpc + .get_anchor_account::(®istered_epoch.forester_epoch_pda) + .await + .unwrap() + .unwrap(); + assert_eq!(forester_epoch_pda.stake_weight, 1_000_000); + assert!(forester_epoch_pda.total_epoch_state_weight.is_none()); + // we advanced to the next epoch so that delegated pending stake becomes active + assert_eq!(forester_epoch_pda.epoch, 1); + let epoch = forester_epoch_pda.epoch; + let forester_pda_pubkey = get_forester_pda_address(&env.forester.pubkey()).0; + let forester_pda = e2e_env + .rpc + .get_anchor_account::(&forester_pda_pubkey) + .await + .unwrap() + .unwrap(); + println!("forester_pda: {:?}", forester_pda); + let expected_stake = forester_pda.active_stake_weight; + println!("expected_stake: {}", expected_stake); + assert_epoch_pda(&mut e2e_env.rpc, epoch, expected_stake).await; + + assert_registered_forester_pda( + &mut e2e_env.rpc, + ®istered_epoch.forester_epoch_pda, + &env.forester.pubkey(), + epoch, + ) + .await; + let current_slot = e2e_env.rpc.get_slot().await.unwrap(); + e2e_env + .rpc + .warp_to_slot(current_slot + protocol_config.active_phase_length) + .unwrap(); + let ix = create_finalize_registration_instruction(&env.forester.pubkey(), epoch); + e2e_env + .rpc + .create_and_send_transaction(&[ix], &env.forester.pubkey(), &[&env.forester]) + .await + .unwrap(); + let epoch_pda = get_epoch_pda_address(epoch); + assert_finalized_epoch_registration( + &mut e2e_env.rpc, + ®istered_epoch.forester_epoch_pda, + &epoch_pda, + ) + .await; + // Refetch after finalization + let forester_epoch_pda: ForesterEpochPda = e2e_env + .rpc + .get_anchor_account::(®istered_epoch.forester_epoch_pda) + .await + .unwrap() + .unwrap(); + let current_solana_slot = e2e_env.rpc.get_slot().await.unwrap(); + registered_epoch.add_trees_with_schedule( + &forester_epoch_pda, + tree_accounts.clone(), + current_solana_slot, + ); + let _forester = Forester { + registration: registered_epoch.clone(), + active: registered_epoch.clone(), + ..Default::default() + }; + // Forester epoch account is assumed to exist (is inited with test program deployment) + let forester = TestForester { + keypair: env.forester.insecure_clone(), + forester: _forester.clone(), + is_registered: Some(0), + }; + e2e_env.foresters.push(forester); + e2e_env.epoch_config = _forester; + e2e_env.epoch = epoch; + + ( + e2e_env, + delegate_keypair, + env, + tree_accounts, + registered_epoch, + ) +} + pub async fn initialize_new_group( group_seed_keypair: &Keypair, payer: &Keypair, @@ -331,7 +601,7 @@ pub fn get_test_env_accounts() -> EnvAccounts { let group_pda = get_group_pda(group_seed_keypair.pubkey()); let payer = Keypair::from_bytes(&PAYER_KEYPAIR).unwrap(); - let authority_pda = get_governance_authority_pda(); + let authority_pda = get_protocol_config_pda_address(); let (_, registered_program_pda) = create_register_program_instruction( payer.pubkey(), authority_pda, @@ -354,13 +624,14 @@ pub fn get_test_env_accounts() -> EnvAccounts { group_pda, governance_authority: payer, governance_authority_pda: authority_pda.0, - registered_forester_epoch_pda: get_forester_epoch_pda_address(&forester.pubkey()).0, + registered_forester_pda: get_forester_pda_address(&forester.pubkey()).0, forester, registered_program_pda, address_merkle_tree_pubkey: address_merkle_tree_keypair.pubkey(), address_merkle_tree_queue_pubkey: address_merkle_tree_queue_keypair.pubkey(), cpi_context_account_pubkey: cpi_signature_keypair.pubkey(), registered_registry_program_pda, + forester_epoch: None, } } @@ -376,7 +647,7 @@ pub async fn create_state_merkle_tree_and_queue_account( merkle_tree_config: &StateMerkleTreeConfig, queue_config: &NullifierQueueConfig, ) { - use light_registry::sdk::create_initialize_merkle_tree_instruction as create_initialize_merkle_tree_instruction_registry; + use light_registry::account_compression_cpi::sdk::create_initialize_merkle_tree_instruction as create_initialize_merkle_tree_instruction_registry; let size = account_compression::state::StateMerkleTreeAccount::size( merkle_tree_config.height as usize, merkle_tree_config.changelog_size as usize, @@ -457,7 +728,7 @@ pub async fn create_address_merkle_tree_and_queue_account( queue_config: &AddressQueueConfig, index: u64, ) { - use light_registry::sdk::create_initialize_address_merkle_tree_and_queue_instruction as create_initialize_address_merkle_tree_and_queue_instruction_registry; + use light_registry::account_compression_cpi::sdk::create_initialize_address_merkle_tree_and_queue_instruction as create_initialize_address_merkle_tree_and_queue_instruction_registry; let size = account_compression::state::QueueAccount::size(queue_config.capacity as usize).unwrap(); @@ -638,14 +909,17 @@ pub async fn register_program_with_registry_program( group_pda: &Pubkey, program_id_keypair: &Keypair, ) -> Result { - let governance_authority_pda = get_governance_authority_pda(); + let governance_authority_pda = get_protocol_config_pda_address(); let (instruction, token_program_registered_program_pda) = create_register_program_instruction( governance_authority.pubkey(), governance_authority_pda, *group_pda, program_id_keypair.pubkey(), ); - let cpi_authority_pda = get_cpi_authority_pda(); + println!("isnt {:?}", instruction.accounts); + println!("governance authority {:?}", governance_authority.pubkey()); + println!("program id {:?}", program_id_keypair.pubkey()); + let cpi_authority_pda = light_registry::utils::get_cpi_authority_pda(); let transfer_instruction = system_instruction::transfer( &governance_authority.pubkey(), &cpi_authority_pda.0, @@ -662,3 +936,140 @@ pub async fn register_program_with_registry_program( .await?; Ok(token_program_registered_program_pda) } + +pub async fn create_delegate( + e2e_env: &mut crate::e2e_test_env::E2ETestEnv< + ProgramTestRpcConnection, + TestIndexer, + >, + env: &EnvAccounts, + deposit_amount: u64, + forester_pda: Pubkey, + epoch: u64, + delegate_keypair: Option, +) -> Keypair { + let (delegate_keypair, delegate_account, delegate_escrow) = + if let Some(delegate_keypair) = delegate_keypair { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + let escrow_account = e2e_env.indexer.get_compressed_token_accounts_by_owner( + &get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0, + ); + ( + delegate_keypair, + delegate_account[0].clone(), + Some(escrow_account[0].clone()), + ) + } else { + (Keypair::new(), None, None) + }; + e2e_env + .rpc + .airdrop_lamports(&delegate_keypair.pubkey(), 1_000_000_000) + .await + .unwrap(); + + mint_standard_tokens::>( + &mut e2e_env.rpc, + &mut e2e_env.indexer, + &env.governance_authority, + &delegate_keypair.pubkey(), + 1_000_000_000, + &env.merkle_tree_pubkey, + ) + .await + .unwrap(); + // let forester_pda = env.registered_forester_pda; + deposit_to_delegate_account_helper( + e2e_env, + &delegate_keypair, + deposit_amount, + env, + epoch, + delegate_account, + delegate_escrow, + ) + .await; + // delegate to forester + { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + println!("delegate pre: delegate_account: {:?}", delegate_account); + let inputs = DelegateInputs { + sender: &delegate_keypair, + amount: deposit_amount, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + forester_pda, + no_sync: false, + output_merkle_tree: env.merkle_tree_pubkey, + }; + delegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) + .await + .unwrap(); + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut e2e_env.indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + println!("delegate post: delegate_account: {:?}", delegate_account); + } + delegate_keypair +} + +pub async fn deposit_to_delegate_account_helper( + e2e_env: &mut crate::e2e_test_env::E2ETestEnv< + ProgramTestRpcConnection, + TestIndexer, + >, + delegate_keypair: &Keypair, + deposit_amount: u64, + env: &EnvAccounts, + epoch: u64, + delegate_account: Option>, + input_escrow_token_account: Option, +) { + let escrow_pda_authority = get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0; + + let token_accounts = e2e_env + .indexer + .get_compressed_token_accounts_by_owner(&delegate_keypair.pubkey()); + // approve amount is expected to equal deposit amount + approve_test( + &delegate_keypair, + &mut e2e_env.rpc, + &mut e2e_env.indexer, + token_accounts, + deposit_amount, + None, + &escrow_pda_authority, + &env.merkle_tree_pubkey, + &env.merkle_tree_pubkey, + None, + ) + .await; + let token_accounts = e2e_env + .indexer + .get_compressed_token_accounts_by_owner(&delegate_keypair.pubkey()) + .iter() + .filter(|a| a.token_data.delegate.is_some()) + .cloned() + .collect::>(); + + let deposit_inputs = DepositInputs { + sender: &delegate_keypair, + amount: deposit_amount, + delegate_account, + input_token_data: token_accounts, + input_escrow_token_account, + epoch, + }; + deposit_test(&mut e2e_env.rpc, &mut e2e_env.indexer, deposit_inputs) + .await + .unwrap(); +} diff --git a/test-utils/src/test_forester.rs b/test-utils/src/test_forester.rs index 7c0e76f44b..8ab0f71505 100644 --- a/test-utils/src/test_forester.rs +++ b/test-utils/src/test_forester.rs @@ -13,11 +13,13 @@ use anchor_lang::{InstructionData, ToAccountMetas}; use light_concurrent_merkle_tree::event::MerkleTreeEvent; use light_hasher::Poseidon; use light_indexed_merkle_tree::copy::IndexedMerkleTreeCopy; -use light_registry::sdk::{ + +use light_registry::account_compression_cpi::sdk::{ create_nullify_instruction, create_update_address_merkle_tree_instruction, CreateNullifyInstructionInputs, UpdateAddressMerkleTreeInstructionInputs, }; -use light_registry::{get_forester_epoch_pda_address, ForesterEpoch, RegisterForester}; +use light_registry::utils::{get_forester_epoch_pda_address, get_forester_pda_address}; +use light_registry::{ForesterEpochPda, RegisterForester}; use light_utils::bigint::bigint_to_be_bytes_array; use log::debug; use solana_sdk::signature::Signature; @@ -47,16 +49,21 @@ pub async fn nullify_compressed_accounts( rpc: &mut R, forester: &Keypair, state_tree_bundle: &mut StateMerkleTreeBundle, + epoch: u64, ) { let nullifier_queue = unsafe { get_hash_set::(rpc, state_tree_bundle.accounts.nullifier_queue).await }; + let forester_pda = get_forester_pda_address(&forester.pubkey()).0; + let forester_epoch_pda = get_forester_epoch_pda_address(&forester_pda, epoch).0; + println!("forester_epoch_pda: {:?}", forester_epoch_pda); + println!("epoch {:?}", epoch); let pre_forester_counter = rpc - .get_anchor_account::(&get_forester_epoch_pda_address(&forester.pubkey()).0) + .get_anchor_account::(&forester_epoch_pda) .await .unwrap() .unwrap() - .counter; + .work_counter; let onchain_merkle_tree = get_concurrent_merkle_tree::( rpc, @@ -110,16 +117,19 @@ pub async fn nullify_compressed_accounts( .to_array::<16>() .unwrap() .to_vec(); - let ix = create_nullify_instruction(CreateNullifyInstructionInputs { - authority: forester.pubkey(), - nullifier_queue: state_tree_bundle.accounts.nullifier_queue, - merkle_tree: state_tree_bundle.accounts.merkle_tree, - change_log_indices: vec![change_log_index], - leaves_queue_indices: vec![*index_in_nullifier_queue as u16], - indices: vec![leaf_index as u64], - proofs: vec![proof], - derivation: forester.pubkey(), - }); + let ix = create_nullify_instruction( + CreateNullifyInstructionInputs { + authority: forester.pubkey(), + nullifier_queue: state_tree_bundle.accounts.nullifier_queue, + merkle_tree: state_tree_bundle.accounts.merkle_tree, + change_log_indices: vec![change_log_index], + leaves_queue_indices: vec![*index_in_nullifier_queue as u16], + indices: vec![leaf_index as u64], + proofs: vec![proof], + derivation: forester.pubkey(), + }, + epoch, + ); let instructions = [ix]; let event = rpc @@ -186,9 +196,11 @@ pub async fn nullify_compressed_accounts( onchain_merkle_tree.root(), state_tree_bundle.merkle_tree.root() ); + let forester_pda = get_forester_pda_address(&forester.pubkey()).0; + assert_forester_counter( rpc, - &get_forester_epoch_pda_address(&forester.pubkey()).0, + &get_forester_epoch_pda_address(&forester_pda, epoch).0, pre_forester_counter, num_nullified, ) @@ -238,16 +250,16 @@ pub async fn assert_forester_counter( num_nullified: u64, ) -> Result<(), RpcError> { let account = rpc - .get_anchor_account::(pubkey) + .get_anchor_account::(pubkey) .await? .unwrap(); - if account.counter != pre + num_nullified { - debug!("account.counter: {}", account.counter); + if account.work_counter != pre + num_nullified { + debug!("account.work_counter: {}", account.work_counter); debug!("pre: {}", pre); debug!("num_nullified: {}", num_nullified); debug!("forester pubkey: {:?}", pubkey); return Err(RpcError::CustomError( - "Forester counter not updated correctly".to_string(), + "ForesterEpochPda counter not updated correctly".to_string(), )); } Ok(()) @@ -269,6 +281,7 @@ pub async fn empty_address_queue_test( rpc: &mut R, address_tree_bundle: &mut AddressMerkleTreeBundle, signer_is_owner: bool, + epoch: u64, ) -> Result<(), RelayerUpdateError> { let address_merkle_tree_pubkey = address_tree_bundle.accounts.merkle_tree; let address_queue_pubkey = address_tree_bundle.accounts.queue; @@ -288,13 +301,13 @@ pub async fn empty_address_queue_test( let mut counter = 0; loop { let pre_forester_counter = if !signer_is_owner { - rpc.get_anchor_account::( - &get_forester_epoch_pda_address(&forester.pubkey()).0, - ) - .await - .map_err(|e| RelayerUpdateError::RpcError)? - .unwrap() - .counter + let forester_pda = get_forester_pda_address(&forester.pubkey()).0; + let forester_epoch_pda = get_forester_epoch_pda_address(&forester_pda, epoch).0; + rpc.get_anchor_account::(&forester_epoch_pda) + .await + .map_err(|e| RelayerUpdateError::RpcError)? + .unwrap() + .work_counter } else { 0 }; @@ -344,6 +357,7 @@ pub async fn empty_address_queue_test( Some(changelog_index), Some(indexed_changelog_index), signer_is_owner, + epoch, ) .await { @@ -437,14 +451,11 @@ pub async fn empty_address_queue_test( if update_successful { if !signer_is_owner { - assert_forester_counter( - rpc, - &get_forester_epoch_pda_address(&forester.pubkey()).0, - pre_forester_counter, - 1, - ) - .await - .unwrap(); + let forester_pda = get_forester_pda_address(&forester.pubkey()).0; + let forester_epoch_pda = get_forester_epoch_pda_address(&forester_pda, epoch).0; + assert_forester_counter(rpc, &forester_epoch_pda, pre_forester_counter, 1) + .await + .unwrap(); } let merkle_tree = get_indexed_merkle_tree::( @@ -543,6 +554,7 @@ pub async fn update_merkle_tree( changelog_index: Option, indexed_changelog_index: Option, signer_is_owner: bool, + epoch: u64, ) -> Result, RpcError> { let changelog_index = match changelog_index { Some(changelog_index) => changelog_index, @@ -571,19 +583,22 @@ pub async fn update_merkle_tree( } }; let update_ix = if !signer_is_owner { - create_update_address_merkle_tree_instruction(UpdateAddressMerkleTreeInstructionInputs { - authority: forester.pubkey(), - address_merkle_tree: address_merkle_tree_pubkey, - address_queue: address_queue_pubkey, - changelog_index, - indexed_changelog_index, - value, - low_address_index, - low_address_value, - low_address_next_index, - low_address_next_value, - low_address_proof, - }) + create_update_address_merkle_tree_instruction( + UpdateAddressMerkleTreeInstructionInputs { + authority: forester.pubkey(), + address_merkle_tree: address_merkle_tree_pubkey, + address_queue: address_queue_pubkey, + changelog_index, + indexed_changelog_index, + value, + low_address_index, + low_address_value, + low_address_next_index, + low_address_next_value, + low_address_proof, + }, + epoch, + ) } else { let instruction_data = UpdateAddressMerkleTree { changelog_index, From 8d29922ef22b04f77e2941f14291f8fd16bba21f Mon Sep 17 00:00:00 2001 From: ananas-block Date: Thu, 1 Aug 2024 00:50:01 +0100 Subject: [PATCH 2/4] registry: added access control in accounts --- js/stateless.js/src/idls/light_registry.ts | 4 +- .../account_compression_cpi/access_control.rs | 0 .../src/account_compression_cpi/nullify.rs | 7 +- .../register_program.rs | 24 ++-- .../rollover_state_tree.rs | 7 +- .../update_address_tree.rs | 15 ++- programs/registry/src/constants.rs | 4 + .../src/decentralization_and_contention.rs | 3 +- .../src/delegate/delegate_instruction.rs | 19 ++-- programs/registry/src/delegate/deposit.rs | 9 -- .../src/delegate/deposit_instruction.rs | 3 +- .../src/epoch/claim_forester_instruction.rs | 18 ++- programs/registry/src/epoch/register_epoch.rs | 8 +- programs/registry/src/epoch/report_work.rs | 5 +- programs/registry/src/epoch/sync_delegate.rs | 15 ++- .../src/epoch/sync_delegate_instruction.rs | 21 ++-- programs/registry/src/forester/state.rs | 42 ++++--- programs/registry/src/lib.rs | 105 ++++++------------ .../src/protocol_config/initialize.rs | 18 +-- programs/registry/src/protocol_config/mint.rs | 27 +++-- .../registry/src/protocol_config/state.rs | 6 - .../registry/src/protocol_config/update.rs | 16 +-- programs/registry/src/sdk.rs | 39 ++++--- programs/registry/src/utils.rs | 42 ++++++- test-programs/registry-test/tests/tests.rs | 4 +- test-utils/src/registry.rs | 13 +-- test-utils/src/test_env.rs | 13 +-- 27 files changed, 239 insertions(+), 248 deletions(-) delete mode 100644 programs/registry/src/account_compression_cpi/access_control.rs create mode 100644 programs/registry/src/constants.rs diff --git a/js/stateless.js/src/idls/light_registry.ts b/js/stateless.js/src/idls/light_registry.ts index 727a3393d9..74f8b3b614 100644 --- a/js/stateless.js/src/idls/light_registry.ts +++ b/js/stateless.js/src/idls/light_registry.ts @@ -3,7 +3,7 @@ export type LightRegistry = { name: 'light_registry'; constants: [ { - name: 'AUTHORITY_PDA_SEED'; + name: 'PROTOCOL_CONFIG_PDA_SEED'; type: 'bytes'; value: '[97, 117, 116, 104, 111, 114, 105, 116, 121]'; }, @@ -1079,7 +1079,7 @@ export const IDL: LightRegistry = { name: 'light_registry', constants: [ { - name: 'AUTHORITY_PDA_SEED', + name: 'PROTOCOL_CONFIG_PDA_SEED', type: 'bytes', value: '[97, 117, 116, 104, 111, 114, 105, 116, 121]', }, diff --git a/programs/registry/src/account_compression_cpi/access_control.rs b/programs/registry/src/account_compression_cpi/access_control.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/programs/registry/src/account_compression_cpi/nullify.rs b/programs/registry/src/account_compression_cpi/nullify.rs index 722ce97fd5..7be49dc398 100644 --- a/programs/registry/src/account_compression_cpi/nullify.rs +++ b/programs/registry/src/account_compression_cpi/nullify.rs @@ -1,4 +1,6 @@ -use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; +use account_compression::{ + program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED, RegisteredProgram, +}; use anchor_lang::prelude::*; use crate::epoch::register_epoch::ForesterEpochPda; @@ -17,8 +19,7 @@ pub struct NullifyLeaves<'info> { #[account( seeds = [&crate::ID.to_bytes()], bump, seeds::program = &account_compression::ID, )] - pub registered_program_pda: - Account<'info, account_compression::instructions::register_program::RegisteredProgram>, + pub registered_program_pda: Account<'info, RegisteredProgram>, pub account_compression_program: Program<'info, AccountCompression>, /// CHECK: when emitting event. pub log_wrapper: UncheckedAccount<'info>, diff --git a/programs/registry/src/account_compression_cpi/register_program.rs b/programs/registry/src/account_compression_cpi/register_program.rs index 32c774538a..e015998e83 100644 --- a/programs/registry/src/account_compression_cpi/register_program.rs +++ b/programs/registry/src/account_compression_cpi/register_program.rs @@ -3,25 +3,29 @@ use account_compression::{ }; use anchor_lang::prelude::*; -use crate::{protocol_config::state::ProtocolConfigPda, AUTHORITY_PDA_SEED}; +use crate::{protocol_config::state::ProtocolConfigPda, PROTOCOL_CONFIG_PDA_SEED}; #[derive(Accounts)] -pub struct RegisteredProgram<'info> { - #[account(mut, constraint = authority.key() == authority_pda.authority)] +pub struct RegisterSystemProgram<'info> { + /// CHECK: authority is protocol config authority. + #[account(mut, constraint = authority.key() == protocol_config_pda.authority)] pub authority: Signer<'info>, - /// CHECK: - #[account(mut, seeds = [AUTHORITY_PDA_SEED], bump)] - pub authority_pda: Account<'info, ProtocolConfigPda>, - /// CHECK: this is - #[account(mut, seeds = [CPI_AUTHORITY_PDA_SEED], bump)] + /// CHECK: (seed constraints). + #[account(seeds = [PROTOCOL_CONFIG_PDA_SEED], bump)] + pub protocol_config_pda: Account<'info, ProtocolConfigPda>, + /// CHECK: (seed constraint). + #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] pub cpi_authority: AccountInfo<'info>, + /// CHECK: (account compression program). #[account(mut)] pub group_pda: Account<'info, GroupAuthority>, pub account_compression_program: Program<'info, AccountCompression>, pub system_program: Program<'info, System>, - /// CHECK: + /// CHECK: is created by the account compression program. #[account(mut)] pub registered_program_pda: AccountInfo<'info>, - /// CHECK: is checked in the account compression program. + /// CHECK: (account compression program). + /// Keypair of the program being registered is signer to prevent a third + /// party from registering it to a security group. pub program_to_be_registered: Signer<'info>, } diff --git a/programs/registry/src/account_compression_cpi/rollover_state_tree.rs b/programs/registry/src/account_compression_cpi/rollover_state_tree.rs index 5347330a85..ccf160c432 100644 --- a/programs/registry/src/account_compression_cpi/rollover_state_tree.rs +++ b/programs/registry/src/account_compression_cpi/rollover_state_tree.rs @@ -1,4 +1,6 @@ -use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; +use account_compression::{ + program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED, RegisteredProgram, +}; use anchor_lang::prelude::*; use crate::epoch::register_epoch::ForesterEpochPda; @@ -18,8 +20,7 @@ pub struct RolloverMerkleTreeAndQueue<'info> { #[account( seeds = [&crate::ID.to_bytes()], bump, seeds::program = &account_compression::ID, )] - pub registered_program_pda: - Account<'info, account_compression::instructions::register_program::RegisteredProgram>, + pub registered_program_pda: Account<'info, RegisteredProgram>, pub account_compression_program: Program<'info, AccountCompression>, /// CHECK: #[account(zero)] diff --git a/programs/registry/src/account_compression_cpi/update_address_tree.rs b/programs/registry/src/account_compression_cpi/update_address_tree.rs index 64b5fde0da..5fde7fff7d 100644 --- a/programs/registry/src/account_compression_cpi/update_address_tree.rs +++ b/programs/registry/src/account_compression_cpi/update_address_tree.rs @@ -1,4 +1,6 @@ -use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; +use account_compression::{ + program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED, RegisteredProgram, +}; use anchor_lang::prelude::*; use crate::epoch::register_epoch::ForesterEpochPda; @@ -17,16 +19,17 @@ pub struct UpdateAddressMerkleTree<'info> { #[account( seeds = [&crate::ID.to_bytes()], bump, seeds::program = &account_compression::ID, )] - pub registered_program_pda: - Account<'info, account_compression::instructions::register_program::RegisteredProgram>, + pub registered_program_pda: Account<'info, RegisteredProgram>, pub account_compression_program: Program<'info, AccountCompression>, - /// CHECK: in account compression program + /// CHECK: (account compression program). + /// State Merkle tree queue. #[account(mut)] pub queue: AccountInfo<'info>, - /// CHECK: in account compression program + /// CHECK: (account compression program). + /// State Merkle tree. #[account(mut)] pub merkle_tree: AccountInfo<'info>, - /// CHECK: when emitting event. + /// CHECK: (account compression program) when emitting event. pub log_wrapper: UncheckedAccount<'info>, } diff --git a/programs/registry/src/constants.rs b/programs/registry/src/constants.rs new file mode 100644 index 0000000000..5a55b05044 --- /dev/null +++ b/programs/registry/src/constants.rs @@ -0,0 +1,4 @@ +pub const FORESTER_SEED: &[u8] = b"forester"; +pub const FORESTER_TOKEN_POOL_SEED: &[u8] = b"forester_token_pool"; +pub const FORESTER_EPOCH_SEED: &[u8] = b"forester_epoch"; +pub const EPOCH_SEED: &[u8] = b"epoch"; diff --git a/programs/registry/src/decentralization_and_contention.rs b/programs/registry/src/decentralization_and_contention.rs index 9bd5780280..40910a44d0 100644 --- a/programs/registry/src/decentralization_and_contention.rs +++ b/programs/registry/src/decentralization_and_contention.rs @@ -184,8 +184,7 @@ mod tests { .is_ok()); // ---------------------------------------------------------------------------------------- // 7. Report work from active epoch phase - report_work_instruction(&mut forester_epoch_pda, &mut epoch_pda, current_solana_slot) - .unwrap(); + process_report_work(&mut forester_epoch_pda, &mut epoch_pda, current_solana_slot).unwrap(); // ---------------------------------------------------------------------------------------- // Report work phase ends (epoch 1) diff --git a/programs/registry/src/delegate/delegate_instruction.rs b/programs/registry/src/delegate/delegate_instruction.rs index 9884c62f77..7288d184c9 100644 --- a/programs/registry/src/delegate/delegate_instruction.rs +++ b/programs/registry/src/delegate/delegate_instruction.rs @@ -11,29 +11,26 @@ pub struct DelegatetOrUndelegateInstruction<'info> { #[account(mut)] pub fee_payer: Signer<'info>, pub authority: Signer<'info>, + pub protocol_config: Account<'info, ProtocolConfigPda>, /// CHECK: #[account( seeds = [CPI_AUTHORITY_PDA_SEED], bump )] pub cpi_authority: AccountInfo<'info>, - pub protocol_config: Account<'info, ProtocolConfigPda>, + /// Forester pda which is being delegated or undelegated to or from. #[account(mut)] pub forester_pda: Account<'info, ForesterAccount>, - /// CHECK: + /// CHECK: (account compression program) as part of light system program invocation. pub registered_program_pda: AccountInfo<'info>, /// CHECK: checked in emit_event.rs. pub noop_program: AccountInfo<'info>, - /// CHECK: + /// CHECK: (account compression program) as part of light system program invocation. + /// Cpi authority of light system program to invoke account + /// compression program. pub account_compression_authority: AccountInfo<'info>, - /// CHECK: pub account_compression_program: Program<'info, AccountCompression>, - /// CHECK: checked in cpi_signer_check. - pub invoking_program: AccountInfo<'info>, - /// CHECK: + /// CHECK: (account compression program) as part of light system program invocation. pub system_program: AccountInfo<'info>, - // /// CHECK: - // #[account(mut)] - // pub cpi_context_account: AccountInfo<'info>, pub self_program: Program<'info, crate::program::LightRegistry>, pub light_system_program: Program<'info, LightSystemProgram>, } @@ -64,7 +61,7 @@ impl<'info> SystemProgramAccounts<'info> for DelegatetOrUndelegateInstruction<'i self.light_system_program.to_account_info() } fn get_self_program(&self) -> AccountInfo<'info> { - self.invoking_program.to_account_info() + unimplemented!() } } diff --git a/programs/registry/src/delegate/deposit.rs b/programs/registry/src/delegate/deposit.rs index bd2ca92d1c..a548d091fd 100644 --- a/programs/registry/src/delegate/deposit.rs +++ b/programs/registry/src/delegate/deposit.rs @@ -46,15 +46,6 @@ pub fn process_deposit_or_withdrawal<'a, 'b, 'c, 'info: 'b + 'c, const IS_DEPOSI change_compressed_account_merkle_tree_index: u8, output_delegate_compressed_account_merkle_tree_index: u8, ) -> Result<()> { - // if !IS_DEPOSIT { - // let slot = Clock::get()?.slot; - // let epoch = ctx.accounts.protocol_config.config.get_current_epoch(slot); - // delegate_account - // .as_ref() - // .unwrap() - // .delegate_account - // .sync_pending_stake_weight(epoch); - // } let mint = &ctx.accounts.protocol_config.config.mint; let slot = Clock::get()?.slot; let epoch = ctx.accounts.protocol_config.config.get_current_epoch(slot); diff --git a/programs/registry/src/delegate/deposit_instruction.rs b/programs/registry/src/delegate/deposit_instruction.rs index bcc3167c41..48258db68a 100644 --- a/programs/registry/src/delegate/deposit_instruction.rs +++ b/programs/registry/src/delegate/deposit_instruction.rs @@ -19,7 +19,8 @@ pub struct DepositOrWithdrawInstruction<'info> { #[account(mut)] pub fee_payer: Signer<'info>, pub authority: Signer<'info>, - /// CHECK: + /// CHECK: (seed constraint). + /// Authority derived from delegate authority and salt. #[account( seeds = [ESCROW_TOKEN_ACCOUNT_SEED, authority.key().as_ref(), salt.to_le_bytes().as_slice()], bump )] diff --git a/programs/registry/src/epoch/claim_forester_instruction.rs b/programs/registry/src/epoch/claim_forester_instruction.rs index 9f41da208e..54c5b3d4f8 100644 --- a/programs/registry/src/epoch/claim_forester_instruction.rs +++ b/programs/registry/src/epoch/claim_forester_instruction.rs @@ -1,10 +1,11 @@ +use crate::constants::{FORESTER_EPOCH_SEED, FORESTER_TOKEN_POOL_SEED}; use crate::delegate::traits::MintToAccounts; use crate::delegate::traits::{ CompressedCpiContextTrait, CompressedTokenProgramAccounts, SignerAccounts, SystemProgramAccounts, }; use crate::errors::RegistryError; -use crate::{EpochPda, ForesterAccount, ForesterEpochPda, FORESTER_EPOCH_SEED}; +use crate::{EpochPda, ForesterAccount, ForesterEpochPda}; use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; use anchor_lang::prelude::*; use anchor_spl::token::{Mint, Token, TokenAccount}; @@ -18,14 +19,15 @@ pub struct ClaimForesterInstruction<'info> { #[account(mut)] pub fee_payer: Signer<'info>, pub authority: Signer<'info>, - /// CHECK: + /// CHECK: (seed constraint). #[account( seeds = [CPI_AUTHORITY_PDA_SEED], bump )] pub cpi_authority: AccountInfo<'info>, - /// CHECK: + // START LIGHT ACCOUNTS + /// CHECK: (light system program). pub registered_program_pda: AccountInfo<'info>, - /// CHECK: checked in emit_event.rs. + /// CHECK: (light system program) in emit_event.rs. pub noop_program: AccountInfo<'info>, /// CHECK: pub account_compression_authority: AccountInfo<'info>, @@ -41,10 +43,14 @@ pub struct ClaimForesterInstruction<'info> { pub light_system_program: Program<'info, LightSystemProgram>, pub compressed_token_program: Program<'info, LightCompressedToken>, pub spl_token_program: Program<'info, Token>, - #[account(mut)] + // END LIGHT ACCOUNTS + /// CHECK: (seed constraint). + /// Pool account for epoch rewards excluding forester fee. + #[account(mut, seeds = [FORESTER_TOKEN_POOL_SEED, forester_pda.key().as_ref()],bump,)] pub forester_token_pool: Account<'info, TokenAccount>, - #[account(mut)] + #[account(mut, has_one = authority)] pub forester_pda: Account<'info, ForesterAccount>, + /// CHECK: (seed constraint) derived from epoch_pda.epoch and forester_pda. #[account(mut, seeds=[FORESTER_EPOCH_SEED, forester_pda.key().as_ref(), epoch_pda.epoch.to_le_bytes().as_ref()], bump ,close=fee_payer)] pub forester_epoch_pda: Account<'info, ForesterEpochPda>, #[account(mut)] diff --git a/programs/registry/src/epoch/register_epoch.rs b/programs/registry/src/epoch/register_epoch.rs index 07ca8eaab9..5d560eccc7 100644 --- a/programs/registry/src/epoch/register_epoch.rs +++ b/programs/registry/src/epoch/register_epoch.rs @@ -1,3 +1,4 @@ +use crate::constants::{EPOCH_SEED, FORESTER_EPOCH_SEED}; use crate::errors::RegistryError; use crate::forester::state::{ForesterAccount, ForesterConfig}; use crate::protocol_config::state::{ProtocolConfig, ProtocolConfigPda}; @@ -5,6 +6,7 @@ use aligned_sized::aligned_sized; use anchor_lang::prelude::*; use anchor_lang::solana_program::pubkey::Pubkey; +//TODO: add mechanism to fund epoch account creation /// Is used for tallying and rewards calculation #[account] #[aligned_sized(anchor)] @@ -14,6 +16,7 @@ pub struct EpochPda { pub protocol_config: ProtocolConfig, pub total_work: u64, pub registered_stake: u64, + pub claimed_stake: u64, } #[aligned_sized(anchor)] @@ -152,9 +155,6 @@ pub fn set_total_registered_stake_instruction( ) { forester_epoch_pda.total_epoch_state_weight = Some(epoch_pda.registered_stake); } -// TODO: move to constants -pub const FORESTER_EPOCH_SEED: &[u8] = b"forester_epoch"; -pub const EPOCH_SEED: &[u8] = b"epoch"; #[derive(Accounts)] pub struct UpdateForesterEpochPda<'info> { @@ -170,12 +170,12 @@ pub struct UpdateForesterEpochPda<'info> { pub struct RegisterForesterEpoch<'info> { #[account(mut)] pub authority: Signer<'info>, + pub protocol_config: Account<'info, ProtocolConfigPda>, #[account(mut, has_one = authority)] pub forester_pda: Account<'info, ForesterAccount>, /// CHECK: #[account(init, seeds = [FORESTER_EPOCH_SEED, forester_pda.key().to_bytes().as_slice(), current_epoch.to_le_bytes().as_slice()], bump, space =ForesterEpochPda::LEN , payer = authority)] pub forester_epoch_pda: Account<'info, ForesterEpochPda>, - pub protocol_config: Account<'info, ProtocolConfigPda>, /// CHECK: TODO: check that this is the correct epoch account #[account(init_if_needed, seeds = [EPOCH_SEED, current_epoch.to_le_bytes().as_slice()], bump, space =EpochPda::LEN, payer = authority)] pub epoch_pda: Account<'info, EpochPda>, diff --git a/programs/registry/src/epoch/report_work.rs b/programs/registry/src/epoch/report_work.rs index a5734bf740..434a58f3b9 100644 --- a/programs/registry/src/epoch/report_work.rs +++ b/programs/registry/src/epoch/report_work.rs @@ -19,7 +19,7 @@ use anchor_lang::prelude::*; /// for weighted cap we need this round, hardcoded cap would work without /// this round) /// - reward could be in sol, or light tokens -pub fn report_work_instruction( +pub fn process_report_work( forester_epoch_pda: &mut ForesterEpochPda, epoch_pda: &mut EpochPda, current_slot: u64, @@ -32,7 +32,7 @@ pub fn report_work_instruction( return err!(RegistryError::InvalidEpochAccount); } if forester_epoch_pda.has_reported_work { - return err!(RegistryError::ForesterAlreadyRegistered); + return err!(RegistryError::ForesterAlreadyReportedWork); } forester_epoch_pda.has_reported_work = true; @@ -43,7 +43,6 @@ pub fn report_work_instruction( #[derive(Accounts)] pub struct ReportWork<'info> { authority: Signer<'info>, - // TODO: rename forester_epoch_pda to forester_epoch_pda #[account(mut, has_one = authority)] pub forester_epoch_pda: Account<'info, ForesterEpochPda>, #[account(mut)] diff --git a/programs/registry/src/epoch/sync_delegate.rs b/programs/registry/src/epoch/sync_delegate.rs index 5ed6e2a784..7203bf58a0 100644 --- a/programs/registry/src/epoch/sync_delegate.rs +++ b/programs/registry/src/epoch/sync_delegate.rs @@ -35,13 +35,12 @@ pub struct SyncDelegateTokenAccount { /// THIS IS INSECURE /// TODO: make secure by checking inclusion of the last compressed forester epoch pda -// 392 bytes + 576 accounts + 64 signature = 1032 bytes -> 200 bytes for 8 +// 360 bytes +( 576 + 32 )accounts + 64 signature = 1032 bytes -> 200 bytes for 8 // CompressedForesterEpochAccountInput compressed accounts pub fn process_sync_delegate_account<'info>( ctx: Context<'_, '_, '_, 'info, SyncDelegateInstruction<'info>>, delegate_account: DelegateAccountWithPackedContext, // 155 bytes previous_hash: [u8; 32], // 32 bytes - forester_pda_pubkey: Pubkey, // 32 bytes compressed_forester_epoch_pdas: Vec, // 4 bytes last_account_root_index: u16, // 2 bytes last_account_merkle_context: PackedMerkleContext, // 7 bytes @@ -51,11 +50,11 @@ pub fn process_sync_delegate_account<'info>( output_token_account_merkle_tree_index: u8, ) -> Result<()> { let authority = ctx.accounts.authority.key(); - let escrow_authority = if let Some(authority) = ctx.accounts.escrow_token_authority.as_ref() { - Some(authority.key()) - } else { - None - }; + let escrow_authority = ctx + .accounts + .escrow_token_authority + .as_ref() + .map(|authority| authority.key()); let slot = Clock::get()?.slot; let epoch = ctx.accounts.protocol_config.config.get_current_epoch(slot); let ( @@ -69,7 +68,7 @@ pub fn process_sync_delegate_account<'info>( delegate_account, compressed_forester_epoch_pdas, previous_hash, - forester_pda_pubkey, + ctx.accounts.forester_pda.key(), last_account_merkle_context, last_account_root_index, &input_escrow_token_account, diff --git a/programs/registry/src/epoch/sync_delegate_instruction.rs b/programs/registry/src/epoch/sync_delegate_instruction.rs index cd73a2cf5d..ad33a22bb4 100644 --- a/programs/registry/src/epoch/sync_delegate_instruction.rs +++ b/programs/registry/src/epoch/sync_delegate_instruction.rs @@ -1,18 +1,21 @@ +use crate::constants::FORESTER_TOKEN_POOL_SEED; +use crate::delegate::deposit::DelegateAccountWithPackedContext; use crate::delegate::traits::{CompressedCpiContextTrait, CompressedTokenProgramAccounts}; -use crate::protocol_config::state::ProtocolConfigPda; - use crate::delegate::{ traits::{SignerAccounts, SystemProgramAccounts}, ESCROW_TOKEN_ACCOUNT_SEED, }; +use crate::protocol_config::state::ProtocolConfigPda; +use crate::ForesterAccount; use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; use anchor_lang::prelude::*; use anchor_spl::token::{Token, TokenAccount}; use light_compressed_token::program::LightCompressedToken; +use light_compressed_token::POOL_SEED; use light_system_program::program::LightSystemProgram; #[derive(Accounts)] -#[instruction(salt: u64)] +#[instruction(salt: u64,delegate_account: DelegateAccountWithPackedContext)] pub struct SyncDelegateInstruction<'info> { /// Fee payer needs to be mutable to pay rollover and protocol fees. #[account(mut)] @@ -29,6 +32,7 @@ pub struct SyncDelegateInstruction<'info> { )] pub cpi_authority: AccountInfo<'info>, pub protocol_config: Account<'info, ProtocolConfigPda>, + // START LIGHT ACCOUNTS /// CHECK: pub registered_program_pda: AccountInfo<'info>, /// CHECK: checked in emit_event.rs. @@ -46,11 +50,14 @@ pub struct SyncDelegateInstruction<'info> { pub compressed_token_program: Option>, /// CHECK: pub token_cpi_authority_pda: Option>, - #[account(mut)] - pub forester_token_pool: Option>, - #[account(mut)] - pub spl_token_pool: Option>, pub spl_token_program: Option>, + #[account(mut, seeds= [POOL_SEED, protocol_config.config.mint.as_ref()], bump, seeds::program= compressed_token_program.as_ref().unwrap())] + pub spl_token_pool: Option>, + // END LIGHT ACCOUNTS + #[account(constraint = forester_pda.key() == delegate_account.delegate_account.delegate_forester_delegate_account.unwrap())] + pub forester_pda: Account<'info, ForesterAccount>, + #[account(mut, seeds = [FORESTER_TOKEN_POOL_SEED, forester_pda.key().as_ref()],bump,)] + pub forester_token_pool: Option>, } impl<'info> SystemProgramAccounts<'info> for SyncDelegateInstruction<'info> { diff --git a/programs/registry/src/forester/state.rs b/programs/registry/src/forester/state.rs index f90ce1e724..67dcb4f598 100644 --- a/programs/registry/src/forester/state.rs +++ b/programs/registry/src/forester/state.rs @@ -1,4 +1,7 @@ -use crate::protocol_config::state::ProtocolConfig; +use crate::{ + constants::{FORESTER_SEED, FORESTER_TOKEN_POOL_SEED}, + protocol_config::state::ProtocolConfig, +}; use account_compression::utils::constants::CPI_AUTHORITY_PDA_SEED; use anchor_lang::prelude::*; use anchor_lang::solana_program::pubkey::Pubkey; @@ -25,6 +28,7 @@ pub struct ForesterAccount { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] pub struct ForesterConfig { + // TODO: switch to promille /// Fee in percentage points. pub fee: u64, pub fee_recipient: Pubkey, @@ -39,60 +43,52 @@ impl ForesterAccount { let current_epoch = protocol_config.get_current_epoch( current_slot.saturating_sub(protocol_config.registration_phase_length), ); - // msg!("current_epoch: {}", current_epoch); - // msg!("self.current_epoch: {}", self.current_epoch); // If the current epoch is greater than the last registered epoch, or next epoch is in registration phase if current_epoch > self.current_epoch || protocol_config.is_registration_phase(current_slot).is_ok() { - // msg!("self pending stake weight: {}", self.pending_undelegated_stake_weight); - // msg!("self active stake weight: {}", self.active_stake_weight); self.current_epoch = current_epoch; self.active_stake_weight += self.pending_undelegated_stake_weight; self.pending_undelegated_stake_weight = 0; - // msg!("self pending stake weight: {}", self.pending_undelegated_stake_weight); - // msg!("self active stake weight: {}", self.active_stake_weight); } Ok(()) } } -pub const FORESTER_SEED: &[u8] = b"forester"; -pub const FORESTER_TOKEN_POOL_SEED: &[u8] = b"forester_token_pool"; #[derive(Accounts)] pub struct RegisterForester<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + pub authority: Signer<'info>, + /// CHECK: + #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] + pub cpi_authority_pda: AccountInfo<'info>, + pub protocol_config_pda: Account<'info, ProtocolConfigPda>, + #[account(constraint = mint.key() == protocol_config_pda.config.mint)] + pub mint: Account<'info, Mint>, /// CHECK: - #[account(init, seeds = [FORESTER_SEED, authority.key().to_bytes().as_slice()], bump, space =ForesterAccount::LEN , payer = signer)] + #[account(init, seeds = [FORESTER_SEED, authority.key().as_ref()], bump, space =ForesterAccount::LEN , payer = fee_payer)] pub forester_pda: Account<'info, ForesterAccount>, #[account( init, seeds = [ - FORESTER_TOKEN_POOL_SEED, authority.key().to_bytes().as_slice(), + FORESTER_TOKEN_POOL_SEED, forester_pda.key().as_ref(), ], bump, - payer = signer, + payer = fee_payer, token::mint = mint, token::authority = cpi_authority_pda, )] pub token_pool_pda: Account<'info, TokenAccount>, - #[account(mut)] - pub signer: Signer<'info>, - pub authority: Signer<'info>, - /// CHECK: - #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] - pub cpi_authority_pda: AccountInfo<'info>, - pub protocol_config_pda: Account<'info, ProtocolConfigPda>, - #[account(constraint = mint.key() == protocol_config_pda.config.mint)] - pub mint: Account<'info, Mint>, system_program: Program<'info, System>, token_program: Program<'info, Token>, } #[derive(Accounts)] pub struct UpdateForester<'info> { - /// CHECK: + pub authority: Signer<'info>, + /// CHECK: authority is forester pda authority. #[account(mut, has_one = authority)] pub forester_pda: Account<'info, ForesterAccount>, - pub authority: Signer<'info>, pub new_authority: Option>, } diff --git a/programs/registry/src/lib.rs b/programs/registry/src/lib.rs index 34db55a60b..be440f7ec5 100644 --- a/programs/registry/src/lib.rs +++ b/programs/registry/src/lib.rs @@ -16,6 +16,7 @@ pub use account_compression_cpi::{ }; pub use protocol_config::{initialize::*, mint::*, update::*}; +pub mod constants; pub mod delegate; pub mod epoch; pub mod forester; @@ -48,8 +49,8 @@ pub mod light_registry { use super::*; - pub fn initialize_governance_authority( - ctx: Context, + pub fn initialize_protocol_config( + ctx: Context, bump: u8, protocol_config: ProtocolConfig, ) -> Result<()> { @@ -58,30 +59,26 @@ pub mod light_registry { { return err!(errors::RegistryError::InvalidMint); } - ctx.accounts.authority_pda.authority = ctx.accounts.authority.key(); - ctx.accounts.authority_pda.bump = bump; - ctx.accounts.authority_pda.config = protocol_config; - msg!("mint: {:?}", ctx.accounts.mint.key()); + ctx.accounts.protocol_config_pda.authority = ctx.accounts.authority.key(); + ctx.accounts.protocol_config_pda.bump = bump; + ctx.accounts.protocol_config_pda.config = protocol_config; Ok(()) } - // TODO: rename to update_protocol_config - pub fn update_governance_authority( - ctx: Context, - _bump: u8, + pub fn update_protocol_config( + ctx: Context, new_authority: Pubkey, new_config: ProtocolConfig, ) -> Result<()> { - ctx.accounts.authority_pda.authority = new_authority; - // ctx.accounts.authority_pda.bump = bump; + ctx.accounts.protocol_config_pda.authority = new_authority; // mint cannot be updated - if ctx.accounts.authority_pda.config.mint != new_config.mint { + if ctx.accounts.protocol_config_pda.config.mint != new_config.mint { return err!(errors::RegistryError::InvalidMint); } // forester registration guarded can only be disabled if !ctx .accounts - .authority_pda + .protocol_config_pda .config .forester_registration_guarded && new_config.forester_registration_guarded @@ -91,6 +88,7 @@ pub mod light_registry { Ok(()) } + /// Mint compressed tokens of protocol_config.mint tokens to the recipients. pub fn mint<'info>( ctx: Context<'_, '_, '_, 'info, Mint<'info>>, amounts: Vec, @@ -105,7 +103,7 @@ pub mod light_registry { ) } - pub fn register_system_program(ctx: Context, bump: u8) -> Result<()> { + pub fn register_system_program(ctx: Context, bump: u8) -> Result<()> { let bump = &[bump]; let seeds = [CPI_AUTHORITY_PDA_SEED, bump]; let signer_seeds = &[&seeds[..]]; @@ -213,7 +211,7 @@ pub mod light_registry { _bump: u8, config: ForesterConfig, ) -> Result<()> { - if ctx.accounts.protocol_config_pda.authority != ctx.accounts.signer.key() + if ctx.accounts.protocol_config_pda.authority != ctx.accounts.fee_payer.key() && ctx .accounts .protocol_config_pda @@ -277,6 +275,7 @@ pub mod light_registry { protocol_config: ctx.accounts.protocol_config.config, total_work: 0, registered_stake: 0, + claimed_stake: 0, }); } register_for_epoch_instruction( @@ -292,9 +291,6 @@ pub mod light_registry { /// This transaction can be included as additional instruction in the first /// work instructions during the active phase. /// Registration Period must be over. - /// TODO: introduce grace period between registration and before - /// active phase starts, do I really need it or isn't it clear who gets the - /// first slot the first sign up? pub fn finalize_registration<'info>( ctx: Context<'_, '_, '_, 'info, FinalizeRegistration<'info>>, ) -> Result<()> { @@ -318,29 +314,21 @@ pub mod light_registry { } pub fn update_forester_epoch_pda( - ctx: Context, - authority: Pubkey, + _ctx: Context, + _authority: Pubkey, ) -> Result<()> { - ctx.accounts.forester_epoch_pda.authority = authority; - Ok(()) + unimplemented!(); + // _ctx.accounts.forester_epoch_pda.authority = _authority; + // Ok(()) } pub fn report_work<'info>(ctx: Context<'_, '_, '_, 'info, ReportWork<'info>>) -> Result<()> { let current_solana_slot = anchor_lang::solana_program::clock::Clock::get()?.slot; - ctx.accounts - .epoch_pda - .protocol_config - .is_report_work_phase(current_solana_slot, ctx.accounts.epoch_pda.epoch)?; - // TODO: unify epoch security checks - if ctx.accounts.epoch_pda.epoch != ctx.accounts.forester_epoch_pda.epoch { - return err!(errors::RegistryError::InvalidEpoch); - } - if ctx.accounts.forester_epoch_pda.has_reported_work { - return err!(errors::RegistryError::ForesterAlreadyReportedWork); - } - ctx.accounts.epoch_pda.total_work += ctx.accounts.forester_epoch_pda.work_counter; - ctx.accounts.forester_epoch_pda.has_reported_work = true; - Ok(()) + process_report_work( + &mut ctx.accounts.forester_epoch_pda, + &mut ctx.accounts.epoch_pda, + current_solana_slot, + ) } #[allow(clippy::too_many_arguments)] @@ -443,15 +431,8 @@ pub mod light_registry { proof: CompressedProof, delegate_account: DelegateAccountWithPackedContext, delegate_amount: u64, - no_sync: bool, ) -> Result<()> { - process_delegate_or_undelegate::( - ctx, - proof, - delegate_account, - delegate_amount, - no_sync, - ) + process_delegate_or_undelegate::(ctx, proof, delegate_account, delegate_amount, false) } pub fn undelegate<'info>( @@ -459,14 +440,13 @@ pub mod light_registry { proof: CompressedProof, delegate_account: DelegateAccountWithPackedContext, delegate_amount: u64, - no_sync: bool, ) -> Result<()> { process_delegate_or_undelegate::( ctx, proof, delegate_account, delegate_amount, - no_sync, + false, ) } @@ -478,14 +458,13 @@ pub mod light_registry { pub fn sync_delegate<'info>( ctx: Context<'_, '_, '_, 'info, SyncDelegateInstruction<'info>>, - _salt: u64, + _salt: u64, // TODO: test integration delegate_account: DelegateAccountWithPackedContext, - previous_hash: [u8; 32], - forester_pda_pubkey: Pubkey, + previous_hash: [u8; 32], // TODO: test integration compressed_forester_epoch_pdas: Vec, - last_account_root_index: u16, + last_account_root_index: u16, // TODO: test integration last_account_merkle_context: PackedMerkleContext, - inclusion_proof: CompressedProof, + inclusion_proof: CompressedProof, //TODO: test integration sync_delegate_token_account: Option, input_escrow_token_account: Option, output_token_account_merkle_tree_index: u8, @@ -494,7 +473,6 @@ pub mod light_registry { ctx, delegate_account, previous_hash, - forester_pda_pubkey, compressed_forester_epoch_pdas, last_account_root_index, last_account_merkle_context, @@ -505,25 +483,8 @@ pub mod light_registry { ) } - // TODO: update rewards field - // signer is light governance authority - - // TODO: sync rewards - // signer is registered relayer - // sync rewards field with Light Governance Authority rewards field - - // TODO: add register relayer - // signer is light governance authority - // creates a registered relayer pda which is derived from the relayer - // pubkey, with fields: signer_pubkey, points_counter, rewards: Vec, - // last_rewards_sync - - // TODO: deregister relayer - // signer is light governance authority - - // TODO: update registered relayer - // signer is registered relayer - // update the relayer signer pubkey in the pda + // TODO: close forester account (cannot be closed can just be marked as closed then nobody can delegate to it anymore) + // signer is light governance authority when guarded or forester authority // TODO: add rollover lookup table with rewards // signer is registered relayer diff --git a/programs/registry/src/protocol_config/initialize.rs b/programs/registry/src/protocol_config/initialize.rs index 89b9f78e8e..481f112785 100644 --- a/programs/registry/src/protocol_config/initialize.rs +++ b/programs/registry/src/protocol_config/initialize.rs @@ -4,23 +4,25 @@ use anchor_lang::prelude::*; use anchor_spl::token::Mint; #[constant] -pub const AUTHORITY_PDA_SEED: &[u8] = b"authority"; +pub const PROTOCOL_CONFIG_PDA_SEED: &[u8] = b"authority"; #[derive(Accounts)] #[instruction(bump: u8)] -pub struct InitializeAuthority<'info> { - // TODO: add check that this is upgrade authority - #[account(mut)] +pub struct InitializeProtocolConfig<'info> { + /// CHECK: initial authority is program keypair. + /// The authority should be updated to a different keypair after + /// initialization. + #[account(mut, constraint= authority.key() == self_program.key())] pub authority: Signer<'info>, - /// CHECK: - #[account(init, seeds = [AUTHORITY_PDA_SEED], bump, space = ProtocolConfigPda::LEN, payer = authority)] - pub authority_pda: Account<'info, ProtocolConfigPda>, + #[account(init, seeds = [PROTOCOL_CONFIG_PDA_SEED], bump, space = ProtocolConfigPda::LEN, payer = authority)] + pub protocol_config_pda: Account<'info, ProtocolConfigPda>, pub system_program: Program<'info, System>, pub mint: Account<'info, Mint>, - /// CHECK: + /// CHECK: (seed derivation). #[account( seeds = [CPI_AUTHORITY_PDA_SEED], bump, )] pub cpi_authority: AccountInfo<'info>, + pub self_program: Program<'info, crate::program::LightRegistry>, } diff --git a/programs/registry/src/protocol_config/mint.rs b/programs/registry/src/protocol_config/mint.rs index 0fd1f591f1..6a59400d82 100644 --- a/programs/registry/src/protocol_config/mint.rs +++ b/programs/registry/src/protocol_config/mint.rs @@ -9,7 +9,7 @@ use crate::{ delegate::traits::{ CompressedTokenProgramAccounts, MintToAccounts, SignerAccounts, SystemProgramAccounts, }, - AUTHORITY_PDA_SEED, + PROTOCOL_CONFIG_PDA_SEED, }; use super::state::ProtocolConfigPda; @@ -21,34 +21,33 @@ pub struct Mint<'info> { #[account(mut)] pub fee_payer: Signer<'info>, pub authority: Signer<'info>, - /// CHECK: - #[account(mut, seeds = [AUTHORITY_PDA_SEED], bump, has_one = authority)] + /// CHECK: (seed constraint) authority is protocol config authority. + #[account(mut, seeds = [PROTOCOL_CONFIG_PDA_SEED], bump, has_one = authority)] pub protocol_config_pda: Account<'info, ProtocolConfigPda>, - #[account(mut)] + /// CHECK: is mint in protocol config. + #[account(mut, constraint = mint.key() == protocol_config_pda.config.mint)] pub mint: Account<'info, SplMint>, - /// CHECK: + /// CHECK: (seed constraint). #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] pub cpi_authority: AccountInfo<'info>, - /// CHECK: + /// CHECK: (compressed token program). pub token_cpi_authority_pda: AccountInfo<'info>, pub compressed_token_program: Program<'info, LightCompressedToken>, - /// CHECK: this account is checked implictly since a mint to from a mint - /// account to a token account of a different mint will fail + /// CHECK: (compressed token program). #[account(mut)] pub token_pool_pda: Account<'info, TokenAccount>, pub token_program: Program<'info, Token>, pub light_system_program: Program<'info, LightSystemProgram>, - /// CHECK: (different program) checked in account compression program + /// CHECK: (account compression program). pub registered_program_pda: AccountInfo<'info>, - /// CHECK: (different program) checked in system and account compression - /// programs + /// CHECK: (account compression program) when emitting event. pub noop_program: AccountInfo<'info>, - /// CHECK: this account in account compression program + /// CHECK: (account compression program). pub account_compression_authority: UncheckedAccount<'info>, - /// CHECK: this account in account compression program pub account_compression_program: Program<'info, account_compression::program::AccountCompression>, - /// CHECK: (different program) will be checked by the system program + /// CHECK: (account compression program). + /// State Merkle tree minted compressed token accounts are stored in. #[account(mut)] pub merkle_tree: UncheckedAccount<'info>, pub system_program: Program<'info, System>, diff --git a/programs/registry/src/protocol_config/state.rs b/programs/registry/src/protocol_config/state.rs index 5807d75a7e..527f1ec9ae 100644 --- a/programs/registry/src/protocol_config/state.rs +++ b/programs/registry/src/protocol_config/state.rs @@ -80,12 +80,6 @@ impl ProtocolConfig { (slot.saturating_sub(self.genesis_slot)) / self.active_phase_length } pub fn get_current_active_epoch(&self, slot: u64) -> Result { - // msg!("slot: {}", slot); - // msg!("genesis_slot: {}", self.genesis_slot); - // msg!( - // "registration_phase_length: {}", - // self.registration_phase_length - // ); let slot = match slot.checked_sub(self.genesis_slot + self.registration_phase_length) { Some(slot) => slot, None => return err!(RegistryError::EpochEnded), diff --git a/programs/registry/src/protocol_config/update.rs b/programs/registry/src/protocol_config/update.rs index 6052563c46..2f018e3c33 100644 --- a/programs/registry/src/protocol_config/update.rs +++ b/programs/registry/src/protocol_config/update.rs @@ -1,17 +1,17 @@ use anchor_lang::prelude::*; -use crate::AUTHORITY_PDA_SEED; +use crate::PROTOCOL_CONFIG_PDA_SEED; use super::state::ProtocolConfigPda; #[derive(Accounts)] -#[instruction(bump: u8)] -pub struct UpdateAuthority<'info> { - #[account(mut, constraint = authority.key() == authority_pda.authority)] +pub struct UpdateProtocolConfig<'info> { + /// CHECK: authority is protocol config authority. + #[account(mut, constraint = authority.key() == protocol_config_pda.authority)] pub authority: Signer<'info>, - /// CHECK: - // TODO: rename to protocol config pda - #[account(mut, seeds = [AUTHORITY_PDA_SEED], bump)] - pub authority_pda: Account<'info, ProtocolConfigPda>, + /// CHECK: (seed constraints). + #[account(mut, seeds = [PROTOCOL_CONFIG_PDA_SEED], bump)] + pub protocol_config_pda: Account<'info, ProtocolConfigPda>, + /// CHECK: is signer to reduce risk of updating with a wrong authority. pub new_authority: Signer<'info>, } diff --git a/programs/registry/src/sdk.rs b/programs/registry/src/sdk.rs index 877cd05583..0ebedeedef 100644 --- a/programs/registry/src/sdk.rs +++ b/programs/registry/src/sdk.rs @@ -75,9 +75,8 @@ pub fn create_update_authority_instruction( new_authority: Pubkey, new_protocol_config: ProtocolConfig, ) -> Instruction { - let authority_pda = get_protocol_config_pda_address(); - let update_authority_ix = crate::instruction::UpdateGovernanceAuthority { - _bump: authority_pda.1, + let protocol_config_pda = get_protocol_config_pda_address(); + let update_authority_ix = crate::instruction::UpdateProtocolConfig { new_authority, new_config: new_protocol_config, }; @@ -87,7 +86,7 @@ pub fn create_update_authority_instruction( program_id: crate::ID, accounts: vec![ AccountMeta::new(signer_pubkey, true), - AccountMeta::new(authority_pda.0, false), + AccountMeta::new(protocol_config_pda.0, false), ], data: update_authority_ix.data(), } @@ -95,7 +94,7 @@ pub fn create_update_authority_instruction( pub fn create_register_program_instruction( signer_pubkey: Pubkey, - authority_pda: (Pubkey, u8), + protocol_config_pda: (Pubkey, u8), group_account: Pubkey, program_id_to_be_registered: Pubkey, ) -> (Instruction, Pubkey) { @@ -106,11 +105,11 @@ pub fn create_register_program_instruction( let register_program_ix = crate::instruction::RegisterSystemProgram { bump: cpi_authority_pda.1, }; - let register_program_accounts = crate::accounts::RegisteredProgram { + let register_program_accounts = crate::accounts::RegisterSystemProgram { authority: signer_pubkey, program_to_be_registered: program_id_to_be_registered, registered_program_pda, - authority_pda: authority_pda.0, + protocol_config_pda: protocol_config_pda.0, group_pda: group_account, cpi_authority: cpi_authority_pda.0, account_compression_program: ID, @@ -129,19 +128,20 @@ pub fn create_initialize_governance_authority_instruction( signer_pubkey: Pubkey, protocol_config: ProtocolConfig, ) -> Instruction { - let authority_pda = get_protocol_config_pda_address(); - let ix = crate::instruction::InitializeGovernanceAuthority { - bump: authority_pda.1, + let protocol_config_pda = get_protocol_config_pda_address(); + let ix = crate::instruction::InitializeProtocolConfig { + bump: protocol_config_pda.1, protocol_config, }; let cpi_authority_pda = get_cpi_authority_pda().0; - let accounts = crate::accounts::InitializeAuthority { - authority_pda: authority_pda.0, + let accounts = crate::accounts::InitializeProtocolConfig { + protocol_config_pda: protocol_config_pda.0, authority: signer_pubkey, system_program: system_program::ID, mint: protocol_config.mint, cpi_authority: cpi_authority_pda, + self_program: crate::ID, }; Instruction { program_id: crate::ID, @@ -151,7 +151,7 @@ pub fn create_initialize_governance_authority_instruction( } pub fn create_register_forester_instruction( - governance_authority: &Pubkey, + fee_payer: &Pubkey, forester_authority: &Pubkey, config: ForesterConfig, ) -> Instruction { @@ -161,7 +161,7 @@ pub fn create_register_forester_instruction( let token_pool_pda = get_forester_token_pool_pda(forester_authority); let accounts = crate::accounts::RegisterForester { forester_pda, - signer: *governance_authority, + fee_payer: *fee_payer, protocol_config_pda, system_program: solana_sdk::system_program::id(), authority: *forester_authority, @@ -548,7 +548,7 @@ pub struct CreateDelegateInstructionInputs { pub output_delegate_compressed_account_merkle_tree: Pubkey, pub proof: CompressedProof, pub root_index: u16, - pub no_sync: bool, + // pub no_sync: bool, pub forester_pda: Pubkey, } @@ -577,7 +577,7 @@ pub fn create_delegate_instruction( delegate_account, delegate_amount: inputs.amount, proof: inputs.proof, - no_sync: inputs.no_sync, + // no_sync: inputs.no_sync, } .data() } else { @@ -585,7 +585,7 @@ pub fn create_delegate_instruction( delegate_account, delegate_amount: inputs.amount, proof: inputs.proof, - no_sync: inputs.no_sync, + // no_sync: inputs.no_sync, } .data() }; @@ -603,7 +603,6 @@ pub fn create_delegate_instruction( account_compression_authority: standard_accounts.account_compression_authority, account_compression_program: standard_accounts.account_compression_program, system_program: standard_accounts.system_program, - invoking_program: standard_registry_accounts.self_program, protocol_config: standard_registry_accounts.protocol_config_pda, self_program: standard_registry_accounts.self_program, forester_pda: inputs.forester_pda, @@ -696,7 +695,7 @@ pub fn create_sync_delegate_instruction( let packed_merkle_context = pack_merkle_context(&[delegate_account.merkle_context], &mut remaining_accounts); DelegateAccountWithPackedContext { - delegate_account: delegate_account.delegate_account.into(), + delegate_account: delegate_account.delegate_account, merkle_context: packed_merkle_context[0], root_index: inputs.root_indices[0], output_merkle_tree_index, @@ -766,7 +765,6 @@ pub fn create_sync_delegate_instruction( _salt: inputs.salt, input_escrow_token_account, delegate_account, - forester_pda_pubkey, previous_hash: inputs.previous_hash, compressed_forester_epoch_pdas: inputs.compressed_forester_epoch_pdas, last_account_merkle_context, @@ -799,6 +797,7 @@ pub fn create_sync_delegate_instruction( forester_token_pool, spl_token_pool, spl_token_program, + forester_pda: forester_pda_pubkey, }; let remaining_accounts = to_account_metas(remaining_accounts); Instruction { diff --git a/programs/registry/src/utils.rs b/programs/registry/src/utils.rs index 45b3c4e536..efea839fad 100644 --- a/programs/registry/src/utils.rs +++ b/programs/registry/src/utils.rs @@ -2,7 +2,8 @@ use account_compression::utils::constants::GROUP_AUTHORITY_SEED; use anchor_lang::solana_program::pubkey::Pubkey; use crate::{ - AUTHORITY_PDA_SEED, EPOCH_SEED, FORESTER_EPOCH_SEED, FORESTER_SEED, FORESTER_TOKEN_POOL_SEED, + constants::{EPOCH_SEED, FORESTER_EPOCH_SEED, FORESTER_SEED, FORESTER_TOKEN_POOL_SEED}, + PROTOCOL_CONFIG_PDA_SEED, }; pub fn get_group_pda(seed: Pubkey) -> Pubkey { @@ -14,7 +15,7 @@ pub fn get_group_pda(seed: Pubkey) -> Pubkey { } pub fn get_protocol_config_pda_address() -> (Pubkey, u8) { - Pubkey::find_program_address(&[AUTHORITY_PDA_SEED], &crate::ID) + Pubkey::find_program_address(&[PROTOCOL_CONFIG_PDA_SEED], &crate::ID) } pub fn get_cpi_authority_pda() -> (Pubkey, u8) { @@ -43,6 +44,39 @@ pub fn get_epoch_pda_address(epoch: u64) -> Pubkey { Pubkey::find_program_address(&[EPOCH_SEED, epoch.to_le_bytes().as_slice()], &crate::ID).0 } -pub fn get_forester_token_pool_pda(authority: &Pubkey) -> Pubkey { - Pubkey::find_program_address(&[FORESTER_TOKEN_POOL_SEED, authority.as_ref()], &crate::ID).0 +pub fn get_forester_token_pool_pda(forester_pda_address: &Pubkey) -> Pubkey { + Pubkey::find_program_address( + &[FORESTER_TOKEN_POOL_SEED, forester_pda_address.as_ref()], + &crate::ID, + ) + .0 +} + +pub struct ForesterAccounts { + pub forester_pda: Pubkey, + pub forester_token_pool: Pubkey, +} + +pub fn get_forester_accounts(authority: &Pubkey) -> ForesterAccounts { + let forester_pda = get_forester_pda_address(authority); + let forester_token_pool = get_forester_token_pool_pda(&forester_pda.0); + ForesterAccounts { + forester_pda: forester_pda.0, + forester_token_pool, + } +} +pub struct ForesterAccountsEpoch { + pub forester_pda: Pubkey, + pub forester_token_pool: Pubkey, + pub forester_epoch_pda: Pubkey, +} + +pub fn get_forester_accounts_epoch(authority: &Pubkey, epoch: u64) -> ForesterAccountsEpoch { + let forester_accounts = get_forester_accounts(authority); + let forester_epoch_pda = get_forester_epoch_pda_address(&forester_accounts.forester_pda, epoch); + ForesterAccountsEpoch { + forester_pda: forester_accounts.forester_pda, + forester_token_pool: forester_accounts.forester_token_pool, + forester_epoch_pda: forester_epoch_pda.0, + } } diff --git a/test-programs/registry-test/tests/tests.rs b/test-programs/registry-test/tests/tests.rs index fbde6b9e3a..305cc5f113 100644 --- a/test-programs/registry-test/tests/tests.rs +++ b/test-programs/registry-test/tests/tests.rs @@ -1368,8 +1368,8 @@ async fn update_registry_governance_on_testnet() { _bump: bump, new_config: ProtocolConfig::default(), }; - let accounts = light_registry::accounts::UpdateAuthority { - authority_pda: env_accounts.governance_authority_pda, + let accounts = light_registry::accounts::UpdateProtocolConfig { + protocol_config_pda: env_accounts.governance_authority_pda, authority: env_accounts.governance_authority.pubkey(), new_authority: updated_keypair.pubkey(), }; diff --git a/test-utils/src/registry.rs b/test-utils/src/registry.rs index 3cbbf8f291..e29d59c7c8 100644 --- a/test-utils/src/registry.rs +++ b/test-utils/src/registry.rs @@ -613,7 +613,6 @@ pub struct DelegateInputs<'a> { pub amount: u64, pub delegate_account: FetchedAccount, pub forester_pda: Pubkey, - pub no_sync: bool, pub output_merkle_tree: Pubkey, } @@ -622,7 +621,6 @@ pub struct UndelegateInputs<'a> { pub amount: u64, pub delegate_account: FetchedAccount, pub forester_pda: Pubkey, - pub no_sync: bool, pub output_merkle_tree: Pubkey, } @@ -644,7 +642,6 @@ pub async fn undelegate_test<'a, R: RpcConnection, I: Indexer>( amount: inputs.amount, delegate_account: inputs.delegate_account, forester_pda: inputs.forester_pda, - no_sync: inputs.no_sync, output_merkle_tree: inputs.output_merkle_tree, }; delegate_or_undelegate_test::(rpc, indexer, inputs).await @@ -697,7 +694,6 @@ pub async fn delegate_or_undelegate_test< proof: proof_rpc_result.proof, forester_pda: inputs.forester_pda, root_index: proof_rpc_result.root_indices[0], - no_sync: inputs.no_sync, }; let ix = create_delegate_instruction::(create_deposit_instruction_inputs.clone()); println!("trying to fetch forester pda"); @@ -749,11 +745,10 @@ pub async fn assert_delegate_or_undelegate( register_forester_and_advance_to_active_phase: bool, ) -> EnvAccounts { let cpi_authority_pda = get_cpi_authority_pda(); - let authority_pda = get_protocol_config_pda_address(); + let protocol_config_pda = get_protocol_config_pda_address(); let token_mint_keypair = Keypair::from_bytes(STANDARD_TOKEN_MINT_KEYPAIR.as_slice()).unwrap(); println!("mint keypair: {:?}", token_mint_keypair.pubkey()); create_mint_helper_with_keypair( @@ -268,7 +268,7 @@ pub async fn initialize_accounts( initialize_new_group(&group_seed_keypair, payer, context, cpi_authority_pda.0).await; let gov_authority = context - .get_anchor_account::(&authority_pda.0) + .get_anchor_account::(&protocol_config_pda.0) .await .unwrap() .unwrap(); @@ -385,7 +385,7 @@ pub async fn initialize_accounts( nullifier_queue_pubkey, group_pda, governance_authority: payer.insecure_clone(), - governance_authority_pda: authority_pda.0, + governance_authority_pda: protocol_config_pda.0, forester: forester.insecure_clone(), registered_program_pda: registered_system_program_pda, address_merkle_tree_pubkey: address_merkle_tree_keypair.pubkey(), @@ -601,10 +601,10 @@ pub fn get_test_env_accounts() -> EnvAccounts { let group_pda = get_group_pda(group_seed_keypair.pubkey()); let payer = Keypair::from_bytes(&PAYER_KEYPAIR).unwrap(); - let authority_pda = get_protocol_config_pda_address(); + let protocol_config_pda = get_protocol_config_pda_address(); let (_, registered_program_pda) = create_register_program_instruction( payer.pubkey(), - authority_pda, + protocol_config_pda, group_pda, light_system_program::ID, ); @@ -623,7 +623,7 @@ pub fn get_test_env_accounts() -> EnvAccounts { nullifier_queue_pubkey, group_pda, governance_authority: payer, - governance_authority_pda: authority_pda.0, + governance_authority_pda: protocol_config_pda.0, registered_forester_pda: get_forester_pda_address(&forester.pubkey()).0, forester, registered_program_pda, @@ -1006,7 +1006,6 @@ pub async fn create_delegate( amount: deposit_amount, delegate_account: delegate_account[0].as_ref().unwrap().clone(), forester_pda, - no_sync: false, output_merkle_tree: env.merkle_tree_pubkey, }; delegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) From 56d192750f62e0aa0743ae35aed26c834589fb0e Mon Sep 17 00:00:00 2001 From: ananas-block Date: Fri, 2 Aug 2024 02:06:58 +0100 Subject: [PATCH 3/4] e2e test works --- programs/compressed-token/src/token_data.rs | 8 +- .../registry/src/delegate/delegate_account.rs | 253 ++++++++++++ .../src/delegate/delegate_instruction.rs | 2 +- programs/registry/src/delegate/mod.rs | 4 +- programs/registry/src/delegate/process_cpi.rs | 10 +- .../registry/src/delegate/process_delegate.rs | 376 +++++++----------- .../{deposit.rs => process_deposit.rs} | 30 +- programs/registry/src/delegate/state.rs | 181 --------- programs/registry/src/epoch/claim_forester.rs | 28 +- programs/registry/src/epoch/register_epoch.rs | 141 ++----- programs/registry/src/epoch/sync_delegate.rs | 180 ++++++--- .../src/epoch/sync_delegate_instruction.rs | 2 +- programs/registry/src/forester/state.rs | 16 +- programs/registry/src/lib.rs | 18 +- .../src/protocol_config/initialize.rs | 6 +- .../registry/src/protocol_config/state.rs | 29 +- .../registry/src/protocol_config/update.rs | 2 +- programs/registry/src/sdk.rs | 42 +- test-programs/registry-test/tests/tests.rs | 65 ++- test-utils/src/e2e_test_env.rs | 5 +- test-utils/src/forester_epoch.rs | 2 +- test-utils/src/lib.rs | 2 +- test-utils/src/registry.rs | 73 +++- test-utils/src/test_env.rs | 66 ++- 24 files changed, 819 insertions(+), 722 deletions(-) create mode 100644 programs/registry/src/delegate/delegate_account.rs rename programs/registry/src/delegate/{deposit.rs => process_deposit.rs} (98%) delete mode 100644 programs/registry/src/delegate/state.rs diff --git a/programs/compressed-token/src/token_data.rs b/programs/compressed-token/src/token_data.rs index 4b10121cbe..9772986dcb 100644 --- a/programs/compressed-token/src/token_data.rs +++ b/programs/compressed-token/src/token_data.rs @@ -94,7 +94,13 @@ impl TokenData { if FROZEN_INPUTS { hash_inputs.push(&state_bytes[..]); } - H::hashv(hash_inputs.as_slice()) + #[cfg(target_os = "solana")] + anchor_lang::solana_program::msg!("hash_inputs: {:?}", hash_inputs); + #[cfg(not(target_os = "solana"))] + println!("hash_inputs: {:?}", hash_inputs); + let hash = H::hashv(hash_inputs.as_slice()); + println!("hash: {:?}", hash); + hash } } diff --git a/programs/registry/src/delegate/delegate_account.rs b/programs/registry/src/delegate/delegate_account.rs new file mode 100644 index 0000000000..5c9ec31738 --- /dev/null +++ b/programs/registry/src/delegate/delegate_account.rs @@ -0,0 +1,253 @@ +use aligned_sized::aligned_sized; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::pubkey::Pubkey; +use light_hasher::{errors::HasherError, DataHasher, Hasher}; +use light_utils::hash_to_bn254_field_size_be; + +/// Instruction data input verion of DelegateAccount The following fields are +/// missing since these are computed onchain: +/// 1. owner +/// 2. escrow_token_account_hash +/// -> we save 64 bytes in instructiond data +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub struct InputDelegateAccount { + pub delegate_forester_delegate_account: Option, + /// Stake weight that is delegated to a forester. + /// Newly delegated stake is not active until the next epoch. + pub delegated_stake_weight: u64, + /// When delegating stake is pending until the next epoch + pub pending_delegated_stake_weight: u64, + /// undelgated stake is stake that is not yet delegated to a forester + pub stake_weight: u64, + pub pending_synced_stake_weight: u64, + /// When undelegating stake is pending until the next epoch + pub pending_undelegated_stake_weight: u64, + pub pending_epoch: u64, + pub last_sync_epoch: u64, + /// Pending token amount are rewards that are not yet claimed to the stake + /// compressed token account. + pub pending_token_amount: u64, +} + +impl From for InputDelegateAccount { + fn from(delegate_account: DelegateAccount) -> Self { + InputDelegateAccount { + delegate_forester_delegate_account: delegate_account.delegate_forester_delegate_account, + delegated_stake_weight: delegate_account.delegated_stake_weight, + stake_weight: delegate_account.stake_weight, + pending_undelegated_stake_weight: delegate_account.pending_undelegated_stake_weight, + pending_epoch: delegate_account.pending_epoch, + last_sync_epoch: delegate_account.last_sync_epoch, + pending_token_amount: delegate_account.pending_token_amount, + pending_synced_stake_weight: delegate_account.pending_synced_stake_weight, + pending_delegated_stake_weight: delegate_account.pending_delegated_stake_weight, + } + } +} + +#[aligned_sized] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub struct DelegateAccount { + pub owner: Pubkey, + pub delegate_forester_delegate_account: Option, + /// Stake weight that is delegated to a forester. + /// Newly delegated stake is not active until the next epoch. + pub delegated_stake_weight: u64, + /// newly delegated stakeweight becomes active after the next epoch + pub pending_delegated_stake_weight: u64, + /// undelgated stake is stake that is not yet delegated to a forester + pub stake_weight: u64, + /// Buffer variable to account for the lag of one epoch for rewards to reach + /// to registration account + pub pending_synced_stake_weight: u64, + /// When undelegating stake is pending until the next epoch + pub pending_undelegated_stake_weight: u64, + pub pending_epoch: u64, + pub last_sync_epoch: u64, + /// Pending token amount are rewards that are not yet claimed to the stake + /// compressed token account. + pub pending_token_amount: u64, + pub escrow_token_account_hash: [u8; 32], +} + +pub trait CompressedAccountTrait { + fn get_owner(&self) -> Pubkey; +} +impl CompressedAccountTrait for DelegateAccount { + fn get_owner(&self) -> Pubkey { + self.owner + } +} + +impl DataHasher for DelegateAccount { + fn hash(&self) -> std::result::Result<[u8; 32], HasherError> { + let hashed_owner = hash_to_bn254_field_size_be(self.owner.as_ref()).unwrap().0; + let hashed_delegate_forester_delegate_account = + if let Some(delegate_forester_delegate_account) = + self.delegate_forester_delegate_account + { + hash_to_bn254_field_size_be(delegate_forester_delegate_account.as_ref()) + .unwrap() + .0 + } else { + [0u8; 32] + }; + H::hashv(&[ + hashed_owner.as_slice(), + hashed_delegate_forester_delegate_account.as_slice(), + &self.delegated_stake_weight.to_le_bytes(), + &self.pending_delegated_stake_weight.to_le_bytes(), + &self.stake_weight.to_le_bytes(), + &self.pending_synced_stake_weight.to_le_bytes(), + &self.pending_undelegated_stake_weight.to_le_bytes(), + &self.pending_epoch.to_le_bytes(), + &self.last_sync_epoch.to_le_bytes(), + &self.pending_token_amount.to_le_bytes(), + &self.escrow_token_account_hash, + ]) + } +} + +impl DelegateAccount { + // TODO: add unit test + pub fn sync_pending_stake_weight(&mut self, current_epoch: u64) { + println!("current_epoch: {}", current_epoch); + println!("pending_epoch: {}", self.pending_epoch); + #[cfg(target_os = "solana")] + { + msg!("current_epoch: {}", current_epoch); + msg!("pending_epoch: {}", self.pending_epoch); + } + if current_epoch > self.pending_epoch { + self.stake_weight = self + .stake_weight + .checked_add(self.pending_undelegated_stake_weight) + .unwrap(); + self.pending_undelegated_stake_weight = 0; + self.delegated_stake_weight = self + .delegated_stake_weight + .checked_add(self.pending_delegated_stake_weight) + .unwrap(); + self.pending_delegated_stake_weight = 0; + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use light_hasher::Poseidon; + use solana_sdk::pubkey::Pubkey; + + #[test] + fn failing_test_hashing_delegate_account() { + let mut vec_previous_hashes = Vec::new(); + let delegate_account = DelegateAccount { + owner: Pubkey::new_unique(), + delegate_forester_delegate_account: None, + delegated_stake_weight: 1000, + pending_delegated_stake_weight: 500, + stake_weight: 1500, + pending_synced_stake_weight: 200, + pending_undelegated_stake_weight: 100, + pending_epoch: 1, + last_sync_epoch: 2, + pending_token_amount: 50, + escrow_token_account_hash: [0u8; 32], + }; + let hash = delegate_account.hash::().unwrap(); + vec_previous_hashes.push(hash); + + // different owner + let mut different_owner_account = delegate_account; + different_owner_account.owner = Pubkey::new_unique(); + let hash2 = different_owner_account.hash::().unwrap(); + assert_to_previous_hashes(hash2, &mut vec_previous_hashes); + + // different delegate_forester_delegate_account + let mut different_delegate_account = delegate_account; + different_delegate_account.delegate_forester_delegate_account = Some(Pubkey::new_unique()); + let hash3 = different_delegate_account.hash::().unwrap(); + assert_to_previous_hashes(hash3, &mut vec_previous_hashes); + + // different other delegate_forester_delegate_account (since initial value is None) + let mut different_delegate_account = delegate_account; + different_delegate_account.delegate_forester_delegate_account = Some(Pubkey::new_unique()); + let hash3 = different_delegate_account.hash::().unwrap(); + assert_to_previous_hashes(hash3, &mut vec_previous_hashes); + + // different delegated_stake_weight + let mut different_stake_weight_account = delegate_account; + different_stake_weight_account.delegated_stake_weight = 2000; + let hash4 = different_stake_weight_account.hash::().unwrap(); + assert_to_previous_hashes(hash4, &mut vec_previous_hashes); + + // different pending_delegated_stake_weight + let mut different_pending_stake_weight_account = delegate_account; + different_pending_stake_weight_account.pending_delegated_stake_weight = 1000; + let hash5 = different_pending_stake_weight_account + .hash::() + .unwrap(); + assert_to_previous_hashes(hash5, &mut vec_previous_hashes); + + // different stake_weight + let mut different_stake_weight_account = delegate_account; + different_stake_weight_account.stake_weight = 2500; + let hash6 = different_stake_weight_account.hash::().unwrap(); + assert_to_previous_hashes(hash6, &mut vec_previous_hashes); + + // different pending_synced_stake_weight + let mut different_pending_synced_stake_weight_account = delegate_account; + different_pending_synced_stake_weight_account.pending_synced_stake_weight = 300; + let hash7 = different_pending_synced_stake_weight_account + .hash::() + .unwrap(); + assert_to_previous_hashes(hash7, &mut vec_previous_hashes); + + // different pending_undelegated_stake_weight + let mut different_pending_undelegated_stake_weight_account = delegate_account; + different_pending_undelegated_stake_weight_account.pending_undelegated_stake_weight = 200; + let hash8 = different_pending_undelegated_stake_weight_account + .hash::() + .unwrap(); + assert_to_previous_hashes(hash8, &mut vec_previous_hashes); + + // different pending_epoch + let mut different_pending_epoch_account = delegate_account; + different_pending_epoch_account.pending_epoch = 3; + let hash9 = different_pending_epoch_account.hash::().unwrap(); + assert_to_previous_hashes(hash9, &mut vec_previous_hashes); + + // different last_sync_epoch + let mut different_last_sync_epoch_account = delegate_account; + different_last_sync_epoch_account.last_sync_epoch = 4; + let hash10 = different_last_sync_epoch_account + .hash::() + .unwrap(); + assert_to_previous_hashes(hash10, &mut vec_previous_hashes); + + // different pending_token_amount + let mut different_pending_token_amount_account = delegate_account; + different_pending_token_amount_account.pending_token_amount = 100; + let hash11 = different_pending_token_amount_account + .hash::() + .unwrap(); + assert_to_previous_hashes(hash11, &mut vec_previous_hashes); + + // different escrow_token_account_hash + let mut different_escrow_token_account_hash = delegate_account; + different_escrow_token_account_hash.escrow_token_account_hash = [1u8; 32]; + let hash12 = different_escrow_token_account_hash + .hash::() + .unwrap(); + assert_to_previous_hashes(hash12, &mut vec_previous_hashes); + } + + fn assert_to_previous_hashes(hash: [u8; 32], previous_hashes: &mut Vec<[u8; 32]>) { + for previous_hash in previous_hashes.iter() { + assert_ne!(hash, *previous_hash); + } + println!("len previous hashes: {}", previous_hashes.len()); + previous_hashes.push(hash); + } +} diff --git a/programs/registry/src/delegate/delegate_instruction.rs b/programs/registry/src/delegate/delegate_instruction.rs index 7288d184c9..34efb02441 100644 --- a/programs/registry/src/delegate/delegate_instruction.rs +++ b/programs/registry/src/delegate/delegate_instruction.rs @@ -61,7 +61,7 @@ impl<'info> SystemProgramAccounts<'info> for DelegatetOrUndelegateInstruction<'i self.light_system_program.to_account_info() } fn get_self_program(&self) -> AccountInfo<'info> { - unimplemented!() + self.self_program.to_account_info() } } diff --git a/programs/registry/src/delegate/mod.rs b/programs/registry/src/delegate/mod.rs index 049ead21c8..2e28b9073f 100644 --- a/programs/registry/src/delegate/mod.rs +++ b/programs/registry/src/delegate/mod.rs @@ -1,9 +1,9 @@ +pub mod delegate_account; pub mod delegate_instruction; -pub mod deposit; pub mod deposit_instruction; pub mod process_cpi; pub mod process_delegate; -pub mod state; +pub mod process_deposit; // TODO: move into cpi dir pub mod traits; use anchor_lang::solana_program::pubkey::Pubkey; diff --git a/programs/registry/src/delegate/process_cpi.rs b/programs/registry/src/delegate/process_cpi.rs index c90b2a95ee..940c6636a7 100644 --- a/programs/registry/src/delegate/process_cpi.rs +++ b/programs/registry/src/delegate/process_cpi.rs @@ -1,10 +1,6 @@ -use super::{ - get_escrow_token_authority, - traits::{ - CompressedCpiContextTrait, CompressedTokenProgramAccounts, MintToAccounts, SignerAccounts, - SystemProgramAccounts, - }, - ESCROW_TOKEN_ACCOUNT_SEED, +use super::traits::{ + CompressedCpiContextTrait, CompressedTokenProgramAccounts, MintToAccounts, SignerAccounts, + SystemProgramAccounts, }; use account_compression::utils::constants::CPI_AUTHORITY_PDA_SEED; use anchor_lang::{prelude::*, Bumps}; diff --git a/programs/registry/src/delegate/process_delegate.rs b/programs/registry/src/delegate/process_delegate.rs index e5bf7a37dc..59671525f9 100644 --- a/programs/registry/src/delegate/process_delegate.rs +++ b/programs/registry/src/delegate/process_delegate.rs @@ -9,11 +9,11 @@ use crate::{errors::RegistryError, protocol_config::state::ProtocolConfig, Fores use super::{ delegate_instruction::DelegatetOrUndelegateInstruction, - deposit::{ + process_cpi::cpi_light_system_program, + process_deposit::{ create_compressed_delegate_account, create_delegate_compressed_account, DelegateAccountWithPackedContext, }, - process_cpi::cpi_light_system_program, }; // TODO: double check that we provide the possibility to pass a different output tree in all instructions @@ -22,7 +22,6 @@ pub fn process_delegate_or_undelegate<'a, 'b, 'c, 'info: 'b + 'c, const IS_DELEG proof: CompressedProof, delegate_account: DelegateAccountWithPackedContext, delegate_amount: u64, - no_sync: bool, ) -> Result<()> { let slot = Clock::get()?.slot; let (input_delegate_pda, output_delegate_pda) = delegate_or_undelegate::( @@ -33,7 +32,6 @@ pub fn process_delegate_or_undelegate<'a, 'b, 'c, 'info: 'b + 'c, const IS_DELEG &mut ctx.accounts.forester_pda, delegate_amount, slot, - no_sync, )?; cpi_light_system_program( @@ -46,6 +44,9 @@ pub fn process_delegate_or_undelegate<'a, 'b, 'c, 'info: 'b + 'c, const IS_DELEG ) } +/// Delegate account has to be synced (sync_delegate instruction) to the last +/// claimed epoch of the forester to delegate or undelegate. +/// (Un)Delegation of newly (un)delegated funds should come into effect in the next epoch (registration phase). pub fn delegate_or_undelegate( authority: &Pubkey, protocol_config: &ProtocolConfig, @@ -54,14 +55,12 @@ pub fn delegate_or_undelegate( forester_pda: &mut ForesterAccount, delegate_amount: u64, current_slot: u64, - no_sync: bool, ) -> Result<( PackedCompressedAccountWithMerkleContext, OutputCompressedAccountWithPackedContext, )> { - if !no_sync { - forester_pda.sync(current_slot, protocol_config)?; - } + forester_pda.sync(current_slot, protocol_config)?; + if *authority != delegate_account.delegate_account.owner { return err!(RegistryError::InvalidAuthority); } @@ -84,25 +83,12 @@ pub fn delegate_or_undelegate( .delegate_forester_delegate_account { if forester_pubkey != *forester_pda_pubkey { - return err!(RegistryError::InvalidForester); + msg!("The delegate account is delegated to a different forester. The provided forester pda is not the same as the one in the delegate account."); + return err!(RegistryError::AlreadyDelegated); } } - let epoch = forester_pda.last_registered_epoch; // protocol_config.get_current_epoch(current_slot); + let epoch = forester_pda.last_registered_epoch; - // check that is not delegated to a different forester - if delegate_account.delegate_account.delegated_stake_weight > 0 - && delegate_account - .delegate_account - .delegate_forester_delegate_account - .is_some() - && *forester_pda_pubkey - != delegate_account - .delegate_account - .delegate_forester_delegate_account - .unwrap() - { - return err!(RegistryError::AlreadyDelegated); - } // modify forester pda if IS_DELEGATE { forester_pda.pending_undelegated_stake_weight = forester_pda @@ -137,6 +123,7 @@ pub fn delegate_or_undelegate( .checked_sub(delegate_amount) .ok_or(RegistryError::ComputeEscrowAmountFailed)?; delegate_account.delegate_forester_delegate_account = Some(*forester_pda_pubkey); + println!("epoch {}", epoch); delegate_account.pending_epoch = epoch; } else { // remove delegated stake weight from delegated_stake_weight @@ -167,13 +154,6 @@ pub fn delegate_or_undelegate( compressed_account: output_account, merkle_tree_index: delegate_account.output_merkle_tree_index, }; - // let output_delegate_compressed_account = update_delegate_compressed_account::( - // *delegate_account, - // delegate_amount, - // delegate_account.output_merkle_tree_index, - // epoch, - // forester_pda_pubkey, - // )?; Ok(( input_delegate_compressed_account, @@ -181,39 +161,44 @@ pub fn delegate_or_undelegate( )) } -/// Creates an updated delegate account. -/// Delegate(IS_DELEGATE): -/// - increase delegated_stake_weight -/// - decrease stake_weight -/// Undelegate(Not(IS_DELEGATE)): -/// - decrease delegated_stake_weight -/// - increase pending_undelegated_stake_weight -fn update_delegate_compressed_account( - input_delegate_account: DelegateAccountWithPackedContext, - delegate_amount: u64, - merkle_tree_index: u8, - epoch: u64, - forester_pda_pubkey: &Pubkey, -) -> Result { - let output_account: CompressedAccount = - create_delegate_compressed_account::(&input_delegate_account.delegate_account)?; - let output_account_with_merkle_context = OutputCompressedAccountWithPackedContext { - compressed_account: output_account, - merkle_tree_index, - }; - Ok(output_account_with_merkle_context) -} +// /// Creates an updated delegate account. +// /// Delegate(IS_DELEGATE): +// /// - increase delegated_stake_weight +// /// - decrease stake_weight +// /// Undelegate(Not(IS_DELEGATE)): +// /// - decrease delegated_stake_weight +// /// - increase pending_undelegated_stake_weight +// fn update_delegate_compressed_account( +// input_delegate_account: DelegateAccountWithPackedContext, +// delegate_amount: u64, +// merkle_tree_index: u8, +// epoch: u64, +// forester_pda_pubkey: &Pubkey, +// ) -> Result { +// let output_account: CompressedAccount = +// create_delegate_compressed_account::(&input_delegate_account.delegate_account)?; +// let output_account_with_merkle_context = OutputCompressedAccountWithPackedContext { +// compressed_account: output_account, +// merkle_tree_index, +// }; +// Ok(output_account_with_merkle_context) +// } #[cfg(test)] mod tests { - use crate::delegate::state::DelegateAccount; + use crate::delegate::delegate_account::DelegateAccount; use super::*; use anchor_lang::solana_program::pubkey::Pubkey; - use light_hasher::{DataHasher, Poseidon}; + // use light_hasher::{DataHasher, Poseidon}; use light_system_program::sdk::compressed_account::PackedMerkleContext; - fn get_test_delegate_account_with_context() -> DelegateAccountWithPackedContext { + fn get_test_delegate_account_with_context( + protocol_config: &ProtocolConfig, + current_slot: u64, + ) -> DelegateAccountWithPackedContext { + let current_epoch = protocol_config.get_current_registration_epoch(current_slot); + DelegateAccountWithPackedContext { root_index: 4, merkle_context: PackedMerkleContext { @@ -230,7 +215,7 @@ mod tests { pending_delegated_stake_weight: 0, pending_undelegated_stake_weight: 50, pending_epoch: 1, - last_sync_epoch: 11, + last_sync_epoch: current_epoch - 1, pending_token_amount: 25, escrow_token_account_hash: [1u8; 32], pending_synced_stake_weight: 0, @@ -239,152 +224,40 @@ mod tests { } } - #[test] - fn test_update_delegate_compressed_account_delegate_pass() { - let input_delegate_account = get_test_delegate_account_with_context(); - let delegate_amount = 50; - let merkle_tree_index = 1; - let epoch = 10; - let forester_pda_pubkey = Pubkey::new_unique(); - - let result = update_delegate_compressed_account::( - input_delegate_account.clone(), - delegate_amount, - merkle_tree_index, - epoch, - &forester_pda_pubkey, - ); - - assert!(result.is_ok()); - - let expected_delegate_account = DelegateAccount { - delegated_stake_weight: input_delegate_account - .delegate_account - .delegated_stake_weight - + delegate_amount, - delegate_forester_delegate_account: Some(forester_pda_pubkey), - stake_weight: input_delegate_account.delegate_account.stake_weight - delegate_amount, - ..input_delegate_account.delegate_account - }; - - let output = result.unwrap(); - assert_eq!(output.merkle_tree_index, merkle_tree_index); - let deserialized_delegate_account = DelegateAccount::deserialize( - &mut &output.compressed_account.data.as_ref().unwrap().data[..], - ) - .unwrap(); - assert_eq!(deserialized_delegate_account, expected_delegate_account); - assert_eq!( - output.compressed_account.data.unwrap().data_hash, - expected_delegate_account.hash::().unwrap() - ); - } - - #[test] - fn test_update_delegate_compressed_account_delegate_fail() { - let input_delegate_account = get_test_delegate_account_with_context(); - let delegate_amount = u64::MAX; - let merkle_tree_index = 1; - let epoch = 10; - let forester_pda_pubkey = Pubkey::new_unique(); - - let result = update_delegate_compressed_account::( - input_delegate_account.clone(), - delegate_amount, - merkle_tree_index, - epoch, - &forester_pda_pubkey, - ); - - assert!(result.is_err()); - } - - #[test] - fn test_update_delegate_compressed_account_undelegate_pass() { - let input_delegate_account = get_test_delegate_account_with_context(); - let delegate_amount = 50; - let merkle_tree_index = 1; - let epoch = 10; - let forester_pda_pubkey = Pubkey::new_unique(); - - let result = update_delegate_compressed_account::( - input_delegate_account.clone(), - delegate_amount, - merkle_tree_index, - epoch, - &forester_pda_pubkey, - ); - - assert!(result.is_ok()); - - let expected_delegate_account = DelegateAccount { - delegated_stake_weight: input_delegate_account - .delegate_account - .delegated_stake_weight - - delegate_amount, - pending_undelegated_stake_weight: input_delegate_account - .delegate_account - .pending_undelegated_stake_weight - + delegate_amount, - pending_epoch: epoch, - ..input_delegate_account.delegate_account - }; - - let output = result.unwrap(); - assert_eq!(output.merkle_tree_index, merkle_tree_index); - let deserialized_delegate_account = DelegateAccount::deserialize( - &mut &output.compressed_account.data.as_ref().unwrap().data[..], - ) - .unwrap(); - assert_eq!(deserialized_delegate_account, expected_delegate_account); - assert_eq!( - output.compressed_account.data.unwrap().data_hash, - expected_delegate_account.hash::().unwrap() - ); - } - - #[test] - fn test_update_delegate_compressed_account_undelegate_fail() { - let input_delegate_account = get_test_delegate_account_with_context(); - let delegate_amount = u64::MAX; - let merkle_tree_index = 1; - let epoch = 10; - let forester_pda_pubkey = Pubkey::new_unique(); - - let result = update_delegate_compressed_account::( - input_delegate_account.clone(), - delegate_amount, - merkle_tree_index, - epoch, - &forester_pda_pubkey, - ); - - assert!(result.is_err()); - } - - fn get_test_forester_account() -> ForesterAccount { + fn get_test_forester_account( + protocol_config: &ProtocolConfig, + current_slot: u64, + ) -> ForesterAccount { + let current_epoch = protocol_config.get_current_registration_epoch(current_slot); ForesterAccount { active_stake_weight: 200, pending_undelegated_stake_weight: 50, + current_epoch: current_epoch - 1, + last_claimed_epoch: current_epoch - 1, + last_registered_epoch: current_epoch - 1, ..Default::default() } } + /// Failing tests: + /// 1. Invalid authority + /// 2. Delegate account not synced + /// 3. Invalid forester + /// 4. Already delegated + /// Functional tests: + /// 1. Outputs are created as expected (rnd test for this) #[test] - fn test_delegate_or_undelegate_delegate_pass() { - let protocol_config = ProtocolConfig { - ..Default::default() - }; - let mut forester_pda = get_test_forester_account(); - let delegate_account = get_test_delegate_account_with_context(); - let authority = delegate_account.delegate_account.owner; - let forester_pda_pubkey = delegate_account - .delegate_account - .delegate_forester_delegate_account - .unwrap(); + fn test_functional_delegate() { + let ( + protocol_config, + current_slot, + mut forester_pda, + mut expected_forester_pda, + delegate_account, + authority, + forester_pda_pubkey, + ) = test_setup(); let delegate_amount = 50; - let current_slot = 10; - let no_sync = true; let result = delegate_or_undelegate::( &authority, @@ -394,16 +267,18 @@ mod tests { &mut forester_pda, delegate_amount, current_slot, - no_sync, ); let (input_delegate_pda, output_delegate_pda) = result.unwrap(); assert_eq!(input_delegate_pda.compressed_account.owner, crate::ID); assert_eq!(output_delegate_pda.compressed_account.owner, crate::ID); - + // TODO: test sync pending stake weight + // Delegate should: + // - sync pending stake weight + // - output pending let expected_delegate_account = DelegateAccount { - delegated_stake_weight: delegate_account.delegate_account.delegated_stake_weight - + delegate_amount, + pending_delegated_stake_weight: delegate_amount, + pending_epoch: forester_pda.last_registered_epoch, stake_weight: delegate_account.delegate_account.stake_weight - delegate_amount, ..delegate_account.delegate_account }; @@ -418,25 +293,68 @@ mod tests { ) .unwrap(); assert_eq!(deserialized_delegate_account, expected_delegate_account); + expected_forester_pda + .sync(current_slot, &protocol_config) + .unwrap(); + assert_eq!(forester_pda, expected_forester_pda); } - #[test] - fn test_delegate_or_undelegate_undelegate_pass() { + fn test_setup() -> ( + ProtocolConfig, + u64, + ForesterAccount, + ForesterAccount, + DelegateAccountWithPackedContext, + Pubkey, + Pubkey, + ) { let protocol_config = ProtocolConfig { ..Default::default() }; - - let mut forester_pda = get_test_forester_account(); - let delegate_account = get_test_delegate_account_with_context(); + // slot in active phase of epoch 2 + let current_slot = protocol_config.genesis_slot + + protocol_config.registration_phase_length + + protocol_config.active_phase_length * 2 + + 1; + let forester_pda = get_test_forester_account(&protocol_config, current_slot); + // setting current epoch to -1 to test that it is synced + assert_eq!(forester_pda.current_epoch, 1); + let mut expected_forester_pda = forester_pda.clone(); + expected_forester_pda.current_epoch = 2; + expected_forester_pda.active_stake_weight += + expected_forester_pda.pending_undelegated_stake_weight; + let delegate_account = + get_test_delegate_account_with_context(&protocol_config, current_slot); let authority = delegate_account.delegate_account.owner; let forester_pda_pubkey = delegate_account .delegate_account .delegate_forester_delegate_account .unwrap(); - let delegate_amount = 50; - let current_slot = 10; - let no_sync = true; + ( + protocol_config, + current_slot, + forester_pda, + expected_forester_pda, + delegate_account, + authority, + forester_pda_pubkey, + ) + } + #[test] + fn test_functional_undelegate() { + let ( + protocol_config, + current_slot, + mut forester_pda, + mut expected_forester_pda, + delegate_account, + authority, + forester_pda_pubkey, + ) = test_setup(); + let delegate_amount = 50; + // let current_slot = 10; + println!("pre forester_pda {:?}", forester_pda); let result = delegate_or_undelegate::( &authority, &protocol_config, @@ -445,7 +363,6 @@ mod tests { &mut forester_pda, delegate_amount, current_slot, - no_sync, ) .unwrap(); @@ -460,7 +377,7 @@ mod tests { .delegate_account .pending_undelegated_stake_weight + delegate_amount, - pending_epoch: protocol_config.get_current_epoch(current_slot), + pending_epoch: forester_pda.last_registered_epoch, ..delegate_account.delegate_account }; @@ -474,20 +391,30 @@ mod tests { ) .unwrap(); assert_eq!(deserialized_delegate_account, expected_delegate_account); + expected_forester_pda + .sync(current_slot, &protocol_config) + .unwrap(); + expected_forester_pda.pending_undelegated_stake_weight -= delegate_amount; + expected_forester_pda.active_stake_weight -= delegate_amount; + + assert_eq!(forester_pda, expected_forester_pda); } #[test] fn test_delegate_or_undelegate_undelegate_fail() { + let ( + protocol_config, + current_slot, + mut forester_pda, + mut expected_forester_pda, + delegate_account, + authority, + forester_pda_pubkey, + ) = test_setup(); let authority = Pubkey::new_unique(); - let protocol_config = ProtocolConfig { - ..Default::default() - }; let forester_pda_pubkey = Pubkey::new_unique(); - let mut forester_pda = get_test_forester_account(); - let delegate_account = get_test_delegate_account_with_context(); let delegate_amount = u64::MAX; let current_slot = 10; - let no_sync = true; let result = delegate_or_undelegate::( &authority, @@ -497,7 +424,6 @@ mod tests { &mut forester_pda, delegate_amount, current_slot, - no_sync, ); assert!(matches!(result, Err(error) if error == RegistryError::InvalidAuthority.into())); @@ -505,16 +431,19 @@ mod tests { #[test] fn test_delegate_or_undelegate_delegate_fail() { - let protocol_config = ProtocolConfig { - ..Default::default() - }; + let ( + protocol_config, + current_slot, + mut forester_pda, + mut expected_forester_pda, + delegate_account, + authority, + forester_pda_pubkey, + ) = test_setup(); let forester_pda_pubkey = Pubkey::new_unique(); - let mut forester_pda = get_test_forester_account(); - let delegate_account = get_test_delegate_account_with_context(); - let authority = delegate_account.delegate_account.owner; + // let authority = delegate_account.delegate_account.owner; let delegate_amount = u64::MAX; let current_slot = 10; - let no_sync = true; let result = delegate_or_undelegate::( &authority, @@ -524,9 +453,8 @@ mod tests { &mut forester_pda, delegate_amount, current_slot, - no_sync, ); - + println!("{:?}", result); assert!(matches!(result, Err(error) if error == RegistryError::AlreadyDelegated.into())); } } diff --git a/programs/registry/src/delegate/deposit.rs b/programs/registry/src/delegate/process_deposit.rs similarity index 98% rename from programs/registry/src/delegate/deposit.rs rename to programs/registry/src/delegate/process_deposit.rs index a548d091fd..6e2bb8e5e8 100644 --- a/programs/registry/src/delegate/deposit.rs +++ b/programs/registry/src/delegate/process_deposit.rs @@ -20,10 +20,10 @@ use light_system_program::{ use light_utils::hash_to_bn254_field_size_be; use super::{ + delegate_account::{DelegateAccount, InputDelegateAccount}, deposit_instruction::DepositOrWithdrawInstruction, get_escrow_token_authority, process_cpi::{cpi_compressed_token_transfer, cpi_light_system_program}, - state::{DelegateAccount, InputDelegateAccount}, DELEGATE_ACCOUNT_DISCRIMINATOR, ESCROW_TOKEN_ACCOUNT_SEED, }; @@ -48,7 +48,11 @@ pub fn process_deposit_or_withdrawal<'a, 'b, 'c, 'info: 'b + 'c, const IS_DEPOSI ) -> Result<()> { let mint = &ctx.accounts.protocol_config.config.mint; let slot = Clock::get()?.slot; - let epoch = ctx.accounts.protocol_config.config.get_current_epoch(slot); + let epoch = ctx + .accounts + .protocol_config + .config + .get_current_registration_epoch(slot); let compressed_accounts = deposit_or_withdraw::( &ctx.accounts.authority.key(), &ctx.accounts.escrow_token_authority.key(), @@ -174,12 +178,21 @@ pub fn deposit_or_withdraw( deposit_amount, escrow_token_account_merkle_tree_index, )?; + let input_escrow_token_account_hash = if let Some(input_escrow_token_account) = input_escrow_token_account.as_ref() { + // msg!( + // "input_escrow_token_account {:?}", + // input_escrow_token_account + // ); + // let hashed_owner = hash_to_bn254_field_size_be(escrow_token_authority.as_ref()) + // .unwrap() + // .0; + Some( hash_input_token_data_with_context( &hashed_mint, - &hashed_owner, + &hashed_escrow_token_authority, input_escrow_token_account.amount, ) .map_err(ProgramError::from)?, @@ -209,7 +222,7 @@ pub fn deposit_or_withdraw( )?; output_token_accounts.push(change_compressed_token_account); } - // TODO: create a close account instruction + let (input_delegate_pda, output_delegate_pda) = update_delegate_compressed_account::( delegate_account, authority, @@ -259,6 +272,10 @@ pub fn hash_input_token_data_with_context( hashed_owner: &[u8; 32], amount: u64, ) -> std::result::Result<[u8; 32], HasherError> { + println!("mint {:?}", mint); + println!("hashed_owner {:?}", hashed_owner); + println!("amount {:?}", amount); + let amount_bytes = amount.to_le_bytes(); TokenData::hash_with_hashed_values::(mint, hashed_owner, &amount_bytes, &None) } @@ -285,6 +302,7 @@ fn update_delegate_compressed_account( let (mut delegate_account, input_account) = create_input_delegate_account(authority, input_escrow_token_account_hash, input)?; delegate_account.escrow_token_account_hash = output_escrow_token_account_hash; + // delegate_account.sync_pending_stake_weight(epoch); (Some(input_account), delegate_account) } else { ( @@ -297,6 +315,10 @@ fn update_delegate_compressed_account( }, ) }; + msg!( + "update_delegate_compressed_account: delegate_account {:?}", + delegate_account + ); if IS_DEPOSIT { delegate_account.stake_weight = delegate_account .stake_weight diff --git a/programs/registry/src/delegate/state.rs b/programs/registry/src/delegate/state.rs deleted file mode 100644 index d415f2f345..0000000000 --- a/programs/registry/src/delegate/state.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::protocol_config::state::ProtocolConfig; -use aligned_sized::aligned_sized; -use anchor_lang::prelude::*; -use anchor_lang::solana_program::pubkey::Pubkey; -use light_hasher::{errors::HasherError, DataHasher, Hasher}; -use light_utils::hash_to_bn254_field_size_be; - -/// Instruction data input verion of DelegateAccount The following fields are -/// missing since these are computed onchain: -/// 1. owner -/// 2. escrow_token_account_hash -/// -> we save 64 bytes in instructiond data -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] -pub struct InputDelegateAccount { - pub delegate_forester_delegate_account: Option, - /// Stake weight that is delegated to a forester. - /// Newly delegated stake is not active until the next epoch. - pub delegated_stake_weight: u64, - /// undelgated stake is stake that is not yet delegated to a forester - pub stake_weight: u64, - /// When delegating stake is pending until the next epoch - pub pending_delegated_stake_weight: u64, - /// When undelegating stake is pending until the next epoch - pub pending_undelegated_stake_weight: u64, - pub pending_synced_stake_weight: u64, - pub pending_epoch: u64, - pub last_sync_epoch: u64, - /// Pending token amount are rewards that are not yet claimed to the stake - /// compressed token account. - pub pending_token_amount: u64, -} - -impl From for InputDelegateAccount { - fn from(delegate_account: DelegateAccount) -> Self { - InputDelegateAccount { - delegate_forester_delegate_account: delegate_account.delegate_forester_delegate_account, - delegated_stake_weight: delegate_account.delegated_stake_weight, - stake_weight: delegate_account.stake_weight, - pending_undelegated_stake_weight: delegate_account.pending_undelegated_stake_weight, - pending_epoch: delegate_account.pending_epoch, - last_sync_epoch: delegate_account.last_sync_epoch, - pending_token_amount: delegate_account.pending_token_amount, - pending_synced_stake_weight: delegate_account.pending_synced_stake_weight, - pending_delegated_stake_weight: delegate_account.pending_delegated_stake_weight, - } - } -} - -#[aligned_sized] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] -pub struct DelegateAccount { - pub owner: Pubkey, - pub delegate_forester_delegate_account: Option, - /// Stake weight that is delegated to a forester. - /// Newly delegated stake is not active until the next epoch. - pub delegated_stake_weight: u64, - /// newly delegated stakeweight becomes active after the next epoch - pub pending_delegated_stake_weight: u64, - /// undelgated stake is stake that is not yet delegated to a forester - pub stake_weight: u64, - /// Buffer variable to account for the lag of one epoch for rewards to reach - /// to registration account - pub pending_synced_stake_weight: u64, - /// When undelegating stake is pending until the next epoch - pub pending_undelegated_stake_weight: u64, - pub pending_epoch: u64, - pub last_sync_epoch: u64, - /// Pending token amount are rewards that are not yet claimed to the stake - /// compressed token account. - pub pending_token_amount: u64, - pub escrow_token_account_hash: [u8; 32], -} - -pub trait CompressedAccountTrait { - fn get_owner(&self) -> Pubkey; -} -impl CompressedAccountTrait for DelegateAccount { - fn get_owner(&self) -> Pubkey { - self.owner - } -} - -// TODO: pass in hashed owner -impl DataHasher for DelegateAccount { - fn hash(&self) -> std::result::Result<[u8; 32], HasherError> { - let hashed_owner = hash_to_bn254_field_size_be(self.owner.as_ref()).unwrap().0; - let hashed_delegate_forester_delegate_account = - if let Some(delegate_forester_delegate_account) = - self.delegate_forester_delegate_account - { - hash_to_bn254_field_size_be(delegate_forester_delegate_account.as_ref()) - .unwrap() - .0 - } else { - [0u8; 32] - }; - H::hashv(&[ - hashed_owner.as_slice(), - hashed_delegate_forester_delegate_account.as_slice(), - &self.delegated_stake_weight.to_le_bytes(), - &self.pending_synced_stake_weight.to_le_bytes(), - &self.stake_weight.to_le_bytes(), - &self.pending_undelegated_stake_weight.to_le_bytes(), - ]) - } -} - -impl DelegateAccount { - pub fn sync_pending_stake_weight(&mut self, current_epoch: u64) { - msg!("sync_pending_stake_weight current_epoch: {}", current_epoch); - msg!( - "sync_pending_stake_weight pending_epoch: {}", - self.pending_epoch - ); - if current_epoch > self.pending_epoch { - self.stake_weight += self.pending_undelegated_stake_weight; - self.pending_undelegated_stake_weight = 0; - // last sync epoch is only relevant for syncing the delegate account with the forester rewards - // self.last_sync_epoch = current_epoch; - self.delegated_stake_weight += self.pending_delegated_stake_weight; - self.pending_delegated_stake_weight = 0; - // self.pending_epoch = 0; - } - } -} - -// pub fn undelegate( -// protocol_config: &ProtocolConfig, -// delegate_account: &mut DelegateAccount, -// forester_pda: &mut ForesterAccount, -// amount: u64, -// current_slot: u64, -// ) -> Result<()> { -// forester_pda.sync(current_slot, protocol_config)?; -// forester_pda.active_stake_weight -= amount; -// delegate_account.delegated_stake_weight -= amount; -// delegate_account.pending_undelegated_stake_weight += amount; -// delegate_account.pending_epoch = protocol_config.get_current_epoch(current_slot); -// if delegate_account.delegated_stake_weight == 0 { -// delegate_account.delegate_forester_delegate_account = None; -// } -// Ok(()) -// } - -// TODO: we need a drastically improved compressed token transfer sdk -// pub fn withdraw_instruction( -// delegate_account: &mut DelegateAccount, -// delegate_token_account: &mut AccountInfo, -// recipient_token_account: &mut AccountInfo, -// protocol_config: ProtocolConfig, -// amount: u64, -// current_slot: u64, -// ) -> Result<()> { -// withdraw(delegate_account, protocol_config, amount, current_slot); -// // transfer tokens -// // TODO: add compressed token transfer -// // delegate_token_account.balance -= amount; -// // recipient_token_account.balance += amount; -// Ok(()) -// } -/** - * User flow: - * 1. Deposit compressed tokens to DelegatePda - * - inputs: InputTokenData, deposit_amount - * - create two outputs, escrow compressed account and change account - * - compressed escrow account is owned by pda derived from authority - * - - */ -#[allow(unused)] -fn withdraw( - delegate_account: &mut DelegateAccount, - protocol_config: ProtocolConfig, - amount: u64, - current_slot: u64, -) { - let current_epoch = protocol_config.get_current_epoch(current_slot); - delegate_account.sync_pending_stake_weight(current_epoch); - // reduce stake weight - // only non delegated stake can be unstaked - delegate_account.stake_weight -= amount; -} diff --git a/programs/registry/src/epoch/claim_forester.rs b/programs/registry/src/epoch/claim_forester.rs index 0f59e35131..ebd457776e 100644 --- a/programs/registry/src/epoch/claim_forester.rs +++ b/programs/registry/src/epoch/claim_forester.rs @@ -1,9 +1,9 @@ use crate::{ delegate::{ + delegate_account::CompressedAccountTrait, process_cpi::{ cpi_compressed_token_mint_to, cpi_light_system_program, mint_spl_to_pool_pda, }, - state::CompressedAccountTrait, FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, }, errors::RegistryError, @@ -24,6 +24,8 @@ use super::{ claim_forester_instruction::ClaimForesterInstruction, register_epoch::{EpochPda, ForesterEpochPda}, }; + +// TODO: make sure that performance based rewards can only be claimed if work has been reported // TODO: add reimbursement for opening the epoch account (close an one epoch account to open a new one of X epochs ago) /// Forester claim rewards: /// 1. Transfer forester fees to foresters compressed token account @@ -169,6 +171,7 @@ pub fn forester_claim_rewards( forester_pda_pubkey: &Pubkey, merkle_tree_index: u8, ) -> Result<(OutputCompressedAccountWithPackedContext, u64, u64)> { + forester_pda.sync(current_slot, &epoch_pda.protocol_config)?; epoch_pda .protocol_config .is_post_epoch(current_slot, forester_epoch_pda.epoch)?; @@ -228,7 +231,10 @@ pub fn forester_claim_rewards( #[cfg(test)] mod tests { - use crate::{protocol_config::state::ProtocolConfig, ForesterConfig}; + use crate::{ + protocol_config::{self, state::ProtocolConfig}, + ForesterConfig, + }; use super::*; use anchor_lang::solana_program::pubkey::Pubkey; @@ -301,13 +307,6 @@ mod tests { ..Default::default() }; - let forester_epoch_pda = ForesterEpochPda { - epoch: 1, - stake_weight: active_stake, - work_counter: 100, - ..Default::default() - }; - let epoch_pda = EpochPda { registered_stake: active_stake, total_work: 100, @@ -324,6 +323,13 @@ mod tests { epoch: 1, ..Default::default() }; + let forester_epoch_pda = ForesterEpochPda { + epoch: 1, + stake_weight: active_stake, + work_counter: 100, + protocol_config: epoch_pda.protocol_config, + ..Default::default() + }; let current_slot = 100; let forester_pda_pubkey = Pubkey::default(); @@ -424,6 +430,10 @@ mod tests { .as_ref() .unwrap() .data_hash; + pre_forester_pda.last_claimed_epoch = forester_epoch_pda.epoch; + pre_forester_pda.current_epoch = epoch_pda + .protocol_config + .get_current_registration_epoch(current_slot); assert_eq!(fee, 5); // 100 * 0.05 assert_eq!(net_reward, 95); // 100 - 5 diff --git a/programs/registry/src/epoch/register_epoch.rs b/programs/registry/src/epoch/register_epoch.rs index 5d560eccc7..92639ad69b 100644 --- a/programs/registry/src/epoch/register_epoch.rs +++ b/programs/registry/src/epoch/register_epoch.rs @@ -29,8 +29,7 @@ pub struct ForesterEpochPda { pub stake_weight: u64, pub work_counter: u64, /// Work can be reported in an extra round to earn extra performance based - /// rewards. // TODO: make sure that performance based rewards can only be - /// claimed if work has been reported + /// rewards. pub has_reported_work: bool, /// Start index of the range that determines when the forester is eligible to perform work. /// End index is forester_start_index + stake_weight @@ -67,7 +66,6 @@ impl ForesterEpochPda { Ok(epoch_progres / self.protocol_config.slot_length) } - // TODO: add function that returns all light slots with start and end solana slots for a given epoch pub fn get_eligible_forester_index( current_light_slot: u64, pubkey: &Pubkey, @@ -119,6 +117,7 @@ impl ForesterEpochPda { current_solana_slot: u64, ) -> Result<()> { if forester_epoch_pda.authority != *authority { + #[cfg(target_os = "solana")] msg!( "Invalid forester: forester_epoch_pda authority {} != provided {}", forester_epoch_pda.authority, @@ -126,9 +125,7 @@ impl ForesterEpochPda { ); return err!(RegistryError::InvalidForester); } - // let current_slot = forester_epoch_pda.get_current_slot(current_solana_slot)?; forester_epoch_pda.check_eligibility(current_solana_slot, queue_pubkey)?; - // TODO: check eligibility forester_epoch_pda.work_counter += 1; Ok(()) } @@ -209,6 +206,12 @@ pub fn register_for_epoch_instruction( epoch_pda: &mut EpochPda, current_slot: u64, ) -> Result<()> { + // Check whether we are in a epoch registration phase and which epoch we are in + let current_epoch_start_slot = epoch_pda + .protocol_config + .is_registration_phase(current_slot, epoch_pda.epoch)?; + // Sync pending stake to active stake if stake hasn't been synced yet. + forester_pda.sync(current_slot, &epoch_pda.protocol_config)?; msg!("epoch_pda.protocol config: {:?}", epoch_pda.protocol_config); if forester_pda.active_stake_weight < epoch_pda.protocol_config.min_stake { return err!(RegistryError::StakeInsuffient); @@ -224,19 +227,10 @@ pub fn register_for_epoch_instruction( return err!(RegistryError::ForesterAlreadyRegistered); } msg!("epoch_pda.epoch: {}", epoch_pda.epoch); - // Check whether we are in a epoch registration phase and which epoch we are in - let current_epoch_start_slot = epoch_pda - .protocol_config - .is_registration_phase(current_slot)?; - msg!("current_epoch_start_slot: {}", current_epoch_start_slot); - // Sync pending stake to active stake if stake hasn't been synced yet. - forester_pda.sync(current_slot, &epoch_pda.protocol_config)?; + forester_pda.last_registered_epoch = epoch_pda.epoch; - msg!("synced forester account stake"); - msg!("register for epoch with forester_pda: {:?}", forester_pda); - msg!("stake: {}", forester_pda.active_stake_weight); // Add forester active stake to epoch registered stake. - // // Initialize forester epoch account. + // Initialize forester epoch account. let initialized_forester_epoch_pda = ForesterEpochPda { authority: *authority, config: forester_pda.config, @@ -350,6 +344,7 @@ mod test { assert_eq!(sum, total_slots); } + // TODO: add randomized test #[test] fn test_onchain_epoch() { let registration_phase_length = 1; @@ -367,142 +362,76 @@ mod test { mint: Pubkey::new_unique(), forester_registration_guarded: false, }; + // Diagram of epochs 0 and 1. // Registration 0 starts at genesis slot. // |---- Registration 0 ----|------------------ Active 0 ------|---- Report Work 0 ----|---- Post 0 ---- // |-- Registration 1 --|------------------ Active 1 ----------------- - let mut current_slot = protocol_config.genesis_slot; + let mut current_solana_slot = protocol_config.genesis_slot; for epoch in 0..1000 { if epoch == 0 { for _ in 0..protocol_config.registration_phase_length { - assert!(protocol_config.is_registration_phase(current_slot).is_ok()); + assert!(protocol_config + .is_registration_phase(current_solana_slot, epoch) + .is_ok()); assert!(protocol_config - .is_active_phase(current_slot, epoch) + .is_active_phase(current_solana_slot, epoch) + .is_err()); + assert!(protocol_config + .is_post_epoch(current_solana_slot, epoch) .is_err()); - assert!(protocol_config.is_post_epoch(current_slot, epoch).is_err()); assert!(protocol_config - .is_report_work_phase(current_slot, epoch) + .is_report_work_phase(current_solana_slot, epoch) .is_err()); - current_slot += 1; + current_solana_slot += 1; } } for i in 0..protocol_config.active_phase_length { - assert!(protocol_config.is_active_phase(current_slot, epoch).is_ok()); + assert!(protocol_config + .is_active_phase(current_solana_slot, epoch) + .is_ok()); if protocol_config.active_phase_length.saturating_sub(i) <= protocol_config.registration_phase_length { - assert!(protocol_config.is_registration_phase(current_slot).is_ok()); - } else { - assert!(protocol_config.is_registration_phase(current_slot).is_err()); - } - if epoch == 0 { - assert!(protocol_config.is_post_epoch(current_slot, epoch).is_err()); - } else { - assert!(protocol_config - .is_post_epoch(current_slot, epoch - 1) - .is_ok()); - } - if epoch == 0 { - assert!(protocol_config - .is_report_work_phase(current_slot, epoch) - .is_err()); - } else if i < protocol_config.report_work_phase_length { assert!(protocol_config - .is_report_work_phase(current_slot, epoch - 1) + .is_registration_phase(current_solana_slot, epoch + 1) .is_ok()); } else { assert!(protocol_config - .is_report_work_phase(current_slot, epoch - 1) + .is_registration_phase(current_solana_slot, epoch) .is_err()); } - assert!(protocol_config - .is_report_work_phase(current_slot, epoch) - .is_err()); - current_slot += 1; - } - } - } - - // TODO: remove - #[test] - fn test_offchain_epoch() { - let registration_phase_length = 1; - let active_phase_length = 7; - let report_work_phase_length = 2; - let protocol_config = ProtocolConfig { - genesis_slot: 20, - registration_phase_length, - active_phase_length, - report_work_phase_length, - epoch_reward: 100_000, - base_reward: 50_000, - min_stake: 0, - slot_length: 1, - mint: Pubkey::new_unique(), - forester_registration_guarded: false, - }; - // Diagram of epochs 0 and 1. - // Registration 0 starts at genesis slot. - // |---- Registration 0 ----|------------------ Active 0 ------|---- Report Work 0 ----|---- Post 0 ---- - // |-- Registration 1 --|------------------ Active 1 ----------------- - - let mut current_slot = protocol_config.genesis_slot; - for epoch in 0..1000 { - if epoch == 0 { - for _ in 0..protocol_config.registration_phase_length { - assert!(protocol_config.is_registration_phase(current_slot).is_ok()); - - assert!(protocol_config - .is_active_phase(current_slot, epoch) - .is_err()); - assert!(protocol_config.is_post_epoch(current_slot, epoch).is_err()); - + if epoch == 0 { assert!(protocol_config - .is_report_work_phase(current_slot, epoch) + .is_post_epoch(current_solana_slot, epoch) .is_err()); - - current_slot += 1; - } - } - - for i in 0..protocol_config.active_phase_length { - assert!(protocol_config.is_active_phase(current_slot, epoch).is_ok()); - if protocol_config.active_phase_length.saturating_sub(i) - <= protocol_config.registration_phase_length - { - assert!(protocol_config.is_registration_phase(current_slot).is_ok()); - } else { - assert!(protocol_config.is_registration_phase(current_slot).is_err()); - } - if epoch == 0 { - assert!(protocol_config.is_post_epoch(current_slot, epoch).is_err()); } else { assert!(protocol_config - .is_post_epoch(current_slot, epoch - 1) + .is_post_epoch(current_solana_slot, epoch - 1) .is_ok()); } if epoch == 0 { assert!(protocol_config - .is_report_work_phase(current_slot, epoch) + .is_report_work_phase(current_solana_slot, epoch) .is_err()); } else if i < protocol_config.report_work_phase_length { assert!(protocol_config - .is_report_work_phase(current_slot, epoch - 1) + .is_report_work_phase(current_solana_slot, epoch - 1) .is_ok()); } else { assert!(protocol_config - .is_report_work_phase(current_slot, epoch - 1) + .is_report_work_phase(current_solana_slot, epoch - 1) .is_err()); } assert!(protocol_config - .is_report_work_phase(current_slot, epoch) + .is_report_work_phase(current_solana_slot, epoch) .is_err()); - current_slot += 1; + current_solana_slot += 1; } } } diff --git a/programs/registry/src/epoch/sync_delegate.rs b/programs/registry/src/epoch/sync_delegate.rs index 7203bf58a0..a0445ca37e 100644 --- a/programs/registry/src/epoch/sync_delegate.rs +++ b/programs/registry/src/epoch/sync_delegate.rs @@ -1,16 +1,18 @@ -use crate::delegate::deposit::{ - create_compressed_delegate_account, create_delegate_compressed_account, - update_escrow_compressed_token_account, DelegateAccountWithPackedContext, -}; use crate::delegate::process_cpi::{ approve_spl_token, cpi_compressed_token_transfer, get_cpi_signer_seeds, }; +use crate::delegate::process_deposit::{ + create_compressed_delegate_account, create_delegate_compressed_account, + hash_input_token_data_with_context, update_escrow_compressed_token_account, + DelegateAccountWithPackedContext, +}; +use crate::delegate::{delegate_account::DelegateAccount, process_cpi::cpi_light_system_program}; use crate::delegate::{ get_escrow_token_authority, ESCROW_TOKEN_ACCOUNT_SEED, FORESTER_EPOCH_RESULT_ACCOUNT_DISCRIMINATOR, }; -use crate::delegate::{process_cpi::cpi_light_system_program, state::DelegateAccount}; use crate::errors::RegistryError; +use crate::MINT; use anchor_lang::prelude::*; use light_compressed_token::process_transfer::{ InputTokenDataWithContext, PackedTokenTransferOutputData, @@ -23,6 +25,7 @@ use light_system_program::sdk::compressed_account::{ use light_system_program::sdk::CompressedCpiContext; use light_system_program::OutputCompressedAccountWithPackedContext; use light_utils::hash_to_bn254_field_size_be; +use num_traits::ToBytes; use super::claim_forester::CompressedForesterEpochAccountInput; use super::sync_delegate_instruction::SyncDelegateInstruction; @@ -56,7 +59,11 @@ pub fn process_sync_delegate_account<'info>( .as_ref() .map(|authority| authority.key()); let slot = Clock::get()?.slot; - let epoch = ctx.accounts.protocol_config.config.get_current_epoch(slot); + let epoch = ctx + .accounts + .protocol_config + .config + .get_current_registration_epoch(slot); let ( input_delegate_compressed_account, // TODO: we need readonly accounts just add a bool to input accounts context and don't nullify, skip in sum checks @@ -201,7 +208,6 @@ fn sync_delegate_account_and_create_compressed_accounts( delegate_account.merkle_context, delegate_account.root_index, )?; - let epoch = compressed_forester_epoch_pdas.last().unwrap().epoch; let last_forester_pda_hash = sync_delegate_account( &mut delegate_account.delegate_account, @@ -209,10 +215,6 @@ fn sync_delegate_account_and_create_compressed_accounts( previous_hash, forester_pda_pubkey, )?; - // could also take epoch from last compressed forester epoch pda - delegate_account - .delegate_account - .sync_pending_stake_weight(epoch); let input_readonly_compressed_forester_epoch_account = create_compressed_forester_epoch_account( last_forester_pda_hash, last_account_merkle_context, @@ -221,18 +223,37 @@ fn sync_delegate_account_and_create_compressed_accounts( let output_escrow_account = if input_escrow_token_account.is_some() { let amount = delegate_account.delegate_account.pending_token_amount; - msg!("pending token amount: {:?}", amount); - delegate_account.delegate_account.pending_token_amount = 0; - Some(update_escrow_compressed_token_account::( + let output_escrow_account = update_escrow_compressed_token_account::( &escrow_token_authority.unwrap(), input_escrow_token_account, amount, merkle_tree_index, - )?) + )?; + delegate_account.delegate_account.pending_token_amount = 0; + let hashed_owner = hash_to_bn254_field_size_be(escrow_token_authority.unwrap().as_ref()) + .unwrap() + .0; + let hashed_mint = hash_to_bn254_field_size_be(MINT.to_bytes().as_ref()) + .unwrap() + .0; + msg!("output_escrow_account: {:?}", output_escrow_account); + let output_escrow_hash = hash_input_token_data_with_context( + &hashed_mint, + &hashed_owner, + output_escrow_account.amount, + ); + msg!("output_escrow_hash: {:?}", output_escrow_hash); + delegate_account.delegate_account.escrow_token_account_hash = output_escrow_hash.unwrap(); + + Some(output_escrow_account) } else { + msg!("no escrow account"); None }; - + println!( + "delegate_account.delegate_account {:?}", + delegate_account.delegate_account + ); let output_account: CompressedAccount = create_delegate_compressed_account::(&delegate_account.delegate_account)?; let output_account_with_merkle_context = OutputCompressedAccountWithPackedContext { @@ -302,7 +323,7 @@ fn create_compressed_forester_epoch_account( * - switch to contention and make that prod ready first * */ - +// TODO: check whether we can simplify this logic /// Sync Delegate Account: /// - syncs the virtual balance of accumulated stake rewards to the stake /// account @@ -319,67 +340,68 @@ fn create_compressed_forester_epoch_account( /// 5. prove inclusion of last hash in State merkle tree (outside of this function) pub fn sync_delegate_account( delegate_account: &mut DelegateAccount, - // pending_synced_stake_weight: &mut u64, - // mut pending_synced_stake_weight: Vec<(u64, u64)>, // (stake_weight, epoch) compressed_forester_epoch_pdas: Vec, - // epoch_synced: Vec, // add last synced epoch to compressed_forester_epoch_pdas (probably need to add this to forester epoch pdas too) // keep rewards of none synced epochs in a vector, its active stake but not synced yet, - // TODO: ensure that this pending stake can also be undelegated + // TODO: ensure that this pending stake can also be undelegated (there could be a case) mut previous_hash: [u8; 32], forester_pubkey: Pubkey, ) -> Result<[u8; 32]> { let last_sync_epoch = delegate_account.last_sync_epoch; - if compressed_forester_epoch_pdas[0].epoch <= last_sync_epoch && last_sync_epoch != 0 { + if !compressed_forester_epoch_pdas.is_empty() + && compressed_forester_epoch_pdas[0].epoch <= last_sync_epoch + && last_sync_epoch != 0 + { return err!(RegistryError::StakeAccountAlreadySynced); } let hashed_forester_pubkey = hash_to_bn254_field_size_be(forester_pubkey.as_ref()) .ok_or(RegistryError::HashToFieldError)? .0; - // let mut epoch_rewards = vec![]; - // let mut last_stake_weight = delegate_account.delegated_stake_weight; let mut last_epoch = delegate_account.last_sync_epoch; for (i, compressed_forester_epoch_pda) in compressed_forester_epoch_pdas.iter().enumerate() { delegate_account.sync_pending_stake_weight(compressed_forester_epoch_pda.epoch); - // Forester pubkey is not hashed thus we use a random value and hash offchain let compressed_forester_epoch_pda = compressed_forester_epoch_pda .into_compressed_forester_epoch_pda(previous_hash, crate::ID); previous_hash = compressed_forester_epoch_pda.hash(hashed_forester_pubkey)?; - msg!( - "delegate_account.delegated_stake_weight: {:?}", - delegate_account.delegated_stake_weight - ); - msg!( - "delegate_account.pending_undelegated_stake_weight {:?}", - delegate_account.pending_undelegated_stake_weight - ); - msg!( - "stake from last epoch {:?}", - delegate_account.delegated_stake_weight - + delegate_account.pending_undelegated_stake_weight - - delegate_account.pending_synced_stake_weight, - ); let pending_synced_stake_weight = if compressed_forester_epoch_pda.epoch - last_epoch == 1 { delegate_account.pending_synced_stake_weight } else { 0 }; - msg!( + println!( "pending_synced_stake_weight: {:?}", pending_synced_stake_weight ); + println!( + "pending_undelegated_stake_weight: {:?}", + delegate_account.pending_undelegated_stake_weight + ); + println!( + "delegated_stake_weight: {:?}", + delegate_account.delegated_stake_weight + ); + println!( + "total stake: {:?}", + compressed_forester_epoch_pda.stake_weight + ); + println!( + "compressed_forester_epoch_pda.rewards_earned {:?}", + compressed_forester_epoch_pda.rewards_earned + ); + println!( + "usable stake {:?}", + delegate_account.delegated_stake_weight + + delegate_account.pending_undelegated_stake_weight + - pending_synced_stake_weight + ); // TODO: double check that this doesn't become an issue when undelegating let get_delegate_epoch_reward = compressed_forester_epoch_pda.get_reward( delegate_account.delegated_stake_weight + delegate_account.pending_undelegated_stake_weight - pending_synced_stake_weight, )?; - msg!( - "compressed_forester_epoch_pda: {:?}", - compressed_forester_epoch_pda - ); - msg!("epoch reward: {:?}", get_delegate_epoch_reward); + println!("get_delegate_epoch_reward: {:?}", get_delegate_epoch_reward); delegate_account.delegated_stake_weight = delegate_account .delegated_stake_weight .checked_add(get_delegate_epoch_reward) @@ -400,10 +422,6 @@ pub fn sync_delegate_account( if i == compressed_forester_epoch_pdas.len() - 1 { let last_delegate_account = compressed_forester_epoch_pda; delegate_account.last_sync_epoch = last_delegate_account.epoch; - println!( - "final pending_synced_stake_weight: {:?}", - delegate_account.pending_synced_stake_weight - ); return Ok(previous_hash); } } @@ -413,7 +431,8 @@ pub fn sync_delegate_account( #[cfg(test)] mod tests { - use core::num; + + use std::f32::MIN; use crate::{ delegate::DELEGATE_ACCOUNT_DISCRIMINATOR, @@ -802,10 +821,10 @@ mod tests { fn test_sync_delegate_account_undelegate_passing() { let ( mut delegate_account, - compressed_forester_epoch_pdas, + mut compressed_forester_epoch_pdas, previous_hash, forester_pda_pubkey, - expected_delegate_account, + mut expected_delegate_account, ) = get_test_data_sync_inconcistent(); // undelegate 50% in epoch 1 -> for the last epoch reward should only be 50% let undelegate = delegate_account.delegated_stake_weight / 2; @@ -813,25 +832,39 @@ mod tests { delegate_account.pending_undelegated_stake_weight += undelegate; delegate_account.delegated_stake_weight -= undelegate; delegate_account.pending_epoch = 0; - // delegate_account.delegated_stake_weight -= undelegate; - // expected_delegate_account.delegated_stake_weight -= undelegate; - // expected_delegate_account.pending_token_amount -= undelegate; - // expected_delegate_account.pending_synced_stake_weight = undelegate; + // third parties delegate additional stake in epoch 1 so that the delegates stake remains 50% even with rewards + compressed_forester_epoch_pdas[2].stake_weight += 990000; + + expected_delegate_account.stake_weight += undelegate; + expected_delegate_account.delegated_stake_weight -= + undelegate + compressed_forester_epoch_pdas[0].rewards_earned; + expected_delegate_account.pending_token_amount -= + compressed_forester_epoch_pdas[0].rewards_earned; + expected_delegate_account.pending_synced_stake_weight = + compressed_forester_epoch_pdas[0].rewards_earned / 2; + println!( + "compressed_forester_epoch_pdas[0..2].to_vec() {:?}", + compressed_forester_epoch_pdas[0..3].to_vec() + ); + + println!("pre delegate account {:?}", delegate_account); let result = sync_delegate_account( &mut delegate_account, - compressed_forester_epoch_pdas.clone(), + compressed_forester_epoch_pdas[0..3].to_vec(), previous_hash, forester_pda_pubkey, ); + println!("undelegated undelegate : {:?}", undelegate); + println!("post delegate account {:?}", delegate_account); assert!(result.is_ok()); - println!( - "{:?}", - 3 * compressed_forester_epoch_pdas[0].rewards_earned - - delegate_account.delegated_stake_weight - ); + // println!( + // "{:?}", + // compressed_forester_epoch_pdas[0].rewards_earned + // - delegate_account.delegated_stake_weight + // ); assert_eq!(delegate_account, expected_delegate_account); } @@ -1108,14 +1141,35 @@ mod tests { .unwrap() .rewards_earned; output_delegate_account.pending_token_amount = 0; + let hashed_escrow_owner = hash_to_bn254_field_size_be(escrow_token_authority.as_ref()) + .unwrap() + .0; + let hashed_bytes = hash_to_bn254_field_size_be(MINT.to_bytes().as_ref()) + .unwrap() + .0; + output_delegate_account.escrow_token_account_hash = hash_input_token_data_with_context( + &hashed_bytes, + &hashed_escrow_owner, + output_escrow_account.unwrap().amount, + ) + .unwrap(); let mut data = Vec::new(); output_delegate_account.serialize(&mut data).unwrap(); - let data = CompressedAccountData { discriminator: DELEGATE_ACCOUNT_DISCRIMINATOR, data_hash: output_delegate_account.hash::().unwrap(), data, }; + let reader = DelegateAccount::deserialize_reader( + &mut &output_account_with_merkle_context + .compressed_account + .data + .as_ref() + .unwrap() + .data[..], + ) + .unwrap(); + println!("reader {:?}", reader); assert_eq!( output_account_with_merkle_context .compressed_account diff --git a/programs/registry/src/epoch/sync_delegate_instruction.rs b/programs/registry/src/epoch/sync_delegate_instruction.rs index ad33a22bb4..f2759863cb 100644 --- a/programs/registry/src/epoch/sync_delegate_instruction.rs +++ b/programs/registry/src/epoch/sync_delegate_instruction.rs @@ -1,5 +1,5 @@ use crate::constants::FORESTER_TOKEN_POOL_SEED; -use crate::delegate::deposit::DelegateAccountWithPackedContext; +use crate::delegate::process_deposit::DelegateAccountWithPackedContext; use crate::delegate::traits::{CompressedCpiContextTrait, CompressedTokenProgramAccounts}; use crate::delegate::{ traits::{SignerAccounts, SystemProgramAccounts}, diff --git a/programs/registry/src/forester/state.rs b/programs/registry/src/forester/state.rs index 67dcb4f598..8866eb5e88 100644 --- a/programs/registry/src/forester/state.rs +++ b/programs/registry/src/forester/state.rs @@ -35,18 +35,16 @@ pub struct ForesterConfig { } impl ForesterAccount { - /// If current epoch changed, move pending stake to active stake and update - /// current epoch field + /// Sync should be called in any instruction which uses the forester + /// account before any action. Delegating to a forester new stakeweight is + /// added to the pending stake. If current epoch changed, move pending stake + /// to active stake and update current epoch field. This method is called in + /// delegate stake so that it is impossible to delegate and not sync before. pub fn sync(&mut self, current_slot: u64, protocol_config: &ProtocolConfig) -> Result<()> { // get last registration epoch, stake sync treats the registration phase // of an epoch like the next active epoch - let current_epoch = protocol_config.get_current_epoch( - current_slot.saturating_sub(protocol_config.registration_phase_length), - ); - // If the current epoch is greater than the last registered epoch, or next epoch is in registration phase - if current_epoch > self.current_epoch - || protocol_config.is_registration_phase(current_slot).is_ok() - { + let current_epoch = protocol_config.get_current_registration_epoch(current_slot); + if current_epoch > self.current_epoch { self.current_epoch = current_epoch; self.active_stake_weight += self.pending_undelegated_stake_weight; self.pending_undelegated_stake_weight = 0; diff --git a/programs/registry/src/lib.rs b/programs/registry/src/lib.rs index be440f7ec5..9dbec26374 100644 --- a/programs/registry/src/lib.rs +++ b/programs/registry/src/lib.rs @@ -23,12 +23,14 @@ pub mod forester; pub mod protocol_config; pub mod utils; use anchor_lang::solana_program::pubkey::Pubkey; -use delegate::deposit::{process_deposit_or_withdrawal, InputDelegateAccountWithPackedContext}; use delegate::process_delegate::process_delegate_or_undelegate; +use delegate::process_deposit::{ + process_deposit_or_withdrawal, InputDelegateAccountWithPackedContext, +}; pub use delegate::{delegate_instruction::*, deposit_instruction::*}; use delegate::{ - deposit::DelegateAccountWithPackedContext, process_cpi::{cpi_compressed_token_mint_to, get_cpi_signer_seeds}, + process_deposit::DelegateAccountWithPackedContext, }; use epoch::claim_forester::process_forester_claim_rewards; use epoch::{ @@ -263,7 +265,7 @@ pub mod light_registry { let protocol_config = ctx.accounts.protocol_config.config; let current_solana_slot = anchor_lang::solana_program::clock::Clock::get()?.slot; // Init epoch account if not initialized - let current_epoch = protocol_config.get_current_epoch(current_solana_slot); + let current_epoch = protocol_config.get_current_registration_epoch(current_solana_slot); // TODO: check that epoch is in registration phase if current_epoch != epoch { return err!(errors::RegistryError::InvalidEpoch); @@ -432,7 +434,7 @@ pub mod light_registry { delegate_account: DelegateAccountWithPackedContext, delegate_amount: u64, ) -> Result<()> { - process_delegate_or_undelegate::(ctx, proof, delegate_account, delegate_amount, false) + process_delegate_or_undelegate::(ctx, proof, delegate_account, delegate_amount) } pub fn undelegate<'info>( @@ -441,13 +443,7 @@ pub mod light_registry { delegate_account: DelegateAccountWithPackedContext, delegate_amount: u64, ) -> Result<()> { - process_delegate_or_undelegate::( - ctx, - proof, - delegate_account, - delegate_amount, - false, - ) + process_delegate_or_undelegate::(ctx, proof, delegate_account, delegate_amount) } pub fn claim_forester_rewards<'info>( diff --git a/programs/registry/src/protocol_config/initialize.rs b/programs/registry/src/protocol_config/initialize.rs index 481f112785..2eb8d2d78b 100644 --- a/programs/registry/src/protocol_config/initialize.rs +++ b/programs/registry/src/protocol_config/initialize.rs @@ -9,12 +9,14 @@ pub const PROTOCOL_CONFIG_PDA_SEED: &[u8] = b"authority"; #[derive(Accounts)] #[instruction(bump: u8)] pub struct InitializeProtocolConfig<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, /// CHECK: initial authority is program keypair. /// The authority should be updated to a different keypair after /// initialization. - #[account(mut, constraint= authority.key() == self_program.key())] + #[account( constraint= authority.key() == self_program.key())] pub authority: Signer<'info>, - #[account(init, seeds = [PROTOCOL_CONFIG_PDA_SEED], bump, space = ProtocolConfigPda::LEN, payer = authority)] + #[account(init, seeds = [PROTOCOL_CONFIG_PDA_SEED], bump, space = ProtocolConfigPda::LEN, payer = fee_payer)] pub protocol_config_pda: Account<'info, ProtocolConfigPda>, pub system_program: Program<'info, System>, pub mint: Account<'info, Mint>, diff --git a/programs/registry/src/protocol_config/state.rs b/programs/registry/src/protocol_config/state.rs index 527f1ec9ae..82d16f9b2e 100644 --- a/programs/registry/src/protocol_config/state.rs +++ b/programs/registry/src/protocol_config/state.rs @@ -52,7 +52,7 @@ impl Default for ProtocolConfig { genesis_slot: 0, epoch_reward: 1_000_000, base_reward: 500_000, - min_stake: 0, + min_stake: 1, slot_length: 10, registration_phase_length: 100, active_phase_length: 1000, @@ -76,7 +76,7 @@ pub enum EpochState { impl ProtocolConfig { /// Current epoch including registration phase Only use to get registration /// phase. - pub fn get_current_epoch(&self, slot: u64) -> u64 { + pub fn get_current_registration_epoch(&self, slot: u64) -> u64 { (slot.saturating_sub(self.genesis_slot)) / self.active_phase_length } pub fn get_current_active_epoch(&self, slot: u64) -> Result { @@ -87,7 +87,7 @@ impl ProtocolConfig { Ok(slot / self.active_phase_length) } - pub fn get_current_epoch_progress(&self, slot: u64) -> u64 { + pub fn get_current_registration_epoch_progress(&self, slot: u64) -> u64 { (slot.saturating_sub(self.genesis_slot)) % self.active_phase_length } @@ -97,35 +97,38 @@ impl ProtocolConfig { } /// In the last part of the active phase the registration phase starts. - pub fn is_registration_phase(&self, slot: u64) -> Result { - let current_epoch = self.get_current_epoch(slot); - let current_epoch_progress = self.get_current_epoch_progress(slot); + pub fn is_registration_phase(&self, solana_slot: u64, expected_epoch: u64) -> Result { + let current_epoch = self.get_current_registration_epoch(solana_slot); + let current_epoch_progress = self.get_current_registration_epoch_progress(solana_slot); if current_epoch_progress >= self.registration_phase_length { return err!(RegistryError::NotInRegistrationPeriod); } + if current_epoch != expected_epoch { + return err!(RegistryError::InvalidEpoch); + } Ok((current_epoch) * self.active_phase_length + self.genesis_slot + self.registration_phase_length) } - pub fn is_active_phase(&self, slot: u64, epoch: u64) -> Result<()> { - if self.get_current_active_epoch(slot)? != epoch { + pub fn is_active_phase(&self, solana_slot: u64, epoch: u64) -> Result<()> { + if self.get_current_active_epoch(solana_slot)? != epoch { return err!(RegistryError::NotInActivePhase); } Ok(()) } - pub fn is_report_work_phase(&self, slot: u64, epoch: u64) -> Result<()> { - self.is_active_phase(slot, epoch + 1)?; - let current_epoch_progress = self.get_current_active_epoch_progress(slot); + pub fn is_report_work_phase(&self, solana_slot: u64, epoch: u64) -> Result<()> { + self.is_active_phase(solana_slot, epoch + 1)?; + let current_epoch_progress = self.get_current_active_epoch_progress(solana_slot); if current_epoch_progress >= self.report_work_phase_length { return err!(RegistryError::NotInReportWorkPhase); } Ok(()) } - pub fn is_post_epoch(&self, slot: u64, epoch: u64) -> Result<()> { - if self.get_current_active_epoch(slot)? == epoch { + pub fn is_post_epoch(&self, solana_slot: u64, epoch: u64) -> Result<()> { + if self.get_current_active_epoch(solana_slot)? <= epoch { return err!(RegistryError::InvalidEpoch); } Ok(()) diff --git a/programs/registry/src/protocol_config/update.rs b/programs/registry/src/protocol_config/update.rs index 2f018e3c33..20f95e5711 100644 --- a/programs/registry/src/protocol_config/update.rs +++ b/programs/registry/src/protocol_config/update.rs @@ -7,7 +7,7 @@ use super::state::ProtocolConfigPda; #[derive(Accounts)] pub struct UpdateProtocolConfig<'info> { /// CHECK: authority is protocol config authority. - #[account(mut, constraint = authority.key() == protocol_config_pda.authority)] + #[account(constraint = authority.key() == protocol_config_pda.authority)] pub authority: Signer<'info>, /// CHECK: (seed constraints). #[account(mut, seeds = [PROTOCOL_CONFIG_PDA_SEED], bump)] diff --git a/programs/registry/src/sdk.rs b/programs/registry/src/sdk.rs index 0ebedeedef..619009e7d9 100644 --- a/programs/registry/src/sdk.rs +++ b/programs/registry/src/sdk.rs @@ -3,11 +3,11 @@ use std::collections::HashMap; use crate::{ delegate::{ - deposit::{ + get_escrow_token_authority, + process_deposit::{ DelegateAccountWithContext, DelegateAccountWithPackedContext, InputDelegateAccountWithPackedContext, }, - get_escrow_token_authority, }, epoch::{ claim_forester::CompressedForesterEpochAccountInput, @@ -15,8 +15,9 @@ use crate::{ }, protocol_config::state::ProtocolConfig, utils::{ - get_cpi_authority_pda, get_epoch_pda_address, get_forester_epoch_pda_address, - get_forester_pda_address, get_forester_token_pool_pda, get_protocol_config_pda_address, + get_cpi_authority_pda, get_epoch_pda_address, get_forester_accounts, + get_forester_accounts_epoch, get_forester_epoch_pda_address, get_forester_pda_address, + get_forester_token_pool_pda, get_protocol_config_pda_address, }, ForesterConfig, MINT, }; @@ -80,14 +81,16 @@ pub fn create_update_authority_instruction( new_authority, new_config: new_protocol_config, }; + let accounts = crate::accounts::UpdateProtocolConfig { + authority: signer_pubkey, + protocol_config_pda: protocol_config_pda.0, + new_authority, + }; // update with new authority Instruction { program_id: crate::ID, - accounts: vec![ - AccountMeta::new(signer_pubkey, true), - AccountMeta::new(protocol_config_pda.0, false), - ], + accounts: accounts.to_account_metas(Some(true)), data: update_authority_ix.data(), } } @@ -125,7 +128,8 @@ pub fn create_register_program_instruction( } pub fn create_initialize_governance_authority_instruction( - signer_pubkey: Pubkey, + fee_payer: Pubkey, + authority: Pubkey, protocol_config: ProtocolConfig, ) -> Instruction { let protocol_config_pda = get_protocol_config_pda_address(); @@ -137,7 +141,8 @@ pub fn create_initialize_governance_authority_instruction( let accounts = crate::accounts::InitializeProtocolConfig { protocol_config_pda: protocol_config_pda.0, - authority: signer_pubkey, + fee_payer, + authority, system_program: system_program::ID, mint: protocol_config.mint, cpi_authority: cpi_authority_pda, @@ -158,7 +163,7 @@ pub fn create_register_forester_instruction( let (forester_pda, _bump) = get_forester_pda_address(forester_authority); let instruction_data = crate::instruction::RegisterForester { _bump: 0, config }; let (protocol_config_pda, _) = get_protocol_config_pda_address(); - let token_pool_pda = get_forester_token_pool_pda(forester_authority); + let token_pool_pda = get_forester_token_pool_pda(&forester_pda); let accounts = crate::accounts::RegisterForester { forester_pda, fee_payer: *fee_payer, @@ -183,7 +188,7 @@ pub fn create_update_forester_epoch_pda_instruction( ) -> Instruction { let (forester_epoch_pda, _bump) = get_forester_pda_address(forester_authority); let instruction_data = crate::instruction::UpdateForesterEpochPda { - authority: *new_authority, + _authority: *new_authority, }; let accounts = crate::accounts::UpdateForesterEpochPda { forester_epoch_pda, @@ -626,9 +631,7 @@ pub fn create_forester_claim_instruction( let (cpi_authority_pda, _) = get_cpi_authority_pda(); let standard_registry_accounts = get_standard_registry_accounts(); - let forester_pda = get_forester_pda_address(&forester_pubkey).0; - let forester_epoch_pda = get_forester_epoch_pda_address(&forester_pda, epoch).0; - let forester_token_pool = get_forester_token_pool_pda(&forester_pubkey); + let forester_accounts = get_forester_accounts_epoch(&forester_pubkey, epoch); let epoch_pda = get_epoch_pda_address(epoch); let accounts = crate::accounts::ClaimForesterInstruction { fee_payer: forester_pubkey, @@ -644,9 +647,9 @@ pub fn create_forester_claim_instruction( system_program: standard_accounts.system_program, invoking_program: standard_registry_accounts.self_program, self_program: standard_registry_accounts.self_program, - forester_token_pool, - forester_epoch_pda, - forester_pda, + forester_token_pool: forester_accounts.forester_token_pool, + forester_epoch_pda: forester_accounts.forester_epoch_pda, + forester_pda: forester_accounts.forester_pda, spl_token_program: anchor_spl::token::ID, epoch_pda, mint: MINT, @@ -745,11 +748,12 @@ pub fn create_sync_delegate_instruction( &inputs.output_token_account_merkle_tree, ) as u8; let standard_accounts = get_standard_compressed_token_program_accounts(MINT); + let forester_accounts = get_forester_accounts(&inputs.forester_pubkey); ( Some(get_escrow_token_authority(&inputs.sender, inputs.salt).0), Some(inputs.cpi_context_account), Some(standard_accounts.compressed_token_program), - Some(get_forester_token_pool_pda(&inputs.forester_pubkey)), + Some(forester_accounts.forester_token_pool), Some(standard_accounts.token_cpi_authority_pda), Some(sync_delegate_token_account), Some(input_token_data_with_context[0].clone()), diff --git a/test-programs/registry-test/tests/tests.rs b/test-programs/registry-test/tests/tests.rs index 305cc5f113..86e9482655 100644 --- a/test-programs/registry-test/tests/tests.rs +++ b/test-programs/registry-test/tests/tests.rs @@ -5,8 +5,8 @@ use light_registry::account_compression_cpi::sdk::{ create_nullify_instruction, create_update_address_merkle_tree_instruction, CreateNullifyInstructionInputs, UpdateAddressMerkleTreeInstructionInputs, }; +use light_registry::delegate::delegate_account::DelegateAccount; use light_registry::delegate::get_escrow_token_authority; -use light_registry::delegate::state::DelegateAccount; use light_registry::epoch::claim_forester::CompressedForesterEpochAccount; use light_registry::errors::RegistryError; use light_registry::protocol_config::state::{ProtocolConfig, ProtocolConfigPda}; @@ -276,7 +276,6 @@ async fn test_delegate() { amount: deposit_amount, delegate_account: delegate_account[0].as_ref().unwrap().clone(), forester_pda, - no_sync: false, output_merkle_tree: env.merkle_tree_pubkey, }; delegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) @@ -300,7 +299,6 @@ async fn test_delegate() { amount: deposit_amount - 1, delegate_account: delegate_account[0].as_ref().unwrap().clone(), forester_pda, - no_sync: false, output_merkle_tree: env.merkle_tree_pubkey, }; undelegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) @@ -319,7 +317,6 @@ async fn test_delegate() { amount: 1, delegate_account: delegate_account[0].as_ref().unwrap().clone(), forester_pda, - no_sync: false, output_merkle_tree: env.merkle_tree_pubkey, }; undelegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) @@ -356,6 +353,7 @@ async fn test_e2e() { .supply; let mut rng = rand::rngs::ThreadRng::default(); let seed = rng.gen::(); + let seed = 0; let mut rng_from_seed = rand::rngs::StdRng::seed_from_u64(seed); // let mut counter = 0; let forester_keypair = env.forester.insecure_clone(); @@ -445,7 +443,7 @@ async fn test_e2e() { ForesterEpochPda::try_deserialize(&mut account.data.as_slice()).unwrap(); println!("forester_epoch_pda: {:?}", forester_epoch_pda); } - let forester_token_pool = get_forester_token_pool_pda(&foresters[0].0.pubkey()); + let forester_token_pool = get_forester_token_pool_pda(&forester_pda_pubkey); let forester_token_pool_account = e2e_env.rpc.get_account(forester_token_pool).await.unwrap(); if let Some(account) = forester_token_pool_account { @@ -700,7 +698,6 @@ async fn test_e2e() { amount, delegate_account: delegate_account[0].as_ref().unwrap().clone(), forester_pda: env.registered_forester_pda, - no_sync: false, output_merkle_tree: env.merkle_tree_pubkey, }; undelegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) @@ -750,8 +747,8 @@ async fn test_e2e() { "added {} delegates -----------------------------------------", num_add_delegates ); - - let forester_token_pool = get_forester_token_pool_pda(&foresters[0].0.pubkey()); + let forester_pda_pubkey = get_forester_pda_address(&foresters[0].0.pubkey()).0; + let forester_token_pool = get_forester_token_pool_pda(&forester_pda_pubkey); let forester_token_pool_account = e2e_env.rpc.get_account(forester_token_pool).await.unwrap(); if let Some(account) = forester_token_pool_account { @@ -859,7 +856,9 @@ async fn test_e2e() { .await .unwrap(); } - let forester_token_pool = get_forester_token_pool_pda(&foresters[0].0.pubkey()); + let forester_pda_pubkey = get_forester_pda_address(&foresters[0].0.pubkey()).0; + + let forester_token_pool = get_forester_token_pool_pda(&forester_pda_pubkey); let forester_token_pool_account = e2e_env.rpc.get_account(forester_token_pool).await.unwrap(); if let Some(account) = forester_token_pool_account { let forester_token_pool_balance = @@ -1158,28 +1157,28 @@ async fn failing_test_forester() { let expected_error_code = anchor_lang::error::ErrorCode::ConstraintAddress as u32; assert_rpc_error(result, 0, expected_error_code).unwrap(); } - // 2. FAIL: Update forester authority with invalid authority - { - let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); - let forester_epoch_pda = get_forester_epoch_pda_address(&forester_pda, 0).0; - let instruction_data = light_registry::instruction::UpdateForesterEpochPda { - authority: Keypair::new().pubkey(), - }; - let accounts = light_registry::accounts::UpdateForesterEpochPda { - forester_epoch_pda, - signer: payer.pubkey(), - }; - let ix = Instruction { - program_id: light_registry::ID, - accounts: accounts.to_account_metas(Some(true)), - data: instruction_data.data(), - }; - let result = rpc - .create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) - .await; - let expected_error_code = anchor_lang::error::ErrorCode::ConstraintAddress as u32; - assert_rpc_error(result, 0, expected_error_code).unwrap(); - } + // // 2. FAIL: Update forester authority with invalid authority + // { + // let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); + // let forester_epoch_pda = get_forester_epoch_pda_address(&forester_pda, 0).0; + // let instruction_data = light_registry::instruction::UpdateForesterEpochPda { + // _authority: Keypair::new().pubkey(), + // }; + // let accounts = light_registry::accounts::UpdateForesterEpochPda { + // forester_epoch_pda, + // signer: payer.pubkey(), + // }; + // let ix = Instruction { + // program_id: light_registry::ID, + // accounts: accounts.to_account_metas(Some(true)), + // data: instruction_data.data(), + // }; + // let result = rpc + // .create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) + // .await; + // let expected_error_code = anchor_lang::error::ErrorCode::ConstraintAddress as u32; + // assert_rpc_error(result, 0, expected_error_code).unwrap(); + // } // 3. FAIL: Nullify with invalid authority { let expected_error_code = @@ -1362,10 +1361,8 @@ async fn update_registry_governance_on_testnet() { let updated_keypair = read_keypair_file("../../target/governance-authority-keypair.json").unwrap(); println!("updated keypair: {:?}", updated_keypair.pubkey()); - let (_, bump) = get_protocol_config_pda_address(); - let instruction = light_registry::instruction::UpdateGovernanceAuthority { + let instruction = light_registry::instruction::UpdateProtocolConfig { new_authority: updated_keypair.pubkey(), - _bump: bump, new_config: ProtocolConfig::default(), }; let accounts = light_registry::accounts::UpdateProtocolConfig { diff --git a/test-utils/src/e2e_test_env.rs b/test-utils/src/e2e_test_env.rs index ab88b6fbeb..6a32d9b513 100644 --- a/test-utils/src/e2e_test_env.rs +++ b/test-utils/src/e2e_test_env.rs @@ -580,8 +580,9 @@ where let current_solana_slot = self.rpc.get_slot().await.unwrap(); // need to detect whether new registration phase started - let current_registration_epoch = - self.protocol_config.get_current_epoch(current_solana_slot); + let current_registration_epoch = self + .protocol_config + .get_current_registration_epoch(current_solana_slot); // If reached new registration phase register all foresters if current_registration_epoch != self.registration_epoch { println!("\n --------------------------------------------------\n\t\t Register Foresters for new Epoch \n --------------------------------------------------"); diff --git a/test-utils/src/forester_epoch.rs b/test-utils/src/forester_epoch.rs index 84f331368b..99ab246220 100644 --- a/test-utils/src/forester_epoch.rs +++ b/test-utils/src/forester_epoch.rs @@ -261,7 +261,7 @@ impl Epoch { ); println!("protocol_config {:?}", protocol_config.active_phase_length); - let mut epoch = protocol_config.get_current_epoch(current_solana_slot); + let mut epoch = protocol_config.get_current_registration_epoch(current_solana_slot); let registration_start_slot = protocol_config.genesis_slot + epoch * protocol_config.active_phase_length; println!("registration_start_slot {:?}", registration_start_slot); diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 933e9d39dd..c1c993bd91 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -6,7 +6,7 @@ use light_concurrent_merkle_tree::copy::ConcurrentMerkleTreeCopy; use light_hash_set::HashSet; use light_hasher::Hasher; use light_indexed_merkle_tree::copy::IndexedMerkleTreeCopy; -use light_registry::delegate::state::CompressedAccountTrait; +use light_registry::delegate::delegate_account::CompressedAccountTrait; use light_system_program::sdk::compressed_account::CompressedAccountWithMerkleContext; use num_traits::{CheckedAdd, CheckedSub, ToBytes, Unsigned}; use solana_sdk::{ diff --git a/test-utils/src/registry.rs b/test-utils/src/registry.rs index e29d59c7c8..9bcd517973 100644 --- a/test-utils/src/registry.rs +++ b/test-utils/src/registry.rs @@ -16,9 +16,9 @@ use light_hasher::{DataHasher, Poseidon}; use light_registry::account_compression_cpi::sdk::{ create_rollover_state_merkle_tree_instruction, CreateRolloverMerkleTreeInstructionInputs, }; -use light_registry::delegate::deposit::DelegateAccountWithContext; +use light_registry::delegate::delegate_account::DelegateAccount; use light_registry::delegate::get_escrow_token_authority; -use light_registry::delegate::state::DelegateAccount; +use light_registry::delegate::process_deposit::DelegateAccountWithContext; use light_registry::epoch::claim_forester::{ CompressedForesterEpochAccount, CompressedForesterEpochAccountInput, }; @@ -33,7 +33,7 @@ use light_registry::utils::{ get_forester_epoch_pda_address, get_forester_pda_address, get_forester_token_pool_pda, get_protocol_config_pda_address, }; -use light_registry::{ForesterAccount, ForesterConfig, ForesterEpochPda}; +use light_registry::{protocol_config, ForesterAccount, ForesterConfig, ForesterEpochPda}; use light_system_program::sdk::compressed_account::CompressedAccountWithMerkleContext; use light_system_program::sdk::event::PublicTransactionEvent; use solana_sdk::account::Account; @@ -449,6 +449,7 @@ pub async fn deposit_or_withdraw_test< if let Some(delegate_account) = inputs.delegate_account.as_ref() { input_compressed_accounts.push(delegate_account.comporessed_account.clone()); + println!("delegate_account: {:?}", delegate_account); }; let cpi_context_account = indexer.get_state_merkle_tree_accounts(&[first_mt])[0].cpi_context; @@ -457,6 +458,7 @@ pub async fn deposit_or_withdraw_test< .iter() .map(|a| a.hash().unwrap()) .collect::>(); + println!("input_hashes: {:?}", input_hashes); let proof_rpc_result = indexer .create_proof_for_compressed_accounts( Some(&input_hashes), @@ -837,7 +839,7 @@ pub async fn forester_claim_test<'a, R: RpcConnection, I: Indexer>( .unwrap() .unwrap(); - let token_pool = get_forester_token_pool_pda(&forester.pubkey()); + let token_pool = get_forester_token_pool_pda(&forester_pda); let pre_token_pool_account = rpc.get_account(token_pool).await.unwrap().unwrap(); let (event, signature, _) = rpc .create_and_send_transaction_with_events::( @@ -912,7 +914,7 @@ pub async fn assert_forester_claim( let pre_amount = spl_token::state::Account::unpack(&pre_token_pool_account.data) .unwrap() .amount; - let token_pool_pda_pubkey = get_forester_token_pool_pda(&forester.pubkey()); + let token_pool_pda_pubkey = get_forester_token_pool_pda(&forester_pda_pubkey); let post_account = rpc .get_account(token_pool_pda_pubkey) .await @@ -929,9 +931,19 @@ pub async fn assert_forester_claim( .unwrap() .unwrap(); { + let current_epoch = get_current_epoch(rpc).await.unwrap(); let expected_forester_pda = { let mut input_pda = pre_forester_pda.clone(); - + let protocol_config_pda = get_protocol_config_pda_address().0; + let protocol_config: ProtocolConfigPda = rpc + .get_anchor_account::(&protocol_config_pda) + .await + .unwrap() + .unwrap(); + let current_slot = rpc.get_slot().await.unwrap(); + input_pda + .sync(current_slot, &protocol_config.config) + .unwrap(); input_pda.last_compressed_forester_epoch_pda_hash = created_output_accounts[0] .compressed_account .data @@ -940,6 +952,7 @@ pub async fn assert_forester_claim( .data_hash; input_pda.active_stake_weight += rewards; input_pda.last_claimed_epoch = epoch; + input_pda.current_epoch = current_epoch; input_pda }; assert_eq!(forester_pda, expected_forester_pda); @@ -969,6 +982,20 @@ pub async fn assert_forester_claim( } } +pub async fn get_current_epoch(rpc: &mut R) -> Result { + let protocol_config_pda = get_protocol_config_pda_address().0; + let protocol_config: ProtocolConfigPda = rpc + .get_anchor_account::(&protocol_config_pda) + .await + .unwrap() + .unwrap(); + let current_slot = rpc.get_slot().await.unwrap(); + let current_epoch = protocol_config + .config + .get_current_registration_epoch(current_slot); + Ok(current_epoch) +} + pub struct SyncDelegateInputs<'a> { pub sender: &'a Keypair, // pub amount: u64, @@ -1094,8 +1121,9 @@ pub async fn sync_delegate_test<'a, R: RpcConnection, I: Indexer>( }; let ix = create_sync_delegate_instruction(create_instruction_inputs.clone()); println!("delegate_account {:?}", delegate_account); + let forester_pda_pubkey = get_forester_pda_address(&inputs.forester).0; - let token_pool = get_forester_token_pool_pda(&inputs.forester); + let token_pool = get_forester_token_pool_pda(&forester_pda_pubkey); let pre_token_pool_account = rpc.get_account(token_pool).await.unwrap().unwrap(); let (event, signature, _) = rpc .create_and_send_transaction_with_events::( @@ -1178,7 +1206,8 @@ pub async fn assert_sync_delegate( let pre_amount = spl_token::state::Account::unpack(&pre_token_pool_account.data) .unwrap() .amount; - let token_pool_pda_pubkey = get_forester_token_pool_pda(&inputs.forester_pubkey); + let forester_pda_pubkey = get_forester_pda_address(&inputs.forester_pubkey).0; + let token_pool_pda_pubkey = get_forester_token_pool_pda(&forester_pda_pubkey); let post_account = rpc .get_account(token_pool_pda_pubkey) .await @@ -1200,7 +1229,12 @@ pub async fn assert_sync_delegate( .unwrap(); let epoch = input_compressed_epochs.last().unwrap().epoch; println!("\n\n epoch {:?} \n\n", epoch); + println!( + "input compressed token accounts: {:?}", + input_compressed_epochs + ); let expected_delegate_account = if let Some(rewards) = rewards { + println!("if"); let expected_delegate_account = { let mut input_pda = inputs.delegate_account.delegate_account.clone(); if epoch > input_pda.pending_epoch { @@ -1216,14 +1250,21 @@ pub async fn assert_sync_delegate( // syncing delegated // input_pda.pending_epoch = 0; // input_pda.pending_epoch = epoch; + input_pda.sync_pending_stake_weight(epoch); + input_pda.last_sync_epoch = epoch; input_pda.pending_synced_stake_weight = deserialized_delegate_account.pending_synced_stake_weight; + input_pda.escrow_token_account_hash = created_output_token_account[0] + .token_data + .hash::() + .unwrap(); input_pda }; expected_delegate_account } else { + println!("else"); let expected_delegate_account = { let mut input_pda = inputs.delegate_account.delegate_account.clone(); input_pda.stake_weight += input_pda.pending_undelegated_stake_weight; @@ -1237,15 +1278,19 @@ pub async fn assert_sync_delegate( input_pda.pending_delegated_stake_weight = 0; input_pda.pending_token_amount += sum_rewards; // input_pda.pending_epoch = epoch; + input_pda.sync_pending_stake_weight(epoch); + input_pda.last_sync_epoch = epoch; input_pda.pending_synced_stake_weight = deserialized_delegate_account.pending_synced_stake_weight; - // input_compressed_epochs - // .iter() - // .last() - // .unwrap() - // .rewards_earned; - + println!( + "token_data {:?}", + created_output_token_account[0].token_data + ); + input_pda.escrow_token_account_hash = created_output_token_account[0] + .token_data + .hash::() + .unwrap(); input_pda }; expected_delegate_account diff --git a/test-utils/src/test_env.rs b/test-utils/src/test_env.rs index 0aac5d657f..cc34940a7c 100644 --- a/test-utils/src/test_env.rs +++ b/test-utils/src/test_env.rs @@ -26,12 +26,13 @@ use account_compression::{NullifierQueueConfig, StateMerkleTreeConfig}; use anchor_lang::{system_program, InstructionData, ToAccountMetas}; use light_hasher::Poseidon; use light_macros::pubkey; +use light_registry::delegate::delegate_account::DelegateAccount; use light_registry::delegate::get_escrow_token_authority; -use light_registry::delegate::state::DelegateAccount; use light_registry::protocol_config::state::ProtocolConfig; use light_registry::sdk::{ create_finalize_registration_instruction, create_initialize_governance_authority_instruction, create_initialize_group_authority_instruction, create_register_program_instruction, + create_update_authority_instruction, }; use light_registry::utils::{ get_cpi_authority_pda, get_epoch_pda_address, get_forester_pda_address, get_group_pda, @@ -255,11 +256,23 @@ pub async fn initialize_accounts( &Some(cpi_authority_pda.0), ) .await; - - let instruction = - create_initialize_governance_authority_instruction(payer.pubkey(), protocol_config); + let registry_program_account_keypair = Keypair::from_bytes(®ISTRY_ID_TEST_KEYPAIR).unwrap(); + let instruction = create_initialize_governance_authority_instruction( + payer.pubkey(), + registry_program_account_keypair.pubkey(), + protocol_config, + ); + let update_instruction = create_update_authority_instruction( + registry_program_account_keypair.pubkey(), + payer.pubkey(), + protocol_config, + ); context - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .create_and_send_transaction( + &[instruction, update_instruction], + &payer.pubkey(), + &[payer, ®istry_program_account_keypair], + ) .await .unwrap(); @@ -444,7 +457,6 @@ pub async fn set_env_with_delegate_and_forester( ) .await; - // TODO: remove let tree_accounts = vec![ TreeAccounts { tree_type: TreeType::State, @@ -462,31 +474,43 @@ pub async fn set_env_with_delegate_and_forester( let slot = protocol_config.genesis_slot + protocol_config.active_phase_length; e2e_env.rpc.warp_to_slot(slot).unwrap(); + let forester_pda_pubkey = get_forester_pda_address(&env.forester.pubkey()).0; + let forester_pda = e2e_env + .rpc + .get_anchor_account::(&forester_pda_pubkey) + .await + .unwrap() + .unwrap(); + println!("forester_pda: {:?}", forester_pda); let registered_epoch = Epoch::register(&mut e2e_env.rpc, &protocol_config, &env.forester) .await .unwrap(); assert!(registered_epoch.is_some()); let mut registered_epoch = registered_epoch.unwrap(); + + let forester_pda_pubkey = get_forester_pda_address(&env.forester.pubkey()).0; + let forester_pda = e2e_env + .rpc + .get_anchor_account::(&forester_pda_pubkey) + .await + .unwrap() + .unwrap(); + println!("forester_pda: {:?}", forester_pda); + let forester_epoch_pda: ForesterEpochPda = e2e_env .rpc .get_anchor_account::(®istered_epoch.forester_epoch_pda) .await .unwrap() .unwrap(); + println!("forester_epoch_pdas: {:?}", forester_epoch_pda); assert_eq!(forester_epoch_pda.stake_weight, 1_000_000); assert!(forester_epoch_pda.total_epoch_state_weight.is_none()); // we advanced to the next epoch so that delegated pending stake becomes active assert_eq!(forester_epoch_pda.epoch, 1); let epoch = forester_epoch_pda.epoch; - let forester_pda_pubkey = get_forester_pda_address(&env.forester.pubkey()).0; - let forester_pda = e2e_env - .rpc - .get_anchor_account::(&forester_pda_pubkey) - .await - .unwrap() - .unwrap(); - println!("forester_pda: {:?}", forester_pda); let expected_stake = forester_pda.active_stake_weight; + println!("expected_stake: {}", expected_stake); assert_epoch_pda(&mut e2e_env.rpc, epoch, expected_stake).await; @@ -955,6 +979,7 @@ pub async fn create_delegate( &delegate_keypair.pubkey(), &light_registry::ID, ); + println!("delegate_account: {:?}", delegate_account); let escrow_account = e2e_env.indexer.get_compressed_token_accounts_by_owner( &get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0, ); @@ -982,7 +1007,7 @@ pub async fn create_delegate( ) .await .unwrap(); - // let forester_pda = env.registered_forester_pda; + deposit_to_delegate_account_helper( e2e_env, &delegate_keypair, @@ -1059,7 +1084,15 @@ pub async fn deposit_to_delegate_account_helper( .filter(|a| a.token_data.delegate.is_some()) .cloned() .collect::>(); - + println!( + "deposit_inputs pre: delegate_account: {:?}", + delegate_account + ); + println!("token_accounts: {:?}", token_accounts); + println!( + "input_escrow_token_account: {:?}", + input_escrow_token_account + ); let deposit_inputs = DepositInputs { sender: &delegate_keypair, amount: deposit_amount, @@ -1068,6 +1101,7 @@ pub async fn deposit_to_delegate_account_helper( input_escrow_token_account, epoch, }; + deposit_test(&mut e2e_env.rpc, &mut e2e_env.indexer, deposit_inputs) .await .unwrap(); From 3b2dbc9fd11372bd4b1971e4f549e2a01b409198 Mon Sep 17 00:00:00 2001 From: ananas-block Date: Sat, 3 Aug 2024 00:23:31 +0100 Subject: [PATCH 4/4] fixed test_register_and_update_forester_pda --- .../registry/src/delegate/delegate_account.rs | 3 + .../src/delegate/deposit_instruction.rs | 3 +- programs/registry/src/delegate/process_cpi.rs | 2 +- .../registry/src/delegate/process_delegate.rs | 54 ++- .../registry/src/delegate/process_deposit.rs | 31 +- programs/registry/src/epoch/claim_forester.rs | 5 +- .../src/epoch/claim_forester_instruction.rs | 8 +- programs/registry/src/lib.rs | 5 + programs/registry/src/protocol_config/mint.rs | 8 +- programs/registry/src/sdk.rs | 29 +- test-programs/e2e-test/tests/test.rs | 44 ++- test-programs/registry-test/tests/tests.rs | 309 ++++++++++++------ test-utils/src/registry.rs | 34 +- test-utils/src/test_env.rs | 189 +++++------ 14 files changed, 475 insertions(+), 249 deletions(-) diff --git a/programs/registry/src/delegate/delegate_account.rs b/programs/registry/src/delegate/delegate_account.rs index 5c9ec31738..a0e2250f5c 100644 --- a/programs/registry/src/delegate/delegate_account.rs +++ b/programs/registry/src/delegate/delegate_account.rs @@ -129,6 +129,9 @@ impl DelegateAccount { .checked_add(self.pending_delegated_stake_weight) .unwrap(); self.pending_delegated_stake_weight = 0; + if self.delegated_stake_weight == 0 { + self.delegate_forester_delegate_account = None; + } } } } diff --git a/programs/registry/src/delegate/deposit_instruction.rs b/programs/registry/src/delegate/deposit_instruction.rs index 48258db68a..8117aa025d 100644 --- a/programs/registry/src/delegate/deposit_instruction.rs +++ b/programs/registry/src/delegate/deposit_instruction.rs @@ -1,4 +1,4 @@ -use crate::protocol_config::state::ProtocolConfigPda; +use crate::{protocol_config::state::ProtocolConfigPda, ForesterAccount}; use super::{ traits::{ @@ -51,6 +51,7 @@ pub struct DepositOrWithdrawInstruction<'info> { pub token_cpi_authority_pda: AccountInfo<'info>, pub light_system_program: Program<'info, LightSystemProgram>, pub compressed_token_program: Program<'info, LightCompressedToken>, + pub forester_pda: Option>, } impl<'info> SystemProgramAccounts<'info> for DepositOrWithdrawInstruction<'info> { diff --git a/programs/registry/src/delegate/process_cpi.rs b/programs/registry/src/delegate/process_cpi.rs index 940c6636a7..fa36fdf2fa 100644 --- a/programs/registry/src/delegate/process_cpi.rs +++ b/programs/registry/src/delegate/process_cpi.rs @@ -96,7 +96,7 @@ pub fn cpi_compressed_token_transfer< proof: Option, compression_amount: Option, is_compress: bool, - salt: u64, + _salt: u64, mut cpi_context: CompressedCpiContext, mint: &Pubkey, input_token_data_with_context: Vec, diff --git a/programs/registry/src/delegate/process_delegate.rs b/programs/registry/src/delegate/process_delegate.rs index 59671525f9..f77d034186 100644 --- a/programs/registry/src/delegate/process_delegate.rs +++ b/programs/registry/src/delegate/process_delegate.rs @@ -50,7 +50,7 @@ pub fn process_delegate_or_undelegate<'a, 'b, 'c, 'info: 'b + 'c, const IS_DELEG pub fn delegate_or_undelegate( authority: &Pubkey, protocol_config: &ProtocolConfig, - delegate_account: DelegateAccountWithPackedContext, + mut delegate_account: DelegateAccountWithPackedContext, forester_pda_pubkey: &Pubkey, forester_pda: &mut ForesterAccount, delegate_amount: u64, @@ -64,6 +64,25 @@ pub fn delegate_or_undelegate( if *authority != delegate_account.delegate_account.owner { return err!(RegistryError::InvalidAuthority); } + + /** + * Scenario 1: Delegate to inactive forester which never does anything after delegation + * - it takes one epoch for stake to become active + * - after one epoch it should be possible to undelegate + * + * Scenario 2: Delegate to somehwat active forester + * - it takes one epoch for stake to become active + * - after one epoch it should be possible to undelegate + * - forester was active in the last epoch but doesn't claim + * - last_registered_epoch = epoch or epoch - n + * - last claimed epoch = epoch - n + * last claimed epoch is the epoch to which the delegate account needs to be synced + */ + // The account needs to be synced to a certain degree so that the sync delegate function works. + // Edge cases: + // - Forester never registered and claimed after delegation + // - (add option for last_claimed_epoch sync is not checked if last claimed is not set) this doesn't work if the forester has registered and claims later + // - never claims after delegation // check that delegate account is synced to last claimed (completed) epoch if forester_pda.last_claimed_epoch != delegate_account.delegate_account.last_sync_epoch && delegate_account @@ -87,8 +106,15 @@ pub fn delegate_or_undelegate( return err!(RegistryError::AlreadyDelegated); } } - let epoch = forester_pda.last_registered_epoch; - + let epoch = // In case of delegating to an inactive forester, the delegate account needs to be synced so that. + if forester_pda.last_registered_epoch <= delegate_account.delegate_account.last_sync_epoch + || forester_pda.last_claimed_epoch <= delegate_account.delegate_account.last_sync_epoch + { + forester_pda.current_epoch + } else { + forester_pda.last_registered_epoch + }; + msg!("epoch: {}", epoch); // modify forester pda if IS_DELEGATE { forester_pda.pending_undelegated_stake_weight = forester_pda @@ -122,10 +148,20 @@ pub fn delegate_or_undelegate( .stake_weight .checked_sub(delegate_amount) .ok_or(RegistryError::ComputeEscrowAmountFailed)?; - delegate_account.delegate_forester_delegate_account = Some(*forester_pda_pubkey); - println!("epoch {}", epoch); + if delegate_account + .delegate_forester_delegate_account + .is_none() + { + delegate_account.delegate_forester_delegate_account = Some(*forester_pda_pubkey); + delegate_account.last_sync_epoch = forester_pda.last_claimed_epoch; + } delegate_account.pending_epoch = epoch; } else { + msg!( + "delegate account delegated stake weight: {}", + delegate_account.delegated_stake_weight + ); + msg!("delegate amount {}", delegate_amount); // remove delegated stake weight from delegated_stake_weight // add delegated stake weight to pending_undelegated_stake_weight delegate_account.delegated_stake_weight = delegate_account @@ -137,9 +173,6 @@ pub fn delegate_or_undelegate( .checked_add(delegate_amount) .ok_or(RegistryError::ComputeEscrowAmountFailed)?; delegate_account.pending_epoch = epoch; - if delegate_account.delegated_stake_weight == 0 { - delegate_account.delegate_forester_delegate_account = None; - } } delegate_account }; @@ -269,6 +302,7 @@ mod tests { current_slot, ); + // This test is currently failing because the delegate the syncing I added changes the delegate account in a different way than before. let (input_delegate_pda, output_delegate_pda) = result.unwrap(); assert_eq!(input_delegate_pda.compressed_account.owner, crate::ID); assert_eq!(output_delegate_pda.compressed_account.owner, crate::ID); @@ -276,12 +310,14 @@ mod tests { // Delegate should: // - sync pending stake weight // - output pending - let expected_delegate_account = DelegateAccount { + let mut expected_delegate_account = DelegateAccount { pending_delegated_stake_weight: delegate_amount, pending_epoch: forester_pda.last_registered_epoch, stake_weight: delegate_account.delegate_account.stake_weight - delegate_amount, ..delegate_account.delegate_account }; + println!("epoch {}", expected_forester_pda.current_epoch); + expected_delegate_account.sync_pending_stake_weight(expected_forester_pda.current_epoch); let deserialized_delegate_account = DelegateAccount::deserialize( &mut &output_delegate_pda diff --git a/programs/registry/src/delegate/process_deposit.rs b/programs/registry/src/delegate/process_deposit.rs index 6e2bb8e5e8..0d577525b2 100644 --- a/programs/registry/src/delegate/process_deposit.rs +++ b/programs/registry/src/delegate/process_deposit.rs @@ -1,4 +1,4 @@ -use crate::errors::RegistryError; +use crate::{errors::RegistryError, forester, ForesterAccount}; use anchor_lang::prelude::*; use anchor_lang::solana_program::pubkey::Pubkey; use light_compressed_token::{ @@ -53,6 +53,11 @@ pub fn process_deposit_or_withdrawal<'a, 'b, 'c, 'info: 'b + 'c, const IS_DEPOSI .protocol_config .config .get_current_registration_epoch(slot); + let forester_pda = if let Some(forester_pda) = &ctx.accounts.forester_pda { + Some(forester_pda.clone().into_inner()) + } else { + None + }; let compressed_accounts = deposit_or_withdraw::( &ctx.accounts.authority.key(), &ctx.accounts.escrow_token_authority.key(), @@ -65,6 +70,7 @@ pub fn process_deposit_or_withdrawal<'a, 'b, 'c, 'info: 'b + 'c, const IS_DEPOSI change_compressed_account_merkle_tree_index, output_delegate_compressed_account_merkle_tree_index, epoch, + forester_pda, )?; if let Some(input_escrow_token_account) = input_escrow_token_account { @@ -136,6 +142,7 @@ pub fn deposit_or_withdraw( change_compressed_account_merkle_tree_index: u8, output_delegate_compressed_account_merkle_tree_index: u8, epoch: u64, + forester_pda: Option, ) -> Result { if delegate_account.is_some() && input_escrow_token_account.is_none() || delegate_account.is_none() && input_escrow_token_account.is_some() @@ -147,7 +154,6 @@ pub fn deposit_or_withdraw( msg!("An input compressed escrow token account is required for withdrawal"); return Err(RegistryError::InputEscrowTokenHashNotProvided.into()); } - let hashed_owner = hash_to_bn254_field_size_be(authority.as_ref()).unwrap().0; let hashed_mint = hash_to_bn254_field_size_be(mint.as_ref()).unwrap().0; let hashed_escrow_token_authority = hash_to_bn254_field_size_be(escrow_token_authority.as_ref()) @@ -231,6 +237,7 @@ pub fn deposit_or_withdraw( deposit_amount, output_delegate_compressed_account_merkle_tree_index, epoch, + forester_pda, )?; Ok(DepositCompressedAccounts { input_delegate_pda, @@ -288,10 +295,12 @@ fn update_delegate_compressed_account( deposit_amount: u64, merkle_tree_index: u8, epoch: u64, + forester_pda: Option, ) -> Result<( Option, OutputCompressedAccountWithPackedContext, )> { + msg!("epoch {:?}", epoch); let (input_account, mut delegate_account) = if let Some(input) = input_delegate_account { let input_escrow_token_account_hash = if let Some(input_escrow_token_account_hash) = input_escrow_token_account_hash { @@ -302,6 +311,18 @@ fn update_delegate_compressed_account( let (mut delegate_account, input_account) = create_input_delegate_account(authority, input_escrow_token_account_hash, input)?; delegate_account.escrow_token_account_hash = output_escrow_token_account_hash; + if let Some(forester_pda) = forester_pda { + let epoch = // In case of delegating to an inactive forester, the delegate account needs to be synced so that. + if forester_pda.last_registered_epoch <= delegate_account.last_sync_epoch + || forester_pda.last_claimed_epoch <= delegate_account.last_sync_epoch + { + epoch + } else { + forester_pda.last_registered_epoch + }; + delegate_account.sync_pending_stake_weight(epoch); + } + msg!("forester_pda {:?}", forester_pda); // delegate_account.sync_pending_stake_weight(epoch); (Some(input_account), delegate_account) } else { @@ -778,6 +799,7 @@ mod tests { deposit_amount, merkle_tree_index, 0, + None, ); assert!(result.is_ok()); @@ -903,6 +925,7 @@ mod tests { change_compressed_account_merkle_tree_index, output_delegate_compressed_account_merkle_tree_index, 0, + None, ); assert!(result.is_ok()); @@ -944,6 +967,7 @@ mod tests { change_compressed_account_merkle_tree_index, output_delegate_compressed_account_merkle_tree_index, 0, + None, ); assert!(result.is_ok()); @@ -987,6 +1011,7 @@ mod tests { change_compressed_account_merkle_tree_index, output_delegate_compressed_account_merkle_tree_index, 0, + None, ); assert!(result.is_ok()); @@ -1031,6 +1056,7 @@ mod tests { change_compressed_account_merkle_tree_index, output_delegate_compressed_account_merkle_tree_index, 0, + None, ); assert!(result.is_ok()); @@ -1073,6 +1099,7 @@ mod tests { change_compressed_account_merkle_tree_index, output_delegate_compressed_account_merkle_tree_index, 0, + None, ); assert!(matches!( diff --git a/programs/registry/src/epoch/claim_forester.rs b/programs/registry/src/epoch/claim_forester.rs index ebd457776e..2058a0116e 100644 --- a/programs/registry/src/epoch/claim_forester.rs +++ b/programs/registry/src/epoch/claim_forester.rs @@ -231,10 +231,7 @@ pub fn forester_claim_rewards( #[cfg(test)] mod tests { - use crate::{ - protocol_config::{self, state::ProtocolConfig}, - ForesterConfig, - }; + use crate::{protocol_config::state::ProtocolConfig, ForesterConfig}; use super::*; use anchor_lang::solana_program::pubkey::Pubkey; diff --git a/programs/registry/src/epoch/claim_forester_instruction.rs b/programs/registry/src/epoch/claim_forester_instruction.rs index 54c5b3d4f8..d1ca624fca 100644 --- a/programs/registry/src/epoch/claim_forester_instruction.rs +++ b/programs/registry/src/epoch/claim_forester_instruction.rs @@ -44,12 +44,12 @@ pub struct ClaimForesterInstruction<'info> { pub compressed_token_program: Program<'info, LightCompressedToken>, pub spl_token_program: Program<'info, Token>, // END LIGHT ACCOUNTS + #[account(mut, has_one = authority)] + pub forester_pda: Account<'info, ForesterAccount>, /// CHECK: (seed constraint). /// Pool account for epoch rewards excluding forester fee. #[account(mut, seeds = [FORESTER_TOKEN_POOL_SEED, forester_pda.key().as_ref()],bump,)] pub forester_token_pool: Account<'info, TokenAccount>, - #[account(mut, has_one = authority)] - pub forester_pda: Account<'info, ForesterAccount>, /// CHECK: (seed constraint) derived from epoch_pda.epoch and forester_pda. #[account(mut, seeds=[FORESTER_EPOCH_SEED, forester_pda.key().as_ref(), epoch_pda.epoch.to_le_bytes().as_ref()], bump ,close=fee_payer)] pub forester_epoch_pda: Account<'info, ForesterEpochPda>, @@ -57,11 +57,11 @@ pub struct ClaimForesterInstruction<'info> { pub epoch_pda: Account<'info, EpochPda>, #[account(mut, constraint= epoch_pda.protocol_config.mint == mint.key() @RegistryError::InvalidMint)] pub mint: Account<'info, Mint>, + #[account(mut, seeds= [POOL_SEED, mint.key().as_ref()], bump, seeds::program= compressed_token_program)] + pub compression_token_pool: Account<'info, TokenAccount>, /// CHECK: (checked in different program) #[account(mut)] pub output_merkle_tree: AccountInfo<'info>, - #[account(mut, seeds= [POOL_SEED, mint.key().as_ref()], bump, seeds::program= compressed_token_program)] - pub compression_token_pool: Account<'info, TokenAccount>, } impl<'info> SystemProgramAccounts<'info> for ClaimForesterInstruction<'info> { diff --git a/programs/registry/src/lib.rs b/programs/registry/src/lib.rs index 9dbec26374..1a240b1b2f 100644 --- a/programs/registry/src/lib.rs +++ b/programs/registry/src/lib.rs @@ -452,6 +452,11 @@ pub mod light_registry { process_forester_claim_rewards(ctx) } + /// Forester accounts need to be connected, check function with trait + /// eligible instructions: + /// - sync_delegate + /// - claim_forester_rewards + /// - pub fn sync_delegate<'info>( ctx: Context<'_, '_, '_, 'info, SyncDelegateInstruction<'info>>, _salt: u64, // TODO: test integration diff --git a/programs/registry/src/protocol_config/mint.rs b/programs/registry/src/protocol_config/mint.rs index 6a59400d82..95e553d007 100644 --- a/programs/registry/src/protocol_config/mint.rs +++ b/programs/registry/src/protocol_config/mint.rs @@ -1,7 +1,7 @@ use account_compression::utils::constants::CPI_AUTHORITY_PDA_SEED; use anchor_lang::prelude::*; use anchor_spl::token::{Mint as SplMint, Token, TokenAccount}; -use light_compressed_token::program::LightCompressedToken; +use light_compressed_token::{program::LightCompressedToken, POOL_SEED}; use light_macros::pubkey; use light_system_program::program::LightSystemProgram; @@ -34,8 +34,8 @@ pub struct Mint<'info> { pub token_cpi_authority_pda: AccountInfo<'info>, pub compressed_token_program: Program<'info, LightCompressedToken>, /// CHECK: (compressed token program). - #[account(mut)] - pub token_pool_pda: Account<'info, TokenAccount>, + #[account(mut, seeds= [POOL_SEED, mint.key().as_ref()], bump, seeds::program= compressed_token_program)] + pub compression_token_pool: Account<'info, TokenAccount>, pub token_program: Program<'info, Token>, pub light_system_program: Program<'info, LightSystemProgram>, /// CHECK: (account compression program). @@ -110,7 +110,7 @@ impl<'info> CompressedTokenProgramAccounts<'info> for Mint<'info> { self.token_program.to_account_info() } fn get_token_pool_pda(&self) -> AccountInfo<'info> { - self.token_pool_pda.to_account_info() + self.compression_token_pool.to_account_info() } fn get_compress_or_decompress_token_account(&self) -> Option> { None diff --git a/programs/registry/src/sdk.rs b/programs/registry/src/sdk.rs index 619009e7d9..659b7b06e2 100644 --- a/programs/registry/src/sdk.rs +++ b/programs/registry/src/sdk.rs @@ -337,7 +337,7 @@ pub fn create_mint_to_instruction( cpi_authority: cpi_authority_pda, token_cpi_authority_pda: standard_accounts.token_cpi_authority_pda, compressed_token_program: standard_accounts.compressed_token_program, - token_pool_pda: standard_accounts.token_pool_pda, + compression_token_pool: standard_accounts.token_pool_pda, token_program: standard_accounts.token_program, light_system_program: standard_accounts.light_system_program, registered_program_pda: standard_accounts.registered_program_pda, @@ -455,17 +455,28 @@ pub fn create_deposit_instruction( &mut remaining_accounts, &inputs.cpi_context_account, ) as u8; - let delegate_account = if let Some(delegate_account) = inputs.delegate_account { + let (delegate_account, forester_pda) = if let Some(delegate_account) = inputs.delegate_account { let packed_merkle_context = pack_merkle_context(&[delegate_account.merkle_context], &mut remaining_accounts); + let forester_pda = if let Some(forester_pda) = delegate_account + .delegate_account + .delegate_forester_delegate_account + { + Some(forester_pda) + } else { + None + }; - Some(InputDelegateAccountWithPackedContext { - delegate_account: delegate_account.delegate_account.into(), - merkle_context: packed_merkle_context[0], - root_index: inputs.root_indices[inputs.root_indices.len() - 1], - }) + ( + Some(InputDelegateAccountWithPackedContext { + delegate_account: delegate_account.delegate_account.into(), + merkle_context: packed_merkle_context[0], + root_index: inputs.root_indices[inputs.root_indices.len() - 1], + }), + forester_pda, + ) } else { - None + (None, None) }; let instruction_data = if IS_DEPOSIT { crate::instruction::Deposit { @@ -536,7 +547,9 @@ pub fn create_deposit_instruction( escrow_token_authority, protocol_config: standard_registry_accounts.protocol_config_pda, self_program: standard_registry_accounts.self_program, + forester_pda, }; + println!("forester_pda: {:?}", forester_pda); let remaining_accounts = to_account_metas(remaining_accounts); Instruction { program_id: crate::ID, diff --git a/test-programs/e2e-test/tests/test.rs b/test-programs/e2e-test/tests/test.rs index 60e9656004..4f69e4ff9b 100644 --- a/test-programs/e2e-test/tests/test.rs +++ b/test-programs/e2e-test/tests/test.rs @@ -5,7 +5,8 @@ use light_test_utils::e2e_test_env::{E2ETestEnv, GeneralActionConfig, KeypairAct use light_test_utils::indexer::TestIndexer; use light_test_utils::rpc::ProgramTestRpcConnection; use light_test_utils::test_env::{ - set_env_with_delegate_and_forester, setup_test_programs_with_accounts_with_protocol_config, + set_env_with_delegate_and_forester, set_env_with_delegate_and_forester_local, + setup_test_programs_with_accounts_with_protocol_config, }; #[tokio::test] @@ -28,27 +29,34 @@ async fn test_10_all() { // KeypairActionConfig::all_default().non_inclusion(), // ) // .await; - let (mut e2e_env, _delegate_keypair, _env, _tree_accounts, _registered_epoch) = - set_env_with_delegate_and_forester( - None, - Some(KeypairActionConfig::all_default()), - Some(GeneralActionConfig::default()), + let (rpc, indexer, _delegate_keypair, env_accounts, _tree_accounts, registered_epoch) = + set_env_with_delegate_and_forester_local(None).await; + + let mut e2e_env = + E2ETestEnv::>::new( + rpc, + indexer, + &env_accounts, + KeypairActionConfig::all_default(), + GeneralActionConfig::default(), 10, None, ) .await; - - // let mut env = - // E2ETestEnv::>::new( - // rpc, - // indexer, - // &env_accounts, - // KeypairActionConfig::all_default(), - // GeneralActionConfig::default(), - // 10, - // None, - // ) - // .await; + // let _forester = Forester { + // registration: registered_epoch.clone(), + // active: registered_epoch.clone(), + // ..Default::default() + // }; + // // Forester epoch account is assumed to exist (is inited with test program deployment) + // let forester = TestForester { + // keypair: env_accounts.forester.insecure_clone(), + // forester: _forester.clone(), + // is_registered: Some(0), + // }; + // e2e_env.foresters.push(forester); + // e2e_env.epoch_config = _forester; + // e2e_env.epoch = registered_epoch.epoch; e2e_env.execute_rounds().await; println!("stats {:?}", e2e_env.stats); } diff --git a/test-programs/registry-test/tests/tests.rs b/test-programs/registry-test/tests/tests.rs index 86e9482655..b4725b972b 100644 --- a/test-programs/registry-test/tests/tests.rs +++ b/test-programs/registry-test/tests/tests.rs @@ -8,6 +8,7 @@ use light_registry::account_compression_cpi::sdk::{ use light_registry::delegate::delegate_account::DelegateAccount; use light_registry::delegate::get_escrow_token_authority; use light_registry::epoch::claim_forester::CompressedForesterEpochAccount; +use light_registry::epoch::register_epoch; use light_registry::errors::RegistryError; use light_registry::protocol_config::state::{ProtocolConfig, ProtocolConfigPda}; use light_registry::sdk::{ @@ -15,15 +16,16 @@ use light_registry::sdk::{ }; use light_registry::utils::{ get_forester_epoch_pda_address, get_forester_pda_address, get_forester_token_pool_pda, - get_protocol_config_pda_address, }; use light_registry::{ForesterAccount, ForesterConfig, ForesterEpochPda, MINT}; use light_test_utils::assert_epoch::{ assert_epoch_pda, assert_finalized_epoch_registration, assert_registered_forester_pda, assert_report_work, fetch_epoch_and_forester_pdas, }; -use light_test_utils::e2e_test_env::{init_program_test_env, TestForester}; -use light_test_utils::forester_epoch::{get_epoch_phases, Epoch, Forester, TreeAccounts, TreeType}; +use light_test_utils::e2e_test_env::{ + init_program_test_env, E2ETestEnv, GeneralActionConfig, KeypairActionConfig, +}; +use light_test_utils::forester_epoch::{get_epoch_phases, Epoch, TreeAccounts, TreeType}; use light_test_utils::indexer::{Indexer, TestIndexer}; use light_test_utils::registry::{ @@ -35,7 +37,8 @@ use light_test_utils::rpc::solana_rpc::SolanaRpcUrl; use light_test_utils::rpc::ProgramTestRpcConnection; use light_test_utils::test_env::{ create_delegate, deposit_to_delegate_account_helper, set_env_with_delegate_and_forester, - setup_accounts_devnet, setup_test_programs_with_accounts_with_protocol_config, EnvAccounts, + set_env_with_delegate_and_forester_local, setup_accounts_devnet, + setup_test_programs_with_accounts_with_protocol_config, EnvAccounts, STANDARD_TOKEN_MINT_KEYPAIR, }; use light_test_utils::test_forester::{empty_address_queue_test, nullify_compressed_accounts}; @@ -46,10 +49,7 @@ use light_test_utils::{ create_rollover_state_merkle_tree_instructions, register_test_forester, }, rpc::{errors::assert_rpc_error, rpc_connection::RpcConnection, SolanaRpcConnection}, - test_env::{ - get_test_env_accounts, register_program_with_registry_program, - setup_test_programs_with_accounts, - }, + test_env::{get_test_env_accounts, register_program_with_registry_program}, }; use rand::Rng; use solana_sdk::program_pack::Pack; @@ -63,7 +63,8 @@ use solana_sdk::{ #[tokio::test] async fn test_register_program() { - let (mut rpc, env) = setup_test_programs_with_accounts(None).await; + // let (mut rpc, env) = setup_test_programs_with_accounts(None).await; + let (mut rpc, _, _, env, _, _) = set_env_with_delegate_and_forester_local(None).await; let random_program_keypair = Keypair::new(); register_program_with_registry_program( &mut rpc, @@ -199,6 +200,12 @@ async fn test_deposit() { .await .unwrap(); } + let slot = e2e_env.rpc.get_slot().await.unwrap(); + // advance to next active phase + e2e_env + .rpc + .warp_to_slot(slot + e2e_env.protocol_config.active_phase_length) + .unwrap(); // 3. Functional withdrawal { println!("\n\n fetching accounts for withdrawal \n\n"); @@ -218,6 +225,7 @@ async fn test_deposit() { amount: 99999, delegate_account: delegate_account[0].as_ref().unwrap().clone(), input_escrow_token_account: escrow_token_accounts[0].clone(), + epoch: 0, }; withdraw_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) .await @@ -225,26 +233,26 @@ async fn test_deposit() { } } +/// Functional tests to delegate, undelegate and withdraw if the forester is not active #[tokio::test] -async fn test_delegate() { - let token_mint_keypair = Keypair::from_bytes(STANDARD_TOKEN_MINT_KEYPAIR.as_slice()).unwrap(); - - let protocol_config = ProtocolConfig { - mint: token_mint_keypair.pubkey(), - ..Default::default() - }; - let (mut rpc, env) = - setup_test_programs_with_accounts_with_protocol_config(None, protocol_config, true).await; +async fn test_sequence_deposit_delegate_undelegate_withdraw() { + // let protocol_config = ProtocolConfig { + // min_stake: 0, + // ..Default::default() + // }; + // let (mut rpc, env) = setup_test_programs_with_accounts(None).await; + let (mut rpc, mut indexer, _, env, _, _) = set_env_with_delegate_and_forester_local(None).await; + let protocol_config = ProtocolConfig::default(); let delegate_keypair = Keypair::new(); rpc.airdrop_lamports(&delegate_keypair.pubkey(), 1_000_000_000) .await .unwrap(); - let mut e2e_env = init_program_test_env(rpc, &env).await; + // let mut e2e_env = init_program_test_env(e2e_env.rpc, &env).await; mint_standard_tokens::>( - &mut e2e_env.rpc, - &mut e2e_env.indexer, + &mut rpc, + &mut indexer, &env.governance_authority, &delegate_keypair.pubkey(), 1_000_000_000, @@ -255,19 +263,21 @@ async fn test_delegate() { let forester_pda = env.registered_forester_pda; let deposit_amount = 1_000_000; deposit_to_delegate_account_helper( - &mut e2e_env, + &mut rpc, + &mut indexer, &delegate_keypair, deposit_amount, &env, - 0, + 2, None, None, ) .await; + println!("created delegate account and deposited"); // delegate to forester { let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( - &mut e2e_env.indexer, + &mut indexer, &delegate_keypair.pubkey(), &light_registry::ID, ); @@ -278,22 +288,45 @@ async fn test_delegate() { forester_pda, output_merkle_tree: env.merkle_tree_pubkey, }; - delegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) - .await - .unwrap(); + delegate_test(&mut rpc, &mut indexer, inputs).await.unwrap(); + println!("created delegated"); } - let current_slot = e2e_env.rpc.get_slot().await.unwrap(); - e2e_env - .rpc - .warp_to_slot(current_slot + protocol_config.active_phase_length) + // let current_slot = e2e_env.rpc.get_slot().await.unwrap(); + // e2e_env + // .rpc + // .warp_to_slot(current_slot + protocol_config.active_phase_length) + // .unwrap(); + // fail undelegate more than delegated (delegated amount is not active yet -> cannot be undelegated until next epoch) + { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + println!("delegate_account: {:?}", delegate_account); + let inputs = UndelegateInputs { + sender: &delegate_keypair, + amount: deposit_amount - 1, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + forester_pda, + output_merkle_tree: env.merkle_tree_pubkey, + }; + let result = undelegate_test(&mut rpc, &mut indexer, inputs).await; + assert_rpc_error(result, 0, RegistryError::ComputeEscrowAmountFailed.into()).unwrap(); + println!("created undelegated"); + } + let slot = rpc.get_slot().await.unwrap(); + // advance to next active phase + rpc.warp_to_slot(slot + protocol_config.active_phase_length) .unwrap(); - // undelegate from forester + // undelegate from forester (amount - 1) { let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( - &mut e2e_env.indexer, + &mut indexer, &delegate_keypair.pubkey(), &light_registry::ID, ); + println!("delegate_account: {:?}", delegate_account); let inputs = UndelegateInputs { sender: &delegate_keypair, amount: deposit_amount - 1, @@ -301,14 +334,15 @@ async fn test_delegate() { forester_pda, output_merkle_tree: env.merkle_tree_pubkey, }; - undelegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) + undelegate_test(&mut rpc, &mut indexer, inputs) .await .unwrap(); + println!("created undelegated"); } - // undelegate from forester + // undelegate from forester (remaining 1) { let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( - &mut e2e_env.indexer, + &mut indexer, &delegate_keypair.pubkey(), &light_registry::ID, ); @@ -319,9 +353,77 @@ async fn test_delegate() { forester_pda, output_merkle_tree: env.merkle_tree_pubkey, }; - undelegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) + undelegate_test(&mut rpc, &mut indexer, inputs) .await .unwrap(); + println!("created undelegated"); + } + // withdraw 1 + { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + let escrow_authority = get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0; + let escrow = indexer.get_compressed_token_accounts_by_owner(&escrow_authority); + let withdraw_inputs = WithdrawInputs { + sender: &delegate_keypair, + amount: 1, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + input_escrow_token_account: escrow[0].clone(), + epoch: 3, + }; + let result = withdraw_test(&mut rpc, &mut indexer, withdraw_inputs).await; + assert_rpc_error(result, 0, RegistryError::ComputeEscrowAmountFailed.into()).unwrap(); + println!("failed withdraw"); + } + let slot = rpc.get_slot().await.unwrap(); + // advance to next active phase + rpc.warp_to_slot(slot + protocol_config.active_phase_length) + .unwrap(); + // withdraw 1 + { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + println!("delegate_account: {:?}", delegate_account); + let escrow_authority = get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0; + let escrow = indexer.get_compressed_token_accounts_by_owner(&escrow_authority); + let withdraw_inputs = WithdrawInputs { + sender: &delegate_keypair, + amount: 1, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + input_escrow_token_account: escrow[0].clone(), + epoch: 4, + }; + withdraw_test(&mut rpc, &mut indexer, withdraw_inputs) + .await + .unwrap(); + println!("created withdraw"); + } + // withdraw remaining + { + let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( + &mut indexer, + &delegate_keypair.pubkey(), + &light_registry::ID, + ); + let escrow_authority = get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0; + let escrow = indexer.get_compressed_token_accounts_by_owner(&escrow_authority); + let withdraw_inputs = WithdrawInputs { + sender: &delegate_keypair, + amount: deposit_amount - 1, + delegate_account: delegate_account[0].as_ref().unwrap().clone(), + input_escrow_token_account: escrow[0].clone(), + epoch: 4, + }; + withdraw_test(&mut rpc, &mut indexer, withdraw_inputs) + .await + .unwrap(); + println!("created withdraw"); } } @@ -339,12 +441,23 @@ use rand::SeedableRng; // TODO: add a test where the stake percentage of one delegate stays constant while others change so that we can assert the rewards #[tokio::test] async fn test_e2e() { - let (mut e2e_env, delegate_keypair, env, tree_accounts, registered_epoch) = - set_env_with_delegate_and_forester(None, None, None, 0, None).await; + let (mut rpc, mut indexer, delegate_keypair, env, tree_accounts, registered_epoch) = + set_env_with_delegate_and_forester_local(None).await; + let mut e2e_env = + E2ETestEnv::>::new( + rpc, + indexer, + &env, + KeypairActionConfig::all_default(), + GeneralActionConfig::default(), + 10, + None, + ) + .await; let mut previous_hash = [0u8; 32]; // let current_epoch = registered_epoch.clone(); let mut phases = registered_epoch.phases.clone(); - let num_epochs = 40; + let num_epochs = 10; let mut completed_epochs = 0; let add_delegates = true; let pre_mint_account = e2e_env.rpc.get_account(MINT).await.unwrap().unwrap(); @@ -365,37 +478,6 @@ async fn test_e2e() { vec![(forester_keypair, delegates, Some(registered_epoch), None)]; // adding a second forester with stake it will not be registered until next epoch // env fails with account not found when registering the second forester - // { - // println!("adding second forester"); - // println!("epoch {}", epoch); - // let forester = TestForester { - // keypair: Keypair::new(), - // forester: Forester::default(), - // is_registered: None, - // }; - // let forester_config = ForesterConfig { - // fee: rng_from_seed.gen_range(0..=100), - // fee_recipient: forester.keypair.pubkey(), - // }; - // register_test_forester( - // &mut e2e_env.rpc, - // &env.governance_authority, - // &forester.keypair, - // forester_config, - // ) - // .await - // .unwrap(); - - // let forester_pda = get_forester_pda_address(&forester.keypair.pubkey()).0; - // // TODO: investigate why + 1 - // let delgate_keypair = - // create_delegate(&mut e2e_env, &env, 1_000_000, forester_pda, epoch + 1, None).await; - // foresters.push((forester.keypair, vec![(delgate_keypair, 0)], None, None)); - // } - - // let pre_forester_two_balance = e2e_env - // .indexer - // .get_compressed_token_balance(&foresters[1].0.pubkey(), &MINT); let mut num_mint_tos = 0; for i in 1..=num_epochs { // Prints @@ -531,8 +613,6 @@ async fn test_e2e() { *next_epoch = None; } } - // // // check that we can still forest the last epoch - // perform_work(&mut e2e_env, &forester_keypair, &env, current_epoch.epoch).await; e2e_env.rpc.warp_to_slot(phases.report_work.start).unwrap(); // report work @@ -708,10 +788,11 @@ async fn test_e2e() { index, amount ); } - // // delegate + // delegate { create_delegate( - &mut e2e_env, + &mut e2e_env.rpc, + &mut e2e_env.indexer, &env, 1_000_000, env.registered_forester_pda, @@ -733,7 +814,8 @@ async fn test_e2e() { for _ in 0..num_add_delegates { let deposit_amount = rng_from_seed.gen_range(1_000_000..1_000_000_000); let delegate_keypair = create_delegate( - &mut e2e_env, + &mut e2e_env.rpc, + &mut e2e_env.indexer, &env, deposit_amount, env.registered_forester_pda, @@ -795,12 +877,6 @@ async fn test_e2e() { println!("forester_pda: {:?}", forester_pda); } } - // // current_epoch = next_registered_epoch; - // for forester in foresters.iter_mut() { - // if let Some(next_registered_epoch) = &forester.3 { - // phases = next_registered_epoch.phases.clone(); - // } - // } phases = get_epoch_phases(&protocol_config, epoch); } let post_mint_account = e2e_env.rpc.get_account(MINT).await.unwrap().unwrap(); @@ -1147,15 +1223,68 @@ async fn test_register_and_update_forester_pda() { /// 6. FAIL: Rollover state tree with invalid authority #[tokio::test] async fn failing_test_forester() { - let (mut rpc, env) = setup_test_programs_with_accounts(None).await; + // let (mut rpc, env) = setup_test_programs_with_accounts_with_protocol_config( + // None, + // ProtocolConfig::default(), + // false, + // ) + // .await; + let (mut rpc, mut indexer, _, env, _, registered_epoch) = + set_env_with_delegate_and_forester_local(None).await; + let protocol_config = ProtocolConfig::default(); + let invalid_forester = Keypair::new(); + let invalid_forester_pda = get_forester_pda_address(&invalid_forester.pubkey()).0; + rpc.airdrop_lamports(&invalid_forester.pubkey(), 1_000_000_000) + .await + .unwrap(); + + // register second forester and advance to next epoch + let (next_epoch_invalid, next_registered_epoch) = { + register_test_forester( + &mut rpc, + &env.governance_authority, + &invalid_forester, + ForesterConfig::default(), + ) + .await + .unwrap(); + + create_delegate( + &mut rpc, + &mut indexer, + &env, + 1000, + invalid_forester_pda, + registered_epoch.epoch, + None, + ) + .await; + + // skip to next registration phase + let next_registry_epoch = registered_epoch.phases.active.end - 1; + rpc.warp_to_slot(next_registry_epoch).unwrap(); + + let next_epoch_invalid = Epoch::register(&mut rpc, &protocol_config, &invalid_forester) + .await + .unwrap() + .unwrap(); + let next_registered_epoch = Epoch::register(&mut rpc, &protocol_config, &env.forester) + .await + .unwrap() + .unwrap(); + + // skip to next active phase + rpc.warp_to_slot(next_registry_epoch + 1).unwrap(); + (next_epoch_invalid, next_registered_epoch) + }; + let payer = rpc.get_payer().insecure_clone(); // 1. FAIL: Register a forester with invalid authority { let result = register_test_forester(&mut rpc, &payer, &Keypair::new(), ForesterConfig::default()) .await; - let expected_error_code = anchor_lang::error::ErrorCode::ConstraintAddress as u32; - assert_rpc_error(result, 0, expected_error_code).unwrap(); + assert_rpc_error(result, 0, RegistryError::InvalidAuthority.into()).unwrap(); } // // 2. FAIL: Update forester authority with invalid authority // { @@ -1194,9 +1323,8 @@ async fn failing_test_forester() { derivation: payer.pubkey(), }; let mut ix = create_nullify_instruction(inputs, 0); - let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); // Swap the derived forester pda with an initialized but invalid one. - ix.accounts[0].pubkey = get_forester_epoch_pda_address(&forester_pda, 0).0; + ix.accounts[0].pubkey = next_epoch_invalid.forester_epoch_pda; let result = rpc .create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) .await; @@ -1225,7 +1353,7 @@ async fn failing_test_forester() { ); let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); // Swap the derived forester pda with an initialized but invalid one. - instruction.accounts[0].pubkey = get_forester_epoch_pda_address(&forester_pda, 0).0; + instruction.accounts[0].pubkey = next_epoch_invalid.forester_epoch_pda; let result = rpc .create_and_send_transaction(&[instruction], &authority.pubkey(), &[&authority]) @@ -1250,7 +1378,7 @@ async fn failing_test_forester() { .await; let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); // Swap the derived forester pda with an initialized but invalid one. - instructions[2].accounts[0].pubkey = get_forester_epoch_pda_address(&forester_pda, 0).0; + instructions[2].accounts[0].pubkey = next_epoch_invalid.forester_epoch_pda; //get_forester_epoch_pda_address(&forester_pda, 0).0; let result = rpc .create_and_send_transaction( @@ -1279,9 +1407,8 @@ async fn failing_test_forester() { 0, // TODO: adapt epoch ) .await; - let (forester_pda, _) = get_forester_pda_address(&env.forester.pubkey()); // Swap the derived forester pda with an initialized but invalid one. - instructions[2].accounts[0].pubkey = get_forester_epoch_pda_address(&forester_pda, 0).0; + instructions[2].accounts[0].pubkey = next_epoch_invalid.forester_epoch_pda; let result = rpc .create_and_send_transaction( diff --git a/test-utils/src/registry.rs b/test-utils/src/registry.rs index 9bcd517973..25d21d1468 100644 --- a/test-utils/src/registry.rs +++ b/test-utils/src/registry.rs @@ -390,6 +390,7 @@ pub struct WithdrawInputs<'a> { pub amount: u64, pub delegate_account: FetchedAccount, pub input_escrow_token_account: TokenDataWithContext, + pub epoch: u64, } pub async fn deposit_test<'a, R: RpcConnection, I: Indexer>( @@ -411,10 +412,11 @@ pub async fn withdraw_test<'a, R: RpcConnection, I: Indexer>( delegate_account: Some(inputs.delegate_account), input_token_data: Vec::new(), input_escrow_token_account: Some(inputs.input_escrow_token_account), - epoch: 0, + epoch: inputs.epoch, }; deposit_or_withdraw_test::(rpc, indexer, inputs).await } + pub async fn deposit_or_withdraw_test< 'a, R: RpcConnection, @@ -573,6 +575,8 @@ pub fn assert_deposit_or_withdrawal( assert_eq!(output_escrow_token_data, expected_escrow_token_data); let expected_delegate_account = if let Some(mut input_pda) = inputs.delegate_account.clone() { + input_pda.delegate_account.sync_pending_stake_weight(epoch); + println!("input pda {:?}", input_pda.delegate_account); input_pda.delegate_account.escrow_token_account_hash = output_escrow_token_data.hash::().unwrap(); if IS_DEPOSIT { @@ -760,8 +764,15 @@ pub async fn assert_delegate_or_undelegate input_pda.delegate_account.pending_epoch { @@ -775,6 +786,14 @@ pub async fn assert_delegate_or_undelegate( } } -pub async fn set_env_with_delegate_and_forester( +pub async fn set_env_with_delegate_and_forester_local( protocol_config: Option, - keypair_config: Option, - general_config: Option, - rounds: u64, - seed: Option, ) -> ( - crate::e2e_test_env::E2ETestEnv< - ProgramTestRpcConnection, - TestIndexer, - >, + ProgramTestRpcConnection, + TestIndexer, Keypair, EnvAccounts, Vec, Epoch, ) { let protocol_config = protocol_config.unwrap_or_default(); + let (rpc, env) = setup_test_programs_with_accounts_with_protocol_config(None, protocol_config, false).await; let indexer: TestIndexer = TestIndexer::init_from_env( @@ -436,19 +431,36 @@ pub async fn set_env_with_delegate_and_forester( KeypairActionConfig::all_default().non_inclusion(), ) .await; - let mut e2e_env = - E2ETestEnv::>::new( - rpc, - indexer, - &env, - keypair_config.unwrap_or(KeypairActionConfig::all_default()), - general_config.unwrap_or_default(), - rounds, - seed, - ) - .await; + set_env_with_delegate_and_forester(rpc, indexer, env, protocol_config).await +} + +pub async fn set_env_with_delegate_and_forester>( + mut rpc: R, + mut indexer: I, + env: EnvAccounts, + protocol_config: ProtocolConfig, +) -> (R, I, Keypair, EnvAccounts, Vec, Epoch) { + // let mut e2e_env = + // E2ETestEnv::>::new( + // rpc, + // indexer, + // &env, + // keypair_config.unwrap_or(KeypairActionConfig::all_default()), + // general_config.unwrap_or_default(), + // rounds, + // seed, + // ) + // .await; + // let indexer: TestIndexer = TestIndexer::init_from_env( + // &env.forester.insecure_clone(), + // &env, + // KeypairActionConfig::all_default().inclusion(), + // KeypairActionConfig::all_default().non_inclusion(), + // ) + // .await; let delegate_keypair = create_delegate( - &mut e2e_env, + &mut rpc, + &mut indexer, &env, 1_000_000, env.registered_forester_pda, @@ -472,33 +484,29 @@ pub async fn set_env_with_delegate_and_forester( }, ]; let slot = protocol_config.genesis_slot + protocol_config.active_phase_length; - e2e_env.rpc.warp_to_slot(slot).unwrap(); + rpc.warp_to_slot(slot).unwrap(); let forester_pda_pubkey = get_forester_pda_address(&env.forester.pubkey()).0; - let forester_pda = e2e_env - .rpc + let forester_pda = rpc .get_anchor_account::(&forester_pda_pubkey) .await .unwrap() .unwrap(); println!("forester_pda: {:?}", forester_pda); - let registered_epoch = Epoch::register(&mut e2e_env.rpc, &protocol_config, &env.forester) + let registered_epoch = Epoch::register(&mut rpc, &protocol_config, &env.forester) .await .unwrap(); assert!(registered_epoch.is_some()); let mut registered_epoch = registered_epoch.unwrap(); let forester_pda_pubkey = get_forester_pda_address(&env.forester.pubkey()).0; - let forester_pda = e2e_env - .rpc + let forester_pda = rpc .get_anchor_account::(&forester_pda_pubkey) .await .unwrap() .unwrap(); - println!("forester_pda: {:?}", forester_pda); - let forester_epoch_pda: ForesterEpochPda = e2e_env - .rpc + let forester_epoch_pda: ForesterEpochPda = rpc .get_anchor_account::(®istered_epoch.forester_epoch_pda) .await .unwrap() @@ -512,63 +520,56 @@ pub async fn set_env_with_delegate_and_forester( let expected_stake = forester_pda.active_stake_weight; println!("expected_stake: {}", expected_stake); - assert_epoch_pda(&mut e2e_env.rpc, epoch, expected_stake).await; + assert_epoch_pda(&mut rpc, epoch, expected_stake).await; assert_registered_forester_pda( - &mut e2e_env.rpc, + &mut rpc, ®istered_epoch.forester_epoch_pda, &env.forester.pubkey(), epoch, ) .await; - let current_slot = e2e_env.rpc.get_slot().await.unwrap(); - e2e_env - .rpc - .warp_to_slot(current_slot + protocol_config.active_phase_length) + // let current_slot = rpc.get_slot().await.unwrap(); + rpc.warp_to_slot(registered_epoch.phases.active.start) .unwrap(); + let ix = create_finalize_registration_instruction(&env.forester.pubkey(), epoch); - e2e_env - .rpc - .create_and_send_transaction(&[ix], &env.forester.pubkey(), &[&env.forester]) + rpc.create_and_send_transaction(&[ix], &env.forester.pubkey(), &[&env.forester]) .await .unwrap(); let epoch_pda = get_epoch_pda_address(epoch); - assert_finalized_epoch_registration( - &mut e2e_env.rpc, - ®istered_epoch.forester_epoch_pda, - &epoch_pda, - ) - .await; + assert_finalized_epoch_registration(&mut rpc, ®istered_epoch.forester_epoch_pda, &epoch_pda) + .await; // Refetch after finalization - let forester_epoch_pda: ForesterEpochPda = e2e_env - .rpc + let forester_epoch_pda: ForesterEpochPda = rpc .get_anchor_account::(®istered_epoch.forester_epoch_pda) .await .unwrap() .unwrap(); - let current_solana_slot = e2e_env.rpc.get_slot().await.unwrap(); + let current_solana_slot = rpc.get_slot().await.unwrap(); registered_epoch.add_trees_with_schedule( &forester_epoch_pda, tree_accounts.clone(), current_solana_slot, ); - let _forester = Forester { - registration: registered_epoch.clone(), - active: registered_epoch.clone(), - ..Default::default() - }; - // Forester epoch account is assumed to exist (is inited with test program deployment) - let forester = TestForester { - keypair: env.forester.insecure_clone(), - forester: _forester.clone(), - is_registered: Some(0), - }; - e2e_env.foresters.push(forester); - e2e_env.epoch_config = _forester; - e2e_env.epoch = epoch; + // let _forester = Forester { + // registration: registered_epoch.clone(), + // active: registered_epoch.clone(), + // ..Default::default() + // }; + // // Forester epoch account is assumed to exist (is inited with test program deployment) + // let forester = TestForester { + // keypair: env.forester.insecure_clone(), + // forester: _forester.clone(), + // is_registered: Some(0), + // }; + // e2e_env.foresters.push(forester); + // e2e_env.epoch_config = _forester; + // e2e_env.epoch = epoch; ( - e2e_env, + rpc, + indexer, delegate_keypair, env, tree_accounts, @@ -961,11 +962,9 @@ pub async fn register_program_with_registry_program( Ok(token_program_registered_program_pda) } -pub async fn create_delegate( - e2e_env: &mut crate::e2e_test_env::E2ETestEnv< - ProgramTestRpcConnection, - TestIndexer, - >, +pub async fn create_delegate>( + rpc: &mut R, + indexer: &mut I, env: &EnvAccounts, deposit_amount: u64, forester_pda: Pubkey, @@ -975,12 +974,12 @@ pub async fn create_delegate( let (delegate_keypair, delegate_account, delegate_escrow) = if let Some(delegate_keypair) = delegate_keypair { let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( - &mut e2e_env.indexer, + indexer, &delegate_keypair.pubkey(), &light_registry::ID, ); println!("delegate_account: {:?}", delegate_account); - let escrow_account = e2e_env.indexer.get_compressed_token_accounts_by_owner( + let escrow_account = indexer.get_compressed_token_accounts_by_owner( &get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0, ); ( @@ -991,15 +990,13 @@ pub async fn create_delegate( } else { (Keypair::new(), None, None) }; - e2e_env - .rpc - .airdrop_lamports(&delegate_keypair.pubkey(), 1_000_000_000) + rpc.airdrop_lamports(&delegate_keypair.pubkey(), 1_000_000_000) .await .unwrap(); - mint_standard_tokens::>( - &mut e2e_env.rpc, - &mut e2e_env.indexer, + mint_standard_tokens( + rpc, + indexer, &env.governance_authority, &delegate_keypair.pubkey(), 1_000_000_000, @@ -1009,7 +1006,8 @@ pub async fn create_delegate( .unwrap(); deposit_to_delegate_account_helper( - e2e_env, + rpc, + indexer, &delegate_keypair, deposit_amount, env, @@ -1020,8 +1018,8 @@ pub async fn create_delegate( .await; // delegate to forester { - let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( - &mut e2e_env.indexer, + let delegate_account = get_custom_compressed_account::<_, I, DelegateAccount>( + indexer, &delegate_keypair.pubkey(), &light_registry::ID, ); @@ -1033,11 +1031,9 @@ pub async fn create_delegate( forester_pda, output_merkle_tree: env.merkle_tree_pubkey, }; - delegate_test(&mut e2e_env.rpc, &mut e2e_env.indexer, inputs) - .await - .unwrap(); - let delegate_account = get_custom_compressed_account::<_, _, DelegateAccount>( - &mut e2e_env.indexer, + delegate_test(rpc, indexer, inputs).await.unwrap(); + let delegate_account = get_custom_compressed_account::<_, I, DelegateAccount>( + indexer, &delegate_keypair.pubkey(), &light_registry::ID, ); @@ -1046,11 +1042,9 @@ pub async fn create_delegate( delegate_keypair } -pub async fn deposit_to_delegate_account_helper( - e2e_env: &mut crate::e2e_test_env::E2ETestEnv< - ProgramTestRpcConnection, - TestIndexer, - >, +pub async fn deposit_to_delegate_account_helper>( + rpc: &mut R, + indexer: &mut I, delegate_keypair: &Keypair, deposit_amount: u64, env: &EnvAccounts, @@ -1060,14 +1054,12 @@ pub async fn deposit_to_delegate_account_helper( ) { let escrow_pda_authority = get_escrow_token_authority(&delegate_keypair.pubkey(), 0).0; - let token_accounts = e2e_env - .indexer - .get_compressed_token_accounts_by_owner(&delegate_keypair.pubkey()); + let token_accounts = indexer.get_compressed_token_accounts_by_owner(&delegate_keypair.pubkey()); // approve amount is expected to equal deposit amount approve_test( &delegate_keypair, - &mut e2e_env.rpc, - &mut e2e_env.indexer, + rpc, + indexer, token_accounts, deposit_amount, None, @@ -1077,8 +1069,7 @@ pub async fn deposit_to_delegate_account_helper( None, ) .await; - let token_accounts = e2e_env - .indexer + let token_accounts = indexer .get_compressed_token_accounts_by_owner(&delegate_keypair.pubkey()) .iter() .filter(|a| a.token_data.delegate.is_some()) @@ -1102,7 +1093,5 @@ pub async fn deposit_to_delegate_account_helper( epoch, }; - deposit_test(&mut e2e_env.rpc, &mut e2e_env.indexer, deposit_inputs) - .await - .unwrap(); + deposit_test(rpc, indexer, deposit_inputs).await.unwrap(); }