Skip to content

Commit c652d03

Browse files
authored
Merge pull request #61 from fystack/feat/badger-backup
Implement badger backup recovery
2 parents 1bad9a7 + 51a3f48 commit c652d03

13 files changed

Lines changed: 1106 additions & 28 deletions

File tree

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,32 @@ Each Mpcium node:
107107
- **Scalable and pluggable**: Easily expand the cluster or integrate additional tools
108108
- **Secure peer authentication**: All inter-node messages are signed and verified using Ed25519
109109

110+
## Configuration
111+
112+
The application uses a YAML configuration file (`config.yaml`) with the following key settings:
113+
114+
### Database Configuration
115+
116+
- `badger_password`: Password for encrypting the BadgerDB database
117+
- `db_path`: Path where the database files are stored
118+
119+
### Backup Configuration
120+
121+
- `backup_enabled`: Enable/disable automatic backups (default: true)
122+
- `backup_period_seconds`: How often to perform backups in seconds (default: 300)
123+
- `backup_dir`: Directory where encrypted backups are stored
124+
125+
### Network Configuration
126+
127+
- `nats.url`: NATS server URL
128+
- `consul.address`: Consul server address
129+
130+
### MPC Configuration
131+
132+
- `mpc_threshold`: Threshold for multi-party computation
133+
- `event_initiator_pubkey`: Public key of the event initiator
134+
- `max_concurrent_keygen`: Maximum concurrent key generation operations
135+
110136
## Installation and Run
111137

112138
For full installation and run instructions, see [INSTALLATION.md](./INSTALLATION.md).
@@ -176,4 +202,3 @@ go test ./... -v
176202
cd e2e
177203
make test
178204
```
179-

cmd/mpcium-cli/main.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,31 @@ func main() {
125125
},
126126
Action: generateInitiatorIdentity,
127127
},
128+
{
129+
Name: "recover",
130+
Usage: "Recover database from encrypted backup files",
131+
Flags: []cli.Flag{
132+
&cli.StringFlag{
133+
Name: "backup-dir",
134+
Aliases: []string{"b"},
135+
Usage: "Directory containing encrypted backup files",
136+
Required: true,
137+
},
138+
&cli.StringFlag{
139+
Name: "recovery-path",
140+
Aliases: []string{"r"},
141+
Usage: "Target path for database recovery",
142+
Required: true,
143+
},
144+
&cli.BoolFlag{
145+
Name: "force",
146+
Aliases: []string{"f"},
147+
Value: false,
148+
Usage: "Force overwrite if recovery path already exists",
149+
},
150+
},
151+
Action: recoverDatabase,
152+
},
128153
{
129154
Name: "version",
130155
Usage: "Display detailed version information",

cmd/mpcium-cli/recovery.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"syscall"
8+
9+
"github.com/fystack/mpcium/pkg/kvstore"
10+
"github.com/urfave/cli/v3"
11+
"golang.org/x/term"
12+
)
13+
14+
// recoverDatabase handles the database recovery from encrypted backup files
15+
func recoverDatabase(ctx context.Context, c *cli.Command) error {
16+
backupDir := c.String("backup-dir")
17+
recoveryPath := c.String("recovery-path")
18+
force := c.Bool("force")
19+
20+
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
21+
return fmt.Errorf("backup directory does not exist: %s", backupDir)
22+
}
23+
24+
if _, err := os.Stat(recoveryPath); err == nil && !force {
25+
return fmt.Errorf("recovery path already exists: %s (use --force to overwrite)", recoveryPath)
26+
}
27+
28+
// Prompt for encryption key
29+
var key []byte
30+
fmt.Print("Enter backup encryption key: ")
31+
keyBytes, err := term.ReadPassword(int(syscall.Stdin))
32+
if err != nil {
33+
return fmt.Errorf("failed to read encryption key: %w", err)
34+
}
35+
fmt.Println() // Add newline after password input
36+
key = keyBytes
37+
if len(key) == 0 {
38+
return fmt.Errorf("encryption key cannot be empty")
39+
}
40+
41+
// Remove existing recovery path if force flag is set
42+
if force {
43+
if err := os.RemoveAll(recoveryPath); err != nil {
44+
return fmt.Errorf("failed to remove existing recovery path: %w", err)
45+
}
46+
}
47+
48+
fmt.Printf("Starting database recovery...\n")
49+
fmt.Printf("Backup directory: %s\n", backupDir)
50+
fmt.Printf("Recovery path: %s\n", recoveryPath)
51+
52+
// Create a temporary backup executor to access the backup files
53+
tempExecutor := kvstore.NewBadgerBackupExecutor("temp", nil, key, backupDir)
54+
55+
// Perform the recovery using the existing method with specified recovery path
56+
if err := tempExecutor.RestoreAllBackupsEncrypted(recoveryPath, key); err != nil {
57+
return fmt.Errorf("recovery failed: %w", err)
58+
}
59+
60+
fmt.Printf("✅ Database recovery completed successfully!\n")
61+
fmt.Printf("Restored database is available at: %s\n", recoveryPath)
62+
return nil
63+
}

cmd/mpcium/main.go

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ import (
2929
)
3030

3131
const (
32-
// Version information
33-
VERSION = "0.2.1"
32+
Version = "0.3.1"
33+
DefaultBackupPeriodSeconds = 300 // (5 minutes)
3434
)
3535

3636
func main() {
3737
app := &cli.Command{
3838
Name: "mpcium",
3939
Usage: "Multi-Party Computation node for threshold signatures",
40-
Version: VERSION,
40+
Version: Version,
4141
Commands: []*cli.Command{
4242
{
4343
Name: "start",
@@ -72,7 +72,7 @@ func main() {
7272
Name: "version",
7373
Usage: "Display detailed version information",
7474
Action: func(ctx context.Context, c *cli.Command) error {
75-
fmt.Printf("mpcium version %s\n", VERSION)
75+
fmt.Printf("mpcium version %s\n", Version)
7676
return nil
7777
},
7878
},
@@ -104,13 +104,21 @@ func runNode(ctx context.Context, c *cli.Command) error {
104104
}
105105

106106
consulClient := infra.GetConsulClient(environment)
107-
badgerKV := NewBadgerKV(nodeName)
108-
defer badgerKV.Close()
109-
110107
keyinfoStore := keyinfo.NewStore(consulClient.KV())
111108
peers := LoadPeersFromConsul(consulClient)
112109
nodeID := GetIDFromName(nodeName, peers)
113110

111+
badgerKV := NewBadgerKV(nodeName, nodeID)
112+
defer badgerKV.Close()
113+
114+
// Start background backup job
115+
backupEnabled := viper.GetBool("backup_enabled")
116+
if backupEnabled {
117+
backupPeriodSeconds := viper.GetInt("backup_period_seconds")
118+
stopBackup := StartPeriodicBackup(ctx, badgerKV, backupPeriodSeconds)
119+
defer stopBackup()
120+
}
121+
114122
identityStore, err := identity.NewFileStore("identity", nodeName, decryptPrivateKey)
115123
if err != nil {
116124
logger.Fatal("Failed to create identity store", err)
@@ -384,7 +392,7 @@ func GetIDFromName(name string, peers []config.Peer) string {
384392
return nodeID
385393
}
386394

387-
func NewBadgerKV(nodeName string) *kvstore.BadgerKVStore {
395+
func NewBadgerKV(nodeName, nodeID string) *kvstore.BadgerKVStore {
388396
// Badger KV DB
389397
// Use configured db_path or default to current directory + "db"
390398
basePath := viper.GetString("db_path")
@@ -393,17 +401,55 @@ func NewBadgerKV(nodeName string) *kvstore.BadgerKVStore {
393401
}
394402
dbPath := filepath.Join(basePath, nodeName)
395403

396-
badgerKv, err := kvstore.NewBadgerKVStore(
397-
dbPath,
398-
[]byte(viper.GetString("badger_password")),
399-
)
404+
// Use configured backup_dir or default to current directory + "backups"
405+
backupDir := viper.GetString("backup_dir")
406+
if backupDir == "" {
407+
backupDir = filepath.Join(".", "backups")
408+
}
409+
410+
// Create BadgerConfig struct
411+
config := kvstore.BadgerConfig{
412+
NodeID: nodeName,
413+
EncryptionKey: []byte(viper.GetString("badger_password")),
414+
BackupEncryptionKey: []byte(viper.GetString("badger_password")), // Using same key for backup encryption
415+
BackupDir: backupDir,
416+
DBPath: dbPath,
417+
}
418+
419+
badgerKv, err := kvstore.NewBadgerKVStore(config)
400420
if err != nil {
401421
logger.Fatal("Failed to create badger kv store", err)
402422
}
403-
logger.Info("Connected to badger kv store", "path", dbPath)
423+
logger.Info("Connected to badger kv store", "path", dbPath, "backup_dir", backupDir)
404424
return badgerKv
405425
}
406426

427+
func StartPeriodicBackup(ctx context.Context, badgerKV *kvstore.BadgerKVStore, periodSeconds int) func() {
428+
if periodSeconds <= 0 {
429+
periodSeconds = DefaultBackupPeriodSeconds
430+
}
431+
backupTicker := time.NewTicker(time.Duration(periodSeconds) * time.Second)
432+
backupCtx, backupCancel := context.WithCancel(ctx)
433+
go func() {
434+
for {
435+
select {
436+
case <-backupCtx.Done():
437+
logger.Info("Backup background job stopped")
438+
return
439+
case <-backupTicker.C:
440+
logger.Info("Running periodic BadgerDB backup...")
441+
err := badgerKV.Backup()
442+
if err != nil {
443+
logger.Error("Periodic BadgerDB backup failed", err)
444+
} else {
445+
logger.Info("Periodic BadgerDB backup completed successfully")
446+
}
447+
}
448+
}
449+
}()
450+
return backupCancel
451+
}
452+
407453
func GetNATSConnection(environment string) (*nats.Conn, error) {
408454
url := viper.GetString("nats.url")
409455
opts := []nats.Option{

config.prod.yaml.template

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ consul:
1111

1212
mpc_threshold: 2
1313
environment: production # Set to production for production environment
14+
backup_enabled: true
15+
backup_period_seconds: 300 # Seconds

config.yaml.template

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ badger_password: "your_badger_password"
99
event_initiator_pubkey: "event_initiator_pubkey"
1010
max_concurrent_keygen: 2
1111
db_path: "."
12+
backup_enabled: true
13+
backup_period_seconds: 300 # 5 minutes
14+
backup_dir: backups

pkg/encryption/aes.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package encryption
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/rand"
7+
)
8+
9+
func EncryptAESGCM(plain, key []byte) (ciphertext, nonce []byte, err error) {
10+
block, err := aes.NewCipher(key)
11+
if err != nil {
12+
return nil, nil, err
13+
}
14+
aead, err := cipher.NewGCM(block)
15+
if err != nil {
16+
return nil, nil, err
17+
}
18+
nonce = make([]byte, aead.NonceSize())
19+
if _, err = rand.Read(nonce); err != nil {
20+
return nil, nil, err
21+
}
22+
ciphertext = aead.Seal(nil, nonce, plain, nil)
23+
return ciphertext, nonce, nil
24+
}
25+
26+
func DecryptAESGCM(ciphertext, key, nonce []byte) ([]byte, error) {
27+
block, err := aes.NewCipher(key)
28+
if err != nil {
29+
return nil, err
30+
}
31+
aead, err := cipher.NewGCM(block)
32+
if err != nil {
33+
return nil, err
34+
}
35+
return aead.Open(nil, nonce, ciphertext, nil)
36+
}

0 commit comments

Comments
 (0)