Files
mpc/pkg/infra/nats_kv.go
zeekay bfafe92470 Merge hanzo/mpc additions: ConsensusKV, HSM providers, policy engine, rename CLI to mpc-cli
- pkg/infra: Add abstract KV interface, ConsensusKV (dual-cert BFT with
  Ed25519 + ML-DSA-65), NatsKV, ConsulKVAdapter, HybridKV, SQLiteMeta
  with full test coverage
- pkg/hsm: Add Provider interface and implementations for AWS KMS,
  GCP Cloud KMS, Azure Key Vault, Zymbit HSM, Hanzo KMS, and file-based
  providers with registry and tests. Rename existing FileProvider to
  FilePasswordProvider to avoid collision with new Provider interface
- pkg/policy: Add transaction governance engine with signers, roles,
  spending limits, whitelist/blacklist, time windows, rate limiting,
  approval workflow, audit log, and FHE private policy evaluation
- contracts: Add ThresholdPolicy.sol for on-chain policy enforcement
- cmd: Rename lux-mpc-cli to mpc-cli
- Makefile: Update build targets for mpc-cli
2025-12-28 03:12:38 -08:00

145 lines
3.8 KiB
Go

package infra
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/luxfi/mpc/pkg/logger"
)
// NatsKV implements KV using NATS JetStream Key-Value store.
// This replaces Consul KV — MPC already depends on NATS for messaging,
// so this eliminates the Consul server entirely.
//
// JetStream KV provides:
// - Persistent key-value storage (replicated across NATS cluster)
// - Watch/subscribe for key changes (peer readiness)
// - TTL support for ephemeral keys
// - History for key versioning
type NatsKV struct {
kv jetstream.KeyValue
bucket string
}
// NewNatsKV creates a NATS JetStream KV store, creating the bucket if needed.
func NewNatsKV(nc *nats.Conn, bucket string) (*NatsKV, error) {
js, err := jetstream.New(nc)
if err != nil {
return nil, fmt.Errorf("nats_kv: failed to create JetStream context: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Try to bind to existing bucket, create if not found
kv, err := js.KeyValue(ctx, bucket)
if err != nil {
// Create the bucket
kv, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{
Bucket: bucket,
Description: "MPC cluster state (replaces Consul KV)",
History: 5,
TTL: 0, // no global TTL
Storage: jetstream.FileStorage,
Replicas: 1, // increase for HA
})
if err != nil {
return nil, fmt.Errorf("nats_kv: failed to create bucket %q: %w", bucket, err)
}
logger.Info("Created NATS KV bucket", "bucket", bucket)
}
return &NatsKV{kv: kv, bucket: bucket}, nil
}
func (n *NatsKV) Put(key string, value []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// NATS KV uses "." as separator, Consul uses "/". Convert.
natsKey := consulKeyToNats(key)
_, err := n.kv.Put(ctx, natsKey, value)
if err != nil {
return fmt.Errorf("nats_kv: put %q failed: %w", key, err)
}
return nil
}
func (n *NatsKV) Get(key string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
natsKey := consulKeyToNats(key)
entry, err := n.kv.Get(ctx, natsKey)
if err != nil {
if errors.Is(err, jetstream.ErrKeyNotFound) {
return nil, nil // not found = nil, matching Consul behavior
}
return nil, fmt.Errorf("nats_kv: get %q failed: %w", key, err)
}
return entry.Value(), nil
}
func (n *NatsKV) Delete(key string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
natsKey := consulKeyToNats(key)
err := n.kv.Delete(ctx, natsKey)
if err != nil && !errors.Is(err, jetstream.ErrKeyNotFound) {
return fmt.Errorf("nats_kv: delete %q failed: %w", key, err)
}
return nil
}
func (n *NatsKV) List(prefix string) ([]*KVPair, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
natsPrefix := consulKeyToNats(prefix)
// List all keys matching prefix
keys, err := n.kv.ListKeys(ctx, jetstream.MetaOnly())
if err != nil {
if errors.Is(err, jetstream.ErrNoKeysFound) {
return nil, nil
}
return nil, fmt.Errorf("nats_kv: list %q failed: %w", prefix, err)
}
var pairs []*KVPair
for key := range keys.Keys() {
if !strings.HasPrefix(key, natsPrefix) {
continue
}
entry, getErr := n.kv.Get(ctx, key)
if getErr != nil {
continue // skip deleted/expired
}
pairs = append(pairs, &KVPair{
Key: natsKeyToConsul(key),
Value: entry.Value(),
})
}
return pairs, nil
}
// consulKeyToNats converts Consul-style "ready/node0" to NATS-style "ready.node0"
func consulKeyToNats(key string) string {
return strings.ReplaceAll(key, "/", ".")
}
// natsKeyToConsul converts NATS-style "ready.node0" back to "ready/node0"
func natsKeyToConsul(key string) string {
return strings.ReplaceAll(key, ".", "/")
}