From c75ceeb8d17d861b82588b076c753a333198b326 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:41:48 -0700 Subject: [PATCH 1/5] Update is_open and max committee member checks --- synthesizer/process/src/tests/test_credits.rs | 266 ++++++++++++++++-- .../program/src/resources/credits.aleo | 104 ++++--- 2 files changed, 298 insertions(+), 72 deletions(-) diff --git a/synthesizer/process/src/tests/test_credits.rs b/synthesizer/process/src/tests/test_credits.rs index 64d14970d0..7ca26b863a 100644 --- a/synthesizer/process/src/tests/test_credits.rs +++ b/synthesizer/process/src/tests/test_credits.rs @@ -941,6 +941,128 @@ fn test_bond_delegator_multiple_bonds() { .unwrap(); } +#[test] +fn test_bond_validator_and_delegator_multiple_times() { + let rng = &mut TestRng::default(); + + // Construct the process. + let process = Process::::load().unwrap(); + + // Initialize a new finalize store. + let finalize_store = FinalizeStore::>::open(None).unwrap(); + + // Initialize the validators and delegators. + let (validators, delegators) = initialize_stakers(&finalize_store, 1, 1, rng).unwrap(); + let (validator_private_key, (validator_address, _)) = validators.first().unwrap(); + let (delegator_private_key, (delegator_address, _)) = delegators.first().unwrap(); + + // Retrieve the account balances. + let validator_public_balance = account_balance(&finalize_store, validator_address).unwrap(); + let delegator_public_balance = account_balance(&finalize_store, delegator_address).unwrap(); + + // Prepare the validator amount. + let validator_amount = MIN_VALIDATOR_STAKE; + // Perform the bond. + bond_public( + &process, + &finalize_store, + validator_private_key, + validator_address, + validator_address, + validator_amount, + rng, + ) + .unwrap(); + + // Check that the committee, bond, unbond, and withdraw states are correct. + assert_eq!(committee_state(&finalize_store, validator_address).unwrap(), Some((validator_amount, true))); + assert_eq!(bond_state(&finalize_store, validator_address).unwrap(), Some((*validator_address, validator_amount))); + assert_eq!(unbond_state(&finalize_store, validator_address).unwrap(), None); + assert_eq!(withdraw_state(&finalize_store, validator_address).unwrap(), Some(*validator_address)); + assert_eq!( + account_balance(&finalize_store, validator_address).unwrap(), + validator_public_balance - validator_amount + ); + + // Bond the delegator to the validator. + let delegator_amount = MIN_DELEGATOR_STAKE; + bond_public( + &process, + &finalize_store, + delegator_private_key, + validator_address, + delegator_address, + delegator_amount, + rng, + ) + .unwrap(); + + // Check that the committee, bond, unbond, and withdraw states are correct. + let combined_amount = validator_amount + delegator_amount; + assert_eq!(committee_state(&finalize_store, validator_address).unwrap(), Some((combined_amount, true))); + assert_eq!(bond_state(&finalize_store, delegator_address).unwrap(), Some((*validator_address, delegator_amount))); + assert_eq!(unbond_state(&finalize_store, delegator_address).unwrap(), None); + assert_eq!(withdraw_state(&finalize_store, delegator_address).unwrap(), Some(*delegator_address)); + assert_eq!( + account_balance(&finalize_store, delegator_address).unwrap(), + delegator_public_balance - delegator_amount + ); + + /* Ensure bonding to a closed validator succeeds for the existing stakers. */ + + // Bond the validator again. + bond_public( + &process, + &finalize_store, + validator_private_key, + validator_address, + validator_address, + validator_amount, + rng, + ) + .unwrap(); + + // Check that the committee, bond, unbond, and withdraw states are correct. + let combined_amount = 2 * validator_amount + delegator_amount; + assert_eq!(committee_state(&finalize_store, validator_address).unwrap(), Some((combined_amount, true))); + assert_eq!( + bond_state(&finalize_store, validator_address).unwrap(), + Some((*validator_address, 2 * validator_amount)) + ); + assert_eq!(unbond_state(&finalize_store, validator_address).unwrap(), None); + assert_eq!(withdraw_state(&finalize_store, validator_address).unwrap(), Some(*validator_address)); + assert_eq!( + account_balance(&finalize_store, validator_address).unwrap(), + validator_public_balance - (2 * validator_amount) + ); + + // Bond the delegator to the validator again. + bond_public( + &process, + &finalize_store, + delegator_private_key, + validator_address, + delegator_address, + delegator_amount, + rng, + ) + .unwrap(); + + // Check that the committee, bond, unbond, and withdraw states are correct. + let combined_amount = 2 * (validator_amount + delegator_amount); + assert_eq!(committee_state(&finalize_store, validator_address).unwrap(), Some((combined_amount, true))); + assert_eq!( + bond_state(&finalize_store, delegator_address).unwrap(), + Some((*validator_address, 2 * delegator_amount)) + ); + assert_eq!(unbond_state(&finalize_store, delegator_address).unwrap(), None); + assert_eq!(withdraw_state(&finalize_store, delegator_address).unwrap(), Some(*delegator_address)); + assert_eq!( + account_balance(&finalize_store, delegator_address).unwrap(), + delegator_public_balance - (2 * delegator_amount) + ); +} + #[test] fn test_bond_delegator_to_nonexistent_validator_fails() { let rng = &mut TestRng::default(); @@ -1682,7 +1804,132 @@ fn test_set_validator_state_for_non_validator_fails() { } #[test] -fn test_bonding_to_closed_fails() { +fn test_bonding_existing_stakers_to_closed_validator() { + let rng = &mut TestRng::default(); + + // Construct the process. + let process = Process::::load().unwrap(); + + // Initialize a new finalize store. + let finalize_store = FinalizeStore::>::open(None).unwrap(); + + // Initialize the validators and delegators. + let (validators, delegators) = initialize_stakers(&finalize_store, 1, 1, rng).unwrap(); + let (validator_private_key, (validator_address, _)) = validators.first().unwrap(); + let (delegator_private_key, (delegator_address, _)) = delegators.first().unwrap(); + + // Retrieve the account balances. + let validator_public_balance = account_balance(&finalize_store, validator_address).unwrap(); + let delegator_public_balance = account_balance(&finalize_store, delegator_address).unwrap(); + + // Prepare the validator amount. + let validator_amount = MIN_VALIDATOR_STAKE; + // Perform the bond. + bond_public( + &process, + &finalize_store, + validator_private_key, + validator_address, + validator_address, + validator_amount, + rng, + ) + .unwrap(); + + // Check that the committee, bond, unbond, and withdraw states are correct. + assert_eq!(committee_state(&finalize_store, validator_address).unwrap(), Some((validator_amount, true))); + assert_eq!(bond_state(&finalize_store, validator_address).unwrap(), Some((*validator_address, validator_amount))); + assert_eq!(unbond_state(&finalize_store, validator_address).unwrap(), None); + assert_eq!(withdraw_state(&finalize_store, validator_address).unwrap(), Some(*validator_address)); + assert_eq!( + account_balance(&finalize_store, validator_address).unwrap(), + validator_public_balance - validator_amount + ); + + // Bond the delegator to the validator. + let delegator_amount = MIN_DELEGATOR_STAKE; + bond_public( + &process, + &finalize_store, + delegator_private_key, + validator_address, + delegator_address, + delegator_amount, + rng, + ) + .unwrap(); + + // Check that the committee, bond, unbond, and withdraw states are correct. + let combined_amount = validator_amount + delegator_amount; + assert_eq!(committee_state(&finalize_store, validator_address).unwrap(), Some((combined_amount, true))); + assert_eq!(bond_state(&finalize_store, delegator_address).unwrap(), Some((*validator_address, delegator_amount))); + assert_eq!(unbond_state(&finalize_store, delegator_address).unwrap(), None); + assert_eq!(withdraw_state(&finalize_store, delegator_address).unwrap(), Some(*delegator_address)); + assert_eq!( + account_balance(&finalize_store, delegator_address).unwrap(), + delegator_public_balance - delegator_amount + ); + + // Set the validator `is_open` state to `false`. + set_validator_state(&process, &finalize_store, validator_private_key, false, rng).unwrap(); + + /* Ensure bonding to a closed validator succeeds for the existing stakers. */ + + // Bond the validator again. + bond_public( + &process, + &finalize_store, + validator_private_key, + validator_address, + validator_address, + validator_amount, + rng, + ) + .unwrap(); + + // Check that the committee, bond, unbond, and withdraw states are correct. + let combined_amount = 2 * validator_amount + delegator_amount; + assert_eq!(committee_state(&finalize_store, validator_address).unwrap(), Some((combined_amount, false))); + assert_eq!( + bond_state(&finalize_store, validator_address).unwrap(), + Some((*validator_address, 2 * validator_amount)) + ); + assert_eq!(unbond_state(&finalize_store, validator_address).unwrap(), None); + assert_eq!(withdraw_state(&finalize_store, validator_address).unwrap(), Some(*validator_address)); + assert_eq!( + account_balance(&finalize_store, validator_address).unwrap(), + validator_public_balance - (2 * validator_amount) + ); + + // Bond the delegator to the validator again. + bond_public( + &process, + &finalize_store, + delegator_private_key, + validator_address, + delegator_address, + delegator_amount, + rng, + ) + .unwrap(); + + // Check that the committee, bond, unbond, and withdraw states are correct. + let combined_amount = 2 * (validator_amount + delegator_amount); + assert_eq!(committee_state(&finalize_store, validator_address).unwrap(), Some((combined_amount, false))); + assert_eq!( + bond_state(&finalize_store, delegator_address).unwrap(), + Some((*validator_address, 2 * delegator_amount)) + ); + assert_eq!(unbond_state(&finalize_store, delegator_address).unwrap(), None); + assert_eq!(withdraw_state(&finalize_store, delegator_address).unwrap(), Some(*delegator_address)); + assert_eq!( + account_balance(&finalize_store, delegator_address).unwrap(), + delegator_public_balance - (2 * delegator_amount) + ); +} + +#[test] +fn test_bonding_new_staker_to_closed_validator_fails() { let rng = &mut TestRng::default(); // Construct the process. @@ -1706,22 +1953,7 @@ fn test_bonding_to_closed_fails() { // Set the validator `is_open` state to `false`. set_validator_state(&process, &finalize_store, validator_private_key, false, rng).unwrap(); - // Ensure that the validator can't bond additional stake. - let validator_amount = MIN_VALIDATOR_STAKE; - assert!( - bond_public( - &process, - &finalize_store, - validator_private_key, - validator_address, - validator_address, - validator_amount, - rng - ) - .is_err() - ); - - // Ensure that delegators can't bond to the validator. + // Ensure that new delegators can't bond to the validator. let delegator_amount = MIN_DELEGATOR_STAKE; assert!( bond_public( diff --git a/synthesizer/program/src/resources/credits.aleo b/synthesizer/program/src/resources/credits.aleo index 51fc62e167..a49e490318 100644 --- a/synthesizer/program/src/resources/credits.aleo +++ b/synthesizer/program/src/resources/credits.aleo @@ -25,11 +25,11 @@ mapping committee: // The value represents the committee state of the validator. value as committee_state.public; -// The `committee_state` struct tracks the total stake of the validator, and whether they are open to stakers. +// The `committee_state` struct tracks the total stake of the validator, and whether they are open to new stakers. struct committee_state: // The amount of microcredits bonded to the validator, by the validator and its delegators. microcredits as u64; - // The boolean flag indicating if the validator is open to stakers. + // The boolean flag indicating if the validator is open to new stakers. is_open as boolean; /**********************************************************************************************************************/ @@ -164,10 +164,6 @@ finalize bond_public: get.or_use metadata[aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc] 0u32 into r7; // Increment the committee size by one. add r7 1u32 into r8; - // Determine if the committee size is less than or equal to 10. - lte r8 10u32 into r9; - // Enforce that the committee size is less than or equal to 10. - assert.eq r9 true; // Set the new committee size. set r8 into metadata[aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc]; // Set the withdrawal address. @@ -178,52 +174,50 @@ finalize bond_public: // Construct the initial committee state. // Note: We set the initial 'is_open' state to 'true'. - cast 0u64 true into r10 as committee_state; + cast 0u64 true into r9 as committee_state; // Retrieve the committee state of the specified validator. - get.or_use committee[r0] r10 into r11; - // Ensure that the validator is open to stakers. - assert.eq r11.is_open true; + get.or_use committee[r0] r9 into r10; // Increment the stake for the specified validator. - add r11.microcredits r3 into r12; + add r10.microcredits r3 into r11; // Construct the updated committee state. - cast r12 r11.is_open into r13 as committee_state; + cast r11 r10.is_open into r12 as committee_state; /* Bonded */ // Construct the initial bond state. - cast r0 0u64 into r14 as bond_state; + cast r0 0u64 into r13 as bond_state; // Get the bond state for the caller, or default to the initial bond state. - get.or_use bonded[r0] r14 into r15; + get.or_use bonded[r0] r13 into r14; // Enforce the validator matches in the bond state. - assert.eq r15.validator r0; + assert.eq r14.validator r0; // Increment the microcredits in the bond state. - add r11.microcredits r3 into r16; + add r14.microcredits r3 into r15; // Determine if the amount is at least 10 million credits. - gte r16 10_000_000_000_000u64 into r17; + gte r15 10_000_000_000_000u64 into r16; // Enforce the amount is at least 10 million credits. - assert.eq r17 true; + assert.eq r16 true; // Construct the updated bond state. - cast r0 r16 into r18 as bond_state; + cast r0 r15 into r17 as bond_state; /* Account */ // Get the balance of the caller. // If the account does not exist, this finalize scope will fail. - get account[r0] into r19; + get account[r0] into r18; // Decrement the balance of the caller. - sub r19 r3 into r20; + sub r18 r3 into r19; /* Writes */ // Update the committee state of the specified validator. - set r13 into committee[r0]; + set r12 into committee[r0]; // Update the bond state for the caller. - set r18 into bonded[r0]; + set r17 into bonded[r0]; // Update the balance of the caller. - set r20 into account[r0]; + set r19 into account[r0]; // Ends the `bond_validator` logic. branch.eq true true to end; @@ -236,36 +230,36 @@ finalize bond_public: /* Committee */ // Check if the caller is a validator. - contains committee[r0] into r21; + contains committee[r0] into r20; // Enforce the caller is *not* a validator. - assert.eq r21 false; + assert.eq r20 false; // Get the stake for the specified validator. // If the validator does not exist, this finalize scope will fail. - get committee[r1] into r22; - // Ensure that the validator is open to stakers. - assert.eq r22.is_open true; + get committee[r1] into r21; // Increment the stake for the specified validator. - add r22.microcredits r3 into r23; + add r21.microcredits r3 into r22; // Construct the updated committee state. - cast r23 r22.is_open into r24 as committee_state; + cast r22 r21.is_open into r23 as committee_state; // Check if the delegator is already bonded to the validator. - contains bonded[r0] into r25; + contains bonded[r0] into r24; // If the delegator is already bonded to the validator, jump to the `continue_bond_delegator` logic. - branch.eq r25 true to continue_bond_delegator; + branch.eq r24 true to continue_bond_delegator; + // Ensure that the validator is open to new stakers. + assert.eq r21.is_open true; // Get the number of delegators. - get.or_use metadata[aleo1qgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqanmpl0] 0u32 into r26; + get.or_use metadata[aleo1qgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqanmpl0] 0u32 into r25; // Increment the number of bonded delegators by one. - add r26 1u32 into r27; + add r25 1u32 into r26; // Determine if the number of delegators is less than or equal to 100_000. - lte r27 100_000u32 into r28; + lte r26 100_000u32 into r27; // Enforce that the number of delegators is less than or equal to 100_000. - assert.eq r28 true; + assert.eq r27 true; // Set the new number of delegators. - set r27 into metadata[aleo1qgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqanmpl0]; + set r26 into metadata[aleo1qgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqanmpl0]; // Set the withdrawal address. set r2 into withdraw[r0]; @@ -275,38 +269,38 @@ finalize bond_public: /* Bonded */ // Construct the initial bond state. - cast r1 0u64 into r29 as bond_state; + cast r1 0u64 into r28 as bond_state; // Get the bond state for the caller, or default to the initial bond state. - get.or_use bonded[r0] r29 into r30; + get.or_use bonded[r0] r28 into r29; // Enforce the validator matches in the bond state. - assert.eq r30.validator r1; + assert.eq r29.validator r1; // Increment the microcredits in the bond state. - add r30.microcredits r3 into r31; + add r29.microcredits r3 into r30; // Determine if the amount is at least 10 thousand credits. - gte r31 10_000_000_000u64 into r32; + gte r30 10_000_000_000u64 into r31; // Enforce the amount is at least 10 thousand credits. - assert.eq r32 true; + assert.eq r31 true; // Construct the updated bond state. - cast r1 r31 into r33 as bond_state; + cast r1 r30 into r32 as bond_state; /* Account */ // Get the balance of the caller. // If the account does not exist, this finalize scope will fail. - get account[r0] into r34; + get account[r0] into r33; // Decrement the balance of the caller. - sub r34 r3 into r35; + sub r33 r3 into r34; /* Writes */ // Update the committee state for the specified validator. - set r24 into committee[r1]; + set r23 into committee[r1]; // Update the bond state for the caller. - set r33 into bonded[r0]; + set r32 into bonded[r0]; // Update the balance of the caller. - set r35 into account[r0]; + set r34 into account[r0]; // The terminus. position end; @@ -566,7 +560,7 @@ finalize unbond_delegator_as_validator: // Get the committee state for the specified validator. // If the validator does not exist, this finalize scope will fail. get committee[r0] into r2; - // Enforce that the validator is closed to stakers. + // Enforce that the validator is closed to new stakers. assert.eq r2.is_open false; // Check if the delegator is a validator. @@ -680,9 +674,9 @@ finalize claim_unbond_public: /**********************************************************************************************************************/ -// This function allows a validator to set their state to be either opened or closed to stakers. -// When the validator is open to stakers, any staker (including the validator) can bond or unbond from the validator. -// When the validator is closed to stakers, all stakers can only unbond from the validator. +// This function allows a validator to set their state to be either opened or closed to new stakers. +// When the validator is open to new stakers, any staker (including the validator) can bond or unbond from the validator. +// When the validator is closed to new stakers, existing stakers can still bond or unbond from the validator, but new stakers cannot bond. // // This function serves two primary purposes: // 1. Allow a validator to leave the committee, by closing themselves to stakers and then unbonding all of their stakers. @@ -690,7 +684,7 @@ finalize claim_unbond_public: function set_validator_state: // Input the 'is_open' state. input r0 as boolean.public; - // Set the validator to be either open or closed to stakers. + // Set the validator to be either open or closed to new stakers. async set_validator_state self.caller r0 into r1; // Output the finalize future. output r1 as credits.aleo/set_validator_state.future; From b0bd13f0c92f69c7323acecdba72456be6b14e0d Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:51:17 -0700 Subject: [PATCH 2/5] nit --- synthesizer/process/src/tests/test_credits.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/synthesizer/process/src/tests/test_credits.rs b/synthesizer/process/src/tests/test_credits.rs index 5ff1c95996..0986c204f9 100644 --- a/synthesizer/process/src/tests/test_credits.rs +++ b/synthesizer/process/src/tests/test_credits.rs @@ -1008,8 +1008,6 @@ fn test_bond_validator_and_delegator_multiple_times() { delegator_public_balance - delegator_amount ); - /* Ensure bonding to a closed validator succeeds for the existing stakers. */ - // Bond the validator again. bond_public( &process, From 4ec6e7c0c2d201911dbaee32431d665590afcc19 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:09:58 -0700 Subject: [PATCH 3/5] Add tests for is_*function* --- ledger/block/src/transition/mod.rs | 8 +-- synthesizer/src/vm/execute.rs | 84 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/ledger/block/src/transition/mod.rs b/ledger/block/src/transition/mod.rs index 0856e20cf4..12a8290912 100644 --- a/ledger/block/src/transition/mod.rs +++ b/ledger/block/src/transition/mod.rs @@ -311,8 +311,8 @@ impl Transition { /// Returns `true` if this is a `bond_public` transition. #[inline] pub fn is_bond_public(&self) -> bool { - self.inputs.len() == 2 - && self.outputs.is_empty() + self.inputs.len() == 3 + && self.outputs.len() == 1 && self.program_id.to_string() == "credits.aleo" && self.function_name.to_string() == "bond_public" } @@ -320,8 +320,8 @@ impl Transition { /// Returns `true` if this is an `unbond_public` transition. #[inline] pub fn is_unbond_public(&self) -> bool { - self.inputs.len() == 2 - && self.outputs.is_empty() + self.inputs.len() == 1 + && self.outputs.len() == 1 && self.program_id.to_string() == "credits.aleo" && self.function_name.to_string() == "unbond_public" } diff --git a/synthesizer/src/vm/execute.rs b/synthesizer/src/vm/execute.rs index af0fbcb03b..328760a8df 100644 --- a/synthesizer/src/vm/execute.rs +++ b/synthesizer/src/vm/execute.rs @@ -244,6 +244,78 @@ mod tests { Ok((vm, records)) } + #[test] + fn test_bond_public_transaction_size() { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng); + let address = Address::try_from(&caller_private_key).unwrap(); + + // Prepare the VM and records. + let (vm, _) = prepare_vm(rng).unwrap(); + + // Prepare the inputs. + let inputs = [ + Value::::from_str(&address.to_string()).unwrap(), + Value::::from_str(&address.to_string()).unwrap(), + Value::::from_str("1_000_000u64").unwrap(), + ] + .into_iter(); + + // Execute. + let transaction = + vm.execute(&caller_private_key, ("credits.aleo", "bond_public"), inputs, None, 0, None, rng).unwrap(); + + // Ensure the transaction is a bond public transition. + assert_eq!(transaction.transitions().count(), 2); + assert!(transaction.transitions().take(1).next().unwrap().is_bond_public()); + + // Assert the size of the transaction. + let transaction_size_in_bytes = transaction.to_bytes_le().unwrap().len(); + assert_eq!(2970, transaction_size_in_bytes, "Update me if serialization has changed"); + + // Assert the size of the execution. + assert!(matches!(transaction, Transaction::Execute(_, _, _))); + if let Transaction::Execute(_, execution, _) = &transaction { + let execution_size_in_bytes = execution.to_bytes_le().unwrap().len(); + assert_eq!(1519, execution_size_in_bytes, "Update me if serialization has changed"); + } + } + + #[test] + fn test_unbond_public_transaction_size() { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng); + + // Prepare the VM and records. + let (vm, _) = prepare_vm(rng).unwrap(); + + // Prepare the inputs. + let inputs = [Value::::from_str("1u64").unwrap()].into_iter(); + + // Execute. + let transaction = + vm.execute(&caller_private_key, ("credits.aleo", "unbond_public"), inputs, None, 0, None, rng).unwrap(); + + // Ensure the transaction is an unbond public transition. + assert_eq!(transaction.transitions().count(), 2); + assert!(transaction.transitions().take(1).next().unwrap().is_unbond_public()); + + // Assert the size of the transaction. + let transaction_size_in_bytes = transaction.to_bytes_le().unwrap().len(); + assert_eq!(2760, transaction_size_in_bytes, "Update me if serialization has changed"); + + // Assert the size of the execution. + assert!(matches!(transaction, Transaction::Execute(_, _, _))); + if let Transaction::Execute(_, execution, _) = &transaction { + let execution_size_in_bytes = execution.to_bytes_le().unwrap().len(); + assert_eq!(1309, execution_size_in_bytes, "Update me if serialization has changed"); + } + } + #[test] fn test_transfer_private_transaction_size() { let rng = &mut TestRng::default(); @@ -408,6 +480,10 @@ mod tests { let transaction = vm.execute(&caller_private_key, ("credits.aleo", "split"), inputs, None, 0, None, rng).unwrap(); + // Ensure the transaction is a split transition. + assert_eq!(transaction.transitions().count(), 2); + assert!(transaction.transitions().take(1).next().unwrap().is_split()); + // Assert the size of the transaction. let transaction_size_in_bytes = transaction.to_bytes_le().unwrap().len(); assert_eq!(2166, transaction_size_in_bytes, "Update me if serialization has changed"); @@ -431,6 +507,10 @@ mod tests { Transaction::Fee(_, fee) => fee, _ => panic!("Expected a fee transaction"), }; + + // Ensure the transition is a fee transition. + assert!(fee.is_fee_private()); + // Assert the size of the transition. let fee_size_in_bytes = fee.to_bytes_le().unwrap().len(); assert_eq!(2043, fee_size_in_bytes, "Update me if serialization has changed"); @@ -447,6 +527,10 @@ mod tests { Transaction::Fee(_, fee) => fee, _ => panic!("Expected a fee transaction"), }; + + // Ensure the transition is a fee transition. + assert!(fee.is_fee_public()); + // Assert the size of the transition. let fee_size_in_bytes = fee.to_bytes_le().unwrap().len(); assert_eq!(1416, fee_size_in_bytes, "Update me if serialization has changed"); From b00dcad9ab9fcec5ded70ce8dd9d3b1b042556a1 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:12:48 -0700 Subject: [PATCH 4/5] Reject transactions that have unbond_public calls that exceeds max committee size --- ledger/src/tests.rs | 3 ++ synthesizer/src/vm/finalize.rs | 60 +++++++++++++++++++++++++++++++++- synthesizer/src/vm/mod.rs | 3 +- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index 31e6e918b7..a8090e1608 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -1589,6 +1589,9 @@ fn test_max_committee_limit_with_bonds() { ) .unwrap(); + // Ensure that the `bond_second_transaction` is rejected. + assert_eq!(block.transactions().num_rejected(), 1); + // Check that the next block is valid. ledger.check_next_block(&block, rng).unwrap(); diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index e9fe5d90f5..9d6b703f69 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -430,7 +430,10 @@ impl> VM { // The finalize operation here involves calling 'update_key_value', // and update the respective leaves of the finalize tree. Transaction::Execute(_, execution, fee) => { - match process.finalize_execution(state, store, execution, fee.as_ref()) { + // Determine if the transaction is safe for execution, and proceed to execute it. + match Self::prepare_for_execution(store, execution) + .and_then(|_| process.finalize_execution(state, store, execution, fee.as_ref())) + { // Construct the accepted execute transaction. Ok(finalize) => { ConfirmedTransaction::accepted_execute(counter, transaction.clone(), finalize) @@ -803,6 +806,61 @@ impl> VM { }) } + /// Performs precondition checks on the transaction prior to execution. + /// + /// This method is used to check the following conditions: + /// - If the transaction contains a `credits.aleo/bond_public` transition, + /// then the outcome should not exceed the maximum committee size. + #[inline] + fn prepare_for_execution(store: &FinalizeStore, execution: &Execution) -> Result<()> { + // Construct the program ID. + let program_id = ProgramID::from_str("credits.aleo")?; + // Construct the committee mapping name. + let committee_mapping = Identifier::from_str("committee")?; + + // Check if the execution has any `bond_public` transitions, and collect + // the unique validator addresses if so. + // Note: This does not dedup for existing and new validator addresses. + let bond_validator_addresses: HashSet<_> = execution + .transitions() + .filter_map(|transition| match transition.is_bond_public() { + // Check the first input of the transition for the validator address. + true => match transition.inputs().first() { + Some(Input::Public(_, Some(Plaintext::Literal(Literal::Address(address), _)))) => Some(address), + _ => None, + }, + false => None, + }) + .collect(); + + // Check if we need to reject the execution if the number of new validators exceeds the maximum committee size. + match bond_validator_addresses.is_empty() { + false => { + // Retrieve the committee members from storage. + let committee_members = store + .get_mapping_speculative(program_id, committee_mapping)? + .into_iter() + .map(|(key, _)| match key { + // Extract the address from the key. + Plaintext::Literal(Literal::Address(address), _) => Ok(address), + _ => Err(anyhow!("Invalid committee key (missing address) - {key}")), + }) + .collect::>>()?; + // Get the number of new validators being bonded to. + let num_new_validators = + bond_validator_addresses.into_iter().filter(|address| !committee_members.contains(address)).count(); + // Compute the next committee size. + let next_committee_size = committee_members.len().saturating_add(num_new_validators); + // Check that the number of new validators being bonded does not exceed the maximum number of validators. + match next_committee_size > Committee::::MAX_COMMITTEE_SIZE as usize { + true => Err(anyhow!("Call to 'credits.aleo/bond_public' exceeds the committee size")), + false => Ok(()), + } + } + true => Ok(()), + } + } + /// Performs the pre-ratifications before finalizing transactions. #[inline] fn atomic_pre_ratify<'a>( diff --git a/synthesizer/src/vm/mod.rs b/synthesizer/src/vm/mod.rs index b767c6d66b..7a6b1437c9 100644 --- a/synthesizer/src/vm/mod.rs +++ b/synthesizer/src/vm/mod.rs @@ -35,6 +35,7 @@ use ledger_block::{ Execution, Fee, Header, + Input, Ratifications, Ratify, Rejected, @@ -64,7 +65,7 @@ use indexmap::{IndexMap, IndexSet}; use itertools::Either; use lru::LruCache; use parking_lot::{Mutex, RwLock}; -use std::{num::NonZeroUsize, sync::Arc}; +use std::{collections::HashSet, num::NonZeroUsize, sync::Arc}; #[cfg(not(feature = "serial"))] use rayon::prelude::*; From bb9bf602509bd31982fc1877922dd235cf98e831 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:50:01 -0700 Subject: [PATCH 5/5] Fix split_transaction_size test --- synthesizer/src/vm/execute.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synthesizer/src/vm/execute.rs b/synthesizer/src/vm/execute.rs index 328760a8df..cd113dbeff 100644 --- a/synthesizer/src/vm/execute.rs +++ b/synthesizer/src/vm/execute.rs @@ -481,8 +481,8 @@ mod tests { vm.execute(&caller_private_key, ("credits.aleo", "split"), inputs, None, 0, None, rng).unwrap(); // Ensure the transaction is a split transition. - assert_eq!(transaction.transitions().count(), 2); - assert!(transaction.transitions().take(1).next().unwrap().is_split()); + assert_eq!(transaction.transitions().count(), 1); + assert!(transaction.transitions().next().unwrap().is_split()); // Assert the size of the transaction. let transaction_size_in_bytes = transaction.to_bytes_le().unwrap().len();