Update airdrop tokens to 3 for fullnode (#2051)
Filter out leader while computing the super majority stake
This commit is contained in:
parent
f5569e76db
commit
2de45a4da5
|
@ -15,7 +15,7 @@ use solana::mint::Mint;
|
||||||
use solana::packet::to_packets_chunked;
|
use solana::packet::to_packets_chunked;
|
||||||
use solana_sdk::hash::hash;
|
use solana_sdk::hash::hash;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
use solana_sdk::signature::{KeypairUtil, Signature};
|
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
|
||||||
use solana_sdk::system_transaction::SystemTransaction;
|
use solana_sdk::system_transaction::SystemTransaction;
|
||||||
use solana_sdk::transaction::Transaction;
|
use solana_sdk::transaction::Transaction;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
|
@ -50,6 +50,7 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||||
|
|
||||||
let (verified_sender, verified_receiver) = channel();
|
let (verified_sender, verified_receiver) = channel();
|
||||||
let bank = Arc::new(Bank::new(&mint));
|
let bank = Arc::new(Bank::new(&mint));
|
||||||
|
let dummy_leader_id = Keypair::new().pubkey();
|
||||||
let dummy = Transaction::system_move(
|
let dummy = Transaction::system_move(
|
||||||
&mint.keypair(),
|
&mint.keypair(),
|
||||||
mint.keypair().pubkey(),
|
mint.keypair().pubkey(),
|
||||||
|
@ -107,6 +108,7 @@ fn bench_banking_stage_multi_accounts(bencher: &mut Bencher) {
|
||||||
Default::default(),
|
Default::default(),
|
||||||
&mint.last_id(),
|
&mint.last_id(),
|
||||||
None,
|
None,
|
||||||
|
dummy_leader_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut id = mint.last_id();
|
let mut id = mint.last_id();
|
||||||
|
@ -137,6 +139,7 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||||
|
|
||||||
let (verified_sender, verified_receiver) = channel();
|
let (verified_sender, verified_receiver) = channel();
|
||||||
let bank = Arc::new(Bank::new(&mint));
|
let bank = Arc::new(Bank::new(&mint));
|
||||||
|
let dummy_leader_id = Keypair::new().pubkey();
|
||||||
let dummy = Transaction::system_move(
|
let dummy = Transaction::system_move(
|
||||||
&mint.keypair(),
|
&mint.keypair(),
|
||||||
mint.keypair().pubkey(),
|
mint.keypair().pubkey(),
|
||||||
|
@ -209,6 +212,7 @@ fn bench_banking_stage_multi_programs(bencher: &mut Bencher) {
|
||||||
Default::default(),
|
Default::default(),
|
||||||
&mint.last_id(),
|
&mint.last_id(),
|
||||||
None,
|
None,
|
||||||
|
dummy_leader_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut id = mint.last_id();
|
let mut id = mint.last_id();
|
||||||
|
|
|
@ -163,13 +163,14 @@ $rsync -vPr "$rsync_leader_url"/config/ "$ledger_config_dir"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# A fullnode requires 2 tokens to function:
|
# A fullnode requires 3 tokens to function:
|
||||||
# - one token to create an instance of the vote_program with
|
# - one token to create an instance of the vote_program with
|
||||||
# - one second token to keep the node identity public key valid.
|
# - one token for the transaction fee
|
||||||
|
# - one token to keep the node identity public key valid.
|
||||||
$solana_wallet \
|
$solana_wallet \
|
||||||
--keypair "$fullnode_id_path" \
|
--keypair "$fullnode_id_path" \
|
||||||
--network "$leader_address" \
|
--network "$leader_address" \
|
||||||
airdrop 2
|
airdrop 3
|
||||||
|
|
||||||
trap 'kill "$pid" && wait "$pid"' INT TERM
|
trap 'kill "$pid" && wait "$pid"' INT TERM
|
||||||
$program \
|
$program \
|
||||||
|
|
|
@ -15,6 +15,7 @@ use result::{Error, Result};
|
||||||
use service::Service;
|
use service::Service;
|
||||||
use sigverify_stage::VerifiedPackets;
|
use sigverify_stage::VerifiedPackets;
|
||||||
use solana_sdk::hash::Hash;
|
use solana_sdk::hash::Hash;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
use solana_sdk::timing;
|
use solana_sdk::timing;
|
||||||
use solana_sdk::transaction::Transaction;
|
use solana_sdk::transaction::Transaction;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
@ -51,6 +52,7 @@ impl BankingStage {
|
||||||
config: Config,
|
config: Config,
|
||||||
last_entry_id: &Hash,
|
last_entry_id: &Hash,
|
||||||
max_tick_height: Option<u64>,
|
max_tick_height: Option<u64>,
|
||||||
|
leader_id: Pubkey,
|
||||||
) -> (Self, Receiver<Vec<Entry>>) {
|
) -> (Self, Receiver<Vec<Entry>>) {
|
||||||
let (entry_sender, entry_receiver) = channel();
|
let (entry_sender, entry_receiver) = channel();
|
||||||
let shared_verified_receiver = Arc::new(Mutex::new(verified_receiver));
|
let shared_verified_receiver = Arc::new(Mutex::new(verified_receiver));
|
||||||
|
@ -63,8 +65,11 @@ impl BankingStage {
|
||||||
let poh_service = PohService::new(poh_recorder.clone(), config);
|
let poh_service = PohService::new(poh_recorder.clone(), config);
|
||||||
|
|
||||||
// Single thread to compute finality
|
// Single thread to compute finality
|
||||||
let compute_finality_service =
|
let compute_finality_service = ComputeLeaderFinalityService::new(
|
||||||
ComputeLeaderFinalityService::new(bank.clone(), poh_service.poh_exit.clone());
|
bank.clone(),
|
||||||
|
leader_id,
|
||||||
|
poh_service.poh_exit.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
// Many banks that process transactions in parallel.
|
// Many banks that process transactions in parallel.
|
||||||
let bank_thread_hdls: Vec<JoinHandle<Option<BankingStageReturnType>>> = (0..NUM_THREADS)
|
let bank_thread_hdls: Vec<JoinHandle<Option<BankingStageReturnType>>> = (0..NUM_THREADS)
|
||||||
|
@ -268,6 +273,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_banking_stage_shutdown1() {
|
fn test_banking_stage_shutdown1() {
|
||||||
let bank = Arc::new(Bank::new(&Mint::new(2)));
|
let bank = Arc::new(Bank::new(&Mint::new(2)));
|
||||||
|
let dummy_leader_id = Keypair::new().pubkey();
|
||||||
let (verified_sender, verified_receiver) = channel();
|
let (verified_sender, verified_receiver) = channel();
|
||||||
let (banking_stage, _entry_receiver) = BankingStage::new(
|
let (banking_stage, _entry_receiver) = BankingStage::new(
|
||||||
&bank,
|
&bank,
|
||||||
|
@ -275,6 +281,7 @@ mod tests {
|
||||||
Default::default(),
|
Default::default(),
|
||||||
&bank.last_id(),
|
&bank.last_id(),
|
||||||
None,
|
None,
|
||||||
|
dummy_leader_id,
|
||||||
);
|
);
|
||||||
drop(verified_sender);
|
drop(verified_sender);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -286,6 +293,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_banking_stage_shutdown2() {
|
fn test_banking_stage_shutdown2() {
|
||||||
let bank = Arc::new(Bank::new(&Mint::new(2)));
|
let bank = Arc::new(Bank::new(&Mint::new(2)));
|
||||||
|
let dummy_leader_id = Keypair::new().pubkey();
|
||||||
let (_verified_sender, verified_receiver) = channel();
|
let (_verified_sender, verified_receiver) = channel();
|
||||||
let (banking_stage, entry_receiver) = BankingStage::new(
|
let (banking_stage, entry_receiver) = BankingStage::new(
|
||||||
&bank,
|
&bank,
|
||||||
|
@ -293,6 +301,7 @@ mod tests {
|
||||||
Default::default(),
|
Default::default(),
|
||||||
&bank.last_id(),
|
&bank.last_id(),
|
||||||
None,
|
None,
|
||||||
|
dummy_leader_id,
|
||||||
);
|
);
|
||||||
drop(entry_receiver);
|
drop(entry_receiver);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -304,6 +313,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_banking_stage_tick() {
|
fn test_banking_stage_tick() {
|
||||||
let bank = Arc::new(Bank::new(&Mint::new(2)));
|
let bank = Arc::new(Bank::new(&Mint::new(2)));
|
||||||
|
let dummy_leader_id = Keypair::new().pubkey();
|
||||||
let start_hash = bank.last_id();
|
let start_hash = bank.last_id();
|
||||||
let (verified_sender, verified_receiver) = channel();
|
let (verified_sender, verified_receiver) = channel();
|
||||||
let (banking_stage, entry_receiver) = BankingStage::new(
|
let (banking_stage, entry_receiver) = BankingStage::new(
|
||||||
|
@ -312,6 +322,7 @@ mod tests {
|
||||||
Config::Sleep(Duration::from_millis(1)),
|
Config::Sleep(Duration::from_millis(1)),
|
||||||
&bank.last_id(),
|
&bank.last_id(),
|
||||||
None,
|
None,
|
||||||
|
dummy_leader_id,
|
||||||
);
|
);
|
||||||
sleep(Duration::from_millis(500));
|
sleep(Duration::from_millis(500));
|
||||||
drop(verified_sender);
|
drop(verified_sender);
|
||||||
|
@ -330,6 +341,7 @@ mod tests {
|
||||||
fn test_banking_stage_entries_only() {
|
fn test_banking_stage_entries_only() {
|
||||||
let mint = Mint::new(2);
|
let mint = Mint::new(2);
|
||||||
let bank = Arc::new(Bank::new(&mint));
|
let bank = Arc::new(Bank::new(&mint));
|
||||||
|
let dummy_leader_id = Keypair::new().pubkey();
|
||||||
let start_hash = bank.last_id();
|
let start_hash = bank.last_id();
|
||||||
let (verified_sender, verified_receiver) = channel();
|
let (verified_sender, verified_receiver) = channel();
|
||||||
let (banking_stage, entry_receiver) = BankingStage::new(
|
let (banking_stage, entry_receiver) = BankingStage::new(
|
||||||
|
@ -338,6 +350,7 @@ mod tests {
|
||||||
Default::default(),
|
Default::default(),
|
||||||
&bank.last_id(),
|
&bank.last_id(),
|
||||||
None,
|
None,
|
||||||
|
dummy_leader_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// good tx
|
// good tx
|
||||||
|
@ -385,6 +398,7 @@ mod tests {
|
||||||
// Entry OR if the verifier tries to parallelize across multiple Entries.
|
// Entry OR if the verifier tries to parallelize across multiple Entries.
|
||||||
let mint = Mint::new(2);
|
let mint = Mint::new(2);
|
||||||
let bank = Arc::new(Bank::new(&mint));
|
let bank = Arc::new(Bank::new(&mint));
|
||||||
|
let dummy_leader_id = Keypair::new().pubkey();
|
||||||
let (verified_sender, verified_receiver) = channel();
|
let (verified_sender, verified_receiver) = channel();
|
||||||
let (banking_stage, entry_receiver) = BankingStage::new(
|
let (banking_stage, entry_receiver) = BankingStage::new(
|
||||||
&bank,
|
&bank,
|
||||||
|
@ -392,6 +406,7 @@ mod tests {
|
||||||
Default::default(),
|
Default::default(),
|
||||||
&bank.last_id(),
|
&bank.last_id(),
|
||||||
None,
|
None,
|
||||||
|
dummy_leader_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process a batch that includes a transaction that receives two tokens.
|
// Process a batch that includes a transaction that receives two tokens.
|
||||||
|
@ -437,6 +452,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_max_tick_height_shutdown() {
|
fn test_max_tick_height_shutdown() {
|
||||||
let bank = Arc::new(Bank::new(&Mint::new(2)));
|
let bank = Arc::new(Bank::new(&Mint::new(2)));
|
||||||
|
let dummy_leader_id = Keypair::new().pubkey();
|
||||||
let (_verified_sender_, verified_receiver) = channel();
|
let (_verified_sender_, verified_receiver) = channel();
|
||||||
let max_tick_height = 10;
|
let max_tick_height = 10;
|
||||||
let (banking_stage, _entry_receiver) = BankingStage::new(
|
let (banking_stage, _entry_receiver) = BankingStage::new(
|
||||||
|
@ -445,6 +461,7 @@ mod tests {
|
||||||
Default::default(),
|
Default::default(),
|
||||||
&bank.last_id(),
|
&bank.last_id(),
|
||||||
Some(max_tick_height),
|
Some(max_tick_height),
|
||||||
|
dummy_leader_id,
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
banking_stage.join().unwrap(),
|
banking_stage.join().unwrap(),
|
||||||
|
|
|
@ -6,6 +6,7 @@ use bank::Bank;
|
||||||
|
|
||||||
use service::Service;
|
use service::Service;
|
||||||
use solana_metrics::{influxdb, submit};
|
use solana_metrics::{influxdb, submit};
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
use solana_sdk::timing;
|
use solana_sdk::timing;
|
||||||
use solana_sdk::vote_program::{self, VoteProgram};
|
use solana_sdk::vote_program::{self, VoteProgram};
|
||||||
use std::result;
|
use std::result;
|
||||||
|
@ -29,6 +30,7 @@ pub struct ComputeLeaderFinalityService {
|
||||||
impl ComputeLeaderFinalityService {
|
impl ComputeLeaderFinalityService {
|
||||||
fn get_last_supermajority_timestamp(
|
fn get_last_supermajority_timestamp(
|
||||||
bank: &Arc<Bank>,
|
bank: &Arc<Bank>,
|
||||||
|
leader_id: Pubkey,
|
||||||
now: u64,
|
now: u64,
|
||||||
last_valid_validator_timestamp: u64,
|
last_valid_validator_timestamp: u64,
|
||||||
) -> result::Result<u64, FinalityError> {
|
) -> result::Result<u64, FinalityError> {
|
||||||
|
@ -48,6 +50,9 @@ impl ComputeLeaderFinalityService {
|
||||||
// by returning None
|
// by returning None
|
||||||
if vote_program::check_id(&account.owner) {
|
if vote_program::check_id(&account.owner) {
|
||||||
if let Ok(vote_state) = VoteProgram::deserialize(&account.userdata) {
|
if let Ok(vote_state) = VoteProgram::deserialize(&account.userdata) {
|
||||||
|
if leader_id == vote_state.node_id {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let validator_stake = bank.get_stake(&vote_state.node_id);
|
let validator_stake = bank.get_stake(&vote_state.node_id);
|
||||||
total_stake += validator_stake;
|
total_stake += validator_stake;
|
||||||
// Filter out any validators that don't have at least one vote
|
// Filter out any validators that don't have at least one vote
|
||||||
|
@ -86,11 +91,18 @@ impl ComputeLeaderFinalityService {
|
||||||
Err(FinalityError::NoValidSupermajority)
|
Err(FinalityError::NoValidSupermajority)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compute_finality(bank: &Arc<Bank>, last_valid_validator_timestamp: &mut u64) {
|
pub fn compute_finality(
|
||||||
|
bank: &Arc<Bank>,
|
||||||
|
leader_id: Pubkey,
|
||||||
|
last_valid_validator_timestamp: &mut u64,
|
||||||
|
) {
|
||||||
let now = timing::timestamp();
|
let now = timing::timestamp();
|
||||||
if let Ok(super_majority_timestamp) =
|
if let Ok(super_majority_timestamp) = Self::get_last_supermajority_timestamp(
|
||||||
Self::get_last_supermajority_timestamp(bank, now, *last_valid_validator_timestamp)
|
bank,
|
||||||
{
|
leader_id,
|
||||||
|
now,
|
||||||
|
*last_valid_validator_timestamp,
|
||||||
|
) {
|
||||||
let finality_ms = now - super_majority_timestamp;
|
let finality_ms = now - super_majority_timestamp;
|
||||||
|
|
||||||
*last_valid_validator_timestamp = super_majority_timestamp;
|
*last_valid_validator_timestamp = super_majority_timestamp;
|
||||||
|
@ -105,7 +117,7 @@ impl ComputeLeaderFinalityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new ComputeLeaderFinalityService for computing finality.
|
/// Create a new ComputeLeaderFinalityService for computing finality.
|
||||||
pub fn new(bank: Arc<Bank>, exit: Arc<AtomicBool>) -> Self {
|
pub fn new(bank: Arc<Bank>, leader_id: Pubkey, exit: Arc<AtomicBool>) -> Self {
|
||||||
let compute_finality_thread = Builder::new()
|
let compute_finality_thread = Builder::new()
|
||||||
.name("solana-leader-finality-stage".to_string())
|
.name("solana-leader-finality-stage".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
|
@ -114,7 +126,7 @@ impl ComputeLeaderFinalityService {
|
||||||
if exit.load(Ordering::Relaxed) {
|
if exit.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Self::compute_finality(&bank, &mut last_valid_validator_timestamp);
|
Self::compute_finality(&bank, leader_id, &mut last_valid_validator_timestamp);
|
||||||
sleep(Duration::from_millis(COMPUTE_FINALITY_MS));
|
sleep(Duration::from_millis(COMPUTE_FINALITY_MS));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -156,6 +168,7 @@ pub mod tests {
|
||||||
logger::setup();
|
logger::setup();
|
||||||
|
|
||||||
let mint = Mint::new(1234);
|
let mint = Mint::new(1234);
|
||||||
|
let dummy_leader_id = Keypair::new().pubkey();
|
||||||
let bank = Arc::new(Bank::new(&mint));
|
let bank = Arc::new(Bank::new(&mint));
|
||||||
// generate 10 validators, but only vote for the first 6 validators
|
// generate 10 validators, but only vote for the first 6 validators
|
||||||
let ids: Vec<_> = (0..10)
|
let ids: Vec<_> = (0..10)
|
||||||
|
@ -195,7 +208,11 @@ pub mod tests {
|
||||||
|
|
||||||
// There isn't 2/3 consensus, so the bank's finality value should be the default
|
// There isn't 2/3 consensus, so the bank's finality value should be the default
|
||||||
let mut last_finality_time = 0;
|
let mut last_finality_time = 0;
|
||||||
ComputeLeaderFinalityService::compute_finality(&bank, &mut last_finality_time);
|
ComputeLeaderFinalityService::compute_finality(
|
||||||
|
&bank,
|
||||||
|
dummy_leader_id,
|
||||||
|
&mut last_finality_time,
|
||||||
|
);
|
||||||
assert_eq!(bank.finality(), std::usize::MAX);
|
assert_eq!(bank.finality(), std::usize::MAX);
|
||||||
|
|
||||||
// Get another validator to vote, so we now have 2/3 consensus
|
// Get another validator to vote, so we now have 2/3 consensus
|
||||||
|
@ -204,7 +221,11 @@ pub mod tests {
|
||||||
let vote_tx = Transaction::vote_new(&vote_account, vote, ids[6], 0);
|
let vote_tx = Transaction::vote_new(&vote_account, vote, ids[6], 0);
|
||||||
bank.process_transaction(&vote_tx).unwrap();
|
bank.process_transaction(&vote_tx).unwrap();
|
||||||
|
|
||||||
ComputeLeaderFinalityService::compute_finality(&bank, &mut last_finality_time);
|
ComputeLeaderFinalityService::compute_finality(
|
||||||
|
&bank,
|
||||||
|
dummy_leader_id,
|
||||||
|
&mut last_finality_time,
|
||||||
|
);
|
||||||
assert!(bank.finality() != std::usize::MAX);
|
assert!(bank.finality() != std::usize::MAX);
|
||||||
assert!(last_finality_time > 0);
|
assert!(last_finality_time > 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -322,6 +322,7 @@ impl Fullnode {
|
||||||
sigverify_disabled,
|
sigverify_disabled,
|
||||||
max_tick_height,
|
max_tick_height,
|
||||||
last_entry_id,
|
last_entry_id,
|
||||||
|
scheduled_leader,
|
||||||
);
|
);
|
||||||
|
|
||||||
let broadcast_service = BroadcastService::new(
|
let broadcast_service = BroadcastService::new(
|
||||||
|
@ -486,6 +487,7 @@ impl Fullnode {
|
||||||
// the window didn't overwrite the slot at for the last entry that the replicate stage
|
// the window didn't overwrite the slot at for the last entry that the replicate stage
|
||||||
// processed. We also want to avoid reading processing the ledger for the last id.
|
// processed. We also want to avoid reading processing the ledger for the last id.
|
||||||
&last_id,
|
&last_id,
|
||||||
|
self.keypair.pubkey(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let broadcast_service = BroadcastService::new(
|
let broadcast_service = BroadcastService::new(
|
||||||
|
|
|
@ -10,6 +10,7 @@ use poh_service::Config;
|
||||||
use service::Service;
|
use service::Service;
|
||||||
use sigverify_stage::SigVerifyStage;
|
use sigverify_stage::SigVerifyStage;
|
||||||
use solana_sdk::hash::Hash;
|
use solana_sdk::hash::Hash;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::Receiver;
|
||||||
|
@ -38,6 +39,7 @@ impl Tpu {
|
||||||
sigverify_disabled: bool,
|
sigverify_disabled: bool,
|
||||||
max_tick_height: Option<u64>,
|
max_tick_height: Option<u64>,
|
||||||
last_entry_id: &Hash,
|
last_entry_id: &Hash,
|
||||||
|
leader_id: Pubkey,
|
||||||
) -> (Self, Receiver<Vec<Entry>>, Arc<AtomicBool>) {
|
) -> (Self, Receiver<Vec<Entry>>, Arc<AtomicBool>) {
|
||||||
let exit = Arc::new(AtomicBool::new(false));
|
let exit = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
@ -52,6 +54,7 @@ impl Tpu {
|
||||||
tick_duration,
|
tick_duration,
|
||||||
last_entry_id,
|
last_entry_id,
|
||||||
max_tick_height,
|
max_tick_height,
|
||||||
|
leader_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
let (ledger_write_stage, entry_forwarder) =
|
let (ledger_write_stage, entry_forwarder) =
|
||||||
|
|
Loading…
Reference in New Issue