From 54b4724b1ac540f36cda71654f030e3dab70c1bd Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Mon, 16 May 2022 15:08:55 -0400 Subject: [PATCH 01/13] Add new Ethereum contract config Signed-off-by: Andrew Richardson --- docs/reference/config.md | 18 +- internal/blockchain/ethereum/config.go | 25 ++- internal/blockchain/ethereum/ethereum.go | 79 +++++--- internal/blockchain/ethereum/ethereum_test.go | 172 ++++++++++++------ internal/blockchain/fabric/fabric.go | 2 +- internal/blockchain/fabric/fabric_test.go | 20 +- internal/coremsgs/en_config_descriptions.go | 16 +- internal/orchestrator/orchestrator.go | 4 +- internal/orchestrator/orchestrator_test.go | 4 +- mocks/blockchainmocks/plugin.go | 10 +- pkg/blockchain/plugin.go | 3 +- 11 files changed, 226 insertions(+), 127 deletions(-) diff --git a/docs/reference/config.md b/docs/reference/config.md index 41ba62a32..c3e0ff0bd 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -163,10 +163,10 @@ nav_order: 3 |batchTimeout|How long Ethconnect should wait for new events to arrive and fill a batch, before sending the batch to FireFly core. Only applies when automatically creating a new event stream|[`time.Duration`](https://pkg.go.dev/time#Duration)|`500` |connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` |expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` -|fromBlock|The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream|Address `string`|`0` +|fromBlock|The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream (deprecated - use fireflyContract[].fromBlock)|Address `string`|`0` |headers|Adds custom headers to HTTP requests|`map[string]string`|`` |idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms` -|instance|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain|Address `string`|`` +|instance|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain (deprecated - use fireflyContract[].address)|Address `string`|`` |maxIdleConns|The max number of idle connections to hold pooled|`int`|`100` |prefixLong|The prefix that will be used for Ethconnect specific HTTP headers when FireFly makes requests to Ethconnect|`string`|`firefly` |prefixShort|The prefix that will be used for Ethconnect specific query parameters when FireFly makes requests to Ethconnect|`string`|`fly` @@ -242,6 +242,13 @@ nav_order: 3 |initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` |maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +## blockchain.ethereum.fireflyContract[] + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|address|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain|Address `string`|`` +|fromBlock|The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream|Address `string`|`` + ## blockchain.fabric.fabconnect |Key|Description|Type|Default Value| @@ -810,6 +817,13 @@ nav_order: 3 |initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` |maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +## plugins.blockchain[].ethereum.fireflyContract[] + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|address|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain|Address `string`|`` +|fromBlock|The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream|Address `string`|`` + ## plugins.blockchain[].fabric.fabconnect |Key|Description|Type|Default Value| diff --git a/internal/blockchain/ethereum/config.go b/internal/blockchain/ethereum/config.go index 688fcac4c..41cdc91e9 100644 --- a/internal/blockchain/ethereum/config.go +++ b/internal/blockchain/ethereum/config.go @@ -36,10 +36,8 @@ const ( ) const ( - // EthconnectConfigKey is a sub-key in the config to contain all the ethconnect specific config, + // EthconnectConfigKey is a sub-key in the config to contain all the ethconnect specific config EthconnectConfigKey = "ethconnect" - // EthconnectConfigInstancePath is the ethereum address of the contract - EthconnectConfigInstancePath = "instance" // EthconnectConfigTopic is the websocket listen topic that the node should register on, which is important if there are multiple // nodes using a single ethconnect EthconnectConfigTopic = "topic" @@ -51,8 +49,17 @@ const ( EthconnectPrefixShort = "prefixShort" // EthconnectPrefixLong is used in HTTP headers in requests to ethconnect EthconnectPrefixLong = "prefixLong" - // EthconnectConfigFromBlock is the configuration of the first block to listen to when creating the listener for the FireFly contract - EthconnectConfigFromBlock = "fromBlock" + // EthconnectConfigInstanceDeprecated is the ethereum address of the FireFly contract + EthconnectConfigInstanceDeprecated = "instance" + // EthconnectConfigFromBlockDeprecated is the configuration of the first block to listen to when creating the listener for the FireFly contract + EthconnectConfigFromBlockDeprecated = "fromBlock" + + // FireFlyContractConfigKey is a sub-key in the config to contain the info on the deployed FireFly contract + FireFlyContractConfigKey = "fireflyContract" + // FireFlyContractAddress is the ethereum address of the FireFly contract + FireFlyContractAddress = "address" + // FireFlyContractFromBlock is the configuration of the first block to listen to when creating the listener + FireFlyContractFromBlock = "fromBlock" // AddressResolverConfigKey is a sub-key in the config to contain an address resolver config. AddressResolverConfigKey = "addressResolver" @@ -78,13 +85,17 @@ const ( func (e *Ethereum) InitConfig(config config.Section) { ethconnectConf := config.SubSection(EthconnectConfigKey) wsclient.InitConfig(ethconnectConf) - ethconnectConf.AddKnownKey(EthconnectConfigInstancePath) ethconnectConf.AddKnownKey(EthconnectConfigTopic) ethconnectConf.AddKnownKey(EthconnectConfigBatchSize, defaultBatchSize) ethconnectConf.AddKnownKey(EthconnectConfigBatchTimeout, defaultBatchTimeout) ethconnectConf.AddKnownKey(EthconnectPrefixShort, defaultPrefixShort) ethconnectConf.AddKnownKey(EthconnectPrefixLong, defaultPrefixLong) - ethconnectConf.AddKnownKey(EthconnectConfigFromBlock, defaultFromBlock) + ethconnectConf.AddKnownKey(EthconnectConfigInstanceDeprecated) + ethconnectConf.AddKnownKey(EthconnectConfigFromBlockDeprecated, defaultFromBlock) + + e.contractConf = config.SubArray(FireFlyContractConfigKey) + e.contractConf.AddKnownKey(FireFlyContractAddress) + e.contractConf.AddKnownKey(FireFlyContractFromBlock) fftmConf := config.SubSection(FFTMConfigKey) ffresty.InitConfig(fftmConf) diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 14e8de634..0ef3854a2 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -48,17 +48,17 @@ const ( ) type Ethereum struct { - ctx context.Context - topic string - instancePath string - prefixShort string - prefixLong string - capabilities *blockchain.Capabilities - callbacks blockchain.Callbacks - client *resty.Client - fftmClient *resty.Client - streams *streamManager - initInfo struct { + ctx context.Context + topic string + contractAddress string + prefixShort string + prefixLong string + capabilities *blockchain.Capabilities + callbacks blockchain.Callbacks + client *resty.Client + fftmClient *resty.Client + streams *streamManager + initInfo struct { stream *eventStream sub *subscription } @@ -66,6 +66,7 @@ type Ethereum struct { closed chan struct{} addressResolver *addressResolver metrics metrics.Manager + contractConf config.ArraySection } type eventStreamWebsocket struct { @@ -156,7 +157,8 @@ func (e *Ethereum) VerifierType() core.VerifierType { return core.VerifierTypeEthAddress } -func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager) (err error) { +func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager, contractIndex int) (err error) { + e.InitConfig(config) ethconnectConf := config.SubSection(EthconnectConfigKey) addressResolverConf := config.SubSection(AddressResolverConfigKey) fftmConf := config.SubSection(FFTMConfigKey) @@ -173,7 +175,7 @@ func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks bl } if ethconnectConf.GetString(ffresty.HTTPConfigURL) == "" { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "blockchain.ethconnect") + return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "blockchain.ethereum.ethconnect") } e.client = ffresty.New(e.ctx, ethconnectConf) @@ -182,30 +184,51 @@ func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks bl e.fftmClient = ffresty.New(e.ctx, fftmConf) } - e.instancePath = ethconnectConf.GetString(EthconnectConfigInstancePath) - if e.instancePath == "" { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "instance", "blockchain.ethconnect") + e.streams = &streamManager{client: e.client} + if e.contractConf.ArraySize() > contractIndex { + // New config (array of contracts) + e.contractAddress = e.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractAddress) + if e.contractAddress == "" { + return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "address", "blockchain.fireflyContract") + } + e.streams.fireFlySubscriptionFromBlock = e.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractFromBlock) + } else { + // Old config (attributes under "ethconnect") + e.contractAddress = ethconnectConf.GetString(EthconnectConfigInstanceDeprecated) + if e.contractAddress != "" { + log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead", + EthconnectConfigKey, EthconnectConfigInstanceDeprecated, + FireFlyContractConfigKey, FireFlyContractAddress) + } else { + return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "instance", "blockchain.ethereum.ethconnect") + } + e.streams.fireFlySubscriptionFromBlock = ethconnectConf.GetString(EthconnectConfigFromBlockDeprecated) + if e.streams.fireFlySubscriptionFromBlock != "" { + log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead", + EthconnectConfigKey, EthconnectConfigFromBlockDeprecated, + FireFlyContractConfigKey, FireFlyContractFromBlock) + } } // Backwards compatibility from when instance path was not a contract address - if strings.HasPrefix(strings.ToLower(e.instancePath), "/contracts/") { - address, err := e.getContractAddress(ctx, e.instancePath) + if strings.HasPrefix(strings.ToLower(e.contractAddress), "/contracts/") { + address, err := e.getContractAddress(ctx, e.contractAddress) if err != nil { return err } - e.instancePath = address - } else if strings.HasPrefix(e.instancePath, "/instances/") { - e.instancePath = strings.Replace(e.instancePath, "/instances/", "", 1) + e.contractAddress = address + } else if strings.HasPrefix(e.contractAddress, "/instances/") { + e.contractAddress = strings.Replace(e.contractAddress, "/instances/", "", 1) } // Ethconnect needs the "0x" prefix in some cases - if !strings.HasPrefix(e.instancePath, "0x") { - e.instancePath = fmt.Sprintf("0x%s", e.instancePath) + if !strings.HasPrefix(e.contractAddress, "0x") { + e.contractAddress = fmt.Sprintf("0x%s", e.contractAddress) } e.topic = ethconnectConf.GetString(EthconnectConfigTopic) if e.topic == "" { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "topic", "blockchain.ethconnect") + return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "topic", "blockchain.ethereum.ethconnect") } e.prefixShort = ethconnectConf.GetString(EthconnectPrefixShort) @@ -222,17 +245,13 @@ func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks bl return err } - e.streams = &streamManager{ - client: e.client, - fireFlySubscriptionFromBlock: ethconnectConf.GetString(EthconnectConfigFromBlock), - } batchSize := ethconnectConf.GetUint(EthconnectConfigBatchSize) batchTimeout := uint(ethconnectConf.GetDuration(EthconnectConfigBatchTimeout).Milliseconds()) if e.initInfo.stream, err = e.streams.ensureEventStream(e.ctx, e.topic, batchSize, batchTimeout); err != nil { return err } log.L(e.ctx).Infof("Event stream: %s (topic=%s)", e.initInfo.stream.ID, e.topic) - if e.initInfo.sub, err = e.streams.ensureFireFlySubscription(e.ctx, e.instancePath, e.initInfo.stream.ID, batchPinEventABI); err != nil { + if e.initInfo.sub, err = e.streams.ensureFireFlySubscription(e.ctx, e.contractAddress, e.initInfo.stream.ID, batchPinEventABI); err != nil { return err } @@ -599,7 +618,7 @@ func (e *Ethereum) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID batch.BatchPayloadRef, ethHashes, } - return e.invokeContractMethod(ctx, e.instancePath, signingKey, batchPinMethodABI, operationID.String(), input) + return e.invokeContractMethod(ctx, e.contractAddress, signingKey, batchPinMethodABI, operationID.String(), input) } func (e *Ethereum) InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error { diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index 16746d337..5ca7037c9 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -68,9 +68,8 @@ func testFFIMethod() *core.FFIMethod { } } -func resetConf() { +func resetConf(e *Ethereum) { coreconfig.Reset() - e := &Ethereum{} e.InitConfig(utConfig) } @@ -83,15 +82,15 @@ func newTestEthereum() (*Ethereum, func()) { mm.On("BlockchainTransaction", mock.Anything, mock.Anything).Return(nil) mm.On("BlockchainQuery", mock.Anything, mock.Anything).Return(nil) e := &Ethereum{ - ctx: ctx, - client: resty.New().SetBaseURL("http://localhost:12345"), - instancePath: "/instances/0x12345", - topic: "topic1", - prefixShort: defaultPrefixShort, - prefixLong: defaultPrefixLong, - callbacks: em, - wsconn: wsm, - metrics: mm, + ctx: ctx, + client: resty.New().SetBaseURL("http://localhost:12345"), + contractAddress: "/instances/0x12345", + topic: "topic1", + prefixShort: defaultPrefixShort, + prefixLong: defaultPrefixLong, + callbacks: em, + wsconn: wsm, + metrics: mm, } return e, func() { cancel() @@ -105,39 +104,39 @@ func newTestEthereum() (*Ethereum, func()) { func TestInitMissingURL(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - resetConf() - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + resetConf(e) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10138.*url", err) } func TestInitBadAddressResolver(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - resetConf() + resetConf(e) utAddressResolverConf.Set(AddressResolverURLTemplate, "{{unclosed}") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10337.*urlTemplate", err) } func TestInitMissingInstance(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10138.*instance", err) } func TestInitMissingTopic(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") - utEthconnectConf.Set(EthconnectConfigInstancePath, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10138.*topic", err) } @@ -172,14 +171,14 @@ func TestInitAllNewStreamsAndWSEventWithFFTM(t *testing.T) { return httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sub12345"})(req) }) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") utFFTMConf.Set(ffresty.HTTPConfigURL, "http://fftm.example.com:12345") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.NoError(t, err) assert.NotNil(t, e.fftmClient) @@ -213,12 +212,12 @@ func TestWSInitFail(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "!!!://") - utEthconnectConf.Set(EthconnectConfigInstancePath, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF00149", err) } @@ -254,13 +253,13 @@ func TestInitAllExistingStreams(t *testing.T) { httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345", httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}})) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Equal(t, 3, httpmock.GetTotalCallCount()) assert.Equal(t, "es12345", e.initInfo.stream.ID) @@ -273,7 +272,7 @@ func TestInitAllExistingStreams(t *testing.T) { func TestInitOldInstancePathContracts(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - resetConf() + resetConf(e) mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) @@ -303,21 +302,21 @@ func TestInitOldInstancePathContracts(t *testing.T) { }), ) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "/contracts/firefly") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/contracts/firefly") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.NoError(t, err) - assert.Equal(t, e.instancePath, "0x12345") + assert.Equal(t, e.contractAddress, "0x12345") } func TestInitOldInstancePathInstances(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - resetConf() + resetConf(e) mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) @@ -337,21 +336,21 @@ func TestInitOldInstancePathInstances(t *testing.T) { return httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sub12345"})(req) }) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.NoError(t, err) - assert.Equal(t, e.instancePath, "0x12345") + assert.Equal(t, e.contractAddress, "0x12345") } func TestInitOldInstancePathError(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - resetConf() + resetConf(e) mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) @@ -374,17 +373,70 @@ func TestInitOldInstancePathError(t *testing.T) { httpmock.NewJsonResponderOrPanic(500, "pop"), ) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "/contracts/firefly") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/contracts/firefly") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10111", err) assert.Regexp(t, "pop", err) } +func TestInitNewConfig(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + resetConf(e) + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + func(req *http.Request) (*http.Response, error) { + var body map[string]interface{} + json.NewDecoder(req.Body).Decode(&body) + assert.Equal(t, "es12345", body["stream"]) + return httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sub12345"})(req) + }) + + resetConf(e) + utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x12345") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + assert.NoError(t, err) + assert.Equal(t, e.contractAddress, "0x12345") +} + +func TestInitNewConfigError(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + resetConf(e) + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + resetConf(e) + utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + assert.Regexp(t, "FF10138", err) +} + func TestStreamQueryError(t *testing.T) { e, cancel := newTestEthereum() @@ -397,14 +449,14 @@ func TestStreamQueryError(t *testing.T) { httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", httpmock.NewStringResponder(500, `pop`)) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10111", err) assert.Regexp(t, "pop", err) @@ -425,14 +477,14 @@ func TestStreamCreateError(t *testing.T) { httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", httpmock.NewStringResponder(500, `pop`)) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10111", err) assert.Regexp(t, "pop", err) @@ -453,14 +505,14 @@ func TestStreamUpdateError(t *testing.T) { httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345", httpmock.NewStringResponder(500, `pop`)) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10111", err) assert.Regexp(t, "pop", err) @@ -483,14 +535,14 @@ func TestSubQueryError(t *testing.T) { httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", httpmock.NewStringResponder(500, `pop`)) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10111", err) assert.Regexp(t, "pop", err) @@ -515,14 +567,14 @@ func TestSubQueryCreateError(t *testing.T) { httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", httpmock.NewStringResponder(500, `pop`)) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10111", err) assert.Regexp(t, "pop", err) @@ -1858,10 +1910,10 @@ func TestGetContractAddressBadJSON(t *testing.T) { httpmock.NewBytesResponder(200, []byte("{not json!")), ) - resetConf() + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstancePath, "0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") e.client = ffresty.New(e.ctx, utEthconnectConf) diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index 910849926..fa744b72c 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -160,7 +160,7 @@ func (f *Fabric) VerifierType() core.VerifierType { return core.VerifierTypeMSPIdentity } -func (f *Fabric) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager) (err error) { +func (f *Fabric) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager, contractIndex int) (err error) { fabconnectConf := config.SubSection(FabconnectConfigKey) f.ctx = log.WithLogField(ctx, "proto", "fabric") diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index dfe1ff753..ec43615e2 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -106,7 +106,7 @@ func TestInitMissingURL(t *testing.T) { e, cancel := newTestFabric() defer cancel() resetConf() - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10138.*url", err) } @@ -117,7 +117,7 @@ func TestInitMissingChaincode(t *testing.T) { utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10138.*chaincode", err) } @@ -129,7 +129,7 @@ func TestInitMissingTopic(t *testing.T) { utFabconnectConf.Set(FabconnectConfigChaincode, "Firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10138.*topic", err) } @@ -172,7 +172,7 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.NoError(t, err) assert.Equal(t, "fabric", e.Name()) @@ -211,7 +211,7 @@ func TestWSInitFail(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF00149", err) } @@ -252,7 +252,7 @@ func TestInitAllExistingStreams(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Equal(t, 2, httpmock.GetTotalCallCount()) assert.Equal(t, "es12345", e.initInfo.stream.ID) @@ -282,7 +282,7 @@ func TestStreamQueryError(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10284", err) assert.Regexp(t, "pop", err) @@ -311,7 +311,7 @@ func TestStreamCreateError(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10284", err) assert.Regexp(t, "pop", err) @@ -342,7 +342,7 @@ func TestSubQueryError(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10284", err) assert.Regexp(t, "pop", err) @@ -375,7 +375,7 @@ func TestSubQueryCreateError(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) assert.Regexp(t, "FF10284", err) assert.Regexp(t, "pop", err) diff --git a/internal/coremsgs/en_config_descriptions.go b/internal/coremsgs/en_config_descriptions.go index 7550426a1..c02e1d5e4 100644 --- a/internal/coremsgs/en_config_descriptions.go +++ b/internal/coremsgs/en_config_descriptions.go @@ -70,18 +70,20 @@ var ( ConfigBlockchainEthereumEthconnectBatchSize = ffc("config.blockchain.ethereum.ethconnect.batchSize", "The number of events Ethconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType) ConfigBlockchainEthereumEthconnectBatchTimeout = ffc("config.blockchain.ethereum.ethconnect.batchTimeout", "How long Ethconnect should wait for new events to arrive and fill a batch, before sending the batch to FireFly core. Only applies when automatically creating a new event stream", i18n.TimeDurationType) - ConfigBlockchainEthereumEthconnectInstance = ffc("config.blockchain.ethereum.ethconnect.instance", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain", "Address "+i18n.StringType) - ConfigBlockchainEthereumEthconnectFromBlock = ffc("config.blockchain.ethereum.ethconnect.fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType) + ConfigBlockchainEthereumEthconnectInstance = ffc("config.blockchain.ethereum.ethconnect.instance", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain (deprecated - use fireflyContract[].address)", "Address "+i18n.StringType) + ConfigBlockchainEthereumEthconnectFromBlock = ffc("config.blockchain.ethereum.ethconnect.fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream (deprecated - use fireflyContract[].fromBlock)", "Address "+i18n.StringType) ConfigBlockchainEthereumEthconnectPrefixLong = ffc("config.blockchain.ethereum.ethconnect.prefixLong", "The prefix that will be used for Ethconnect specific HTTP headers when FireFly makes requests to Ethconnect", i18n.StringType) ConfigBlockchainEthereumEthconnectPrefixShort = ffc("config.blockchain.ethereum.ethconnect.prefixShort", "The prefix that will be used for Ethconnect specific query parameters when FireFly makes requests to Ethconnect", i18n.StringType) ConfigBlockchainEthereumEthconnectTopic = ffc("config.blockchain.ethereum.ethconnect.topic", "The websocket listen topic that the node should register on, which is important if there are multiple nodes using a single ethconnect", i18n.StringType) ConfigBlockchainEthereumEthconnectURL = ffc("config.blockchain.ethereum.ethconnect.url", "The URL of the Ethconnect instance", "URL "+i18n.StringType) + ConfigBlockchainEthereumEthconnectProxyURL = ffc("config.blockchain.ethereum.ethconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Ethconnect", "URL "+i18n.StringType) + + ConfigBlockchainEthereumContractAddress = ffc("config.blockchain.ethereum.fireflyContract[].address", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain", "Address "+i18n.StringType) + ConfigBlockchainEthereumContractFromBlock = ffc("config.blockchain.ethereum.fireflyContract[].fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType) ConfigBlockchainEthereumFFTMURL = ffc("config.blockchain.ethereum.fftm.url", "The URL of the FireFly Transaction Manager runtime, if enabled", i18n.StringType) ConfigBlockchainEthereumFFTMProxyURL = ffc("config.blockchain.ethereum.fftm.proxy.url", "Optional HTTP proxy server to use when connecting to the Transaction Manager", i18n.StringType) - ConfigBlockchainEthereumEthconnectProxyURL = ffc("config.blockchain.ethereum.ethconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Ethconnect", "URL "+i18n.StringType) - ConfigBlockchainFabricFabconnectBatchSize = ffc("config.blockchain.fabric.fabconnect.batchSize", "The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType) ConfigBlockchainFabricFabconnectBatchTimeout = ffc("config.blockchain.fabric.fabconnect.batchTimeout", "The maximum amount of time to wait for a batch to complete", i18n.TimeDurationType) ConfigBlockchainFabricFabconnectChaincode = ffc("config.blockchain.fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions", i18n.StringType) @@ -137,12 +139,14 @@ var ( ConfigPluginBlockchainEthereumEthconnectPrefixShort = ffc("config.plugins.blockchain[].ethereum.ethconnect.prefixShort", "The prefix that will be used for Ethconnect specific query parameters when FireFly makes requests to Ethconnect", i18n.StringType) ConfigPluginBlockchainEthereumEthconnectTopic = ffc("config.plugins.blockchain[].ethereum.ethconnect.topic", "The websocket listen topic that the node should register on, which is important if there are multiple nodes using a single ethconnect", i18n.StringType) ConfigPluginBlockchainEthereumEthconnectURL = ffc("config.plugins.blockchain[].ethereum.ethconnect.url", "The URL of the Ethconnect instance", "URL "+i18n.StringType) + ConfigPluginBlockchainEthereumEthconnectProxyURL = ffc("config.plugins.blockchain[].ethereum.ethconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Ethconnect", "URL "+i18n.StringType) + + ConfigPluginsBlockchainEthereumContractAddress = ffc("config.plugins.blockchain[].ethereum.fireflyContract[].address", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain", "Address "+i18n.StringType) + ConfigPluginsBlockchainEthereumContractFromBlock = ffc("config.plugins.blockchain[].ethereum.fireflyContract[].fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType) ConfigPluginBlockchainEthereumFFTMURL = ffc("config.plugins.blockchain[].ethereum.fftm.url", "The URL of the FireFly Transaction Manager runtime, if enabled", i18n.StringType) ConfigPluginBlockchainEthereumFFTMProxyURL = ffc("config.plugins.blockchain[].ethereum.fftm.proxy.url", "Optional HTTP proxy server to use when connecting to the Transaction Manager", i18n.StringType) - ConfigPluginBlockchainEthereumEthconnectProxyURL = ffc("config.plugins.blockchain[].ethereum.ethconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Ethconnect", "URL "+i18n.StringType) - ConfigPluginBlockchainFabricFabconnectBatchSize = ffc("config.plugins.blockchain[].fabric.fabconnect.batchSize", "The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType) ConfigPluginBlockchainFabricFabconnectBatchTimeout = ffc("config.plugins.blockchain[].fabric.fabconnect.batchTimeout", "The maximum amount of time to wait for a batch to complete", i18n.TimeDurationType) ConfigPluginBlockchainFabricFabconnectChaincode = ffc("config.plugins.blockchain[].fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions", i18n.StringType) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index be19fc3a8..5101b77ff 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -660,7 +660,7 @@ func (or *orchestrator) getBlockchainPlugins(ctx context.Context) (plugins []blo func (or *orchestrator) initDeprecatedBlockchainPlugin(ctx context.Context, plugin blockchain.Plugin) (err error) { log.L(ctx).Warnf("Your blockchain config uses a deprecated configuration structure - the blockchain configuration has been moved under the 'plugins' section") - err = plugin.Init(ctx, deprecatedBlockchainConfig.SubSection(plugin.Name()), &or.bc, or.metrics) + err = plugin.Init(ctx, deprecatedBlockchainConfig.SubSection(plugin.Name()), &or.bc, or.metrics, 0) if err != nil { return err } @@ -687,7 +687,7 @@ func (or *orchestrator) initDeprecatedDatabasePlugin(ctx context.Context, plugin func (or *orchestrator) initBlockchainPlugins(ctx context.Context, plugins []blockchain.Plugin) (err error) { for idx, plugin := range plugins { config := blockchainConfig.ArrayEntry(idx) - err = plugin.Init(ctx, config, &or.bc, or.metrics) + err = plugin.Init(ctx, config, &or.bc, or.metrics, 0) if err != nil { return err } diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index a4a2a5bdf..650642805 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -472,7 +472,7 @@ func TestBlockchainInitPlugins(t *testing.T) { blockchainConfig.AddKnownKey(coreconfig.PluginConfigType, "ethereum") plugins := make([]blockchain.Plugin, 1) mbp := &blockchainmocks.Plugin{} - mbp.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + mbp.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything, 0).Return(nil) plugins[0] = mbp ctx := context.Background() err := or.initBlockchainPlugins(ctx, plugins) @@ -484,7 +484,7 @@ func TestDeprecatedBlockchainInitPlugin(t *testing.T) { defer or.cleanup(t) bifactory.InitConfigDeprecated(deprecatedBlockchainConfig) deprecatedBlockchainConfig.AddKnownKey(coreconfig.PluginConfigType, "ethereum") - or.mbi.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + or.mbi.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything, 0).Return(nil) ctx := context.Background() err := or.initDeprecatedBlockchainPlugin(ctx, or.mbi) assert.NoError(t, err) diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go index efc2270f5..dd68f0024 100644 --- a/mocks/blockchainmocks/plugin.go +++ b/mocks/blockchainmocks/plugin.go @@ -126,13 +126,13 @@ func (_m *Plugin) GetFFIParamValidator(ctx context.Context) (core.FFIParamValida return r0, r1 } -// Init provides a mock function with given fields: ctx, _a1, callbacks, _a3 -func (_m *Plugin) Init(ctx context.Context, _a1 config.Section, callbacks blockchain.Callbacks, _a3 metrics.Manager) error { - ret := _m.Called(ctx, _a1, callbacks, _a3) +// Init provides a mock function with given fields: ctx, _a1, callbacks, _a3, contractIndex +func (_m *Plugin) Init(ctx context.Context, _a1 config.Section, callbacks blockchain.Callbacks, _a3 metrics.Manager, contractIndex int) error { + ret := _m.Called(ctx, _a1, callbacks, _a3, contractIndex) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, config.Section, blockchain.Callbacks, metrics.Manager) error); ok { - r0 = rf(ctx, _a1, callbacks, _a3) + if rf, ok := ret.Get(0).(func(context.Context, config.Section, blockchain.Callbacks, metrics.Manager, int) error); ok { + r0 = rf(ctx, _a1, callbacks, _a3, contractIndex) } else { r0 = ret.Error(0) } diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index 7a203a8b0..a25ac62c5 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -33,8 +33,7 @@ type Plugin interface { InitConfig(config config.Section) // Init initializes the plugin, with configuration - // Returns the supported featureset of the interface - Init(ctx context.Context, config config.Section, callbacks Callbacks, metrics metrics.Manager) error + Init(ctx context.Context, config config.Section, callbacks Callbacks, metrics metrics.Manager, contractIndex int) error // Blockchain interface must not deliver any events until start is called Start() error From 2ddb05dcc8e03b41d24fa27d32a77a18d4e58e2b Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Fri, 20 May 2022 16:21:47 -0400 Subject: [PATCH 02/13] Split Ethereum websocket init from other init Signed-off-by: Andrew Richardson --- internal/blockchain/ethereum/config.go | 20 +-- internal/blockchain/ethereum/ethereum.go | 60 +++++---- internal/blockchain/ethereum/ethereum_test.go | 126 ++++++++++++------ internal/blockchain/ethereum/eventstream.go | 3 +- internal/blockchain/fabric/fabric.go | 8 +- internal/blockchain/fabric/fabric_test.go | 24 ++-- internal/coremsgs/en_error_messages.go | 1 + internal/orchestrator/orchestrator.go | 6 +- internal/orchestrator/orchestrator_test.go | 10 +- mocks/blockchainmocks/plugin.go | 25 ++-- pkg/blockchain/plugin.go | 7 +- 11 files changed, 176 insertions(+), 114 deletions(-) diff --git a/internal/blockchain/ethereum/config.go b/internal/blockchain/ethereum/config.go index 41cdc91e9..8d8ad8f73 100644 --- a/internal/blockchain/ethereum/config.go +++ b/internal/blockchain/ethereum/config.go @@ -83,19 +83,19 @@ const ( ) func (e *Ethereum) InitConfig(config config.Section) { - ethconnectConf := config.SubSection(EthconnectConfigKey) - wsclient.InitConfig(ethconnectConf) - ethconnectConf.AddKnownKey(EthconnectConfigTopic) - ethconnectConf.AddKnownKey(EthconnectConfigBatchSize, defaultBatchSize) - ethconnectConf.AddKnownKey(EthconnectConfigBatchTimeout, defaultBatchTimeout) - ethconnectConf.AddKnownKey(EthconnectPrefixShort, defaultPrefixShort) - ethconnectConf.AddKnownKey(EthconnectPrefixLong, defaultPrefixLong) - ethconnectConf.AddKnownKey(EthconnectConfigInstanceDeprecated) - ethconnectConf.AddKnownKey(EthconnectConfigFromBlockDeprecated, defaultFromBlock) + e.ethconnectConf = config.SubSection(EthconnectConfigKey) + wsclient.InitConfig(e.ethconnectConf) + e.ethconnectConf.AddKnownKey(EthconnectConfigTopic) + e.ethconnectConf.AddKnownKey(EthconnectConfigBatchSize, defaultBatchSize) + e.ethconnectConf.AddKnownKey(EthconnectConfigBatchTimeout, defaultBatchTimeout) + e.ethconnectConf.AddKnownKey(EthconnectPrefixShort, defaultPrefixShort) + e.ethconnectConf.AddKnownKey(EthconnectPrefixLong, defaultPrefixLong) + e.ethconnectConf.AddKnownKey(EthconnectConfigInstanceDeprecated) + e.ethconnectConf.AddKnownKey(EthconnectConfigFromBlockDeprecated, defaultFromBlock) e.contractConf = config.SubArray(FireFlyContractConfigKey) e.contractConf.AddKnownKey(FireFlyContractAddress) - e.contractConf.AddKnownKey(FireFlyContractFromBlock) + e.contractConf.AddKnownKey(FireFlyContractFromBlock, "oldest") fftmConf := config.SubSection(FFTMConfigKey) ffresty.InitConfig(fftmConf) diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 0ef3854a2..cd06b27f0 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -66,6 +66,7 @@ type Ethereum struct { closed chan struct{} addressResolver *addressResolver metrics metrics.Manager + ethconnectConf config.Section contractConf config.ArraySection } @@ -157,9 +158,8 @@ func (e *Ethereum) VerifierType() core.VerifierType { return core.VerifierTypeEthAddress } -func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager, contractIndex int) (err error) { +func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager) (err error) { e.InitConfig(config) - ethconnectConf := config.SubSection(EthconnectConfigKey) addressResolverConf := config.SubSection(AddressResolverConfigKey) fftmConf := config.SubSection(FFTMConfigKey) @@ -174,27 +174,42 @@ func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks bl } } - if ethconnectConf.GetString(ffresty.HTTPConfigURL) == "" { + if e.ethconnectConf.GetString(ffresty.HTTPConfigURL) == "" { return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "blockchain.ethereum.ethconnect") } - e.client = ffresty.New(e.ctx, ethconnectConf) - + e.client = ffresty.New(e.ctx, e.ethconnectConf) if fftmConf.GetString(ffresty.HTTPConfigURL) != "" { e.fftmClient = ffresty.New(e.ctx, fftmConf) } + e.topic = e.ethconnectConf.GetString(EthconnectConfigTopic) + if e.topic == "" { + return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "topic", "blockchain.ethereum.ethconnect") + } + e.prefixShort = e.ethconnectConf.GetString(EthconnectPrefixShort) + e.prefixLong = e.ethconnectConf.GetString(EthconnectPrefixLong) + + return nil +} + +func (e *Ethereum) initStreams(contractIndex int) (err error) { + + ctx := e.ctx e.streams = &streamManager{client: e.client} - if e.contractConf.ArraySize() > contractIndex { - // New config (array of contracts) + if e.contractConf.ArraySize() > 0 || contractIndex > 0 { + // New config (array of objects under "fireflyContract") + if contractIndex >= e.contractConf.ArraySize() { + return i18n.NewError(ctx, coremsgs.MsgInvalidFireFlyContractIndex, fmt.Sprintf("blockchain.ethereum.fireflyContract[%d]", contractIndex)) + } e.contractAddress = e.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractAddress) if e.contractAddress == "" { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "address", "blockchain.fireflyContract") + return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "address", "blockchain.ethereum.fireflyContract") } e.streams.fireFlySubscriptionFromBlock = e.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractFromBlock) } else { // Old config (attributes under "ethconnect") - e.contractAddress = ethconnectConf.GetString(EthconnectConfigInstanceDeprecated) + e.contractAddress = e.ethconnectConf.GetString(EthconnectConfigInstanceDeprecated) if e.contractAddress != "" { log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead", EthconnectConfigKey, EthconnectConfigInstanceDeprecated, @@ -202,7 +217,7 @@ func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks bl } else { return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "instance", "blockchain.ethereum.ethconnect") } - e.streams.fireFlySubscriptionFromBlock = ethconnectConf.GetString(EthconnectConfigFromBlockDeprecated) + e.streams.fireFlySubscriptionFromBlock = e.ethconnectConf.GetString(EthconnectConfigFromBlockDeprecated) if e.streams.fireFlySubscriptionFromBlock != "" { log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead", EthconnectConfigKey, EthconnectConfigFromBlockDeprecated, @@ -226,27 +241,17 @@ func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks bl e.contractAddress = fmt.Sprintf("0x%s", e.contractAddress) } - e.topic = ethconnectConf.GetString(EthconnectConfigTopic) - if e.topic == "" { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "topic", "blockchain.ethereum.ethconnect") - } - - e.prefixShort = ethconnectConf.GetString(EthconnectPrefixShort) - e.prefixLong = ethconnectConf.GetString(EthconnectPrefixLong) - - wsConfig := wsclient.GenerateConfig(ethconnectConf) - + wsConfig := wsclient.GenerateConfig(e.ethconnectConf) if wsConfig.WSKeyPath == "" { wsConfig.WSKeyPath = "/ws" } - e.wsconn, err = wsclient.New(ctx, wsConfig, nil, e.afterConnect) if err != nil { return err } - batchSize := ethconnectConf.GetUint(EthconnectConfigBatchSize) - batchTimeout := uint(ethconnectConf.GetDuration(EthconnectConfigBatchTimeout).Milliseconds()) + batchSize := e.ethconnectConf.GetUint(EthconnectConfigBatchSize) + batchTimeout := uint(e.ethconnectConf.GetDuration(EthconnectConfigBatchTimeout).Milliseconds()) if e.initInfo.stream, err = e.streams.ensureEventStream(e.ctx, e.topic, batchSize, batchTimeout); err != nil { return err } @@ -261,10 +266,17 @@ func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks bl return nil } -func (e *Ethereum) Start() error { +func (e *Ethereum) Start(contractIndex int) (err error) { + if err = e.initStreams(contractIndex); err != nil { + return err + } return e.wsconn.Connect() } +func (e *Ethereum) Stop() { + e.wsconn.Close() +} + func (e *Ethereum) Capabilities() *blockchain.Capabilities { return e.capabilities } diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index 5ca7037c9..4a138fea3 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -105,7 +105,7 @@ func TestInitMissingURL(t *testing.T) { e, cancel := newTestEthereum() defer cancel() resetConf(e) - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10138.*url", err) } @@ -114,7 +114,7 @@ func TestInitBadAddressResolver(t *testing.T) { defer cancel() resetConf(e) utAddressResolverConf.Set(AddressResolverURLTemplate, "{{unclosed}") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10337.*urlTemplate", err) } @@ -125,7 +125,9 @@ func TestInitMissingInstance(t *testing.T) { utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) assert.Regexp(t, "FF10138.*instance", err) } @@ -136,11 +138,11 @@ func TestInitMissingTopic(t *testing.T) { utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10138.*topic", err) } -func TestInitAllNewStreamsAndWSEventWithFFTM(t *testing.T) { +func TestInitAndStartWithFFTM(t *testing.T) { log.SetLevel("trace") e, cancel := newTestEthereum() @@ -178,20 +180,21 @@ func TestInitAllNewStreamsAndWSEventWithFFTM(t *testing.T) { utEthconnectConf.Set(EthconnectConfigTopic, "topic1") utFFTMConf.Set(ffresty.HTTPConfigURL, "http://fftm.example.com:12345") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) assert.NotNil(t, e.fftmClient) assert.Equal(t, "ethereum", e.Name()) assert.Equal(t, core.VerifierTypeEthAddress, e.VerifierType()) + + err = e.Start(0) + assert.NoError(t, err) + assert.Equal(t, 4, httpmock.GetTotalCallCount()) assert.Equal(t, "es12345", e.initInfo.stream.ID) assert.Equal(t, "sub12345", e.initInfo.sub.ID) assert.NotNil(t, e.Capabilities()) - err = e.Start() - assert.NoError(t, err) - startupMessage := <-toServer assert.Equal(t, `{"type":"listen","topic":"topic1"}`, startupMessage) startupMessage = <-toServer @@ -217,22 +220,23 @@ func TestWSInitFail(t *testing.T) { utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.Start(0) assert.Regexp(t, "FF00149", err) } -func TestWSConnectFail(t *testing.T) { +func TestWSClose(t *testing.T) { wsm := &wsmocks.WSClient{} e := &Ethereum{ ctx: context.Background(), wsconn: wsm, } - wsm.On("Connect").Return(fmt.Errorf("pop")) + wsm.On("Close").Return(nil) + e.Stop() - err := e.Start() - assert.EqualError(t, err, "pop") } func TestInitAllExistingStreams(t *testing.T) { @@ -259,14 +263,15 @@ func TestInitAllExistingStreams(t *testing.T) { utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) + assert.NoError(t, err) assert.Equal(t, 3, httpmock.GetTotalCallCount()) assert.Equal(t, "es12345", e.initInfo.stream.ID) assert.Equal(t, "sub12345", e.initInfo.sub.ID) - assert.NoError(t, err) - } func TestInitOldInstancePathContracts(t *testing.T) { @@ -308,8 +313,11 @@ func TestInitOldInstancePathContracts(t *testing.T) { utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/contracts/firefly") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) assert.NoError(t, err) + assert.Equal(t, e.contractAddress, "0x12345") } @@ -342,8 +350,11 @@ func TestInitOldInstancePathInstances(t *testing.T) { utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) assert.NoError(t, err) + assert.Equal(t, e.contractAddress, "0x12345") } @@ -379,9 +390,10 @@ func TestInitOldInstancePathError(t *testing.T) { utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/contracts/firefly") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) - assert.Regexp(t, "FF10111", err) - assert.Regexp(t, "pop", err) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10111.*pop", err) } func TestInitNewConfig(t *testing.T) { @@ -413,8 +425,11 @@ func TestInitNewConfig(t *testing.T) { utEthconnectConf.Set(EthconnectConfigTopic, "topic1") utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x12345") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) assert.NoError(t, err) + assert.Equal(t, e.contractAddress, "0x12345") } @@ -433,10 +448,33 @@ func TestInitNewConfigError(t *testing.T) { utEthconnectConf.Set(EthconnectConfigTopic, "topic1") utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) assert.Regexp(t, "FF10138", err) } +func TestInitNewConfigBadIndex(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + resetConf(e) + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + resetConf(e) + utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(1) + assert.Regexp(t, "FF10387", err) +} + func TestStreamQueryError(t *testing.T) { e, cancel := newTestEthereum() @@ -456,10 +494,10 @@ func TestStreamQueryError(t *testing.T) { utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) - - assert.Regexp(t, "FF10111", err) - assert.Regexp(t, "pop", err) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10111.*pop", err) } @@ -484,10 +522,10 @@ func TestStreamCreateError(t *testing.T) { utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) - - assert.Regexp(t, "FF10111", err) - assert.Regexp(t, "pop", err) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10111.*pop", err) } @@ -512,10 +550,10 @@ func TestStreamUpdateError(t *testing.T) { utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) - - assert.Regexp(t, "FF10111", err) - assert.Regexp(t, "pop", err) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10111.*pop", err) } @@ -542,10 +580,10 @@ func TestSubQueryError(t *testing.T) { utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) - - assert.Regexp(t, "FF10111", err) - assert.Regexp(t, "pop", err) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10111.*pop", err) } @@ -574,10 +612,10 @@ func TestSubQueryCreateError(t *testing.T) { utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) - - assert.Regexp(t, "FF10111", err) - assert.Regexp(t, "pop", err) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10111.*pop", err) } diff --git a/internal/blockchain/ethereum/eventstream.go b/internal/blockchain/ethereum/eventstream.go index 6e26a45da..1a574bde2 100644 --- a/internal/blockchain/ethereum/eventstream.go +++ b/internal/blockchain/ethereum/eventstream.go @@ -30,8 +30,7 @@ import ( ) type streamManager struct { - client *resty.Client - + client *resty.Client fireFlySubscriptionFromBlock string } diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index fa744b72c..afca24d91 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -160,7 +160,7 @@ func (f *Fabric) VerifierType() core.VerifierType { return core.VerifierTypeMSPIdentity } -func (f *Fabric) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager, contractIndex int) (err error) { +func (f *Fabric) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager) (err error) { fabconnectConf := config.SubSection(FabconnectConfigKey) f.ctx = log.WithLogField(ctx, "proto", "fabric") @@ -224,10 +224,14 @@ func (f *Fabric) Init(ctx context.Context, config config.Section, callbacks bloc return nil } -func (f *Fabric) Start() error { +func (f *Fabric) Start(contractIndex int) error { return f.wsconn.Connect() } +func (f *Fabric) Stop() { + f.wsconn.Close() +} + func (f *Fabric) Capabilities() *blockchain.Capabilities { return f.capabilities } diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index ec43615e2..2ee6882b9 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -106,7 +106,7 @@ func TestInitMissingURL(t *testing.T) { e, cancel := newTestFabric() defer cancel() resetConf() - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10138.*url", err) } @@ -117,7 +117,7 @@ func TestInitMissingChaincode(t *testing.T) { utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10138.*chaincode", err) } @@ -129,7 +129,7 @@ func TestInitMissingTopic(t *testing.T) { utFabconnectConf.Set(FabconnectConfigChaincode, "Firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10138.*topic", err) } @@ -172,7 +172,7 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) assert.Equal(t, "fabric", e.Name()) @@ -182,7 +182,7 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) { assert.Equal(t, "sub12345", e.initInfo.sub.ID) assert.NotNil(t, e.Capabilities()) - err = e.Start() + err = e.Start(0) assert.NoError(t, err) startupMessage := <-toServer @@ -211,7 +211,7 @@ func TestWSInitFail(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF00149", err) } @@ -225,7 +225,7 @@ func TestWSConnectFail(t *testing.T) { } wsm.On("Connect").Return(fmt.Errorf("pop")) - err := e.Start() + err := e.Start(0) assert.EqualError(t, err, "pop") } @@ -252,7 +252,7 @@ func TestInitAllExistingStreams(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Equal(t, 2, httpmock.GetTotalCallCount()) assert.Equal(t, "es12345", e.initInfo.stream.ID) @@ -282,7 +282,7 @@ func TestStreamQueryError(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10284", err) assert.Regexp(t, "pop", err) @@ -311,7 +311,7 @@ func TestStreamCreateError(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10284", err) assert.Regexp(t, "pop", err) @@ -342,7 +342,7 @@ func TestSubQueryError(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10284", err) assert.Regexp(t, "pop", err) @@ -375,7 +375,7 @@ func TestSubQueryCreateError(t *testing.T) { utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}, 0) + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10284", err) assert.Regexp(t, "pop", err) diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go index 752fe41f9..48ddba891 100644 --- a/internal/coremsgs/en_error_messages.go +++ b/internal/coremsgs/en_error_messages.go @@ -233,4 +233,5 @@ var ( MsgRouteDescriptionMissing = ffe("FF10384", "API route description missing for route '%s'") MsgInvalidOutputOption = ffe("FF10385", "invalid output option '%s'") MsgInvalidPluginConfiguration = ffe("FF10386", "Invalid %s plugin configuration - name and type are required") + MsgInvalidFireFlyContractIndex = ffe("FF10387", "No configuration found for FireFly contract at %s") ) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 5101b77ff..960cd87d2 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -223,7 +223,7 @@ func (or *orchestrator) Start() (err error) { } if err == nil { for _, el := range or.blockchains { - if err = el.Start(); err != nil { + if err = el.Start(0); err != nil { break } } @@ -660,7 +660,7 @@ func (or *orchestrator) getBlockchainPlugins(ctx context.Context) (plugins []blo func (or *orchestrator) initDeprecatedBlockchainPlugin(ctx context.Context, plugin blockchain.Plugin) (err error) { log.L(ctx).Warnf("Your blockchain config uses a deprecated configuration structure - the blockchain configuration has been moved under the 'plugins' section") - err = plugin.Init(ctx, deprecatedBlockchainConfig.SubSection(plugin.Name()), &or.bc, or.metrics, 0) + err = plugin.Init(ctx, deprecatedBlockchainConfig.SubSection(plugin.Name()), &or.bc, or.metrics) if err != nil { return err } @@ -687,7 +687,7 @@ func (or *orchestrator) initDeprecatedDatabasePlugin(ctx context.Context, plugin func (or *orchestrator) initBlockchainPlugins(ctx context.Context, plugins []blockchain.Plugin) (err error) { for idx, plugin := range plugins { config := blockchainConfig.ArrayEntry(idx) - err = plugin.Init(ctx, config, &or.bc, or.metrics, 0) + err = plugin.Init(ctx, config, &or.bc, or.metrics) if err != nil { return err } diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 650642805..2c64d6808 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -472,7 +472,7 @@ func TestBlockchainInitPlugins(t *testing.T) { blockchainConfig.AddKnownKey(coreconfig.PluginConfigType, "ethereum") plugins := make([]blockchain.Plugin, 1) mbp := &blockchainmocks.Plugin{} - mbp.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything, 0).Return(nil) + mbp.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) plugins[0] = mbp ctx := context.Background() err := or.initBlockchainPlugins(ctx, plugins) @@ -484,7 +484,7 @@ func TestDeprecatedBlockchainInitPlugin(t *testing.T) { defer or.cleanup(t) bifactory.InitConfigDeprecated(deprecatedBlockchainConfig) deprecatedBlockchainConfig.AddKnownKey(coreconfig.PluginConfigType, "ethereum") - or.mbi.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything, 0).Return(nil) + or.mbi.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) ctx := context.Background() err := or.initDeprecatedBlockchainPlugin(ctx, or.mbi) assert.NoError(t, err) @@ -978,7 +978,7 @@ func TestStartTokensFail(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) - or.mbi.On("Start").Return(nil) + or.mbi.On("Start", 0).Return(nil) or.mba.On("Start").Return(nil) or.mem.On("Start").Return(nil) or.mbm.On("Start").Return(nil) @@ -994,7 +994,7 @@ func TestStartBlockchainsFail(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) - or.mbi.On("Start").Return(fmt.Errorf("pop")) + or.mbi.On("Start", 0).Return(fmt.Errorf("pop")) or.mba.On("Start").Return(nil) err := or.Start() assert.EqualError(t, err, "pop") @@ -1004,7 +1004,7 @@ func TestStartStopOk(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) - or.mbi.On("Start").Return(nil) + or.mbi.On("Start", 0).Return(nil) or.mba.On("Start").Return(nil) or.mem.On("Start").Return(nil) or.mbm.On("Start").Return(nil) diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go index dd68f0024..d0c1ab56b 100644 --- a/mocks/blockchainmocks/plugin.go +++ b/mocks/blockchainmocks/plugin.go @@ -126,13 +126,13 @@ func (_m *Plugin) GetFFIParamValidator(ctx context.Context) (core.FFIParamValida return r0, r1 } -// Init provides a mock function with given fields: ctx, _a1, callbacks, _a3, contractIndex -func (_m *Plugin) Init(ctx context.Context, _a1 config.Section, callbacks blockchain.Callbacks, _a3 metrics.Manager, contractIndex int) error { - ret := _m.Called(ctx, _a1, callbacks, _a3, contractIndex) +// Init provides a mock function with given fields: ctx, _a1, callbacks, _a3 +func (_m *Plugin) Init(ctx context.Context, _a1 config.Section, callbacks blockchain.Callbacks, _a3 metrics.Manager) error { + ret := _m.Called(ctx, _a1, callbacks, _a3) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, config.Section, blockchain.Callbacks, metrics.Manager, int) error); ok { - r0 = rf(ctx, _a1, callbacks, _a3, contractIndex) + if rf, ok := ret.Get(0).(func(context.Context, config.Section, blockchain.Callbacks, metrics.Manager) error); ok { + r0 = rf(ctx, _a1, callbacks, _a3) } else { r0 = ret.Error(0) } @@ -240,13 +240,13 @@ func (_m *Plugin) QueryContract(ctx context.Context, location *fftypes.JSONAny, return r0, r1 } -// Start provides a mock function with given fields: -func (_m *Plugin) Start() error { - ret := _m.Called() +// Start provides a mock function with given fields: contractIndex +func (_m *Plugin) Start(contractIndex int) error { + ret := _m.Called(contractIndex) var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() + if rf, ok := ret.Get(0).(func(int) error); ok { + r0 = rf(contractIndex) } else { r0 = ret.Error(0) } @@ -254,6 +254,11 @@ func (_m *Plugin) Start() error { return r0 } +// Stop provides a mock function with given fields: +func (_m *Plugin) Stop() { + _m.Called() +} + // SubmitBatchPin provides a mock function with given fields: ctx, operationID, signingKey, batch func (_m *Plugin) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, signingKey string, batch *blockchain.BatchPin) error { ret := _m.Called(ctx, operationID, signingKey, batch) diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index a25ac62c5..b8fd02bea 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -33,10 +33,13 @@ type Plugin interface { InitConfig(config config.Section) // Init initializes the plugin, with configuration - Init(ctx context.Context, config config.Section, callbacks Callbacks, metrics metrics.Manager, contractIndex int) error + Init(ctx context.Context, config config.Section, callbacks Callbacks, metrics metrics.Manager) error // Blockchain interface must not deliver any events until start is called - Start() error + Start(contractIndex int) error + + // Stop listening for events until start is called again + Stop() // Capabilities returns capabilities - not called until after Init Capabilities() *Capabilities From 26fdac40651fe7796763762d17703e6f801b0a43 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Fri, 20 May 2022 16:34:36 -0400 Subject: [PATCH 03/13] Add new Fabric contract config Signed-off-by: Andrew Richardson --- docs/reference/config.md | 18 ++- internal/blockchain/fabric/config.go | 35 ++-- internal/blockchain/fabric/eventstream.go | 7 +- internal/blockchain/fabric/fabric.go | 76 ++++++--- internal/blockchain/fabric/fabric_test.go | 168 +++++++++++++++----- internal/coremsgs/en_config_descriptions.go | 16 +- 6 files changed, 228 insertions(+), 92 deletions(-) diff --git a/docs/reference/config.md b/docs/reference/config.md index c3e0ff0bd..5e2de70e6 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -255,7 +255,7 @@ nav_order: 3 |---|-----------|----|-------------| |batchSize|The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream|`int`|`50` |batchTimeout|The maximum amount of time to wait for a batch to complete|[`time.Duration`](https://pkg.go.dev/time#Duration)|`500` -|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions|`string`|`` +|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use fireflyContract[].chaincode)|`string`|`` |channel|The Fabric channel that FireFly will use for BatchPin transactions|`string`|`` |connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` |expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` @@ -302,6 +302,13 @@ nav_order: 3 |readBufferSize|The size in bytes of the read buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb` |writeBufferSize|The size in bytes of the write buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb` +## blockchain.fabric.fireflyContract[] + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions|`string`|`` +|fromBlock|The first event this FireFly instance should listen to from the BatchPin chaincode. Default=0. Only affects initial creation of the event stream|Address `string`|`` + ## blockchainevent.cache |Key|Description|Type|Default Value| @@ -830,7 +837,7 @@ nav_order: 3 |---|-----------|----|-------------| |batchSize|The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream|`int`|`50` |batchTimeout|The maximum amount of time to wait for a batch to complete|[`time.Duration`](https://pkg.go.dev/time#Duration)|`500` -|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions|`string`|`` +|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use fireflyContract[].chaincode)|`string`|`` |channel|The Fabric channel that FireFly will use for BatchPin transactions|`string`|`` |connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` |expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` @@ -877,6 +884,13 @@ nav_order: 3 |readBufferSize|The size in bytes of the read buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb` |writeBufferSize|The size in bytes of the write buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb` +## plugins.blockchain[].fabric.fireflyContract[] + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|chaincode|The name of the Fabric chaincode that FireFly will use for BatchPin transactions|`string`|`` +|fromBlock|The first event this FireFly instance should listen to from the BatchPin chaincode. Default=0. Only affects initial creation of the event stream|Address `string`|`` + ## plugins.database[] |Key|Description|Type|Default Value| diff --git a/internal/blockchain/fabric/config.go b/internal/blockchain/fabric/config.go index fea3db8d1..63c7ab564 100644 --- a/internal/blockchain/fabric/config.go +++ b/internal/blockchain/fabric/config.go @@ -34,8 +34,6 @@ const ( // FabconnectConfigDefaultChannel is the default Fabric channel to use if no "ledger" is specified in requests FabconnectConfigDefaultChannel = "channel" - // FabconnectConfigChaincode is the Fabric Firefly chaincode deployed to the Firefly channels - FabconnectConfigChaincode = "chaincode" // FabconnectConfigSigner is the signer identity used to subscribe to FireFly chaincode events FabconnectConfigSigner = "signer" // FabconnectConfigTopic is the websocket listen topic that the node should register on, which is important if there are multiple @@ -49,17 +47,30 @@ const ( FabconnectPrefixShort = "prefixShort" // FabconnectPrefixLong is used in HTTP headers in requests to ethconnect FabconnectPrefixLong = "prefixLong" + // FabconnectConfigChaincodeDeprecated is the Fabric Firefly chaincode deployed to the Firefly channels + FabconnectConfigChaincodeDeprecated = "chaincode" + + // FireFlyContractConfigKey is a sub-key in the config to contain the info on the deployed FireFly contract + FireFlyContractConfigKey = "fireflyContract" + // FireFlyContractChaincode is the Fabric Firefly chaincode deployed to the Firefly channels + FireFlyContractChaincode = "chaincode" + // FireFlyContractFromBlock is the configuration of the first block to listen to when creating the listener + FireFlyContractFromBlock = "fromBlock" ) func (f *Fabric) InitConfig(config config.Section) { - fabconnectConf := config.SubSection(FabconnectConfigKey) - wsclient.InitConfig(fabconnectConf) - fabconnectConf.AddKnownKey(FabconnectConfigDefaultChannel) - fabconnectConf.AddKnownKey(FabconnectConfigChaincode) - fabconnectConf.AddKnownKey(FabconnectConfigSigner) - fabconnectConf.AddKnownKey(FabconnectConfigTopic) - fabconnectConf.AddKnownKey(FabconnectConfigBatchSize, defaultBatchSize) - fabconnectConf.AddKnownKey(FabconnectConfigBatchTimeout, defaultBatchTimeout) - fabconnectConf.AddKnownKey(FabconnectPrefixShort, defaultPrefixShort) - fabconnectConf.AddKnownKey(FabconnectPrefixLong, defaultPrefixLong) + f.fabconnectConf = config.SubSection(FabconnectConfigKey) + wsclient.InitConfig(f.fabconnectConf) + f.fabconnectConf.AddKnownKey(FabconnectConfigDefaultChannel) + f.fabconnectConf.AddKnownKey(FabconnectConfigChaincodeDeprecated) + f.fabconnectConf.AddKnownKey(FabconnectConfigSigner) + f.fabconnectConf.AddKnownKey(FabconnectConfigTopic) + f.fabconnectConf.AddKnownKey(FabconnectConfigBatchSize, defaultBatchSize) + f.fabconnectConf.AddKnownKey(FabconnectConfigBatchTimeout, defaultBatchTimeout) + f.fabconnectConf.AddKnownKey(FabconnectPrefixShort, defaultPrefixShort) + f.fabconnectConf.AddKnownKey(FabconnectPrefixLong, defaultPrefixLong) + + f.contractConf = config.SubArray(FireFlyContractConfigKey) + f.contractConf.AddKnownKey(FireFlyContractChaincode) + f.contractConf.AddKnownKey(FireFlyContractFromBlock, "oldest") } diff --git a/internal/blockchain/fabric/eventstream.go b/internal/blockchain/fabric/eventstream.go index 14fceb613..f00d080bf 100644 --- a/internal/blockchain/fabric/eventstream.go +++ b/internal/blockchain/fabric/eventstream.go @@ -27,8 +27,9 @@ import ( ) type streamManager struct { - client *resty.Client - signer string + client *resty.Client + signer string + fireFlySubscriptionFromBlock string } type eventStream struct { @@ -164,7 +165,7 @@ func (s *streamManager) ensureSubscription(ctx context.Context, location *Locati } if sub == nil { - if sub, err = s.createSubscription(ctx, location, stream, subName, event, string(core.SubOptsFirstEventOldest)); err != nil { + if sub, err = s.createSubscription(ctx, location, stream, subName, event, s.fireFlySubscriptionFromBlock); err != nil { return nil, err } } diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index afca24d91..ea412f6c8 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -59,10 +59,12 @@ type Fabric struct { stream *eventStream sub *subscription } - idCache map[string]*fabIdentity - wsconn wsclient.WSClient - closed chan struct{} - metrics metrics.Manager + idCache map[string]*fabIdentity + wsconn wsclient.WSClient + closed chan struct{} + metrics metrics.Manager + fabconnectConf config.Section + contractConf config.ArraySection } type eventStreamWebsocket struct { @@ -161,7 +163,7 @@ func (f *Fabric) VerifierType() core.VerifierType { } func (f *Fabric) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager) (err error) { - fabconnectConf := config.SubSection(FabconnectConfigKey) + f.InitConfig(config) f.ctx = log.WithLogField(ctx, "proto", "fabric") f.callbacks = callbacks @@ -169,43 +171,62 @@ func (f *Fabric) Init(ctx context.Context, config config.Section, callbacks bloc f.metrics = metrics f.capabilities = &blockchain.Capabilities{} - if fabconnectConf.GetString(ffresty.HTTPConfigURL) == "" { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "blockchain.fabconnect") - } - f.defaultChannel = fabconnectConf.GetString(FabconnectConfigDefaultChannel) - f.chaincode = fabconnectConf.GetString(FabconnectConfigChaincode) - if f.chaincode == "" { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "chaincode", "blockchain.fabconnect") + if f.fabconnectConf.GetString(ffresty.HTTPConfigURL) == "" { + return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "blockchain.fabric.fabconnect") } + f.defaultChannel = f.fabconnectConf.GetString(FabconnectConfigDefaultChannel) // the org identity is guaranteed to be configured by the core - f.signer = fabconnectConf.GetString(FabconnectConfigSigner) - f.topic = fabconnectConf.GetString(FabconnectConfigTopic) + f.signer = f.fabconnectConf.GetString(FabconnectConfigSigner) + f.topic = f.fabconnectConf.GetString(FabconnectConfigTopic) if f.topic == "" { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "topic", "blockchain.fabconnect") + return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "topic", "blockchain.fabric.fabconnect") } - f.prefixShort = fabconnectConf.GetString(FabconnectPrefixShort) - f.prefixLong = fabconnectConf.GetString(FabconnectPrefixLong) + f.prefixShort = f.fabconnectConf.GetString(FabconnectPrefixShort) + f.prefixLong = f.fabconnectConf.GetString(FabconnectPrefixLong) - f.client = ffresty.New(f.ctx, fabconnectConf) + f.client = ffresty.New(f.ctx, f.fabconnectConf) - wsConfig := wsclient.GenerateConfig(fabconnectConf) + return nil +} +func (f *Fabric) initStreams(contractIndex int) (err error) { + ctx := f.ctx + wsConfig := wsclient.GenerateConfig(f.fabconnectConf) if wsConfig.WSKeyPath == "" { wsConfig.WSKeyPath = "/ws" } - f.wsconn, err = wsclient.New(ctx, wsConfig, nil, f.afterConnect) if err != nil { return err } - f.streams = &streamManager{ - client: f.client, - signer: f.signer, + f.streams = &streamManager{client: f.client, signer: f.signer} + if f.contractConf.ArraySize() > 0 || contractIndex > 0 { + // New config (array of objects under "fireflyContract") + if contractIndex >= f.contractConf.ArraySize() { + return i18n.NewError(ctx, coremsgs.MsgInvalidFireFlyContractIndex, fmt.Sprintf("blockchain.fabric.fireflyContract[%d]", contractIndex)) + } + f.chaincode = f.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractChaincode) + if f.chaincode == "" { + return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "address", "blockchain.fabric.fireflyContract") + } + f.streams.fireFlySubscriptionFromBlock = f.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractFromBlock) + } else { + // Old config (attributes under "ethconnect") + f.chaincode = f.fabconnectConf.GetString(FabconnectConfigChaincodeDeprecated) + if f.chaincode != "" { + log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead", + FabconnectConfigKey, FabconnectConfigChaincodeDeprecated, + FireFlyContractConfigKey, FireFlyContractChaincode) + f.streams.fireFlySubscriptionFromBlock = string(core.SubOptsFirstEventOldest) + } else { + return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "chaincode", "blockchain.fabric.fabconnect") + } } - batchSize := fabconnectConf.GetUint(FabconnectConfigBatchSize) - batchTimeout := uint(fabconnectConf.GetDuration(FabconnectConfigBatchTimeout).Milliseconds()) + + batchSize := f.fabconnectConf.GetUint(FabconnectConfigBatchSize) + batchTimeout := uint(f.fabconnectConf.GetDuration(FabconnectConfigBatchTimeout).Milliseconds()) if f.initInfo.stream, err = f.streams.ensureEventStream(f.ctx, f.topic, batchSize, batchTimeout); err != nil { return err } @@ -224,7 +245,10 @@ func (f *Fabric) Init(ctx context.Context, config config.Section, callbacks bloc return nil } -func (f *Fabric) Start(contractIndex int) error { +func (f *Fabric) Start(contractIndex int) (err error) { + if err = f.initStreams(contractIndex); err != nil { + return err + } return f.wsconn.Connect() } diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index 2ee6882b9..88d07d5ae 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -46,9 +46,8 @@ var utConfig = config.RootSection("fab_unit_tests") var utFabconnectConf = utConfig.SubSection(FabconnectConfigKey) var signer = "orgMSP::x509::CN=signer001,OU=client::CN=fabric-ca" -func resetConf() { +func resetConf(e *Fabric) { coreconfig.Reset() - e := &Fabric{} e.InitConfig(utConfig) } @@ -105,7 +104,7 @@ func testFFIMethod() *core.FFIMethod { func TestInitMissingURL(t *testing.T) { e, cancel := newTestFabric() defer cancel() - resetConf() + resetConf(e) err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.Regexp(t, "FF10138.*url", err) } @@ -113,20 +112,22 @@ func TestInitMissingURL(t *testing.T) { func TestInitMissingChaincode(t *testing.T) { e, cancel := newTestFabric() defer cancel() - resetConf() + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) assert.Regexp(t, "FF10138.*chaincode", err) } func TestInitMissingTopic(t *testing.T) { e, cancel := newTestFabric() defer cancel() - resetConf() + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") - utFabconnectConf.Set(FabconnectConfigChaincode, "Firefly") + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "Firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) @@ -165,10 +166,10 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) { return httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sub12345"})(req) }) - resetConf() + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utFabconnectConf.Set(FabconnectConfigChaincode, "firefly") + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") @@ -177,14 +178,15 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) { assert.Equal(t, "fabric", e.Name()) assert.Equal(t, core.VerifierTypeMSPIdentity, e.VerifierType()) + + err = e.Start(0) + assert.NoError(t, err) + assert.Equal(t, 4, httpmock.GetTotalCallCount()) assert.Equal(t, "es12345", e.initInfo.stream.ID) assert.Equal(t, "sub12345", e.initInfo.sub.ID) assert.NotNil(t, e.Capabilities()) - err = e.Start(0) - assert.NoError(t, err) - startupMessage := <-toServer assert.Equal(t, `{"type":"listen","topic":"topic1"}`, startupMessage) startupMessage = <-toServer @@ -205,28 +207,29 @@ func TestWSInitFail(t *testing.T) { e, cancel := newTestFabric() defer cancel() - resetConf() + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "!!!://") - utFabconnectConf.Set(FabconnectConfigChaincode, "firefly") + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.Start(0) assert.Regexp(t, "FF00149", err) } -func TestWSConnectFail(t *testing.T) { +func TestWSClose(t *testing.T) { wsm := &wsmocks.WSClient{} e := &Fabric{ ctx: context.Background(), wsconn: wsm, } - wsm.On("Connect").Return(fmt.Errorf("pop")) + wsm.On("Close").Return(nil) + e.Stop() - err := e.Start(0) - assert.EqualError(t, err, "pop") } func TestInitAllExistingStreams(t *testing.T) { @@ -245,21 +248,102 @@ func TestInitAllExistingStreams(t *testing.T) { {ID: "sub12345", Stream: "es12345", Name: "BatchPin"}, })) - resetConf() + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utFabconnectConf.Set(FabconnectConfigChaincode, "firefly") + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) + assert.NoError(t, err) assert.Equal(t, 2, httpmock.GetTotalCallCount()) assert.Equal(t, "es12345", e.initInfo.stream.ID) assert.Equal(t, "sub12345", e.initInfo.sub.ID) +} + +func TestInitNewConfig(t *testing.T) { + + e, cancel := newTestFabric() + defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{ + {ID: "sub12345", Stream: "es12345", Name: "BatchPin"}, + })) + + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigSigner, "signer001") + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) assert.NoError(t, err) + assert.Equal(t, 2, httpmock.GetTotalCallCount()) + assert.Equal(t, "es12345", e.initInfo.stream.ID) + assert.Equal(t, "sub12345", e.initInfo.sub.ID) + +} + +func TestInitNewConfigError(t *testing.T) { + + e, cancel := newTestFabric() + defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigSigner, "signer001") + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10138", err) + +} + +func TestInitNewConfigBadIndex(t *testing.T) { + + e, cancel := newTestFabric() + defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigSigner, "signer001") + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.initStreams(1) + assert.Regexp(t, "FF10387", err) + } func TestStreamQueryError(t *testing.T) { @@ -274,18 +358,18 @@ func TestStreamQueryError(t *testing.T) { httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", httpmock.NewStringResponder(500, `pop`)) - resetConf() + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utFabconnectConf.Set(FabconnectConfigChaincode, "firefly") + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - - assert.Regexp(t, "FF10284", err) - assert.Regexp(t, "pop", err) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10284.*pop", err) } @@ -303,18 +387,18 @@ func TestStreamCreateError(t *testing.T) { httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", httpmock.NewStringResponder(500, `pop`)) - resetConf() + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utFabconnectConf.Set(FabconnectConfigChaincode, "firefly") + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - - assert.Regexp(t, "FF10284", err) - assert.Regexp(t, "pop", err) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10284.*pop", err) } @@ -334,18 +418,18 @@ func TestSubQueryError(t *testing.T) { httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", httpmock.NewStringResponder(500, `pop`)) - resetConf() + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utFabconnectConf.Set(FabconnectConfigChaincode, "firefly") + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - - assert.Regexp(t, "FF10284", err) - assert.Regexp(t, "pop", err) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10284.*pop", err) } @@ -367,18 +451,18 @@ func TestSubQueryCreateError(t *testing.T) { httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", httpmock.NewStringResponder(500, `pop`)) - resetConf() + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utFabconnectConf.Set(FabconnectConfigChaincode, "firefly") + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - - assert.Regexp(t, "FF10284", err) - assert.Regexp(t, "pop", err) + assert.NoError(t, err) + err = e.initStreams(0) + assert.Regexp(t, "FF10284.*pop", err) } @@ -1202,8 +1286,7 @@ func TestAddSubscriptionFail(t *testing.T) { err := e.AddContractListener(context.Background(), sub) - assert.Regexp(t, "FF10284", err) - assert.Regexp(t, "pop", err) + assert.Regexp(t, "FF10284.*pop", err) } func TestDeleteSubscription(t *testing.T) { @@ -1253,8 +1336,7 @@ func TestDeleteSubscriptionFail(t *testing.T) { err := e.DeleteContractListener(context.Background(), sub) - assert.Regexp(t, "FF10284", err) - assert.Regexp(t, "pop", err) + assert.Regexp(t, "FF10284.*pop", err) } func TestHandleMessageContractEvent(t *testing.T) { diff --git a/internal/coremsgs/en_config_descriptions.go b/internal/coremsgs/en_config_descriptions.go index c02e1d5e4..00eec34ed 100644 --- a/internal/coremsgs/en_config_descriptions.go +++ b/internal/coremsgs/en_config_descriptions.go @@ -86,15 +86,17 @@ var ( ConfigBlockchainFabricFabconnectBatchSize = ffc("config.blockchain.fabric.fabconnect.batchSize", "The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType) ConfigBlockchainFabricFabconnectBatchTimeout = ffc("config.blockchain.fabric.fabconnect.batchTimeout", "The maximum amount of time to wait for a batch to complete", i18n.TimeDurationType) - ConfigBlockchainFabricFabconnectChaincode = ffc("config.blockchain.fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions", i18n.StringType) + ConfigBlockchainFabricFabconnectChaincode = ffc("config.blockchain.fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use fireflyContract[].chaincode)", i18n.StringType) ConfigBlockchainFabricFabconnectChannel = ffc("config.blockchain.fabric.fabconnect.channel", "The Fabric channel that FireFly will use for BatchPin transactions", i18n.StringType) ConfigBlockchainFabricFabconnectPrefixLong = ffc("config.blockchain.fabric.fabconnect.prefixLong", "The prefix that will be used for Fabconnect specific HTTP headers when FireFly makes requests to Fabconnect", i18n.StringType) ConfigBlockchainFabricFabconnectPrefixShort = ffc("config.blockchain.fabric.fabconnect.prefixShort", "The prefix that will be used for Fabconnect specific query parameters when FireFly makes requests to Fabconnect", i18n.StringType) ConfigBlockchainFabricFabconnectSigner = ffc("config.blockchain.fabric.fabconnect.signer", "The Fabric signing key to use when submitting transactions to Fabconnect", i18n.StringType) ConfigBlockchainFabricFabconnectTopic = ffc("config.blockchain.fabric.fabconnect.topic", "The websocket listen topic that the node should register on, which is important if there are multiple nodes using a single Fabconnect", i18n.StringType) ConfigBlockchainFabricFabconnectURL = ffc("config.blockchain.fabric.fabconnect.url", "The URL of the Fabconnect instance", "URL "+i18n.StringType) + ConfigBlockchainFabricFabconnectProxyURL = ffc("config.blockchain.fabric.fabconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Fabconnect", "URL "+i18n.StringType) - ConfigBlockchainFabricFabconnectProxyURL = ffc("config.blockchain.fabric.fabconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Fabconnect", "URL "+i18n.StringType) + ConfigBlockchainFabricContractChaincode = ffc("config.blockchain.fabric.fireflyContract[].chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions", i18n.StringType) + ConfigBlockchainFabricContractFromBlock = ffc("config.blockchain.fabric.fireflyContract[].fromBlock", "The first event this FireFly instance should listen to from the BatchPin chaincode. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType) ConfigPluginDatabase = ffc("config.plugins.database", "The list of configured Database plugins", i18n.StringType) ConfigPluginDatabaseName = ffc("config.plugins.database[].name", "The name of the Database plugin", i18n.StringType) @@ -141,23 +143,25 @@ var ( ConfigPluginBlockchainEthereumEthconnectURL = ffc("config.plugins.blockchain[].ethereum.ethconnect.url", "The URL of the Ethconnect instance", "URL "+i18n.StringType) ConfigPluginBlockchainEthereumEthconnectProxyURL = ffc("config.plugins.blockchain[].ethereum.ethconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Ethconnect", "URL "+i18n.StringType) - ConfigPluginsBlockchainEthereumContractAddress = ffc("config.plugins.blockchain[].ethereum.fireflyContract[].address", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain", "Address "+i18n.StringType) - ConfigPluginsBlockchainEthereumContractFromBlock = ffc("config.plugins.blockchain[].ethereum.fireflyContract[].fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType) + ConfigPluginBlockchainEthereumContractAddress = ffc("config.plugins.blockchain[].ethereum.fireflyContract[].address", "The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain", "Address "+i18n.StringType) + ConfigPluginBlockchainEthereumContractFromBlock = ffc("config.plugins.blockchain[].ethereum.fireflyContract[].fromBlock", "The first event this FireFly instance should listen to from the BatchPin smart contract. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType) ConfigPluginBlockchainEthereumFFTMURL = ffc("config.plugins.blockchain[].ethereum.fftm.url", "The URL of the FireFly Transaction Manager runtime, if enabled", i18n.StringType) ConfigPluginBlockchainEthereumFFTMProxyURL = ffc("config.plugins.blockchain[].ethereum.fftm.proxy.url", "Optional HTTP proxy server to use when connecting to the Transaction Manager", i18n.StringType) ConfigPluginBlockchainFabricFabconnectBatchSize = ffc("config.plugins.blockchain[].fabric.fabconnect.batchSize", "The number of events Fabconnect should batch together for delivery to FireFly core. Only applies when automatically creating a new event stream", i18n.IntType) ConfigPluginBlockchainFabricFabconnectBatchTimeout = ffc("config.plugins.blockchain[].fabric.fabconnect.batchTimeout", "The maximum amount of time to wait for a batch to complete", i18n.TimeDurationType) - ConfigPluginBlockchainFabricFabconnectChaincode = ffc("config.plugins.blockchain[].fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions", i18n.StringType) + ConfigPluginBlockchainFabricFabconnectChaincode = ffc("config.plugins.blockchain[].fabric.fabconnect.chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions (deprecated - use fireflyContract[].chaincode)", i18n.StringType) ConfigPluginBlockchainFabricFabconnectChannel = ffc("config.plugins.blockchain[].fabric.fabconnect.channel", "The Fabric channel that FireFly will use for BatchPin transactions", i18n.StringType) ConfigPluginBlockchainFabricFabconnectPrefixLong = ffc("config.plugins.blockchain[].fabric.fabconnect.prefixLong", "The prefix that will be used for Fabconnect specific HTTP headers when FireFly makes requests to Fabconnect", i18n.StringType) ConfigPluginBlockchainFabricFabconnectPrefixShort = ffc("config.plugins.blockchain[].fabric.fabconnect.prefixShort", "The prefix that will be used for Fabconnect specific query parameters when FireFly makes requests to Fabconnect", i18n.StringType) ConfigPluginBlockchainFabricFabconnectSigner = ffc("config.plugins.blockchain[].fabric.fabconnect.signer", "The Fabric signing key to use when submitting transactions to Fabconnect", i18n.StringType) ConfigPluginBlockchainFabricFabconnectTopic = ffc("config.plugins.blockchain[].fabric.fabconnect.topic", "The websocket listen topic that the node should register on, which is important if there are multiple nodes using a single Fabconnect", i18n.StringType) ConfigPluginBlockchainFabricFabconnectURL = ffc("config.plugins.blockchain[].fabric.fabconnect.url", "The URL of the Fabconnect instance", "URL "+i18n.StringType) + ConfigPluginBlockchainFabricFabconnectProxyURL = ffc("config.plugins.blockchain[].fabric.fabconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Fabconnect", "URL "+i18n.StringType) - ConfigPluginBlockchainFabricFabconnectProxyURL = ffc("config.plugins.blockchain[].fabric.fabconnect.proxy.url", "Optional HTTP proxy server to use when connecting to Fabconnect", "URL "+i18n.StringType) + ConfigPluginBlockchainFabricContractChaincode = ffc("config.plugins.blockchain[].fabric.fireflyContract[].chaincode", "The name of the Fabric chaincode that FireFly will use for BatchPin transactions", i18n.StringType) + ConfigPluginBlockchainFabricContractFromBlock = ffc("config.plugins.blockchain[].fabric.fireflyContract[].fromBlock", "The first event this FireFly instance should listen to from the BatchPin chaincode. Default=0. Only affects initial creation of the event stream", "Address "+i18n.StringType) ConfigBroadcastBatchAgentTimeout = ffc("config.broadcast.batch.agentTimeout", "How long to keep around a batching agent for a sending identity before disposal", i18n.StringType) ConfigBroadcastBatchPayloadLimit = ffc("config.broadcast.batch.payloadLimit", "The maximum payload size of a batch for broadcast messages", i18n.ByteSizeType) From cd6d752825fd28854d44f0e8c10893bf54d19ce0 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Fri, 20 May 2022 16:54:28 -0400 Subject: [PATCH 04/13] Support migration of the FireFly contract from one location to another Send a specially formatted "BatchPin" transaction to signal that all members should migrate to a specific version of the contract from their config. Signed-off-by: Andrew Richardson --- ...00090_add_namespace_contractindex.down.sql | 3 + .../000090_add_namespace_contractindex.up.sql | 3 + ...00090_add_namespace_contractindex.down.sql | 1 + .../000090_add_namespace_contractindex.up.sql | 1 + docs/swagger/swagger.yaml | 55 +++++++ .../apiserver/route_post_network_migrate.go | 42 +++++ .../route_post_network_migrate_test.go | 43 +++++ internal/apiserver/routes.go | 1 + internal/blockchain/ethereum/ethereum.go | 30 +++- internal/blockchain/ethereum/ethereum_test.go | 69 ++++++++ internal/blockchain/fabric/fabric.go | 31 +++- internal/blockchain/fabric/fabric_test.go | 65 ++++++++ internal/coremsgs/en_api_translations.go | 1 + internal/coremsgs/en_struct_descriptions.go | 13 +- internal/database/sqlcommon/namespace_sql.go | 4 + .../database/sqlcommon/namespace_sql_test.go | 26 +-- internal/events/blockchain_event.go | 2 +- internal/events/event_manager.go | 1 + internal/events/operator_action.go | 58 +++++++ internal/events/operator_action_test.go | 148 ++++++++++++++++++ internal/orchestrator/bound_callbacks.go | 4 + internal/orchestrator/bound_callbacks_test.go | 4 + internal/orchestrator/orchestrator.go | 18 ++- internal/orchestrator/orchestrator_test.go | 23 +++ mocks/blockchainmocks/callbacks.go | 14 ++ mocks/blockchainmocks/plugin.go | 14 ++ mocks/eventmocks/event_manager.go | 14 ++ mocks/orchestratormocks/orchestrator.go | 14 ++ pkg/blockchain/plugin.go | 19 ++- pkg/core/namespace.go | 17 +- pkg/database/plugin.go | 6 +- 31 files changed, 700 insertions(+), 44 deletions(-) create mode 100644 db/migrations/postgres/000090_add_namespace_contractindex.down.sql create mode 100644 db/migrations/postgres/000090_add_namespace_contractindex.up.sql create mode 100644 db/migrations/sqlite/000090_add_namespace_contractindex.down.sql create mode 100644 db/migrations/sqlite/000090_add_namespace_contractindex.up.sql create mode 100644 internal/apiserver/route_post_network_migrate.go create mode 100644 internal/apiserver/route_post_network_migrate_test.go create mode 100644 internal/events/operator_action.go create mode 100644 internal/events/operator_action_test.go diff --git a/db/migrations/postgres/000090_add_namespace_contractindex.down.sql b/db/migrations/postgres/000090_add_namespace_contractindex.down.sql new file mode 100644 index 000000000..30a75a161 --- /dev/null +++ b/db/migrations/postgres/000090_add_namespace_contractindex.down.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE namespaces DROP COLUMN contract_index; +COMMIT; diff --git a/db/migrations/postgres/000090_add_namespace_contractindex.up.sql b/db/migrations/postgres/000090_add_namespace_contractindex.up.sql new file mode 100644 index 000000000..370aae8d1 --- /dev/null +++ b/db/migrations/postgres/000090_add_namespace_contractindex.up.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE namespaces ADD COLUMN contract_index INTEGER DEFAULT 0; +COMMIT; diff --git a/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql b/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql new file mode 100644 index 000000000..2899a529f --- /dev/null +++ b/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql @@ -0,0 +1 @@ +ALTER TABLE namespaces DROP COLUMN contract_index; diff --git a/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql b/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql new file mode 100644 index 000000000..93f8a8d02 --- /dev/null +++ b/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql @@ -0,0 +1 @@ +ALTER TABLE namespaces ADD COLUMN contract_index INTEGER DEFAULT 0; diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 0956bb71b..f8ed2b0ed 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -8136,6 +8136,10 @@ paths: schema: items: properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer created: description: The time the namespace was created format: date-time @@ -8206,6 +8210,10 @@ paths: application/json: schema: properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer created: description: The time the namespace was created format: date-time @@ -8241,6 +8249,10 @@ paths: application/json: schema: properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer created: description: The time the namespace was created format: date-time @@ -8300,6 +8312,10 @@ paths: application/json: schema: properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer created: description: The time the namespace was created format: date-time @@ -22395,6 +22411,45 @@ paths: description: "" tags: - Global + /network/migrate: + post: + description: Instruct the network to unsubscribe from the current FireFly contract + and migrate to the next one configured + operationId: postNetworkMigrate + parameters: + - description: Server-side request timeout (millseconds, or set a custom suffix + like 10s) + in: header + name: Request-Timeout + schema: + default: 120s + type: string + requestBody: + content: + application/json: + schema: + properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer + type: object + responses: + "202": + content: + application/json: + schema: + properties: + contractIndex: + description: The index of the configured FireFly smart contract + currently being used + type: integer + type: object + description: Success + default: + description: "" + tags: + - Global /network/nodes: get: description: Gets a list of nodes in the network diff --git a/internal/apiserver/route_post_network_migrate.go b/internal/apiserver/route_post_network_migrate.go new file mode 100644 index 000000000..c0c7ae556 --- /dev/null +++ b/internal/apiserver/route_post_network_migrate.go @@ -0,0 +1,42 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiserver + +import ( + "net/http" + + "github.com/hyperledger/firefly/internal/coremsgs" + "github.com/hyperledger/firefly/internal/oapispec" + "github.com/hyperledger/firefly/pkg/core" +) + +var postNetworkMigrate = &oapispec.Route{ + Name: "postNetworkMigrate", + Path: "network/migrate", + Method: http.MethodPost, + PathParams: nil, + QueryParams: nil, + FilterFactory: nil, + Description: coremsgs.APIEndpointsPostNetworkMigrate, + JSONInputValue: func() interface{} { return &core.NamespaceMigration{} }, + JSONOutputValue: func() interface{} { return &core.NamespaceMigration{} }, + JSONOutputCodes: []int{http.StatusAccepted}, + JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { + err = getOr(r.Ctx).MigrateNetwork(r.Ctx, r.Input.(*core.NamespaceMigration).ContractIndex) + return r.Input, err + }, +} diff --git a/internal/apiserver/route_post_network_migrate_test.go b/internal/apiserver/route_post_network_migrate_test.go new file mode 100644 index 000000000..62a64d998 --- /dev/null +++ b/internal/apiserver/route_post_network_migrate_test.go @@ -0,0 +1,43 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiserver + +import ( + "bytes" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/hyperledger/firefly/pkg/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostNetworkMigrate(t *testing.T) { + o, r := newTestAPIServer() + input := core.NamespaceMigration{ContractIndex: 1} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + req := httptest.NewRequest("POST", "/api/v1/network/migrate", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + o.On("MigrateNetwork", mock.Anything, 1).Return(nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 202, res.Result().StatusCode) +} diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go index d943792b1..f6015da71 100644 --- a/internal/apiserver/routes.go +++ b/internal/apiserver/routes.go @@ -38,6 +38,7 @@ var routes = append( getStatusBatchManager, getStatusPins, getStatusWebSockets, + postNetworkMigrate, postNewNamespace, postNewOrganization, postNewOrganizationSelf, diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index cd06b27f0..7fd094786 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -312,7 +312,7 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON logIndex := msgJSON.GetInt64("logIndex") dataJSON := msgJSON.GetObject("data") authorAddress := dataJSON.GetString("author") - ns := dataJSON.GetString("namespace") + nsOrAction := dataJSON.GetString("namespace") sUUIDs := dataJSON.GetString("uuids") sBatchHash := dataJSON.GetString("batchHash") sPayloadRef := dataJSON.GetString("payloadRef") @@ -338,6 +338,16 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON log.L(ctx).Errorf("BatchPin event is not valid - bad from address (%s): %+v", err, msgJSON) return nil // move on } + verifier := &core.VerifierRef{ + Type: core.VerifierTypeEthAddress, + Value: authorAddress, + } + + // Check if this is actually an operator action + if strings.HasPrefix(nsOrAction, blockchain.FireFlyActionPrefix) { + action := nsOrAction[len(blockchain.FireFlyActionPrefix):] + return e.callbacks.BlockchainOperatorAction(action, sPayloadRef, verifier) + } hexUUIDs, err := hex.DecodeString(strings.TrimPrefix(sUUIDs, "0x")) if err != nil || len(hexUUIDs) != 32 { @@ -369,7 +379,7 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON delete(msgJSON, "data") batch := &blockchain.BatchPin{ - Namespace: ns, + Namespace: nsOrAction, TransactionID: &txnID, BatchID: &batchID, BatchHash: &batchHash, @@ -389,10 +399,7 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON } // If there's an error dispatching the event, we must return the error and shutdown - return e.callbacks.BatchPinComplete(batch, &core.VerifierRef{ - Type: core.VerifierTypeEthAddress, - Value: authorAddress, - }) + return e.callbacks.BatchPinComplete(batch, verifier) } func (e *Ethereum) handleContractEvent(ctx context.Context, msgJSON fftypes.JSONObject) (err error) { @@ -633,6 +640,17 @@ func (e *Ethereum) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID return e.invokeContractMethod(ctx, e.contractAddress, signingKey, batchPinMethodABI, operationID.String(), input) } +func (e *Ethereum) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action, payload string) error { + input := []interface{}{ + blockchain.FireFlyActionPrefix + action, + ethHexFormatB32(nil), + ethHexFormatB32(nil), + payload, + []string{}, + } + return e.invokeContractMethod(ctx, e.contractAddress, signingKey, batchPinMethodABI, operationID.String(), input) +} + func (e *Ethereum) InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error { ethereumLocation, err := parseContractLocation(ctx, location) if err != nil { diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index 4a138fea3..cccc933b7 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -2597,3 +2597,72 @@ func TestGenerateEventSignatureInvalid(t *testing.T) { signature := e.GenerateEventSignature(context.Background(), event) assert.Equal(t, "", signature) } + +func TestSubmitOperatorAction(t *testing.T) { + e, _ := newTestEthereum() + httpmock.ActivateNonDefault(e.client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("POST", `http://localhost:12345/`, + func(req *http.Request) (*http.Response, error) { + var body map[string]interface{} + json.NewDecoder(req.Body).Decode(&body) + params := body["params"].([]interface{}) + headers := body["headers"].(map[string]interface{}) + assert.Equal(t, "SendTransaction", headers["type"]) + assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", params[1]) + assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", params[2]) + assert.Equal(t, "1", params[3]) + return httpmock.NewJsonResponderOrPanic(200, "")(req) + }) + + err := e.SubmitOperatorAction(context.Background(), fftypes.NewUUID(), "0x123", blockchain.OperatorActionMigrate, "1") + assert.NoError(t, err) +} + +func TestHandleOperatorAction(t *testing.T) { + data := fftypes.JSONAnyPtr(` +[ + { + "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", + "blockNumber": "38011", + "transactionIndex": "0x0", + "transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628", + "data": { + "author": "0X91D2B4381A4CD5C7C0F27565A7D4B829844C8635", + "namespace": "firefly:migrate", + "uuids": "0x0000000000000000000000000000000000000000000000000000000000000000", + "batchHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "payloadRef": "1", + "contexts": [] + }, + "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", + "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", + "logIndex": "50", + "timestamp": "1620576488" + } +]`) + + em := &blockchainmocks.Callbacks{} + e := &Ethereum{ + callbacks: em, + } + e.initInfo.sub = &subscription{ + ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", + } + + expectedSigningKeyRef := &core.VerifierRef{ + Type: core.VerifierTypeEthAddress, + Value: "0x91d2b4381a4cd5c7c0f27565a7d4b829844c8635", + } + + em.On("BlockchainOperatorAction", "migrate", "1", expectedSigningKeyRef).Return(nil) + + var events []interface{} + err := json.Unmarshal(data.Bytes(), &events) + assert.NoError(t, err) + err = e.handleMessageBatch(context.Background(), events) + assert.NoError(t, err) + + em.AssertExpectations(t) + +} diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index ea412f6c8..6a50d65a8 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -304,12 +304,23 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb eventIndex := msgJSON.GetInt64("eventIndex") timestamp := msgJSON.GetInt64("timestamp") signer := payload.GetString("signer") - ns := payload.GetString("namespace") + nsOrAction := payload.GetString("namespace") sUUIDs := payload.GetString("uuids") sBatchHash := payload.GetString("batchHash") sPayloadRef := payload.GetString("payloadRef") sContexts := payload.GetStringArray("contexts") + verifier := &core.VerifierRef{ + Type: core.VerifierTypeMSPIdentity, + Value: signer, + } + + // Check if this is actually an operator action + if strings.HasPrefix(nsOrAction, blockchain.FireFlyActionPrefix) { + action := nsOrAction[len(blockchain.FireFlyActionPrefix):] + return f.callbacks.BlockchainOperatorAction(action, sPayloadRef, verifier) + } + hexUUIDs, err := hex.DecodeString(strings.TrimPrefix(sUUIDs, "0x")) if err != nil || len(hexUUIDs) != 32 { log.L(ctx).Errorf("BatchPin event is not valid - bad uuids (%s): %s", sUUIDs, err) @@ -340,7 +351,7 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb delete(msgJSON, "payload") batch := &blockchain.BatchPin{ - Namespace: ns, + Namespace: nsOrAction, TransactionID: &txnID, BatchID: &batchID, BatchHash: &batchHash, @@ -360,10 +371,7 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb } // If there's an error dispatching the event, we must return the error and shutdown - return f.callbacks.BatchPinComplete(batch, &core.VerifierRef{ - Type: core.VerifierTypeMSPIdentity, - Value: signer, - }) + return f.callbacks.BatchPinComplete(batch, verifier) } func (f *Fabric) buildEventLocationString(msgJSON fftypes.JSONObject) string { @@ -613,7 +621,18 @@ func (f *Fabric) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, "payloadRef": batch.BatchPayloadRef, "contexts": hashes, } + input, _ := jsonEncodeInput(pinInput) + return f.invokeContractMethod(ctx, f.defaultChannel, f.chaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) +} +func (f *Fabric) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action, payload string) error { + pinInput := map[string]interface{}{ + "namespace": "firefly:" + action, + "uuids": hexFormatB32(nil), + "batchHash": hexFormatB32(nil), + "payloadRef": payload, + "contexts": []string{}, + } input, _ := jsonEncodeInput(pinInput) return f.invokeContractMethod(ctx, f.defaultChannel, f.chaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) } diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index 88d07d5ae..af2d4199e 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -1766,3 +1766,68 @@ func TestGenerateEventSignature(t *testing.T) { signature := e.GenerateEventSignature(context.Background(), &core.FFIEventDefinition{Name: "Changed"}) assert.Equal(t, "Changed", signature) } + +func TestSubmitOperatorAction(t *testing.T) { + + e, cancel := newTestFabric() + defer cancel() + httpmock.ActivateNonDefault(e.client.GetClient()) + defer httpmock.DeactivateAndReset() + + signer := "signer001" + + httpmock.RegisterResponder("POST", `http://localhost:12345/transactions`, + func(req *http.Request) (*http.Response, error) { + var body map[string]interface{} + json.NewDecoder(req.Body).Decode(&body) + assert.Equal(t, signer, (body["headers"].(map[string]interface{}))["signer"]) + assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["uuids"]) + assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["batchHash"]) + assert.Equal(t, "1", (body["args"].(map[string]interface{}))["payloadRef"]) + return httpmock.NewJsonResponderOrPanic(200, "")(req) + }) + + err := e.SubmitOperatorAction(context.Background(), nil, signer, "migrate", "1") + assert.NoError(t, err) + +} + +func TestHandleOperatorAction(t *testing.T) { + data := []byte(` +[ + { + "chaincodeId": "firefly", + "blockNumber": 91, + "transactionId": "ce79343000e851a0c742f63a733ce19a5f8b9ce1c719b6cecd14f01bcf81fff2", + "transactionIndex": 2, + "eventIndex": 50, + "eventName": "BatchPin", + "payload": "eyJzaWduZXIiOiJ1MHZnd3U5czAwLXg1MDk6OkNOPXVzZXIyLE9VPWNsaWVudDo6Q049ZmFicmljLWNhLXNlcnZlciIsInRpbWVzdGFtcCI6eyJzZWNvbmRzIjoxNjMwMDMxNjY3LCJuYW5vcyI6NzkxNDk5MDAwfSwibmFtZXNwYWNlIjoiZmlyZWZseTptaWdyYXRlIiwidXVpZHMiOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJiYXRjaEhhc2giOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJwYXlsb2FkUmVmIjoiMSIsImNvbnRleHRzIjpbXX0=", + "subId": "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e" + } +]`) + + em := &blockchainmocks.Callbacks{} + e := &Fabric{ + callbacks: em, + } + e.initInfo.sub = &subscription{ + ID: "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e", + } + + expectedSigningKeyRef := &core.VerifierRef{ + Type: core.VerifierTypeMSPIdentity, + Value: "u0vgwu9s00-x509::CN=user2,OU=client::CN=fabric-ca-server", + } + + em.On("BlockchainOperatorAction", "migrate", "1", expectedSigningKeyRef).Return(nil) + + var events []interface{} + err := json.Unmarshal(data, &events) + assert.NoError(t, err) + err = e.handleMessageBatch(context.Background(), events) + assert.NoError(t, err) + + em.AssertExpectations(t) + +} diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go index 7826cc5f9..faba3f0e9 100644 --- a/internal/coremsgs/en_api_translations.go +++ b/internal/coremsgs/en_api_translations.go @@ -168,6 +168,7 @@ var ( APIEndpointsPutContractAPI = ffm("api.endpoints.putContractAPI", "Updates an existing contract API") APIEndpointsPutSubscription = ffm("api.endpoints.putSubscription", "Update an existing subscription") APIEndpointsGetContractAPIInterface = ffm("api.endpoints.getContractAPIInterface", "Gets a contract interface for a contract API") + APIEndpointsPostNetworkMigrate = ffm("api.endpoints.postNetworkMigrate", "Instruct the network to unsubscribe from the current FireFly contract and migrate to the next one configured") APISuccessResponse = ffm("api.success", "Success") APIRequestTimeoutDesc = ffm("api.requestTimeout", "Server-side request timeout (millseconds, or set a custom suffix like 10s)") diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go index 34e00a987..13568e6c9 100644 --- a/internal/coremsgs/en_struct_descriptions.go +++ b/internal/coremsgs/en_struct_descriptions.go @@ -371,12 +371,13 @@ var ( VerifierCreated = ffm("Verifier.created", "The time this verifier was created on this node") // Namespace field descriptions - NamespaceID = ffm("Namespace.id", "The UUID of the namespace. For locally established namespaces will be different on each node in the network. For broadcast namespaces, will be the same on every node") - NamespaceMessage = ffm("Namespace.message", "The UUID of broadcast message used to establish the namespace. Unset for local namespaces") - NamespaceName = ffm("Namespace.name", "The namespace name") - NamespaceDescription = ffm("Namespace.description", "A description of the namespace") - NamespaceType = ffm("Namespace.type", "The type of the namespace") - NamespaceCreated = ffm("Namespace.created", "The time the namespace was created") + NamespaceID = ffm("Namespace.id", "The UUID of the namespace. For locally established namespaces will be different on each node in the network. For broadcast namespaces, will be the same on every node") + NamespaceMessage = ffm("Namespace.message", "The UUID of broadcast message used to establish the namespace. Unset for local namespaces") + NamespaceName = ffm("Namespace.name", "The namespace name") + NamespaceDescription = ffm("Namespace.description", "A description of the namespace") + NamespaceType = ffm("Namespace.type", "The type of the namespace") + NamespaceCreated = ffm("Namespace.created", "The time the namespace was created") + NamespaceContractIndex = ffm("Namespace.contractIndex", "The index of the configured FireFly smart contract currently being used") // NodeStatus field descriptions NodeStatusNode = ffm("NodeStatus.node", "Details of the local node") diff --git a/internal/database/sqlcommon/namespace_sql.go b/internal/database/sqlcommon/namespace_sql.go index 18c8c086e..f7b4c0c00 100644 --- a/internal/database/sqlcommon/namespace_sql.go +++ b/internal/database/sqlcommon/namespace_sql.go @@ -37,6 +37,7 @@ var ( "name", "description", "created", + "contract_index", } namespaceFilterFieldMap = map[string]string{ "message": "message_id", @@ -90,6 +91,7 @@ func (s *SQLCommon) UpsertNamespace(ctx context.Context, namespace *core.Namespa Set("name", namespace.Name). Set("description", namespace.Description). Set("created", namespace.Created). + Set("contract_index", namespace.ContractIndex). Where(sq.Eq{"name": namespace.Name}), func() { s.callbacks.UUIDCollectionEvent(database.CollectionNamespaces, core.ChangeEventTypeUpdated, namespace.ID) @@ -112,6 +114,7 @@ func (s *SQLCommon) UpsertNamespace(ctx context.Context, namespace *core.Namespa namespace.Name, namespace.Description, namespace.Created, + namespace.ContractIndex, ), func() { s.callbacks.UUIDCollectionEvent(database.CollectionNamespaces, core.ChangeEventTypeCreated, namespace.ID) @@ -133,6 +136,7 @@ func (s *SQLCommon) namespaceResult(ctx context.Context, row *sql.Rows) (*core.N &namespace.Name, &namespace.Description, &namespace.Created, + &namespace.ContractIndex, ) if err != nil { return nil, i18n.WrapError(ctx, err, coremsgs.MsgDBReadErr, namespacesTable) diff --git a/internal/database/sqlcommon/namespace_sql_test.go b/internal/database/sqlcommon/namespace_sql_test.go index 5861f0f27..d04c0ecd4 100644 --- a/internal/database/sqlcommon/namespace_sql_test.go +++ b/internal/database/sqlcommon/namespace_sql_test.go @@ -40,11 +40,12 @@ func TestNamespacesE2EWithDB(t *testing.T) { // Create a new namespace entry namespace := &core.Namespace{ - ID: nil, // generated for us - Message: fftypes.NewUUID(), - Type: core.NamespaceTypeLocal, - Name: "namespace1", - Created: fftypes.Now(), + ID: nil, // generated for us + Message: fftypes.NewUUID(), + Type: core.NamespaceTypeLocal, + Name: "namespace1", + Created: fftypes.Now(), + ContractIndex: 1, } s.callbacks.On("UUIDCollectionEvent", database.CollectionNamespaces, core.ChangeEventTypeCreated, mock.Anything, mock.Anything).Return() @@ -239,14 +240,15 @@ func TestGetNamespaceByIDSuccess(t *testing.T) { nsID := fftypes.NewUUID() currTime := fftypes.Now() nsMock := &core.Namespace{ - ID: nsID, - Message: msgID, - Name: "ns1", - Type: core.NamespaceTypeLocal, - Description: "foo", - Created: currTime, + ID: nsID, + Message: msgID, + Name: "ns1", + Type: core.NamespaceTypeLocal, + Description: "foo", + Created: currTime, + ContractIndex: 0, } - mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id", "message", "type", "name", "description", "created"}).AddRow(nsID.String(), msgID.String(), core.NamespaceTypeLocal, "ns1", "foo", currTime.String())) + mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id", "message", "type", "name", "description", "created", "contract_index"}).AddRow(nsID.String(), msgID.String(), core.NamespaceTypeLocal, "ns1", "foo", currTime.String(), 0)) ns, err := s.GetNamespaceByID(context.Background(), nsID) assert.NoError(t, err) assert.Equal(t, nsMock, ns) diff --git a/internal/events/blockchain_event.go b/internal/events/blockchain_event.go index 62a5c52d7..561a66ba6 100644 --- a/internal/events/blockchain_event.go +++ b/internal/events/blockchain_event.go @@ -117,7 +117,7 @@ func (em *eventManager) emitBlockchainEventMetric(event *blockchain.Event) { } func (em *eventManager) BlockchainEvent(event *blockchain.EventWithSubscription) error { - return em.retry.Do(em.ctx, "persist contract event", func(attempt int) (bool, error) { + return em.retry.Do(em.ctx, "persist blockchain event", func(attempt int) (bool, error) { err := em.database.RunAsGroup(em.ctx, func(ctx context.Context) error { sub, err := em.getChainListenerByProtocolIDCached(ctx, event.Subscription) if err != nil { diff --git a/internal/events/event_manager.go b/internal/events/event_manager.go index b74080fa5..e90a641a3 100644 --- a/internal/events/event_manager.go +++ b/internal/events/event_manager.go @@ -66,6 +66,7 @@ type EventManager interface { // Bound blockchain callbacks BatchPinComplete(bi blockchain.Plugin, batch *blockchain.BatchPin, signingKey *core.VerifierRef) error BlockchainEvent(event *blockchain.EventWithSubscription) error + BlockchainOperatorAction(bi blockchain.Plugin, action, payload string, signingKey *core.VerifierRef) error // Bound dataexchange callbacks DXEvent(dx dataexchange.Plugin, event dataexchange.DXEvent) diff --git a/internal/events/operator_action.go b/internal/events/operator_action.go new file mode 100644 index 000000000..439429b5f --- /dev/null +++ b/internal/events/operator_action.go @@ -0,0 +1,58 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package events + +import ( + "strconv" + + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly/pkg/blockchain" + "github.com/hyperledger/firefly/pkg/core" +) + +func (em *eventManager) actionMigrate(bi blockchain.Plugin, payload string) error { + ns, err := em.database.GetNamespace(em.ctx, core.SystemNamespace) + if err != nil { + return err + } + idx, err := strconv.Atoi(payload) + if err != nil { + return err + } + if ns.ContractIndex == idx { + log.L(em.ctx).Debugf("Ignoring namespace migration for %s (already at %d)", ns.Name, ns.ContractIndex) + return nil + } + ns.ContractIndex = idx + log.L(em.ctx).Infof("Migrating namespace %s to contract index %d", ns.Name, ns.ContractIndex) + bi.Stop() + if err := bi.Start(ns.ContractIndex); err != nil { + return err + } + return em.database.UpsertNamespace(em.ctx, ns, true) +} + +func (em *eventManager) BlockchainOperatorAction(bi blockchain.Plugin, action, payload string, signingKey *core.VerifierRef) error { + return em.retry.Do(em.ctx, "handle operator action", func(attempt int) (bool, error) { + // TODO: verify signing identity + if action == blockchain.OperatorActionMigrate { + return true, em.actionMigrate(bi, payload) + } + log.L(em.ctx).Errorf("Ignoring unrecognized operator action: %s", action) + return false, nil + }) +} diff --git a/internal/events/operator_action_test.go b/internal/events/operator_action_test.go new file mode 100644 index 000000000..debaba5be --- /dev/null +++ b/internal/events/operator_action_test.go @@ -0,0 +1,148 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package events + +import ( + "fmt" + "testing" + + "github.com/hyperledger/firefly/mocks/blockchainmocks" + "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/pkg/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestOperatorAction(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) + mdi.On("UpsertNamespace", em.ctx, mock.MatchedBy(func(ns *core.Namespace) bool { + return ns.ContractIndex == 1 + }), true).Return(nil) + mbi.On("Stop").Return() + mbi.On("Start", 1).Return(nil) + + err := em.BlockchainOperatorAction(mbi, "migrate", "1", &core.VerifierRef{}) + assert.NoError(t, err) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestOperatorActionUnknown(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + + err := em.BlockchainOperatorAction(mbi, "bad", "", &core.VerifierRef{}) + assert.NoError(t, err) + + mbi.AssertExpectations(t) +} + +func TestActionMigrateQueryFail(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(nil, fmt.Errorf("pop")) + + err := em.actionMigrate(mbi, "1") + assert.EqualError(t, err, "pop") + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestActionMigrateBadIndex(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) + + err := em.actionMigrate(mbi, "!bad") + assert.Regexp(t, "Atoi", err) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestActionMigrateSkip(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{ContractIndex: 1}, nil) + + err := em.actionMigrate(mbi, "1") + assert.NoError(t, err) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestActionMigrateStartFail(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) + mbi.On("Stop").Return(nil) + mbi.On("Start", 1).Return(fmt.Errorf("pop")) + + err := em.actionMigrate(mbi, "1") + assert.EqualError(t, err, "pop") + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestActionMigrateUpsertFail(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) + mdi.On("UpsertNamespace", em.ctx, mock.MatchedBy(func(ns *core.Namespace) bool { + return ns.ContractIndex == 1 + }), true).Return(fmt.Errorf("pop")) + mbi.On("Stop").Return(nil) + mbi.On("Start", 1).Return(nil) + + err := em.actionMigrate(mbi, "1") + assert.EqualError(t, err, "pop") + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go index 9b9f011fe..ca6c1fd21 100644 --- a/internal/orchestrator/bound_callbacks.go +++ b/internal/orchestrator/bound_callbacks.go @@ -59,6 +59,10 @@ func (bc *boundCallbacks) BatchPinComplete(batch *blockchain.BatchPin, signingKe return bc.ei.BatchPinComplete(bc.bi, batch, signingKey) } +func (bc *boundCallbacks) BlockchainOperatorAction(action, payload string, signingKey *core.VerifierRef) error { + return bc.ei.BlockchainOperatorAction(bc.bi, action, payload, signingKey) +} + func (bc *boundCallbacks) DXEvent(event dataexchange.DXEvent) { switch event.Type() { case dataexchange.DXEventTypeTransferResult: diff --git a/internal/orchestrator/bound_callbacks_test.go b/internal/orchestrator/bound_callbacks_test.go index 665c5066a..1232a8320 100644 --- a/internal/orchestrator/bound_callbacks_test.go +++ b/internal/orchestrator/bound_callbacks_test.go @@ -57,6 +57,10 @@ func TestBoundCallbacks(t *testing.T) { err := bc.BatchPinComplete(batch, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}) assert.EqualError(t, err, "pop") + mei.On("BlockchainOperatorAction", mbi, "migrate", "1", &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop")) + err = bc.BlockchainOperatorAction("migrate", "1", &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}) + assert.EqualError(t, err, "pop") + mom.On("SubmitOperationUpdate", mock.Anything, &operations.OperationUpdate{ ID: opID, Status: core.OpStatusFailed, diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 960cd87d2..8d6830738 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -18,6 +18,7 @@ package orchestrator import ( "context" + "strconv" "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -140,6 +141,9 @@ type Orchestrator interface { // Message Routing RequestReply(ctx context.Context, ns string, msg *core.MessageInOut) (reply *core.MessageInOut, err error) + + // Network Operations + MigrateNetwork(ctx context.Context, contractIndex int) error } type orchestrator struct { @@ -221,9 +225,13 @@ func (or *orchestrator) Start() (err error) { if err == nil { err = or.batch.Start() } + var ns *core.Namespace + if err == nil { + ns, err = or.database.GetNamespace(or.ctx, core.SystemNamespace) + } if err == nil { for _, el := range or.blockchains { - if err = el.Start(0); err != nil { + if err = el.Start(ns.ContractIndex); err != nil { break } } @@ -866,3 +874,11 @@ func (or *orchestrator) initNamespaces(ctx context.Context) (err error) { } return or.namespace.Init(ctx, or.database) } + +func (or *orchestrator) MigrateNetwork(ctx context.Context, contractIndex int) error { + verifier, err := or.identity.GetNodeOwnerBlockchainKey(ctx) + if err != nil { + return err + } + return or.blockchain.SubmitOperatorAction(ctx, fftypes.NewUUID(), verifier.Value, blockchain.OperatorActionMigrate, strconv.Itoa(contractIndex)) +} diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 2c64d6808..d196ef769 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -978,6 +978,8 @@ func TestStartTokensFail(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) + or.database = or.mdi + or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) or.mbi.On("Start", 0).Return(nil) or.mba.On("Start").Return(nil) or.mem.On("Start").Return(nil) @@ -994,6 +996,8 @@ func TestStartBlockchainsFail(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) + or.database = or.mdi + or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) or.mbi.On("Start", 0).Return(fmt.Errorf("pop")) or.mba.On("Start").Return(nil) err := or.Start() @@ -1004,6 +1008,8 @@ func TestStartStopOk(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) + or.database = or.mdi + or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) or.mbi.On("Start", 0).Return(nil) or.mba.On("Start").Return(nil) or.mem.On("Start").Return(nil) @@ -1111,3 +1117,20 @@ func TestInitDataExchangeWithNodes(t *testing.T) { err := or.initDataExchange(or.ctx) assert.NoError(t, err) } + +func TestMigrateNetwork(t *testing.T) { + or := newTestOrchestrator() + or.blockchain = or.mbi + verifier := &core.VerifierRef{Value: "0x123"} + or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(verifier, nil) + or.mbi.On("SubmitOperatorAction", context.Background(), mock.Anything, "0x123", "migrate", "1").Return(nil) + err := or.MigrateNetwork(context.Background(), 1) + assert.NoError(t, err) +} + +func TestMigrateNetworkBadKey(t *testing.T) { + or := newTestOrchestrator() + or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(nil, fmt.Errorf("pop")) + err := or.MigrateNetwork(context.Background(), 1) + assert.EqualError(t, err, "pop") +} diff --git a/mocks/blockchainmocks/callbacks.go b/mocks/blockchainmocks/callbacks.go index 5144272be..de7b9830b 100644 --- a/mocks/blockchainmocks/callbacks.go +++ b/mocks/blockchainmocks/callbacks.go @@ -48,3 +48,17 @@ func (_m *Callbacks) BlockchainEvent(event *blockchain.EventWithSubscription) er func (_m *Callbacks) BlockchainOpUpdate(plugin blockchain.Plugin, operationID *fftypes.UUID, txState core.OpStatus, blockchainTXID string, errorMessage string, opOutput fftypes.JSONObject) { _m.Called(plugin, operationID, txState, blockchainTXID, errorMessage, opOutput) } + +// BlockchainOperatorAction provides a mock function with given fields: action, payload, signingKey +func (_m *Callbacks) BlockchainOperatorAction(action string, payload string, signingKey *core.VerifierRef) error { + ret := _m.Called(action, payload, signingKey) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, *core.VerifierRef) error); ok { + r0 = rf(action, payload, signingKey) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go index d0c1ab56b..765d2679f 100644 --- a/mocks/blockchainmocks/plugin.go +++ b/mocks/blockchainmocks/plugin.go @@ -273,6 +273,20 @@ func (_m *Plugin) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, return r0 } +// SubmitOperatorAction provides a mock function with given fields: ctx, operationID, signingKey, action, payload +func (_m *Plugin) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action string, payload string) error { + ret := _m.Called(ctx, operationID, signingKey, action, payload) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, string, string, string) error); ok { + r0 = rf(ctx, operationID, signingKey, action, payload) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // VerifierType provides a mock function with given fields: func (_m *Plugin) VerifierType() core.FFEnum { ret := _m.Called() diff --git a/mocks/eventmocks/event_manager.go b/mocks/eventmocks/event_manager.go index 04df8e4a9..3d801ff83 100644 --- a/mocks/eventmocks/event_manager.go +++ b/mocks/eventmocks/event_manager.go @@ -69,6 +69,20 @@ func (_m *EventManager) BlockchainEvent(event *blockchain.EventWithSubscription) return r0 } +// BlockchainOperatorAction provides a mock function with given fields: bi, action, payload, signingKey +func (_m *EventManager) BlockchainOperatorAction(bi blockchain.Plugin, action string, payload string, signingKey *core.VerifierRef) error { + ret := _m.Called(bi, action, payload, signingKey) + + var r0 error + if rf, ok := ret.Get(0).(func(blockchain.Plugin, string, string, *core.VerifierRef) error); ok { + r0 = rf(bi, action, payload, signingKey) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // CreateUpdateDurableSubscription provides a mock function with given fields: ctx, subDef, mustNew func (_m *EventManager) CreateUpdateDurableSubscription(ctx context.Context, subDef *core.Subscription, mustNew bool) error { ret := _m.Called(ctx, subDef, mustNew) diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go index 1a5e68d3e..3710b5928 100644 --- a/mocks/orchestratormocks/orchestrator.go +++ b/mocks/orchestratormocks/orchestrator.go @@ -1262,6 +1262,20 @@ func (_m *Orchestrator) Metrics() metrics.Manager { return r0 } +// MigrateNetwork provides a mock function with given fields: ctx, contractIndex +func (_m *Orchestrator) MigrateNetwork(ctx context.Context, contractIndex int) error { + ret := _m.Called(ctx, contractIndex) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { + r0 = rf(ctx, contractIndex) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // NetworkMap provides a mock function with given fields: func (_m *Orchestrator) NetworkMap() networkmap.Manager { ret := _m.Called() diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index b8fd02bea..317be26eb 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -55,6 +55,9 @@ type Plugin interface { // SubmitBatchPin sequences a batch of message globally to all viewers of a given ledger SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, signingKey string, batch *BatchPin) error + // SubmitOperatorAction writes a special "BatchPin" event which signals the plugin to take an action + SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action, payload string) error + // InvokeContract submits a new transaction to be executed by custom on-chain logic InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error @@ -80,6 +83,13 @@ type Plugin interface { GenerateEventSignature(ctx context.Context, event *core.FFIEventDefinition) string } +const ( + // OperatorActionMigrate request all network members to stop using the current contract and move to the next one configured + OperatorActionMigrate = "migrate" +) + +const FireFlyActionPrefix = "firefly:" + // Callbacks is the interface provided to the blockchain plugin, to allow it to pass events back to firefly. // // Events must be delivered sequentially, such that event 2 is not delivered until the callback invoked for event 1 @@ -91,16 +101,19 @@ type Callbacks interface { // opOutput can be used to add opaque protocol specific JSON from the plugin (protocol transaction ID etc.) // Note this is an optional hook information, and stored separately to the confirmation of the actual event that was being submitted/sequenced. // Only the party submitting the transaction will see this data. - // - // Error should will only be returned in shutdown scenarios BlockchainOpUpdate(plugin Plugin, operationID *fftypes.UUID, txState TransactionStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) // BatchPinComplete notifies on the arrival of a sequenced batch of messages, which might have been // submitted by us, or by any other authorized party in the network. // - // Error should will only be returned in shutdown scenarios + // Error should only be returned in shutdown scenarios BatchPinComplete(batch *BatchPin, signingKey *core.VerifierRef) error + // BlockchainOperatorAction notifies on the arrival of a network operator action + // + // Error should only be returned in shutdown scenarios + BlockchainOperatorAction(action, payload string, signingKey *core.VerifierRef) error + // BlockchainEvent notifies on the arrival of any event from a user-created subscription. BlockchainEvent(event *EventWithSubscription) error } diff --git a/pkg/core/namespace.go b/pkg/core/namespace.go index 2c2cee297..b004822c1 100644 --- a/pkg/core/namespace.go +++ b/pkg/core/namespace.go @@ -39,12 +39,17 @@ var ( // Namespace is a isolate set of named resources, to allow multiple applications to co-exist in the same network, with the same named objects. // Can be used for use case segregation, or multi-tenancy. type Namespace struct { - ID *fftypes.UUID `ffstruct:"Namespace" json:"id" ffexcludeinput:"true"` - Message *fftypes.UUID `ffstruct:"Namespace" json:"message,omitempty" ffexcludeinput:"true"` - Name string `ffstruct:"Namespace" json:"name"` - Description string `ffstruct:"Namespace" json:"description"` - Type NamespaceType `ffstruct:"Namespace" json:"type" ffenum:"namespacetype" ffexcludeinput:"true"` - Created *fftypes.FFTime `ffstruct:"Namespace" json:"created" ffexcludeinput:"true"` + ID *fftypes.UUID `ffstruct:"Namespace" json:"id" ffexcludeinput:"true"` + Message *fftypes.UUID `ffstruct:"Namespace" json:"message,omitempty" ffexcludeinput:"true"` + Name string `ffstruct:"Namespace" json:"name"` + Description string `ffstruct:"Namespace" json:"description"` + Type NamespaceType `ffstruct:"Namespace" json:"type" ffenum:"namespacetype" ffexcludeinput:"true"` + Created *fftypes.FFTime `ffstruct:"Namespace" json:"created" ffexcludeinput:"true"` + ContractIndex int `ffstruct:"Namespace" json:"contractIndex" ffexcludeinput:"true"` +} + +type NamespaceMigration struct { + ContractIndex int `ffstruct:"Namespace" json:"contractIndex" ` } func (ns *Namespace) Validate(ctx context.Context, existing bool) (err error) { diff --git a/pkg/database/plugin.go b/pkg/database/plugin.go index f8c45aca8..0316cdb58 100644 --- a/pkg/database/plugin.go +++ b/pkg/database/plugin.go @@ -67,13 +67,13 @@ type iNamespaceCollection interface { DeleteNamespace(ctx context.Context, id *fftypes.UUID) (err error) // GetNamespace - Get an namespace by name - GetNamespace(ctx context.Context, name string) (offset *core.Namespace, err error) + GetNamespace(ctx context.Context, name string) (namespace *core.Namespace, err error) // GetNamespaceByID - Get a namespace by ID - GetNamespaceByID(ctx context.Context, id *fftypes.UUID) (offset *core.Namespace, err error) + GetNamespaceByID(ctx context.Context, id *fftypes.UUID) (namespace *core.Namespace, err error) // GetNamespaces - Get namespaces - GetNamespaces(ctx context.Context, filter Filter) (offset []*core.Namespace, res *FilterResult, err error) + GetNamespaces(ctx context.Context, filter Filter) (namespaces []*core.Namespace, res *FilterResult, err error) } type iMessageCollection interface { From 6f8257fc76dc4a7543d40c53fe937c5580bb2993 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Tue, 24 May 2022 12:37:13 -0400 Subject: [PATCH 05/13] Store termination info for every previous FireFly contract Signed-off-by: Andrew Richardson --- ...00090_add_namespace_contractindex.down.sql | 2 +- .../000090_add_namespace_contractindex.up.sql | 2 +- ...00090_add_namespace_contractindex.down.sql | 2 +- .../000090_add_namespace_contractindex.up.sql | 2 +- docs/swagger/swagger.yaml | 216 +++++++++++-- .../apiserver/route_post_network_migrate.go | 6 +- .../route_post_network_migrate_test.go | 4 +- internal/blockchain/ethereum/config.go | 1 + internal/blockchain/ethereum/ethereum.go | 302 ++++++++++-------- internal/blockchain/ethereum/ethereum_test.go | 207 ++++++------ internal/blockchain/ethereum/eventstream.go | 7 +- internal/blockchain/fabric/config.go | 1 + internal/blockchain/fabric/eventstream.go | 9 +- internal/blockchain/fabric/fabric.go | 279 +++++++++------- internal/blockchain/fabric/fabric_test.go | 81 ++--- internal/coremsgs/en_error_messages.go | 1 + internal/coremsgs/en_struct_descriptions.go | 19 +- internal/database/sqlcommon/namespace_sql.go | 8 +- .../database/sqlcommon/namespace_sql_test.go | 31 +- internal/events/event_manager.go | 2 +- internal/events/operator_action.go | 55 ++-- internal/events/operator_action_test.go | 88 ++--- internal/orchestrator/bound_callbacks.go | 4 +- internal/orchestrator/bound_callbacks_test.go | 5 +- internal/orchestrator/orchestrator.go | 15 +- internal/orchestrator/orchestrator_test.go | 17 +- mocks/blockchainmocks/callbacks.go | 10 +- mocks/blockchainmocks/plugin.go | 53 ++- mocks/eventmocks/event_manager.go | 10 +- mocks/orchestratormocks/orchestrator.go | 10 +- pkg/blockchain/plugin.go | 26 +- pkg/core/namespace.go | 50 ++- 32 files changed, 888 insertions(+), 637 deletions(-) diff --git a/db/migrations/postgres/000090_add_namespace_contractindex.down.sql b/db/migrations/postgres/000090_add_namespace_contractindex.down.sql index 30a75a161..ba47391d6 100644 --- a/db/migrations/postgres/000090_add_namespace_contractindex.down.sql +++ b/db/migrations/postgres/000090_add_namespace_contractindex.down.sql @@ -1,3 +1,3 @@ BEGIN; -ALTER TABLE namespaces DROP COLUMN contract_index; +ALTER TABLE namespaces DROP COLUMN firefly_contracts; COMMIT; diff --git a/db/migrations/postgres/000090_add_namespace_contractindex.up.sql b/db/migrations/postgres/000090_add_namespace_contractindex.up.sql index 370aae8d1..20ef62799 100644 --- a/db/migrations/postgres/000090_add_namespace_contractindex.up.sql +++ b/db/migrations/postgres/000090_add_namespace_contractindex.up.sql @@ -1,3 +1,3 @@ BEGIN; -ALTER TABLE namespaces ADD COLUMN contract_index INTEGER DEFAULT 0; +ALTER TABLE namespaces ADD COLUMN firefly_contracts TEXT; COMMIT; diff --git a/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql b/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql index 2899a529f..384148d1c 100644 --- a/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql +++ b/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql @@ -1 +1 @@ -ALTER TABLE namespaces DROP COLUMN contract_index; +ALTER TABLE namespaces DROP COLUMN firefly_contracts; diff --git a/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql b/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql index 93f8a8d02..2230dde7b 100644 --- a/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql +++ b/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql @@ -1 +1 @@ -ALTER TABLE namespaces ADD COLUMN contract_index INTEGER DEFAULT 0; +ALTER TABLE namespaces ADD COLUMN firefly_contracts TEXT; diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index f8ed2b0ed..90fd7f343 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -8136,10 +8136,6 @@ paths: schema: items: properties: - contractIndex: - description: The index of the configured FireFly smart contract - currently being used - type: integer created: description: The time the namespace was created format: date-time @@ -8147,6 +8143,52 @@ paths: description: description: A description of the namespace type: string + fireflyContract: + description: Info on the FireFly smart contract configured for + this namespace + properties: + active: + description: The currently active FireFly smart contract + properties: + finalEvent: + description: The identifier for the final blockchain + event received from this contract before termination + type: string + index: + description: The index of this contract in the config + file + type: integer + info: + additionalProperties: + description: Blockchain-specific info on the contract, + such as its location on chain + description: Blockchain-specific info on the contract, + such as its location on chain + type: object + type: object + terminated: + description: Previously-terminated FireFly smart contracts + items: + description: Previously-terminated FireFly smart contracts + properties: + finalEvent: + description: The identifier for the final blockchain + event received from this contract before termination + type: string + index: + description: The index of this contract in the config + file + type: integer + info: + additionalProperties: + description: Blockchain-specific info on the contract, + such as its location on chain + description: Blockchain-specific info on the contract, + such as its location on chain + type: object + type: object + type: array + type: object id: description: The UUID of the namespace. For locally established namespaces will be different on each node in the network. @@ -8210,10 +8252,6 @@ paths: application/json: schema: properties: - contractIndex: - description: The index of the configured FireFly smart contract - currently being used - type: integer created: description: The time the namespace was created format: date-time @@ -8221,6 +8259,52 @@ paths: description: description: A description of the namespace type: string + fireflyContract: + description: Info on the FireFly smart contract configured for + this namespace + properties: + active: + description: The currently active FireFly smart contract + properties: + finalEvent: + description: The identifier for the final blockchain event + received from this contract before termination + type: string + index: + description: The index of this contract in the config + file + type: integer + info: + additionalProperties: + description: Blockchain-specific info on the contract, + such as its location on chain + description: Blockchain-specific info on the contract, + such as its location on chain + type: object + type: object + terminated: + description: Previously-terminated FireFly smart contracts + items: + description: Previously-terminated FireFly smart contracts + properties: + finalEvent: + description: The identifier for the final blockchain + event received from this contract before termination + type: string + index: + description: The index of this contract in the config + file + type: integer + info: + additionalProperties: + description: Blockchain-specific info on the contract, + such as its location on chain + description: Blockchain-specific info on the contract, + such as its location on chain + type: object + type: object + type: array + type: object id: description: The UUID of the namespace. For locally established namespaces will be different on each node in the network. For @@ -8249,10 +8333,6 @@ paths: application/json: schema: properties: - contractIndex: - description: The index of the configured FireFly smart contract - currently being used - type: integer created: description: The time the namespace was created format: date-time @@ -8260,6 +8340,52 @@ paths: description: description: A description of the namespace type: string + fireflyContract: + description: Info on the FireFly smart contract configured for + this namespace + properties: + active: + description: The currently active FireFly smart contract + properties: + finalEvent: + description: The identifier for the final blockchain event + received from this contract before termination + type: string + index: + description: The index of this contract in the config + file + type: integer + info: + additionalProperties: + description: Blockchain-specific info on the contract, + such as its location on chain + description: Blockchain-specific info on the contract, + such as its location on chain + type: object + type: object + terminated: + description: Previously-terminated FireFly smart contracts + items: + description: Previously-terminated FireFly smart contracts + properties: + finalEvent: + description: The identifier for the final blockchain + event received from this contract before termination + type: string + index: + description: The index of this contract in the config + file + type: integer + info: + additionalProperties: + description: Blockchain-specific info on the contract, + such as its location on chain + description: Blockchain-specific info on the contract, + such as its location on chain + type: object + type: object + type: array + type: object id: description: The UUID of the namespace. For locally established namespaces will be different on each node in the network. For @@ -8312,10 +8438,6 @@ paths: application/json: schema: properties: - contractIndex: - description: The index of the configured FireFly smart contract - currently being used - type: integer created: description: The time the namespace was created format: date-time @@ -8323,6 +8445,52 @@ paths: description: description: A description of the namespace type: string + fireflyContract: + description: Info on the FireFly smart contract configured for + this namespace + properties: + active: + description: The currently active FireFly smart contract + properties: + finalEvent: + description: The identifier for the final blockchain event + received from this contract before termination + type: string + index: + description: The index of this contract in the config + file + type: integer + info: + additionalProperties: + description: Blockchain-specific info on the contract, + such as its location on chain + description: Blockchain-specific info on the contract, + such as its location on chain + type: object + type: object + terminated: + description: Previously-terminated FireFly smart contracts + items: + description: Previously-terminated FireFly smart contracts + properties: + finalEvent: + description: The identifier for the final blockchain + event received from this contract before termination + type: string + index: + description: The index of this contract in the config + file + type: integer + info: + additionalProperties: + description: Blockchain-specific info on the contract, + such as its location on chain + description: Blockchain-specific info on the contract, + such as its location on chain + type: object + type: object + type: array + type: object id: description: The UUID of the namespace. For locally established namespaces will be different on each node in the network. For @@ -22427,24 +22595,12 @@ paths: requestBody: content: application/json: - schema: - properties: - contractIndex: - description: The index of the configured FireFly smart contract - currently being used - type: integer - type: object + schema: {} responses: "202": content: application/json: - schema: - properties: - contractIndex: - description: The index of the configured FireFly smart contract - currently being used - type: integer - type: object + schema: {} description: Success default: description: "" diff --git a/internal/apiserver/route_post_network_migrate.go b/internal/apiserver/route_post_network_migrate.go index c0c7ae556..bc4ece492 100644 --- a/internal/apiserver/route_post_network_migrate.go +++ b/internal/apiserver/route_post_network_migrate.go @@ -32,11 +32,11 @@ var postNetworkMigrate = &oapispec.Route{ QueryParams: nil, FilterFactory: nil, Description: coremsgs.APIEndpointsPostNetworkMigrate, - JSONInputValue: func() interface{} { return &core.NamespaceMigration{} }, - JSONOutputValue: func() interface{} { return &core.NamespaceMigration{} }, + JSONInputValue: func() interface{} { return &core.NamespaceMigrationRequest{} }, + JSONOutputValue: func() interface{} { return &core.NamespaceMigrationRequest{} }, JSONOutputCodes: []int{http.StatusAccepted}, JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { - err = getOr(r.Ctx).MigrateNetwork(r.Ctx, r.Input.(*core.NamespaceMigration).ContractIndex) + err = getOr(r.Ctx).MigrateNetwork(r.Ctx) return r.Input, err }, } diff --git a/internal/apiserver/route_post_network_migrate_test.go b/internal/apiserver/route_post_network_migrate_test.go index 62a64d998..672ae81e1 100644 --- a/internal/apiserver/route_post_network_migrate_test.go +++ b/internal/apiserver/route_post_network_migrate_test.go @@ -29,14 +29,14 @@ import ( func TestPostNetworkMigrate(t *testing.T) { o, r := newTestAPIServer() - input := core.NamespaceMigration{ContractIndex: 1} + input := core.NamespaceMigrationRequest{} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) req := httptest.NewRequest("POST", "/api/v1/network/migrate", &buf) req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() - o.On("MigrateNetwork", mock.Anything, 1).Return(nil) + o.On("MigrateNetwork", mock.Anything).Return(nil) r.ServeHTTP(res, req) assert.Equal(t, 202, res.Result().StatusCode) diff --git a/internal/blockchain/ethereum/config.go b/internal/blockchain/ethereum/config.go index 8d8ad8f73..7bca63bfb 100644 --- a/internal/blockchain/ethereum/config.go +++ b/internal/blockchain/ethereum/config.go @@ -96,6 +96,7 @@ func (e *Ethereum) InitConfig(config config.Section) { e.contractConf = config.SubArray(FireFlyContractConfigKey) e.contractConf.AddKnownKey(FireFlyContractAddress) e.contractConf.AddKnownKey(FireFlyContractFromBlock, "oldest") + e.contractConfSize = e.contractConf.ArraySize() fftmConf := config.SubSection(FFTMConfigKey) ffresty.InitConfig(fftmConf) diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 7fd094786..15ef21ce4 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -48,26 +48,29 @@ const ( ) type Ethereum struct { - ctx context.Context - topic string - contractAddress string - prefixShort string - prefixLong string - capabilities *blockchain.Capabilities - callbacks blockchain.Callbacks - client *resty.Client - fftmClient *resty.Client - streams *streamManager - initInfo struct { + ctx context.Context + topic string + fireflyContract string + fireflyFromBlock string + finalEvents map[string]string + prefixShort string + prefixLong string + capabilities *blockchain.Capabilities + callbacks blockchain.Callbacks + client *resty.Client + fftmClient *resty.Client + streams *streamManager + initInfo struct { stream *eventStream sub *subscription } - wsconn wsclient.WSClient - closed chan struct{} - addressResolver *addressResolver - metrics metrics.Manager - ethconnectConf config.Section - contractConf config.ArraySection + wsconn wsclient.WSClient + closed chan struct{} + addressResolver *addressResolver + metrics metrics.Manager + ethconnectConf config.Section + contractConf config.ArraySection + contractConfSize int } type eventStreamWebsocket struct { @@ -160,6 +163,7 @@ func (e *Ethereum) VerifierType() core.VerifierType { func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager) (err error) { e.InitConfig(config) + ethconnectConf := e.ethconnectConf addressResolverConf := config.SubSection(AddressResolverConfigKey) fftmConf := config.SubSection(FFTMConfigKey) @@ -174,51 +178,74 @@ func (e *Ethereum) Init(ctx context.Context, config config.Section, callbacks bl } } - if e.ethconnectConf.GetString(ffresty.HTTPConfigURL) == "" { + if ethconnectConf.GetString(ffresty.HTTPConfigURL) == "" { return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "blockchain.ethereum.ethconnect") } + e.client = ffresty.New(e.ctx, ethconnectConf) - e.client = ffresty.New(e.ctx, e.ethconnectConf) if fftmConf.GetString(ffresty.HTTPConfigURL) != "" { e.fftmClient = ffresty.New(e.ctx, fftmConf) } - e.topic = e.ethconnectConf.GetString(EthconnectConfigTopic) + e.topic = ethconnectConf.GetString(EthconnectConfigTopic) if e.topic == "" { return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "topic", "blockchain.ethereum.ethconnect") } - e.prefixShort = e.ethconnectConf.GetString(EthconnectPrefixShort) - e.prefixLong = e.ethconnectConf.GetString(EthconnectPrefixLong) + e.prefixShort = ethconnectConf.GetString(EthconnectPrefixShort) + e.prefixLong = ethconnectConf.GetString(EthconnectPrefixLong) + + wsConfig := wsclient.GenerateConfig(ethconnectConf) + if wsConfig.WSKeyPath == "" { + wsConfig.WSKeyPath = "/ws" + } + e.wsconn, err = wsclient.New(ctx, wsConfig, nil, e.afterConnect) + if err != nil { + return err + } + + e.streams = &streamManager{client: e.client} + batchSize := ethconnectConf.GetUint(EthconnectConfigBatchSize) + batchTimeout := uint(ethconnectConf.GetDuration(EthconnectConfigBatchTimeout).Milliseconds()) + if e.initInfo.stream, err = e.streams.ensureEventStream(e.ctx, e.topic, batchSize, batchTimeout); err != nil { + return err + } + log.L(e.ctx).Infof("Event stream: %s (topic=%s)", e.initInfo.stream.ID, e.topic) + + e.closed = make(chan struct{}) + go e.eventLoop() return nil } -func (e *Ethereum) initStreams(contractIndex int) (err error) { +func (e *Ethereum) Start() (err error) { + return e.wsconn.Connect() +} - ctx := e.ctx - e.streams = &streamManager{client: e.client} - if e.contractConf.ArraySize() > 0 || contractIndex > 0 { +func (e *Ethereum) resolveFireFlyContract(ctx context.Context, contractIndex int) (address, fromBlock string, err error) { + + if e.contractConfSize > 0 || contractIndex > 0 { // New config (array of objects under "fireflyContract") - if contractIndex >= e.contractConf.ArraySize() { - return i18n.NewError(ctx, coremsgs.MsgInvalidFireFlyContractIndex, fmt.Sprintf("blockchain.ethereum.fireflyContract[%d]", contractIndex)) + if contractIndex >= e.contractConfSize { + return "", "", i18n.NewError(ctx, coremsgs.MsgInvalidFireFlyContractIndex, fmt.Sprintf("blockchain.ethereum.fireflyContract[%d]", contractIndex)) } - e.contractAddress = e.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractAddress) - if e.contractAddress == "" { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "address", "blockchain.ethereum.fireflyContract") + entry := e.contractConf.ArrayEntry(contractIndex) + address = entry.GetString(FireFlyContractAddress) + if address == "" { + return "", "", i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "address", "blockchain.ethereum.fireflyContract") } - e.streams.fireFlySubscriptionFromBlock = e.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractFromBlock) + fromBlock = entry.GetString(FireFlyContractFromBlock) } else { // Old config (attributes under "ethconnect") - e.contractAddress = e.ethconnectConf.GetString(EthconnectConfigInstanceDeprecated) - if e.contractAddress != "" { + address = e.ethconnectConf.GetString(EthconnectConfigInstanceDeprecated) + if address != "" { log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead", EthconnectConfigKey, EthconnectConfigInstanceDeprecated, FireFlyContractConfigKey, FireFlyContractAddress) } else { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "instance", "blockchain.ethereum.ethconnect") + return "", "", i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "instance", "blockchain.ethereum.ethconnect") } - e.streams.fireFlySubscriptionFromBlock = e.ethconnectConf.GetString(EthconnectConfigFromBlockDeprecated) - if e.streams.fireFlySubscriptionFromBlock != "" { + fromBlock = e.ethconnectConf.GetString(EthconnectConfigFromBlockDeprecated) + if fromBlock != "" { log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead", EthconnectConfigKey, EthconnectConfigFromBlockDeprecated, FireFlyContractConfigKey, FireFlyContractFromBlock) @@ -226,55 +253,67 @@ func (e *Ethereum) initStreams(contractIndex int) (err error) { } // Backwards compatibility from when instance path was not a contract address - if strings.HasPrefix(strings.ToLower(e.contractAddress), "/contracts/") { - address, err := e.getContractAddress(ctx, e.contractAddress) + if strings.HasPrefix(strings.ToLower(address), "/contracts/") { + address, err = e.getContractAddress(ctx, address) if err != nil { - return err + return "", "", err } - e.contractAddress = address - } else if strings.HasPrefix(e.contractAddress, "/instances/") { - e.contractAddress = strings.Replace(e.contractAddress, "/instances/", "", 1) + } else if strings.HasPrefix(address, "/instances/") { + address = strings.Replace(address, "/instances/", "", 1) } - // Ethconnect needs the "0x" prefix in some cases - if !strings.HasPrefix(e.contractAddress, "0x") { - e.contractAddress = fmt.Sprintf("0x%s", e.contractAddress) - } + address, err = validateEthAddress(ctx, address) + return address, fromBlock, err +} - wsConfig := wsclient.GenerateConfig(e.ethconnectConf) - if wsConfig.WSKeyPath == "" { - wsConfig.WSKeyPath = "/ws" +func (e *Ethereum) ConfigureContract(contracts *core.FireFlyContracts) (err error) { + + finalEvents := make(map[string]string, len(contracts.Terminated)) + for i, oldContract := range contracts.Terminated { + address := oldContract.Info.GetString("address") + if address != "" { + finalEvents[address] = contracts.Terminated[i].FinalEvent + } } - e.wsconn, err = wsclient.New(ctx, wsConfig, nil, e.afterConnect) + + log.L(e.ctx).Infof("Resolving FireFly contract at index %d", contracts.Active.Index) + address, fromBlock, err := e.resolveFireFlyContract(e.ctx, contracts.Active.Index) if err != nil { return err } - - batchSize := e.ethconnectConf.GetUint(EthconnectConfigBatchSize) - batchTimeout := uint(e.ethconnectConf.GetDuration(EthconnectConfigBatchTimeout).Milliseconds()) - if e.initInfo.stream, err = e.streams.ensureEventStream(e.ctx, e.topic, batchSize, batchTimeout); err != nil { - return err + if _, ok := finalEvents[address]; ok { + return i18n.NewError(e.ctx, coremsgs.MsgCannotReuseFireFlyContract, address) } - log.L(e.ctx).Infof("Event stream: %s (topic=%s)", e.initInfo.stream.ID, e.topic) - if e.initInfo.sub, err = e.streams.ensureFireFlySubscription(e.ctx, e.contractAddress, e.initInfo.stream.ID, batchPinEventABI); err != nil { - return err + contracts.Active.Info = fftypes.JSONObject{ + "address": address, + "fromBlock": fromBlock, } - e.closed = make(chan struct{}) - go e.eventLoop() - - return nil + e.fireflyContract = address + e.fireflyFromBlock = fromBlock + e.finalEvents = finalEvents + e.initInfo.sub, err = e.streams.ensureFireFlySubscription(e.ctx, e.fireflyContract, e.fireflyFromBlock, e.initInfo.stream.ID, batchPinEventABI) + return err } -func (e *Ethereum) Start(contractIndex int) (err error) { - if err = e.initStreams(contractIndex); err != nil { +func (e *Ethereum) TerminateContract(contracts *core.FireFlyContracts, termination *blockchain.Event) (err error) { + + address, err := validateEthAddress(e.ctx, termination.Info.GetString("address")) + if err != nil { return err } - return e.wsconn.Connect() -} - -func (e *Ethereum) Stop() { - e.wsconn.Close() + if address != e.fireflyContract { + log.L(e.ctx).Warnf("Ignoring termination request from address %s, which differs from active address %s", address, e.fireflyContract) + return nil + } + log.L(e.ctx).Infof("Processing termination request from address %s", address) + contracts.Terminated = append(contracts.Terminated, core.FireFlyContractInfo{ + Index: contracts.Active.Index, + Info: contracts.Active.Info, + FinalEvent: termination.ProtocolID, + }) + contracts.Active = core.FireFlyContractInfo{Index: contracts.Active.Index + 1} + return e.ConfigureContract(contracts) } func (e *Ethereum) Capabilities() *blockchain.Capabilities { @@ -304,31 +343,67 @@ func ethHexFormatB32(b *fftypes.Bytes32) string { return "0x" + hex.EncodeToString(b[0:32]) } -func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONObject) (err error) { +func (e *Ethereum) parseBlockchainEvent(ctx context.Context, msgJSON fftypes.JSONObject) *blockchain.Event { sBlockNumber := msgJSON.GetString("blockNumber") sTransactionHash := msgJSON.GetString("transactionHash") blockNumber := msgJSON.GetInt64("blockNumber") txIndex := msgJSON.GetInt64("transactionIndex") logIndex := msgJSON.GetInt64("logIndex") dataJSON := msgJSON.GetObject("data") - authorAddress := dataJSON.GetString("author") - nsOrAction := dataJSON.GetString("namespace") - sUUIDs := dataJSON.GetString("uuids") - sBatchHash := dataJSON.GetString("batchHash") - sPayloadRef := dataJSON.GetString("payloadRef") - sContexts := dataJSON.GetStringArray("contexts") + signature := msgJSON.GetString("signature") + name := strings.SplitN(signature, "(", 2)[0] timestampStr := msgJSON.GetString("timestamp") timestamp, err := fftypes.ParseTimeString(timestampStr) if err != nil { - log.L(ctx).Errorf("BatchPin event is not valid - missing timestamp: %+v", msgJSON) + log.L(ctx).Errorf("Blockchain event is not valid - missing timestamp: %+v", msgJSON) + return nil // move on + } + + if sBlockNumber == "" || sTransactionHash == "" { + log.L(ctx).Errorf("Blockchain event is not valid - missing data: %+v", msgJSON) + return nil // move on + } + + delete(msgJSON, "data") + return &blockchain.Event{ + BlockchainTXID: sTransactionHash, + Source: e.Name(), + Name: name, + ProtocolID: fmt.Sprintf("%.12d/%.6d/%.6d", blockNumber, txIndex, logIndex), + Output: dataJSON, + Info: msgJSON, + Timestamp: timestamp, + Location: e.buildEventLocationString(msgJSON), + Signature: signature, + } +} + +func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONObject) (err error) { + event := e.parseBlockchainEvent(ctx, msgJSON) + if event == nil { + return nil // move on + } + address, err := validateEthAddress(ctx, event.Info.GetString("address")) + if err != nil { + log.L(ctx).Errorf("Failed to parse blockchain event address: %s", err) return nil // move on } - if sBlockNumber == "" || - sTransactionHash == "" || - authorAddress == "" || - sUUIDs == "" || - sBatchHash == "" { + if finalEvent, ok := e.finalEvents[address]; ok { + if event.ProtocolID > finalEvent { + log.L(ctx).Warnf("Ignoring BatchPin event %s received after termination event %s", event.ProtocolID, finalEvent) + return nil // move on + } + } + + authorAddress := event.Output.GetString("author") + nsOrAction := event.Output.GetString("namespace") + sUUIDs := event.Output.GetString("uuids") + sBatchHash := event.Output.GetString("batchHash") + sPayloadRef := event.Output.GetString("payloadRef") + sContexts := event.Output.GetStringArray("contexts") + + if authorAddress == "" || sUUIDs == "" || sBatchHash == "" { log.L(ctx).Errorf("BatchPin event is not valid - missing data: %+v", msgJSON) return nil // move on } @@ -346,7 +421,7 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON // Check if this is actually an operator action if strings.HasPrefix(nsOrAction, blockchain.FireFlyActionPrefix) { action := nsOrAction[len(blockchain.FireFlyActionPrefix):] - return e.callbacks.BlockchainOperatorAction(action, sPayloadRef, verifier) + return e.callbacks.BlockchainOperatorAction(action, event, verifier) } hexUUIDs, err := hex.DecodeString(strings.TrimPrefix(sUUIDs, "0x")) @@ -377,7 +452,6 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON contexts[i] = &hash } - delete(msgJSON, "data") batch := &blockchain.BatchPin{ Namespace: nsOrAction, TransactionID: &txnID, @@ -385,17 +459,7 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON BatchHash: &batchHash, BatchPayloadRef: sPayloadRef, Contexts: contexts, - Event: blockchain.Event{ - BlockchainTXID: sTransactionHash, - Source: e.Name(), - Name: "BatchPin", - ProtocolID: fmt.Sprintf("%.12d/%.6d/%.6d", blockNumber, txIndex, logIndex), - Output: dataJSON, - Info: msgJSON, - Timestamp: timestamp, - Location: e.buildEventLocationString(msgJSON), - Signature: msgJSON.GetString("signature"), - }, + Event: *event, } // If there's an error dispatching the event, we must return the error and shutdown @@ -403,38 +467,14 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON } func (e *Ethereum) handleContractEvent(ctx context.Context, msgJSON fftypes.JSONObject) (err error) { - sTransactionHash := msgJSON.GetString("transactionHash") - blockNumber := msgJSON.GetInt64("blockNumber") - txIndex := msgJSON.GetInt64("transactionIndex") - logIndex := msgJSON.GetInt64("logIndex") - sub := msgJSON.GetString("subId") - signature := msgJSON.GetString("signature") - dataJSON := msgJSON.GetObject("data") - name := strings.SplitN(signature, "(", 2)[0] - timestampStr := msgJSON.GetString("timestamp") - timestamp, err := fftypes.ParseTimeString(timestampStr) - if err != nil { - log.L(ctx).Errorf("Contract event is not valid - missing timestamp: %+v", msgJSON) - return err // move on - } - delete(msgJSON, "data") - - event := &blockchain.EventWithSubscription{ - Subscription: sub, - Event: blockchain.Event{ - BlockchainTXID: sTransactionHash, - Source: e.Name(), - Name: name, - ProtocolID: fmt.Sprintf("%.12d/%.6d/%.6d", blockNumber, txIndex, logIndex), - Output: dataJSON, - Info: msgJSON, - Timestamp: timestamp, - Location: e.buildEventLocationString(msgJSON), - Signature: msgJSON.GetString("signature"), - }, + event := e.parseBlockchainEvent(ctx, msgJSON) + if event == nil { + return nil // move on } - - return e.callbacks.BlockchainEvent(event) + return e.callbacks.BlockchainEvent(&blockchain.EventWithSubscription{ + Event: *event, + Subscription: msgJSON.GetString("subId"), + }) } func (e *Ethereum) handleReceipt(ctx context.Context, reply fftypes.JSONObject) { @@ -637,18 +677,18 @@ func (e *Ethereum) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID batch.BatchPayloadRef, ethHashes, } - return e.invokeContractMethod(ctx, e.contractAddress, signingKey, batchPinMethodABI, operationID.String(), input) + return e.invokeContractMethod(ctx, e.fireflyContract, signingKey, batchPinMethodABI, operationID.String(), input) } -func (e *Ethereum) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action, payload string) error { +func (e *Ethereum) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action string) error { input := []interface{}{ blockchain.FireFlyActionPrefix + action, ethHexFormatB32(nil), ethHexFormatB32(nil), - payload, + "", []string{}, } - return e.invokeContractMethod(ctx, e.contractAddress, signingKey, batchPinMethodABI, operationID.String(), input) + return e.invokeContractMethod(ctx, e.fireflyContract, signingKey, batchPinMethodABI, operationID.String(), input) } func (e *Ethereum) InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error { diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index cccc933b7..c9967cd6d 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -84,7 +84,7 @@ func newTestEthereum() (*Ethereum, func()) { e := &Ethereum{ ctx: ctx, client: resty.New().SetBaseURL("http://localhost:12345"), - contractAddress: "/instances/0x12345", + fireflyContract: "/instances/0x12345", topic: "topic1", prefixShort: defaultPrefixShort, prefixLong: defaultPrefixLong, @@ -118,19 +118,6 @@ func TestInitBadAddressResolver(t *testing.T) { assert.Regexp(t, "FF10337.*urlTemplate", err) } -func TestInitMissingInstance(t *testing.T) { - e, cancel := newTestEthereum() - defer cancel() - resetConf(e) - utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") - utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.initStreams(0) - assert.Regexp(t, "FF10138.*instance", err) -} - func TestInitMissingTopic(t *testing.T) { e, cancel := newTestEthereum() defer cancel() @@ -176,7 +163,7 @@ func TestInitAndStartWithFFTM(t *testing.T) { resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") utFFTMConf.Set(ffresty.HTTPConfigURL, "http://fftm.example.com:12345") @@ -187,7 +174,7 @@ func TestInitAndStartWithFFTM(t *testing.T) { assert.Equal(t, "ethereum", e.Name()) assert.Equal(t, core.VerifierTypeEthAddress, e.VerifierType()) - err = e.Start(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, 4, httpmock.GetTotalCallCount()) @@ -195,6 +182,9 @@ func TestInitAndStartWithFFTM(t *testing.T) { assert.Equal(t, "sub12345", e.initInfo.sub.ID) assert.NotNil(t, e.Capabilities()) + err = e.Start() + assert.NoError(t, err) + startupMessage := <-toServer assert.Equal(t, `{"type":"listen","topic":"topic1"}`, startupMessage) startupMessage = <-toServer @@ -217,25 +207,37 @@ func TestWSInitFail(t *testing.T) { resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "!!!://") - utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.Start(0) assert.Regexp(t, "FF00149", err) } -func TestWSClose(t *testing.T) { +func TestInitMissingInstance(t *testing.T) { - wsm := &wsmocks.WSClient{} - e := &Ethereum{ - ctx: context.Background(), - wsconn: wsm, - } - wsm.On("Close").Return(nil) - e.Stop() + e, cancel := newTestEthereum() + defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + + resetConf(e) + utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.ConfigureContract(&core.FireFlyContracts{}) + assert.Regexp(t, "FF10138.*instance", err) } @@ -252,7 +254,7 @@ func TestInitAllExistingStreams(t *testing.T) { httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}})) httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", httpmock.NewJsonResponderOrPanic(200, []subscription{ - {ID: "sub12345", Stream: "es12345", Name: "BatchPin_30783132333435e3" /* this is the subname for our combo of instance path and BatchPin */}, + {ID: "sub12345", Stream: "es12345", Name: "BatchPin_3078373163373635" /* this is the subname for our combo of instance path and BatchPin */}, })) httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345", httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}})) @@ -260,12 +262,12 @@ func TestInitAllExistingStreams(t *testing.T) { resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, 3, httpmock.GetTotalCallCount()) @@ -299,7 +301,7 @@ func TestInitOldInstancePathContracts(t *testing.T) { httpmock.RegisterResponder("GET", "http://localhost:12345/contracts/firefly", httpmock.NewJsonResponderOrPanic(200, map[string]string{ "created": "2022-02-08T22:10:10Z", - "address": "12345", + "address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "path": "/contracts/firefly", "abi": "fc49dec3-0660-4dc7-61af-65af4c3ac456", "openapi": "/contracts/firefly?swagger", @@ -315,10 +317,10 @@ func TestInitOldInstancePathContracts(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.NoError(t, err) - assert.Equal(t, e.contractAddress, "0x12345") + assert.Equal(t, e.fireflyContract, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f") } func TestInitOldInstancePathInstances(t *testing.T) { @@ -347,15 +349,15 @@ func TestInitOldInstancePathInstances(t *testing.T) { resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.NoError(t, err) - assert.Equal(t, e.contractAddress, "0x12345") + assert.Equal(t, e.fireflyContract, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f") } func TestInitOldInstancePathError(t *testing.T) { @@ -392,7 +394,7 @@ func TestInitOldInstancePathError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.Regexp(t, "FF10111.*pop", err) } @@ -412,25 +414,20 @@ func TestInitNewConfig(t *testing.T) { httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", httpmock.NewJsonResponderOrPanic(200, []subscription{})) httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", - func(req *http.Request) (*http.Response, error) { - var body map[string]interface{} - json.NewDecoder(req.Body).Decode(&body) - assert.Equal(t, "es12345", body["stream"]) - return httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sub12345"})(req) - }) + httpmock.NewJsonResponderOrPanic(200, subscription{})) resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x12345") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.NoError(t, err) - assert.Equal(t, e.contractAddress, "0x12345") + assert.Equal(t, e.fireflyContract, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f") } func TestInitNewConfigError(t *testing.T) { @@ -442,6 +439,11 @@ func TestInitNewConfigError(t *testing.T) { httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) @@ -450,7 +452,7 @@ func TestInitNewConfigError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.Regexp(t, "FF10138", err) } @@ -463,6 +465,11 @@ func TestInitNewConfigBadIndex(t *testing.T) { httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) @@ -471,10 +478,49 @@ func TestInitNewConfigBadIndex(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(1) + err = e.ConfigureContract(&core.FireFlyContracts{ + Active: core.FireFlyContractInfo{Index: 1}, + }) assert.Regexp(t, "FF10387", err) } +func TestInitNewConfigSwitchBack(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + resetConf(e) + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, subscription{})) + + resetConf(e) + utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.ConfigureContract(&core.FireFlyContracts{ + Terminated: []core.FireFlyContractInfo{ + { + Info: fftypes.JSONObject{"address": "0x71c7656ec7ab88b098defb751b7401b5f6d8976f"}, + FinalEvent: "1", + }, + }, + }) + assert.Regexp(t, "FF10388", err) +} + func TestStreamQueryError(t *testing.T) { e, cancel := newTestEthereum() @@ -491,12 +537,10 @@ func TestStreamQueryError(t *testing.T) { utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.initStreams(0) assert.Regexp(t, "FF10111.*pop", err) } @@ -519,12 +563,10 @@ func TestStreamCreateError(t *testing.T) { utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.initStreams(0) assert.Regexp(t, "FF10111.*pop", err) } @@ -547,12 +589,10 @@ func TestStreamUpdateError(t *testing.T) { utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.initStreams(0) assert.Regexp(t, "FF10111.*pop", err) } @@ -577,12 +617,12 @@ func TestSubQueryError(t *testing.T) { utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.Regexp(t, "FF10111.*pop", err) } @@ -609,12 +649,12 @@ func TestSubQueryCreateError(t *testing.T) { utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x12345") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.Regexp(t, "FF10111.*pop", err) } @@ -1615,41 +1655,6 @@ func TestHandleMessageContractEvent(t *testing.T) { em.AssertExpectations(t) } -func TestHandleMessageContractEventNoTimestamp(t *testing.T) { - data := fftypes.JSONAnyPtr(` -[ - { - "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", - "blockNumber": "38011", - "transactionIndex": "0x0", - "transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628", - "data": { - "from": "0x91D2B4381A4CD5C7C0F27565A7D4B829844C8635", - "value": "1" - }, - "subId": "sub2", - "signature": "Changed(address,uint256)", - "logIndex": "50" - } -]`) - - em := &blockchainmocks.Callbacks{} - e := &Ethereum{ - callbacks: em, - } - e.initInfo.sub = &subscription{ - ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", - } - - em.On("BlockchainEvent", mock.Anything).Return(nil) - - var events []interface{} - err := json.Unmarshal(data.Bytes(), &events) - assert.NoError(t, err) - err = e.handleMessageBatch(context.Background(), events) - assert.Regexp(t, "FF00136", err) -} - func TestHandleMessageContractEventError(t *testing.T) { data := fftypes.JSONAnyPtr(` [ @@ -2611,11 +2616,11 @@ func TestSubmitOperatorAction(t *testing.T) { assert.Equal(t, "SendTransaction", headers["type"]) assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", params[1]) assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", params[2]) - assert.Equal(t, "1", params[3]) + assert.Equal(t, "", params[3]) return httpmock.NewJsonResponderOrPanic(200, "")(req) }) - err := e.SubmitOperatorAction(context.Background(), fftypes.NewUUID(), "0x123", blockchain.OperatorActionMigrate, "1") + err := e.SubmitOperatorAction(context.Background(), fftypes.NewUUID(), "0x123", blockchain.OperatorActionTerminate) assert.NoError(t, err) } @@ -2629,10 +2634,10 @@ func TestHandleOperatorAction(t *testing.T) { "transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628", "data": { "author": "0X91D2B4381A4CD5C7C0F27565A7D4B829844C8635", - "namespace": "firefly:migrate", + "namespace": "firefly:terminate", "uuids": "0x0000000000000000000000000000000000000000000000000000000000000000", "batchHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "payloadRef": "1", + "payloadRef": "", "contexts": [] }, "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", @@ -2655,7 +2660,7 @@ func TestHandleOperatorAction(t *testing.T) { Value: "0x91d2b4381a4cd5c7c0f27565a7d4b829844c8635", } - em.On("BlockchainOperatorAction", "migrate", "1", expectedSigningKeyRef).Return(nil) + em.On("BlockchainOperatorAction", "terminate", mock.Anything, expectedSigningKeyRef).Return(nil) var events []interface{} err := json.Unmarshal(data.Bytes(), &events) diff --git a/internal/blockchain/ethereum/eventstream.go b/internal/blockchain/ethereum/eventstream.go index 1a574bde2..4059a60a4 100644 --- a/internal/blockchain/ethereum/eventstream.go +++ b/internal/blockchain/ethereum/eventstream.go @@ -30,8 +30,7 @@ import ( ) type streamManager struct { - client *resty.Client - fireFlySubscriptionFromBlock string + client *resty.Client } type eventStream struct { @@ -171,7 +170,7 @@ func (s *streamManager) deleteSubscription(ctx context.Context, subID string) er return nil } -func (s *streamManager) ensureFireFlySubscription(ctx context.Context, instancePath, stream string, abi ABIElementMarshaling) (sub *subscription, err error) { +func (s *streamManager) ensureFireFlySubscription(ctx context.Context, instancePath, fromBlock, stream string, abi ABIElementMarshaling) (sub *subscription, err error) { // Include a hash of the instance path in the subscription, so if we ever point at a different // contract configuration, we re-subscribe from block 0. // We don't need full strength hashing, so just use the first 16 chars for readability. @@ -199,7 +198,7 @@ func (s *streamManager) ensureFireFlySubscription(ctx context.Context, instanceP } if sub == nil { - if sub, err = s.createSubscription(ctx, location, stream, subName, s.fireFlySubscriptionFromBlock, abi); err != nil { + if sub, err = s.createSubscription(ctx, location, stream, subName, fromBlock, abi); err != nil { return nil, err } } diff --git a/internal/blockchain/fabric/config.go b/internal/blockchain/fabric/config.go index 63c7ab564..86695309b 100644 --- a/internal/blockchain/fabric/config.go +++ b/internal/blockchain/fabric/config.go @@ -73,4 +73,5 @@ func (f *Fabric) InitConfig(config config.Section) { f.contractConf = config.SubArray(FireFlyContractConfigKey) f.contractConf.AddKnownKey(FireFlyContractChaincode) f.contractConf.AddKnownKey(FireFlyContractFromBlock, "oldest") + f.contractConfSize = f.contractConf.ArraySize() } diff --git a/internal/blockchain/fabric/eventstream.go b/internal/blockchain/fabric/eventstream.go index f00d080bf..325a2b931 100644 --- a/internal/blockchain/fabric/eventstream.go +++ b/internal/blockchain/fabric/eventstream.go @@ -27,9 +27,8 @@ import ( ) type streamManager struct { - client *resty.Client - signer string - fireFlySubscriptionFromBlock string + client *resty.Client + signer string } type eventStream struct { @@ -151,7 +150,7 @@ func (s *streamManager) deleteSubscription(ctx context.Context, subID string) er return nil } -func (s *streamManager) ensureSubscription(ctx context.Context, location *Location, stream, event string) (sub *subscription, err error) { +func (s *streamManager) ensureFireFlySubscription(ctx context.Context, location *Location, fromBlock, stream, event string) (sub *subscription, err error) { existingSubs, err := s.getSubscriptions(ctx) if err != nil { return nil, err @@ -165,7 +164,7 @@ func (s *streamManager) ensureSubscription(ctx context.Context, location *Locati } if sub == nil { - if sub, err = s.createSubscription(ctx, location, stream, subName, event, s.fireFlySubscriptionFromBlock); err != nil { + if sub, err = s.createSubscription(ctx, location, stream, subName, event, fromBlock); err != nil { return nil, err } } diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index 6a50d65a8..c32d665b0 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -23,7 +23,6 @@ import ( "encoding/json" "fmt" "regexp" - "strconv" "strings" "github.com/go-resty/resty/v2" @@ -44,27 +43,30 @@ const ( ) type Fabric struct { - ctx context.Context - topic string - defaultChannel string - chaincode string - signer string - prefixShort string - prefixLong string - capabilities *blockchain.Capabilities - callbacks blockchain.Callbacks - client *resty.Client - streams *streamManager - initInfo struct { + ctx context.Context + topic string + defaultChannel string + fireflyChaincode string + fireflyFromBlock string + finalEvents map[string]string + signer string + prefixShort string + prefixLong string + capabilities *blockchain.Capabilities + callbacks blockchain.Callbacks + client *resty.Client + streams *streamManager + initInfo struct { stream *eventStream sub *subscription } - idCache map[string]*fabIdentity - wsconn wsclient.WSClient - closed chan struct{} - metrics metrics.Manager - fabconnectConf config.Section - contractConf config.ArraySection + idCache map[string]*fabIdentity + wsconn wsclient.WSClient + closed chan struct{} + metrics metrics.Manager + fabconnectConf config.Section + contractConf config.ArraySection + contractConfSize int } type eventStreamWebsocket struct { @@ -164,6 +166,7 @@ func (f *Fabric) VerifierType() core.VerifierType { func (f *Fabric) Init(ctx context.Context, config config.Section, callbacks blockchain.Callbacks, metrics metrics.Manager) (err error) { f.InitConfig(config) + fabconnectConf := f.fabconnectConf f.ctx = log.WithLogField(ctx, "proto", "fabric") f.callbacks = callbacks @@ -171,89 +174,125 @@ func (f *Fabric) Init(ctx context.Context, config config.Section, callbacks bloc f.metrics = metrics f.capabilities = &blockchain.Capabilities{} - if f.fabconnectConf.GetString(ffresty.HTTPConfigURL) == "" { + if fabconnectConf.GetString(ffresty.HTTPConfigURL) == "" { return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "blockchain.fabric.fabconnect") } - f.defaultChannel = f.fabconnectConf.GetString(FabconnectConfigDefaultChannel) + f.client = ffresty.New(f.ctx, fabconnectConf) + + f.defaultChannel = fabconnectConf.GetString(FabconnectConfigDefaultChannel) // the org identity is guaranteed to be configured by the core - f.signer = f.fabconnectConf.GetString(FabconnectConfigSigner) - f.topic = f.fabconnectConf.GetString(FabconnectConfigTopic) + f.signer = fabconnectConf.GetString(FabconnectConfigSigner) + f.topic = fabconnectConf.GetString(FabconnectConfigTopic) if f.topic == "" { return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "topic", "blockchain.fabric.fabconnect") } + f.prefixShort = fabconnectConf.GetString(FabconnectPrefixShort) + f.prefixLong = fabconnectConf.GetString(FabconnectPrefixLong) - f.prefixShort = f.fabconnectConf.GetString(FabconnectPrefixShort) - f.prefixLong = f.fabconnectConf.GetString(FabconnectPrefixLong) - - f.client = ffresty.New(f.ctx, f.fabconnectConf) - - return nil -} - -func (f *Fabric) initStreams(contractIndex int) (err error) { - ctx := f.ctx - wsConfig := wsclient.GenerateConfig(f.fabconnectConf) + wsConfig := wsclient.GenerateConfig(fabconnectConf) if wsConfig.WSKeyPath == "" { wsConfig.WSKeyPath = "/ws" } - f.wsconn, err = wsclient.New(ctx, wsConfig, nil, f.afterConnect) + f.wsconn, err = wsclient.New(f.ctx, wsConfig, nil, f.afterConnect) if err != nil { return err } f.streams = &streamManager{client: f.client, signer: f.signer} - if f.contractConf.ArraySize() > 0 || contractIndex > 0 { + batchSize := f.fabconnectConf.GetUint(FabconnectConfigBatchSize) + batchTimeout := uint(f.fabconnectConf.GetDuration(FabconnectConfigBatchTimeout).Milliseconds()) + if f.initInfo.stream, err = f.streams.ensureEventStream(f.ctx, f.topic, batchSize, batchTimeout); err != nil { + return err + } + log.L(f.ctx).Infof("Event stream: %s", f.initInfo.stream.ID) + + f.closed = make(chan struct{}) + go f.eventLoop() + + return nil +} + +func (f *Fabric) resolveFireFlyContract(ctx context.Context, contractIndex int) (chaincode, fromBlock string, err error) { + + if f.contractConfSize > 0 || contractIndex > 0 { // New config (array of objects under "fireflyContract") - if contractIndex >= f.contractConf.ArraySize() { - return i18n.NewError(ctx, coremsgs.MsgInvalidFireFlyContractIndex, fmt.Sprintf("blockchain.fabric.fireflyContract[%d]", contractIndex)) + if contractIndex >= f.contractConfSize { + return "", "", i18n.NewError(ctx, coremsgs.MsgInvalidFireFlyContractIndex, fmt.Sprintf("blockchain.fabric.fireflyContract[%d]", contractIndex)) } - f.chaincode = f.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractChaincode) - if f.chaincode == "" { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "address", "blockchain.fabric.fireflyContract") + chaincode = f.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractChaincode) + if chaincode == "" { + return "", "", i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "address", "blockchain.fabric.fireflyContract") } - f.streams.fireFlySubscriptionFromBlock = f.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractFromBlock) + fromBlock = f.contractConf.ArrayEntry(contractIndex).GetString(FireFlyContractFromBlock) } else { // Old config (attributes under "ethconnect") - f.chaincode = f.fabconnectConf.GetString(FabconnectConfigChaincodeDeprecated) - if f.chaincode != "" { + chaincode = f.fabconnectConf.GetString(FabconnectConfigChaincodeDeprecated) + if chaincode != "" { log.L(ctx).Warnf("The %s.%s config key has been deprecated. Please use %s.%s instead", FabconnectConfigKey, FabconnectConfigChaincodeDeprecated, FireFlyContractConfigKey, FireFlyContractChaincode) - f.streams.fireFlySubscriptionFromBlock = string(core.SubOptsFirstEventOldest) + fromBlock = string(core.SubOptsFirstEventOldest) } else { - return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "chaincode", "blockchain.fabric.fabconnect") + return "", "", i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "chaincode", "blockchain.fabric.fabconnect") } } - batchSize := f.fabconnectConf.GetUint(FabconnectConfigBatchSize) - batchTimeout := uint(f.fabconnectConf.GetDuration(FabconnectConfigBatchTimeout).Milliseconds()) - if f.initInfo.stream, err = f.streams.ensureEventStream(f.ctx, f.topic, batchSize, batchTimeout); err != nil { + return chaincode, fromBlock, nil +} + +func (f *Fabric) ConfigureContract(contracts *core.FireFlyContracts) (err error) { + + fireflyChaincode, fireflyFromBlock, err := f.resolveFireFlyContract(f.ctx, contracts.Active.Index) + if err != nil { return err } - log.L(f.ctx).Infof("Event stream: %s", f.initInfo.stream.ID) - location := &Location{ - Channel: f.defaultChannel, - Chaincode: f.chaincode, + if _, ok := f.finalEvents[fireflyChaincode]; ok { + log.L(f.ctx).Warnf("Cannot switch back to previously-used chaincode %s", fireflyChaincode) + return nil } - if f.initInfo.sub, err = f.streams.ensureSubscription(f.ctx, location, f.initInfo.stream.ID, batchPinEvent); err != nil { - return err + contracts.Active.Info = fftypes.JSONObject{ + "chaincode": fireflyChaincode, + "fromBlock": fireflyFromBlock, } - f.closed = make(chan struct{}) - go f.eventLoop() + f.fireflyChaincode = fireflyChaincode + f.fireflyFromBlock = fireflyFromBlock + f.finalEvents = make(map[string]string, len(contracts.Terminated)) - return nil + for i, oldContract := range contracts.Terminated { + chaincode := oldContract.Info.GetString("chaincode") + if chaincode != "" { + f.finalEvents[chaincode] = contracts.Terminated[i].FinalEvent + } + } + + location := &Location{ + Channel: f.defaultChannel, + Chaincode: f.fireflyChaincode, + } + f.initInfo.sub, err = f.streams.ensureFireFlySubscription(f.ctx, location, f.fireflyFromBlock, f.initInfo.stream.ID, batchPinEvent) + return err } -func (f *Fabric) Start(contractIndex int) (err error) { - if err = f.initStreams(contractIndex); err != nil { - return err +func (f *Fabric) TerminateContract(contracts *core.FireFlyContracts, termination *blockchain.Event) (err error) { + + chaincode := termination.Info.GetString("chaincodeId") + if chaincode != f.fireflyChaincode { + log.L(f.ctx).Warnf("Ignoring termination request from chaincode %s, which differs from active chaincode %s", chaincode, f.fireflyChaincode) + return nil } - return f.wsconn.Connect() + log.L(f.ctx).Infof("Processing termination request from chaincode %s", chaincode) + contracts.Terminated = append(contracts.Terminated, core.FireFlyContractInfo{ + Index: contracts.Active.Index, + Info: contracts.Active.Info, + FinalEvent: termination.ProtocolID, + }) + contracts.Active = core.FireFlyContractInfo{Index: contracts.Active.Index + 1} + return f.ConfigureContract(contracts) } -func (f *Fabric) Stop() { - f.wsconn.Close() +func (f *Fabric) Start() (err error) { + return f.wsconn.Connect() } func (f *Fabric) Capabilities() *blockchain.Capabilities { @@ -291,7 +330,7 @@ func decodeJSONPayload(ctx context.Context, payloadString string) *fftypes.JSONO return &payload } -func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONObject) (err error) { +func (f *Fabric) parseBlockchainEvent(ctx context.Context, msgJSON fftypes.JSONObject) *blockchain.Event { payloadString := msgJSON.GetString("payload") payload := decodeJSONPayload(ctx, payloadString) if payload == nil { @@ -302,13 +341,44 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb blockNumber := msgJSON.GetInt64("blockNumber") transactionIndex := msgJSON.GetInt64("transactionIndex") eventIndex := msgJSON.GetInt64("eventIndex") + name := msgJSON.GetString("eventName") timestamp := msgJSON.GetInt64("timestamp") - signer := payload.GetString("signer") - nsOrAction := payload.GetString("namespace") - sUUIDs := payload.GetString("uuids") - sBatchHash := payload.GetString("batchHash") - sPayloadRef := payload.GetString("payloadRef") - sContexts := payload.GetStringArray("contexts") + chaincode := msgJSON.GetString("chaincodeId") + + delete(msgJSON, "payload") + return &blockchain.Event{ + BlockchainTXID: sTransactionHash, + Source: f.Name(), + Name: name, + ProtocolID: fmt.Sprintf("%.12d/%.6d/%.6d", blockNumber, transactionIndex, eventIndex), + Output: *payload, + Info: msgJSON, + Timestamp: fftypes.UnixTime(timestamp), + Location: f.buildEventLocationString(chaincode), + Signature: name, + } +} + +func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONObject) (err error) { + event := f.parseBlockchainEvent(ctx, msgJSON) + if event == nil { + return nil // move on + } + + chaincode := event.Info.GetString("chaincodeId") + if finalEvent, ok := f.finalEvents[chaincode]; ok { + if event.ProtocolID > finalEvent { + log.L(ctx).Warnf("Ignoring BatchPin event %s received after termination event %s", event.ProtocolID, finalEvent) + return nil // move on + } + } + + signer := event.Output.GetString("signer") + nsOrAction := event.Output.GetString("namespace") + sUUIDs := event.Output.GetString("uuids") + sBatchHash := event.Output.GetString("batchHash") + sPayloadRef := event.Output.GetString("payloadRef") + sContexts := event.Output.GetStringArray("contexts") verifier := &core.VerifierRef{ Type: core.VerifierTypeMSPIdentity, @@ -318,7 +388,7 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb // Check if this is actually an operator action if strings.HasPrefix(nsOrAction, blockchain.FireFlyActionPrefix) { action := nsOrAction[len(blockchain.FireFlyActionPrefix):] - return f.callbacks.BlockchainOperatorAction(action, sPayloadRef, verifier) + return f.callbacks.BlockchainOperatorAction(action, event, verifier) } hexUUIDs, err := hex.DecodeString(strings.TrimPrefix(sUUIDs, "0x")) @@ -349,7 +419,6 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb contexts[i] = &hash } - delete(msgJSON, "payload") batch := &blockchain.BatchPin{ Namespace: nsOrAction, TransactionID: &txnID, @@ -357,64 +426,26 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb BatchHash: &batchHash, BatchPayloadRef: sPayloadRef, Contexts: contexts, - Event: blockchain.Event{ - BlockchainTXID: sTransactionHash, - Source: f.Name(), - Name: "BatchPin", - ProtocolID: fmt.Sprintf("%.12d/%.6d/%.6d", blockNumber, transactionIndex, eventIndex), - Output: *payload, - Info: msgJSON, - Timestamp: fftypes.UnixTime(timestamp), - Location: f.buildEventLocationString(msgJSON), - Signature: "BatchPin", - }, + Event: *event, } // If there's an error dispatching the event, we must return the error and shutdown return f.callbacks.BatchPinComplete(batch, verifier) } -func (f *Fabric) buildEventLocationString(msgJSON fftypes.JSONObject) string { - return fmt.Sprintf("chaincode=%s", msgJSON.GetString("chaincodeId")) +func (f *Fabric) buildEventLocationString(chaincode string) string { + return fmt.Sprintf("chaincode=%s", chaincode) } func (f *Fabric) handleContractEvent(ctx context.Context, msgJSON fftypes.JSONObject) (err error) { - payloadString := msgJSON.GetString("payload") - payload := decodeJSONPayload(ctx, payloadString) - if payload == nil { + event := f.parseBlockchainEvent(ctx, msgJSON) + if event == nil { return nil // move on } - delete(msgJSON, "payload") - - sTransactionHash := msgJSON.GetString("transactionId") - blockNumber := msgJSON.GetInt64("blockNumber") - transactionIndex := msgJSON.GetInt64("transactionIndex") - eventIndex := msgJSON.GetInt64("eventIndex") - sub := msgJSON.GetString("subId") - name := msgJSON.GetString("eventName") - sTimestamp := msgJSON.GetString("timestamp") - timestamp, err := strconv.ParseInt(sTimestamp, 10, 64) - if err != nil { - log.L(ctx).Errorf("Blockchain event is not valid - bad timestamp (%s): %s", sTimestamp, err) - // Continue with zero timestamp - } - - event := &blockchain.EventWithSubscription{ - Subscription: sub, - Event: blockchain.Event{ - BlockchainTXID: sTransactionHash, - Source: f.Name(), - Name: name, - ProtocolID: fmt.Sprintf("%.12d/%.6d/%.6d", blockNumber, transactionIndex, eventIndex), - Output: *payload, - Info: msgJSON, - Timestamp: fftypes.UnixTime(timestamp), - Location: f.buildEventLocationString(msgJSON), - Signature: name, - }, - } - - return f.callbacks.BlockchainEvent(event) + return f.callbacks.BlockchainEvent(&blockchain.EventWithSubscription{ + Event: *event, + Subscription: msgJSON.GetString("subId"), + }) } func (f *Fabric) handleReceipt(ctx context.Context, reply fftypes.JSONObject) { @@ -622,19 +653,19 @@ func (f *Fabric) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, "contexts": hashes, } input, _ := jsonEncodeInput(pinInput) - return f.invokeContractMethod(ctx, f.defaultChannel, f.chaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) + return f.invokeContractMethod(ctx, f.defaultChannel, f.fireflyChaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) } -func (f *Fabric) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action, payload string) error { +func (f *Fabric) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action string) error { pinInput := map[string]interface{}{ "namespace": "firefly:" + action, "uuids": hexFormatB32(nil), "batchHash": hexFormatB32(nil), - "payloadRef": payload, + "payloadRef": "", "contexts": []string{}, } input, _ := jsonEncodeInput(pinInput) - return f.invokeContractMethod(ctx, f.defaultChannel, f.chaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) + return f.invokeContractMethod(ctx, f.defaultChannel, f.fireflyChaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) } func (f *Fabric) InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error { diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index af2d4199e..86ada6437 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -56,15 +56,15 @@ func newTestFabric() (*Fabric, func()) { em := &blockchainmocks.Callbacks{} wsm := &wsmocks.WSClient{} e := &Fabric{ - ctx: ctx, - client: resty.New().SetBaseURL("http://localhost:12345"), - defaultChannel: "firefly", - chaincode: "firefly", - topic: "topic1", - prefixShort: defaultPrefixShort, - prefixLong: defaultPrefixLong, - callbacks: em, - wsconn: wsm, + ctx: ctx, + client: resty.New().SetBaseURL("http://localhost:12345"), + defaultChannel: "firefly", + fireflyChaincode: "firefly", + topic: "topic1", + prefixShort: defaultPrefixShort, + prefixLong: defaultPrefixLong, + callbacks: em, + wsconn: wsm, } return e, func() { cancel() @@ -109,19 +109,6 @@ func TestInitMissingURL(t *testing.T) { assert.Regexp(t, "FF10138.*url", err) } -func TestInitMissingChaincode(t *testing.T) { - e, cancel := newTestFabric() - defer cancel() - resetConf(e) - utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") - utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.initStreams(0) - assert.Regexp(t, "FF10138.*chaincode", err) -} - func TestInitMissingTopic(t *testing.T) { e, cancel := newTestFabric() defer cancel() @@ -179,7 +166,9 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) { assert.Equal(t, "fabric", e.Name()) assert.Equal(t, core.VerifierTypeMSPIdentity, e.VerifierType()) - err = e.Start(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) + assert.NoError(t, err) + err = e.Start() assert.NoError(t, err) assert.Equal(t, 4, httpmock.GetTotalCallCount()) @@ -214,24 +203,10 @@ func TestWSInitFail(t *testing.T) { utFabconnectConf.Set(FabconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.Start(0) assert.Regexp(t, "FF00149", err) } -func TestWSClose(t *testing.T) { - - wsm := &wsmocks.WSClient{} - e := &Fabric{ - ctx: context.Background(), - wsconn: wsm, - } - wsm.On("Close").Return(nil) - e.Stop() - -} - func TestInitAllExistingStreams(t *testing.T) { e, cancel := newTestFabric() @@ -257,7 +232,7 @@ func TestInitAllExistingStreams(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, 2, httpmock.GetTotalCallCount()) @@ -291,7 +266,7 @@ func TestInitNewConfig(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, 2, httpmock.GetTotalCallCount()) @@ -309,6 +284,9 @@ func TestInitNewConfigError(t *testing.T) { httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}})) + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) @@ -318,7 +296,7 @@ func TestInitNewConfigError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.Regexp(t, "FF10138", err) } @@ -332,6 +310,9 @@ func TestInitNewConfigBadIndex(t *testing.T) { httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", WebSocket: eventStreamWebsocket{Topic: "topic1"}}})) + resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) @@ -341,7 +322,9 @@ func TestInitNewConfigBadIndex(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(1) + err = e.ConfigureContract(&core.FireFlyContracts{ + Active: core.FireFlyContractInfo{Index: 1}, + }) assert.Regexp(t, "FF10387", err) } @@ -367,8 +350,6 @@ func TestStreamQueryError(t *testing.T) { utFabconnectConf.Set(FabconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.initStreams(0) assert.Regexp(t, "FF10284.*pop", err) } @@ -396,8 +377,6 @@ func TestStreamCreateError(t *testing.T) { utFabconnectConf.Set(FabconnectConfigTopic, "topic1") err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.initStreams(0) assert.Regexp(t, "FF10284.*pop", err) } @@ -428,7 +407,7 @@ func TestSubQueryError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.Regexp(t, "FF10284.*pop", err) } @@ -461,7 +440,7 @@ func TestSubQueryCreateError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.initStreams(0) + err = e.ConfigureContract(&core.FireFlyContracts{}) assert.Regexp(t, "FF10284.*pop", err) } @@ -1783,11 +1762,11 @@ func TestSubmitOperatorAction(t *testing.T) { assert.Equal(t, signer, (body["headers"].(map[string]interface{}))["signer"]) assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["uuids"]) assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", (body["args"].(map[string]interface{}))["batchHash"]) - assert.Equal(t, "1", (body["args"].(map[string]interface{}))["payloadRef"]) + assert.Equal(t, "", (body["args"].(map[string]interface{}))["payloadRef"]) return httpmock.NewJsonResponderOrPanic(200, "")(req) }) - err := e.SubmitOperatorAction(context.Background(), nil, signer, "migrate", "1") + err := e.SubmitOperatorAction(context.Background(), nil, signer, "terminate") assert.NoError(t, err) } @@ -1802,7 +1781,7 @@ func TestHandleOperatorAction(t *testing.T) { "transactionIndex": 2, "eventIndex": 50, "eventName": "BatchPin", - "payload": "eyJzaWduZXIiOiJ1MHZnd3U5czAwLXg1MDk6OkNOPXVzZXIyLE9VPWNsaWVudDo6Q049ZmFicmljLWNhLXNlcnZlciIsInRpbWVzdGFtcCI6eyJzZWNvbmRzIjoxNjMwMDMxNjY3LCJuYW5vcyI6NzkxNDk5MDAwfSwibmFtZXNwYWNlIjoiZmlyZWZseTptaWdyYXRlIiwidXVpZHMiOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJiYXRjaEhhc2giOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJwYXlsb2FkUmVmIjoiMSIsImNvbnRleHRzIjpbXX0=", + "payload": "eyJzaWduZXIiOiJ1MHZnd3U5czAwLXg1MDk6OkNOPXVzZXIyLE9VPWNsaWVudDo6Q049ZmFicmljLWNhLXNlcnZlciIsInRpbWVzdGFtcCI6eyJzZWNvbmRzIjoxNjMwMDMxNjY3LCJuYW5vcyI6NzkxNDk5MDAwfSwibmFtZXNwYWNlIjoiZmlyZWZseTp0ZXJtaW5hdGUiLCJ1dWlkcyI6IjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsImJhdGNoSGFzaCI6IjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsInBheWxvYWRSZWYiOiIiLCJjb250ZXh0cyI6W119", "subId": "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e" } ]`) @@ -1820,7 +1799,7 @@ func TestHandleOperatorAction(t *testing.T) { Value: "u0vgwu9s00-x509::CN=user2,OU=client::CN=fabric-ca-server", } - em.On("BlockchainOperatorAction", "migrate", "1", expectedSigningKeyRef).Return(nil) + em.On("BlockchainOperatorAction", "terminate", mock.Anything, expectedSigningKeyRef).Return(nil) var events []interface{} err := json.Unmarshal(data, &events) diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go index 48ddba891..a23142c0c 100644 --- a/internal/coremsgs/en_error_messages.go +++ b/internal/coremsgs/en_error_messages.go @@ -234,4 +234,5 @@ var ( MsgInvalidOutputOption = ffe("FF10385", "invalid output option '%s'") MsgInvalidPluginConfiguration = ffe("FF10386", "Invalid %s plugin configuration - name and type are required") MsgInvalidFireFlyContractIndex = ffe("FF10387", "No configuration found for FireFly contract at %s") + MsgCannotReuseFireFlyContract = ffe("FF10388", "Cannot reuse previously-terminated FireFly contract at %s") ) diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go index 13568e6c9..8c18aca42 100644 --- a/internal/coremsgs/en_struct_descriptions.go +++ b/internal/coremsgs/en_struct_descriptions.go @@ -371,13 +371,18 @@ var ( VerifierCreated = ffm("Verifier.created", "The time this verifier was created on this node") // Namespace field descriptions - NamespaceID = ffm("Namespace.id", "The UUID of the namespace. For locally established namespaces will be different on each node in the network. For broadcast namespaces, will be the same on every node") - NamespaceMessage = ffm("Namespace.message", "The UUID of broadcast message used to establish the namespace. Unset for local namespaces") - NamespaceName = ffm("Namespace.name", "The namespace name") - NamespaceDescription = ffm("Namespace.description", "A description of the namespace") - NamespaceType = ffm("Namespace.type", "The type of the namespace") - NamespaceCreated = ffm("Namespace.created", "The time the namespace was created") - NamespaceContractIndex = ffm("Namespace.contractIndex", "The index of the configured FireFly smart contract currently being used") + NamespaceID = ffm("Namespace.id", "The UUID of the namespace. For locally established namespaces will be different on each node in the network. For broadcast namespaces, will be the same on every node") + NamespaceMessage = ffm("Namespace.message", "The UUID of broadcast message used to establish the namespace. Unset for local namespaces") + NamespaceName = ffm("Namespace.name", "The namespace name") + NamespaceDescription = ffm("Namespace.description", "A description of the namespace") + NamespaceType = ffm("Namespace.type", "The type of the namespace") + NamespaceCreated = ffm("Namespace.created", "The time the namespace was created") + NamespaceContract = ffm("Namespace.fireflyContract", "Info on the FireFly smart contract configured for this namespace") + FireFlyContractsActive = ffm("FireFlyContracts.active", "The currently active FireFly smart contract") + FireFlyContractsTerminated = ffm("FireFlyContracts.terminated", "Previously-terminated FireFly smart contracts") + FireFlyContractIndex = ffm("FireFlyContractInfo.index", "The index of this contract in the config file") + FireFlyContractFinalEvent = ffm("FireFlyContractInfo.finalEvent", "The identifier for the final blockchain event received from this contract before termination") + FireFlyContractInfo = ffm("FireFlyContractInfo.info", "Blockchain-specific info on the contract, such as its location on chain") // NodeStatus field descriptions NodeStatusNode = ffm("NodeStatus.node", "Details of the local node") diff --git a/internal/database/sqlcommon/namespace_sql.go b/internal/database/sqlcommon/namespace_sql.go index f7b4c0c00..7a3c2623e 100644 --- a/internal/database/sqlcommon/namespace_sql.go +++ b/internal/database/sqlcommon/namespace_sql.go @@ -37,7 +37,7 @@ var ( "name", "description", "created", - "contract_index", + "firefly_contracts", } namespaceFilterFieldMap = map[string]string{ "message": "message_id", @@ -91,7 +91,7 @@ func (s *SQLCommon) UpsertNamespace(ctx context.Context, namespace *core.Namespa Set("name", namespace.Name). Set("description", namespace.Description). Set("created", namespace.Created). - Set("contract_index", namespace.ContractIndex). + Set("firefly_contracts", namespace.Contracts). Where(sq.Eq{"name": namespace.Name}), func() { s.callbacks.UUIDCollectionEvent(database.CollectionNamespaces, core.ChangeEventTypeUpdated, namespace.ID) @@ -114,7 +114,7 @@ func (s *SQLCommon) UpsertNamespace(ctx context.Context, namespace *core.Namespa namespace.Name, namespace.Description, namespace.Created, - namespace.ContractIndex, + namespace.Contracts, ), func() { s.callbacks.UUIDCollectionEvent(database.CollectionNamespaces, core.ChangeEventTypeCreated, namespace.ID) @@ -136,7 +136,7 @@ func (s *SQLCommon) namespaceResult(ctx context.Context, row *sql.Rows) (*core.N &namespace.Name, &namespace.Description, &namespace.Created, - &namespace.ContractIndex, + &namespace.Contracts, ) if err != nil { return nil, i18n.WrapError(ctx, err, coremsgs.MsgDBReadErr, namespacesTable) diff --git a/internal/database/sqlcommon/namespace_sql_test.go b/internal/database/sqlcommon/namespace_sql_test.go index d04c0ecd4..b930de9a0 100644 --- a/internal/database/sqlcommon/namespace_sql_test.go +++ b/internal/database/sqlcommon/namespace_sql_test.go @@ -40,12 +40,16 @@ func TestNamespacesE2EWithDB(t *testing.T) { // Create a new namespace entry namespace := &core.Namespace{ - ID: nil, // generated for us - Message: fftypes.NewUUID(), - Type: core.NamespaceTypeLocal, - Name: "namespace1", - Created: fftypes.Now(), - ContractIndex: 1, + ID: nil, // generated for us + Message: fftypes.NewUUID(), + Type: core.NamespaceTypeLocal, + Name: "namespace1", + Created: fftypes.Now(), + Contracts: core.FireFlyContracts{ + Active: core.FireFlyContractInfo{ + Index: 1, + }, + }, } s.callbacks.On("UUIDCollectionEvent", database.CollectionNamespaces, core.ChangeEventTypeCreated, mock.Anything, mock.Anything).Return() @@ -240,15 +244,14 @@ func TestGetNamespaceByIDSuccess(t *testing.T) { nsID := fftypes.NewUUID() currTime := fftypes.Now() nsMock := &core.Namespace{ - ID: nsID, - Message: msgID, - Name: "ns1", - Type: core.NamespaceTypeLocal, - Description: "foo", - Created: currTime, - ContractIndex: 0, + ID: nsID, + Message: msgID, + Name: "ns1", + Type: core.NamespaceTypeLocal, + Description: "foo", + Created: currTime, } - mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id", "message", "type", "name", "description", "created", "contract_index"}).AddRow(nsID.String(), msgID.String(), core.NamespaceTypeLocal, "ns1", "foo", currTime.String(), 0)) + mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id", "message", "type", "name", "description", "created", "firefly_contracts"}).AddRow(nsID.String(), msgID.String(), core.NamespaceTypeLocal, "ns1", "foo", currTime.String(), "")) ns, err := s.GetNamespaceByID(context.Background(), nsID) assert.NoError(t, err) assert.Equal(t, nsMock, ns) diff --git a/internal/events/event_manager.go b/internal/events/event_manager.go index e90a641a3..2e524dda8 100644 --- a/internal/events/event_manager.go +++ b/internal/events/event_manager.go @@ -66,7 +66,7 @@ type EventManager interface { // Bound blockchain callbacks BatchPinComplete(bi blockchain.Plugin, batch *blockchain.BatchPin, signingKey *core.VerifierRef) error BlockchainEvent(event *blockchain.EventWithSubscription) error - BlockchainOperatorAction(bi blockchain.Plugin, action, payload string, signingKey *core.VerifierRef) error + BlockchainOperatorAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error // Bound dataexchange callbacks DXEvent(dx dataexchange.Plugin, event dataexchange.DXEvent) diff --git a/internal/events/operator_action.go b/internal/events/operator_action.go index 439429b5f..334f63fcc 100644 --- a/internal/events/operator_action.go +++ b/internal/events/operator_action.go @@ -17,42 +17,43 @@ package events import ( - "strconv" + "context" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/core" ) -func (em *eventManager) actionMigrate(bi blockchain.Plugin, payload string) error { - ns, err := em.database.GetNamespace(em.ctx, core.SystemNamespace) - if err != nil { - return err - } - idx, err := strconv.Atoi(payload) - if err != nil { - return err - } - if ns.ContractIndex == idx { - log.L(em.ctx).Debugf("Ignoring namespace migration for %s (already at %d)", ns.Name, ns.ContractIndex) - return nil - } - ns.ContractIndex = idx - log.L(em.ctx).Infof("Migrating namespace %s to contract index %d", ns.Name, ns.ContractIndex) - bi.Stop() - if err := bi.Start(ns.ContractIndex); err != nil { - return err - } - return em.database.UpsertNamespace(em.ctx, ns, true) +func (em *eventManager) actionTerminate(bi blockchain.Plugin, event *blockchain.Event) error { + return em.database.RunAsGroup(em.ctx, func(ctx context.Context) error { + ns, err := em.database.GetNamespace(ctx, core.SystemNamespace) + if err != nil { + return err + } + if err := bi.TerminateContract(&ns.Contracts, event); err != nil { + return err + } + return em.database.UpsertNamespace(ctx, ns, true) + }) } -func (em *eventManager) BlockchainOperatorAction(bi blockchain.Plugin, action, payload string, signingKey *core.VerifierRef) error { - return em.retry.Do(em.ctx, "handle operator action", func(attempt int) (bool, error) { +func (em *eventManager) BlockchainOperatorAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error { + return em.retry.Do(em.ctx, "handle operator action", func(attempt int) (retry bool, err error) { // TODO: verify signing identity - if action == blockchain.OperatorActionMigrate { - return true, em.actionMigrate(bi, payload) + + if action == blockchain.OperatorActionTerminate { + err = em.actionTerminate(bi, event) + } else { + log.L(em.ctx).Errorf("Ignoring unrecognized operator action: %s", action) + return false, nil + } + + if err == nil { + chainEvent := buildBlockchainEvent(core.SystemNamespace, nil, event, &core.BlockchainTransactionRef{ + BlockchainID: event.BlockchainTXID, + }) + err = em.maybePersistBlockchainEvent(em.ctx, chainEvent) } - log.L(em.ctx).Errorf("Ignoring unrecognized operator action: %s", action) - return false, nil + return true, err }) } diff --git a/internal/events/operator_action_test.go b/internal/events/operator_action_test.go index debaba5be..fbae03e10 100644 --- a/internal/events/operator_action_test.go +++ b/internal/events/operator_action_test.go @@ -20,8 +20,11 @@ import ( "fmt" "testing" + "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly/mocks/blockchainmocks" "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/txcommonmocks" + "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -31,21 +34,27 @@ func TestOperatorAction(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() + event := &blockchain.Event{ProtocolID: "0001"} + mbi := &blockchainmocks.Plugin{} mdi := em.database.(*databasemocks.Plugin) + mth := em.txHelper.(*txcommonmocks.Helper) + mdi.On("GetBlockchainEventByProtocolID", em.ctx, "ff_system", (*fftypes.UUID)(nil), "0001").Return(nil, nil) + mth.On("InsertBlockchainEvent", em.ctx, mock.MatchedBy(func(be *core.BlockchainEvent) bool { + return be.ProtocolID == "0001" + })).Return(nil) + mdi.On("InsertEvent", em.ctx, mock.Anything).Return(nil) mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) - mdi.On("UpsertNamespace", em.ctx, mock.MatchedBy(func(ns *core.Namespace) bool { - return ns.ContractIndex == 1 - }), true).Return(nil) - mbi.On("Stop").Return() - mbi.On("Start", 1).Return(nil) + mdi.On("UpsertNamespace", em.ctx, mock.AnythingOfType("*core.Namespace"), true).Return(nil) + mbi.On("TerminateContract", mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil) - err := em.BlockchainOperatorAction(mbi, "migrate", "1", &core.VerifierRef{}) + err := em.BlockchainOperatorAction(mbi, "terminate", event, &core.VerifierRef{}) assert.NoError(t, err) mbi.AssertExpectations(t) mdi.AssertExpectations(t) + mth.AssertExpectations(t) } func TestOperatorActionUnknown(t *testing.T) { @@ -54,13 +63,13 @@ func TestOperatorActionUnknown(t *testing.T) { mbi := &blockchainmocks.Plugin{} - err := em.BlockchainOperatorAction(mbi, "bad", "", &core.VerifierRef{}) + err := em.BlockchainOperatorAction(mbi, "bad", &blockchain.Event{}, &core.VerifierRef{}) assert.NoError(t, err) mbi.AssertExpectations(t) } -func TestActionMigrateQueryFail(t *testing.T) { +func TestActionTerminateQueryFail(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() @@ -69,64 +78,14 @@ func TestActionMigrateQueryFail(t *testing.T) { mdi.On("GetNamespace", em.ctx, "ff_system").Return(nil, fmt.Errorf("pop")) - err := em.actionMigrate(mbi, "1") - assert.EqualError(t, err, "pop") - - mbi.AssertExpectations(t) - mdi.AssertExpectations(t) -} - -func TestActionMigrateBadIndex(t *testing.T) { - em, cancel := newTestEventManager(t) - defer cancel() - - mbi := &blockchainmocks.Plugin{} - mdi := em.database.(*databasemocks.Plugin) - - mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) - - err := em.actionMigrate(mbi, "!bad") - assert.Regexp(t, "Atoi", err) - - mbi.AssertExpectations(t) - mdi.AssertExpectations(t) -} - -func TestActionMigrateSkip(t *testing.T) { - em, cancel := newTestEventManager(t) - defer cancel() - - mbi := &blockchainmocks.Plugin{} - mdi := em.database.(*databasemocks.Plugin) - - mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{ContractIndex: 1}, nil) - - err := em.actionMigrate(mbi, "1") - assert.NoError(t, err) - - mbi.AssertExpectations(t) - mdi.AssertExpectations(t) -} - -func TestActionMigrateStartFail(t *testing.T) { - em, cancel := newTestEventManager(t) - defer cancel() - - mbi := &blockchainmocks.Plugin{} - mdi := em.database.(*databasemocks.Plugin) - - mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) - mbi.On("Stop").Return(nil) - mbi.On("Start", 1).Return(fmt.Errorf("pop")) - - err := em.actionMigrate(mbi, "1") + err := em.actionTerminate(mbi, &blockchain.Event{}) assert.EqualError(t, err, "pop") mbi.AssertExpectations(t) mdi.AssertExpectations(t) } -func TestActionMigrateUpsertFail(t *testing.T) { +func TestActionTerminateUpsertFail(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() @@ -134,13 +93,10 @@ func TestActionMigrateUpsertFail(t *testing.T) { mdi := em.database.(*databasemocks.Plugin) mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) - mdi.On("UpsertNamespace", em.ctx, mock.MatchedBy(func(ns *core.Namespace) bool { - return ns.ContractIndex == 1 - }), true).Return(fmt.Errorf("pop")) - mbi.On("Stop").Return(nil) - mbi.On("Start", 1).Return(nil) + mdi.On("UpsertNamespace", em.ctx, mock.AnythingOfType("*core.Namespace"), true).Return(fmt.Errorf("pop")) + mbi.On("TerminateContract", mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil) - err := em.actionMigrate(mbi, "1") + err := em.actionTerminate(mbi, &blockchain.Event{}) assert.EqualError(t, err, "pop") mbi.AssertExpectations(t) diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go index ca6c1fd21..7ac96f039 100644 --- a/internal/orchestrator/bound_callbacks.go +++ b/internal/orchestrator/bound_callbacks.go @@ -59,8 +59,8 @@ func (bc *boundCallbacks) BatchPinComplete(batch *blockchain.BatchPin, signingKe return bc.ei.BatchPinComplete(bc.bi, batch, signingKey) } -func (bc *boundCallbacks) BlockchainOperatorAction(action, payload string, signingKey *core.VerifierRef) error { - return bc.ei.BlockchainOperatorAction(bc.bi, action, payload, signingKey) +func (bc *boundCallbacks) BlockchainOperatorAction(action string, event *blockchain.Event, signingKey *core.VerifierRef) error { + return bc.ei.BlockchainOperatorAction(bc.bi, action, event, signingKey) } func (bc *boundCallbacks) DXEvent(event dataexchange.DXEvent) { diff --git a/internal/orchestrator/bound_callbacks_test.go b/internal/orchestrator/bound_callbacks_test.go index 1232a8320..067b8c7a4 100644 --- a/internal/orchestrator/bound_callbacks_test.go +++ b/internal/orchestrator/bound_callbacks_test.go @@ -50,6 +50,7 @@ func TestBoundCallbacks(t *testing.T) { pool := &tokens.TokenPool{} transfer := &tokens.TokenTransfer{} approval := &tokens.TokenApproval{} + event := &blockchain.Event{} hash := fftypes.NewRandB32() opID := fftypes.NewUUID() @@ -57,8 +58,8 @@ func TestBoundCallbacks(t *testing.T) { err := bc.BatchPinComplete(batch, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}) assert.EqualError(t, err, "pop") - mei.On("BlockchainOperatorAction", mbi, "migrate", "1", &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop")) - err = bc.BlockchainOperatorAction("migrate", "1", &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}) + mei.On("BlockchainOperatorAction", mbi, "migrate", event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop")) + err = bc.BlockchainOperatorAction("migrate", event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}) assert.EqualError(t, err, "pop") mom.On("SubmitOperationUpdate", mock.Anything, &operations.OperationUpdate{ diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 8d6830738..162554205 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -18,7 +18,6 @@ package orchestrator import ( "context" - "strconv" "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -143,7 +142,7 @@ type Orchestrator interface { RequestReply(ctx context.Context, ns string, msg *core.MessageInOut) (reply *core.MessageInOut, err error) // Network Operations - MigrateNetwork(ctx context.Context, contractIndex int) error + MigrateNetwork(ctx context.Context) error } type orchestrator struct { @@ -231,9 +230,15 @@ func (or *orchestrator) Start() (err error) { } if err == nil { for _, el := range or.blockchains { - if err = el.Start(ns.ContractIndex); err != nil { + if err = el.ConfigureContract(&ns.Contracts); err != nil { break } + if err = el.Start(); err != nil { + break + } + } + if err == nil { + err = or.database.UpsertNamespace(or.ctx, ns, true) } } if err == nil { @@ -875,10 +880,10 @@ func (or *orchestrator) initNamespaces(ctx context.Context) (err error) { return or.namespace.Init(ctx, or.database) } -func (or *orchestrator) MigrateNetwork(ctx context.Context, contractIndex int) error { +func (or *orchestrator) MigrateNetwork(ctx context.Context) error { verifier, err := or.identity.GetNodeOwnerBlockchainKey(ctx) if err != nil { return err } - return or.blockchain.SubmitOperatorAction(ctx, fftypes.NewUUID(), verifier.Value, blockchain.OperatorActionMigrate, strconv.Itoa(contractIndex)) + return or.blockchain.SubmitOperatorAction(ctx, fftypes.NewUUID(), verifier.Value, blockchain.OperatorActionTerminate) } diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index d196ef769..c951e922e 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -980,7 +980,8 @@ func TestStartTokensFail(t *testing.T) { defer or.cleanup(t) or.database = or.mdi or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) - or.mbi.On("Start", 0).Return(nil) + or.mbi.On("ConfigureContract", &core.FireFlyContracts{}).Return(nil) + or.mbi.On("Start").Return(nil) or.mba.On("Start").Return(nil) or.mem.On("Start").Return(nil) or.mbm.On("Start").Return(nil) @@ -988,6 +989,7 @@ func TestStartTokensFail(t *testing.T) { or.msd.On("Start").Return(nil) or.mom.On("Start").Return(nil) or.mti.On("Start").Return(fmt.Errorf("pop")) + or.mdi.On("UpsertNamespace", mock.Anything, mock.Anything, true).Return(nil) err := or.Start() assert.EqualError(t, err, "pop") } @@ -998,7 +1000,8 @@ func TestStartBlockchainsFail(t *testing.T) { defer or.cleanup(t) or.database = or.mdi or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) - or.mbi.On("Start", 0).Return(fmt.Errorf("pop")) + or.mbi.On("ConfigureContract", &core.FireFlyContracts{}).Return(nil) + or.mbi.On("Start").Return(fmt.Errorf("pop")) or.mba.On("Start").Return(nil) err := or.Start() assert.EqualError(t, err, "pop") @@ -1010,7 +1013,8 @@ func TestStartStopOk(t *testing.T) { defer or.cleanup(t) or.database = or.mdi or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) - or.mbi.On("Start", 0).Return(nil) + or.mbi.On("ConfigureContract", &core.FireFlyContracts{}).Return(nil) + or.mbi.On("Start").Return(nil) or.mba.On("Start").Return(nil) or.mem.On("Start").Return(nil) or.mbm.On("Start").Return(nil) @@ -1025,6 +1029,7 @@ func TestStartStopOk(t *testing.T) { or.msd.On("WaitStop").Return(nil) or.mom.On("WaitStop").Return(nil) or.mae.On("WaitStop").Return(nil) + or.mdi.On("UpsertNamespace", mock.Anything, mock.Anything, true).Return(nil) err := or.Start() assert.NoError(t, err) or.WaitStop() @@ -1123,14 +1128,14 @@ func TestMigrateNetwork(t *testing.T) { or.blockchain = or.mbi verifier := &core.VerifierRef{Value: "0x123"} or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(verifier, nil) - or.mbi.On("SubmitOperatorAction", context.Background(), mock.Anything, "0x123", "migrate", "1").Return(nil) - err := or.MigrateNetwork(context.Background(), 1) + or.mbi.On("SubmitOperatorAction", context.Background(), mock.Anything, "0x123", "terminate").Return(nil) + err := or.MigrateNetwork(context.Background()) assert.NoError(t, err) } func TestMigrateNetworkBadKey(t *testing.T) { or := newTestOrchestrator() or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(nil, fmt.Errorf("pop")) - err := or.MigrateNetwork(context.Background(), 1) + err := or.MigrateNetwork(context.Background()) assert.EqualError(t, err, "pop") } diff --git a/mocks/blockchainmocks/callbacks.go b/mocks/blockchainmocks/callbacks.go index de7b9830b..f256640f5 100644 --- a/mocks/blockchainmocks/callbacks.go +++ b/mocks/blockchainmocks/callbacks.go @@ -49,13 +49,13 @@ func (_m *Callbacks) BlockchainOpUpdate(plugin blockchain.Plugin, operationID *f _m.Called(plugin, operationID, txState, blockchainTXID, errorMessage, opOutput) } -// BlockchainOperatorAction provides a mock function with given fields: action, payload, signingKey -func (_m *Callbacks) BlockchainOperatorAction(action string, payload string, signingKey *core.VerifierRef) error { - ret := _m.Called(action, payload, signingKey) +// BlockchainOperatorAction provides a mock function with given fields: action, event, signingKey +func (_m *Callbacks) BlockchainOperatorAction(action string, event *blockchain.Event, signingKey *core.VerifierRef) error { + ret := _m.Called(action, event, signingKey) var r0 error - if rf, ok := ret.Get(0).(func(string, string, *core.VerifierRef) error); ok { - r0 = rf(action, payload, signingKey) + if rf, ok := ret.Get(0).(func(string, *blockchain.Event, *core.VerifierRef) error); ok { + r0 = rf(action, event, signingKey) } else { r0 = ret.Error(0) } diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go index 765d2679f..c4141f775 100644 --- a/mocks/blockchainmocks/plugin.go +++ b/mocks/blockchainmocks/plugin.go @@ -52,6 +52,20 @@ func (_m *Plugin) Capabilities() *blockchain.Capabilities { return r0 } +// ConfigureContract provides a mock function with given fields: contracts +func (_m *Plugin) ConfigureContract(contracts *core.FireFlyContracts) error { + ret := _m.Called(contracts) + + var r0 error + if rf, ok := ret.Get(0).(func(*core.FireFlyContracts) error); ok { + r0 = rf(contracts) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteContractListener provides a mock function with given fields: ctx, subscription func (_m *Plugin) DeleteContractListener(ctx context.Context, subscription *core.ContractListener) error { ret := _m.Called(ctx, subscription) @@ -240,13 +254,13 @@ func (_m *Plugin) QueryContract(ctx context.Context, location *fftypes.JSONAny, return r0, r1 } -// Start provides a mock function with given fields: contractIndex -func (_m *Plugin) Start(contractIndex int) error { - ret := _m.Called(contractIndex) +// Start provides a mock function with given fields: +func (_m *Plugin) Start() error { + ret := _m.Called() var r0 error - if rf, ok := ret.Get(0).(func(int) error); ok { - r0 = rf(contractIndex) + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() } else { r0 = ret.Error(0) } @@ -254,11 +268,6 @@ func (_m *Plugin) Start(contractIndex int) error { return r0 } -// Stop provides a mock function with given fields: -func (_m *Plugin) Stop() { - _m.Called() -} - // SubmitBatchPin provides a mock function with given fields: ctx, operationID, signingKey, batch func (_m *Plugin) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, signingKey string, batch *blockchain.BatchPin) error { ret := _m.Called(ctx, operationID, signingKey, batch) @@ -273,13 +282,27 @@ func (_m *Plugin) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, return r0 } -// SubmitOperatorAction provides a mock function with given fields: ctx, operationID, signingKey, action, payload -func (_m *Plugin) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action string, payload string) error { - ret := _m.Called(ctx, operationID, signingKey, action, payload) +// SubmitOperatorAction provides a mock function with given fields: ctx, operationID, signingKey, action +func (_m *Plugin) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action string) error { + ret := _m.Called(ctx, operationID, signingKey, action) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, string, string) error); ok { + r0 = rf(ctx, operationID, signingKey, action) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TerminateContract provides a mock function with given fields: contracts, termination +func (_m *Plugin) TerminateContract(contracts *core.FireFlyContracts, termination *blockchain.Event) error { + ret := _m.Called(contracts, termination) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, string, string, string) error); ok { - r0 = rf(ctx, operationID, signingKey, action, payload) + if rf, ok := ret.Get(0).(func(*core.FireFlyContracts, *blockchain.Event) error); ok { + r0 = rf(contracts, termination) } else { r0 = ret.Error(0) } diff --git a/mocks/eventmocks/event_manager.go b/mocks/eventmocks/event_manager.go index 3d801ff83..ad5e364de 100644 --- a/mocks/eventmocks/event_manager.go +++ b/mocks/eventmocks/event_manager.go @@ -69,13 +69,13 @@ func (_m *EventManager) BlockchainEvent(event *blockchain.EventWithSubscription) return r0 } -// BlockchainOperatorAction provides a mock function with given fields: bi, action, payload, signingKey -func (_m *EventManager) BlockchainOperatorAction(bi blockchain.Plugin, action string, payload string, signingKey *core.VerifierRef) error { - ret := _m.Called(bi, action, payload, signingKey) +// BlockchainOperatorAction provides a mock function with given fields: bi, action, event, signingKey +func (_m *EventManager) BlockchainOperatorAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error { + ret := _m.Called(bi, action, event, signingKey) var r0 error - if rf, ok := ret.Get(0).(func(blockchain.Plugin, string, string, *core.VerifierRef) error); ok { - r0 = rf(bi, action, payload, signingKey) + if rf, ok := ret.Get(0).(func(blockchain.Plugin, string, *blockchain.Event, *core.VerifierRef) error); ok { + r0 = rf(bi, action, event, signingKey) } else { r0 = ret.Error(0) } diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go index 3710b5928..d055ff374 100644 --- a/mocks/orchestratormocks/orchestrator.go +++ b/mocks/orchestratormocks/orchestrator.go @@ -1262,13 +1262,13 @@ func (_m *Orchestrator) Metrics() metrics.Manager { return r0 } -// MigrateNetwork provides a mock function with given fields: ctx, contractIndex -func (_m *Orchestrator) MigrateNetwork(ctx context.Context, contractIndex int) error { - ret := _m.Called(ctx, contractIndex) +// MigrateNetwork provides a mock function with given fields: ctx +func (_m *Orchestrator) MigrateNetwork(ctx context.Context) error { + ret := _m.Called(ctx) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { - r0 = rf(ctx, contractIndex) + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) } else { r0 = ret.Error(0) } diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index 317be26eb..f1e0b6ac4 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -35,11 +35,19 @@ type Plugin interface { // Init initializes the plugin, with configuration Init(ctx context.Context, config config.Section, callbacks Callbacks, metrics metrics.Manager) error - // Blockchain interface must not deliver any events until start is called - Start(contractIndex int) error + // ConfigureContract initializes the subscription to the FireFly contract + // - Checks the provided contract info against the plugin's configuration, and updates it as needed + // - Initializes the contract info for performing BatchPin transactions, and initializes subscriptions for BatchPin events + ConfigureContract(contracts *core.FireFlyContracts) (err error) + + // TerminateContract marks the given event as the last one to be parsed on the current FireFly contract + // - Validates that the event came from the currently active FireFly contract + // - Re-initializes the plugin against the next configured FireFly contract + // - Updates the provided contract info to record the point of termination and the newly active contract + TerminateContract(contracts *core.FireFlyContracts, termination *Event) (err error) - // Stop listening for events until start is called again - Stop() + // Blockchain interface must not deliver any events until start is called + Start() error // Capabilities returns capabilities - not called until after Init Capabilities() *Capabilities @@ -56,7 +64,7 @@ type Plugin interface { SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, signingKey string, batch *BatchPin) error // SubmitOperatorAction writes a special "BatchPin" event which signals the plugin to take an action - SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action, payload string) error + SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action string) error // InvokeContract submits a new transaction to be executed by custom on-chain logic InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error @@ -84,8 +92,8 @@ type Plugin interface { } const ( - // OperatorActionMigrate request all network members to stop using the current contract and move to the next one configured - OperatorActionMigrate = "migrate" + // OperatorActionTerminate request all network members to stop using the current contract and move to the next one configured + OperatorActionTerminate = "terminate" ) const FireFlyActionPrefix = "firefly:" @@ -112,7 +120,7 @@ type Callbacks interface { // BlockchainOperatorAction notifies on the arrival of a network operator action // // Error should only be returned in shutdown scenarios - BlockchainOperatorAction(action, payload string, signingKey *core.VerifierRef) error + BlockchainOperatorAction(action string, event *Event, signingKey *core.VerifierRef) error // BlockchainEvent notifies on the arrival of any event from a user-created subscription. BlockchainEvent(event *EventWithSubscription) error @@ -175,7 +183,7 @@ type Event struct { // Name is a short name for the event Name string - // ProtocolID is a protocol-specific identifier for the event + // ProtocolID is an alphanumerically sortable string that represents this event uniquely on the blockchain ProtocolID string // Output is the raw output data from the event diff --git a/pkg/core/namespace.go b/pkg/core/namespace.go index b004822c1..5946050da 100644 --- a/pkg/core/namespace.go +++ b/pkg/core/namespace.go @@ -19,6 +19,8 @@ package core import ( "context" "crypto/sha256" + "database/sql/driver" + "encoding/json" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" @@ -39,17 +41,27 @@ var ( // Namespace is a isolate set of named resources, to allow multiple applications to co-exist in the same network, with the same named objects. // Can be used for use case segregation, or multi-tenancy. type Namespace struct { - ID *fftypes.UUID `ffstruct:"Namespace" json:"id" ffexcludeinput:"true"` - Message *fftypes.UUID `ffstruct:"Namespace" json:"message,omitempty" ffexcludeinput:"true"` - Name string `ffstruct:"Namespace" json:"name"` - Description string `ffstruct:"Namespace" json:"description"` - Type NamespaceType `ffstruct:"Namespace" json:"type" ffenum:"namespacetype" ffexcludeinput:"true"` - Created *fftypes.FFTime `ffstruct:"Namespace" json:"created" ffexcludeinput:"true"` - ContractIndex int `ffstruct:"Namespace" json:"contractIndex" ffexcludeinput:"true"` + ID *fftypes.UUID `ffstruct:"Namespace" json:"id" ffexcludeinput:"true"` + Message *fftypes.UUID `ffstruct:"Namespace" json:"message,omitempty" ffexcludeinput:"true"` + Name string `ffstruct:"Namespace" json:"name"` + Description string `ffstruct:"Namespace" json:"description"` + Type NamespaceType `ffstruct:"Namespace" json:"type" ffenum:"namespacetype" ffexcludeinput:"true"` + Created *fftypes.FFTime `ffstruct:"Namespace" json:"created" ffexcludeinput:"true"` + Contracts FireFlyContracts `ffstruct:"Namespace" json:"fireflyContract" ffexcludeinput:"true"` } -type NamespaceMigration struct { - ContractIndex int `ffstruct:"Namespace" json:"contractIndex" ` +type FireFlyContracts struct { + Active FireFlyContractInfo `ffstruct:"FireFlyContracts" json:"active"` + Terminated []FireFlyContractInfo `ffstruct:"FireFlyContracts" json:"terminated"` +} + +type FireFlyContractInfo struct { + Index int `ffstruct:"FireFlyContractInfo" json:"index"` + FinalEvent string `ffstruct:"FireFlyContractInfo" json:"finalEvent,omitempty"` + Info fftypes.JSONObject `ffstruct:"FireFlyContractInfo" json:"info"` +} + +type NamespaceMigrationRequest struct { } func (ns *Namespace) Validate(ctx context.Context, existing bool) (err error) { @@ -84,3 +96,23 @@ func (ns *Namespace) Topic() string { func (ns *Namespace) SetBroadcastMessage(msgID *fftypes.UUID) { ns.Message = msgID } + +// Scan implements sql.Scanner +func (fc *FireFlyContracts) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + if len(src) == 0 { + return nil + } + return json.Unmarshal(src, fc) + case string: + return fc.Scan([]byte(src)) + default: + return i18n.NewError(context.Background(), i18n.MsgTypeRestoreFailed, src, fc) + } +} + +// Value implements sql.Valuer +func (fc FireFlyContracts) Value() (driver.Value, error) { + return json.Marshal(fc) +} From a0428c2d8c3e0b77558c26053148e2d47a6071bc Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Tue, 24 May 2022 13:27:54 -0400 Subject: [PATCH 06/13] Replace /network/migrate with /network/operatoraction Signed-off-by: Andrew Richardson --- docs/swagger/swagger.yaml | 67 +++++++++++-------- ... => route_post_network_operator_action.go} | 14 ++-- ...oute_post_network_operator_action_test.go} | 8 +-- internal/apiserver/routes.go | 2 +- internal/blockchain/ethereum/ethereum_test.go | 2 +- internal/coremsgs/en_api_translations.go | 2 +- internal/coremsgs/en_error_messages.go | 1 + internal/coremsgs/en_struct_descriptions.go | 1 + internal/events/operator_action.go | 2 +- internal/orchestrator/orchestrator.go | 9 ++- internal/orchestrator/orchestrator_test.go | 8 +-- mocks/orchestratormocks/orchestrator.go | 28 ++++---- pkg/blockchain/plugin.go | 5 -- pkg/core/namespace.go | 11 ++- 14 files changed, 91 insertions(+), 69 deletions(-) rename internal/apiserver/{route_post_network_migrate.go => route_post_network_operator_action.go} (71%) rename internal/apiserver/{route_post_network_migrate_test.go => route_post_network_operator_action_test.go} (80%) diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 90fd7f343..ac1bbf3fc 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -22579,33 +22579,6 @@ paths: description: "" tags: - Global - /network/migrate: - post: - description: Instruct the network to unsubscribe from the current FireFly contract - and migrate to the next one configured - operationId: postNetworkMigrate - parameters: - - description: Server-side request timeout (millseconds, or set a custom suffix - like 10s) - in: header - name: Request-Timeout - schema: - default: 120s - type: string - requestBody: - content: - application/json: - schema: {} - responses: - "202": - content: - application/json: - schema: {} - description: Success - default: - description: "" - tags: - - Global /network/nodes: get: description: Gets a list of nodes in the network @@ -23070,6 +23043,46 @@ paths: description: "" tags: - Global + /network/operatoraction: + post: + description: Notify all nodes in the network of a new governance action + operationId: postNetworkOperatorAction + parameters: + - description: Server-side request timeout (millseconds, or set a custom suffix + like 10s) + in: header + name: Request-Timeout + schema: + default: 120s + type: string + requestBody: + content: + application/json: + schema: + properties: + type: + description: The action to be performed + enum: + - terminate + type: string + type: object + responses: + "202": + content: + application/json: + schema: + properties: + type: + description: The action to be performed + enum: + - terminate + type: string + type: object + description: Success + default: + description: "" + tags: + - Global /network/organizations: get: description: Gets a list of orgs in the network diff --git a/internal/apiserver/route_post_network_migrate.go b/internal/apiserver/route_post_network_operator_action.go similarity index 71% rename from internal/apiserver/route_post_network_migrate.go rename to internal/apiserver/route_post_network_operator_action.go index bc4ece492..a7c8f1fb9 100644 --- a/internal/apiserver/route_post_network_migrate.go +++ b/internal/apiserver/route_post_network_operator_action.go @@ -24,19 +24,19 @@ import ( "github.com/hyperledger/firefly/pkg/core" ) -var postNetworkMigrate = &oapispec.Route{ - Name: "postNetworkMigrate", - Path: "network/migrate", +var postNetworkOperatorAction = &oapispec.Route{ + Name: "postNetworkOperatorAction", + Path: "network/operatoraction", Method: http.MethodPost, PathParams: nil, QueryParams: nil, FilterFactory: nil, - Description: coremsgs.APIEndpointsPostNetworkMigrate, - JSONInputValue: func() interface{} { return &core.NamespaceMigrationRequest{} }, - JSONOutputValue: func() interface{} { return &core.NamespaceMigrationRequest{} }, + Description: coremsgs.APIEndpointsPostNetworkOperatorAction, + JSONInputValue: func() interface{} { return &core.OperatorAction{} }, + JSONOutputValue: func() interface{} { return &core.OperatorAction{} }, JSONOutputCodes: []int{http.StatusAccepted}, JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { - err = getOr(r.Ctx).MigrateNetwork(r.Ctx) + err = getOr(r.Ctx).SubmitOperatorAction(r.Ctx, r.Input.(*core.OperatorAction)) return r.Input, err }, } diff --git a/internal/apiserver/route_post_network_migrate_test.go b/internal/apiserver/route_post_network_operator_action_test.go similarity index 80% rename from internal/apiserver/route_post_network_migrate_test.go rename to internal/apiserver/route_post_network_operator_action_test.go index 672ae81e1..3182c86e9 100644 --- a/internal/apiserver/route_post_network_migrate_test.go +++ b/internal/apiserver/route_post_network_operator_action_test.go @@ -27,16 +27,16 @@ import ( "github.com/stretchr/testify/mock" ) -func TestPostNetworkMigrate(t *testing.T) { +func TestPostNetworkOperatorAction(t *testing.T) { o, r := newTestAPIServer() - input := core.NamespaceMigrationRequest{} + input := core.OperatorAction{} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) - req := httptest.NewRequest("POST", "/api/v1/network/migrate", &buf) + req := httptest.NewRequest("POST", "/api/v1/network/operatoraction", &buf) req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() - o.On("MigrateNetwork", mock.Anything).Return(nil) + o.On("SubmitOperatorAction", mock.Anything, mock.AnythingOfType("*core.OperatorAction")).Return(nil) r.ServeHTTP(res, req) assert.Equal(t, 202, res.Result().StatusCode) diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go index f6015da71..dc2e318de 100644 --- a/internal/apiserver/routes.go +++ b/internal/apiserver/routes.go @@ -38,7 +38,7 @@ var routes = append( getStatusBatchManager, getStatusPins, getStatusWebSockets, - postNetworkMigrate, + postNetworkOperatorAction, postNewNamespace, postNewOrganization, postNewOrganizationSelf, diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index c9967cd6d..10a4dbf38 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -2620,7 +2620,7 @@ func TestSubmitOperatorAction(t *testing.T) { return httpmock.NewJsonResponderOrPanic(200, "")(req) }) - err := e.SubmitOperatorAction(context.Background(), fftypes.NewUUID(), "0x123", blockchain.OperatorActionTerminate) + err := e.SubmitOperatorAction(context.Background(), fftypes.NewUUID(), "0x123", core.OperatorActionTerminate.String()) assert.NoError(t, err) } diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go index faba3f0e9..f0706489a 100644 --- a/internal/coremsgs/en_api_translations.go +++ b/internal/coremsgs/en_api_translations.go @@ -168,7 +168,7 @@ var ( APIEndpointsPutContractAPI = ffm("api.endpoints.putContractAPI", "Updates an existing contract API") APIEndpointsPutSubscription = ffm("api.endpoints.putSubscription", "Update an existing subscription") APIEndpointsGetContractAPIInterface = ffm("api.endpoints.getContractAPIInterface", "Gets a contract interface for a contract API") - APIEndpointsPostNetworkMigrate = ffm("api.endpoints.postNetworkMigrate", "Instruct the network to unsubscribe from the current FireFly contract and migrate to the next one configured") + APIEndpointsPostNetworkOperatorAction = ffm("api.endpoints.postNetworkOperatorAction", "Notify all nodes in the network of a new governance action") APISuccessResponse = ffm("api.success", "Success") APIRequestTimeoutDesc = ffm("api.requestTimeout", "Server-side request timeout (millseconds, or set a custom suffix like 10s)") diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go index a23142c0c..88b6ee092 100644 --- a/internal/coremsgs/en_error_messages.go +++ b/internal/coremsgs/en_error_messages.go @@ -235,4 +235,5 @@ var ( MsgInvalidPluginConfiguration = ffe("FF10386", "Invalid %s plugin configuration - name and type are required") MsgInvalidFireFlyContractIndex = ffe("FF10387", "No configuration found for FireFly contract at %s") MsgCannotReuseFireFlyContract = ffe("FF10388", "Cannot reuse previously-terminated FireFly contract at %s") + MsgUnrecognizedOperatorAction = ffe("FF10389", "Unrecognized operator action: %s", 400) ) diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go index 8c18aca42..1f3c8f593 100644 --- a/internal/coremsgs/en_struct_descriptions.go +++ b/internal/coremsgs/en_struct_descriptions.go @@ -383,6 +383,7 @@ var ( FireFlyContractIndex = ffm("FireFlyContractInfo.index", "The index of this contract in the config file") FireFlyContractFinalEvent = ffm("FireFlyContractInfo.finalEvent", "The identifier for the final blockchain event received from this contract before termination") FireFlyContractInfo = ffm("FireFlyContractInfo.info", "Blockchain-specific info on the contract, such as its location on chain") + OperatorActionType = ffm("OperatorAction.type", "The action to be performed") // NodeStatus field descriptions NodeStatusNode = ffm("NodeStatus.node", "Details of the local node") diff --git a/internal/events/operator_action.go b/internal/events/operator_action.go index 334f63fcc..c3196a84d 100644 --- a/internal/events/operator_action.go +++ b/internal/events/operator_action.go @@ -41,7 +41,7 @@ func (em *eventManager) BlockchainOperatorAction(bi blockchain.Plugin, action st return em.retry.Do(em.ctx, "handle operator action", func(attempt int) (retry bool, err error) { // TODO: verify signing identity - if action == blockchain.OperatorActionTerminate { + if action == core.OperatorActionTerminate.String() { err = em.actionTerminate(bi, event) } else { log.L(em.ctx).Errorf("Ignoring unrecognized operator action: %s", action) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 162554205..378d3c6b0 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -142,7 +142,7 @@ type Orchestrator interface { RequestReply(ctx context.Context, ns string, msg *core.MessageInOut) (reply *core.MessageInOut, err error) // Network Operations - MigrateNetwork(ctx context.Context) error + SubmitOperatorAction(ctx context.Context, action *core.OperatorAction) error } type orchestrator struct { @@ -880,10 +880,13 @@ func (or *orchestrator) initNamespaces(ctx context.Context) (err error) { return or.namespace.Init(ctx, or.database) } -func (or *orchestrator) MigrateNetwork(ctx context.Context) error { +func (or *orchestrator) SubmitOperatorAction(ctx context.Context, action *core.OperatorAction) error { verifier, err := or.identity.GetNodeOwnerBlockchainKey(ctx) if err != nil { return err } - return or.blockchain.SubmitOperatorAction(ctx, fftypes.NewUUID(), verifier.Value, blockchain.OperatorActionTerminate) + if action.Type != core.OperatorActionTerminate { + return i18n.NewError(ctx, coremsgs.MsgUnrecognizedOperatorAction, action.Type) + } + return or.blockchain.SubmitOperatorAction(ctx, fftypes.NewUUID(), verifier.Value, action.Type.String()) } diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index c951e922e..79e456d3f 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -1123,19 +1123,19 @@ func TestInitDataExchangeWithNodes(t *testing.T) { assert.NoError(t, err) } -func TestMigrateNetwork(t *testing.T) { +func TestOperatorAction(t *testing.T) { or := newTestOrchestrator() or.blockchain = or.mbi verifier := &core.VerifierRef{Value: "0x123"} or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(verifier, nil) or.mbi.On("SubmitOperatorAction", context.Background(), mock.Anything, "0x123", "terminate").Return(nil) - err := or.MigrateNetwork(context.Background()) + err := or.SubmitOperatorAction(context.Background(), &core.OperatorAction{Type: core.OperatorActionTerminate}) assert.NoError(t, err) } -func TestMigrateNetworkBadKey(t *testing.T) { +func TestOperatorActionBadKey(t *testing.T) { or := newTestOrchestrator() or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(nil, fmt.Errorf("pop")) - err := or.MigrateNetwork(context.Background()) + err := or.SubmitOperatorAction(context.Background(), &core.OperatorAction{Type: core.OperatorActionTerminate}) assert.EqualError(t, err, "pop") } diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go index d055ff374..4108ca14c 100644 --- a/mocks/orchestratormocks/orchestrator.go +++ b/mocks/orchestratormocks/orchestrator.go @@ -1262,20 +1262,6 @@ func (_m *Orchestrator) Metrics() metrics.Manager { return r0 } -// MigrateNetwork provides a mock function with given fields: ctx -func (_m *Orchestrator) MigrateNetwork(ctx context.Context) error { - ret := _m.Called(ctx) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(ctx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // NetworkMap provides a mock function with given fields: func (_m *Orchestrator) NetworkMap() networkmap.Manager { ret := _m.Called() @@ -1361,6 +1347,20 @@ func (_m *Orchestrator) Start() error { return r0 } +// SubmitOperatorAction provides a mock function with given fields: ctx, action +func (_m *Orchestrator) SubmitOperatorAction(ctx context.Context, action *core.OperatorAction) error { + ret := _m.Called(ctx, action) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *core.OperatorAction) error); ok { + r0 = rf(ctx, action) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // WaitStop provides a mock function with given fields: func (_m *Orchestrator) WaitStop() { _m.Called() diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index f1e0b6ac4..8dcc6efac 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -91,11 +91,6 @@ type Plugin interface { GenerateEventSignature(ctx context.Context, event *core.FFIEventDefinition) string } -const ( - // OperatorActionTerminate request all network members to stop using the current contract and move to the next one configured - OperatorActionTerminate = "terminate" -) - const FireFlyActionPrefix = "firefly:" // Callbacks is the interface provided to the blockchain plugin, to allow it to pass events back to firefly. diff --git a/pkg/core/namespace.go b/pkg/core/namespace.go index 5946050da..250a5d7ab 100644 --- a/pkg/core/namespace.go +++ b/pkg/core/namespace.go @@ -61,7 +61,16 @@ type FireFlyContractInfo struct { Info fftypes.JSONObject `ffstruct:"FireFlyContractInfo" json:"info"` } -type NamespaceMigrationRequest struct { +// OperatorActionType is a type of action to perform +type OperatorActionType = FFEnum + +var ( + // OperatorActionTerminate request all network members to stop using the current contract and move to the next one configured + OperatorActionTerminate = ffEnum("operatoractiontype", "terminate") +) + +type OperatorAction struct { + Type OperatorActionType `ffstruct:"OperatorAction" json:"type" ffenum:"operatoractiontype"` } func (ns *Namespace) Validate(ctx context.Context, existing bool) (err error) { From a94a153cf39a99aac97645a88ab4eaea9a91311c Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Tue, 24 May 2022 14:57:10 -0400 Subject: [PATCH 07/13] Test coverage and cleanup Signed-off-by: Andrew Richardson --- internal/blockchain/ethereum/ethereum.go | 52 +-- internal/blockchain/ethereum/ethereum_test.go | 303 +++++++++++++++++- internal/blockchain/fabric/fabric.go | 62 ++-- internal/blockchain/fabric/fabric_test.go | 220 ++++++++++++- internal/events/operator_action.go | 2 +- internal/events/operator_action_test.go | 21 +- internal/orchestrator/orchestrator.go | 4 +- internal/orchestrator/orchestrator_test.go | 28 +- mocks/blockchainmocks/plugin.go | 24 +- pkg/blockchain/plugin.go | 6 +- pkg/core/namespace_test.go | 38 +++ 11 files changed, 655 insertions(+), 105 deletions(-) diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 15ef21ce4..dde5e4cdb 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -266,7 +266,7 @@ func (e *Ethereum) resolveFireFlyContract(ctx context.Context, contractIndex int return address, fromBlock, err } -func (e *Ethereum) ConfigureContract(contracts *core.FireFlyContracts) (err error) { +func (e *Ethereum) ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) (err error) { finalEvents := make(map[string]string, len(contracts.Terminated)) for i, oldContract := range contracts.Terminated { @@ -276,44 +276,44 @@ func (e *Ethereum) ConfigureContract(contracts *core.FireFlyContracts) (err erro } } - log.L(e.ctx).Infof("Resolving FireFly contract at index %d", contracts.Active.Index) - address, fromBlock, err := e.resolveFireFlyContract(e.ctx, contracts.Active.Index) + log.L(ctx).Infof("Resolving FireFly contract at index %d", contracts.Active.Index) + address, fromBlock, err := e.resolveFireFlyContract(ctx, contracts.Active.Index) if err != nil { return err } if _, ok := finalEvents[address]; ok { - return i18n.NewError(e.ctx, coremsgs.MsgCannotReuseFireFlyContract, address) - } - contracts.Active.Info = fftypes.JSONObject{ - "address": address, - "fromBlock": fromBlock, + return i18n.NewError(ctx, coremsgs.MsgCannotReuseFireFlyContract, address) } e.fireflyContract = address e.fireflyFromBlock = fromBlock e.finalEvents = finalEvents - e.initInfo.sub, err = e.streams.ensureFireFlySubscription(e.ctx, e.fireflyContract, e.fireflyFromBlock, e.initInfo.stream.ID, batchPinEventABI) + e.initInfo.sub, err = e.streams.ensureFireFlySubscription(ctx, e.fireflyContract, e.fireflyFromBlock, e.initInfo.stream.ID, batchPinEventABI) + if err == nil { + contracts.Active.Info = fftypes.JSONObject{ + "address": address, + "fromBlock": fromBlock, + "subscription": e.initInfo.sub.ID, + } + } return err } -func (e *Ethereum) TerminateContract(contracts *core.FireFlyContracts, termination *blockchain.Event) (err error) { +func (e *Ethereum) TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *blockchain.Event) (err error) { - address, err := validateEthAddress(e.ctx, termination.Info.GetString("address")) + address, err := validateEthAddress(ctx, termination.Info.GetString("address")) if err != nil { return err } if address != e.fireflyContract { - log.L(e.ctx).Warnf("Ignoring termination request from address %s, which differs from active address %s", address, e.fireflyContract) + log.L(ctx).Warnf("Ignoring termination request from address %s, which differs from active address %s", address, e.fireflyContract) return nil } - log.L(e.ctx).Infof("Processing termination request from address %s", address) - contracts.Terminated = append(contracts.Terminated, core.FireFlyContractInfo{ - Index: contracts.Active.Index, - Info: contracts.Active.Info, - FinalEvent: termination.ProtocolID, - }) + log.L(ctx).Infof("Processing termination request from address %s", address) + contracts.Active.FinalEvent = termination.ProtocolID + contracts.Terminated = append(contracts.Terminated, contracts.Active) contracts.Active = core.FireFlyContractInfo{Index: contracts.Active.Index + 1} - return e.ConfigureContract(contracts) + return e.ConfigureContract(ctx, contracts) } func (e *Ethereum) Capabilities() *blockchain.Capabilities { @@ -468,13 +468,13 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON func (e *Ethereum) handleContractEvent(ctx context.Context, msgJSON fftypes.JSONObject) (err error) { event := e.parseBlockchainEvent(ctx, msgJSON) - if event == nil { - return nil // move on + if event != nil { + err = e.callbacks.BlockchainEvent(&blockchain.EventWithSubscription{ + Event: *event, + Subscription: msgJSON.GetString("subId"), + }) } - return e.callbacks.BlockchainEvent(&blockchain.EventWithSubscription{ - Event: *event, - Subscription: msgJSON.GetString("subId"), - }) + return err } func (e *Ethereum) handleReceipt(ctx context.Context, reply fftypes.JSONObject) { @@ -680,7 +680,7 @@ func (e *Ethereum) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID return e.invokeContractMethod(ctx, e.fireflyContract, signingKey, batchPinMethodABI, operationID.String(), input) } -func (e *Ethereum) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action string) error { +func (e *Ethereum) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.OperatorActionType) error { input := []interface{}{ blockchain.FireFlyActionPrefix + action, ethHexFormatB32(nil), diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index 10a4dbf38..e849ee3ff 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -174,7 +174,7 @@ func TestInitAndStartWithFFTM(t *testing.T) { assert.Equal(t, "ethereum", e.Name()) assert.Equal(t, core.VerifierTypeEthAddress, e.VerifierType()) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, 4, httpmock.GetTotalCallCount()) @@ -236,7 +236,7 @@ func TestInitMissingInstance(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.Regexp(t, "FF10138.*instance", err) } @@ -267,7 +267,7 @@ func TestInitAllExistingStreams(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, 3, httpmock.GetTotalCallCount()) @@ -317,7 +317,7 @@ func TestInitOldInstancePathContracts(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, e.fireflyContract, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f") @@ -354,7 +354,7 @@ func TestInitOldInstancePathInstances(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, e.fireflyContract, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f") @@ -394,7 +394,7 @@ func TestInitOldInstancePathError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.Regexp(t, "FF10111.*pop", err) } @@ -424,7 +424,7 @@ func TestInitNewConfig(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, e.fireflyContract, "0x71c7656ec7ab88b098defb751b7401b5f6d8976f") @@ -452,7 +452,7 @@ func TestInitNewConfigError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.Regexp(t, "FF10138", err) } @@ -478,7 +478,7 @@ func TestInitNewConfigBadIndex(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{ + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{ Active: core.FireFlyContractInfo{Index: 1}, }) assert.Regexp(t, "FF10387", err) @@ -510,7 +510,7 @@ func TestInitNewConfigSwitchBack(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{ + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{ Terminated: []core.FireFlyContractInfo{ { Info: fftypes.JSONObject{"address": "0x71c7656ec7ab88b098defb751b7401b5f6d8976f"}, @@ -521,6 +521,142 @@ func TestInitNewConfigSwitchBack(t *testing.T) { assert.Regexp(t, "FF10388", err) } +func TestInitTerminateContract(t *testing.T) { + e, _ := newTestEthereum() + + contracts := &core.FireFlyContracts{} + event := &blockchain.Event{ + ProtocolID: "000000000011/000000/000050", + Info: fftypes.JSONObject{ + "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", + }, + } + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sb-1"})) + + resetConf(e) + utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x1C197604587F046FD40684A8f21f4609FB811A7b") + utConfig.AddKnownKey(FireFlyContractConfigKey+".1."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.ConfigureContract(e.ctx, contracts) + assert.NoError(t, err) + + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sb-2"})) + + err = e.TerminateContract(e.ctx, contracts, event) + assert.NoError(t, err) + + assert.Equal(t, 1, contracts.Active.Index) + assert.Equal(t, fftypes.JSONObject{ + "address": "0x71c7656ec7ab88b098defb751b7401b5f6d8976f", + "fromBlock": "oldest", + "subscription": "sb-2", + }, contracts.Active.Info) + assert.Len(t, contracts.Terminated, 1) + assert.Equal(t, 0, contracts.Terminated[0].Index) + assert.Equal(t, fftypes.JSONObject{ + "address": "0x1c197604587f046fd40684a8f21f4609fb811a7b", + "fromBlock": "oldest", + "subscription": "sb-1", + }, contracts.Terminated[0].Info) + assert.Equal(t, event.ProtocolID, contracts.Terminated[0].FinalEvent) +} + +func TestInitTerminateContractIgnore(t *testing.T) { + e, _ := newTestEthereum() + + contracts := &core.FireFlyContracts{} + event := &blockchain.Event{ + ProtocolID: "000000000011/000000/000050", + Info: fftypes.JSONObject{ + "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", + }, + } + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, subscription{})) + + resetConf(e) + utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.ConfigureContract(e.ctx, contracts) + assert.NoError(t, err) + + err = e.TerminateContract(e.ctx, contracts, event) + assert.NoError(t, err) +} + +func TestInitTerminateContractBadEvent(t *testing.T) { + e, _ := newTestEthereum() + + contracts := &core.FireFlyContracts{} + event := &blockchain.Event{ + ProtocolID: "000000000011/000000/000050", + Info: fftypes.JSONObject{ + "address": "bad", + }, + } + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, subscription{})) + + resetConf(e) + utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.ConfigureContract(e.ctx, contracts) + assert.NoError(t, err) + + err = e.TerminateContract(e.ctx, contracts, event) + assert.Regexp(t, "FF10141", err) +} + func TestStreamQueryError(t *testing.T) { e, cancel := newTestEthereum() @@ -622,7 +758,7 @@ func TestSubQueryError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.Regexp(t, "FF10111.*pop", err) } @@ -654,7 +790,7 @@ func TestSubQueryCreateError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.Regexp(t, "FF10111.*pop", err) } @@ -932,6 +1068,141 @@ func TestHandleMessageBatchPinOK(t *testing.T) { } +func TestHandleMessageBatchPinIgnore(t *testing.T) { + data := fftypes.JSONAnyPtr(` +[ + { + "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", + "blockNumber": "38011", + "transactionIndex": "0x0", + "transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628", + "data": { + "author": "0X91D2B4381A4CD5C7C0F27565A7D4B829844C8635", + "namespace": "ns1", + "uuids": "0xe19af8b390604051812d7597d19adfb9847d3bfd074249efb65d3fed15f5b0a6", + "batchHash": "0xd71eb138d74c229a388eb0e1abc03f4c7cbb21d4fc4b839fbf0ec73e4263f6be", + "payloadRef": "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD", + "contexts": [ + "0x68e4da79f805bca5b912bcda9c63d03e6e867108dabb9b944109aea541ef522a", + "0x19b82093de5ce92a01e333048e877e2374354bf846dd034864ef6ffbd6438771" + ] + }, + "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", + "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", + "logIndex": "50", + "timestamp": "1620576488" + } +]`) + + em := &blockchainmocks.Callbacks{} + e := &Ethereum{ + callbacks: em, + finalEvents: map[string]string{ + "0x1c197604587f046fd40684a8f21f4609fb811a7b": "000000038011/000000/000049", + }, + } + e.initInfo.sub = &subscription{ + ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", + } + + var events []interface{} + err := json.Unmarshal(data.Bytes(), &events) + assert.NoError(t, err) + err = e.handleMessageBatch(context.Background(), events) + assert.NoError(t, err) + + em.AssertExpectations(t) + +} + +func TestHandleMessageBatchPinMissingAuthor(t *testing.T) { + data := fftypes.JSONAnyPtr(` +[ + { + "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", + "blockNumber": "38011", + "transactionIndex": "0x0", + "transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628", + "data": { + "author": "", + "namespace": "ns1", + "uuids": "0xe19af8b390604051812d7597d19adfb9847d3bfd074249efb65d3fed15f5b0a6", + "batchHash": "0xd71eb138d74c229a388eb0e1abc03f4c7cbb21d4fc4b839fbf0ec73e4263f6be", + "payloadRef": "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD", + "contexts": [ + "0x68e4da79f805bca5b912bcda9c63d03e6e867108dabb9b944109aea541ef522a", + "0x19b82093de5ce92a01e333048e877e2374354bf846dd034864ef6ffbd6438771" + ] + }, + "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", + "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", + "logIndex": "50", + "timestamp": "1620576488" + } +]`) + + em := &blockchainmocks.Callbacks{} + e := &Ethereum{ + callbacks: em, + } + e.initInfo.sub = &subscription{ + ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", + } + + var events []interface{} + err := json.Unmarshal(data.Bytes(), &events) + assert.NoError(t, err) + err = e.handleMessageBatch(context.Background(), events) + assert.NoError(t, err) + + em.AssertExpectations(t) + +} + +func TestHandleMessageBatchPinBadAddress(t *testing.T) { + data := fftypes.JSONAnyPtr(` +[ + { + "address": "!bad", + "blockNumber": "38011", + "transactionIndex": "0x0", + "transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628", + "data": { + "author": "0X91D2B4381A4CD5C7C0F27565A7D4B829844C8635", + "namespace": "ns1", + "uuids": "0xe19af8b390604051812d7597d19adfb9847d3bfd074249efb65d3fed15f5b0a6", + "batchHash": "0xd71eb138d74c229a388eb0e1abc03f4c7cbb21d4fc4b839fbf0ec73e4263f6be", + "payloadRef": "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD", + "contexts": [ + "0x68e4da79f805bca5b912bcda9c63d03e6e867108dabb9b944109aea541ef522a", + "0x19b82093de5ce92a01e333048e877e2374354bf846dd034864ef6ffbd6438771" + ] + }, + "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", + "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", + "logIndex": "50", + "timestamp": "1620576488" + } +]`) + + em := &blockchainmocks.Callbacks{} + e := &Ethereum{ + callbacks: em, + } + e.initInfo.sub = &subscription{ + ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", + } + + var events []interface{} + err := json.Unmarshal(data.Bytes(), &events) + assert.NoError(t, err) + err = e.handleMessageBatch(context.Background(), events) + assert.NoError(t, err) + + em.AssertExpectations(t) + +} + func TestHandleMessageEmptyPayloadRef(t *testing.T) { data := fftypes.JSONAnyPtr(` [ @@ -1089,6 +1360,7 @@ func TestHandleMessageBatchPinBadTransactionID(t *testing.T) { ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", } data := fftypes.JSONAnyPtr(`[{ + "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", "blockNumber": "38011", @@ -1122,6 +1394,7 @@ func TestHandleMessageBatchPinBadIDentity(t *testing.T) { ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", } data := fftypes.JSONAnyPtr(`[{ + "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", "blockNumber": "38011", @@ -1155,6 +1428,7 @@ func TestHandleMessageBatchPinBadBatchHash(t *testing.T) { ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", } data := fftypes.JSONAnyPtr(`[{ + "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", "blockNumber": "38011", @@ -1188,6 +1462,7 @@ func TestHandleMessageBatchPinBadPin(t *testing.T) { ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", } data := fftypes.JSONAnyPtr(`[{ + "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", "blockNumber": "38011", @@ -1358,7 +1633,7 @@ func TestHandleBadPayloadsAndThenReceiptFailure(t *testing.T) { <-done } -func TestHandleMsgBatchBadDAta(t *testing.T) { +func TestHandleMsgBatchBadData(t *testing.T) { em := &blockchainmocks.Callbacks{} wsm := &wsmocks.WSClient{} e := &Ethereum{ @@ -2620,7 +2895,7 @@ func TestSubmitOperatorAction(t *testing.T) { return httpmock.NewJsonResponderOrPanic(200, "")(req) }) - err := e.SubmitOperatorAction(context.Background(), fftypes.NewUUID(), "0x123", core.OperatorActionTerminate.String()) + err := e.SubmitOperatorAction(context.Background(), fftypes.NewUUID(), "0x123", core.OperatorActionTerminate) assert.NoError(t, err) } diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index c32d665b0..b764d9b1e 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -240,55 +240,51 @@ func (f *Fabric) resolveFireFlyContract(ctx context.Context, contractIndex int) return chaincode, fromBlock, nil } -func (f *Fabric) ConfigureContract(contracts *core.FireFlyContracts) (err error) { - - fireflyChaincode, fireflyFromBlock, err := f.resolveFireFlyContract(f.ctx, contracts.Active.Index) - if err != nil { - return err - } - if _, ok := f.finalEvents[fireflyChaincode]; ok { - log.L(f.ctx).Warnf("Cannot switch back to previously-used chaincode %s", fireflyChaincode) - return nil - } - contracts.Active.Info = fftypes.JSONObject{ - "chaincode": fireflyChaincode, - "fromBlock": fireflyFromBlock, - } - - f.fireflyChaincode = fireflyChaincode - f.fireflyFromBlock = fireflyFromBlock - f.finalEvents = make(map[string]string, len(contracts.Terminated)) +func (f *Fabric) ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) (err error) { + finalEvents := make(map[string]string, len(contracts.Terminated)) for i, oldContract := range contracts.Terminated { chaincode := oldContract.Info.GetString("chaincode") if chaincode != "" { - f.finalEvents[chaincode] = contracts.Terminated[i].FinalEvent + finalEvents[chaincode] = contracts.Terminated[i].FinalEvent } } - location := &Location{ - Channel: f.defaultChannel, - Chaincode: f.fireflyChaincode, + chaincode, fromBlock, err := f.resolveFireFlyContract(ctx, contracts.Active.Index) + if err != nil { + return err + } + if _, ok := finalEvents[chaincode]; ok { + return i18n.NewError(ctx, coremsgs.MsgCannotReuseFireFlyContract, chaincode) + } + + f.fireflyChaincode = chaincode + f.fireflyFromBlock = fromBlock + f.finalEvents = finalEvents + location := &Location{Channel: f.defaultChannel, Chaincode: chaincode} + f.initInfo.sub, err = f.streams.ensureFireFlySubscription(ctx, location, f.fireflyFromBlock, f.initInfo.stream.ID, batchPinEvent) + if err == nil { + contracts.Active.Info = fftypes.JSONObject{ + "chaincode": chaincode, + "fromBlock": fromBlock, + "subscription": f.initInfo.sub.ID, + } } - f.initInfo.sub, err = f.streams.ensureFireFlySubscription(f.ctx, location, f.fireflyFromBlock, f.initInfo.stream.ID, batchPinEvent) return err } -func (f *Fabric) TerminateContract(contracts *core.FireFlyContracts, termination *blockchain.Event) (err error) { +func (f *Fabric) TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *blockchain.Event) (err error) { chaincode := termination.Info.GetString("chaincodeId") if chaincode != f.fireflyChaincode { - log.L(f.ctx).Warnf("Ignoring termination request from chaincode %s, which differs from active chaincode %s", chaincode, f.fireflyChaincode) + log.L(ctx).Warnf("Ignoring termination request from chaincode %s, which differs from active chaincode %s", chaincode, f.fireflyChaincode) return nil } - log.L(f.ctx).Infof("Processing termination request from chaincode %s", chaincode) - contracts.Terminated = append(contracts.Terminated, core.FireFlyContractInfo{ - Index: contracts.Active.Index, - Info: contracts.Active.Info, - FinalEvent: termination.ProtocolID, - }) + log.L(ctx).Infof("Processing termination request from chaincode %s", chaincode) + contracts.Active.FinalEvent = termination.ProtocolID + contracts.Terminated = append(contracts.Terminated, contracts.Active) contracts.Active = core.FireFlyContractInfo{Index: contracts.Active.Index + 1} - return f.ConfigureContract(contracts) + return f.ConfigureContract(ctx, contracts) } func (f *Fabric) Start() (err error) { @@ -656,7 +652,7 @@ func (f *Fabric) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, return f.invokeContractMethod(ctx, f.defaultChannel, f.fireflyChaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) } -func (f *Fabric) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action string) error { +func (f *Fabric) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.OperatorActionType) error { pinInput := map[string]interface{}{ "namespace": "firefly:" + action, "uuids": hexFormatB32(nil), diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index 86ada6437..ff818830c 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -166,7 +166,7 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) { assert.Equal(t, "fabric", e.Name()) assert.Equal(t, core.VerifierTypeMSPIdentity, e.VerifierType()) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.NoError(t, err) err = e.Start() assert.NoError(t, err) @@ -207,6 +207,32 @@ func TestWSInitFail(t *testing.T) { } +func TestInitMissingInstance(t *testing.T) { + + e, cancel := newTestFabric() + defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) + assert.Regexp(t, "FF10138.*chaincode", err) + +} + func TestInitAllExistingStreams(t *testing.T) { e, cancel := newTestFabric() @@ -232,7 +258,7 @@ func TestInitAllExistingStreams(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, 2, httpmock.GetTotalCallCount()) @@ -266,7 +292,7 @@ func TestInitNewConfig(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.NoError(t, err) assert.Equal(t, 2, httpmock.GetTotalCallCount()) @@ -296,7 +322,7 @@ func TestInitNewConfigError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.Regexp(t, "FF10138", err) } @@ -322,13 +348,155 @@ func TestInitNewConfigBadIndex(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{ + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{ Active: core.FireFlyContractInfo{Index: 1}, }) assert.Regexp(t, "FF10387", err) } +func TestInitNewConfigSwitchBack(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + resetConf(e) + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, subscription{})) + + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{ + Terminated: []core.FireFlyContractInfo{ + { + Info: fftypes.JSONObject{"chaincode": "firefly"}, + FinalEvent: "1", + }, + }, + }) + assert.Regexp(t, "FF10388", err) +} + +func TestInitTerminateContract(t *testing.T) { + e, _ := newTestFabric() + + contracts := &core.FireFlyContracts{} + event := &blockchain.Event{ + ProtocolID: "000000000011/000000/000050", + Info: fftypes.JSONObject{ + "chaincodeId": "firefly", + }, + } + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sb-1"})) + + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly") + utConfig.AddKnownKey(FireFlyContractConfigKey+".1."+FireFlyContractChaincode, "firefly2") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.ConfigureContract(e.ctx, contracts) + assert.NoError(t, err) + + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sb-2"})) + + err = e.TerminateContract(e.ctx, contracts, event) + assert.NoError(t, err) + + assert.Equal(t, 1, contracts.Active.Index) + assert.Equal(t, fftypes.JSONObject{ + "chaincode": "firefly2", + "fromBlock": "oldest", + "subscription": "sb-2", + }, contracts.Active.Info) + assert.Len(t, contracts.Terminated, 1) + assert.Equal(t, 0, contracts.Terminated[0].Index) + assert.Equal(t, fftypes.JSONObject{ + "chaincode": "firefly", + "fromBlock": "oldest", + "subscription": "sb-1", + }, contracts.Terminated[0].Info) + assert.Equal(t, event.ProtocolID, contracts.Terminated[0].FinalEvent) +} + +func TestInitTerminateContractIgnore(t *testing.T) { + e, _ := newTestFabric() + + contracts := &core.FireFlyContracts{} + event := &blockchain.Event{ + ProtocolID: "000000000011/000000/000050", + Info: fftypes.JSONObject{ + "chaincodeId": "firefly2", + }, + } + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sb-1"})) + + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") + utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly") + + err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) + assert.NoError(t, err) + err = e.ConfigureContract(e.ctx, contracts) + assert.NoError(t, err) + + err = e.TerminateContract(e.ctx, contracts, event) + assert.NoError(t, err) + + assert.Equal(t, 0, contracts.Active.Index) + assert.Equal(t, fftypes.JSONObject{ + "chaincode": "firefly", + "fromBlock": "oldest", + "subscription": "sb-1", + }, contracts.Active.Info) + assert.Len(t, contracts.Terminated, 0) +} + func TestStreamQueryError(t *testing.T) { e, cancel := newTestFabric() @@ -407,7 +575,7 @@ func TestSubQueryError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.Regexp(t, "FF10284.*pop", err) } @@ -440,7 +608,7 @@ func TestSubQueryCreateError(t *testing.T) { err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) assert.NoError(t, err) - err = e.ConfigureContract(&core.FireFlyContracts{}) + err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{}) assert.Regexp(t, "FF10284.*pop", err) } @@ -725,6 +893,42 @@ func TestHandleMessageBatchPinOK(t *testing.T) { } +func TestHandleMessageBatchPinIgnore(t *testing.T) { + data := []byte(` +[ + { + "chaincodeId": "firefly", + "blockNumber": 91, + "transactionId": "ce79343000e851a0c742f63a733ce19a5f8b9ce1c719b6cecd14f01bcf81fff2", + "transactionIndex": 2, + "eventIndex": 50, + "eventName": "BatchPin", + "payload": "eyJzaWduZXIiOiJ1MHZnd3U5czAwLXg1MDk6OkNOPXVzZXIyLE9VPWNsaWVudDo6Q049ZmFicmljLWNhLXNlcnZlciIsInRpbWVzdGFtcCI6eyJzZWNvbmRzIjoxNjMwMDMxNjY3LCJuYW5vcyI6NzkxNDk5MDAwfSwibmFtZXNwYWNlIjoibnMxIiwidXVpZHMiOiIweGUxOWFmOGIzOTA2MDQwNTE4MTJkNzU5N2QxOWFkZmI5ODQ3ZDNiZmQwNzQyNDllZmI2NWQzZmVkMTVmNWIwYTYiLCJiYXRjaEhhc2giOiIweGQ3MWViMTM4ZDc0YzIyOWEzODhlYjBlMWFiYzAzZjRjN2NiYjIxZDRmYzRiODM5ZmJmMGVjNzNlNDI2M2Y2YmUiLCJwYXlsb2FkUmVmIjoiUW1mNDEyalFaaXVWVXRkZ25CMzZGWEZYN3hnNVY2S0ViU0o0ZHBRdWhrTHlmRCIsImNvbnRleHRzIjpbIjB4NjhlNGRhNzlmODA1YmNhNWI5MTJiY2RhOWM2M2QwM2U2ZTg2NzEwOGRhYmI5Yjk0NDEwOWFlYTU0MWVmNTIyYSIsIjB4MTliODIwOTNkZTVjZTkyYTAxZTMzMzA0OGU4NzdlMjM3NDM1NGJmODQ2ZGQwMzQ4NjRlZjZmZmJkNjQzODc3MSJdfQ==", + "subId": "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e" + } +]`) + + em := &blockchainmocks.Callbacks{} + e := &Fabric{ + callbacks: em, + finalEvents: map[string]string{ + "firefly": "000000000090/000000/000000", + }, + } + e.initInfo.sub = &subscription{ + ID: "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e", + } + + var events []interface{} + err := json.Unmarshal(data, &events) + assert.NoError(t, err) + err = e.handleMessageBatch(context.Background(), events) + assert.NoError(t, err) + + em.AssertExpectations(t) + +} + func TestHandleMessageEmptyPayloadRef(t *testing.T) { data := []byte(` [ @@ -1766,7 +1970,7 @@ func TestSubmitOperatorAction(t *testing.T) { return httpmock.NewJsonResponderOrPanic(200, "")(req) }) - err := e.SubmitOperatorAction(context.Background(), nil, signer, "terminate") + err := e.SubmitOperatorAction(context.Background(), nil, signer, core.OperatorActionTerminate) assert.NoError(t, err) } diff --git a/internal/events/operator_action.go b/internal/events/operator_action.go index c3196a84d..a94fa8c69 100644 --- a/internal/events/operator_action.go +++ b/internal/events/operator_action.go @@ -30,7 +30,7 @@ func (em *eventManager) actionTerminate(bi blockchain.Plugin, event *blockchain. if err != nil { return err } - if err := bi.TerminateContract(&ns.Contracts, event); err != nil { + if err := bi.TerminateContract(ctx, &ns.Contracts, event); err != nil { return err } return em.database.UpsertNamespace(ctx, ns, true) diff --git a/internal/events/operator_action_test.go b/internal/events/operator_action_test.go index fbae03e10..54c07070d 100644 --- a/internal/events/operator_action_test.go +++ b/internal/events/operator_action_test.go @@ -47,7 +47,7 @@ func TestOperatorAction(t *testing.T) { mdi.On("InsertEvent", em.ctx, mock.Anything).Return(nil) mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) mdi.On("UpsertNamespace", em.ctx, mock.AnythingOfType("*core.Namespace"), true).Return(nil) - mbi.On("TerminateContract", mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil) + mbi.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil) err := em.BlockchainOperatorAction(mbi, "terminate", event, &core.VerifierRef{}) assert.NoError(t, err) @@ -85,6 +85,23 @@ func TestActionTerminateQueryFail(t *testing.T) { mdi.AssertExpectations(t) } +func TestActionTerminateFail(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + mbi := &blockchainmocks.Plugin{} + mdi := em.database.(*databasemocks.Plugin) + + mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) + mbi.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(fmt.Errorf("pop")) + + err := em.actionTerminate(mbi, &blockchain.Event{}) + assert.EqualError(t, err, "pop") + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + func TestActionTerminateUpsertFail(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() @@ -94,7 +111,7 @@ func TestActionTerminateUpsertFail(t *testing.T) { mdi.On("GetNamespace", em.ctx, "ff_system").Return(&core.Namespace{}, nil) mdi.On("UpsertNamespace", em.ctx, mock.AnythingOfType("*core.Namespace"), true).Return(fmt.Errorf("pop")) - mbi.On("TerminateContract", mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil) + mbi.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil) err := em.actionTerminate(mbi, &blockchain.Event{}) assert.EqualError(t, err, "pop") diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 378d3c6b0..fb47cf759 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -230,7 +230,7 @@ func (or *orchestrator) Start() (err error) { } if err == nil { for _, el := range or.blockchains { - if err = el.ConfigureContract(&ns.Contracts); err != nil { + if err = el.ConfigureContract(or.ctx, &ns.Contracts); err != nil { break } if err = el.Start(); err != nil { @@ -888,5 +888,5 @@ func (or *orchestrator) SubmitOperatorAction(ctx context.Context, action *core.O if action.Type != core.OperatorActionTerminate { return i18n.NewError(ctx, coremsgs.MsgUnrecognizedOperatorAction, action.Type) } - return or.blockchain.SubmitOperatorAction(ctx, fftypes.NewUUID(), verifier.Value, action.Type.String()) + return or.blockchain.SubmitOperatorAction(ctx, fftypes.NewUUID(), verifier.Value, action.Type) } diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 79e456d3f..ad4ee94df 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -980,7 +980,7 @@ func TestStartTokensFail(t *testing.T) { defer or.cleanup(t) or.database = or.mdi or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) - or.mbi.On("ConfigureContract", &core.FireFlyContracts{}).Return(nil) + or.mbi.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(nil) or.mbi.On("Start").Return(nil) or.mba.On("Start").Return(nil) or.mem.On("Start").Return(nil) @@ -1000,20 +1000,32 @@ func TestStartBlockchainsFail(t *testing.T) { defer or.cleanup(t) or.database = or.mdi or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) - or.mbi.On("ConfigureContract", &core.FireFlyContracts{}).Return(nil) + or.mbi.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(nil) or.mbi.On("Start").Return(fmt.Errorf("pop")) or.mba.On("Start").Return(nil) err := or.Start() assert.EqualError(t, err, "pop") } +func TestStartBlockchainsConfigureFail(t *testing.T) { + coreconfig.Reset() + or := newTestOrchestrator() + defer or.cleanup(t) + or.database = or.mdi + or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) + or.mbi.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(fmt.Errorf("pop")) + or.mba.On("Start").Return(nil) + err := or.Start() + assert.EqualError(t, err, "pop") +} + func TestStartStopOk(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) or.database = or.mdi or.mdi.On("GetNamespace", mock.Anything, "ff_system").Return(&core.Namespace{}, nil) - or.mbi.On("ConfigureContract", &core.FireFlyContracts{}).Return(nil) + or.mbi.On("ConfigureContract", mock.Anything, &core.FireFlyContracts{}).Return(nil) or.mbi.On("Start").Return(nil) or.mba.On("Start").Return(nil) or.mem.On("Start").Return(nil) @@ -1128,7 +1140,7 @@ func TestOperatorAction(t *testing.T) { or.blockchain = or.mbi verifier := &core.VerifierRef{Value: "0x123"} or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(verifier, nil) - or.mbi.On("SubmitOperatorAction", context.Background(), mock.Anything, "0x123", "terminate").Return(nil) + or.mbi.On("SubmitOperatorAction", context.Background(), mock.Anything, "0x123", core.OperatorActionTerminate).Return(nil) err := or.SubmitOperatorAction(context.Background(), &core.OperatorAction{Type: core.OperatorActionTerminate}) assert.NoError(t, err) } @@ -1139,3 +1151,11 @@ func TestOperatorActionBadKey(t *testing.T) { err := or.SubmitOperatorAction(context.Background(), &core.OperatorAction{Type: core.OperatorActionTerminate}) assert.EqualError(t, err, "pop") } + +func TestOperatorActionBadType(t *testing.T) { + or := newTestOrchestrator() + verifier := &core.VerifierRef{Value: "0x123"} + or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(verifier, nil) + err := or.SubmitOperatorAction(context.Background(), &core.OperatorAction{Type: "bad"}) + assert.Regexp(t, "FF10389", err) +} diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go index c4141f775..40671ba35 100644 --- a/mocks/blockchainmocks/plugin.go +++ b/mocks/blockchainmocks/plugin.go @@ -52,13 +52,13 @@ func (_m *Plugin) Capabilities() *blockchain.Capabilities { return r0 } -// ConfigureContract provides a mock function with given fields: contracts -func (_m *Plugin) ConfigureContract(contracts *core.FireFlyContracts) error { - ret := _m.Called(contracts) +// ConfigureContract provides a mock function with given fields: ctx, contracts +func (_m *Plugin) ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) error { + ret := _m.Called(ctx, contracts) var r0 error - if rf, ok := ret.Get(0).(func(*core.FireFlyContracts) error); ok { - r0 = rf(contracts) + if rf, ok := ret.Get(0).(func(context.Context, *core.FireFlyContracts) error); ok { + r0 = rf(ctx, contracts) } else { r0 = ret.Error(0) } @@ -283,11 +283,11 @@ func (_m *Plugin) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, } // SubmitOperatorAction provides a mock function with given fields: ctx, operationID, signingKey, action -func (_m *Plugin) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action string) error { +func (_m *Plugin) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.FFEnum) error { ret := _m.Called(ctx, operationID, signingKey, action) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, string, string) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, string, core.FFEnum) error); ok { r0 = rf(ctx, operationID, signingKey, action) } else { r0 = ret.Error(0) @@ -296,13 +296,13 @@ func (_m *Plugin) SubmitOperatorAction(ctx context.Context, operationID *fftypes return r0 } -// TerminateContract provides a mock function with given fields: contracts, termination -func (_m *Plugin) TerminateContract(contracts *core.FireFlyContracts, termination *blockchain.Event) error { - ret := _m.Called(contracts, termination) +// TerminateContract provides a mock function with given fields: ctx, contracts, termination +func (_m *Plugin) TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *blockchain.Event) error { + ret := _m.Called(ctx, contracts, termination) var r0 error - if rf, ok := ret.Get(0).(func(*core.FireFlyContracts, *blockchain.Event) error); ok { - r0 = rf(contracts, termination) + if rf, ok := ret.Get(0).(func(context.Context, *core.FireFlyContracts, *blockchain.Event) error); ok { + r0 = rf(ctx, contracts, termination) } else { r0 = ret.Error(0) } diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index 8dcc6efac..4b77293d3 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -38,13 +38,13 @@ type Plugin interface { // ConfigureContract initializes the subscription to the FireFly contract // - Checks the provided contract info against the plugin's configuration, and updates it as needed // - Initializes the contract info for performing BatchPin transactions, and initializes subscriptions for BatchPin events - ConfigureContract(contracts *core.FireFlyContracts) (err error) + ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) (err error) // TerminateContract marks the given event as the last one to be parsed on the current FireFly contract // - Validates that the event came from the currently active FireFly contract // - Re-initializes the plugin against the next configured FireFly contract // - Updates the provided contract info to record the point of termination and the newly active contract - TerminateContract(contracts *core.FireFlyContracts, termination *Event) (err error) + TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *Event) (err error) // Blockchain interface must not deliver any events until start is called Start() error @@ -64,7 +64,7 @@ type Plugin interface { SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, signingKey string, batch *BatchPin) error // SubmitOperatorAction writes a special "BatchPin" event which signals the plugin to take an action - SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey, action string) error + SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.OperatorActionType) error // InvokeContract submits a new transaction to be executed by custom on-chain logic InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error diff --git a/pkg/core/namespace_test.go b/pkg/core/namespace_test.go index c225584a7..769fbecd3 100644 --- a/pkg/core/namespace_test.go +++ b/pkg/core/namespace_test.go @@ -51,3 +51,41 @@ func TestNamespaceValidation(t *testing.T) { assert.NotNil(t, ns.Message) } + +func TestFireFlyContractsDatabaseSerialization(t *testing.T) { + contracts1 := &FireFlyContracts{ + Active: FireFlyContractInfo{ + Index: 1, + Info: fftypes.JSONObject{"address": "0x1234"}, + }, + Terminated: []FireFlyContractInfo{ + { + Index: 0, + Info: fftypes.JSONObject{"address": "0x0000"}, + FinalEvent: "50", + }, + }, + } + + // Verify it serializes as bytes to the database + val1, err := contracts1.Value() + assert.NoError(t, err) + assert.Equal(t, `{"active":{"index":1,"info":{"address":"0x1234"}},"terminated":[{"index":0,"finalEvent":"50","info":{"address":"0x0000"}}]}`, string(val1.([]byte))) + + // Verify it restores ok + contracts2 := &FireFlyContracts{} + err = contracts2.Scan(val1) + assert.NoError(t, err) + assert.Equal(t, 1, contracts2.Active.Index) + assert.Equal(t, fftypes.JSONObject{"address": "0x1234"}, contracts2.Active.Info) + assert.Len(t, contracts2.Terminated, 1) + + // Verify it ignores a blank string + err = contracts2.Scan("") + assert.NoError(t, err) + assert.Equal(t, 1, contracts2.Active.Index) + + // Out of luck with anything else + err = contracts2.Scan(false) + assert.Regexp(t, "FF00105", err) +} From 864af2ef286a0356b662a57ea96890f2a271e6b5 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Tue, 24 May 2022 16:26:37 -0400 Subject: [PATCH 08/13] Remove tracking of "final events" from blockchain event processing This is already covered by tracking of the current subscription ID, which will filter out events from unrecognized/old subscriptions. Signed-off-by: Andrew Richardson --- internal/blockchain/ethereum/ethereum.go | 25 ---- internal/blockchain/ethereum/ethereum_test.go | 128 ------------------ internal/blockchain/fabric/fabric.go | 21 --- internal/blockchain/fabric/fabric_test.go | 73 ---------- internal/coremsgs/en_error_messages.go | 3 +- internal/orchestrator/orchestrator_test.go | 2 +- 6 files changed, 2 insertions(+), 250 deletions(-) diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index dde5e4cdb..e39b29607 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -52,7 +52,6 @@ type Ethereum struct { topic string fireflyContract string fireflyFromBlock string - finalEvents map[string]string prefixShort string prefixLong string capabilities *blockchain.Capabilities @@ -268,26 +267,14 @@ func (e *Ethereum) resolveFireFlyContract(ctx context.Context, contractIndex int func (e *Ethereum) ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) (err error) { - finalEvents := make(map[string]string, len(contracts.Terminated)) - for i, oldContract := range contracts.Terminated { - address := oldContract.Info.GetString("address") - if address != "" { - finalEvents[address] = contracts.Terminated[i].FinalEvent - } - } - log.L(ctx).Infof("Resolving FireFly contract at index %d", contracts.Active.Index) address, fromBlock, err := e.resolveFireFlyContract(ctx, contracts.Active.Index) if err != nil { return err } - if _, ok := finalEvents[address]; ok { - return i18n.NewError(ctx, coremsgs.MsgCannotReuseFireFlyContract, address) - } e.fireflyContract = address e.fireflyFromBlock = fromBlock - e.finalEvents = finalEvents e.initInfo.sub, err = e.streams.ensureFireFlySubscription(ctx, e.fireflyContract, e.fireflyFromBlock, e.initInfo.stream.ID, batchPinEventABI) if err == nil { contracts.Active.Info = fftypes.JSONObject{ @@ -383,18 +370,6 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON if event == nil { return nil // move on } - address, err := validateEthAddress(ctx, event.Info.GetString("address")) - if err != nil { - log.L(ctx).Errorf("Failed to parse blockchain event address: %s", err) - return nil // move on - } - - if finalEvent, ok := e.finalEvents[address]; ok { - if event.ProtocolID > finalEvent { - log.L(ctx).Warnf("Ignoring BatchPin event %s received after termination event %s", event.ProtocolID, finalEvent) - return nil // move on - } - } authorAddress := event.Output.GetString("author") nsOrAction := event.Output.GetString("namespace") diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index 499670cae..7215e537f 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -484,43 +484,6 @@ func TestInitNewConfigBadIndex(t *testing.T) { assert.Regexp(t, "FF10388", err) } -func TestInitNewConfigSwitchBack(t *testing.T) { - e, cancel := newTestEthereum() - defer cancel() - resetConf(e) - - mockedClient := &http.Client{} - httpmock.ActivateNonDefault(mockedClient) - defer httpmock.DeactivateAndReset() - - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{})) - httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) - httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", - httpmock.NewJsonResponderOrPanic(200, []subscription{})) - httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", - httpmock.NewJsonResponderOrPanic(200, subscription{})) - - resetConf(e) - utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") - utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractAddress, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") - - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{ - Terminated: []core.FireFlyContractInfo{ - { - Info: fftypes.JSONObject{"address": "0x71c7656ec7ab88b098defb751b7401b5f6d8976f"}, - FinalEvent: "1", - }, - }, - }) - assert.Regexp(t, "FF10389", err) -} - func TestInitTerminateContract(t *testing.T) { e, _ := newTestEthereum() @@ -1068,53 +1031,6 @@ func TestHandleMessageBatchPinOK(t *testing.T) { } -func TestHandleMessageBatchPinIgnore(t *testing.T) { - data := fftypes.JSONAnyPtr(` -[ - { - "address": "0x1C197604587F046FD40684A8f21f4609FB811A7b", - "blockNumber": "38011", - "transactionIndex": "0x0", - "transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628", - "data": { - "author": "0X91D2B4381A4CD5C7C0F27565A7D4B829844C8635", - "namespace": "ns1", - "uuids": "0xe19af8b390604051812d7597d19adfb9847d3bfd074249efb65d3fed15f5b0a6", - "batchHash": "0xd71eb138d74c229a388eb0e1abc03f4c7cbb21d4fc4b839fbf0ec73e4263f6be", - "payloadRef": "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD", - "contexts": [ - "0x68e4da79f805bca5b912bcda9c63d03e6e867108dabb9b944109aea541ef522a", - "0x19b82093de5ce92a01e333048e877e2374354bf846dd034864ef6ffbd6438771" - ] - }, - "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", - "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", - "logIndex": "50", - "timestamp": "1620576488" - } -]`) - - em := &blockchainmocks.Callbacks{} - e := &Ethereum{ - callbacks: em, - finalEvents: map[string]string{ - "0x1c197604587f046fd40684a8f21f4609fb811a7b": "000000038011/000000/000049", - }, - } - e.initInfo.sub = &subscription{ - ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", - } - - var events []interface{} - err := json.Unmarshal(data.Bytes(), &events) - assert.NoError(t, err) - err = e.handleMessageBatch(context.Background(), events) - assert.NoError(t, err) - - em.AssertExpectations(t) - -} - func TestHandleMessageBatchPinMissingAuthor(t *testing.T) { data := fftypes.JSONAnyPtr(` [ @@ -1159,50 +1075,6 @@ func TestHandleMessageBatchPinMissingAuthor(t *testing.T) { } -func TestHandleMessageBatchPinBadAddress(t *testing.T) { - data := fftypes.JSONAnyPtr(` -[ - { - "address": "!bad", - "blockNumber": "38011", - "transactionIndex": "0x0", - "transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628", - "data": { - "author": "0X91D2B4381A4CD5C7C0F27565A7D4B829844C8635", - "namespace": "ns1", - "uuids": "0xe19af8b390604051812d7597d19adfb9847d3bfd074249efb65d3fed15f5b0a6", - "batchHash": "0xd71eb138d74c229a388eb0e1abc03f4c7cbb21d4fc4b839fbf0ec73e4263f6be", - "payloadRef": "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD", - "contexts": [ - "0x68e4da79f805bca5b912bcda9c63d03e6e867108dabb9b944109aea541ef522a", - "0x19b82093de5ce92a01e333048e877e2374354bf846dd034864ef6ffbd6438771" - ] - }, - "subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5", - "signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])", - "logIndex": "50", - "timestamp": "1620576488" - } -]`) - - em := &blockchainmocks.Callbacks{} - e := &Ethereum{ - callbacks: em, - } - e.initInfo.sub = &subscription{ - ID: "sb-b5b97a4e-a317-4053-6400-1474650efcb5", - } - - var events []interface{} - err := json.Unmarshal(data.Bytes(), &events) - assert.NoError(t, err) - err = e.handleMessageBatch(context.Background(), events) - assert.NoError(t, err) - - em.AssertExpectations(t) - -} - func TestHandleMessageEmptyPayloadRef(t *testing.T) { data := fftypes.JSONAnyPtr(` [ diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index b764d9b1e..eb1c1b167 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -48,7 +48,6 @@ type Fabric struct { defaultChannel string fireflyChaincode string fireflyFromBlock string - finalEvents map[string]string signer string prefixShort string prefixLong string @@ -242,25 +241,13 @@ func (f *Fabric) resolveFireFlyContract(ctx context.Context, contractIndex int) func (f *Fabric) ConfigureContract(ctx context.Context, contracts *core.FireFlyContracts) (err error) { - finalEvents := make(map[string]string, len(contracts.Terminated)) - for i, oldContract := range contracts.Terminated { - chaincode := oldContract.Info.GetString("chaincode") - if chaincode != "" { - finalEvents[chaincode] = contracts.Terminated[i].FinalEvent - } - } - chaincode, fromBlock, err := f.resolveFireFlyContract(ctx, contracts.Active.Index) if err != nil { return err } - if _, ok := finalEvents[chaincode]; ok { - return i18n.NewError(ctx, coremsgs.MsgCannotReuseFireFlyContract, chaincode) - } f.fireflyChaincode = chaincode f.fireflyFromBlock = fromBlock - f.finalEvents = finalEvents location := &Location{Channel: f.defaultChannel, Chaincode: chaincode} f.initInfo.sub, err = f.streams.ensureFireFlySubscription(ctx, location, f.fireflyFromBlock, f.initInfo.stream.ID, batchPinEvent) if err == nil { @@ -361,14 +348,6 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb return nil // move on } - chaincode := event.Info.GetString("chaincodeId") - if finalEvent, ok := f.finalEvents[chaincode]; ok { - if event.ProtocolID > finalEvent { - log.L(ctx).Warnf("Ignoring BatchPin event %s received after termination event %s", event.ProtocolID, finalEvent) - return nil // move on - } - } - signer := event.Output.GetString("signer") nsOrAction := event.Output.GetString("namespace") sUUIDs := event.Output.GetString("uuids") diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index 71ce27a40..f5b99860e 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -355,43 +355,6 @@ func TestInitNewConfigBadIndex(t *testing.T) { } -func TestInitNewConfigSwitchBack(t *testing.T) { - e, cancel := newTestFabric() - defer cancel() - resetConf(e) - - mockedClient := &http.Client{} - httpmock.ActivateNonDefault(mockedClient) - defer httpmock.DeactivateAndReset() - - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{})) - httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) - httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", - httpmock.NewJsonResponderOrPanic(200, []subscription{})) - httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", - httpmock.NewJsonResponderOrPanic(200, subscription{})) - - resetConf(e) - utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") - utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - utConfig.AddKnownKey(FireFlyContractConfigKey+".0."+FireFlyContractChaincode, "firefly") - - err := e.Init(e.ctx, utConfig, &blockchainmocks.Callbacks{}, &metricsmocks.Manager{}) - assert.NoError(t, err) - err = e.ConfigureContract(e.ctx, &core.FireFlyContracts{ - Terminated: []core.FireFlyContractInfo{ - { - Info: fftypes.JSONObject{"chaincode": "firefly"}, - FinalEvent: "1", - }, - }, - }) - assert.Regexp(t, "FF10389", err) -} - func TestInitTerminateContract(t *testing.T) { e, _ := newTestFabric() @@ -893,42 +856,6 @@ func TestHandleMessageBatchPinOK(t *testing.T) { } -func TestHandleMessageBatchPinIgnore(t *testing.T) { - data := []byte(` -[ - { - "chaincodeId": "firefly", - "blockNumber": 91, - "transactionId": "ce79343000e851a0c742f63a733ce19a5f8b9ce1c719b6cecd14f01bcf81fff2", - "transactionIndex": 2, - "eventIndex": 50, - "eventName": "BatchPin", - "payload": "eyJzaWduZXIiOiJ1MHZnd3U5czAwLXg1MDk6OkNOPXVzZXIyLE9VPWNsaWVudDo6Q049ZmFicmljLWNhLXNlcnZlciIsInRpbWVzdGFtcCI6eyJzZWNvbmRzIjoxNjMwMDMxNjY3LCJuYW5vcyI6NzkxNDk5MDAwfSwibmFtZXNwYWNlIjoibnMxIiwidXVpZHMiOiIweGUxOWFmOGIzOTA2MDQwNTE4MTJkNzU5N2QxOWFkZmI5ODQ3ZDNiZmQwNzQyNDllZmI2NWQzZmVkMTVmNWIwYTYiLCJiYXRjaEhhc2giOiIweGQ3MWViMTM4ZDc0YzIyOWEzODhlYjBlMWFiYzAzZjRjN2NiYjIxZDRmYzRiODM5ZmJmMGVjNzNlNDI2M2Y2YmUiLCJwYXlsb2FkUmVmIjoiUW1mNDEyalFaaXVWVXRkZ25CMzZGWEZYN3hnNVY2S0ViU0o0ZHBRdWhrTHlmRCIsImNvbnRleHRzIjpbIjB4NjhlNGRhNzlmODA1YmNhNWI5MTJiY2RhOWM2M2QwM2U2ZTg2NzEwOGRhYmI5Yjk0NDEwOWFlYTU0MWVmNTIyYSIsIjB4MTliODIwOTNkZTVjZTkyYTAxZTMzMzA0OGU4NzdlMjM3NDM1NGJmODQ2ZGQwMzQ4NjRlZjZmZmJkNjQzODc3MSJdfQ==", - "subId": "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e" - } -]`) - - em := &blockchainmocks.Callbacks{} - e := &Fabric{ - callbacks: em, - finalEvents: map[string]string{ - "firefly": "000000000090/000000/000000", - }, - } - e.initInfo.sub = &subscription{ - ID: "sb-0910f6a8-7bd6-4ced-453e-2db68149ce8e", - } - - var events []interface{} - err := json.Unmarshal(data, &events) - assert.NoError(t, err) - err = e.handleMessageBatch(context.Background(), events) - assert.NoError(t, err) - - em.AssertExpectations(t) - -} - func TestHandleMessageEmptyPayloadRef(t *testing.T) { data := []byte(` [ diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go index 3246db1ec..68b82fbe1 100644 --- a/internal/coremsgs/en_error_messages.go +++ b/internal/coremsgs/en_error_messages.go @@ -235,6 +235,5 @@ var ( MsgInvalidPluginConfiguration = ffe("FF10386", "Invalid %s plugin configuration - name and type are required") MsgReferenceMarkdownMissing = ffe("FF10387", "Reference markdown file missing: '%s'") MsgInvalidFireFlyContractIndex = ffe("FF10388", "No configuration found for FireFly contract at %s") - MsgCannotReuseFireFlyContract = ffe("FF10389", "Cannot reuse previously-terminated FireFly contract at %s") - MsgUnrecognizedOperatorAction = ffe("FF10390", "Unrecognized operator action: %s", 400) + MsgUnrecognizedOperatorAction = ffe("FF10389", "Unrecognized operator action: %s", 400) ) diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 3ae6390af..ad4ee94df 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -1157,5 +1157,5 @@ func TestOperatorActionBadType(t *testing.T) { verifier := &core.VerifierRef{Value: "0x123"} or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(verifier, nil) err := or.SubmitOperatorAction(context.Background(), &core.OperatorAction{Type: "bad"}) - assert.Regexp(t, "FF10390", err) + assert.Regexp(t, "FF10389", err) } From 7b540f6c3faa559133d12ff15aa0007560b92813 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Tue, 24 May 2022 19:59:39 -0400 Subject: [PATCH 09/13] Add mutex and some extra comments Signed-off-by: Andrew Richardson --- internal/blockchain/ethereum/ethereum.go | 27 +++++++++++++++++++----- internal/blockchain/fabric/fabric.go | 27 +++++++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index e39b29607..8a7219c94 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -23,6 +23,7 @@ import ( "fmt" "regexp" "strings" + "sync" "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-common/pkg/config" @@ -52,6 +53,7 @@ type Ethereum struct { topic string fireflyContract string fireflyFromBlock string + fireflyMux sync.Mutex prefixShort string prefixLong string capabilities *blockchain.Capabilities @@ -273,10 +275,12 @@ func (e *Ethereum) ConfigureContract(ctx context.Context, contracts *core.FireFl return err } - e.fireflyContract = address - e.fireflyFromBlock = fromBlock - e.initInfo.sub, err = e.streams.ensureFireFlySubscription(ctx, e.fireflyContract, e.fireflyFromBlock, e.initInfo.stream.ID, batchPinEventABI) + e.initInfo.sub, err = e.streams.ensureFireFlySubscription(ctx, address, fromBlock, e.initInfo.stream.ID, batchPinEventABI) if err == nil { + e.fireflyMux.Lock() + e.fireflyContract = address + e.fireflyFromBlock = fromBlock + e.fireflyMux.Unlock() contracts.Active.Info = fftypes.JSONObject{ "address": address, "fromBlock": fromBlock, @@ -292,10 +296,14 @@ func (e *Ethereum) TerminateContract(ctx context.Context, contracts *core.FireFl if err != nil { return err } + e.fireflyMux.Lock() if address != e.fireflyContract { log.L(ctx).Warnf("Ignoring termination request from address %s, which differs from active address %s", address, e.fireflyContract) + e.fireflyMux.Unlock() return nil } + e.fireflyMux.Unlock() + log.L(ctx).Infof("Processing termination request from address %s", address) contracts.Active.FinalEvent = termination.ProtocolID contracts.Terminated = append(contracts.Terminated, contracts.Active) @@ -500,6 +508,7 @@ func (e *Ethereum) handleMessageBatch(ctx context.Context, messages []interface{ l1.Tracef("Message: %+v", msgJSON) if sub == e.initInfo.sub.ID { + // Matches the active FireFly BatchPin subscription switch signature { case broadcastBatchEventSignature: if err := e.handleBatchPinEvent(ctx1, msgJSON); err != nil { @@ -508,8 +517,12 @@ func (e *Ethereum) handleMessageBatch(ctx context.Context, messages []interface{ default: l.Infof("Ignoring event with unknown signature: %s", signature) } - } else if err := e.handleContractEvent(ctx1, msgJSON); err != nil { - return err + } else { + // Subscription not recognized - assume it's from a custom contract listener + // (event manager will reject it if it's not) + if err := e.handleContractEvent(ctx1, msgJSON); err != nil { + return err + } } } @@ -652,6 +665,8 @@ func (e *Ethereum) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID batch.BatchPayloadRef, ethHashes, } + e.fireflyMux.Lock() + defer e.fireflyMux.Unlock() return e.invokeContractMethod(ctx, e.fireflyContract, signingKey, batchPinMethodABI, operationID.String(), input) } @@ -663,6 +678,8 @@ func (e *Ethereum) SubmitOperatorAction(ctx context.Context, operationID *fftype "", []string{}, } + e.fireflyMux.Lock() + defer e.fireflyMux.Unlock() return e.invokeContractMethod(ctx, e.fireflyContract, signingKey, batchPinMethodABI, operationID.String(), input) } diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index eb1c1b167..537a7f421 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -24,6 +24,7 @@ import ( "fmt" "regexp" "strings" + "sync" "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-common/pkg/config" @@ -48,6 +49,7 @@ type Fabric struct { defaultChannel string fireflyChaincode string fireflyFromBlock string + fireflyMux sync.Mutex signer string prefixShort string prefixLong string @@ -246,11 +248,13 @@ func (f *Fabric) ConfigureContract(ctx context.Context, contracts *core.FireFlyC return err } - f.fireflyChaincode = chaincode - f.fireflyFromBlock = fromBlock location := &Location{Channel: f.defaultChannel, Chaincode: chaincode} - f.initInfo.sub, err = f.streams.ensureFireFlySubscription(ctx, location, f.fireflyFromBlock, f.initInfo.stream.ID, batchPinEvent) + f.initInfo.sub, err = f.streams.ensureFireFlySubscription(ctx, location, fromBlock, f.initInfo.stream.ID, batchPinEvent) if err == nil { + f.fireflyMux.Lock() + f.fireflyChaincode = chaincode + f.fireflyFromBlock = fromBlock + f.fireflyMux.Unlock() contracts.Active.Info = fftypes.JSONObject{ "chaincode": chaincode, "fromBlock": fromBlock, @@ -263,10 +267,14 @@ func (f *Fabric) ConfigureContract(ctx context.Context, contracts *core.FireFlyC func (f *Fabric) TerminateContract(ctx context.Context, contracts *core.FireFlyContracts, termination *blockchain.Event) (err error) { chaincode := termination.Info.GetString("chaincodeId") + f.fireflyMux.Lock() if chaincode != f.fireflyChaincode { log.L(ctx).Warnf("Ignoring termination request from chaincode %s, which differs from active chaincode %s", chaincode, f.fireflyChaincode) + f.fireflyMux.Unlock() return nil } + f.fireflyMux.Unlock() + log.L(ctx).Infof("Processing termination request from chaincode %s", chaincode) contracts.Active.FinalEvent = termination.ProtocolID contracts.Terminated = append(contracts.Terminated, contracts.Active) @@ -467,6 +475,7 @@ func (f *Fabric) handleMessageBatch(ctx context.Context, messages []interface{}) l1.Tracef("Message: %+v", msgJSON) if sub == f.initInfo.sub.ID { + // Matches the active FireFly BatchPin subscription switch eventName { case broadcastBatchEventName: if err := f.handleBatchPinEvent(ctx1, msgJSON); err != nil { @@ -475,8 +484,12 @@ func (f *Fabric) handleMessageBatch(ctx context.Context, messages []interface{}) default: l.Infof("Ignoring event with unknown name: %s", eventName) } - } else if err := f.handleContractEvent(ctx, msgJSON); err != nil { - return err + } else { + // Subscription not recognized - assume it's from a custom contract listener + // (event manager will reject it if it's not) + if err := f.handleContractEvent(ctx, msgJSON); err != nil { + return err + } } } @@ -628,6 +641,8 @@ func (f *Fabric) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, "contexts": hashes, } input, _ := jsonEncodeInput(pinInput) + f.fireflyMux.Lock() + defer f.fireflyMux.Unlock() return f.invokeContractMethod(ctx, f.defaultChannel, f.fireflyChaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) } @@ -640,6 +655,8 @@ func (f *Fabric) SubmitOperatorAction(ctx context.Context, operationID *fftypes. "contexts": []string{}, } input, _ := jsonEncodeInput(pinInput) + f.fireflyMux.Lock() + defer f.fireflyMux.Unlock() return f.invokeContractMethod(ctx, f.defaultChannel, f.fireflyChaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) } From ebd93718f993e8afe848a61f24cbdf9ca5c287c8 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Thu, 26 May 2022 13:57:49 -0400 Subject: [PATCH 10/13] Use the simpler "network action" over "network operator action" Signed-off-by: Andrew Richardson --- docs/swagger/swagger.yaml | 80 +++++++++---------- ...action.go => route_post_network_action.go} | 14 ++-- ...t.go => route_post_network_action_test.go} | 8 +- internal/apiserver/routes.go | 2 +- internal/blockchain/ethereum/ethereum.go | 4 +- internal/blockchain/ethereum/ethereum_test.go | 8 +- internal/blockchain/fabric/fabric.go | 4 +- internal/blockchain/fabric/fabric_test.go | 8 +- internal/coremsgs/en_api_translations.go | 2 +- internal/coremsgs/en_error_messages.go | 2 +- internal/coremsgs/en_struct_descriptions.go | 2 +- internal/events/event_manager.go | 2 +- .../{operator_action.go => network_action.go} | 8 +- ..._action_test.go => network_action_test.go} | 8 +- internal/orchestrator/bound_callbacks.go | 4 +- internal/orchestrator/bound_callbacks_test.go | 4 +- internal/orchestrator/orchestrator.go | 10 +-- internal/orchestrator/orchestrator_test.go | 14 ++-- mocks/blockchainmocks/callbacks.go | 14 ++-- mocks/blockchainmocks/plugin.go | 4 +- mocks/eventmocks/event_manager.go | 4 +- mocks/orchestratormocks/orchestrator.go | 6 +- pkg/blockchain/plugin.go | 8 +- pkg/core/namespace.go | 12 +-- 24 files changed, 116 insertions(+), 116 deletions(-) rename internal/apiserver/{route_post_network_operator_action.go => route_post_network_action.go} (71%) rename internal/apiserver/{route_post_network_operator_action_test.go => route_post_network_action_test.go} (80%) rename internal/events/{operator_action.go => network_action.go} (80%) rename internal/events/{operator_action_test.go => network_action_test.go} (93%) diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 36ecbfdcf..b07645700 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -22786,6 +22786,46 @@ paths: description: "" tags: - Non-Default Namespace + /network/action: + post: + description: Notify all nodes in the network of a new governance action + operationId: postNetworkAction + parameters: + - description: Server-side request timeout (millseconds, or set a custom suffix + like 10s) + in: header + name: Request-Timeout + schema: + default: 120s + type: string + requestBody: + content: + application/json: + schema: + properties: + type: + description: The action to be performed + enum: + - terminate + type: string + type: object + responses: + "202": + content: + application/json: + schema: + properties: + type: + description: The action to be performed + enum: + - terminate + type: string + type: object + description: Success + default: + description: "" + tags: + - Global /network/diddocs/{did}: get: description: Gets a DID document by its DID @@ -23671,46 +23711,6 @@ paths: description: "" tags: - Global - /network/operatoraction: - post: - description: Notify all nodes in the network of a new governance action - operationId: postNetworkOperatorAction - parameters: - - description: Server-side request timeout (millseconds, or set a custom suffix - like 10s) - in: header - name: Request-Timeout - schema: - default: 120s - type: string - requestBody: - content: - application/json: - schema: - properties: - type: - description: The action to be performed - enum: - - terminate - type: string - type: object - responses: - "202": - content: - application/json: - schema: - properties: - type: - description: The action to be performed - enum: - - terminate - type: string - type: object - description: Success - default: - description: "" - tags: - - Global /network/organizations: get: description: Gets a list of orgs in the network diff --git a/internal/apiserver/route_post_network_operator_action.go b/internal/apiserver/route_post_network_action.go similarity index 71% rename from internal/apiserver/route_post_network_operator_action.go rename to internal/apiserver/route_post_network_action.go index a7c8f1fb9..02b22e320 100644 --- a/internal/apiserver/route_post_network_operator_action.go +++ b/internal/apiserver/route_post_network_action.go @@ -24,19 +24,19 @@ import ( "github.com/hyperledger/firefly/pkg/core" ) -var postNetworkOperatorAction = &oapispec.Route{ - Name: "postNetworkOperatorAction", - Path: "network/operatoraction", +var postNetworkAction = &oapispec.Route{ + Name: "postNetworkAction", + Path: "network/action", Method: http.MethodPost, PathParams: nil, QueryParams: nil, FilterFactory: nil, - Description: coremsgs.APIEndpointsPostNetworkOperatorAction, - JSONInputValue: func() interface{} { return &core.OperatorAction{} }, - JSONOutputValue: func() interface{} { return &core.OperatorAction{} }, + Description: coremsgs.APIEndpointsPostNetworkAction, + JSONInputValue: func() interface{} { return &core.NetworkAction{} }, + JSONOutputValue: func() interface{} { return &core.NetworkAction{} }, JSONOutputCodes: []int{http.StatusAccepted}, JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { - err = getOr(r.Ctx).SubmitOperatorAction(r.Ctx, r.Input.(*core.OperatorAction)) + err = getOr(r.Ctx).SubmitNetworkAction(r.Ctx, r.Input.(*core.NetworkAction)) return r.Input, err }, } diff --git a/internal/apiserver/route_post_network_operator_action_test.go b/internal/apiserver/route_post_network_action_test.go similarity index 80% rename from internal/apiserver/route_post_network_operator_action_test.go rename to internal/apiserver/route_post_network_action_test.go index 3182c86e9..fe401d258 100644 --- a/internal/apiserver/route_post_network_operator_action_test.go +++ b/internal/apiserver/route_post_network_action_test.go @@ -27,16 +27,16 @@ import ( "github.com/stretchr/testify/mock" ) -func TestPostNetworkOperatorAction(t *testing.T) { +func TestPostNetworkAction(t *testing.T) { o, r := newTestAPIServer() - input := core.OperatorAction{} + input := core.NetworkAction{} var buf bytes.Buffer json.NewEncoder(&buf).Encode(&input) - req := httptest.NewRequest("POST", "/api/v1/network/operatoraction", &buf) + req := httptest.NewRequest("POST", "/api/v1/network/action", &buf) req.Header.Set("Content-Type", "application/json; charset=utf-8") res := httptest.NewRecorder() - o.On("SubmitOperatorAction", mock.Anything, mock.AnythingOfType("*core.OperatorAction")).Return(nil) + o.On("SubmitNetworkAction", mock.Anything, mock.AnythingOfType("*core.NetworkAction")).Return(nil) r.ServeHTTP(res, req) assert.Equal(t, 202, res.Result().StatusCode) diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go index dc2e318de..cc7e18eff 100644 --- a/internal/apiserver/routes.go +++ b/internal/apiserver/routes.go @@ -38,7 +38,7 @@ var routes = append( getStatusBatchManager, getStatusPins, getStatusWebSockets, - postNetworkOperatorAction, + postNetworkAction, postNewNamespace, postNewOrganization, postNewOrganizationSelf, diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 8a7219c94..8995b1954 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -404,7 +404,7 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSON // Check if this is actually an operator action if strings.HasPrefix(nsOrAction, blockchain.FireFlyActionPrefix) { action := nsOrAction[len(blockchain.FireFlyActionPrefix):] - return e.callbacks.BlockchainOperatorAction(action, event, verifier) + return e.callbacks.BlockchainNetworkAction(action, event, verifier) } hexUUIDs, err := hex.DecodeString(strings.TrimPrefix(sUUIDs, "0x")) @@ -670,7 +670,7 @@ func (e *Ethereum) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID return e.invokeContractMethod(ctx, e.fireflyContract, signingKey, batchPinMethodABI, operationID.String(), input) } -func (e *Ethereum) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.OperatorActionType) error { +func (e *Ethereum) SubmitNetworkAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.NetworkActionType) error { input := []interface{}{ blockchain.FireFlyActionPrefix + action, ethHexFormatB32(nil), diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index 7215e537f..42fa5ad3b 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -2750,7 +2750,7 @@ func TestGenerateEventSignatureInvalid(t *testing.T) { assert.Equal(t, "", signature) } -func TestSubmitOperatorAction(t *testing.T) { +func TestSubmitNetworkAction(t *testing.T) { e, _ := newTestEthereum() httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() @@ -2767,11 +2767,11 @@ func TestSubmitOperatorAction(t *testing.T) { return httpmock.NewJsonResponderOrPanic(200, "")(req) }) - err := e.SubmitOperatorAction(context.Background(), fftypes.NewUUID(), "0x123", core.OperatorActionTerminate) + err := e.SubmitNetworkAction(context.Background(), fftypes.NewUUID(), "0x123", core.NetworkActionTerminate) assert.NoError(t, err) } -func TestHandleOperatorAction(t *testing.T) { +func TestHandleNetworkAction(t *testing.T) { data := fftypes.JSONAnyPtr(` [ { @@ -2807,7 +2807,7 @@ func TestHandleOperatorAction(t *testing.T) { Value: "0x91d2b4381a4cd5c7c0f27565a7d4b829844c8635", } - em.On("BlockchainOperatorAction", "terminate", mock.Anything, expectedSigningKeyRef).Return(nil) + em.On("BlockchainNetworkAction", "terminate", mock.Anything, expectedSigningKeyRef).Return(nil) var events []interface{} err := json.Unmarshal(data.Bytes(), &events) diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index 537a7f421..2f5a67a91 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -371,7 +371,7 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, msgJSON fftypes.JSONOb // Check if this is actually an operator action if strings.HasPrefix(nsOrAction, blockchain.FireFlyActionPrefix) { action := nsOrAction[len(blockchain.FireFlyActionPrefix):] - return f.callbacks.BlockchainOperatorAction(action, event, verifier) + return f.callbacks.BlockchainNetworkAction(action, event, verifier) } hexUUIDs, err := hex.DecodeString(strings.TrimPrefix(sUUIDs, "0x")) @@ -646,7 +646,7 @@ func (f *Fabric) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, return f.invokeContractMethod(ctx, f.defaultChannel, f.fireflyChaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) } -func (f *Fabric) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.OperatorActionType) error { +func (f *Fabric) SubmitNetworkAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.NetworkActionType) error { pinInput := map[string]interface{}{ "namespace": "firefly:" + action, "uuids": hexFormatB32(nil), diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index f5b99860e..beeff3f09 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -1877,7 +1877,7 @@ func TestGenerateEventSignature(t *testing.T) { assert.Equal(t, "Changed", signature) } -func TestSubmitOperatorAction(t *testing.T) { +func TestSubmitNetworkAction(t *testing.T) { e, cancel := newTestFabric() defer cancel() @@ -1897,12 +1897,12 @@ func TestSubmitOperatorAction(t *testing.T) { return httpmock.NewJsonResponderOrPanic(200, "")(req) }) - err := e.SubmitOperatorAction(context.Background(), nil, signer, core.OperatorActionTerminate) + err := e.SubmitNetworkAction(context.Background(), nil, signer, core.NetworkActionTerminate) assert.NoError(t, err) } -func TestHandleOperatorAction(t *testing.T) { +func TestHandleNetworkAction(t *testing.T) { data := []byte(` [ { @@ -1930,7 +1930,7 @@ func TestHandleOperatorAction(t *testing.T) { Value: "u0vgwu9s00-x509::CN=user2,OU=client::CN=fabric-ca-server", } - em.On("BlockchainOperatorAction", "terminate", mock.Anything, expectedSigningKeyRef).Return(nil) + em.On("BlockchainNetworkAction", "terminate", mock.Anything, expectedSigningKeyRef).Return(nil) var events []interface{} err := json.Unmarshal(data, &events) diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go index 23ee62c32..ff53723e3 100644 --- a/internal/coremsgs/en_api_translations.go +++ b/internal/coremsgs/en_api_translations.go @@ -168,7 +168,7 @@ var ( APIEndpointsPutContractAPI = ffm("api.endpoints.putContractAPI", "Updates an existing contract API") APIEndpointsPutSubscription = ffm("api.endpoints.putSubscription", "Update an existing subscription") APIEndpointsGetContractAPIInterface = ffm("api.endpoints.getContractAPIInterface", "Gets a contract interface for a contract API") - APIEndpointsPostNetworkOperatorAction = ffm("api.endpoints.postNetworkOperatorAction", "Notify all nodes in the network of a new governance action") + APIEndpointsPostNetworkAction = ffm("api.endpoints.postNetworkAction", "Notify all nodes in the network of a new governance action") APISuccessResponse = ffm("api.success", "Success") APIRequestTimeoutDesc = ffm("api.requestTimeout", "Server-side request timeout (millseconds, or set a custom suffix like 10s)") diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go index 68b82fbe1..3ff1d5865 100644 --- a/internal/coremsgs/en_error_messages.go +++ b/internal/coremsgs/en_error_messages.go @@ -235,5 +235,5 @@ var ( MsgInvalidPluginConfiguration = ffe("FF10386", "Invalid %s plugin configuration - name and type are required") MsgReferenceMarkdownMissing = ffe("FF10387", "Reference markdown file missing: '%s'") MsgInvalidFireFlyContractIndex = ffe("FF10388", "No configuration found for FireFly contract at %s") - MsgUnrecognizedOperatorAction = ffe("FF10389", "Unrecognized operator action: %s", 400) + MsgUnrecognizedNetworkAction = ffe("FF10389", "Unrecognized network action: %s", 400) ) diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go index 2b9dec900..0b57a2649 100644 --- a/internal/coremsgs/en_struct_descriptions.go +++ b/internal/coremsgs/en_struct_descriptions.go @@ -383,7 +383,7 @@ var ( FireFlyContractIndex = ffm("FireFlyContractInfo.index", "The index of this contract in the config file") FireFlyContractFinalEvent = ffm("FireFlyContractInfo.finalEvent", "The identifier for the final blockchain event received from this contract before termination") FireFlyContractInfo = ffm("FireFlyContractInfo.info", "Blockchain-specific info on the contract, such as its location on chain") - OperatorActionType = ffm("OperatorAction.type", "The action to be performed") + NetworkActionType = ffm("NetworkAction.type", "The action to be performed") // NodeStatus field descriptions NodeStatusNode = ffm("NodeStatus.node", "Details of the local node") diff --git a/internal/events/event_manager.go b/internal/events/event_manager.go index 2e524dda8..300dec4b0 100644 --- a/internal/events/event_manager.go +++ b/internal/events/event_manager.go @@ -66,7 +66,7 @@ type EventManager interface { // Bound blockchain callbacks BatchPinComplete(bi blockchain.Plugin, batch *blockchain.BatchPin, signingKey *core.VerifierRef) error BlockchainEvent(event *blockchain.EventWithSubscription) error - BlockchainOperatorAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error + BlockchainNetworkAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error // Bound dataexchange callbacks DXEvent(dx dataexchange.Plugin, event dataexchange.DXEvent) diff --git a/internal/events/operator_action.go b/internal/events/network_action.go similarity index 80% rename from internal/events/operator_action.go rename to internal/events/network_action.go index a94fa8c69..b6582b4ec 100644 --- a/internal/events/operator_action.go +++ b/internal/events/network_action.go @@ -37,14 +37,14 @@ func (em *eventManager) actionTerminate(bi blockchain.Plugin, event *blockchain. }) } -func (em *eventManager) BlockchainOperatorAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error { - return em.retry.Do(em.ctx, "handle operator action", func(attempt int) (retry bool, err error) { +func (em *eventManager) BlockchainNetworkAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error { + return em.retry.Do(em.ctx, "handle network action", func(attempt int) (retry bool, err error) { // TODO: verify signing identity - if action == core.OperatorActionTerminate.String() { + if action == core.NetworkActionTerminate.String() { err = em.actionTerminate(bi, event) } else { - log.L(em.ctx).Errorf("Ignoring unrecognized operator action: %s", action) + log.L(em.ctx).Errorf("Ignoring unrecognized network action: %s", action) return false, nil } diff --git a/internal/events/operator_action_test.go b/internal/events/network_action_test.go similarity index 93% rename from internal/events/operator_action_test.go rename to internal/events/network_action_test.go index 54c07070d..29a2a3e42 100644 --- a/internal/events/operator_action_test.go +++ b/internal/events/network_action_test.go @@ -30,7 +30,7 @@ import ( "github.com/stretchr/testify/mock" ) -func TestOperatorAction(t *testing.T) { +func TestNetworkAction(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() @@ -49,7 +49,7 @@ func TestOperatorAction(t *testing.T) { mdi.On("UpsertNamespace", em.ctx, mock.AnythingOfType("*core.Namespace"), true).Return(nil) mbi.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil) - err := em.BlockchainOperatorAction(mbi, "terminate", event, &core.VerifierRef{}) + err := em.BlockchainNetworkAction(mbi, "terminate", event, &core.VerifierRef{}) assert.NoError(t, err) mbi.AssertExpectations(t) @@ -57,13 +57,13 @@ func TestOperatorAction(t *testing.T) { mth.AssertExpectations(t) } -func TestOperatorActionUnknown(t *testing.T) { +func TestNetworkActionUnknown(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() mbi := &blockchainmocks.Plugin{} - err := em.BlockchainOperatorAction(mbi, "bad", &blockchain.Event{}, &core.VerifierRef{}) + err := em.BlockchainNetworkAction(mbi, "bad", &blockchain.Event{}, &core.VerifierRef{}) assert.NoError(t, err) mbi.AssertExpectations(t) diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go index 7ac96f039..4c6205120 100644 --- a/internal/orchestrator/bound_callbacks.go +++ b/internal/orchestrator/bound_callbacks.go @@ -59,8 +59,8 @@ func (bc *boundCallbacks) BatchPinComplete(batch *blockchain.BatchPin, signingKe return bc.ei.BatchPinComplete(bc.bi, batch, signingKey) } -func (bc *boundCallbacks) BlockchainOperatorAction(action string, event *blockchain.Event, signingKey *core.VerifierRef) error { - return bc.ei.BlockchainOperatorAction(bc.bi, action, event, signingKey) +func (bc *boundCallbacks) BlockchainNetworkAction(action string, event *blockchain.Event, signingKey *core.VerifierRef) error { + return bc.ei.BlockchainNetworkAction(bc.bi, action, event, signingKey) } func (bc *boundCallbacks) DXEvent(event dataexchange.DXEvent) { diff --git a/internal/orchestrator/bound_callbacks_test.go b/internal/orchestrator/bound_callbacks_test.go index 067b8c7a4..853f7650e 100644 --- a/internal/orchestrator/bound_callbacks_test.go +++ b/internal/orchestrator/bound_callbacks_test.go @@ -58,8 +58,8 @@ func TestBoundCallbacks(t *testing.T) { err := bc.BatchPinComplete(batch, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}) assert.EqualError(t, err, "pop") - mei.On("BlockchainOperatorAction", mbi, "migrate", event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop")) - err = bc.BlockchainOperatorAction("migrate", event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}) + mei.On("BlockchainNetworkAction", mbi, "terminate", event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop")) + err = bc.BlockchainNetworkAction("terminate", event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}) assert.EqualError(t, err, "pop") mom.On("SubmitOperationUpdate", mock.Anything, &operations.OperationUpdate{ diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index fb47cf759..bfa860952 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -142,7 +142,7 @@ type Orchestrator interface { RequestReply(ctx context.Context, ns string, msg *core.MessageInOut) (reply *core.MessageInOut, err error) // Network Operations - SubmitOperatorAction(ctx context.Context, action *core.OperatorAction) error + SubmitNetworkAction(ctx context.Context, action *core.NetworkAction) error } type orchestrator struct { @@ -880,13 +880,13 @@ func (or *orchestrator) initNamespaces(ctx context.Context) (err error) { return or.namespace.Init(ctx, or.database) } -func (or *orchestrator) SubmitOperatorAction(ctx context.Context, action *core.OperatorAction) error { +func (or *orchestrator) SubmitNetworkAction(ctx context.Context, action *core.NetworkAction) error { verifier, err := or.identity.GetNodeOwnerBlockchainKey(ctx) if err != nil { return err } - if action.Type != core.OperatorActionTerminate { - return i18n.NewError(ctx, coremsgs.MsgUnrecognizedOperatorAction, action.Type) + if action.Type != core.NetworkActionTerminate { + return i18n.NewError(ctx, coremsgs.MsgUnrecognizedNetworkAction, action.Type) } - return or.blockchain.SubmitOperatorAction(ctx, fftypes.NewUUID(), verifier.Value, action.Type) + return or.blockchain.SubmitNetworkAction(ctx, fftypes.NewUUID(), verifier.Value, action.Type) } diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index ad4ee94df..c66f43317 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -1135,27 +1135,27 @@ func TestInitDataExchangeWithNodes(t *testing.T) { assert.NoError(t, err) } -func TestOperatorAction(t *testing.T) { +func TestNetworkAction(t *testing.T) { or := newTestOrchestrator() or.blockchain = or.mbi verifier := &core.VerifierRef{Value: "0x123"} or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(verifier, nil) - or.mbi.On("SubmitOperatorAction", context.Background(), mock.Anything, "0x123", core.OperatorActionTerminate).Return(nil) - err := or.SubmitOperatorAction(context.Background(), &core.OperatorAction{Type: core.OperatorActionTerminate}) + or.mbi.On("SubmitNetworkAction", context.Background(), mock.Anything, "0x123", core.NetworkActionTerminate).Return(nil) + err := or.SubmitNetworkAction(context.Background(), &core.NetworkAction{Type: core.NetworkActionTerminate}) assert.NoError(t, err) } -func TestOperatorActionBadKey(t *testing.T) { +func TestNetworkActionBadKey(t *testing.T) { or := newTestOrchestrator() or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(nil, fmt.Errorf("pop")) - err := or.SubmitOperatorAction(context.Background(), &core.OperatorAction{Type: core.OperatorActionTerminate}) + err := or.SubmitNetworkAction(context.Background(), &core.NetworkAction{Type: core.NetworkActionTerminate}) assert.EqualError(t, err, "pop") } -func TestOperatorActionBadType(t *testing.T) { +func TestNetworkActionBadType(t *testing.T) { or := newTestOrchestrator() verifier := &core.VerifierRef{Value: "0x123"} or.mim.On("GetNodeOwnerBlockchainKey", context.Background()).Return(verifier, nil) - err := or.SubmitOperatorAction(context.Background(), &core.OperatorAction{Type: "bad"}) + err := or.SubmitNetworkAction(context.Background(), &core.NetworkAction{Type: "bad"}) assert.Regexp(t, "FF10389", err) } diff --git a/mocks/blockchainmocks/callbacks.go b/mocks/blockchainmocks/callbacks.go index f256640f5..f26f550bf 100644 --- a/mocks/blockchainmocks/callbacks.go +++ b/mocks/blockchainmocks/callbacks.go @@ -44,13 +44,8 @@ func (_m *Callbacks) BlockchainEvent(event *blockchain.EventWithSubscription) er return r0 } -// BlockchainOpUpdate provides a mock function with given fields: plugin, operationID, txState, blockchainTXID, errorMessage, opOutput -func (_m *Callbacks) BlockchainOpUpdate(plugin blockchain.Plugin, operationID *fftypes.UUID, txState core.OpStatus, blockchainTXID string, errorMessage string, opOutput fftypes.JSONObject) { - _m.Called(plugin, operationID, txState, blockchainTXID, errorMessage, opOutput) -} - -// BlockchainOperatorAction provides a mock function with given fields: action, event, signingKey -func (_m *Callbacks) BlockchainOperatorAction(action string, event *blockchain.Event, signingKey *core.VerifierRef) error { +// BlockchainNetworkAction provides a mock function with given fields: action, event, signingKey +func (_m *Callbacks) BlockchainNetworkAction(action string, event *blockchain.Event, signingKey *core.VerifierRef) error { ret := _m.Called(action, event, signingKey) var r0 error @@ -62,3 +57,8 @@ func (_m *Callbacks) BlockchainOperatorAction(action string, event *blockchain.E return r0 } + +// BlockchainOpUpdate provides a mock function with given fields: plugin, operationID, txState, blockchainTXID, errorMessage, opOutput +func (_m *Callbacks) BlockchainOpUpdate(plugin blockchain.Plugin, operationID *fftypes.UUID, txState core.OpStatus, blockchainTXID string, errorMessage string, opOutput fftypes.JSONObject) { + _m.Called(plugin, operationID, txState, blockchainTXID, errorMessage, opOutput) +} diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go index 40671ba35..9ac59f56d 100644 --- a/mocks/blockchainmocks/plugin.go +++ b/mocks/blockchainmocks/plugin.go @@ -282,8 +282,8 @@ func (_m *Plugin) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, return r0 } -// SubmitOperatorAction provides a mock function with given fields: ctx, operationID, signingKey, action -func (_m *Plugin) SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.FFEnum) error { +// SubmitNetworkAction provides a mock function with given fields: ctx, operationID, signingKey, action +func (_m *Plugin) SubmitNetworkAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.FFEnum) error { ret := _m.Called(ctx, operationID, signingKey, action) var r0 error diff --git a/mocks/eventmocks/event_manager.go b/mocks/eventmocks/event_manager.go index ad5e364de..82c759cbc 100644 --- a/mocks/eventmocks/event_manager.go +++ b/mocks/eventmocks/event_manager.go @@ -69,8 +69,8 @@ func (_m *EventManager) BlockchainEvent(event *blockchain.EventWithSubscription) return r0 } -// BlockchainOperatorAction provides a mock function with given fields: bi, action, event, signingKey -func (_m *EventManager) BlockchainOperatorAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error { +// BlockchainNetworkAction provides a mock function with given fields: bi, action, event, signingKey +func (_m *EventManager) BlockchainNetworkAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error { ret := _m.Called(bi, action, event, signingKey) var r0 error diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go index 4108ca14c..48be922cc 100644 --- a/mocks/orchestratormocks/orchestrator.go +++ b/mocks/orchestratormocks/orchestrator.go @@ -1347,12 +1347,12 @@ func (_m *Orchestrator) Start() error { return r0 } -// SubmitOperatorAction provides a mock function with given fields: ctx, action -func (_m *Orchestrator) SubmitOperatorAction(ctx context.Context, action *core.OperatorAction) error { +// SubmitNetworkAction provides a mock function with given fields: ctx, action +func (_m *Orchestrator) SubmitNetworkAction(ctx context.Context, action *core.NetworkAction) error { ret := _m.Called(ctx, action) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *core.OperatorAction) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *core.NetworkAction) error); ok { r0 = rf(ctx, action) } else { r0 = ret.Error(0) diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index 4b77293d3..9b6198dc4 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -63,8 +63,8 @@ type Plugin interface { // SubmitBatchPin sequences a batch of message globally to all viewers of a given ledger SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, signingKey string, batch *BatchPin) error - // SubmitOperatorAction writes a special "BatchPin" event which signals the plugin to take an action - SubmitOperatorAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.OperatorActionType) error + // SubmitNetworkAction writes a special "BatchPin" event which signals the plugin to take an action + SubmitNetworkAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.NetworkActionType) error // InvokeContract submits a new transaction to be executed by custom on-chain logic InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error @@ -112,10 +112,10 @@ type Callbacks interface { // Error should only be returned in shutdown scenarios BatchPinComplete(batch *BatchPin, signingKey *core.VerifierRef) error - // BlockchainOperatorAction notifies on the arrival of a network operator action + // BlockchainNetworkAction notifies on the arrival of a network operator action // // Error should only be returned in shutdown scenarios - BlockchainOperatorAction(action string, event *Event, signingKey *core.VerifierRef) error + BlockchainNetworkAction(action string, event *Event, signingKey *core.VerifierRef) error // BlockchainEvent notifies on the arrival of any event from a user-created subscription. BlockchainEvent(event *EventWithSubscription) error diff --git a/pkg/core/namespace.go b/pkg/core/namespace.go index 2a0d2fd35..da1cecb6d 100644 --- a/pkg/core/namespace.go +++ b/pkg/core/namespace.go @@ -61,16 +61,16 @@ type FireFlyContractInfo struct { Info fftypes.JSONObject `ffstruct:"FireFlyContractInfo" json:"info,omitempty"` } -// OperatorActionType is a type of action to perform -type OperatorActionType = FFEnum +// NetworkActionType is a type of action to perform +type NetworkActionType = FFEnum var ( - // OperatorActionTerminate request all network members to stop using the current contract and move to the next one configured - OperatorActionTerminate = ffEnum("operatoractiontype", "terminate") + // NetworkActionTerminate request all network members to stop using the current contract and move to the next one configured + NetworkActionTerminate = ffEnum("networkactiontype", "terminate") ) -type OperatorAction struct { - Type OperatorActionType `ffstruct:"OperatorAction" json:"type" ffenum:"operatoractiontype"` +type NetworkAction struct { + Type NetworkActionType `ffstruct:"NetworkAction" json:"type" ffenum:"networkactiontype"` } func (ns *Namespace) Validate(ctx context.Context, existing bool) (err error) { From c715fab576f3c67e6c74fa3f4b1eaf040638b9f2 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Thu, 26 May 2022 13:59:22 -0400 Subject: [PATCH 11/13] Rename database migration files Signed-off-by: Andrew Richardson --- ...ex.down.sql => 000090_add_namespace_fireflycontracts.down.sql} | 0 ...tindex.up.sql => 000090_add_namespace_fireflycontracts.up.sql} | 0 ...ex.down.sql => 000090_add_namespace_fireflycontracts.down.sql} | 0 ...tindex.up.sql => 000090_add_namespace_fireflycontracts.up.sql} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename db/migrations/postgres/{000090_add_namespace_contractindex.down.sql => 000090_add_namespace_fireflycontracts.down.sql} (100%) rename db/migrations/postgres/{000090_add_namespace_contractindex.up.sql => 000090_add_namespace_fireflycontracts.up.sql} (100%) rename db/migrations/sqlite/{000090_add_namespace_contractindex.down.sql => 000090_add_namespace_fireflycontracts.down.sql} (100%) rename db/migrations/sqlite/{000090_add_namespace_contractindex.up.sql => 000090_add_namespace_fireflycontracts.up.sql} (100%) diff --git a/db/migrations/postgres/000090_add_namespace_contractindex.down.sql b/db/migrations/postgres/000090_add_namespace_fireflycontracts.down.sql similarity index 100% rename from db/migrations/postgres/000090_add_namespace_contractindex.down.sql rename to db/migrations/postgres/000090_add_namespace_fireflycontracts.down.sql diff --git a/db/migrations/postgres/000090_add_namespace_contractindex.up.sql b/db/migrations/postgres/000090_add_namespace_fireflycontracts.up.sql similarity index 100% rename from db/migrations/postgres/000090_add_namespace_contractindex.up.sql rename to db/migrations/postgres/000090_add_namespace_fireflycontracts.up.sql diff --git a/db/migrations/sqlite/000090_add_namespace_contractindex.down.sql b/db/migrations/sqlite/000090_add_namespace_fireflycontracts.down.sql similarity index 100% rename from db/migrations/sqlite/000090_add_namespace_contractindex.down.sql rename to db/migrations/sqlite/000090_add_namespace_fireflycontracts.down.sql diff --git a/db/migrations/sqlite/000090_add_namespace_contractindex.up.sql b/db/migrations/sqlite/000090_add_namespace_fireflycontracts.up.sql similarity index 100% rename from db/migrations/sqlite/000090_add_namespace_contractindex.up.sql rename to db/migrations/sqlite/000090_add_namespace_fireflycontracts.up.sql From 620881c408c6f6b821a26a273a3bd0ec71634d97 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Thu, 26 May 2022 14:09:12 -0400 Subject: [PATCH 12/13] Reduce the scope of mutex in ethereum/fabric contract calls Signed-off-by: Andrew Richardson --- internal/blockchain/ethereum/ethereum.go | 10 ++++++---- internal/blockchain/fabric/fabric.go | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 8995b1954..aed2f12dc 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -666,8 +666,9 @@ func (e *Ethereum) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID ethHashes, } e.fireflyMux.Lock() - defer e.fireflyMux.Unlock() - return e.invokeContractMethod(ctx, e.fireflyContract, signingKey, batchPinMethodABI, operationID.String(), input) + address := e.fireflyContract + e.fireflyMux.Unlock() + return e.invokeContractMethod(ctx, address, signingKey, batchPinMethodABI, operationID.String(), input) } func (e *Ethereum) SubmitNetworkAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.NetworkActionType) error { @@ -679,8 +680,9 @@ func (e *Ethereum) SubmitNetworkAction(ctx context.Context, operationID *fftypes []string{}, } e.fireflyMux.Lock() - defer e.fireflyMux.Unlock() - return e.invokeContractMethod(ctx, e.fireflyContract, signingKey, batchPinMethodABI, operationID.String(), input) + address := e.fireflyContract + e.fireflyMux.Unlock() + return e.invokeContractMethod(ctx, address, signingKey, batchPinMethodABI, operationID.String(), input) } func (e *Ethereum) InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error { diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index 2f5a67a91..2932837ab 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -642,8 +642,9 @@ func (f *Fabric) SubmitBatchPin(ctx context.Context, operationID *fftypes.UUID, } input, _ := jsonEncodeInput(pinInput) f.fireflyMux.Lock() - defer f.fireflyMux.Unlock() - return f.invokeContractMethod(ctx, f.defaultChannel, f.fireflyChaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) + chaincode := f.fireflyChaincode + f.fireflyMux.Unlock() + return f.invokeContractMethod(ctx, f.defaultChannel, chaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) } func (f *Fabric) SubmitNetworkAction(ctx context.Context, operationID *fftypes.UUID, signingKey string, action core.NetworkActionType) error { @@ -656,8 +657,9 @@ func (f *Fabric) SubmitNetworkAction(ctx context.Context, operationID *fftypes.U } input, _ := jsonEncodeInput(pinInput) f.fireflyMux.Lock() - defer f.fireflyMux.Unlock() - return f.invokeContractMethod(ctx, f.defaultChannel, f.fireflyChaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) + chaincode := f.fireflyChaincode + f.fireflyMux.Unlock() + return f.invokeContractMethod(ctx, f.defaultChannel, chaincode, batchPinMethodName, signingKey, operationID.String(), batchPinPrefixItems, input) } func (f *Fabric) InvokeContract(ctx context.Context, operationID *fftypes.UUID, signingKey string, location *fftypes.JSONAny, method *core.FFIMethod, input map[string]interface{}) error { From 904cacc1fe987c5db25a2871da8dd65cd4bd3f7d Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Thu, 26 May 2022 14:27:41 -0400 Subject: [PATCH 13/13] Validate that all network actions come from a registered root org Signed-off-by: Andrew Richardson --- internal/events/network_action.go | 14 +++++- internal/events/network_action_test.go | 68 +++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/internal/events/network_action.go b/internal/events/network_action.go index b6582b4ec..205465f77 100644 --- a/internal/events/network_action.go +++ b/internal/events/network_action.go @@ -39,7 +39,19 @@ func (em *eventManager) actionTerminate(bi blockchain.Plugin, event *blockchain. func (em *eventManager) BlockchainNetworkAction(bi blockchain.Plugin, action string, event *blockchain.Event, signingKey *core.VerifierRef) error { return em.retry.Do(em.ctx, "handle network action", func(attempt int) (retry bool, err error) { - // TODO: verify signing identity + // Verify that the action came from a registered root org + resolvedAuthor, err := em.identity.FindIdentityForVerifier(em.ctx, []core.IdentityType{core.IdentityTypeOrg}, core.SystemNamespace, signingKey) + if err != nil { + return true, err + } + if resolvedAuthor == nil { + log.L(em.ctx).Errorf("Ignoring network action %s from unknown identity %s", action, signingKey.Value) + return false, nil + } + if resolvedAuthor.Parent != nil { + log.L(em.ctx).Errorf("Ignoring network action %s from non-root identity %s", action, signingKey.Value) + return false, nil + } if action == core.NetworkActionTerminate.String() { err = em.actionTerminate(bi, event) diff --git a/internal/events/network_action_test.go b/internal/events/network_action_test.go index 29a2a3e42..b27bd673d 100644 --- a/internal/events/network_action_test.go +++ b/internal/events/network_action_test.go @@ -23,6 +23,7 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly/mocks/blockchainmocks" "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/identitymanagermocks" "github.com/hyperledger/firefly/mocks/txcommonmocks" "github.com/hyperledger/firefly/pkg/blockchain" "github.com/hyperledger/firefly/pkg/core" @@ -35,11 +36,17 @@ func TestNetworkAction(t *testing.T) { defer cancel() event := &blockchain.Event{ProtocolID: "0001"} + verifier := &core.VerifierRef{ + Type: core.VerifierTypeEthAddress, + Value: "0x1234", + } mbi := &blockchainmocks.Plugin{} mdi := em.database.(*databasemocks.Plugin) mth := em.txHelper.(*txcommonmocks.Helper) + mii := em.identity.(*identitymanagermocks.Manager) + mii.On("FindIdentityForVerifier", em.ctx, []core.IdentityType{core.IdentityTypeOrg}, "ff_system", verifier).Return(&core.Identity{}, nil) mdi.On("GetBlockchainEventByProtocolID", em.ctx, "ff_system", (*fftypes.UUID)(nil), "0001").Return(nil, nil) mth.On("InsertBlockchainEvent", em.ctx, mock.MatchedBy(func(be *core.BlockchainEvent) bool { return be.ProtocolID == "0001" @@ -49,24 +56,81 @@ func TestNetworkAction(t *testing.T) { mdi.On("UpsertNamespace", em.ctx, mock.AnythingOfType("*core.Namespace"), true).Return(nil) mbi.On("TerminateContract", em.ctx, mock.AnythingOfType("*core.FireFlyContracts"), mock.AnythingOfType("*blockchain.Event")).Return(nil) - err := em.BlockchainNetworkAction(mbi, "terminate", event, &core.VerifierRef{}) + err := em.BlockchainNetworkAction(mbi, "terminate", event, verifier) assert.NoError(t, err) mbi.AssertExpectations(t) mdi.AssertExpectations(t) mth.AssertExpectations(t) + mii.AssertExpectations(t) +} + +func TestNetworkActionUnknownIdentity(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + verifier := &core.VerifierRef{ + Type: core.VerifierTypeEthAddress, + Value: "0x1234", + } + + mbi := &blockchainmocks.Plugin{} + mii := em.identity.(*identitymanagermocks.Manager) + + mii.On("FindIdentityForVerifier", em.ctx, []core.IdentityType{core.IdentityTypeOrg}, "ff_system", verifier).Return(nil, fmt.Errorf("pop")).Once() + mii.On("FindIdentityForVerifier", em.ctx, []core.IdentityType{core.IdentityTypeOrg}, "ff_system", verifier).Return(nil, nil).Once() + + err := em.BlockchainNetworkAction(mbi, "terminate", &blockchain.Event{}, verifier) + assert.NoError(t, err) + + mbi.AssertExpectations(t) + mii.AssertExpectations(t) +} + +func TestNetworkActionNonRootIdentity(t *testing.T) { + em, cancel := newTestEventManager(t) + defer cancel() + + verifier := &core.VerifierRef{ + Type: core.VerifierTypeEthAddress, + Value: "0x1234", + } + + mbi := &blockchainmocks.Plugin{} + mii := em.identity.(*identitymanagermocks.Manager) + + mii.On("FindIdentityForVerifier", em.ctx, []core.IdentityType{core.IdentityTypeOrg}, "ff_system", verifier).Return(&core.Identity{ + IdentityBase: core.IdentityBase{ + Parent: fftypes.NewUUID(), + }, + }, nil) + + err := em.BlockchainNetworkAction(mbi, "terminate", &blockchain.Event{}, verifier) + assert.NoError(t, err) + + mbi.AssertExpectations(t) + mii.AssertExpectations(t) } func TestNetworkActionUnknown(t *testing.T) { em, cancel := newTestEventManager(t) defer cancel() + verifier := &core.VerifierRef{ + Type: core.VerifierTypeEthAddress, + Value: "0x1234", + } + mbi := &blockchainmocks.Plugin{} + mii := em.identity.(*identitymanagermocks.Manager) + + mii.On("FindIdentityForVerifier", em.ctx, []core.IdentityType{core.IdentityTypeOrg}, "ff_system", verifier).Return(&core.Identity{}, nil) - err := em.BlockchainNetworkAction(mbi, "bad", &blockchain.Event{}, &core.VerifierRef{}) + err := em.BlockchainNetworkAction(mbi, "bad", &blockchain.Event{}, verifier) assert.NoError(t, err) mbi.AssertExpectations(t) + mii.AssertExpectations(t) } func TestActionTerminateQueryFail(t *testing.T) {