您正在查看: Solana-新手教程 分类下的文章

Solana 区块浏览器定制网络配置

github: https://github.com/solana-labs/explorer

Diff

From 88f5c222a19c983fae1c950cc3612d6bbc53a31c Mon Sep 17 00:00:00 2001
From: Hendrik Hofstadt <hendrik@bool.capital>
Date: Sun, 18 Feb 2024 10:19:46 +0100
Subject: [PATCH] reduce cluster options to SPE

---
 app/providers/cluster.tsx                  |  5 ++++-
 app/providers/stats/SolanaPingProvider.tsx |  2 +-
 app/utils/cluster.ts                       | 11 +++++++++--
 3 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/app/providers/cluster.tsx b/app/providers/cluster.tsx
index deef8a2..04bcc6b 100644
--- a/app/providers/cluster.tsx
+++ b/app/providers/cluster.tsx
@@ -59,9 +59,12 @@ function parseQuery(searchParams: ReadonlyURLSearchParams | null): Cluster {
             return Cluster.Devnet;
         case 'testnet':
             return Cluster.Testnet;
+        case 'localspe':
+            return Cluster.SPE;
         case 'mainnet-beta':
-        default:
             return Cluster.MainnetBeta;
+        default:
+            return Cluster.SPE;
     }
 }

diff --git a/app/providers/stats/SolanaPingProvider.tsx b/app/providers/stats/SolanaPingProvider.tsx
index 76b2682..6d5df04 100644
--- a/app/providers/stats/SolanaPingProvider.tsx
+++ b/app/providers/stats/SolanaPingProvider.tsx
@@ -12,7 +12,7 @@ const FETCH_PING_INTERVAL = 60 * 1000;
 function getPingUrl(cluster: Cluster) {
     const slug = clusterSlug(cluster);

-    if (slug === 'custom') {
+    if (slug === 'custom' || slug === 'localspe') {
         return undefined;
     }

diff --git a/app/utils/cluster.ts b/app/utils/cluster.ts
index f30963f..734f121 100644
--- a/app/utils/cluster.ts
+++ b/app/utils/cluster.ts
@@ -8,10 +8,11 @@ export enum Cluster {
     MainnetBeta,
     Testnet,
     Devnet,
+    SPE,
     Custom,
 }

-export const CLUSTERS = [Cluster.MainnetBeta, Cluster.Testnet, Cluster.Devnet, Cluster.Custom];
+export const CLUSTERS = [Cluster.SPE, Cluster.Custom];

 export function clusterSlug(cluster: Cluster): string {
     switch (cluster) {
@@ -21,6 +22,8 @@ export function clusterSlug(cluster: Cluster): string {
             return 'testnet';
         case Cluster.Devnet:
             return 'devnet';
+        case Cluster.SPE:
+            return 'localspe';
         case Cluster.Custom:
             return 'custom';
     }
@@ -34,6 +37,8 @@ export function clusterName(cluster: Cluster): string {
             return 'Testnet';
         case Cluster.Devnet:
             return 'Devnet';
+        case Cluster.SPE:
+            return 'Local SPE';
         case Cluster.Custom:
             return 'Custom';
     }
@@ -59,9 +64,11 @@ export function clusterUrl(cluster: Cluster, customUrl: string): string {
             return process.env.NEXT_PUBLIC_MAINNET_RPC_URL ?? modifyUrl(MAINNET_BETA_URL);
         case Cluster.Testnet:
             return process.env.NEXT_PUBLIC_TESTNET_RPC_URL ?? modifyUrl(TESTNET_URL);
+        case Cluster.SPE:
+            return "http://localhost:8899";
         case Cluster.Custom:
             return customUrl;
     }
 }

-export const DEFAULT_CLUSTER = Cluster.MainnetBeta;
+export const DEFAULT_CLUSTER = Cluster.SPE;
--
2.42.0

Solana 修改最小质押量

需求

自定义最小的质押量,限制外部的加入门槛

代码分析

sdk/program/src/stake/instruction.rs

#[error("delegation amount is less than the minimum")]
InsufficientDelegation,

programs/stake/src/stake_state.rs

fn validate_delegated_amount(
    account: &BorrowedAccount,
    meta: &Meta,
    feature_set: &FeatureSet,
) -> Result<ValidatedDelegatedInfo, InstructionError> {
    let stake_amount = account
        .get_lamports()
        .saturating_sub(meta.rent_exempt_reserve); // 确保委托的质押金额有效。这将检查帐户是否满足委托质押的最低余额要求。如果不满足,则返回错误。

    // 质押账户可能会以低于最低委托额的质押额进行初始化,因此请在委托前检查是否满足最低要求。
    if stake_amount < crate::get_minimum_delegation(feature_set) {
        return Err(StakeError::InsufficientDelegation.into());
    }
    Ok(ValidatedDelegatedInfo { stake_amount })
}
/// 可以委托的最低股权金额,以 lampors 为单位。
/// 注意:这也用于计算股权账户的最低余额,即租金豁免储备加上最低股权委托。
#[inline(always)]
pub fn get_minimum_delegation(feature_set: &FeatureSet) -> u64 {
    if feature_set.is_active(&feature_set::stake_raise_minimum_delegation_to_1_sol::id()) {
        const MINIMUM_DELEGATION_SOL: u64 = 1;
        MINIMUM_DELEGATION_SOL * LAMPORTS_PER_SOL
    } else {
        #[allow(deprecated)]
        solana_sdk::stake::MINIMUM_STAKE_DELEGATION
    }
}

总结

目前测试验证节点,直接硬编码,设置一个较大值来限制外部加入,修改测试代码如下


/// 可以委托的最低股权金额,以 lampors 为单位。
/// 注意:这也用于计算股权账户的最低余额,即租金豁免储备加上最低股权委托。
#[inline(always)]
pub fn get_minimum_delegation(feature_set: &FeatureSet) -> u64 {
    const MINIMUM_DELEGATION_SOL: u64 = 1000000000000;
    MINIMUM_DELEGATION_SOL * LAMPORTS_PER_SOL
}

Solana 如何限制外部注册和质押

限制参与

  1. 限制外部validator注册
  2. 限制外部质押->奖励分配

genesis初始化

从genesis初始化逻辑,切入validator注册和质押逻辑

--bootstrap-validator "$SOLANA_CONFIG_DIR"/bootstrap-validator/identity.json
                        "$SOLANA_CONFIG_DIR"/bootstrap-validator/vote-account.json
                        "$SOLANA_CONFIG_DIR"/bootstrap-validator/stake-account.json

支持多个validator,每三个一组,分别对应,且不能重复

  • identity_pubkey
  • vote_pubkey
  • stake_pubkey
let bootstrap_validator_pubkeys = pubkeys_of(&matches, "bootstrap_validator").unwrap();
assert_eq!(bootstrap_validator_pubkeys.len() % 3, 0);

初始化配置

let bootstrap_validator_lamports =
        value_t_or_exit!(matches, "bootstrap_validator_lamports", u64); // 分配给引导验证器的 Lamport 数量

let bootstrap_validator_stake_lamports = rent_exempt_check( // 分配给引导验证者质押账户的 lampor 数量
    &matches,
    "bootstrap_validator_stake_lamports",
    rent.minimum_balance(StakeStateV2::size_of()),
)?;

let bootstrap_stake_authorized_pubkey =
pubkey_of(&matches, "bootstrap_stake_authorized_pubkey"); // 包含授权管理引导验证者权益的公钥的文件路径 [默认值:--bootstrap-validator IDENTITY_PUBKEY]

初始化

let commission = value_t_or_exit!(matches, "vote_commission_percentage", u8);

    let mut bootstrap_validator_pubkeys_iter = bootstrap_validator_pubkeys.iter();
    loop {
        let Some(identity_pubkey) = bootstrap_validator_pubkeys_iter.next() else {
            break;
        };
        let vote_pubkey = bootstrap_validator_pubkeys_iter.next().unwrap();
        let stake_pubkey = bootstrap_validator_pubkeys_iter.next().unwrap();

        genesis_config.add_account( //添加账户,并初始增加bootstrap_validator_lamports数量余额
            *identity_pubkey,
            AccountSharedData::new(bootstrap_validator_lamports, 0, &system_program::id()),
        );

        let vote_account = vote_state::create_account_with_authorized( // 初始化账户和余额,并VoteInit初始化VoteSigner
            identity_pubkey,
            identity_pubkey,
            identity_pubkey,
            commission,
            VoteState::get_rent_exempt_reserve(&genesis_config.rent).max(1),
        );

        genesis_config.add_account(
            *stake_pubkey,
            stake_state::create_account( // 
                bootstrap_stake_authorized_pubkey
                    .as_ref()
                    .unwrap_or(identity_pubkey),
                vote_pubkey,
                &vote_account,
                &genesis_config.rent,
                bootstrap_validator_stake_lamports,
            ),
        );

        genesis_config.add_account(*vote_pubkey, vote_account);
    }

Validator初始化逻辑

pub fn create_account_with_authorized(
    node_pubkey: &Pubkey,
    authorized_voter: &Pubkey,
    authorized_withdrawer: &Pubkey,
    commission: u8,
    lamports: u64,
) -> AccountSharedData {
    let mut vote_account = AccountSharedData::new(lamports, VoteState::size_of(), &id()); // 初始化账户和余额

    let vote_state = VoteState::new( // 初始化vote_state
        &VoteInit {
            node_pubkey: *node_pubkey,
            authorized_voter: *authorized_voter,
            authorized_withdrawer: *authorized_withdrawer,
            commission,
        },
        &Clock::default(),
    );

    VoteState::serialize( // 序列化状态
        &VoteStateVersions::Current(Box::new(vote_state)),
        vote_account.data_as_mut_slice(),
    )
    .unwrap();

    vote_account
}

质押逻辑

fn do_create_account(
    authorized: &Pubkey,
    voter_pubkey: &Pubkey,
    vote_account: &AccountSharedData,
    rent: &Rent,
    lamports: u64,
    activation_epoch: Epoch,
) -> AccountSharedData {
    let mut stake_account = AccountSharedData::new(lamports, StakeStateV2::size_of(), &id()); // 初始化账户和余额

    let vote_state = vote_state::from(vote_account).expect("vote_state"); // 获取投票状态

    let rent_exempt_reserve = rent.minimum_balance(stake_account.data().len()); // genesis_config.rent 需要预留的余额

    stake_account
        .set_state(&StakeStateV2::Stake(
            Meta {
                authorized: Authorized::auto(authorized),
                rent_exempt_reserve,
                ..Meta::default()
            },
            new_stake(
                lamports - rent_exempt_reserve, // 总余额 - 需要预留的,等于本次可质押的
                voter_pubkey,
                &vote_state,
                activation_epoch,
            ),
            StakeFlags::empty(),
        ))
        .expect("set_state");

    stake_account
}

综合分析

对应代码,Validator的注册和质押流程如下

  • Validator->VoteState::new(VoteInit)->VoteState::serialize
  • StakeStateV2::Stake

在对应下面的流程图

验证者注册与委托流程图

节点之间Vote消息是如何传递的

Validator

core/src/validator.rs

pub fn new(
        mut node: Node,
        identity_keypair: Arc<Keypair>,
        ledger_path: &Path,
        vote_account: &Pubkey,
        authorized_voter_keypairs: Arc<RwLock<Vec<Arc<Keypair>>>>,
        cluster_entrypoints: Vec<ContactInfo>,
        config: &ValidatorConfig,
        should_check_duplicate_instance: bool,
        rpc_to_plugin_manager_receiver: Option<Receiver<GeyserPluginManagerRequest>>,
        start_progress: Arc<RwLock<ValidatorStartProgress>>,
        socket_addr_space: SocketAddrSpace,
        use_quic: bool,
        tpu_connection_pool_size: usize,
        tpu_enable_udp: bool, // 关注参数
        admin_rpc_service_post_init: Arc<RwLock<Option<AdminRpcRequestMetadataPostInit>>>,
    ) -> Result<Self, String> {
    ...
    let (tpu, mut key_notifies) = Tpu::new(
            &cluster_info,
            &poh_recorder,
            entry_receiver,
            retransmit_slots_receiver,
            TpuSockets {
                transactions: node.sockets.tpu,
                transaction_forwards: node.sockets.tpu_forwards,
                vote: node.sockets.tpu_vote,
                broadcast: node.sockets.broadcast,
                transactions_quic: node.sockets.tpu_quic,
                transactions_forwards_quic: node.sockets.tpu_forwards_quic,
            },
            &rpc_subscriptions,
            transaction_status_sender,
            entry_notification_sender,
            blockstore.clone(),
            &config.broadcast_stage_type,
            exit,
            node.info.shred_version(),
            vote_tracker,
            bank_forks.clone(),
            verified_vote_sender,
            gossip_verified_vote_hash_sender,
            replay_vote_receiver,
            replay_vote_sender,
            bank_notification_sender.map(|sender| sender.sender),
            config.tpu_coalesce,
            duplicate_confirmed_slot_sender,
            &connection_cache,
            turbine_quic_endpoint_sender,
            &identity_keypair,
            config.runtime_config.log_messages_bytes_limit,
            &staked_nodes,
            config.staked_nodes_overrides.clone(),
            banking_tracer,
            tracer_thread,
            tpu_enable_udp, // 关注参数
            &prioritization_fee_cache,
            config.block_production_method.clone(),
            config.generator_config.clone(),
        );

core/src/tpu.rs

impl Tpu {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        cluster_info: &Arc<ClusterInfo>,
        poh_recorder: &Arc<RwLock<PohRecorder>>,
        entry_receiver: Receiver<WorkingBankEntry>,
        retransmit_slots_receiver: Receiver<Slot>,
        sockets: TpuSockets,
        subscriptions: &Arc<RpcSubscriptions>,
        transaction_status_sender: Option<TransactionStatusSender>,
        entry_notification_sender: Option<EntryNotifierSender>,
        blockstore: Arc<Blockstore>,
        broadcast_type: &BroadcastStageType,
        exit: Arc<AtomicBool>,
        shred_version: u16,
        vote_tracker: Arc<VoteTracker>,
        bank_forks: Arc<RwLock<BankForks>>,
        verified_vote_sender: VerifiedVoteSender,
        gossip_verified_vote_hash_sender: GossipVerifiedVoteHashSender,
        replay_vote_receiver: ReplayVoteReceiver,
        replay_vote_sender: ReplayVoteSender,
        bank_notification_sender: Option<BankNotificationSender>,
        tpu_coalesce: Duration,
        duplicate_confirmed_slot_sender: DuplicateConfirmedSlotsSender,
        connection_cache: &Arc<ConnectionCache>,
        turbine_quic_endpoint_sender: AsyncSender<(SocketAddr, Bytes)>,
        keypair: &Keypair,
        log_messages_bytes_limit: Option<usize>,
        staked_nodes: &Arc<RwLock<StakedNodes>>,
        shared_staked_nodes_overrides: Arc<RwLock<HashMap<Pubkey, u64>>>,
        banking_tracer: Arc<BankingTracer>,
        tracer_thread_hdl: TracerThread,
        tpu_enable_udp: bool,
        prioritization_fee_cache: &Arc<PrioritizationFeeCache>,
        block_production_method: BlockProductionMethod,
        _generator_config: Option<GeneratorConfig>, /* vestigial code for replay invalidator */
    ) -> (Self, Vec<Arc<dyn NotifyKeyUpdate + Sync + Send>>) {
    ...
    let fetch_stage = FetchStage::new_with_sender(
            transactions_sockets,
            tpu_forwards_sockets,
            tpu_vote_sockets,
            exit.clone(),
            &packet_sender,
            &vote_packet_sender,
            &forwarded_packet_sender,
            forwarded_packet_receiver,
            poh_recorder,
            tpu_coalesce,
            Some(bank_forks.read().unwrap().get_vote_only_mode_signal()),
            tpu_enable_udp,
        );
    ...
    let (gossip_vote_sender, gossip_vote_receiver) =
            banking_tracer.create_channel_gossip_vote();
        let cluster_info_vote_listener = ClusterInfoVoteListener::new(
            exit.clone(),
            cluster_info.clone(),
            gossip_vote_sender,
            poh_recorder.clone(),
            vote_tracker,
            bank_forks.clone(),
            subscriptions.clone(),
            verified_vote_sender,
            gossip_verified_vote_hash_sender,
            replay_vote_receiver,
            blockstore.clone(),
            bank_notification_sender,
            duplicate_confirmed_slot_sender,
        );

core/src/cluster_info_vote_listener.rs

pub fn new(
        exit: Arc<AtomicBool>,
        cluster_info: Arc<ClusterInfo>,
        verified_packets_sender: BankingPacketSender,
        poh_recorder: Arc<RwLock<PohRecorder>>,
        vote_tracker: Arc<VoteTracker>,
        bank_forks: Arc<RwLock<BankForks>>,
        subscriptions: Arc<RpcSubscriptions>,
        verified_vote_sender: VerifiedVoteSender,
        gossip_verified_vote_hash_sender: GossipVerifiedVoteHashSender,
        replay_votes_receiver: ReplayVoteReceiver,
        blockstore: Arc<Blockstore>,
        bank_notification_sender: Option<BankNotificationSender>,
        duplicate_confirmed_slot_sender: DuplicateConfirmedSlotsSender,
    ) -> Self {
        let (verified_vote_label_packets_sender, verified_vote_label_packets_receiver) =
            unbounded();
        let (verified_vote_transactions_sender, verified_vote_transactions_receiver) = unbounded();
        let listen_thread = {
            let exit = exit.clone();
            let bank_forks = bank_forks.clone();
            Builder::new()
                .name("solCiVoteLstnr".to_string())
                .spawn(move || {
                    let _ = Self::recv_loop(
                        exit,
                        &cluster_info,
                        &bank_forks,
                        verified_vote_label_packets_sender,
                        verified_vote_transactions_sender,
                    );
                })
                .unwrap()
        };
        let bank_send_thread = {
            let exit = exit.clone();
            Builder::new()
                .name("solCiBankSend".to_string())
                .spawn(move || {
                    let _ = Self::bank_send_loop(
                        exit,
                        verified_vote_label_packets_receiver,
                        poh_recorder,
                        &verified_packets_sender,
                    );
                })
                .unwrap()
        };

        let send_thread = Builder::new()
            .name("solCiProcVotes".to_string())
            .spawn(move || {
                let _ = Self::process_votes_loop(
                    exit,
                    verified_vote_transactions_receiver,
                    vote_tracker,
                    bank_forks,
                    subscriptions,
                    gossip_verified_vote_hash_sender,
                    verified_vote_sender,
                    replay_votes_receiver,
                    blockstore,
                    bank_notification_sender,
                    duplicate_confirmed_slot_sender,
                );
            })
            .unwrap();

        Self {
            thread_hdls: vec![listen_thread, send_thread, bank_send_thread],
        }
    }
fn process_votes_loop(
        exit: Arc<AtomicBool>,
        gossip_vote_txs_receiver: VerifiedVoteTransactionsReceiver,
        vote_tracker: Arc<VoteTracker>,
        bank_forks: Arc<RwLock<BankForks>>,
        subscriptions: Arc<RpcSubscriptions>,
        gossip_verified_vote_hash_sender: GossipVerifiedVoteHashSender,
        verified_vote_sender: VerifiedVoteSender,
        replay_votes_receiver: ReplayVoteReceiver,
        blockstore: Arc<Blockstore>,
        bank_notification_sender: Option<BankNotificationSender>,
        duplicate_confirmed_slot_sender: DuplicateConfirmedSlotsSender,
    ) -> Result<()> {
        let mut confirmation_verifier =
            OptimisticConfirmationVerifier::new(bank_forks.read().unwrap().root());
        let mut latest_vote_slot_per_validator = HashMap::new();
        let mut last_process_root = Instant::now();
        let duplicate_confirmed_slot_sender = Some(duplicate_confirmed_slot_sender);
        let mut vote_processing_time = Some(VoteProcessingTiming::default());
        loop {
            if exit.load(Ordering::Relaxed) {
                return Ok(());
            }

            let root_bank = bank_forks.read().unwrap().root_bank();
            if last_process_root.elapsed().as_millis() > DEFAULT_MS_PER_SLOT as u128 {
                let unrooted_optimistic_slots = confirmation_verifier
                    .verify_for_unrooted_optimistic_slots(&root_bank, &blockstore);
                // SlotVoteTracker's for all `slots` in `unrooted_optimistic_slots`
                // should still be available because we haven't purged in
                // `progress_with_new_root_bank()` yet, which is called below
                OptimisticConfirmationVerifier::log_unrooted_optimistic_slots(
                    &root_bank,
                    &vote_tracker,
                    &unrooted_optimistic_slots,
                );
                vote_tracker.progress_with_new_root_bank(&root_bank);
                last_process_root = Instant::now();
            }
            let confirmed_slots = Self::listen_and_confirm_votes(
                &gossip_vote_txs_receiver,
                &vote_tracker,
                &root_bank,
                &subscriptions,
                &gossip_verified_vote_hash_sender,
                &verified_vote_sender,
                &replay_votes_receiver,
                &bank_notification_sender,
                &duplicate_confirmed_slot_sender,
                &mut vote_processing_time,
                &mut latest_vote_slot_per_validator,
            );
            match confirmed_slots {
                Ok(confirmed_slots) => {
                    confirmation_verifier
                        .add_new_optimistic_confirmed_slots(confirmed_slots.clone(), &blockstore);
                }
                Err(e) => match e {
                    Error::RecvTimeout(RecvTimeoutError::Disconnected) => {
                        return Ok(());
                    }
                    Error::ReadyTimeout => (),
                    _ => {
                        error!("thread {:?} error {:?}", thread::current().name(), e);
                    }
                },
            }
        }
    }
fn listen_and_confirm_votes(
        gossip_vote_txs_receiver: &VerifiedVoteTransactionsReceiver,
        vote_tracker: &VoteTracker,
        root_bank: &Bank,
        subscriptions: &RpcSubscriptions,
        gossip_verified_vote_hash_sender: &GossipVerifiedVoteHashSender,
        verified_vote_sender: &VerifiedVoteSender,
        replay_votes_receiver: &ReplayVoteReceiver,
        bank_notification_sender: &Option<BankNotificationSender>,
        duplicate_confirmed_slot_sender: &Option<DuplicateConfirmedSlotsSender>,
        vote_processing_time: &mut Option<VoteProcessingTiming>,
        latest_vote_slot_per_validator: &mut HashMap<Pubkey, Slot>,
    ) -> Result<ThresholdConfirmedSlots> {
        let mut sel = Select::new();
        sel.recv(gossip_vote_txs_receiver);
        sel.recv(replay_votes_receiver);
        let mut remaining_wait_time = Duration::from_millis(200);
        while remaining_wait_time > Duration::ZERO {
            let start = Instant::now();
            // Wait for one of the receivers to be ready. `ready_timeout`
            // will return if channels either have something, or are
            // disconnected. `ready_timeout` can wake up spuriously,
            // hence the loop
            let _ = sel.ready_timeout(remaining_wait_time)?;

            // Should not early return from this point onwards until `process_votes()`
            // returns below to avoid missing any potential `optimistic_confirmed_slots`
            let gossip_vote_txs: Vec<_> = gossip_vote_txs_receiver.try_iter().flatten().collect();
            let replay_votes: Vec<_> = replay_votes_receiver.try_iter().collect();
            if !gossip_vote_txs.is_empty() || !replay_votes.is_empty() {
                return Ok(Self::filter_and_confirm_with_new_votes(
                    vote_tracker,
                    gossip_vote_txs,
                    replay_votes,
                    root_bank,
                    subscriptions,
                    gossip_verified_vote_hash_sender,
                    verified_vote_sender,
                    bank_notification_sender,
                    duplicate_confirmed_slot_sender,
                    vote_processing_time,
                    latest_vote_slot_per_validator,
                ));
            }
            remaining_wait_time = remaining_wait_time.saturating_sub(start.elapsed());
        }
        Ok(vec![])
    }
#[allow(clippy::too_many_arguments)]
    fn filter_and_confirm_with_new_votes(
        vote_tracker: &VoteTracker,
        gossip_vote_txs: Vec<Transaction>,
        replayed_votes: Vec<ParsedVote>,
        root_bank: &Bank,
        subscriptions: &RpcSubscriptions,
        gossip_verified_vote_hash_sender: &GossipVerifiedVoteHashSender,
        verified_vote_sender: &VerifiedVoteSender,
        bank_notification_sender: &Option<BankNotificationSender>,
        duplicate_confirmed_slot_sender: &Option<DuplicateConfirmedSlotsSender>,
        vote_processing_time: &mut Option<VoteProcessingTiming>,
        latest_vote_slot_per_validator: &mut HashMap<Pubkey, Slot>,
    ) -> ThresholdConfirmedSlots {
        let mut diff: HashMap<Slot, HashMap<Pubkey, bool>> = HashMap::new();
        let mut new_optimistic_confirmed_slots = vec![];

        // Process votes from gossip and ReplayStage
        let mut gossip_vote_txn_processing_time = Measure::start("gossip_vote_processing_time");
        let votes = gossip_vote_txs
            .iter()
            .filter_map(vote_parser::parse_vote_transaction)
            .zip(repeat(/*is_gossip:*/ true))
            .chain(replayed_votes.into_iter().zip(repeat(/*is_gossip:*/ false)));
        for ((vote_pubkey, vote, _switch_proof, signature), is_gossip) in votes {
            Self::track_new_votes_and_notify_confirmations(
                vote,
                &vote_pubkey,
                signature,
                vote_tracker,
                root_bank,
                subscriptions,
                verified_vote_sender,
                gossip_verified_vote_hash_sender,
                &mut diff,
                &mut new_optimistic_confirmed_slots,
                is_gossip, // 是否通过gossip发送投票信息
                bank_notification_sender,
                duplicate_confirmed_slot_sender,
                latest_vote_slot_per_validator,
            );
        }
 #[allow(clippy::too_many_arguments)]
    fn track_new_votes_and_notify_confirmations(
        vote: VoteTransaction,
        vote_pubkey: &Pubkey,
        vote_transaction_signature: Signature,
        vote_tracker: &VoteTracker,
        root_bank: &Bank,
        subscriptions: &RpcSubscriptions,
        verified_vote_sender: &VerifiedVoteSender,
        gossip_verified_vote_hash_sender: &GossipVerifiedVoteHashSender,
        diff: &mut HashMap<Slot, HashMap<Pubkey, bool>>,
        new_optimistic_confirmed_slots: &mut ThresholdConfirmedSlots,
        is_gossip_vote: bool, // 是否通过gossip发送投票信息
        bank_notification_sender: &Option<BankNotificationSender>,
        duplicate_confirmed_slot_sender: &Option<DuplicateConfirmedSlotsSender>,
        latest_vote_slot_per_validator: &mut HashMap<Pubkey, Slot>,
    ) {
        if vote.is_empty() {
            return;
        }

        let (last_vote_slot, last_vote_hash) = vote.last_voted_slot_hash().unwrap();

        let latest_vote_slot = latest_vote_slot_per_validator
            .entry(*vote_pubkey)
            .or_insert(0);

        let root = root_bank.slot();
        let mut is_new_vote = false;
        let vote_slots = vote.slots();
        // If slot is before the root, ignore it
        for slot in vote_slots.iter().filter(|slot| **slot > root).rev() {
            let slot = *slot;

            // if we don't have stake information, ignore it
            let epoch = root_bank.epoch_schedule().get_epoch(slot);
            let epoch_stakes = root_bank.epoch_stakes(epoch);
            if epoch_stakes.is_none() {
                continue;
            }
            let epoch_stakes = epoch_stakes.unwrap();

            // The last vote slot, which is the greatest slot in the stack
            // of votes in a vote transaction, qualifies for optimistic confirmation.
            // We cannot count any other slots in this vote toward optimistic confirmation because:
            // 1) There may have been a switch between the earlier vote and the last vote
            // 2) We do not know the hash of the earlier slot
            if slot == last_vote_slot {
                let vote_accounts = epoch_stakes.stakes().vote_accounts();
                let stake = vote_accounts.get_delegated_stake(vote_pubkey);
                let total_stake = epoch_stakes.total_stake();

                // 快速处理投票交易中的最后一个时段
                // 以便可以尽快发送乐观确认通知。
                let (reached_threshold_results, is_new) = Self::track_optimistic_confirmation_vote(
                    vote_tracker,
                    last_vote_slot,
                    last_vote_hash,
                    *vote_pubkey,
                    stake,
                    total_stake,
                );

                if is_gossip_vote && is_new && stake > 0 {
                    let _ = gossip_verified_vote_hash_sender.send(( // 通过Gossip发送投票信息
                        *vote_pubkey,
                        last_vote_slot,
                        last_vote_hash,
                    ));
                }

                if reached_threshold_results[0] {
                    if let Some(sender) = duplicate_confirmed_slot_sender {
                        let _ = sender.send(vec![(last_vote_slot, last_vote_hash)]);
                    }
                }
                if reached_threshold_results[1] {
                    new_optimistic_confirmed_slots.push((last_vote_slot, last_vote_hash));
                    // Notify subscribers about new optimistic confirmation
                    if let Some(sender) = bank_notification_sender {
                        sender
                            .send(BankNotification::OptimisticallyConfirmed(last_vote_slot))
                            .unwrap_or_else(|err| {
                                warn!("bank_notification_sender failed: {:?}", err)
                            });
                    }
                }

                if !is_new && !is_gossip_vote {
                    // By now:
                    // 1) The vote must have come from ReplayStage,
                    // 2) We've seen this vote from replay for this hash before
                    // (`track_optimistic_confirmation_vote()` will not set `is_new == true`
                    // for same slot different hash), so short circuit because this vote
                    // has no new information

                    // Note gossip votes will always be processed because those should be unique
                    // and we need to update the gossip-only stake in the `VoteTracker`.
                    return;
                }

                is_new_vote = is_new;
            }

            if slot < *latest_vote_slot {
                // 重要的是,我们在 `last_vote_slot` 检查之后进行过滤,因为即使这个投票
                // 是旧的,我们仍然需要跟踪乐观确认。
                // 但是,可以过滤下面传播检查跟踪的其余槽,
                // 因为传播检查能够为后代汇总投票,这与乐观确认不同。
                continue;
            }

            diff.entry(slot)
                .or_default()
                .entry(*vote_pubkey)
                .and_modify(|seen_in_gossip_previously| {
                    *seen_in_gossip_previously = *seen_in_gossip_previously || is_gossip_vote
                })
                .or_insert(is_gossip_vote);
        }

        *latest_vote_slot = max(*latest_vote_slot, last_vote_slot);

        if is_new_vote {
            subscriptions.notify_vote(*vote_pubkey, vote, vote_transaction_signature);
            let _ = verified_vote_sender.send((*vote_pubkey, vote_slots));
        }
    }
fn track_optimistic_confirmation_vote(
        vote_tracker: &VoteTracker,
        slot: Slot,
        hash: Hash,
        pubkey: Pubkey,
        stake: u64,
        total_epoch_stake: u64,
    ) -> (Vec<bool>, bool) {
        let slot_tracker = vote_tracker.get_or_insert_slot_tracker(slot);
        // Insert vote and check for optimistic confirmation
        let mut w_slot_tracker = slot_tracker.write().unwrap();

        w_slot_tracker
            .get_or_insert_optimistic_votes_tracker(hash)
            .add_vote_pubkey(pubkey, stake, total_epoch_stake, &THRESHOLDS_TO_CHECK)
    }

core/src/consensus/vote_stake_tracker.rs

pub fn add_vote_pubkey(
        &mut self,
        vote_pubkey: Pubkey,
        stake: u64,
        total_stake: u64,
        thresholds_to_check: &[f64],
    ) -> (Vec<bool>, bool) {
        let is_new = !self.voted.contains(&vote_pubkey);
        if is_new {
            self.voted.insert(vote_pubkey);
            let old_stake = self.stake;
            let new_stake = self.stake + stake;
            self.stake = new_stake;
            let reached_threshold_results: Vec<bool> = thresholds_to_check
                .iter()
                .map(|threshold| {
                    let threshold_stake = (total_stake as f64 * threshold) as u64;
                    old_stake <= threshold_stake && threshold_stake < new_stake
                })
                .collect();
            (reached_threshold_results, is_new)
        } else {
            (vec![false; thresholds_to_check.len()], is_new)
        }
    }

为指定账户增加质押,并返回是否为首次加入

综合分析

validator 启动时会启动gossip通信,接收和转发收到的质押信息

Vote账户如何创建

查看一下cli工具命令代码
cli/src/cli.rs

pub fn process_command(config: &CliConfig) -> ProcessResult {
    if config.verbose && config.output_format == OutputFormat::DisplayVerbose {
        println_name_value("RPC URL:", &config.json_rpc_url);
        println_name_value("Default Signer Path:", &config.keypair_path);
        if config.keypair_path.starts_with("usb://") {
            let pubkey = config
                .pubkey()
                .map(|pubkey| format!("{pubkey:?}"))
                .unwrap_or_else(|_| "Unavailable".to_string());
            println_name_value("Pubkey:", &pubkey);
        }
        println_name_value("Commitment:", &config.commitment.commitment.to_string());
    }

    let rpc_client = if config.rpc_client.is_none() {
        Arc::new(RpcClient::new_with_timeouts_and_commitment(
            config.json_rpc_url.to_string(),
            config.rpc_timeout,
            config.commitment,
            config.confirm_transaction_initial_timeout,
        ))
    } else {
        // Primarily for testing
        config.rpc_client.as_ref().unwrap().clone()
    };

    match &config.command {
        ...
        // Create vote account
        CliCommand::CreateVoteAccount {
            vote_account,
            seed,
            identity_account,
            authorized_voter,
            authorized_withdrawer,
            commission,
            sign_only,
            dump_transaction_message,
            blockhash_query,
            ref nonce_account,
            nonce_authority,
            memo,
            fee_payer,
            compute_unit_price,
        } => process_create_vote_account(
            &rpc_client,
            config,
            *vote_account,
            seed,
            *identity_account,
            authorized_voter,
            *authorized_withdrawer,
            *commission,
            *sign_only,
            *dump_transaction_message,
            blockhash_query,
            nonce_account.as_ref(),
            *nonce_authority,
            memo.as_ref(),
            *fee_payer,
            compute_unit_price.as_ref(),
        )
        ...
    }
}

创建Vote账户交易的组装和发送

#[allow(clippy::too_many_arguments)]
pub fn process_create_vote_account(
    rpc_client: &RpcClient,
    config: &CliConfig,
    vote_account: SignerIndex,
    seed: &Option<String>,
    identity_account: SignerIndex,
    authorized_voter: &Option<Pubkey>,
    authorized_withdrawer: Pubkey,
    commission: u8,
    sign_only: bool,
    dump_transaction_message: bool,
    blockhash_query: &BlockhashQuery,
    nonce_account: Option<&Pubkey>,
    nonce_authority: SignerIndex,
    memo: Option<&String>,
    fee_payer: SignerIndex,
    compute_unit_price: Option<&u64>,
) -> ProcessResult {
    // vote_account账户信息获取
    let vote_account = config.signers[vote_account];
    let vote_account_pubkey = vote_account.pubkey();
    let vote_account_address = if let Some(seed) = seed {
        Pubkey::create_with_seed(&vote_account_pubkey, seed, &solana_vote_program::id())?
    } else {
        vote_account_pubkey
    };
    check_unique_pubkeys(
        (&config.signers[0].pubkey(), "cli keypair".to_string()),
        (&vote_account_address, "vote_account".to_string()),
    )?;

    // identity账户信息获取
    let identity_account = config.signers[identity_account];
    let identity_pubkey = identity_account.pubkey();
    check_unique_pubkeys(
        (&vote_account_address, "vote_account".to_string()),
        (&identity_pubkey, "identity_pubkey".to_string()),
    )?;
    // 所需余额计算
    let required_balance = rpc_client
        .get_minimum_balance_for_rent_exemption(VoteState::size_of())? // RPC链上获取 TODO 可以设置一个无限制?https://solana.com/docs/rpc/http/getminimumbalanceforrentexemption
        .max(1);
    let amount = SpendAmount::Some(required_balance);

    let fee_payer = config.signers[fee_payer];
    let nonce_authority = config.signers[nonce_authority];

    let is_feature_active = (!sign_only)
        .then(solana_sdk::feature_set::vote_state_add_vote_latency::id)
        .and_then(|feature_address| rpc_client.get_account(&feature_address).ok())
        .and_then(|account| feature::from_account(&account))
        .map_or(false, |feature| feature.activated_at.is_some());
    let space = VoteStateVersions::vote_state_size_of(is_feature_active) as u64;

    let build_message = |lamports| { // 组装交易信息
        let vote_init = VoteInit {
            node_pubkey: identity_pubkey,
            authorized_voter: authorized_voter.unwrap_or(identity_pubkey),
            authorized_withdrawer,
            commission,
        };
        let mut create_vote_account_config = CreateVoteAccountConfig {
            space,
            ..CreateVoteAccountConfig::default()
        };
        let to = if let Some(seed) = seed {
            create_vote_account_config.with_seed = Some((&vote_account_pubkey, seed));
            &vote_account_address
        } else {
            &vote_account_pubkey
        };

        let ixs = vote_instruction::create_account_with_config( // 组装具体交易
            &config.signers[0].pubkey(),
            to,
            &vote_init,
            lamports,
            create_vote_account_config,
        )
        .with_memo(memo)
        .with_compute_unit_price(compute_unit_price);

        if let Some(nonce_account) = &nonce_account {
            Message::new_with_nonce(
                ixs,
                Some(&fee_payer.pubkey()),
                nonce_account,
                &nonce_authority.pubkey(),
            )
        } else {
            Message::new(&ixs, Some(&fee_payer.pubkey()))
        }
    };

    let recent_blockhash = blockhash_query.get_blockhash(rpc_client, config.commitment)?;

    let (message, _) = resolve_spend_tx_and_check_account_balances(
        rpc_client,
        sign_only,
        amount,
        &recent_blockhash,
        &config.signers[0].pubkey(),
        &fee_payer.pubkey(),
        build_message,
        config.commitment,
    )?;

    if !sign_only {
        if let Ok(response) =
            rpc_client.get_account_with_commitment(&vote_account_address, config.commitment)
        {
            if let Some(vote_account) = response.value {
                let err_msg = if vote_account.owner == solana_vote_program::id() {
                    format!("Vote account {vote_account_address} already exists")
                } else {
                    format!(
                        "Account {vote_account_address} already exists and is not a vote account"
                    )
                };
                return Err(CliError::BadParameter(err_msg).into());
            }
        }

        if let Some(nonce_account) = &nonce_account {
            let nonce_account = solana_rpc_client_nonce_utils::get_account_with_commitment(
                rpc_client,
                nonce_account,
                config.commitment,
            )?;
            check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
        }
    }

    let mut tx = Transaction::new_unsigned(message);
    if sign_only {
        tx.try_partial_sign(&config.signers, recent_blockhash)?;
        return_signers_with_config(
            &tx,
            &config.output_format,
            &ReturnSignersConfig {
                dump_transaction_message,
            },
        )
    } else {
        tx.try_sign(&config.signers, recent_blockhash)?; // signers 进行签名
        let result = rpc_client.send_and_confirm_transaction_with_spinner(&tx); // 发送交易
        log_instruction_custom_error::<SystemError>(result, config)
    }
}
pub fn create_account_with_config(
    from_pubkey: &Pubkey,
    vote_pubkey: &Pubkey,
    vote_init: &VoteInit,
    lamports: u64,
    config: CreateVoteAccountConfig,
) -> Vec<Instruction> {
    let create_ix =
        system_instruction::create_account(from_pubkey, vote_pubkey, lamports, config.space, &id()); // 系统指令 类似于EVM native code 级别  SystemInstruction::CreateAccount
    let init_ix = initialize_account(vote_pubkey, vote_init); // 同上  VoteInstruction::InitializeAccount(
    vec![create_ix, init_ix]
}

sdk/program/src/system_instruction.rs

pub fn create_account(
    from_pubkey: &Pubkey,
    to_pubkey: &Pubkey,
    lamports: u64,
    space: u64,
    owner: &Pubkey,
) -> Instruction {
    let account_metas = vec![
        AccountMeta::new(*from_pubkey, true),
        AccountMeta::new(*to_pubkey, true),
    ];
    Instruction::new_with_bincode(
        system_program::id(),
        &SystemInstruction::CreateAccount {
            lamports,
            space,
            owner: *owner,
        },
        account_metas,
    )
}

总结

本想直接禁用底层的执行方法,和EVM还不一样,对应方法是通用的

pub fn create_account_with_activation_epoch(
    authorized: &Pubkey,
    voter_pubkey: &Pubkey,
    vote_account: &AccountSharedData,
    rent: &Rent,
    lamports: u64,
    activation_epoch: Epoch,
) -> AccountSharedData {
    do_create_account(
        authorized,
        voter_pubkey,
        vote_account,
        rent,
        lamports,
        activation_epoch,
    )
}

// TODO 还没分析完,先保存,稍后再继续

附加

使用 Solana CLI 质押 SOL

https://docs.anza.xyz/cli/examples/delegate-stake

投票账户管理

solana-cli 中与质押和验证者有关的参数

参数 解释
close-vote-account 关闭投票账户并提取所有剩余资金
create-stake-account 创建质押账户
create-stake-account-checked 创建一个质押账户,检查作为签名者的提现权限
create-vote-account 创建投票账户
deactivate-stake 从质押账户中取消委托质押
delegate-stake 将质押委托给投票账户
split-stake 复制一个质押账户,将代币拆分到两个账户中
stake-account 显示质押账户的内容
stake-authorize 为给定的质押账户授权新的签名密钥对
stake-authorize-checked 为给定的质押账户授权新的签名密钥对,检查作为签名者的权限
stake-history 显示质押历史记录
stake-minimum-delegation 获取质押最低委托额度
stake-set-lockup 设置质押账户锁仓
stake-set-lockup-checked 设置质押账户的 Lockup,检查作为签名者的新权限
stakes 显示质押账户信息
validator-info 发布~获取 Solana 上的验证者信息
validators 显示当前验证器的摘要信息
verify-offchain-signature 验证链下消息签名
vote-account 显示投票账户的内容
vote-authorize-voter 为给定的投票账户授权新的投票签名密钥对
vote-authorize-voter-checked 为给定的投票账户授权新的投票签名密钥对,检查作为签名者的新权限
vote-authorize-withdrawer 为给定的投票账户授权新的提款签名密钥对
vote-authorize-withdrawer-checked 为给定的投票账户授权新的提款签名密钥对,检查作为签名者的新权限
vote-update-commission 更新投票账户的佣金
vote-update-validator 更新投票账户的验证者身份
wait-for-max-stake 等待任何一个节点的最大股权降至总股权的一定百分比以下。
withdraw-from-nonce-account 从 nonce 账户中提取 SOL
withdraw-from-vote-account 将投票账户中的 lamport 提现到指定账户
withdraw-stake 从质押账户中提取未质押的 SOL

Solana FeeStructure(综合费用) 计算手续费和优先级费用

计算交易打包检查

core/src/banking_stage/transaction_scheduler/scheduler_controller.rs

 fn buffer_packets(&mut self, packets: Vec<ImmutableDeserializedPacket>) {
     ...
      for ((transaction, fee_budget_limits), _) in transactions
                .into_iter()
                .zip(fee_budget_limits_vec)
                .zip(check_results)
                .filter(|(_, check_result)| check_result.0.is_ok())
            {
            ...
            let transaction_id = self.transaction_id_generator.next();

                let (priority, cost) =
                    Self::calculate_priority_and_cost(&transaction, &fee_budget_limits, &bank);// 计算优先级和手续费费用
                let transaction_ttl = SanitizedTransactionTTL {
                    transaction,
                    max_age_slot: last_slot_in_epoch,
                };

                if self.container.insert_new_transaction(
                    transaction_id,
                    transaction_ttl,
                    priority,
                    cost,
                ) {

计算交易所需的Fee

fn calculate_priority_and_cost(
        transaction: &SanitizedTransaction,
        fee_budget_limits: &FeeBudgetLimits,
        bank: &Bank,
    ) -> (u64, u64) {
        let cost = CostModel::calculate_cost(transaction, &bank.feature_set).sum(); 
        let fee = bank.fee_structure.calculate_fee(
            transaction.message(),
            5_000, // this just needs to be non-zero
            fee_budget_limits,
            bank.feature_set
                .is_active(&include_loaded_accounts_data_size_in_fee_calculation::id()),
            bank.feature_set
                .is_active(&remove_rounding_in_fee_calculation::id()),
        );

        // 我们在这里需要一个乘数,以避免过度向下舍入。
        // 对于许多交易来说,成本将高于原始 Lamport 的费用。
        // 为了计算优先级,我们将费用乘以一个大数,以便成本只是一小部分。
        // 分母中使用 1 的偏移量来明确避免除以零。
        const MULTIPLIER: u64 = 1_000_000;
        (
            fee.saturating_mul(MULTIPLIER)
                .saturating_div(cost.saturating_add(1)), // 优先级费用
            cost, // 交易fee
        )
    }

从返回值作用,cost等于交易的Fee, 而calculate_fee计算的fee用于计算交易的优先级

计算Cost

cost-model/src/cost_model.rs

pub fn calculate_cost(
        transaction: &SanitizedTransaction,
        feature_set: &FeatureSet,
    ) -> TransactionCost {
        if transaction.is_simple_vote_transaction() { // 判断是否为简单的验证者投票交易
            TransactionCost::SimpleVote {
                writable_accounts: Self::get_writable_accounts(transaction),
            }
        } else { // 常规交易
            let mut tx_cost = UsageCostDetails::new_with_default_capacity();

            Self::get_signature_cost(&mut tx_cost, transaction);
            Self::get_write_lock_cost(&mut tx_cost, transaction, feature_set);
            Self::get_transaction_cost(&mut tx_cost, transaction, feature_set);
            tx_cost.account_data_size = Self::calculate_account_data_size(transaction);

            debug!("transaction {:?} has cost {:?}", transaction, tx_cost);
            TransactionCost::Transaction(tx_cost)
        }
    }
impl Default for UsageCostDetails {
    fn default() -> Self {
        Self {
            writable_accounts: Vec::with_capacity(MAX_WRITABLE_ACCOUNTS),
            signature_cost: 0u64,
            write_lock_cost: 0u64,
            data_bytes_cost: 0u64,
            programs_execution_cost: 0u64,
            loaded_accounts_data_size_cost: 0u64,
            account_data_size: 0u64,
            num_transaction_signatures: 0u64,
            num_secp256k1_instruction_signatures: 0u64,
            num_ed25519_instruction_signatures: 0u64,
        }
    }
}

综合费用因素计算

  1. 签名数量
    每个签名固定费率
  2. 写锁数量
    每个可写账户的固定利率
  3. 数据字节成本
    所有交易指令数据的长度总和的每字节固定费率
  4. 账户规模
    账户规模无法预先得知,但可以占到交易在网络上产生的相当一部分负载。付款人将预先支付最大账户规模(1000 万)的费用,并在实际账户规模得知后退还差额。
  5. 计算预算
    每笔交易将获得默认的交易范围计算预算 20 万个单位,并可选择通过计算预算指令请求更大的预算,最高可达 100 万个单位。此预算用于限制处理交易所需的时间。费用的计算预算部分将根据默认或请求的金额预先收取。处理后,将知道实际消耗的单位数,并将向付款人退还差额,因此付款人只需支付他们使用的费用。内置程序将具有固定成本,而 SBF 程序的成本将在运行时衡量。
  6. 预编译程序
    预编译程序正在执行计算密集型操作。预编译程序所产生的工作可以根据指令的数据数组进行预测。因此,将根据指令数据的解析为每个预编译程序分配成本。由于预编译程序是在银行之外处理的,因此它们的计算成本不会反映在计算预算中,也不会用于事务调度决策。

计算Fee

sdk/src/fee.rs

#[cfg(not(target_os = "solana"))]
    pub fn calculate_fee(
        &self,
        message: &SanitizedMessage,
        lamports_per_signature: u64,
        budget_limits: &FeeBudgetLimits,
        include_loaded_account_data_size_in_fee: bool,
        remove_rounding_in_fee_calculation: bool,
    ) -> u64 {
        // Fee based on compute units and signatures
        let congestion_multiplier = if lamports_per_signature == 0 {
            0 // test only
        } else {
            1 // multiplier that has no effect
        };

        self.calculate_fee_details(
            message,
            budget_limits,
            include_loaded_account_data_size_in_fee,
        )
        .total_fee(remove_rounding_in_fee_calculation)
        .saturating_mul(congestion_multiplier)
    }
#[cfg(not(target_os = "solana"))]
    pub fn calculate_fee_details(
        &self,
        message: &SanitizedMessage,
        budget_limits: &FeeBudgetLimits,
        include_loaded_account_data_size_in_fee: bool,
    ) -> FeeDetails {
        let signature_fee = message
            .num_signatures()
            .saturating_mul(self.lamports_per_signature);
        let write_lock_fee = message
            .num_write_locks()
            .saturating_mul(self.lamports_per_write_lock);

        // `compute_fee` covers costs for both requested_compute_units and
        // requested_loaded_account_data_size
        let loaded_accounts_data_size_cost = if include_loaded_account_data_size_in_fee {
            FeeStructure::calculate_memory_usage_cost(
                budget_limits.loaded_accounts_data_size_limit,
                budget_limits.heap_cost,
            )
        } else {
            0_u64
        };
        let total_compute_units =
            loaded_accounts_data_size_cost.saturating_add(budget_limits.compute_unit_limit);
        let compute_fee = self
            .compute_fee_bins
            .iter()
            .find(|bin| total_compute_units <= bin.limit)
            .map(|bin| bin.fee)
            .unwrap_or_else(|| {
                self.compute_fee_bins
                    .last()
                    .map(|bin| bin.fee)
                    .unwrap_or_default()
            });

        FeeDetails {
            transaction_fee: signature_fee
                .saturating_add(write_lock_fee)
                .saturating_add(compute_fee),
            prioritization_fee: budget_limits.prioritization_fee,
        }
    }

交易打包余额检查

交易执行检查时,一样会计算交易的fee,确保支付地址余额充足

core/src/banking_stage/unprocessed_transaction_storage.rs

fn consume_scan_should_process_packet(
    bank: &Bank,
    banking_stage_stats: &BankingStageStats,
    packet: &ImmutableDeserializedPacket,
    payload: &mut ConsumeScannerPayload,
) -> ProcessingDecision {
    ...
        // 仅当我们确实可以采取锁定时才检查费用支付者
        // 我们不会在此立即丢弃检查锁定失败,
        // 因为优先级保护要求我们始终采取锁定
        // 除非在丢弃交易的情况下(即“从不”)。
        if payload.account_locks.check_locks(message)
            && Consumer::check_fee_payer_unlocked(bank, message, &mut payload.error_counters)
                .is_err()
        {
            payload
                .message_hash_to_transaction
                .remove(packet.message_hash());
            return ProcessingDecision::Never;
        }

core/src/banking_stage/consumer.rs

pub fn check_fee_payer_unlocked(
        bank: &Bank,
        message: &SanitizedMessage,
        error_counters: &mut TransactionErrorMetrics,
    ) -> Result<(), TransactionError> {
        let fee_payer = message.fee_payer();
        let budget_limits =
            process_compute_budget_instructions(message.program_instructions_iter())?.into();
        let fee = bank.fee_structure.calculate_fee( // 同上,计算交易的fee
            message,
            bank.get_lamports_per_signature(),
            &budget_limits,
            bank.feature_set.is_active(
                &feature_set::include_loaded_accounts_data_size_in_fee_calculation::id(),
            ),
            bank.feature_set
                .is_active(&feature_set::remove_rounding_in_fee_calculation::id()),
        );
        let (mut fee_payer_account, _slot) = bank
            .rc
            .accounts
            .accounts_db
            .load_with_fixed_root(&bank.ancestors, fee_payer)
            .ok_or(TransactionError::AccountNotFound)?;

        validate_fee_payer(
            fee_payer,
            &mut fee_payer_account,
            0,
            error_counters,
            bank.rent_collector(),
            fee,
        )
    }

Solana genesis参数target_lamports_per_signature如何强制设置

背景

Solana默认初始化手续费价格是在创建genesis时通过--target-lamports-per-signature参数传入的,但是后期可能需要强制调整,需要分析下代码,看下如果修改

数据写入和读取

先分析下现有逻辑参数数据的整体写入和读取逻辑

写入

genesis/src/main.rs

...
.arg( // 通过参数传入
    Arg::with_name("target_lamports_per_signature")
    .long("target-lamports-per-signature")
    .value_name("LAMPORTS")
    .takes_value(true)
    .default_value(default_target_lamports_per_signature)
    .help(
    "The cost in lamports that the cluster will charge for signature \
    verification when the cluster is operating at target-signatures-per-slot",
    ),
)
...
 let mut fee_rate_governor = FeeRateGovernor::new(
        value_t_or_exit!(matches, "target_lamports_per_signature", u64), // 获取参数值
        value_t_or_exit!(matches, "target_signatures_per_slot", u64),
    );
...
let mut genesis_config = GenesisConfig {
        native_instruction_processors: vec![],
        ticks_per_slot,
        poh_config,
        fee_rate_governor, // 组装genesis数据
        rent,
        epoch_schedule,
        cluster_type,
        ..GenesisConfig::default()
    };
...
create_new_ledger( // 创建存储
        &ledger_path,
        &genesis_config,
        max_genesis_archive_unpacked_size,
        LedgerColumnOptions::default(),
    )?;
pub fn create_new_ledger(
    ledger_path: &Path,
    genesis_config: &GenesisConfig,
    max_genesis_archive_unpacked_size: u64,
    column_options: LedgerColumnOptions,
) -> Result<Hash> {
    Blockstore::destroy(ledger_path)?;
    genesis_config.write(ledger_path)?; // 写入 genesis
    ...
    // 用链接回 genesis_config 的刻度填充插槽 0,以引导分类账。
     let blockstore_dir = column_options.shred_storage_type.blockstore_directory();
    let blockstore = Blockstore::open_with_options(
        ledger_path,
        BlockstoreOptions {
            access_type: AccessType::Primary,
            recovery_mode: None,
            enforce_ulimit_nofile: false,
            column_options: column_options.clone(),
        },
    )?;
    ...
    blockstore.insert_shreds(shreds, None, false)?;
    blockstore.set_roots(std::iter::once(&0))?;
    // Explicitly close the blockstore before we create the archived genesis file
    drop(blockstore);

Solana geneis 初始化步骤和 geth init 基本一致,写入genesis文件,以及初始化第一个slot

读取

sdk/program/src/fee_calculator.rs

pub fn new_derived(
        base_fee_rate_governor: &FeeRateGovernor,
        latest_signatures_per_slot: u64,
    ) -> Self {
        let mut me = base_fee_rate_governor.clone();

        if me.target_signatures_per_slot > 0 {
            // lamports_per_signature can range from 50% to 1000% of
            // target_lamports_per_signature
            me.min_lamports_per_signature = std::cmp::max(1, me.target_lamports_per_signature / 2); // 主要看下target_lamports_per_signature的初始化读取位置
            me.max_lamports_per_signature = me.target_lamports_per_signature * 10;
impl FeeRateGovernor {
    pub fn new(target_lamports_per_signature: u64, target_signatures_per_slot: u64) -> Self {
        let base_fee_rate_governor = Self {
            target_lamports_per_signature,
            lamports_per_signature: target_lamports_per_signature,

runtime/src/bank.rs

n _new_from_parent(
        parent: Arc<Bank>,
        collector_id: &Pubkey,
        slot: Slot,
        reward_calc_tracer: Option<impl RewardCalcTracer>,
        new_bank_options: NewBankOptions,
    ) -> Self {
    ...
     let (fee_rate_governor, fee_components_time_us) = measure_us!(
            FeeRateGovernor::new_derived(&parent.fee_rate_governor, parent.signature_count())
        );

从parent.fee_rate_governor获取

ledger/src/blockstore_processor.rs

fn process_next_slots(
    bank: &Arc<Bank>,
    meta: &SlotMeta,
    blockstore: &Blockstore,
    leader_schedule_cache: &LeaderScheduleCache,
    pending_slots: &mut Vec<(SlotMeta, Bank, Hash)>,
    halt_at_slot: Option<Slot>,
) -> result::Result<(), BlockstoreProcessorError> {
    if meta.next_slots.is_empty() {
        return Ok(());
    }
    ...
    if next_meta.is_full() {
            let next_bank = Bank::new_from_parent( // 读取数据
                bank.clone(),
                &leader_schedule_cache
                    .slot_leader_at(*next_slot, Some(bank))
                    .unwrap(),
                *next_slot,
            );
            trace!(
                "New bank for slot {}, parent slot is {}",
                next_slot,
                bank.slot(),
            );
            pending_slots.push((next_meta, next_bank, bank.last_blockhash()));
        }

综合分析

下一个FeeRateGovernor都是从前一个Slot读出的,依次迭代更新,现有逻辑无法后期设置,且模块和代码耦合较深

强制更改的方案

  1. 截获原有迭代更新的逻辑:FeeRateGovernor是附加模块,并且逻辑耦合较深,并且可能引起Slot校验等问题,不适合
  2. 固定设置:对于作为L2的场景,节点权限可控,并且版本稳定后,价格更新频率较低,简单有效

固定设置-代码定制

原则:最小改动,最小风险,优先使用

pub const MIN_TARGET_LAMPORTS_PER_SIGNATURE: u64 = 0;
pub const MAX_TARGET_LAMPORTS_PER_SIGNATURE: u64 = 0;

代码修改后

pub fn new_derived(
        base_fee_rate_governor: &FeeRateGovernor,
        latest_signatures_per_slot: u64,
    ) -> Self {
        let mut me = base_fee_rate_governor.clone();

        if me.target_signatures_per_slot > 0 && DEFAULT_TARGET_SIGNATURES_PER_SLOT > 0{
            // lamports_per_signature can range from 50% to 1000% of
            // target_lamports_per_signature
            // Support mandatory settings
            if MIN_TARGET_LAMPORTS_PER_SIGNATURE > 0 {
                me.min_lamports_per_signature = MIN_TARGET_LAMPORTS_PER_SIGNATURE;
            } else {
                me.min_lamports_per_signature = std::cmp::max(1, me.target_lamports_per_signature / 2);
            }

            if MAX_TARGET_LAMPORTS_PER_SIGNATURE > 0 {
                me.max_lamports_per_signature = MAX_TARGET_LAMPORTS_PER_SIGNATURE;
            } else {
                me.max_lamports_per_signature = me.target_lamports_per_signature * 10;
            }
            // Prevent a single setting from causing high data non-compliance
            if me.min_lamports_per_signature > me.max_lamports_per_signature {
                me.min_lamports_per_signature = me.max_lamports_per_signature;
            }

            // What the cluster should charge at `latest_signatures_per_slot`
            let desired_lamports_per_signature =
                me.max_lamports_per_signature
                    .min(me.min_lamports_per_signature.max(
                        me.target_lamports_per_signature
                            * std::cmp::min(latest_signatures_per_slot, std::u32::MAX as u64)
                            / me.target_signatures_per_slot,
                    ));

            trace!(
                "desired_lamports_per_signature: {}",
                desired_lamports_per_signature
            );

            let gap = desired_lamports_per_signature as i64
                - base_fee_rate_governor.lamports_per_signature as i64;

            if gap == 0 {
                me.lamports_per_signature = desired_lamports_per_signature;
            } else {
                // Adjust fee by 5% of target_lamports_per_signature to produce a smooth
                // increase/decrease in fees over time.
                let gap_adjust =
                    std::cmp::max(1, me.target_lamports_per_signature / 20) as i64 * gap.signum();

                trace!(
                    "lamports_per_signature gap is {}, adjusting by {}",
                    gap,
                    gap_adjust
                );

                me.lamports_per_signature =
                    me.max_lamports_per_signature
                        .min(me.min_lamports_per_signature.max(
                            (base_fee_rate_governor.lamports_per_signature as i64 + gap_adjust)
                                as u64,
                        ));
            }
        } else {
            me.lamports_per_signature = base_fee_rate_governor.target_lamports_per_signature;

            if MIN_TARGET_LAMPORTS_PER_SIGNATURE > 0 && me.lamports_per_signature < MIN_TARGET_LAMPORTS_PER_SIGNATURE {
                me.lamports_per_signature = MIN_TARGET_LAMPORTS_PER_SIGNATURE
            } else if MAX_TARGET_LAMPORTS_PER_SIGNATURE > 0 && me.lamports_per_signature > MAX_TARGET_LAMPORTS_PER_SIGNATURE {
                me.lamports_per_signature = MAX_TARGET_LAMPORTS_PER_SIGNATURE
            }

            me.min_lamports_per_signature = me.target_lamports_per_signature;
            me.max_lamports_per_signature = me.target_lamports_per_signature;
        }
        debug!(
            "new_derived(): lamports_per_signature: {}",
            me.lamports_per_signature
        );
        me
    }

Diff

 sdk/program/src/fee_calculator.rs | 30 +++++++++++++++++++++++++++---
 1 file changed, 27 insertions(+), 3 deletions(-)

diff --git a/sdk/program/src/fee_calculator.rs b/sdk/program/src/fee_calculator.rs
index c4dcb572cf..fc4415f2ab 100644
--- a/sdk/program/src/fee_calculator.rs
+++ b/sdk/program/src/fee_calculator.rs
@@ -75,6 +75,9 @@ pub struct FeeRateGovernor {
 pub const DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE: u64 = 10_000;
 pub const DEFAULT_TARGET_SIGNATURES_PER_SLOT: u64 = 50 * DEFAULT_MS_PER_SLOT;

+pub const MIN_TARGET_LAMPORTS_PER_SIGNATURE: u64 = 0;
+pub const MAX_TARGET_LAMPORTS_PER_SIGNATURE: u64 = 0;
+
 // Percentage of tx fees to burn
 pub const DEFAULT_BURN_PERCENT: u8 = 50;

@@ -109,11 +112,25 @@ impl FeeRateGovernor {
     ) -> Self {
         let mut me = base_fee_rate_governor.clone();

-        if me.target_signatures_per_slot > 0 {
+        if me.target_signatures_per_slot > 0 && DEFAULT_TARGET_SIGNATURES_PER_SLOT > 0{
             // lamports_per_signature can range from 50% to 1000% of
             // target_lamports_per_signature
-            me.min_lamports_per_signature = std::cmp::max(1, me.target_lamports_per_signature / 2);
-            me.max_lamports_per_signature = me.target_lamports_per_signature * 10;
+            // Support mandatory settings
+            if MIN_TARGET_LAMPORTS_PER_SIGNATURE > 0 {
+                me.min_lamports_per_signature = MIN_TARGET_LAMPORTS_PER_SIGNATURE;
+            } else {
+                me.min_lamports_per_signature = std::cmp::max(1, me.target_lamports_per_signature / 2);
+            }
+
+            if MAX_TARGET_LAMPORTS_PER_SIGNATURE > 0 {
+                me.max_lamports_per_signature = MAX_TARGET_LAMPORTS_PER_SIGNATURE;
+            } else {
+                me.max_lamports_per_signature = me.target_lamports_per_signature * 10;
+            }
+            // Prevent a single setting from causing high data non-compliance
+            if me.min_lamports_per_signature > me.max_lamports_per_signature {
+                me.min_lamports_per_signature = me.max_lamports_per_signature;
+            }

             // What the cluster should charge at `latest_signatures_per_slot`
             let desired_lamports_per_signature =
@@ -155,6 +172,13 @@ impl FeeRateGovernor {
             }
         } else {
             me.lamports_per_signature = base_fee_rate_governor.target_lamports_per_signature;
+
+            if MIN_TARGET_LAMPORTS_PER_SIGNATURE > 0 && me.lamports_per_signature < MIN_TARGET_LAMPORTS_PER_SIGNATURE {
+                me.lamports_per_signature = MIN_TARGET_LAMPORTS_PER_SIGNATURE
+            } else if MAX_TARGET_LAMPORTS_PER_SIGNATURE > 0 && me.lamports_per_signature > MAX_TARGET_LAMPORTS_PER_SIGNATURE {
+                me.lamports_per_signature = MAX_TARGET_LAMPORTS_PER_SIGNATURE
+            }
+
             me.min_lamports_per_signature = me.target_lamports_per_signature;
             me.max_lamports_per_signature = me.target_lamports_per_signature;
         }

控制介绍

假设目前已经基于genesis使用前期参数初始化,并启动测试网

  1. 通过控制DEFAULT_TARGET_SIGNATURES_PER_SLOT是否为0,强制切换是否动态调节lamports_per_signature
  2. 通过设置MIN_TARGET_LAMPORTS_PER_SIGNATURE和MAX_TARGET_LAMPORTS_PER_SIGNATURE,强制设置lamports_per_signature的最小和最大允许值范围