test_vm_execute_and_finalize.rs
1 // Copyright (c) 2019-2025 Alpha-Delta Network Inc. 2 // This file is part of the deltavm 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 mod utilities; 17 18 use alphastd::StorageMode; 19 use deltavm_console::{ 20 account::{PrivateKey, ViewKey}, 21 network::prelude::*, 22 program::{Entry, Identifier, Literal, Plaintext, ProgramID, Record, U64, Value}, 23 types::{Boolean, Field}, 24 }; 25 use deltavm_ledger_block::{ 26 Block, 27 ConfirmedTransaction, 28 Header, 29 Metadata, 30 Ratifications, 31 Transaction, 32 Transactions, 33 Transition, 34 }; 35 use deltavm_ledger_store::{ConsensusStorage, ConsensusStore}; 36 use deltavm_synthesizer::{Authorization, VM, program::FinalizeOperation}; 37 use deltavm_synthesizer_process::{execution_cost, execution_cost_for_authorization}; 38 use deltavm_synthesizer_program::FinalizeGlobalState; 39 40 use deltavm_console::account::Address; 41 use anyhow::Result; 42 use indexmap::IndexMap; 43 use utilities::*; 44 45 #[cfg(not(feature = "rocks"))] 46 type LedgerType = deltavm_ledger_store::helpers::memory::ConsensusMemory<CurrentNetwork>; 47 #[cfg(feature = "rocks")] 48 type LedgerType = deltavm_ledger_store::helpers::rocksdb::ConsensusDB<CurrentNetwork>; 49 50 #[test] 51 fn test_vm_execute_and_finalize() { 52 // Load the tests. 53 let tests = 54 load_tests::<_, ProgramTest>("./tests/vm/execute_and_finalize", "./expectations/vm/execute_and_finalize"); 55 56 // Run each test and compare it against its corresponding expectation. 57 tests.iter().for_each(|test| { 58 // Run the test. 59 let output = run_test(test); 60 // Check against the expected output. 61 test.check(&output).unwrap(); 62 // Save the output. 63 test.save(&output).unwrap(); 64 }); 65 } 66 67 // A helper function to run the test and extract the outputs as YAML, to be compared against the expectation. 68 fn run_test(test: &ProgramTest) -> serde_yaml::Mapping { 69 // Initialize the RNG. 70 let rng = &mut match test.randomness() { 71 None => TestRng::fixed(123456789), 72 Some(randomness) => TestRng::fixed(randomness), 73 }; 74 75 // Initialize a private key. 76 let genesis_private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap(); 77 78 // Initialize the VM. 79 let (vm, _) = initialize_vm(&genesis_private_key, test.start_height(), rng); 80 81 // Fund the additional keys. 82 for key in test.keys() { 83 // Transfer 1_000_000_000_000 84 let transaction = vm 85 .execute( 86 &genesis_private_key, 87 ("credits.delta", "transfer_public"), 88 vec![ 89 Value::Plaintext(Plaintext::from(Literal::Address(Address::try_from(key).unwrap()))), 90 Value::Plaintext(Plaintext::from(Literal::U64(U64::new(1_000_000_000_000)))), 91 ] 92 .iter(), 93 None, 94 0, 95 None, 96 rng, 97 ) 98 .unwrap(); 99 let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; 100 let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm 101 .speculate( 102 construct_finalize_global_state(&vm, time_since_last_block), 103 time_since_last_block, 104 Some(0u64), 105 vec![], 106 &None.into(), 107 [transaction].iter(), 108 rng, 109 ) 110 .unwrap(); 111 assert!(aborted_transaction_ids.is_empty()); 112 113 let block = construct_next_block( 114 &vm, 115 time_since_last_block, 116 &genesis_private_key, 117 ratifications, 118 transactions, 119 aborted_transaction_ids, 120 ratified_finalize_operations, 121 rng, 122 ); 123 vm.add_next_block(&block.unwrap()).unwrap(); 124 } 125 126 // Deploy the programs. 127 for program in test.programs() { 128 let transaction = match vm.deploy(&genesis_private_key, program, None, 0, None, rng) { 129 Ok(transaction) => transaction, 130 Err(error) => { 131 let mut output = serde_yaml::Mapping::new(); 132 output.insert( 133 serde_yaml::Value::String("errors".to_string()), 134 serde_yaml::Value::Sequence(vec![serde_yaml::Value::String(format!( 135 "Failed to run `VM::deploy for program {}: {}", 136 program.id(), 137 error 138 ))]), 139 ); 140 output 141 .insert(serde_yaml::Value::String("outputs".to_string()), serde_yaml::Value::Sequence(Vec::new())); 142 return output; 143 } 144 }; 145 146 let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; 147 let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm 148 .speculate( 149 construct_finalize_global_state(&vm, time_since_last_block), 150 time_since_last_block, 151 Some(0u64), 152 vec![], 153 &None.into(), 154 [transaction].iter(), 155 rng, 156 ) 157 .unwrap(); 158 assert!(aborted_transaction_ids.is_empty()); 159 160 let block = construct_next_block( 161 &vm, 162 time_since_last_block, 163 &genesis_private_key, 164 ratifications, 165 transactions, 166 aborted_transaction_ids, 167 ratified_finalize_operations, 168 rng, 169 ) 170 .unwrap(); 171 vm.add_next_block(&block).unwrap(); 172 } 173 174 // Run each test case, aggregating the errors, outputs, and additional information. 175 let mut outputs = Vec::with_capacity(test.cases().len()); 176 let mut additional = Vec::with_capacity(test.cases().len()); 177 178 for value in test.cases() { 179 // TODO: Dedup from other integration tests. 180 // Extract the function name, inputs, and optional private key. 181 let value = value.as_mapping().expect("expected mapping for test case"); 182 let program_id = ProgramID::<CurrentNetwork>::from_str( 183 value 184 .get("program") 185 .expect("expected program name for test case") 186 .as_str() 187 .expect("expected string for program name"), 188 ) 189 .expect("unable to parse program name"); 190 let function_name = Identifier::<CurrentNetwork>::from_str( 191 value 192 .get("function") 193 .expect("expected function name for test case") 194 .as_str() 195 .expect("expected string for function name"), 196 ) 197 .expect("unable to parse function name"); 198 let inputs = value 199 .get("inputs") 200 .expect("expected inputs for test case") 201 .as_sequence() 202 .expect("expected sequence for inputs") 203 .iter() 204 .map(|input| match &input { 205 serde_yaml::Value::Bool(bool) => Value::<CurrentNetwork>::from(Literal::Boolean(Boolean::new(*bool))), 206 _ => Value::<CurrentNetwork>::from_str(input.as_str().expect("expected string for input")) 207 .expect("unable to parse input"), 208 }) 209 .collect_vec(); 210 // TODO: Support fee records for custom private keys. 211 let private_key = match value.get("private_key") { 212 Some(private_key) => { 213 PrivateKey::<CurrentNetwork>::from_str(private_key.as_str().expect("expected string for private key")) 214 .expect("unable to parse private key") 215 } 216 None => genesis_private_key, 217 }; 218 219 // A helper function to run the test and extract the outputs as YAML, to be compared against the expectation. 220 let mut run_test = || -> (serde_yaml::Value, serde_yaml::Value) { 221 // Create a mapping to store the result of the test. 222 let mut result = serde_yaml::Mapping::new(); 223 // Create a mapping to store the other items. 224 let mut other = serde_yaml::Mapping::new(); 225 226 // Execute the function, extracting the transaction. 227 let transaction = 228 match vm.execute(&private_key, (program_id, function_name), inputs.iter(), None, 0u64, None, rng) { 229 Ok(transaction) => transaction, 230 // If the execution fails, return the error. 231 Err(err) => { 232 result.insert( 233 serde_yaml::Value::String("execute".to_string()), 234 serde_yaml::Value::String(err.to_string()), 235 ); 236 return (serde_yaml::Value::Mapping(result), serde_yaml::Value::Mapping(Default::default())); 237 } 238 }; 239 240 // Test cost computation for Authorization 241 if transaction.is_execute() { 242 let consensus_version = 243 CurrentNetwork::CONSENSUS_VERSION(vm.block_store().current_block_height()).unwrap(); 244 245 if consensus_version >= ConsensusVersion::V4 { 246 let execution = transaction.execution().unwrap(); 247 248 let actual_cost = execution_cost(&vm.process().read(), execution, consensus_version).unwrap(); 249 250 let authorization = 251 Authorization::from_unchecked((vec![], execution.transitions().cloned().collect())); 252 let expected_cost = 253 execution_cost_for_authorization(&vm.process().read(), &authorization, consensus_version) 254 .unwrap(); 255 256 assert_eq!(actual_cost, expected_cost); 257 } 258 } 259 260 // Attempt to verify the transaction. 261 let verified = vm.check_transaction(&transaction, None, rng).is_ok(); 262 // Store the verification result. 263 result.insert(serde_yaml::Value::String("verified".to_string()), serde_yaml::Value::Bool(verified)); 264 265 // For each root transition in the transaction, extract the transition outputs and the inputs for finalize. 266 let mut execute = serde_yaml::Mapping::new(); 267 // Store the outputs for child transitions separately, so that they are not checked for consistency. 268 let mut child_outputs = serde_yaml::Mapping::new(); 269 270 let transitions = transaction.transitions().collect::<Vec<_>>(); 271 for transition in transitions.iter() { 272 let mut transition_output = serde_yaml::Mapping::new(); 273 let outputs = transition 274 .outputs() 275 .iter() 276 .map(|output| serde_yaml::Value::String(output.to_string())) 277 .collect::<Vec<_>>(); 278 transition_output 279 .insert(serde_yaml::Value::String("outputs".to_string()), serde_yaml::Value::Sequence(outputs)); 280 281 // If this is the last transition, add the outputs to the `execute` mapping. 282 if transition.program_id() == &program_id && transition.function_name() == &function_name { 283 execute.insert( 284 serde_yaml::Value::String(format!( 285 "{}/{}", 286 transition.program_id(), 287 transition.function_name() 288 )), 289 serde_yaml::Value::Mapping(transition_output), 290 ); 291 } 292 // Otherwise, add the outputs to the `child_outputs` mapping. 293 // This is done to avoid checking the sub-transitions for consistency (since they change every execution). 294 else { 295 child_outputs.insert( 296 serde_yaml::Value::String(format!( 297 "{}/{}", 298 transition.program_id(), 299 transition.function_name() 300 )), 301 serde_yaml::Value::Mapping(transition_output), 302 ); 303 } 304 } 305 306 // Add the `execute` mapping to `result` mapping. 307 result.insert(serde_yaml::Value::String("execute".to_string()), serde_yaml::Value::Mapping(execute)); 308 // Add the child outputs to the `other` mapping. 309 other.insert( 310 serde_yaml::Value::String("child_outputs".to_string()), 311 serde_yaml::Value::Mapping(child_outputs), 312 ); 313 314 // Speculate on the ratifications, solutions, and transaction. 315 let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; 316 let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = match vm 317 .speculate( 318 construct_finalize_global_state(&vm, time_since_last_block), 319 time_since_last_block, 320 Some(0u64), 321 vec![], 322 &None.into(), 323 [transaction].iter(), 324 rng, 325 ) { 326 Ok((ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations)) => { 327 result.insert( 328 serde_yaml::Value::String("speculate".to_string()), 329 serde_yaml::Value::String(match transactions.iter().next().unwrap() { 330 ConfirmedTransaction::AcceptedExecute(_, _, _) => "the execution was accepted".to_string(), 331 ConfirmedTransaction::RejectedExecute(_, _, _, _) => { 332 "the execution was rejected".to_string() 333 } 334 ConfirmedTransaction::AcceptedDeploy(_, _, _) 335 | ConfirmedTransaction::RejectedDeploy(_, _, _, _) => { 336 unreachable!("unexpected deployment transaction") 337 } 338 }), 339 ); 340 (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) 341 } 342 Err(err) => { 343 result.insert( 344 serde_yaml::Value::String("speculate".to_string()), 345 serde_yaml::Value::String(err.to_string()), 346 ); 347 return (serde_yaml::Value::Mapping(result), serde_yaml::Value::Mapping(Default::default())); 348 } 349 }; 350 assert!(aborted_transaction_ids.is_empty()); 351 352 // Construct the next block. 353 let block = construct_next_block( 354 &vm, 355 time_since_last_block, 356 &private_key, 357 ratifications, 358 transactions, 359 aborted_transaction_ids, 360 ratified_finalize_operations, 361 rng, 362 ) 363 .unwrap(); 364 // Add the next block. 365 result.insert( 366 serde_yaml::Value::String("add_next_block".to_string()), 367 serde_yaml::Value::String(match vm.add_next_block(&block) { 368 Ok(_) => "succeeded.".to_string(), 369 Err(err) => err.to_string(), 370 }), 371 ); 372 (serde_yaml::Value::Mapping(result), serde_yaml::Value::Mapping(other)) 373 }; 374 375 // Run the test. 376 let (result, other) = run_test(); 377 outputs.push(result); 378 additional.push(other); 379 } 380 381 let mut output = serde_yaml::Mapping::new(); 382 output.insert(serde_yaml::Value::String("errors".to_string()), serde_yaml::Value::Sequence(vec![])); 383 output.insert(serde_yaml::Value::String("outputs".to_string()), serde_yaml::Value::Sequence(outputs)); 384 output.insert(serde_yaml::Value::String("additional".to_string()), serde_yaml::Value::Sequence(additional)); 385 output 386 } 387 388 // A helper function to initialize the VM. 389 // Returns a VM and the first record in the genesis block. 390 #[allow(clippy::type_complexity)] 391 fn initialize_vm<R: Rng + CryptoRng>( 392 private_key: &PrivateKey<CurrentNetwork>, 393 height: u32, 394 rng: &mut R, 395 ) -> (VM<CurrentNetwork, LedgerType>, Vec<Record<CurrentNetwork, Plaintext<CurrentNetwork>>>) { 396 // Initialize a VM. 397 let vm: VM<CurrentNetwork, LedgerType> = 398 VM::from(ConsensusStore::open(StorageMode::new_test(None)).unwrap()).unwrap(); 399 400 // Initialize the genesis block. 401 let genesis = vm.genesis_beacon(private_key, rng).unwrap(); 402 403 // Select a record to spend. 404 let view_key = ViewKey::try_from(private_key).unwrap(); 405 let records = genesis.transitions().cloned().flat_map(Transition::into_records).collect::<IndexMap<_, _>>(); 406 let records = records.values().map(|record| record.decrypt(&view_key).unwrap()).collect::<Vec<_>>(); 407 408 // Add the genesis block to the VM. 409 vm.add_next_block(&genesis).unwrap(); 410 411 // If the desired height is greater than zero, add additional blocks to the VM. 412 for _ in 0..height { 413 let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; 414 let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm 415 .speculate( 416 construct_finalize_global_state(&vm, time_since_last_block), 417 time_since_last_block, 418 Some(0u64), 419 vec![], 420 &None.into(), 421 [].into_iter(), 422 rng, 423 ) 424 .unwrap(); 425 assert!(aborted_transaction_ids.is_empty()); 426 427 let block = construct_next_block( 428 &vm, 429 time_since_last_block, 430 private_key, 431 ratifications, 432 transactions, 433 aborted_transaction_ids, 434 ratified_finalize_operations, 435 rng, 436 ) 437 .unwrap(); 438 vm.add_next_block(&block).unwrap(); 439 } 440 441 (vm, records) 442 } 443 444 // A helper function construct the desired number of fee records from an initial record, all owned by the same key. 445 #[allow(unused)] 446 fn construct_fee_records<C: ConsensusStorage<CurrentNetwork>, R: Rng + CryptoRng>( 447 vm: &VM<CurrentNetwork, C>, 448 private_key: &PrivateKey<CurrentNetwork>, 449 records: Vec<Record<CurrentNetwork, Plaintext<CurrentNetwork>>>, 450 num_fee_records: usize, 451 rng: &mut R, 452 ) -> Vec<(Record<CurrentNetwork, Plaintext<CurrentNetwork>>, u64)> { 453 // Helper function to get the balance of a `credits.delta` record. 454 let get_balance = |record: &Record<CurrentNetwork, Plaintext<CurrentNetwork>>| -> u64 { 455 match record.data().get(&Identifier::from_str("microcredits").unwrap()).unwrap() { 456 Entry::Private(Plaintext::Literal(Literal::U64(amount), ..)) => **amount, 457 _ => unreachable!("Invalid entry type for credits.delta."), 458 } 459 }; 460 461 println!("Splitting the initial fee record into {num_fee_records} fee records."); 462 463 // Construct fee records for the tests. 464 let mut fee_records = records 465 .into_iter() 466 .map(|record| { 467 let balance = get_balance(&record); 468 (record, balance) 469 }) 470 .collect::<Vec<_>>(); 471 let mut fee_counter = 1; 472 while fee_records.len() < num_fee_records { 473 let mut transactions = Vec::with_capacity(fee_records.len()); 474 for (fee_record, balance) in fee_records.drain(..).collect_vec() { 475 if fee_counter < num_fee_records { 476 println!("Splitting out the {}-th record of size {}.", fee_counter, balance / 2); 477 let (mut records, txns) = split(vm, private_key, fee_record, balance / 2, rng); 478 let second = records.pop().unwrap(); 479 let first = records.pop().unwrap(); 480 let balance = get_balance(&first); 481 fee_records.push((first, balance)); 482 let balance = get_balance(&second); 483 fee_records.push((second, balance)); 484 transactions.extend(txns); 485 fee_counter += 1; 486 } else { 487 fee_records.push((fee_record, balance)); 488 } 489 } 490 491 let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; 492 let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm 493 .speculate( 494 construct_finalize_global_state(vm, time_since_last_block), 495 time_since_last_block, 496 Some(0u64), 497 vec![], 498 &None.into(), 499 transactions.iter(), 500 rng, 501 ) 502 .unwrap(); 503 assert!(aborted_transaction_ids.is_empty()); 504 505 // Create a block for the fee transactions and add them to the VM. 506 let block = construct_next_block( 507 vm, 508 time_since_last_block, 509 private_key, 510 ratifications, 511 transactions, 512 aborted_transaction_ids, 513 ratified_finalize_operations, 514 rng, 515 ) 516 .unwrap(); 517 vm.add_next_block(&block).unwrap(); 518 } 519 520 println!("Constructed fee records."); 521 522 fee_records 523 } 524 525 // A helper function to construct the next block. 526 #[allow(clippy::too_many_arguments)] 527 fn construct_next_block<C: ConsensusStorage<CurrentNetwork>, R: Rng + CryptoRng>( 528 vm: &VM<CurrentNetwork, C>, 529 time_since_last_block: i64, 530 private_key: &PrivateKey<CurrentNetwork>, 531 ratifications: Ratifications<CurrentNetwork>, 532 transactions: Transactions<CurrentNetwork>, 533 aborted_transaction_ids: Vec<<CurrentNetwork as Network>::TransactionID>, 534 ratified_finalize_operations: Vec<FinalizeOperation<CurrentNetwork>>, 535 rng: &mut R, 536 ) -> Result<Block<CurrentNetwork>> { 537 // Get the most recent block. 538 let block_hash = vm.block_store().get_block_hash(vm.block_store().max_height().unwrap()).unwrap().unwrap(); 539 let previous_block = vm.block_store().get_block(&block_hash).unwrap().unwrap(); 540 541 // Construct the metadata associated with the block. 542 let metadata = Metadata::new( 543 CurrentNetwork::ID, 544 previous_block.round() + 1, 545 previous_block.height() + 1, 546 0, 547 0, 548 CurrentNetwork::GENESIS_COINBASE_TARGET, 549 CurrentNetwork::GENESIS_PROOF_TARGET, 550 previous_block.last_coinbase_target(), 551 previous_block.last_coinbase_timestamp(), 552 previous_block.timestamp().saturating_add(time_since_last_block), 553 )?; 554 // Construct the block header. 555 let header = Header::from( 556 vm.block_store().current_state_root(), 557 transactions.to_transactions_root().unwrap(), 558 transactions.to_finalize_root(ratified_finalize_operations).unwrap(), 559 ratifications.to_ratifications_root().unwrap(), 560 Field::zero(), 561 Field::zero(), 562 metadata, 563 )?; 564 565 // Construct the new block. 566 Block::new_beacon( 567 private_key, 568 previous_block.hash(), 569 header, 570 ratifications, 571 None.into(), 572 vec![], 573 transactions, 574 aborted_transaction_ids, 575 rng, 576 ) 577 } 578 579 // A helper function to invoke `credits.delta/split`. 580 #[allow(clippy::type_complexity, unused)] 581 fn split<C: ConsensusStorage<CurrentNetwork>, R: Rng + CryptoRng>( 582 vm: &VM<CurrentNetwork, C>, 583 private_key: &PrivateKey<CurrentNetwork>, 584 record: Record<CurrentNetwork, Plaintext<CurrentNetwork>>, 585 amount: u64, 586 rng: &mut R, 587 ) -> (Vec<Record<CurrentNetwork, Plaintext<CurrentNetwork>>>, Vec<Transaction<CurrentNetwork>>) { 588 let inputs = vec![Value::Record(record), Value::Plaintext(Plaintext::from(Literal::U64(U64::new(amount))))]; 589 let transaction = vm.execute(private_key, ("credits.delta", "split"), inputs.iter(), None, 0, None, rng).unwrap(); 590 let records = transaction 591 .records() 592 .map(|(_, record)| record.decrypt(&ViewKey::try_from(private_key).unwrap()).unwrap()) 593 .collect_vec(); 594 assert_eq!(records.len(), 2); 595 (records, vec![transaction]) 596 } 597 598 // Construct `FinalizeGlobalState` from the current `VM` state. 599 fn construct_finalize_global_state<C: ConsensusStorage<CurrentNetwork>>( 600 vm: &VM<CurrentNetwork, C>, 601 time_since_last_block: i64, 602 ) -> FinalizeGlobalState { 603 // Retrieve the latest block. 604 let block_height = vm.block_store().max_height().unwrap(); 605 let latest_block_hash = vm.block_store().get_block_hash(block_height).unwrap().unwrap(); 606 let latest_block = vm.block_store().get_block(&latest_block_hash).unwrap().unwrap(); 607 // Retrieve the latest round. 608 let latest_round = latest_block.round(); 609 // Retrieve the latest height. 610 let latest_height = latest_block.height(); 611 // Retrieve the latest cumulative weight. 612 let latest_cumulative_weight = latest_block.cumulative_weight(); 613 614 // Compute the next round number./ 615 let next_round = latest_round.saturating_add(1); 616 // Compute the next height. 617 let next_height = latest_height.saturating_add(1); 618 619 // Determine the block timestamp based on the consensus version. 620 let block_timestamp = 621 match next_height >= CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V12).unwrap_or_default() { 622 true => Some(latest_block.timestamp().saturating_add(time_since_last_block)), 623 false => None, 624 }; 625 // Construct the finalize state. 626 FinalizeGlobalState::new::<CurrentNetwork>( 627 next_round, 628 next_height, 629 block_timestamp, 630 latest_cumulative_weight, 631 0u128, 632 latest_block.hash(), 633 ) 634 .unwrap() 635 }