1. Consensus

Pharos’ consensus algorithm adopts a Multi Concurrent Proposer (MCP) model. Under this model, each active Validator is allowed to independently propose a sub-proposal. These sub-proposals are eventually assembled into the final block, which is then executed by clients.

In a setting where every node can propose independently, some nodes may fail to collect a sufficient set of valid transactions and therefore be unable to make a proposal. To prevent monopoly and preserve decentralization, transactions in the mempool are deterministically assigned to specific validators for proposal. If a proposer includes transactions that are not assigned to it, those transactions will be identified and removed during the consensus sanity ****check phase.

On the other hand, to avoid excessive message complexity within a single block, the algorithm introduces a proposal rotation mechanism. For each block, a committee is randomly selected to participate in proposing. The committee size is a system parameter, currently set to 21. Only nodes in the committee have proposal rights, while all nodes retain voting rights.

Under the MCP model, there is no single leader responsible for driving or coordinating the consensus process. As a result, all nodes are required to participate promptly in each voting round. Due to geographical distribution and varying network latencies, sub-proposals may arrive at different nodes at different times. To maximize the collection of proposals from all nodes, the consensus module employs a pace_keeping mechanism, which introduces additional waiting time to accommodate potentially slow nodes. In this scenario, if a node remains offline for an extended period, the pace_keeping mechanism may be triggered in each proposal round, increasing overall latency.

To mitigate this, the consensus module maintains a reputation-based mechanism. Nodes that repeatedly fail to respond will have their messages deprioritized, or may no longer be waited for at all. This behavior is controlled by a system configuration parameter, currently set to 1. It is important to note that this mechanism does not penalize nodes permanently: once an offline node comes back online, it can quickly rejoin the network and participate in consensus as normal.

After block execution completes, all nodes perform a final confirmation of the execution result. During this process, each node signs the block result using its BLS key. Once a node has collected signatures from at least two-thirds (2/3) of the validators, it considers the block to be stable, aggregates the BLS signatures, and stores the aggregated proof. Notably, signature collection does not require all nodes to obtain an identical set of signatures. For any given node, as long as it gathers a sufficient 2/3 threshold, it may finalize the block. Consequently, the signature lists included in block proofs may differ across nodes.

Pharos’ consensus algorithm is described in the paper “Flexible Advancement in Asynchronous BFT Consensus,” which was published at SOSP ’23. For a more detailed explanation of the algorithm design, please refer to the paper: https://dl.acm.org/doi/10.1145/3600006.3613164

2. Validation

2.1 Node basic check

The contract is still under development and audit, so the ABI may change.

Pharos uses system contracts to manage nodes. This contract is deployed at the special address 4100000000000000000000000000000000000000. Each node is identified by a DomainID, which is derived from the hash of its consensus signing key. The DomainID can be found using ops tool: https://github.com/PharosNetwork/ops?tab=readme-ov-file#3-get-node-id

In the contract, the Validator struct includes descriptive information, the public key, staking amount, and other related data.

The ABI related to node infos is as follows:

[
  {
    "inputs": [],
    "name": "getActiveValidators",
    "outputs": [
      {
        "internalType": "bytes32[]",
        "name": "",
        "type": "bytes32[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "validators",
    "outputs": [
      {"internalType":"string","name":"description","type":"string"},
      {"internalType":"string","name":"publicKey","type":"string"},
      {"internalType":"string","name":"publicKeyPop","type":"string"},
      {"internalType":"string","name":"blsPublicKey","type":"string"},
      {"internalType":"string","name":"blsPublicKeyPop","type":"string"},
      {"internalType":"string","name":"endpoint","type":"string"},
      {"internalType":"uint8","name":"status","type":"uint8"},
      {"internalType":"bytes32","name":"poolId","type":"bytes32"},
      {"internalType":"uint256","name":"totalStake","type":"uint256"},
      {"internalType":"address","name":"owner","type":"address"},
      {"internalType":"uint256","name":"stakeSnapshot","type":"uint256"},
      {"internalType":"uint256","name":"pendingWithdrawStake","type":"uint256"},
      {"internalType":"uint8","name":"pendingWithdrawWindow","type":"uint8"}
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

When checking the status of your own node, you can use the getActiveValidators method to query the latest list of active validators and verify whether your DomainID is included in the returned list.

cast call 0x4100000000000000000000000000000000000000 "getActiveValidators()(bytes32[])" --rpc-url <https://atlantic.dplabs-internal.com> --json