diff --git a/.github/workflows/build_external.yml b/.github/workflows/build_external.yml index 6a5d5c56e..9aa0a3637 100644 --- a/.github/workflows/build_external.yml +++ b/.github/workflows/build_external.yml @@ -31,14 +31,17 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - name: Build the image - uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/build-image@v2.0.21 + uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/build-image@912bed7e07a1df4d06ea53a031e9773bb65dc7bd # v2.3.0 env: GH_TOKEN: ${{ github.token }} with: push_tag: "" cl_repo: smartcontractkit/chainlink cl_ref: develop - dep_relay_sha: ${{ github.event.pull_request.head.sha }} + dep_common_sha: ${{ github.event.pull_request.head.sha }} + should_checkout: true + QA_AWS_REGION: "" + QA_AWS_ROLE_TO_ASSUME: "" solana-build-relay: environment: integration permissions: diff --git a/Makefile b/Makefile index ec6a87616..c851d26a3 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ godoc: PHONY: install-protoc install-protoc: - script/install-protoc.sh 24.2 / + script/install-protoc.sh 25.1 / go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.31; go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 .PHONY: mockery @@ -26,4 +26,4 @@ generate: mockery install-protoc .PHONY: golangci-lint golangci-lint: ## Run golangci-lint for all issues. [ -d "./golangci-lint" ] || mkdir ./golangci-lint && \ - docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:v1.55.2 golangci-lint run --max-issues-per-linter 0 --max-same-issues 0 > ./golangci-lint/$(shell date +%Y-%m-%d_%H:%M:%S).txt \ No newline at end of file + docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:v1.55.2 golangci-lint run --max-issues-per-linter 0 --max-same-issues 0 > ./golangci-lint/$(shell date +%Y-%m-%d_%H:%M:%S).txt diff --git a/go.mod b/go.mod index 2f3c37658..5f51b2b86 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/smartcontractkit/chainlink-common go 1.21 require ( - github.com/confluentinc/confluent-kafka-go v1.9.2 + github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 github.com/fxamacker/cbor/v2 v2.5.0 github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 github.com/google/uuid v1.3.1 @@ -58,7 +58,7 @@ require ( github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.1.1 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect diff --git a/go.sum b/go.sum index 61c7a2eba..19224b433 100644 --- a/go.sum +++ b/go.sum @@ -36,12 +36,14 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA= -github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0RsT/ee8YIgGY/xpEQgQ= -github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/hcsshim v0.9.4 h1:mnUj0ivWy6UzbB1uLFqKR6F+ZyiDc7j4iGgHTpO+5+I= +github.com/Microsoft/hcsshim v0.9.4/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= @@ -49,7 +51,6 @@ github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -58,36 +59,37 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/confluentinc/confluent-kafka-go v1.9.2 h1:gV/GxhMBUb03tFWkN+7kdhg+zf+QUM+wVkI9zwh770Q= -github.com/confluentinc/confluent-kafka-go v1.9.2/go.mod h1:ptXNqsuDfYbAE/LBW6pnwWZElUoWxHoV8E43DCrliyo= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8= +github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= +github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= +github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= +github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 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/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= -github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= -github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= -github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -100,12 +102,16 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -126,21 +132,17 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -148,10 +150,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -161,10 +161,8 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20211008130755-947d60d73cc0/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -173,41 +171,25 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0 h1:f4tg github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0/go.mod h1:hKAkSgNkL0FII46ZkJcpVEAai4KV+swlIWCKfekd1pA= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3 h1:o95KDiV/b1xdkumY5YbLR0/n2+wBxUpgf3HgfKgTyLI= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3/go.mod h1:hTxjzRcX49ogbTGVJ1sM5mz5s+SSgiGIyL3jjPxl32E= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= -github.com/hamba/avro v1.5.6/go.mod h1:3vNT0RLXXpFm2Tb/5KC71ZRJlOroggq1Rcitb6k4Fr8= 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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/heetch/avro v0.3.1/go.mod h1:4xn38Oz/+hiEUTpbVfGVLfvOg0yKLlRP7Q9+gJJILgA= -github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/invopop/jsonschema v0.4.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= -github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= -github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= -github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= -github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= -github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 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.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -216,13 +198,11 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/linkedin/goavro v2.1.0+incompatible/go.mod h1:bBCwI2eGYpUI/4820s67MElg9tdeLbINjLjiM2xZFYM= github.com/linkedin/goavro/v2 v2.9.7/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= -github.com/linkedin/goavro/v2 v2.10.0/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= -github.com/linkedin/goavro/v2 v2.10.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= -github.com/linkedin/goavro/v2 v2.11.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= github.com/linkedin/goavro/v2 v2.12.0 h1:rIQQSj8jdAUlKQh6DttK8wCRv4t4QO09g1C4aBWXslg= github.com/linkedin/goavro/v2 v2.12.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -237,18 +217,26 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= +github.com/moby/sys/mount v0.3.3/go.mod h1:PBaEorSNTLG5t/+4EgukEQVlAvVEc6ZjTySwKdqp5K0= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/nolag/mapstructure v1.5.1 h1:jEMB2AM8NXEosSMTPXlbycOpBsqttEBh5owT0gJs9/I= github.com/nolag/mapstructure v1.5.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/nrwiersma/avro-benchmarks v0.0.0-20210913175520-21aec48c8f76/go.mod h1:iKyFMidsk/sVYONJRE372sJuX/QTRPacU7imPqqsu7g= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= +github.com/opencontainers/runc v1.1.3/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -265,18 +253,16 @@ github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwa github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/riferrei/srclient v0.5.4 h1:dfwyR5u23QF7beuVl2WemUY2KXh5+Sc4DHKyPXBNYuc= github.com/riferrei/srclient v0.5.4/go.mod h1:vbkLmWcgYa7JgfPvuy/+K8fTS0p1bApqadxrxi/S1MI= -github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= -github.com/santhosh-tekuri/jsonschema/v5 v5.1.1 h1:lEOLY2vyGIqKWUI9nzsOJRV3mb3WC9dXYORsLEUcoeY= -github.com/santhosh-tekuri/jsonschema/v5 v5.1.1/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= +github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 h1:WCcC4vZDS1tYNxjWlwRJZQy28r8CMoggKnxNzxsVDMQ= +github.com/santhosh-tekuri/jsonschema/v5 v5.2.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306 h1:ko88+ZznniNJZbZPWAvHQU8SwKAdHngdDZ+pvVgB5ss= github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f h1:hgJif132UCdjo8u43i7iPN1/MFnu49hv7lFGFftCHKU= @@ -288,7 +274,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -298,6 +283,8 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/testcontainers/testcontainers-go v0.14.0 h1:h0D5GaYG9mhOWr2qHdEKDXpkce/VlvaYOCzTRi6UBi8= +github.com/testcontainers/testcontainers-go v0.14.0/go.mod h1:hSRGJ1G8Q5Bw2gXgPulJOLlEBaYJHeBSOkQM5JLG+JQ= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -309,6 +296,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0 h1:RsQi0qJ2imFfCvZabqzM9cNXBG8k6gXMv1A0cXRmH6A= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0/go.mod h1:vsh3ySueQCiKPxFLvjWC4Z135gIa34TQ/NSqkDTZYUM= go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= @@ -323,7 +312,6 @@ go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJ go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= @@ -392,7 +380,6 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -402,7 +389,6 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= @@ -460,10 +446,8 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= @@ -515,7 +499,6 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -574,7 +557,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -583,7 +565,6 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0= google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= @@ -602,12 +583,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/grpc/examples v0.0.0-20210424002626-9572fd6faeae/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= @@ -623,27 +600,15 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/avro.v0 v0.0.0-20171217001914-a730b5802183/go.mod h1:FvqrFXt+jCsyQibeRv4xxEJBL5iG2DDW5aeJwzDiq4A= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/httprequest.v1 v1.2.1/go.mod h1:x2Otw96yda5+8+6ZeWwHIJTFkEHWP/qP8pJOzqEtWPM= -gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/codec/modifier_codec.go b/pkg/codec/modifier_codec.go index d4c228a4c..cfeac330c 100644 --- a/pkg/codec/modifier_codec.go +++ b/pkg/codec/modifier_codec.go @@ -11,7 +11,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types" ) -func NewModifierCodec(codec types.CodecTypeProvider, modifier Modifier, hooks ...mapstructure.DecodeHookFunc) (types.CodecTypeProvider, error) { +func NewModifierCodec(codec types.RemoteCodec, modifier Modifier, hooks ...mapstructure.DecodeHookFunc) (types.RemoteCodec, error) { if codec == nil || modifier == nil { return nil, errors.New("inputs must not be nil") } @@ -26,7 +26,7 @@ func NewModifierCodec(codec types.CodecTypeProvider, modifier Modifier, hooks .. var _ types.TypeProvider = &modifierCodec{} type modifierCodec struct { - codec types.CodecTypeProvider + codec types.RemoteCodec modifier Modifier hook mapstructure.DecodeHookFunc } diff --git a/pkg/codec/modifier_codec_test.go b/pkg/codec/modifier_codec_test.go index f92d6d780..57a27741f 100644 --- a/pkg/codec/modifier_codec_test.go +++ b/pkg/codec/modifier_codec_test.go @@ -32,52 +32,53 @@ func TestModifierCodec(t *testing.T) { require.NoError(t, err) t.Run("Nil codec returns error", func(t *testing.T) { - _, err := codec.NewModifierCodec(nil, testModifier{}) + _, err = codec.NewModifierCodec(nil, testModifier{}) assert.Error(t, err) }) t.Run("Nil modifier returns error", func(t *testing.T) { - _, err := codec.NewModifierCodec(&testCodec{}, nil) + _, err = codec.NewModifierCodec(&testCodec{}, nil) assert.Error(t, err) }) + var encoded []byte t.Run("Encode calls modifiers then encodes", func(t *testing.T) { - encoded, err := mod.Encode(ctx, &modifierCodecOffChainType{Z: anyValue}, anyItemType) + encoded, err = mod.Encode(ctx, &modifierCodecOffChainType{Z: anyValue}, anyItemType) require.NoError(t, err) assert.Equal(t, anyTestBytes, encoded) }) t.Run("Encode works on compatible types", func(t *testing.T) { - encoded, err := mod.Encode(ctx, ModifierCodecOffChainCompatibleType{Z: anyValue}, anyItemType) + encoded, err = mod.Encode(ctx, ModifierCodecOffChainCompatibleType{Z: anyValue}, anyItemType) require.NoError(t, err) assert.Equal(t, anyTestBytes, encoded) }) t.Run("Encode works on compatible squashed types", func(t *testing.T) { - encoded, err := mod.Encode(ctx, modifierCodecOffChainSquashCompatibleType{ModifierCodecOffChainCompatibleType{Z: anyValue}}, anyItemType) + encoded, err = mod.Encode(ctx, modifierCodecOffChainSquashCompatibleType{ModifierCodecOffChainCompatibleType{Z: anyValue}}, anyItemType) require.NoError(t, err) assert.Equal(t, anyTestBytes, encoded) }) t.Run("Encode works on slices", func(t *testing.T) { - encoded, err := mod.Encode(ctx, &[]modifierCodecOffChainType{{Z: anyValue}, {Z: anyValue + 1}}, anySliceItemType) + encoded, err = mod.Encode(ctx, &[]modifierCodecOffChainType{{Z: anyValue}, {Z: anyValue + 1}}, anySliceItemType) require.NoError(t, err) assert.Equal(t, anyTestBytes, encoded) }) t.Run("Encode works on slices without a pointer", func(t *testing.T) { - encoded, err := mod.Encode(ctx, []modifierCodecOffChainType{{Z: anyValue}, {Z: anyValue + 1}}, anyNonPointerSliceItemType) + encoded, err = mod.Encode(ctx, []modifierCodecOffChainType{{Z: anyValue}, {Z: anyValue + 1}}, anyNonPointerSliceItemType) require.NoError(t, err) assert.Equal(t, anyTestBytes, encoded) }) t.Run("Encode works on compatible slices", func(t *testing.T) { - encoded, err := mod.Encode(ctx, &[]ModifierCodecOffChainCompatibleType{{Z: anyValue}, {Z: anyValue + 1}}, anySliceItemType) + encoded, err = mod.Encode(ctx, &[]ModifierCodecOffChainCompatibleType{{Z: anyValue}, {Z: anyValue + 1}}, anySliceItemType) require.NoError(t, err) assert.Equal(t, anyTestBytes, encoded) @@ -138,8 +139,9 @@ func TestModifierCodec(t *testing.T) { assert.True(t, errors.Is(err, types.ErrInvalidType)) }) + var actual any t.Run("CreateType returns modified type", func(t *testing.T) { - actual, err := mod.(types.TypeProvider).CreateType(anyItemType, anyForEncoding) + actual, err = mod.(types.TypeProvider).CreateType(anyItemType, anyForEncoding) require.NoError(t, err) assert.Equal(t, reflect.TypeOf(&modifierCodecOffChainType{}), reflect.TypeOf(actual)) }) @@ -149,14 +151,15 @@ func TestModifierCodec(t *testing.T) { assert.Equal(t, types.ErrInvalidType, err) }) + var size int t.Run("GetMaxEncodingSize delegates", func(t *testing.T) { - size, err := mod.GetMaxEncodingSize(ctx, anyValue, anyItemType) + size, err = mod.GetMaxEncodingSize(ctx, anyValue, anyItemType) require.NoError(t, err) assert.Equal(t, anyMaxEncodingSize, size) }) t.Run("GetMaxDecodingSize delegates", func(t *testing.T) { - size, err := mod.GetMaxDecodingSize(ctx, anyValue, anyItemType) + size, err = mod.GetMaxDecodingSize(ctx, anyValue, anyItemType) require.NoError(t, err) assert.Equal(t, anyMaxDecodingSize, size) }) @@ -168,15 +171,17 @@ func TestModifierCodec(t *testing.T) { } return from.Interface(), nil } - mod, err := codec.NewModifierCodec(&testCodec{}, testModifier{}, hook) + + var hookMod types.RemoteCodec + hookMod, err = codec.NewModifierCodec(&testCodec{}, testModifier{}, hook) require.NoError(t, err) decoded := &modifierCodecDiffType{} - require.NoError(t, mod.Decode(ctx, anyTestBytes, decoded, anyItemType)) + require.NoError(t, hookMod.Decode(ctx, anyTestBytes, decoded, anyItemType)) assert.Equal(t, "5", decoded.Z) }) t.Run("encode works wil nil input", func(t *testing.T) { - actual, err := mod.Encode(ctx, nil, anyItemType) + actual, err = mod.Encode(ctx, nil, anyItemType) require.NoError(t, err) assert.Equal(t, anyNilBytes, actual) }) @@ -235,7 +240,7 @@ func (t *testCodec) GetMaxEncodingSize(_ context.Context, n int, itemType string } if n != anyValue { - return 0, types.ErrUnknown + return 0, types.ErrInvalidEncoding } return anyMaxEncodingSize, nil @@ -274,7 +279,7 @@ func (t *testCodec) GetMaxDecodingSize(_ context.Context, n int, itemType string } if n != anyValue { - return 0, types.ErrUnknown + return 0, types.ErrInvalidEncoding } return anyMaxDecodingSize, nil diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index b4af751ee..66bb77e63 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -70,6 +70,11 @@ func Test(tb testing.TB) Logger { return &logger{zaptest.NewLogger(tb).Sugar()} } +// TestSugared returns a new test SugaredLogger. +func TestSugared(tb testing.TB) SugaredLogger { + return Sugared(&logger{zaptest.NewLogger(tb).Sugar()}) +} + // TestObserved returns a new test Logger for tb and ObservedLogs at the given Level. func TestObserved(tb testing.TB, lvl zapcore.Level) (Logger, *observer.ObservedLogs) { sl, logs := testObserved(tb, lvl) diff --git a/pkg/logger/sugared.go b/pkg/logger/sugared.go index 909bb5736..2b57a1df2 100644 --- a/pkg/logger/sugared.go +++ b/pkg/logger/sugared.go @@ -4,7 +4,9 @@ package logger type SugaredLogger interface { Logger // AssumptionViolation variants log at error level with the message prefix "AssumptionViolation: ". + AssumptionViolation(args ...interface{}) AssumptionViolationf(format string, vals ...interface{}) + AssumptionViolationw(format string, vals ...interface{}) // ErrorIf logs the error if present. ErrorIf(err error, msg string) // ErrorIfFn calls fn() and logs any returned error along with msg. @@ -37,7 +39,17 @@ func (s *sugared) ErrorIfFn(fn func() error, msg string) { } } +// AssumptionViolation wraps Error logs with assumption violation tag. +func (s *sugared) AssumptionViolation(args ...interface{}) { + s.h.Error(append([]interface{}{"AssumptionViolation:"}, args...)) +} + // AssumptionViolationf wraps Errorf logs with assumption violation tag. func (s *sugared) AssumptionViolationf(format string, vals ...interface{}) { s.h.Errorf("AssumptionViolation: "+format, vals...) } + +// AssumptionViolationw wraps Errorw logs with assumption violation tag. +func (s *sugared) AssumptionViolationw(msg string, keyvals ...interface{}) { + s.h.Errorw("AssumptionViolation: "+msg, keyvals...) +} diff --git a/pkg/logger/trace.go b/pkg/logger/trace.go new file mode 100644 index 000000000..7dda4c3c1 --- /dev/null +++ b/pkg/logger/trace.go @@ -0,0 +1,53 @@ +//go:build trace + +package logger + +const tracePrefix = "[trace] " + +func Trace(l Logger, args ...interface{}) { + switch t := l.(type) { + case *logger: + t.DPanic(args...) + return + } + c, ok := l.(interface { + Trace(args ...interface{}) + }) + if ok { + c.Trace(args...) + return + } + l.Error(append([]any{tracePrefix}, args...)...) +} + +func Tracef(l Logger, format string, values ...interface{}) { + switch t := l.(type) { + case *logger: + t.DPanicf(format, values...) + return + } + c, ok := l.(interface { + Tracef(format string, values ...interface{}) + }) + if ok { + c.Tracef(format, values...) + return + } + l.Errorf(tracePrefix+format, values...) +} + +func Tracew(l Logger, msg string, keysAndValues ...interface{}) { + switch t := l.(type) { + case *logger: + t.DPanicw(msg, keysAndValues...) + return + } + c, ok := l.(interface { + Tracew(msg string, keysAndValues ...interface{}) + }) + if ok { + c.Tracew(msg, keysAndValues...) + return + } + l.Errorf(tracePrefix+msg, keysAndValues) +} diff --git a/pkg/logger/trace_noop.go b/pkg/logger/trace_noop.go new file mode 100644 index 000000000..7b05cd892 --- /dev/null +++ b/pkg/logger/trace_noop.go @@ -0,0 +1,9 @@ +//go:build !trace + +package logger + +func Trace(l Logger, args ...interface{}) {} + +func Tracef(l Logger, format string, values ...interface{}) {} + +func Tracew(l Logger, msg string, keysAndValues ...interface{}) {} diff --git a/pkg/loop/internal/chain_reader.go b/pkg/loop/internal/chain_reader.go index db30a97d8..2fc0fdf47 100644 --- a/pkg/loop/internal/chain_reader.go +++ b/pkg/loop/internal/chain_reader.go @@ -3,14 +3,11 @@ package internal import ( "context" jsonv1 "encoding/json" - "errors" "fmt" - "strings" jsonv2 "github.com/go-json-experiment/json" "github.com/fxamacker/cbor/v2" - "google.golang.org/grpc/status" "github.com/smartcontractkit/chainlink-common/pkg/loop/internal/pb" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -96,7 +93,7 @@ func (c *chainReaderClient) GetLatestValue(ctx context.Context, bc types.BoundCo reply, err := c.grpc.GetLatestValue(ctx, &pb.GetLatestValueRequest{Bc: &boundContract, Method: method, Params: versionedParams}) if err != nil { - return types.UnwrapClientError(err) + return wrapRPCErr(err) } return decodeVersionedBytes(retVal, reply.RetVal) @@ -148,34 +145,3 @@ func getEncodedType(itemType string, possibleTypeProvider any, forEncoding bool) return &map[string]any{}, nil } - -func unwrapClientError(err error) error { - if err == nil { - return nil - } - errTypes := []error{ - types.ErrInvalidType, - types.ErrFieldNotFound, - types.ErrInvalidEncoding, - types.ErrWrongNumberOfElements, - types.ErrNotASlice, - types.ErrUnknown, - } - - s, ok := status.FromError(err) - if !ok { - return fmt.Errorf("%w: %w", types.ErrUnknown, err) - } - - msg := s.Message() - for _, etype := range errTypes { - if msg == etype.Error() { - return etype - } else if strings.HasPrefix(msg, etype.Error()+":") { - rest := strings.SplitN(msg, ":", 2)[1] - return fmt.Errorf("%w: %w", etype, errors.New(rest)) - } - } - - return fmt.Errorf("%w: %w", types.ErrUnknown, err) -} diff --git a/pkg/loop/internal/chain_reader_test.go b/pkg/loop/internal/chain_reader_test.go index 9261c07c7..77468df3f 100644 --- a/pkg/loop/internal/chain_reader_test.go +++ b/pkg/loop/internal/chain_reader_test.go @@ -80,7 +80,7 @@ func TestChainReaderClient(t *testing.T) { t.Run("GetLatestValue unwraps errors from server "+errorType.Error(), func(t *testing.T) { err := client.GetLatestValue(ctx, types.BoundContract{}, "method", "anything", "anything") - assert.IsType(t, errorType, err) + assert.True(t, errors.Is(err, errorType)) }) } diff --git a/pkg/loop/internal/client.go b/pkg/loop/internal/client.go index ef6c237a1..d1b6c547c 100644 --- a/pkg/loop/internal/client.go +++ b/pkg/loop/internal/client.go @@ -2,6 +2,7 @@ package internal import ( "context" + "strings" "sync" "sync/atomic" "time" @@ -162,3 +163,24 @@ func isErrTerminal(err error) bool { } return false } + +func wrapRPCErr(err error) error { + if err == nil { + return nil + } + return &wrappedError{err: err, status: status.Convert(err)} +} + +type wrappedError struct { + err error + status *status.Status +} + +func (w wrappedError) Error() string { + return w.err.Error() +} + +func (w wrappedError) Is(target error) bool { + s := status.Convert(target) + return w.status.Code() == s.Code() && strings.Contains(s.Message(), w.status.Message()) +} diff --git a/pkg/loop/internal/codec.go b/pkg/loop/internal/codec.go index d65ac130a..57b344287 100644 --- a/pkg/loop/internal/codec.go +++ b/pkg/loop/internal/codec.go @@ -26,7 +26,7 @@ func (c *codecClient) Encode(ctx context.Context, item any, itemType string) ([] }) if err != nil { - return nil, unwrapClientError(err) + return nil, wrapRPCErr(err) } return reply.RetVal, nil @@ -39,7 +39,7 @@ func (c *codecClient) Decode(ctx context.Context, raw []byte, into any, itemType } resp, err := c.grpc.GetDecoding(ctx, request) if err != nil { - return unwrapClientError(err) + return wrapRPCErr(err) } return decodeVersionedBytes(into, resp.RetVal) @@ -48,7 +48,7 @@ func (c *codecClient) Decode(ctx context.Context, raw []byte, into any, itemType func (c *codecClient) GetMaxEncodingSize(ctx context.Context, n int, itemType string) (int, error) { res, err := c.grpc.GetMaxSize(ctx, &pb.GetMaxSizeRequest{N: int32(n), ItemType: itemType, ForEncoding: true}) if err != nil { - return 0, unwrapClientError(err) + return 0, wrapRPCErr(err) } return int(res.SizeInBytes), nil @@ -57,7 +57,7 @@ func (c *codecClient) GetMaxEncodingSize(ctx context.Context, n int, itemType st func (c *codecClient) GetMaxDecodingSize(ctx context.Context, n int, itemType string) (int, error) { res, err := c.grpc.GetMaxSize(ctx, &pb.GetMaxSizeRequest{N: int32(n), ItemType: itemType, ForEncoding: false}) if err != nil { - return 0, unwrapClientError(err) + return 0, wrapRPCErr(err) } return int(res.SizeInBytes), nil diff --git a/pkg/loop/internal/pb/chain_reader.pb.go b/pkg/loop/internal/pb/chain_reader.pb.go index 49fd7babb..5f70c82af 100644 --- a/pkg/loop/internal/pb/chain_reader.pb.go +++ b/pkg/loop/internal/pb/chain_reader.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc v4.24.4 // source: chain_reader.proto package pb diff --git a/pkg/loop/internal/pb/chain_reader_grpc.pb.go b/pkg/loop/internal/pb/chain_reader_grpc.pb.go index a90932335..0584ece28 100644 --- a/pkg/loop/internal/pb/chain_reader_grpc.pb.go +++ b/pkg/loop/internal/pb/chain_reader_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.24.2 +// - protoc v4.24.4 // source: chain_reader.proto package pb diff --git a/pkg/loop/internal/pb/codec.pb.go b/pkg/loop/internal/pb/codec.pb.go index caa11836b..af5946e23 100644 --- a/pkg/loop/internal/pb/codec.pb.go +++ b/pkg/loop/internal/pb/codec.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc v4.24.4 // source: codec.proto package pb diff --git a/pkg/loop/internal/pb/codec_grpc.pb.go b/pkg/loop/internal/pb/codec_grpc.pb.go index aa4b31d56..504f58dd3 100644 --- a/pkg/loop/internal/pb/codec_grpc.pb.go +++ b/pkg/loop/internal/pb/codec_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.24.2 +// - protoc v4.24.4 // source: codec.proto package pb diff --git a/pkg/loop/internal/pb/median.pb.go b/pkg/loop/internal/pb/median.pb.go index d73ae923c..ec1596fcc 100644 --- a/pkg/loop/internal/pb/median.pb.go +++ b/pkg/loop/internal/pb/median.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc v4.24.4 // source: median.proto package pb diff --git a/pkg/loop/internal/pb/median_grpc.pb.go b/pkg/loop/internal/pb/median_grpc.pb.go index 3260a2fae..91896a096 100644 --- a/pkg/loop/internal/pb/median_grpc.pb.go +++ b/pkg/loop/internal/pb/median_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.24.2 +// - protoc v4.24.4 // source: median.proto package pb diff --git a/pkg/loop/internal/pb/pipeline_runner.pb.go b/pkg/loop/internal/pb/pipeline_runner.pb.go index 81b4746c7..3dcbc5abb 100644 --- a/pkg/loop/internal/pb/pipeline_runner.pb.go +++ b/pkg/loop/internal/pb/pipeline_runner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc v4.24.4 // source: pipeline_runner.proto package pb diff --git a/pkg/loop/internal/pb/pipeline_runner_grpc.pb.go b/pkg/loop/internal/pb/pipeline_runner_grpc.pb.go index c218a8ead..0667161cf 100644 --- a/pkg/loop/internal/pb/pipeline_runner_grpc.pb.go +++ b/pkg/loop/internal/pb/pipeline_runner_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.24.2 +// - protoc v4.24.4 // source: pipeline_runner.proto package pb diff --git a/pkg/loop/internal/pb/relayer.pb.go b/pkg/loop/internal/pb/relayer.pb.go index aeb44a56b..a20a1d9ff 100644 --- a/pkg/loop/internal/pb/relayer.pb.go +++ b/pkg/loop/internal/pb/relayer.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc v4.24.4 // source: relayer.proto package pb diff --git a/pkg/loop/internal/pb/relayer_grpc.pb.go b/pkg/loop/internal/pb/relayer_grpc.pb.go index c715e31ff..4e4cfe99d 100644 --- a/pkg/loop/internal/pb/relayer_grpc.pb.go +++ b/pkg/loop/internal/pb/relayer_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.24.2 +// - protoc v4.24.4 // source: relayer.proto package pb diff --git a/pkg/loop/internal/pb/reporting.pb.go b/pkg/loop/internal/pb/reporting.pb.go index f72d5f57b..a0bacbd49 100644 --- a/pkg/loop/internal/pb/reporting.pb.go +++ b/pkg/loop/internal/pb/reporting.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc v4.24.4 // source: reporting.proto package pb diff --git a/pkg/loop/internal/pb/reporting_grpc.pb.go b/pkg/loop/internal/pb/reporting_grpc.pb.go index 34835ae37..f9e361944 100644 --- a/pkg/loop/internal/pb/reporting_grpc.pb.go +++ b/pkg/loop/internal/pb/reporting_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.24.2 +// - protoc v4.24.4 // source: reporting.proto package pb diff --git a/pkg/loop/internal/pb/reporting_plugin_service.pb.go b/pkg/loop/internal/pb/reporting_plugin_service.pb.go index 28080191a..56d2d12b2 100644 --- a/pkg/loop/internal/pb/reporting_plugin_service.pb.go +++ b/pkg/loop/internal/pb/reporting_plugin_service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc v4.24.4 // source: reporting_plugin_service.proto package pb diff --git a/pkg/loop/internal/pb/reporting_plugin_service_grpc.pb.go b/pkg/loop/internal/pb/reporting_plugin_service_grpc.pb.go index 9a33328eb..5a602ac94 100644 --- a/pkg/loop/internal/pb/reporting_plugin_service_grpc.pb.go +++ b/pkg/loop/internal/pb/reporting_plugin_service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.24.2 +// - protoc v4.24.4 // source: reporting_plugin_service.proto package pb diff --git a/pkg/loop/internal/pb/telemetry.pb.go b/pkg/loop/internal/pb/telemetry.pb.go index 0ba45e37c..dbf9e64d2 100644 --- a/pkg/loop/internal/pb/telemetry.pb.go +++ b/pkg/loop/internal/pb/telemetry.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc v4.24.4 // source: telemetry.proto package pb diff --git a/pkg/loop/internal/pb/telemetry_grpc.pb.go b/pkg/loop/internal/pb/telemetry_grpc.pb.go index 66264fc1d..47fc95812 100644 --- a/pkg/loop/internal/pb/telemetry_grpc.pb.go +++ b/pkg/loop/internal/pb/telemetry_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.24.2 +// - protoc v4.24.4 // source: telemetry.proto package pb diff --git a/pkg/loop/median_service_test.go b/pkg/loop/median_service_test.go index 6fe45e56b..f2bd8adbb 100644 --- a/pkg/loop/median_service_test.go +++ b/pkg/loop/median_service_test.go @@ -6,14 +6,11 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/loop/internal" "github.com/smartcontractkit/chainlink-common/pkg/loop/internal/test" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" ) func TestMedianService(t *testing.T) { @@ -23,8 +20,7 @@ func TestMedianService(t *testing.T) { return NewHelperProcessCommand(loop.PluginMedianName) }, test.StaticMedianProvider{}, test.StaticDataSource(), test.StaticJuelsPerFeeCoinDataSource(), &test.StaticErrorLog{}) hook := median.PluginService.XXXTestHook() - require.NoError(t, median.Start(tests.Context(t))) - t.Cleanup(func() { assert.NoError(t, median.Close()) }) + servicetest.Run(t, median) t.Run("control", func(t *testing.T) { test.ReportingPluginFactory(t, median) @@ -59,8 +55,7 @@ func TestMedianService_recovery(t *testing.T) { } return h.New() }, test.StaticMedianProvider{}, test.StaticDataSource(), test.StaticJuelsPerFeeCoinDataSource(), &test.StaticErrorLog{}) - require.NoError(t, median.Start(tests.Context(t))) - t.Cleanup(func() { assert.NoError(t, median.Close()) }) + servicetest.Run(t, median) test.ReportingPluginFactory(t, median) } diff --git a/pkg/loop/plugin_median_test.go b/pkg/loop/plugin_median_test.go index 9c624eda5..3755ea1e0 100644 --- a/pkg/loop/plugin_median_test.go +++ b/pkg/loop/plugin_median_test.go @@ -6,12 +6,12 @@ import ( "time" "github.com/hashicorp/go-plugin" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/loop/internal/test" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" "github.com/smartcontractkit/chainlink-common/pkg/types" ) @@ -67,13 +67,11 @@ func newMedianProvider(t *testing.T, pr loop.PluginRelayer) types.MedianProvider ctx := context.Background() r, err := pr.NewRelayer(ctx, test.ConfigTOML, test.StaticKeystore{}) require.NoError(t, err) - require.NoError(t, r.Start(ctx)) - t.Cleanup(func() { assert.NoError(t, r.Close()) }) + servicetest.Run(t, r) p, err := r.NewPluginProvider(ctx, test.RelayArgs, test.PluginArgs) mp, ok := p.(types.MedianProvider) require.True(t, ok) require.NoError(t, err) - require.NoError(t, mp.Start(ctx)) - t.Cleanup(func() { assert.NoError(t, mp.Close()) }) + servicetest.Run(t, mp) return mp } diff --git a/pkg/loop/prom_test.go b/pkg/loop/prom_test.go index 8c51d6367..00f6ba2b1 100644 --- a/pkg/loop/prom_test.go +++ b/pkg/loop/prom_test.go @@ -9,6 +9,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -27,18 +28,17 @@ func TestPromServer(t *testing.T) { // check that port is not resolved yet require.Equal(t, -1, s.Port()) require.NoError(t, s.Start()) + t.Cleanup(func() { assert.NoError(t, s.Close()) }) url := fmt.Sprintf("http://localhost:%d/metrics", s.Port()) resp, err := http.Get(url) //nolint require.NoError(t, err) + defer resp.Body.Close() require.NoError(t, err, "endpoint %s", url) require.NotNil(t, resp.Body) b, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Contains(t, string(b), "test_metric") - defer resp.Body.Close() - - require.NoError(t, s.Close()) } // Port is the resolved port and is only known after Start(). diff --git a/pkg/loop/relayer_service_test.go b/pkg/loop/relayer_service_test.go index 4e87f9f47..d38e9c5bb 100644 --- a/pkg/loop/relayer_service_test.go +++ b/pkg/loop/relayer_service_test.go @@ -6,14 +6,11 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/loop/internal" "github.com/smartcontractkit/chainlink-common/pkg/loop/internal/test" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" ) func TestRelayerService(t *testing.T) { @@ -22,8 +19,7 @@ func TestRelayerService(t *testing.T) { return NewHelperProcessCommand(loop.PluginRelayerName) }, test.ConfigTOML, test.StaticKeystore{}) hook := relayer.XXXTestHook() - require.NoError(t, relayer.Start(tests.Context(t))) - t.Cleanup(func() { assert.NoError(t, relayer.Close()) }) + servicetest.Run(t, relayer) t.Run("control", func(t *testing.T) { test.RunRelayer(t, relayer) @@ -58,8 +54,7 @@ func TestRelayerService_recovery(t *testing.T) { } return h.New() }, test.ConfigTOML, test.StaticKeystore{}) - require.NoError(t, relayer.Start(tests.Context(t))) - t.Cleanup(func() { assert.NoError(t, relayer.Close()) }) + servicetest.Run(t, relayer) test.RunRelayer(t, relayer) } diff --git a/pkg/loop/reportingplugins/loopp_service_test.go b/pkg/loop/reportingplugins/loopp_service_test.go index 9eaf68843..7c15687a2 100644 --- a/pkg/loop/reportingplugins/loopp_service_test.go +++ b/pkg/loop/reportingplugins/loopp_service_test.go @@ -6,16 +6,13 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/loop/internal" "github.com/smartcontractkit/chainlink-common/pkg/loop/internal/test" "github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" "github.com/smartcontractkit/chainlink-common/pkg/types" - utilstests "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) type HelperProcessCommand test.HelperProcessCommand @@ -48,8 +45,7 @@ func TestLOOPPService(t *testing.T) { return NewHelperProcessCommand(ts.Plugin) }, types.ReportingPluginServiceConfig{}, test.MockConn{}, &test.StaticPipelineRunnerService{}, &test.StaticTelemetry{}, &test.StaticErrorLog{}) hook := looppSvc.XXXTestHook() - require.NoError(t, looppSvc.Start(utilstests.Context(t))) - t.Cleanup(func() { assert.NoError(t, looppSvc.Close()) }) + servicetest.Run(t, looppSvc) t.Run("control", func(t *testing.T) { test.ReportingPluginFactory(t, looppSvc) @@ -85,8 +81,7 @@ func TestLOOPPService_recovery(t *testing.T) { } return h.New() }, types.ReportingPluginServiceConfig{}, test.MockConn{}, &test.StaticPipelineRunnerService{}, &test.StaticTelemetry{}, &test.StaticErrorLog{}) - require.NoError(t, looppSvc.Start(utilstests.Context(t))) - t.Cleanup(func() { assert.NoError(t, looppSvc.Close()) }) + servicetest.Run(t, looppSvc) test.ReportingPluginFactory(t, looppSvc) } diff --git a/pkg/loop/server.go b/pkg/loop/server.go index 1f17dea71..db856f636 100644 --- a/pkg/loop/server.go +++ b/pkg/loop/server.go @@ -85,7 +85,7 @@ func (s *Server) start() error { return fmt.Errorf("error starting prometheus server: %w", err) } - s.checker = services.NewChecker() + s.checker = services.NewChecker("", "") if err := s.checker.Start(); err != nil { return fmt.Errorf("error starting health checker: %w", err) } diff --git a/pkg/monitoring/monitor_test.go b/pkg/monitoring/monitor_test.go index e6cae9cde..1885d1332 100644 --- a/pkg/monitoring/monitor_test.go +++ b/pkg/monitoring/monitor_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/confluentinc/confluent-kafka-go/kafka" + "github.com/confluentinc/confluent-kafka-go/v2/kafka" "github.com/stretchr/testify/require" "go.uber.org/goleak" diff --git a/pkg/monitoring/pb/offchainreporting2_monitoring_median_config.pb.go b/pkg/monitoring/pb/offchainreporting2_monitoring_median_config.pb.go index b8f817b6e..07a01a1ba 100644 --- a/pkg/monitoring/pb/offchainreporting2_monitoring_median_config.pb.go +++ b/pkg/monitoring/pb/offchainreporting2_monitoring_median_config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc v4.25.1 // source: offchainreporting2_monitoring_median_config.proto package pb diff --git a/pkg/monitoring/pb/offchainreporting2_monitoring_offchain_config.pb.go b/pkg/monitoring/pb/offchainreporting2_monitoring_offchain_config.pb.go index b85d14309..2ba9ed717 100644 --- a/pkg/monitoring/pb/offchainreporting2_monitoring_offchain_config.pb.go +++ b/pkg/monitoring/pb/offchainreporting2_monitoring_offchain_config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc v4.25.1 // source: offchainreporting2_monitoring_offchain_config.proto package pb diff --git a/pkg/monitoring/producer.go b/pkg/monitoring/producer.go index bdf2a1140..a5ea76965 100644 --- a/pkg/monitoring/producer.go +++ b/pkg/monitoring/producer.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/confluentinc/confluent-kafka-go/kafka" + "github.com/confluentinc/confluent-kafka-go/v2/kafka" "github.com/smartcontractkit/chainlink-common/pkg/monitoring/config" ) diff --git a/pkg/reportingplugins/README.md b/pkg/reportingplugins/README.md deleted file mode 100644 index caa917ed2..000000000 --- a/pkg/reportingplugins/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# OCR2 ReportingPlugins - -This directory contains OCR2 `ReportingPlugin` packages, with implementations of [ReportingPlugin](https://pkg.go.dev/github.com/smartcontractkit/libocr@v0.0.0-20230112103111-f100435787c8/offchainreporting2/types#ReportingPlugin) & [ReportingPluginFactory](https://pkg.go.dev/github.com/smartcontractkit/libocr@v0.0.0-20230112103111-f100435787c8/offchainreporting2/types#ReportingPluginFactory). diff --git a/pkg/reportingplugins/mercury/aggregate_functions.go b/pkg/reportingplugins/mercury/aggregate_functions.go deleted file mode 100644 index b3422917b..000000000 --- a/pkg/reportingplugins/mercury/aggregate_functions.go +++ /dev/null @@ -1,168 +0,0 @@ -package mercury - -import ( - "fmt" - "math/big" - "sort" -) - -var Zero = big.NewInt(0) - -// NOTE: All aggregate functions assume at least one element in the passed slice -// The passed slice might be mutated (sorted) - -// GetConsensusTimestamp gets the median timestamp -func GetConsensusTimestamp(paos []PAO) uint32 { - sort.Slice(paos, func(i, j int) bool { - return paos[i].GetTimestamp() < paos[j].GetTimestamp() - }) - return paos[len(paos)/2].GetTimestamp() -} - -// GetConsensusBenchmarkPrice gets the median benchmark price -func GetConsensusBenchmarkPrice(paos []PAO, f int) (*big.Int, error) { - var validBenchmarkPrices []*big.Int - for _, pao := range paos { - bmPrice, valid := pao.GetBenchmarkPrice() - if valid { - validBenchmarkPrices = append(validBenchmarkPrices, bmPrice) - } - } - - if len(validBenchmarkPrices) < f+1 { - return nil, fmt.Errorf("fewer than f+1 observations have a valid price (got: %d/%d)", len(validBenchmarkPrices), len(paos)) - } - sort.Slice(validBenchmarkPrices, func(i, j int) bool { - return validBenchmarkPrices[i].Cmp(validBenchmarkPrices[j]) < 0 - }) - - return validBenchmarkPrices[len(validBenchmarkPrices)/2], nil -} - -type PAOBid interface { - GetBid() (*big.Int, bool) -} - -// GetConsensusBid gets the median bid -func GetConsensusBid(paos []PAOBid, f int) (*big.Int, error) { - var validBids []*big.Int - for _, pao := range paos { - bid, valid := pao.GetBid() - if valid { - validBids = append(validBids, bid) - } - } - if len(validBids) < f+1 { - return nil, fmt.Errorf("fewer than f+1 observations have a valid price (got: %d/%d)", len(validBids), len(paos)) - } - sort.Slice(validBids, func(i, j int) bool { - return validBids[i].Cmp(validBids[j]) < 0 - }) - - return validBids[len(validBids)/2], nil -} - -type PAOAsk interface { - GetAsk() (*big.Int, bool) -} - -// GetConsensusAsk gets the median ask -func GetConsensusAsk(paos []PAOAsk, f int) (*big.Int, error) { - var validAsks []*big.Int - for _, pao := range paos { - ask, valid := pao.GetAsk() - if valid { - validAsks = append(validAsks, ask) - } - } - if len(validAsks) < f+1 { - return nil, fmt.Errorf("fewer than f+1 observations have a valid price (got: %d/%d)", len(validAsks), len(paos)) - } - sort.Slice(validAsks, func(i, j int) bool { - return validAsks[i].Cmp(validAsks[j]) < 0 - }) - - return validAsks[len(validAsks)/2], nil -} - -type PAOMaxFinalizedTimestamp interface { - GetMaxFinalizedTimestamp() (int64, bool) -} - -// GetConsensusMaxFinalizedTimestamp returns the highest count with > f observations -func GetConsensusMaxFinalizedTimestamp(paos []PAOMaxFinalizedTimestamp, f int) (int64, error) { - var validTimestampCount int - timestampFrequencyMap := map[int64]int{} - for _, pao := range paos { - ts, valid := pao.GetMaxFinalizedTimestamp() - if valid { - validTimestampCount++ - timestampFrequencyMap[ts]++ - } - } - - // check if we have enough valid timestamps at all - if validTimestampCount < f+1 { - return 0, fmt.Errorf("fewer than f+1 observations have a valid maxFinalizedTimestamp (got: %d/%d)", validTimestampCount, len(paos)) - } - - var maxTs int64 = -2 // -1 is smallest valid amount - for ts, cnt := range timestampFrequencyMap { - // ignore any timestamps with <= f observations - if cnt > f && ts > maxTs { - maxTs = ts - } - } - - if maxTs < -1 { - return 0, fmt.Errorf("no valid maxFinalizedTimestamp with at least f+1 votes (got counts: %v)", timestampFrequencyMap) - } - - return maxTs, nil -} - -type PAOLinkFee interface { - GetLinkFee() (*big.Int, bool) -} - -// GetConsensusLinkFee gets the median link fee -func GetConsensusLinkFee(paos []PAOLinkFee, f int) (*big.Int, error) { - var validLinkFees []*big.Int - for _, pao := range paos { - fee, valid := pao.GetLinkFee() - if valid && fee.Sign() >= 0 { - validLinkFees = append(validLinkFees, fee) - } - } - if len(validLinkFees) < f+1 { - return nil, fmt.Errorf("fewer than f+1 observations have a valid linkFee (got: %d/%d)", len(validLinkFees), len(paos)) - } - sort.Slice(validLinkFees, func(i, j int) bool { - return validLinkFees[i].Cmp(validLinkFees[j]) < 0 - }) - - return validLinkFees[len(validLinkFees)/2], nil -} - -type PAONativeFee interface { - GetNativeFee() (*big.Int, bool) -} - -// GetConsensusNativeFee gets the median native fee -func GetConsensusNativeFee(paos []PAONativeFee, f int) (*big.Int, error) { - var validNativeFees []*big.Int - for _, pao := range paos { - fee, valid := pao.GetNativeFee() - if valid && fee.Sign() >= 0 { - validNativeFees = append(validNativeFees, fee) - } - } - if len(validNativeFees) < f+1 { - return nil, fmt.Errorf("fewer than f+1 observations have a valid nativeFee (got: %d/%d)", len(validNativeFees), len(paos)) - } - sort.Slice(validNativeFees, func(i, j int) bool { - return validNativeFees[i].Cmp(validNativeFees[j]) < 0 - }) - - return validNativeFees[len(validNativeFees)/2], nil -} diff --git a/pkg/reportingplugins/mercury/aggregate_functions_test.go b/pkg/reportingplugins/mercury/aggregate_functions_test.go deleted file mode 100644 index 28df8cecd..000000000 --- a/pkg/reportingplugins/mercury/aggregate_functions_test.go +++ /dev/null @@ -1,418 +0,0 @@ -package mercury - -import ( - "math/big" - "testing" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type testParsedAttributedObservation struct { - Timestamp uint32 - BenchmarkPrice *big.Int - BenchmarkPriceValid bool - Bid *big.Int - BidValid bool - Ask *big.Int - AskValid bool - MaxFinalizedTimestamp int64 - MaxFinalizedTimestampValid bool - LinkFee *big.Int - LinkFeeValid bool - NativeFee *big.Int - NativeFeeValid bool -} - -func (t testParsedAttributedObservation) GetObserver() commontypes.OracleID { return 0 } -func (t testParsedAttributedObservation) GetTimestamp() uint32 { return t.Timestamp } -func (t testParsedAttributedObservation) GetBenchmarkPrice() (*big.Int, bool) { - return t.BenchmarkPrice, t.BenchmarkPriceValid -} -func (t testParsedAttributedObservation) GetBid() (*big.Int, bool) { - return t.Bid, t.BidValid -} -func (t testParsedAttributedObservation) GetAsk() (*big.Int, bool) { - return t.Ask, t.AskValid -} -func (t testParsedAttributedObservation) GetMaxFinalizedTimestamp() (int64, bool) { - return t.MaxFinalizedTimestamp, t.MaxFinalizedTimestampValid -} -func (t testParsedAttributedObservation) GetLinkFee() (*big.Int, bool) { - return t.LinkFee, t.LinkFeeValid -} -func (t testParsedAttributedObservation) GetNativeFee() (*big.Int, bool) { - return t.NativeFee, t.NativeFeeValid -} -func newValidParsedAttributedObservations() []testParsedAttributedObservation { - return []testParsedAttributedObservation{ - testParsedAttributedObservation{ - Timestamp: 1689648456, - - BenchmarkPrice: big.NewInt(123), - BenchmarkPriceValid: true, - Bid: big.NewInt(120), - BidValid: true, - Ask: big.NewInt(130), - AskValid: true, - - MaxFinalizedTimestamp: 1679448456, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(1), - LinkFeeValid: true, - NativeFee: big.NewInt(1), - NativeFeeValid: true, - }, - testParsedAttributedObservation{ - Timestamp: 1689648456, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: true, - Bid: big.NewInt(450), - BidValid: true, - Ask: big.NewInt(460), - AskValid: true, - - MaxFinalizedTimestamp: 1679448456, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(2), - LinkFeeValid: true, - NativeFee: big.NewInt(2), - NativeFeeValid: true, - }, - testParsedAttributedObservation{ - Timestamp: 1689648789, - - BenchmarkPrice: big.NewInt(789), - BenchmarkPriceValid: true, - Bid: big.NewInt(780), - BidValid: true, - Ask: big.NewInt(800), - AskValid: true, - - MaxFinalizedTimestamp: 1679448456, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(3), - LinkFeeValid: true, - NativeFee: big.NewInt(3), - NativeFeeValid: true, - }, - testParsedAttributedObservation{ - Timestamp: 1689648789, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: true, - Bid: big.NewInt(450), - BidValid: true, - Ask: big.NewInt(460), - AskValid: true, - - MaxFinalizedTimestamp: 1679513477, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(4), - LinkFeeValid: true, - NativeFee: big.NewInt(4), - NativeFeeValid: true, - }, - } -} -func NewValidParsedAttributedObservations(paos ...testParsedAttributedObservation) []testParsedAttributedObservation { - if len(paos) == 0 { - paos = newValidParsedAttributedObservations() - } - return []testParsedAttributedObservation{ - paos[0], - paos[1], - paos[2], - paos[3], - } -} - -func NewInvalidParsedAttributedObservations() []testParsedAttributedObservation { - return []testParsedAttributedObservation{ - testParsedAttributedObservation{ - Timestamp: 1, - - BenchmarkPrice: big.NewInt(123), - BenchmarkPriceValid: false, - Bid: big.NewInt(120), - BidValid: false, - Ask: big.NewInt(130), - AskValid: false, - - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: false, - - LinkFee: big.NewInt(1), - LinkFeeValid: false, - NativeFee: big.NewInt(1), - NativeFeeValid: false, - }, - testParsedAttributedObservation{ - Timestamp: 2, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: false, - Bid: big.NewInt(450), - BidValid: false, - Ask: big.NewInt(460), - AskValid: false, - - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: false, - - LinkFee: big.NewInt(2), - LinkFeeValid: false, - NativeFee: big.NewInt(2), - NativeFeeValid: false, - }, - testParsedAttributedObservation{ - Timestamp: 2, - - BenchmarkPrice: big.NewInt(789), - BenchmarkPriceValid: false, - Bid: big.NewInt(780), - BidValid: false, - Ask: big.NewInt(800), - AskValid: false, - - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: false, - - LinkFee: big.NewInt(3), - LinkFeeValid: false, - NativeFee: big.NewInt(3), - NativeFeeValid: false, - }, - testParsedAttributedObservation{ - Timestamp: 3, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: true, - Bid: big.NewInt(450), - BidValid: true, - Ask: big.NewInt(460), - AskValid: true, - - MaxFinalizedTimestamp: 1679513477, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(4), - LinkFeeValid: true, - NativeFee: big.NewInt(4), - NativeFeeValid: true, - }, - } -} - -func Test_AggregateFunctions(t *testing.T) { - f := 1 - validPaos := NewValidParsedAttributedObservations() - invalidPaos := NewInvalidParsedAttributedObservations() - - t.Run("GetConsensusTimestamp", func(t *testing.T) { - validMPaos := convert(validPaos) - ts := GetConsensusTimestamp(validMPaos) - - assert.Equal(t, 1689648789, int(ts)) - }) - - t.Run("GetConsensusBenchmarkPrice", func(t *testing.T) { - t.Run("gets consensus price when prices are valid", func(t *testing.T) { - validMPaos := convert(validPaos) - bp, err := GetConsensusBenchmarkPrice(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, "456", bp.String()) - }) - - t.Run("fails when fewer than f+1 prices are valid", func(t *testing.T) { - invalidMPaos := convert(invalidPaos) - _, err := GetConsensusBenchmarkPrice(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid price (got: 1/4)") - }) - }) - - t.Run("GetConsensusBid", func(t *testing.T) { - t.Run("gets consensus bid when prices are valid", func(t *testing.T) { - validMPaos := convertBid(validPaos) - bid, err := GetConsensusBid(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, "450", bid.String()) - }) - - t.Run("fails when fewer than f+1 prices are valid", func(t *testing.T) { - invalidMPaos := convertBid(invalidPaos) - _, err := GetConsensusBid(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid price (got: 1/4)") - }) - }) - - t.Run("GetConsensusAsk", func(t *testing.T) { - t.Run("gets consensus ask when prices are valid", func(t *testing.T) { - validMPaos := convertAsk(validPaos) - bid, err := GetConsensusAsk(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, "460", bid.String()) - }) - - t.Run("fails when fewer than f+1 prices are valid", func(t *testing.T) { - invalidMPaos := convertAsk(invalidPaos) - _, err := GetConsensusAsk(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid price (got: 1/4)") - }) - }) - - t.Run("GetConsensusMaxFinalizedTimestamp", func(t *testing.T) { - t.Run("gets consensus on maxFinalizedTimestamp when valid", func(t *testing.T) { - validMPaos := convertMaxFinalizedTimestamp(validPaos) - ts, err := GetConsensusMaxFinalizedTimestamp(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, int64(1679448456), ts) - }) - - t.Run("uses highest value as tiebreaker", func(t *testing.T) { - paos := newValidParsedAttributedObservations() - (paos[0]).MaxFinalizedTimestamp = 1679513477 - validMPaos := convertMaxFinalizedTimestamp(NewValidParsedAttributedObservations(paos...)) - ts, err := GetConsensusMaxFinalizedTimestamp(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, int64(1679513477), ts) - }) - - t.Run("fails when fewer than f+1 maxFinalizedTimestamps are valid", func(t *testing.T) { - invalidMPaos := convertMaxFinalizedTimestamp(invalidPaos) - _, err := GetConsensusMaxFinalizedTimestamp(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid maxFinalizedTimestamp (got: 1/4)") - }) - - t.Run("fails when cannot come to consensus f+1 maxFinalizedTimestamps", func(t *testing.T) { - paos := []PAOMaxFinalizedTimestamp{ - testParsedAttributedObservation{ - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: true, - }, - testParsedAttributedObservation{ - MaxFinalizedTimestamp: 1679648457, - MaxFinalizedTimestampValid: true, - }, - testParsedAttributedObservation{ - MaxFinalizedTimestamp: 1679648458, - MaxFinalizedTimestampValid: true, - }, - testParsedAttributedObservation{ - MaxFinalizedTimestamp: 1679513477, - MaxFinalizedTimestampValid: true, - }, - } - _, err := GetConsensusMaxFinalizedTimestamp(paos, f) - assert.EqualError(t, err, "no valid maxFinalizedTimestamp with at least f+1 votes (got counts: map[1679513477:1 1679648456:1 1679648457:1 1679648458:1])") - }) - }) - - t.Run("GetConsensusLinkFee", func(t *testing.T) { - t.Run("gets consensus on linkFee when valid", func(t *testing.T) { - validMPaos := convertLinkFee(validPaos) - linkFee, err := GetConsensusLinkFee(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, big.NewInt(3), linkFee) - }) - t.Run("treats zero values as valid", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].LinkFee = big.NewInt(0) - } - linkFee, err := GetConsensusLinkFee(convertLinkFee(paos), f) - require.NoError(t, err) - assert.Equal(t, big.NewInt(0), linkFee) - }) - t.Run("treats negative values as invalid", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].LinkFee = big.NewInt(int64(0 - i)) - } - _, err := GetConsensusLinkFee(convertLinkFee(paos), f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid linkFee (got: 1/4)") - }) - - t.Run("fails when fewer than f+1 linkFees are valid", func(t *testing.T) { - invalidMPaos := convertLinkFee(invalidPaos) - _, err := GetConsensusLinkFee(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid linkFee (got: 1/4)") - }) - }) - - t.Run("GetConsensusNativeFee", func(t *testing.T) { - t.Run("gets consensus on nativeFee when valid", func(t *testing.T) { - validMPaos := convertNativeFee(validPaos) - nativeFee, err := GetConsensusNativeFee(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, big.NewInt(3), nativeFee) - }) - t.Run("treats zero values as valid", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].NativeFee = big.NewInt(0) - } - nativeFee, err := GetConsensusNativeFee(convertNativeFee(paos), f) - require.NoError(t, err) - assert.Equal(t, big.NewInt(0), nativeFee) - }) - t.Run("treats negative values as invalid", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].NativeFee = big.NewInt(int64(0 - i)) - } - _, err := GetConsensusNativeFee(convertNativeFee(paos), f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid nativeFee (got: 1/4)") - }) - t.Run("fails when fewer than f+1 nativeFees are valid", func(t *testing.T) { - invalidMPaos := convertNativeFee(invalidPaos) - _, err := GetConsensusNativeFee(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid nativeFee (got: 1/4)") - }) - }) -} - -// convert funcs are necessary because go is not smart enough to cast -// []interface1 to []interface2 even if interface1 is a superset of interface2 -func convert(pao []testParsedAttributedObservation) (ret []PAO) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertMaxFinalizedTimestamp(pao []testParsedAttributedObservation) (ret []PAOMaxFinalizedTimestamp) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertAsk(pao []testParsedAttributedObservation) (ret []PAOAsk) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertBid(pao []testParsedAttributedObservation) (ret []PAOBid) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertLinkFee(pao []testParsedAttributedObservation) (ret []PAOLinkFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertNativeFee(pao []testParsedAttributedObservation) (ret []PAONativeFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} diff --git a/pkg/reportingplugins/mercury/epochround.go b/pkg/reportingplugins/mercury/epochround.go deleted file mode 100644 index 65a039693..000000000 --- a/pkg/reportingplugins/mercury/epochround.go +++ /dev/null @@ -1,10 +0,0 @@ -package mercury - -type EpochRound struct { - Epoch uint32 - Round uint8 -} - -func (x EpochRound) Less(y EpochRound) bool { - return x.Epoch < y.Epoch || (x.Epoch == y.Epoch && x.Round < y.Round) -} diff --git a/pkg/reportingplugins/mercury/fees.go b/pkg/reportingplugins/mercury/fees.go deleted file mode 100644 index 2d03fea1b..000000000 --- a/pkg/reportingplugins/mercury/fees.go +++ /dev/null @@ -1,38 +0,0 @@ -package mercury - -import ( - "math/big" - - "github.com/shopspring/decimal" -) - -// PriceScalingFactor indicates the multiplier applied to token prices that we expect from data source -// e.g. for a 1e8 multiplier, a LINK/USD value of 7.42 will be represented as 742000000 -var PRICE_SCALING_FACTOR = decimal.NewFromInt(1e8) //nolint:revive - -// FeeScalingFactor indicates the multiplier applied to fees. -// e.g. for a 1e18 multiplier, a LINK fee of 7.42 will be represented as 7.42e18 -// This is what will be baked into the report for use on-chain. -var FEE_SCALING_FACTOR = decimal.NewFromInt(1e18) //nolint:revive - -// CalculateFee outputs a fee in wei according to the formula: baseUSDFee * scaleFactor / tokenPriceInUSD -func CalculateFee(tokenPriceInUSD *big.Int, baseUSDFee decimal.Decimal) *big.Int { - if tokenPriceInUSD.Cmp(big.NewInt(0)) == 0 || baseUSDFee.IsZero() { - // zero fee if token price or base fee is zero - return big.NewInt(0) - } - - // scale baseFee in USD - baseFeeScaled := baseUSDFee.Mul(PRICE_SCALING_FACTOR) - - tokenPrice := decimal.NewFromBigInt(tokenPriceInUSD, 0) - - // fee denominated in token - fee := baseFeeScaled.Div(tokenPrice) - - // scale fee to the expected format - fee = fee.Mul(FEE_SCALING_FACTOR) - - // convert to big.Int - return fee.BigInt() -} diff --git a/pkg/reportingplugins/mercury/fees_test.go b/pkg/reportingplugins/mercury/fees_test.go deleted file mode 100644 index 550e685a9..000000000 --- a/pkg/reportingplugins/mercury/fees_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package mercury - -import ( - "math/big" - "testing" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func scalePrice(usdPrice float64) *big.Int { - scaledPrice := new(big.Float).Mul(big.NewFloat(usdPrice), big.NewFloat(1e8)) - scaledPriceInt, _ := scaledPrice.Int(nil) - return scaledPriceInt -} - -func Test_Fees(t *testing.T) { - BaseUSDFee, err := decimal.NewFromString("0.70") - require.NoError(t, err) - t.Run("with token price > 1", func(t *testing.T) { - tokenPriceInUSD := scalePrice(1630) - fee := CalculateFee(tokenPriceInUSD, BaseUSDFee) - expectedFee := big.NewInt(429447852760700) // 0.0004294478527607 18 decimals - if fee.Cmp(expectedFee) != 0 { - t.Errorf("Expected fee to be %v, got %v", expectedFee, fee) - } - }) - - t.Run("with token price < 1", func(t *testing.T) { - tokenPriceInUSD := scalePrice(0.4) - fee := CalculateFee(tokenPriceInUSD, BaseUSDFee) - expectedFee := big.NewInt(1750000000000000000) // 1.75 18 decimals - if fee.Cmp(expectedFee) != 0 { - t.Errorf("Expected fee to be %v, got %v", expectedFee, fee) - } - }) - - t.Run("with token price == 0", func(t *testing.T) { - tokenPriceInUSD := scalePrice(0) - fee := CalculateFee(tokenPriceInUSD, BaseUSDFee) - assert.Equal(t, big.NewInt(0), fee) - }) - - t.Run("with base fee == 0", func(t *testing.T) { - tokenPriceInUSD := scalePrice(123) - BaseUSDFee = decimal.NewFromInt32(0) - fee := CalculateFee(tokenPriceInUSD, BaseUSDFee) - assert.Equal(t, big.NewInt(0), fee) - }) -} diff --git a/pkg/reportingplugins/mercury/mercury_config.proto b/pkg/reportingplugins/mercury/mercury_config.proto deleted file mode 100644 index b0343f4f4..000000000 --- a/pkg/reportingplugins/mercury/mercury_config.proto +++ /dev/null @@ -1,7 +0,0 @@ -syntax="proto3"; - -package mercury; -option go_package = ".;mercury"; - -// Empty for now; might add in future -message MercuryConfigProto {} diff --git a/pkg/reportingplugins/mercury/offchain_config.go b/pkg/reportingplugins/mercury/offchain_config.go deleted file mode 100644 index 9fd05544d..000000000 --- a/pkg/reportingplugins/mercury/offchain_config.go +++ /dev/null @@ -1,25 +0,0 @@ -package mercury - -import ( - "encoding/json" - "fmt" - - "github.com/shopspring/decimal" -) - -type OffchainConfig struct { - ExpirationWindow uint32 `json:"expirationWindow"` // Integer number of seconds - BaseUSDFee decimal.Decimal `json:"baseUSDFee"` // Base USD fee -} - -func DecodeOffchainConfig(b []byte) (o OffchainConfig, err error) { - err = json.Unmarshal(b, &o) - if err != nil { - return o, fmt.Errorf("failed to decode offchain config: must be valid JSON (got: 0x%x); %w", b, err) - } - return -} - -func (c OffchainConfig) Encode() ([]byte, error) { - return json.Marshal(c) -} diff --git a/pkg/reportingplugins/mercury/offchain_config_test.go b/pkg/reportingplugins/mercury/offchain_config_test.go deleted file mode 100644 index db06f8959..000000000 --- a/pkg/reportingplugins/mercury/offchain_config_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package mercury - -import ( - "testing" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_OffchainConfig(t *testing.T) { - t.Run("decoding", func(t *testing.T) { - t.Run("with number type for USD fee", func(t *testing.T) { - json := ` -{ - "expirationWindow": 42, - "baseUSDFee": 123.456 -} -` - c, err := DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromFloat32(123.456), c.BaseUSDFee) - - json = ` -{ - "expirationWindow": 42, - "baseUSDFee": 123 -} -` - c, err = DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromInt32(123), c.BaseUSDFee) - - json = ` -{ - "expirationWindow": 42, - "baseUSDFee": 0.12 -} -` - c, err = DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromFloat32(0.12), c.BaseUSDFee) - }) - t.Run("with string type for USD fee", func(t *testing.T) { - json := ` -{ - "expirationWindow": 42, - "baseUSDFee": "123.456" -} -` - c, err := DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromFloat32(123.456), c.BaseUSDFee) - - json = ` -{ - "expirationWindow": 42, - "baseUSDFee": "123" -} -` - c, err = DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromInt32(123), c.BaseUSDFee) - - json = ` -{ - "expirationWindow": 42, - "baseUSDFee": "0.12" -} -` - c, err = DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromFloat32(0.12), c.BaseUSDFee) - }) - }) - t.Run("serialize/deserialize", func(t *testing.T) { - c := OffchainConfig{32, decimal.NewFromFloat32(1.23)} - - serialized, err := c.Encode() - require.NoError(t, err) - - deserialized, err := DecodeOffchainConfig(serialized) - require.NoError(t, err) - - assert.Equal(t, c, deserialized) - }) -} diff --git a/pkg/reportingplugins/mercury/onchain_config.go b/pkg/reportingplugins/mercury/onchain_config.go deleted file mode 100644 index f7ae3c412..000000000 --- a/pkg/reportingplugins/mercury/onchain_config.go +++ /dev/null @@ -1,81 +0,0 @@ -package mercury - -import ( - "math/big" - - pkgerrors "github.com/pkg/errors" - - "github.com/smartcontractkit/libocr/bigbigendian" -) - -const onchainConfigVersion = 1 - -var onchainConfigVersionBig = big.NewInt(onchainConfigVersion) - -const onchainConfigEncodedLength = 96 // 3x 32bit evm words, version + min + max - -type OnchainConfig struct { - // applies to all values: price, bid and ask - Min *big.Int - Max *big.Int -} - -var _ OnchainConfigCodec = StandardOnchainConfigCodec{} - -// StandardOnchainConfigCodec provides a mercury-specific implementation of -// OnchainConfigCodec. -// -// An encoded onchain config is expected to be in the format -// -// where version is a uint8 and min and max are in the format -// returned by EncodeValueInt192. -type StandardOnchainConfigCodec struct{} - -func (StandardOnchainConfigCodec) Decode(b []byte) (OnchainConfig, error) { - if len(b) != onchainConfigEncodedLength { - return OnchainConfig{}, pkgerrors.Errorf("unexpected length of OnchainConfig, expected %v, got %v", onchainConfigEncodedLength, len(b)) - } - - v, err := bigbigendian.DeserializeSigned(32, b[:32]) - if err != nil { - return OnchainConfig{}, err - } - if v.Cmp(onchainConfigVersionBig) != 0 { - return OnchainConfig{}, pkgerrors.Errorf("unexpected version of OnchainConfig, expected %v, got %v", onchainConfigVersion, v) - } - - min, err := bigbigendian.DeserializeSigned(32, b[32:64]) - if err != nil { - return OnchainConfig{}, err - } - max, err := bigbigendian.DeserializeSigned(32, b[64:96]) - if err != nil { - return OnchainConfig{}, err - } - - if !(min.Cmp(max) <= 0) { - return OnchainConfig{}, pkgerrors.Errorf("OnchainConfig min (%v) should not be greater than max(%v)", min, max) - } - - return OnchainConfig{min, max}, nil -} - -func (StandardOnchainConfigCodec) Encode(c OnchainConfig) ([]byte, error) { - verBytes, err := bigbigendian.SerializeSigned(32, onchainConfigVersionBig) - if err != nil { - return nil, err - } - minBytes, err := bigbigendian.SerializeSigned(32, c.Min) - if err != nil { - return nil, err - } - maxBytes, err := bigbigendian.SerializeSigned(32, c.Max) - if err != nil { - return nil, err - } - result := make([]byte, 0, onchainConfigEncodedLength) - result = append(result, verBytes...) - result = append(result, minBytes...) - result = append(result, maxBytes...) - return result, nil -} diff --git a/pkg/reportingplugins/mercury/onchain_config_test.go b/pkg/reportingplugins/mercury/onchain_config_test.go deleted file mode 100644 index f11bedd0f..000000000 --- a/pkg/reportingplugins/mercury/onchain_config_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package mercury - -import ( - "bytes" - "math/big" - "testing" -) - -func FuzzDecodeOnchainConfig(f *testing.F) { - valid, err := StandardOnchainConfigCodec{}.Encode(OnchainConfig{big.NewInt(1), big.NewInt(1000)}) - if err != nil { - f.Fatalf("failed to construct valid OnchainConfig: %s", err) - } - - f.Add([]byte{}) - f.Add(valid) - f.Fuzz(func(t *testing.T, encoded []byte) { - decoded, err := StandardOnchainConfigCodec{}.Decode(encoded) - if err != nil { - return - } - - encoded2, err := StandardOnchainConfigCodec{}.Encode(decoded) - if err != nil { - t.Fatalf("failed to re-encode decoded input: %s", err) - } - - if !bytes.Equal(encoded, encoded2) { - t.Fatalf("re-encoding of decoded input %x did not match original input %x", encoded2, encoded) - } - }) -} diff --git a/pkg/reportingplugins/mercury/v1/aggregate_functions.go b/pkg/reportingplugins/mercury/v1/aggregate_functions.go deleted file mode 100644 index 3d7e72abf..000000000 --- a/pkg/reportingplugins/mercury/v1/aggregate_functions.go +++ /dev/null @@ -1,126 +0,0 @@ -package mercury_v1 //nolint:revive - -import ( - "fmt" - "sort" -) - -// GetConsensusLatestBlock gets the latest block that has at least f+1 votes -// Assumes that LatestBlocks are ordered by block number desc -func GetConsensusLatestBlock(paos []PAO, f int) (hash []byte, num int64, ts uint64, err error) { - // observed blocks grouped by their block number - groupingsM := make(map[int64][]Block) - for _, pao := range paos { - if blocks := pao.GetLatestBlocks(); len(blocks) > 0 { - for _, block := range blocks { - groupingsM[block.Num] = append(groupingsM[block.Num], block) - } - } else { // DEPRECATED - // TODO: Remove this handling after deployment (https://smartcontract-it.atlassian.net/browse/MERC-2272) - blockHash, valid := pao.GetCurrentBlockHash() - if !valid { - continue - } - blockNum, valid := pao.GetCurrentBlockNum() - if !valid { - continue - } - blockTs, valid := pao.GetCurrentBlockTimestamp() - if !valid { - continue - } - groupingsM[blockNum] = append(groupingsM[blockNum], NewBlock(blockNum, blockHash, blockTs)) - } - } - - // sort by latest block number desc - groupings := make([][]Block, len(groupingsM)) - { - i := 0 - for _, blocks := range groupingsM { - groupings[i] = blocks - i++ - } - } - - // each grouping will have all blocks with the same block number, sorted desc - sort.Slice(groupings, func(i, j int) bool { - return groupings[i][0].Num > groupings[j][0].Num - }) - - // take highest block number with at least f+1 in agreement on everything - for _, blocks := range groupings { - m := map[Block]int{} - maxCnt := 0 - // count unique blocks - for _, b := range blocks { - m[b]++ - if cnt := m[b]; cnt > maxCnt { - maxCnt = cnt - } - } - if maxCnt >= f+1 { - // at least one set of blocks has f+1 in agreement - - // take the blocks with highest count - var usableBlocks []Block - for b, cnt := range m { - if cnt == maxCnt { - usableBlocks = append(usableBlocks, b) - } - } - sort.Slice(usableBlocks, func(i, j int) bool { - return usableBlocks[j].less(usableBlocks[i]) - }) - - return usableBlocks[0].HashBytes(), usableBlocks[0].Num, usableBlocks[0].Ts, nil - } - // this grouping does not have any identical blocks with at least f+1 in agreement, try next block number down - } - - return nil, 0, 0, fmt.Errorf("cannot come to consensus on latest block number, got observations: %#v", paos) -} - -// GetConsensusMaxFinalizedBlockNum gets the most common (mode) -// ConsensusMaxFinalizedBlockNum In the event of a tie, the lower number is -// chosen -func GetConsensusMaxFinalizedBlockNum(paos []PAO, f int) (int64, error) { - var validPaos []PAO - for _, pao := range paos { - _, valid := pao.GetMaxFinalizedBlockNumber() - if valid { - validPaos = append(validPaos, pao) - } - } - if len(validPaos) < f+1 { - return 0, fmt.Errorf("fewer than f+1 observations have a valid maxFinalizedBlockNumber (got: %d/%d, f=%d)", len(validPaos), len(paos), f) - } - // pick the most common block number with at least f+1 votes - m := map[int64]int{} - maxCnt := 0 - for _, pao := range validPaos { - n, _ := pao.GetMaxFinalizedBlockNumber() - m[n]++ - if cnt := m[n]; cnt > maxCnt { - maxCnt = cnt - } - } - - var nums []int64 - for num, cnt := range m { - if cnt == maxCnt { - nums = append(nums, num) - } - } - - if maxCnt < f+1 { - return 0, fmt.Errorf("no valid maxFinalizedBlockNumber with at least f+1 votes (got counts: %v, f=%d)", m, f) - } - // guaranteed to be at least one num after this - - // determistic tie-break for number - sort.Slice(nums, func(i, j int) bool { - return nums[i] < nums[j] - }) - return nums[0], nil -} diff --git a/pkg/reportingplugins/mercury/v1/aggregate_functions_test.go b/pkg/reportingplugins/mercury/v1/aggregate_functions_test.go deleted file mode 100644 index 7c8dcf4f9..000000000 --- a/pkg/reportingplugins/mercury/v1/aggregate_functions_test.go +++ /dev/null @@ -1,666 +0,0 @@ -package mercury_v1 //nolint:revive - -import ( - "encoding/hex" - "math/big" - "testing" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mustDecodeHex(s string) []byte { - b, err := hex.DecodeString(s) - if err != nil { - panic(err) - } - return b -} - -var ChainViewBase = []Block{ - NewBlock(16634362, mustDecodeHex("6f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908172), - NewBlock(16634361, mustDecodeHex("5f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908170), -} - -// ChainView1 vs ChainView2 simulates a re-org based off of a common block 16634362 -func MakeChainView1() []Block { - return append([]Block{ - NewBlock(16634365, mustDecodeHex("9f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908181), - NewBlock(16634364, mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908180), - NewBlock(16634363, mustDecodeHex("7f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908176), - }, ChainViewBase...) -} - -var ChainView2 = append([]Block{ - NewBlock(16634365, mustDecodeHex("8e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908180), - NewBlock(16634364, mustDecodeHex("7e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908177), - NewBlock(16634363, mustDecodeHex("6e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908173), -}, ChainViewBase...) - -var ChainView3 = append([]Block{ - NewBlock(16634366, mustDecodeHex("9e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908181), - NewBlock(16634365, mustDecodeHex("8e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908180), - NewBlock(16634364, mustDecodeHex("7e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908177), - NewBlock(16634363, mustDecodeHex("6e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908173), -}, ChainViewBase...) - -func NewRawPAOS() []parsedAttributedObservation { - return []parsedAttributedObservation{ - parsedAttributedObservation{ - Timestamp: 1676484822, - Observer: commontypes.OracleID(1), - - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(343), - Ask: big.NewInt(347), - PricesValid: true, - - LatestBlocks: []Block{}, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484826, - Observer: commontypes.OracleID(2), - - BenchmarkPrice: big.NewInt(335), - Bid: big.NewInt(332), - Ask: big.NewInt(336), - PricesValid: true, - - CurrentBlockNum: 16634364, - CurrentBlockHash: mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), - CurrentBlockTimestamp: 1682908180, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484828, - Observer: commontypes.OracleID(3), - - BenchmarkPrice: big.NewInt(347), - Bid: big.NewInt(345), - Ask: big.NewInt(350), - PricesValid: true, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484830, - Observer: commontypes.OracleID(4), - - BenchmarkPrice: big.NewInt(346), - Bid: big.NewInt(347), - Ask: big.NewInt(350), - PricesValid: true, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - } -} - -func NewValidLegacyParsedAttributedObservations() []PAO { - return []PAO{ - parsedAttributedObservation{ - Timestamp: 1676484822, - Observer: commontypes.OracleID(1), - - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(343), - Ask: big.NewInt(347), - PricesValid: true, - - CurrentBlockNum: 16634364, - CurrentBlockHash: mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), - CurrentBlockTimestamp: 1682908180, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484826, - Observer: commontypes.OracleID(2), - - BenchmarkPrice: big.NewInt(335), - Bid: big.NewInt(332), - Ask: big.NewInt(336), - PricesValid: true, - - CurrentBlockNum: 16634364, - CurrentBlockHash: mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), - CurrentBlockTimestamp: 1682908180, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484828, - Observer: commontypes.OracleID(3), - - BenchmarkPrice: big.NewInt(347), - Bid: big.NewInt(345), - Ask: big.NewInt(350), - PricesValid: true, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484830, - Observer: commontypes.OracleID(4), - - BenchmarkPrice: big.NewInt(346), - Bid: big.NewInt(347), - Ask: big.NewInt(350), - PricesValid: true, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - } -} - -func NewInvalidParsedAttributedObservations() []PAO { - return []PAO{ - parsedAttributedObservation{ - Timestamp: 1676484822, - Observer: commontypes.OracleID(1), - - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(343), - Ask: big.NewInt(347), - PricesValid: false, - - CurrentBlockNum: 16634364, - CurrentBlockHash: mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), - CurrentBlockTimestamp: 1682908180, - CurrentBlockValid: false, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1676484826, - Observer: commontypes.OracleID(2), - - BenchmarkPrice: big.NewInt(335), - Bid: big.NewInt(332), - Ask: big.NewInt(336), - PricesValid: false, - - CurrentBlockNum: 16634364, - CurrentBlockHash: mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), - CurrentBlockTimestamp: 1682908180, - CurrentBlockValid: false, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1676484828, - Observer: commontypes.OracleID(3), - - BenchmarkPrice: big.NewInt(347), - Bid: big.NewInt(345), - Ask: big.NewInt(350), - PricesValid: false, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: false, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1676484830, - Observer: commontypes.OracleID(4), - - BenchmarkPrice: big.NewInt(346), - Bid: big.NewInt(347), - Ask: big.NewInt(350), - PricesValid: false, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: false, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: false, - }, - } -} - -func Test_AggregateFunctions(t *testing.T) { - f := 1 - invalidPaos := NewInvalidParsedAttributedObservations() - validLegacyPaos := NewValidLegacyParsedAttributedObservations() - - t.Run("GetConsensusLatestBlock", func(t *testing.T) { - makePAO := func(blocks []Block) PAO { - return parsedAttributedObservation{LatestBlocks: blocks} - } - - makeLegacyPAO := func(num int64, hash string, ts uint64) PAO { - return parsedAttributedObservation{CurrentBlockNum: num, CurrentBlockHash: mustDecodeHex(hash), CurrentBlockTimestamp: ts, CurrentBlockValid: true} - } - - t.Run("when all paos are using legacy 'current block'", func(t *testing.T) { - t.Run("succeeds in the valid case", func(t *testing.T) { - hash, num, ts, err := GetConsensusLatestBlock(validLegacyPaos, f) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, uint64(1682591344), ts) - }) - - t.Run("if invalid, fails", func(t *testing.T) { - _, _, _, err := GetConsensusLatestBlock(invalidPaos, f) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - t.Run("if there are not at least f+1 in consensus about hash", func(t *testing.T) { - _, _, _, err := GetConsensusLatestBlock(validLegacyPaos, 2) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - _, _, _, err = GetConsensusLatestBlock(validLegacyPaos, 3) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - t.Run("if there are not at least f+1 in consensus about block number", func(t *testing.T) { - badPaos := []PAO{ - parsedAttributedObservation{ - CurrentBlockNum: 100, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockNum: 200, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockNum: 300, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockNum: 400, - CurrentBlockValid: true, - }, - } - _, _, _, err := GetConsensusLatestBlock(badPaos, f) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - t.Run("if there are not at least f+1 in consensus about timestamp", func(t *testing.T) { - badPaos := []PAO{ - parsedAttributedObservation{ - CurrentBlockTimestamp: 100, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockTimestamp: 200, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockTimestamp: 300, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockTimestamp: 400, - CurrentBlockValid: true, - }, - } - _, _, _, err := GetConsensusLatestBlock(badPaos, f) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - t.Run("in the event of an even split for block number/hash, take the higher block number", func(t *testing.T) { - validFrom := int64(26014056) - // values below are from a real observed case of this happening in the wild - paos := []PAO{ - parsedAttributedObservation{ - Timestamp: 1686759784, - Observer: commontypes.OracleID(2), - BenchmarkPrice: big.NewInt(90700), - Bid: big.NewInt(26200), - Ask: big.NewInt(17500), - PricesValid: true, - CurrentBlockNum: 26014055, - CurrentBlockHash: mustDecodeHex("1a2b96ef9a29614c9fc4341a5ca6690ed8ee1a2cd6b232c90ba8bea65a4b93b5"), - CurrentBlockTimestamp: 1686759784, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: 0, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1686759784, - Observer: commontypes.OracleID(3), - BenchmarkPrice: big.NewInt(92000), - Bid: big.NewInt(21300), - Ask: big.NewInt(74700), - PricesValid: true, - CurrentBlockNum: 26014056, - CurrentBlockHash: mustDecodeHex("bdeb0181416f88812028c4e1ee9e049296c909c1ee15d57cf67d4ce869ed6518"), - CurrentBlockTimestamp: 1686759784, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: 0, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1686759784, - Observer: commontypes.OracleID(1), - BenchmarkPrice: big.NewInt(67300), - Bid: big.NewInt(70100), - Ask: big.NewInt(83200), - PricesValid: true, - CurrentBlockNum: 26014056, - CurrentBlockHash: mustDecodeHex("bdeb0181416f88812028c4e1ee9e049296c909c1ee15d57cf67d4ce869ed6518"), - CurrentBlockTimestamp: 1686759784, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: 0, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1686759784, - Observer: commontypes.OracleID(0), - BenchmarkPrice: big.NewInt(8600), - Bid: big.NewInt(89100), - Ask: big.NewInt(53300), - PricesValid: true, - CurrentBlockNum: 26014055, - CurrentBlockHash: mustDecodeHex("1a2b96ef9a29614c9fc4341a5ca6690ed8ee1a2cd6b232c90ba8bea65a4b93b5"), - CurrentBlockTimestamp: 1686759784, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: 0, - MaxFinalizedBlockNumberValid: false, - }, - } - hash, num, _, err := GetConsensusLatestBlock(paos, f) - assert.NoError(t, err) - assert.Equal(t, mustDecodeHex("bdeb0181416f88812028c4e1ee9e049296c909c1ee15d57cf67d4ce869ed6518"), hash) - assert.Equal(t, int64(26014056), num) - assert.GreaterOrEqual(t, num, validFrom) - }) - t.Run("when there are multiple possible blocks meeting > f+1 hashes, takes the hash with the most block numbers in agreement", func(t *testing.T) { - paos := []PAO{ - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(41, "3333333333333333333333333333333333333333333333333333333333333333", 0), - makeLegacyPAO(41, "3333333333333333333333333333333333333333333333333333333333333333", 0), - makeLegacyPAO(41, "3333333333333333333333333333333333333333333333333333333333333333", 0), - makeLegacyPAO(42, "1111111111111111111111111111111111111111111111111111111111111111", 1), - makeLegacyPAO(42, "1111111111111111111111111111111111111111111111111111111111111111", 1), - makeLegacyPAO(41, "1111111111111111111111111111111111111111111111111111111111111111", 1), - makeLegacyPAO(43, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - assert.NoError(t, err) - assert.Equal(t, mustDecodeHex("3333333333333333333333333333333333333333333333333333333333333333"), hash) - assert.Equal(t, int64(42), num) - assert.Equal(t, uint64(1), ts) - }) - t.Run("in the event of an even split of numbers/hashes, takes the hash with the highest block number", func(t *testing.T) { - paos := []PAO{ - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(41, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(41, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(41, "2222222222222222222222222222222222222222222222222222222222222222", 1), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - assert.NoError(t, err) - assert.Equal(t, mustDecodeHex("3333333333333333333333333333333333333333333333333333333333333333"), hash) - assert.Equal(t, int64(42), num) - assert.Equal(t, uint64(1), ts) - }) - t.Run("in the case where all block numbers are equal but timestamps differ, tie-breaks on latest timestamp", func(t *testing.T) { - paos := []PAO{ - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 2), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 2), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 2), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - assert.NoError(t, err) - assert.Equal(t, mustDecodeHex("3333333333333333333333333333333333333333333333333333333333333333"), hash) - assert.Equal(t, int64(42), num) - assert.Equal(t, uint64(2), ts) - }) - t.Run("in the case where all block numbers and timestamps are equal, tie-breaks by taking the 'lowest' hash", func(t *testing.T) { - paos := []PAO{ - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - assert.NoError(t, err) - assert.Equal(t, mustDecodeHex("2222222222222222222222222222222222222222222222222222222222222222"), hash) - assert.Equal(t, int64(42), num) - assert.Equal(t, uint64(1), ts) - }) - }) - - t.Run("when there is a mix of PAOS, some with legacy 'current block' and some with LatestBlocks", func(t *testing.T) { - t.Run("succeeds in the valid case where all agree", func(t *testing.T) { - cv := MakeChainView1() - paos := []PAO{ - makePAO(cv), - makePAO(cv), - makeLegacyPAO(cv[0].Num, hex.EncodeToString([]byte(cv[0].Hash)), cv[0].Ts), - makeLegacyPAO(cv[0].Num, hex.EncodeToString([]byte(cv[0].Hash)), cv[0].Ts), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("9f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, 1682908181, int(ts)) - }) - - t.Run("succeeds in the valid case with two different chain views, and returns the highest common block with f+1 observations", func(t *testing.T) { - cv := MakeChainView1() - cv2 := ChainView2 - paos := []PAO{ - makePAO(cv[1:]), - makePAO(cv2), - makeLegacyPAO(cv[3].Num, hex.EncodeToString([]byte(cv[3].Hash)), cv[3].Ts), - makeLegacyPAO(cv2[0].Num, hex.EncodeToString([]byte(cv2[0].Hash)), cv2[0].Ts), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, 1) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("8e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, 1682908180, int(ts)) - - hash, num, ts, err = GetConsensusLatestBlock(paos, 2) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("6f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634362, int(num)) - assert.Equal(t, 1682908172, int(ts)) - }) - }) - - t.Run("when all PAOS are using LatestBlocks", func(t *testing.T) { - t.Run("succeeds in the valid case where all agree", func(t *testing.T) { - paos := []PAO{ - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("9f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, 1682908181, int(ts)) - }) - - t.Run("succeeds in the valid case with two different chain views, and returns the highest common block with f+1 observations", func(t *testing.T) { - paos := []PAO{ - makePAO(ChainView2), - makePAO(ChainView2), - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, 3) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("6f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634362, int(num)) - assert.Equal(t, 1682908172, int(ts)) - }) - - t.Run("succeeds in the case with many different chain views, and returns the highest common block with f+1 observations", func(t *testing.T) { - paos := []PAO{ - makePAO(ChainView3), - makePAO(ChainView2), - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, 1) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("9f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, 1682908181, int(ts)) - }) - - t.Run("takes highest with at least f+1 when some observations are behind", func(t *testing.T) { - paos := []PAO{ - makePAO(MakeChainView1()[2:]), - makePAO(MakeChainView1()[1:]), - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, 3) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("7f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634363, int(num)) - assert.Equal(t, 1682908176, int(ts)) - }) - - t.Run("tie-breaks using smaller hash", func(t *testing.T) { - cv1 := MakeChainView1()[0:2] - cv2 := MakeChainView1()[0:1] - cv3 := MakeChainView1()[0:3] - cv4 := MakeChainView1()[0:3] - - cv1[0].Hash = string(mustDecodeHex("0000000000000000000000000000000000000000000000000000000000000000")) - cv4[0].Hash = string(mustDecodeHex("0000000000000000000000000000000000000000000000000000000000000000")) - - paos := []PAO{ - makePAO(cv1), - makePAO(cv2), - makePAO(cv3), - makePAO(cv4), - } - - hash, num, ts, err := GetConsensusLatestBlock(paos, 1) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("0000000000000000000000000000000000000000000000000000000000000000"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, 1682908181, int(ts)) - }) - - t.Run("fails in the case where there is no common block with at least f+1 observations", func(t *testing.T) { - paos := []PAO{ - makePAO(ChainView2[0:3]), - makePAO(ChainView2[0:3]), - makePAO(MakeChainView1()[0:3]), - makePAO(MakeChainView1()[0:3]), - } - _, _, _, err := GetConsensusLatestBlock(paos, 3) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - - t.Run("if invalid, fails", func(t *testing.T) { - _, _, _, err := GetConsensusLatestBlock(invalidPaos, f) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - }) - }) - - t.Run("GetConsensusMaxFinalizedBlockNum", func(t *testing.T) { - t.Run("in the valid case", func(t *testing.T) { - num, err := GetConsensusMaxFinalizedBlockNum(validLegacyPaos, f) - - require.NoError(t, err) - assert.Equal(t, 16634355, int(num)) - }) - - t.Run("errors if there are not at least f+1 valid", func(t *testing.T) { - _, err := GetConsensusMaxFinalizedBlockNum(invalidPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid maxFinalizedBlockNumber (got: 0/4, f=1)") - }) - - t.Run("errors if there are not at least f+1 in consensus about number", func(t *testing.T) { - badPaos := []PAO{ - parsedAttributedObservation{ - MaxFinalizedBlockNumber: 100, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - MaxFinalizedBlockNumber: 200, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - MaxFinalizedBlockNumber: 300, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - MaxFinalizedBlockNumber: 400, - MaxFinalizedBlockNumberValid: true, - }, - } - - _, err := GetConsensusMaxFinalizedBlockNum(badPaos, f) - assert.EqualError(t, err, "no valid maxFinalizedBlockNumber with at least f+1 votes (got counts: map[100:1 200:1 300:1 400:1], f=1)") - }) - }) -} diff --git a/pkg/reportingplugins/mercury/v1/mercury.go b/pkg/reportingplugins/mercury/v1/mercury.go deleted file mode 100644 index cf6250b78..000000000 --- a/pkg/reportingplugins/mercury/v1/mercury.go +++ /dev/null @@ -1,466 +0,0 @@ -package mercury_v1 //nolint:revive - -import ( - "context" - "errors" - "fmt" - "math/big" - "sort" - "time" - - pkgerrors "github.com/pkg/errors" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" -) - -// MaxAllowedBlocks indicates the maximum len of LatestBlocks in any given observation. -// observations that violate this will be discarded -const MaxAllowedBlocks = 5 - -// Mercury-specific reporting plugin, based off of median: -// https://github.com/smartcontractkit/offchain-reporting/blob/master/lib/offchainreporting2/reportingplugin/median/median.go - -type Observation struct { - BenchmarkPrice mercury.ObsResult[*big.Int] - Bid mercury.ObsResult[*big.Int] - Ask mercury.ObsResult[*big.Int] - - CurrentBlockNum mercury.ObsResult[int64] - CurrentBlockHash mercury.ObsResult[[]byte] - CurrentBlockTimestamp mercury.ObsResult[uint64] - - LatestBlocks []Block - - // MaxFinalizedBlockNumber comes from previous report when present and is - // only observed from mercury server when previous report is nil - MaxFinalizedBlockNumber mercury.ObsResult[int64] -} - -// DataSource implementations must be thread-safe. Observe may be called by many -// different threads concurrently. -type DataSource interface { - // Observe queries the data source. Returns a value or an error. Once the - // context is expires, Observe may still do cheap computations and return a - // result, but should return as quickly as possible. - // - // More details: In the current implementation, the context passed to - // Observe will time out after MaxDurationObservation. However, Observe - // should *not* make any assumptions about context timeout behavior. Once - // the context times out, Observe should prioritize returning as quickly as - // possible, but may still perform fast computations to return a result - // rather than error. For example, if Observe medianizes a number of data - // sources, some of which already returned a result to Observe prior to the - // context's expiry, Observe might still compute their median, and return it - // instead of an error. - // - // Important: Observe should not perform any potentially time-consuming - // actions like database access, once the context passed has expired. - Observe(ctx context.Context, repts ocrtypes.ReportTimestamp, fetchMaxFinalizedBlockNum bool) (Observation, error) -} - -var _ ocr3types.MercuryPluginFactory = Factory{} - -// Maximum length in bytes of Observation, Report returned by the -// MercuryPlugin. Used for defending against spam attacks. -const maxObservationLength = 4 + // timestamp - mercury.ByteWidthInt192 + // benchmarkPrice - mercury.ByteWidthInt192 + // bid - mercury.ByteWidthInt192 + // ask - 1 + // pricesValid - 8 + // currentBlockNum - 32 + // currentBlockHash - 8 + // currentBlockTimestamp - 1 + // currentBlockValid - 8 + // maxFinalizedBlockNumber - 1 + // maxFinalizedBlockNumberValid - 32 + // [> overapprox. of protobuf overhead <] - MaxAllowedBlocks*(8+ // num - 32+ // hash - 8+ // ts - 32) // [> overapprox. of protobuf overhead <] - -type Factory struct { - dataSource DataSource - logger logger.Logger - onchainConfigCodec mercury.OnchainConfigCodec - reportCodec ReportCodec -} - -func NewFactory(ds DataSource, lggr logger.Logger, occ mercury.OnchainConfigCodec, rc ReportCodec) Factory { - return Factory{ds, lggr, occ, rc} -} - -func (fac Factory) NewMercuryPlugin(configuration ocr3types.MercuryPluginConfig) (ocr3types.MercuryPlugin, ocr3types.MercuryPluginInfo, error) { - offchainConfig, err := mercury.DecodeOffchainConfig(configuration.OffchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - onchainConfig, err := fac.onchainConfigCodec.Decode(configuration.OnchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - maxReportLength, err := fac.reportCodec.MaxReportLength(configuration.N) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - r := &reportingPlugin{ - offchainConfig, - onchainConfig, - fac.dataSource, - fac.logger, - fac.reportCodec, - configuration.ConfigDigest, - configuration.F, - mercury.EpochRound{}, - maxReportLength, - } - - return r, ocr3types.MercuryPluginInfo{ - Name: "Mercury", - Limits: ocr3types.MercuryPluginLimits{ - MaxObservationLength: maxObservationLength, - MaxReportLength: maxReportLength, - }, - }, nil -} - -var _ ocr3types.MercuryPlugin = (*reportingPlugin)(nil) - -type reportingPlugin struct { - offchainConfig mercury.OffchainConfig - onchainConfig mercury.OnchainConfig - dataSource DataSource - logger logger.Logger - reportCodec ReportCodec - - configDigest ocrtypes.ConfigDigest - f int - latestAcceptedEpochRound mercury.EpochRound - maxReportLength int -} - -func (rp *reportingPlugin) Observation(ctx context.Context, repts ocrtypes.ReportTimestamp, previousReport types.Report) (ocrtypes.Observation, error) { - obs, err := rp.dataSource.Observe(ctx, repts, previousReport == nil) - if err != nil { - return nil, pkgerrors.Errorf("DataSource.Observe returned an error: %s", err) - } - - p := MercuryObservationProto{Timestamp: uint32(time.Now().Unix())} - - var obsErrors []error - if previousReport == nil { - // if previousReport we fall back to the observed MaxFinalizedBlockNumber - if obs.MaxFinalizedBlockNumber.Err != nil { - obsErrors = append(obsErrors, err) - } else if obs.CurrentBlockNum.Err == nil && obs.CurrentBlockNum.Val < obs.MaxFinalizedBlockNumber.Val { - obsErrors = append(obsErrors, pkgerrors.Errorf("failed to observe ValidFromBlockNum; current block number %d (hash: 0x%x) < max finalized block number %d; ignoring observation for out-of-date RPC", obs.CurrentBlockNum.Val, obs.CurrentBlockHash.Val, obs.MaxFinalizedBlockNumber.Val)) - } else { - p.MaxFinalizedBlockNumber = obs.MaxFinalizedBlockNumber.Val // MaxFinalizedBlockNumber comes as -1 if unset - p.MaxFinalizedBlockNumberValid = true - } - } - - var bpErr, bidErr, askErr error - if obs.BenchmarkPrice.Err != nil { - bpErr = pkgerrors.Wrap(obs.BenchmarkPrice.Err, "failed to observe BenchmarkPrice") - obsErrors = append(obsErrors, bpErr) - } else if benchmarkPrice, err := mercury.EncodeValueInt192(obs.BenchmarkPrice.Val); err != nil { - bpErr = pkgerrors.Wrap(err, "failed to observe BenchmarkPrice; encoding failed") - obsErrors = append(obsErrors, bpErr) - } else { - p.BenchmarkPrice = benchmarkPrice - } - - if obs.Bid.Err != nil { - bidErr = pkgerrors.Wrap(obs.Bid.Err, "failed to observe Bid") - obsErrors = append(obsErrors, bidErr) - } else if bid, err := mercury.EncodeValueInt192(obs.Bid.Val); err != nil { - bidErr = pkgerrors.Wrap(err, "failed to observe Bid; encoding failed") - obsErrors = append(obsErrors, bidErr) - } else { - p.Bid = bid - } - - if obs.Ask.Err != nil { - askErr = pkgerrors.Wrap(obs.Ask.Err, "failed to observe Ask") - obsErrors = append(obsErrors, askErr) - } else if ask, err := mercury.EncodeValueInt192(obs.Ask.Val); err != nil { - askErr = pkgerrors.Wrap(err, "failed to observe Ask; encoding failed") - obsErrors = append(obsErrors, askErr) - } else { - p.Ask = ask - } - - if bpErr == nil && bidErr == nil && askErr == nil { - p.PricesValid = true - } - - if obs.CurrentBlockNum.Err != nil { - obsErrors = append(obsErrors, pkgerrors.Wrap(obs.CurrentBlockNum.Err, "failed to observe CurrentBlockNum")) - } else { - p.CurrentBlockNum = obs.CurrentBlockNum.Val - } - - if obs.CurrentBlockHash.Err != nil { - obsErrors = append(obsErrors, pkgerrors.Wrap(obs.CurrentBlockHash.Err, "failed to observe CurrentBlockHash")) - } else { - p.CurrentBlockHash = obs.CurrentBlockHash.Val - } - - if obs.CurrentBlockTimestamp.Err != nil { - obsErrors = append(obsErrors, pkgerrors.Wrap(obs.CurrentBlockTimestamp.Err, "failed to observe CurrentBlockTimestamp")) - } else { - p.CurrentBlockTimestamp = obs.CurrentBlockTimestamp.Val - } - - if obs.CurrentBlockNum.Err == nil && obs.CurrentBlockHash.Err == nil && obs.CurrentBlockTimestamp.Err == nil { - p.CurrentBlockValid = true - } - - if len(obsErrors) > 0 { - rp.logger.Warnw(fmt.Sprintf("Observe failed %d/6 observations", len(obsErrors)), "err", errors.Join(obsErrors...)) - } - - p.LatestBlocks = make([]*BlockProto, len(obs.LatestBlocks)) - for i, b := range obs.LatestBlocks { - p.LatestBlocks[i] = &BlockProto{Num: b.Num, Hash: []byte(b.Hash), Ts: b.Ts} - } - if len(p.LatestBlocks) == 0 { - rp.logger.Warn("Observation had no LatestBlocks") - } - - return proto.Marshal(&p) -} - -func parseAttributedObservation(ao ocrtypes.AttributedObservation) (PAO, error) { - var pao parsedAttributedObservation - var obs MercuryObservationProto - if err := proto.Unmarshal(ao.Observation, &obs); err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("attributed observation cannot be unmarshaled: %s", err) - } - - pao.Timestamp = obs.Timestamp - pao.Observer = ao.Observer - - if obs.PricesValid { - var err error - pao.BenchmarkPrice, err = mercury.DecodeValueInt192(obs.BenchmarkPrice) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("benchmarkPrice cannot be converted to big.Int: %s", err) - } - pao.Bid, err = mercury.DecodeValueInt192(obs.Bid) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("bid cannot be converted to big.Int: %s", err) - } - pao.Ask, err = mercury.DecodeValueInt192(obs.Ask) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("ask cannot be converted to big.Int: %s", err) - } - pao.PricesValid = true - } - - if len(obs.LatestBlocks) > 0 { - if len(obs.LatestBlocks) > MaxAllowedBlocks { - return parsedAttributedObservation{}, pkgerrors.Errorf("LatestBlocks too large; got: %d, max: %d", len(obs.LatestBlocks), MaxAllowedBlocks) - } - for _, b := range obs.LatestBlocks { - pao.LatestBlocks = append(pao.LatestBlocks, NewBlock(b.Num, b.Hash, b.Ts)) - - // Ignore observation if it has duplicate blocks by number or hash - // for security to avoid the case where one node can "throw" block - // numbers by including a bunch of duplicates - nums := make(map[int64]struct{}, len(pao.LatestBlocks)) - hashes := make(map[string]struct{}, len(pao.LatestBlocks)) - for _, block := range pao.LatestBlocks { - if _, exists := nums[block.Num]; exists { - return parsedAttributedObservation{}, pkgerrors.Errorf("observation invalid for observer %d; got duplicate block number: %d", ao.Observer, block.Num) - } - if _, exists := hashes[block.Hash]; exists { - return parsedAttributedObservation{}, pkgerrors.Errorf("observation invalid for observer %d; got duplicate block hash: 0x%x", ao.Observer, block.HashBytes()) - } - nums[block.Num] = struct{}{} - hashes[block.Hash] = struct{}{} - - if len(block.Hash) != mercury.EvmHashLen { - return parsedAttributedObservation{}, pkgerrors.Errorf("wrong len for hash: %d (expected: %d)", len(block.Hash), mercury.EvmHashLen) - } - if block.Num < 0 { - return parsedAttributedObservation{}, pkgerrors.Errorf("negative block number: %d", block.Num) - } - } - - // sort desc - sort.SliceStable(pao.LatestBlocks, func(i, j int) bool { - // NOTE: This ought to be redundant since observing nodes - // should give us the blocks pre-sorted, but is included here - // for safety - return pao.LatestBlocks[j].less(pao.LatestBlocks[i]) - }) - } - } else if obs.CurrentBlockValid { - // DEPRECATED - // TODO: Remove this handling after deployment (https://smartcontract-it.atlassian.net/browse/MERC-2272) - if len(obs.CurrentBlockHash) != mercury.EvmHashLen { - return parsedAttributedObservation{}, pkgerrors.Errorf("wrong len for hash: %d (expected: %d)", len(obs.CurrentBlockHash), mercury.EvmHashLen) - } - pao.CurrentBlockHash = obs.CurrentBlockHash - if obs.CurrentBlockNum < 0 { - return parsedAttributedObservation{}, pkgerrors.Errorf("negative block number: %d", obs.CurrentBlockNum) - } - pao.CurrentBlockNum = obs.CurrentBlockNum - pao.CurrentBlockTimestamp = obs.CurrentBlockTimestamp - pao.CurrentBlockValid = true - } - - if obs.MaxFinalizedBlockNumberValid { - pao.MaxFinalizedBlockNumber = obs.MaxFinalizedBlockNumber - pao.MaxFinalizedBlockNumberValid = true - } - - return pao, nil -} - -func parseAttributedObservations(lggr logger.Logger, aos []ocrtypes.AttributedObservation) []PAO { - paos := make([]PAO, 0, len(aos)) - for i, ao := range aos { - pao, err := parseAttributedObservation(ao) - if err != nil { - lggr.Warnw("parseAttributedObservations: dropping invalid observation", - "observer", ao.Observer, - "error", err, - "i", i, - ) - continue - } - paos = append(paos, pao) - } - return paos -} - -func (rp *reportingPlugin) Report(repts types.ReportTimestamp, previousReport types.Report, aos []types.AttributedObservation) (shouldReport bool, report types.Report, err error) { - paos := parseAttributedObservations(rp.logger, aos) - - if len(paos) == 0 { - return false, nil, errors.New("got zero valid attributed observations") - } - - // By assumption, we have at most f malicious oracles, so there should be at least f+1 valid paos - if !(rp.f+1 <= len(paos)) { - return false, nil, pkgerrors.Errorf("only received %v valid attributed observations, but need at least f+1 (%v)", len(paos), rp.f+1) - } - - rf, err := rp.buildReportFields(previousReport, paos) - if err != nil { - rp.logger.Errorw("failed to build report fields", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts, "err", err) - return false, nil, err - } - - if rf.CurrentBlockNum < rf.ValidFromBlockNum { - rp.logger.Debugw("shouldReport: no (overlap)", "currentBlockNum", rf.CurrentBlockNum, "validFromBlockNum", rf.ValidFromBlockNum, "repts", repts) - return false, nil, nil - } - - if err = rp.validateReport(rf); err != nil { - rp.logger.Errorw("shouldReport: no (validation error)", "reportFields", rf, "err", err, "repts", repts, "paos", paos) - return false, nil, err - } - rp.logger.Debugw("shouldReport: yes", - "timestamp", repts, - ) - - report, err = rp.reportCodec.BuildReport(rf) - if err != nil { - rp.logger.Debugw("failed to BuildReport", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts) - return false, nil, err - } - if !(len(report) <= rp.maxReportLength) { - return false, nil, pkgerrors.Errorf("report with len %d violates MaxReportLength limit set by ReportCodec (%d)", len(report), rp.maxReportLength) - } else if len(report) == 0 { - return false, nil, errors.New("report may not have zero length (invariant violation)") - } - - return true, report, nil -} - -func (rp *reportingPlugin) buildReportFields(previousReport types.Report, paos []PAO) (rf ReportFields, merr error) { - var err error - if previousReport != nil { - var maxFinalizedBlockNumber int64 - maxFinalizedBlockNumber, err = rp.reportCodec.CurrentBlockNumFromReport(previousReport) - if err != nil { - merr = errors.Join(merr, err) - } else { - rf.ValidFromBlockNum = maxFinalizedBlockNumber + 1 - } - } else { - var maxFinalizedBlockNumber int64 - maxFinalizedBlockNumber, err = GetConsensusMaxFinalizedBlockNum(paos, rp.f) - if err != nil { - merr = errors.Join(merr, err) - } else { - rf.ValidFromBlockNum = maxFinalizedBlockNumber + 1 - } - } - - mPaos := convert(paos) - - rf.Timestamp = mercury.GetConsensusTimestamp(mPaos) - - rf.BenchmarkPrice, err = mercury.GetConsensusBenchmarkPrice(mPaos, rp.f) - merr = errors.Join(merr, pkgerrors.Wrap(err, "GetConsensusBenchmarkPrice failed")) - - rf.Bid, err = mercury.GetConsensusBid(convertBid(paos), rp.f) - merr = errors.Join(merr, pkgerrors.Wrap(err, "GetConsensusBid failed")) - - rf.Ask, err = mercury.GetConsensusAsk(convertAsk(paos), rp.f) - merr = errors.Join(merr, pkgerrors.Wrap(err, "GetConsensusAsk failed")) - - rf.CurrentBlockHash, rf.CurrentBlockNum, rf.CurrentBlockTimestamp, err = GetConsensusLatestBlock(paos, rp.f) - merr = errors.Join(merr, pkgerrors.Wrap(err, "GetConsensusCurrentBlock failed")) - - return rf, merr -} - -func (rp *reportingPlugin) validateReport(rf ReportFields) error { - return errors.Join( - mercury.ValidateBetween("median benchmark price", rf.BenchmarkPrice, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateBetween("median bid", rf.Bid, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateBetween("median ask", rf.Ask, rp.onchainConfig.Min, rp.onchainConfig.Max), - ValidateCurrentBlock(rf), - ) -} - -func (rp *reportingPlugin) Close() error { - return nil -} - -// convert funcs are necessary because go is not smart enough to cast -// []interface1 to []interface2 even if interface1 is a superset of interface2 -func convert(pao []PAO) (ret []mercury.PAO) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertBid(pao []PAO) (ret []mercury.PAOBid) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertAsk(pao []PAO) (ret []mercury.PAOAsk) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} diff --git a/pkg/reportingplugins/mercury/v1/mercury_observation_v1.proto b/pkg/reportingplugins/mercury/v1/mercury_observation_v1.proto deleted file mode 100644 index 07645b18a..000000000 --- a/pkg/reportingplugins/mercury/v1/mercury_observation_v1.proto +++ /dev/null @@ -1,38 +0,0 @@ -syntax="proto3"; - -package mercury_v1; -option go_package = ".;mercury_v1"; - -message MercuryObservationProto { - uint32 timestamp = 1; - - // Prices - bytes benchmarkPrice = 2; - bytes bid = 3; - bytes ask = 4; - // All three prices must be valid, or none are (they all should come from one API query and hold invariant bid <= bm <= ask) - bool pricesValid = 5; - - // DEPRECATED: replaced by "latestBlocks" - // https://smartcontract-it.atlassian.net/browse/MERC-2272 - // Current block - int64 currentBlockNum = 6; - bytes currentBlockHash = 7; - uint64 currentBlockTimestamp = 8; - // All three block observations must be valid, or none are (they all come from the same block) - bool currentBlockValid = 9; - - // MaxFinalizedBlockNumber comes from previous report when present and is - // only observed from mercury server when previous report is nil - int64 maxFinalizedBlockNumber = 10; - bool maxFinalizedBlockNumberValid = 11; - - // Latest blocks - repeated BlockProto latestBlocks = 12; -} - -message BlockProto { - int64 num = 1; - bytes hash = 2; - uint64 ts = 3; -} diff --git a/pkg/reportingplugins/mercury/v1/mercury_test.go b/pkg/reportingplugins/mercury/v1/mercury_test.go deleted file mode 100644 index ac85d3a45..000000000 --- a/pkg/reportingplugins/mercury/v1/mercury_test.go +++ /dev/null @@ -1,975 +0,0 @@ -package mercury_v1 //nolint:revive - -import ( - "context" - crand "crypto/rand" - "fmt" - "math" - "math/big" - "math/rand" - "reflect" - "slices" - "testing" - "time" - - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" -) - -type testReportCodec struct { - currentBlock int64 - currentBlockErr error - builtReport ocrtypes.Report - buildReportShouldFail bool - - builtReportFields *ReportFields -} - -func (trc *testReportCodec) reset() { - trc.currentBlockErr = nil - trc.buildReportShouldFail = false - trc.builtReportFields = nil -} - -func (trc *testReportCodec) BuildReport(rf ReportFields) (ocrtypes.Report, error) { - if trc.buildReportShouldFail { - return nil, errors.New("buildReportShouldFail=true") - } - trc.builtReportFields = &rf - return trc.builtReport, nil -} - -func (trc *testReportCodec) MaxReportLength(n int) (int, error) { - return 8*32 + // feed ID - 32 + // timestamp - 192 + // benchmarkPrice - 192 + // bid - 192 + // ask - 64 + //currentBlockNum - 8*32 + // currentBlockHash - 64, // validFromBlockNum - nil -} - -func (trc *testReportCodec) CurrentBlockNumFromReport(types.Report) (int64, error) { - return trc.currentBlock, trc.currentBlockErr -} - -func newReportingPlugin(t *testing.T, codec *testReportCodec) *reportingPlugin { - maxReportLength, err := codec.MaxReportLength(4) - require.NoError(t, err) - return &reportingPlugin{ - f: 1, - onchainConfig: mercury.OnchainConfig{Min: big.NewInt(0), Max: big.NewInt(1000)}, - logger: logger.Test(t), - reportCodec: codec, - maxReportLength: maxReportLength, - } -} - -func newValidReportFields() ReportFields { - return ReportFields{ - BenchmarkPrice: big.NewInt(42), - Bid: big.NewInt(42), - Ask: big.NewInt(42), - CurrentBlockNum: 42, - ValidFromBlockNum: 42, - CurrentBlockHash: make([]byte, 32), - } -} - -func Test_ReportingPlugin_validateReport(t *testing.T) { - rp := newReportingPlugin(t, &testReportCodec{}) - rf := newValidReportFields() - - t.Run("reports if currentBlockNum > validFromBlockNum", func(t *testing.T) { - rf.CurrentBlockNum = 500 - rf.ValidFromBlockNum = 499 - err := rp.validateReport(rf) - require.NoError(t, err) - }) - t.Run("reports if currentBlockNum == validFromBlockNum", func(t *testing.T) { - rf.CurrentBlockNum = 500 - rf.ValidFromBlockNum = 500 - err := rp.validateReport(rf) - require.NoError(t, err) - }) - t.Run("does not report if currentBlockNum < validFromBlockNum", func(t *testing.T) { - rf.CurrentBlockNum = 499 - rf.ValidFromBlockNum = 500 - err := rp.validateReport(rf) - require.Error(t, err) - assert.Contains(t, err.Error(), "validFromBlockNum (Value: 500) must be less than or equal to CurrentBlockNum (Value: 499)") - }) -} - -var _ DataSource = &mockDataSource{} - -type mockDataSource struct{ obs Observation } - -func (m mockDataSource) Observe(context.Context, ocrtypes.ReportTimestamp, bool) (Observation, error) { - return m.obs, nil -} - -func randBigInt() *big.Int { - return big.NewInt(rand.Int63()) -} - -func randBytes(n int) []byte { - b := make([]byte, n) - _, err := crand.Read(b) - if err != nil { - panic(err) - } - return b -} - -func mustDecodeBigInt(b []byte) *big.Int { - n, err := mercury.DecodeValueInt192(b) - if err != nil { - panic(err) - } - return n -} - -func Test_Plugin_Observation(t *testing.T) { - ctx := context.Background() - repts := ocrtypes.ReportTimestamp{} - codec := &testReportCodec{ - currentBlock: int64(rand.Int31()), - builtReport: []byte{}, - } - - rp := newReportingPlugin(t, codec) - - t.Run("with previous report", func(t *testing.T) { - // content of previousReport is irrelevant, the only thing that matters - // for this test is that it's not nil - previousReport := ocrtypes.Report{} - - t.Run("when all observations are successful", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Bid: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Ask: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - CurrentBlockNum: mercury.ObsResult[int64]{ - Val: rand.Int63(), - }, - CurrentBlockHash: mercury.ObsResult[[]byte]{ - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercury.ObsResult[uint64]{ - Val: rand.Uint64(), - }, - LatestBlocks: []Block{ - Block{Num: rand.Int63(), Hash: string(randBytes(32)), Ts: rand.Uint64()}, - Block{Num: rand.Int63(), Hash: string(randBytes(32)), Ts: rand.Uint64()}, - Block{Num: rand.Int63(), Hash: string(randBytes(32)), Ts: rand.Uint64()}, - }, - } - - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, previousReport) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.Equal(t, obs.Bid.Val, mustDecodeBigInt(p.Bid)) - assert.Equal(t, obs.Ask.Val, mustDecodeBigInt(p.Ask)) - assert.Equal(t, obs.CurrentBlockNum.Val, p.CurrentBlockNum) - assert.Equal(t, obs.CurrentBlockHash.Val, p.CurrentBlockHash) - assert.Equal(t, obs.CurrentBlockTimestamp.Val, p.CurrentBlockTimestamp) - assert.Equal(t, len(obs.LatestBlocks), len(p.LatestBlocks)) - for i := range obs.LatestBlocks { - assert.Equal(t, obs.LatestBlocks[i].Num, p.LatestBlocks[i].Num) - assert.Equal(t, []byte(obs.LatestBlocks[i].Hash), p.LatestBlocks[i].Hash) - assert.Equal(t, obs.LatestBlocks[i].Ts, p.LatestBlocks[i].Ts) - } - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.Zero(t, p.MaxFinalizedBlockNumber) - - assert.True(t, p.PricesValid) - assert.True(t, p.CurrentBlockValid) - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.False(t, p.MaxFinalizedBlockNumberValid) - }) - - t.Run("when all observations have failed", func(t *testing.T) { - obs := Observation{ - // Vals should be ignored, this is asserted with .Zero below - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - Err: errors.New("benchmarkPrice exploded"), - }, - Bid: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - Err: errors.New("bid exploded"), - }, - Ask: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - Err: errors.New("ask exploded"), - }, - CurrentBlockNum: mercury.ObsResult[int64]{ - Err: errors.New("currentBlockNum exploded"), - Val: rand.Int63(), - }, - CurrentBlockHash: mercury.ObsResult[[]byte]{ - Err: errors.New("currentBlockHash exploded"), - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercury.ObsResult[uint64]{ - Err: errors.New("currentBlockTimestamp exploded"), - Val: rand.Uint64(), - }, - LatestBlocks: ([]Block)(nil), - } - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, previousReport) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.Zero(t, p.Bid) - assert.Zero(t, p.Ask) - assert.Zero(t, p.CurrentBlockNum) - assert.Zero(t, p.CurrentBlockHash) - assert.Zero(t, p.CurrentBlockTimestamp) - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.Zero(t, p.MaxFinalizedBlockNumber) - assert.Len(t, p.LatestBlocks, 0) - - assert.False(t, p.PricesValid) - assert.False(t, p.CurrentBlockValid) - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.False(t, p.MaxFinalizedBlockNumberValid) - }) - - t.Run("when some observations have failed", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Bid: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Ask: mercury.ObsResult[*big.Int]{ - Err: errors.New("ask exploded"), - }, - CurrentBlockNum: mercury.ObsResult[int64]{ - Err: errors.New("currentBlockNum exploded"), - }, - CurrentBlockHash: mercury.ObsResult[[]byte]{ - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercury.ObsResult[uint64]{ - Val: rand.Uint64(), - }, - LatestBlocks: []Block{ - Block{Num: rand.Int63(), Hash: string(randBytes(32)), Ts: rand.Uint64()}, - }, - } - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, previousReport) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.Equal(t, obs.Bid.Val, mustDecodeBigInt(p.Bid)) - assert.Zero(t, p.Ask) - assert.Zero(t, p.CurrentBlockNum) - assert.Equal(t, obs.CurrentBlockHash.Val, p.CurrentBlockHash) - assert.Equal(t, obs.CurrentBlockTimestamp.Val, p.CurrentBlockTimestamp) - assert.Equal(t, len(obs.LatestBlocks), len(p.LatestBlocks)) - for i := range obs.LatestBlocks { - assert.Equal(t, obs.LatestBlocks[i].Num, p.LatestBlocks[i].Num) - assert.Equal(t, []byte(obs.LatestBlocks[i].Hash), p.LatestBlocks[i].Hash) - assert.Equal(t, obs.LatestBlocks[i].Ts, p.LatestBlocks[i].Ts) - } - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.Zero(t, p.MaxFinalizedBlockNumber) - - assert.False(t, p.PricesValid) - assert.False(t, p.CurrentBlockValid) - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.False(t, p.MaxFinalizedBlockNumberValid) - }) - - t.Run("when encoding fails on some price observations", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - // too large to encode - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - Bid: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Ask: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - CurrentBlockNum: mercury.ObsResult[int64]{ - Val: rand.Int63(), - }, - CurrentBlockHash: mercury.ObsResult[[]byte]{ - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercury.ObsResult[uint64]{ - Val: rand.Uint64(), - }, - } - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, previousReport) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.False(t, p.PricesValid) - assert.Zero(t, p.BenchmarkPrice) - assert.NotZero(t, p.Bid) - assert.NotZero(t, p.Ask) - }) - t.Run("when encoding fails on all price observations", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - // too large to encode - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - Bid: mercury.ObsResult[*big.Int]{ - // too large to encode - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - Ask: mercury.ObsResult[*big.Int]{ - // too large to encode - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - CurrentBlockNum: mercury.ObsResult[int64]{ - Val: rand.Int63(), - }, - CurrentBlockHash: mercury.ObsResult[[]byte]{ - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercury.ObsResult[uint64]{ - Val: rand.Uint64(), - }, - } - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, previousReport) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.False(t, p.PricesValid) - assert.Zero(t, p.BenchmarkPrice) - assert.Zero(t, p.Bid) - assert.Zero(t, p.Ask) - }) - }) - - t.Run("without previous report, includes maxFinalizedBlockNumber observation", func(t *testing.T) { - currentBlockNum := int64(rand.Int31()) - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Bid: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Ask: mercury.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - CurrentBlockNum: mercury.ObsResult[int64]{ - Val: currentBlockNum, - }, - CurrentBlockHash: mercury.ObsResult[[]byte]{ - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercury.ObsResult[uint64]{ - Val: rand.Uint64(), - }, - MaxFinalizedBlockNumber: mercury.ObsResult[int64]{ - Val: currentBlockNum - 42, - }, - } - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.Equal(t, obs.Bid.Val, mustDecodeBigInt(p.Bid)) - assert.Equal(t, obs.Ask.Val, mustDecodeBigInt(p.Ask)) - assert.Equal(t, obs.CurrentBlockNum.Val, p.CurrentBlockNum) - assert.Equal(t, obs.CurrentBlockHash.Val, p.CurrentBlockHash) - assert.Equal(t, obs.CurrentBlockTimestamp.Val, p.CurrentBlockTimestamp) - assert.Equal(t, obs.MaxFinalizedBlockNumber.Val, p.MaxFinalizedBlockNumber) - - assert.True(t, p.PricesValid) - assert.True(t, p.CurrentBlockValid) - assert.True(t, p.MaxFinalizedBlockNumberValid) - }) -} - -func newAttributedObservation(t *testing.T, p *MercuryObservationProto) ocrtypes.AttributedObservation { - marshalledObs, err := proto.Marshal(p) - require.NoError(t, err) - return ocrtypes.AttributedObservation{ - Observation: ocrtypes.Observation(marshalledObs), - Observer: commontypes.OracleID(42), - } -} - -func newUnparseableAttributedObservation() ocrtypes.AttributedObservation { - return ocrtypes.AttributedObservation{ - Observation: []byte{1, 2}, - Observer: commontypes.OracleID(42), - } -} - -func genRandHash(seed int64) []byte { - r := rand.New(rand.NewSource(seed)) - - b := make([]byte, 32) - _, err := r.Read(b) - if err != nil { - panic(err) - } - return b -} - -func newValidMercuryObservationProto() *MercuryObservationProto { - var latestBlocks []*BlockProto - for i := 0; i < MaxAllowedBlocks; i++ { - latestBlocks = append(latestBlocks, &BlockProto{Num: int64(49 - i), Hash: genRandHash(int64(i)), Ts: uint64(46 - i)}) - } - - return &MercuryObservationProto{ - Timestamp: 42, - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(43)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(44)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(45)), - PricesValid: true, - CurrentBlockNum: latestBlocks[0].Num, - CurrentBlockHash: latestBlocks[0].Hash, - CurrentBlockTimestamp: latestBlocks[0].Ts, - CurrentBlockValid: true, - LatestBlocks: latestBlocks, - MaxFinalizedBlockNumber: 47, - MaxFinalizedBlockNumberValid: true, - } -} - -func newInvalidMercuryObservationProto() *MercuryObservationProto { - return &MercuryObservationProto{ - PricesValid: false, - CurrentBlockValid: false, - MaxFinalizedBlockNumberValid: false, - } -} - -func Test_Plugin_parseAttributedObservation(t *testing.T) { - t.Run("with all valid values, and > 0 LatestBlocks", func(t *testing.T) { - obs := newValidMercuryObservationProto() - ao := newAttributedObservation(t, obs) - expectedLatestBlocks := make([]Block, len(obs.LatestBlocks)) - for i, b := range obs.LatestBlocks { - expectedLatestBlocks[i] = NewBlock(b.Num, b.Hash, b.Ts) - } - - pao, err := parseAttributedObservation(ao) - require.NoError(t, err) - - assert.Equal(t, - parsedAttributedObservation{ - Timestamp: 0x2a, - Observer: 0x2a, - BenchmarkPrice: big.NewInt(43), - Bid: big.NewInt(44), - Ask: big.NewInt(45), - PricesValid: true, - MaxFinalizedBlockNumber: 47, - MaxFinalizedBlockNumberValid: true, - LatestBlocks: expectedLatestBlocks, - }, - pao, - ) - t.Run("with 0 LatestBlocks", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks = nil - ao := newAttributedObservation(t, obs) - - pao, err := parseAttributedObservation(ao) - require.NoError(t, err) - - assert.Equal(t, - parsedAttributedObservation{ - Timestamp: 0x2a, - Observer: 0x2a, - BenchmarkPrice: big.NewInt(43), - Bid: big.NewInt(44), - Ask: big.NewInt(45), - PricesValid: true, - CurrentBlockNum: 49, - CurrentBlockHash: obs.CurrentBlockHash, - CurrentBlockTimestamp: 46, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: 47, - MaxFinalizedBlockNumberValid: true, - }, - pao, - ) - }) - }) - - t.Run("with all invalid values", func(t *testing.T) { - obs := newInvalidMercuryObservationProto() - ao := newAttributedObservation(t, obs) - - pao, err := parseAttributedObservation(ao) - assert.NoError(t, err) - - assert.Equal(t, - parsedAttributedObservation{ - Observer: 0x2a, - PricesValid: false, - CurrentBlockValid: false, - MaxFinalizedBlockNumberValid: false, - LatestBlocks: ([]Block)(nil), - }, - pao, - ) - }) - - t.Run("when LatestBlocks is valid", func(t *testing.T) { - t.Run("sorts blocks if they are out of order", func(t *testing.T) { - obs := newValidMercuryObservationProto() - slices.Reverse(obs.LatestBlocks) - - ao := newAttributedObservation(t, obs) - - pao, err := parseAttributedObservation(ao) - assert.NoError(t, err) - - assert.Len(t, pao.GetLatestBlocks(), MaxAllowedBlocks) - assert.Equal(t, 49, int(pao.GetLatestBlocks()[0].Num)) - assert.Equal(t, 48, int(pao.GetLatestBlocks()[1].Num)) - assert.Equal(t, 47, int(pao.GetLatestBlocks()[2].Num)) - assert.Equal(t, 46, int(pao.GetLatestBlocks()[3].Num)) - assert.Equal(t, 45, int(pao.GetLatestBlocks()[4].Num)) - }) - }) - - t.Run("when LatestBlocks is invalid", func(t *testing.T) { - t.Run("contains duplicate block numbers", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks = []*BlockProto{&BlockProto{Num: 32, Hash: randBytes(32)}, &BlockProto{Num: 32, Hash: randBytes(32)}} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "observation invalid for observer 42; got duplicate block number: 32") - }) - t.Run("contains duplicate block hashes", func(t *testing.T) { - obs := newValidMercuryObservationProto() - h := randBytes(32) - obs.LatestBlocks = []*BlockProto{&BlockProto{Num: 1, Hash: h}, &BlockProto{Num: 2, Hash: h}} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, fmt.Sprintf("observation invalid for observer 42; got duplicate block hash: 0x%x", h)) - }) - t.Run("contains too many blocks", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks = nil - for i := 0; i < MaxAllowedBlocks+1; i++ { - obs.LatestBlocks = append(obs.LatestBlocks, &BlockProto{Num: int64(i)}) - } - - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, fmt.Sprintf("LatestBlocks too large; got: %d, max: %d", MaxAllowedBlocks+1, MaxAllowedBlocks)) - }) - }) - - t.Run("with unparseable values", func(t *testing.T) { - t.Run("ao cannot be unmarshalled", func(t *testing.T) { - ao := newUnparseableAttributedObservation() - - _, err := parseAttributedObservation(ao) - require.Error(t, err) - assert.Contains(t, err.Error(), "attributed observation cannot be unmarshaled") - }) - t.Run("bad benchmark price", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.BenchmarkPrice = randBytes(16) - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "benchmarkPrice cannot be converted to big.Int: expected b to have length 24, but got length 16") - }) - t.Run("bad bid", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.Bid = []byte{1} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "bid cannot be converted to big.Int: expected b to have length 24, but got length 1") - }) - t.Run("bad ask", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.Ask = []byte{1} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "ask cannot be converted to big.Int: expected b to have length 24, but got length 1") - }) - t.Run("bad block hash", func(t *testing.T) { - t.Run("CurrentBlockHash", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks = nil - obs.CurrentBlockHash = []byte{1} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "wrong len for hash: 1 (expected: 32)") - }) - - t.Run("LatestBlocks", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks[0].Hash = []byte{1} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "wrong len for hash: 1 (expected: 32)") - }) - }) - t.Run("negative block number", func(t *testing.T) { - t.Run("CurrentBlockNum", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks = nil - obs.CurrentBlockNum = -1 - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "negative block number: -1") - }) - t.Run("LatestBlocks", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks[0].Num = -1 - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "negative block number: -1") - }) - }) - }) -} - -func Test_Plugin_Report(t *testing.T) { - repts := types.ReportTimestamp{} - - t.Run("when previous report is nil", func(t *testing.T) { - codec := &testReportCodec{ - currentBlock: int64(rand.Int31()), - builtReport: []byte{1, 2, 3, 4}, - } - rp := newReportingPlugin(t, codec) - - t.Run("errors if not enough attributed observations", func(t *testing.T) { - _, _, err := rp.Report(repts, nil, []types.AttributedObservation{}) - assert.EqualError(t, err, "got zero valid attributed observations") - }) - t.Run("succeeds, ignoring unparseable attributed observations", func(t *testing.T) { - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newUnparseableAttributedObservation(), - } - should, report, err := rp.Report(repts, nil, aos) - - assert.NoError(t, err) - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - }) - t.Run("succeeds and generates validFromBlockNum from maxFinalizedBlockNumber", func(t *testing.T) { - codec.reset() - - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - } - should, report, err := rp.Report(repts, nil, aos) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - assert.NoError(t, err) - - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, 48, int(codec.builtReportFields.ValidFromBlockNum)) - }) - t.Run("errors if cannot get consensus maxFinalizedBlockNumber", func(t *testing.T) { - obs := []*MercuryObservationProto{ - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - } - for i := range obs { - obs[i].MaxFinalizedBlockNumber = int64(i) - } - aos := []types.AttributedObservation{ - newAttributedObservation(t, obs[0]), - newAttributedObservation(t, obs[1]), - newAttributedObservation(t, obs[2]), - newAttributedObservation(t, obs[3]), - } - _, _, err := rp.Report(repts, nil, aos) - - assert.EqualError(t, err, "no valid maxFinalizedBlockNumber with at least f+1 votes (got counts: map[0:1 1:1 2:1 3:1], f=1)") - }) - t.Run("errors if it cannot come to consensus about currentBlockNum", func(t *testing.T) { - obs := []*MercuryObservationProto{ - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - } - for i := range obs { - obs[i].LatestBlocks = nil - obs[i].CurrentBlockNum = int64(i) - } - aos := []types.AttributedObservation{ - newAttributedObservation(t, obs[0]), - newAttributedObservation(t, obs[1]), - newAttributedObservation(t, obs[2]), - newAttributedObservation(t, obs[3]), - } - _, _, err := rp.Report(repts, nil, aos) - - require.Error(t, err) - assert.Contains(t, err.Error(), "GetConsensusCurrentBlock failed: cannot come to consensus on latest block number") - }) - t.Run("errors if it cannot come to consensus on LatestBlocks", func(t *testing.T) { - obs := []*MercuryObservationProto{ - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - } - for i := range obs { - for j := range obs[i].LatestBlocks { - obs[i].LatestBlocks[j].Hash = randBytes(32) - } - } - aos := []types.AttributedObservation{ - newAttributedObservation(t, obs[0]), - newAttributedObservation(t, obs[1]), - newAttributedObservation(t, obs[2]), - newAttributedObservation(t, obs[3]), - } - _, _, err := rp.Report(repts, nil, aos) - - require.Error(t, err) - assert.Contains(t, err.Error(), "GetConsensusCurrentBlock failed: cannot come to consensus on latest block number") - }) - t.Run("errors if price is invalid", func(t *testing.T) { - obs := []*MercuryObservationProto{ - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - } - for i := range obs { - obs[i].BenchmarkPrice = mercury.MustEncodeValueInt192(big.NewInt(-1)) // benchmark price below min of 0, cannot report - } - aos := []types.AttributedObservation{ - newAttributedObservation(t, obs[0]), - newAttributedObservation(t, obs[1]), - newAttributedObservation(t, obs[2]), - newAttributedObservation(t, obs[3]), - } - should, report, err := rp.Report(repts, nil, aos) - - assert.False(t, should) - assert.Nil(t, report) - assert.EqualError(t, err, "median benchmark price (Value: -1) is outside of allowable range (Min: 0, Max: 1000)") - }) - t.Run("BuildReport failures", func(t *testing.T) { - t.Run("errors if BuildReport returns error", func(t *testing.T) { - codec.buildReportShouldFail = true - defer codec.reset() - - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newUnparseableAttributedObservation(), - } - _, _, err := rp.Report(repts, nil, aos) - - assert.EqualError(t, err, "buildReportShouldFail=true") - }) - t.Run("errors if BuildReport returns a report that is too long", func(t *testing.T) { - codec.builtReport = randBytes(9999) - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newUnparseableAttributedObservation(), - } - _, _, err := rp.Report(repts, nil, aos) - - assert.EqualError(t, err, "report with len 9999 violates MaxReportLength limit set by ReportCodec (1248)") - }) - t.Run("errors if BuildReport returns a report that is too short", func(t *testing.T) { - codec.builtReport = []byte{} - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newUnparseableAttributedObservation(), - } - _, _, err := rp.Report(repts, nil, aos) - - assert.EqualError(t, err, "report may not have zero length (invariant violation)") - }) - }) - }) - t.Run("when previous report is present", func(t *testing.T) { - codec := &testReportCodec{ - currentBlock: int64(rand.Int31()), - builtReport: []byte{1, 2, 3, 4}, - } - rp := newReportingPlugin(t, codec) - previousReport := types.Report{} - - t.Run("succeeds and uses block number in previous report if valid", func(t *testing.T) { - currentBlock := int64(32) - codec.currentBlock = currentBlock - - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - } - should, report, err := rp.Report(repts, previousReport, aos) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - assert.NoError(t, err) - - require.NotNil(t, codec.builtReportFields) - // current block of previous report + 1 is the validFromBlockNum of current report - assert.Equal(t, 33, int(codec.builtReportFields.ValidFromBlockNum)) - }) - t.Run("errors if cannot extract block number from previous report", func(t *testing.T) { - codec.currentBlockErr = errors.New("test error current block fail") - - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - } - should, _, err := rp.Report(repts, previousReport, aos) - - assert.False(t, should) - assert.EqualError(t, err, "test error current block fail") - }) - t.Run("does not report if currentBlockNum < validFromBlockNum", func(t *testing.T) { - codec.currentBlock = 49 // means that validFromBlockNum=50 which is > currentBlockNum of 49 - codec.currentBlockErr = nil - - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - } - should, _, err := rp.Report(repts, previousReport, aos) - - assert.False(t, should) - assert.NoError(t, err) - }) - }) -} - -func Test_MaxObservationLength(t *testing.T) { - t.Run("maximally sized pbuf does not exceed maxObservationLength", func(t *testing.T) { - maxInt192Bytes := make([]byte, 24) - for i := 0; i < 24; i++ { - maxInt192Bytes[i] = 255 - } - maxHash := make([]byte, 32) - for i := 0; i < 32; i++ { - maxHash[i] = 255 - } - maxLatestBlocks := []*BlockProto{} - for i := 0; i < MaxAllowedBlocks; i++ { - maxLatestBlocks = append(maxLatestBlocks, &BlockProto{Num: math.MaxInt64, Hash: maxHash, Ts: math.MaxUint64}) - } - obs := MercuryObservationProto{ - Timestamp: math.MaxUint32, - BenchmarkPrice: maxInt192Bytes, - Bid: maxInt192Bytes, - Ask: maxInt192Bytes, - PricesValid: true, - CurrentBlockNum: math.MaxInt64, - CurrentBlockHash: maxHash, - CurrentBlockTimestamp: math.MaxUint64, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: math.MaxInt64, - MaxFinalizedBlockNumberValid: true, - LatestBlocks: maxLatestBlocks, - } - // This assertion is here to force this test to fail if a new field is - // added to the protobuf. In this case, you must add the max value of - // the field to the MercuryObservationProto in the test and only after - // that increment the count below - numFields := reflect.TypeOf(obs).NumField() //nolint:all - // 3 fields internal to pbuf struct - require.Equal(t, 12, numFields-3) - - // the actual test - b, err := proto.Marshal(&obs) - require.NoError(t, err) - assert.LessOrEqual(t, len(b), maxObservationLength) - }) -} diff --git a/pkg/reportingplugins/mercury/v1/observation.go b/pkg/reportingplugins/mercury/v1/observation.go deleted file mode 100644 index 884e80bc4..000000000 --- a/pkg/reportingplugins/mercury/v1/observation.go +++ /dev/null @@ -1,111 +0,0 @@ -package mercury_v1 //nolint:revive - -import ( - "math/big" - - "github.com/smartcontractkit/libocr/commontypes" -) - -var _ PAO = parsedAttributedObservation{} - -type parsedAttributedObservation struct { - Timestamp uint32 - Observer commontypes.OracleID - - BenchmarkPrice *big.Int - Bid *big.Int - Ask *big.Int - // All three prices must be valid, or none are (they all should come from one API query and hold invariant bid <= bm <= ask) - PricesValid bool - - // DEPRECATED - // TODO: Remove this handling after deployment (https://smartcontract-it.atlassian.net/browse/MERC-2272) - CurrentBlockNum int64 // inclusive; current block - CurrentBlockHash []byte - CurrentBlockTimestamp uint64 - // All three block observations must be valid, or none are (they all come from the same block) - CurrentBlockValid bool - - LatestBlocks []Block - - // MaxFinalizedBlockNumber comes from previous report when present and is - // only observed from mercury server when previous report is nil - // - // MaxFinalizedBlockNumber will be -1 if there is none - MaxFinalizedBlockNumber int64 - MaxFinalizedBlockNumberValid bool -} - -func NewParsedAttributedObservation(ts uint32, observer commontypes.OracleID, bp *big.Int, bid *big.Int, ask *big.Int, pricesValid bool, - currentBlockNum int64, currentBlockHash []byte, currentBlockTimestamp uint64, currentBlockValid bool, - maxFinalizedBlockNumber int64, maxFinalizedBlockNumberValid bool) PAO { - return parsedAttributedObservation{ - Timestamp: ts, - Observer: observer, - - BenchmarkPrice: bp, - Bid: bid, - Ask: ask, - PricesValid: pricesValid, - - CurrentBlockNum: currentBlockNum, - CurrentBlockHash: currentBlockHash, - CurrentBlockTimestamp: currentBlockTimestamp, - CurrentBlockValid: currentBlockValid, - - MaxFinalizedBlockNumber: maxFinalizedBlockNumber, - MaxFinalizedBlockNumberValid: maxFinalizedBlockNumberValid, - } -} - -func (pao parsedAttributedObservation) GetTimestamp() uint32 { - return pao.Timestamp -} - -func (pao parsedAttributedObservation) GetObserver() commontypes.OracleID { - return pao.Observer -} - -func (pao parsedAttributedObservation) GetBenchmarkPrice() (*big.Int, bool) { - return pao.BenchmarkPrice, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetBid() (*big.Int, bool) { - return pao.Bid, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetAsk() (*big.Int, bool) { - return pao.Ask, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetCurrentBlockNum() (int64, bool) { - return pao.CurrentBlockNum, pao.CurrentBlockValid -} - -func (pao parsedAttributedObservation) GetCurrentBlockHash() ([]byte, bool) { - return pao.CurrentBlockHash, pao.CurrentBlockValid -} - -func (pao parsedAttributedObservation) GetCurrentBlockTimestamp() (uint64, bool) { - return pao.CurrentBlockTimestamp, pao.CurrentBlockValid -} - -func (pao parsedAttributedObservation) GetLatestBlocks() []Block { - return pao.LatestBlocks -} - -func (pao parsedAttributedObservation) GetMaxFinalizedBlockNumber() (int64, bool) { - return pao.MaxFinalizedBlockNumber, pao.MaxFinalizedBlockNumberValid -} - -func (pao parsedAttributedObservation) GetMaxFinalizedTimestamp() (uint32, bool) { - panic("current observation doesn't contain the field") -} - -func (pao parsedAttributedObservation) GetLinkFee() (*big.Int, bool) { - panic("current observation doesn't contain the field") -} - -func (pao parsedAttributedObservation) GetNativeFee() (*big.Int, bool) { - panic("current observation doesn't contain the field") -} diff --git a/pkg/reportingplugins/mercury/v1/validation.go b/pkg/reportingplugins/mercury/v1/validation.go deleted file mode 100644 index d87a73c0a..000000000 --- a/pkg/reportingplugins/mercury/v1/validation.go +++ /dev/null @@ -1,28 +0,0 @@ -package mercury_v1 //nolint:revive - -import ( - "fmt" - - "github.com/pkg/errors" - - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" -) - -// ValidateCurrentBlock sanity checks number and hash -func ValidateCurrentBlock(rf ReportFields) error { - if rf.ValidFromBlockNum < 0 { - return fmt.Errorf("validFromBlockNum must be >= 0 (got: %d)", rf.ValidFromBlockNum) - } - if rf.CurrentBlockNum < 0 { - return fmt.Errorf("currentBlockNum must be >= 0 (got: %d)", rf.ValidFromBlockNum) - } - if rf.ValidFromBlockNum > rf.CurrentBlockNum { - return fmt.Errorf("validFromBlockNum (Value: %d) must be less than or equal to CurrentBlockNum (Value: %d)", rf.ValidFromBlockNum, rf.CurrentBlockNum) - } - // NOTE: hardcoded ethereum hash - if len(rf.CurrentBlockHash) != mercury.EvmHashLen { - return errors.Errorf("invalid length for hash; expected %d (got: %d)", mercury.EvmHashLen, len(rf.CurrentBlockHash)) - } - - return nil -} diff --git a/pkg/reportingplugins/mercury/v1/validation_test.go b/pkg/reportingplugins/mercury/v1/validation_test.go deleted file mode 100644 index 42076b770..000000000 --- a/pkg/reportingplugins/mercury/v1/validation_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package mercury_v1 //nolint:revive - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestValidation(t *testing.T) { - rf := ReportFields{ - CurrentBlockHash: make([]byte, 32), - } - - t.Run("ValidateCurrentBlock", func(t *testing.T) { - t.Run("succeeds when validFromBlockNum < current block num", func(t *testing.T) { - rf.ValidFromBlockNum = 16634363 - rf.CurrentBlockNum = 16634364 - err := ValidateCurrentBlock(rf) - assert.NoError(t, err) - }) - t.Run("succeeds when validFromBlockNum is equal to current block number", func(t *testing.T) { - rf.ValidFromBlockNum = 16634364 - rf.CurrentBlockNum = 16634364 - err := ValidateCurrentBlock(rf) - assert.NoError(t, err) - }) - t.Run("zero is ok", func(t *testing.T) { - rf.ValidFromBlockNum = 0 - rf.CurrentBlockNum = 0 - err := ValidateCurrentBlock(rf) - assert.NoError(t, err) - }) - t.Run("errors when validFromBlockNum number < 0", func(t *testing.T) { - rf.ValidFromBlockNum = -1 - rf.CurrentBlockNum = -1 - err := ValidateCurrentBlock(rf) - assert.EqualError(t, err, "validFromBlockNum must be >= 0 (got: -1)") - }) - t.Run("errors when validFrom > block number", func(t *testing.T) { - rf.CurrentBlockNum = 1 - rf.ValidFromBlockNum = 16634366 - err := ValidateCurrentBlock(rf) - assert.EqualError(t, err, "validFromBlockNum (Value: 16634366) must be less than or equal to CurrentBlockNum (Value: 1)") - }) - t.Run("errors when validFrom < 0", func(t *testing.T) { - rf.ValidFromBlockNum = -1 - err := ValidateCurrentBlock(rf) - assert.EqualError(t, err, "validFromBlockNum must be >= 0 (got: -1)") - }) - t.Run("errors when hash has incorrect length", func(t *testing.T) { - rf.ValidFromBlockNum = 16634363 - rf.CurrentBlockNum = 16634364 - rf.CurrentBlockHash = []byte{} - err := ValidateCurrentBlock(rf) - assert.EqualError(t, err, "invalid length for hash; expected 32 (got: 0)") - rf.CurrentBlockHash = make([]byte, 64) - err = ValidateCurrentBlock(rf) - assert.EqualError(t, err, "invalid length for hash; expected 32 (got: 64)") - }) - }) -} diff --git a/pkg/reportingplugins/mercury/v2/mercury.go b/pkg/reportingplugins/mercury/v2/mercury.go deleted file mode 100644 index b7bc3aebc..000000000 --- a/pkg/reportingplugins/mercury/v2/mercury.go +++ /dev/null @@ -1,411 +0,0 @@ -package mercury_v2 //nolint:revive - -import ( - "context" - "errors" - "fmt" - "math" - "math/big" - "time" - - pkgerrors "github.com/pkg/errors" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" -) - -type Observation struct { - BenchmarkPrice mercury.ObsResult[*big.Int] - - MaxFinalizedTimestamp mercury.ObsResult[int64] - - LinkPrice mercury.ObsResult[*big.Int] - NativePrice mercury.ObsResult[*big.Int] -} - -// DataSource implementations must be thread-safe. Observe may be called by many -// different threads concurrently. -type DataSource interface { - // Observe queries the data source. Returns a value or an error. Once the - // context is expires, Observe may still do cheap computations and return a - // result, but should return as quickly as possible. - // - // More details: In the current implementation, the context passed to - // Observe will time out after MaxDurationObservation. However, Observe - // should *not* make any assumptions about context timeout behavior. Once - // the context times out, Observe should prioritize returning as quickly as - // possible, but may still perform fast computations to return a result - // rather than error. For example, if Observe medianizes a number of data - // sources, some of which already returned a result to Observe prior to the - // context's expiry, Observe might still compute their median, and return it - // instead of an error. - // - // Important: Observe should not perform any potentially time-consuming - // actions like database access, once the context passed has expired. - Observe(ctx context.Context, repts ocrtypes.ReportTimestamp, fetchMaxFinalizedTimestamp bool) (Observation, error) -} - -var _ ocr3types.MercuryPluginFactory = Factory{} - -const maxObservationLength = 32 + // feedID - 4 + // timestamp - mercury.ByteWidthInt192 + // benchmarkPrice - 4 + // validFromTimestamp - mercury.ByteWidthInt192 + // linkFee - mercury.ByteWidthInt192 + // nativeFee - 16 /* overapprox. of protobuf overhead */ - -type Factory struct { - dataSource DataSource - logger logger.Logger - onchainConfigCodec mercury.OnchainConfigCodec - reportCodec ReportCodec -} - -func NewFactory(ds DataSource, lggr logger.Logger, occ mercury.OnchainConfigCodec, rc ReportCodec) Factory { - return Factory{ds, lggr, occ, rc} -} - -func (fac Factory) NewMercuryPlugin(configuration ocr3types.MercuryPluginConfig) (ocr3types.MercuryPlugin, ocr3types.MercuryPluginInfo, error) { - offchainConfig, err := mercury.DecodeOffchainConfig(configuration.OffchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - onchainConfig, err := fac.onchainConfigCodec.Decode(configuration.OnchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - maxReportLength, err := fac.reportCodec.MaxReportLength(configuration.N) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - r := &reportingPlugin{ - offchainConfig, - onchainConfig, - fac.dataSource, - fac.logger, - fac.reportCodec, - configuration.ConfigDigest, - configuration.F, - mercury.EpochRound{}, - new(big.Int), - maxReportLength, - } - - return r, ocr3types.MercuryPluginInfo{ - Name: "Mercury", - Limits: ocr3types.MercuryPluginLimits{ - MaxObservationLength: maxObservationLength, - MaxReportLength: maxReportLength, - }, - }, nil -} - -var _ ocr3types.MercuryPlugin = (*reportingPlugin)(nil) - -type reportingPlugin struct { - offchainConfig mercury.OffchainConfig - onchainConfig mercury.OnchainConfig - dataSource DataSource - logger logger.Logger - reportCodec ReportCodec - - configDigest ocrtypes.ConfigDigest - f int - latestAcceptedEpochRound mercury.EpochRound - latestAcceptedMedian *big.Int - maxReportLength int -} - -var MissingPrice = big.NewInt(-1) - -func (rp *reportingPlugin) Observation(ctx context.Context, repts ocrtypes.ReportTimestamp, previousReport ocrtypes.Report) (ocrtypes.Observation, error) { - obs, err := rp.dataSource.Observe(ctx, repts, previousReport == nil) - if err != nil { - return nil, pkgerrors.Errorf("DataSource.Observe returned an error: %s", err) - } - - observationTimestamp := time.Now() - if observationTimestamp.Unix() > math.MaxUint32 { - return nil, fmt.Errorf("current unix epoch %d exceeds max uint32", observationTimestamp.Unix()) - } - p := MercuryObservationProto{Timestamp: uint32(observationTimestamp.Unix())} - var obsErrors []error - - var bpErr error - if obs.BenchmarkPrice.Err != nil { - bpErr = pkgerrors.Wrap(obs.BenchmarkPrice.Err, "failed to observe BenchmarkPrice") - obsErrors = append(obsErrors, bpErr) - } else if benchmarkPrice, err := mercury.EncodeValueInt192(obs.BenchmarkPrice.Val); err != nil { - bpErr = pkgerrors.Wrapf(err, "failed to encode BenchmarkPrice; val=%s", obs.BenchmarkPrice.Val) - obsErrors = append(obsErrors, bpErr) - } else { - p.BenchmarkPrice = benchmarkPrice - p.PricesValid = true - } - - var maxFinalizedTimestampErr error - if obs.MaxFinalizedTimestamp.Err != nil { - maxFinalizedTimestampErr = pkgerrors.Wrap(obs.MaxFinalizedTimestamp.Err, "failed to observe MaxFinalizedTimestamp") - obsErrors = append(obsErrors, maxFinalizedTimestampErr) - } else { - p.MaxFinalizedTimestamp = obs.MaxFinalizedTimestamp.Val - p.MaxFinalizedTimestampValid = true - } - - var linkErr error - if obs.LinkPrice.Err != nil { - linkErr = pkgerrors.Wrap(obs.LinkPrice.Err, "failed to observe LINK price") - obsErrors = append(obsErrors, linkErr) - } else if obs.LinkPrice.Val.Cmp(MissingPrice) <= 0 { - p.LinkFee = mercury.MaxInt192Enc - } else { - linkFee := mercury.CalculateFee(obs.LinkPrice.Val, rp.offchainConfig.BaseUSDFee) - if linkFeeEncoded, err := mercury.EncodeValueInt192(linkFee); err != nil { - linkErr = pkgerrors.Wrapf(err, "failed to encode LINK fee; val=%s", linkFee) - obsErrors = append(obsErrors, linkErr) - } else { - p.LinkFee = linkFeeEncoded - } - } - - if linkErr == nil { - p.LinkFeeValid = true - } - - var nativeErr error - if obs.NativePrice.Err != nil { - nativeErr = pkgerrors.Wrap(obs.NativePrice.Err, "failed to observe native price") - obsErrors = append(obsErrors, nativeErr) - } else if obs.NativePrice.Val.Cmp(MissingPrice) <= 0 { - p.NativeFee = mercury.MaxInt192Enc - } else { - nativeFee := mercury.CalculateFee(obs.NativePrice.Val, rp.offchainConfig.BaseUSDFee) - if nativeFeeEncoded, err := mercury.EncodeValueInt192(nativeFee); err != nil { - nativeErr = pkgerrors.Wrapf(err, "failed to encode native fee; val=%s", nativeFee) - obsErrors = append(obsErrors, nativeErr) - } else { - p.NativeFee = nativeFeeEncoded - } - } - - if nativeErr == nil { - p.NativeFeeValid = true - } - - if len(obsErrors) > 0 { - rp.logger.Warnw(fmt.Sprintf("Observe failed %d/4 observations", len(obsErrors)), "err", errors.Join(obsErrors...)) - } - - return proto.Marshal(&p) -} - -func parseAttributedObservation(ao ocrtypes.AttributedObservation) (PAO, error) { - var pao parsedAttributedObservation - var obs MercuryObservationProto - if err := proto.Unmarshal(ao.Observation, &obs); err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("attributed observation cannot be unmarshaled: %s", err) - } - - pao.Timestamp = obs.Timestamp - pao.Observer = ao.Observer - - if obs.PricesValid { - var err error - pao.BenchmarkPrice, err = mercury.DecodeValueInt192(obs.BenchmarkPrice) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("benchmarkPrice cannot be converted to big.Int: %s", err) - } - pao.PricesValid = true - } - - if obs.MaxFinalizedTimestampValid { - pao.MaxFinalizedTimestamp = obs.MaxFinalizedTimestamp - pao.MaxFinalizedTimestampValid = true - } - - if obs.LinkFeeValid { - var err error - pao.LinkFee, err = mercury.DecodeValueInt192(obs.LinkFee) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("link price cannot be converted to big.Int: %s", err) - } - pao.LinkFeeValid = true - } - if obs.NativeFeeValid { - var err error - pao.NativeFee, err = mercury.DecodeValueInt192(obs.NativeFee) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("native price cannot be converted to big.Int: %s", err) - } - pao.NativeFeeValid = true - } - - return pao, nil -} - -func parseAttributedObservations(lggr logger.Logger, aos []ocrtypes.AttributedObservation) []PAO { - paos := make([]PAO, 0, len(aos)) - for i, ao := range aos { - pao, err := parseAttributedObservation(ao) - if err != nil { - lggr.Warnw("parseAttributedObservations: dropping invalid observation", - "observer", ao.Observer, - "error", err, - "i", i, - ) - continue - } - paos = append(paos, pao) - } - return paos -} - -func (rp *reportingPlugin) Report(repts ocrtypes.ReportTimestamp, previousReport ocrtypes.Report, aos []ocrtypes.AttributedObservation) (shouldReport bool, report ocrtypes.Report, err error) { - paos := parseAttributedObservations(rp.logger, aos) - - if len(paos) == 0 { - return false, nil, errors.New("got zero valid attributed observations") - } - - // By assumption, we have at most f malicious oracles, so there should be at least f+1 valid paos - if !(rp.f+1 <= len(paos)) { - return false, nil, pkgerrors.Errorf("only received %v valid attributed observations, but need at least f+1 (%v)", len(paos), rp.f+1) - } - - rf, err := rp.buildReportFields(previousReport, paos) - if err != nil { - rp.logger.Errorw("failed to build report fields", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts, "err", err) - return false, nil, err - } - - if rf.Timestamp < rf.ValidFromTimestamp { - rp.logger.Debugw("shouldReport: no (overlap)", "observationTimestamp", rf.Timestamp, "validFromTimestamp", rf.ValidFromTimestamp, "repts", repts) - return false, nil, nil - } - - if err = rp.validateReport(rf); err != nil { - rp.logger.Errorw("shouldReport: no (validation error)", "reportFields", rf, "err", err, "repts", repts, "paos", paos) - return false, nil, err - } - rp.logger.Debugw("shouldReport: yes", "repts", repts) - - report, err = rp.reportCodec.BuildReport(rf) - if err != nil { - rp.logger.Debugw("failed to BuildReport", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts) - return false, nil, err - } - - if !(len(report) <= rp.maxReportLength) { - return false, nil, pkgerrors.Errorf("report with len %d violates MaxReportLength limit set by ReportCodec (%d)", len(report), rp.maxReportLength) - } else if len(report) == 0 { - return false, nil, errors.New("report may not have zero length (invariant violation)") - } - - return true, report, nil -} - -func (rp *reportingPlugin) buildReportFields(previousReport ocrtypes.Report, paos []PAO) (rf ReportFields, merr error) { - mPaos := convert(paos) - rf.Timestamp = mercury.GetConsensusTimestamp(mPaos) - - var err error - if previousReport != nil { - var maxFinalizedTimestamp uint32 - maxFinalizedTimestamp, err = rp.reportCodec.ObservationTimestampFromReport(previousReport) - merr = errors.Join(merr, err) - rf.ValidFromTimestamp = maxFinalizedTimestamp + 1 - } else { - var maxFinalizedTimestamp int64 - maxFinalizedTimestamp, err = mercury.GetConsensusMaxFinalizedTimestamp(convertMaxFinalizedTimestamp(paos), rp.f) - if err != nil { - merr = errors.Join(merr, err) - } else if maxFinalizedTimestamp < 0 { - // no previous observation timestamp available, e.g. in case of new - // feed; use current timestamp as start of range - rf.ValidFromTimestamp = rf.Timestamp - } else if maxFinalizedTimestamp+1 > math.MaxUint32 { - merr = errors.Join(err, fmt.Errorf("maxFinalizedTimestamp is too large, got: %d", maxFinalizedTimestamp)) - } else { - rf.ValidFromTimestamp = uint32(maxFinalizedTimestamp + 1) - } - } - - rf.BenchmarkPrice, err = mercury.GetConsensusBenchmarkPrice(mPaos, rp.f) - merr = errors.Join(merr, pkgerrors.Wrap(err, "GetConsensusBenchmarkPrice failed")) - - rf.LinkFee, err = mercury.GetConsensusLinkFee(convertLinkFee(paos), rp.f) - if err != nil { - // It is better to generate a report that will validate for free, - // rather than no report at all, if we cannot come to consensus on a - // valid fee. - rp.logger.Errorw("Cannot come to consensus on LINK fee, falling back to 0", "err", err, "paos", paos) - rf.LinkFee = big.NewInt(0) - } - - rf.NativeFee, err = mercury.GetConsensusNativeFee(convertNativeFee(paos), rp.f) - if err != nil { - // It is better to generate a report that will validate for free, - // rather than no report at all, if we cannot come to consensus on a - // valid fee. - rp.logger.Errorw("Cannot come to consensus on Native fee, falling back to 0", "err", err, "paos", paos) - rf.NativeFee = big.NewInt(0) - } - - if int64(rf.Timestamp)+int64(rp.offchainConfig.ExpirationWindow) > math.MaxUint32 { - merr = errors.Join(merr, fmt.Errorf("timestamp %d + expiration window %d overflows uint32", rf.Timestamp, rp.offchainConfig.ExpirationWindow)) - } else { - rf.ExpiresAt = rf.Timestamp + rp.offchainConfig.ExpirationWindow - } - - return rf, merr -} - -func (rp *reportingPlugin) validateReport(rf ReportFields) error { - return errors.Join( - mercury.ValidateBetween("median benchmark price", rf.BenchmarkPrice, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateFee("median link fee", rf.LinkFee), - mercury.ValidateFee("median native fee", rf.NativeFee), - mercury.ValidateValidFromTimestamp(rf.Timestamp, rf.ValidFromTimestamp), - mercury.ValidateExpiresAt(rf.Timestamp, rf.ExpiresAt), - ) -} - -func (rp *reportingPlugin) Close() error { - return nil -} - -// convert funcs are necessary because go is not smart enough to cast -// []interface1 to []interface2 even if interface1 is a superset of interface2 -func convert(pao []PAO) (ret []mercury.PAO) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertMaxFinalizedTimestamp(pao []PAO) (ret []mercury.PAOMaxFinalizedTimestamp) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertLinkFee(pao []PAO) (ret []mercury.PAOLinkFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertNativeFee(pao []PAO) (ret []mercury.PAONativeFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} diff --git a/pkg/reportingplugins/mercury/v2/mercury_observation_v2.proto b/pkg/reportingplugins/mercury/v2/mercury_observation_v2.proto deleted file mode 100644 index 26737fc7a..000000000 --- a/pkg/reportingplugins/mercury/v2/mercury_observation_v2.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax="proto3"; - -package mercury_v2; -option go_package = ".;mercury_v2"; - -message MercuryObservationProto { - uint32 timestamp = 1; - - bytes benchmarkPrice = 2; - bool pricesValid = 3; - - int64 maxFinalizedTimestamp = 4; - bool maxFinalizedTimestampValid = 5; - - bytes linkFee = 6; - bool linkFeeValid = 7; - bytes nativeFee = 8; - bool nativeFeeValid = 9; -} diff --git a/pkg/reportingplugins/mercury/v2/mercury_test.go b/pkg/reportingplugins/mercury/v2/mercury_test.go deleted file mode 100644 index 52fbf108f..000000000 --- a/pkg/reportingplugins/mercury/v2/mercury_test.go +++ /dev/null @@ -1,664 +0,0 @@ -package mercury_v2 //nolint:revive - -import ( - "context" - "math" - "math/big" - "math/rand" - "reflect" - "testing" - "time" - - "github.com/pkg/errors" - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/commontypes" - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" -) - -type testDataSource struct { - Obs Observation -} - -func (ds testDataSource) Observe(ctx context.Context, repts ocrtypes.ReportTimestamp, fetchMaxFinalizedTimestamp bool) (Observation, error) { - return ds.Obs, nil -} - -type testReportCodec struct { - observationTimestamp uint32 - builtReport ocrtypes.Report - - builtReportFields *ReportFields - err error -} - -func (rc *testReportCodec) BuildReport(rf ReportFields) (ocrtypes.Report, error) { - rc.builtReportFields = &rf - - return rc.builtReport, nil -} - -func (rc testReportCodec) MaxReportLength(n int) (int, error) { - return 123, nil -} - -func (rc testReportCodec) ObservationTimestampFromReport(ocrtypes.Report) (uint32, error) { - return rc.observationTimestamp, rc.err -} - -func newTestReportPlugin(t *testing.T, codec *testReportCodec, ds *testDataSource) *reportingPlugin { - offchainConfig := mercury.OffchainConfig{ - ExpirationWindow: 1, - BaseUSDFee: decimal.NewFromInt32(1), - } - onchainConfig := mercury.OnchainConfig{ - Min: big.NewInt(1), - Max: big.NewInt(1000), - } - maxReportLength, _ := codec.MaxReportLength(4) - return &reportingPlugin{ - offchainConfig: offchainConfig, - onchainConfig: onchainConfig, - dataSource: ds, - logger: logger.Test(t), - reportCodec: codec, - configDigest: ocrtypes.ConfigDigest{}, - f: 1, - latestAcceptedEpochRound: mercury.EpochRound{}, - latestAcceptedMedian: big.NewInt(0), - maxReportLength: maxReportLength, - } -} - -func newValidProtos() []*MercuryObservationProto { - return []*MercuryObservationProto{ - &MercuryObservationProto{ - Timestamp: 42, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(123)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.1e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.1e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 45, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(234)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.2e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.2e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 47, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(345)), - PricesValid: true, - - MaxFinalizedTimestamp: 39, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.3e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.3e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 39, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(456)), - PricesValid: true, - - MaxFinalizedTimestamp: 39, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.4e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.4e18)), - NativeFeeValid: true, - }, - } -} - -func newValidAos(t *testing.T, protos ...*MercuryObservationProto) (aos []ocrtypes.AttributedObservation) { - if len(protos) == 0 { - protos = newValidProtos() - } - aos = make([]ocrtypes.AttributedObservation, len(protos)) - for i := range aos { - marshalledObs, err := proto.Marshal(protos[i]) - require.NoError(t, err) - aos[i] = ocrtypes.AttributedObservation{ - Observation: marshalledObs, - Observer: commontypes.OracleID(i), - } - } - return -} - -func Test_Plugin_Report(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{ - builtReport: []byte{1, 2, 3, 4}, - } - rp := newTestReportPlugin(t, codec, dataSource) - repts := ocrtypes.ReportTimestamp{} - - t.Run("when previous report is nil", func(t *testing.T) { - t.Run("errors if not enough attributed observations", func(t *testing.T) { - _, _, err := rp.Report(repts, nil, newValidAos(t)[0:1]) - assert.EqualError(t, err, "only received 1 valid attributed observations, but need at least f+1 (2)") - }) - t.Run("errors if too many maxFinalizedTimestamp observations are invalid", func(t *testing.T) { - ps := newValidProtos() - ps[0].MaxFinalizedTimestampValid = false - ps[1].MaxFinalizedTimestampValid = false - ps[2].MaxFinalizedTimestampValid = false - aos := newValidAos(t, ps...) - - should, _, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - assert.False(t, should) - assert.EqualError(t, err, "fewer than f+1 observations have a valid maxFinalizedTimestamp (got: 1/4)") - }) - t.Run("errors if maxFinalizedTimestamp is too large", func(t *testing.T) { - ps := newValidProtos() - ps[0].MaxFinalizedTimestamp = math.MaxUint32 - ps[1].MaxFinalizedTimestamp = math.MaxUint32 - ps[2].MaxFinalizedTimestamp = math.MaxUint32 - ps[3].MaxFinalizedTimestamp = math.MaxUint32 - aos := newValidAos(t, ps...) - - should, _, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - assert.False(t, should) - assert.EqualError(t, err, "maxFinalizedTimestamp is too large, got: 4294967295") - }) - - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is positive", func(t *testing.T) { - aos := newValidAos(t) - - should, report, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, ReportFields{ - ValidFromTimestamp: 41, // consensus maxFinalizedTimestamp is 40, so validFrom should be 40+1 - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - }, *codec.builtReportFields) - }) - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is zero", func(t *testing.T) { - protos := newValidProtos() - for i := range protos { - protos[i].MaxFinalizedTimestamp = 0 - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, ReportFields{ - ValidFromTimestamp: 1, - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - }, *codec.builtReportFields) - }) - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is -1 (missing feed)", func(t *testing.T) { - protos := newValidProtos() - for i := range protos { - protos[i].MaxFinalizedTimestamp = -1 - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, ReportFields{ - ValidFromTimestamp: 45, // in case of missing feed, ValidFromTimestamp=Timestamp for first report - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - }, *codec.builtReportFields) - }) - - t.Run("succeeds, ignoring unparseable attributed observation", func(t *testing.T) { - aos := newValidAos(t) - aos[0] = newUnparseableAttributedObservation() - - should, report, err := rp.Report(repts, nil, aos) - require.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, ReportFields{ - ValidFromTimestamp: 40, // consensus maxFinalizedTimestamp is 39, so validFrom should be 39+1 - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - }, *codec.builtReportFields) - }) - }) - - t.Run("when previous report is present", func(t *testing.T) { - *codec = testReportCodec{ - observationTimestamp: uint32(rand.Int31n(math.MaxInt16)), - builtReport: []byte{1, 2, 3, 4}, - } - previousReport := ocrtypes.Report{} - - t.Run("succeeds and uses timestamp from previous report if valid", func(t *testing.T) { - protos := newValidProtos() - ts := codec.observationTimestamp + 1 - for i := range protos { - protos[i].Timestamp = ts - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(repts, previousReport, aos) - require.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, ReportFields{ - ValidFromTimestamp: codec.observationTimestamp + 1, // previous observation timestamp +1 second - Timestamp: ts, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: ts + 1, - BenchmarkPrice: big.NewInt(345), - }, *codec.builtReportFields) - }) - t.Run("errors if cannot extract timestamp from previous report", func(t *testing.T) { - codec.err = errors.New("something exploded trying to extract timestamp") - aos := newValidAos(t) - - should, _, err := rp.Report(ocrtypes.ReportTimestamp{}, previousReport, aos) - assert.False(t, should) - assert.EqualError(t, err, "something exploded trying to extract timestamp") - }) - t.Run("does not report if observationTimestamp < validFromTimestamp", func(t *testing.T) { - codec.observationTimestamp = 43 - codec.err = nil - - protos := newValidProtos() - for i := range protos { - protos[i].Timestamp = 42 - } - aos := newValidAos(t, protos...) - - should, _, err := rp.Report(ocrtypes.ReportTimestamp{}, previousReport, aos) - assert.False(t, should) - assert.NoError(t, err) - }) - t.Run("uses 0 values for link/native if they are invalid", func(t *testing.T) { - codec.observationTimestamp = 42 - codec.err = nil - - protos := newValidProtos() - for i := range protos { - protos[i].LinkFeeValid = false - protos[i].NativeFeeValid = false - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(ocrtypes.ReportTimestamp{}, previousReport, aos) - assert.True(t, should) - assert.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, "0", codec.builtReportFields.LinkFee.String()) - assert.Equal(t, "0", codec.builtReportFields.NativeFee.String()) - }) - }) - - t.Run("buildReport failures", func(t *testing.T) { - t.Run("Report errors when the report is too large", func(t *testing.T) { - aos := newValidAos(t) - codec.builtReport = make([]byte, 1<<16) - - _, _, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - - assert.EqualError(t, err, "report with len 65536 violates MaxReportLength limit set by ReportCodec (123)") - }) - - t.Run("Report errors when the report length is 0", func(t *testing.T) { - aos := newValidAos(t) - codec.builtReport = []byte{} - _, _, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - - assert.EqualError(t, err, "report may not have zero length (invariant violation)") - }) - }) -} - -func Test_Plugin_validateReport(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{} - rp := newTestReportPlugin(t, codec, dataSource) - - t.Run("valid reports", func(t *testing.T) { - rf := ReportFields{ - ValidFromTimestamp: 42, - Timestamp: 43, - NativeFee: big.NewInt(100), - LinkFee: big.NewInt(50), - ExpiresAt: 44, - BenchmarkPrice: big.NewInt(150), - } - err := rp.validateReport(rf) - require.NoError(t, err) - - rf = ReportFields{ - ValidFromTimestamp: 42, - Timestamp: 42, - NativeFee: big.NewInt(0), - LinkFee: big.NewInt(0), - ExpiresAt: 42, - BenchmarkPrice: big.NewInt(1), - } - err = rp.validateReport(rf) - require.NoError(t, err) - }) - t.Run("fails validation", func(t *testing.T) { - rf := ReportFields{ - ValidFromTimestamp: 44, // later than timestamp not allowed - Timestamp: 43, - NativeFee: big.NewInt(-1), // negative value not allowed - LinkFee: big.NewInt(-1), // negative value not allowed - ExpiresAt: 42, // before timestamp - BenchmarkPrice: big.NewInt(150000), // exceeds max - } - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median benchmark price (Value: 150000) is outside of allowable range (Min: 1, Max: 1000)") - assert.Contains(t, err.Error(), "median link fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "median native fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "observationTimestamp (Value: 43) must be >= validFromTimestamp (Value: 44)") - assert.Contains(t, err.Error(), "expiresAt (Value: 42) must be ahead of observation timestamp (Value: 43)") - }) - - t.Run("zero values", func(t *testing.T) { - rf := ReportFields{} - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median benchmark price: got nil value") - assert.Contains(t, err.Error(), "median native fee: got nil value") - assert.Contains(t, err.Error(), "median link fee: got nil value") - }) -} - -func mustDecodeBigInt(b []byte) *big.Int { - n, err := mercury.DecodeValueInt192(b) - if err != nil { - panic(err) - } - return n -} - -func Test_Plugin_Observation(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{} - rp := newTestReportPlugin(t, codec, dataSource) - t.Run("Observation protobuf doesn't exceed maxObservationLength", func(t *testing.T) { - obs := MercuryObservationProto{ - Timestamp: math.MaxUint32, - BenchmarkPrice: make([]byte, 24), - PricesValid: true, - MaxFinalizedTimestamp: math.MaxUint32, - MaxFinalizedTimestampValid: true, - LinkFee: make([]byte, 24), - LinkFeeValid: true, - NativeFee: make([]byte, 24), - NativeFeeValid: true, - } - // This assertion is here to force this test to fail if a new field is - // added to the protobuf. In this case, you must add the max value of - // the field to the MercuryObservationProto in the test and only after - // that increment the count below - numFields := reflect.TypeOf(obs).NumField() //nolint:all - // 3 fields internal to pbuf struct - require.Equal(t, 9, numFields-3) - - b, err := proto.Marshal(&obs) - require.NoError(t, err) - assert.LessOrEqual(t, len(b), maxObservationLength) - }) - - t.Run("all observations succeeded", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - MaxFinalizedTimestamp: mercury.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.True(t, p.PricesValid) - assert.Equal(t, obs.MaxFinalizedTimestamp.Val, p.MaxFinalizedTimestamp) - assert.True(t, p.MaxFinalizedTimestampValid) - - fee := mercury.CalculateFee(obs.LinkPrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - - fee = mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("negative link/native prices set fee to max int192", func(t *testing.T) { - obs := Observation{ - LinkPrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(-1), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(-1), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Equal(t, mercury.MaxInt192, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - assert.Equal(t, mercury.MaxInt192, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("some observations failed", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - Err: errors.New("bechmarkPrice error"), - }, - MaxFinalizedTimestamp: mercury.ObsResult[int64]{ - Val: rand.Int63(), - Err: errors.New("maxFinalizedTimestamp error"), - }, - LinkPrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - Err: errors.New("linkPrice error"), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - assert.Zero(t, p.MaxFinalizedTimestamp) - assert.False(t, p.MaxFinalizedTimestampValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - - fee := mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("all observations failed", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Err: errors.New("bechmarkPrice error"), - }, - MaxFinalizedTimestamp: mercury.ObsResult[int64]{ - Err: errors.New("maxFinalizedTimestamp error"), - }, - LinkPrice: mercury.ObsResult[*big.Int]{ - Err: errors.New("linkPrice error"), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Err: errors.New("nativePrice error"), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - assert.Zero(t, p.MaxFinalizedTimestamp) - assert.False(t, p.MaxFinalizedTimestampValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - assert.Zero(t, p.NativeFee) - assert.False(t, p.NativeFeeValid) - }) - - t.Run("encoding fails on some observations", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - MaxFinalizedTimestamp: mercury.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - }) - - t.Run("encoding fails on all observations", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - MaxFinalizedTimestamp: mercury.ObsResult[int64]{ - Val: rand.Int63(), - }, - // encoding never fails on calculated fees - LinkPrice: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - }) -} - -func newUnparseableAttributedObservation() ocrtypes.AttributedObservation { - return ocrtypes.AttributedObservation{ - Observation: []byte{1, 2}, - Observer: commontypes.OracleID(42), - } -} diff --git a/pkg/reportingplugins/mercury/v2/observation.go b/pkg/reportingplugins/mercury/v2/observation.go deleted file mode 100644 index 792f4bf05..000000000 --- a/pkg/reportingplugins/mercury/v2/observation.go +++ /dev/null @@ -1,79 +0,0 @@ -package mercury_v2 //nolint:revive - -import ( - "math/big" - - "github.com/smartcontractkit/libocr/commontypes" -) - -var _ PAO = parsedAttributedObservation{} - -type parsedAttributedObservation struct { - Timestamp uint32 - Observer commontypes.OracleID - - BenchmarkPrice *big.Int - PricesValid bool - - MaxFinalizedTimestamp int64 - MaxFinalizedTimestampValid bool - - LinkFee *big.Int - LinkFeeValid bool - - NativeFee *big.Int - NativeFeeValid bool -} - -func NewParsedAttributedObservation(ts uint32, observer commontypes.OracleID, - bp *big.Int, pricesValid bool, mfts int64, - mftsValid bool, linkFee *big.Int, linkFeeValid bool, nativeFee *big.Int, nativeFeeValid bool) PAO { - return parsedAttributedObservation{ - Timestamp: ts, - Observer: observer, - - BenchmarkPrice: bp, - PricesValid: pricesValid, - - MaxFinalizedTimestamp: mfts, - MaxFinalizedTimestampValid: mftsValid, - - LinkFee: linkFee, - LinkFeeValid: linkFeeValid, - - NativeFee: nativeFee, - NativeFeeValid: nativeFeeValid, - } -} - -func (pao parsedAttributedObservation) GetTimestamp() uint32 { - return pao.Timestamp -} - -func (pao parsedAttributedObservation) GetObserver() commontypes.OracleID { - return pao.Observer -} - -func (pao parsedAttributedObservation) GetBenchmarkPrice() (*big.Int, bool) { - return pao.BenchmarkPrice, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetBid() (*big.Int, bool) { - panic("current observation doesn't contain the field") -} - -func (pao parsedAttributedObservation) GetAsk() (*big.Int, bool) { - panic("current observation doesn't contain the field") -} - -func (pao parsedAttributedObservation) GetMaxFinalizedTimestamp() (int64, bool) { - return pao.MaxFinalizedTimestamp, pao.MaxFinalizedTimestampValid -} - -func (pao parsedAttributedObservation) GetLinkFee() (*big.Int, bool) { - return pao.LinkFee, pao.LinkFeeValid -} - -func (pao parsedAttributedObservation) GetNativeFee() (*big.Int, bool) { - return pao.NativeFee, pao.NativeFeeValid -} diff --git a/pkg/reportingplugins/mercury/v3/mercury.go b/pkg/reportingplugins/mercury/v3/mercury.go deleted file mode 100644 index d502c010d..000000000 --- a/pkg/reportingplugins/mercury/v3/mercury.go +++ /dev/null @@ -1,466 +0,0 @@ -package mercury_v3 //nolint:revive - -import ( - "context" - "errors" - "fmt" - "math" - "math/big" - "time" - - pkgerrors "github.com/pkg/errors" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" -) - -type Observation struct { - BenchmarkPrice mercury.ObsResult[*big.Int] - Bid mercury.ObsResult[*big.Int] - Ask mercury.ObsResult[*big.Int] - - MaxFinalizedTimestamp mercury.ObsResult[int64] - - LinkPrice mercury.ObsResult[*big.Int] - NativePrice mercury.ObsResult[*big.Int] -} - -// DataSource implementations must be thread-safe. Observe may be called by many -// different threads concurrently. -type DataSource interface { - // Observe queries the data source. Returns a value or an error. Once the - // context is expires, Observe may still do cheap computations and return a - // result, but should return as quickly as possible. - // - // More details: In the current implementation, the context passed to - // Observe will time out after MaxDurationObservation. However, Observe - // should *not* make any assumptions about context timeout behavior. Once - // the context times out, Observe should prioritize returning as quickly as - // possible, but may still perform fast computations to return a result - // rather than error. For example, if Observe medianizes a number of data - // sources, some of which already returned a result to Observe prior to the - // context's expiry, Observe might still compute their median, and return it - // instead of an error. - // - // Important: Observe should not perform any potentially time-consuming - // actions like database access, once the context passed has expired. - Observe(ctx context.Context, repts ocrtypes.ReportTimestamp, fetchMaxFinalizedTimestamp bool) (Observation, error) -} - -var _ ocr3types.MercuryPluginFactory = Factory{} - -const maxObservationLength = 32 + // feedID - 4 + // timestamp - mercury.ByteWidthInt192 + // benchmarkPrice - mercury.ByteWidthInt192 + // bid - mercury.ByteWidthInt192 + // ask - 4 + // validFromTimestamp - mercury.ByteWidthInt192 + // linkFee - mercury.ByteWidthInt192 + // nativeFee - 16 /* overapprox. of protobuf overhead */ - -type Factory struct { - dataSource DataSource - logger logger.Logger - onchainConfigCodec mercury.OnchainConfigCodec - reportCodec ReportCodec -} - -func NewFactory(ds DataSource, lggr logger.Logger, occ mercury.OnchainConfigCodec, rc ReportCodec) Factory { - return Factory{ds, lggr, occ, rc} -} - -func (fac Factory) NewMercuryPlugin(configuration ocr3types.MercuryPluginConfig) (ocr3types.MercuryPlugin, ocr3types.MercuryPluginInfo, error) { - offchainConfig, err := mercury.DecodeOffchainConfig(configuration.OffchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - onchainConfig, err := fac.onchainConfigCodec.Decode(configuration.OnchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - maxReportLength, err := fac.reportCodec.MaxReportLength(configuration.N) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - r := &reportingPlugin{ - offchainConfig, - onchainConfig, - fac.dataSource, - fac.logger, - fac.reportCodec, - configuration.ConfigDigest, - configuration.F, - mercury.EpochRound{}, - new(big.Int), - maxReportLength, - } - - return r, ocr3types.MercuryPluginInfo{ - Name: "Mercury", - Limits: ocr3types.MercuryPluginLimits{ - MaxObservationLength: maxObservationLength, - MaxReportLength: maxReportLength, - }, - }, nil -} - -var _ ocr3types.MercuryPlugin = (*reportingPlugin)(nil) - -type reportingPlugin struct { - offchainConfig mercury.OffchainConfig - onchainConfig mercury.OnchainConfig - dataSource DataSource - logger logger.Logger - reportCodec ReportCodec - - configDigest ocrtypes.ConfigDigest - f int - latestAcceptedEpochRound mercury.EpochRound - latestAcceptedMedian *big.Int - maxReportLength int -} - -var MissingPrice = big.NewInt(-1) - -func (rp *reportingPlugin) Observation(ctx context.Context, repts ocrtypes.ReportTimestamp, previousReport ocrtypes.Report) (ocrtypes.Observation, error) { - obs, err := rp.dataSource.Observe(ctx, repts, previousReport == nil) - if err != nil { - return nil, pkgerrors.Errorf("DataSource.Observe returned an error: %s", err) - } - - observationTimestamp := time.Now() - if observationTimestamp.Unix() > math.MaxUint32 { - return nil, fmt.Errorf("current unix epoch %d exceeds max uint32", observationTimestamp.Unix()) - } - p := MercuryObservationProto{Timestamp: uint32(observationTimestamp.Unix())} - var obsErrors []error - - var bpErr, bidErr, askErr error - if obs.BenchmarkPrice.Err != nil { - bpErr = pkgerrors.Wrap(obs.BenchmarkPrice.Err, "failed to observe BenchmarkPrice") - obsErrors = append(obsErrors, bpErr) - } else if benchmarkPrice, err := mercury.EncodeValueInt192(obs.BenchmarkPrice.Val); err != nil { - bpErr = pkgerrors.Wrapf(err, "failed to encode BenchmarkPrice; val=%s", obs.BenchmarkPrice.Val) - obsErrors = append(obsErrors, bpErr) - } else { - p.BenchmarkPrice = benchmarkPrice - } - - if obs.Bid.Err != nil { - bidErr = pkgerrors.Wrap(obs.Bid.Err, "failed to observe Bid") - obsErrors = append(obsErrors, bidErr) - } else if bid, err := mercury.EncodeValueInt192(obs.Bid.Val); err != nil { - bidErr = pkgerrors.Wrapf(err, "failed to encode Bid; val=%s", obs.Bid.Val) - obsErrors = append(obsErrors, bidErr) - } else { - p.Bid = bid - } - - if obs.Ask.Err != nil { - askErr = pkgerrors.Wrap(obs.Ask.Err, "failed to observe Ask") - obsErrors = append(obsErrors, askErr) - } else if ask, err := mercury.EncodeValueInt192(obs.Ask.Val); err != nil { - askErr = pkgerrors.Wrapf(err, "failed to encode Ask; val=%s", obs.Ask.Val) - obsErrors = append(obsErrors, askErr) - } else { - p.Ask = ask - } - - if bpErr == nil && bidErr == nil && askErr == nil { - p.PricesValid = true - } - - var maxFinalizedTimestampErr error - if obs.MaxFinalizedTimestamp.Err != nil { - maxFinalizedTimestampErr = pkgerrors.Wrap(obs.MaxFinalizedTimestamp.Err, "failed to observe MaxFinalizedTimestamp") - obsErrors = append(obsErrors, maxFinalizedTimestampErr) - } else { - p.MaxFinalizedTimestamp = obs.MaxFinalizedTimestamp.Val - p.MaxFinalizedTimestampValid = true - } - - var linkErr error - if obs.LinkPrice.Err != nil { - linkErr = pkgerrors.Wrap(obs.LinkPrice.Err, "failed to observe LINK price") - obsErrors = append(obsErrors, linkErr) - } else if obs.LinkPrice.Val.Cmp(MissingPrice) <= 0 { - p.LinkFee = mercury.MaxInt192Enc - } else { - linkFee := mercury.CalculateFee(obs.LinkPrice.Val, rp.offchainConfig.BaseUSDFee) - if linkFeeEncoded, err := mercury.EncodeValueInt192(linkFee); err != nil { - linkErr = pkgerrors.Wrapf(err, "failed to encode LINK fee; val=%s", linkFee) - obsErrors = append(obsErrors, linkErr) - } else { - p.LinkFee = linkFeeEncoded - } - } - - if linkErr == nil { - p.LinkFeeValid = true - } - - var nativeErr error - if obs.NativePrice.Err != nil { - nativeErr = pkgerrors.Wrap(obs.NativePrice.Err, "failed to observe native price") - obsErrors = append(obsErrors, nativeErr) - } else if obs.NativePrice.Val.Cmp(MissingPrice) <= 0 { - p.NativeFee = mercury.MaxInt192Enc - } else { - nativeFee := mercury.CalculateFee(obs.NativePrice.Val, rp.offchainConfig.BaseUSDFee) - if nativeFeeEncoded, err := mercury.EncodeValueInt192(nativeFee); err != nil { - nativeErr = pkgerrors.Wrapf(err, "failed to encode native fee; val=%s", nativeFee) - obsErrors = append(obsErrors, nativeErr) - } else { - p.NativeFee = nativeFeeEncoded - } - } - - if nativeErr == nil { - p.NativeFeeValid = true - } - - if len(obsErrors) > 0 { - rp.logger.Warnw(fmt.Sprintf("Observe failed %d/6 observations", len(obsErrors)), "err", errors.Join(obsErrors...)) - } - - return proto.Marshal(&p) -} - -func parseAttributedObservation(ao ocrtypes.AttributedObservation) (PAO, error) { - var pao parsedAttributedObservation - var obs MercuryObservationProto - if err := proto.Unmarshal(ao.Observation, &obs); err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("attributed observation cannot be unmarshaled: %s", err) - } - - pao.Timestamp = obs.Timestamp - pao.Observer = ao.Observer - - if obs.PricesValid { - var err error - pao.BenchmarkPrice, err = mercury.DecodeValueInt192(obs.BenchmarkPrice) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("benchmarkPrice cannot be converted to big.Int: %s", err) - } - pao.Bid, err = mercury.DecodeValueInt192(obs.Bid) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("bid cannot be converted to big.Int: %s", err) - } - pao.Ask, err = mercury.DecodeValueInt192(obs.Ask) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("ask cannot be converted to big.Int: %s", err) - } - pao.PricesValid = true - } - - if obs.MaxFinalizedTimestampValid { - pao.MaxFinalizedTimestamp = obs.MaxFinalizedTimestamp - pao.MaxFinalizedTimestampValid = true - } - - if obs.LinkFeeValid { - var err error - pao.LinkFee, err = mercury.DecodeValueInt192(obs.LinkFee) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("link price cannot be converted to big.Int: %s", err) - } - pao.LinkFeeValid = true - } - if obs.NativeFeeValid { - var err error - pao.NativeFee, err = mercury.DecodeValueInt192(obs.NativeFee) - if err != nil { - return parsedAttributedObservation{}, pkgerrors.Errorf("native price cannot be converted to big.Int: %s", err) - } - pao.NativeFeeValid = true - } - - return pao, nil -} - -func parseAttributedObservations(lggr logger.Logger, aos []ocrtypes.AttributedObservation) []PAO { - paos := make([]PAO, 0, len(aos)) - for i, ao := range aos { - pao, err := parseAttributedObservation(ao) - if err != nil { - lggr.Warnw("parseAttributedObservations: dropping invalid observation", - "observer", ao.Observer, - "error", err, - "i", i, - ) - continue - } - paos = append(paos, pao) - } - return paos -} - -func (rp *reportingPlugin) Report(repts ocrtypes.ReportTimestamp, previousReport ocrtypes.Report, aos []ocrtypes.AttributedObservation) (shouldReport bool, report ocrtypes.Report, err error) { - paos := parseAttributedObservations(rp.logger, aos) - - if len(paos) == 0 { - return false, nil, errors.New("got zero valid attributed observations") - } - - // By assumption, we have at most f malicious oracles, so there should be at least f+1 valid paos - if !(rp.f+1 <= len(paos)) { - return false, nil, pkgerrors.Errorf("only received %v valid attributed observations, but need at least f+1 (%v)", len(paos), rp.f+1) - } - - rf, err := rp.buildReportFields(previousReport, paos) - if err != nil { - rp.logger.Errorw("failed to build report fields", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts, "err", err) - return false, nil, err - } - - if rf.Timestamp < rf.ValidFromTimestamp { - rp.logger.Debugw("shouldReport: no (overlap)", "observationTimestamp", rf.Timestamp, "validFromTimestamp", rf.ValidFromTimestamp, "repts", repts) - return false, nil, nil - } - - if err = rp.validateReport(rf); err != nil { - rp.logger.Errorw("shouldReport: no (validation error)", "reportFields", rf, "err", err, "repts", repts, "paos", paos) - return false, nil, err - } - rp.logger.Debugw("shouldReport: yes", "repts", repts) - - report, err = rp.reportCodec.BuildReport(rf) - if err != nil { - rp.logger.Debugw("failed to BuildReport", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts) - return false, nil, err - } - - if !(len(report) <= rp.maxReportLength) { - return false, nil, pkgerrors.Errorf("report with len %d violates MaxReportLength limit set by ReportCodec (%d)", len(report), rp.maxReportLength) - } else if len(report) == 0 { - return false, nil, errors.New("report may not have zero length (invariant violation)") - } - - return true, report, nil -} - -func (rp *reportingPlugin) buildReportFields(previousReport ocrtypes.Report, paos []PAO) (rf ReportFields, merr error) { - mPaos := convert(paos) - rf.Timestamp = mercury.GetConsensusTimestamp(mPaos) - - var err error - if previousReport != nil { - var maxFinalizedTimestamp uint32 - maxFinalizedTimestamp, err = rp.reportCodec.ObservationTimestampFromReport(previousReport) - merr = errors.Join(merr, err) - rf.ValidFromTimestamp = maxFinalizedTimestamp + 1 - } else { - var maxFinalizedTimestamp int64 - maxFinalizedTimestamp, err = mercury.GetConsensusMaxFinalizedTimestamp(convertMaxFinalizedTimestamp(paos), rp.f) - if err != nil { - merr = errors.Join(merr, err) - } else if maxFinalizedTimestamp < 0 { - // no previous observation timestamp available, e.g. in case of new - // feed; use current timestamp as start of range - rf.ValidFromTimestamp = rf.Timestamp - } else if maxFinalizedTimestamp+1 > math.MaxUint32 { - merr = errors.Join(err, fmt.Errorf("maxFinalizedTimestamp is too large, got: %d", maxFinalizedTimestamp)) - } else { - rf.ValidFromTimestamp = uint32(maxFinalizedTimestamp + 1) - } - } - - rf.BenchmarkPrice, err = mercury.GetConsensusBenchmarkPrice(mPaos, rp.f) - merr = errors.Join(merr, pkgerrors.Wrap(err, "GetConsensusBenchmarkPrice failed")) - - rf.Bid, err = mercury.GetConsensusBid(convertBid(paos), rp.f) - merr = errors.Join(merr, pkgerrors.Wrap(err, "GetConsensusBid failed")) - - rf.Ask, err = mercury.GetConsensusAsk(convertAsk(paos), rp.f) - merr = errors.Join(merr, pkgerrors.Wrap(err, "GetConsensusAsk failed")) - - rf.LinkFee, err = mercury.GetConsensusLinkFee(convertLinkFee(paos), rp.f) - if err != nil { - // It is better to generate a report that will validate for free, - // rather than no report at all, if we cannot come to consensus on a - // valid fee. - rp.logger.Errorw("Cannot come to consensus on LINK fee, falling back to 0", "err", err, "paos", paos) - rf.LinkFee = big.NewInt(0) - } - - rf.NativeFee, err = mercury.GetConsensusNativeFee(convertNativeFee(paos), rp.f) - if err != nil { - // It is better to generate a report that will validate for free, - // rather than no report at all, if we cannot come to consensus on a - // valid fee. - rp.logger.Errorw("Cannot come to consensus on Native fee, falling back to 0", "err", err, "paos", paos) - rf.NativeFee = big.NewInt(0) - } - - if int64(rf.Timestamp)+int64(rp.offchainConfig.ExpirationWindow) > math.MaxUint32 { - merr = errors.Join(merr, fmt.Errorf("timestamp %d + expiration window %d overflows uint32", rf.Timestamp, rp.offchainConfig.ExpirationWindow)) - } else { - rf.ExpiresAt = rf.Timestamp + rp.offchainConfig.ExpirationWindow - } - - return rf, merr -} - -func (rp *reportingPlugin) validateReport(rf ReportFields) error { - return errors.Join( - mercury.ValidateBetween("median benchmark price", rf.BenchmarkPrice, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateBetween("median bid", rf.Bid, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateBetween("median ask", rf.Ask, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateFee("median link fee", rf.LinkFee), - mercury.ValidateFee("median native fee", rf.NativeFee), - mercury.ValidateValidFromTimestamp(rf.Timestamp, rf.ValidFromTimestamp), - mercury.ValidateExpiresAt(rf.Timestamp, rf.ExpiresAt), - ) -} - -func (rp *reportingPlugin) Close() error { - return nil -} - -// convert funcs are necessary because go is not smart enough to cast -// []interface1 to []interface2 even if interface1 is a superset of interface2 -func convert(pao []PAO) (ret []mercury.PAO) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertMaxFinalizedTimestamp(pao []PAO) (ret []mercury.PAOMaxFinalizedTimestamp) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertBid(pao []PAO) (ret []mercury.PAOBid) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertAsk(pao []PAO) (ret []mercury.PAOAsk) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertLinkFee(pao []PAO) (ret []mercury.PAOLinkFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertNativeFee(pao []PAO) (ret []mercury.PAONativeFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} diff --git a/pkg/reportingplugins/mercury/v3/mercury_observation_v3.proto b/pkg/reportingplugins/mercury/v3/mercury_observation_v3.proto deleted file mode 100644 index 926f44f4b..000000000 --- a/pkg/reportingplugins/mercury/v3/mercury_observation_v3.proto +++ /dev/null @@ -1,21 +0,0 @@ -syntax="proto3"; - -package mercury_v3; -option go_package = ".;mercury_v3"; - -message MercuryObservationProto { - uint32 timestamp = 1; - - bytes benchmarkPrice = 2; - bytes bid = 3; - bytes ask = 4; - bool pricesValid = 5; - - int64 maxFinalizedTimestamp = 6; - bool maxFinalizedTimestampValid = 7; - - bytes linkFee = 8; - bool linkFeeValid = 9; - bytes nativeFee = 10; - bool nativeFeeValid = 11; -} diff --git a/pkg/reportingplugins/mercury/v3/mercury_test.go b/pkg/reportingplugins/mercury/v3/mercury_test.go deleted file mode 100644 index 7ab6b5bc4..000000000 --- a/pkg/reportingplugins/mercury/v3/mercury_test.go +++ /dev/null @@ -1,717 +0,0 @@ -package mercury_v3 //nolint:revive - -import ( - "context" - "math" - "math/big" - "math/rand" - "reflect" - "testing" - "time" - - "github.com/pkg/errors" - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/commontypes" - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" -) - -type testDataSource struct { - Obs Observation -} - -func (ds testDataSource) Observe(ctx context.Context, repts ocrtypes.ReportTimestamp, fetchMaxFinalizedTimestamp bool) (Observation, error) { - return ds.Obs, nil -} - -type testReportCodec struct { - observationTimestamp uint32 - builtReport ocrtypes.Report - - builtReportFields *ReportFields - err error -} - -func (rc *testReportCodec) BuildReport(rf ReportFields) (ocrtypes.Report, error) { - rc.builtReportFields = &rf - - return rc.builtReport, nil -} - -func (rc testReportCodec) MaxReportLength(n int) (int, error) { - return 123, nil -} - -func (rc testReportCodec) ObservationTimestampFromReport(ocrtypes.Report) (uint32, error) { - return rc.observationTimestamp, rc.err -} - -func newTestReportPlugin(t *testing.T, codec *testReportCodec, ds *testDataSource) *reportingPlugin { - offchainConfig := mercury.OffchainConfig{ - ExpirationWindow: 1, - BaseUSDFee: decimal.NewFromInt32(1), - } - onchainConfig := mercury.OnchainConfig{ - Min: big.NewInt(1), - Max: big.NewInt(1000), - } - maxReportLength, _ := codec.MaxReportLength(4) - return &reportingPlugin{ - offchainConfig: offchainConfig, - onchainConfig: onchainConfig, - dataSource: ds, - logger: logger.Test(t), - reportCodec: codec, - configDigest: ocrtypes.ConfigDigest{}, - f: 1, - latestAcceptedEpochRound: mercury.EpochRound{}, - latestAcceptedMedian: big.NewInt(0), - maxReportLength: maxReportLength, - } -} - -func newValidProtos() []*MercuryObservationProto { - return []*MercuryObservationProto{ - &MercuryObservationProto{ - Timestamp: 42, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(123)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(120)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(130)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.1e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.1e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 45, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(234)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(230)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(240)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.2e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.2e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 47, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(345)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(340)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(350)), - PricesValid: true, - - MaxFinalizedTimestamp: 39, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.3e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.3e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 39, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(456)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(450)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(460)), - PricesValid: true, - - MaxFinalizedTimestamp: 39, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.4e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.4e18)), - NativeFeeValid: true, - }, - } -} - -func newValidAos(t *testing.T, protos ...*MercuryObservationProto) (aos []ocrtypes.AttributedObservation) { - if len(protos) == 0 { - protos = newValidProtos() - } - aos = make([]ocrtypes.AttributedObservation, len(protos)) - for i := range aos { - marshalledObs, err := proto.Marshal(protos[i]) - require.NoError(t, err) - aos[i] = ocrtypes.AttributedObservation{ - Observation: marshalledObs, - Observer: commontypes.OracleID(i), - } - } - return -} - -func Test_Plugin_Report(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{ - builtReport: []byte{1, 2, 3, 4}, - } - rp := newTestReportPlugin(t, codec, dataSource) - repts := ocrtypes.ReportTimestamp{} - - t.Run("when previous report is nil", func(t *testing.T) { - t.Run("errors if not enough attributed observations", func(t *testing.T) { - _, _, err := rp.Report(repts, nil, newValidAos(t)[0:1]) - assert.EqualError(t, err, "only received 1 valid attributed observations, but need at least f+1 (2)") - }) - - t.Run("errors if too many maxFinalizedTimestamp observations are invalid", func(t *testing.T) { - ps := newValidProtos() - ps[0].MaxFinalizedTimestampValid = false - ps[1].MaxFinalizedTimestampValid = false - ps[2].MaxFinalizedTimestampValid = false - aos := newValidAos(t, ps...) - - should, _, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - assert.False(t, should) - assert.EqualError(t, err, "fewer than f+1 observations have a valid maxFinalizedTimestamp (got: 1/4)") - }) - t.Run("errors if maxFinalizedTimestamp is too large", func(t *testing.T) { - ps := newValidProtos() - ps[0].MaxFinalizedTimestamp = math.MaxUint32 - ps[1].MaxFinalizedTimestamp = math.MaxUint32 - ps[2].MaxFinalizedTimestamp = math.MaxUint32 - ps[3].MaxFinalizedTimestamp = math.MaxUint32 - aos := newValidAos(t, ps...) - - should, _, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - assert.False(t, should) - assert.EqualError(t, err, "maxFinalizedTimestamp is too large, got: 4294967295") - }) - - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is positive", func(t *testing.T) { - aos := newValidAos(t) - - should, report, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, ReportFields{ - ValidFromTimestamp: 41, // consensus maxFinalizedTimestamp is 40, so validFrom should be 40+1 - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(340), - Ask: big.NewInt(350), - }, *codec.builtReportFields) - }) - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is zero", func(t *testing.T) { - protos := newValidProtos() - for i := range protos { - protos[i].MaxFinalizedTimestamp = 0 - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, ReportFields{ - ValidFromTimestamp: 1, - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(340), - Ask: big.NewInt(350), - }, *codec.builtReportFields) - }) - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is -1 (missing feed)", func(t *testing.T) { - protos := newValidProtos() - for i := range protos { - protos[i].MaxFinalizedTimestamp = -1 - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, ReportFields{ - ValidFromTimestamp: 45, // in case of missing feed, ValidFromTimestamp=Timestamp for first report - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(340), - Ask: big.NewInt(350), - }, *codec.builtReportFields) - }) - - t.Run("succeeds, ignoring unparseable attributed observation", func(t *testing.T) { - aos := newValidAos(t) - aos[0] = newUnparseableAttributedObservation() - - should, report, err := rp.Report(repts, nil, aos) - require.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, ReportFields{ - ValidFromTimestamp: 40, // consensus maxFinalizedTimestamp is 39, so validFrom should be 39+1 - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(340), - Ask: big.NewInt(350), - }, *codec.builtReportFields) - }) - }) - - t.Run("when previous report is present", func(t *testing.T) { - *codec = testReportCodec{ - observationTimestamp: uint32(rand.Int31n(math.MaxInt16)), - builtReport: []byte{1, 2, 3, 4}, - } - previousReport := ocrtypes.Report{} - - t.Run("succeeds and uses timestamp from previous report if valid", func(t *testing.T) { - protos := newValidProtos() - ts := codec.observationTimestamp + 1 - for i := range protos { - protos[i].Timestamp = ts - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(repts, previousReport, aos) - require.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, ReportFields{ - ValidFromTimestamp: codec.observationTimestamp + 1, // previous observation timestamp +1 second - Timestamp: ts, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: ts + 1, - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(340), - Ask: big.NewInt(350), - }, *codec.builtReportFields) - }) - t.Run("errors if cannot extract timestamp from previous report", func(t *testing.T) { - codec.err = errors.New("something exploded trying to extract timestamp") - aos := newValidAos(t) - - should, _, err := rp.Report(ocrtypes.ReportTimestamp{}, previousReport, aos) - assert.False(t, should) - assert.EqualError(t, err, "something exploded trying to extract timestamp") - }) - t.Run("does not report if observationTimestamp < validFromTimestamp", func(t *testing.T) { - codec.observationTimestamp = 43 - codec.err = nil - - protos := newValidProtos() - for i := range protos { - protos[i].Timestamp = 42 - } - aos := newValidAos(t, protos...) - - should, _, err := rp.Report(ocrtypes.ReportTimestamp{}, previousReport, aos) - assert.False(t, should) - assert.NoError(t, err) - }) - t.Run("uses 0 values for link/native if they are invalid", func(t *testing.T) { - codec.observationTimestamp = 42 - codec.err = nil - - protos := newValidProtos() - for i := range protos { - protos[i].LinkFeeValid = false - protos[i].NativeFeeValid = false - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(ocrtypes.ReportTimestamp{}, previousReport, aos) - assert.True(t, should) - assert.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, "0", codec.builtReportFields.LinkFee.String()) - assert.Equal(t, "0", codec.builtReportFields.NativeFee.String()) - }) - }) - - t.Run("buildReport failures", func(t *testing.T) { - t.Run("Report errors when the report is too large", func(t *testing.T) { - aos := newValidAos(t) - codec.builtReport = make([]byte, 1<<16) - - _, _, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - - assert.EqualError(t, err, "report with len 65536 violates MaxReportLength limit set by ReportCodec (123)") - }) - - t.Run("Report errors when the report length is 0", func(t *testing.T) { - aos := newValidAos(t) - codec.builtReport = []byte{} - _, _, err := rp.Report(ocrtypes.ReportTimestamp{}, nil, aos) - - assert.EqualError(t, err, "report may not have zero length (invariant violation)") - }) - }) -} - -func Test_Plugin_validateReport(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{} - rp := newTestReportPlugin(t, codec, dataSource) - - t.Run("valid reports", func(t *testing.T) { - rf := ReportFields{ - ValidFromTimestamp: 42, - Timestamp: 43, - NativeFee: big.NewInt(100), - LinkFee: big.NewInt(50), - ExpiresAt: 44, - BenchmarkPrice: big.NewInt(150), - Bid: big.NewInt(140), - Ask: big.NewInt(160), - } - err := rp.validateReport(rf) - require.NoError(t, err) - - rf = ReportFields{ - ValidFromTimestamp: 42, - Timestamp: 42, - NativeFee: big.NewInt(0), - LinkFee: big.NewInt(0), - ExpiresAt: 42, - BenchmarkPrice: big.NewInt(1), - Bid: big.NewInt(1), - Ask: big.NewInt(1), - } - err = rp.validateReport(rf) - require.NoError(t, err) - }) - t.Run("fails validation", func(t *testing.T) { - rf := ReportFields{ - ValidFromTimestamp: 44, // later than timestamp not allowed - Timestamp: 43, - NativeFee: big.NewInt(-1), // negative value not allowed - LinkFee: big.NewInt(-1), // negative value not allowed - ExpiresAt: 42, // before timestamp - BenchmarkPrice: big.NewInt(150000), // exceeds max - Bid: big.NewInt(150000), // exceeds max - Ask: big.NewInt(150000), // exceeds max - } - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median benchmark price (Value: 150000) is outside of allowable range (Min: 1, Max: 1000)") - assert.Contains(t, err.Error(), "median bid (Value: 150000) is outside of allowable range (Min: 1, Max: 1000)") - assert.Contains(t, err.Error(), "median ask (Value: 150000) is outside of allowable range (Min: 1, Max: 1000)") - assert.Contains(t, err.Error(), "median link fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "median native fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "observationTimestamp (Value: 43) must be >= validFromTimestamp (Value: 44)") - assert.Contains(t, err.Error(), "expiresAt (Value: 42) must be ahead of observation timestamp (Value: 43)") - }) - - t.Run("zero values", func(t *testing.T) { - rf := ReportFields{} - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median benchmark price: got nil value") - assert.Contains(t, err.Error(), "median bid: got nil value") - assert.Contains(t, err.Error(), "median ask: got nil value") - assert.Contains(t, err.Error(), "median native fee: got nil value") - assert.Contains(t, err.Error(), "median link fee: got nil value") - }) -} - -func mustDecodeBigInt(b []byte) *big.Int { - n, err := mercury.DecodeValueInt192(b) - if err != nil { - panic(err) - } - return n -} - -func Test_Plugin_Observation(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{} - rp := newTestReportPlugin(t, codec, dataSource) - t.Run("Observation protobuf doesn't exceed maxObservationLength", func(t *testing.T) { - obs := MercuryObservationProto{ - Timestamp: math.MaxUint32, - BenchmarkPrice: make([]byte, 24), - Bid: make([]byte, 24), - Ask: make([]byte, 24), - PricesValid: true, - MaxFinalizedTimestamp: math.MaxUint32, - MaxFinalizedTimestampValid: true, - LinkFee: make([]byte, 24), - LinkFeeValid: true, - NativeFee: make([]byte, 24), - NativeFeeValid: true, - } - // This assertion is here to force this test to fail if a new field is - // added to the protobuf. In this case, you must add the max value of - // the field to the MercuryObservationProto in the test and only after - // that increment the count below - numFields := reflect.TypeOf(obs).NumField() //nolint:all - // 3 fields internal to pbuf struct - require.Equal(t, 11, numFields-3) - - b, err := proto.Marshal(&obs) - require.NoError(t, err) - assert.LessOrEqual(t, len(b), maxObservationLength) - }) - - t.Run("all observations succeeded", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - Bid: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - Ask: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - MaxFinalizedTimestamp: mercury.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.True(t, p.PricesValid) - assert.Equal(t, obs.MaxFinalizedTimestamp.Val, p.MaxFinalizedTimestamp) - assert.True(t, p.MaxFinalizedTimestampValid) - - fee := mercury.CalculateFee(obs.LinkPrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - - fee = mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("negative link/native prices set fee to max int192", func(t *testing.T) { - obs := Observation{ - LinkPrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(-1), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(-1), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Equal(t, mercury.MaxInt192, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - assert.Equal(t, mercury.MaxInt192, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("some observations failed", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - Err: errors.New("bechmarkPrice error"), - }, - MaxFinalizedTimestamp: mercury.ObsResult[int64]{ - Val: rand.Int63(), - Err: errors.New("maxFinalizedTimestamp error"), - }, - LinkPrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - Err: errors.New("linkPrice error"), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - assert.Zero(t, p.MaxFinalizedTimestamp) - assert.False(t, p.MaxFinalizedTimestampValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - - fee := mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("all observations failed", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Err: errors.New("bechmarkPrice error"), - }, - Bid: mercury.ObsResult[*big.Int]{ - Err: errors.New("bid error"), - }, - Ask: mercury.ObsResult[*big.Int]{ - Err: errors.New("ask error"), - }, - MaxFinalizedTimestamp: mercury.ObsResult[int64]{ - Err: errors.New("maxFinalizedTimestamp error"), - }, - LinkPrice: mercury.ObsResult[*big.Int]{ - Err: errors.New("linkPrice error"), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Err: errors.New("nativePrice error"), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.Zero(t, p.Bid) - assert.Zero(t, p.Ask) - assert.False(t, p.PricesValid) - assert.Zero(t, p.MaxFinalizedTimestamp) - assert.False(t, p.MaxFinalizedTimestampValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - assert.Zero(t, p.NativeFee) - assert.False(t, p.NativeFeeValid) - }) - - t.Run("encoding fails on some observations", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - MaxFinalizedTimestamp: mercury.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - }) - - t.Run("encoding fails on all observations", func(t *testing.T) { - obs := Observation{ - BenchmarkPrice: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - Bid: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - Ask: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - MaxFinalizedTimestamp: mercury.ObsResult[int64]{ - Val: rand.Int63(), - }, - // encoding never fails on calculated fees - LinkPrice: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - NativePrice: mercury.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), ocrtypes.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Zero(t, p.BenchmarkPrice) - assert.Zero(t, p.Bid) - assert.Zero(t, p.Ask) - assert.False(t, p.PricesValid) - }) -} - -func newUnparseableAttributedObservation() ocrtypes.AttributedObservation { - return ocrtypes.AttributedObservation{ - Observation: []byte{1, 2}, - Observer: commontypes.OracleID(42), - } -} diff --git a/pkg/reportingplugins/mercury/v3/observation.go b/pkg/reportingplugins/mercury/v3/observation.go deleted file mode 100644 index 6e590b1bb..000000000 --- a/pkg/reportingplugins/mercury/v3/observation.go +++ /dev/null @@ -1,87 +0,0 @@ -package mercury_v3 //nolint:revive - -import ( - "math/big" - - "github.com/smartcontractkit/libocr/commontypes" -) - -var _ PAO = parsedAttributedObservation{} - -type parsedAttributedObservation struct { - Timestamp uint32 - Observer commontypes.OracleID - - BenchmarkPrice *big.Int - Bid *big.Int - Ask *big.Int - PricesValid bool - - MaxFinalizedTimestamp int64 - MaxFinalizedTimestampValid bool - - LinkFee *big.Int - LinkFeeValid bool - - NativeFee *big.Int - NativeFeeValid bool -} - -func NewParsedAttributedObservation(ts uint32, observer commontypes.OracleID, - bp *big.Int, bid *big.Int, ask *big.Int, pricesValid bool, mfts int64, - mftsValid bool, linkFee *big.Int, linkFeeValid bool, nativeFee *big.Int, nativeFeeValid bool) PAO { - return parsedAttributedObservation{ - Timestamp: ts, - Observer: observer, - - BenchmarkPrice: bp, - Bid: bid, - Ask: ask, - PricesValid: pricesValid, - - MaxFinalizedTimestamp: mfts, - MaxFinalizedTimestampValid: mftsValid, - - LinkFee: linkFee, - LinkFeeValid: linkFeeValid, - - NativeFee: nativeFee, - NativeFeeValid: nativeFeeValid, - } -} - -func (pao parsedAttributedObservation) GetTimestamp() uint32 { - return pao.Timestamp -} - -func (pao parsedAttributedObservation) GetObserver() commontypes.OracleID { - return pao.Observer -} - -func (pao parsedAttributedObservation) GetBenchmarkPrice() (*big.Int, bool) { - return pao.BenchmarkPrice, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetBid() (*big.Int, bool) { - return pao.Bid, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetAsk() (*big.Int, bool) { - return pao.Ask, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetMaxFinalizedTimestamp() (int64, bool) { - if pao.MaxFinalizedTimestamp < -1 { - // values below -1 are not valid - return 0, false - } - return pao.MaxFinalizedTimestamp, pao.MaxFinalizedTimestampValid -} - -func (pao parsedAttributedObservation) GetLinkFee() (*big.Int, bool) { - return pao.LinkFee, pao.LinkFeeValid -} - -func (pao parsedAttributedObservation) GetNativeFee() (*big.Int, bool) { - return pao.NativeFee, pao.NativeFeeValid -} diff --git a/pkg/reportingplugins/mercury/validation.go b/pkg/reportingplugins/mercury/validation.go deleted file mode 100644 index d56663107..000000000 --- a/pkg/reportingplugins/mercury/validation.go +++ /dev/null @@ -1,43 +0,0 @@ -package mercury - -import ( - "fmt" - "math/big" - - pkgerrors "github.com/pkg/errors" -) - -// NOTE: hardcoded for now, this may need to change if we support block range on chains other than eth -const EvmHashLen = 32 - -// ValidateBetween checks that value is between min and max -func ValidateBetween(name string, answer *big.Int, min, max *big.Int) error { - if answer == nil { - return fmt.Errorf("%s: got nil value", name) - } - if !(min.Cmp(answer) <= 0 && answer.Cmp(max) <= 0) { - return pkgerrors.Errorf("%s (Value: %s) is outside of allowable range (Min: %s, Max: %s)", name, answer, min, max) - } - - return nil -} - -func ValidateValidFromTimestamp(observationTimestamp uint32, validFromTimestamp uint32) error { - if observationTimestamp < validFromTimestamp { - return pkgerrors.Errorf("observationTimestamp (Value: %d) must be >= validFromTimestamp (Value: %d)", observationTimestamp, validFromTimestamp) - } - - return nil -} - -func ValidateExpiresAt(observationTimestamp uint32, expiresAt uint32) error { - if observationTimestamp > expiresAt { - return pkgerrors.Errorf("expiresAt (Value: %d) must be ahead of observation timestamp (Value: %d)", expiresAt, observationTimestamp) - } - - return nil -} - -func ValidateFee(name string, answer *big.Int) error { - return ValidateBetween(name, answer, big.NewInt(0), MaxInt192) -} diff --git a/pkg/reportingplugins/mercury/validation_test.go b/pkg/reportingplugins/mercury/validation_test.go deleted file mode 100644 index 87446b5e4..000000000 --- a/pkg/reportingplugins/mercury/validation_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package mercury - -import ( - "math/big" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestValidation(t *testing.T) { - min := big.NewInt(0) - max := big.NewInt(10_000) - - badMin := big.NewInt(9_000) - badMax := big.NewInt(10) - - t.Run("ValidateValidFromTimestamp", func(t *testing.T) { - t.Run("succeeds when observationTimestamp is >= validFromTimestamp", func(t *testing.T) { - err := ValidateValidFromTimestamp(456, 123) - assert.NoError(t, err) - err = ValidateValidFromTimestamp(123, 123) - assert.NoError(t, err) - }) - t.Run("fails when observationTimestamp is < validFromTimestamp", func(t *testing.T) { - err := ValidateValidFromTimestamp(111, 112) - assert.EqualError(t, err, "observationTimestamp (Value: 111) must be >= validFromTimestamp (Value: 112)") - }) - }) - t.Run("ValidateExpiresAt", func(t *testing.T) { - t.Run("succeeds when observationTimestamp <= expiresAt", func(t *testing.T) { - err := ValidateExpiresAt(123, 456) - assert.NoError(t, err) - err = ValidateExpiresAt(123, 123) - assert.NoError(t, err) - }) - - t.Run("fails when observationTimestamp > expiresAt", func(t *testing.T) { - err := ValidateExpiresAt(112, 111) - assert.EqualError(t, err, "expiresAt (Value: 111) must be ahead of observation timestamp (Value: 112)") - }) - }) - t.Run("ValidateBetween", func(t *testing.T) { - bm := big.NewInt(346) - err := ValidateBetween("test foo", bm, min, max) - assert.NoError(t, err) - - err = ValidateBetween("test bar", bm, min, badMax) - assert.EqualError(t, err, "test bar (Value: 346) is outside of allowable range (Min: 0, Max: 10)") - err = ValidateBetween("test baz", bm, badMin, max) - assert.EqualError(t, err, "test baz (Value: 346) is outside of allowable range (Min: 9000, Max: 10000)") - }) -} diff --git a/pkg/reportingplugins/mercury/value.go b/pkg/reportingplugins/mercury/value.go deleted file mode 100644 index 50677e3bc..000000000 --- a/pkg/reportingplugins/mercury/value.go +++ /dev/null @@ -1,45 +0,0 @@ -package mercury - -import ( - "math/big" - - "github.com/smartcontractkit/libocr/bigbigendian" -) - -var MaxInt192 *big.Int -var MaxInt192Enc []byte - -func init() { - one := big.NewInt(1) - // Compute the maximum value of int192 - // 1<<191 - 1 - MaxInt192 = new(big.Int).Lsh(one, 191) - MaxInt192.Sub(MaxInt192, one) - - var err error - MaxInt192Enc, err = EncodeValueInt192(MaxInt192) - if err != nil { - panic(err) - } -} - -// Bounds on an int192 -const ByteWidthInt192 = 24 - -// Encodes a value using 24-byte big endian two's complement representation. This function never panics. -func EncodeValueInt192(i *big.Int) ([]byte, error) { - return bigbigendian.SerializeSigned(ByteWidthInt192, i) -} - -// Decodes a value using 24-byte big endian two's complement representation. This function never panics. -func DecodeValueInt192(s []byte) (*big.Int, error) { - return bigbigendian.DeserializeSigned(ByteWidthInt192, s) -} - -func MustEncodeValueInt192(i *big.Int) []byte { - val, err := EncodeValueInt192(i) - if err != nil { - panic(err) - } - return val -} diff --git a/pkg/reportingplugins/mercury/value_test.go b/pkg/reportingplugins/mercury/value_test.go deleted file mode 100644 index 900e0d6a3..000000000 --- a/pkg/reportingplugins/mercury/value_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package mercury - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_Values(t *testing.T) { - t.Run("serializes max int192", func(t *testing.T) { - encoded, err := EncodeValueInt192(MaxInt192) - require.NoError(t, err) - decoded, err := DecodeValueInt192(encoded) - require.NoError(t, err) - assert.Equal(t, MaxInt192, decoded) - }) -} diff --git a/pkg/services/health.go b/pkg/services/health.go index fad971a6c..541783a5a 100644 --- a/pkg/services/health.go +++ b/pkg/services/health.go @@ -37,6 +37,8 @@ type HealthChecker struct { chStop chan struct{} chDone chan struct{} + ver, sha string + servicesMu sync.RWMutex services map[string]HealthReporter @@ -70,8 +72,23 @@ var ( ) ) -func NewChecker() *HealthChecker { +func NewChecker(ver, sha string) *HealthChecker { + if ver == "" || sha == "" { + if bi, ok := debug.ReadBuildInfo(); ok { + if ver == "" { + ver = bi.Main.Version + } + if sha == "" { + sha = bi.Main.Sum + } + } + } + if len(sha) > 7 { + sha = sha[:7] + } return &HealthChecker{ + ver: ver, + sha: sha, services: make(map[string]HealthReporter, 10), healthy: make(map[string]error, 10), ready: make(map[string]error, 10), @@ -82,13 +99,7 @@ func NewChecker() *HealthChecker { func (c *HealthChecker) Start() error { return c.StartOnce("HealthCheck", func() error { - if bi, ok := debug.ReadBuildInfo(); ok { - hash := bi.Main.Sum - if len(hash) > 7 { - hash = hash[:7] - } - version.WithLabelValues(bi.Main.Version, hash).Inc() - } + version.WithLabelValues(c.ver, c.sha).Inc() // update immediately c.update() diff --git a/pkg/services/servicetest/run.go b/pkg/services/servicetest/run.go new file mode 100644 index 000000000..59c6e6f87 --- /dev/null +++ b/pkg/services/servicetest/run.go @@ -0,0 +1,74 @@ +package servicetest + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" +) + +type Runnable interface { + Start(context.Context) error + Close() error +} + +type TestingT interface { + require.TestingT + Helper() + Cleanup(func()) +} + +// Run fails tb if the service fails to start or close. +func Run(tb TestingT, s Runnable) { + tb.Helper() + require.NoError(tb, s.Start(tests.Context(tb)), "service failed to start") + tb.Cleanup(func() { assert.NoError(tb, s.Close(), "error closing service") }) +} + +// RunHealthy fails tb if the service fails to start, close, is never ready, or is ever unhealthy (based on periodic checks). +// - after starting, readiness will always be checked at least once, before closing +// - if ever ready, then health will be checked at least once, before closing +func RunHealthy(tb TestingT, s services.Service) { + tb.Helper() + Run(tb, s) + + done := make(chan struct{}) + tb.Cleanup(func() { + done <- struct{}{} + <-done + }) + go func() { + defer close(done) + hp := func() (err error) { + for k, v := range s.HealthReport() { + err = errors.Join(err, fmt.Errorf("%s: %w", k, v)) + } + return + } + for s.Ready() != nil { + select { + case <-done: + if assert.NoError(tb, s.Ready(), "service never ready") { + assert.NoError(tb, hp(), "service unhealthy") + } + return + case <-time.After(time.Second): + } + } + for { + select { + case <-done: + assert.NoError(tb, hp(), "service unhealthy") + return + case <-time.After(time.Second): + assert.NoError(tb, hp(), "service unhealthy") + } + } + }() +} diff --git a/pkg/services/servicetest/run_test.go b/pkg/services/servicetest/run_test.go new file mode 100644 index 000000000..0c2c342e8 --- /dev/null +++ b/pkg/services/servicetest/run_test.go @@ -0,0 +1,98 @@ +package servicetest + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +func TestRunHealthy(t *testing.T) { + for _, test := range []struct { + name string + s services.Service + expFail bool + }{ + {name: "healty", s: &fakeService{}}, + {name: "start", s: &fakeService{start: errors.New("test")}, expFail: true}, + {name: "close", s: &fakeService{close: errors.New("test")}, expFail: true}, + {name: "unready", s: &fakeService{ready: errors.New("test")}, expFail: true}, + {name: "unhealthy", s: &fakeService{healthReport: map[string]error{ + "foo.bar": errors.New("baz"), + }}, expFail: true}, + } { + t.Run(test.name, func(t *testing.T) { + _, failed := runFake(func(t TestingT) { + RunHealthy(t, test.s) + }) + assert.Equal(t, test.expFail, failed) + }) + } +} + +func runFake(fn func(t TestingT)) ([]string, bool) { + var t fakeTest + func() { + defer func() { + for i := len(t.cleanup) - 1; i >= 0; i-- { + t.cleanup[i]() + } + if r := recover(); r != nil { + if _, ok := r.(failNow); ok { + return + } + panic(r) + } + }() + fn(&t) + }() + return t.errors, t.failed +} + +type failNow struct{} + +type fakeTest struct { + cleanup []func() + errors []string + failed bool +} + +func (f *fakeTest) Cleanup(fn func()) { + f.cleanup = append(f.cleanup, fn) +} + +func (f *fakeTest) Errorf(format string, args ...interface{}) { + f.errors = append(f.errors, fmt.Sprintf(format, args...)) + f.failed = true +} + +func (f *fakeTest) FailNow() { + if f.failed == true { + return // only panic the first time + } + f.failed = true + panic(failNow{}) +} + +func (f *fakeTest) Helper() {} + +type fakeService struct { + start error + close error + ready error + healthReport map[string]error +} + +func (h *fakeService) Name() string { return "fakeService" } + +func (h *fakeService) Start(ctx context.Context) error { return h.start } + +func (h *fakeService) Close() error { return h.close } + +func (h *fakeService) Ready() error { return h.ready } + +func (h *fakeService) HealthReport() map[string]error { return h.healthReport } diff --git a/pkg/types/chain_reader.go b/pkg/types/chain_reader.go index 03df94274..219ff2c69 100644 --- a/pkg/types/chain_reader.go +++ b/pkg/types/chain_reader.go @@ -2,38 +2,36 @@ package types import ( "context" - "errors" "time" - - "google.golang.org/grpc/status" ) // Errors exposed to product plugins -type errCodecAndChainReader string - -func (e errCodecAndChainReader) Error() string { return string(e) } - const ( - ErrInvalidType = errCodecAndChainReader("invalid type") - ErrFieldNotFound = errCodecAndChainReader("field not found") - ErrInvalidEncoding = errCodecAndChainReader("invalid encoding") - ErrWrongNumberOfElements = errCodecAndChainReader("wrong number of elements in slice") - ErrNotASlice = errCodecAndChainReader("element is not a slice") - ErrUnknown = errCodecAndChainReader("unknown error") + ErrInvalidType = InvalidArgumentError("invalid type") + ErrInvalidConfig = InvalidArgumentError("invalid configuration") + ErrChainReaderConfigMissing = UnimplementedError("ChainReader entry missing from RelayConfig") ) -func UnwrapClientError(err error) error { - if s, ok := status.FromError(err); ok { - return errCodecAndChainReader(s.String()) - } - return err -} - -var ErrInvalidConfig = errors.New("invalid configuration") - type ChainReader interface { - // returnVal should satisfy Marshaller interface + // GetLatestValue gets the latest value.... + // The params argument can be any object which maps a set of generic parameters into chain specific parameters defined in RelayConfig. It must encode as an object via [json.Marshal]. + // Typically, would be either an anonymous map such as `map[string]any{"baz": 42, "test": true}}`, a struct with `json` tags, or something which implements [json.Marshaler]. + // + // returnVal must [json.Unmarshal] as an object, and so should be a map, struct, or implement the [json.Unmarshaler] interface. + // + // Example use: + // type ProductParams struct { + // Arg int `json:"arg"` + // } + // type ProductReturn struct { + // Foo string `json:"foo"` + // Bar *big.Int `json:"bar"` + // } + // func do(ctx context.Context, cr ChainReader) (resp ProductReturn, err error) { + // err = cr.GetLatestValue(ctx, bc, "method", ProductParams{Arg:1}, &resp) + // return + // } GetLatestValue(ctx context.Context, bc BoundContract, method string, params, returnVal any) error } diff --git a/pkg/types/codec.go b/pkg/types/codec.go index aaf7f996a..8835f11cc 100644 --- a/pkg/types/codec.go +++ b/pkg/types/codec.go @@ -4,6 +4,13 @@ import ( "context" ) +const ( + ErrFieldNotFound = InvalidArgumentError("field not found") + ErrInvalidEncoding = InvalidArgumentError("invalid encoding") + ErrWrongNumberOfElements = InvalidArgumentError("wrong number of elements in slice") + ErrNotASlice = InvalidArgumentError("element is not a slice") +) + type Encoder interface { Encode(ctx context.Context, item any, itemType string) ([]byte, error) // GetMaxEncodingSize returns the max size in bytes if n elements are supplied for all top level dynamically sized elements. @@ -31,7 +38,7 @@ type TypeProvider interface { CreateType(itemType string, forEncoding bool) (any, error) } -type CodecTypeProvider interface { +type RemoteCodec interface { Codec TypeProvider } diff --git a/pkg/types/interfacetests/chain_reader_interface_tests.go b/pkg/types/interfacetests/chain_reader_interface_tests.go index d77adfc51..f804f4788 100644 --- a/pkg/types/interfacetests/chain_reader_interface_tests.go +++ b/pkg/types/interfacetests/chain_reader_interface_tests.go @@ -110,6 +110,5 @@ func RunChainReaderInterfaceTests(t *testing.T, tester ChainReaderInterfaceTeste }, }, } - runTests(t, tester, tests) } diff --git a/pkg/reportingplugins/mercury/types.go b/pkg/types/mercury/types.go similarity index 80% rename from pkg/reportingplugins/mercury/types.go rename to pkg/types/mercury/types.go index ed756f485..8f0d05734 100644 --- a/pkg/reportingplugins/mercury/types.go +++ b/pkg/types/mercury/types.go @@ -4,28 +4,26 @@ import ( "context" "math/big" - "github.com/smartcontractkit/libocr/commontypes" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ) -type PAO interface { - // These fields are common to all observations - GetTimestamp() uint32 - GetObserver() commontypes.OracleID - GetBenchmarkPrice() (*big.Int, bool) -} - type ObsResult[T any] struct { Val T Err error } +type OnchainConfig struct { + // applies to all values: price, bid and ask + Min *big.Int + Max *big.Int +} + type OnchainConfigCodec interface { Encode(OnchainConfig) ([]byte, error) Decode([]byte) (OnchainConfig, error) } -type MercuryServerFetcher interface { //nolint:revive +type ServerFetcher interface { // FetchInitialMaxFinalizedBlockNumber should fetch the initial max finalized block number FetchInitialMaxFinalizedBlockNumber(context.Context) (*int64, error) LatestPrice(ctx context.Context, feedID [32]byte) (*big.Int, error) @@ -33,7 +31,7 @@ type MercuryServerFetcher interface { //nolint:revive } type Transmitter interface { - MercuryServerFetcher + ServerFetcher // NOTE: Mercury doesn't actually transmit on-chain, so there is no // "contract" involved with the transmitter. // - Transmit should be implemented and send to Mercury server diff --git a/pkg/reportingplugins/mercury/v1/types.go b/pkg/types/mercury/v1/types.go similarity index 72% rename from pkg/reportingplugins/mercury/v1/types.go rename to pkg/types/mercury/v1/types.go index f2246c945..cb92f9939 100644 --- a/pkg/reportingplugins/mercury/v1/types.go +++ b/pkg/types/mercury/v1/types.go @@ -1,4 +1,4 @@ -package mercury_v1 //nolint:revive +package v1 import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" + "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" ) type Block struct { @@ -24,12 +24,11 @@ func NewBlock(num int64, hash []byte, ts uint64) Block { } } -// b1 is less than b2 if it is: -// smaller block number -// smaller timestamp -// largest hash -// evaluated in that order -func (b Block) less(b2 Block) bool { +// Less returns true if b1 is less than b2 by comparing in order: +// - smaller block number +// - smaller timestamp +// - largest hash +func (b Block) Less(b2 Block) bool { if b.Num == b2.Num && b.Ts == b2.Ts { // tie-break on hash, all else being equal return b.Hash > b2.Hash @@ -49,22 +48,6 @@ func (b Block) HashBytes() []byte { return []byte(b.Hash) } -type PAO interface { - mercury.PAO - - GetBid() (*big.Int, bool) - GetAsk() (*big.Int, bool) - - // DEPRECATED - // TODO: Remove this handling after deployment (https://smartcontract-it.atlassian.net/browse/MERC-2272) - GetCurrentBlockNum() (int64, bool) - GetCurrentBlockHash() ([]byte, bool) - GetCurrentBlockTimestamp() (uint64, bool) - - GetLatestBlocks() []Block - GetMaxFinalizedBlockNumber() (int64, bool) -} - type ReportFields struct { Timestamp uint32 BenchmarkPrice *big.Int @@ -92,3 +75,19 @@ type ReportCodec interface { // CurrentBlockNumFromReport returns the median current block number from a report CurrentBlockNumFromReport(types.Report) (int64, error) } + +type Observation struct { + BenchmarkPrice mercury.ObsResult[*big.Int] + Bid mercury.ObsResult[*big.Int] + Ask mercury.ObsResult[*big.Int] + + CurrentBlockNum mercury.ObsResult[int64] + CurrentBlockHash mercury.ObsResult[[]byte] + CurrentBlockTimestamp mercury.ObsResult[uint64] + + LatestBlocks []Block + + // MaxFinalizedBlockNumber comes from previous report when present and is + // only observed from mercury server when previous report is nil + MaxFinalizedBlockNumber mercury.ObsResult[int64] +} diff --git a/pkg/reportingplugins/mercury/v2/types.go b/pkg/types/mercury/v2/types.go similarity index 78% rename from pkg/reportingplugins/mercury/v2/types.go rename to pkg/types/mercury/v2/types.go index 62ed5b244..504473720 100644 --- a/pkg/reportingplugins/mercury/v2/types.go +++ b/pkg/types/mercury/v2/types.go @@ -1,20 +1,13 @@ -package mercury_v2 //nolint:revive +package v2 import ( "math/big" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" + "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" ) -type PAO interface { - mercury.PAO - GetMaxFinalizedTimestamp() (int64, bool) - GetLinkFee() (*big.Int, bool) - GetNativeFee() (*big.Int, bool) -} - type ReportFields struct { ValidFromTimestamp uint32 Timestamp uint32 @@ -39,3 +32,12 @@ type ReportCodec interface { ObservationTimestampFromReport(ocrtypes.Report) (uint32, error) } + +type Observation struct { + BenchmarkPrice mercury.ObsResult[*big.Int] + + MaxFinalizedTimestamp mercury.ObsResult[int64] + + LinkPrice mercury.ObsResult[*big.Int] + NativePrice mercury.ObsResult[*big.Int] +} diff --git a/pkg/reportingplugins/mercury/v3/types.go b/pkg/types/mercury/v3/types.go similarity index 74% rename from pkg/reportingplugins/mercury/v3/types.go rename to pkg/types/mercury/v3/types.go index e9ac7f264..346eaf10d 100644 --- a/pkg/reportingplugins/mercury/v3/types.go +++ b/pkg/types/mercury/v3/types.go @@ -1,22 +1,13 @@ -package mercury_v3 //nolint:revive +package v3 import ( "math/big" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" + "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" ) -type PAO interface { - mercury.PAO - GetBid() (*big.Int, bool) - GetAsk() (*big.Int, bool) - GetMaxFinalizedTimestamp() (int64, bool) - GetLinkFee() (*big.Int, bool) - GetNativeFee() (*big.Int, bool) -} - type ReportFields struct { ValidFromTimestamp uint32 Timestamp uint32 @@ -43,3 +34,14 @@ type ReportCodec interface { ObservationTimestampFromReport(ocrtypes.Report) (uint32, error) } + +type Observation struct { + BenchmarkPrice mercury.ObsResult[*big.Int] + Bid mercury.ObsResult[*big.Int] + Ask mercury.ObsResult[*big.Int] + + MaxFinalizedTimestamp mercury.ObsResult[int64] + + LinkPrice mercury.ObsResult[*big.Int] + NativePrice mercury.ObsResult[*big.Int] +} diff --git a/pkg/types/provider.go b/pkg/types/provider.go index 64e496a1e..7f4109323 100644 --- a/pkg/types/provider.go +++ b/pkg/types/provider.go @@ -1,6 +1,13 @@ package types -import ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +import ( + "slices" + "strings" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) // The bootstrap jobs only watch config. type ConfigProvider interface { @@ -21,3 +28,57 @@ type PluginProvider interface { ChainReader() ChainReader Codec() Codec } + +// General error types for providers to return--can be used to wrap more specific errors. +// These should work with or without LOOP enabled, to help the client decide how to handle +// an error. The structure of any wrapped errors would normally be automatically flattened +// to a single string, making it difficult for the client to respond to different categories +// of errors in different ways. This lessons the need for doing our own custom parsing of +// error strings. +type InvalidArgumentError string + +func (e InvalidArgumentError) Error() string { + return string(e) +} + +func (e InvalidArgumentError) GRPCStatus() *status.Status { + return status.New(codes.InvalidArgument, e.Error()) +} + +func (e InvalidArgumentError) Is(target error) bool { + if e == target { + return true + } + + return grpcErrorHasTypeAndMessage(target, string(e), codes.InvalidArgument) +} + +type UnimplementedError string + +func (e UnimplementedError) Error() string { + return string(e) +} + +func (e UnimplementedError) GRPCStatus() *status.Status { + return status.New(codes.Unimplemented, e.Error()) +} + +func (e UnimplementedError) Is(target error) bool { + if e == target { + return true + } + + return grpcErrorHasTypeAndMessage(target, string(e), codes.Unimplemented) +} + +func grpcErrorHasTypeAndMessage(target error, msg string, code codes.Code) bool { + s, ok := status.FromError(target) + if !ok || s.Code() != code { + return false + } + + errs := strings.Split(s.Message(), ":") + return slices.ContainsFunc(errs, func(err string) bool { + return strings.Trim(err, " ") == msg + }) +} diff --git a/pkg/types/provider_mercury.go b/pkg/types/provider_mercury.go index 6a36e995a..ba3b62c4f 100644 --- a/pkg/types/provider_mercury.go +++ b/pkg/types/provider_mercury.go @@ -1,10 +1,10 @@ package types import ( - "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury" - v1 "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury/v1" - v2 "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury/v2" - v3 "github.com/smartcontractkit/chainlink-common/pkg/reportingplugins/mercury/v3" + "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" + v1 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v1" + v2 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v2" + v3 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v3" ) // MercuryProvider provides components needed for a mercury OCR2 plugin. @@ -16,6 +16,6 @@ type MercuryProvider interface { ReportCodecV2() v2.ReportCodec ReportCodecV3() v3.ReportCodec OnchainConfigCodec() mercury.OnchainConfigCodec - MercuryServerFetcher() mercury.MercuryServerFetcher + MercuryServerFetcher() mercury.ServerFetcher MercuryChainReader() mercury.ChainReader } diff --git a/pkg/utils/mailbox/mailbox.go b/pkg/utils/mailbox/mailbox.go new file mode 100644 index 000000000..6b84eb524 --- /dev/null +++ b/pkg/utils/mailbox/mailbox.go @@ -0,0 +1,126 @@ +package mailbox + +import ( + "sync" + "sync/atomic" +) + +// Mailbox contains a notify channel, +// a mutual exclusive lock, +// a queue of interfaces, +// and a queue capacity. +type Mailbox[T any] struct { + mu sync.Mutex + chNotify chan struct{} + queue []T + queueLen atomic.Int64 // atomic so monitor can read w/o blocking the queue + + // capacity - number of items the mailbox can buffer + // NOTE: if the capacity is 1, it's possible that an empty Retrieve may occur after a notification. + capacity uint64 + // onCloseFn is a hook used to stop monitoring, if non-nil + onCloseFn func() +} + +// NewHighCapacity create a new mailbox with a capacity +// that is better able to handle e.g. large log replays. +func NewHighCapacity[T any]() *Mailbox[T] { + return New[T](100_000) +} + +// NewSingle returns a new Mailbox with capacity one. +func NewSingle[T any]() *Mailbox[T] { return New[T](1) } + +// New creates a new mailbox instance. If name is non-empty, it must be unique and calling Start will launch +// prometheus metric monitor that periodically reports mailbox load until Close() is called. +func New[T any](capacity uint64) *Mailbox[T] { + queueCap := capacity + if queueCap == 0 { + queueCap = 100 + } + return &Mailbox[T]{ + chNotify: make(chan struct{}, 1), + queue: make([]T, 0, queueCap), + capacity: capacity, + } +} + +// Notify returns the contents of the notify channel +func (m *Mailbox[T]) Notify() <-chan struct{} { + return m.chNotify +} + +func (m *Mailbox[T]) Close() error { + if m.onCloseFn != nil { + m.onCloseFn() + } + return nil +} + +func (m *Mailbox[T]) onClose(fn func()) { m.onCloseFn = fn } + +func (m *Mailbox[T]) load() (capacity uint64, loadPercent float64) { + capacity = m.capacity + loadPercent = 100 * float64(m.queueLen.Load()) / float64(capacity) + return +} + +// Deliver appends to the queue and returns true if the queue was full, causing a message to be dropped. +func (m *Mailbox[T]) Deliver(x T) (wasOverCapacity bool) { + m.mu.Lock() + defer m.mu.Unlock() + + m.queue = append([]T{x}, m.queue...) + if uint64(len(m.queue)) > m.capacity && m.capacity > 0 { + m.queue = m.queue[:len(m.queue)-1] + wasOverCapacity = true + } else { + m.queueLen.Add(1) + } + + select { + case m.chNotify <- struct{}{}: + default: + } + return +} + +// Retrieve fetches one element from the queue. +func (m *Mailbox[T]) Retrieve() (t T, ok bool) { + m.mu.Lock() + defer m.mu.Unlock() + if len(m.queue) == 0 { + return + } + t = m.queue[len(m.queue)-1] + m.queue = m.queue[:len(m.queue)-1] + m.queueLen.Add(-1) + ok = true + return +} + +// RetrieveAll fetches all elements from the queue. +func (m *Mailbox[T]) RetrieveAll() []T { + m.mu.Lock() + defer m.mu.Unlock() + queue := m.queue + m.queue = nil + m.queueLen.Store(0) + for i, j := 0, len(queue)-1; i < j; i, j = i+1, j-1 { + queue[i], queue[j] = queue[j], queue[i] + } + return queue +} + +// RetrieveLatestAndClear fetch the latest value (or nil), and clears the rest of the queue (if any). +func (m *Mailbox[T]) RetrieveLatestAndClear() (t T) { + m.mu.Lock() + defer m.mu.Unlock() + if len(m.queue) == 0 { + return + } + t = m.queue[0] + m.queue = nil + m.queueLen.Store(0) + return +} diff --git a/pkg/utils/mailbox/mailbox_prom.go b/pkg/utils/mailbox/mailbox_prom.go new file mode 100644 index 000000000..a502d6f53 --- /dev/null +++ b/pkg/utils/mailbox/mailbox_prom.go @@ -0,0 +1,94 @@ +package mailbox + +import ( + "context" + "strconv" + "strings" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/utils" +) + +var mailboxLoad = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "mailbox_load_percent", + Help: "Percent of mailbox capacity used", +}, + []string{"appID", "name", "capacity"}, +) + +const mailboxPromInterval = 5 * time.Second + +type Monitor struct { + services.StateMachine + appID string + + mailboxes sync.Map + stop func() + done chan struct{} +} + +func NewMonitor(appID string) *Monitor { + return &Monitor{appID: appID} +} + +func (m *Monitor) Name() string { return "Monitor" } + +func (m *Monitor) Start(context.Context) error { + return m.StartOnce("Monitor", func() error { + t := time.NewTicker(utils.WithJitter(mailboxPromInterval)) + ctx, cancel := context.WithCancel(context.Background()) + m.stop = func() { + t.Stop() + cancel() + } + m.done = make(chan struct{}) + go m.monitorLoop(ctx, t.C) + return nil + }) +} + +func (m *Monitor) Close() error { + return m.StopOnce("Monitor", func() error { + m.stop() + <-m.done + return nil + }) +} + +func (m *Monitor) HealthReport() map[string]error { + return map[string]error{m.Name(): m.Healthy()} +} + +func (m *Monitor) monitorLoop(ctx context.Context, c <-chan time.Time) { + defer close(m.done) + for { + select { + case <-ctx.Done(): + return + case <-c: + m.mailboxes.Range(func(k, v any) bool { + name, mb := k.(string), v.(mailbox) + c, p := mb.load() + capacity := strconv.FormatUint(c, 10) + mailboxLoad.WithLabelValues(m.appID, name, capacity).Set(p) + return true + }) + } + } +} + +type mailbox interface { + load() (capacity uint64, percent float64) + onClose(func()) +} + +func (m *Monitor) Monitor(mb mailbox, name ...string) { + n := strings.Join(name, ".") + m.mailboxes.Store(n, mb) + mb.onClose(func() { m.mailboxes.Delete(n) }) +} diff --git a/pkg/utils/mailbox/mailbox_test.go b/pkg/utils/mailbox/mailbox_test.go new file mode 100644 index 000000000..8c63d96dd --- /dev/null +++ b/pkg/utils/mailbox/mailbox_test.go @@ -0,0 +1,181 @@ +package mailbox + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMailbox(t *testing.T) { + var ( + expected = []int{2, 3, 4, 5, 6, 7, 8, 9, 10, 11} + toDeliver = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} + ) + + const capacity = 10 + m := New[int](capacity) + + // Queue deliveries + for i, d := range toDeliver { + atCapacity := m.Deliver(d) + if atCapacity && i < capacity { + t.Errorf("mailbox at capacity %d", i) + } else if !atCapacity && i >= capacity { + t.Errorf("mailbox below capacity %d", i) + } + } + + // Retrieve them + var recvd []int + chDone := make(chan struct{}) + go func() { + defer close(chDone) + for range m.Notify() { + for { + x, exists := m.Retrieve() + if !exists { + break + } + recvd = append(recvd, x) + } + } + }() + + close(m.chNotify) + <-chDone + + require.Equal(t, expected, recvd) +} + +func TestMailbox_RetrieveAll(t *testing.T) { + var ( + expected = []int{2, 3, 4, 5, 6, 7, 8, 9, 10, 11} + toDeliver = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} + ) + + const capacity = 10 + m := New[int](capacity) + + // Queue deliveries + for i, d := range toDeliver { + atCapacity := m.Deliver(d) + if atCapacity && i < capacity { + t.Errorf("mailbox at capacity %d", i) + } else if !atCapacity && i >= capacity { + t.Errorf("mailbox below capacity %d", i) + } + } + + require.Equal(t, expected, m.RetrieveAll()) +} + +func TestMailbox_RetrieveLatestAndClear(t *testing.T) { + var ( + expected = 11 + toDeliver = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} + ) + + const capacity = 10 + m := New[int](capacity) + + // Queue deliveries + for i, d := range toDeliver { + atCapacity := m.Deliver(d) + if atCapacity && i < capacity { + t.Errorf("mailbox at capacity %d", i) + } else if !atCapacity && i >= capacity { + t.Errorf("mailbox below capacity %d", i) + } + } + + require.Equal(t, expected, m.RetrieveLatestAndClear()) + require.Len(t, m.RetrieveAll(), 0) +} + +func TestMailbox_NoEmptyReceivesWhenCapacityIsTwo(t *testing.T) { + m := New[int](2) + + var ( + recvd []int + emptyReceives []int + ) + + chDone := make(chan struct{}) + go func() { + defer close(chDone) + for range m.Notify() { + x, exists := m.Retrieve() + if !exists { + emptyReceives = append(emptyReceives, recvd[len(recvd)-1]) + } else { + recvd = append(recvd, x) + } + } + }() + + for i := 0; i < 100000; i++ { + m.Deliver(i) + } + close(m.chNotify) + + <-chDone + require.Len(t, emptyReceives, 0) +} + +func TestMailbox_load(t *testing.T) { + for _, tt := range []struct { + name string + capacity uint64 + deliver []int + exp float64 + + retrieve int + exp2 float64 + + all bool + }{ + {"single-all", 1, []int{1}, 100, 0, 100, true}, + {"single-latest", 1, []int{1}, 100, 0, 100, false}, + {"ten-low", 10, []int{1}, 10, 1, 0.0, false}, + {"ten-full-all", 10, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 100, 5, 50, true}, + {"ten-full-latest", 10, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 100, 5, 50, false}, + {"ten-overflow", 10, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 100, 5, 50, false}, + {"nine", 9, []int{1, 2, 3}, 100.0 / 3.0, 2, 100.0 / 9.0, true}, + } { + t.Run(tt.name, func(t *testing.T) { + m := New[int](tt.capacity) + + // Queue deliveries + for i, d := range tt.deliver { + atCapacity := m.Deliver(d) + if atCapacity && i < int(tt.capacity) { + t.Errorf("mailbox at capacity %d", i) + } else if !atCapacity && i >= int(tt.capacity) { + t.Errorf("mailbox below capacity %d", i) + } + } + gotCap, gotLoad := m.load() + require.Equal(t, gotCap, tt.capacity) + require.Equal(t, gotLoad, tt.exp) + + // Retrieve some + for i := 0; i < tt.retrieve; i++ { + _, ok := m.Retrieve() + require.True(t, ok) + } + gotCap, gotLoad = m.load() + require.Equal(t, gotCap, tt.capacity) + require.Equal(t, gotLoad, tt.exp2) + + // Drain it + if tt.all { + m.RetrieveAll() + } else { + m.RetrieveLatestAndClear() + } + gotCap, gotLoad = m.load() + require.Equal(t, gotCap, tt.capacity) + require.Equal(t, gotLoad, 0.0) + }) + } +} diff --git a/pkg/utils/mathutil/mathutil.go b/pkg/utils/mathutil/mathutil.go new file mode 100644 index 000000000..e9659a1f1 --- /dev/null +++ b/pkg/utils/mathutil/mathutil.go @@ -0,0 +1,23 @@ +package mathutil + +import "golang.org/x/exp/constraints" + +func Max[V constraints.Ordered](first V, vals ...V) V { + max := first + for _, v := range vals { + if v > max { + max = v + } + } + return max +} + +func Min[V constraints.Ordered](first V, vals ...V) V { + min := first + for _, v := range vals { + if v < min { + min = v + } + } + return min +} diff --git a/pkg/utils/mathutil/mathutil_test.go b/pkg/utils/mathutil/mathutil_test.go new file mode 100644 index 000000000..24a464bb3 --- /dev/null +++ b/pkg/utils/mathutil/mathutil_test.go @@ -0,0 +1,33 @@ +package mathutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMax(t *testing.T) { + // Happy path + assert.Equal(t, 3, Max(3, 2, 1)) + // Single element + assert.Equal(t, 3, Max(3)) + // Signed + assert.Equal(t, -1, Max(-2, -1)) + // Uint64 + assert.Equal(t, uint64(2), Max(uint64(0), uint64(2))) + // String + assert.Equal(t, "c", Max("a", []string{"b", "c"}...)) +} + +func TestMin(t *testing.T) { + // Happy path + assert.Equal(t, 1, Min(3, 2, 1)) + // Single element + assert.Equal(t, 3, Min(3)) + // Signed + assert.Equal(t, -2, Min(-2, -1)) + // Uint64 + assert.Equal(t, uint64(0), Min(uint64(0), uint64(2))) + // String + assert.Equal(t, "a", Min("a", []string{"b", "c"}...)) +} diff --git a/pkg/utils/null/uint32.go b/pkg/utils/null/uint32.go new file mode 100644 index 000000000..fa0123fb6 --- /dev/null +++ b/pkg/utils/null/uint32.go @@ -0,0 +1,149 @@ +package null + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// Uint32 encapsulates the value and validity (not null) of a uint32 value, +// to differentiate nil from 0 in json and sql. +type Uint32 struct { + Uint32 uint32 + Valid bool +} + +// NewUint32 returns an instance of Uint32 with the passed parameters. +func NewUint32(i uint32, valid bool) Uint32 { + return Uint32{ + Uint32: i, + Valid: valid, + } +} + +// Uint32From creates a new Uint32 that will always be valid. +func Uint32From(i uint32) Uint32 { + return NewUint32(i, true) +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Int. +func (i *Uint32) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + if err = json.Unmarshal(data, &v); err != nil { + return err + } + switch x := v.(type) { + case float64: + // Unmarshal again, directly to value, to avoid intermediate float64 + err = json.Unmarshal(data, &i.Uint32) + case string: + if len(x) == 0 { + i.Valid = false + return nil + } + i.Uint32, err = parse(x) + case nil: + i.Valid = false + return nil + default: + err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Uint32", reflect.TypeOf(v).Name()) + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Uint32 if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Uint32) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + i.Uint32, err = parse(string(text)) + i.Valid = err == nil + return err +} + +func parse(str string) (uint32, error) { + v, err := strconv.ParseUint(str, 10, 32) + return uint32(v), err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Uint32 is null. +func (i Uint32) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatUint(uint64(i.Uint32), 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Uint32 is null. +func (i Uint32) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatUint(uint64(i.Uint32), 10)), nil +} + +// SetValid changes this Uint32's value and also sets it to be non-null. +func (i *Uint32) SetValid(n uint32) { + i.Uint32 = n + i.Valid = true +} + +// Value returns this instance serialized for database storage. +func (i Uint32) Value() (driver.Value, error) { + if !i.Valid { + return nil, nil + } + + // golang's sql driver types as determined by IsValue only supports: + // []byte, bool, float64, int64, string, time.Time + // https://golang.org/src/database/sql/driver/types.go + return int64(i.Uint32), nil +} + +// Scan reads the database value and returns an instance. +func (i *Uint32) Scan(value interface{}) error { + if value == nil { + *i = Uint32{} + return nil + } + + switch typed := value.(type) { + case int: + safe := uint32(typed) + if int(safe) != typed { + return fmt.Errorf("unable to convert %v of %T to Uint32; overflow", value, value) + } + *i = Uint32From(safe) + case int64: + safe := uint32(typed) + if int64(safe) != typed { + return fmt.Errorf("unable to convert %v of %T to Uint32; overflow", value, value) + } + *i = Uint32From(safe) + case uint: + safe := uint32(typed) + if uint(safe) != typed { + return fmt.Errorf("unable to convert %v of %T to Uint32; overflow", value, value) + } + *i = Uint32From(safe) + case uint32: + safe := typed + *i = Uint32From(safe) + default: + return fmt.Errorf("unable to convert %v of %T to Uint32", value, value) + } + return nil +} diff --git a/pkg/utils/null/uint32_test.go b/pkg/utils/null/uint32_test.go new file mode 100644 index 000000000..128d2f60f --- /dev/null +++ b/pkg/utils/null/uint32_test.go @@ -0,0 +1,200 @@ +package null_test + +import ( + "encoding/json" + "fmt" + "math" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/utils/null" +) + +func TestUint32From(t *testing.T) { + tests := []struct { + input uint32 + }{ + {12345}, + {0}, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%d", test.input), func(t *testing.T) { + i := null.Uint32From(test.input) + assert.True(t, i.Valid) + assert.Equal(t, test.input, i.Uint32) + }) + } +} + +func TestUnmarshalUint32_Valid(t *testing.T) { + tests := []struct { + name, input string + }{ + {"int json", `12345`}, + {"int string json", `"12345"`}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var i null.Uint32 + err := json.Unmarshal([]byte(test.input), &i) + require.NoError(t, err) + assert.True(t, i.Valid) + assert.Equal(t, uint32(12345), i.Uint32) + }) + } +} + +func TestUnmarshalUint32_Invalid(t *testing.T) { + tests := []struct { + name, input string + }{ + {"blank json string", `""`}, + {"null json", `null`}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var i null.Uint32 + err := json.Unmarshal([]byte(test.input), &i) + require.NoError(t, err) + assert.False(t, i.Valid) + }) + } +} + +func TestUnmarshalUint32_Error(t *testing.T) { + tests := []struct { + name, input string + }{ + {"wrong type json", `true`}, + {"invalid json", `:)`}, + {"float", `1.2345`}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var i null.Uint32 + err := json.Unmarshal([]byte(test.input), &i) + require.Error(t, err) + assert.False(t, i.Valid) + }) + } +} + +func TestUnmarshalUint32Overflow(t *testing.T) { + maxUint32 := uint64(math.MaxUint32) + + // Max uint32 should decode successfully + var i null.Uint32 + err := json.Unmarshal([]byte(strconv.FormatUint(maxUint32, 10)), &i) + require.NoError(t, err) + + // Attempt to overflow + err = json.Unmarshal([]byte(strconv.FormatUint(maxUint32+1, 10)), &i) + require.Error(t, err) +} + +func TestTextUnmarshalInt_Valid(t *testing.T) { + var i null.Uint32 + err := i.UnmarshalText([]byte("12345")) + require.NoError(t, err) + assert.True(t, i.Valid) + assert.Equal(t, uint32(12345), i.Uint32) +} + +func TestTextUnmarshalInt_Invalid(t *testing.T) { + tests := []struct { + name, input string + }{ + {"empty", ""}, + {"null", "null"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var i null.Uint32 + err := i.UnmarshalText([]byte(test.input)) + require.NoError(t, err) + assert.False(t, i.Valid) + }) + } +} + +func TestMarshalInt(t *testing.T) { + i := null.Uint32From(12345) + data, err := json.Marshal(i) + require.NoError(t, err) + assertJSONEquals(t, data, "12345", "non-empty json marshal") + + // invalid values should be encoded as null + null := null.NewUint32(0, false) + data, err = json.Marshal(null) + require.NoError(t, err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalIntText(t *testing.T) { + i := null.Uint32From(12345) + data, err := i.MarshalText() + require.NoError(t, err) + assertJSONEquals(t, data, "12345", "non-empty text marshal") + + // invalid values should be encoded as null + null := null.NewUint32(0, false) + data, err = null.MarshalText() + require.NoError(t, err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestUint32SetValid(t *testing.T) { + change := null.NewUint32(0, false) + change.SetValid(12345) + assert.True(t, change.Valid) + assert.Equal(t, uint32(12345), change.Uint32) +} + +func TestUint32Scan(t *testing.T) { + var i null.Uint32 + err := i.Scan(12345) + require.NoError(t, err) + assert.True(t, i.Valid) + assert.Equal(t, uint32(12345), i.Uint32) + + err = i.Scan(int64(12345)) + require.NoError(t, err) + assert.True(t, i.Valid) + assert.Equal(t, uint32(12345), i.Uint32) + + // int64 overflows uint32 + err = i.Scan(int64(math.MaxInt64)) + require.Error(t, err) + + err = i.Scan(uint(12345)) + require.NoError(t, err) + assert.True(t, i.Valid) + assert.Equal(t, uint32(12345), i.Uint32) + + // uint overflows uint32 + err = i.Scan(uint(math.MaxUint64)) + require.Error(t, err) + + err = i.Scan(uint32(12345)) + require.NoError(t, err) + assert.True(t, i.Valid) + assert.Equal(t, uint32(12345), i.Uint32) + + err = i.Scan(nil) + require.NoError(t, err) + assert.False(t, i.Valid) +} + +func assertJSONEquals(t *testing.T, data []byte, cmp string, from string) { + if string(data) != cmp { + t.Errorf("bad %s data: %s ≠ %s\n", from, data, cmp) + } +} diff --git a/pkg/utils/tests/tests.go b/pkg/utils/tests/tests.go index 510a2d7a3..68aefbc1c 100644 --- a/pkg/utils/tests/tests.go +++ b/pkg/utils/tests/tests.go @@ -6,20 +6,34 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func Context(t *testing.T) context.Context { - ctx := context.Background() +type TestingT interface { + require.TestingT + Helper() + Cleanup(func()) +} + +func Context(tb TestingT) (ctx context.Context) { + ctx = context.Background() var cancel func() - if d, ok := t.Deadline(); ok { - ctx, cancel = context.WithDeadline(ctx, d) - } else { - ctx, cancel = context.WithCancel(ctx) + t, isTest := tb.(interface { + Deadline() (deadline time.Time, ok bool) + }) + if isTest { + d, hasDeadline := t.Deadline() + if hasDeadline { + ctx, cancel = context.WithDeadline(ctx, d) + tb.Cleanup(cancel) + return + } } - t.Cleanup(cancel) - return ctx + ctx, cancel = context.WithCancel(ctx) + tb.Cleanup(cancel) + return } // DefaultWaitTimeout is the default wait timeout. If you have a *testing.T, use WaitTimeout instead. diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 6e588380e..9adff93e7 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -4,6 +4,7 @@ import ( "context" "math" mrand "math/rand" + "strings" "time" "github.com/smartcontractkit/chainlink-common/pkg/services" @@ -44,3 +45,16 @@ func ContextWithDeadlineFn(ctx context.Context, deadlineFn func(orig time.Time) } return ctx, cancel } + +func IsZero[C comparable](val C) bool { + var zero C + return zero == val +} + +// EnsureHexPrefix adds the prefix (0x) to a given hex string. +func EnsureHexPrefix(str string) string { + if !strings.HasPrefix(str, "0x") { + str = "0x" + str + } + return str +}