Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REFACTOR: Simplified keeper bot #36

Merged
merged 21 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ test-ledger
*/test-ledger
.idea/
**/targets
**/credentials
**/config
**/*.env
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"datapoint"
]
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove this + add to .gitignore

53 changes: 53 additions & 0 deletions keepers/validator-keeper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
## Keeper State
The `KeeperState` keeps track of:
- current epoch data
- running tally of all operations successes and failures for the given epoch
- all accounts fetched from the RPC that are needed downstream

Note: All maps are key'd by the `vote_account`

## Program

### Initialize
Gather all needed arguments, and initialize the global `KeeperState`.

### Loop
The forever loop consists of three parts: **Fetch**, **Fire** and **Emit**. There is only ever one **Fetch** and **Emit** section, and there can be several **Fire** sections.

The **Fire** sections can run on independent cadences - say we want the Validator History functions to run every 300sec and we want to emit metrics every 60sec.

The **Fetch** section is run _before_ and **Fire** section.
The **Emit** section is *one tick* after any **Fire** section.

#### Fetch
The **Fetch** section is in charge of three operations:
- Keeping track of the current epoch and resetting the runs and error counts for each operation
- Creating any missing accounts needed for the downstream **Fires** to run
- Fetching and updating all of the needed accounts needed downstream

This is accomplished is by running three functions within the **Fetch** section
- `pre_create_update` - Updates epoch, and fetches all needed accounts that are not dependant on any missing accounts.
- `create_missing_accounts` - Creates the missing accounts, which can be determined by the accounts fetched in the previous step
- `post_create_update` - Fetches any last accounts that needed the missing accounts

Since this is run before every **FIRE** section, some accounts will be fetched that are not needed. This may seem wasteful but the simplicity of having a synchronized global is worth the cost.

Notes:
- The **Fetch** section is the only section that will mutate the `KeeperState`.
- If anything in the **Fetch** section fails, no **Fires** will run

#### Fire
There are several **Fire** sections running at their own cadence. Before any **Fire** section is run, the **Fetch** section will be called.

Each **Fire** is a call to `operations::operation_name::fire` which will fire off the operation and return the new count of runs and errors for that operation to be saved in the `KeeperState`

Notes:
- Each **Fire** is self contained, one should not be dependant on another.
- No **Fire* will fetch any accounts, if there are needs for them, they should be added to the `KeeperState`


#### Emit
This section emits the state of the Keeper one tick after any operation has been called. This is because we want to emit a failure of any **Fetch** operation, which on failure advances the tick.



48 changes: 0 additions & 48 deletions keepers/validator-keeper/src/cluster_info.rs

This file was deleted.

52 changes: 52 additions & 0 deletions keepers/validator-keeper/src/entries/copy_vote_account_entry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use anchor_lang::{InstructionData, ToAccountMetas};
use keeper_core::{Address, UpdateInstruction};
use solana_sdk::instruction::Instruction;
use solana_sdk::pubkey::Pubkey;
use validator_history::Config;
use validator_history::ValidatorHistory;

pub struct CopyVoteAccountEntry {
pub vote_account: Pubkey,
pub validator_history_account: Pubkey,
pub config_address: Pubkey,
pub program_id: Pubkey,
pub signer: Pubkey,
}

impl CopyVoteAccountEntry {
pub fn new(vote_account: &Pubkey, program_id: &Pubkey, signer: &Pubkey) -> Self {
let (validator_history_account, _) = Pubkey::find_program_address(
&[ValidatorHistory::SEED, &vote_account.to_bytes()],
program_id,
);
let (config_address, _) = Pubkey::find_program_address(&[Config::SEED], program_id);
Self {
vote_account: *vote_account,
validator_history_account,
config_address,
program_id: *program_id,
signer: *signer,
}
}
}

impl Address for CopyVoteAccountEntry {
fn address(&self) -> Pubkey {
self.validator_history_account
}
}

impl UpdateInstruction for CopyVoteAccountEntry {
fn update_instruction(&self) -> Instruction {
Instruction {
program_id: self.program_id,
accounts: validator_history::accounts::CopyVoteAccount {
validator_history_account: self.validator_history_account,
vote_account: self.vote_account,
signer: self.signer,
}
.to_account_metas(None),
data: validator_history::instruction::CopyVoteAccount {}.data(),
}
}
}
159 changes: 159 additions & 0 deletions keepers/validator-keeper/src/entries/gossip_entry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use anchor_lang::InstructionData;
use anchor_lang::ToAccountMetas;
use bytemuck::{bytes_of, Pod, Zeroable};
use keeper_core::Address;
use solana_sdk::{
compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey,
signature::Signature,
};

use crate::{derive_validator_history_address, derive_validator_history_config_address};

#[derive(Clone, Debug)]
pub struct GossipEntry {
pub vote_account: Pubkey,
pub validator_history_account: Pubkey,
pub config: Pubkey,
pub signature: Signature,
pub message: Vec<u8>,
pub program_id: Pubkey,
pub identity: Pubkey,
pub signer: Pubkey,
}

impl GossipEntry {
pub fn new(
vote_account: &Pubkey,
signature: &Signature,
message: &[u8],
program_id: &Pubkey,
identity: &Pubkey,
signer: &Pubkey,
) -> Self {
let validator_history_account = derive_validator_history_address(vote_account, program_id);
let config = derive_validator_history_config_address(program_id);
Self {
vote_account: *vote_account,
validator_history_account,
config,
signature: *signature,
message: message.to_vec(),
program_id: *program_id,
identity: *identity,
signer: *signer,
}
}
}

impl Address for GossipEntry {
fn address(&self) -> Pubkey {
self.validator_history_account
}
}

impl GossipEntry {
pub fn build_update_tx(&self, priority_fee: u64) -> Vec<Instruction> {
let mut ixs = vec![
ComputeBudgetInstruction::set_compute_unit_limit(100_000),
ComputeBudgetInstruction::set_compute_unit_price(priority_fee),
build_verify_signature_ix(
self.signature.as_ref(),
self.identity.to_bytes(),
&self.message,
),
];

ixs.push(Instruction {
program_id: self.program_id,
accounts: validator_history::accounts::CopyGossipContactInfo {
validator_history_account: self.validator_history_account,
vote_account: self.vote_account,
instructions: solana_program::sysvar::instructions::id(),
config: self.config,
oracle_authority: self.signer,
}
.to_account_metas(None),
data: validator_history::instruction::CopyGossipContactInfo {}.data(),
});
ixs
}
}

// CODE BELOW SLIGHTLY MODIFIED FROM
// solana_sdk/src/ed25519_instruction.rs

pub const PUBKEY_SERIALIZED_SIZE: usize = 32;
pub const SIGNATURE_SERIALIZED_SIZE: usize = 64;
pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 14;
// bytemuck requires structures to be aligned
pub const SIGNATURE_OFFSETS_START: usize = 2;
pub const DATA_START: usize = SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START;

#[derive(Default, Debug, Copy, Clone, Zeroable, Pod, Eq, PartialEq)]
#[repr(C)]
pub struct Ed25519SignatureOffsets {
signature_offset: u16, // offset to ed25519 signature of 64 bytes
signature_instruction_index: u16, // instruction index to find signature
public_key_offset: u16, // offset to public key of 32 bytes
public_key_instruction_index: u16, // instruction index to find public key
message_data_offset: u16, // offset to start of message data
message_data_size: u16, // size of message data
message_instruction_index: u16, // index of instruction data to get message data
}

// This code is modified from solana_sdk/src/ed25519_instruction.rs
// due to that function requiring a keypair, and generating the signature within the function.
// In our case we don't have the keypair, we just have the signature and pubkey.
pub fn build_verify_signature_ix(
signature: &[u8],
pubkey: [u8; 32],
message: &[u8],
) -> Instruction {
assert_eq!(pubkey.len(), PUBKEY_SERIALIZED_SIZE);
assert_eq!(signature.len(), SIGNATURE_SERIALIZED_SIZE);

let mut instruction_data = Vec::with_capacity(
DATA_START
.saturating_add(SIGNATURE_SERIALIZED_SIZE)
.saturating_add(PUBKEY_SERIALIZED_SIZE)
.saturating_add(message.len()),
);

let num_signatures: u8 = 1;
let public_key_offset = DATA_START;
let signature_offset = public_key_offset.saturating_add(PUBKEY_SERIALIZED_SIZE);
let message_data_offset = signature_offset.saturating_add(SIGNATURE_SERIALIZED_SIZE);

// add padding byte so that offset structure is aligned
instruction_data.extend_from_slice(bytes_of(&[num_signatures, 0]));

let offsets = Ed25519SignatureOffsets {
signature_offset: signature_offset as u16,
signature_instruction_index: u16::MAX,
public_key_offset: public_key_offset as u16,
public_key_instruction_index: u16::MAX,
message_data_offset: message_data_offset as u16,
message_data_size: message.len() as u16,
message_instruction_index: u16::MAX,
};

instruction_data.extend_from_slice(bytes_of(&offsets));

debug_assert_eq!(instruction_data.len(), public_key_offset);

instruction_data.extend_from_slice(&pubkey);

debug_assert_eq!(instruction_data.len(), signature_offset);

instruction_data.extend_from_slice(signature);

debug_assert_eq!(instruction_data.len(), message_data_offset);

instruction_data.extend_from_slice(message);

Instruction {
program_id: solana_program::ed25519_program::id(),
accounts: vec![],
data: instruction_data,
}
}
Loading
Loading