HotStuff BFT Consensus Framework

repository·main·Indexed 42 days ago

https://github.com/relab/hotstuff

A modular framework for experimenting with and validating HotStuff and similar Byzantine Fault Tolerant (BFT) consensus protocols. It provides extensible interfaces for consensus logic, cryptography, and networking, along with a CLI for running distributed experiments, safety testing, and performance visualization via tools like latencygen and plot.

Tokens
12.4K
Snippets
51
Records
61
Agent score
53%

What's inside HotStuff

  1. Implement a new consensus protocol

    main

    There are two primary ways to implement a new consensus protocol in hotstuff:

    1. The Consensus interface: This is the most general and powerful interface. It is highly integrated with other modules (Blockchain, Acceptor, Executor, Synchronizer, etc.). Use this if you need full control over how the protocol interacts with the system.
    2. The Rules and ProposeRuler interfaces: These are simplified interfaces designed for implementing only the core consensus logic (e.g., deciding whether to vote for a proposal or if a block is safe to commit). A default implementation of the Consensus interface handles the complex module interactions for you, allowing you to focus solely on the protocol rules.

    You can also use these interfaces to override existing behaviors, such as implementing Byzantine behaviors via the rules/byzantine package.

  2. Implement a new crypto algorithm

    main

    To implement new cryptographic primitives, you can use two different levels of abstraction:

    1. The Crypto interface: A high-level interface that provides methods for working with complex structures like quorum certificates.
    2. The CryptoImpl interface: A lower-level interface used to implement the actual signature and threshold signature logic. The Crypto interface uses CryptoImpl internally to construct quorum certificates.

    If you are implementing a new algorithm, you should typically implement CryptoImpl first.

  3. How Twins testing works

    main

    Twins is a strategy for testing Byzantine consensus protocols for safety and liveness. It emulates attacks from Byzantine replicas by running two copies (twins) of the same replica.

    Because both twins share the same private key, they are indistinguishable from a single replica behaving erratically. The system is implemented using two main components:

    1. Scenario Generator: Responsible for creating descriptions of replicas, twins, network partitions, and leaders for each view.
    2. Scenario Executor: Implements the Configuration and Replica interfaces from the consensus package. This allows the executor to control message delivery and dropping to simulate network partitions and Byzantine behavior.
  4. How the HotStuff module system works

    main

    The hotstuff project uses a modular architecture to allow different implementations of consensus protocols, view synchronizers (pacemakers), and other components to interoperate. To handle the complexities of modularity, the system employs two key patterns:

    1. Deferred Initialization: To solve circular dependencies (e.g., a consensus module needing a synchronizer that in turn needs the consensus module), modules are first constructed in an uninitialized state and then fully initialized later.
    2. Dependency Injection via Module Registry: A modules.Builder collects module instances, and a modules.Core object is used to retrieve them during the initialization phase.

    The Module Lifecycle

    1. Registration: Modules register themselves with a global registry using modules.RegisterModule.
    2. Construction: Modules are instantiated (e.g., via modules.New(name)).
    3. Composition: Module instances are added to a modules.Builder.
    4. Building: Calling builder.Build() triggers the InitModule method on all modules that implement the Module interface.
    5. Retrieval: During InitModule, a module uses the provided *modules.Core to fetch its dependencies using Get, GetAll, or TryGet.
    // 1. Register the module
    func init() {
      modules.RegisterModule("A1", NewA1)
    }
    
    // 2. Implement the Module interface for deferred initialization
    type A1 struct{ b B }
    
    func (a *A1) InitModule(mods *modules.Core) {
      mods.Get(&a.b) // Fetch dependency B
    }
    
    // 3. Compose and Build
    func compose(choiceA string) {
      builder := modules.NewBuilder()
      builder.Add(
        NewB1(),
        modules.New(choiceA),
      )
      mods := builder.Build()
    
      var (
        a A
        b B
      )
      mods.GetAll(&a, &b) // Retrieve the final composed modules
    }
  5. Understand the modular architecture of hotstuff

    main

    The hotstuff framework is designed with interchangeable modules. You can swap or implement custom versions of the following components:

    • Consensus: The core logic deciding when to vote for proposals and commit blocks. Implementations include chainedhotstuff, fasthotstuff, and simplehotstuff.
    • Crypto: Handles cryptographic primitives and quorum certificates. Implementations include ecdsa and bls12 (threshold signatures).
    • Synchronizer: Manages view-synchronization (e.g., based on DiemBFT's RoundState).
    • Blockchain: Provides block storage (currently an in-memory cache).
    • Leader rotation: Determines the leader for a view (e.g., fixed or round-robin).
    • Networking/Backend: Powered by Gorums.
  6. How to create a new metric for collection

    main

    To add a new metric to the hotstuff experimentation framework, follow these four steps:

    1. Define a Protobuf message: Create a message in a .proto file (e.g., metrics/types.proto). The message must include a field named Event of type types.Event to ensure compatibility with the plotting package.
    2. Emit events from modules: If your metric depends on specific system events, define that event type in the relevant module's package and add it to the event loop using eventLoop.AddEvent(...).
    3. Implement the modules.Module interface: Create a struct that implements InitModule. In this function, use mods.Get to retrieve the eventloop.EventLoop and logging.Logger. Register handlers for your custom events and an observer for types.TickEvent to periodically log measurements.
    4. Register the metric: In your metric's package, use an init() function to call RegisterReplicaMetric or RegisterClientMetric. This makes the metric available via the --metrics CLI flag.
    message LatencyMeasurement {
      Event Event = 1;
      double Latency = 2;
      double Variance = 3;
      uint64 Count = 4;
    }
    // Example InitModule implementation
    func (t *Throughput) InitModule(mods *modules.Core) {
        var (
            eventLoop *eventloop.EventLoop
            logger logging.Logger
        )
        mods.Get(&eventLoop, &logger)
    
        eventLoop.RegisterHandler(consensus.CommitEvent{}, func(event interface{}) {
            commitEvent := event.(consensus.CommitEvent)
            t.recordCommit(commitEvent.Commands)
        })
    
        eventLoop.RegisterObserver(types.TickEvent{}, func(event interface{}) {
            t.tick(event.(types.TickEvent))
        })
    
        logger.Info("Throughput metric enabled")
    }
    
    // Example Registration
    func init() {
        RegisterReplicaMetric("throughput", func() interface{} {
            return &Throughput{}
        })
    }
  7. Install and build hotstuff

    main

    To use relab/hotstuff, you must first install the build dependencies and then compile the project based on your operating system.

    Build Dependencies

    • Go: version 1.18 or higher.
    • Protobuf compiler: version 3.15 or higher (required if you modify .proto files).
    • Protobuf plugins:
      • Linux/macOS: Run make tools to install protoc-gen-go and protoc-gen-gorums.
      • Windows: The build script handles these automatically.
      • Manual Installation:
        go install github.com/relab/gorums/cmd/protoc-gen-gorums
        go install google.golang.org/protobuf/cmd/protoc-gen-go

    Compilation

    • Linux and macOS:
      make
    • Windows (using PowerShell):
      .\build.ps1
      Note: On Windows, use ./hotstuff.exe instead of ./hotstuff when executing commands.
    make
  8. Generate Twins testing scenarios

    main

    Use the twins generate subcommand to create JSON scenario files for testing Byzantine consensus protocols. Scenarios can be generated ahead of time and written to files, or generated on the fly during execution.

    Configuration Flags

    • --scenarios: Total number of scenarios to generate. If omitted, all possible scenarios are generated (can result in very large files).
    • --scenarios-per-file: Number of scenarios per output file. If set to 0 (default), --output is the filename. If set to a non-zero value, --output is the directory where multiple files will be created.
    • --output: The file path or directory where scenarios are written.
    • --replicas: Number of replicas to create.
    • --twins: Number of replicas that should have a twin.
    • --partitions: Number of network partitions to divide replicas and twins into.
    • --views: Number of views to run.
    • --shuffle: Randomizes the order of scenarios.
    • --seed: Used to reproduce a specific randomized order.

    Note: The generator is deterministic; given the same inputs, it produces the same scenarios unless --shuffle is used.

    # Generate 100 scenarios, 10 per file, into the 'results' directory
    ./hotstuff twins generate --scenarios 100 --scenarios-per-file 10 --output ./results --replicas 5 --twins 2 --partitions 3 --views 10
  9. Run experiments locally using the hotstuff CLI

    main

    To run experiments locally, first compile the CLI using make from the project root (requires Go 1.18+ and a protobuf compiler).

    Use the ./hotstuff run command to start a local experiment. By default, this starts 4 replicas and 1 client for 10 seconds. You can customize the experiment using command-line flags or a configuration file.

    Note: Each flag corresponds to a field in the config file with the same name (minus the leading dashes).

    make
    ./hotstuff run --replicas 4 --clients 1 --duration 30s
  10. Run experiments on remote hosts via SSH

    main

    The CLI can deploy and run experiments on remote hosts using SSH. To use this feature, ensure the following requirements are met:

    1. Authentication: You must use public key authentication.
    2. Host Verification: All target hosts must be present in a known_hosts file.
    3. Environment: Hosts must support SFTP and provide a Unix-like environment.

    SSH Configuration

    You must provide an SSH config file to the CLI. The CLI uses the ssh_config package, which supports key options like User, Port, IdentityFile, and UserKnownHostsFile.

    Remote Experiment Flags

    In addition to standard experiment flags, use these when targeting remote hosts:

    • --exe: Path to the executable to deploy. Use this if the remote host has a different OS or architecture than your local machine. If omitted, the CLI deploys the current binary.
    • --ssh-config: Path to your SSH config file. Defaults to ~/.ssh/config if not specified.
    • --hosts: A comma-separated list of host names (preferably those defined in your SSH config).
    • --worker: Runs a worker on your local machine in addition to the remote hosts.
    # Example SSH config
    Host 127.0.0.1 localhost
        User root
        Port 2020
        IdentityFile ./scripts/id
        UserKnownHostsFile ./scripts/known_hosts
  11. Execute Twins testing scenarios

    main

    Use the twins run subcommand to execute scenarios against a consensus implementation. You can either provide a pre-generated JSON file or configure the generator to create scenarios on the fly.

    Configuration Flags

    • --input: Path to a JSON file containing pre-generated scenarios. If this is not provided, the command uses the generator flags (see Generating Scenarios).
    • --consensus: Specifies the consensus implementation to test.
    • --output: Where to write results.
    • --scenarios-per-file: Controls the grouping of output files.
    • --log-all: By default, the executor only writes failed scenarios. Use this flag to log all scenarios.
    • --concurrency: Number of concurrent executions. Set to 0 to use all available CPUs for faster execution.
    # Run scenarios from a file using all available CPUs
    ./hotstuff twins run --input ./scenarios.json --consensus my-consensus-impl --concurrency 0
  12. Configure internal addresses and manual assignments in TOML

    main

    When running remote experiments, you can use a configuration file (passed via the --config flag) to manage how hosts communicate and how roles are distributed.

    Internal Addresses

    If the controller connects to remote hosts via a global address but the hosts should communicate with each other using local network addresses, specify an internal-address for each host in the [[hosts-config]] section.

    Manual Client and Replica Assignment

    By default, the controller distributes clients and replicas evenly across all workers. To override this, you can manually specify the number of clients and replicas for specific hosts using [[hosts-config]].

    Note: If you manually configure hosts, the sum of all configured clients and replicas must equal the total number requested for the experiment.

    # Global settings
    clients = 2
    replicas = 8
    
    hosts = [
        "hotstuff-worker-1",
        "hotstuff-worker-2",
        "hotstuff-worker-3",
        "hotstuff-worker-4",
    ]
    
    # Configure specific host behavior
    [[hosts-config]]
    name = "hotstuff-worker-1"
    internal-address = "192.168.10.2"
    clients = 2
    replicas = 0