Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
tests: [unit-tests, unit-tests-race]
tests: [unit-tests, unit-tests-race, unit-tests-regression]

steps:
- name: Checkout code
Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,19 @@ download-fabric:

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

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

.PHONY: unit-tests-regression
unit-tests-regression:
@go test -race -timeout 0 -cover $(shell go list ./... | grep -v '/integration/' | grep 'regression')

.PHONY: install-softhsm
install-softhsm:
./ci/scripts/install_softhsm.sh
Expand Down
79 changes: 78 additions & 1 deletion token/core/zkatdlog/nogh/v1/benchmark/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

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

type SetupConfigurationSer struct {
PP string `json:"pp"` // Encoded in base64
Bits uint64 `json:"bits"`
CurveID int `json:"curveID"`
}

// SetupConfiguration holds the prepared public parameters and related
// identities/signers used by a single benchmark configuration.
//
Expand All @@ -42,6 +51,8 @@ type SetupConfiguration struct {
OwnerIdentity *OwnerIdentity
AuditorSigner *Signer
IssuerSigner *Signer
Bits uint64
CurveID math.CurveID
}

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

func key(bits uint64, curveID math.CurveID) string {
return fmt.Sprintf("%d-%d", bits, curveID)
return fmt.Sprintf("%d-%s", bits, math2.CurveIDToString(curveID))
}

// SaveTo writes each configuration to disk under the provided directory.
// For each entry in the Configurations map a folder with the map key is
// created. Inside that folder a file named `pp.json` is written. The file
// contains a JSON document with the base64-encoded serialized public
// parameters and the metadata bits and curve_id.
func (c *SetupConfigurations) SaveTo(dir string) error {
if c == nil {
return errors.Errorf("nil SetupConfigurations")
}
// Ensure target base directory exists
if err := os.MkdirAll(dir, 0o755); err != nil {
return errors.Wrapf(err, "failed creating base dir [%s]")
}

for k, cfg := range c.Configurations {
if strings.ContainsAny(k, "/\\") {
return errors.Errorf("invalid configuration key: %s", k)
}
if cfg == nil {
return errors.Errorf("nil configuration for key: %s", k)
}
if cfg.PP == nil {
return errors.Errorf("nil public parameters for key: %s", k)
}

// serialize public params
ppBytes, err := cfg.PP.Serialize()
if err != nil {
return errors.WithMessagef(err, "failed serializing public params for key: %s", k)
}

// prepare JSON payload
payload := &SetupConfigurationSer{
PP: base64.StdEncoding.EncodeToString(ppBytes),
Bits: cfg.Bits,
CurveID: int(cfg.CurveID),
}
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return errors.Wrap(err, "failed marshalling json payload")
}

// create target directory and write
targetDir := filepath.Join(dir, filepath.Base(k))
if err := os.MkdirAll(targetDir, 0o755); err != nil {
return errors.WithMessagef(err, "failed creating dir for key: %s", k)
}

// Write params.txt containing base64(ppBytes)
paramsEncoded := payload.PP
finalParamsPath := filepath.Join(targetDir, "params.txt")
if err := os.WriteFile(finalParamsPath, []byte(paramsEncoded), 0o644); err != nil {
return errors.WithMessagef(err, "failed writing params file for key: %s", k)
}

// Write pp.json file
finalPath := filepath.Join(targetDir, "pp.json")
if err := os.WriteFile(finalPath, data, 0o644); err != nil {
return errors.WithMessagef(err, "failed writing pp.json for key: %s", k)
}
}
return nil
}

// OwnerIdentity represents the owner identity used by benchmarks. It
Expand Down
71 changes: 71 additions & 0 deletions token/core/zkatdlog/nogh/v1/benchmark/setup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package benchmark

import (
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
"testing"

math "github.com/IBM/mathlib"
"github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/v1/setup"
"github.com/stretchr/testify/require"
)

func TestSaveTo(t *testing.T) {
require := require.New(t)
dir := t.TempDir()
bits := uint64(16)
curve := math.BN254
pp, err := setup.Setup(bits, []byte("dummy-issuer-pk"), curve)
require.NoError(err, "failed to create public params")

cfg := &SetupConfigurations{
Configurations: map[string]*SetupConfiguration{
key(bits, curve): {
Bits: bits,
CurveID: curve,
PP: pp,
},
},
}

require.NoError(cfg.SaveTo(dir), "SaveTo failed")

targetDir := filepath.Join(dir, key(bits, curve))
st, err := os.Stat(targetDir)
require.NoError(err, "expected target dir to exist")
require.True(st.IsDir(), "expected target to be a directory")

filePath := filepath.Join(targetDir, "pp.json")
data, err := os.ReadFile(filePath)
require.NoError(err, "failed reading pp.json")

var payload SetupConfigurationSer
require.NoError(json.Unmarshal(data, &payload), "failed to unmarshal pp.json")

// bits and curve_id are numbers decoded as float64
require.Equal(bits, payload.Bits, "bits mismatch")
require.Equal(int(curve), payload.CurveID, "curve_id mismatch")

ppB64 := payload.PP
decoded, err := base64.StdEncoding.DecodeString(ppB64)
require.NoError(err, "failed to base64 decode pp")
require.NotEmpty(decoded, "decoded pp is empty")

// check params.txt exists and contains the same base64 string
paramsPath := filepath.Join(targetDir, "params.txt")
paramsData, err := os.ReadFile(paramsPath)
require.NoError(err, "failed reading params.txt")
require.Equal(ppB64, string(paramsData), "params.txt content mismatch")

// try deserializing the stored public params to ensure it's valid
_, err = setup.NewPublicParamsFromBytes(decoded, pp.DriverName, pp.DriverVersion)
require.NoError(err, "failed to deserialize stored public params")
}
130 changes: 130 additions & 0 deletions token/core/zkatdlog/nogh/v1/validator/regression/regression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package regression_test

import (
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"path/filepath"
"testing"

"github.com/hyperledger-labs/fabric-token-sdk/token"
"github.com/hyperledger-labs/fabric-token-sdk/token/core"
fabtoken "github.com/hyperledger-labs/fabric-token-sdk/token/core/fabtoken/v1/driver"
dlog "github.com/hyperledger-labs/fabric-token-sdk/token/core/zkatdlog/nogh/v1/driver"
"github.com/hyperledger-labs/fabric-token-sdk/token/services/network/fabric/tcc"
tk "github.com/hyperledger-labs/fabric-token-sdk/token/token"
"github.com/stretchr/testify/require"
)

//go:embed testdata
var testDataFS embed.FS

// TestRegression runs previously recorded regression test vectors contained in the
// `testdata/<variant>` directories. Each directory contains a `params.txt` and a
// sequence of JSON files under `transfers/output.<n>.json` representing serialized
// token requests and their associated txids. The test unmarshals each recorded
// request and verifies it against the validator to ensure the library remains
// backwards compatible.
//
// Notes:
// - The testdata used here is generated by `testdata/generator`. If you need to
// re-generate vectors for a new setup, run that generator and commit the
// produced artifacts to the corresponding `testdata/...` directory.
// - The test expects 64 transfer vectors (output.0..output.63). Update the loop
// range in `testRegression` if you add or remove vectors.
func TestRegression(t *testing.T) {
t.Parallel()
for _, action := range []string{"transfers", "issues", "redeems", "swaps"} {
testRegressionParallel(t, "testdata/32-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i1_o1", action))
testRegressionParallel(t, "testdata/32-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i1_o2", action))
testRegressionParallel(t, "testdata/32-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i2_o1", action))
testRegressionParallel(t, "testdata/32-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i2_o2", action))

testRegressionParallel(t, "testdata/64-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i1_o1", action))
testRegressionParallel(t, "testdata/64-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i1_o2", action))
testRegressionParallel(t, "testdata/64-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i2_o1", action))
testRegressionParallel(t, "testdata/64-BLS12_381_BBS_GURVY", fmt.Sprintf("%s_i2_o2", action))

testRegressionParallel(t, "testdata/32-BN254", fmt.Sprintf("%s_i1_o1", action))
testRegressionParallel(t, "testdata/32-BN254", fmt.Sprintf("%s_i1_o2", action))
testRegressionParallel(t, "testdata/32-BN254", fmt.Sprintf("%s_i2_o1", action))
testRegressionParallel(t, "testdata/32-BN254", fmt.Sprintf("%s_i2_o2", action))

testRegressionParallel(t, "testdata/64-BN254", fmt.Sprintf("%s_i1_o1", action))
testRegressionParallel(t, "testdata/64-BN254", fmt.Sprintf("%s_i1_o2", action))
testRegressionParallel(t, "testdata/64-BN254", fmt.Sprintf("%s_i2_o1", action))
testRegressionParallel(t, "testdata/64-BN254", fmt.Sprintf("%s_i2_o2", action))
}
}

func testRegressionParallel(t *testing.T, rootDir, subFolder string) {
t.Helper()
t.Run(fmt.Sprintf("%s-%s", rootDir, subFolder), func(t *testing.T) {
t.Parallel()
testRegression(t, rootDir, subFolder)
})
}

func testRegression(t *testing.T, rootDir, subFolder string) {
t.Helper()
t.Logf("regression test for [%s:%s]", rootDir, subFolder)
paramsData, err := testDataFS.ReadFile(filepath.Join(rootDir, "params.txt"))
require.NoError(t, err)

ppRaw, err := base64.StdEncoding.DecodeString(string(paramsData))
require.NoError(t, err)

_, tokenValidator, err := tokenServicesFactory(ppRaw)
require.NoError(t, err)

var tokenData struct {
ReqRaw []byte `json:"req_raw"`
TXID string `json:"txid"`
}
for i := range 64 {
jsonData, err := testDataFS.ReadFile(
filepath.Join(
rootDir,
subFolder,
fmt.Sprintf("output.%d.json", i),
),
)
require.NoError(t, err)
err = json.Unmarshal(jsonData, &tokenData)
require.NoError(t, err)
_, _, err = tokenValidator.UnmarshallAndVerifyWithMetadata(
t.Context(),
&fakeLedger{},
token.RequestAnchor(tokenData.TXID),
tokenData.ReqRaw,
)
require.NoError(t, err)
}
}

func tokenServicesFactory(bytes []byte) (tcc.PublicParameters, tcc.Validator, error) {
is := core.NewPPManagerFactoryService(fabtoken.NewPPMFactory(), dlog.NewPPMFactory())

ppm, err := is.PublicParametersFromBytes(bytes)
if err != nil {
return nil, nil, err
}
v, err := is.DefaultValidator(ppm)
if err != nil {
return nil, nil, err
}
return ppm, token.NewValidator(v), nil
}

type fakeLedger struct{}

func (*fakeLedger) GetState(_ tk.ID) ([]byte, error) {
panic("ciao")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"txid": "1",
"req_raw": "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"
}
Loading
Loading