Skip to content

Commit

Permalink
Implement custom heap to support SlotHistory deserialization (#22)
Browse files Browse the repository at this point in the history
The SlotHistory sysvar is too big to deserialize on the stack, and too
big for the default heap size (32kb). Solana supports up to 256KB of
heap, but a custom heap implementation is needed, even if you request an
increased heap size with the compute budget instruction, since the
default global_allocator just uses the hard-coded 32kb.

Mango's custom heap implementation implements a bottom-up heap as
opposed to the top-down heap implemented by
`solana_program::entrypoint:BumpAllocator`. The issue with the top down
approach is, if you initialize the standard BumpAllocator with a larger
heap length, instructions that don't need the extra heap space will
start out allocating outside of the default heap bounds unless they
submit a request_heap_frame instruction. By allocating from bottom up,
allocs start in the valid bounds and don't need to submit the ix unless
they go over 32kb.
  • Loading branch information
ebatsell authored Feb 26, 2024
1 parent 2b59311 commit c8a5c36
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 5 deletions.
3 changes: 2 additions & 1 deletion programs/validator-history/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
default = ["custom-heap"]
custom-heap = []

[dependencies]
anchor-lang = "0.28.0"
Expand Down
87 changes: 87 additions & 0 deletions programs/validator-history/src/allocator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// // Copied from https://github.com/blockworks-foundation/mango-v4/blob/dev/programs/mango-v4/src/allocator.rs
#![allow(dead_code)]

use std::alloc::{GlobalAlloc, Layout};

/// The end of the region where heap space may be reserved for the program.
///
/// The actual size of the heap is currently not available at runtime.
/// https://solana.com/docs/programs/faq#memory-map
pub const HEAP_END_ADDRESS: usize = 0x400000000;

#[cfg(not(feature = "no-entrypoint"))]
#[global_allocator]
pub static ALLOCATOR: BumpAllocator = BumpAllocator {};

pub fn heap_used() -> usize {
#[cfg(not(feature = "no-entrypoint"))]
return ALLOCATOR.used();

#[cfg(feature = "no-entrypoint")]
return 0;
}

/// Custom bump allocator for on-chain operations
///
/// The default allocator is also a bump one, but grows from a fixed
/// HEAP_START + 32kb downwards and has no way of making use of extra
/// heap space requested for the transaction.
///
/// This implementation starts at HEAP_START and grows upward, producing
/// a segfault once out of available heap memory.
pub struct BumpAllocator {}

unsafe impl GlobalAlloc for BumpAllocator {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let heap_start = anchor_lang::solana_program::entrypoint::HEAP_START_ADDRESS as usize;
let pos_ptr = heap_start as *mut usize;

let mut pos = *pos_ptr;
if pos == 0 {
// First time, override the current position to be just past the location
// where the current heap position is stored.
pos = heap_start + 8;
}

// The result address needs to be aligned to layout.align(),
// which is guaranteed to be a power of two.
// Find the first address >=pos that has the required alignment.
// Wrapping ops are used for performance.
let mask = layout.align().wrapping_sub(1);
let begin = pos.wrapping_add(mask) & (!mask);

// Update allocator state
let end = begin.checked_add(layout.size()).unwrap();
*pos_ptr = end;

// Ensure huge allocations can't escape the dedicated heap memory region
assert!(end < HEAP_END_ADDRESS);

// Write a byte to trigger heap overflow errors early
let end_ptr = end as *mut u8;
*end_ptr = 0;

begin as *mut u8
}
#[inline]
unsafe fn dealloc(&self, _: *mut u8, _: Layout) {
// I'm a bump allocator, I don't free
}
}

impl BumpAllocator {
#[inline]
pub fn used(&self) -> usize {
let heap_start = anchor_lang::solana_program::entrypoint::HEAP_START_ADDRESS as usize;
unsafe {
let pos_ptr = heap_start as *mut usize;

let pos = *pos_ptr;
if pos == 0 {
return 0;
}
pos - heap_start
}
}
}
1 change: 1 addition & 0 deletions programs/validator-history/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anchor_lang::prelude::*;

mod allocator;
pub mod constants;
pub mod crds_value;
pub mod errors;
Expand Down
1 change: 1 addition & 0 deletions tests/src/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ impl TestFixture {
.to_account_metas(None),
data: validator_history::instruction::SetNewTipDistributionProgram {}.data(),
};

let transaction = Transaction::new_signed_with_payer(
&[instruction, set_tip_distribution_instruction],
Some(&self.keypair.pubkey()),
Expand Down
11 changes: 7 additions & 4 deletions tests/tests/test_cluster_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ use anchor_lang::{
InstructionData, ToAccountMetas,
};
use solana_program_test::*;
use solana_sdk::{clock::Clock, signer::Signer, transaction::Transaction};
use solana_sdk::{
clock::Clock, compute_budget::ComputeBudgetInstruction, signer::Signer,
transaction::Transaction,
};
use tests::fixtures::TestFixture;
use validator_history::ClusterHistory;

#[tokio::test]
#[ignore] // TODO: fix failing test
async fn test_copy_cluster_info() {
// Initialize
let fixture = TestFixture::new().await;
Expand All @@ -28,7 +30,6 @@ async fn test_copy_cluster_info() {
let latest_slot = ctx.borrow_mut().banks_client.get_root_slot().await.unwrap();
slot_history.add(latest_slot);
slot_history.add(latest_slot + 1);
println!("latest_slot: {}", latest_slot);

// Submit instruction
let instruction = Instruction {
Expand All @@ -41,9 +42,11 @@ async fn test_copy_cluster_info() {
}
.to_account_metas(None),
};
let heap_request_ix = ComputeBudgetInstruction::request_heap_frame(256 * 1024);
let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000);

let transaction = Transaction::new_signed_with_payer(
&[instruction],
&[heap_request_ix, compute_budget_ix, instruction],
Some(&fixture.keypair.pubkey()),
&[&fixture.keypair],
ctx.borrow().last_blockhash,
Expand Down

0 comments on commit c8a5c36

Please sign in to comment.