Skip to content

Commit 73af27c

Browse files
authored
regression tests (#1303)
Signed-off-by: Angelo De Caro <[email protected]>
1 parent 86c37e5 commit 73af27c

File tree

4,113 files changed

+17460
-486
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

4,113 files changed

+17460
-486
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
runs-on: ubuntu-latest
5252
strategy:
5353
matrix:
54-
tests: [unit-tests, unit-tests-race]
54+
tests: [unit-tests, unit-tests-race, unit-tests-regression]
5555

5656
steps:
5757
- name: Checkout code

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,19 @@ download-fabric:
3939

4040
.PHONY: unit-tests
4141
unit-tests:
42-
@go test -cover $(shell go list ./... | grep -v '/integration/')
42+
@go test -cover $(shell go list ./... | grep -v '/integration/' | grep -v 'regression')
4343
cd integration/nwo/; go test -cover ./...
4444
cd token/services/storage/db/kvs/hashicorp/; go test -cover ./...
4545

4646
.PHONY: unit-tests-race
4747
unit-tests-race:
48-
@export GORACE=history_size=7; go test -race -cover $(shell go list ./... | grep -v '/integration/')
48+
@export GORACE=history_size=7; go test -race -cover $(shell go list ./... | grep -v '/integration/' | grep -v 'regression')
4949
cd integration/nwo/; go test -cover ./...
5050

51+
.PHONY: unit-tests-regression
52+
unit-tests-regression:
53+
@go test -race -timeout 0 -cover $(shell go list ./... | grep -v '/integration/' | grep 'regression')
54+
5155
.PHONY: install-softhsm
5256
install-softhsm:
5357
./ci/scripts/install_softhsm.sh

token/core/zkatdlog/nogh/v1/benchmark/setup.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ import (
1111
"crypto/ecdsa"
1212
"crypto/elliptic"
1313
"crypto/rand"
14+
"encoding/base64"
15+
"encoding/json"
1416
"fmt"
1517
"os"
1618
"path/filepath"
19+
"strings"
1720

1821
"github.com/IBM/idemix/bccsp/types"
1922
math "github.com/IBM/mathlib"
@@ -30,6 +33,12 @@ import (
3033
"github.com/hyperledger-labs/fabric-token-sdk/token/services/storage/db/kvs"
3134
)
3235

36+
type SetupConfigurationSer struct {
37+
PP string `json:"pp"` // Encoded in base64
38+
Bits uint64 `json:"bits"`
39+
CurveID int `json:"curveID"`
40+
}
41+
3342
// SetupConfiguration holds the prepared public parameters and related
3443
// identities/signers used by a single benchmark configuration.
3544
//
@@ -42,6 +51,8 @@ type SetupConfiguration struct {
4251
OwnerIdentity *OwnerIdentity
4352
AuditorSigner *Signer
4453
IssuerSigner *Signer
54+
Bits uint64
55+
CurveID math.CurveID
4556
}
4657

4758
// SetupConfigurations contains a set of named benchmark configurations.
@@ -112,6 +123,8 @@ func NewSetupConfigurations(idemixTestdataPath string, bits []uint64, curveIDs [
112123
}
113124
pp.AddAuditor(auditorID)
114125
configurations[key(bit, curveID)] = &SetupConfiguration{
126+
Bits: bit,
127+
CurveID: curveID,
115128
PP: pp,
116129
OwnerIdentity: oID,
117130
AuditorSigner: auditorSigner,
@@ -146,7 +159,71 @@ func (c *SetupConfigurations) GetSetupConfiguration(bits uint64, curveID math.Cu
146159
}
147160

148161
func key(bits uint64, curveID math.CurveID) string {
149-
return fmt.Sprintf("%d-%d", bits, curveID)
162+
return fmt.Sprintf("%d-%s", bits, math2.CurveIDToString(curveID))
163+
}
164+
165+
// SaveTo writes each configuration to disk under the provided directory.
166+
// For each entry in the Configurations map a folder with the map key is
167+
// created. Inside that folder a file named `pp.json` is written. The file
168+
// contains a JSON document with the base64-encoded serialized public
169+
// parameters and the metadata bits and curve_id.
170+
func (c *SetupConfigurations) SaveTo(dir string) error {
171+
if c == nil {
172+
return errors.Errorf("nil SetupConfigurations")
173+
}
174+
// Ensure target base directory exists
175+
if err := os.MkdirAll(dir, 0o755); err != nil {
176+
return errors.Wrapf(err, "failed creating base dir [%s]")
177+
}
178+
179+
for k, cfg := range c.Configurations {
180+
if strings.ContainsAny(k, "/\\") {
181+
return errors.Errorf("invalid configuration key: %s", k)
182+
}
183+
if cfg == nil {
184+
return errors.Errorf("nil configuration for key: %s", k)
185+
}
186+
if cfg.PP == nil {
187+
return errors.Errorf("nil public parameters for key: %s", k)
188+
}
189+
190+
// serialize public params
191+
ppBytes, err := cfg.PP.Serialize()
192+
if err != nil {
193+
return errors.WithMessagef(err, "failed serializing public params for key: %s", k)
194+
}
195+
196+
// prepare JSON payload
197+
payload := &SetupConfigurationSer{
198+
PP: base64.StdEncoding.EncodeToString(ppBytes),
199+
Bits: cfg.Bits,
200+
CurveID: int(cfg.CurveID),
201+
}
202+
data, err := json.MarshalIndent(payload, "", " ")
203+
if err != nil {
204+
return errors.Wrap(err, "failed marshalling json payload")
205+
}
206+
207+
// create target directory and write
208+
targetDir := filepath.Join(dir, filepath.Base(k))
209+
if err := os.MkdirAll(targetDir, 0o755); err != nil {
210+
return errors.WithMessagef(err, "failed creating dir for key: %s", k)
211+
}
212+
213+
// Write params.txt containing base64(ppBytes)
214+
paramsEncoded := payload.PP
215+
finalParamsPath := filepath.Join(targetDir, "params.txt")
216+
if err := os.WriteFile(finalParamsPath, []byte(paramsEncoded), 0o644); err != nil {
217+
return errors.WithMessagef(err, "failed writing params file for key: %s", k)
218+
}
219+
220+
// Write pp.json file
221+
finalPath := filepath.Join(targetDir, "pp.json")
222+
if err := os.WriteFile(finalPath, data, 0o644); err != nil {
223+
return errors.WithMessagef(err, "failed writing pp.json for key: %s", k)
224+
}
225+
}
226+
return nil
150227
}
151228

152229
// OwnerIdentity represents the owner identity used by benchmarks. It
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package benchmark
8+
9+
import (
10+
"encoding/base64"
11+
"encoding/json"
12+
"os"
13+
"path/filepath"
14+
"testing"
15+
16+
math "github.com/IBM/mathlib"
17+
"github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/v1/setup"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
func TestSaveTo(t *testing.T) {
22+
require := require.New(t)
23+
dir := t.TempDir()
24+
bits := uint64(16)
25+
curve := math.BN254
26+
pp, err := setup.Setup(bits, []byte("dummy-issuer-pk"), curve)
27+
require.NoError(err, "failed to create public params")
28+
29+
cfg := &SetupConfigurations{
30+
Configurations: map[string]*SetupConfiguration{
31+
key(bits, curve): {
32+
Bits: bits,
33+
CurveID: curve,
34+
PP: pp,
35+
},
36+
},
37+
}
38+
39+
require.NoError(cfg.SaveTo(dir), "SaveTo failed")
40+
41+
targetDir := filepath.Join(dir, key(bits, curve))
42+
st, err := os.Stat(targetDir)
43+
require.NoError(err, "expected target dir to exist")
44+
require.True(st.IsDir(), "expected target to be a directory")
45+
46+
filePath := filepath.Join(targetDir, "pp.json")
47+
data, err := os.ReadFile(filePath)
48+
require.NoError(err, "failed reading pp.json")
49+
50+
var payload SetupConfigurationSer
51+
require.NoError(json.Unmarshal(data, &payload), "failed to unmarshal pp.json")
52+
53+
// bits and curve_id are numbers decoded as float64
54+
require.Equal(bits, payload.Bits, "bits mismatch")
55+
require.Equal(int(curve), payload.CurveID, "curve_id mismatch")
56+
57+
ppB64 := payload.PP
58+
decoded, err := base64.StdEncoding.DecodeString(ppB64)
59+
require.NoError(err, "failed to base64 decode pp")
60+
require.NotEmpty(decoded, "decoded pp is empty")
61+
62+
// check params.txt exists and contains the same base64 string
63+
paramsPath := filepath.Join(targetDir, "params.txt")
64+
paramsData, err := os.ReadFile(paramsPath)
65+
require.NoError(err, "failed reading params.txt")
66+
require.Equal(ppB64, string(paramsData), "params.txt content mismatch")
67+
68+
// try deserializing the stored public params to ensure it's valid
69+
_, err = setup.NewPublicParamsFromBytes(decoded, pp.DriverName, pp.DriverVersion)
70+
require.NoError(err, "failed to deserialize stored public params")
71+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package regression_test
8+
9+
import (
10+
"embed"
11+
"encoding/base64"
12+
"encoding/json"
13+
"fmt"
14+
"path/filepath"
15+
"testing"
16+
17+
"github.com/hyperledger-labs/fabric-token-sdk/token"
18+
"github.com/hyperledger-labs/fabric-token-sdk/token/core"
19+
fabtoken "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/v1/driver"
20+
dlog "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/v1/driver"
21+
"github.com/hyperledger-labs/fabric-token-sdk/token/services/network/fabric/tcc"
22+
tk "github.com/hyperledger-labs/fabric-token-sdk/token/token"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
//go:embed testdata
27+
var testDataFS embed.FS
28+
29+
// TestRegression runs previously recorded regression test vectors contained in the
30+
// `testdata/<variant>` directories. Each directory contains a `params.txt` and a
31+
// sequence of JSON files under `transfers/output.<n>.json` representing serialized
32+
// token requests and their associated txids. The test unmarshals each recorded
33+
// request and verifies it against the validator to ensure the library remains
34+
// backwards compatible.
35+
//
36+
// Notes:
37+
// - The testdata used here is generated by `testdata/generator`. If you need to
38+
// re-generate vectors for a new setup, run that generator and commit the
39+
// produced artifacts to the corresponding `testdata/...` directory.
40+
// - The test expects 64 transfer vectors (output.0..output.63). Update the loop
41+
// range in `testRegression` if you add or remove vectors.
42+
func TestRegression(t *testing.T) {
43+
t.Parallel()
44+
for _, action := range []string{"transfers", "issues", "redeems", "swaps"} {
45+
testRegressionParallel(t, "testdata/32-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i1_o1", action))
46+
testRegressionParallel(t, "testdata/32-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i1_o2", action))
47+
testRegressionParallel(t, "testdata/32-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i2_o1", action))
48+
testRegressionParallel(t, "testdata/32-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i2_o2", action))
49+
50+
testRegressionParallel(t, "testdata/64-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i1_o1", action))
51+
testRegressionParallel(t, "testdata/64-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i1_o2", action))
52+
testRegressionParallel(t, "testdata/64-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i2_o1", action))
53+
testRegressionParallel(t, "testdata/64-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i2_o2", action))
54+
55+
testRegressionParallel(t, "testdata/32-BN254", fmt.Sprintf("%s_i1_o1", action))
56+
testRegressionParallel(t, "testdata/32-BN254", fmt.Sprintf("%s_i1_o2", action))
57+
testRegressionParallel(t, "testdata/32-BN254", fmt.Sprintf("%s_i2_o1", action))
58+
testRegressionParallel(t, "testdata/32-BN254", fmt.Sprintf("%s_i2_o2", action))
59+
60+
testRegressionParallel(t, "testdata/64-BN254", fmt.Sprintf("%s_i1_o1", action))
61+
testRegressionParallel(t, "testdata/64-BN254", fmt.Sprintf("%s_i1_o2", action))
62+
testRegressionParallel(t, "testdata/64-BN254", fmt.Sprintf("%s_i2_o1", action))
63+
testRegressionParallel(t, "testdata/64-BN254", fmt.Sprintf("%s_i2_o2", action))
64+
}
65+
}
66+
67+
func testRegressionParallel(t *testing.T, rootDir, subFolder string) {
68+
t.Helper()
69+
t.Run(fmt.Sprintf("%s-%s", rootDir, subFolder), func(t *testing.T) {
70+
t.Parallel()
71+
testRegression(t, rootDir, subFolder)
72+
})
73+
}
74+
75+
func testRegression(t *testing.T, rootDir, subFolder string) {
76+
t.Helper()
77+
t.Logf("regression test for [%s:%s]", rootDir, subFolder)
78+
paramsData, err := testDataFS.ReadFile(filepath.Join(rootDir, "params.txt"))
79+
require.NoError(t, err)
80+
81+
ppRaw, err := base64.StdEncoding.DecodeString(string(paramsData))
82+
require.NoError(t, err)
83+
84+
_, tokenValidator, err := tokenServicesFactory(ppRaw)
85+
require.NoError(t, err)
86+
87+
var tokenData struct {
88+
ReqRaw []byte `json:"req_raw"`
89+
TXID string `json:"txid"`
90+
}
91+
for i := range 64 {
92+
jsonData, err := testDataFS.ReadFile(
93+
filepath.Join(
94+
rootDir,
95+
subFolder,
96+
fmt.Sprintf("output.%d.json", i),
97+
),
98+
)
99+
require.NoError(t, err)
100+
err = json.Unmarshal(jsonData, &tokenData)
101+
require.NoError(t, err)
102+
_, _, err = tokenValidator.UnmarshallAndVerifyWithMetadata(
103+
t.Context(),
104+
&fakeLedger{},
105+
token.RequestAnchor(tokenData.TXID),
106+
tokenData.ReqRaw,
107+
)
108+
require.NoError(t, err)
109+
}
110+
}
111+
112+
func tokenServicesFactory(bytes []byte) (tcc.PublicParameters, tcc.Validator, error) {
113+
is := core.NewPPManagerFactoryService(fabtoken.NewPPMFactory(), dlog.NewPPMFactory())
114+
115+
ppm, err := is.PublicParametersFromBytes(bytes)
116+
if err != nil {
117+
return nil, nil, err
118+
}
119+
v, err := is.DefaultValidator(ppm)
120+
if err != nil {
121+
return nil, nil, err
122+
}
123+
return ppm, token.NewValidator(v), nil
124+
}
125+
126+
type fakeLedger struct{}
127+
128+
func (*fakeLedger) GetState(_ tk.ID) ([]byte, error) {
129+
panic("ciao")
130+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"txid": "1",
3+
"req_raw": "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"
4+
}

0 commit comments

Comments
 (0)