diff --git a/.gitignore b/.gitignore index f716852eb8..0d73ffa57f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ __debug* !deploy/charts/firefly containerlogs .vscode/*.log -.idea -doc-site/site \ No newline at end of file +.idea/ +doc-site/site +*.iml diff --git a/.golangci.yml b/.golangci.yml index e5221806bc..87bbf218a7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,6 +21,7 @@ linters-settings: values: regexp: COMPANY: .* + YEAR: '\d\d\d\d(-\d\d\d\d)?' template: |- Copyright © {{ YEAR }} {{ COMPANY }} diff --git a/Makefile b/Makefile index bcd40335eb..d77d30cb73 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ GOGC=30 all: build test go-mod-tidy test: deps lint - $(VGO) test ./internal/... ./pkg/... ./cmd/... ./doc-site ./ffconfig/... -cover -coverprofile=coverage.txt -covermode=atomic -timeout=30s ${TEST_ARGS} + $(VGO) test ./internal/... ./pkg/... ./cmd/... ./doc-site ./ffconfig/... -cover -coverprofile=coverage.txt -covermode=atomic -timeout=45s ${TEST_ARGS} coverage.html: $(VGO) tool cover -html=coverage.txt coverage: test coverage.html diff --git a/doc-site/docs/reference/types/subscription.md b/doc-site/docs/reference/types/subscription.md index d6a8767ccb..903cf0d93f 100644 --- a/doc-site/docs/reference/types/subscription.md +++ b/doc-site/docs/reference/types/subscription.md @@ -86,7 +86,7 @@ title: Subscription | Field Name | Description | Type | |------------|-------------|------| | `firstEvent` | Whether your application would like to receive events from the 'oldest' event emitted by your FireFly node (from the beginning of time), or the 'newest' event (from now), or a specific event sequence. Default is 'newest' | `SubOptsFirstEvent` | -| `readAhead` | The number of events to stream ahead to your application, while waiting for confirmation of consumption of those events. At least once delivery semantics are used in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts | `uint16` | +| `readAhead` | The number of events to stream ahead to your application, while waiting for confirmation of consumption of those events. At least once delivery semantics are used in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts | `uint` | | `withData` | Whether message events delivered over the subscription, should be packaged with the full data of those messages in-line as part of the event JSON payload. Or if the application should make separate REST calls to download that data. May not be supported on some transports. | `bool` | | `batch` | Events are delivered in batches in an ordered array. The batch size is capped to the readAhead limit. The event payload is always an array even if there is a single event in the batch, allowing client-side optimizations when processing the events in a group. Available for both Webhooks and WebSockets. | `bool` | | `batchTimeout` | When batching is enabled, the optional timeout to send events even when the batch hasn't filled. | `string` | diff --git a/doc-site/docs/reference/types/wsstart.md b/doc-site/docs/reference/types/wsstart.md index 9c674338b4..07e3f3223f 100644 --- a/doc-site/docs/reference/types/wsstart.md +++ b/doc-site/docs/reference/types/wsstart.md @@ -77,7 +77,7 @@ title: WSStart | Field Name | Description | Type | |------------|-------------|------| | `firstEvent` | Whether your application would like to receive events from the 'oldest' event emitted by your FireFly node (from the beginning of time), or the 'newest' event (from now), or a specific event sequence. Default is 'newest' | `SubOptsFirstEvent` | -| `readAhead` | The number of events to stream ahead to your application, while waiting for confirmation of consumption of those events. At least once delivery semantics are used in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts | `uint16` | +| `readAhead` | The number of events to stream ahead to your application, while waiting for confirmation of consumption of those events. At least once delivery semantics are used in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts | `uint` | | `withData` | Whether message events delivered over the subscription, should be packaged with the full data of those messages in-line as part of the event JSON payload. Or if the application should make separate REST calls to download that data. May not be supported on some transports. | `bool` | | `batch` | Events are delivered in batches in an ordered array. The batch size is capped to the readAhead limit. The event payload is always an array even if there is a single event in the batch, allowing client-side optimizations when processing the events in a group. Available for both Webhooks and WebSockets. | `bool` | | `batchTimeout` | When batching is enabled, the optional timeout to send events even when the batch hasn't filled. | `string` | diff --git a/doc-site/docs/swagger/swagger.yaml b/doc-site/docs/swagger/swagger.yaml index 60a2e7b24d..e56fed9a17 100644 --- a/doc-site/docs/swagger/swagger.yaml +++ b/doc-site/docs/swagger/swagger.yaml @@ -29287,7 +29287,6 @@ paths: used in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -29572,7 +29571,6 @@ paths: At least once delivery semantics are used in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -29846,7 +29844,6 @@ paths: in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -30128,7 +30125,6 @@ paths: At least once delivery semantics are used in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -30402,7 +30398,6 @@ paths: in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -30750,7 +30745,6 @@ paths: in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -38574,7 +38568,6 @@ paths: used in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -38852,7 +38845,6 @@ paths: At least once delivery semantics are used in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -39126,7 +39118,6 @@ paths: in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -39401,7 +39392,6 @@ paths: At least once delivery semantics are used in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -39675,7 +39665,6 @@ paths: in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: @@ -40009,7 +39998,6 @@ paths: in FireFly, so if your application crashes/reconnects this is the maximum number of events you would expect to be redelivered after it restarts - maximum: 65535 minimum: 0 type: integer reply: diff --git a/go.work.sum b/go.work.sum index c2ffc74006..7f670373fe 100644 --- a/go.work.sum +++ b/go.work.sum @@ -21,6 +21,7 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.7.4/go.mod h1:/aTEh45LzplQgFYdQdwPMR9YdX0UlhBmvB84uAmQKUc= @@ -91,10 +92,12 @@ cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvj cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.11.3/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= @@ -149,6 +152,7 @@ cloud.google.com/go/eventarc v1.13.3/go.mod h1:RWH10IAZIRcj1s/vClXkBgMHwh59ts7hS cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= cloud.google.com/go/filestore v1.7.4/go.mod h1:S5JCxIbFjeBhWMTfIYH2Jx24J6BqjwpkkPl+nBA5DlI= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= @@ -162,199 +166,395 @@ cloud.google.com/go/gkeconnect v0.8.4/go.mod h1:84hZz4UMlDCKl8ifVW8layK4WHlMAFeq cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= cloud.google.com/go/gkehub v0.14.4/go.mod h1:Xispfu2MqnnFt8rV/2/3o73SK1snL8s9dYJ9G2oQMfc= cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/spanner v1.51.0 h1:l3exhhsVMKsx1E7Xd1QajYSvHmI1KZoWPW5tRxIIdvQ= cloud.google.com/go/spanner v1.51.0/go.mod h1:c5KNo5LQ1X5tJwma9rSQZsXNBDNvj4/n8BVc3LNahq0= +cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 h1:rTnT/Jrcm+figWlYz4Ixzt0SJVR2cMC8lvZcimipiEY= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 h1:+5VZ72z0Qan5Bog5C+ZkgSqUbeVUd9wgtHOrIKuc5b8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest/adal v0.9.16 h1:P8An8Z9rH1ldbOLdFpxYorgOt2sywL9V24dAwWHPuGc= github.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/ClickHouse/clickhouse-go v1.4.3 h1:iAFMa2UrQdR5bHJ2/yaSLffZkxpcOYQMCUuKeNXGdqc= github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/aws/aws-sdk-go v1.49.6 h1:yNldzF5kzLBRvKlKz1S0bkvc2+04R1kt13KfBWQBfFA= github.com/aws/aws-sdk-go v1.49.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v1.16.16 h1:M1fj4FE2lB4NzRb9Y0xdWsn2P0+2UHVxwKyOa4YJNjk= github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8 h1:tcFliCWne+zOuUfKNRn8JdFBuWPDuISDH08wD2ULkhk= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8/go.mod h1:JTnlBSot91steJeti4ryyu/tLd4Sk84O5W22L7O2EQU= +github.com/aws/aws-sdk-go-v2/credentials v1.12.20 h1:9+ZhlDY7N9dPnUmf7CDfW9In4sW5Ff3bh7oy4DzS1IE= github.com/aws/aws-sdk-go-v2/credentials v1.12.20/go.mod h1:UKY5HyIux08bbNA7Blv4PcXQ8cTkGh7ghHMFklaviR4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33 h1:fAoVmNGhir6BR+RU0/EI+6+D7abM+MCwWf8v4ip5jNI= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33/go.mod h1:84XgODVR8uRhmOnUkKGUZKqIMxmjmLOR8Uyp7G/TPwc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 h1:s4g/wnzMf+qepSNgTvaQQHNxyMLKSawNhKCPNy++2xY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 h1:/K482T5A3623WJgWT8w1yRAFK4RzGzEl7y39yhtn9eA= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14 h1:ZSIPAkAsCCjYrhqfw2+lNzWDzxzHXEckFkTePL5RSWQ= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14/go.mod h1:AyGgqiKv9ECM6IZeNQtdT8NnMvUb3/2wokeq2Fgryto= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9 h1:Lh1AShsuIJTwMkoxVCAYPJgNG5H+eN6SmoUn8nOZ5wE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9/go.mod h1:a9j48l6yL5XINLHLcOKInjdvknN+vWqPBxqeIDw7ktw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18 h1:BBYoNQt2kUZUUK4bIPsKrCcjVPUMNsgQpNAwhznK/zo= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18/go.mod h1:NS55eQ4YixUJPTC+INxi2/jCqe1y2Uw3rnh9wEOVJxY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 h1:Jrd/oMh0PKQc6+BowB+pLEwLIgaQF29eYbe7E1Av9Ug= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17/go.mod h1:4nYOrY41Lrbk2170/BGkcJKBhws9Pfn8MG3aGqjjeFI= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17 h1:HfVVR1vItaG6le+Bpw6P4midjBDMKnjMyZnw9MXYUcE= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17/go.mod h1:YqMdV+gEKCQ59NrB7rzrJdALeBIsYiVi8Inj3+KcqHI= +github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11 h1:3/gm/JTX9bX8CpzTgIlrtYpB3EVBDxyg/GY/QdcIEZw= github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11/go.mod h1:fmgDANqTUCxciViKl9hb/zD5LFbvPINFRgWhDbR+vZo= +github.com/aws/smithy-go v1.13.3 h1:l7LYxGuzK6/K+NzJ2mC+VvLUbae0sL3bXU//04MkmnA= github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/cockroach-go/v2 v2.1.1 h1:3XzfSMuUT0wBe1a3o5C0eOTcArhmmFAg2Jzh/7hhKqo= github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= +github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE= github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw= +github.com/cucumber/godog v0.12.6 h1:3IToXviU45G7FgijwTk/LdB4iojn8zUFDfQLj4MMiHc= github.com/cucumber/godog v0.12.6/go.mod h1:Y02TTpimPXDb70PnG6M3zpODXm1+bjCsuZzcW76xAww= +github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY= github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM= github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fsouza/fake-gcs-server v1.17.0 h1:OeH75kBZcZa3ZE+zz/mFdJ2btt9FgqfjI7gIh9+5fvk= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= +github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gobuffalo/logger v1.0.0 h1:xw9Ko9EcC5iAFprrjJ6oZco9UpzS5MQ4jAwghsLHdy4= +github.com/gobuffalo/packr/v2 v2.5.1 h1:TFOeY2VoGamPjQLiNDT3mn//ytzk236VMO2j7iHxJR4= +github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556 h1:N/MD/sr6o61X+iZBAT2qEUF023s4KbA8RWfKzl0L6MQ= github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ= github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 h1:zC34cGQu69FG7qzJ3WiKW244WfhDC3xxYMeNOX2gtUQ= github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/consul/api v1.25.1 h1:CqrdhYzc8XZuPnhIYZWH45toM0LB9ZeYr/gvpLVI3PE= github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.3 h1:oGfEWrFuxtIUF3W2q/Jzt6G85TrMk9ey6XfYLvVe1Wo= github.com/hashicorp/go-memdb v1.3.3/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= -github.com/hyperledger/firefly-common v1.4.11 h1:WKv2hQuNpS7yP51THxzpzrqU3jkln23C9vq5iminzBk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q= github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/k0kubun/pp v2.3.0+incompatible h1:EKhKbi34VQDWJtq+zpsKSEhkHHs9w2P8Izbq8IhLVSo= github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/karrick/godirwalk v1.10.12 h1:BqUm+LuJcXjGv1d2mj3gBiQyrQ57a0rYoAmhvJQ7RDU= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/ktrysmt/go-bitbucket v0.6.4 h1:C8dUGp0qkwncKtAnozHCbbqhptefzEd1I0sfnuy9rYQ= github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= +github.com/markbates/pkger v0.15.1 h1:3MPelV53RnGSW07izx5xGxl4e/sdRD6zqseIk0rMASY= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microsoft/go-mssqldb v1.0.0 h1:k2p2uuG8T5T/7Hp7/e3vMGTnnR0sU4h8d1CcC71iLHU= github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/mutecomm/go-sqlcipher/v4 v4.4.0 h1:sV1tWCWGAVlPhNGT95Q+z/txFxuhAYWwHD1afF5bMZg= github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= +github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY= github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba h1:fhFP5RliM2HW/8XdcO5QngSfFli9GcRIpMXvypTQt6E= github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pierrec/lz4/v4 v4.1.16 h1:kQPfno+wyx6C5572ABwV+Uo3pDFzQ7yhyGchSyRda0c= github.com/pierrec/lz4/v4 v4.1.16/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79 h1:V7x0hCAgL8lNGezuex1RW1sh7VXXCqfw8nXZti66iFg= github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/sagikazarmark/crypt v0.17.0 h1:ZA/7pXyjkHoK4bW4mIdnCLvL8hd+Nrbiw7Dqk7D4qUk= github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/snowflakedb/gosnowflake v1.6.19 h1:KSHXrQ5o7uso25hNIzi/RObXtnSGkFgie91X82KcvMY= github.com/snowflakedb/gosnowflake v1.6.19/go.mod h1:FM1+PWUdwB9udFDsXdfD58NONC0m+MlOSmQRvimobSM= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= +go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= +go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/client/v2 v2.305.10 h1:MrmRktzv/XF8CvtQt+P6wLUlURaNpSDJHFZhe//2QE4= go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA= +go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= +go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI= go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4= google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +gopkg.in/bson.v2 v2.0.0-20171018101713-d8c8987b8862 h1:l7JQszYQzJc0GspaN+sivv8wScShqfkhS3nsgID8ees= gopkg.in/bson.v2 v2.0.0-20171018101713-d8c8987b8862/go.mod h1:VN8wuk/3Ksp8lVZ82HHf/MI1FHOBDt5bPK9VZ8DvymM= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/b v1.0.0 h1:vpvqeyp17ddcQWF29Czawql4lDdABCDRbXRAS4+aF2o= modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= +modernc.org/cc/v3 v3.36.3 h1:uISP3F66UlixxWEcKuIWERa4TwrZENHSL8tWxZz8bHg= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/db v1.0.0 h1:2c6NdCfaLnshSvY7OU09cyAY0gYXUZj4lmg5ItHyucg= modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= +modernc.org/file v1.0.0 h1:9/PdvjVxd5+LcWUQIfapAWRGOkDLK90rloa8s/au06A= modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= +modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +modernc.org/golex v1.0.0 h1:wWpDlbK8ejRfSyi0frMyhilD3JBvtcx2AdGDnU+JtsE= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/internal v1.0.0 h1:XMDsFDcBDsibbBnHB2xzljZ+B1yrOVLEFkKL2u15Glw= modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= +modernc.org/libc v1.17.1 h1:Q8/Cpi36V/QBfuQaFVeisEBs3WqoGAJprZzmf7TfEYI= modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/lldb v1.0.0 h1:6vjDJxQEfhlOLwl4bhpwIz00uyFK4EmSYcbwqwbynsc= modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.2.1 h1:dkRh86wgmq/bJu2cAS2oqBCz/KsMZU7TUM4CibQ7eBs= modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/ql v1.0.0 h1:bIQ/trWNVjQPlinI6jdOQsi195SIturGo3mp5hsDqVU= modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= +modernc.org/sortutil v1.1.0 h1:oP3U4uM+NT/qBQcbg/K2iqAX0Nx7B1b6YZtq3Gk/PjM= modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= +modernc.org/sqlite v1.18.1 h1:ko32eKt3jf7eqIkCgPAeHMBXw3riNSLhl2f3loEF7o8= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/zappy v1.0.0 h1:dPVaP+3ueIUv4guk8PuZ2wiUGcJ1WUVvIheeSSTD0yk= modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= diff --git a/internal/apiserver/ffi2swagger.go b/internal/apiserver/ffi2swagger.go index b5699b50d5..c7c3373127 100644 --- a/internal/apiserver/ffi2swagger.go +++ b/internal/apiserver/ffi2swagger.go @@ -81,7 +81,7 @@ func (swg *ffiSwaggerGen) Build(ctx context.Context, api *core.ContractAPI, ffi func addFFIMethod(ctx context.Context, routes []*ffapi.Route, method *fftypes.FFIMethod, hasLocation bool) []*ffapi.Route { description := method.Description - if method.Details != nil && len(method.Details) > 0 { + if len(method.Details) > 0 { additionalDetailsHeader := i18n.Expand(ctx, coremsgs.APISmartContractDetails) description = fmt.Sprintf("%s\n\n%s:\n\n%s", description, additionalDetailsHeader, buildDetailsTable(ctx, method.Details)) } @@ -117,7 +117,7 @@ func addFFIMethod(ctx context.Context, routes []*ffapi.Route, method *fftypes.FF func addFFIEvent(ctx context.Context, routes []*ffapi.Route, event *fftypes.FFIEvent, hasLocation bool) []*ffapi.Route { description := event.Description - if event.Details != nil && len(event.Details) > 0 { + if len(event.Details) > 0 { additionalDetailsHeader := i18n.Expand(ctx, coremsgs.APISmartContractDetails) description = fmt.Sprintf("%s\n\n%s:\n\n%s", description, additionalDetailsHeader, buildDetailsTable(ctx, event.Details)) } diff --git a/internal/apiserver/route_get_subscription_events_filtered_test.go b/internal/apiserver/route_get_subscription_events_filtered_test.go index fd480294dd..152e4637af 100644 --- a/internal/apiserver/route_get_subscription_events_filtered_test.go +++ b/internal/apiserver/route_get_subscription_events_filtered_test.go @@ -82,4 +82,4 @@ func TestGetSubscriptionEventsFilteredNoSequenceIDsProvided(t *testing.T) { r.ServeHTTP(res, req) assert.Equal(t, 200, res.Result().StatusCode) -} \ No newline at end of file +} diff --git a/internal/batch/batch_manager.go b/internal/batch/batch_manager.go index 95dd845a4e..ee77ce3b3b 100644 --- a/internal/batch/batch_manager.go +++ b/internal/batch/batch_manager.go @@ -44,7 +44,11 @@ func NewBatchManager(ctx context.Context, ns string, di database.Plugin, dm data return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "BatchManager") } pCtx, cancelCtx := context.WithCancel(log.WithLogField(ctx, "role", "batchmgr")) - readPageSize := config.GetUint(coreconfig.BatchManagerReadPageSize) + readPageSize := uint16(1) + confReadPageSize := config.GetUint64(coreconfig.BatchManagerReadPageSize) + if confReadPageSize > 0 && confReadPageSize <= 65535 { + readPageSize = uint16(confReadPageSize) + } bm := &batchManager{ ctx: pCtx, cancelCtx: cancelCtx, @@ -54,7 +58,7 @@ func NewBatchManager(ctx context.Context, ns string, di database.Plugin, dm data data: dm, txHelper: txHelper, readOffset: -1, // On restart we trawl for all ready messages - readPageSize: uint64(readPageSize), + readPageSize: readPageSize, minimumPollDelay: config.GetDuration(coreconfig.BatchManagerMinimumPollDelay), messagePollTimeout: config.GetDuration(coreconfig.BatchManagerReadPollTimeout), startupOffsetRetryAttempts: config.GetInt(coreconfig.OrchestratorStartupAttempts), @@ -116,7 +120,7 @@ type batchManager struct { inflightSequences map[int64]*batchProcessor inflightFlushed []int64 shoulderTap chan bool - readPageSize uint64 + readPageSize uint16 minimumPollDelay time.Duration messagePollTimeout time.Duration startupOffsetRetryAttempts int @@ -126,7 +130,7 @@ type DispatchHandler func(context.Context, *DispatchPayload) error type DispatcherOptions struct { BatchType core.BatchType - BatchMaxSize uint + BatchMaxSize int BatchMaxBytes int64 BatchTimeout time.Duration DisposeTimeout time.Duration @@ -279,11 +283,11 @@ func (bm *batchManager) readPage(lastPageFull bool) ([]*core.IDAndSequence, bool // Read a page from the DB var ids []*core.IDAndSequence err := bm.retry.Do(bm.ctx, "retrieve messages", func(attempt int) (retry bool, err error) { - fb := database.MessageQueryFactory.NewFilterLimit(bm.ctx, bm.readPageSize) + fb := database.MessageQueryFactory.NewFilterLimit(bm.ctx, uint64(bm.readPageSize)) ids, err = bm.database.GetMessageIDs(bm.ctx, bm.namespace, fb.And( fb.Gt("sequence", bm.readOffset), fb.Eq("state", core.MessageStateReady), - ).Sort("sequence").Limit(bm.readPageSize)) + ).Sort("sequence").Limit(uint64(bm.readPageSize))) return true, err }) @@ -549,6 +553,7 @@ func (bm *batchManager) maskContext(ctx context.Context, state *dispatchState, m // Now we have the nonce, add that at the end of the hash to make it unqiue to this message nonceBytes := make([]byte, 8) + //nolint:gosec binary.BigEndian.PutUint64(nonceBytes, uint64(nonce)) hashBuilder.Write(nonceBytes) diff --git a/internal/batch/batch_manager_test.go b/internal/batch/batch_manager_test.go index 00987c9ddd..61e436ee9d 100644 --- a/internal/batch/batch_manager_test.go +++ b/internal/batch/batch_manager_test.go @@ -53,6 +53,7 @@ func newTestBatchManager(t *testing.T) (*batchManager, func()) { cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(ctx, 100, 5*time.Minute), nil) txHelper, _ := txcommon.NewTransactionHelper(ctx, "ns1", mdi, mdm, cmi) + config.Set(coreconfig.BatchManagerReadPageSize, 0) // will get min value of 1 bm, err := NewBatchManager(context.Background(), "ns1", mdi, mdm, mim, txHelper) assert.NoError(t, err) return bm.(*batchManager), bm.(*batchManager).cancelCtx diff --git a/internal/batch/batch_processor.go b/internal/batch/batch_processor.go index c4496f0ac7..950abefc68 100644 --- a/internal/batch/batch_processor.go +++ b/internal/batch/batch_processor.go @@ -228,7 +228,7 @@ func (bp *batchProcessor) addWork(newWork *batchWork) (full, overflow bool) { bp.assemblyQueueBytes += newWork.estimateSize() bp.assemblyQueue = newQueue - full = len(bp.assemblyQueue) >= int(bp.conf.BatchMaxSize) || bp.assemblyQueueBytes >= bp.conf.BatchMaxBytes + full = len(bp.assemblyQueue) >= bp.conf.BatchMaxSize || bp.assemblyQueueBytes >= bp.conf.BatchMaxBytes overflow = len(bp.assemblyQueue) > 1 && (batchOfOne || bp.assemblyQueueBytes > bp.conf.BatchMaxBytes) } diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index afa3c2c068..ec6a8b8768 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -196,7 +196,7 @@ func (e *Ethereum) Init(ctx context.Context, cancelCtx context.CancelFunc, conf e.streamID = make(map[string]string) e.closed = make(map[string]chan struct{}) e.wsconn = make(map[string]wsclient.WSClient) - e.streams = newStreamManager(e.client, e.cache, e.ethconnectConf.GetUint(EthconnectConfigBatchSize), uint(e.ethconnectConf.GetDuration(EthconnectConfigBatchTimeout).Milliseconds())) + e.streams = newStreamManager(e.client, e.cache, e.ethconnectConf.GetUint(EthconnectConfigBatchSize), e.ethconnectConf.GetDuration(EthconnectConfigBatchTimeout).Milliseconds()) return nil } diff --git a/internal/blockchain/ethereum/eventstream.go b/internal/blockchain/ethereum/eventstream.go index 078ad95107..687590e64a 100644 --- a/internal/blockchain/ethereum/eventstream.go +++ b/internal/blockchain/ethereum/eventstream.go @@ -38,7 +38,7 @@ type streamManager struct { client *resty.Client cache cache.CInterface batchSize uint - batchTimeout uint + batchTimeout int64 } type eventStream struct { @@ -46,7 +46,7 @@ type eventStream struct { Name string `json:"name"` ErrorHandling string `json:"errorHandling"` BatchSize uint `json:"batchSize"` - BatchTimeoutMS uint `json:"batchTimeoutMS"` + BatchTimeoutMS int64 `json:"batchTimeoutMS"` Type string `json:"type"` WebSocket eventStreamWebsocket `json:"websocket"` Timestamps bool `json:"timestamps"` @@ -73,7 +73,7 @@ type subscriptionCheckpoint struct { Catchup bool `json:"catchup,omitempty"` } -func newStreamManager(client *resty.Client, cache cache.CInterface, batchSize, batchTimeout uint) *streamManager { +func newStreamManager(client *resty.Client, cache cache.CInterface, batchSize uint, batchTimeout int64) *streamManager { return &streamManager{ client: client, cache: cache, @@ -93,7 +93,7 @@ func (s *streamManager) getEventStreams(ctx context.Context) (streams []*eventSt return streams, nil } -func buildEventStream(topic string, batchSize, batchTimeout uint) *eventStream { +func buildEventStream(topic string, batchSize uint, batchTimeout int64) *eventStream { return &eventStream{ Name: topic, ErrorHandling: "block", @@ -120,7 +120,7 @@ func (s *streamManager) createEventStream(ctx context.Context, topic string) (*e return stream, nil } -func (s *streamManager) updateEventStream(ctx context.Context, topic string, batchSize, batchTimeout uint, eventStreamID string) (*eventStream, error) { +func (s *streamManager) updateEventStream(ctx context.Context, topic string, batchSize uint, batchTimeout int64, eventStreamID string) (*eventStream, error) { stream := buildEventStream(topic, batchSize, batchTimeout) res, err := s.client.R(). SetContext(ctx). diff --git a/internal/blockchain/fabric/eventstream.go b/internal/blockchain/fabric/eventstream.go index 48d1b72712..9d4e67480b 100644 --- a/internal/blockchain/fabric/eventstream.go +++ b/internal/blockchain/fabric/eventstream.go @@ -36,7 +36,7 @@ type streamManager struct { signer string cache cache.CInterface batchSize uint - batchTimeoutMS uint + batchTimeoutMS int64 } type eventStream struct { @@ -44,7 +44,7 @@ type eventStream struct { Name string `json:"name"` ErrorHandling string `json:"errorHandling"` BatchSize uint `json:"batchSize"` - BatchTimeoutMS uint `json:"batchTimeoutMS"` + BatchTimeoutMS int64 `json:"batchTimeoutMS"` Type string `json:"type"` WebSocket eventStreamWebsocket `json:"websocket"` Timestamps bool `json:"timestamps"` @@ -65,7 +65,7 @@ type eventFilter struct { EventFilter string `json:"eventFilter"` } -func newStreamManager(client *resty.Client, signer string, cache cache.CInterface, batchSize, batchTimeout uint) *streamManager { +func newStreamManager(client *resty.Client, signer string, cache cache.CInterface, batchSize uint, batchTimeout int64) *streamManager { return &streamManager{ client: client, signer: signer, @@ -86,7 +86,7 @@ func (s *streamManager) getEventStreams(ctx context.Context) (streams []*eventSt return streams, nil } -func buildEventStream(topic string, batchSize, batchTimeout uint) *eventStream { +func buildEventStream(topic string, batchSize uint, batchTimeout int64) *eventStream { return &eventStream{ Name: topic, ErrorHandling: "block", diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index 574e5d8e94..f909e22c8c 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -242,7 +242,7 @@ func (f *Fabric) Init(ctx context.Context, cancelCtx context.CancelFunc, conf co f.streamID = make(map[string]string) f.closed = make(map[string]chan struct{}) f.wsconn = make(map[string]wsclient.WSClient) - f.streams = newStreamManager(f.client, f.signer, f.cache, f.fabconnectConf.GetUint(FabconnectConfigBatchSize), uint(f.fabconnectConf.GetDuration(FabconnectConfigBatchTimeout).Milliseconds())) + f.streams = newStreamManager(f.client, f.signer, f.cache, f.fabconnectConf.GetUint(FabconnectConfigBatchSize), f.fabconnectConf.GetDuration(FabconnectConfigBatchTimeout).Milliseconds()) return nil } diff --git a/internal/blockchain/tezos/eventstream.go b/internal/blockchain/tezos/eventstream.go index f48f93d094..06b761fbb9 100644 --- a/internal/blockchain/tezos/eventstream.go +++ b/internal/blockchain/tezos/eventstream.go @@ -36,7 +36,7 @@ type streamManager struct { client *resty.Client cache cache.CInterface batchSize uint - batchTimeout uint + batchTimeout int64 } type eventStream struct { @@ -44,7 +44,7 @@ type eventStream struct { Name string `json:"name"` ErrorHandling string `json:"errorHandling"` BatchSize uint `json:"batchSize"` - BatchTimeoutMS uint `json:"batchTimeoutMS"` + BatchTimeoutMS int64 `json:"batchTimeoutMS"` Type string `json:"type"` Timestamps bool `json:"timestamps"` } @@ -64,7 +64,7 @@ type eventFilter struct { EventFilter string `json:"eventFilter"` } -func newStreamManager(client *resty.Client, cache cache.CInterface, batchSize, batchTimeout uint) *streamManager { +func newStreamManager(client *resty.Client, cache cache.CInterface, batchSize uint, batchTimeout int64) *streamManager { return &streamManager{ client: client, cache: cache, @@ -84,7 +84,7 @@ func (s *streamManager) getEventStreams(ctx context.Context) (streams []*eventSt return streams, nil } -func buildEventStream(topic string, batchSize, batchTimeout uint) *eventStream { +func buildEventStream(topic string, batchSize uint, batchTimeout int64) *eventStream { return &eventStream{ Name: topic, ErrorHandling: "block", @@ -108,7 +108,7 @@ func (s *streamManager) createEventStream(ctx context.Context, topic string) (*e return stream, nil } -func (s *streamManager) updateEventStream(ctx context.Context, topic string, batchSize, batchTimeout uint, eventStreamID string) (*eventStream, error) { +func (s *streamManager) updateEventStream(ctx context.Context, topic string, batchSize uint, batchTimeout int64, eventStreamID string) (*eventStream, error) { stream := buildEventStream(topic, batchSize, batchTimeout) res, err := s.client.R(). SetContext(ctx). diff --git a/internal/blockchain/tezos/tezos.go b/internal/blockchain/tezos/tezos.go index 8dd1fb8d4f..9229cf8df1 100644 --- a/internal/blockchain/tezos/tezos.go +++ b/internal/blockchain/tezos/tezos.go @@ -203,7 +203,7 @@ func (t *Tezos) Init(ctx context.Context, cancelCtx context.CancelFunc, conf con } t.cache = cache - t.streams = newStreamManager(t.client, t.cache, t.tezosconnectConf.GetUint(TezosconnectConfigBatchSize), uint(t.tezosconnectConf.GetDuration(TezosconnectConfigBatchTimeout).Milliseconds())) + t.streams = newStreamManager(t.client, t.cache, t.tezosconnectConf.GetUint(TezosconnectConfigBatchSize), t.tezosconnectConf.GetDuration(TezosconnectConfigBatchTimeout).Milliseconds()) t.backgroundStart = t.tezosconnectConf.GetBool(TezosconnectBackgroundStart) if t.backgroundStart { diff --git a/internal/broadcast/manager.go b/internal/broadcast/manager.go index 415585bbb5..c0c8ff696c 100644 --- a/internal/broadcast/manager.go +++ b/internal/broadcast/manager.go @@ -99,7 +99,7 @@ func NewBroadcastManager(ctx context.Context, ns *core.Namespace, di database.Pl if ba != nil && mult != nil { bo := batch.DispatcherOptions{ BatchType: core.BatchTypeBroadcast, - BatchMaxSize: config.GetUint(coreconfig.BroadcastBatchSize), + BatchMaxSize: config.GetInt(coreconfig.BroadcastBatchSize), BatchMaxBytes: bm.maxBatchPayloadLength, BatchTimeout: config.GetDuration(coreconfig.BroadcastBatchTimeout), DisposeTimeout: config.GetDuration(coreconfig.BroadcastBatchAgentTimeout), diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go index 8cccd5c344..e67cebbab9 100644 --- a/internal/coremsgs/en_error_messages.go +++ b/internal/coremsgs/en_error_messages.go @@ -316,4 +316,9 @@ var ( MsgFiltersEmpty = ffe("FF10475", "No filters specified in contract listener: %s.", 500) MsgContractListenerBlockchainFilterLimit = ffe("FF10476", "Blockchain plugin only supports one filter for contract listener: %s.", 500) MsgDuplicateContractListenerFilterLocation = ffe("FF10477", "Duplicate filter provided for contract listener for location", 400) + MsgInvalidNamespaceForOperationUpdate = ffe("FF10478", "Received different namespace for operation update '%s' than expected for manager '%s'") + MsgEmptyPluginForOperationUpdate = ffe("FF10479", "Received empty plugin for operation update '%s'") + MsgInvalidIdentityPatch = ffe("FF10480", "A profile must be provided when updating an identity", 400) + MsgNodeNotProvidedForCheck = ffe("FF10481", "Node not provided for check", 500) + MsgNodeMissingProfile = ffe("FF10482", "Node provided for check does not have a profile", 500) ) diff --git a/internal/database/sqlcommon/chart_sql.go b/internal/database/sqlcommon/chart_sql.go index b207020be1..86d7fb27f0 100644 --- a/internal/database/sqlcommon/chart_sql.go +++ b/internal/database/sqlcommon/chart_sql.go @@ -60,7 +60,7 @@ func (s *SQLCommon) getSelectStatements(ns string, tableName string, intervals [ sq.Eq{"namespace": ns}, }). OrderBy(timestampKey). - Limit(uint64(config.GetInt(coreconfig.HistogramsMaxChartRows)))) + Limit(config.GetUint64(coreconfig.HistogramsMaxChartRows))) } return queries diff --git a/internal/database/sqlcommon/event_sql_test.go b/internal/database/sqlcommon/event_sql_test.go index 17a95f0add..06d74a0c7d 100644 --- a/internal/database/sqlcommon/event_sql_test.go +++ b/internal/database/sqlcommon/event_sql_test.go @@ -107,7 +107,7 @@ func TestGetEventsInSequenceRangeE2EWithDB(t *testing.T) { Type: core.EventTypeMessageConfirmed, Reference: fftypes.NewUUID(), Correlator: fftypes.NewUUID(), - Topic: fmt.Sprintf("topic%d", i % 2), + Topic: fmt.Sprintf("topic%d", i%2), Created: fftypes.Now(), } err := s.InsertEvent(ctx, event) @@ -322,10 +322,9 @@ func TestGetEventsInSequenceRangeBuildQueryFail(t *testing.T) { func TestGetEventsInSequenceRangeShouldCallGetEventsWhenNoSequencedProvidedAndThrowAnError(t *testing.T) { s, mock := newMockProvider().init() - mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id", }).AddRow("only one")) + mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("only one")) f := database.EventQueryFactory.NewFilter(context.Background()).And() _, _, err := s.GetEventsInSequenceRange(context.Background(), "ns1", f, -1, -1) assert.NotNil(t, err) assert.NoError(t, mock.ExpectationsWereMet()) } - diff --git a/internal/database/sqlcommon/subscription_sql_test.go b/internal/database/sqlcommon/subscription_sql_test.go index 30a0802754..b7bf305ac6 100644 --- a/internal/database/sqlcommon/subscription_sql_test.go +++ b/internal/database/sqlcommon/subscription_sql_test.go @@ -62,7 +62,7 @@ func TestSubscriptionsE2EWithDB(t *testing.T) { // Update the subscription (this is testing what's possible at the database layer, // and does not account for the verification that happens at the higher level) newest := core.SubOptsFirstEventNewest - fifty := uint16(50) + fifty := uint(50) subOpts := core.SubscriptionOptions{ SubscriptionCoreOptions: core.SubscriptionCoreOptions{ FirstEvent: &newest, diff --git a/internal/dataexchange/ffdx/ffdx.go b/internal/dataexchange/ffdx/ffdx.go index 00c63dd53a..f8920d163e 100644 --- a/internal/dataexchange/ffdx/ffdx.go +++ b/internal/dataexchange/ffdx/ffdx.go @@ -18,11 +18,17 @@ package ffdx import ( "context" + "crypto/x509" "encoding/json" + "encoding/pem" + "errors" "fmt" "io" "strings" "sync" + "time" + + "github.com/hyperledger/firefly/internal/metrics" "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-common/pkg/config" @@ -54,6 +60,8 @@ type FFDX struct { retry *retry.Retry backgroundStart bool backgroundRetry *retry.Retry + + metrics metrics.Manager // optional } type dxNode struct { @@ -168,7 +176,7 @@ func (h *FFDX) Name() string { return "ffdx" } -func (h *FFDX) Init(ctx context.Context, cancelCtx context.CancelFunc, config config.Section) (err error) { +func (h *FFDX) Init(ctx context.Context, cancelCtx context.CancelFunc, config config.Section, metrics metrics.Manager) (err error) { h.ctx = log.WithLogField(ctx, "dx", "https") h.cancelCtx = cancelCtx h.ackChannel = make(chan *ack) @@ -179,6 +187,7 @@ func (h *FFDX) Init(ctx context.Context, cancelCtx context.CancelFunc, config co } h.needsInit = config.GetBool(DataExchangeInitEnabled) h.nodes = make(map[string]*dxNode) + h.metrics = metrics if config.GetString(ffresty.HTTPConfigURL) == "" { return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "dataexchange.ffdx") @@ -295,6 +304,11 @@ func (h *FFDX) beforeConnect(ctx context.Context, w wsclient.WSClient) error { return fmt.Errorf("DX returned non-ready status: %s", status.Status) } } + + for _, cb := range h.callbacks.handlers { + cb.DXConnect(h) + } + h.initialized = true return nil } @@ -448,6 +462,95 @@ func (h *FFDX) TransferBlob(ctx context.Context, nsOpID string, peer, sender fft return nil } +func (h *FFDX) CheckNodeIdentityStatus(ctx context.Context, node *core.Identity) error { + if err := h.checkInitialized(ctx); err != nil { + return err + } + + if node == nil { + return i18n.NewError(ctx, coremsgs.MsgNodeNotProvidedForCheck) + } + + var mismatchState = metrics.NodeIdentityDXCertMismatchStatusUnknown + defer func() { + if h.metrics != nil && h.metrics.IsMetricsEnabled() { + h.metrics.NodeIdentityDXCertMismatch(node.Namespace, mismatchState) + } + log.L(ctx).Debugf("Identity status checked against DX node='%s' mismatch_state='%s'", node.Name, mismatchState) + }() + + dxPeer, err := h.GetEndpointInfo(ctx, node.Name) // should be the same as the local node + if err != nil { + return err + } + + dxPeerCert := dxPeer.GetString("cert") + // if this occurs, it is either a misconfigured / broken DX or likely a DX that is compatible from an API perspective + // but does not have the same peer info as the HTTPS mTLS DX + if dxPeerCert == "" { + log.L(ctx).Debugf("DX peer does not have a 'cert', DX plugin may be unsupported") + return nil + } + + expiry, err := extractSoonestExpiryFromCertBundle(strings.ReplaceAll(dxPeerCert, `\n`, "\n")) + if err == nil { + if expiry.Before(time.Now()) { + log.L(ctx).Warnf("DX certificate for node '%s' has expired", node.Name) + } + + if h.metrics != nil && h.metrics.IsMetricsEnabled() { + h.metrics.NodeIdentityDXCertExpiry(node.Namespace, expiry) + } + } else { + log.L(ctx).Errorf("Failed to find x509 cert within DX cert bundle node='%s' namespace='%s'", node.Name, node.Namespace) + } + + if node.Profile == nil { + return i18n.NewError(ctx, coremsgs.MsgNodeNotProvidedForCheck) + } + + nodeCert := node.Profile.GetString("cert") + if nodeCert != "" { + mismatchState = metrics.NodeIdentityDXCertMismatchStatusHealthy + if dxPeerCert != nodeCert { + log.L(ctx).Warnf("DX certificate for node '%s' is out-of-sync with on-chain identity", node.Name) + mismatchState = metrics.NodeIdentityDXCertMismatchStatusMismatched + } + } + + return nil +} + +// We assume the cert with the soonest expiry is the leaf cert, but even if its the CA, +// that's what will invalidate the leaf anyways, so really we only care about the soonest expiry. +// So we loop through the bundle finding the soonest expiry, not necessarily the leaf. +func extractSoonestExpiryFromCertBundle(certBundle string) (time.Time, error) { + var expiringCert *x509.Certificate + var block *pem.Block + var rest = []byte(certBundle) + + for { + block, rest = pem.Decode(rest) + if block == nil { + break + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return time.Time{}, fmt.Errorf("failed to parse non-certificate within bundle: %v", err) + } + if expiringCert == nil || cert.NotAfter.Before(expiringCert.NotAfter) { + expiringCert = cert + } + } + + if expiringCert == nil { + return time.Time{}, errors.New("no valid certificate found") + } + + return expiringCert.NotAfter.UTC(), nil +} + func (h *FFDX) ackLoop() { for { select { diff --git a/internal/dataexchange/ffdx/ffdx_test.go b/internal/dataexchange/ffdx/ffdx_test.go index 63efbcd8d0..cb37910948 100644 --- a/internal/dataexchange/ffdx/ffdx_test.go +++ b/internal/dataexchange/ffdx/ffdx_test.go @@ -20,11 +20,17 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" + "github.com/hyperledger/firefly/internal/metrics" "io/ioutil" "net/http" "net/url" + "strings" "testing" + "time" + + "github.com/hyperledger/firefly/mocks/metricsmocks" "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/ffresty" @@ -45,6 +51,106 @@ import ( var utConfig = config.RootSection("ffdx_unit_tests") +const ( + // NOTE: the CA cert expires on Monday, February 28, 2035 7:30:57 PM UTC, + // and the leaf cert expires on Monday, February 28, 2028 7:30:57 PM UTC + testCertBundle = ` +-----BEGIN CERTIFICATE----- +MIIDqTCCApGgAwIBAgIUbZT+Ds4f2oDmGpgVi+SaQq9gxvcwDQYJKoZIhvcNAQEL +BQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xEzARBgNVBAoMCkV4YW1wbGUgQ0ExEzARBgNVBAMMCmV4 +YW1wbGUtY2EwHhcNMjUwMjI4MTkzMDM4WhcNMzUwMjI2MTkzMDM4WjBkMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j +aXNjbzETMBEGA1UECgwKRXhhbXBsZSBDQTETMBEGA1UEAwwKZXhhbXBsZS1jYTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOWBryFqk0YqQ6pzGJvBDjbV +4BnkMzsv+Fq869Xks09OP4eW44oqfFUmpCFyS3fEmRCz+389t4mKvxcCRIJMW0f5 +K9jffG1QKUKL4UuNfEPFpM0MXTwhI+dCdvofdelzc+KBGA6CDYlnWYcCKFSuWeSu +xrb/qCEvhcCaSYt3e2WcRHRuK+OLzM3REeJctC4G/pq858OUV5CZU2B6aGV/9uFL +ZW3TCrOaj+Khzzt5FNvjVdLiUw0FS8VESxFA4kH8p+XUshs9S0e7LfIBSID2NU8+ ++5D6HliqNqikbsny1Ps6GhLa+nI37LOVj7nFcG7uk+gb6HUN1+0YvjOJ0/zvnLEC +AwEAAaNTMFEwHQYDVR0OBBYEFJfNoXmIn5S6W7Lcj5G/huW5q1YQMB8GA1UdIwQY +MBaAFJfNoXmIn5S6W7Lcj5G/huW5q1YQMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBALsdRYJHQMkhjLcrO4Yha1KXh2d+irmi8AqQqQgbLIsSzuqG +bKFiYnJ8PKHaISHlev2xRM9kEjDZ/9q8T4aUELg4eBjj7VK+gs+gSBO6peJ+AcEg +TepsE5GHmhoIIiE/3dIP6XnaM6NBb8q0ewsIg1c5vLlrt8W96LY6Og7f+742VvoV +H31srpGjy7c5nYjBTn/Bu84eb5Lxfvy10sJjnenkXDJvzkUcnfbRzDQ9k5ZuPa05 +x+BsxonN0iaeZH91F+Y3kgJidLnU5EhIB/1KXYjuEbl9qUxD6GFHRststPRPeOmj +7C+BtJCIjjavysSqVMvQWLQ6rXms3SpRPAimWqM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgIUWnobAQ4vq8gWBAXZBf7XZG3oSiUwDQYJKoZIhvcNAQEL +BQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xEzARBgNVBAoMCkV4YW1wbGUgQ0ExEzARBgNVBAMMCmV4 +YW1wbGUtY2EwHhcNMjUwMjI4MTkzMDU3WhcNMjgwMjI4MTkzMDU3WjB2MQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j +aXNjbzEcMBoGA1UECgwTSHlwZXJsZWRnZXIgRmlyZWZseTEcMBoGA1UEAwwTaHlw +ZXJsZWRnZXItZmlyZWZseTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKlbQ+7cWWS0+QPp03PrdxsnAAtG2tWOk2CEG7HS3AlBU82YImhCidKOw+jQPS68 +2f2d0tYBhugqB2Ki6HsfYMGTjHDLbUQ5y+cLk6PFbvhjm39Ayd+WGmhWht5qFtRN +gllTa/SbG8+iGaSPIVFCyvg1IzxsFnBGn+05Gu+KjpL4i0l1RqDmy5ItxKGP77in +RPEUkejiUozg/X3v2TWAGagIVF5+EQ2Cswot9W1faAvyu/QmSGLLfSH22GdEDHXa +U4DV5ArJ2U2eNkOuasSWGKBopa/Wh1SZjKrNsy5Gw84ihAI4k7ARoP+vu1dIPdaX +ElipmGMtUWu0Azn2l9QJZpMCAwEAAaNCMEAwHQYDVR0OBBYEFL798jEmX2+hw70t +SmfJA78PZnnHMB8GA1UdIwQYMBaAFJfNoXmIn5S6W7Lcj5G/huW5q1YQMA0GCSqG +SIb3DQEBCwUAA4IBAQBY1NXTuQJZvjip33dRXyWP6GsSDKbXTSCcSF38P4/m+pcH +r/q/upo+K+8eTtPqUwBsIywH5bypWqoIPtM+rkd3FVBe7uti2FExufpcOruzEGsY +rNTfiFZbc7eHmFRTkKXWW4j6b6ElygrBvV999BhCRNf6NS0/syjqsbALHkFGeIcl +78wdaR+m2XVJBV7SmPmZ/EQzxvhCZONNVyU5zvW2sehI7sRbZt9/FG5U1Ng0LarW +R0gnXX/IZFnLhLh6UpLOBB0KIGENh75EEU7755jMKDKFj16D0uA1Lzrh5YxicTMy +ydFYQLpLycsWl2oV3JB4pO5TIzjY9awkRE0MeMMc +-----END CERTIFICATE----- +` + // The CA is at the bottom of the bundle and expires same as above, + // Monday, February 28, 2035 7:30:57 PM. The leaf cert at the top of + // the bundle is already expired. + testExpiredCert = ` +-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgIUWnobAQ4vq8gWBAXZBf7XZG3oSicwDQYJKoZIhvcNAQEL +BQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xEzARBgNVBAoMCkV4YW1wbGUgQ0ExEzARBgNVBAMMCmV4 +YW1wbGUtY2EwHhcNMjUwMzAyMTQxNDA1WhcNMjUwMzAyMTQxNDA1WjB2MQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j +aXNjbzEcMBoGA1UECgwTSHlwZXJsZWRnZXIgRmlyZWZseTEcMBoGA1UEAwwTaHlw +ZXJsZWRnZXItZmlyZWZseTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKlbQ+7cWWS0+QPp03PrdxsnAAtG2tWOk2CEG7HS3AlBU82YImhCidKOw+jQPS68 +2f2d0tYBhugqB2Ki6HsfYMGTjHDLbUQ5y+cLk6PFbvhjm39Ayd+WGmhWht5qFtRN +gllTa/SbG8+iGaSPIVFCyvg1IzxsFnBGn+05Gu+KjpL4i0l1RqDmy5ItxKGP77in +RPEUkejiUozg/X3v2TWAGagIVF5+EQ2Cswot9W1faAvyu/QmSGLLfSH22GdEDHXa +U4DV5ArJ2U2eNkOuasSWGKBopa/Wh1SZjKrNsy5Gw84ihAI4k7ARoP+vu1dIPdaX +ElipmGMtUWu0Azn2l9QJZpMCAwEAAaNCMEAwHQYDVR0OBBYEFL798jEmX2+hw70t +SmfJA78PZnnHMB8GA1UdIwQYMBaAFJfNoXmIn5S6W7Lcj5G/huW5q1YQMA0GCSqG +SIb3DQEBCwUAA4IBAQA1dY8UCf1YSLG3Vu/u20ucw5q9tnYYoTi4fHm/g+imTvEQ +KgTQBd5s9EHmj0BSjVFy9eSSTx5XiH6JqzGhCJRSbOIQ8RwrXpUlTuLr7gp0cO9c +Ykxz6wt0k1F+9Iq+K8Eb6jzXoZe/ebMz611zUqY7+9lIl1AIIgx96MoZcDS/LA0e +p/TUQ6q+Mg3W9pSXqLm8jmWNBfDViQF1v9Z3ASFYHUF/yak8jMdBEUpAqDadd/ay +BHm9m8IvFevQjpUw6kyyg77ehEBBn7H/ISTL3HTCpUbkR3qUnFjOyBJ0G02XoozB +I/hI0mpd6y+/JwyvG0smbD2lioiO/JQaUEZGU8pU +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDqTCCApGgAwIBAgIUbZT+Ds4f2oDmGpgVi+SaQq9gxvcwDQYJKoZIhvcNAQEL +BQAwZDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xEzARBgNVBAoMCkV4YW1wbGUgQ0ExEzARBgNVBAMMCmV4 +YW1wbGUtY2EwHhcNMjUwMjI4MTkzMDM4WhcNMzUwMjI2MTkzMDM4WjBkMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j +aXNjbzETMBEGA1UECgwKRXhhbXBsZSBDQTETMBEGA1UEAwwKZXhhbXBsZS1jYTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOWBryFqk0YqQ6pzGJvBDjbV +4BnkMzsv+Fq869Xks09OP4eW44oqfFUmpCFyS3fEmRCz+389t4mKvxcCRIJMW0f5 +K9jffG1QKUKL4UuNfEPFpM0MXTwhI+dCdvofdelzc+KBGA6CDYlnWYcCKFSuWeSu +xrb/qCEvhcCaSYt3e2WcRHRuK+OLzM3REeJctC4G/pq858OUV5CZU2B6aGV/9uFL +ZW3TCrOaj+Khzzt5FNvjVdLiUw0FS8VESxFA4kH8p+XUshs9S0e7LfIBSID2NU8+ ++5D6HliqNqikbsny1Ps6GhLa+nI37LOVj7nFcG7uk+gb6HUN1+0YvjOJ0/zvnLEC +AwEAAaNTMFEwHQYDVR0OBBYEFJfNoXmIn5S6W7Lcj5G/huW5q1YQMB8GA1UdIwQY +MBaAFJfNoXmIn5S6W7Lcj5G/huW5q1YQMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBALsdRYJHQMkhjLcrO4Yha1KXh2d+irmi8AqQqQgbLIsSzuqG +bKFiYnJ8PKHaISHlev2xRM9kEjDZ/9q8T4aUELg4eBjj7VK+gs+gSBO6peJ+AcEg +TepsE5GHmhoIIiE/3dIP6XnaM6NBb8q0ewsIg1c5vLlrt8W96LY6Og7f+742VvoV +H31srpGjy7c5nYjBTn/Bu84eb5Lxfvy10sJjnenkXDJvzkUcnfbRzDQ9k5ZuPa05 +x+BsxonN0iaeZH91F+Y3kgJidLnU5EhIB/1KXYjuEbl9qUxD6GFHRststPRPeOmj +7C+BtJCIjjavysSqVMvQWLQ6rXms3SpRPAimWqM= +-----END CERTIFICATE----- +` +) + func newTestFFDX(t *testing.T, manifestEnabled bool) (h *FFDX, toServer, fromServer chan string, httpURL string, done func()) { mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) @@ -64,8 +170,9 @@ func newTestFFDX(t *testing.T, manifestEnabled bool) (h *FFDX, toServer, fromSer h = &FFDX{initialized: true} h.InitConfig(utConfig) + mmm := metricsmocks.NewManager(t) dxCtx, dxCancel := context.WithCancel(context.Background()) - err := h.Init(dxCtx, dxCancel, utConfig) + err := h.Init(dxCtx, dxCancel, utConfig, mmm) assert.NoError(t, err) assert.Equal(t, "ffdx", h.Name()) assert.NotNil(t, h.Capabilities()) @@ -114,7 +221,7 @@ func TestInitBadURL(t *testing.T) { h.InitConfig(utConfig) utConfig.Set(ffresty.HTTPConfigURL, "::::////") ctx, cancel := context.WithCancel(context.Background()) - err := h.Init(ctx, cancel, utConfig) + err := h.Init(ctx, cancel, utConfig, nil) assert.Regexp(t, "FF00149", err) } @@ -127,7 +234,7 @@ func TestInitBadTLS(t *testing.T) { tlsConfig.Set(fftls.HTTPConfTLSEnabled, true) tlsConfig.Set(fftls.HTTPConfTLSCAFile, "badCA") ctx, cancel := context.WithCancel(context.Background()) - err := h.Init(ctx, cancel, utConfig) + err := h.Init(ctx, cancel, utConfig, nil) assert.Regexp(t, "FF00153", err) } @@ -136,7 +243,7 @@ func TestInitMissingURL(t *testing.T) { h := &FFDX{} h.InitConfig(utConfig) ctx, cancel := context.WithCancel(context.Background()) - err := h.Init(ctx, cancel, utConfig) + err := h.Init(ctx, cancel, utConfig, nil) assert.Regexp(t, "FF10138", err) } @@ -159,7 +266,7 @@ func TestInitWithBackgroundStart(t *testing.T) { h.InitConfig(utConfig) ctx, cancel := context.WithCancel(context.Background()) - err := h.Init(ctx, cancel, utConfig) + err := h.Init(ctx, cancel, utConfig, nil) assert.NoError(t, err) assert.NotNil(t, h.backgroundRetry) @@ -453,7 +560,7 @@ func TestBackgroundStartWSFail(t *testing.T) { dxCtx, dxCancel := context.WithCancel(context.Background()) defer dxCancel() - err := h.Init(dxCtx, dxCancel, utConfig) + err := h.Init(dxCtx, dxCancel, utConfig, nil) assert.NoError(t, err) assert.Equal(t, "ffdx", h.Name()) assert.NotNil(t, h.Capabilities()) @@ -480,7 +587,7 @@ func TestMessageEventsBackgroundStart(t *testing.T) { // Starting in background mode and making sure the event loop are started as well // to listen to messages utConfig.Set(DataExchangeBackgroundStart, true) - h.Init(h.ctx, h.cancelCtx, utConfig) + h.Init(h.ctx, h.cancelCtx, utConfig, nil) mcb := &dataexchangemocks.Callbacks{} h.SetHandler("ns1", "node1", mcb) @@ -488,6 +595,8 @@ func TestMessageEventsBackgroundStart(t *testing.T) { h.SetOperationHandler("ns1", ocb) h.AddNode(context.Background(), "ns1", "node1", fftypes.JSONObject{"id": "peer1"}) + mcb.On("DXConnect", h).Return(nil) + err := h.Start() assert.NoError(t, err) @@ -547,6 +656,7 @@ func TestMessageEvents(t *testing.T) { ocb := &coremocks.OperationCallbacks{} h.SetOperationHandler("ns1", ocb) h.AddNode(context.Background(), "ns1", "node1", fftypes.JSONObject{"id": "peer1"}) + mcb.On("DXConnect", h).Return(nil) err := h.Start() assert.NoError(t, err) @@ -612,6 +722,7 @@ func TestBlobEvents(t *testing.T) { ocb := &coremocks.OperationCallbacks{} h.SetOperationHandler("ns1", ocb) h.AddNode(context.Background(), "ns1", "node1", fftypes.JSONObject{"id": "peer1"}) + mcb.On("DXConnect", h).Return(nil) err := h.Start() assert.NoError(t, err) @@ -807,7 +918,7 @@ func TestWebsocketWithReinit(t *testing.T) { h.InitConfig(utConfig) ctx, cancel := context.WithCancel(context.Background()) - err := h.Init(ctx, cancel, utConfig) + err := h.Init(ctx, cancel, utConfig, nil) assert.NoError(t, err) h.AddNode(context.Background(), "ns1", "node1", fftypes.JSONObject{}) @@ -855,8 +966,11 @@ func TestWebsocketWithEmptyNodesInit(t *testing.T) { h.InitConfig(utConfig) ctx, cancel := context.WithCancel(context.Background()) - err := h.Init(ctx, cancel, utConfig) + err := h.Init(ctx, cancel, utConfig, nil) assert.NoError(t, err) + dxc := &dataexchangemocks.Callbacks{} + h.callbacks = callbacks{handlers: map[string]dataexchange.Callbacks{"ns1": dxc}} + dxc.On("DXConnect", h).Return(nil) err = h.Start() assert.NoError(t, err) @@ -905,3 +1019,252 @@ func TestDeleteBlobFail(t *testing.T) { err := h.DeleteBlob(context.Background(), fmt.Sprintf("ns1/%s", u)) assert.Regexp(t, "FF10229", err) } + +type mockDXCallbacks struct { + connectCalls int +} + +func (m *mockDXCallbacks) DXConnect(plugin dataexchange.Plugin) { + m.connectCalls++ +} + +func (m *mockDXCallbacks) DXEvent(plugin dataexchange.Plugin, event dataexchange.DXEvent) error { + panic("implement me") +} + +func TestWebsocketDXConnect(t *testing.T) { + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + _, _, wsURL, cancel := wsclient.NewTestWSServer(nil) + defer cancel() + + u, _ := url.Parse(wsURL) + u.Scheme = "http" + httpURL := u.String() + h := &FFDX{} + + coreconfig.Reset() + h.InitConfig(utConfig) + utConfig.Set(ffresty.HTTPConfigURL, httpURL) + utConfig.Set(ffresty.HTTPCustomClient, mockedClient) + utConfig.Set(DataExchangeInitEnabled, true) + + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/init", httpURL), + func(req *http.Request) (*http.Response, error) { + var reqNodes []fftypes.JSONObject + + // we want to make sure when theres are no peer nodes, an empty list is being + // passed as the req, not "null" + err := json.NewDecoder(req.Body).Decode(&reqNodes) + assert.NoError(t, err) + assert.Empty(t, reqNodes) + assert.NotNil(t, reqNodes) + + return httpmock.NewJsonResponse(200, fftypes.JSONObject{ + "status": "ready", + }) + }) + + h.InitConfig(utConfig) + ctx, cancel := context.WithCancel(context.Background()) + err := h.Init(ctx, cancel, utConfig, nil) + assert.NoError(t, err) + dxc := &mockDXCallbacks{ + connectCalls: 0, + } + h.callbacks = callbacks{handlers: map[string]dataexchange.Callbacks{"ns1": dxc}} + + err = h.Start() + assert.NoError(t, err) + + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, h.initialized) + assert.Equal(t, 1, dxc.connectCalls) +} + +func TestExtractSoonestExpiryFromCertBundleEmpty(t *testing.T) { + _, err := extractSoonestExpiryFromCertBundle("") + assert.ErrorContains(t, err, "no valid certificate found") + +} + +func TestExtractSoonestExpiryFromCertBundleBadBundle(t *testing.T) { + nonCertPEMBundle := ` +-----BEGIN NON-CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJALa6+u2k5u2kMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApxdWVlbnNsYW5kMREwDwYDVQQHDAhCcm9va2ZpZWxk +-----END NON-CERTIFICATE----- +` + + _, err := extractSoonestExpiryFromCertBundle(nonCertPEMBundle) + assert.ErrorContains(t, err, "failed to parse non-certificate within bundle") +} + +func TestCheckNodeIdentityStatusReturnsErrorWhenNotInitialized(t *testing.T) { + h := &FFDX{initialized: false} + err := h.CheckNodeIdentityStatus(context.Background(), &core.Identity{}) + assert.Regexp(t, "FF10342", err) +} + +func TestCheckNodeIdentityStatusNodeNil(t *testing.T) { + mmm := metricsmocks.NewManager(t) + + h := &FFDX{initialized: true, metrics: mmm} + + err := h.CheckNodeIdentityStatus(context.Background(), nil) + assert.Error(t, err) +} + +func TestCheckNodeIdentityStatusReturnsNilWhenCertIsEmpty(t *testing.T) { + h, _, _, httpURL, done := newTestFFDX(t, false) + defer done() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/api/v1/id", httpURL), + httpmock.NewJsonResponderOrPanic(200, fftypes.JSONObject{ + "id": "peer1", + "endpoint": "https://peer1.example.com", + "cert": "", + })) + + node := &core.Identity{ + IdentityBase: core.IdentityBase{ + Namespace: "ns1", + }, + } + + h.metrics.(*metricsmocks.Manager).On("IsMetricsEnabled").Return(true) + h.metrics.(*metricsmocks.Manager).On("NodeIdentityDXCertMismatch", "ns1", metrics.NodeIdentityDXCertMismatchStatusUnknown).Return(true) + + err := h.CheckNodeIdentityStatus(context.Background(), node) + assert.NoError(t, err) +} + +func TestCheckNodeIdentityStatusReturnsErrorWhenNodeProfileIsNil(t *testing.T) { + h, _, _, httpURL, done := newTestFFDX(t, false) + defer done() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/api/v1/id", httpURL), + httpmock.NewJsonResponderOrPanic(200, fftypes.JSONObject{ + "id": "peer1", + "endpoint": "https://peer1.example.com", + "cert": "a-cert", + })) + + node := &core.Identity{ + IdentityBase: core.IdentityBase{ + Namespace: "ns1", + }, + } + h.metrics.(*metricsmocks.Manager).On("IsMetricsEnabled").Return(false) + + err := h.CheckNodeIdentityStatus(context.Background(), node) + assert.Error(t, err) +} + +func TestCheckNodeIdentityStatusEndpointInfoFails(t *testing.T) { + h, _, _, httpURL, done := newTestFFDX(t, false) + defer done() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/api/v1/id", httpURL), + httpmock.NewErrorResponder(errors.New("failed to get peer info"))) + + h.metrics.(*metricsmocks.Manager).On("IsMetricsEnabled").Return(true) + h.metrics.(*metricsmocks.Manager).On("NodeIdentityDXCertMismatch", "ns1", metrics.NodeIdentityDXCertMismatchStatusUnknown).Return(true) + + node := &core.Identity{ + IdentityBase: core.IdentityBase{ + Namespace: "ns1", + }, + } + err := h.CheckNodeIdentityStatus(context.Background(), node) + assert.Error(t, err) +} + +func TestCheckNodeIdentityStatusSetsMismatchStateWhenCertsDiffer(t *testing.T) { + h, _, _, httpURL, done := newTestFFDX(t, false) + defer done() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/api/v1/id", httpURL), + httpmock.NewJsonResponderOrPanic(200, fftypes.JSONObject{ + "id": "peer1", + "endpoint": "https://peer1.example.com", + "cert": "a-cert", + })) + + node := &core.Identity{ + IdentityBase: core.IdentityBase{ + Namespace: "ns1", + }, + IdentityProfile: core.IdentityProfile{ + Profile: fftypes.JSONObject{"cert": "b-cert"}, + }, + } + + h.metrics.(*metricsmocks.Manager).On("IsMetricsEnabled").Return(true) + h.metrics.(*metricsmocks.Manager).On("NodeIdentityDXCertMismatch", "ns1", metrics.NodeIdentityDXCertMismatchStatusMismatched).Return(true) + err := h.CheckNodeIdentityStatus(context.Background(), node) + assert.NoError(t, err) +} + +func TestCheckNodeIdentityStatusSetsHealthyStateWhenCertsMatch(t *testing.T) { + h, _, _, httpURL, done := newTestFFDX(t, false) + defer done() + + jsonFriendlyCert := strings.ReplaceAll(testCertBundle, "\n", `\n`) + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/api/v1/id", httpURL), + httpmock.NewJsonResponderOrPanic(200, fftypes.JSONObject{ + "id": "peer1", + "endpoint": "https://peer1.example.com", + "cert": jsonFriendlyCert, + })) + + node := &core.Identity{ + IdentityBase: core.IdentityBase{ + Namespace: "ns1", + }, + IdentityProfile: core.IdentityProfile{ + Profile: fftypes.JSONObject{"cert": jsonFriendlyCert}, + }, + } + + h.metrics.(*metricsmocks.Manager).On("IsMetricsEnabled").Return(true) + h.metrics.(*metricsmocks.Manager).On("NodeIdentityDXCertMismatch", "ns1", metrics.NodeIdentityDXCertMismatchStatusHealthy).Return(true) + expiry := time.Unix(1835379057, 0).UTC() + h.metrics.(*metricsmocks.Manager).On("NodeIdentityDXCertExpiry", "ns1", expiry).Return(true) + + err := h.CheckNodeIdentityStatus(context.Background(), node) + assert.NoError(t, err) +} + +func TestCheckNodeIdentityStatusSetsHealthyStateWhenCertsExpire(t *testing.T) { + h, _, _, httpURL, done := newTestFFDX(t, false) + defer done() + + jsonFriendlyCert := strings.ReplaceAll(testExpiredCert, "\n", `\n`) + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/api/v1/id", httpURL), + httpmock.NewJsonResponderOrPanic(200, fftypes.JSONObject{ + "id": "peer1", + "endpoint": "https://peer1.example.com", + "cert": jsonFriendlyCert, + })) + node := &core.Identity{ + IdentityBase: core.IdentityBase{ + Namespace: "ns1", + }, + IdentityProfile: core.IdentityProfile{ + Profile: fftypes.JSONObject{"cert": jsonFriendlyCert}, + }, + } + + h.metrics.(*metricsmocks.Manager).On("IsMetricsEnabled").Return(true) + h.metrics.(*metricsmocks.Manager).On("NodeIdentityDXCertMismatch", "ns1", metrics.NodeIdentityDXCertMismatchStatusHealthy).Return(true) + expiry := time.Unix(1740924845, 0).UTC() + h.metrics.(*metricsmocks.Manager).On("NodeIdentityDXCertExpiry", "ns1", expiry).Return(true) + + err := h.CheckNodeIdentityStatus(context.Background(), node) + assert.NoError(t, err) +} diff --git a/internal/definitions/handler_identity_claim.go b/internal/definitions/handler_identity_claim.go index 8dff1604db..fcbfa16342 100644 --- a/internal/definitions/handler_identity_claim.go +++ b/internal/definitions/handler_identity_claim.go @@ -246,5 +246,23 @@ func (dh *definitionHandler) handleIdentityClaim(ctx context.Context, state *cor event := core.NewEvent(core.EventTypeIdentityConfirmed, identity.Namespace, identity.ID, nil, core.SystemTopicDefinitions) return dh.database.InsertEvent(ctx, event) }) + + if dh.multiparty && identity.Type == core.IdentityTypeNode { + nodeDID, err := dh.identity.GetLocalNodeDID(ctx) + if err != nil { + return HandlerResult{Action: core.ActionRetry}, err + } + + if nodeDID == identity.DID { + state.AddFinalize(func(ctx context.Context) error { + err := dh.exchange.CheckNodeIdentityStatus(ctx, identity) + if err != nil { + log.L(ctx).Warnf("Failed to check node identity status relative to dataexchange: %s", err) + } + return nil + }) + } + } + return HandlerResult{Action: core.ActionConfirm}, nil } diff --git a/internal/definitions/handler_identity_claim_test.go b/internal/definitions/handler_identity_claim_test.go index 17074f044c..a042c58115 100644 --- a/internal/definitions/handler_identity_claim_test.go +++ b/internal/definitions/handler_identity_claim_test.go @@ -19,6 +19,7 @@ package definitions import ( "context" "encoding/json" + "errors" "fmt" "testing" @@ -53,6 +54,33 @@ func testOrgIdentity(t *testing.T, name string) *core.Identity { return i } +func testNodeIdentity(t *testing.T, name string, parent *core.Identity) *core.Identity { + i := &core.Identity{ + IdentityBase: core.IdentityBase{ + ID: fftypes.NewUUID(), + Type: core.IdentityTypeNode, + Namespace: "ns1", + Name: name, + Parent: parent.ID, + }, + IdentityProfile: core.IdentityProfile{ + Description: "desc", + Profile: fftypes.JSONObject{ + "cert": "some cert", + "endpoint": "https://a-dx-endpoint", + "id": "dx id", + }, + }, + Messages: core.IdentityMessages{ + Claim: fftypes.NewUUID(), + }, + } + var err error + i.DID, err = i.GenerateDID(context.Background()) + assert.NoError(t, err) + return i +} + func testCustomIdentity(t *testing.T, name string, parent *core.Identity) *core.Identity { i := &core.Identity{ IdentityBase: core.IdentityBase{ @@ -136,6 +164,38 @@ func testCustomClaimAndVerification(t *testing.T) (*core.Identity, *core.Identit return custom1, org1, claimMsg, claimData, verifyMsg, verifyData } +func testNodeRegistrationAndVerification(t *testing.T) (*core.Identity, *core.Identity, *core.Message, *core.Data) { + org1 := testOrgIdentity(t, "org1") + node1 := testNodeIdentity(t, "node1", org1) + + ic := &core.IdentityClaim{ + Identity: node1, + } + b, err := json.Marshal(&ic) + assert.NoError(t, err) + claimData := &core.Data{ + ID: fftypes.NewUUID(), + Value: fftypes.JSONAnyPtrBytes(b), + } + + claimMsg := &core.Message{ + Header: core.MessageHeader{ + Namespace: "ns1", + ID: node1.Messages.Claim, + Type: core.MessageTypeDefinition, + Tag: core.SystemTagIdentityClaim, + Topics: fftypes.FFStringArray{node1.Topic()}, + SignerRef: core.SignerRef{ + Author: org1.DID, + Key: "0x12345", + }, + }, + } + claimMsg.Hash = fftypes.NewRandB32() + + return node1, org1, claimMsg, claimData +} + func TestHandleDefinitionIdentityClaimCustomWithExistingParentVerificationOk(t *testing.T) { dh, bs := newTestDefinitionHandler(t) defer dh.cleanup(t) @@ -539,3 +599,76 @@ func TestHandleDefinitionIdentityClaimBadData(t *testing.T) { bs.assertNoFinalizers() } + +func TestHandleDefinitionIdentityClaimLocalNodeWithFailingStatusCheck(t *testing.T) { + dh, bs := newTestDefinitionHandler(t) + defer dh.cleanup(t) + + ctx := context.Background() + node1, org1, claimMsg, claimData := testNodeRegistrationAndVerification(t) + + dh.mim.On("VerifyIdentityChain", ctx, node1).Return(org1, false, nil) + dh.mdi.On("GetIdentityByName", ctx, core.IdentityTypeNode, node1.Namespace, node1.Name).Return(nil, nil) + dh.mdi.On("GetIdentityByID", ctx, "ns1", node1.ID).Return(nil, nil) + dh.mdi.On("GetVerifierByValue", ctx, core.VerifierTypeFFDXPeerID, "ns1", "a dx").Return(nil, nil) + dh.mdi.On("UpsertIdentity", ctx, mock.MatchedBy(func(identity *core.Identity) bool { + assert.Equal(t, *claimMsg.Header.ID, *identity.Messages.Claim) + return true + }), database.UpsertOptimizationNew).Return(nil) + dh.mdi.On("UpsertVerifier", ctx, mock.MatchedBy(func(verifier *core.Verifier) bool { + assert.Equal(t, core.VerifierTypeFFDXPeerID, verifier.Type) + assert.Equal(t, "a dx", verifier.Value) + assert.Equal(t, *node1.ID, *verifier.Identity) + return true + }), database.UpsertOptimizationNew).Return(nil) + dh.mdi.On("InsertEvent", mock.Anything, mock.MatchedBy(func(event *core.Event) bool { + return event.Type == core.EventTypeIdentityConfirmed + })).Return(nil) + + dh.mdx.On("GetPeerID", node1.Profile).Return("a dx") + dh.mdx.On("AddNode", ctx, "ns1", node1.Name, node1.Profile).Return(nil) + dh.mim.On("GetLocalNodeDID", ctx).Return(node1.DID, nil) + dh.mdx.On("CheckNodeIdentityStatus", ctx, node1).Return(errors.New("failed to check status but no worries")) + + dh.multiparty = true + action, err := dh.HandleDefinitionBroadcast(ctx, &bs.BatchState, claimMsg, core.DataArray{claimData}, fftypes.NewUUID()) + assert.Equal(t, HandlerResult{Action: core.ActionConfirm}, action) + assert.NoError(t, err) + assert.Equal(t, []string{node1.DID}, bs.ConfirmedDIDClaims) + + err = bs.RunPreFinalize(ctx) + assert.NoError(t, err) + err = bs.RunFinalize(ctx) + assert.NoError(t, err) +} + +func TestHandleDefinitionIdentityClaimLocaNodeMisconfigured(t *testing.T) { + dh, bs := newTestDefinitionHandler(t) + defer dh.cleanup(t) + + ctx := context.Background() + node1, org1, claimMsg, claimData := testNodeRegistrationAndVerification(t) + + dh.mim.On("VerifyIdentityChain", ctx, node1).Return(org1, false, nil) + dh.mdi.On("GetIdentityByName", ctx, core.IdentityTypeNode, node1.Namespace, node1.Name).Return(nil, nil) + dh.mdi.On("GetIdentityByID", ctx, "ns1", node1.ID).Return(nil, nil) + dh.mdi.On("GetVerifierByValue", ctx, core.VerifierTypeFFDXPeerID, "ns1", "a dx").Return(nil, nil) + dh.mdi.On("UpsertIdentity", ctx, mock.MatchedBy(func(identity *core.Identity) bool { + assert.Equal(t, *claimMsg.Header.ID, *identity.Messages.Claim) + return true + }), database.UpsertOptimizationNew).Return(nil) + dh.mdi.On("UpsertVerifier", ctx, mock.MatchedBy(func(verifier *core.Verifier) bool { + assert.Equal(t, core.VerifierTypeFFDXPeerID, verifier.Type) + assert.Equal(t, "a dx", verifier.Value) + assert.Equal(t, *node1.ID, *verifier.Identity) + return true + }), database.UpsertOptimizationNew).Return(nil) + + dh.mdx.On("GetPeerID", node1.Profile).Return("a dx") + dh.mim.On("GetLocalNodeDID", ctx).Return(node1.DID, errors.New("somehow local node isnt configured but we got this far")) + + dh.multiparty = true + action, err := dh.HandleDefinitionBroadcast(ctx, &bs.BatchState, claimMsg, core.DataArray{claimData}, fftypes.NewUUID()) + assert.Equal(t, HandlerResult{Action: core.ActionRetry}, action) + assert.Error(t, err) +} diff --git a/internal/definitions/handler_identity_update.go b/internal/definitions/handler_identity_update.go index c1f793870d..db9a65b9f0 100644 --- a/internal/definitions/handler_identity_update.go +++ b/internal/definitions/handler_identity_update.go @@ -87,6 +87,24 @@ func (dh *definitionHandler) handleIdentityUpdate(ctx context.Context, state *co event := core.NewEvent(core.EventTypeIdentityUpdated, identity.Namespace, identity.ID, nil, core.SystemTopicDefinitions) return dh.database.InsertEvent(ctx, event) }) + + if dh.multiparty && identity.Type == core.IdentityTypeNode { + nodeDID, err := dh.identity.GetLocalNodeDID(ctx) + if err != nil { + return HandlerResult{Action: core.ActionRetry}, err + } + + if nodeDID == identity.DID { + state.AddFinalize(func(ctx context.Context) error { + err := dh.exchange.CheckNodeIdentityStatus(ctx, identity) + if err != nil { + log.L(ctx).Warnf("Failed to check node identity status relative to dataexchange: %s", err) + } + return nil + }) + } + } + return HandlerResult{Action: core.ActionConfirm}, err } diff --git a/internal/definitions/handler_identity_update_test.go b/internal/definitions/handler_identity_update_test.go index 7b95615518..9fa7db23ca 100644 --- a/internal/definitions/handler_identity_update_test.go +++ b/internal/definitions/handler_identity_update_test.go @@ -19,6 +19,7 @@ package definitions import ( "context" "encoding/json" + "errors" "fmt" "testing" @@ -65,6 +66,43 @@ func testIdentityUpdate(t *testing.T) (*core.Identity, *core.Message, *core.Data return org1, updateMsg, updateData, iu } +func testNodeIdentityUpdate(t *testing.T) (*core.Identity, *core.Identity, *core.Message, *core.Data, *core.IdentityUpdate) { + org1 := testOrgIdentity(t, "org1") + org1.Parent = fftypes.NewUUID() // Not involved in verification for updates, just must not change + node1 := testNodeIdentity(t, "node1", org1) + + iu := &core.IdentityUpdate{ + Identity: node1.IdentityBase, + Updates: core.IdentityProfile{ + Profile: fftypes.JSONObject{ + "new": "profile", + }, + Description: "new description", + }, + } + b, err := json.Marshal(&iu) + assert.NoError(t, err) + updateData := &core.Data{ + ID: fftypes.NewUUID(), + Value: fftypes.JSONAnyPtrBytes(b), + } + + updateMsg := &core.Message{ + Header: core.MessageHeader{ + ID: fftypes.NewUUID(), + Type: core.MessageTypeDefinition, + Tag: core.SystemTagIdentityUpdate, + Topics: fftypes.FFStringArray{node1.Topic()}, + SignerRef: core.SignerRef{ + Author: org1.DID, + Key: "0x12345", + }, + }, + } + + return org1, node1, updateMsg, updateData, iu +} + func TestHandleDefinitionIdentityUpdateOk(t *testing.T) { dh, bs := newTestDefinitionHandler(t) ctx := context.Background() @@ -250,3 +288,57 @@ func TestHandleDefinitionIdentityMissingData(t *testing.T) { bs.assertNoFinalizers() } + +func TestHandleDefinitionIdentityUpdateLocalNodeOk(t *testing.T) { + dh, bs := newTestDefinitionHandler(t) + ctx := context.Background() + + org1, node1, updateMsg, updateData, iu := testNodeIdentityUpdate(t) + + dh.mim.On("VerifyIdentityChain", ctx, node1).Return(org1, false, nil) + dh.mim.On("CachedIdentityLookupByID", ctx, node1.ID).Return(node1, nil) + dh.mdi.On("UpsertIdentity", ctx, mock.MatchedBy(func(identity *core.Identity) bool { + assert.Equal(t, *updateMsg.Header.ID, *identity.Messages.Update) + assert.Equal(t, node1.IdentityBase, identity.IdentityBase) + assert.Equal(t, iu.Updates, identity.IdentityProfile) + return true + }), database.UpsertOptimizationExisting).Return(nil) + dh.mdi.On("InsertEvent", mock.Anything, mock.MatchedBy(func(event *core.Event) bool { + return event.Type == core.EventTypeIdentityUpdated + })).Return(nil) + dh.mim.On("GetLocalNodeDID", ctx).Return(node1.DID, nil) + dh.mdx.On("CheckNodeIdentityStatus", ctx, node1).Return(errors.New("failed to check status but no worries")) + + dh.multiparty = true + action, err := dh.HandleDefinitionBroadcast(ctx, &bs.BatchState, updateMsg, core.DataArray{updateData}, fftypes.NewUUID()) + assert.Equal(t, HandlerResult{Action: core.ActionConfirm}, action) + assert.NoError(t, err) + + err = bs.RunFinalize(ctx) + assert.NoError(t, err) +} + +func TestHandleDefinitionIdentityUpdateLocalNodeMisconfigured(t *testing.T) { + dh, bs := newTestDefinitionHandler(t) + ctx := context.Background() + + org1, node1, updateMsg, updateData, iu := testNodeIdentityUpdate(t) + + dh.mim.On("VerifyIdentityChain", ctx, node1).Return(org1, false, nil) + dh.mim.On("CachedIdentityLookupByID", ctx, node1.ID).Return(node1, nil) + dh.mdi.On("UpsertIdentity", ctx, mock.MatchedBy(func(identity *core.Identity) bool { + assert.Equal(t, *updateMsg.Header.ID, *identity.Messages.Update) + assert.Equal(t, node1.IdentityBase, identity.IdentityBase) + assert.Equal(t, iu.Updates, identity.IdentityProfile) + return true + }), database.UpsertOptimizationExisting).Return(nil) + dh.mdi.On("InsertEvent", mock.Anything, mock.MatchedBy(func(event *core.Event) bool { + return event.Type == core.EventTypeIdentityUpdated + })).Return(nil) + dh.mim.On("GetLocalNodeDID", ctx).Return(node1.DID, errors.New("no local node but somehow we got this far")) + + dh.multiparty = true + action, err := dh.HandleDefinitionBroadcast(ctx, &bs.BatchState, updateMsg, core.DataArray{updateData}, fftypes.NewUUID()) + assert.Equal(t, HandlerResult{Action: core.ActionRetry}, action) + assert.Error(t, err) +} diff --git a/internal/definitions/handler_network_node_test.go b/internal/definitions/handler_network_node_test.go index 0877ee7a4d..d709efc594 100644 --- a/internal/definitions/handler_network_node_test.go +++ b/internal/definitions/handler_network_node_test.go @@ -122,6 +122,7 @@ func TestHandleDeprecatedNodeDefinitionOK(t *testing.T) { })).Return(nil) dh.mdx.On("GetPeerID", node.DX.Endpoint).Return("member_0") dh.mdx.On("AddNode", ctx, "ns1", node.Name, node.DX.Endpoint).Return(nil) + dh.mim.On("GetLocalNodeDID", ctx).Return("different node", nil) dh.multiparty = true diff --git a/internal/events/aggregator.go b/internal/events/aggregator.go index 70078f1788..9f630663e0 100644 --- a/internal/events/aggregator.go +++ b/internal/events/aggregator.go @@ -86,13 +86,17 @@ func privatePinHash(topic string, group *fftypes.Bytes32, identity string, nonce h.Write((*group)[:]) h.Write([]byte(identity)) nonceBytes := make([]byte, 8) - binary.BigEndian.PutUint64(nonceBytes, uint64(nonce)) + n := uint64(0) + if nonce > 0 { + n = uint64(nonce) + } + binary.BigEndian.PutUint64(nonceBytes, n) h.Write(nonceBytes) return fftypes.HashResult(h) } func newAggregator(ctx context.Context, ns string, di database.Plugin, bi blockchain.Plugin, pm privatemessaging.Manager, sh definitions.Handler, im identity.Manager, dm data.Manager, en *eventNotifier, mm metrics.Manager, cacheManager cache.Manager) (*aggregator, error) { - batchSize := config.GetInt(coreconfig.EventAggregatorBatchSize) + batchSize := config.GetUint64(coreconfig.EventAggregatorBatchSize) ag := &aggregator{ ctx: log.WithLogField(ctx, "role", "aggregator"), namespace: ns, diff --git a/internal/events/batch_pin_complete_test.go b/internal/events/batch_pin_complete_test.go index fc78d63ac8..75ee4c328c 100644 --- a/internal/events/batch_pin_complete_test.go +++ b/internal/events/batch_pin_complete_test.go @@ -660,8 +660,9 @@ func TestPersistBatchGoodDataMessageFail(t *testing.T) { em.mdi.On("InsertOrGetBatch", mock.Anything, mock.Anything).Return(nil, nil) em.mdi.On("InsertDataArray", mock.Anything, mock.Anything).Return(nil) - em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimzation miss")) - em.mdi.On("UpsertMessage", mock.Anything, mock.Anything, database.UpsertOptimizationExisting, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("pop")) + em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimzation miss")).Once() + em.mdi.On("GetMessageIDs", mock.Anything, "ns1", mock.Anything).Return([]*core.IDAndSequence{}, nil) + em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("pop")).Once() em.mim.On("GetLocalNode", mock.Anything).Return(testNode, nil) diff --git a/internal/events/dx_callbacks_test.go b/internal/events/dx_callbacks_test.go index 2f356d473a..751ce3e838 100644 --- a/internal/events/dx_callbacks_test.go +++ b/internal/events/dx_callbacks_test.go @@ -609,7 +609,7 @@ func TestMessageReceiveMessagePersistMessageFail(t *testing.T) { em.mdi.On("InsertOrGetBatch", em.ctx, mock.Anything).Return(nil, nil) em.mdi.On("InsertDataArray", em.ctx, mock.Anything).Return(nil) em.mdi.On("InsertMessages", em.ctx, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimization fail")) - em.mdi.On("UpsertMessage", em.ctx, mock.Anything, database.UpsertOptimizationExisting, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("pop")) + em.mdi.On("GetMessageIDs", mock.Anything, "ns1", mock.Anything).Return(nil, fmt.Errorf("pop")) // no ack as we are simulating termination mid retry mde := newMessageReceivedNoAck("peer1", tw) diff --git a/internal/events/event_dispatcher.go b/internal/events/event_dispatcher.go index f97a0174a8..7e65a0aa59 100644 --- a/internal/events/event_dispatcher.go +++ b/internal/events/event_dispatcher.go @@ -68,7 +68,7 @@ type eventDispatcher struct { eventDelivery chan []*core.EventDelivery mux sync.Mutex namespace string - readAhead int + readAhead uint64 batch bool subscription *subscription txHelper txcommon.Helper @@ -76,9 +76,9 @@ type eventDispatcher struct { func newEventDispatcher(ctx context.Context, enricher *eventEnricher, ei events.Plugin, di database.Plugin, dm data.Manager, bm broadcast.Manager, pm privatemessaging.Manager, connID string, sub *subscription, en *eventNotifier, txHelper txcommon.Helper) *eventDispatcher { ctx, cancelCtx := context.WithCancel(ctx) - readAhead := uint(0) + readAhead := uint64(0) if sub.definition.Options.ReadAhead != nil { - readAhead = uint(*sub.definition.Options.ReadAhead) + readAhead = uint64(*sub.definition.Options.ReadAhead) } batchTimeout := defaultBatchTimeout @@ -105,7 +105,7 @@ func newEventDispatcher(ctx context.Context, enricher *eventEnricher, ei events. namespace: sub.definition.Namespace, inflight: make(map[fftypes.UUID]*core.Event), eventDelivery: make(chan []*core.EventDelivery, readAhead+1), - readAhead: int(readAhead), + readAhead: readAhead, acksNacks: make(chan ackNack), closed: make(chan struct{}), txHelper: txHelper, @@ -113,7 +113,7 @@ func newEventDispatcher(ctx context.Context, enricher *eventEnricher, ei events. } pollerConf := &eventPollerConf{ - eventBatchSize: config.GetInt(coreconfig.EventDispatcherBufferLength), + eventBatchSize: config.GetUint64(coreconfig.EventDispatcherBufferLength), eventBatchTimeout: config.GetDuration(coreconfig.EventDispatcherBatchTimeout), eventPollTimeout: config.GetDuration(coreconfig.EventDispatcherPollTimeout), startupOffsetRetryAttempts: 0, // We need to keep trying to start indefinitely @@ -143,7 +143,7 @@ func newEventDispatcher(ctx context.Context, enricher *eventEnricher, ei events. pollerConf.eventPollTimeout = batchTimeout } } - if batch || pollerConf.eventBatchSize < int(readAhead) { + if batch || pollerConf.eventBatchSize < readAhead { pollerConf.eventBatchSize = ed.readAhead } @@ -243,9 +243,9 @@ func (ed *eventDispatcher) bufferedDelivery(events []core.LocallySequenced) (boo for { ed.mux.Lock() var dispatchable []*core.EventDelivery - inflightCount := len(ed.inflight) + inflightCount := uint64(len(ed.inflight)) maxDispatch := 1 + ed.readAhead - inflightCount - if maxDispatch >= len(matching) { + if maxDispatch >= uint64(len(matching)) { dispatchable = matching matching = nil } else if maxDispatch > 0 { @@ -260,7 +260,7 @@ func (ed *eventDispatcher) bufferedDelivery(events []core.LocallySequenced) (boo for _, event := range dispatchable { ed.mux.Lock() ed.inflight[*event.ID] = &event.Event - inflightCount = len(ed.inflight) + inflightCount = uint64(len(ed.inflight)) ed.mux.Unlock() dispatched++ diff --git a/internal/events/event_dispatcher_test.go b/internal/events/event_dispatcher_test.go index a056c32518..a752da0a2a 100644 --- a/internal/events/event_dispatcher_test.go +++ b/internal/events/event_dispatcher_test.go @@ -65,7 +65,7 @@ func newTestEventDispatcher(sub *subscription) (*eventDispatcher, func()) { } func TestEventDispatcherStartStop(t *testing.T) { - ten := uint16(10) + ten := uint(10) oldest := core.SubOptsFirstEventOldest ed, cancel := newTestEventDispatcher(&subscription{ dispatcherElection: make(chan bool, 1), @@ -88,7 +88,7 @@ func TestEventDispatcherStartStop(t *testing.T) { <-confirmedElected } - assert.Equal(t, int(10), ed.readAhead) + assert.Equal(t, uint64(10), ed.readAhead) ed.start() confirmedElected <- true close(confirmedElected) @@ -97,7 +97,7 @@ func TestEventDispatcherStartStop(t *testing.T) { } func TestEventDispatcherStartStopBatched(t *testing.T) { - ten := uint16(10) + ten := uint(10) oldest := core.SubOptsFirstEventOldest truthy := true ed, cancel := newTestEventDispatcher(&subscription{ @@ -122,7 +122,7 @@ func TestEventDispatcherStartStopBatched(t *testing.T) { <-confirmedElected } - assert.Equal(t, int(10), ed.readAhead) + assert.Equal(t, uint64(10), ed.readAhead) ed.start() confirmedElected <- true close(confirmedElected) @@ -172,7 +172,7 @@ func TestEventDispatcherLeaderElection(t *testing.T) { func TestEventDispatcherReadAheadOutOfOrderAcks(t *testing.T) { log.SetLevel("debug") - var five = uint16(5) + var five = uint(5) subID := fftypes.NewUUID() sub := &subscription{ dispatcherElection: make(chan bool, 1), @@ -364,7 +364,7 @@ func TestEventDispatcherNoReadAheadInOrder(t *testing.T) { func TestEventDispatcherBatchBased(t *testing.T) { log.SetLevel("debug") - three := uint16(3) + three := uint(3) longTime := "1m" subID := fftypes.NewUUID() truthy := true @@ -466,7 +466,7 @@ func TestEventDispatcherBatchBased(t *testing.T) { func TestEventDispatcherBatchDispatchFail(t *testing.T) { log.SetLevel("debug") - two := uint16(2) + two := uint(2) longTime := "1m" subID := fftypes.NewUUID() truthy := true @@ -1202,7 +1202,7 @@ func TestDeliverEventsWithDataFail(t *testing.T) { func TestEventDispatcherWithReply(t *testing.T) { log.SetLevel("debug") - var two = uint16(5) + var two = uint(5) sub := &subscription{ dispatcherElection: make(chan bool, 1), definition: &core.Subscription{ @@ -1253,7 +1253,7 @@ func TestEventDispatcherWithReply(t *testing.T) { func TestEventDeliveryBatch(t *testing.T) { log.SetLevel("debug") - var five = uint16(5) + var five = uint(5) truthy := true sub := &subscription{ dispatcherElection: make(chan bool, 1), diff --git a/internal/events/event_poller.go b/internal/events/event_poller.go index cd509cc204..774e303788 100644 --- a/internal/events/event_poller.go +++ b/internal/events/event_poller.go @@ -46,7 +46,7 @@ type newEventsHandler func(events []core.LocallySequenced) (bool, error) type eventPollerConf struct { ephemeral bool - eventBatchSize int + eventBatchSize uint64 eventBatchTimeout time.Duration eventPollTimeout time.Duration firstEvent *core.SubOptsFirstEvent @@ -179,7 +179,7 @@ func (ep *eventPoller) readPage() ([]core.LocallySequenced, error) { fb.Gt("sequence", pollingOffset), ) filter = ep.conf.addCriteria(filter) - items, err = ep.conf.getItems(ep.ctx, filter.Sort("sequence").Limit(uint64(ep.conf.eventBatchSize)), pollingOffset) + items, err = ep.conf.getItems(ep.ctx, filter.Sort("sequence").Limit(ep.conf.eventBatchSize), pollingOffset) if err != nil { return true, err // Retry indefinitely, until context cancelled } @@ -209,7 +209,7 @@ func (ep *eventPoller) eventLoop() { return } - eventCount := len(events) + eventCount := uint64(len(events)) // We might want to wait for the batch to fill - so we delay and re-poll if ep.conf.eventBatchTimeout > 0 && !doBatchDelay && eventCount < ep.conf.eventBatchSize { @@ -304,7 +304,7 @@ func (ep *eventPoller) waitForBatchTimeout() { } } -func (ep *eventPoller) waitForShoulderTapOrPollTimeout(lastEventCount int) bool { +func (ep *eventPoller) waitForShoulderTapOrPollTimeout(lastEventCount uint64) bool { l := log.L(ep.ctx) longTimeoutDuration := ep.conf.eventPollTimeout diff --git a/internal/events/persist_batch.go b/internal/events/persist_batch.go index 831cfb493e..170be743c1 100644 --- a/internal/events/persist_batch.go +++ b/internal/events/persist_batch.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -18,7 +18,9 @@ package events import ( "context" + "database/sql/driver" + "github.com/hyperledger/firefly-common/pkg/ffapi" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly/pkg/core" @@ -298,22 +300,56 @@ func (em *eventManager) persistBatchContent(ctx context.Context, batch *core.Bat }) if err != nil { log.L(ctx).Debugf("Batch message insert optimization failed for batch '%s': %s", batch.ID, err) - // Fall back to individual upserts - for i, msg := range batch.Payload.Messages { - postHookUpdateMessageCache := func() { - mm := matchedMsgs[i] - em.data.UpdateMessageCache(mm.message, mm.data) - } - if err = em.database.UpsertMessage(ctx, msg, database.UpsertOptimizationExisting, postHookUpdateMessageCache); err != nil { - if err == database.HashMismatch { - log.L(ctx).Errorf("Invalid message entry %d in batch'%s'. Hash mismatch with existing record with same UUID '%s' Hash=%s", i, batch.ID, msg.Header.ID, msg.Hash) - return false, nil // This is not retryable. skip this data entry + + // Messages are immutable in their contents, and it's entirely possible we're being sent + // messages we've already been sent in a previous batch, and subsequently modified th + // state of (they've been confirmed etc.). + // So we find a list of those that aren't in the DB and so and insert just those. + var foundIDs []*core.IDAndSequence + foundIDs, err = em.database.GetMessageIDs(ctx, batch.Namespace, messageIDFilter(ctx, batch.Payload.Messages)) + if err == nil { + remainingInserts := make([]*core.Message, 0, len(batch.Payload.Messages)) + for _, m := range batch.Payload.Messages { + isFound := false + for _, foundID := range foundIDs { + if foundID.ID.Equals(m.Header.ID) { + isFound = true + log.L(ctx).Warnf("Message %s in batch '%s' is a duplicate", m.Header.ID, batch.ID) + break + } + } + if !isFound { + remainingInserts = append(remainingInserts, m) } - log.L(ctx).Errorf("Failed to insert message entry %d in batch '%s': %s", i, batch.ID, err) - return false, err // a persistence failure here is considered retryable (so returned) + } + if len(remainingInserts) > 0 { + // Only the remaining ones get updates + err = em.database.InsertMessages(ctx, batch.Payload.Messages, func() { + for _, mm := range matchedMsgs { + for _, m := range remainingInserts { + if mm.message.Header.ID.Equals(m.Header.ID) { + em.data.UpdateMessageCache(mm.message, mm.data) + } + } + } + }) } } + // If we have an error at this point, we cannot insert (must not be a duplicate) + if err != nil { + log.L(ctx).Errorf("Failed to insert messages: %s", err) + return false, err // a persistence failure here is considered retryable (so returned) + } } return true, nil } + +func messageIDFilter(ctx context.Context, msgs []*core.Message) ffapi.Filter { + fb := database.MessageQueryFactory.NewFilter(ctx) + ids := make([]driver.Value, len(msgs)) + for i, msg := range msgs { + ids[i] = msg.Header.ID + } + return fb.In("id", ids) +} diff --git a/internal/events/persist_batch_test.go b/internal/events/persist_batch_test.go index 8ce8d2c49d..57d4934d39 100644 --- a/internal/events/persist_batch_test.go +++ b/internal/events/persist_batch_test.go @@ -225,7 +225,7 @@ func TestPersistBatchContentSentByUsNotFoundFallback(t *testing.T) { } -func TestPersistBatchContentSentByUsFoundMismatch(t *testing.T) { +func TestPersistBatchContentSentByUsFoundDup(t *testing.T) { em := newTestEventManager(t) defer em.cleanup(t) @@ -234,21 +234,24 @@ func TestPersistBatchContentSentByUsFoundMismatch(t *testing.T) { batch := sampleBatch(t, core.BatchTypeBroadcast, core.TransactionTypeBatchPin, core.DataArray{data}) batch.Node = testNodeID + msgID := batch.Payload.Messages[0].Header.ID em.mdm.On("GetMessageWithDataCached", em.ctx, batch.Payload.Messages[0].Header.ID).Return(&core.Message{ Header: core.MessageHeader{ - ID: fftypes.NewUUID(), + ID: msgID, }, }, nil, true, nil) em.mdi.On("InsertDataArray", mock.Anything, mock.Anything).Return(nil) em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimization miss")) - em.mdi.On("UpsertMessage", mock.Anything, mock.Anything, database.UpsertOptimizationExisting, mock.AnythingOfType("database.PostCompletionHook")).Return(database.HashMismatch) + em.mdi.On("GetMessageIDs", mock.Anything, "ns1", mock.Anything).Return([]*core.IDAndSequence{ + {ID: *msgID}, + }, nil) em.mim.On("GetLocalNode", mock.Anything).Return(testNode, nil) ok, err := em.persistBatchContent(em.ctx, batch, []*messageAndData{}) assert.NoError(t, err) - assert.False(t, ok) + assert.True(t, ok) } @@ -261,9 +264,10 @@ func TestPersistBatchContentInsertMessagesFail(t *testing.T) { batch := sampleBatch(t, core.BatchTypeBroadcast, core.TransactionTypeBatchPin, core.DataArray{data}) em.mdi.On("InsertDataArray", mock.Anything, mock.Anything).Return(nil) - em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimization miss")) - em.mdi.On("UpsertMessage", mock.Anything, mock.Anything, database.UpsertOptimizationExisting, mock.AnythingOfType("database.PostCompletionHook")).Return(nil).Run(func(args mock.Arguments) { - args[3].(database.PostCompletionHook)() + em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimization miss")).Once() + em.mdi.On("GetMessageIDs", mock.Anything, "ns1", mock.Anything).Return([]*core.IDAndSequence{}, nil) + em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(nil).Once().Run(func(args mock.Arguments) { + args[2].(database.PostCompletionHook)() }) msgData := &messageAndData{ diff --git a/internal/events/subscription_manager.go b/internal/events/subscription_manager.go index a31b937e5a..a11dcf2d6e 100644 --- a/internal/events/subscription_manager.go +++ b/internal/events/subscription_manager.go @@ -92,7 +92,7 @@ type subscriptionManager struct { deletedSubscriptions chan *fftypes.UUID retry retry.Retry - defaultBatchSize uint16 + defaultBatchSize uint defaultBatchTimeout time.Duration } @@ -120,7 +120,7 @@ func newSubscriptionManager(ctx context.Context, ns *core.Namespace, enricher *e MaximumDelay: config.GetDuration(coreconfig.SubscriptionsRetryMaxDelay), Factor: config.GetFloat64(coreconfig.SubscriptionsRetryFactor), }, - defaultBatchSize: uint16(config.GetInt(coreconfig.SubscriptionDefaultsBatchSize)), + defaultBatchSize: config.GetUint(coreconfig.SubscriptionDefaultsBatchSize), defaultBatchTimeout: config.GetDuration(coreconfig.SubscriptionDefaultsBatchTimeout), } @@ -534,7 +534,7 @@ func (sm *subscriptionManager) connectionClosed(ei events.Plugin, connID string) sm.mux.Lock() conn, ok := sm.connections[connID] if ok && conn.ei != ei { - log.L(sm.ctx).Warnf(i18n.ExpandWithCode(sm.ctx, i18n.MessageKey(coremsgs.MsgMismatchedTransport), connID, ei.Name(), conn.ei.Name())) + log.L(sm.ctx).Warn(i18n.ExpandWithCode(sm.ctx, i18n.MessageKey(coremsgs.MsgMismatchedTransport), connID, ei.Name(), conn.ei.Name())) sm.mux.Unlock() return } diff --git a/internal/events/subscription_manager_test.go b/internal/events/subscription_manager_test.go index ef509c7057..008bdff925 100644 --- a/internal/events/subscription_manager_test.go +++ b/internal/events/subscription_manager_test.go @@ -571,7 +571,7 @@ func TestCreateSubscriptionSuccessBatch(t *testing.T) { }) assert.NoError(t, err) - assert.Equal(t, uint16(50), *sub.definition.Options.ReadAhead) + assert.Equal(t, uint(50), *sub.definition.Options.ReadAhead) assert.Equal(t, "50ms", *sub.definition.Options.BatchTimeout) } diff --git a/internal/events/system/events.go b/internal/events/system/events.go index 354be298d7..c12c174114 100644 --- a/internal/events/system/events.go +++ b/internal/events/system/events.go @@ -42,7 +42,7 @@ type Events struct { mux sync.Mutex listeners map[string][]EventListener connID string - readAhead uint16 + readAhead uint } type callbacks struct { @@ -66,7 +66,7 @@ func (se *Events) Init(ctx context.Context, config config.Section) (err error) { handlers: make(map[string]events.Callbacks), }, listeners: make(map[string][]EventListener), - readAhead: uint16(config.GetInt(SystemEventsConfReadAhead)), + readAhead: config.GetUint(SystemEventsConfReadAhead), connID: fftypes.ShortID(), } return nil diff --git a/internal/events/websockets/websocket_connection.go b/internal/events/websockets/websocket_connection.go index 58de422fa0..804b58d53f 100644 --- a/internal/events/websockets/websocket_connection.go +++ b/internal/events/websockets/websocket_connection.go @@ -102,12 +102,12 @@ func isBoolQuerySet(query url.Values, boolOption string) bool { return hasOptionValues && (len(optionValues) == 0 || optionValues[0] != "false") } -func (wc *websocketConnection) getReadAhead(query url.Values, isBatch bool) *uint16 { +func (wc *websocketConnection) getReadAhead(query url.Values, isBatch bool) *uint { readaheadStr := query.Get("readahead") if readaheadStr != "" { readAheadInt, err := strconv.ParseUint(readaheadStr, 10, 16) if err == nil { - readahead := uint16(readAheadInt) + readahead := uint(readAheadInt) return &readahead } } diff --git a/internal/events/websockets/websockets_test.go b/internal/events/websockets/websockets_test.go index 031adebdfe..cba25ebfad 100644 --- a/internal/events/websockets/websockets_test.go +++ b/internal/events/websockets/websockets_test.go @@ -1219,4 +1219,4 @@ func TestHandleStartWrongNamespace(t *testing.T) { err := wc.handleStart(startMessage) assert.Error(t, err) assert.Regexp(t, "FF10462", err) -} \ No newline at end of file +} diff --git a/internal/identity/identitymanager.go b/internal/identity/identitymanager.go index 18757ff8ad..122c476e40 100644 --- a/internal/identity/identitymanager.go +++ b/internal/identity/identitymanager.go @@ -51,6 +51,7 @@ type Manager interface { CachedIdentityLookupMustExist(ctx context.Context, did string) (identity *core.Identity, retryable bool, err error) CachedIdentityLookupNilOK(ctx context.Context, did string) (identity *core.Identity, retryable bool, err error) GetLocalNode(ctx context.Context) (node *core.Identity, err error) + GetLocalNodeDID(ctx context.Context) (string, error) GetRootOrgDID(ctx context.Context) (string, error) GetRootOrg(ctx context.Context) (org *core.Identity, err error) VerifyIdentityChain(ctx context.Context, identity *core.Identity) (immediateParent *core.Identity, retryable bool, err error) @@ -103,13 +104,23 @@ func ParseKeyNormalizationConfig(strConfigVal string) int { } } -func (im *identityManager) GetLocalNode(ctx context.Context) (node *core.Identity, err error) { +// GetLocalNodeDID returns the expected local node DID based on the multiparty configuration, the node +// identity does not need to be registered yet in order for this to succeed. +func (im *identityManager) GetLocalNodeDID(ctx context.Context) (string, error) { nodeName := im.multiparty.LocalNode().Name if nodeName == "" { - return nil, i18n.NewError(ctx, coremsgs.MsgLocalNodeNotSet) + return "", i18n.NewError(ctx, coremsgs.MsgLocalNodeNotSet) } - nodeDID := fmt.Sprintf("%s%s", core.FireFlyNodeDIDPrefix, nodeName) + return fmt.Sprintf("%s%s", core.FireFlyNodeDIDPrefix, nodeName), nil +} + +// GetLocalNode returns the local node identity, if it has been registered and is in the DB/cache. +func (im *identityManager) GetLocalNode(ctx context.Context) (node *core.Identity, err error) { + nodeDID, err := im.GetLocalNodeDID(ctx) + if err != nil { + return nil, err + } node, _, err = im.CachedIdentityLookupNilOK(ctx, nodeDID) return node, err } diff --git a/internal/metrics/batch_pin.go b/internal/metrics/batch_pin.go index 6d0cbacb81..dc47b6620d 100644 --- a/internal/metrics/batch_pin.go +++ b/internal/metrics/batch_pin.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -20,16 +20,16 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -var BatchPinCounter prometheus.Counter +var BatchPinCounter *prometheus.CounterVec // MetricsBatchPin is the prometheus metric for total number of batch pins submitted var MetricsBatchPin = "ff_batchpin_total" func InitBatchPinMetrics() { - BatchPinCounter = prometheus.NewCounter(prometheus.CounterOpts{ + BatchPinCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: MetricsBatchPin, Help: "Number of batch pins submitted", - }) + }, namespaceLabels) } func RegisterBatchPinMetrics() { diff --git a/internal/metrics/broadcast.go b/internal/metrics/broadcast.go index 545e398a49..412ad00a30 100644 --- a/internal/metrics/broadcast.go +++ b/internal/metrics/broadcast.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -20,10 +20,10 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -var BroadcastSubmittedCounter prometheus.Counter -var BroadcastConfirmedCounter prometheus.Counter -var BroadcastRejectedCounter prometheus.Counter -var BroadcastHistogram prometheus.Histogram +var BroadcastSubmittedCounter *prometheus.CounterVec +var BroadcastConfirmedCounter *prometheus.CounterVec +var BroadcastRejectedCounter *prometheus.CounterVec +var BroadcastHistogram *prometheus.HistogramVec // BroadcastSubmittedCounterName is the prometheus metric for tracking the total number of broadcasts submitted var BroadcastSubmittedCounterName = "ff_broadcast_submitted_total" @@ -38,22 +38,22 @@ var BroadcastRejectedCounterName = "ff_broadcast_rejected_total" var BroadcastHistogramName = "ff_broadcast_histogram" func InitBroadcastMetrics() { - BroadcastSubmittedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + BroadcastSubmittedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: BroadcastSubmittedCounterName, Help: "Number of submitted broadcasts", - }) - BroadcastConfirmedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + }, namespaceLabels) + BroadcastConfirmedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: BroadcastConfirmedCounterName, Help: "Number of confirmed broadcasts", - }) - BroadcastRejectedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + }, namespaceLabels) + BroadcastRejectedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: BroadcastRejectedCounterName, Help: "Number of rejected broadcasts", - }) - BroadcastHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ + }, namespaceLabels) + BroadcastHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: BroadcastHistogramName, Help: "Histogram of broadcasts, bucketed by time to finished", - }) + }, namespaceLabels) } func RegisterBroadcastMetrics() { diff --git a/internal/metrics/contracts.go b/internal/metrics/contracts.go index baf3b9e604..f57240baa3 100644 --- a/internal/metrics/contracts.go +++ b/internal/metrics/contracts.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/metrics/identity.go b/internal/metrics/identity.go new file mode 100644 index 0000000000..55ff55915a --- /dev/null +++ b/internal/metrics/identity.go @@ -0,0 +1,77 @@ +// Copyright © 2025 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 metrics + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +var NodeIdentityDXCertMismatchGauge *prometheus.GaugeVec +var NodeIdentityDXCertExpiryGauge *prometheus.GaugeVec + +const ( + NodeIdentityDXCertMismatch = "ff_multiparty_node_identity_dx_mismatch" + NodeIdentityDXCertExpiry = "ff_multiparty_node_identity_dx_expiry_epoch" +) + +func InitIdentityMetrics() { + NodeIdentityDXCertMismatchGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: NodeIdentityDXCertMismatch, + Help: "Status of node identity DX cert mismatch", + }, namespaceLabels) + + NodeIdentityDXCertExpiryGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: NodeIdentityDXCertExpiry, + Help: "Timestamp, in Unix epoch format, of node identity DX cert's expiry", + }, namespaceLabels) +} + +func RegisterIdentityMetrics() { + registry.MustRegister(NodeIdentityDXCertMismatchGauge) + registry.MustRegister(NodeIdentityDXCertExpiryGauge) +} + +// TODO should this type live elsewhere ?? +type NodeIdentityDXCertMismatchStatus string + +const ( + NodeIdentityDXCertMismatchStatusMismatched NodeIdentityDXCertMismatchStatus = "mismatched" + NodeIdentityDXCertMismatchStatusHealthy NodeIdentityDXCertMismatchStatus = "healthy" + NodeIdentityDXCertMismatchStatusUnknown NodeIdentityDXCertMismatchStatus = "unknown" +) + +func (mm *metricsManager) NodeIdentityDXCertMismatch(namespace string, state NodeIdentityDXCertMismatchStatus) { + var gaugeState float64 + switch state { + case NodeIdentityDXCertMismatchStatusMismatched: + gaugeState = 1.0 + case NodeIdentityDXCertMismatchStatusHealthy: + gaugeState = 0.0 + case NodeIdentityDXCertMismatchStatusUnknown: + fallthrough + default: + gaugeState = -1.0 + } + + NodeIdentityDXCertMismatchGauge.WithLabelValues(namespace).Set(gaugeState) +} + +func (mm *metricsManager) NodeIdentityDXCertExpiry(namespace string, expiry time.Time) { + NodeIdentityDXCertExpiryGauge.WithLabelValues(namespace).Set(float64(expiry.UTC().Unix())) +} diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 35f3d5b19a..8c1e056a2f 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -29,8 +29,10 @@ import ( var mutex = &sync.Mutex{} +var namespaceLabels = []string{"ns"} + type Manager interface { - CountBatchPin() + CountBatchPin(namespace string) MessageSubmitted(msg *core.Message) MessageConfirmed(msg *core.Message, eventType fftypes.FFEnum) TransferSubmitted(transfer *core.TokenTransfer) @@ -39,6 +41,8 @@ type Manager interface { BlockchainTransaction(location, methodName string) BlockchainQuery(location, methodName string) BlockchainEvent(location, signature string) + NodeIdentityDXCertMismatch(namespace string, mismatch NodeIdentityDXCertMismatchStatus) + NodeIdentityDXCertExpiry(namespace string, expiry time.Time) AddTime(id string) GetTime(id string) time.Time DeleteTime(id string) @@ -61,17 +65,17 @@ func NewMetricsManager(ctx context.Context) Manager { return mm } -func (mm *metricsManager) CountBatchPin() { - BatchPinCounter.Inc() +func (mm *metricsManager) CountBatchPin(namespace string) { + BatchPinCounter.WithLabelValues(namespace).Inc() } func (mm *metricsManager) MessageSubmitted(msg *core.Message) { if len(msg.Header.ID.String()) > 0 { switch msg.Header.Type { case core.MessageTypeBroadcast: - BroadcastSubmittedCounter.Inc() + BroadcastSubmittedCounter.WithLabelValues(msg.LocalNamespace).Inc() case core.MessageTypePrivate: - PrivateMsgSubmittedCounter.Inc() + PrivateMsgSubmittedCounter.WithLabelValues(msg.LocalNamespace).Inc() } mm.AddTime(msg.Header.ID.String()) } @@ -87,23 +91,23 @@ func (mm *metricsManager) MessageConfirmed(msg *core.Message, eventType fftypes. if !eventTime.IsZero() { // Check that we recorded the submission // as we might not be the party submitting - BroadcastHistogram.Observe(timeElapsed) + BroadcastHistogram.WithLabelValues(msg.LocalNamespace).Observe(timeElapsed) } if eventType == core.EventTypeMessageConfirmed { // Broadcast Confirmed - BroadcastConfirmedCounter.Inc() + BroadcastConfirmedCounter.WithLabelValues(msg.LocalNamespace).Inc() } else if eventType == core.EventTypeMessageRejected { // Broadcast Rejected - BroadcastRejectedCounter.Inc() + BroadcastRejectedCounter.WithLabelValues(msg.LocalNamespace).Inc() } case core.MessageTypePrivate: if !eventTime.IsZero() { // Check that we recorded the submission // as we might not be the party submitting - PrivateMsgHistogram.Observe(timeElapsed) + PrivateMsgHistogram.WithLabelValues(msg.LocalNamespace).Observe(timeElapsed) } if eventType == core.EventTypeMessageConfirmed { // Private Msg Confirmed - PrivateMsgConfirmedCounter.Inc() + PrivateMsgConfirmedCounter.WithLabelValues(msg.LocalNamespace).Inc() } else if eventType == core.EventTypeMessageRejected { // Private Msg Rejected - PrivateMsgRejectedCounter.Inc() + PrivateMsgRejectedCounter.WithLabelValues(msg.LocalNamespace).Inc() } } } @@ -112,11 +116,11 @@ func (mm *metricsManager) TransferSubmitted(transfer *core.TokenTransfer) { if len(transfer.LocalID.String()) > 0 { switch transfer.Type { case core.TokenTransferTypeMint: // Mint submitted - MintSubmittedCounter.Inc() + MintSubmittedCounter.WithLabelValues(transfer.Namespace).Inc() case core.TokenTransferTypeTransfer: // Transfer submitted - TransferSubmittedCounter.Inc() + TransferSubmittedCounter.WithLabelValues(transfer.Namespace).Inc() case core.TokenTransferTypeBurn: // Burn submitted - BurnSubmittedCounter.Inc() + BurnSubmittedCounter.WithLabelValues(transfer.Namespace).Inc() } mm.AddTime(transfer.LocalID.String()) } @@ -130,19 +134,19 @@ func (mm *metricsManager) TransferConfirmed(transfer *core.TokenTransfer) { switch transfer.Type { case core.TokenTransferTypeMint: // Mint confirmed if !transferEvent.IsZero() { - MintHistogram.Observe(timeElapsed) + MintHistogram.WithLabelValues(transfer.Namespace).Observe(timeElapsed) } - MintConfirmedCounter.Inc() + MintConfirmedCounter.WithLabelValues(transfer.Namespace).Inc() case core.TokenTransferTypeTransfer: // Transfer confirmed if !transferEvent.IsZero() { - TransferHistogram.Observe(timeElapsed) + TransferHistogram.WithLabelValues(transfer.Namespace).Observe(timeElapsed) } - TransferConfirmedCounter.Inc() + TransferConfirmedCounter.WithLabelValues(transfer.Namespace).Inc() case core.TokenTransferTypeBurn: // Burn confirmed if !transferEvent.IsZero() { - BurnHistogram.Observe(timeElapsed) + BurnHistogram.WithLabelValues(transfer.Namespace).Observe(timeElapsed) } - BurnConfirmedCounter.Inc() + BurnConfirmedCounter.WithLabelValues(transfer.Namespace).Inc() } } diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go index 7e8c685f46..caa0e4c110 100644 --- a/internal/metrics/metrics_test.go +++ b/internal/metrics/metrics_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -62,7 +62,7 @@ func newTestMetricsManager(t *testing.T) (*metricsManager, func()) { func TestCountBatchPin(t *testing.T) { mm, cancel := newTestMetricsManager(t) defer cancel() - mm.CountBatchPin() + mm.CountBatchPin("a-ns") } func TestMessageSubmittedBroadcast(t *testing.T) { @@ -228,3 +228,44 @@ func TestIsMetricsEnabledFalse(t *testing.T) { mm.metricsEnabled = false assert.Equal(t, mm.IsMetricsEnabled(), false) } + +func TestNodeIdentityDXCertMismatchSetsMismatchedState(t *testing.T) { + mm, cancel := newTestMetricsManager(t) + defer cancel() + mm.NodeIdentityDXCertMismatch("test-namespace", NodeIdentityDXCertMismatchStatusMismatched) + gaugeValue := testutil.ToFloat64(NodeIdentityDXCertMismatchGauge.WithLabelValues("test-namespace")) + assert.Equal(t, 1.0, gaugeValue) +} + +func TestNodeIdentityDXCertMismatchSetsHealthyState(t *testing.T) { + mm, cancel := newTestMetricsManager(t) + defer cancel() + mm.NodeIdentityDXCertMismatch("test-namespace", NodeIdentityDXCertMismatchStatusHealthy) + gaugeValue := testutil.ToFloat64(NodeIdentityDXCertMismatchGauge.WithLabelValues("test-namespace")) + assert.Equal(t, 0.0, gaugeValue) +} + +func TestNodeIdentityDXCertMismatchSetsUnknownState(t *testing.T) { + mm, cancel := newTestMetricsManager(t) + defer cancel() + mm.NodeIdentityDXCertMismatch("test-namespace", NodeIdentityDXCertMismatchStatusUnknown) + gaugeValue := testutil.ToFloat64(NodeIdentityDXCertMismatchGauge.WithLabelValues("test-namespace")) + assert.Equal(t, -1.0, gaugeValue) +} + +func TestNodeIdentityDXCertMismatchSetsDefaultState(t *testing.T) { + mm, cancel := newTestMetricsManager(t) + defer cancel() + mm.NodeIdentityDXCertMismatch("test-namespace", "invalid-state") + gaugeValue := testutil.ToFloat64(NodeIdentityDXCertMismatchGauge.WithLabelValues("test-namespace")) + assert.Equal(t, -1.0, gaugeValue) +} + +func TestNodeIdentityDXCertExpiry(t *testing.T) { + mm, cancel := newTestMetricsManager(t) + defer cancel() + now := time.Now().UTC() + mm.NodeIdentityDXCertExpiry("test-namespace", now) + gaugeValue := testutil.ToFloat64(NodeIdentityDXCertExpiryGauge.WithLabelValues("test-namespace")) + assert.Equal(t, float64(now.Unix()), gaugeValue) +} diff --git a/internal/metrics/private_msg.go b/internal/metrics/private_msg.go index de654e3a88..a6d7355f8e 100644 --- a/internal/metrics/private_msg.go +++ b/internal/metrics/private_msg.go @@ -20,10 +20,10 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -var PrivateMsgSubmittedCounter prometheus.Counter -var PrivateMsgConfirmedCounter prometheus.Counter -var PrivateMsgRejectedCounter prometheus.Counter -var PrivateMsgHistogram prometheus.Histogram +var PrivateMsgSubmittedCounter *prometheus.CounterVec +var PrivateMsgConfirmedCounter *prometheus.CounterVec +var PrivateMsgRejectedCounter *prometheus.CounterVec +var PrivateMsgHistogram *prometheus.HistogramVec // PrivateMsgSubmittedCounterName is the prometheus metric for tracking the total number of private messages submitted var PrivateMsgSubmittedCounterName = "ff_private_msg_submitted_total" @@ -40,22 +40,22 @@ var PrivateMsgRejectedCounterName = "ff_private_msg_rejected_total" var PrivateMsgHistogramName = "ff_private_msg_histogram" func InitPrivateMsgMetrics() { - PrivateMsgSubmittedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + PrivateMsgSubmittedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: PrivateMsgSubmittedCounterName, Help: "Number of submitted private messages", - }) - PrivateMsgConfirmedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + }, namespaceLabels) + PrivateMsgConfirmedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: PrivateMsgConfirmedCounterName, Help: "Number of confirmed private messages", - }) - PrivateMsgRejectedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + }, namespaceLabels) + PrivateMsgRejectedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: PrivateMsgRejectedCounterName, Help: "Number of rejected private messages", - }) - PrivateMsgHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ + }, namespaceLabels) + PrivateMsgHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: PrivateMsgHistogramName, Help: "Histogram of private messages, bucketed by time to finished", - }) + }, namespaceLabels) } func RegisterPrivateMsgMetrics() { diff --git a/internal/metrics/prometheus.go b/internal/metrics/prometheus.go index f9576e8498..6bf09f93ea 100644 --- a/internal/metrics/prometheus.go +++ b/internal/metrics/prometheus.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -88,6 +88,7 @@ func initMetricsCollectors() { InitTokenBurnMetrics() InitBatchPinMetrics() InitBlockchainMetrics() + InitIdentityMetrics() } func registerMetricsCollectors() { @@ -101,4 +102,5 @@ func registerMetricsCollectors() { RegisterTokenTransferMetrics() RegisterTokenBurnMetrics() RegisterBlockchainMetrics() + RegisterIdentityMetrics() } diff --git a/internal/metrics/prometheus_test.go b/internal/metrics/prometheus_test.go index 6a7770d00b..b0846a95d0 100644 --- a/internal/metrics/prometheus_test.go +++ b/internal/metrics/prometheus_test.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // diff --git a/internal/metrics/token_burn.go b/internal/metrics/token_burn.go index 9b0eefea7f..7cc6b33d6b 100644 --- a/internal/metrics/token_burn.go +++ b/internal/metrics/token_burn.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -20,10 +20,10 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -var BurnSubmittedCounter prometheus.Counter -var BurnConfirmedCounter prometheus.Counter -var BurnRejectedCounter prometheus.Counter -var BurnHistogram prometheus.Histogram +var BurnSubmittedCounter *prometheus.CounterVec +var BurnConfirmedCounter *prometheus.CounterVec +var BurnRejectedCounter *prometheus.CounterVec +var BurnHistogram *prometheus.HistogramVec // BurnSubmittedCounterName is the prometheus metric for tracking the total number of burns submitted var BurnSubmittedCounterName = "ff_burn_submitted_total" @@ -38,22 +38,22 @@ var BurnRejectedCounterName = "ff_burn_rejected_total" var BurnHistogramName = "ff_burn_histogram" func InitTokenBurnMetrics() { - BurnSubmittedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + BurnSubmittedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: BurnSubmittedCounterName, Help: "Number of submitted burns", - }) - BurnConfirmedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + }, namespaceLabels) + BurnConfirmedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: BurnConfirmedCounterName, Help: "Number of confirmed burns", - }) - BurnRejectedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + }, namespaceLabels) + BurnRejectedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: BurnRejectedCounterName, Help: "Number of rejected burns", - }) - BurnHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ + }, namespaceLabels) + BurnHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: BurnHistogramName, Help: "Histogram of burns, bucketed by time to finished", - }) + }, namespaceLabels) } func RegisterTokenBurnMetrics() { diff --git a/internal/metrics/token_mint.go b/internal/metrics/token_mint.go index 36aa9eb4dd..1d6eea674d 100644 --- a/internal/metrics/token_mint.go +++ b/internal/metrics/token_mint.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -20,10 +20,10 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -var MintSubmittedCounter prometheus.Counter -var MintConfirmedCounter prometheus.Counter -var MintRejectedCounter prometheus.Counter -var MintHistogram prometheus.Histogram +var MintSubmittedCounter *prometheus.CounterVec +var MintConfirmedCounter *prometheus.CounterVec +var MintRejectedCounter *prometheus.CounterVec +var MintHistogram *prometheus.HistogramVec // MintSubmittedCounterName is the prometheus metric for tracking the total number of mints submitted var MintSubmittedCounterName = "ff_mint_submitted_total" @@ -38,22 +38,22 @@ var MintRejectedCounterName = "ff_mint_rejected_total" var MintHistogramName = "ff_mint_histogram" func InitTokenMintMetrics() { - MintSubmittedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + MintSubmittedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: MintSubmittedCounterName, Help: "Number of submitted mints", - }) - MintConfirmedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + }, namespaceLabels) + MintConfirmedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: MintConfirmedCounterName, Help: "Number of confirmed mints", - }) - MintRejectedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + }, namespaceLabels) + MintRejectedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: MintRejectedCounterName, Help: "Number of rejected mints", - }) - MintHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ + }, namespaceLabels) + MintHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: MintHistogramName, Help: "Histogram of mints, bucketed by time to finished", - }) + }, namespaceLabels) } func RegisterTokenMintMetrics() { diff --git a/internal/metrics/token_transfer.go b/internal/metrics/token_transfer.go index 0aaf2571ac..e3a16008b6 100644 --- a/internal/metrics/token_transfer.go +++ b/internal/metrics/token_transfer.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -20,10 +20,10 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -var TransferSubmittedCounter prometheus.Counter -var TransferConfirmedCounter prometheus.Counter -var TransferRejectedCounter prometheus.Counter -var TransferHistogram prometheus.Histogram +var TransferSubmittedCounter *prometheus.CounterVec +var TransferConfirmedCounter *prometheus.CounterVec +var TransferRejectedCounter *prometheus.CounterVec +var TransferHistogram *prometheus.HistogramVec // TransferSubmittedCounterName is the prometheus metric for tracking the total number of transfers submitted var TransferSubmittedCounterName = "ff_transfer_submitted_total" @@ -38,22 +38,22 @@ var TransferRejectedCounterName = "ff_transfer_rejected_total" var TransferHistogramName = "ff_transfer_histogram" func InitTokenTransferMetrics() { - TransferSubmittedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + TransferSubmittedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: TransferSubmittedCounterName, Help: "Number of submitted transfers", - }) - TransferConfirmedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + }, namespaceLabels) + TransferConfirmedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: TransferConfirmedCounterName, Help: "Number of confirmed transfers", - }) - TransferRejectedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + }, namespaceLabels) + TransferRejectedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: TransferRejectedCounterName, Help: "Number of rejected transfers", - }) - TransferHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ + }, namespaceLabels) + TransferHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: TransferHistogramName, Help: "Histogram of transfers, bucketed by time to finished", - }) + }, namespaceLabels) } func RegisterTokenTransferMetrics() { diff --git a/internal/multiparty/manager.go b/internal/multiparty/manager.go index 5ac4ac35b0..d1ffd058b5 100644 --- a/internal/multiparty/manager.go +++ b/internal/multiparty/manager.go @@ -283,7 +283,7 @@ func (mm *multipartyManager) SubmitBatchPin(ctx context.Context, batch *core.Bat } if mm.metrics.IsMetricsEnabled() { - mm.metrics.CountBatchPin() + mm.metrics.CountBatchPin(mm.namespace.Name) } _, err := mm.operations.RunOperation(ctx, opBatchPin(op, batch, contexts, payloadRef), idempotentSubmit) return err diff --git a/internal/multiparty/manager_test.go b/internal/multiparty/manager_test.go index dc9392c8c5..1a4ebefe9c 100644 --- a/internal/multiparty/manager_test.go +++ b/internal/multiparty/manager_test.go @@ -485,7 +485,7 @@ func TestSubmitPinnedBatchWithMetricsOk(t *testing.T) { return true })).Return(nil) mp.mmi.On("IsMetricsEnabled").Return(true) - mp.mmi.On("CountBatchPin").Return() + mp.mmi.On("CountBatchPin", "ns1").Return() mp.mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *core.PreparedOperation) bool { data := op.Data.(txcommon.BatchPinData) return op.Type == core.OpTypeBlockchainPinBatch && data.Batch == batch @@ -618,7 +618,7 @@ func TestSubmitBatchPinWithBatchOpNotFound(t *testing.T) { return true })).Return(nil) mp.mmi.On("IsMetricsEnabled").Return(true) - mp.mmi.On("CountBatchPin").Return() + mp.mmi.On("CountBatchPin", "ns1").Return() mp.mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *core.PreparedOperation) bool { data := op.Data.(txcommon.BatchPinData) return op.Type == core.OpTypeBlockchainPinBatch && data.Batch == batch diff --git a/internal/namespace/configreload_test.go b/internal/namespace/configreload_test.go index 9afd00b91d..214fc7d0f1 100644 --- a/internal/namespace/configreload_test.go +++ b/internal/namespace/configreload_test.go @@ -413,7 +413,7 @@ func mockInitConfig(nmm *nmMocks) { nmm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil) nmm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return() nmm.mbi.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - nmm.mdx.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil) + nmm.mdx.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) nmm.mps.On("Init", mock.Anything, mock.Anything).Return(nil) nmm.mti[1].On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) nmm.mei[0].On("Init", mock.Anything, mock.Anything).Return(nil) diff --git a/internal/namespace/manager.go b/internal/namespace/manager.go index b19ed57029..962043c844 100644 --- a/internal/namespace/manager.go +++ b/internal/namespace/manager.go @@ -702,7 +702,7 @@ func (nm *namespaceManager) initPlugins(pluginsToStart map[string]*plugin) (err return err } case pluginCategoryDataexchange: - if err = p.dataexchange.Init(p.ctx, nm.cancelCtx /* allow plugin to stop whole process */, p.config); err != nil { + if err = p.dataexchange.Init(p.ctx, nm.cancelCtx /* allow plugin to stop whole process */, p.config, nm.metrics); err != nil { return err } case pluginCategorySharedstorage: diff --git a/internal/namespace/manager_test.go b/internal/namespace/manager_test.go index 9dce67d2b6..43c8d1515c 100644 --- a/internal/namespace/manager_test.go +++ b/internal/namespace/manager_test.go @@ -273,7 +273,7 @@ func newTestNamespaceManager(t *testing.T, initConfig bool) (*namespaceManager, nmm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil).Once() nmm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return().Once() nmm.mbi.On("Init", mock.Anything, mock.Anything, mock.Anything, nmm.mmi, mock.Anything).Return(nil).Once() - nmm.mdx.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + nmm.mdx.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() nmm.mps.On("Init", mock.Anything, mock.Anything).Return(nil).Once() nmm.mti[0].On("Init", mock.Anything, mock.Anything, "erc721", mock.Anything).Return(nil).Once() nmm.mti[1].On("Init", mock.Anything, mock.Anything, "erc1155", mock.Anything).Return(nil).Once() @@ -363,7 +363,7 @@ func TestInitDataExchangeFail(t *testing.T) { nm, nmm, cleanup := newTestNamespaceManager(t, true) defer cleanup() - nmm.mdx.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + nmm.mdx.On("Init", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) err := nm.initPlugins(map[string]*plugin{ "ffdx": nm.plugins["ffdx"], diff --git a/internal/networkmap/check_node.go b/internal/networkmap/check_node.go new file mode 100644 index 0000000000..fc1bdbe54e --- /dev/null +++ b/internal/networkmap/check_node.go @@ -0,0 +1,30 @@ +// Copyright © 2025 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 networkmap + +import ( + "context" +) + +func (nm *networkMap) CheckNodeIdentityStatus(ctx context.Context) error { + node, err := nm.identity.GetLocalNode(ctx) + if err != nil { + return err + } + + return nm.exchange.CheckNodeIdentityStatus(ctx, node) +} diff --git a/internal/networkmap/check_node_test.go b/internal/networkmap/check_node_test.go new file mode 100644 index 0000000000..3955b45f21 --- /dev/null +++ b/internal/networkmap/check_node_test.go @@ -0,0 +1,84 @@ +// Copyright © 2025 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 networkmap + +import ( + "context" + "errors" + "testing" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly/mocks/dataexchangemocks" + "github.com/hyperledger/firefly/mocks/identitymanagermocks" + "github.com/hyperledger/firefly/pkg/core" + "github.com/stretchr/testify/assert" +) + +func TestCheckNodeIdentityStatusSuccess(t *testing.T) { + nm, cancel := newTestNetworkmap(t) + defer cancel() + ctx := context.TODO() + + aProfile := fftypes.JSONAnyPtr(`{"cert": "a-cert" }`).JSONObject() + node := &core.Identity{ + IdentityProfile: core.IdentityProfile{ + Profile: aProfile, + }, + } + nm.identity.(*identitymanagermocks.Manager).On("GetLocalNode", ctx).Return(node, nil) + nm.exchange.(*dataexchangemocks.Plugin).On("CheckNodeIdentityStatus", ctx, node).Return(nil) + + err := nm.CheckNodeIdentityStatus(context.TODO()) + assert.NoError(t, err) + + nm.identity.(*identitymanagermocks.Manager).AssertExpectations(t) + nm.exchange.(*dataexchangemocks.Plugin).AssertExpectations(t) +} + +func TestCheckNodeIdentityStatusLocalNodeFails(t *testing.T) { + nm, cancel := newTestNetworkmap(t) + defer cancel() + ctx := context.TODO() + + nm.identity.(*identitymanagermocks.Manager).On("GetLocalNode", ctx).Return(&core.Identity{}, errors.New("local node not set")) + + err := nm.CheckNodeIdentityStatus(ctx) + assert.Error(t, err) + + nm.identity.(*identitymanagermocks.Manager).AssertExpectations(t) +} + +func TestCheckNodeIdentityStatusFails(t *testing.T) { + nm, cancel := newTestNetworkmap(t) + defer cancel() + ctx := context.TODO() + + aProfile := fftypes.JSONAnyPtr(`{"cert": "a-cert" }`).JSONObject() + node := &core.Identity{ + IdentityProfile: core.IdentityProfile{ + Profile: aProfile, + }, + } + nm.identity.(*identitymanagermocks.Manager).On("GetLocalNode", ctx).Return(node, nil) + nm.exchange.(*dataexchangemocks.Plugin).On("CheckNodeIdentityStatus", ctx, node).Return(errors.New("identity status check failed")) + + err := nm.CheckNodeIdentityStatus(context.TODO()) + assert.Error(t, err) + + nm.identity.(*identitymanagermocks.Manager).AssertExpectations(t) + nm.exchange.(*dataexchangemocks.Plugin).AssertExpectations(t) +} diff --git a/internal/networkmap/manager.go b/internal/networkmap/manager.go index bb45434704..364c0a6a07 100644 --- a/internal/networkmap/manager.go +++ b/internal/networkmap/manager.go @@ -37,6 +37,7 @@ type Manager interface { RegisterNodeOrganization(ctx context.Context, waitConfirm bool) (org *core.Identity, err error) RegisterIdentity(ctx context.Context, dto *core.IdentityCreateDTO, waitConfirm bool) (identity *core.Identity, err error) UpdateIdentity(ctx context.Context, id string, dto *core.IdentityUpdateDTO, waitConfirm bool) (identity *core.Identity, err error) + CheckNodeIdentityStatus(ctx context.Context) error GetOrganizationByNameOrID(ctx context.Context, nameOrID string) (*core.Identity, error) GetOrganizations(ctx context.Context, filter ffapi.AndFilter) ([]*core.Identity, *ffapi.FilterResult, error) diff --git a/internal/networkmap/register_node.go b/internal/networkmap/register_node.go index 3b5a0b6978..e7850ff403 100644 --- a/internal/networkmap/register_node.go +++ b/internal/networkmap/register_node.go @@ -25,7 +25,6 @@ import ( ) func (nm *networkMap) RegisterNode(ctx context.Context, waitConfirm bool) (identity *core.Identity, err error) { - nodeOwningOrg, err := nm.identity.GetRootOrg(ctx) if err != nil { return nil, err diff --git a/internal/networkmap/register_node_test.go b/internal/networkmap/register_node_test.go index 5f1e2fd1a9..4789833e2f 100644 --- a/internal/networkmap/register_node_test.go +++ b/internal/networkmap/register_node_test.go @@ -45,10 +45,11 @@ func TestRegisterNodeOk(t *testing.T) { mim.On("ResolveIdentitySigner", nm.ctx, parentOrg).Return(signerRef, nil) mdx := nm.exchange.(*dataexchangemocks.Plugin) - mdx.On("GetEndpointInfo", nm.ctx, "node1").Return(fftypes.JSONObject{ + dxPeer := fftypes.JSONObject{ "id": "peer1", "endpoint": "details", - }, nil) + } + mdx.On("GetEndpointInfo", nm.ctx, "node1").Return(dxPeer, nil) mds := nm.defsender.(*definitionsmocks.Sender) mds.On("ClaimIdentity", nm.ctx, @@ -124,5 +125,4 @@ func TestRegisterNodeGetOwnerFail(t *testing.T) { _, err := nm.RegisterNode(nm.ctx, false) assert.Regexp(t, "pop", err) - } diff --git a/internal/networkmap/update_identity.go b/internal/networkmap/update_identity.go index d675c8e75b..af1f7c9df0 100644 --- a/internal/networkmap/update_identity.go +++ b/internal/networkmap/update_identity.go @@ -44,6 +44,11 @@ func (nm *networkMap) updateIdentityID(ctx context.Context, id *fftypes.UUID, dt return nil, i18n.NewError(ctx, coremsgs.Msg404NoResult) } + // TODO is this right ? code below assumes this is true and errors otherwise + if dto.IdentityProfile.Profile == nil { + return nil, i18n.NewError(ctx, coremsgs.MsgInvalidIdentityPatch) + } + // We can't sparse merge the generic JSON fields, but we need to propagate the ID if dto.IdentityProfile.Profile.GetString("id") == "" { existingID := identity.IdentityProfile.Profile.GetString("id") diff --git a/internal/networkmap/update_identity_test.go b/internal/networkmap/update_identity_test.go index 08507082b7..c19977a6e2 100644 --- a/internal/networkmap/update_identity_test.go +++ b/internal/networkmap/update_identity_test.go @@ -268,3 +268,16 @@ func TestUpdateIdentityProfileOverwrite(t *testing.T) { mim.AssertExpectations(t) mds.AssertExpectations(t) } + +func TestUpdateIdentityProfileNotProvided(t *testing.T) { + nm, cancel := newTestNetworkmap(t) + defer cancel() + + identity := testOrg("org1") + + mim := nm.identity.(*identitymanagermocks.Manager) + mim.On("CachedIdentityLookupByID", nm.ctx, identity.ID).Return(identity, nil) + + _, err := nm.UpdateIdentity(nm.ctx, identity.ID.String(), &core.IdentityUpdateDTO{}, true) + assert.Regexp(t, "FF10480", err) +} diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go index 5fc3c3df79..e3f1944bee 100644 --- a/internal/orchestrator/bound_callbacks.go +++ b/internal/orchestrator/bound_callbacks.go @@ -20,6 +20,8 @@ import ( "context" "sync" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly/internal/coremsgs" @@ -93,3 +95,13 @@ func (bc *boundCallbacks) TokensApproved(plugin tokens.Plugin, approval *tokens. } return bc.o.events.TokensApproved(plugin, approval) } + +func (bc *boundCallbacks) DXConnect(plugin dataexchange.Plugin) { + err := bc.checkStopped() + if err == nil { + err = bc.o.networkmap.CheckNodeIdentityStatus(bc.o.ctx) + } + if err != nil { + log.L(bc.o.ctx).Errorf("Error handling DX connect callback: %s", err) + } +} diff --git a/internal/orchestrator/bound_callbacks_test.go b/internal/orchestrator/bound_callbacks_test.go index 06bd633831..2275ca4da9 100644 --- a/internal/orchestrator/bound_callbacks_test.go +++ b/internal/orchestrator/bound_callbacks_test.go @@ -21,6 +21,8 @@ import ( "fmt" "testing" + "github.com/hyperledger/firefly/mocks/networkmapmocks" + "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly/mocks/dataexchangemocks" "github.com/hyperledger/firefly/mocks/eventmocks" @@ -34,17 +36,19 @@ import ( "github.com/stretchr/testify/mock" ) -func newTestBoundCallbacks(t *testing.T) (*eventmocks.EventManager, *sharedstoragemocks.Plugin, *operationmocks.Manager, *boundCallbacks) { +func newTestBoundCallbacks(t *testing.T) (*eventmocks.EventManager, *sharedstoragemocks.Plugin, *operationmocks.Manager, *networkmapmocks.Manager, *boundCallbacks) { mei := &eventmocks.EventManager{} mss := &sharedstoragemocks.Plugin{} mom := &operationmocks.Manager{} + mnm := &networkmapmocks.Manager{} bc := &boundCallbacks{ o: &orchestrator{ - ctx: context.Background(), - namespace: &core.Namespace{Name: "ns1"}, - started: true, - events: mei, + ctx: context.Background(), + namespace: &core.Namespace{Name: "ns1"}, + networkmap: mnm, + started: true, + events: mei, plugins: &Plugins{ SharedStorage: SharedStoragePlugin{ Plugin: mss, @@ -53,12 +57,12 @@ func newTestBoundCallbacks(t *testing.T) (*eventmocks.EventManager, *sharedstora operations: mom, }, } - return mei, mss, mom, bc + return mei, mss, mom, mnm, bc } func TestBoundCallbacks(t *testing.T) { - mei, mss, mom, bc := newTestBoundCallbacks(t) + mei, mss, mom, mnm, bc := newTestBoundCallbacks(t) mdx := &dataexchangemocks.Plugin{} mti := &tokenmocks.Plugin{} @@ -94,6 +98,10 @@ func TestBoundCallbacks(t *testing.T) { err = bc.DXEvent(mdx, &dataexchangemocks.DXEvent{}) assert.NoError(t, err) + mnm.On("CheckNodeIdentityStatus", mock.Anything, mock.Anything).Return(nil) + bc.DXConnect(mdx) + assert.NoError(t, err) + mei.On("TokenPoolCreated", mock.Anything, mti, &tokens.TokenPool{}).Return(nil) err = bc.TokenPoolCreated(context.Background(), mti, &tokens.TokenPool{}) assert.NoError(t, err) @@ -109,11 +117,12 @@ func TestBoundCallbacks(t *testing.T) { mei.AssertExpectations(t) mss.AssertExpectations(t) mom.AssertExpectations(t) + mnm.AssertExpectations(t) } func TestBoundCallbacksStopped(t *testing.T) { - _, _, _, bc := newTestBoundCallbacks(t) + _, _, _, _, bc := newTestBoundCallbacks(t) bc.o.started = false _, err := bc.SharedStorageBatchDownloaded("payload1", []byte(`{}`)) @@ -128,6 +137,9 @@ func TestBoundCallbacksStopped(t *testing.T) { err = bc.DXEvent(nil, &dataexchangemocks.DXEvent{}) assert.Regexp(t, "FF10446", err) + bc.DXConnect(nil) + // no-op + err = bc.TokenPoolCreated(context.Background(), nil, &tokens.TokenPool{}) assert.Regexp(t, "FF10446", err) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 5909aeaa87..40d6e0a85c 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -304,6 +304,13 @@ func (or *orchestrator) Start() (err error) { } or.started = true + + if or.config.Multiparty.Enabled { + err := or.networkmap.CheckNodeIdentityStatus(or.ctx) + if err != nil { + log.L(or.ctx).Errorf("Error checking node identity status: %s", err.Error()) + } + } return err } diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 6c2cc85e67..1a328a0ecf 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -459,6 +459,7 @@ func TestStartBatchFail(t *testing.T) { defer or.cleanup(t) or.mdm.On("Start").Return(nil) or.mba.On("Start").Return(fmt.Errorf("pop")) + or.mnm.On("CheckNodeIdentityStatus", or.ctx).Return(nil) err := or.Start() assert.EqualError(t, err, "pop") } @@ -482,6 +483,7 @@ func TestStartStopOk(t *testing.T) { or.mem.On("Start").Return(nil) or.mbm.On("Start").Return(nil) or.msd.On("Start").Return(nil) + or.mnm.On("CheckNodeIdentityStatus", or.ctx).Return(nil) or.mom.On("Start").Return(nil) or.mtw.On("Start").Return() or.mam.On("Start").Return(nil) @@ -505,6 +507,7 @@ func TestStartStopOk(t *testing.T) { or.mem.On("Start").Return(nil) or.mbm.On("Start").Return(nil) or.msd.On("Start").Return(nil) + or.mnm.On("CheckNodeIdentityStatus", or.ctx).Return(errors.New("benign error")) or.mom.On("Start").Return(nil) or.mtw.On("Start").Return() or.mam.On("Start").Return(nil) diff --git a/internal/orchestrator/subscriptions.go b/internal/orchestrator/subscriptions.go index d7ebe97435..c0f9dadae7 100644 --- a/internal/orchestrator/subscriptions.go +++ b/internal/orchestrator/subscriptions.go @@ -180,9 +180,14 @@ func (or *orchestrator) GetSubscriptionEventsHistorical(ctx context.Context, sub return nil, nil, err } + intLimit := len(filteredEvents) + if requestedFiltering.Limit < uint64(intLimit) { + //nolint:gosec + intLimit = int(requestedFiltering.Limit) + } var filteredEventsMatchingSubscription []*core.EnrichedEvent - if len(filteredEvents) > int(requestedFiltering.Limit) { - filteredEventsMatchingSubscription = filteredEvents[len(filteredEvents)-int(requestedFiltering.Limit):] + if len(filteredEvents) > intLimit { + filteredEventsMatchingSubscription = filteredEvents[len(filteredEvents)-intLimit:] } else { filteredEventsMatchingSubscription = filteredEvents } diff --git a/internal/privatemessaging/operations.go b/internal/privatemessaging/operations.go index 5a6ef8c0f4..8e02609c71 100644 --- a/internal/privatemessaging/operations.go +++ b/internal/privatemessaging/operations.go @@ -130,6 +130,7 @@ func (pm *privateMessaging) PrepareOperation(ctx context.Context, op *core.Opera return nil, err } transport := &core.TransportWrapper{Group: group, Batch: batch} + pm.prepareBatchForNetworkTransport(ctx, transport) return opSendBatch(op, node, transport), nil default: diff --git a/internal/privatemessaging/operations_test.go b/internal/privatemessaging/operations_test.go index 8b1ad8df84..1cac94f071 100644 --- a/internal/privatemessaging/operations_test.go +++ b/internal/privatemessaging/operations_test.go @@ -149,6 +149,7 @@ func TestPrepareAndRunBatchSend(t *testing.T) { assert.Equal(t, node, po.Data.(batchSendData).Node) assert.Equal(t, group, po.Data.(batchSendData).Transport.Group) assert.Equal(t, batch, po.Data.(batchSendData).Transport.Batch) + assert.Equal(t, "ns1-remote", po.Data.(batchSendData).Transport.Batch.Namespace) // ensure its set to the network name not the local namespace name _, phase, err := pm.RunOperation(context.Background(), po) diff --git a/internal/privatemessaging/privatemessaging.go b/internal/privatemessaging/privatemessaging.go index d38d163900..a5f0e9630b 100644 --- a/internal/privatemessaging/privatemessaging.go +++ b/internal/privatemessaging/privatemessaging.go @@ -130,7 +130,7 @@ func NewPrivateMessaging(ctx context.Context, ns *core.Namespace, di database.Pl bo := batch.DispatcherOptions{ BatchType: core.BatchTypePrivate, - BatchMaxSize: config.GetUint(coreconfig.PrivateMessagingBatchSize), + BatchMaxSize: config.GetInt(coreconfig.PrivateMessagingBatchSize), BatchMaxBytes: pm.maxBatchPayloadLength, BatchTimeout: config.GetDuration(coreconfig.PrivateMessagingBatchTimeout), DisposeTimeout: config.GetDuration(coreconfig.PrivateMessagingBatchAgentTimeout), @@ -179,12 +179,20 @@ func (pm *privateMessaging) dispatchUnpinnedBatch(ctx context.Context, payload * return pm.dispatchBatchCommon(ctx, payload) } +// prepareBatchForNetworkTransport is mainly for documentation purposes, we need to "normalize" a batch before sending +// it over the network to other parties, so that all nodes see the same batch, regardless of what they call their local +// namespace. This is used when we dispatch a batch per the regular messaging flow, and when we retry a private messaging +// send operation. +func (pm *privateMessaging) prepareBatchForNetworkTransport(ctx context.Context, tw *core.TransportWrapper) { + tw.Batch.Namespace = pm.namespace.NetworkName +} + func (pm *privateMessaging) dispatchBatchCommon(ctx context.Context, payload *batch.DispatchPayload) error { batch := payload.Batch.GenInflight(payload.Messages, payload.Data) - batch.Namespace = pm.namespace.NetworkName tw := &core.TransportWrapper{ Batch: batch, } + pm.prepareBatchForNetworkTransport(ctx, tw) // Retrieve the group group, nodes, err := pm.groupManager.getGroupNodes(ctx, batch.Group, false /* fail if not found */) diff --git a/internal/privatemessaging/privatemessaging_test.go b/internal/privatemessaging/privatemessaging_test.go index dab7daccd7..77cc3a92b3 100644 --- a/internal/privatemessaging/privatemessaging_test.go +++ b/internal/privatemessaging/privatemessaging_test.go @@ -84,7 +84,7 @@ func newTestPrivateMessagingCommon(t *testing.T, metricsEnabled bool) (*privateM mmi.On("IsMetricsEnabled").Return(metricsEnabled) mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) - ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} + ns := &core.Namespace{Name: "ns1", NetworkName: "ns1-remote"} pm, err := NewPrivateMessaging(ctx, ns, mdi, mdx, mbi, mim, mba, mdm, msa, mmp, mmi, mom, cmi) assert.NoError(t, err) cmi.AssertCalled(t, "GetCache", cache.NewCacheConfig( diff --git a/internal/privatemessaging/recipients_test.go b/internal/privatemessaging/recipients_test.go index d8d97af30d..664ed6a93d 100644 --- a/internal/privatemessaging/recipients_test.go +++ b/internal/privatemessaging/recipients_test.go @@ -85,7 +85,7 @@ func TestResolveMemberListNewGroupE2E(t *testing.T) { um.RunFn = func(a mock.Arguments) { msg := a[1].(*core.Message) assert.Equal(t, core.MessageTypeGroupInit, msg.Header.Type) - assert.Equal(t, "ns1", msg.Header.Namespace) + assert.Equal(t, "ns1-remote", msg.Header.Namespace) // note this matches the remote network name, not the local namespace assert.Len(t, msg.Data, 1) assert.Equal(t, *dataID, *msg.Data[0].ID) } diff --git a/internal/reference/reference.go b/internal/reference/reference.go index 699c6aab1b..1befee9cd3 100644 --- a/internal/reference/reference.go +++ b/internal/reference/reference.go @@ -50,7 +50,7 @@ type TypeReferenceDoc struct { func GenerateObjectsReferenceMarkdown(ctx context.Context) (map[string][]byte, error) { newest := core.SubOptsFirstEventNewest - fifty := uint16(50) + fifty := uint(50) falseVar := false types := []interface{}{ @@ -790,14 +790,14 @@ func generateObjectReferenceMarkdown(ctx context.Context, descRequired bool, exa if typeReferenceDoc.Description != nil { buff.Write(typeReferenceDoc.Description) } - if typeReferenceDoc.Example != nil && len(typeReferenceDoc.Example) > 0 { + if len(typeReferenceDoc.Example) > 0 { if sectionCount > 1 { buff.WriteString("### Example\n\n```json\n") } buff.Write(typeReferenceDoc.Example) buff.WriteString("\n```\n\n") } - if typeReferenceDoc.FieldDescriptions != nil && len(typeReferenceDoc.FieldDescriptions) > 0 { + if len(typeReferenceDoc.FieldDescriptions) > 0 { if sectionCount > 1 { buff.WriteString("### Field Descriptions\n\n") } @@ -805,7 +805,7 @@ func generateObjectReferenceMarkdown(ctx context.Context, descRequired bool, exa buff.WriteString("\n") } - if typeReferenceDoc.SubFieldTables != nil && len(typeReferenceDoc.SubFieldTables) > 0 { + if len(typeReferenceDoc.SubFieldTables) > 0 { buff.Write(typeReferenceDoc.SubFieldTables) } diff --git a/internal/syncasync/sync_async_bridge.go b/internal/syncasync/sync_async_bridge.go index 6328ab9c25..6b7f31b20c 100644 --- a/internal/syncasync/sync_async_bridge.go +++ b/internal/syncasync/sync_async_bridge.go @@ -19,7 +19,7 @@ package syncasync import ( "context" "encoding/json" - "fmt" + "errors" "sync" "time" @@ -615,7 +615,7 @@ func (sa *syncAsyncBridge) resolveSuccessfulOperation(inflight *inflightRequest, func (sa *syncAsyncBridge) resolveFailedOperation(inflight *inflightRequest, typeName string, op *core.Operation) { log.L(sa.ctx).Debugf("Resolving %s request '%s' with error '%s'", typeName, inflight.id, op.Error) - inflight.response <- inflightResponse{err: fmt.Errorf(op.Error)} + inflight.response <- inflightResponse{err: errors.New(op.Error)} } func (sa *syncAsyncBridge) sendAndWait(ctx context.Context, ns string, id *fftypes.UUID, reqType requestType, send SendFunction) (interface{}, error) { diff --git a/mocks/dataexchangemocks/callbacks.go b/mocks/dataexchangemocks/callbacks.go index e39b409a0d..e6c18462bf 100644 --- a/mocks/dataexchangemocks/callbacks.go +++ b/mocks/dataexchangemocks/callbacks.go @@ -12,6 +12,11 @@ type Callbacks struct { mock.Mock } +// DXConnect provides a mock function with given fields: plugin +func (_m *Callbacks) DXConnect(plugin dataexchange.Plugin) { + _m.Called(plugin) +} + // DXEvent provides a mock function with given fields: plugin, event func (_m *Callbacks) DXEvent(plugin dataexchange.Plugin, event dataexchange.DXEvent) error { ret := _m.Called(plugin, event) diff --git a/mocks/dataexchangemocks/plugin.go b/mocks/dataexchangemocks/plugin.go index ecd545651e..9374cef632 100644 --- a/mocks/dataexchangemocks/plugin.go +++ b/mocks/dataexchangemocks/plugin.go @@ -15,6 +15,8 @@ import ( io "io" + metrics "github.com/hyperledger/firefly/internal/metrics" + mock "github.com/stretchr/testify/mock" ) @@ -61,6 +63,24 @@ func (_m *Plugin) Capabilities() *dataexchange.Capabilities { return r0 } +// CheckNodeIdentityStatus provides a mock function with given fields: ctx, node +func (_m *Plugin) CheckNodeIdentityStatus(ctx context.Context, node *core.Identity) error { + ret := _m.Called(ctx, node) + + if len(ret) == 0 { + panic("no return value specified for CheckNodeIdentityStatus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *core.Identity) error); ok { + r0 = rf(ctx, node) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteBlob provides a mock function with given fields: ctx, payloadRef func (_m *Plugin) DeleteBlob(ctx context.Context, payloadRef string) error { ret := _m.Called(ctx, payloadRef) @@ -157,17 +177,17 @@ func (_m *Plugin) GetPeerID(peer fftypes.JSONObject) string { return r0 } -// Init provides a mock function with given fields: ctx, cancelCtx, _a2 -func (_m *Plugin) Init(ctx context.Context, cancelCtx context.CancelFunc, _a2 config.Section) error { - ret := _m.Called(ctx, cancelCtx, _a2) +// Init provides a mock function with given fields: ctx, cancelCtx, _a2, _a3 +func (_m *Plugin) Init(ctx context.Context, cancelCtx context.CancelFunc, _a2 config.Section, _a3 metrics.Manager) error { + ret := _m.Called(ctx, cancelCtx, _a2, _a3) if len(ret) == 0 { panic("no return value specified for Init") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, context.CancelFunc, config.Section) error); ok { - r0 = rf(ctx, cancelCtx, _a2) + if rf, ok := ret.Get(0).(func(context.Context, context.CancelFunc, config.Section, metrics.Manager) error); ok { + r0 = rf(ctx, cancelCtx, _a2, _a3) } else { r0 = ret.Error(0) } diff --git a/mocks/identitymanagermocks/manager.go b/mocks/identitymanagermocks/manager.go index 51b648ea92..7fe92951cb 100644 --- a/mocks/identitymanagermocks/manager.go +++ b/mocks/identitymanagermocks/manager.go @@ -183,6 +183,34 @@ func (_m *Manager) GetLocalNode(ctx context.Context) (*core.Identity, error) { return r0, r1 } +// GetLocalNodeDID provides a mock function with given fields: ctx +func (_m *Manager) GetLocalNodeDID(ctx context.Context) (string, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetLocalNodeDID") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (string, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) string); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetRootOrg provides a mock function with given fields: ctx func (_m *Manager) GetRootOrg(ctx context.Context) (*core.Identity, error) { ret := _m.Called(ctx) diff --git a/mocks/metricsmocks/manager.go b/mocks/metricsmocks/manager.go index 33a5e7bd78..62d1da5825 100644 --- a/mocks/metricsmocks/manager.go +++ b/mocks/metricsmocks/manager.go @@ -6,6 +6,8 @@ import ( fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" core "github.com/hyperledger/firefly/pkg/core" + metrics "github.com/hyperledger/firefly/internal/metrics" + mock "github.com/stretchr/testify/mock" time "time" @@ -41,9 +43,9 @@ func (_m *Manager) BlockchainTransaction(location string, methodName string) { _m.Called(location, methodName) } -// CountBatchPin provides a mock function with given fields: -func (_m *Manager) CountBatchPin() { - _m.Called() +// CountBatchPin provides a mock function with given fields: namespace +func (_m *Manager) CountBatchPin(namespace string) { + _m.Called(namespace) } // DeleteTime provides a mock function with given fields: id @@ -97,6 +99,16 @@ func (_m *Manager) MessageSubmitted(msg *core.Message) { _m.Called(msg) } +// NodeIdentityDXCertExpiry provides a mock function with given fields: namespace, expiry +func (_m *Manager) NodeIdentityDXCertExpiry(namespace string, expiry time.Time) { + _m.Called(namespace, expiry) +} + +// NodeIdentityDXCertMismatch provides a mock function with given fields: namespace, mismatch +func (_m *Manager) NodeIdentityDXCertMismatch(namespace string, mismatch metrics.NodeIdentityDXCertMismatchStatus) { + _m.Called(namespace, mismatch) +} + // TransferConfirmed provides a mock function with given fields: transfer func (_m *Manager) TransferConfirmed(transfer *core.TokenTransfer) { _m.Called(transfer) diff --git a/mocks/networkmapmocks/manager.go b/mocks/networkmapmocks/manager.go index 2751287852..9123bced05 100644 --- a/mocks/networkmapmocks/manager.go +++ b/mocks/networkmapmocks/manager.go @@ -18,6 +18,24 @@ type Manager struct { mock.Mock } +// CheckNodeIdentityStatus provides a mock function with given fields: ctx +func (_m *Manager) CheckNodeIdentityStatus(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CheckNodeIdentityStatus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetDIDDocForIndentityByDID provides a mock function with given fields: ctx, did func (_m *Manager) GetDIDDocForIndentityByDID(ctx context.Context, did string) (*networkmap.DIDDocument, error) { ret := _m.Called(ctx, did) diff --git a/pkg/core/subscription.go b/pkg/core/subscription.go index e9e789d1dc..bf9c4845c6 100644 --- a/pkg/core/subscription.go +++ b/pkg/core/subscription.go @@ -91,7 +91,7 @@ const ( // REMEMBER TO ADD OPTIONS HERE TO MarshalJSON() type SubscriptionCoreOptions struct { FirstEvent *SubOptsFirstEvent `ffstruct:"SubscriptionCoreOptions" json:"firstEvent,omitempty"` - ReadAhead *uint16 `ffstruct:"SubscriptionCoreOptions" json:"readAhead,omitempty"` + ReadAhead *uint `ffstruct:"SubscriptionCoreOptions" json:"readAhead,omitempty"` WithData *bool `ffstruct:"SubscriptionCoreOptions" json:"withData,omitempty"` Batch *bool `ffstruct:"SubscriptionCoreOptions" json:"batch,omitempty"` BatchTimeout *string `ffstruct:"SubscriptionCoreOptions" json:"batchTimeout,omitempty"` diff --git a/pkg/core/subscription_test.go b/pkg/core/subscription_test.go index 45d1ebf00c..2cae4ffddb 100644 --- a/pkg/core/subscription_test.go +++ b/pkg/core/subscription_test.go @@ -26,7 +26,7 @@ import ( func TestSubscriptionOptionsDatabaseSerialization(t *testing.T) { firstEvent := SubOptsFirstEventNewest - readAhead := uint16(50) + readAhead := uint(50) yes := true oneSec := "1s" sub1 := &Subscription{ @@ -77,7 +77,7 @@ func TestSubscriptionOptionsDatabaseSerialization(t *testing.T) { b2, err := sub1.Options.Value() assert.NoError(t, err) assert.Equal(t, SubOptsFirstEventNewest, *sub2.Options.FirstEvent) - assert.Equal(t, uint16(50), *sub2.Options.ReadAhead) + assert.Equal(t, uint(50), *sub2.Options.ReadAhead) assert.Equal(t, "myconfig", sub2.Options.TLSConfigName) assert.Equal(t, string(b1.([]byte)), string(b2.([]byte))) diff --git a/pkg/dataexchange/plugin.go b/pkg/dataexchange/plugin.go index c6d5a407e1..a481f43c80 100644 --- a/pkg/dataexchange/plugin.go +++ b/pkg/dataexchange/plugin.go @@ -20,6 +20,8 @@ import ( "context" "io" + "github.com/hyperledger/firefly/internal/metrics" + "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly/pkg/core" @@ -60,7 +62,7 @@ type Plugin interface { InitConfig(config config.Section) // Init initializes the plugin, with configuration - Init(ctx context.Context, cancelCtx context.CancelFunc, config config.Section) error + Init(ctx context.Context, cancelCtx context.CancelFunc, config config.Section, metrics metrics.Manager) error // SetHandler registers a handler to receive callbacks // Plugin will attempt (but is not guaranteed) to deliver events only for the given namespace and node @@ -101,10 +103,14 @@ type Plugin interface { // GetPeerID extracts the peer ID from the peer JSON GetPeerID(peer fftypes.JSONObject) string + + // CheckNodeIdentityStatus checks the status of the local node's network identity relative to the DX plugin's config + CheckNodeIdentityStatus(ctx context.Context, node *core.Identity) error } // Callbacks is the interface provided to the data exchange plugin, to allow it to pass events back to firefly. type Callbacks interface { + DXConnect(plugin Plugin) // Event has sub-types as defined below, and can be processed and ack'd asynchronously DXEvent(plugin Plugin, event DXEvent) error }