Skip to content

Commit

Permalink
Remove accounts with zero balance from storage (#2959)
Browse files Browse the repository at this point in the history
* Make `open_chain` method asynchronous

Prepare to be able to call a new asynchronous `debit` method.

* Refactor to add a `debit` helper method

Move common code into the new method.

* Remove accounts with zero balances

Prune storage a little once an account becomes empty.

* Update tests to not have empty accounts

Update the computation of the state execution hash, considering that now
empty accounts aren't included in it.

* Move system unit tests to a separate file

Follow the convention used for other unit tests.

* Test if empty accounts are pruned

Ensure that they are removed from the system execution state view.
  • Loading branch information
jvff authored Nov 26, 2024
1 parent 34c4c5c commit 5e00b93
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 134 deletions.
4 changes: 2 additions & 2 deletions linera-core/src/unit_tests/worker_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2219,7 +2219,7 @@ where
Vec::new(),
&committee,
Amount::ZERO,
BTreeMap::from_iter([(sender, Amount::from_tokens(0))]),
BTreeMap::new(),
&worker,
Some(&certificate1),
)
Expand Down Expand Up @@ -2313,7 +2313,7 @@ where
}],
&committee,
Amount::ZERO,
BTreeMap::from_iter([(sender, Amount::from_tokens(0))]),
BTreeMap::new(),
&worker,
Some(&certificate2),
)
Expand Down
2 changes: 1 addition & 1 deletion linera-execution/src/execution_state_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ where
balance,
application_permissions,
};
let messages = self.system.open_chain(config, next_message_id)?;
let messages = self.system.open_chain(config, next_message_id).await?;
callback.respond(messages)
}

Expand Down
169 changes: 38 additions & 131 deletions linera-execution/src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
// Copyright (c) Zefchain Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

#[cfg(test)]
#[path = "./unit_tests/system_tests.rs"]
mod tests;

#[cfg(with_metrics)]
use std::sync::LazyLock;
use std::{
Expand Down Expand Up @@ -449,7 +453,7 @@ where
match operation {
OpenChain(config) => {
let next_message_id = context.next_message_id(txn_tracker.next_message_index());
let messages = self.open_chain(config, next_message_id)?;
let messages = self.open_chain(config, next_message_id).await?;
outcome.messages.extend(messages);
#[cfg(with_metrics)]
OPEN_CHAIN_COUNT.with_label_values(&[]).inc();
Expand Down Expand Up @@ -697,13 +701,7 @@ where
amount > Amount::ZERO,
SystemExecutionError::IncorrectTransferAmount
);
let balance = match &owner {
Some(owner) => self.balances.get_mut_or_default(owner).await?,
None => self.balance.get_mut(),
};
balance
.try_sub_assign(amount)
.map_err(|_| SystemExecutionError::InsufficientFunding { balance: *balance })?;
self.debit(owner.as_ref(), amount).await?;
match recipient {
Recipient::Account(account) => {
let message = RawOutgoingMessage {
Expand Down Expand Up @@ -754,6 +752,35 @@ where
})
}

/// Debits an [`Amount`] of tokens from an account's balance.
async fn debit(
&mut self,
account: Option<&Owner>,
amount: Amount,
) -> Result<(), SystemExecutionError> {
let balance = if let Some(owner) = account {
self.balances.get_mut(owner).await?.ok_or_else(|| {
SystemExecutionError::InsufficientFunding {
balance: Amount::ZERO,
}
})?
} else {
self.balance.get_mut()
};

balance
.try_sub_assign(amount)
.map_err(|_| SystemExecutionError::InsufficientFunding { balance: *balance })?;

if let Some(owner) = account {
if balance.is_zero() {
self.balances.remove(owner)?;
}
}

Ok(())
}

/// Executes a cross-chain message that represents the recipient's side of an operation.
pub async fn execute_message(
&mut self,
Expand Down Expand Up @@ -791,10 +818,7 @@ where
SystemExecutionError::UnauthenticatedClaimOwner
);

let balance = self.balances.get_mut_or_default(&owner).await?;
balance
.try_sub_assign(amount)
.map_err(|_| SystemExecutionError::InsufficientFunding { balance: *balance })?;
self.debit(Some(&owner), amount).await?;
match recipient {
Recipient::Account(account) => {
let message = RawOutgoingMessage {
Expand Down Expand Up @@ -911,7 +935,7 @@ where

/// Returns the messages to open a new chain, and subtracts the new chain's balance
/// from this chain's.
pub fn open_chain(
pub async fn open_chain(
&mut self,
config: OpenChainConfig,
next_message_id: MessageId,
Expand All @@ -933,10 +957,7 @@ where
epoch: config.epoch,
}
);
let balance = self.balance.get_mut();
balance
.try_sub_assign(config.balance)
.map_err(|_| SystemExecutionError::InsufficientFunding { balance: *balance })?;
self.debit(None, config.balance).await?;
let open_chain_message = RawOutgoingMessage {
destination: Destination::Recipient(child_id),
authenticated: false,
Expand Down Expand Up @@ -1042,117 +1063,3 @@ where
}
}
}

#[cfg(test)]
mod tests {
use linera_base::{
data_types::{Blob, BlockHeight, Bytecode},
identifiers::ApplicationId,
};
use linera_views::context::MemoryContext;

use super::*;
use crate::{ExecutionOutcome, ExecutionStateView, TestExecutionRuntimeContext};

/// Returns an execution state view and a matching operation context, for epoch 1, with root
/// chain 0 as the admin ID and one empty committee.
async fn new_view_and_context() -> (
ExecutionStateView<MemoryContext<TestExecutionRuntimeContext>>,
OperationContext,
) {
let description = ChainDescription::Root(5);
let context = OperationContext {
chain_id: ChainId::from(description),
authenticated_signer: None,
authenticated_caller_id: None,
height: BlockHeight::from(7),
index: Some(2),
};
let state = SystemExecutionState {
description: Some(description),
epoch: Some(Epoch(1)),
admin_id: Some(ChainId::root(0)),
committees: BTreeMap::new(),
..SystemExecutionState::default()
};
let view = state.into_view().await;
(view, context)
}

#[tokio::test]
async fn application_message_index() -> anyhow::Result<()> {
let (mut view, context) = new_view_and_context().await;
let contract = Bytecode::new(b"contract".into());
let service = Bytecode::new(b"service".into());
let contract_blob = Blob::new_contract_bytecode(contract.compress());
let service_blob = Blob::new_service_bytecode(service.compress());
let bytecode_id = BytecodeId::new(contract_blob.id().hash, service_blob.id().hash);

let operation = SystemOperation::CreateApplication {
bytecode_id,
parameters: vec![],
instantiation_argument: vec![],
required_application_ids: vec![],
};
let mut txn_tracker = TransactionTracker::default();
view.context()
.extra()
.add_blobs([contract_blob, service_blob])
.await?;
let new_application = view
.system
.execute_operation(context, operation, &mut txn_tracker)
.await?;
let [ExecutionOutcome::System(result)] = &txn_tracker.destructure()?.0[..] else {
panic!("Unexpected outcome");
};
assert_eq!(
result.messages[CREATE_APPLICATION_MESSAGE_INDEX as usize].message,
SystemMessage::ApplicationCreated
);
let creation = MessageId {
chain_id: context.chain_id,
height: context.height,
index: CREATE_APPLICATION_MESSAGE_INDEX,
};
let id = ApplicationId {
bytecode_id,
creation,
};
assert_eq!(new_application, Some((id, vec![])));

Ok(())
}

#[tokio::test]
async fn open_chain_message_index() {
let (mut view, context) = new_view_and_context().await;
let epoch = view.system.epoch.get().unwrap();
let admin_id = view.system.admin_id.get().unwrap();
let committees = view.system.committees.get().clone();
let ownership = ChainOwnership::single(PublicKey::test_key(0));
let config = OpenChainConfig {
ownership,
committees,
epoch,
admin_id,
balance: Amount::ZERO,
application_permissions: Default::default(),
};
let mut txn_tracker = TransactionTracker::default();
let operation = SystemOperation::OpenChain(config.clone());
let new_application = view
.system
.execute_operation(context, operation, &mut txn_tracker)
.await
.unwrap();
assert_eq!(new_application, None);
let [ExecutionOutcome::System(result)] = &txn_tracker.destructure().unwrap().0[..] else {
panic!("Unexpected outcome");
};
assert_eq!(
result.messages[OPEN_CHAIN_MESSAGE_INDEX as usize].message,
SystemMessage::OpenChain(config)
);
}
}
134 changes: 134 additions & 0 deletions linera-execution/src/unit_tests/system_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (c) Zefchain Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use linera_base::{
data_types::{Blob, BlockHeight, Bytecode},
identifiers::ApplicationId,
};
use linera_views::context::MemoryContext;

use super::*;
use crate::{ExecutionOutcome, ExecutionStateView, TestExecutionRuntimeContext};

/// Returns an execution state view and a matching operation context, for epoch 1, with root
/// chain 0 as the admin ID and one empty committee.
async fn new_view_and_context() -> (
ExecutionStateView<MemoryContext<TestExecutionRuntimeContext>>,
OperationContext,
) {
let description = ChainDescription::Root(5);
let context = OperationContext {
chain_id: ChainId::from(description),
authenticated_signer: None,
authenticated_caller_id: None,
height: BlockHeight::from(7),
index: Some(2),
};
let state = SystemExecutionState {
description: Some(description),
epoch: Some(Epoch(1)),
admin_id: Some(ChainId::root(0)),
committees: BTreeMap::new(),
..SystemExecutionState::default()
};
let view = state.into_view().await;
(view, context)
}

#[tokio::test]
async fn application_message_index() -> anyhow::Result<()> {
let (mut view, context) = new_view_and_context().await;
let contract = Bytecode::new(b"contract".into());
let service = Bytecode::new(b"service".into());
let contract_blob = Blob::new_contract_bytecode(contract.compress());
let service_blob = Blob::new_service_bytecode(service.compress());
let bytecode_id = BytecodeId::new(contract_blob.id().hash, service_blob.id().hash);

let operation = SystemOperation::CreateApplication {
bytecode_id,
parameters: vec![],
instantiation_argument: vec![],
required_application_ids: vec![],
};
let mut txn_tracker = TransactionTracker::default();
view.context()
.extra()
.add_blobs([contract_blob, service_blob])
.await?;
let new_application = view
.system
.execute_operation(context, operation, &mut txn_tracker)
.await?;
let [ExecutionOutcome::System(result)] = &txn_tracker.destructure()?.0[..] else {
panic!("Unexpected outcome");
};
assert_eq!(
result.messages[CREATE_APPLICATION_MESSAGE_INDEX as usize].message,
SystemMessage::ApplicationCreated
);
let creation = MessageId {
chain_id: context.chain_id,
height: context.height,
index: CREATE_APPLICATION_MESSAGE_INDEX,
};
let id = ApplicationId {
bytecode_id,
creation,
};
assert_eq!(new_application, Some((id, vec![])));

Ok(())
}

#[tokio::test]
async fn open_chain_message_index() {
let (mut view, context) = new_view_and_context().await;
let epoch = view.system.epoch.get().unwrap();
let admin_id = view.system.admin_id.get().unwrap();
let committees = view.system.committees.get().clone();
let ownership = ChainOwnership::single(PublicKey::test_key(0));
let config = OpenChainConfig {
ownership,
committees,
epoch,
admin_id,
balance: Amount::ZERO,
application_permissions: Default::default(),
};
let mut txn_tracker = TransactionTracker::default();
let operation = SystemOperation::OpenChain(config.clone());
let new_application = view
.system
.execute_operation(context, operation, &mut txn_tracker)
.await
.unwrap();
assert_eq!(new_application, None);
let [ExecutionOutcome::System(result)] = &txn_tracker.destructure().unwrap().0[..] else {
panic!("Unexpected outcome");
};
assert_eq!(
result.messages[OPEN_CHAIN_MESSAGE_INDEX as usize].message,
SystemMessage::OpenChain(config)
);
}

/// Tests if an account is removed from storage if it is drained.
#[tokio::test]
async fn empty_accounts_are_removed() -> anyhow::Result<()> {
let owner = Owner(CryptoHash::test_hash("account owner"));
let amount = Amount::from_tokens(99);

let mut view = SystemExecutionState {
description: Some(ChainDescription::Root(0)),
balances: BTreeMap::from([(owner, amount)]),
..SystemExecutionState::default()
}
.into_view()
.await;

view.system.debit(Some(&owner), amount).await?;

assert!(view.system.balances.indices().await?.is_empty());

Ok(())
}

0 comments on commit 5e00b93

Please sign in to comment.