Skip to content

Commit

Permalink
fix(nns): Fix maturity for neurons that temporarily fail to spawn (#3323
Browse files Browse the repository at this point in the history
)

A previous pull request sets the maturity to the modulated amount, when
it should be set to the original amount so that modulation is not
applied more than once.

---------

Co-authored-by: oggy-dfin <[email protected]>
  • Loading branch information
max-dfinity and oggy-dfin authored Jan 7, 2025
1 parent a18576a commit 3f4397c
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 5 deletions.
6 changes: 3 additions & 3 deletions rs/nns/governance/src/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6946,11 +6946,11 @@ impl Governance {
.with_neuron(&neuron_id, |neuron| neuron.clone())
.expect("Neuron should exist, just found in list");

let maturity = neuron.maturity_e8s_equivalent;
let original_maturity = neuron.maturity_e8s_equivalent;
let subaccount = neuron.subaccount();

let neuron_stake: u64 = match apply_maturity_modulation(
maturity,
original_maturity,
maturity_modulation,
) {
Ok(neuron_stake) => neuron_stake,
Expand Down Expand Up @@ -7019,7 +7019,7 @@ impl Governance {
error,
);
match self.with_neuron_mut(&neuron_id, |neuron| {
neuron.maturity_e8s_equivalent = neuron_stake;
neuron.maturity_e8s_equivalent = original_maturity;
neuron.cached_neuron_stake_e8s = 0;
neuron.spawn_at_timestamp_seconds =
original_spawn_at_timestamp_seconds;
Expand Down
50 changes: 48 additions & 2 deletions rs/nns/governance/tests/fake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,15 @@ impl Default for FakeState {
/// advanced, and ledger accounts manipulated.
pub struct FakeDriver {
pub state: Arc<Mutex<FakeState>>,
pub error_on_next_ledger_call: Arc<Mutex<Option<NervousSystemError>>>,
}

/// Create a default mock driver.
impl Default for FakeDriver {
fn default() -> Self {
Self {
state: Arc::new(Mutex::new(Default::default())),
error_on_next_ledger_call: Arc::new(Mutex::new(None)),
}
}
}
Expand Down Expand Up @@ -181,19 +183,22 @@ impl FakeDriver {
pub fn get_fake_env(&self) -> Box<dyn Environment> {
Box::new(FakeDriver {
state: Arc::clone(&self.state),
error_on_next_ledger_call: Arc::clone(&self.error_on_next_ledger_call),
})
}

/// Constructs a `Ledger` that interacts with this driver.
pub fn get_fake_ledger(&self) -> Box<dyn IcpLedger> {
Box::new(FakeDriver {
state: Arc::clone(&self.state),
error_on_next_ledger_call: Arc::clone(&self.error_on_next_ledger_call),
})
}

pub fn get_fake_cmc(&self) -> Box<dyn CMC> {
Box::new(FakeDriver {
state: Arc::clone(&self.state),
error_on_next_ledger_call: Arc::clone(&self.error_on_next_ledger_call),
})
}

Expand Down Expand Up @@ -241,6 +246,13 @@ impl FakeDriver {
num_accounts
);
}

pub fn fail_next_ledger_call(&self) {
self.error_on_next_ledger_call
.lock()
.unwrap()
.replace(NervousSystemError::new());
}
}

#[async_trait]
Expand Down Expand Up @@ -276,6 +288,18 @@ impl IcpLedger for FakeDriver {
]))
);

if let Some(err) = self.error_on_next_ledger_call.lock().unwrap().take() {
println!("Failing the ledger transfer because we were instructed to fail the next ledger call");
tla_log_response!(
Destination::new("ledger"),
tla::TlaValue::Variant {
tag: "Fail".to_string(),
value: Box::new(tla::TlaValue::Constant("UNIT".to_string()))
}
);
return Err(err);
}

let accounts = &mut self.state.try_lock().unwrap().accounts;

let from_e8s = accounts
Expand All @@ -286,6 +310,13 @@ impl IcpLedger for FakeDriver {

if !is_minting_operation {
if *from_e8s < requested_e8s {
tla_log_response!(
Destination::new("ledger"),
tla::TlaValue::Variant {
tag: "Fail".to_string(),
value: Box::new(tla::TlaValue::Constant("UNIT".to_string()))
}
);
return Err(NervousSystemError::new_with_message(format!(
"Insufficient funds. Available {} requested {}",
*from_e8s, requested_e8s
Expand All @@ -307,15 +338,17 @@ impl IcpLedger for FakeDriver {
}

async fn total_supply(&self) -> Result<Tokens, NervousSystemError> {
if let Some(err) = self.error_on_next_ledger_call.lock().unwrap().take() {
return Err(err);
}

Ok(self.get_supply())
}

async fn account_balance(
&self,
account: AccountIdentifier,
) -> Result<Tokens, NervousSystemError> {
let accounts = &mut self.state.try_lock().unwrap().accounts;

tla_log_request!(
"WaitForBalanceQuery",
Destination::new("ledger"),
Expand All @@ -326,6 +359,19 @@ impl IcpLedger for FakeDriver {
)]))
);

if let Some(err) = self.error_on_next_ledger_call.lock().unwrap().take() {
tla_log_response!(
Destination::new("ledger"),
tla::TlaValue::Variant {
tag: "Fail".to_string(),
value: Box::new(tla::TlaValue::Constant("UNIT".to_string())),
}
);
return Err(err);
}

let accounts = &mut self.state.try_lock().unwrap().accounts;

let account_e8s = accounts.get(&account).unwrap_or(&0);
tla_log_response!(
Destination::new("ledger"),
Expand Down
121 changes: 121 additions & 0 deletions rs/nns/governance/tests/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6233,6 +6233,127 @@ fn test_neuron_spawn_with_subaccount() {
assert_eq!(child_neuron.maturity_e8s_equivalent, 0);
}

#[test]
fn test_maturity_correctly_reset_if_spawn_fails() {
let from = *TEST_NEURON_1_OWNER_PRINCIPAL;
// Compute the subaccount to which the transfer would have been made
let nonce = 1234u64;

let block_height = 543212234;
let dissolve_delay_seconds = MIN_DISSOLVE_DELAY_FOR_VOTE_ELIGIBILITY_SECONDS;
let neuron_stake_e8s = 1_000_000_000;

let (mut driver, mut gov, id, _) = governance_with_staked_neuron(
dissolve_delay_seconds,
neuron_stake_e8s,
block_height,
from,
nonce,
);
run_periodic_tasks_often_enough_to_update_maturity_modulation(&mut gov);

let now = driver.now();
assert_eq!(
gov.with_neuron(&id, |neuron| neuron
.get_neuron_info(gov.voting_power_economics(), now, *RANDOM_PRINCIPAL_ID)
.state())
.unwrap(),
NeuronState::NotDissolving
);

// Artificially set the neuron's maturity to sufficient value
let parent_maturity_e8s_equivalent: u64 = 123_456_789;
assert!(
parent_maturity_e8s_equivalent
> NetworkEconomics::with_default_values().neuron_minimum_stake_e8s
);
gov.with_neuron_mut(&id, |neuron| {
neuron.maturity_e8s_equivalent = parent_maturity_e8s_equivalent;
})
.expect("Neuron did not exist");

// Advance the time so that we can check that the spawned neuron has the age
// and the right creation timestamp
driver.advance_time_by(1);

// Nonce used for spawn (given as input).
let nonce_spawn = driver.random_u64().expect("Could not get random number");

let child_controller = *TEST_NEURON_2_OWNER_PRINCIPAL;

let child_nid = gov
.spawn_neuron(
&id,
&from,
&Spawn {
new_controller: Some(child_controller),
nonce: Some(nonce_spawn),
percentage_to_spawn: None,
},
)
.unwrap();

let creation_timestamp = driver.now();

// We should now have 2 neurons.
assert_eq!(gov.neuron_store.len(), 2);
// And we should have one ledger accounts.
driver.assert_num_neuron_accounts_exist(1);

// Running periodic tasks shouldn't cause the ICP to be minted.
gov.maybe_spawn_neurons().now_or_never().unwrap();
driver.assert_num_neuron_accounts_exist(1);

let parent_neuron = gov
.with_neuron(&id, |neuron| neuron.clone())
.expect("The parent neuron is missing");
// Maturity on the parent neuron should be reset.
assert_eq!(parent_neuron.maturity_e8s_equivalent, 0);

// Advance the time by one week, should cause the neuron's ICP
// to be minted.
driver.advance_time_by(7 * 86400);
driver.fail_next_ledger_call();

gov.maybe_spawn_neurons().now_or_never().unwrap();

//Driver should fail to finish spawning neurons now (i.e. minting) b/c ledger returns error

driver.assert_num_neuron_accounts_exist(1);

let child_neuron = gov
.get_full_neuron(&child_nid, &child_controller)
.expect("The child neuron is missing");

assert_eq!(child_neuron.created_timestamp_seconds, creation_timestamp);
assert_eq!(child_neuron.aging_since_timestamp_seconds, u64::MAX);
assert_eq!(child_neuron.spawn_at_timestamp_seconds, Some(1730737555));
assert_eq!(
child_neuron.dissolve_state,
Some(DissolveState::WhenDissolvedTimestampSeconds(
creation_timestamp
+ gov
.heap_data
.economics
.as_ref()
.unwrap()
.neuron_spawn_dissolve_delay_seconds
))
);
assert_eq!(child_neuron.kyc_verified, true);
assert_eq!(child_neuron.cached_neuron_stake_e8s, 0);
// We expect maturity modulation of 100 basis points, which is 1%, because of the
// result returned from FakeDriver when pretending to be CMC.
assert_eq!(
gov.heap_data
.cached_daily_maturity_modulation_basis_points
.unwrap(),
100
);
// The value here should be reset to the original value, without being modulated.
assert_eq!(child_neuron.maturity_e8s_equivalent, 123_456_789);
}

/// Checks that:
/// * Specifying a percentage_to_spawn different from 100 lead to the proper fractional maturity
/// to be spawned.
Expand Down

0 comments on commit 3f4397c

Please sign in to comment.