Anatomy of Slow Clap

Overview

The Slow Clap module is responsible for handling incoming bridge functionality. Its core purpose is to enable multiple independent validators to reach a consensus on specific actions occurring on another network. To achieve this, the module relies on events emitted by EVM-compatible chains, which can be retrieved via an EVM JSON-RPC API.

As you can see from the documentation:

  1. fromBlock - The starting block of the range to monitor
  2. toBlock - The ending block of the range to monitor
  3. address - The address of the Gatekeeper smart contract that emits bridging events
  4. topics - The hashed event signature and argument types

The Gatekeeper contract emits the following event types:

  • Ghosted - Indicates that funds are being bridged into the Ghost network.
    Arguments: receiver address (on Ghost network) and the amount being bridged
  • Materialized - Indicates that funds are being bridged out of the Ghost network.
    Arguments: receiver address (on EVM chain) and the amount being bridged
  • Rotated - Signals a rotation of the collective governance key. Note: This functionality is not part of the Slow Clap module.

Block Ranges

As described above, we need to maintain the fromBlock and toBlock values. In an ideal world, it would be great to retrieve all events from the entire history, but real-world constraints make this impractical. For instance, some node operators behind RPC endpoints may disable eth_getLogs entirely or impose very strict rate limits.

That’s why Slow Clap continuously fetches the latest block numbers to ensure we’re operating within a valid range for retrieving events. fromBlock and toBlock are updated during each successful off-chain worker run, which occurs approximately every 6 seconds. The update rules are as follows:

  1. fromBlock must be less than toBlock to request all events in the range. If this condition isn’t met, a new block is requested
  2. The block number returned from the RPC response is stored as toBlock , but with the network’s finality deviation subtracted
  3. toBlock cannot increase by more than max_block_distance in a single off-chain worker execution. If it would, it is capped at toBlock + max_block_distance

NOTE: due to the fact that not all EVM-based networks switched to PoS, so not all off them has safe, finalized, and etc. block statuses from JSON RPC API. Thus while we are aiming to get to uniformely working for any network we should play around the raw block numbers.

RPCs

Important note: Not all EVM-based networks have transitioned to Proof of Stake, so not all of them support block statuses like safe or finalized via the JSON-RPC API. Since our goal is to have a solution that works uniformly across any network, we must work directly with raw block numbers.

On the other hand, node operators can change their RPC endpoints at any time, which means there’s no guarantee about which validators are using which RPCs (or how many) making the system resistant to targeted attacks.

During each off-chain worker execution, the module retrieves all configured endpoints (default ones are used if no custom endpoints are set; otherwise, only custom endpoints are considered) and attempts to query each of them. The responses are then collected and processed according to the following rules:

  1. Store the median block number calculated from all successful responses
  2. “Clap” for the most frequently occurring set of events among all successful responses
  3. Handle significant deviation if we have an even number of successful responses and the spread between the two middle values exceeds max_block_distance, special handling is applied
  4. Resolve ties in event sets: If multiple distinct event sets occur with the same highest frequency, offchain-worker marked as failed and going to next one.

Example of strange block median: Given responses [420, 1337] with max_block_distance = 69 , the median would be 878.5. However, since 420 + 69 < 878.5 < 1337 - 69 , this indicates the two groups are too far apart to reliably merge.

Claps & Applauses

Each validator performs a Clap for the events they discover. Validators vote on each bridging transaction using their staked amount. The unique identifier for an EVM transaction is derived from the keccak256 hash of the following components:

  1. Receiver address
  2. Bridged amount
  3. Network ID

When more than 50% of the total staked amount and at least 2/3 + 1 of all validators agree on the exact same transaction, this consensus is called an Applause . At this point, the transaction is executed - funds are created from the void (minted) and the bridging imbalance is recorded.

Commissions are accumulated throughout the era and later distributed to all validators based on their points (i.e., the number of blocks they produced during the era).

If a transaction reaches Applause , any validator who did not clap for it will be slashed and disabled for the remainder of the era. The slash amount is calculated as a percentage equal to the missed amount relative to the total stake of all validators.

Block Commitments

We want to provide users who are bridging with as much transparency as possible. To achieve this, each validator performs a heartbeat every 10 minutes, during which they register their locally stored block number. This is done via an unsigned transaction containing a signed payload which is a small hack to prevent fake validator commitments. The payload includes:

  1. The block number for the specified network
  2. The Unix timestamp when the heartbeat occurred

Every 3 commitments, each validator emits a cross-check for block numbers. This means the network evaluates the median block number and identifies validators whose block height is delayed by more than 6 hours, based on the network’s avg_block_speed . It also checks the median value of the registered Unix timestamps to detect silent validators.

Validators that fail this check are slashed and disabled for the remainder of the current era. The slash amount is calculated using the formula:

slash_percentage = pow((delayed_validators - (total_validators * 9%)) * 4, 2)
5 Likes

Ongoing work on the pallet-slow-clap has revealed several areas for potential improvement. While most of these are non-critical for now, they are worth considering for future updates.

Unix Timestamp vs Inner Block Number

Currently, cross-checking is performed by validators. It would be better if the network handled this automatically. Ideally, validators would only submit the block number and network ID, with the timestamp being recorded at the moment the transaction is applied.

Pros

  • Eliminates the need for validators to perform median calculations every 30 minutes to identify and slash malicious actors
  • Removes reliance on validator-provided timestamps, which can be easily manipulated

Cons

  • The bridging UI in ghost-dao would need to be updated. This would require subscribing to the latest chain block and calculating the registration time using:
    register_timestamp = register_stored_block * current_timestamp / current_block_number

Re-benchmarking

The average block time appears to fluctuate significantly, sometimes reaching up to 18 seconds, despite the target being 6 seconds. This suggests that some weights may be incorrect, especially since several pallets still use default weights, while Ghost’s reference machine is 2.5x slower than Substrate’s default.

Pros

  • Should resolve the issue of abnormally long block production times
  • Provides more headroom for protocol-based transactions, ensuring they execute as expected

User-Agent

Logs indicate that some RPC endpoints return non-200 response codes. After closer inspection, it was discovered that 0xrpc requires a non-empty User-Agent header. It’s unclear whether 0xrpc or other node operators filter requests based on this header, but setting a generic value (e.g., curl or a common browser string) could resolve the issue.

Pros

  • At least one additional legitimate RPC from the default list will work reliably

Cons

  • Hardcoding a User-Agent could potentially cause issues with other RPCs in the future

Transaction Hash

Currently, the transaction_hash from the EVM event is never used. Instead, a unique hash is generated from the receiver , amount , and network_id . However, this combination could theoretically be duplicated within a single epoch (session), leading to unpredictable behavior.

Pros

  • Increases uniqueness, which is inherently beneficial
  • Prevents issues that could arise from duplicate bridging transactions within the same epoch

Cons

  • Would require a complex data migration (not sure is that even possible)
  • The Ghost bridging UI would need to be updated, and historical transaction data would become inaccessible (empty)
5 Likes