diff --git a/cmd/artifactgen/go.mod b/cmd/artifactgen/go.mod index 077ae31944..c82cdab2db 100644 --- a/cmd/artifactgen/go.mod +++ b/cmd/artifactgen/go.mod @@ -32,7 +32,6 @@ require ( github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect - github.com/bytedance/gopkg v0.1.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/errors v1.14.0 // indirect diff --git a/cmd/artifactgen/go.sum b/cmd/artifactgen/go.sum index bb75b71fa4..c09caca9d3 100644 --- a/cmd/artifactgen/go.sum +++ b/cmd/artifactgen/go.sum @@ -636,8 +636,6 @@ github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoG github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= -github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw= github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= diff --git a/docs/development/debug-integration-tests.md b/docs/development/debug-integration-tests.md index 6fc98b3c57..be37b20bca 100644 --- a/docs/development/debug-integration-tests.md +++ b/docs/development/debug-integration-tests.md @@ -54,6 +54,35 @@ are already covered by CI's parallel per-infra jobs. It saves one cleaned, timestamp-stripped log file per failed job under `pr__failed_logs/`. +## Public Parameters Setup Failures (fabricx) + +On a fabricx network the token public parameters are installed by invoking the +`SetupPublicParams` view on the `issuer` FSC node +(`integration/nwo/token/fabricx/factory.go`). Two things about how that failure is +reported are worth knowing when a suite goes red: + +- **Installation is asynchronous.** `Backend.InstallPublicParams` is called from + `PostRun` and returns as soon as the work is scheduled, because it has to wait for the + issuer node to accept connections. Its outcome is recorded on the backend and reported + later: + - `NetworkHandler.UpdatePublicParams` checks it first, so a spec that updates the + public parameters fails with the original installation error rather than with a + confusing follow-up failure; + - `NetworkHandler.Cleanup` logs it at teardown (`public params installation for [...] + failed: ...`), so grep the suite log for that line when a network never became usable + but no spec pointed at the public parameters; + - a test can also block on it explicitly with + `Backend.WaitForPublicParams(tms, timeout)`. +- **A not-yet-started issuer is a wait, not a failure.** Both `InstallPublicParams` and + `UpdatePublicParams` retry the issuer client lookup (60 attempts, 1s apart by default, + configurable via the `ClientRetries`/`ClientRetryDelay`/`InstallDelay` fields of + `Backend`). `client [issuer] not ready after 60 attempts` therefore means the issuer FSC + node never came up — look at its own logs, not at the token platform. +- **Neither path panics.** A `SetupPublicParams` failure surfaces as a test failure + wrapping the view error (`failed setting up the public params on + [network:channel:namespace:driver]`). A process that dies with + `panic: failed updating pps` is running an old build. + ## Debugging Techniques - **Manual Inspection**: Use `time.Sleep()` or pause loops in tests to inspect Docker state - **Network Preservation**: Check for `no-cleanup` option or manually comment test suite cleanup diff --git a/integration/go.mod b/integration/go.mod index 8e4c871f59..bdfdb5c258 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -8,7 +8,6 @@ require ( github.com/IBM/idemix v0.2.0 github.com/IBM/mathlib v0.3.0 github.com/LFDT-Panurus/panurus v0.0.0 - github.com/bytedance/gopkg v0.1.3 github.com/gin-gonic/gin v1.12.0 github.com/hyperledger-labs/fabric-smart-client v0.17.0 github.com/hyperledger-labs/fabric-smart-client/integration v0.17.0 @@ -37,6 +36,7 @@ require ( github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect diff --git a/integration/nwo/token/fabric/cc/tcc.go b/integration/nwo/token/fabric/cc/tcc.go index 91773d4a19..69dafbb1d8 100644 --- a/integration/nwo/token/fabric/cc/tcc.go +++ b/integration/nwo/token/fabric/cc/tcc.go @@ -24,6 +24,7 @@ import ( "github.com/LFDT-Panurus/panurus/token/services/network/fabric/tcc/ccpackage" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fabric/packager" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fabric/topology" + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" "github.com/onsi/gomega" ) @@ -115,11 +116,15 @@ func (p *GenericBackend) PrepareNamespace(tms *topology3.TMS) { p.Fabric(tms).Topology().AddChaincode(cc) } -func (p *GenericBackend) InstallPublicParams(tms *topology3.TMS, raw []byte) { - // nothing to do here cause the chaincode initialization is done already in the fabric platform +// InstallPublicParams does nothing, cause the chaincode initialization is done already in +// the fabric platform. +func (p *GenericBackend) InstallPublicParams(tms *topology3.TMS, raw []byte) error { + return nil } -func (p *GenericBackend) UpdatePublicParams(tms *topology3.TMS, ppRaw []byte) { +// UpdatePublicParams repackages the token chaincode of tms with the given public parameters +// and upgrades it, returning any failure as an error. +func (p *GenericBackend) UpdatePublicParams(tms *topology3.TMS, ppRaw []byte) error { var cc *topology.ChannelChaincode for _, chaincode := range p.Fabric(tms).Topology().Chaincodes { if chaincode.Chaincode.Name == tms.Namespace { @@ -128,7 +133,9 @@ func (p *GenericBackend) UpdatePublicParams(tms *topology3.TMS, ppRaw []byte) { break } } - gomega.Expect(cc).NotTo(gomega.BeNil(), "failed to find chaincode [%s]", tms.Namespace) + if cc == nil { + return errors.Errorf("failed to find chaincode [%s]", tms.Namespace) + } packageDir := filepath.Join( p.TokenPlatform.GetContext().RootDir(), @@ -144,7 +151,9 @@ func (p *GenericBackend) UpdatePublicParams(tms *topology3.TMS, ppRaw []byte) { packageDir, cc.Chaincode.Name+newChaincodeVersion+".tar.gz", ) - gomega.Expect(os.MkdirAll(packageDir, 0750)).ToNot(gomega.HaveOccurred()) + if err := os.MkdirAll(packageDir, 0750); err != nil { + return errors.Wrapf(err, "failed creating package dir [%s]", packageDir) + } paramsFile := PublicParamsTemplate(ppRaw) @@ -171,11 +180,15 @@ func (p *GenericBackend) UpdatePublicParams(tms *topology3.TMS, ppRaw []byte) { return "", nil }, ) - gomega.Expect(err).ToNot(gomega.HaveOccurred()) + if err != nil { + return errors.Wrapf(err, "failed packaging chaincode [%s]", cc.Chaincode.Name) + } cc.Chaincode.PackageFile = packageFile p.Fabric(tms).UpdateChaincode(cc.Chaincode.Name, newChaincodeVersion, cc.Chaincode.Path, cc.Chaincode.PackageFile) + + return nil } func (p *GenericBackend) tccSetup(tms *topology3.TMS, cc *topology.ChannelChaincode) (*topology.ChannelChaincode, uint16) { diff --git a/integration/nwo/token/fabric/fabric.go b/integration/nwo/token/fabric/fabric.go index 33b74b05a8..a44e5c99da 100644 --- a/integration/nwo/token/fabric/fabric.go +++ b/integration/nwo/token/fabric/fabric.go @@ -38,8 +38,17 @@ type Entry struct { type Backend interface { PrepareNamespace(tms *topology2.TMS) - UpdatePublicParams(tms *topology2.TMS, raw []byte) - InstallPublicParams(tms *topology2.TMS, raw []byte) + UpdatePublicParams(tms *topology2.TMS, raw []byte) error + InstallPublicParams(tms *topology2.TMS, raw []byte) error +} + +// PublicParamsInstallWatcher is implemented by backends that install the public parameters +// in the background and can report the outcome of that work after InstallPublicParams has +// returned. +type PublicParamsInstallWatcher interface { + // PendingInstallError returns the failure recorded by the background installation of the + // public parameters of tms, or nil if it succeeded, is still running, or never started. + PendingInstallError(tms *topology2.TMS) error } type NetworkHandler struct { @@ -149,7 +158,8 @@ func (p *NetworkHandler) PostRun(load bool, tms *topology2.TMS) { } } - p.Backend.InstallPublicParams(tms, p.TokenPlatform.PublicParameters(tms)) + err := p.Backend.InstallPublicParams(tms, p.TokenPlatform.PublicParameters(tms)) + gomega.Expect(err).ToNot(gomega.HaveOccurred(), "failed installing public params for [%s]", tms.ID()) } func (p *NetworkHandler) Cleanup() { @@ -157,11 +167,34 @@ func (p *NetworkHandler) Cleanup() { if entry.CA != nil { entry.CA.Stop() } + // report a background public params installation that failed after PostRun returned, + // so that it does not go unnoticed. Cleanup runs at teardown, where raising an + // assertion would hide the failures of the tests themselves. + if err := p.pendingInstallError(entry.TMS); err != nil { + logger.Errorf("public params installation for [%s] failed: %v", entry.TMS.ID(), err) + } } } func (p *NetworkHandler) UpdatePublicParams(tms *topology2.TMS, ppRaw []byte) { - p.Backend.UpdatePublicParams(tms, ppRaw) + // a failed background installation is the most likely cause of a failing update, + // and it carries the original error, so report it first + err := p.pendingInstallError(tms) + gomega.Expect(err).ToNot(gomega.HaveOccurred(), "public params installation for [%s] failed", tms.ID()) + + err = p.Backend.UpdatePublicParams(tms, ppRaw) + gomega.Expect(err).ToNot(gomega.HaveOccurred(), "failed updating public params for [%s]", tms.ID()) +} + +// pendingInstallError returns the failure of the background public params installation for +// tms, if the backend tracks one, and nil otherwise. +func (p *NetworkHandler) pendingInstallError(tms *topology2.TMS) error { + watcher, ok := p.Backend.(PublicParamsInstallWatcher) + if !ok { + return nil + } + + return watcher.PendingInstallError(tms) } func (p *NetworkHandler) GenIssuerCryptoMaterial(tms *topology2.TMS, nodeID string, walletID string) string { diff --git a/integration/nwo/token/fabricx/factory.go b/integration/nwo/token/fabricx/factory.go index 8989f23dee..f097881d39 100644 --- a/integration/nwo/token/fabricx/factory.go +++ b/integration/nwo/token/fabricx/factory.go @@ -7,26 +7,93 @@ SPDX-License-Identifier: Apache-2.0 package fabricx import ( + "encoding/json" "fmt" + "sync" "time" "github.com/LFDT-Panurus/panurus/integration/nwo/token/fabric" tokentopology "github.com/LFDT-Panurus/panurus/integration/nwo/token/topology" "github.com/LFDT-Panurus/panurus/integration/token/fungible/views/ppsetup" - "github.com/bytedance/gopkg/util/logger" + "github.com/LFDT-Panurus/panurus/token/services/logging" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/api" - common2 "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/common" fabrictopology "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fabric/topology" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fabricx" + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" "github.com/onsi/gomega" ) +const ( + // issuerClientID is the name of the FSC node whose view sets up the public parameters. + issuerClientID = "issuer" + // setupPublicParamsView is the view invoked on the issuer to set up the public parameters. + setupPublicParamsView = "SetupPublicParams" + // setupPublicParamsTimeout is the timeout passed to the SetupPublicParams view. + setupPublicParamsTimeout = 2 * time.Minute + // defaultClientRetries is how many times we look for a ready issuer client before giving up. + defaultClientRetries = 60 + // defaultClientRetryDelay is how long we wait between two issuer client lookups. + defaultClientRetryDelay = 1 * time.Second + // defaultInstallDelay is how long the background installation waits before its first attempt, + // giving the FSC nodes time to come up. + defaultInstallDelay = 10 * time.Second +) + +var logger = logging.MustGetLogger() + type ClientProvider interface { Client(string) api.GRPCClient } +// Backend installs and updates the token public parameters of a fabricx network by +// invoking the SetupPublicParams view on the issuer FSC node. +// +// Neither InstallPublicParams nor UpdatePublicParams panics: both wait for the issuer +// client to become ready and report any failure as an error. InstallPublicParams does its +// work on a background goroutine, whose outcome is retrieved with WaitForPublicParams or +// PendingInstallError. type Backend struct { ClientProvider ClientProvider + + // ClientRetries is how many times to look for a ready issuer client before giving up. + // Zero means defaultClientRetries. + ClientRetries int + // ClientRetryDelay is how long to wait between two issuer client lookups. + // Zero means defaultClientRetryDelay. + ClientRetryDelay time.Duration + // InstallDelay is how long InstallPublicParams waits before its first attempt. + // Zero means defaultInstallDelay. + InstallDelay time.Duration + + mutex sync.Mutex + installs map[string]*installTask +} + +// installTask tracks the outcome of one background public-params installation. err is +// written before done is closed and only read after done is closed, so it needs no lock. +type installTask struct { + done chan struct{} + once sync.Once + err error +} + +func newInstallTask() *installTask { + return &installTask{done: make(chan struct{})} +} + +// complete records the outcome of the installation and unblocks any waiter. Only the +// first call has an effect, so a panic recovered after an error was already recorded +// does not overwrite the original cause. +func (t *installTask) complete(err error) { + t.once.Do(func() { + t.err = err + close(t.done) + }) +} + +// result returns the recorded outcome. It must only be called once done is closed. +func (t *installTask) result() error { + return t.err } func (b *Backend) PrepareNamespace(tms *tokentopology.TMS) { @@ -67,52 +134,187 @@ func addNamespace(n *fabrictopology.Topology, tms *tokentopology.TMS, orgs ...st n.AddNamespace(tms.Namespace, policy, peers...) } -func (b *Backend) InstallPublicParams(tms *tokentopology.TMS, ppRaw []byte) { - time.Sleep(10 * time.Second) +// InstallPublicParams starts the installation of the public parameters of tms in the +// background and returns as soon as that work is scheduled. The installation waits for the +// issuer FSC node to become reachable, so it must not run synchronously during network +// bring-up. +// +// The returned error only reports a failure to schedule the work. The outcome of the +// installation itself is obtained with WaitForPublicParams or PendingInstallError; it is +// never raised as a panic on the background goroutine. +func (b *Backend) InstallPublicParams(tms *tokentopology.TMS, ppRaw []byte) error { + if b.ClientProvider == nil { + return errors.Errorf("no client provider available, cannot install public params on [%s]", tms.ID()) + } + task := b.newInstallTask(tms) go func() { - // let's wait for a maximum of one minute - for range 60 { - logger.Infof("installing public params on [%s:%s:%s:%s]...", tms.Network, tms.Channel, tms.Namespace, tms.Driver) - issuer := b.ClientProvider.Client("issuer") - if issuer != nil { - _, err := b.ClientProvider.Client("issuer").CallView("SetupPublicParams", common2.JSONMarshall( - &ppsetup.SetupPublicParams{ - Network: tms.Network, - Channel: tms.Channel, - Namespace: tms.Namespace, - PublicParamsRaw: ppRaw, - Timeout: 2 * time.Minute, - }, - )) - if err != nil { - logger.Error("installing public params on [%s:%s:%s:%s]...failed [%v]", tms.Network, tms.Channel, tms.Namespace, tms.Driver, err) - - panic("failed updating pps: " + err.Error()) - } - logger.Infof("installing public params on [%s:%s:%s:%s]...done", tms.Network, tms.Channel, tms.Namespace, tms.Driver) - - return + // nothing above this goroutine is watching for a panic, so turn one into a + // recorded error instead of letting it take down the whole process + defer func() { + if r := recover(); r != nil { + task.complete(errors.Errorf("panic while installing public params on [%s]: %v", tms.ID(), r)) } + }() - logger.Infof("installing public params on [%s:%s:%s:%s]...client not ready, wait a bit...", tms.Network, tms.Channel, tms.Namespace, tms.Driver) - time.Sleep(1 * time.Second) - } - panic("failed installing public params") + task.complete(b.installPublicParams(tms, ppRaw)) }() + + return nil } -func (b *Backend) UpdatePublicParams(tms *tokentopology.TMS, ppRaw []byte) { - _, err := b.ClientProvider.Client("issuer").CallView("SetupPublicParams", common2.JSONMarshall( - &ppsetup.SetupPublicParams{ - Network: tms.Network, - Channel: tms.Channel, - Namespace: tms.Namespace, - PublicParamsRaw: ppRaw, - Timeout: 2 * time.Minute, - }, - )) +// installPublicParams performs the actual installation, reporting any failure as an error. +func (b *Backend) installPublicParams(tms *tokentopology.TMS, ppRaw []byte) error { + time.Sleep(b.installDelay()) + + logger.Infof("installing public params on [%s]...", tms.ID()) + if err := b.setupPublicParams(tms, ppRaw); err != nil { + logger.Errorf("installing public params on [%s]...failed [%v]", tms.ID(), err) + + return err + } + logger.Infof("installing public params on [%s]...done", tms.ID()) + + return nil +} + +// UpdatePublicParams replaces the public parameters of tms by invoking the SetupPublicParams +// view on the issuer FSC node. It waits for the issuer client to become available, so calling +// it while the issuer node is still starting is a wait rather than a failure, and returns any +// failure as an error. +func (b *Backend) UpdatePublicParams(tms *tokentopology.TMS, ppRaw []byte) error { + if b.ClientProvider == nil { + return errors.Errorf("no client provider available, cannot update public params on [%s]", tms.ID()) + } + + logger.Infof("updating public params on [%s]...", tms.ID()) + if err := b.setupPublicParams(tms, ppRaw); err != nil { + logger.Errorf("updating public params on [%s]...failed [%v]", tms.ID(), err) + + return err + } + logger.Infof("updating public params on [%s]...done", tms.ID()) + + return nil +} + +// WaitForPublicParams blocks until the background installation started by +// InstallPublicParams for tms has finished, and returns its outcome. It returns an error if +// the installation did not finish within timeout, and nil if no installation was ever +// started for tms. It can be called repeatedly and always reports the same outcome. +func (b *Backend) WaitForPublicParams(tms *tokentopology.TMS, timeout time.Duration) error { + task := b.installTaskFor(tms) + if task == nil { + return nil + } + + select { + case <-task.done: + return task.result() + case <-time.After(timeout): + return errors.Errorf("timeout waiting for the installation of the public params on [%s]", tms.ID()) + } +} + +// PendingInstallError returns the failure recorded by the background installation of the +// public parameters of tms, or nil if that installation succeeded, is still running, or was +// never started. Unlike WaitForPublicParams it never blocks. +func (b *Backend) PendingInstallError(tms *tokentopology.TMS) error { + task := b.installTaskFor(tms) + if task == nil { + return nil + } + + select { + case <-task.done: + return task.result() + default: + return nil + } +} + +// newInstallTask registers a fresh task for tms, replacing any previous one. +func (b *Backend) newInstallTask(tms *tokentopology.TMS) *installTask { + b.mutex.Lock() + defer b.mutex.Unlock() + + if b.installs == nil { + b.installs = map[string]*installTask{} + } + task := newInstallTask() + b.installs[tms.ID()] = task + + return task +} + +// installTaskFor returns the task registered for tms, or nil if there is none. +func (b *Backend) installTaskFor(tms *tokentopology.TMS) *installTask { + b.mutex.Lock() + defer b.mutex.Unlock() + + return b.installs[tms.ID()] +} + +// setupPublicParams waits for the issuer client to become available, then invokes the +// SetupPublicParams view on it. It returns an error if the client is still not available +// after the configured number of attempts, or if the view fails. +func (b *Backend) setupPublicParams(tms *tokentopology.TMS, ppRaw []byte) error { + retries, delay := b.clientRetries(), b.clientRetryDelay() + for range retries { + issuer := b.ClientProvider.Client(issuerClientID) + if issuer != nil { + return callSetupPublicParamsView(issuer, tms, ppRaw) + } + + logger.Infof("public params setup on [%s]...client [%s] not ready, wait a bit...", tms.ID(), issuerClientID) + time.Sleep(delay) + } + + return errors.Errorf("client [%s] not ready after %d attempts, cannot set up the public params on [%s]", issuerClientID, retries, tms.ID()) +} + +func (b *Backend) clientRetries() int { + if b.ClientRetries > 0 { + return b.ClientRetries + } + + return defaultClientRetries +} + +func (b *Backend) clientRetryDelay() time.Duration { + if b.ClientRetryDelay > 0 { + return b.ClientRetryDelay + } + + return defaultClientRetryDelay +} + +func (b *Backend) installDelay() time.Duration { + if b.InstallDelay > 0 { + return b.InstallDelay + } + + return defaultInstallDelay +} + +// callSetupPublicParamsView invokes the SetupPublicParams view on the given client. +func callSetupPublicParamsView(issuer api.GRPCClient, tms *tokentopology.TMS, ppRaw []byte) error { + // marshalling here rather than via common.JSONMarshall, whose failure mode is an + // assertion that needs a registered gomega fail handler + input, err := json.Marshal(&ppsetup.SetupPublicParams{ + Network: tms.Network, + Channel: tms.Channel, + Namespace: tms.Namespace, + PublicParamsRaw: ppRaw, + Timeout: setupPublicParamsTimeout, + }) if err != nil { - panic("failed updating pps: " + err.Error()) + return errors.Wrapf(err, "failed marshalling the public params setup request for [%s]", tms.ID()) + } + + if _, err := issuer.CallView(setupPublicParamsView, input); err != nil { + return errors.Wrapf(err, "failed setting up the public params on [%s:%s:%s:%s]", tms.Network, tms.Channel, tms.Namespace, tms.Driver) } + + return nil } diff --git a/integration/nwo/token/fabricx/factory_test.go b/integration/nwo/token/fabricx/factory_test.go new file mode 100644 index 0000000000..07887f6b6c --- /dev/null +++ b/integration/nwo/token/fabricx/factory_test.go @@ -0,0 +1,264 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fabricx + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "sync/atomic" + "testing" + "time" + + tokentopology "github.com/LFDT-Panurus/panurus/integration/nwo/token/topology" + "github.com/LFDT-Panurus/panurus/integration/token/fungible/views/ppsetup" + "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/api" + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + viewclient "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/view/grpc/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// installCrashEnv marks the child process of +// TestInstallPublicParamsBackgroundFailureDoesNotCrashProcess. +const installCrashEnv = "FTS_TEST_FABRICX_INSTALL_PP_CHILD" + +// fakeClient is an api.GRPCClient whose CallView behaviour is driven by the test. +type fakeClient struct { + callView func(fid string, in []byte) (any, error) + calls atomic.Int32 + lastFID atomic.Value + lastIn atomic.Value +} + +func (c *fakeClient) CallView(fid string, in []byte) (any, error) { + c.calls.Add(1) + c.lastFID.Store(fid) + c.lastIn.Store(in) + + return c.callView(fid, in) +} + +func (c *fakeClient) CallViewWithContext(_ context.Context, fid string, in []byte) (any, error) { + return c.CallView(fid, in) +} + +func (c *fakeClient) StreamCallView(string, []byte) (*viewclient.Stream, error) { + return nil, errors.New("not implemented") +} + +// fakeClientProvider returns nil for the first nilTimes lookups, and client afterwards. +type fakeClientProvider struct { + client api.GRPCClient + nilTimes int + lookups atomic.Int32 +} + +//nolint:ireturn // the return type is fixed by the ClientProvider interface under test +func (p *fakeClientProvider) Client(string) api.GRPCClient { + if int(p.lookups.Add(1)) <= p.nilTimes { + return nil + } + + return p.client +} + +func newTMS() *tokentopology.TMS { + return &tokentopology.TMS{ + Network: "testnet", + Channel: "testchannel", + Namespace: "tns", + Driver: "fabtoken", + } +} + +// newBackend returns a Backend with test-sized retry budgets, so that a failing lookup does +// not take the production minute. +func newBackend(provider ClientProvider) *Backend { + return &Backend{ + ClientProvider: provider, + ClientRetries: 3, + ClientRetryDelay: time.Millisecond, + InstallDelay: time.Millisecond, + } +} + +func okClient() *fakeClient { + return &fakeClient{callView: func(string, []byte) (any, error) { return nil, nil }} +} + +func failingClient(msg string) *fakeClient { + return &fakeClient{callView: func(string, []byte) (any, error) { return nil, errors.New(msg) }} +} + +func TestUpdatePublicParamsClientNeverReady(t *testing.T) { + backend := newBackend(&fakeClientProvider{client: nil, nilTimes: 1000}) + tms := newTMS() + + var err error + require.NotPanics(t, func() { err = backend.UpdatePublicParams(tms, []byte("pp")) }) + require.Error(t, err) + assert.Contains(t, err.Error(), "client [issuer] not ready after 3 attempts") + assert.Contains(t, err.Error(), tms.ID()) +} + +func TestUpdatePublicParamsNoClientProvider(t *testing.T) { + backend := &Backend{} + + var err error + require.NotPanics(t, func() { err = backend.UpdatePublicParams(newTMS(), []byte("pp")) }) + require.ErrorContains(t, err, "no client provider available") +} + +func TestUpdatePublicParamsClientBecomesReady(t *testing.T) { + client := okClient() + provider := &fakeClientProvider{client: client, nilTimes: 2} + backend := newBackend(provider) + + require.NoError(t, backend.UpdatePublicParams(newTMS(), []byte("pp"))) + assert.Equal(t, int32(1), client.calls.Load()) + assert.Equal(t, int32(3), provider.lookups.Load()) +} + +func TestUpdatePublicParamsCallViewError(t *testing.T) { + backend := newBackend(&fakeClientProvider{client: failingClient("connection refused")}) + tms := newTMS() + + var err error + require.NotPanics(t, func() { err = backend.UpdatePublicParams(tms, []byte("pp")) }) + require.Error(t, err) + assert.Contains(t, err.Error(), "connection refused") + assert.Contains(t, err.Error(), "failed setting up the public params on [testnet:testchannel:tns:fabtoken]") +} + +func TestUpdatePublicParamsSendsExpectedPayload(t *testing.T) { + client := okClient() + backend := newBackend(&fakeClientProvider{client: client}) + tms := newTMS() + ppRaw := []byte("public-parameters") + + require.NoError(t, backend.UpdatePublicParams(tms, ppRaw)) + + assert.Equal(t, "SetupPublicParams", client.lastFID.Load()) + in, ok := client.lastIn.Load().([]byte) + require.True(t, ok, "no payload recorded") + var sent ppsetup.SetupPublicParams + require.NoError(t, json.Unmarshal(in, &sent)) + assert.Equal(t, tms.Network, sent.Network) + assert.Equal(t, tms.Channel, sent.Channel) + assert.Equal(t, tms.Namespace, sent.Namespace) + assert.Equal(t, ppRaw, sent.PublicParamsRaw) + assert.Equal(t, 2*time.Minute, sent.Timeout) +} + +func TestInstallPublicParamsSuccess(t *testing.T) { + client := okClient() + backend := newBackend(&fakeClientProvider{client: client}) + tms := newTMS() + + require.NoError(t, backend.InstallPublicParams(tms, []byte("pp"))) + require.NoError(t, backend.WaitForPublicParams(tms, 10*time.Second)) + assert.Equal(t, int32(1), client.calls.Load()) + assert.NoError(t, backend.PendingInstallError(tms)) +} + +func TestInstallPublicParamsNoClientProvider(t *testing.T) { + backend := &Backend{} + + require.ErrorContains(t, backend.InstallPublicParams(newTMS(), []byte("pp")), "no client provider available") +} + +func TestInstallPublicParamsCallViewErrorIsReported(t *testing.T) { + backend := newBackend(&fakeClientProvider{client: failingClient("connection refused")}) + tms := newTMS() + + // scheduling the installation succeeds, the failure surfaces when waiting for it + require.NoError(t, backend.InstallPublicParams(tms, []byte("pp"))) + err := backend.WaitForPublicParams(tms, 10*time.Second) + require.Error(t, err) + assert.Contains(t, err.Error(), "connection refused") + // the outcome is memoised: asking twice reports the same failure + require.ErrorContains(t, backend.WaitForPublicParams(tms, 10*time.Second), "connection refused") + require.ErrorContains(t, backend.PendingInstallError(tms), "connection refused") +} + +func TestInstallPublicParamsClientNeverReadyIsReported(t *testing.T) { + backend := newBackend(&fakeClientProvider{client: nil, nilTimes: 1000}) + tms := newTMS() + + require.NoError(t, backend.InstallPublicParams(tms, []byte("pp"))) + require.ErrorContains(t, backend.WaitForPublicParams(tms, 10*time.Second), "client [issuer] not ready after 3 attempts") +} + +func TestInstallPublicParamsPanicIsRecovered(t *testing.T) { + panicking := &fakeClient{callView: func(string, []byte) (any, error) { panic("boom") }} + backend := newBackend(&fakeClientProvider{client: panicking}) + tms := newTMS() + + require.NoError(t, backend.InstallPublicParams(tms, []byte("pp"))) + err := backend.WaitForPublicParams(tms, 10*time.Second) + require.Error(t, err) + assert.Contains(t, err.Error(), "panic while installing public params") + assert.Contains(t, err.Error(), "boom") +} + +func TestWaitForPublicParamsTimeout(t *testing.T) { + blocked := make(chan struct{}) + defer close(blocked) + slow := &fakeClient{callView: func(string, []byte) (any, error) { + <-blocked + + return nil, nil + }} + backend := newBackend(&fakeClientProvider{client: slow}) + tms := newTMS() + + require.NoError(t, backend.InstallPublicParams(tms, []byte("pp"))) + require.ErrorContains(t, backend.WaitForPublicParams(tms, 50*time.Millisecond), "timeout waiting for the installation") + // an installation still in flight is not reported as a failure + assert.NoError(t, backend.PendingInstallError(tms)) +} + +func TestWaitForPublicParamsWithoutInstall(t *testing.T) { + backend := newBackend(&fakeClientProvider{client: okClient()}) + + assert.NoError(t, backend.WaitForPublicParams(newTMS(), time.Millisecond)) + assert.NoError(t, backend.PendingInstallError(newTMS())) +} + +// TestInstallPublicParamsBackgroundFailureDoesNotCrashProcess is the regression test for the +// original defect: the background installation goroutine panicked on a CallView error, which +// terminated the whole process asynchronously. An unrecovered goroutine panic kills the test +// binary too, so the check runs in a child process: the parent re-executes this same test +// with installCrashEnv set and asserts the child exits cleanly. +func TestInstallPublicParamsBackgroundFailureDoesNotCrashProcess(t *testing.T) { + if os.Getenv(installCrashEnv) != "1" { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + //nolint:gosec // re-executes this very test binary with a compile-time test name + cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^"+t.Name()+"$", "-test.v") + cmd.Env = append(os.Environ(), installCrashEnv+"=1") + out, runErr := cmd.CombinedOutput() + + require.NoError(t, runErr, "the background installation failure must not crash the process:\n%s", out) + assert.NotContains(t, string(out), "panic:", "the background goroutine must not panic:\n%s", out) + assert.Contains(t, string(out), "PASS") + + return + } + + // child process: a failing installation must be reported, not panicked + backend := newBackend(&fakeClientProvider{client: failingClient("connection refused")}) + tms := newTMS() + + require.NoError(t, backend.InstallPublicParams(tms, []byte("pp"))) + require.ErrorContains(t, backend.WaitForPublicParams(tms, 10*time.Second), "connection refused") + // leave the goroutine time to do any further damage before the process exits + time.Sleep(100 * time.Millisecond) +}