Skip to main content

Consensus

The Angstrom consensus algorithm is designed to integrate tightly with Ethereum, leveraging its capabilities for settlement, data availability, and synchronization via block header events. This integration simplifies the consensus process, sidestepping the indefinite consensus rounds that are typical in many algorithms, thanks to Ethereum's consistent block generation.

Unlike Tendermint, which operates independently and focuses solely on block validity, Angstrom co-exists with Ethereum and focuses on producing valid and near-optimal blocks for their height and round. This dual criterion — is_valid(block) && is_best_block_in_set(block) — requires an initial consensus on the set of valid orders for inclusion.

This approach recognizes that better blocks can emerge over time. Without a strategy to handle this, an optimal block chosen by an honest leader might soon become outdated, leading to continuous selections without reaching consensus.

Overview

For each block, the Angstrom network will select a node to be the Proposer. While all nodes in the network are responsible for receiving and socializing orders during the block, the Proposer is the only node that will combine all orders and PreProposals into a completed Proposal for the actions of the network to be executed on the chain. The completed and signed Proposal will then be validated by the remainder of the nodes on the network to ensure correctness.

Proposer Selection

Angstrom uses a Weighted Round Robin (WRR) proposer selection mechanism enhanced by priority centering and scaling, inspired by Tendermint but refined for better proposer fairness and responsiveness over time.

Key elements include:

  • Voting power-weighted priorities: Nodes with higher voting power accumulate priority faster.
  • Priority centering: Keeps validator priorities balanced around a central value to prevent overflow or drift.
  • Priority scaling: Normalizes priority differences when they exceed a threshold.
  • Penalty for new validators: Newly joined validators receive a temporary priority penalty to prevent manipulation.

The proposer selection proceeds as follows:

fn choose_proposer(&mut self, block_number: BlockNumber) -> Option<Address> {
if block_number == self.block_number {
if self.last_proposer.is_none() {
self.last_proposer = Some(self.proposer_selection());
}
return self.last_proposer;
}

let rounds_to_catchup = (block_number - self.block_number) as usize;
let mut leader = None;
for _ in 0..rounds_to_catchup {
self.center_priorities();
self.scale_priorities();
leader = Some(self.proposer_selection());
self.last_proposer = leader;
}

self.block_number = block_number;
leader
}

This logic ensures deterministic proposer selection while accommodating block reorgs and validator set changes.

Order Socialization

After a new block has begun, nodes will receive and socialize orders between them. User orders and searcher orders received through the Angstrom RPC will be accepted by node and then broadcast throughout the Angstrom network to ensure that all nodes have as complete a view of all orders as possible.

function getOrder(O, source) {
// Drop an invalid order
if (!validateOrder()) {
return;
}
// Drop an already seen order
if (!unseenOrder()) {
return;
}
// Get the pool index for this order
const pool = getPoolIndex(O);
if (isBid(O)) {
bids[pool].push(O);
broadcastOrder(O);
} else if (isAsk(O)) {
asks[pool].push(O);
broadcastOrder(O);
} else if (isSearcher(O)) {
broadcastOrder(O);
} else if (isComposable(O)) {
return;
}
}

PreProposals

After a given timeout, nodes will bundle all of the orders they have retained for this block into a signed PreProposal that is then broadcast to the entire Angstrom network. In an ideal scenario, all node PreProposals will be identical, however it is possible that there may be some small discrepancies due to latency or network partitioning that results in some orders not being seen by some nodes.

Once a node has sent off its PreProposal, the current block is considered to be completed and any additional orders received will be queued for the next block.

PreProposalAggregation

PreProposal Aggregation phase. During this stage, every validator listens for and collects the PreProposals broadcast by its peers.

A validator continues this collection process until it has successfully received and validated PreProposals from at least two-thirds (2/3) of all active validators in the network. Once this threshold is met, the validator creates its own signed PreProposalAggregation message. This message essentially attests: "I have received and acknowledged the following set of PreProposals, which represents a 2/3 super majority view of the network's submissions for this block."

Proposals

The designated Proposer (i.e., the block leader) for the current block is tasked with constructing the Final Proposal. To do this, the Proposer collects the signed PreProposalAggregation messages broadcast by all other validators in the network.

Once the Proposer has received valid PreProposalAggregation from at least two-thirds (2/3) of the network's validators it will allow for a short delay to accept any additional "late" PreProposalAggregation that might arrive, and then compute a Proposal.

This is the point at which orders are matched, the Uniform Clearing Price is determined. The leader submits this Proposal for execution optimistically, the validation of the proposal will be done retroactively by the other nodes on the network during the next block.

function getPreProposalAgg(PP, sourceNode) {
preProposalAggCount += 1
preProposalsAgg[sourceNode] = PP
if (isProposer(self) && preProposalCount > twoThirdsNodeCount) {
// Wait for any additional preProposals
await catchupTimeout
// Build and send our Proposal
Propose()
}
}

Validation

Once the proposer has produced and broadcast a Proposal, all nodes must validate that:

  • The Proposal been signed by the appropriate node.
  • The Proposal include the PreProposal generated by this node.
  • The Proposal include enough PreProposals to be valid (2/3rds of the overall node count).
  • All included PreProposals are validly signed by the source nodes.
  • The Proposal computed locally based on the included signed PreProposals results in an identical Proposal than the one submitted by the leader.

If all these criteria are met, we can broadcast a Commit message to the Angstrom network that recognizes this Proposal as being proper

// When we get a proposal from the appropriate proposer for our block
function getProposal(P) {
// If the proposal doesn't have a valid signature, reject
if !validSignature(pSignature) {
broadcastCommit(null)
} else if preProposalAgg.length < ((2 * nodeCount) / 3) {
broadcastCommit(null)
// If any preproposal has an invalid signature, reject
} else if anyInvalidSignature(preProposals) {
broadcastCommit(null)
} else {
// Build a proposal ourselves
pVerify = buildProposal(preProposals)
// If the proposal we build differs, reject
if pVerify !== P {
broadcastCommit(null)
} else {
// If the proposal we build is the same, accept
broadcastCommit(pVerify)
}
}
}