Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
1ba61cc
feat: Implement FastSync V2 with ImmuDB for block and account data ma…
neerajvipparla Mar 26, 2026
96e1b30
test: load settings before running the Merkle tree test
neerajvipparla Mar 26, 2026
64a3e95
feat: Add new `FastSyncV2` gRPC endpoint with corresponding client an…
neerajvipparla Mar 26, 2026
10b7e41
refactor: construct and use targetNodeInfo for fastsync requests inst…
neerajvipparla Mar 26, 2026
99c82ea
fix: prevent uint64 underflow in CheckNonceAndGetLatest block scan (#22)
neerajvipparla Mar 26, 2026
84fec1a
feat: Update checksum version to 2 and hardcode protocol version for …
neerajvipparla Mar 26, 2026
1241e26
refactor: update sync protocol with extended transaction fields, ZK c…
neerajvipparla Mar 30, 2026
d1a82fd
Fix/fastsync (#23)
neerajvipparla Mar 30, 2026
375cbfa
fix: update sync version parameters to ensure correct transport selec…
neerajvipparla Mar 30, 2026
2bf189a
Merge branch 'Fastsync' into fix/Fastsync
neerajvipparla Mar 30, 2026
6c51c62
chore: update priorsyncVersion to 2 in fastsyncv2.go
neerajvipparla Mar 30, 2026
a3f6c09
Merge branch 'fix/Fastsync' of https://github.com/JupiterMetaLabs/jmd…
neerajvipparla Mar 30, 2026
c4fc2be
fix: implement force update logic for existing blocks in immudb writers
neerajvipparla Mar 30, 2026
f285a59
feat: implement event-driven vote collection and notification system
neerajvipparla Mar 31, 2026
51cc6a0
feat: implement incremental startup sync in FastsyncV2 to catch up on…
neerajvipparla Apr 1, 2026
1e26941
Merge branch 'fix/Fastsync' of https://github.com/JupiterMetaLabs/jmd…
neerajvipparla Apr 1, 2026
eda70ca
chore: add project planning documentation and workspace configuration…
neerajvipparla Apr 2, 2026
05afeb9
docs: add documentation for consensus architecture and MCP tools, and…
neerajvipparla Apr 8, 2026
6990e9c
feat: implement fastsyncv2 CLI command and add nil-safety checks for …
neerajvipparla Apr 8, 2026
7649dde
fix: handle key not found errors in immudb account manager by returni…
neerajvipparla Apr 8, 2026
133ca81
refactor: update PoTS to use commsVersion and resolve full peer addre…
neerajvipparla Apr 10, 2026
00caaaf
feat: add script to scan accounts database for duplicate nonces
neerajvipparla Apr 10, 2026
ac5334a
feat: add detailed account listing and simplify duplicate nonce repor…
neerajvipparla Apr 10, 2026
7a3128a
fix: implement robust block marker reconciliation and increase databa…
saishibunb Apr 14, 2026
815deee
feat: introduce FastSync V2 configuration and initialization
neerajvipparla Apr 17, 2026
82d4a72
feat: enhance FastSync V2 configuration with new settings and conditi…
neerajvipparla Apr 17, 2026
54d0856
refactor(config): standardize fastsync defaults and struct order
i-naman Apr 17, 2026
3d559ea
fix: prevent HeaderSync from prematurely advancing the latest_block m…
neerajvipparla Apr 17, 2026
186e28c
Merge branch 'fix/Fastsync' of https://github.com/JupiterMetaLabs/jmd…
neerajvipparla Apr 17, 2026
5211afa
refactor: simplify GetLatestBlockNumber by removing retry logic and r…
neerajvipparla Apr 21, 2026
d7cb4cb
refactor: remove manual block reconciliation logic from immudb_adapte…
neerajvipparla Apr 21, 2026
02f5ad6
chore(deps): pin JMDN-FastSync to fix/auth-error-handling branch comm…
i-naman Apr 21, 2026
9abee45
chore(deps): upgrade JMDN-FastSync to e464cfd (auth propagation fix)
i-naman Apr 21, 2026
bef6f93
refactor: add Timestamp field to Transaction struct in multiple files…
neerajvipparla May 13, 2026
eeba3e0
chore: update dependencies in go.mod and go.sum, including upgrades t…
neerajvipparla May 13, 2026
ace63c0
Merge branch 'fix/Fastsync' of https://github.com/JupiterMetaLabs/jmd…
neerajvipparla May 13, 2026
cd96609
feat: implement account management features in immudb_account_manager…
neerajvipparla May 18, 2026
09c1325
chore: clean up go.mod and go.sum by removing unused dependencies and…
neerajvipparla May 18, 2026
31546a9
feat: enhance account retrieval and header synchronization in immudb
neerajvipparla May 18, 2026
99b300c
feat: enhance account fetching and local account creation in fastsyncv2
neerajvipparla May 18, 2026
397a7ac
Revert "feat: enhance account fetching and local account creation in …
neerajvipparla May 18, 2026
afe109c
feat: add account synchronization command and functionality in fastsy…
neerajvipparla May 18, 2026
07a92de
feat: add AccountSync method to CLIService for account synchronization
neerajvipparla May 18, 2026
d2818b1
feat: implement accountsync command and functionality for account syn…
neerajvipparla May 18, 2026
f411014
chore: update JMDN-FastSync dependency version in go.mod and go.sum
neerajvipparla May 19, 2026
dc4ee51
feat(fastsync): enforce strict read-only/serve-only sync guardrails
i-naman May 19, 2026
9dce864
refactor(fastsync): standardize config naming to clarify network dire…
i-naman May 19, 2026
0576279
feat(scripts): add merkle_check.go for post-sync block integrity vali…
i-naman May 19, 2026
e04f9aa
chore: increase HTTP server read and write timeouts to 60 seconds
i-naman May 19, 2026
60ca959
feat: enhance BatchRestoreAccounts with deduplication and chunked ope…
neerajvipparla May 20, 2026
6199ee4
Merge branch 'fix/Fastsync' of https://github.com/JupiterMetaLabs/jmd…
neerajvipparla May 20, 2026
1641351
feat: optimize BatchRestoreAccounts with pre-fetching of existing acc…
neerajvipparla May 20, 2026
7cec217
chore: update JMDN-FastSync dependency version in go.mod and go.sum
neerajvipparla May 21, 2026
b3f546c
feat: introduce cursor-based pagination for account listing
saishibunb May 21, 2026
d51407c
Fix/accountsync/performance (#35)
neerajvipparla May 26, 2026
a5a5f95
feat(logging): add custom headers support to OTEL config and upgrade …
i-naman May 26, 2026
18ace20
Add/redisstreams (#37)
neerajvipparla May 26, 2026
b137dfe
Remove unused functions for transaction and access list conversion in…
neerajvipparla May 26, 2026
a564546
feat(config): add Redis authentication and complete Viper env bindings
i-naman May 27, 2026
4d931e8
fix(fastsync): map missing ChainID, AccessList, and LogsBloom fields
i-naman May 28, 2026
abe2d6d
fix(accountsdb): preserve DIDs and metadata during sync
i-naman May 28, 2026
397a4e4
feat(migration): separate nonce patching scripts and use O(1) nonce f…
i-naman May 28, 2026
1e0469a
fix(security): resolve same-block nonce replay vulnerabilities and de…
i-naman May 28, 2026
ded0def
fix(redis): remove hard startup dependency for account sync
i-naman Jun 2, 2026
49e269c
refactor(account_sync): streamline account sync worker initialization…
neerajvipparla Jun 2, 2026
7cda66e
Merge branch 'fix/Fastsync' of https://github.com/JupiterMetaLabs/jmd…
neerajvipparla Jun 2, 2026
f8a155f
chore(deps): update JMDN-FastSync to v0.0.0-20260601052219-40e74741de…
neerajvipparla Jun 2, 2026
a06e3a7
fix(accountsdb): remove dead PutNonceofAccount to prevent time.Now() …
i-naman Jun 3, 2026
5764193
refactor: cleanly decouple consensus account origination and fix Stat…
i-naman Jun 4, 2026
83dbb29
Merge remote-tracking branch 'origin/fix/Fastsync' into fix/nonce-mig…
i-naman Jun 4, 2026
11221b4
feat(accounts): align struct to Fastsync by renaming StateID to Nonce…
i-naman Jun 4, 2026
fab4188
Implement bounded enqueue for account synchronization
neerajvipparla Jun 5, 2026
df5f00d
fix: Correct Ethereum TxNonce validation logic to prevent intra-block…
i-naman Jun 5, 2026
8e2ce3f
fix(pubsub): close topic race condition
i-naman Jun 8, 2026
5300398
Merge remote-tracking branch 'origin/fix/Fastsync' into review/fastsy…
i-naman Jun 9, 2026
0ecead5
Merge remote-tracking branch 'origin/fix/Fastsync' into fix/nonce-mig…
i-naman Jun 9, 2026
bc7b3b1
Merge remote-tracking branch 'origin/fix/Fastsync' into review/fastsy…
i-naman Jun 9, 2026
1b9e85f
Merge remote-tracking branch 'origin/fix/nonce-migration-and-consensu…
i-naman Jun 9, 2026
7c309c0
chore: fix govet deprecation warnings for Go 1.25+
i-naman Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,9 @@ __debug_bin
vendor/

# Internal team references
docs/SONARQUBE_SETUP_GUIDE.md
docs/SONARQUBE_SETUP_GUIDE.md
jmdn.yaml
internal/WAL/.tmp/*
.claude/*
.code-review-graph/*
.cursor/*
7 changes: 3 additions & 4 deletions AVC/BLS/bls-sign/bls-sgin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
Expand Down Expand Up @@ -97,7 +96,7 @@ func GenerateBLSKeyPair() ([]byte, []byte, error) {
PubKey string `json:"bls_pub"`
}

if data, err := ioutil.ReadFile(config.BLSFile); err == nil {
if data, err := os.ReadFile(config.BLSFile); err == nil {
var bf blsFile
if err := json.Unmarshal(data, &bf); err == nil && bf.PrivKey != "" && bf.PubKey != "" {
if priv, err := base64.StdEncoding.DecodeString(bf.PrivKey); err == nil {
Expand Down Expand Up @@ -128,7 +127,7 @@ func GenerateBLSKeyPair() ([]byte, []byte, error) {
PeerID string `json:"peer_id"`
}
var pf peerFile
if pdata, err := ioutil.ReadFile(config.PeerFile); err == nil {
if pdata, err := os.ReadFile(config.PeerFile); err == nil {
_ = json.Unmarshal(pdata, &pf)
}

Expand All @@ -138,7 +137,7 @@ func GenerateBLSKeyPair() ([]byte, []byte, error) {
PubKey: base64.StdEncoding.EncodeToString(pubBytes),
}
if out, err := json.MarshalIndent(bf, "", " "); err == nil {
_ = ioutil.WriteFile(config.BLSFile, out, 0o600)
_ = os.WriteFile(config.BLSFile, out, 0o600)
}

return privBytes, pubBytes, nil
Expand Down
163 changes: 39 additions & 124 deletions CLI/CLI.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"gossipnode/Block"
CLICommon "gossipnode/CLI/common"
"gossipnode/DB_OPs"
"gossipnode/FastsyncV2"
"gossipnode/config"
"gossipnode/config/GRO"
"gossipnode/config/version"
Expand Down Expand Up @@ -52,13 +53,15 @@ type CommandHandler struct {
Node *config.Node
NodeManager *node.NodeManager
FastSyncer *fastsync.FastSync
FastSyncerV2 *FastsyncV2.FastsyncV2
MainClient *config.PooledConnection
DIDClient *config.PooledConnection
SeedNode string
EnableYggdrasil bool
ChainID int
FacadePort int
WSPort int
PullAllowed bool
}

// Simple helper to print the CLI prompt in color
Expand Down Expand Up @@ -104,8 +107,8 @@ func PrintFuncs() {
fmt.Println(" mempoolStats - Show mempool statistics")
fmt.Println(" stats - Show messaging statistics")
fmt.Println(" broadcast <message> - Broadcast a message to all connected peers")
fmt.Println(" fastsync <peer_multiaddr> - Fast sync blockchain data with a peer")
fmt.Println(" firstsync <peer_multiaddr> <server|client> - First sync: get all data from peer (server) or receive all data (client)")
fmt.Println(" fastsync <peer_multiaddr> - Fast sync blockchain data with a peer (V2 Engine)")
fmt.Println(" accountsync <peer_multiaddr> - Sync missing accounts only (skip block sync)")
fmt.Println(" dbstate - Show current ImmuDB database state")
fmt.Println(" propagateDID <did> <public_key> - Propagate a DID to the network")
fmt.Println(" getDID <did> - Get a DID document from the network")
Expand Down Expand Up @@ -263,10 +266,10 @@ func (h *CommandHandler) handleCommand(parts []string) {
h.handleShowStats()
case "broadcast":
h.handleBroadcast(parts)
case "fastsync":
case "fastsync", "fastsyncv2", "firstsync":
h.handleFastSync(parts)
case "firstsync":
h.handleFirstSync(parts)
case "accountsync":
h.handleAccountSync(parts)
case "propagateDID":
h.handlePropagateDID(parts)
case "syncinfo":
Expand Down Expand Up @@ -577,15 +580,8 @@ func (h *CommandHandler) handleFastSync(parts []string) {
return
}

err := h.checkDBClient()
if err != nil {
fmt.Printf("Database client not initialized: %v\n", err)
return
}

err = h.checkDIDClient()
if err != nil {
fmt.Printf("DID database client not initialized: %v\n", err)
if h.FastSyncerV2 == nil {
fmt.Println("Error: FastsyncV2 engine is not initialized")
return
}

Expand All @@ -603,140 +599,59 @@ func (h *CommandHandler) handleFastSync(parts []string) {
return
}

// Get both database states before sync
mainState, err := DB_OPs.GetDatabaseState(h.MainClient.Client)
if err != nil {
fmt.Printf("Failed to get main database state: %v\n", err)
return
}

accountsState, err := DB_OPs.GetDatabaseState(h.DIDClient.Client)
if err != nil {
fmt.Printf("Failed to get accounts database state: %v\n", err)
return
// Show pre-sync DB state if clients are available
if h.MainClient != nil && h.DIDClient != nil {
mainState, err := DB_OPs.GetDatabaseState(h.MainClient.Client)
if err == nil {
fmt.Printf("Pre-sync main DB state: TxID=%d, Root=%x\n", mainState.TxId, mainState.TxHash)
}
}

fmt.Printf("Starting blockchain sync with peer %s\n", addrInfo.ID.String())
fmt.Printf("Our current main DB state: TxID=%d, Root=%x\n", mainState.TxId, mainState.TxHash)
fmt.Printf("Our current accounts DB state: TxID=%d, Root=%x\n", accountsState.TxId, accountsState.TxHash)
fmt.Printf("Starting blockchain fastsync (V2 Engine) with peer %s\n", addrInfo.ID.String())

// Start the sync process
startTime := time.Now().UTC()

maxRetries := 3
var syncErr error

for retry := 0; retry < maxRetries; retry++ {
if retry > 0 {
fmt.Printf("Retry %d/%d after error: %v\n", retry+1, maxRetries, syncErr)
time.Sleep(2 * time.Second)
}

_, syncErr = h.FastSyncer.HandleSync(addrInfo.ID)
if syncErr == nil {
break
}
}

syncErr := h.FastSyncerV2.HandleSync(parts[1])
if syncErr != nil {
fmt.Printf("Sync failed after %d attempts: %v\n", maxRetries, syncErr)
return
}

// Get post-sync states
newMainState, err := DB_OPs.GetDatabaseState(h.MainClient.Client)
if err != nil {
fmt.Printf("Failed to get main database state after sync: %v\n", err)
fmt.Printf("Fastsync failed: %v\n", syncErr)
return
}

newAccountsState, err := DB_OPs.GetDatabaseState(h.DIDClient.Client)
if err != nil {
fmt.Printf("Failed to get accounts database state after sync: %v\n", err)
return
// Show post-sync DB state if clients are available
if h.MainClient != nil && h.DIDClient != nil {
newMainState, err := DB_OPs.GetDatabaseState(h.MainClient.Client)
if err == nil {
fmt.Printf("Post-sync main DB state: TxID=%d, Root=%x\n", newMainState.TxId, newMainState.TxHash)
}
newAccountsState, err := DB_OPs.GetDatabaseState(h.DIDClient.Client)
if err == nil {
fmt.Printf("Post-sync accounts DB state: TxID=%d, Root=%x\n", newAccountsState.TxId, newAccountsState.TxHash)
}
}

fmt.Printf("Sync completed in %v\n", time.Since(startTime))
fmt.Printf("New main DB state: TxID=%d, Root=%x\n", newMainState.TxId, newMainState.TxHash)
fmt.Printf("New accounts DB state: TxID=%d, Root=%x\n", newAccountsState.TxId, newAccountsState.TxHash)
fmt.Printf("Fastsync completed in %v\n", time.Since(startTime))
printDashes()
}

func (h *CommandHandler) handleFirstSync(parts []string) {
if len(parts) != 3 {
fmt.Println("Usage: firstsync <peer_multiaddr> <server|client>")
fmt.Println(" server - Export and send all data from this node")
fmt.Println(" client - Receive and load all data from peer")
return
}

err := h.checkDBClient()
if err != nil {
fmt.Printf("Database client not initialized: %v\n", err)
return
}

err = h.checkDIDClient()
if err != nil {
fmt.Printf("DID database client not initialized: %v\n", err)
return
}

// Parse the multiaddr
addr, err := ma.NewMultiaddr(parts[1])
if err != nil {
fmt.Printf("Invalid multiaddress: %v\n", err)
return
}

// Extract peer ID from multiaddr
addrInfo, err := peer.AddrInfoFromP2pAddr(addr)
if err != nil {
fmt.Printf("Failed to extract peer info: %v\n", err)
func (h *CommandHandler) handleAccountSync(parts []string) {
if len(parts) != 2 {
fmt.Println("Usage: accountsync <peer_multiaddr>")
return
}

mode := strings.ToLower(parts[2])
if mode != "server" && mode != "client" {
fmt.Printf("Invalid mode: %s. Must be 'server' or 'client'\n", parts[2])
if h.FastSyncerV2 == nil {
fmt.Println("Error: FastsyncV2 engine is not initialized")
return
}

fmt.Printf("Starting first sync with peer %s (mode: %s)\n", addrInfo.ID.String(), mode)
fmt.Printf("Starting account-only sync with peer %s\n", parts[1])
startTime := time.Now().UTC()

var syncErr error
if mode == "server" {
// Server mode: export and send all data
fmt.Println(">>> Running in SERVER mode - exporting all data...")
syncErr = h.FastSyncer.FirstSyncServer(addrInfo.ID)
} else {
// Client mode: receive and load all data
fmt.Println(">>> Running in CLIENT mode - receiving all data...")
syncErr = h.FastSyncer.FirstSyncClient(addrInfo.ID)
}

if syncErr != nil {
fmt.Printf("First sync failed: %v\n", syncErr)
return
}

// Get post-sync states
newMainState, err := DB_OPs.GetDatabaseState(h.MainClient.Client)
if err != nil {
fmt.Printf("Failed to get main database state after sync: %v\n", err)
return
}

newAccountsState, err := DB_OPs.GetDatabaseState(h.DIDClient.Client)
synced, err := h.FastSyncerV2.AccountSyncOnly(parts[1])
if err != nil {
fmt.Printf("Failed to get accounts database state after sync: %v\n", err)
fmt.Printf("AccountSync failed: %v\n", err)
return
}

fmt.Printf("First sync completed in %v\n", time.Since(startTime))
fmt.Printf("New main DB state: TxID=%d, Root=%x\n", newMainState.TxId, newMainState.TxHash)
fmt.Printf("New accounts DB state: TxID=%d, Root=%x\n", newAccountsState.TxId, newAccountsState.TxHash)
fmt.Printf("AccountSync complete: %d missing accounts synced in %v\n", synced, time.Since(startTime))
printDashes()
}

Expand Down
73 changes: 72 additions & 1 deletion CLI/CLI_GRPC.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ func (h *CommandHandler) HandleFastSync(peeraddr string) (SyncStats, error) {
if peeraddr == "" {
return SyncStats{}, fmt.Errorf("usage: fastsync <peer_multiaddr>")
}
if !h.PullAllowed {
return SyncStats{}, fmt.Errorf("node is configured as a serve-only participant (pulling disabled). cannot pull data")
}

err := h.checkDBClient()
if err != nil {
Expand Down Expand Up @@ -291,6 +294,70 @@ func (h *CommandHandler) HandleFastSync(peeraddr string) (SyncStats, error) {
}, nil
}

func (h *CommandHandler) HandleFastSyncV2(peeraddr string) (SyncStats, error) {
if peeraddr == "" {
return SyncStats{}, fmt.Errorf("usage: fastsyncv2 <peer_multiaddr>")
}
if !h.PullAllowed {
return SyncStats{}, fmt.Errorf("node is configured as a serve-only participant (pulling disabled). cannot pull data")
}

// Make sure engine exists
if h.FastSyncerV2 == nil {
return SyncStats{}, fmt.Errorf("FastsyncV2 engine is inactive")
}

startTime := time.Now().UTC()
err := h.FastSyncerV2.HandleSync(peeraddr)
if err != nil {
return SyncStats{}, fmt.Errorf("FastsyncV2 failed: %w", err)
}

// Re-fetch DB states to report. FastsyncV2 doesn't require MainClient/DIDClient
// for the sync itself, so guard against nil before querying.
var newMainState, newAccountsState *schema.ImmutableState
if h.MainClient != nil {
newMainState, _ = DB_OPs.GetDatabaseState(h.MainClient.Client)
}
if h.DIDClient != nil {
newAccountsState, _ = DB_OPs.GetDatabaseState(h.DIDClient.Client)
}

return SyncStats{
TimeTaken: time.Since(startTime),
MainState: newMainState,
AccountsState: newAccountsState,
}, nil
}

func (h *CommandHandler) HandleAccountSync(peeraddr string) (SyncStats, error) {
if peeraddr == "" {
return SyncStats{}, fmt.Errorf("usage: accountsync <peer_multiaddr>")
}
if !h.PullAllowed {
return SyncStats{}, fmt.Errorf("node is configured as a serve-only participant (pulling disabled). cannot pull data")
}
if h.FastSyncerV2 == nil {
return SyncStats{}, fmt.Errorf("FastsyncV2 engine is inactive")
}

startTime := time.Now().UTC()
_, err := h.FastSyncerV2.AccountSyncOnly(peeraddr)
if err != nil {
return SyncStats{}, fmt.Errorf("AccountSync failed: %w", err)
}

var newAccountsState *schema.ImmutableState
if h.DIDClient != nil {
newAccountsState, _ = DB_OPs.GetDatabaseState(h.DIDClient.Client)
}

return SyncStats{
TimeTaken: time.Since(startTime),
AccountsState: newAccountsState,
}, nil
}

func (h *CommandHandler) HandleFirstSync(peeraddr string, mode string) (SyncStats, error) {
if peeraddr == "" {
return SyncStats{}, fmt.Errorf("usage: firstsync <peer_multiaddr> <server|client>")
Expand All @@ -300,6 +367,11 @@ func (h *CommandHandler) HandleFirstSync(peeraddr string, mode string) (SyncStat
return SyncStats{}, fmt.Errorf("usage: firstsync <peer_multiaddr> <server|client>")
}

modeLower := strings.ToLower(mode)
if modeLower == "client" && !h.PullAllowed {
return SyncStats{}, fmt.Errorf("node is configured as a serve-only participant (pulling disabled). cannot pull data")
}

err := h.checkDBClient()
if err != nil {
return SyncStats{}, fmt.Errorf("database client not initialized: %v", err)
Expand All @@ -322,7 +394,6 @@ func (h *CommandHandler) HandleFirstSync(peeraddr string, mode string) (SyncStat
return SyncStats{}, fmt.Errorf("failed to extract peer info: %v", err)
}

modeLower := strings.ToLower(mode)
if modeLower != "server" && modeLower != "client" {
return SyncStats{}, fmt.Errorf("invalid mode: %s. Must be 'server' or 'client'", mode)
}
Expand Down
Loading
Loading