Unless you’ve not been paying attention to recent blockchain developments, you have likely heard about the new and novel layer-1 blockchain called Aptos, which has raised $350m from investors such as FTX Ventures, Jump Crypto, Multicoin Capital, and a16z. Today, in the first of a three part series, we will dive into some of the features of Aptos and compare them to Solana.
So, what is Aptos?
Aptos is a new layer-1 Proof-of-Stake (PoS) blockchain that claims to be both secure and scalable. The company was founded by developers who formerly worked on Diem, Meta’s blockchain initiative, so it’s safe to say that Aptos already has a solid foundation off of which to build their products. The network can handle over 130k transactions per second using its parallel execution engine (Block-STM), which results in low transaction costs for users.
Recently launched in March 2022, there are already over 13,000 commits and more than 1,800 Github users have forked the Aptos-core repository. Additionally, the layer-1 has amassed well over 100 projects, such as Switchboard, Notifi Network, Pontem Network, Topaz Market, PayMagic, Solrise Finance and more have already begun building and testing on the network. This progress is undoubtedly very impressive and optimistic.
Comparison between Aptos and Solana: The Mempool
Every node in a network keeps a copy of the mempool (memory pool), which is a smaller database of unconfirmed or pending transactions. A transaction has to be validated to be added to the mempool. Higher gas transactions are often prioritized and the lowest gas transactions are abandoned if the mempool is full.
How are transactions sent from clients to validators?
On Aptos, there are two types of nodes: validator nodes and FullNodes. Clients submit transactions to a FullNode and then this FullNode forwards them to validator nodes. FullNodes and validators share the same code, but FullNodes do not participate in consensus. Validator nodes communicate directly with other validator nodes over a permissioned network and run a distributed consensus protocol to agree on the next block. Transactions are validated and then distributed to the mempools of other validator nodes in the network.
/// Processes transactions from other nodes.
pub(crate) async fn process_transaction_broadcast<V>(
smp: SharedMempool<V>,
transactions: Vec<SignedTransaction>,
request_id: Vec<u8>,
timeline_state: TimelineState,
peer: PeerNetworkId,
timer: HistogramTimer,
) where
V: TransactionValidation,
{
timer.stop_and_record();
let _timer = counters::process_txn_submit_latency_timer(peer.network_id());
let results = process_incoming_transactions(&smp, transactions, timeline_state);
log_txn_process_results(&results, Some(peer));
let ack_response = gen_ack_response(request_id, results, &peer);
let network_sender = smp.network_interface.sender();
if let Err(e) = network_sender.send_to(peer, ack_response) {
counters::network_send_fail_inc(counters::ACK_TXNS);
error!(
LogSchema::event_log(LogEntry::BroadcastACK, LogEvent::NetworkSendFail)
.peer(&peer)
.error(&e.into())
);
return;
}
notify_subscribers(SharedMempoolNotification::ACK, &smp.subscribers);
}
On the contrary, Solana has a mempool-less innovation called Gulf Stream in which transactions are sent directly to the upcoming leaders in the network since the network is permissionless and the leader schedule is known and public. Upon receiving new transactions, each validator will decide whether it will hold transactions (because it will be the leader soon), hold and forward the transactions (if it will be the leader within 20 slots), or forward the transactions (if it is not the leader within 20 slots). This allows validators to process transactions faster and reduce confirmation times. However, without protection against DoS attacks, the Solana network is susceptible to malicious actors that can cause known consecutive leaders to be out of service and take down the network.
fn consume_or_forward_packets(
my_pubkey: &Pubkey,
leader_pubkey: Option<Pubkey>,
bank_still_processing_txs: Option<&Arc<Bank>>,
would_be_leader: bool,
would_be_leader_shortly: bool,
) -> BufferedPacketsDecision {
// If has active bank, then immediately process buffered packets
// otherwise, based on leader schedule to either forward or hold packets
if let Some(bank) = bank_still_processing_txs {
// If the bank is available, this node is the leader
BufferedPacketsDecision::Consume(bank.ns_per_slot)
} else if would_be_leader_shortly {
// If the node will be the leader soon, hold the packets for now
BufferedPacketsDecision::Hold
} else if would_be_leader {
// Node will be leader within ~20 slots, hold the transactions in
// case it is the only node which produces an accepted slot.
BufferedPacketsDecision::ForwardAndHold
} else if let Some(x) = leader_pubkey {
if x != *my_pubkey {
// If the current node is not the leader, forward the buffered packets
BufferedPacketsDecision::Forward
} else {
// If the current node is the leader, return the buffered packets as is
BufferedPacketsDecision::Hold
}
} else {
// We don't know the leader. Hold the packets for now
BufferedPacketsDecision::Hold
}
}
Signature verification.
On Aptos, signature verification is done by the move adapter. The mempool performs various checks on the transactions to ensure transaction validity and protect against DoS attacks. More specifically, it performs signature verification for any transaction, and also checks if the sender and secondary signers' signatures in the SignedTransaction are consistent with their public keys and the content of the transaction. That is, on Aptos, signatures are required to come from the primary sender if the transaction is single-agent, or both primary and secondary senders if the transaction is multi-agent. Thus, Aptos will reject transactions without valid signatures from the right senders at the start.
/// Return Ok if all AccountAuthenticator's public keys match their signatures, Err otherwise
pub fn verify(&self, raw_txn: &RawTransaction) -> Result<()> {
let num_sigs: usize = self.sender().number_of_signatures()
+ self
.secondary_signers()
.iter()
.map(|auth| auth.number_of_signatures())
.sum::<usize>();
if num_sigs > MAX_NUM_OF_SIGS {
return Err(Error::new(AuthenticationError::MaxSignaturesExceeded));
}
match self {
Self::Ed25519 {
public_key,
signature,
} => signature.verify(raw_txn, public_key),
Self::MultiEd25519 {
public_key,
signature,
} => signature.verify(raw_txn, public_key),
Self::MultiAgent {
sender,
secondary_signer_addresses,
secondary_signers,
} => {
let message = RawTransactionWithData::new_multi_agent(
raw_txn.clone(),
secondary_signer_addresses.clone(),
);
sender.verify(&message)?;
for signer in secondary_signers {
signer.verify(&message)?;
}
Ok(())
}
}
}
On the other hand, Solana provides a very generic programming model in a way that the programs (smart contracts) specify who are the signers. Smart Contract data is scattered in different accounts; thus, programs have to declare input accounts (and their readability and writability), as well as, signature requirements.
Here is an example in the Solana stake-pool program in which the signature requirement is set for the readonly staker account.
pub fn set_preferred_validator(
program_id: &Pubkey,
stake_pool_address: &Pubkey,
staker: &Pubkey,
validator_list_address: &Pubkey,
validator_type: PreferredValidatorType,
validator_vote_address: Option<Pubkey>,
) -> Instruction {
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new(*stake_pool_address, false),
AccountMeta::new_readonly(*staker, true),
AccountMeta::new_readonly(*validator_list_address, false),
],
data: StakePoolInstruction::SetPreferredValidator {
validator_type,
validator_vote_address,
}
.try_to_vec()
.unwrap(),
}
}
As a result, the transaction processing unit (TPU) can only verify if the transaction has all required signatures at the execution stage (TPU’s banking stage). Coupled with the current practice of indiscriminately accepting transactions on a first-come-first-served basis, the Solana network is often vulnerable to DoS attacks. On the bright side, however, the Solana team is working toward using stake and fee to prioritize transactions.
let (load_and_execute_transactions_output, load_execute_time) = measure!(
bank.load_and_execute_transactions(
batch,
MAX_PROCESSING_AGE,
transaction_status_sender.is_some(),
transaction_status_sender.is_some(),
transaction_status_sender.is_some(),
&mut execute_and_commit_timings.execute_timings,
None, // account_overrides
log_messages_bytes_limit
),
"load_execute",
);
Note that the signatures will also be verified before the execution stage, but the validator can only check if the signatures are valid… it cannot check if they are the required signatures because that information cannot be known until the execution stage when the programs are read and executed.
Coming up…
Coming up in this three part series, we will delve into:
Aptos’s BFT vs. Solana’s PoS consensus mechanisms
Aptos’s parallel execution engine vs. Solana’s concurrent transaction processor, and
AptosNet vs. Solana’s UDP-based protocol
Comments