/ node / bft / ledger-service / src / ledger.rs
ledger.rs
  1  // Copyright (c) 2025 ADnet Contributors
  2  // This file is part of the AlphaOS library.
  3  
  4  // Licensed under the Apache License, Version 2.0 (the "License");
  5  // you may not use this file except in compliance with the License.
  6  // You may obtain a copy of the License at:
  7  
  8  // http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  // Unless required by applicable law or agreed to in writing, software
 11  // distributed under the License is distributed on an "AS IS" BASIS,
 12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  // See the License for the specific language governing permissions and
 14  // limitations under the License.
 15  
 16  use crate::{LedgerService, fmt_id, spawn_blocking};
 17  
 18  use alphaos_utilities::Stoppable;
 19  
 20  use alphavm::{
 21      ledger::{
 22          Block,
 23          Ledger,
 24          PendingBlock,
 25          Transaction,
 26          committee::Committee,
 27          narwhal::{BatchCertificate, Data, Subdag, Transmission, TransmissionID},
 28          puzzle::{Solution, SolutionID},
 29          store::ConsensusStorage,
 30      },
 31      prelude::{
 32          Address,
 33          ConsensusVersion,
 34          Field,
 35          FromBytes,
 36          Network,
 37          Result,
 38          bail,
 39          cfg_into_iter,
 40          consensus_config_value_by_version,
 41          deploy_compute_cost_in_microcredits,
 42          deployment_cost,
 43          execute_compute_cost_in_microcredits,
 44          execution_cost,
 45      },
 46  };
 47  
 48  use anyhow::ensure;
 49  use indexmap::IndexMap;
 50  #[cfg(feature = "locktick")]
 51  use locktick::parking_lot::RwLock;
 52  #[cfg(not(feature = "locktick"))]
 53  use parking_lot::RwLock;
 54  #[cfg(not(feature = "serial"))]
 55  use rayon::prelude::*;
 56  
 57  use std::{fmt, io::Read, ops::Range, sync::Arc};
 58  
 59  /// A core ledger service.
 60  #[allow(clippy::type_complexity)]
 61  pub struct CoreLedgerService<N: Network, C: ConsensusStorage<N>> {
 62      ledger: Ledger<N, C>,
 63      latest_leader: Arc<RwLock<Option<(u64, Address<N>)>>>,
 64      stoppable: Arc<dyn Stoppable>,
 65  }
 66  
 67  impl<N: Network, C: ConsensusStorage<N>> CoreLedgerService<N, C> {
 68      /// Initializes a new core ledger service.
 69      pub fn new(ledger: Ledger<N, C>, stoppable: Arc<dyn Stoppable>) -> Self {
 70          Self { ledger, latest_leader: Default::default(), stoppable }
 71      }
 72  }
 73  
 74  impl<N: Network, C: ConsensusStorage<N>> fmt::Debug for CoreLedgerService<N, C> {
 75      /// Implements a custom `fmt::Debug` for `CoreLedgerService`.
 76      fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 77          f.debug_struct("CoreLedgerService").field("current_committee", &self.current_committee()).finish()
 78      }
 79  }
 80  
 81  #[async_trait]
 82  impl<N: Network, C: ConsensusStorage<N>> LedgerService<N> for CoreLedgerService<N, C> {
 83      /// Returns the latest round in the ledger.
 84      fn latest_round(&self) -> u64 {
 85          self.ledger.latest_round()
 86      }
 87  
 88      /// Returns the latest block height in the ledger.
 89      fn latest_block_height(&self) -> u32 {
 90          self.ledger.latest_height()
 91      }
 92  
 93      /// Returns the latest block in the ledger.
 94      fn latest_block(&self) -> Block<N> {
 95          self.ledger.latest_block()
 96      }
 97  
 98      /// Returns the latest restrictions ID in the ledger.
 99      fn latest_restrictions_id(&self) -> Field<N> {
100          self.ledger.vm().restrictions().restrictions_id()
101      }
102  
103      /// Returns the latest cached leader and its associated round.
104      fn latest_leader(&self) -> Option<(u64, Address<N>)> {
105          *self.latest_leader.read()
106      }
107  
108      /// Updates the latest cached leader and its associated round.
109      fn update_latest_leader(&self, round: u64, leader: Address<N>) {
110          *self.latest_leader.write() = Some((round, leader));
111      }
112  
113      /// Returns `true` if the given block height exists in the ledger.
114      fn contains_block_height(&self, height: u32) -> bool {
115          self.ledger.contains_block_height(height).unwrap_or(false)
116      }
117  
118      /// Returns the block height for the given block hash, if it exists.
119      fn get_block_height(&self, hash: &N::BlockHash) -> Result<u32> {
120          self.ledger.get_height(hash)
121      }
122  
123      /// Returns the block hash for the given block height, if it exists.
124      fn get_block_hash(&self, height: u32) -> Result<N::BlockHash> {
125          self.ledger.get_hash(height)
126      }
127  
128      /// Returns the block round for the given block height, if it exists.
129      fn get_block_round(&self, height: u32) -> Result<u64> {
130          self.ledger.get_block(height).map(|block| block.round())
131      }
132  
133      /// Returns the block for the given block height.
134      fn get_block(&self, height: u32) -> Result<Block<N>> {
135          self.ledger.get_block(height)
136      }
137  
138      /// Returns the blocks in the given block range.
139      /// The range is inclusive of the start and exclusive of the end.
140      fn get_blocks(&self, heights: Range<u32>) -> Result<Vec<Block<N>>> {
141          cfg_into_iter!(heights).map(|height| self.get_block(height)).collect()
142      }
143  
144      /// Returns the solution for the given solution ID.
145      fn get_solution(&self, solution_id: &SolutionID<N>) -> Result<Solution<N>> {
146          self.ledger.get_solution(solution_id)
147      }
148  
149      /// Returns the unconfirmed transaction for the given transaction ID.
150      fn get_unconfirmed_transaction(&self, transaction_id: N::TransactionID) -> Result<Transaction<N>> {
151          self.ledger.get_unconfirmed_transaction(&transaction_id)
152      }
153  
154      /// Returns the batch certificate for the given batch certificate ID.
155      fn get_batch_certificate(&self, certificate_id: &Field<N>) -> Result<BatchCertificate<N>> {
156          match self.ledger.get_batch_certificate(certificate_id) {
157              Ok(Some(certificate)) => Ok(certificate),
158              Ok(None) => bail!("No batch certificate found for certificate ID {certificate_id} in the ledger"),
159              Err(error) => Err(error),
160          }
161      }
162  
163      /// Returns the current committee.
164      fn current_committee(&self) -> Result<Committee<N>> {
165          self.ledger.latest_committee()
166      }
167  
168      /// Returns the committee for the given round.
169      fn get_committee_for_round(&self, round: u64) -> Result<Committee<N>> {
170          match self.ledger.get_committee_for_round(round)? {
171              Some(committee) => Ok(committee),
172              None => bail!("No committee found for round {round} in the ledger"),
173          }
174      }
175  
176      /// Returns the committee lookback for the given round.
177      fn get_committee_lookback_for_round(&self, round: u64) -> Result<Committee<N>> {
178          // Get the round number for the previous committee. Note, we subtract 2 from odd rounds,
179          // because committees are updated in even rounds.
180          let previous_round = match round % 2 == 0 {
181              true => round.saturating_sub(1),
182              false => round.saturating_sub(2),
183          };
184  
185          // Get the committee lookback round.
186          let committee_lookback_round = previous_round.saturating_sub(Committee::<N>::COMMITTEE_LOOKBACK_RANGE);
187  
188          // Retrieve the committee for the committee lookback round.
189          self.get_committee_for_round(committee_lookback_round)
190      }
191  
192      /// Returns `true` if the ledger contains the given certificate ID in block history.
193      fn contains_certificate(&self, certificate_id: &Field<N>) -> Result<bool> {
194          self.ledger.contains_certificate(certificate_id)
195      }
196  
197      /// Returns `true` if the transmission exists in the ledger.
198      fn contains_transmission(&self, transmission_id: &TransmissionID<N>) -> Result<bool> {
199          match transmission_id {
200              TransmissionID::Ratification => Ok(false),
201              TransmissionID::Solution(solution_id, _) => self.ledger.contains_solution_id(solution_id),
202              TransmissionID::Transaction(transaction_id, _) => self.ledger.contains_transaction_id(transaction_id),
203          }
204      }
205  
206      /// Ensures that the given transmission is not a fee and matches the given transmission ID.
207      fn ensure_transmission_is_well_formed(
208          &self,
209          transmission_id: TransmissionID<N>,
210          transmission: &mut Transmission<N>,
211      ) -> Result<()> {
212          match (transmission_id, transmission) {
213              (TransmissionID::Ratification, Transmission::Ratification) => {
214                  bail!("Ratification transmissions are currently not supported.")
215              }
216              (
217                  TransmissionID::Transaction(expected_transaction_id, expected_checksum),
218                  Transmission::Transaction(transaction_data),
219              ) => {
220                  // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
221                  let transaction = match transaction_data.clone() {
222                      Data::Object(transaction) => transaction,
223                      Data::Buffer(bytes) => Transaction::<N>::read_le(&mut bytes.take(N::MAX_TRANSACTION_SIZE as u64))?,
224                  };
225                  // Ensure the transaction ID matches the expected transaction ID.
226                  if transaction.id() != expected_transaction_id {
227                      bail!(
228                          "Received mismatching transaction ID - expected {}, found {}",
229                          fmt_id(expected_transaction_id),
230                          fmt_id(transaction.id()),
231                      );
232                  }
233  
234                  // Ensure the transmission checksum matches the expected checksum.
235                  let checksum = transaction_data.to_checksum::<N>()?;
236                  if checksum != expected_checksum {
237                      bail!(
238                          "Received mismatching checksum for transaction {} - expected {expected_checksum} but found {checksum}",
239                          fmt_id(expected_transaction_id)
240                      );
241                  }
242  
243                  // Ensure the transaction is not a fee transaction.
244                  if transaction.is_fee() {
245                      bail!("Received a fee transaction in a transmission");
246                  }
247  
248                  // Update the transmission with the deserialized transaction.
249                  *transaction_data = Data::Object(transaction);
250              }
251              (
252                  TransmissionID::Solution(expected_solution_id, expected_checksum),
253                  Transmission::Solution(solution_data),
254              ) => {
255                  match solution_data.clone().deserialize_blocking() {
256                      Ok(solution) => {
257                          if solution.id() != expected_solution_id {
258                              bail!(
259                                  "Received mismatching solution ID - expected {}, found {}",
260                                  fmt_id(expected_solution_id),
261                                  fmt_id(solution.id()),
262                              );
263                          }
264  
265                          // Ensure the transmission checksum matches the expected checksum.
266                          let checksum = solution_data.to_checksum::<N>()?;
267                          if checksum != expected_checksum {
268                              bail!(
269                                  "Received mismatching checksum for solution {} - expected {expected_checksum} but found {checksum}",
270                                  fmt_id(expected_solution_id)
271                              );
272                          }
273  
274                          // Update the transmission with the deserialized solution.
275                          *solution_data = Data::Object(solution);
276                      }
277                      Err(err) => {
278                          bail!("Failed to deserialize solution: {err}");
279                      }
280                  }
281              }
282              _ => {
283                  bail!("Mismatching `(transmission_id, transmission)` pair");
284              }
285          }
286  
287          Ok(())
288      }
289  
290      /// Checks the given solution is well-formed.
291      async fn check_solution_basic(&self, solution_id: SolutionID<N>, solution: Data<Solution<N>>) -> Result<()> {
292          // Deserialize the solution.
293          let solution = spawn_blocking!(solution.deserialize_blocking())?;
294          // Ensure the solution ID matches in the solution.
295          if solution_id != solution.id() {
296              bail!("Invalid solution - expected {solution_id}, found {}", solution.id());
297          }
298  
299          // Check if the prover has reached their solution limit.
300          // While snarkVM will ultimately abort any excess solutions for safety, performing this check
301          // here prevents the to-be aborted solutions from propagating through the network.
302          let prover_address = solution.address();
303          if self.ledger.is_solution_limit_reached(&prover_address, 0) {
304              bail!(
305                  "Invalid Solution '{}' - Prover '{prover_address}' has reached their solution limit for the current epoch",
306                  fmt_id(solution.id())
307              );
308          }
309          // Compute the current epoch hash.
310          let epoch_hash = self.ledger.latest_epoch_hash()?;
311          // Retrieve the current proof target.
312          let proof_target = self.ledger.latest_proof_target();
313  
314          // Ensure that the solution is valid for the given epoch.
315          let puzzle = self.ledger.puzzle().clone();
316          match spawn_blocking!(puzzle.check_solution(&solution, epoch_hash, proof_target)) {
317              Ok(()) => Ok(()),
318              Err(e) => bail!("Invalid solution '{}' for the current epoch - {e}", fmt_id(solution_id)),
319          }
320      }
321  
322      /// Checks the given transaction is well-formed and unique.
323      async fn check_transaction_basic(
324          &self,
325          transaction_id: N::TransactionID,
326          transaction: Transaction<N>,
327      ) -> Result<()> {
328          // Ensure the transaction ID matches in the transaction.
329          if transaction_id != transaction.id() {
330              bail!("Invalid transaction - expected {transaction_id}, found {}", transaction.id());
331          }
332          // Check if the transmission is a fee transaction.
333          if transaction.is_fee() {
334              bail!("Invalid transaction - 'Transaction::fee' type is not valid at this stage ({})", transaction.id());
335          }
336          // Check the transaction is well-formed.
337          let ledger = self.ledger.clone();
338          spawn_blocking!(ledger.check_transaction_basic(&transaction, None, &mut rand::thread_rng()))
339      }
340  
341      fn check_block_subdag(&self, block: Block<N>, prefix: &[PendingBlock<N>]) -> Result<PendingBlock<N>> {
342          Ok(self.ledger.check_block_subdag(block, prefix)?)
343      }
344  
345      fn check_block_content(&self, block: PendingBlock<N>) -> Result<Block<N>> {
346          Ok(self.ledger.check_block_content(block, &mut rand::thread_rng())?)
347      }
348  
349      /// Checks the given block is valid next block.
350      fn check_next_block(&self, block: &Block<N>) -> Result<()> {
351          self.ledger.check_next_block(block, &mut rand::thread_rng())
352      }
353  
354      /// Returns a candidate for the next block in the ledger, using a committed subdag and its transmissions.
355      #[cfg(feature = "ledger-write")]
356      fn prepare_advance_to_next_quorum_block(
357          &self,
358          subdag: Subdag<N>,
359          transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
360      ) -> Result<Block<N>> {
361          self.ledger.prepare_advance_to_next_quorum_block(subdag, transmissions, &mut rand::thread_rng())
362      }
363  
364      /// Adds the given block as the next block in the ledger.
365      #[cfg(feature = "ledger-write")]
366      fn advance_to_next_block(&self, block: &Block<N>) -> Result<()> {
367          // If the Ctrl-C handler registered the signal, then skip advancing to the next block.
368          if self.stoppable.is_stopped() {
369              bail!("Skipping advancing to block {} - The node is shutting down", block.height());
370          }
371          // Advance to the next block.
372          self.ledger.advance_to_next_block(block)?;
373          // Update BFT metrics.
374          #[cfg(feature = "metrics")]
375          {
376              let num_sol = block.solutions().len();
377              let num_tx = block.transactions().len();
378  
379              metrics::gauge(metrics::bft::HEIGHT, block.height() as f64);
380              metrics::gauge(metrics::bft::LAST_COMMITTED_ROUND, block.round() as f64);
381              metrics::increment_gauge(metrics::blocks::SOLUTIONS, num_sol as f64);
382              metrics::increment_gauge(metrics::blocks::TRANSACTIONS, num_tx as f64);
383              metrics::update_block_metrics(block);
384          }
385  
386          tracing::info!("\n\nAdvanced to block {} at round {} - {}\n", block.height(), block.round(), block.hash());
387          Ok(())
388      }
389  
390      /// Returns the spend for a transaction in microcredits.
391      /// This is used to limit the amount of compute in the block generation hot
392      /// path. This does NOT represent the full costs which a user has to pay.
393      fn transaction_spend_in_microcredits(
394          &self,
395          transaction: &Transaction<N>,
396          consensus_version: ConsensusVersion,
397      ) -> Result<u64> {
398          let transaction_spend_limit =
399              consensus_config_value_by_version!(N, TRANSACTION_SPEND_LIMIT, consensus_version).unwrap();
400          let id = transaction.id();
401          match transaction {
402              Transaction::Deploy(_, _, _, deployment, _) => {
403                  let (_, cost_details) =
404                      deployment_cost(&self.ledger.vm().process().read(), deployment, consensus_version)?;
405                  let compute_spend = deploy_compute_cost_in_microcredits(cost_details, consensus_version)?;
406                  ensure!(
407                      compute_spend <= transaction_spend_limit,
408                      "Transaction '{id}' exceeds the transaction spend limit with compute_spend: '{compute_spend}'"
409                  );
410                  Ok(compute_spend)
411              }
412              Transaction::Execute(_, _, execution, _) => {
413                  let (_, cost_details) =
414                      execution_cost(&self.ledger.vm().process().read(), execution, consensus_version)?;
415                  let compute_spend = execute_compute_cost_in_microcredits(cost_details, consensus_version)?;
416                  if consensus_version >= ConsensusVersion::V11 {
417                      // From V11, add this check for consistency with our deployment checks.
418                      ensure!(
419                          compute_spend <= transaction_spend_limit,
420                          "Transaction '{id}' exceeds the transaction spend limit with compute_spend: '{compute_spend}'"
421                      );
422                  }
423                  Ok(compute_spend)
424              }
425              Transaction::Fee(..) => {
426                  bail!("Fee transactions are internal to the VM, transaction {id} is invalid.")
427              }
428          }
429      }
430  }