masa-finance / roadmap

The protocol
0 stars 0 forks source link

spike: refactor nodeData updates to optimize gossip and allow only a validator to distribute `nodeData` #66

Closed teslashibe closed 1 month ago

teslashibe commented 1 month ago

Problem

nodeData is currently updated from goroutines in the nodeData and should be limited to only trusted gossip and connections from a validator. This means that only a validator can update nodeData on the network. It is important to note that the only time node flags should be updated is when the node joins the network. This means that any node can spoof an update by gossiping adversarial nodeData to the network.

Note

Currently it appears that the validator is constantly refreshing node data to all peers in a broadcast like process. Instead of the validator broadcasting once with a signed and timestamped (when it was sent by the validator - sign plus send) nodeData that is then gossiped between nodes.

teslashibe commented 1 month ago

@mudler , @restevens402 and I added this into the future release as part of refactoring and cleaning up of the P2P layer

mudler commented 1 month ago

This is tangentially related to https://github.com/masa-finance/masa-oracle/issues/427. https://github.com/masa-finance/masa-oracle/issues/427 talks about who can be validator and who can't - in this case instead we want to make sure that only validators can write nodeData.

However when I spoke with @jdutchak it looks like nodeData isn't part of the ledger but only constructed by the node - isn't just an internal view of the node state?

mudler commented 1 month ago

This seems also to be depending on having a consensus algorithm first ( https://github.com/masa-finance/masa-oracle/issues/496 )

teslashibe commented 1 month ago

Implement nodeData distribution with BadgerDB and Raft Consensus

Overview

Implement a hybrid solution for a distributed data storage and synchronization system using BadgerDB for local storage, libp2p DHT for distributed data access, a custom Data Submission Protocol for submitting 'nodeData', and Raft consensus for coordinating writes among validator nodes, with a focus on maintaining consistent nodeData across the network.

Objectives

  1. Implement persistent local storage using BadgerDB
  2. Set up a libp2p DHT for distributed data access
  3. Create a custom Data Submission Protocol for non-validators
  4. Integrate Raft consensus for coordinating writes among validators
  5. Implement synchronization between BadgerDB, nodeData, and DHT
  6. Ensure consistent nodeData updates across the network
  7. Address considerations for consistency, performance, network partitions, scalability, security, data migration, monitoring, debugging, and cluster size management

Technical Details

Components

  1. BadgerDB: Local key-value store
  2. libp2p DHT: Distributed Hash Table for network-wide data access
  3. Custom libp2p protocol: For data submission from non-validators to validators
  4. Raft consensus (using hashicorp/raft): For write coordination among validators
  5. NodeData structure: For maintaining node-specific information

Implementation Steps

  1. Set up BadgerDB
    • Initialize BadgerDB instance for each validator node
    • Implement CRUD operations for local data storage
import "github.com/dgraph-io/badger/v3"

func initBadgerDB(path string) (*badger.DB, error) {
    opts := badger.DefaultOptions(path)
    return badger.Open(opts)
}
  1. Configure libp2p DHT
    • Set up libp2p host and DHT for each node
    • Implement methods for putting and getting values from the DHT
import (
    dht "github.com/libp2p/go-libp2p-kad-dht"
    "github.com/libp2p/go-libp2p/core/host"
)

func setupDHT(ctx context.Context, host host.Host) (*dht.IpfsDHT, error) {
    kdht, err := dht.New(ctx, host)
    if err != nil {
        return nil, err
    }
    if err = kdht.Bootstrap(ctx); err != nil {
        return nil, err
    }
    return kdht, nil
}
  1. Develop Custom Data Submission Protocol
    • Create a new libp2p protocol for data submission
    • Implement handlers for receiving and processing submitted data
const DataSubmissionProtocol = "/masa/data-submission/1.0.0"

func (vn *ValidatorNode) setupDataSubmissionProtocol() {
    vn.host.SetStreamHandler(protocol.ID(DataSubmissionProtocol), vn.handleDataSubmission)
}

func (vn *ValidatorNode) handleDataSubmission(stream network.Stream) {
    // Read data from stream
    // Validate data
    // Process data through Raft consensus
}
  1. Integrate Raft Consensus
    • Set up Raft cluster configuration for validator nodes
    • Implement BadgerFSM (Finite State Machine) for Raft
    • Create Libp2pTransport for Raft communication over libp2p
    • Configure Raft cluster with appropriate quorum based on the number of validator nodes
import (
    "github.com/hashicorp/raft"
)

func setupRaftCluster(nodeID string, fsm raft.FSM, transport raft.Transport, peers []string) (*raft.Raft, error) {
    config := raft.DefaultConfig()
    config.LocalID = raft.ServerID(nodeID)

    // Calculate the number of peers needed for quorum
    peerCount := len(peers) + 1 // Include self
    quorum := (peerCount / 2) + 1

    logrus.Infof("Raft cluster size: %d, Quorum required: %d", peerCount, quorum)

    // Configure the cluster
    configuration := raft.Configuration{}
    for _, peerID := range peers {
        server := raft.Server{
            ID:      raft.ServerID(peerID),
            Address: raft.ServerAddress(peerID),
        }
        configuration.Servers = append(configuration.Servers, server)
    }

    // Add self to the configuration
    configuration.Servers = append(configuration.Servers, raft.Server{
        ID:      raft.ServerID(nodeID),
        Address: raft.ServerAddress(nodeID),
    })

    // Create the Raft instance
    r, err := raft.NewRaft(config, fsm, logStore, stableStore, snapshotStore, transport)
    if err != nil {
        return nil, err
    }

    // Bootstrap the cluster if this is the first run
    future := r.BootstrapCluster(configuration)
    if err := future.Error(); err != nil && err != raft.ErrCantBootstrap {
        return nil, err
    }

    return r, nil
}

// In ValidatorNode initialization
peers := []string{"peer1ID", "peer2ID", "peer3ID"} // Get this list dynamically
raftInstance, err := setupRaftCluster(vn.nodeData.PeerId.String(), fsm, transport, peers)
if err != nil {
    return nil, fmt.Errorf("failed to set up Raft cluster: %v", err)
}
vn.raft = raftInstance
  1. Implement ValidatorNode structure and methods
    • Create ValidatorNode struct with all necessary components
    • Implement handleDataSubmission method for processing incoming data
    • Add methods for Raft proposal and DHT publication
type ValidatorNode struct {
    host       host.Host
    dht        *dht.IpfsDHT
    db         *badger.DB
    raft       *raft.Raft
    raftConfig *raft.Config
    nodeData   *pubsub.NodeData
}

func (vn *ValidatorNode) handleDataSubmission(stream network.Stream) {
    // Read and validate data
    // ...

    // Prepare Raft command
    cmd := Command{
        Op:    "set",
        Key:   getKey(data),
        Value: data,
    }
    cmdData, _ := json.Marshal(cmd)

    // Propose to Raft cluster
    future := vn.raft.Apply(cmdData, 5*time.Second)
    if err := future.Error(); err != nil {
        // Handle error
        return
    }

    // Publish to DHT
    vn.dht.PutValue(context.Background(), getKey(data), data)
}
  1. Develop NodeData Synchronization
    • Update BadgerFSM to modify NodeData structure on writes
    • Implement periodic synchronization between BadgerDB, NodeData, and DHT
func (vn *ValidatorNode) startPeriodicSync() {
    ticker := time.NewTicker(5 * time.Minute)
    for range ticker.C {
        vn.syncWithDHT()
    }
}

func (vn *ValidatorNode) syncWithDHT() {
    // Iterate through BadgerDB and update DHT
    err := vn.db.View(func(txn *badger.Txn) error {
        opts := badger.DefaultIteratorOptions
        it := txn.NewIterator(opts)
        defer it.Close()
        for it.Rewind(); it.Valid(); it.Next() {
            item := it.Item()
            k := item.Key()
            err := item.Value(func(v []byte) error {
                return vn.dht.PutValue(context.Background(), k, v)
            })
            if err != nil {
                return err
            }
        }
        return nil
    })
    if err != nil {
        logrus.Errorf("Error syncing with DHT: %v", err)
    }
}
  1. Error Handling and Logging
    • Implement comprehensive error handling throughout the system
    • Set up logging for important events and error conditions
import "github.com/sirupsen/logrus"

func init() {
    logrus.SetFormatter(&logrus.TextFormatter{
        FullTimestamp: true,
    })
    logrus.SetLevel(logrus.InfoLevel)
}

// Use throughout the code
logrus.Infof("Handling node data for: %s", data.PeerId)
logrus.Errorf("Error updating node data: %v", err)
  1. Testing
    • Unit tests for individual components (BadgerDB operations, DHT interactions, etc.)
    • Integration tests for the entire system
    • Stress tests to ensure system stability under high load
func TestValidatorNodeSync(t *testing.T) {
    // Set up test ValidatorNode
    // ...

    // Add test data
    testData := []byte("test data")
    vn.handleDataSubmission(testData)

    // Force sync
    vn.syncWithDHT()

    // Verify data in DHT
    value, err := vn.dht.GetValue(context.Background(), getKey(testData))
    assert.NoError(t, err)
    assert.Equal(t, testData, value)
}
  1. Consistency and Network Partitions
    • Implement a leader election mechanism using Raft
    • Set up a heartbeat system to detect node failures
    • Implement a read-after-write consistency check
func (vn *ValidatorNode) setupLeaderElection() {
    go func() {
        for {
            select {
            case isLeader := <-vn.raft.LeaderCh():
                if isLeader {
                    logrus.Info("This node has been elected leader")
                    vn.becomeLeader()
                } else {
                    logrus.Info("This node is no longer the leader")
                    vn.stepDownAsLeader()
                }
            }
        }
    }()
}

func (vn *ValidatorNode) becomeLeader() {
    // Implement leader-specific logic
}

func (vn *ValidatorNode) stepDownAsLeader() {
    // Implement follower-specific logic
}
  1. Performance Monitoring
    • Implement metrics collection for Raft operations, BadgerDB, and DHT
    • Set up Prometheus for metrics aggregation
    • Create Grafana dashboards for visualizing performance
import "github.com/prometheus/client_golang/prometheus"

var (
    raftApplyDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
        Name: "raft_apply_duration_seconds",
        Help: "Duration of Raft apply operations",
    })
)

func init() {
    prometheus.MustRegister(raftApplyDuration)
}

func (vn *ValidatorNode) handleDataSubmission(stream network.Stream) {
    // ... existing code ...

    timer := prometheus.NewTimer(raftApplyDuration)
    future := vn.raft.Apply(cmdData, 5*time.Second)
    timer.ObserveDuration()

    // ... rest of the code ...
}
  1. Scalability
    • Implement dynamic peer discovery using libp2p's peer discovery mechanisms
    • Set up a mechanism to add/remove nodes from the Raft cluster dynamically
func (vn *ValidatorNode) discoveryHandler(peer peer.AddrInfo) {
    if vn.shouldAddPeer(peer) {
        vn.raft.AddVoter(raft.ServerID(peer.ID.String()), raft.ServerAddress(peer.Addrs[0].String()), 0, 0)
    }
}

func (vn *ValidatorNode) shouldAddPeer(peer peer.AddrInfo) bool {
    // Implement logic to decide if a peer should be added to the Raft cluster
}
  1. Security
    • Implement TLS for all libp2p connections
    • Set up an authentication system for validators and non-validators
    • Implement rate limiting for data submissions
import "github.com/libp2p/go-libp2p/p2p/security/tls"

func setupSecureHost() (host.Host, error) {
    priv, _, err := crypto.GenerateKeyPair(crypto.Ed25519, -1)
    if err != nil {
        return nil, err
    }

    tlsTransport, err := tls.New(priv)
    if err != nil {
        return nil, err
    }

    return libp2p.New(
        libp2p.Identity(priv),
        libp2p.Security(tls.ID, tlsTransport),
    )
}
  1. Data Migration
    • Implement a versioning system for the data schema
    • Create migration scripts for updating data structures
type DataMigrator struct {
    currentVersion int
    migrations     map[int]func(*badger.Txn) error
}

func (dm *DataMigrator) Migrate(db *badger.DB) error {
    return db.Update(func(txn *badger.Txn) error {
        for v := dm.currentVersion + 1; v <= len(dm.migrations); v++ {
            if err := dm.migrations[v](txn); err != nil {
                return err
            }
            dm.currentVersion = v
        }
        return nil
    })
}
  1. Monitoring and Debugging
    • Implement detailed logging throughout the system
    • Set up distributed tracing using OpenTelemetry
    • Create health check endpoints for each component
import "go.opentelemetry.io/otel"

func (vn *ValidatorNode) handleDataSubmission(stream network.Stream) {
    ctx, span := otel.Tracer("validator").Start(context.Background(), "handle_data_submission")
    defer span.End()

    // ... existing code with added tracing ...
}

func (vn *ValidatorNode) healthCheck(w http.ResponseWriter, r *http.Request) {
    status := struct {
        BadgerDB string `json:"badgerdb"`
        Raft     string `json:"raft"`
        DHT      string `json:"dht"`
    }{
        BadgerDB: vn.checkBadgerDB(),
        Raft:     vn.checkRaft(),
        DHT:      vn.checkDHT(),
    }
    json.NewEncoder(w).Encode(status)
}
  1. Quorum and Cluster Size Management
    • Implement a configuration system for setting the initial cluster size
    • Create a mechanism for adjusting the cluster size dynamically
func (vn *ValidatorNode) adjustClusterSize(newSize int) error {
    currentSize := len(vn.raft.GetConfiguration().Configuration().Servers)
    if newSize == currentSize {
        return nil
    }

    if newSize > currentSize {
        // Add new nodes
        for i := currentSize; i < newSize; i++ {
            // Logic to add a new node
        }
    } else {
        // Remove nodes
        for i := currentSize; i > newSize; i-- {
            // Logic to remove a node
        }
    }

    return nil
}

Considerations and Challenges

  1. Consistency: Ensure strong consistency among validator nodes while maintaining eventual consistency across the entire network.
  2. Performance: Monitor and optimize performance, especially considering the overhead of Raft consensus.
  3. Network Partitions: Implement strategies to handle network partitions and prevent split-brain scenarios.
  4. Scalability: Ensure the system can scale with an increasing number of nodes and data volume.
  5. Security: Implement proper authentication and authorization mechanisms for data submission and node communication.
  6. Data Migration: Plan for potential future needs to migrate data or update the data schema.
  7. Monitoring and Debugging: Implement comprehensive monitoring and debugging tools for the distributed system.
  8. Quorum and Cluster Size: Carefully consider the number of validator nodes in the Raft cluster. While a larger cluster increases fault tolerance, it also increases the number of nodes required for consensus, potentially impacting write performance. A common practice is to use an odd number of nodes (e.g., 3, 5, or 7) to avoid split-vote scenarios.

Acceptance Criteria

  1. Validator nodes can successfully store and retrieve data locally using BadgerDB.
  2. Non-validator nodes can submit data to validators using the custom protocol.
  3. Validators reach consensus on data writes using Raft.
  4. Data is successfully distributed and retrievable via the libp2p DHT.
  5. NodeData structure is consistently updated with BadgerDB and DHT.
  6. System demonstrates fault tolerance by continuing to operate with a minority of validator nodes offline.
  7. Performance metrics are within acceptable ranges under various load conditions.
  8. The system can handle network partitions gracefully, maintaining consistency once the partition is resolved.
  9. New nodes can be added to the network dynamically, and the Raft cluster can be resized without downtime.
  10. All communications are secured using TLS, and proper authentication is in place for validators and non-validators.
  11. Data migrations can be performed without service interruption.
  12. Comprehensive monitoring and debugging tools are in place, providing insights into system health and performance.

Additional Considerations We Should Think About

teslashibe commented 1 month ago

Spike: Implement nodeDatadistribution with LevelDB and Raft Consensus

Overview

Implement a solution to maintain consistent nodeData across the network using LevelDB for local storage, libp2p DHT for distributed data access, and Raft consensus for coordinating writes among validator nodes, while maintaining a clear distinction between validator nodes and peers.

Objectives

  1. Utilize and refactor LevelDB for persistent storage
  2. Implement Raft consensus for coordinating writes among validators
  3. Ensure consistent nodeData updates across the network
  4. Optimize synchronization between LevelDB, nodeData, and DHT
  5. Maintain clear separation between validator nodes and peers
  6. Implement a mechanism for peers to submit data to validators
  7. Implement a reliable validator discovery mechanism
  8. Ensure secure data submission from peers to validators

Technical Details

Components

  1. LevelDB: Local key-value store
  2. libp2p DHT: Distributed Hash Table for network-wide data access
  3. Raft consensus: For write coordination among validators
  4. NodeData structure: For maintaining node-specific information
  5. Custom protocol: For peers to submit data to validators

Implementation Steps

  1. Refactor LevelDB usage and optimize configuration (4-5 days)
    • Remove existing LevelDB cache implementation
    • Refactor code to use LevelDB directly for all storage operations
    • Optimize LevelDB configuration for performance
    • Set up Raft consensus to work with the refactored LevelDB implementation

An example version of the LevelDB wrapper that removes caching and focuses on direct storage:

import (
    "github.com/syndtr/goleveldb/leveldb"
    "github.com/syndtr/goleveldb/leveldb/opt"
)

type LevelDBWrapper struct {
    db *leveldb.DB
}

func NewLevelDBWrapper(path string) (*LevelDBWrapper, error) {
    opts := &opt.Options{
        WriteBuffer:            64 * 1024 * 1024, // 64MB
        CompactionTableSize:    2 * 1024 * 1024,  // 2MB
        CompactionTotalSize:    10 * 1024 * 1024, // 10MB
        MaxOpenFiles:           1000,
        DisableSeeksCompaction: true,
    }
    db, err := leveldb.OpenFile(path, opts)
    if err != nil {
        return nil, err
    }
    return &LevelDBWrapper{db: db}, nil
}

func (l *LevelDBWrapper) Put(key []byte, value []byte) error {
    return l.db.Put(key, value, nil)
}

func (l *LevelDBWrapper) Get(key []byte) ([]byte, error) {
    return l.db.Get(key, nil)
}

func (l *LevelDBWrapper) Delete(key []byte) error {
    return l.db.Delete(key, nil)
}

func (l *LevelDBWrapper) Close() error {
    return l.db.Close()
}
  1. Optimize LevelDB Configuration
    • Configure LevelDB for optimal read and write performance
import (
    "github.com/syndtr/goleveldb/leveldb"
    "github.com/syndtr/goleveldb/leveldb/opt"
)

func NewLevelDBWrapper(path string) (*LevelDBWrapper, error) {
    opts := &opt.Options{
        BlockCacheCapacity:     32 * 1024 * 1024, // 32MB
        WriteBuffer:            64 * 1024 * 1024, // 64MB
        CompactionTableSize:    2 * 1024 * 1024,  // 2MB
        CompactionTotalSize:    10 * 1024 * 1024, // 10MB
        MaxOpenFiles:           1000,
        DisableSeeksCompaction: true,
    }
    db, err := leveldb.OpenFile(path, opts)
    if err != nil {
        return nil, err
    }
    return &LevelDBWrapper{db: db}, nil
}
  1. Integrate Raft Consensus
    • Set up Raft cluster configuration for validator nodes
    • Implement LevelDBFSM (Finite State Machine) for Raft
    • Create Libp2pTransport for Raft communication over libp2p
import (
    "github.com/hashicorp/raft"
)

type LevelDBFSM struct {
    db         *LevelDBWrapper
    nodeData   *pubsub.NodeData
}

func (l *LevelDBFSM) Apply(log *raft.Log) interface{} {
    var c Command
    if err := json.Unmarshal(log.Data, &c); err != nil {
        return err
    }

    switch c.Op {
    case "set":
        err := l.db.Put(context.Background(), datastore.NewKey(c.Key), c.Value)
        if err != nil {
            return err
        }
        return l.updateNodeData(c.Key, c.Value)
    case "updateNodeData":
        var nodeData pubsub.NodeData
        if err := json.Unmarshal(c.Value, &nodeData); err != nil {
            return err
        }
        return l.updateNodeData(c.Key, c.Value)
    // ... other operations ...
    }
}

func (l *LevelDBFSM) updateNodeData(key string, value []byte) error {
    // Update nodeData based on the key and value
    return nil
}
  1. Update ValidatorNode Structure
    • Modify the ValidatorNode structure to use LevelDBWrapper and Raft
type ValidatorNode struct {
    host         host.Host
    dht          *dht.IpfsDHT
    levelDB      *LevelDBWrapper
    raft         *raft.Raft
    raftConfig   *raft.Config
    nodeData     *pubsub.NodeData
    isValidator  bool
}
  1. Implement Data Submission Protocol for Peers
    • Create a custom libp2p protocol for peers to submit their data to validators
    • Implement handlers on validator nodes to receive and process this data
const DataSubmissionProtocol = "/masa/data-submission/1.0.0"

func (vn *ValidatorNode) setupDataSubmissionProtocol() {
    vn.host.SetStreamHandler(protocol.ID(DataSubmissionProtocol), vn.handleDataSubmission)
}

func (vn *ValidatorNode) handleDataSubmission(stream network.Stream) {
    defer stream.Close()

    // Authenticate the peer
    if !vn.authenticatePeer(stream.Conn().RemotePeer()) {
        logrus.Errorf("Unauthorized data submission attempt from %s", stream.Conn().RemotePeer())
        return
    }

    var data pubsub.NodeData
    if err := json.NewDecoder(stream).Decode(&data); err != nil {
        logrus.Errorf("Error decoding submitted data: %v", err)
        return
    }

    if err := vn.validateSubmittedData(&data); err != nil {
        logrus.Errorf("Invalid data submission: %v", err)
        return
    }

    if err := vn.processNodeDataUpdate(&data); err != nil {
        logrus.Errorf("Error processing node data update: %v", err)
        return
    }

    if err := json.NewEncoder(stream).Encode("ACK"); err != nil {
        logrus.Errorf("Error sending ACK: %v", err)
    }
}

func (vn *ValidatorNode) authenticatePeer(peerID peer.ID) bool {
    // Implement authentication logic
    return true
}

func (vn *ValidatorNode) validateSubmittedData(data *pubsub.NodeData) error {
    // Implement validation logic
    return nil
}

func (vn *ValidatorNode) processNodeDataUpdate(data *pubsub.NodeData) error {
    cmd := Command{
        Op:    "updateNodeData",
        Key:   data.PeerId,
        Value: data,
    }
    cmdData, _ := json.Marshal(cmd)

    future := vn.raft.Apply(cmdData, 5*time.Second)
    return future.Error()
}
  1. Implement Peer Node Structure and Methods
    • Create a PeerNode struct for peer nodes
    • Implement methods for peers to submit their data to validators
type PeerNode struct {
    host     host.Host
    dht      *dht.IpfsDHT
    nodeData *pubsub.NodeData
}

func (pn *PeerNode) submitNodeData(ctx context.Context) error {
    validatorPeer, err := pn.findValidator()
    if err != nil {
        return fmt.Errorf("failed to find a validator: %w", err)
    }

    stream, err := pn.host.NewStream(ctx, validatorPeer, protocol.ID(DataSubmissionProtocol))
    if err != nil {
        return fmt.Errorf("failed to open stream to validator: %w", err)
    }
    defer stream.Close()

    if err := json.NewEncoder(stream).Encode(pn.nodeData); err != nil {
        return fmt.Errorf("failed to send node data: %w", err)
    }

    var ack string
    if err := json.NewDecoder(stream).Decode(&ack); err != nil {
        return fmt.Errorf("failed to receive ACK: %w", err)
    }

    if ack != "ACK" {
        return fmt.Errorf("unexpected response from validator: %s", ack)
    }

    return nil
}

func (pn *PeerNode) findValidator() (peer.ID, error) {
    // Query the DHT for validator nodes
    validatorKey := "/masa/validators"
    validatorPeers, err := pn.dht.GetValues(context.Background(), validatorKey)
    if err != nil {
        return "", fmt.Errorf("failed to find validators: %w", err)
    }

    // Select a random validator from the list
    if len(validatorPeers) == 0 {
        return "", fmt.Errorf("no validators found")
    }
    randomIndex := rand.Intn(len(validatorPeers))
    return peer.ID(validatorPeers[randomIndex]), nil
}
  1. Implement Consistent Write Operations
    • Update the WriteData function to use Raft for consensus before writing to LevelDB
func WriteData(node *ValidatorNode, key string, value []byte) error {
    cmd := Command{
        Op:    "set",
        Key:   key,
        Value: value,
    }
    cmdData, _ := json.Marshal(cmd)

    future := node.raft.Apply(cmdData, 5*time.Second)
    if err := future.Error(); err != nil {
        return fmt.Errorf("failed to apply Raft log: %w", err)
    }

    return nil
}
  1. Update Synchronization Mechanism
    • Modify the existing sync and iterateAndPublish functions to work with the new Raft-based system
func iterateAndPublish(ctx context.Context, node *ValidatorNode) {
    snapshot, err := node.raft.Snapshot()
    if err != nil {
        logrus.Errorf("Error getting Raft snapshot: %v", err)
        return
    }
    defer snapshot.Close()

    snapshotData, err := io.ReadAll(snapshot)
    if err != nil {
        logrus.Errorf("Error reading Raft snapshot: %v", err)
        return
    }

    var records []Record
    if err := json.Unmarshal(snapshotData, &records); err != nil {
        logrus.Errorf("Error unmarshalling snapshot data: %v", err)
        return
    }

    for _, record := range records {
        if err := node.dht.PutValue(ctx, record.Key, record.Value); err != nil {
            logrus.Debugf("Error publishing to DHT: %v", err)
        }
    }
}
  1. Update monitorNodeData Function
    • Modify the monitorNodeData function to work with Raft and LevelDB
func monitorNodeData(ctx context.Context, node *ValidatorNode) {
    syncInterval := time.Second * 60
    ticker := time.NewTicker(syncInterval)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            nodeData := node.nodeData
            jsonData, _ := json.Marshal(nodeData)

            if err := WriteData(node, node.Host.ID().String(), jsonData); err != nil {
                logrus.Errorf("Error writing node data: %v", err)
            }

            if err := node.dht.PutValue(ctx, node.Host.ID().String(), jsonData); err != nil {
                logrus.Errorf("Error publishing node data to DHT: %v", err)
            }

        case <-ctx.Done():
            return
        }
    }
}
  1. Implement Raft Cluster Management
    • Add functions to manage the Raft cluster, including adding and removing nodes
func (vn *ValidatorNode) addNodeToCluster(nodeID string, addr string) error {
    return vn.raft.AddVoter(raft.ServerID(nodeID), raft.ServerAddress(addr), 0, 0).Error()
}

func (vn *ValidatorNode) removeNodeFromCluster(nodeID string) error {
    return vn.raft.RemoveServer(raft.ServerID(nodeID), 0, 0).Error()
}
  1. Implement Validator Discovery
    • Add a method for validators to register themselves in the DHT
    • Implement a discovery mechanism for peers to find validators
func (vn *ValidatorNode) registerAsValidator() error {
    validatorKey := "/masa/validators"
    return vn.dht.PutValue(context.Background(), validatorKey, []byte(vn.host.ID().String()))
}

func (pn *PeerNode) findValidators() ([]peer.ID, error) {
    validatorKey := "/masa/validators"
    validatorPeers, err := pn.dht.GetValues(context.Background(), validatorKey)
    if err != nil {
        return nil, fmt.Errorf("failed to find validators: %w", err)
    }

    var validators []peer.ID
    for _, peerID := range validatorPeers {
        validators = append(validators, peer.ID(peerID))
    }
    return validators, nil
}
  1. Implement Security Measures
    • Add authentication and authorization for peer data submissions
    • Implement encryption for data transmission between nodes
func (vn *ValidatorNode) authenticatePeer(peerID peer.ID) bool {
    // Implement authentication logic, e.g., check against a whitelist or verify a signature
    return true // Placeholder
}

func (pn *PeerNode) encryptData(data []byte) ([]byte, error) {
    // Implement encryption logic
    return data, nil // Placeholder
}

func (vn *ValidatorNode) decryptData(data []byte) ([]byte, error) {
    // Implement decryption logic
    return data, nil // Placeholder
}

Considerations and Challenges

  1. Consistency: Ensure strong consistency among validator nodes while maintaining eventual consistency across the entire network.
  2. Performance: Monitor and optimize performance, especially considering the overhead of Raft consensus.
  3. Network Partitions: Implement strategies to handle network partitions and prevent split-brain scenarios.
  4. Scalability: Ensure the system can scale with an increasing number of nodes and data volume.
  5. Error Handling: Implement robust error handling and recovery mechanisms.
  6. Validator Discovery: Ensure the validator discovery mechanism is reliable and efficient.
  7. Security: Implement strong authentication and encryption measures to protect against unauthorized access and data breaches.

Acceptance Criteria

  1. Validator nodes can successfully store and retrieve data locally using LevelDB.
  2. Validators reach consensus on data writes using Raft.
  3. NodeData structure is consistently updated across all validator nodes.
  4. Peer nodes can successfully submit their data to validator nodes.
  5. System demonstrates fault tolerance by continuing to operate with a minority of validator nodes offline.
  6. Performance metrics are within acceptable ranges under various load conditions.
  7. Existing functionality (e.g., DHT synchronization) continues to work with the new Raft-based system.
  8. Clear separation of roles between validator nodes and peers is maintained.
  9. Peer nodes can reliably discover and connect to validator nodes.
  10. Only authorized peer nodes can submit data to validators.
  11. Data transmission between nodes is secure and encrypted.

Implementation Plan

  1. Set up Raft consensus and optimize LevelDB
  2. Refactor ValidatorNode and implement write operations
  3. Implement PeerNode and data submission protocol
  4. Update synchronization mechanisms
  5. Implement Raft cluster management
  6. Implement validator discovery mechanism
  7. Implement security measures (authentication and encryption)
  8. Testing and performance optimization
  9. Documentation and code review
teslashibe commented 1 month ago

Improvement Spike: Transitioning from Raft to HotStuff Consensus (FUTURE IMPROVEMENT ONCE INITIAL IMPLEMENTATION DONE)

Overview

This spike outlines the process of transitioning our decentralized data protocol from Raft consensus to HotStuff consensus. The goal is to improve scalability, performance, and Byzantine fault tolerance while maintaining the existing functionality and integration with LevelDB and libp2p DHT.

Objectives

  1. Replace Raft consensus with HotStuff consensus for coordinating writes among validators
  2. Maintain and improve consistency of nodeData updates across the network
  3. Optimize performance and scalability for larger validator sets
  4. Ensure backward compatibility during the transition phase
  5. Implement Byzantine fault tolerance
  6. Adapt existing components to work with HotStuff

Technical Details

Components to Modify

  1. ConsensusEngine: Replace Raft implementation with HotStuff
  2. ValidatorNode: Update to use HotStuff instead of Raft
  3. LevelDBFSM: Adapt to HotStuff's block-based consensus
  4. WriteData function: Modify to use HotStuff's block proposal mechanism
  5. Synchronization mechanisms: Update to work with HotStuff's view-based system

Implementation Steps

  1. Implement HotStuff Consensus Engine (7-10 days)
    • Create a new HotStuffEngine struct to replace the existing RaftEngine
    • Implement core HotStuff algorithm including view synchronization, leader election, and block management
    • Implement Quorum Certificates (QCs) for tracking the voting process
import (
    "crypto"
    "github.com/gohotstuff/hotstuff" // Hypothetical HotStuff library
)

type HotStuffEngine struct {
    privateKey       crypto.PrivateKey
    publicKey        crypto.PublicKey
    validators       map[string]*Validator
    currentView      uint64
    highestQC        *QuorumCertificate
    pendingBlock     *Block
    committedBlocks  []*Block
    fsm              *LevelDBFSM
}

func NewHotStuffEngine(privateKey crypto.PrivateKey, validators map[string]*Validator, fsm *LevelDBFSM) *HotStuffEngine {
    // Initialize HotStuff engine
}

func (e *HotStuffEngine) ProposeBlock(transactions []Transaction) (*Block, error) {
    // Leader creates and proposes a new block
}

func (e *HotStuffEngine) ValidateBlock(block *Block) bool {
    // Validators check the validity of the proposed block
}

type QuorumCertificate struct {
    BlockHash []byte
    View      uint64
    Signatures map[string][]byte
}

func (e *HotStuffEngine) CreateQC(block *Block) (*QuorumCertificate, error) {
    // Create and gather signatures for a QC
}
  1. Update ValidatorNode Structure (3-4 days)
    • Modify the ValidatorNode structure to use HotStuffEngine instead of Raft
    • Update related methods to interact with HotStuff consensus
type ValidatorNode struct {
    host         host.Host
    dht          *dht.IpfsDHT
    levelDB      *LevelDBWrapper
    hotStuff     *HotStuffEngine
    nodeData     *pubsub.NodeData
    isValidator  bool
}

func (vn *ValidatorNode) StartConsensus() error {
    // Initialize and start HotStuff consensus
}
  1. Adapt LevelDBFSM for HotStuff (2-3 days)
    • Modify LevelDBFSM to work with HotStuff's block-based consensus instead of Raft's log-based approach
type LevelDBFSM struct {
    db         *LevelDBWrapper
    nodeData   *pubsub.NodeData
}

func (l *LevelDBFSM) ApplyBlock(block *Block) error {
    // Apply the transactions in the committed block to LevelDB
    for _, tx := range block.Transactions {
        if err := l.db.Put(tx.Key, tx.Value); err != nil {
            return err
        }
    }
    return l.updateNodeData(block)
}

func (l *LevelDBFSM) updateNodeData(block *Block) error {
    // Update nodeData based on the transactions in the block
    return nil
}
  1. Update WriteData Function (1-2 days)
    • Modify the WriteData function to use HotStuff's block proposal mechanism
func WriteData(node *ValidatorNode, key string, value []byte) error {
    transaction := Transaction{Key: key, Value: value}
    block, err := node.hotStuff.ProposeBlock([]Transaction{transaction})
    if err != nil {
        return fmt.Errorf("failed to propose block: %w", err)
    }

    // Wait for the block to be committed
    committed := <-node.hotStuff.CommitChan
    if committed.Hash() != block.Hash() {
        return fmt.Errorf("proposed block was not committed")
    }

    return nil
}
  1. Update Synchronization Mechanisms (3-4 days)
    • Modify existing sync and iterateAndPublish functions to work with HotStuff's view-based system
    • Implement view synchronization for HotStuff
func (vn *ValidatorNode) SyncView() error {
    // Implement view synchronization logic
    return nil
}

func iterateAndPublish(ctx context.Context, node *ValidatorNode) {
    latestBlock := node.hotStuff.GetLatestCommittedBlock()
    for _, tx := range latestBlock.Transactions {
        if err := node.dht.PutValue(ctx, tx.Key, tx.Value); err != nil {
            logrus.Debugf("Error publishing to DHT: %v", err)
        }
    }
}
  1. Implement Byzantine Fault Tolerance Measures (4-5 days)
    • Add mechanisms to detect and handle Byzantine behavior
    • Implement view-change protocol for leader rotation
func (e *HotStuffEngine) DetectByzantineBehavior(node *ValidatorNode) bool {
    // Implement Byzantine behavior detection
    return false
}

func (e *HotStuffEngine) InitiateViewChange() error {
    // Implement view-change protocol
    return nil
}
  1. Update Cluster Management (2-3 days)
    • Modify functions for adding and removing nodes to work with HotStuff
func (vn *ValidatorNode) AddNodeToCluster(nodeID string, pubKey crypto.PublicKey) error {
    return vn.hotStuff.AddValidator(nodeID, pubKey)
}

func (vn *ValidatorNode) RemoveNodeFromCluster(nodeID string) error {
    return vn.hotStuff.RemoveValidator(nodeID)
}
  1. Implement Backward Compatibility (5-7 days)
    • Create a transition mechanism to support both Raft and HotStuff during the migration period
    • Implement a version negotiation protocol for nodes to determine which consensus mechanism to use
type ConsensusEngine interface {
    ProposeBlock(transactions []Transaction) (*Block, error)
    ValidateBlock(block *Block) bool
    // Other common methods
}

func (vn *ValidatorNode) NegotiateConsensusVersion(peer peer.ID) (string, error) {
    // Implement version negotiation logic
    return "hotstuff", nil
}

Considerations and Challenges

  1. Migration Strategy: Develop a strategy for migrating the network from Raft to HotStuff without disrupting ongoing operations.
  2. Performance Impact: Monitor and optimize performance during and after the transition, especially for larger validator sets.
  3. Data Consistency: Ensure data remains consistent during the transition and with the new consensus mechanism.
  4. Byzantine Fault Tolerance: Implement and thoroughly test BFT mechanisms to handle malicious nodes.
  5. Scalability: Verify that the HotStuff implementation provides the expected scalability improvements over Raft.
  6. Compatibility: Ensure all existing components (e.g., peer data submission, validator discovery) remain functional with HotStuff.
  7. Testing: Develop comprehensive test suites to verify the correct operation of HotStuff consensus, including Byzantine fault scenarios.

Acceptance Criteria

  1. HotStuff consensus is successfully implemented and integrated with existing components.
  2. Validator nodes can reach consensus on data writes using HotStuff.
  3. The system demonstrates Byzantine fault tolerance by continuing to operate correctly with up to f faulty nodes in a 3f+1 node network.
  4. Performance metrics show improvement over the Raft-based system, especially with larger validator sets.
  5. All existing functionality (e.g., DHT synchronization, peer data submission) continues to work with the new HotStuff-based system.
  6. The system can smoothly transition from Raft to HotStuff without data loss or extended downtime.
  7. New nodes can join the network and sync correctly using the HotStuff consensus.
mudler commented 1 month ago

@teslashibe I'm a bit confused by the comments - this card as it is formulated would be an implementation one, can't be picked up until we groom out https://github.com/masa-finance/masa-oracle/issues/496 and we agree on what should look like, no?

For instance, I found good reads about hotstuff here: https://decentralizedthoughts.github.io/2021-07-17-simplifying-raft-with-chaining/ but there seems to be no implementation already done on top of libp2p (while for raft-only, there is) - but that would require its own spike around after we work on having a basic form of consensus

Update: re raft/libp2p, I've pushed https://github.com/mudler/go-libp2p-simple-raft as an example on how to integrate with libp2p directly the raft library and updated the consensus ticket accordingly

teslashibe commented 1 month ago

Yeah agree on your comments @mudler we do need to groom #496 first and decide there what we will use for consensus.

mudler commented 1 month ago

output from planning:

mudler commented 1 month ago

I think we have covered already the spike with @teslashibe 's investigations above. I'm now closing this one with follow ups tracked in: https://github.com/masa-finance/masa-oracle/issues/496 and https://github.com/masa-finance/masa-oracle/issues/427