Skip to content

Commit ef3ea35

Browse files
sid200727AkramBitar
authored andcommitted
feat(nfttx/uniqueness): add badger-based KVS backend
Signed-off-by: Siddhi Khandelwal <siddhi.200727@gmail.com>
1 parent 314cb03 commit ef3ea35

4 files changed

Lines changed: 344 additions & 0 deletions

File tree

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/IBM/idemix/bccsp/types v0.0.0-20260501050258-bb91d87b1252
99
github.com/IBM/mathlib v0.1.0
1010
github.com/consensys/gnark-crypto v0.20.1
11+
github.com/dgraph-io/badger/v4 v4.9.2
1112
github.com/dgraph-io/ristretto/v2 v2.4.0
1213
github.com/go-co-op/gocron/v2 v2.21.2
1314
github.com/go-viper/mapstructure/v2 v2.5.0
@@ -67,6 +68,7 @@ require (
6768
github.com/goccy/go-yaml v1.19.2 // indirect
6869
github.com/gogo/protobuf v1.3.2 // indirect
6970
github.com/golang/protobuf v1.5.4 // indirect
71+
github.com/google/flatbuffers v25.2.10+incompatible // indirect
7072
github.com/google/go-cmp v0.7.0 // indirect
7173
github.com/google/uuid v1.6.0 // indirect
7274
github.com/gorilla/websocket v1.5.3 // indirect
@@ -78,6 +80,7 @@ require (
7880
github.com/jackc/puddle/v2 v2.2.2 // indirect
7981
github.com/jonboulle/clockwork v0.5.0 // indirect
8082
github.com/kilic/bls12-381 v0.1.0 // indirect
83+
github.com/klauspost/compress v1.18.0 // indirect
8184
github.com/knadh/koanf/maps v0.1.2 // indirect
8285
github.com/knadh/koanf/parsers/yaml v1.1.0 // indirect
8386
github.com/knadh/koanf/providers/env/v2 v2.0.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
684684
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
685685
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
686686
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
687+
github.com/dgraph-io/badger/v4 v4.9.2 h1:Wb5qw8gElqwV1a8msHTeQKova9b1V10heFKMIiPd80E=
688+
github.com/dgraph-io/badger/v4 v4.9.2/go.mod h1:nJjaJTUOSsQEBhsq209FmwCvMJzEA3e74RjZw6V2pQI=
687689
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
688690
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
689691
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
@@ -807,6 +809,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
807809
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
808810
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
809811
github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
812+
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
813+
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
810814
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
811815
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
812816
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package uniqueness
6+
7+
import (
8+
"context"
9+
"os"
10+
11+
"github.com/LFDT-Panurus/panurus/token/services/logging"
12+
badger "github.com/dgraph-io/badger/v4"
13+
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
14+
)
15+
16+
var logger = logging.MustGetLogger()
17+
18+
// BadgerKVS is a badger-backed implementation of the KVS interface.
19+
type BadgerKVS struct {
20+
db *badger.DB
21+
}
22+
23+
// NewBadgerKVS creates a new badger-backed KVS at the given path.
24+
// If path is empty, an in-memory badger instance is used. If path is non-empty,
25+
// it must point to an existing directory; otherwise the call fails fast with a
26+
// clear error rather than surfacing badger's lower-level open failure.
27+
func NewBadgerKVS(path string) (*BadgerKVS, error) {
28+
opts := badger.DefaultOptions(path)
29+
if path == "" {
30+
opts = opts.WithInMemory(true)
31+
} else {
32+
info, err := os.Stat(path)
33+
if err != nil {
34+
return nil, errors.Wrapf(err, "badger path %q is not accessible", path)
35+
}
36+
if !info.IsDir() {
37+
return nil, errors.Errorf("badger path %q exists but is not a directory", path)
38+
}
39+
}
40+
opts = opts.WithLogger(nil)
41+
42+
db, err := badger.Open(opts)
43+
if err != nil {
44+
return nil, errors.Wrap(err, "failed to open badger db")
45+
}
46+
47+
return &BadgerKVS{db: db}, nil
48+
}
49+
50+
// Close closes the underlying badger db.
51+
func (b *BadgerKVS) Close() error {
52+
return b.db.Close()
53+
}
54+
55+
// Exists returns true if the key exists in the store.
56+
//
57+
// BadgerDB transactions are goroutine-safe via MVCC, so no external locking is
58+
// required around Exists/Get/Put.
59+
//
60+
// The KVS interface forces Exists to return bool with no error, so any error
61+
// other than ErrKeyNotFound (e.g. txn conflict, corruption) is logged here
62+
// rather than silently swallowed - callers will see Exists return false, but
63+
// the underlying failure will be visible in the logs for diagnosis.
64+
func (b *BadgerKVS) Exists(_ context.Context, k string) bool {
65+
err := b.db.View(func(txn *badger.Txn) error {
66+
_, err := txn.Get([]byte(k))
67+
68+
return err
69+
})
70+
if err == nil {
71+
return true
72+
}
73+
if !errors.Is(err, badger.ErrKeyNotFound) {
74+
logger.Warnf("badger Exists(%q) returned unexpected error: %v", k, err)
75+
}
76+
77+
return false
78+
}
79+
80+
// Get retrieves the value for the given key.
81+
func (b *BadgerKVS) Get(_ context.Context, k string, v any) error {
82+
ptr, ok := v.(*[]byte)
83+
if !ok {
84+
return errors.Errorf("value must be *[]byte")
85+
}
86+
87+
return b.db.View(func(txn *badger.Txn) error {
88+
item, err := txn.Get([]byte(k))
89+
if err != nil {
90+
if errors.Is(err, badger.ErrKeyNotFound) {
91+
// Wrap (not Errorf) so callers can still errors.Is(err, badger.ErrKeyNotFound).
92+
return errors.Wrapf(err, "key %s not found", k)
93+
}
94+
95+
return err
96+
}
97+
98+
return item.Value(func(val []byte) error {
99+
*ptr = append([]byte{}, val...)
100+
101+
return nil
102+
})
103+
})
104+
}
105+
106+
// Put stores the value for the given key.
107+
func (b *BadgerKVS) Put(_ context.Context, k string, v any) error {
108+
val, ok := v.([]byte)
109+
if !ok {
110+
return errors.Errorf("value must be []byte")
111+
}
112+
113+
return b.db.Update(func(txn *badger.Txn) error {
114+
return txn.Set([]byte(k), val)
115+
})
116+
}
117+
118+
// NewBadgerService returns a uniqueness service backed by a badger KVS at the given path.
119+
// If path is empty, an in-memory badger instance is used.
120+
func NewBadgerService(path string) (*Service, error) {
121+
kvs, err := NewBadgerKVS(path)
122+
if err != nil {
123+
return nil, err
124+
}
125+
126+
return &Service{kvs: kvs}, nil
127+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package uniqueness
6+
7+
import (
8+
"fmt"
9+
"os"
10+
"sync"
11+
"testing"
12+
13+
badger "github.com/dgraph-io/badger/v4"
14+
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func newTestBadgerKVS(t *testing.T) *BadgerKVS {
20+
t.Helper()
21+
kvs, err := NewBadgerKVS("")
22+
require.NoError(t, err)
23+
t.Cleanup(func() {
24+
require.NoError(t, kvs.Close())
25+
})
26+
27+
return kvs
28+
}
29+
30+
func TestBadgerKVS_PutAndGet(t *testing.T) {
31+
t.Parallel()
32+
b := newTestBadgerKVS(t)
33+
ctx := t.Context()
34+
key := "test-key"
35+
val := []byte("test-value")
36+
37+
require.False(t, b.Exists(ctx, key))
38+
require.NoError(t, b.Put(ctx, key, val))
39+
require.True(t, b.Exists(ctx, key))
40+
41+
var result []byte
42+
require.NoError(t, b.Get(ctx, key, &result))
43+
require.Equal(t, val, result)
44+
}
45+
46+
func TestBadgerKVS_GetNotFound(t *testing.T) {
47+
t.Parallel()
48+
b := newTestBadgerKVS(t)
49+
var result []byte
50+
err := b.Get(t.Context(), "missing", &result)
51+
require.Error(t, err)
52+
}
53+
54+
func TestBadgerKVS_PutInvalidValue(t *testing.T) {
55+
t.Parallel()
56+
b := newTestBadgerKVS(t)
57+
err := b.Put(t.Context(), "key", "not-bytes")
58+
require.Error(t, err)
59+
}
60+
61+
func TestBadgerKVS_GetInvalidPointer(t *testing.T) {
62+
t.Parallel()
63+
b := newTestBadgerKVS(t)
64+
require.NoError(t, b.Put(t.Context(), "key", []byte("val")))
65+
var wrong string
66+
err := b.Get(t.Context(), "key", &wrong)
67+
require.Error(t, err)
68+
}
69+
70+
func TestNewBadgerKVS_InMemory(t *testing.T) {
71+
t.Parallel()
72+
kvs, err := NewBadgerKVS("")
73+
require.NoError(t, err)
74+
require.NotNil(t, kvs)
75+
defer func() { require.NoError(t, kvs.Close()) }()
76+
}
77+
78+
func TestNewBadgerKVS_OnDisk(t *testing.T) {
79+
t.Parallel()
80+
dir := t.TempDir()
81+
kvs, err := NewBadgerKVS(dir)
82+
require.NoError(t, err)
83+
require.NotNil(t, kvs)
84+
defer func() { require.NoError(t, kvs.Close()) }()
85+
86+
require.NoError(t, kvs.Put(t.Context(), "k", []byte("v")))
87+
var result []byte
88+
require.NoError(t, kvs.Get(t.Context(), "k", &result))
89+
require.Equal(t, []byte("v"), result)
90+
}
91+
92+
func TestNewBadgerService(t *testing.T) {
93+
t.Parallel()
94+
svc, err := NewBadgerService("")
95+
require.NoError(t, err)
96+
require.NotNil(t, svc)
97+
98+
id, err := svc.ComputeID(t.Context(), "test-state")
99+
require.NoError(t, err)
100+
require.NotEmpty(t, id)
101+
}
102+
103+
func TestBadgerService_ComputeID_Idempotent(t *testing.T) {
104+
t.Parallel()
105+
svc, err := NewBadgerService("")
106+
require.NoError(t, err)
107+
108+
id1, err := svc.ComputeID(t.Context(), "test-state")
109+
require.NoError(t, err)
110+
id2, err := svc.ComputeID(t.Context(), "test-state")
111+
require.NoError(t, err)
112+
require.Equal(t, id1, id2)
113+
}
114+
115+
func TestBadgerService_ComputeID_DistinctStates(t *testing.T) {
116+
t.Parallel()
117+
svc, err := NewBadgerService("")
118+
require.NoError(t, err)
119+
120+
type Asset struct {
121+
ID string
122+
Value int
123+
}
124+
125+
id1, err := svc.ComputeID(t.Context(), Asset{ID: "a1", Value: 100})
126+
require.NoError(t, err)
127+
require.NotEmpty(t, id1)
128+
129+
id2, err := svc.ComputeID(t.Context(), Asset{ID: "a2", Value: 200})
130+
require.NoError(t, err)
131+
require.NotEmpty(t, id2)
132+
133+
require.NotEqual(t, id1, id2)
134+
}
135+
136+
func TestBadgerKVS_GetNotFound_PreservesErrKeyNotFound(t *testing.T) {
137+
t.Parallel()
138+
b := newTestBadgerKVS(t)
139+
140+
var result []byte
141+
err := b.Get(t.Context(), "missing", &result)
142+
require.Error(t, err)
143+
require.True(t, errors.Is(err, badger.ErrKeyNotFound),
144+
"expected error to wrap badger.ErrKeyNotFound, got: %v", err)
145+
}
146+
147+
func TestNewBadgerKVS_NonExistentPath(t *testing.T) {
148+
t.Parallel()
149+
kvs, err := NewBadgerKVS("/nonexistent/path/that/should/not/exist")
150+
require.Error(t, err)
151+
require.Nil(t, kvs)
152+
}
153+
154+
func TestNewBadgerKVS_PathIsFile(t *testing.T) {
155+
t.Parallel()
156+
dir := t.TempDir()
157+
file := dir + "/not-a-dir"
158+
require.NoError(t, writeEmptyFile(file))
159+
160+
kvs, err := NewBadgerKVS(file)
161+
require.Error(t, err)
162+
require.Nil(t, kvs)
163+
}
164+
165+
func TestBadgerKVS_ConcurrentReadsAndWrites(t *testing.T) {
166+
t.Parallel()
167+
b := newTestBadgerKVS(t)
168+
ctx := t.Context()
169+
170+
const workers = 32
171+
const opsPerWorker = 50
172+
173+
var wg sync.WaitGroup
174+
wg.Add(workers)
175+
for w := range workers {
176+
go func(w int) {
177+
defer wg.Done()
178+
// Use assert (not require) inside goroutines: require.* calls
179+
// t.FailNow, which is only safe on the goroutine running the test.
180+
// assert.* records the failure and lets the goroutine return cleanly.
181+
for i := range opsPerWorker {
182+
key := fmt.Sprintf("worker-%d-key-%d", w, i)
183+
val := fmt.Appendf(nil, "worker-%d-val-%d", w, i)
184+
185+
assert.NoError(t, b.Put(ctx, key, val))
186+
assert.True(t, b.Exists(ctx, key))
187+
188+
var got []byte
189+
assert.NoError(t, b.Get(ctx, key, &got))
190+
assert.Equal(t, val, got)
191+
}
192+
}(w)
193+
}
194+
wg.Wait()
195+
196+
for w := range workers {
197+
for i := range opsPerWorker {
198+
require.True(t, b.Exists(ctx, fmt.Sprintf("worker-%d-key-%d", w, i)))
199+
}
200+
}
201+
}
202+
203+
func writeEmptyFile(path string) error {
204+
f, err := os.Create(path)
205+
if err != nil {
206+
return err
207+
}
208+
209+
return f.Close()
210+
}

0 commit comments

Comments
 (0)