diff --git a/go.mod b/go.mod index 6ccebf82d..4568d0e02 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,14 @@ require ( github.com/gagliardetto/solana-go v1.4.1-0.20220428092759-5250b4abbb27 github.com/gagliardetto/treeout v0.1.4 github.com/google/uuid v1.3.0 + github.com/onsi/gomega v1.24.1 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.14.0 - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230525161650-dce1bc58b504 - github.com/smartcontractkit/libocr v0.0.0-20230525150148-a75f6e244bb3 - github.com/stretchr/testify v1.8.2 - go.uber.org/multierr v1.8.0 + github.com/prometheus/client_golang v1.15.0 + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230623025050-a286a91d29e6 + github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1 + github.com/stretchr/testify v1.8.4 + github.com/test-go/testify v1.1.4 + go.uber.org/multierr v1.11.0 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 golang.org/x/sync v0.1.0 @@ -26,7 +28,7 @@ require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect - github.com/benbjohnson/clock v1.1.0 // indirect + github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blendle/zapdriver v1.3.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -34,8 +36,8 @@ require ( github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect github.com/fatih/color v1.13.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -45,17 +47,16 @@ require ( github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect - github.com/onsi/gomega v1.24.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/riferrei/srclient v0.5.4 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.1.1 // indirect github.com/stretchr/objx v0.5.0 // indirect @@ -64,11 +65,13 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect go.opencensus.io v0.23.0 // indirect - go.uber.org/atomic v1.9.0 // indirect + go.uber.org/atomic v1.10.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect - golang.org/x/crypto v0.5.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.4.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bf3dadc9a..4970389bb 100644 --- a/go.sum +++ b/go.sum @@ -9,29 +9,19 @@ cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -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= contrib.go.opencensus.io/exporter/stackdriver v0.12.6/go.mod h1:8x999/OcIPy5ivx/wDiV7Gx4D+VUPODf0mWRGRc5kSk= contrib.go.opencensus.io/exporter/stackdriver v0.13.4 h1:ksUxwH3OD5sxkjzEqGxNTl+Xjsmu3BnC/300MhSVTSc= contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= @@ -50,10 +40,7 @@ github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0R github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -64,8 +51,8 @@ github.com/aws/aws-sdk-go v1.22.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -78,7 +65,6 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/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= @@ -139,13 +125,9 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 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= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -160,14 +142,11 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb 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= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -178,12 +157,14 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD 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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 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 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 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= @@ -191,25 +172,20 @@ github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzr 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= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/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.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/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= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 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= @@ -264,7 +240,6 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -281,13 +256,12 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -317,8 +291,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -342,13 +316,14 @@ github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nrwiersma/avro-benchmarks v0.0.0-20210913175520-21aec48c8f76/go.mod h1:iKyFMidsk/sVYONJRE372sJuX/QTRPacU7imPqqsu7g= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= +github.com/onsi/ginkgo/v2 v2.5.0 h1:TRtrvv2vdQqzkwrQ1ke6vtXf7IK34RBUJafIy1wMwls= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= @@ -360,39 +335,27 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/riferrei/srclient v0.5.4 h1:dfwyR5u23QF7beuVl2WemUY2KXh5+Sc4DHKyPXBNYuc= github.com/riferrei/srclient v0.5.4/go.mod h1:vbkLmWcgYa7JgfPvuy/+K8fTS0p1bApqadxrxi/S1MI= @@ -401,8 +364,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So 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 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= @@ -413,12 +376,10 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230525161650-dce1bc58b504 h1:LQB12lOqT0Kg6s9zq/zDF5egSPXi7Hyar3QBmjkD32w= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230525161650-dce1bc58b504/go.mod h1:zfUba6Okm7zTBxap24I78Vq9z+twHmjXSMBAl2C2Qgc= -github.com/smartcontractkit/libocr v0.0.0-20230525150148-a75f6e244bb3 h1:/Gel/U5eIZ/BGGr25OrHaXiVDTAJ5DYX5+UlXp3q7Gg= -github.com/smartcontractkit/libocr v0.0.0-20230525150148-a75f6e244bb3/go.mod h1:5JnCHuYgmIP9ZyXzgAfI5Iwu0WxBtBKp+ApeT5o1Cjw= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230623025050-a286a91d29e6 h1:Prz5n1XFdP4RouzTyy+bBL3C9B+HY6QTuyBTXCl4GMU= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230623025050-a286a91d29e6/go.mod h1:MfZBUifutkv3aK7abyw5YmTJbqt8iFwcQDFikrxC/uI= +github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1 h1:caG9BWjnCxN/HPBA5ltDGadDraZAsjGIct4S8lh8D5c= +github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1/go.mod h1:2lyRkw/qLQgUWlrWWmq5nj0y90rWeO6Y+v+fCakRgb0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -446,8 +407,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 h1:3SNcvBmEPE1YlB1JpVZouslJpI3GBNoiqW7+wb0Rz7w= @@ -467,7 +428,6 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -483,15 +443,15 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= @@ -508,8 +468,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -541,7 +501,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -556,9 +515,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -566,33 +523,23 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -600,9 +547,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -617,7 +562,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -630,45 +574,30 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -676,8 +605,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -717,19 +646,9 @@ golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -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-20200601175630-2caf76543d99/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -744,20 +663,14 @@ google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -776,19 +689,9 @@ google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 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= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/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-20220503193339-ba3ae3f07e29/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -801,9 +704,6 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -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.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= @@ -817,14 +717,13 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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= @@ -847,7 +746,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/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.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -862,7 +760,6 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pkg/internal/cltest/cltest.go b/pkg/internal/cltest/cltest.go new file mode 100644 index 000000000..47ad82dcd --- /dev/null +++ b/pkg/internal/cltest/cltest.go @@ -0,0 +1,258 @@ +package cltest + +import ( + "context" + "fmt" + "math/big" + "sync/atomic" + "testing" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + + clientmocks "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types/mocks" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +// TODO: write tests for this package + +// Chain Specific Test utils + +// Head returns a new Head with the given block height +func Head(val interface{}) *types.Head { + time := solana.UnixTimeSeconds(0) + chainId := types.Mainnet + blockHeight := getBlockHeight(val) + parentSlot := getParentSlot(blockHeight) + block := getBlock(blockHeight, parentSlot, time) + h := createHead(val, blockHeight, block, chainId) + return h +} + +func getBlockHeight(val interface{}) uint64 { + switch t := val.(type) { + case int: + return uint64(t) + case uint64: + return t + case int64: + return uint64(t) + case *big.Int: + return t.Uint64() + default: + panic(fmt.Sprintf("Could not convert %v of type %T to Head", val, val)) + } +} + +func getParentSlot(blockHeight uint64) uint64 { + if blockHeight > 1 { + return blockHeight - 1 + } + return 0 +} + +func getBlock(blockHeight, parentSlot uint64, time solana.UnixTimeSeconds) rpc.GetBlockResult { + return rpc.GetBlockResult{ + Blockhash: utils.NewSolanaHash(), + PreviousBlockhash: utils.NewSolanaHash(), + ParentSlot: parentSlot, + Transactions: nil, + Rewards: nil, + BlockTime: &time, + BlockHeight: &blockHeight, + } +} + +func createHead(val interface{}, blockHeight uint64, block rpc.GetBlockResult, chainId types.ChainID) *types.Head { + switch t := val.(type) { + case int, uint64: + return types.NewHead(int64(blockHeight), block, nil, chainId) + case int64: + return types.NewHead(t, block, nil, chainId) + case *big.Int: + return types.NewHead(t.Int64(), block, nil, chainId) + default: + panic(fmt.Sprintf("Could not convert %v of type %T to Head", val, val)) + } +} + +// Blocks - a helper logic to construct a range of linked heads +// and an ability to fork and create logs from them +type Blocks struct { + t *testing.T + Hashes []types.Hash + mHashes map[int64]types.Hash + Heads map[int64]*types.Head +} + +func (b *Blocks) Head(number uint64) *types.Head { + return b.Heads[int64(number)] +} + +func NewBlocks(t *testing.T, numHashes int) *Blocks { + hashes := make([]types.Hash, 0) + heads := make(map[int64]*types.Head) + for i := int64(0); i < int64(numHashes); i++ { + hash := utils.NewHash() + hashes = append(hashes, hash) + + heads[i] = Head(i) + if i > 0 { + parent := heads[i-1] + heads[i].Parent = parent + heads[i].Block.PreviousBlockhash = parent.BlockHash().Hash + } + } + + hashesMap := make(map[int64]types.Hash) + for i := 0; i < len(hashes); i++ { + hashesMap[int64(i)] = hashes[i] + } + + return &Blocks{ + t: t, + Hashes: hashes, + mHashes: hashesMap, + Heads: heads, + } +} + +func (b *Blocks) NewHead(number uint64) *types.Head { + parentNumber := number - 1 + parent, ok := b.Heads[int64(parentNumber)] + if !ok { + b.t.Fatalf("Can't find parent block at index: %v", parentNumber) + } + + head := Head(number) + head.Parent = parent + head.Block.PreviousBlockhash = parent.BlockHash().Hash + + return head +} + +func (b *Blocks) ForkAt(t *testing.T, blockNum int64, numHashes int) *Blocks { + forked := NewBlocks(t, len(b.Heads)+numHashes) + if _, exists := forked.Heads[blockNum]; !exists { + t.Fatalf("Not enough length for block num: %v", blockNum) + } + + for i := int64(0); i < blockNum; i++ { + forked.Heads[i] = b.Heads[i] + } + + forked.Heads[blockNum].Block.PreviousBlockhash = b.Heads[blockNum].Block.PreviousBlockhash + forked.Heads[blockNum].Parent = b.Heads[blockNum].Parent + return forked +} + +// HeadBuffer - stores heads in sequence, with increasing timestamps +type HeadBuffer struct { + t *testing.T + Heads []*types.Head +} + +func NewHeadBuffer(t *testing.T) *HeadBuffer { + return &HeadBuffer{ + t: t, + Heads: make([]*types.Head, 0), + } +} + +func (hb *HeadBuffer) Append(head *types.Head) { + // Create a copy of the head, so that we can modify it + cloned := &types.Head{ + Slot: head.Slot, + Block: head.Block, + Parent: head.Parent, + ID: head.ID, + } + hb.Heads = append(hb.Heads, cloned) +} + +// MockHeadTrackable allows you to mock HeadTrackable +type MockHeadTrackable struct { + onNewHeadCount atomic.Int32 +} + +// OnNewLongestChain increases the OnNewLongestChainCount count by one +func (m *MockHeadTrackable) OnNewLongestChain(context.Context, *types.Head) { + m.onNewHeadCount.Add(1) +} + +// OnNewLongestChainCount returns the count of new heads, safely. +func (m *MockHeadTrackable) OnNewLongestChainCount() int32 { + return m.onNewHeadCount.Load() +} + +type Awaiter chan struct{} + +func NewAwaiter() Awaiter { return make(Awaiter) } + +func (a Awaiter) ItHappened() { close(a) } + +func (a Awaiter) AssertHappened(t *testing.T, expected bool) { + t.Helper() + select { + case <-a: + if !expected { + t.Fatal("It happened") + } + default: + if expected { + t.Fatal("It didn't happen") + } + } +} + +func (a Awaiter) AwaitOrFail(t testing.TB, durationParams ...time.Duration) { + t.Helper() + + duration := 10 * time.Second + if len(durationParams) > 0 { + duration = durationParams[0] + } + + select { + case <-a: + case <-time.After(duration): + t.Fatal("Timed out waiting for Awaiter to get ItHappened") + } +} + +func NewClientMock(t *testing.T) *clientmocks.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash] { + return clientmocks.NewClient[*types.Head, commontypes.Subscription, + types.ChainID, types.Hash](t) +} + +func NewClientMockWithDefaultChain(t *testing.T) *clientmocks.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash] { + c := NewClientMock(t) + c.On("ConfiguredChainID").Return(types.Mainnet).Maybe() + c.On("IsL2").Return(false).Maybe() + return c +} + +func ConfigureBlockResult() rpc.GetBlockResult { + result := rpc.GetBlockResult{ + Blockhash: utils.NewSolanaHash(), + PreviousBlockhash: utils.NewSolanaHash(), + ParentSlot: 0, + Transactions: []rpc.TransactionWithMeta{}, + Signatures: []solana.Signature{}, + Rewards: []rpc.BlockReward{}, + BlockTime: nil, + BlockHeight: nil, + } + return result +} diff --git a/pkg/internal/testutils/testutils.go b/pkg/internal/testutils/testutils.go new file mode 100644 index 000000000..0025c51a4 --- /dev/null +++ b/pkg/internal/testutils/testutils.go @@ -0,0 +1,131 @@ +package testutils + +import ( + "context" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/mock" + + clientmocks "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types/mocks" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + commonmocks "github.com/smartcontractkit/chainlink-relay/pkg/types/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +// TODO: These can refactor to chainlink internal testutils + +// Chain Agnostic Test Utils + +// Context returns a context with the test's deadline, if available. +func Context(tb testing.TB) context.Context { + ctx := context.Background() + var cancel func() + switch t := tb.(type) { + case *testing.T: + if d, ok := t.Deadline(); ok { + ctx, cancel = context.WithDeadline(ctx, d) + } + } + if cancel == nil { + ctx, cancel = context.WithCancel(ctx) + } + tb.Cleanup(cancel) + return ctx +} + +// DefaultWaitTimeout is the default wait timeout. If you have a *testing.T, use WaitTimeout instead. +const DefaultWaitTimeout = 30 * time.Second + +// WaitTimeout returns a timeout based on the test's Deadline, if available. +// Especially important to use in parallel tests, as their individual execution +// can get paused for arbitrary amounts of time. +func WaitTimeout(t *testing.T) time.Duration { + if d, ok := t.Deadline(); ok { + // 10% buffer for cleanup and scheduling delay + return time.Until(d) * 9 / 10 + } + return DefaultWaitTimeout +} + +// TestInterval is just a sensible poll interval that gives fast tests without +// risk of spamming +const TestInterval = 100 * time.Millisecond + +// NewHeadtrackerConfig returns a new Solana Headtracker Config with overrides. +func NewHeadtrackerConfig(config *headtracker.Config, overrideFn func(*headtracker.Config)) *headtracker.Config { + overrideFn(config) + return config +} + +type MockChain struct { + Client *clientmocks.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash] + + CheckFilterLogs func(int64, int64) + subsMu sync.RWMutex + subs []*commonmocks.Subscription + errChs []chan error + subscribeCalls atomic.Int32 + unsubscribeCalls atomic.Int32 +} + +func (m *MockChain) SubscribeCallCount() int32 { + return m.subscribeCalls.Load() +} + +func (m *MockChain) UnsubscribeCallCount() int32 { + return m.unsubscribeCalls.Load() +} + +func (m *MockChain) NewSub(t *testing.T) commontypes.Subscription { + m.subscribeCalls.Add(1) + sub := commonmocks.NewSubscription(t) + errCh := make(chan error) + sub.On("Err"). + Return(func() <-chan error { return errCh }).Maybe() + sub.On("Unsubscribe"). + Run(func(mock.Arguments) { + m.unsubscribeCalls.Add(1) + close(errCh) + }).Return().Maybe() + m.subsMu.Lock() + m.subs = append(m.subs, sub) + m.errChs = append(m.errChs, errCh) + m.subsMu.Unlock() + return sub +} + +func (m *MockChain) SubsErr(err error) { + m.subsMu.Lock() + defer m.subsMu.Unlock() + for _, errCh := range m.errChs { + errCh <- err + } +} + +type RawSub[T any] struct { + ch chan<- T + err <-chan error +} + +func NewRawSub[T any](ch chan<- T, err <-chan error) RawSub[T] { + return RawSub[T]{ch: ch, err: err} +} + +func (r *RawSub[T]) CloseCh() { + close(r.ch) +} + +func (r *RawSub[T]) TrySend(t T) { + select { + case <-r.err: + case r.ch <- t: + } +} diff --git a/pkg/internal/utils/hash_helper.go b/pkg/internal/utils/hash_helper.go new file mode 100644 index 000000000..efb1f40d0 --- /dev/null +++ b/pkg/internal/utils/hash_helper.go @@ -0,0 +1,24 @@ +package utils + +import ( + "crypto/rand" + + "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +// NewSolanaHash returns a random solana.Hash using SHA-256. +func NewSolanaHash() solana.Hash { + b := make([]byte, 32) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return solana.HashFromBytes(b) +} + +func NewHash() types.Hash { + return types.Hash{ + Hash: NewSolanaHash(), + } +} diff --git a/pkg/internal/utils/hash_helper_test.go b/pkg/internal/utils/hash_helper_test.go new file mode 100644 index 000000000..9f3ebb743 --- /dev/null +++ b/pkg/internal/utils/hash_helper_test.go @@ -0,0 +1,23 @@ +package utils_test + +import ( + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/utils" + "github.com/stretchr/testify/assert" +) + +func TestNewSolanaHash(t *testing.T) { + t.Parallel() + + h1 := utils.NewSolanaHash() + h2 := utils.NewSolanaHash() + // Check that the two hashes are not the same. + assert.NotEqual(t, h1, h2) + + // Check that neither hash is equal to a zero hash. + zeroHash := solana.Hash{} + assert.NotEqual(t, h1, zeroHash) + assert.NotEqual(t, h2, zeroHash) +} diff --git a/pkg/monitoring/mocks/ChainReader.go b/pkg/monitoring/mocks/ChainReader.go index d1d9498d4..7df58df51 100644 --- a/pkg/monitoring/mocks/ChainReader.go +++ b/pkg/monitoring/mocks/ChainReader.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.12.0. DO NOT EDIT. +// Code generated by mockery v2.28.1. DO NOT EDIT. package mocks @@ -12,8 +12,6 @@ import ( rpc "github.com/gagliardetto/solana-go/rpc" solana "github.com/gagliardetto/solana-go" - - testing "testing" ) // ChainReader is an autogenerated mock type for the ChainReader type @@ -26,6 +24,10 @@ func (_m *ChainReader) GetBalance(ctx context.Context, account solana.PublicKey, ret := _m.Called(ctx, account, commitment) var r0 *rpc.GetBalanceResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (*rpc.GetBalanceResult, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) *rpc.GetBalanceResult); ok { r0 = rf(ctx, account, commitment) } else { @@ -34,7 +36,6 @@ func (_m *ChainReader) GetBalance(ctx context.Context, account solana.PublicKey, } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r1 = rf(ctx, account, commitment) } else { @@ -49,20 +50,23 @@ func (_m *ChainReader) GetLatestTransmission(ctx context.Context, account solana ret := _m.Called(ctx, account, commitment) var r0 pkgsolana.Answer + var r1 uint64 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (pkgsolana.Answer, uint64, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) pkgsolana.Answer); ok { r0 = rf(ctx, account, commitment) } else { r0 = ret.Get(0).(pkgsolana.Answer) } - var r1 uint64 if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) uint64); ok { r1 = rf(ctx, account, commitment) } else { r1 = ret.Get(1).(uint64) } - var r2 error if rf, ok := ret.Get(2).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r2 = rf(ctx, account, commitment) } else { @@ -77,6 +81,10 @@ func (_m *ChainReader) GetSignaturesForAddressWithOpts(ctx context.Context, acco ret := _m.Called(ctx, account, opts) var r0 []*rpc.TransactionSignature + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error)); ok { + return rf(ctx, account, opts) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) []*rpc.TransactionSignature); ok { r0 = rf(ctx, account, opts) } else { @@ -85,7 +93,6 @@ func (_m *ChainReader) GetSignaturesForAddressWithOpts(ctx context.Context, acco } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) error); ok { r1 = rf(ctx, account, opts) } else { @@ -100,20 +107,23 @@ func (_m *ChainReader) GetState(ctx context.Context, account solana.PublicKey, c ret := _m.Called(ctx, account, commitment) var r0 pkgsolana.State + var r1 uint64 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (pkgsolana.State, uint64, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) pkgsolana.State); ok { r0 = rf(ctx, account, commitment) } else { r0 = ret.Get(0).(pkgsolana.State) } - var r1 uint64 if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) uint64); ok { r1 = rf(ctx, account, commitment) } else { r1 = ret.Get(1).(uint64) } - var r2 error if rf, ok := ret.Get(2).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r2 = rf(ctx, account, commitment) } else { @@ -128,6 +138,10 @@ func (_m *ChainReader) GetTokenAccountBalance(ctx context.Context, account solan ret := _m.Called(ctx, account, commitment) var r0 *rpc.GetTokenAccountBalanceResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (*rpc.GetTokenAccountBalanceResult, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) *rpc.GetTokenAccountBalanceResult); ok { r0 = rf(ctx, account, commitment) } else { @@ -136,7 +150,6 @@ func (_m *ChainReader) GetTokenAccountBalance(ctx context.Context, account solan } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r1 = rf(ctx, account, commitment) } else { @@ -151,6 +164,10 @@ func (_m *ChainReader) GetTransaction(ctx context.Context, txSig solana.Signatur ret := _m.Called(ctx, txSig, opts) var r0 *rpc.GetTransactionResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error)); ok { + return rf(ctx, txSig, opts) + } if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) *rpc.GetTransactionResult); ok { r0 = rf(ctx, txSig, opts) } else { @@ -159,7 +176,6 @@ func (_m *ChainReader) GetTransaction(ctx context.Context, txSig solana.Signatur } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) error); ok { r1 = rf(ctx, txSig, opts) } else { @@ -169,8 +185,13 @@ func (_m *ChainReader) GetTransaction(ctx context.Context, txSig solana.Signatur return r0, r1 } -// NewChainReader creates a new instance of ChainReader. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. -func NewChainReader(t testing.TB) *ChainReader { +type mockConstructorTestingTNewChainReader interface { + mock.TestingT + Cleanup(func()) +} + +// NewChainReader creates a new instance of ChainReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewChainReader(t mockConstructorTestingTNewChainReader) *ChainReader { mock := &ChainReader{} mock.Mock.Test(t) diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index 766959f3a..c5444905d 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -3,14 +3,18 @@ package client import ( "context" "fmt" + "math/big" "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/pkg/errors" + "golang.org/x/sync/singleflight" + + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logger" - "golang.org/x/sync/singleflight" ) const ( @@ -47,6 +51,9 @@ type Writer interface { var _ ReaderWriter = (*Client)(nil) +var _ htrktypes.Client[*types.Head, *Subscription, types.ChainID, types.Hash] = (*Client)(nil) + +//go:generate mockery --quiet --name Client --output ./mocks/ --case=underscore type Client struct { rpc *rpc.Client skipPreflight bool // to enable or disable preflight checks @@ -55,6 +62,8 @@ type Client struct { txTimeout time.Duration contextDuration time.Duration log logger.Logger + pollingInterval time.Duration + chainId types.ChainID // provides a duplicate function call suppression mechanism requestGroup *singleflight.Group @@ -67,6 +76,7 @@ func NewClient(endpoint string, cfg config.Config, requestTimeout time.Duration, commitment: cfg.Commitment(), maxRetries: cfg.MaxRetries(), txTimeout: cfg.TxTimeout(), + pollingInterval: cfg.PollingInterval(), //TODO: Add this in the config in core contextDuration: requestTimeout, log: log, requestGroup: &singleflight.Group{}, @@ -139,6 +149,21 @@ func (c *Client) ChainID() (string, error) { return network, nil } +func (c *Client) ConfiguredChainID() types.ChainID { + if c.chainId != types.Unknown { + return c.chainId + } + + chainID, err := c.ChainID() + if err != nil { + c.log.Warnf("Unable to determine configured chain ID: %v", err) + return types.Unknown + } + + c.chainId = types.StringToChainID(chainID) + return c.chainId +} + func (c *Client) GetFeeForMessage(msg string) (uint64, error) { // msg is base58 encoded data @@ -209,3 +234,132 @@ func (c *Client) SendTx(ctx context.Context, tx *solana.Transaction) (solana.Sig return c.rpc.SendTransactionWithOpts(ctx, tx, opts) } + +func (c *Client) HeadByNumber(ctx context.Context, number *big.Int) (*types.Head, error) { + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + block, err := c.GetBlock(ctx, number.Uint64()) + if err != nil { + return nil, err + } + if block == nil { + return nil, errors.New("invalid block in HeadByNumber") + } + chainId := c.ConfiguredChainID() + head := &types.Head{ + Slot: number.Int64(), + Block: *block, + ID: chainId, + } + return head, nil +} + +// SubscribeNewHead polls the RPC endpoint for new blocks. +func (c *Client) SubscribeNewHead(ctx context.Context, ch chan<- *types.Head) (*Subscription, error) { + subscription := NewSubscription(ctx, c) + + go func() { + ticker := time.NewTicker(c.pollingInterval) + + for { + select { + case <-ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + block, slot, err := c.getLatestBlock(ctx) + + if err != nil { + subscription.errChan <- err + return + } + + // Create a new Head object and send to channel + head := &types.Head{ + Slot: int64(slot), + Block: *block, + ID: c.ConfiguredChainID(), + } + ch <- head + } + } + }() + + return subscription, nil +} + +// getLatestBlock queries the latest slot and returns the block. +func (c *Client) getLatestBlock(ctx context.Context) (block *rpc.GetBlockResult, slot uint64, err error) { + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + + slot, err = c.GetLatestSlot(ctx) + if err != nil { + return nil, slot, errors.Wrap(err, "error in GetLatestSlot") + } + + block, err = c.GetBlock(ctx, slot) + if err != nil { + return nil, slot, err + } + + return block, slot, nil +} + +func (c *Client) GetBlock(ctx context.Context, slot uint64) (out *rpc.GetBlockResult, err error) { + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + + res, err, _ := c.requestGroup.Do("GetBlock", func() (interface{}, error) { + return c.rpc.GetBlock(ctx, slot) + }) + + if err != nil { + return nil, errors.Wrap(err, "error in GetBlock") + } + if res == nil { + return nil, errors.New("nil pointer in GetBlock") + } + + return res.(*rpc.GetBlockResult), err +} + +func (c *Client) GetLatestSlot(ctx context.Context) (uint64, error) { + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + + res, err, _ := c.requestGroup.Do("GetSlot", func() (interface{}, error) { + return c.rpc.GetSlot(ctx, c.commitment) + }) + + if err != nil { + return 0, errors.Wrap(err, "error in GetSlot") + } + + if res == nil { + return 0, errors.New("nil pointer in GetSlot") + } + + return res.(uint64), err +} + +func (c *Client) GetBlocks(ctx context.Context, startSlot, endSlot uint64) (blocks []uint64, err error) { + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + + res, err, _ := c.requestGroup.Do("GetBlocks", func() (interface{}, error) { + return c.rpc.GetBlocks(ctx, startSlot, &endSlot, c.commitment) + }) + + if err != nil { + return nil, errors.Wrap(err, "error in GetBlocks") + } + if res == nil { + return nil, errors.New("nil pointer in GetBlocks") + } + + blocks = make([]uint64, len(res.(rpc.BlocksResult))) + copy(blocks, res.(rpc.BlocksResult)) + + return blocks, err +} diff --git a/pkg/solana/client/client_test.go b/pkg/solana/client/client_test.go index cc378f4fb..94ad18ead 100644 --- a/pkg/solana/client/client_test.go +++ b/pkg/solana/client/client_test.go @@ -2,7 +2,9 @@ package client import ( "context" + "encoding/json" "fmt" + "math/big" "math/rand" "net/http" "net/http/httptest" @@ -13,6 +15,7 @@ import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/programs/system" "github.com/gagliardetto/solana-go/rpc" + "github.com/gagliardetto/solana-go/rpc/jsonrpc" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,6 +24,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" + headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) func TestClient_Reader_Integration(t *testing.T) { @@ -276,3 +280,215 @@ func TestClient_SendTxDuplicates_Integration(t *testing.T) { assert.NoError(t, err) assert.Equal(t, initBal-endBal, uint64(5_000)) } + +func TestClient_SubscribeNewHead(t *testing.T) { + requestCounter := 0 + var slot uint64 + + slotResponses := []uint64{ + 428, + 199750878, + 199750877, + } + blockResponses := map[int]string{ + 428: `{ + "blockHeight": 428, + "blockTime": null, + "blockhash": "3Eq21vXNB5s86c62bVuUfTeaMif1N2kUqRPBmGRJhyTA", + "parentSlot": 429, + "previousBlockhash": "mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B", + "transactions": [] + }`, + 199750878: `{ + "blockHeight": 199750878, + "blockTime": 1686852740, + "blockhash": "7XmsC2yHyHWhF1WQGgHEZGZc9jyvyaY3V7eMhwB2ovEY", + "parentSlot": 199750877, + "previousBlockhash": "5k8ayeVNWk2dXaMmMNYvgwaB6rQwizNqpyRKRScczP34", + "transactions": [] + }`, + 199750877: `{ + "blockHeight": 199750877, + "blockTime": 1686852680, + "blockhash": "5k8ayeVNWk2dXaMmMNYvgwaB6rQwizNqpyRKRScczP34", + "parentSlot": 199750876, + "previousBlockhash": "CLDZ8BDLFtgqk3j4ksEX5HMjws5R9mMDu71X7UvNE5i8", + "transactions": [] + }`, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewConfig(db.ChainCfg{}, lggr) + + // Mock Server for GetBlock requests. + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var rpcReq jsonrpc.RPCRequest + err := json.NewDecoder(r.Body).Decode(&rpcReq) + require.NoError(t, err) + + switch rpcReq.Method { + case "getSlot": + slot = slotResponses[requestCounter] + out := fmt.Sprintf(`{"jsonrpc":"2.0","result":%d,"id":1}`, slot) + _, err := w.Write([]byte(out)) + require.NoError(t, err) + + case "getBlock": + out := fmt.Sprintf(`{"jsonrpc":"2.0","result":%s,"id":1}`, blockResponses[int(slot)]) + requestCounter++ + _, err := w.Write([]byte(out)) + require.NoError(t, err) + case "getGenesisHash": + out := fmt.Sprintf(`{"jsonrpc":"2.0","result":"%s","id":1}`, MainnetGenesisHash) + _, err := w.Write([]byte(out)) + require.NoError(t, err) + + default: + // respond with error + http.Error(w, "Method not supported", http.StatusMethodNotAllowed) + } + })) + defer mockServer.Close() + + c, err := NewClient(mockServer.URL, cfg, requestTimeout, lggr) + require.NoError(t, err) + + headCh := make(chan *headtracker.Head) + + subscription, err := c.SubscribeNewHead(ctx, headCh) + require.NoError(t, err) + + // Consume from the head channel and make assertions. + for i := 0; i < len(slotResponses); i++ { + select { + case head := <-headCh: + slotResponse := slotResponses[i] + + require.Equal(t, slotResponse, *head.Block.BlockHeight) + + case <-time.After(5 * time.Second): + t.Fatalf("Did not receive new head in time") + } + } + + // Make sure there are no more heads. + select { + case head := <-headCh: + t.Fatalf("Received unexpected head: %+v", head) + + case <-time.After(time.Second): + // No more heads, as expected. + } + + subscription.Unsubscribe() +} + +func TestClient_HeadByNumber(t *testing.T) { + url := SetupLocalSolNode(t) + + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewConfig(db.ChainCfg{}, lggr) + c, err := NewClient(url, cfg, requestTimeout, lggr) + assert.NoError(t, err) + + t.Run("happy case, valid block number", func(t *testing.T) { + ctx := context.Background() + // Get most recent height + slotHeight, err := c.SlotHeight() + assert.NoError(t, err) + + // Get List of blocks + blockNumbers, err := c.GetBlocks(ctx, 0, slotHeight) + assert.NoError(t, err) + assert.NotEmpty(t, blockNumbers) + + // Use the first block for our test + firstBlockNumber := blockNumbers[0] + block, err := c.HeadByNumber(ctx, big.NewInt(int64(firstBlockNumber))) + + assert.NoError(t, err) + assert.Equal(t, int64(firstBlockNumber), block.Slot) + }) + + t.Run("negative block number", func(t *testing.T) { + ctx := context.Background() + + block, err := c.HeadByNumber(ctx, big.NewInt(-1)) + assert.Error(t, err) // expecting error + assert.Nil(t, block) // expecting no block + }) + + t.Run("block does not exist", func(t *testing.T) { + ctx := context.Background() + + block, err := c.HeadByNumber(ctx, big.NewInt(99999999999)) + assert.Error(t, err) + assert.Nil(t, block) + }) + +} + +func TestClient_GetBlock(t *testing.T) { + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewConfig(db.ChainCfg{}, lggr) + + blockHeight := uint64(199750875) + blockHash, err := solana.HashFromBase58("FDJBEXcTgD3Z17BdVM2K6o2j35JHJRXUf7NkHK5w7AbD") + if err != nil { + t.Fatal(err) + } + + previousBlockHash, err := solana.HashFromBase58("3rQRaHFL8uC8jMERbXeTJjhgSomtiuEPVAGYtjrickxr") + if err != nil { + t.Fatal(err) + } + + blockTime := solana.UnixTimeSeconds(1626110123) + + block := &rpc.GetBlockResult{ + BlockHeight: &blockHeight, + Blockhash: blockHash, + ParentSlot: uint64(199750874), + PreviousBlockhash: previousBlockHash, + Rewards: []rpc.BlockReward{}, + Transactions: []rpc.TransactionWithMeta{}, + BlockTime: &blockTime, + } + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + out := fmt.Sprintf(`{"jsonrpc":"2.0","result":%s,"id":1}`, MustJSON(block)) + _, err := w.Write([]byte(out)) + require.NoError(t, err) + })) + defer mockServer.Close() + + c, err := NewClient(mockServer.URL, cfg, requestTimeout, lggr) + require.NoError(t, err) + + ctx := context.Background() + out, err := c.GetBlock(ctx, uint64(100)) + + assert.NoError(t, err) + assert.Equal(t, block, out) +} + +func TestClient_GetLatestSlot(t *testing.T) { + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewConfig(db.ChainCfg{}, lggr) + url := SetupLocalSolNode(t) + + c, err := NewClient(url, cfg, requestTimeout, lggr) + require.NoError(t, err) + + ctx := context.Background() + slot, err := c.GetLatestSlot(ctx) + assert.NoError(t, err) + assert.Greater(t, slot, uint64(0)) +} diff --git a/pkg/solana/client/subscription.go b/pkg/solana/client/subscription.go new file mode 100644 index 000000000..4e6323cc0 --- /dev/null +++ b/pkg/solana/client/subscription.go @@ -0,0 +1,34 @@ +package client + +import ( + "context" + + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" +) + +var _ commontypes.Subscription = (*Subscription)(nil) + +type Subscription struct { + ctx context.Context + cancel context.CancelFunc + errChan chan error + client *Client +} + +func NewSubscription(ctx context.Context, client *Client) *Subscription { + ctx, cancel := context.WithCancel(ctx) + return &Subscription{ + ctx: ctx, + cancel: cancel, + client: client, + errChan: make(chan error), + } +} + +func (s *Subscription) Unsubscribe() { + s.cancel() +} + +func (s *Subscription) Err() <-chan error { + return s.errChan +} diff --git a/pkg/solana/client/subscription_test.go b/pkg/solana/client/subscription_test.go new file mode 100644 index 000000000..3ab9e3ebc --- /dev/null +++ b/pkg/solana/client/subscription_test.go @@ -0,0 +1,118 @@ +package client + +import ( + "context" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" + "github.com/stretchr/testify/assert" + "github.com/test-go/testify/require" +) + +func initClient(t *testing.T) (*Client, context.Context) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + url := DummyUrl(t) + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewConfig(db.ChainCfg{}, lggr) + + c, err := NewClient(url, cfg, requestTimeout, lggr) + require.NoError(t, err) + + return c, ctx +} + +func TestSubscription_New(t *testing.T) { + c, ctx := initClient(t) + + t.Run("happy path", func(t *testing.T) { + subscription := NewSubscription(ctx, c) + assert.NotNil(t, subscription) + assert.NotNil(t, subscription.ctx) + assert.NotNil(t, subscription.cancel) + assert.NotNil(t, subscription.client) + assert.NotNil(t, subscription.errChan) + }) + + // Edge case: pass a nil client + t.Run("nil client", func(t *testing.T) { + subscription := NewSubscription(ctx, nil) + assert.NotNil(t, subscription) + assert.Nil(t, subscription.client) + }) +} + +func TestSubscription_Unsubscribe(t *testing.T) { + c, ctx := initClient(t) + + t.Run("happy path", func(t *testing.T) { + subscription := NewSubscription(ctx, c) + + // The Done channel should not be closed yet + select { + case <-subscription.ctx.Done(): + t.Fatal("Expected context to not be done yet") + default: + } + + subscription.Unsubscribe() + + select { + // Success + case <-subscription.ctx.Done(): + return + case <-time.After(100 * time.Millisecond): + t.Fatal("Expected context to be done") + } + }) + + // Edge case: unsubscribe twice + t.Run("double unsubscribe", func(t *testing.T) { + subscription := NewSubscription(ctx, c) + subscription.Unsubscribe() + subscription.Unsubscribe() // Shouldn't panic or error + }) +} + +func TestSubscription_Err(t *testing.T) { + c, ctx := initClient(t) + t.Run("happy path", func(t *testing.T) { + subscription := NewSubscription(ctx, c) + + errCh := subscription.Err() + assert.NotNil(t, errCh) + + // Send an error to the error channel + expectedError := errors.New("mock error") + go func() { + subscription.errChan <- expectedError + }() + + select { + case err := <-errCh: + assert.ErrorIs(t, err, expectedError) + case <-time.After(100 * time.Millisecond): + t.Fatal("Expected error was not received") + } + }) + + // Edge case: no error sent + t.Run("no error", func(t *testing.T) { + subscription := NewSubscription(ctx, c) + errCh := subscription.Err() + assert.NotNil(t, errCh) + + select { + case err := <-errCh: + t.Fatalf("Did not expect error, got %v", err) + case <-time.After(100 * time.Millisecond): + // Success: no error received as expected + } + }) +} diff --git a/pkg/solana/client/test_helpers.go b/pkg/solana/client/test_helpers.go index e763607df..ab4c98ce9 100644 --- a/pkg/solana/client/test_helpers.go +++ b/pkg/solana/client/test_helpers.go @@ -3,6 +3,8 @@ package client import ( "bytes" "context" + "encoding/json" + "log" "os/exec" "testing" "time" @@ -45,7 +47,8 @@ func SetupLocalSolNode(t *testing.T) string { client := rpc.New(url) out, err := client.GetHealth(context.Background()) if err != nil || out != rpc.HealthOk { - t.Logf("API server not ready yet (attempt %d)\n", i+1) + t.Logf("API server not ready yet (attempt %d)\nCmd output: %s\nCmd error: %s\n", + i+1, stdOut.String(), stdErr.String()) continue } ready = true @@ -68,3 +71,18 @@ func FundTestAccounts(t *testing.T, keys []solana.PublicKey, url string) { require.NoError(t, err) } } + +func DummyUrl(t *testing.T) string { + port := utils.MustRandomPort(t) + url := "http://127.0.0.1:" + port + return url +} + +// MustJSON marshals an object into a JSON string and panics if there's an error. +func MustJSON(obj interface{}) string { + jsonBytes, err := json.Marshal(obj) + if err != nil { + log.Fatalf("Error marshalling object: %v", err) + } + return string(jsonBytes) +} diff --git a/pkg/solana/config/config.go b/pkg/solana/config/config.go index c680b09fc..85b9cbbfc 100644 --- a/pkg/solana/config/config.go +++ b/pkg/solana/config/config.go @@ -10,6 +10,7 @@ import ( relaycfg "github.com/smartcontractkit/chainlink-relay/pkg/config" "github.com/smartcontractkit/chainlink-relay/pkg/utils" + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logger" ) @@ -33,6 +34,14 @@ var defaultConfigSet = configSet{ ComputeUnitPriceMin: 0, ComputeUnitPriceDefault: 0, FeeBumpPeriod: 3 * time.Second, + + // headtracker + BlockEmissionIdleWarningThreshold: 30 * time.Second, + FinalityDepth: 50, + HeadTrackerHistoryDepth: 100, + HeadTrackerMaxBufferSize: 3, + HeadTrackerSamplingInterval: 1 * time.Second, + PollingInterval: 2 * time.Second, } //go:generate mockery --name Config --output ./mocks/ --case=underscore --filename config.go @@ -54,6 +63,14 @@ type Config interface { ComputeUnitPriceMin() uint64 ComputeUnitPriceDefault() uint64 FeeBumpPeriod() time.Duration + + // headtracker + BlockEmissionIdleWarningThreshold() time.Duration + FinalityDepth() uint32 + HeadTrackerHistoryDepth() uint32 + HeadTrackerMaxBufferSize() uint32 + HeadTrackerSamplingInterval() time.Duration + PollingInterval() time.Duration } // opt: remove @@ -74,9 +91,17 @@ type configSet struct { ComputeUnitPriceMin uint64 ComputeUnitPriceDefault uint64 FeeBumpPeriod time.Duration + + BlockEmissionIdleWarningThreshold time.Duration + FinalityDepth uint32 + HeadTrackerHistoryDepth uint32 + HeadTrackerMaxBufferSize uint32 + HeadTrackerSamplingInterval time.Duration + PollingInterval time.Duration } var _ Config = (*config)(nil) +var _ htrktypes.Config = (*config)(nil) // Deprecated type config struct { @@ -242,22 +267,52 @@ func (c *config) FeeBumpPeriod() time.Duration { return c.defaults.FeeBumpPeriod } +func (c *config) BlockEmissionIdleWarningThreshold() time.Duration { + return c.defaults.BlockEmissionIdleWarningThreshold +} + +func (c *config) FinalityDepth() uint32 { + return c.defaults.FinalityDepth +} + +func (c *config) HeadTrackerHistoryDepth() uint32 { + return c.defaults.HeadTrackerHistoryDepth +} + +func (c *config) HeadTrackerMaxBufferSize() uint32 { + return c.defaults.HeadTrackerMaxBufferSize +} + +func (c *config) HeadTrackerSamplingInterval() time.Duration { + return c.defaults.HeadTrackerSamplingInterval +} + +func (c *config) PollingInterval() time.Duration { + return c.defaults.PollingInterval +} + type Chain struct { - BalancePollPeriod *utils.Duration - ConfirmPollPeriod *utils.Duration - OCR2CachePollPeriod *utils.Duration - OCR2CacheTTL *utils.Duration - TxTimeout *utils.Duration - TxRetryTimeout *utils.Duration - TxConfirmTimeout *utils.Duration - SkipPreflight *bool - Commitment *string - MaxRetries *int64 - FeeEstimatorMode *string - ComputeUnitPriceMax *uint64 - ComputeUnitPriceMin *uint64 - ComputeUnitPriceDefault *uint64 - FeeBumpPeriod *utils.Duration + BalancePollPeriod *utils.Duration + ConfirmPollPeriod *utils.Duration + OCR2CachePollPeriod *utils.Duration + OCR2CacheTTL *utils.Duration + TxTimeout *utils.Duration + TxRetryTimeout *utils.Duration + TxConfirmTimeout *utils.Duration + SkipPreflight *bool + Commitment *string + MaxRetries *int64 + FeeEstimatorMode *string + ComputeUnitPriceMax *uint64 + ComputeUnitPriceMin *uint64 + ComputeUnitPriceDefault *uint64 + FeeBumpPeriod *utils.Duration + BlockEmissionIdleWarningThreshold *utils.Duration + FinalityDepth *uint32 + HeadTrackerHistoryDepth *uint32 + HeadTrackerMaxBufferSize *uint32 + HeadTrackerSamplingInterval *utils.Duration + PollingInterval *utils.Duration } func (c *Chain) SetDefaults() { @@ -307,6 +362,25 @@ func (c *Chain) SetDefaults() { if c.FeeBumpPeriod == nil { c.FeeBumpPeriod = utils.MustNewDuration(defaultConfigSet.FeeBumpPeriod) } + if c.BlockEmissionIdleWarningThreshold == nil { + c.BlockEmissionIdleWarningThreshold = utils.MustNewDuration(defaultConfigSet.BlockEmissionIdleWarningThreshold) + } + if c.FinalityDepth == nil { + c.FinalityDepth = &defaultConfigSet.FinalityDepth + } + if c.HeadTrackerHistoryDepth == nil { + c.HeadTrackerHistoryDepth = &defaultConfigSet.HeadTrackerHistoryDepth + } + if c.HeadTrackerMaxBufferSize == nil { + c.HeadTrackerMaxBufferSize = &defaultConfigSet.HeadTrackerMaxBufferSize + } + if c.HeadTrackerSamplingInterval == nil { + c.HeadTrackerSamplingInterval = utils.MustNewDuration(defaultConfigSet.HeadTrackerSamplingInterval) + } + if c.PollingInterval == nil { + c.PollingInterval = utils.MustNewDuration(defaultConfigSet.PollingInterval) + } + return } diff --git a/pkg/solana/config/mocks/config.go b/pkg/solana/config/mocks/config.go index f9f35c3a5..c765b7d44 100644 --- a/pkg/solana/config/mocks/config.go +++ b/pkg/solana/config/mocks/config.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.28.1. DO NOT EDIT. package mocks @@ -28,6 +28,20 @@ func (_m *Config) BalancePollPeriod() time.Duration { return r0 } +// BlockEmissionIdleWarningThreshold provides a mock function with given fields: +func (_m *Config) BlockEmissionIdleWarningThreshold() time.Duration { + ret := _m.Called() + + var r0 time.Duration + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + return r0 +} + // Commitment provides a mock function with given fields: func (_m *Config) Commitment() rpc.CommitmentType { ret := _m.Called() @@ -126,6 +140,62 @@ func (_m *Config) FeeEstimatorMode() string { return r0 } +// FinalityDepth provides a mock function with given fields: +func (_m *Config) FinalityDepth() uint32 { + ret := _m.Called() + + var r0 uint32 + if rf, ok := ret.Get(0).(func() uint32); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint32) + } + + return r0 +} + +// HeadTrackerHistoryDepth provides a mock function with given fields: +func (_m *Config) HeadTrackerHistoryDepth() uint32 { + ret := _m.Called() + + var r0 uint32 + if rf, ok := ret.Get(0).(func() uint32); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint32) + } + + return r0 +} + +// HeadTrackerMaxBufferSize provides a mock function with given fields: +func (_m *Config) HeadTrackerMaxBufferSize() uint32 { + ret := _m.Called() + + var r0 uint32 + if rf, ok := ret.Get(0).(func() uint32); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint32) + } + + return r0 +} + +// HeadTrackerSamplingInterval provides a mock function with given fields: +func (_m *Config) HeadTrackerSamplingInterval() time.Duration { + ret := _m.Called() + + var r0 time.Duration + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + return r0 +} + // MaxRetries provides a mock function with given fields: func (_m *Config) MaxRetries() *uint { ret := _m.Called() @@ -170,6 +240,20 @@ func (_m *Config) OCR2CacheTTL() time.Duration { return r0 } +// PollingInterval provides a mock function with given fields: +func (_m *Config) PollingInterval() time.Duration { + ret := _m.Called() + + var r0 time.Duration + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + return r0 +} + // SkipPreflight provides a mock function with given fields: func (_m *Config) SkipPreflight() bool { ret := _m.Called() diff --git a/pkg/solana/headtracker/config.go b/pkg/solana/headtracker/config.go new file mode 100644 index 000000000..1b36ffb59 --- /dev/null +++ b/pkg/solana/headtracker/config.go @@ -0,0 +1,74 @@ +package headtracker + +import ( + "time" + + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" +) + +// This Config serves as a POC for headtracker. +// It should be replaced with a more robust Config +// such as the one in pkg/solana/Config + +type Config struct { + Defaults configSet +} + +type configSet struct { + BlockEmissionIdleWarningThreshold time.Duration + FinalityDepth uint32 + HeadTrackerHistoryDepth uint32 + HeadTrackerMaxBufferSize uint32 + HeadTrackerSamplingInterval time.Duration + PollingInterval time.Duration +} + +var defaultConfigSet = configSet{ + // headtracker + BlockEmissionIdleWarningThreshold: 30 * time.Second, + FinalityDepth: 50, + HeadTrackerHistoryDepth: 100, + HeadTrackerMaxBufferSize: 3, + HeadTrackerSamplingInterval: 1 * time.Second, + PollingInterval: 2 * time.Second, +} + +func NewConfig() *Config { + return &Config{ + Defaults: defaultConfigSet, + } +} + +var _ htrktypes.Config = &Config{} + +func (c *Config) BlockEmissionIdleWarningThreshold() time.Duration { + return c.Defaults.BlockEmissionIdleWarningThreshold +} + +func (c *Config) FinalityDepth() uint32 { + return c.Defaults.FinalityDepth +} + +func (c *Config) HeadTrackerHistoryDepth() uint32 { + return c.Defaults.HeadTrackerHistoryDepth +} + +func (c *Config) HeadTrackerMaxBufferSize() uint32 { + return c.Defaults.HeadTrackerMaxBufferSize +} + +func (c *Config) HeadTrackerSamplingInterval() time.Duration { + return c.Defaults.HeadTrackerSamplingInterval +} + +func (c *Config) PollingInterval() time.Duration { + return c.Defaults.PollingInterval +} + +func (c *Config) SetHeadTrackerSamplingInterval(d time.Duration) { + c.Defaults.HeadTrackerSamplingInterval = d +} + +func (c *Config) SetHeadTrackerMaxBufferSize(n uint32) { + c.Defaults.HeadTrackerMaxBufferSize = n +} diff --git a/pkg/solana/headtracker/head_broadcaster.go b/pkg/solana/headtracker/head_broadcaster.go new file mode 100644 index 000000000..4d143ee69 --- /dev/null +++ b/pkg/solana/headtracker/head_broadcaster.go @@ -0,0 +1,18 @@ +package headtracker + +import ( + "github.com/smartcontractkit/chainlink-relay/pkg/headtracker" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +type HeadBroadcaster = headtracker.HeadBroadcaster[*types.Head, types.Hash] + +var _ commontypes.HeadBroadcaster[*types.Head, types.Hash] = &HeadBroadcaster{} + +func NewBroadcaster( + lggr logger.Logger, +) *HeadBroadcaster { + return headtracker.NewHeadBroadcaster[*types.Head, types.Hash](lggr) +} diff --git a/pkg/solana/headtracker/head_broadcaster_test.go b/pkg/solana/headtracker/head_broadcaster_test.go new file mode 100644 index 000000000..630e047e4 --- /dev/null +++ b/pkg/solana/headtracker/head_broadcaster_test.go @@ -0,0 +1,186 @@ +package headtracker_test + +import ( + "context" + "testing" + "time" + + "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + commonhtrk "github.com/smartcontractkit/chainlink-relay/pkg/headtracker" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-relay/pkg/services" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + commonmocks "github.com/smartcontractkit/chainlink-relay/pkg/types/mocks" + "github.com/smartcontractkit/chainlink-relay/pkg/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +func waitHeadBroadcasterToStart(t *testing.T, hb commontypes.HeadBroadcaster[*types.Head, types.Hash]) { + t.Helper() + + subscriber := &cltest.MockHeadTrackable{} + _, unsubscribe := hb.Subscribe(subscriber) + defer unsubscribe() + + hb.BroadcastNewLongestChain(cltest.Head(1)) + g := gomega.NewWithT(t) + g.Eventually(subscriber.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) +} + +func TestHeadBroadcaster_Subscribe(t *testing.T) { + t.Parallel() + g := gomega.NewWithT(t) + g.SetDefaultEventuallyTimeout(1 * time.Second) + + lggr, _ := logger.New() + sub := commonmocks.NewSubscription(t) + client := cltest.NewClientMockWithDefaultChain(t) + cfg := headtracker.NewConfig() + + chchHeaders := make(chan chan<- *types.Head, 1) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + chchHeaders <- args.Get(1).(chan<- *types.Head) + }). + Return(sub, nil) + client.On("HeadByNumber", mock.Anything, mock.Anything).Return(cltest.Head(1), nil) + + sub.On("Unsubscribe").Return() + sub.On("Err").Return(nil) + + checker1 := &cltest.MockHeadTrackable{} + checker2 := &cltest.MockHeadTrackable{} + + hb := headtracker.NewBroadcaster(lggr) + hs := headtracker.NewSaver(cfg, lggr) + mailMon := utils.NewMailboxMonitor(t.Name()) + ht := headtracker.NewTracker(lggr, client, cfg, hb, hs, mailMon) + + var ms services.MultiStart + require.NoError(t, ms.Start(testutils.Context(t), mailMon, hb, ht)) + t.Cleanup(func() { require.NoError(t, services.CloseAll(mailMon, hb, ht)) }) + + latest1, unsubscribe1 := hb.Subscribe(checker1) + // "latest head" is nil here because we didn't receive any yet + assert.Equal(t, (*types.Head)(nil), latest1) + + firstHead := cltest.Head(1) + secondHead := cltest.Head(2) + firstHead.Parent = secondHead + secondHead.Block.PreviousBlockhash = firstHead.Block.Blockhash + + headers := <-chchHeaders + headers <- firstHead + g.Eventually(checker1.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) + + latest2, _ := hb.Subscribe(checker2) + + // "latest head" is set here to the most recent head received + assert.NotNil(t, latest2) + assert.Equal(t, firstHead.BlockNumber(), latest2.BlockNumber()) + + unsubscribe1() + + headers <- secondHead + g.SetDefaultEventuallyTimeout(2 * time.Second) + g.Eventually(checker2.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) +} + +func TestHeadBroadcaster_BroadcastNewLongestChain(t *testing.T) { + t.Parallel() + g := gomega.NewWithT(t) + + lggr, _ := logger.New() + broadcaster := headtracker.NewBroadcaster(lggr) + + err := broadcaster.Start(testutils.Context(t)) + require.NoError(t, err) + + waitHeadBroadcasterToStart(t, broadcaster) + + subscriber1 := &cltest.MockHeadTrackable{} + subscriber2 := &cltest.MockHeadTrackable{} + _, unsubscribe1 := broadcaster.Subscribe(subscriber1) + _, unsubscribe2 := broadcaster.Subscribe(subscriber2) + + broadcaster.BroadcastNewLongestChain(cltest.Head(1)) + g.Eventually(subscriber1.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) + + unsubscribe1() + + broadcaster.BroadcastNewLongestChain(cltest.Head(2)) + g.Eventually(subscriber2.OnNewLongestChainCount).Should(gomega.Equal(int32(2))) + + unsubscribe2() + + subscriber3 := &cltest.MockHeadTrackable{} + _, unsubscribe3 := broadcaster.Subscribe(subscriber3) + broadcaster.BroadcastNewLongestChain(cltest.Head(1)) + g.Eventually(subscriber3.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) + + unsubscribe3() + + // no subscribers - shall do nothing + broadcaster.BroadcastNewLongestChain(cltest.Head(0)) + + err = broadcaster.Close() + require.NoError(t, err) + + require.Equal(t, int32(1), subscriber3.OnNewLongestChainCount()) +} + +func TestHeadBroadcaster_TrackableCallbackTimeout(t *testing.T) { + t.Parallel() + + lggr, _ := logger.New() + broadcaster := headtracker.NewBroadcaster(lggr) + + err := broadcaster.Start(testutils.Context(t)) + require.NoError(t, err) + + waitHeadBroadcasterToStart(t, broadcaster) + + slowAwaiter := cltest.NewAwaiter() + fastAwaiter := cltest.NewAwaiter() + slow := &sleepySubscriber{awaiter: slowAwaiter, delay: commonhtrk.TrackableCallbackTimeout * 2} + fast := &sleepySubscriber{awaiter: fastAwaiter, delay: commonhtrk.TrackableCallbackTimeout / 2} + _, unsubscribe1 := broadcaster.Subscribe(slow) + _, unsubscribe2 := broadcaster.Subscribe(fast) + + broadcaster.BroadcastNewLongestChain(cltest.Head(1)) + slowAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + fastAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + + require.True(t, slow.contextDone) + require.False(t, fast.contextDone) + + unsubscribe1() + unsubscribe2() + + err = broadcaster.Close() + require.NoError(t, err) +} + +type sleepySubscriber struct { + awaiter cltest.Awaiter + delay time.Duration + contextDone bool +} + +func (ss *sleepySubscriber) OnNewLongestChain(ctx context.Context, head *types.Head) { + time.Sleep(ss.delay) + select { + case <-ctx.Done(): + ss.contextDone = true + default: + } + ss.awaiter.ItHappened() +} diff --git a/pkg/solana/headtracker/head_listener.go b/pkg/solana/headtracker/head_listener.go new file mode 100644 index 000000000..e6c621d0b --- /dev/null +++ b/pkg/solana/headtracker/head_listener.go @@ -0,0 +1,23 @@ +package headtracker + +import ( + "github.com/smartcontractkit/chainlink-relay/pkg/headtracker" + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +type HeadListener = headtracker.HeadListener[*types.Head, commontypes.Subscription, types.ChainID, types.Hash] + +var _ commontypes.HeadListener[*types.Head, types.Hash] = &HeadListener{} + +func NewListener( + lggr logger.Logger, + solanaClient htrktypes.Client[*types.Head, commontypes.Subscription, types.ChainID, types.Hash], + config htrktypes.Config, + chStop chan struct{}, +) *HeadListener { + return headtracker.NewHeadListener[*types.Head, commontypes.Subscription, + types.ChainID, types.Hash](lggr, solanaClient, config, chStop) +} diff --git a/pkg/solana/headtracker/head_listener_test.go b/pkg/solana/headtracker/head_listener_test.go new file mode 100644 index 000000000..181c69a71 --- /dev/null +++ b/pkg/solana/headtracker/head_listener_test.go @@ -0,0 +1,229 @@ +package headtracker_test + +import ( + "context" + "errors" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stretchr/testify/mock" + + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commonmocks "github.com/smartcontractkit/chainlink-relay/pkg/types/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +func Test_HeadListener_HappyPath(t *testing.T) { + lggr, _ := logger.New() + client := cltest.NewClientMockWithDefaultChain(t) + cfg := headtracker.NewConfig() + chStop := make(chan struct{}) + hl := headtracker.NewListener(lggr, client, cfg, chStop) + + var headCount atomic.Int32 + handler := func(context.Context, *types.Head) error { + headCount.Add(1) + return nil + } + + subscribeAwaiter := cltest.NewAwaiter() + unsubscribeAwaiter := cltest.NewAwaiter() + var chHeads chan<- *types.Head + var chErr = make(chan error) + var chSubErr <-chan error = chErr + sub := commonmocks.NewSubscription(t) + client.On("SubscribeNewHead", mock.Anything, mock.AnythingOfType("chan<- *types.Head")).Return(sub, nil).Once().Run(func(args mock.Arguments) { + chHeads = args.Get(1).(chan<- *types.Head) + subscribeAwaiter.ItHappened() + }) + sub.On("Err").Return(chSubErr) + sub.On("Unsubscribe").Return().Once().Run(func(mock.Arguments) { + unsubscribeAwaiter.ItHappened() + close(chHeads) + close(chErr) + }) + + doneAwaiter := cltest.NewAwaiter() + done := func() { + doneAwaiter.ItHappened() + } + go hl.ListenForNewHeads(handler, done) + + subscribeAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + require.Eventually(t, hl.Connected, testutils.WaitTimeout(t), testutils.TestInterval) + + chHeads <- cltest.Head(0) + chHeads <- cltest.Head(1) + chHeads <- cltest.Head(2) + + require.True(t, hl.ReceivingHeads()) + + close(chStop) + doneAwaiter.AwaitOrFail(t) + + unsubscribeAwaiter.AwaitOrFail(t) + require.Equal(t, int32(3), headCount.Load()) +} + +func Test_HeadListener_NotReceivingHeads(t *testing.T) { + lggr, _ := logger.New() + client := cltest.NewClientMockWithDefaultChain(t) + cfg := headtracker.NewConfig() + overridenConfig := testutils.NewHeadtrackerConfig(cfg, func(c *headtracker.Config) { + c.Defaults.BlockEmissionIdleWarningThreshold = 1 * time.Second + }) + chStop := make(chan struct{}) + hl := headtracker.NewListener(lggr, client, overridenConfig, chStop) + + firstHeadAwaiter := cltest.NewAwaiter() + handler := func(context.Context, *types.Head) error { + firstHeadAwaiter.ItHappened() + return nil + } + + subscribeAwaiter := cltest.NewAwaiter() + unsubscribeAwaiter := cltest.NewAwaiter() + var chHeads chan<- *types.Head + var chErr = make(chan error) + var chSubErr <-chan error = chErr + sub := commonmocks.NewSubscription(t) + client.On("SubscribeNewHead", mock.Anything, mock.AnythingOfType("chan<- *types.Head")).Return(sub, nil).Once().Run(func(args mock.Arguments) { + chHeads = args.Get(1).(chan<- *types.Head) + subscribeAwaiter.ItHappened() + }) + sub.On("Err").Return(chSubErr) + sub.On("Unsubscribe").Return().Once().Run(func(_ mock.Arguments) { + unsubscribeAwaiter.ItHappened() + close(chHeads) + close(chErr) + }) + + doneAwaiter := cltest.NewAwaiter() + done := func() { + doneAwaiter.ItHappened() + } + go hl.ListenForNewHeads(handler, done) + + subscribeAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + require.Eventually(t, hl.Connected, testutils.WaitTimeout(t), testutils.TestInterval) + + chHeads <- cltest.Head(0) + firstHeadAwaiter.AwaitOrFail(t) + + require.True(t, hl.ReceivingHeads()) + + time.Sleep(time.Second * 2) + + require.False(t, hl.ReceivingHeads()) + + close(chStop) + doneAwaiter.AwaitOrFail(t) +} + +func Test_HeadListener_SubscriptionErr(t *testing.T) { + tests := []struct { + name string + err error + closeErr bool + }{ + {"nil error", nil, false}, + {"socket error", errors.New("close 1006 (abnormal closure): unexpected EOF"), false}, + {"close Err channel", nil, true}, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + lggr, _ := logger.New() + client := cltest.NewClientMockWithDefaultChain(t) + cfg := headtracker.NewConfig() + chStop := make(chan struct{}) + hl := headtracker.NewListener(lggr, client, cfg, chStop) + + hnhCalled := make(chan *types.Head) + hnh := func(_ context.Context, header *types.Head) error { + hnhCalled <- header + return nil + } + doneAwaiter := cltest.NewAwaiter() + done := doneAwaiter.ItHappened + + chSubErrTest := make(chan error) + var chSubErr <-chan error = chSubErrTest + sub := commonmocks.NewSubscription(t) + // sub.Err is called twice because we enter the select loop two times: once + // initially and once again after exactly one head has been received + sub.On("Err").Return(chSubErr).Twice() + + subscribeAwaiter := cltest.NewAwaiter() + var headsCh chan<- *types.Head + // Initial subscribe + client.On("SubscribeNewHead", mock.Anything, mock.AnythingOfType("chan<- *types.Head")).Return(sub, nil).Once().Run(func(args mock.Arguments) { + headsCh = args.Get(1).(chan<- *types.Head) + subscribeAwaiter.ItHappened() + }) + go func() { + hl.ListenForNewHeads(hnh, done) + }() + + // Put a head on the channel to ensure we test all code paths + subscribeAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + head := cltest.Head(0) + headsCh <- head + + h := <-hnhCalled + assert.Equal(t, head, h) + + // Expect a call to unsubscribe on error + sub.On("Unsubscribe").Once().Run(func(_ mock.Arguments) { + close(headsCh) + if !test.closeErr { + close(chSubErrTest) + } + }) + // Expect a resubscribe + chSubErrTest2 := make(chan error) + var chSubErr2 <-chan error = chSubErrTest2 + sub2 := commonmocks.NewSubscription(t) + sub2.On("Err").Return(chSubErr2) + subscribeAwaiter2 := cltest.NewAwaiter() + + var headsCh2 chan<- *types.Head + client.On("SubscribeNewHead", mock.Anything, mock.AnythingOfType("chan<- *types.Head")).Return(sub2, nil).Once().Run(func(args mock.Arguments) { + headsCh2 = args.Get(1).(chan<- *types.Head) + subscribeAwaiter2.ItHappened() + }) + + // Sending test error + if test.closeErr { + close(chSubErrTest) + } else { + chSubErrTest <- test.err + } + + // Wait for it to resubscribe + subscribeAwaiter2.AwaitOrFail(t, testutils.WaitTimeout(t)) + + head2 := cltest.Head(1) + headsCh2 <- head2 + + h2 := <-hnhCalled + assert.Equal(t, head2, h2) + + // Second call to unsubscribe on close + sub2.On("Unsubscribe").Once().Run(func(_ mock.Arguments) { + close(headsCh2) + close(chSubErrTest2) + }) + close(chStop) + doneAwaiter.AwaitOrFail(t) + }) + } +} diff --git a/pkg/solana/headtracker/head_saver_in_mem.go b/pkg/solana/headtracker/head_saver_in_mem.go new file mode 100644 index 000000000..2660f41a2 --- /dev/null +++ b/pkg/solana/headtracker/head_saver_in_mem.go @@ -0,0 +1,191 @@ +package headtracker + +import ( + "context" + "errors" + "sync" + + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +type InMemoryHeadSaver[H htrktypes.Head[BLOCK_HASH, CHAIN_ID], BLOCK_HASH commontypes.Hashable, CHAIN_ID commontypes.ID] struct { + config htrktypes.Config + logger logger.Logger + latestHead H + Heads map[BLOCK_HASH]H + HeadsNumber map[int64][]H + mu sync.RWMutex + getNilHead func() H + getNilHash func() BLOCK_HASH + setParent func(H, H) +} + +type HeadSaver = InMemoryHeadSaver[*types.Head, types.Hash, types.ChainID] + +var _ commontypes.HeadSaver[*types.Head, types.Hash] = (*HeadSaver)(nil) + +func NewInMemoryHeadSaver[ + H htrktypes.Head[BLOCK_HASH, CHAIN_ID], + BLOCK_HASH commontypes.Hashable, + CHAIN_ID commontypes.ID]( + config htrktypes.Config, + lggr logger.Logger, + getNilHead func() H, + getNilHash func() BLOCK_HASH, + setParent func(H, H), +) *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID] { + return &InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]{ + config: config, + logger: logger.Named(lggr, "InMemoryHeadSaver"), + Heads: make(map[BLOCK_HASH]H), + HeadsNumber: make(map[int64][]H), + getNilHead: getNilHead, + getNilHash: getNilHash, + setParent: setParent, + } +} + +// Creates a new In Memory HeadSaver for solana +func NewSaver(config htrktypes.Config, lggr logger.Logger) *HeadSaver { + return NewInMemoryHeadSaver[*types.Head, types.Hash, types.ChainID]( + config, + lggr, + func() *types.Head { return nil }, + func() types.Hash { return types.Hash{} }, + func(head, parent *types.Head) { head.Parent = parent }, + ) +} + +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Save(ctx context.Context, head H) error { + if !head.IsValid() { + return errors.New("invalid head passed to Save method of InMemoryHeadSaver") + } + + historyDepth := int64(hs.config.HeadTrackerHistoryDepth()) + hs.AddHeads(historyDepth, head) + + return nil +} + +// No OP function for Solana +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Load(ctx context.Context) (H, error) { + return hs.LatestChain(), nil +} + +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) LatestChain() H { + head := hs.getLatestHead() + + if head.ChainLength() < hs.config.FinalityDepth() { + hs.logger.Debugw("chain shorter than EvmFinalityDepth", "chainLen", head.ChainLength(), "evmFinalityDepth", hs.config.FinalityDepth()) + } + return head +} + +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Chain(blockHash BLOCK_HASH) H { + hs.mu.RLock() + defer hs.mu.RUnlock() + + if head, exists := hs.Heads[blockHash]; exists { + return head + } + + return hs.getNilHead() +} + +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) HeadByNumber(blockNumber int64) []H { + hs.mu.RLock() + defer hs.mu.RUnlock() + + if heads, exists := hs.HeadsNumber[blockNumber]; exists { + return heads + } + + return []H{} +} + +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) HeadByHash(hash BLOCK_HASH) (H, error) { + hs.mu.RLock() + defer hs.mu.RUnlock() + + if head, exists := hs.Heads[hash]; exists { + return head, nil + } + + return hs.getNilHead(), errors.New("head not found") +} + +// Assembles the heads together and populates the Heads Map +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) AddHeads(historyDepth int64, newHeads ...H) { + hs.mu.Lock() + defer hs.mu.Unlock() + + hs.trimHeads(historyDepth) + for _, head := range newHeads { + blockHash := head.BlockHash() + blockNumber := head.BlockNumber() + parentHash := head.GetParentHash() + + if _, exists := hs.Heads[blockHash]; exists { + continue + } + + if parentHash != hs.getNilHash() { + if parent, exists := hs.Heads[parentHash]; exists { + hs.setParent(head, parent) + } else { + // If parent's head is too old, we should set it to nil + hs.setParent(head, hs.getNilHead()) + } + } + + hs.Heads[blockHash] = head + hs.HeadsNumber[blockNumber] = append(hs.HeadsNumber[blockNumber], head) + + // Set the parent of the existing heads to the new heads added + for _, existingHead := range hs.Heads { + parentHash := existingHead.GetParentHash() + if parentHash != hs.getNilHash() { + if parent, exists := hs.Heads[parentHash]; exists { + hs.setParent(existingHead, parent) + } + } + } + + if !hs.latestHead.IsValid() { + hs.latestHead = head + } else if head.BlockNumber() > hs.latestHead.BlockNumber() { + hs.latestHead = head + } + } +} + +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) TrimOldHeads(historyDepth int64) { + hs.mu.Lock() + defer hs.mu.Unlock() + + hs.trimHeads(historyDepth) +} + +// trimHeads() is should only be called by functions with mutex locking. +// trimHeads() is an internal function without locking to prevent deadlocks +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) trimHeads(historyDepth int64) { + for headNumber, headNumberList := range hs.HeadsNumber { + if hs.latestHead.BlockNumber()-headNumber >= historyDepth { + for _, head := range headNumberList { + delete(hs.Heads, head.BlockHash()) + } + + delete(hs.HeadsNumber, headNumber) + } + } +} + +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) getLatestHead() H { + hs.mu.RLock() + defer hs.mu.RUnlock() + + return hs.latestHead +} diff --git a/pkg/solana/headtracker/head_saver_in_mem_test.go b/pkg/solana/headtracker/head_saver_in_mem_test.go new file mode 100644 index 000000000..98cdd8ae3 --- /dev/null +++ b/pkg/solana/headtracker/head_saver_in_mem_test.go @@ -0,0 +1,242 @@ +package headtracker_test + +import ( + "sync" + "testing" + + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/stretchr/testify/require" +) + +func configureInMemorySaver(t *testing.T) *headtracker.HeadSaver { + htCfg := headtracker.NewConfig() + lggr, _ := logger.New() + return headtracker.NewSaver(htCfg, lggr) +} + +func TestInMemoryHeadSaver_Save(t *testing.T) { + t.Parallel() + saver := configureInMemorySaver(t) + + t.Run("happy path, saving heads", func(t *testing.T) { + head := cltest.Head(1) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + latest := saver.LatestChain() + require.NoError(t, err) + require.Equal(t, int64(1), latest.BlockNumber()) + + latest = saver.LatestChain() + require.NotNil(t, latest) + require.Equal(t, int64(1), latest.BlockNumber()) + + latest = saver.Chain(head.BlockHash()) + require.NotNil(t, latest) + require.Equal(t, int64(1), latest.BlockNumber()) + + // Add more heads + head = cltest.Head(2) + err = saver.Save(testutils.Context(t), head) + require.NoError(t, err) + head = cltest.Head(3) + err = saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + latest = saver.LatestChain() + require.Equal(t, int64(3), latest.BlockNumber()) + + // Check total number of heads + require.Equal(t, 3, len(saver.Heads)) + }) + + t.Run("save invalid head", func(t *testing.T) { + err := saver.Save(testutils.Context(t), nil) + require.Error(t, err) + }) + + t.Run("saving heads with same block number", func(t *testing.T) { + head := cltest.Head(4) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + head = cltest.Head(4) + err = saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + head = cltest.Head(4) + err = saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + latest := saver.LatestChain() + require.NoError(t, err) + require.Equal(t, int64(4), latest.BlockNumber()) + + headsWithSameNumber := len(saver.HeadByNumber(4)) + require.Equal(t, 3, headsWithSameNumber) + }) + t.Run("concurrent calls to Save", func(t *testing.T) { + var wg sync.WaitGroup + numRoutines := 10 + wg.Add(numRoutines) + + for i := 1; i <= numRoutines; i++ { + go func(num int) { + defer wg.Done() + head := cltest.Head(num) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + }(i) + } + + wg.Wait() + + latest := saver.LatestChain() + require.Equal(t, int64(numRoutines), latest.BlockNumber()) + }) +} + +func TestInMemoryHeadSaver_TrimOldHeads(t *testing.T) { + t.Parallel() + saver := configureInMemorySaver(t) + + t.Run("happy path, trimming old heads", func(t *testing.T) { + // Save heads with block numbers 1, 2, 3, and 4 + for i := 1; i <= 4; i++ { + head := cltest.Head(i) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + } + + require.Equal(t, 4, len(saver.Heads)) + + // Trim old heads, keeping only the last 3 blocks + saver.TrimOldHeads(3) + + // Check that the correct heads remain + require.Equal(t, 3, len(saver.Heads)) + require.Equal(t, 1, len(saver.HeadByNumber(3))) + require.Equal(t, 1, len(saver.HeadByNumber(4))) + require.Equal(t, 0, len(saver.HeadByNumber(1))) + + // Check that the latest head is correct + latest := saver.LatestChain() + require.Equal(t, int64(4), latest.BlockNumber()) + + // Clear All Heads + saver.TrimOldHeads(0) + require.Equal(t, 0, len(saver.Heads)) + require.Equal(t, 0, len(saver.HeadsNumber)) + }) + + t.Run("error path, block number lower than highest chain", func(t *testing.T) { + for i := 1; i <= 4; i++ { + head := cltest.Head(i) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + } + + saver.TrimOldHeads(4) + + // Check that no heads are removed + require.Equal(t, 4, len(saver.Heads)) + require.Equal(t, 4, len(saver.HeadsNumber)) + + latest := saver.LatestChain() + require.Equal(t, int64(4), latest.BlockNumber()) + }) + + t.Run("concurrent calls to TrimOldHeads", func(t *testing.T) { + // Save heads with block numbers 1, 2, 3, and 4 + for i := 1; i <= 4; i++ { + head := cltest.Head(i) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + } + + // Concurrently add multiple heads with different block numbers + var wg sync.WaitGroup + wg.Add(4) + for i := 5; i <= 8; i++ { + go func(num int) { + defer wg.Done() + head := cltest.Head(num) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + }(i) + } + wg.Wait() + + // Concurrently trim old heads of depth 3 + wg.Add(1) + go func() { + defer wg.Done() + saver.TrimOldHeads(3) + }() + wg.Wait() + + // Check that the correct heads remain after concurrent calls to TrimOldHeads + require.Equal(t, 3, len(saver.Heads)) + require.Equal(t, 1, len(saver.HeadByNumber(7))) + require.Equal(t, 1, len(saver.HeadByNumber(8))) + require.Equal(t, 0, len(saver.HeadByNumber(1))) + + latest := saver.LatestChain() + require.Equal(t, int64(8), latest.BlockNumber()) + }) +} + +func TestInMemoryHeadSaver_Chain(t *testing.T) { + t.Parallel() + saver := configureInMemorySaver(t) + + t.Run("happy path, valid block hash", func(t *testing.T) { + head1 := cltest.Head(1) + head2 := cltest.Head(2) + err := saver.Save(testutils.Context(t), head1) + require.NoError(t, err) + err = saver.Save(testutils.Context(t), head2) + require.NoError(t, err) + + retrievedHead1 := saver.Chain(head1.BlockHash()) + retrievedHead2 := saver.Chain(head2.BlockHash()) + + require.Equal(t, head1, retrievedHead1) + require.Equal(t, head2, retrievedHead2) + + }) + + t.Run("invalid block hash", func(t *testing.T) { + head1 := cltest.Head(1) + err := saver.Save(testutils.Context(t), head1) + require.NoError(t, err) + head2 := cltest.Head(2) + err = saver.Save(testutils.Context(t), head2) + require.NoError(t, err) + + saver.TrimOldHeads(1) + + invalidBlockHash := head1.BlockHash() + retrievedHead := saver.Chain(invalidBlockHash) + + require.Nil(t, retrievedHead) + }) +} + +func TestInMemoryHeadSaver_LatestChain(t *testing.T) { + t.Parallel() + saver := configureInMemorySaver(t) + + t.Run("happy path", func(t *testing.T) { + // Save a valid head + head := cltest.Head(1) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + latest := saver.LatestChain() + require.Equal(t, int64(1), latest.BlockNumber()) + }) +} diff --git a/pkg/solana/headtracker/head_tracker.go b/pkg/solana/headtracker/head_tracker.go new file mode 100644 index 000000000..86e5e2172 --- /dev/null +++ b/pkg/solana/headtracker/head_tracker.go @@ -0,0 +1,33 @@ +package headtracker + +import ( + "github.com/smartcontractkit/chainlink-relay/pkg/headtracker" + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-relay/pkg/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +type headTracker = headtracker.HeadTracker[*types.Head, commontypes.Subscription, types.ChainID, types.Hash] + +var _ commontypes.HeadTracker[*types.Head, types.Hash] = (*headTracker)(nil) + +func NewTracker( + lggr logger.Logger, + solanaClient htrktypes.Client[*types.Head, commontypes.Subscription, types.ChainID, types.Hash], + config htrktypes.Config, + headBroadcaster commontypes.HeadBroadcaster[*types.Head, types.Hash], + headSaver commontypes.HeadSaver[*types.Head, types.Hash], + mailMon *utils.MailboxMonitor, +) commontypes.HeadTracker[*types.Head, types.Hash] { + return headtracker.NewHeadTracker( + lggr, + solanaClient, + config, + headBroadcaster, + headSaver, + mailMon, + func() *types.Head { return nil }, + ) +} diff --git a/pkg/solana/headtracker/head_tracker_test.go b/pkg/solana/headtracker/head_tracker_test.go new file mode 100644 index 000000000..b6769185e --- /dev/null +++ b/pkg/solana/headtracker/head_tracker_test.go @@ -0,0 +1,1060 @@ +package headtracker_test + +import ( + "context" + "errors" + "math" + "math/big" + "sync" + "testing" + "time" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" + + "github.com/onsi/gomega" + + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + commonmocks "github.com/smartcontractkit/chainlink-relay/pkg/types/mocks" + "github.com/smartcontractkit/chainlink-relay/pkg/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" + + "github.com/stretchr/testify/mock" + "github.com/test-go/testify/assert" + "github.com/test-go/testify/require" +) + +// Why do we need this? +// Allow us to retreive the earliest head in our HeadSaver +func firstHead( + t *testing.T, + hs *headtracker.InMemoryHeadSaver[ + *types.Head, + types.Hash, + types.ChainID, + ]) (h *types.Head) { + + // Get all the Heads in the HeadSaver and find the one with lowest block number + // Iterate over HeadsNumber + // HeadsNumber is a map[int64][]H + lowestBlockNumber := int64(math.MaxInt64) + for blockNumber := range hs.HeadsNumber { + if blockNumber < lowestBlockNumber { + lowestBlockNumber = blockNumber + } + } + + return hs.HeadsNumber[lowestBlockNumber][0] +} + +func TestHeadTracker_New(t *testing.T) { + t.Parallel() + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + client := cltest.NewClientMockWithDefaultChain(t) + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) + headSaver := headtracker.NewSaver(cfg, lggr) + assert.Nil(t, headSaver.Save(testutils.Context(t), cltest.Head(1))) + last := cltest.Head(16) + assert.Nil(t, headSaver.Save(testutils.Context(t), last)) + assert.Nil(t, headSaver.Save(testutils.Context(t), cltest.Head(10))) + + ht := createHeadTracker(t, cfg, client, headSaver) + ht.Start(t) + + latest := ht.headSaver.LatestChain() + require.NotNil(t, latest) + assert.Equal(t, last.BlockNumber(), latest.BlockNumber()) +} + +// // The function `TestHeadTracker_Save_InsertsAndTrimsTable` tests the functionality of inserting and +// // trimming a table in the head tracker. +func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { + t.Parallel() + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + client := cltest.NewClientMockWithDefaultChain(t) + + // Generate 200 consecutive heads + for idx := 0; idx < 200; idx++ { + idxHead := cltest.Head(idx) + parentHead := headSaver.LatestChain() + + if parentHead != nil { + idxHead.Parent = parentHead + } + parentHead = idxHead + assert.Nil(t, headSaver.Save(testutils.Context(t), idxHead)) + } + + ht := createHeadTracker(t, headtracker.NewConfig(), client, headSaver) + + h := cltest.Head(200) + h.Parent = headSaver.LatestChain() + require.NoError(t, ht.headSaver.Save(testutils.Context(t), h)) + assert.Equal(t, int64(200), ht.headSaver.LatestChain().BlockNumber()) + + firstHead := firstHead(t, headSaver) + + assert.Equal(t, int64(100), firstHead.BlockNumber()) + + lastHead := headSaver.LatestChain() + assert.Equal(t, int64(200), lastHead.BlockNumber()) +} + +func TestHeadTracker_Get(t *testing.T) { + t.Parallel() + + start := cltest.Head(5) + + tests := []struct { + name string + initial *types.Head + toSave *types.Head + want int64 + }{ + {"greater", start, cltest.Head(6), int64(6)}, + {"less than", start, cltest.Head(1), int64(5)}, + {"zero", start, cltest.Head(0), int64(5)}, + {"nil", start, nil, int64(5)}, + {"nil no initial", nil, nil, int64(0)}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chStarted := make(chan struct{}) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Maybe(). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + defer close(chStarted) + return mockChain.NewSub(t) + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) + + fnCall := client.On("HeadByNumber", mock.Anything, mock.Anything) + fnCall.RunFn = func(args mock.Arguments) { + num := args.Get(1).(*big.Int) + fnCall.ReturnArguments = mock.Arguments{cltest.Head(num), nil} + } + + if test.initial != nil { + assert.Nil(t, headSaver.Save(testutils.Context(t), test.initial)) + } + + ht := createHeadTracker(t, cfg, client, headSaver) + ht.Start(t) + + if test.toSave != nil { + err := ht.headSaver.Save(testutils.Context(t), test.toSave) + assert.NoError(t, err) + } + + // Check if that is the correct head that we want + assert.Equal(t, test.want, ht.headSaver.LatestChain().BlockNumber()) + }) + } +} + +func TestHeadTracker_Start_NewHeads(t *testing.T) { + t.Parallel() + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + chStarted := make(chan struct{}) + mockChain := &testutils.MockChain{Client: client} + + sub := mockChain.NewSub(t) + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Run(func(mock.Arguments) { + close(chStarted) + }). + Return(sub, nil) + + ht := createHeadTracker(t, cfg, client, headSaver) + ht.Start(t) + + <-chStarted +} + +func TestHeadTracker_Start_CancelContext(t *testing.T) { + t.Parallel() + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + chStarted := make(chan struct{}) + mockChain := &testutils.MockChain{Client: client} + + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Run(func(args mock.Arguments) { + ctx := args.Get(0).(context.Context) + select { + case <-ctx.Done(): + return + case <-time.After(10 * time.Second): + assert.FailNow(t, "context was not cancelled within 10s") + } + }).Return(cltest.Head(0), nil) + sub := mockChain.NewSub(t) + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Run(func(mock.Arguments) { + close(chStarted) + }). + Return(sub, nil). + Maybe() + + ht := createHeadTracker(t, cfg, client, headSaver) + ctx, cancel := context.WithCancel(testutils.Context(t)) + go func() { + time.Sleep(1 * time.Second) + cancel() + }() + err := ht.headTracker.Start(ctx) + require.NoError(t, err) + require.NoError(t, ht.headTracker.Close()) +} + +func TestHeadTracker_CallsHeadTrackableCallbacks(t *testing.T) { + t.Parallel() + g := gomega.NewWithT(t) + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chchHeaders := make(chan testutils.RawSub[*types.Head], 1) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + sub := mockChain.NewSub(t) + chchHeaders <- testutils.NewRawSub(ch, sub.Err()) + return sub + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + client.On("HeadByNumber", mock.Anything, mock.Anything).Return(cltest.Head(0), nil) + + checker := &cltest.MockHeadTrackable{} + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) + ht.Start(t) + assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) + + headers := <-chchHeaders + headers.TrySend(cltest.Head(1)) + + g.Eventually(checker.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) + + ht.Stop(t) + assert.Equal(t, int32(1), checker.OnNewLongestChainCount()) +} + +func TestHeadTracker_ReconnectOnError(t *testing.T) { + t.Parallel() + g := gomega.NewWithT(t) + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { return mockChain.NewSub(t) }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + client.On("SubscribeNewHead", mock.Anything, mock.Anything).Return(nil, errors.New("cannot reconnect")) + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { return mockChain.NewSub(t) }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) + + checker := &cltest.MockHeadTrackable{} + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) + + // connect + ht.Start(t) + assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) + + // trigger reconnect loop + mockChain.SubsErr(errors.New("test error to force reconnect")) + g.SetDefaultEventuallyTimeout(2 * time.Second) + g.Eventually(checker.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) +} + +func TestHeadTracker_ResubscribeOnSubscriptionError(t *testing.T) { + t.Parallel() + g := gomega.NewWithT(t) + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chchHeaders := make(chan testutils.RawSub[*types.Head], 1) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + sub := mockChain.NewSub(t) + chchHeaders <- testutils.NewRawSub(ch, sub.Err()) + return sub + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + client.On("HeadByNumber", mock.Anything, mock.Anything).Return(cltest.Head(0), nil) + + checker := &cltest.MockHeadTrackable{} + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) + + ht.Start(t) + assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) + + headers := <-chchHeaders + go func() { + headers.TrySend(cltest.Head(1)) + }() + + g.Eventually(func() bool { + report := ht.headTracker.HealthReport() + return !slices.ContainsFunc(maps.Values(report), func(e error) bool { return e != nil }) + }, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(true)) + + // trigger reconnect loop + headers.CloseCh() + + // wait for full disconnect and a new subscription + g.Eventually(checker.OnNewLongestChainCount, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(int32(1))) +} + +func TestHeadTracker_Start_LoadsLatestChain(t *testing.T) { + t.Parallel() + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chchHeaders := make(chan testutils.RawSub[*types.Head], 1) + + heads := []*types.Head{ + cltest.Head(0), + cltest.Head(1), + cltest.Head(2), + cltest.Head(3), + } + + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(heads[3], nil).Maybe() + client.On("HeadByNumber", mock.Anything, big.NewInt(2)).Return(heads[2], nil).Maybe() + client.On("HeadByNumber", mock.Anything, big.NewInt(1)).Return(heads[1], nil).Maybe() + client.On("HeadByNumber", mock.Anything, big.NewInt(0)).Return(heads[0], nil).Maybe() + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + sub := mockChain.NewSub(t) + chchHeaders <- testutils.NewRawSub(ch, sub.Err()) + return sub + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + + trackable := &cltest.MockHeadTrackable{} + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, trackable) + + require.NoError(t, headSaver.Save(testutils.Context(t), heads[2])) + + ht.Start(t) + + assert.Equal(t, int32(0), trackable.OnNewLongestChainCount()) + + headers := <-chchHeaders + go func() { + headers.TrySend(cltest.Head(1)) + }() + + gomega.NewWithT(t).Eventually(func() bool { + report := ht.headTracker.HealthReport() + maps.Copy(report, ht.headBroadcaster.HealthReport()) + return !slices.ContainsFunc(maps.Values(report), func(e error) bool { return e != nil }) + }, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(true)) + + h := headSaver.LatestChain() + require.NotNil(t, h) + assert.Equal(t, h.BlockNumber(), int64(3)) +} + +func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) { + t.Parallel() + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chchHeaders := make(chan testutils.RawSub[*types.Head], 1) + checker := commonmocks.NewHeadTrackable[*types.Head, types.Hash](t) + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) + + cfg.SetHeadTrackerSamplingInterval(2500 * time.Millisecond) + cfg.SetHeadTrackerMaxBufferSize(100) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + sub := mockChain.NewSub(t) + chchHeaders <- testutils.NewRawSub(ch, sub.Err()) + return sub + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + + // --------------------- + blocks := cltest.NewBlocks(t, 10) + + head0 := blocks.Head(0) + // Initial query + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) + ht.Start(t) + + headSeq := cltest.NewHeadBuffer(t) + headSeq.Append(blocks.Head(0)) + headSeq.Append(blocks.Head(1)) + + // Blocks 2 and 3 are out of order + headSeq.Append(blocks.Head(3)) + headSeq.Append(blocks.Head(2)) + + // Block 4 comes in + headSeq.Append(blocks.Head(4)) + + // Another block at level 4 comes in, that will be uncled + headSeq.Append(blocks.NewHead(4)) + + // Reorg happened forking from block 2 + blocksForked := blocks.ForkAt(t, 2, 5) + headSeq.Append(blocksForked.Head(2)) + headSeq.Append(blocksForked.Head(3)) + headSeq.Append(blocksForked.Head(4)) + headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer + + lastLongestChainAwaiter := cltest.NewAwaiter() + + // the callback is only called for head number 5 because of head sampling + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + + assert.Equal(t, int64(5), h.BlockNumber()) + assert.Equal(t, blocksForked.Head(5).BlockHash(), h.BlockHash()) + + // This is the new longest chain, check that it came with its parents + if !assert.NotNil(t, h.Parent) { + return + } + assert.Equal(t, h.Parent.BlockHash(), blocksForked.Head(4).BlockHash()) + if !assert.NotNil(t, h.Parent.Parent) { + return + } + assert.Equal(t, h.Parent.Parent.BlockHash(), blocksForked.Head(3).BlockHash()) + if !assert.NotNil(t, h.Parent.Parent.Parent) { + return + } + assert.Equal(t, h.Parent.Parent.Parent.BlockHash(), blocksForked.Head(2).BlockHash()) + if !assert.NotNil(t, h.Parent.Parent.Parent.Parent) { + return + } + + assert.Equal(t, blocksForked.Head(1).BlockHash(), h.Parent.Parent.Parent.Parent.BlockHash()) + lastLongestChainAwaiter.ItHappened() + }).Return().Once() + + headers := <-chchHeaders + + // This grotesque construction is the only way to do dynamic return values using + // the mock package. We need dynamic returns because we're simulating reorgs. + latestHeadByNumber := make(map[int64]*types.Head) + latestHeadByNumberMu := new(sync.Mutex) + + fnCall := client.On("HeadByNumber", mock.Anything, mock.Anything) + fnCall.RunFn = func(args mock.Arguments) { + latestHeadByNumberMu.Lock() + defer latestHeadByNumberMu.Unlock() + num := args.Get(1).(*big.Int) + head, exists := latestHeadByNumber[num.Int64()] + if !exists { + head = cltest.Head(num.Int64()) + latestHeadByNumber[num.Int64()] = head + } + fnCall.ReturnArguments = mock.Arguments{head, nil} + } + + for _, h := range headSeq.Heads { + latestHeadByNumberMu.Lock() + latestHeadByNumber[h.BlockNumber()] = h + latestHeadByNumberMu.Unlock() + headers.TrySend(h) + } + + // default 10s may not be sufficient, so using testutils.WaitTimeout(t) + lastLongestChainAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + ht.Stop(t) + assert.Equal(t, int64(5), ht.headSaver.LatestChain().BlockNumber()) + + for _, h := range headSeq.Heads { + c := ht.headSaver.Chain(h.BlockHash()) + require.NotNil(t, c) + assert.Equal(t, c.GetParentHash(), h.GetParentHash()) + assert.Equal(t, c.BlockNumber(), h.BlockNumber()) + } +} + +func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingDisabled(t *testing.T) { + t.Parallel() + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chchHeaders := make(chan testutils.RawSub[*types.Head], 1) + checker := commonmocks.NewHeadTrackable[*types.Head, types.Hash](t) + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) + cfg.SetHeadTrackerSamplingInterval(0) + cfg.SetHeadTrackerMaxBufferSize(100) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + sub := mockChain.NewSub(t) + chchHeaders <- testutils.NewRawSub(ch, sub.Err()) + return sub + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + + // --------------------- + blocks := cltest.NewBlocks(t, 10) + + head0 := blocks.Head(0) + // Initial query + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) + + headSeq := cltest.NewHeadBuffer(t) + headSeq.Append(blocks.Head(0)) + headSeq.Append(blocks.Head(1)) + + // Blocks 2 and 3 are out of order + headSeq.Append(blocks.Head(3)) + headSeq.Append(blocks.Head(2)) + + // Block 4 comes in + headSeq.Append(blocks.Head(4)) + + // Another block at level 4 comes in, that will be uncled + headSeq.Append(blocks.NewHead(4)) + + // Reorg happened forking from block 2 + blocksForked := blocks.ForkAt(t, 2, 5) + headSeq.Append(blocksForked.Head(2)) + headSeq.Append(blocksForked.Head(3)) + headSeq.Append(blocksForked.Head(4)) + headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer + + lastLongestChainAwaiter := cltest.NewAwaiter() + + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + require.Equal(t, int64(0), h.BlockNumber()) + require.Equal(t, blocks.Head(0).BlockHash(), h.BlockHash()) + }).Return().Once() + + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + require.Equal(t, int64(1), h.BlockNumber()) + require.Equal(t, blocks.Head(1).BlockHash(), h.BlockHash()) + }).Return().Once() + + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + require.Equal(t, int64(3), h.BlockNumber()) + require.Equal(t, blocks.Head(3).BlockHash(), h.BlockHash()) + }).Return().Once() + + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + require.Equal(t, int64(4), h.BlockNumber()) + require.Equal(t, blocks.Head(4).BlockHash(), h.BlockHash()) + + // Check that the block came with its parents + require.NotNil(t, h.Parent) + require.Equal(t, h.Parent.BlockHash(), blocks.Head(3).BlockHash()) + require.NotNil(t, h.Parent.Parent.BlockHash()) // 2 + require.Equal(t, h.Parent.Parent.BlockHash(), blocks.Head(2).BlockHash()) + require.NotNil(t, h.Parent.Parent.Parent) + require.Equal(t, h.Parent.Parent.Parent.BlockHash(), blocks.Head(1).BlockHash()) + }).Return().Once() + + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + + require.Equal(t, int64(5), h.BlockNumber()) + require.Equal(t, blocksForked.Head(5).BlockHash(), h.BlockHash()) + + // This is the new longest chain, check that it came with its parents + require.NotNil(t, h.Parent) + require.Equal(t, h.Parent.BlockHash(), blocksForked.Head(4).BlockHash()) + require.NotNil(t, h.Parent.Parent) + require.Equal(t, h.Parent.Parent.BlockHash(), blocksForked.Head(3).BlockHash()) + require.NotNil(t, h.Parent.Parent.Parent) + require.Equal(t, h.Parent.Parent.Parent.BlockHash(), blocksForked.Head(2).BlockHash()) + require.NotNil(t, h.Parent.Parent.Parent.Parent) + require.Equal(t, h.Parent.Parent.Parent.Parent.BlockHash(), blocksForked.Head(1).BlockHash()) + lastLongestChainAwaiter.ItHappened() + }).Return().Once() + + ht.Start(t) + + headers := <-chchHeaders + + // This grotesque construction is the only way to do dynamic return values using + // the mock package. We need dynamic returns because we're simulating reorgs. + latestHeadByNumber := make(map[int64]*types.Head) + latestHeadByNumberMu := new(sync.Mutex) + + fnCall := client.On("HeadByNumber", mock.Anything, mock.Anything) + fnCall.RunFn = func(args mock.Arguments) { + latestHeadByNumberMu.Lock() + defer latestHeadByNumberMu.Unlock() + num := args.Get(1).(*big.Int) + head, exists := latestHeadByNumber[num.Int64()] + if !exists { + head = cltest.Head(num) + latestHeadByNumber[num.Int64()] = head + } + fnCall.ReturnArguments = mock.Arguments{head, nil} + } + + for _, h := range headSeq.Heads { + latestHeadByNumberMu.Lock() + latestHeadByNumber[h.BlockNumber()] = h + latestHeadByNumberMu.Unlock() + headers.TrySend(h) + time.Sleep(testutils.TestInterval) + } + + // default 10s may not be sufficient, so using testutils.WaitTimeout(t) + lastLongestChainAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + ht.Stop(t) + assert.Equal(t, int64(5), ht.headSaver.LatestChain().BlockNumber()) + + for _, h := range headSeq.Heads { + c := ht.headSaver.Chain(h.BlockHash()) + require.NotNil(t, c) + assert.Equal(t, c.GetParentHash(), h.GetParentHash()) + assert.Equal(t, c.BlockNumber(), h.BlockNumber()) + } +} + +func TestHeadTracker_Backfill(t *testing.T) { + t.Parallel() + + // Heads are arranged as follows: + // headN indicates an unpersisted ethereum header + // hN indicates a persisted head record + // + // (1)->(H0) + // + // (14Orphaned)-+ + // +->(13)->(12)->(11)->(H10)->(9)->(H8) + // (15)->(14)---------+ + + head0 := cltest.Head(0) + + h1 := cltest.Head(1) + h1.Block.PreviousBlockhash = head0.BlockHash().Hash + + h8 := cltest.Head(8) + + h9 := cltest.Head(9) + h9.Block.PreviousBlockhash = h8.BlockHash().Hash + + h10 := cltest.Head(10) + h10.Block.PreviousBlockhash = h9.BlockHash().Hash + + h11 := cltest.Head(11) + h11.Block.PreviousBlockhash = h10.BlockHash().Hash + + h12 := cltest.Head(12) + h12.Block.PreviousBlockhash = h11.BlockHash().Hash + + h13 := cltest.Head(13) + h13.Block.PreviousBlockhash = h12.BlockHash().Hash + + h14Orphaned := cltest.Head(14) + h14Orphaned.Block.PreviousBlockhash = h13.BlockHash().Hash + + h14 := cltest.Head(14) + h14.Block.PreviousBlockhash = h13.BlockHash().Hash + + h15 := cltest.Head(15) + h15.Block.PreviousBlockhash = h14.BlockHash().Hash + + heads := []types.Head{ + *h9, + *h11, + *h12, + *h13, + *h14Orphaned, + *h14, + *h15, + } + + ctx := testutils.Context(t) + + t.Run("does nothing if all the heads are in headsaver", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + err := ht.Backfill(ctx, h12, 2) + require.NoError(t, err) + }) + + t.Run("fetches a missing head", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + client.On("HeadByNumber", mock.Anything, big.NewInt(10)). + Return(h10, nil) + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + var depth uint = 3 + + // Should backfill h10 + err := ht.Backfill(ctx, h12, depth) + require.NoError(t, err) + + h := ht.headSaver.Chain(h12.BlockHash()) + + assert.Equal(t, int64(12), h.BlockNumber()) + require.NotNil(t, h.Parent) + assert.Equal(t, int64(11), h.Parent.BlockNumber()) + require.NotNil(t, h.Parent.Parent) + assert.Equal(t, int64(10), h.Parent.Parent.BlockNumber()) + require.NotNil(t, h.Parent.Parent.Parent) + assert.Equal(t, int64(9), h.Parent.Parent.Parent.BlockNumber()) + + writtenHead, err := headSaver.HeadByHash(h10.BlockHash()) + require.NoError(t, err) + assert.Equal(t, int64(10), writtenHead.BlockNumber()) + }) + + t.Run("fetches only heads that are missing", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + client.On("HeadByNumber", mock.Anything, big.NewInt(10)). + Return(h10, nil) + client.On("HeadByNumber", mock.Anything, big.NewInt(8)). + Return(h8, nil) + + // Needs to be 8 because there are 8 heads in chain (15,14,13,12,11,10,9,8) + var depth uint = 8 + + err := ht.Backfill(ctx, h15, depth) + require.NoError(t, err) + + h := ht.headSaver.Chain(h15.BlockHash()) + + require.Equal(t, uint32(8), h.ChainLength()) + earliestInChain := h.EarliestHeadInChain() + assert.Equal(t, h8.BlockNumber(), earliestInChain.BlockNumber()) + assert.Equal(t, h8.BlockHash(), earliestInChain.BlockHash()) + }) + + t.Run("does not backfill if chain length is already greater than or equal to depth", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + err := ht.Backfill(ctx, h15, 3) + require.NoError(t, err) + + err = ht.Backfill(ctx, h15, 5) + require.NoError(t, err) + }) + + t.Run("only backfills to height 0 if chain length would otherwise cause it to try and fetch a negative head", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + client.On("HeadByNumber", mock.Anything, big.NewInt(0)). + Return(head0, nil) + + require.NoError(t, headSaver.Save(testutils.Context(t), h1)) + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + err := ht.Backfill(ctx, h1, 400) + require.NoError(t, err) + + h := ht.headSaver.Chain(h1.BlockHash()) + require.NotNil(t, h) + + require.Equal(t, uint32(2), h.ChainLength()) + require.Equal(t, int64(0), h.EarliestHeadInChain().BlockNumber()) + }) + + t.Run("abandons backfill and returns error if the eth node returns not found", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + + client.On("HeadByNumber", mock.Anything, big.NewInt(10)). + Return(h10, nil). + Once() + client.On("HeadByNumber", mock.Anything, big.NewInt(8)). + Return(cltest.Head(0), errors.New("not found")). + Once() + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + err := ht.Backfill(ctx, h12, 400) + require.Error(t, err) + require.EqualError(t, err, "fetchAndSaveHead failed: not found") + + h := ht.headSaver.Chain(h12.BlockHash()) + + // Should contain 12, 11, 10, 9 + assert.Equal(t, 4, int(h.ChainLength())) + assert.Equal(t, int64(9), h.EarliestHeadInChain().BlockNumber()) + }) + + t.Run("abandons backfill and returns error if the context time budget is exceeded", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + client.On("HeadByNumber", mock.Anything, big.NewInt(10)). + Return(h10, nil) + client.On("HeadByNumber", mock.Anything, big.NewInt(8)). + Return(cltest.Head(0), context.DeadlineExceeded) + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + err := ht.Backfill(ctx, h12, 400) + require.Error(t, err) + require.EqualError(t, err, "fetchAndSaveHead failed: context deadline exceeded") + + h := ht.headSaver.Chain(h12.BlockHash()) + + // Should contain 12, 11, 10, 9 + assert.Equal(t, 4, int(h.ChainLength())) + assert.Equal(t, int64(9), h.EarliestHeadInChain().BlockNumber()) + }) +} + +// Helper Functions + +func createHeadTracker( + t *testing.T, + config *headtracker.Config, + solanaClient htrktypes.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash], + hs *headtracker.InMemoryHeadSaver[ + *types.Head, + types.Hash, + types.ChainID], +) *headTrackerUniverse { + lggr, _ := logger.New() + hb := headtracker.NewBroadcaster(lggr) + mailMon := utils.NewMailboxMonitor(t.Name()) + ht := headtracker.NewTracker(lggr, solanaClient, config, hb, hs, mailMon) + return &headTrackerUniverse{ + mu: new(sync.Mutex), + headTracker: ht, + headBroadcaster: hb, + headSaver: hs, + mailMon: mailMon, + } +} + +func createHeadTrackerWithNeverSleeper(t *testing.T, + config *headtracker.Config, + solanaClient htrktypes.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash], + hs *headtracker.InMemoryHeadSaver[ + *types.Head, + types.Hash, + types.ChainID]) *headTrackerUniverse { + lggr, _ := logger.New() + hb := headtracker.NewBroadcaster(lggr) + mailMon := utils.NewMailboxMonitor(t.Name()) + ht := headtracker.NewTracker(lggr, solanaClient, config, hb, hs, mailMon) + return &headTrackerUniverse{ + mu: new(sync.Mutex), + headTracker: ht, + headBroadcaster: hb, + headSaver: hs, + mailMon: mailMon, + } +} + +func createHeadTrackerWithChecker(t *testing.T, + config *headtracker.Config, + solanaClient htrktypes.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash], + hs *headtracker.InMemoryHeadSaver[ + *types.Head, + types.Hash, + types.ChainID], + checker commontypes.HeadTrackable[*types.Head, types.Hash], +) *headTrackerUniverse { + lggr, _ := logger.New() + hb := headtracker.NewBroadcaster(lggr) + + hb.Subscribe(checker) + mailMon := utils.NewMailboxMonitor(t.Name()) + ht := headtracker.NewTracker(lggr, solanaClient, config, hb, hs, mailMon) + return &headTrackerUniverse{ + mu: new(sync.Mutex), + headTracker: ht, + headBroadcaster: hb, + headSaver: hs, + mailMon: mailMon, + } +} + +type headTrackerUniverse struct { + mu *sync.Mutex + stopped bool + headTracker commontypes.HeadTracker[*types.Head, types.Hash] + headBroadcaster commontypes.HeadBroadcaster[*types.Head, types.Hash] + headSaver commontypes.HeadSaver[*types.Head, types.Hash] + mailMon *utils.MailboxMonitor +} + +func (u *headTrackerUniverse) Backfill(ctx context.Context, head *types.Head, depth uint) error { + return u.headTracker.Backfill(ctx, head, depth) +} + +func (u *headTrackerUniverse) Start(t *testing.T) { + u.mu.Lock() + defer u.mu.Unlock() + ctx := testutils.Context(t) + require.NoError(t, u.headBroadcaster.Start(ctx)) + require.NoError(t, u.headTracker.Start(ctx)) + require.NoError(t, u.mailMon.Start(ctx)) + + g := gomega.NewWithT(t) + g.Eventually(func() bool { + report := u.headBroadcaster.HealthReport() + return !slices.ContainsFunc(maps.Values(report), func(e error) bool { return e != nil }) + }, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(true)) + + t.Cleanup(func() { + u.Stop(t) + }) +} + +func (u *headTrackerUniverse) Stop(t *testing.T) { + u.mu.Lock() + defer u.mu.Unlock() + if u.stopped { + return + } + u.stopped = true + require.NoError(t, u.headBroadcaster.Close()) + require.NoError(t, u.headTracker.Close()) + require.NoError(t, u.mailMon.Close()) +} + +func ptr[T any](t T) *T { return &t } diff --git a/pkg/solana/headtracker/types/chain.go b/pkg/solana/headtracker/types/chain.go new file mode 100644 index 000000000..998fc6b3f --- /dev/null +++ b/pkg/solana/headtracker/types/chain.go @@ -0,0 +1,42 @@ +package types + +type ChainID int + +const ( + Mainnet ChainID = iota + Testnet + Devnet + Localnet + Unknown +) + +// String returns the string representation of the Network value. +func (id ChainID) String() string { + switch id { + case Mainnet: + return "mainnet" + case Testnet: + return "testnet" + case Devnet: + return "devnet" + case Localnet: + return "localnet" + default: + return "unknown" + } +} + +func StringToChainID(id string) ChainID { + switch id { + case "mainnet": + return Mainnet + case "testnet": + return Testnet + case "devnet": + return Devnet + case "localnet": + return Localnet + default: + return Unknown + } +} diff --git a/pkg/solana/headtracker/types/hash.go b/pkg/solana/headtracker/types/hash.go new file mode 100644 index 000000000..152cea45c --- /dev/null +++ b/pkg/solana/headtracker/types/hash.go @@ -0,0 +1,13 @@ +package types + +import ( + "github.com/gagliardetto/solana-go" +) + +type Hash struct { + solana.Hash +} + +func (h Hash) Bytes() []byte { + return h.Hash[:] +} diff --git a/pkg/solana/headtracker/types/hash_test.go b/pkg/solana/headtracker/types/hash_test.go new file mode 100644 index 000000000..c619633fd --- /dev/null +++ b/pkg/solana/headtracker/types/hash_test.go @@ -0,0 +1,26 @@ +package types + +import ( + "bytes" + "testing" + + "github.com/gagliardetto/solana-go" +) + +func TestHash_Bytes(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + // Create a solana.Hash with 32 bytes. + expectedBytes := []byte("abcdefghabcdefghabcdefghabcdefgh") + var solanaHash solana.Hash + copy(solanaHash[:], expectedBytes) + + // Create a Hash instance with the solana.Hash we just created. + testHash := Hash{solanaHash} + actualBytes := testHash.Bytes() + + // Check that the bytes returned by the method match the bytes we put into the solana.Hash. + if !bytes.Equal(actualBytes, expectedBytes) { + t.Errorf("Bytes() returned %v, want %v", actualBytes, expectedBytes) + } + }) +} diff --git a/pkg/solana/headtracker/types/head.go b/pkg/solana/headtracker/types/head.go new file mode 100644 index 000000000..5e629b4ae --- /dev/null +++ b/pkg/solana/headtracker/types/head.go @@ -0,0 +1,112 @@ +package types + +import ( + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" +) + +var _ commontypes.Head[Hash] = (*Head)(nil) +var _ htrktypes.Head[Hash, ChainID] = (*Head)(nil) + +type Head struct { + Slot int64 + Block rpc.GetBlockResult + Parent *Head + ID ChainID +} + +// NewHead returns an instance of Head +func NewHead(slot int64, block rpc.GetBlockResult, parent *Head, id ChainID) *Head { + return &Head{ + Slot: slot, + Block: block, + Parent: parent, + ID: id, + } +} + +func (h *Head) BlockNumber() int64 { + return h.Slot +} + +// ChainLength returns the length of the chain followed by recursively looking up parents +func (h *Head) ChainLength() uint32 { + if h == nil { + return 0 + } + l := uint32(1) + + for { + if h.Parent != nil { + l++ + if h == h.Parent { + panic("circular reference detected") + } + h = h.Parent + } else { + break + } + } + return l +} + +func (h *Head) EarliestHeadInChain() commontypes.Head[Hash] { + return h.earliestInChain() +} + +func (h *Head) earliestInChain() *Head { + for h.Parent != nil { + h = h.Parent + } + return h +} + +func (h *Head) BlockHash() Hash { + return Hash{Hash: h.blockHash()} +} + +func (h *Head) blockHash() solana.Hash { + return h.Block.Blockhash +} + +func (h *Head) GetParent() commontypes.Head[Hash] { + if h.Parent == nil { + return nil + } + return h.Parent +} + +func (h *Head) GetParentHash() Hash { + return Hash{Hash: h.Block.PreviousBlockhash} +} + +func (h *Head) HashAtHeight(slotNum int64) Hash { + for { + if h.Slot == slotNum { + return h.BlockHash() + } + if h.Parent != nil { + h = h.Parent + } else { + break + } + } + return Hash{} +} + +func (h *Head) ChainID() ChainID { + return h.ID +} + +func (h *Head) HasChainID() bool { + if h == nil { + return false + } + return h.ChainID().String() != "unknown" +} + +func (h *Head) IsValid() bool { + return h != nil +} diff --git a/pkg/solana/headtracker/types/head_test.go b/pkg/solana/headtracker/types/head_test.go new file mode 100644 index 000000000..2efc6697c --- /dev/null +++ b/pkg/solana/headtracker/types/head_test.go @@ -0,0 +1,145 @@ +package types_test + +import ( + "strconv" + "testing" + + "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" + "github.com/stretchr/testify/assert" +) + +func TestHead_NewHead(t *testing.T) { + emptyBlockResult := cltest.ConfigureBlockResult() + t.Parallel() + + tests := []struct { + slot int64 + block rpc.GetBlockResult + parent *types.Head + id types.ChainID + wantSlot int64 + }{ + // with no parent + {10, emptyBlockResult, nil, types.Mainnet, 10}, + // with parent + {20, emptyBlockResult, + types.NewHead(10, emptyBlockResult, nil, types.Mainnet), + types.Mainnet, 20}, + {30, emptyBlockResult, + types.NewHead(20, emptyBlockResult, + types.NewHead(10, emptyBlockResult, nil, types.Mainnet), + types.Mainnet), + types.Mainnet, 30}, + } + + for _, test := range tests { + t.Run( + strconv.FormatInt(test.wantSlot, 10), // convert to base 10 + func(t *testing.T) { + head := types.NewHead(test.slot, test.block, test.parent, test.id) + assert.Equal(t, test.wantSlot, head.Slot) + assert.Equal(t, test.block, head.Block) + assert.Equal(t, test.parent, head.Parent) + assert.Equal(t, test.id, head.ID) + }) + } +} + +func TestHead_ChainLength(t *testing.T) { + blockResult := cltest.ConfigureBlockResult() + id := types.Mainnet + + head := types.NewHead(0, blockResult, + types.NewHead(0, blockResult, + types.NewHead(0, blockResult, nil, id), id), id) + + assert.Equal(t, uint32(3), head.ChainLength()) + + var head2 *types.Head + assert.Equal(t, uint32(0), head2.ChainLength()) +} + +func TestHead_EarliestHeadInChain(t *testing.T) { + blockResult := cltest.ConfigureBlockResult() + id := types.Mainnet + + head := types.NewHead(3, blockResult, + types.NewHead(2, blockResult, + types.NewHead(1, blockResult, nil, id), id), id) + + assert.Equal(t, int64(1), head.EarliestHeadInChain().BlockNumber()) +} + +func TestHead_GetParentHash(t *testing.T) { + id := types.Mainnet + + blockResult0 := cltest.ConfigureBlockResult() + h0 := types.NewHead(0, blockResult0, nil, id) + + blockResult1 := cltest.ConfigureBlockResult() + blockResult1.ParentSlot = 0 + blockResult1.PreviousBlockhash = blockResult0.Blockhash + h1 := types.NewHead(1, blockResult1, h0, id) + + blockResult2 := cltest.ConfigureBlockResult() + blockResult2.ParentSlot = 1 + blockResult2.PreviousBlockhash = blockResult1.Blockhash + h2 := types.NewHead(2, blockResult2, h1, id) + + blockResult3 := cltest.ConfigureBlockResult() + blockResult3.ParentSlot = 2 + blockResult3.PreviousBlockhash = blockResult2.Blockhash + h3 := types.NewHead(3, blockResult3, h2, id) + + // h3 -> h2 -> h1 -> h0 + assert.Equal(t, h2.BlockHash(), h3.GetParentHash()) + assert.Equal(t, h1.BlockHash(), h2.GetParentHash()) + assert.Equal(t, h0.BlockHash(), h1.GetParentHash()) +} + +func TestHead_GetParent(t *testing.T) { + blockResult := cltest.ConfigureBlockResult() + id := types.Mainnet + + head := types.NewHead(3, blockResult, + types.NewHead(2, blockResult, + types.NewHead(1, blockResult, nil, id), id), id) + + assert.Equal(t, head.Parent, head.GetParent()) +} + +func TestHead_HasChainID(t *testing.T) { + t.Parallel() + blockResult := cltest.ConfigureBlockResult() // Assuming this function creates a mock rpc.GetBlockResult + + tests := []struct { + name string + chainID types.ChainID + want bool + }{ + { + "HasChainID returns true when ChainID is not 'unknown'", + types.Devnet, // replace with correct initialization + true, + }, + { + "HasChainID returns false when ChainID is 'unknown'", + 99, + false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + head := types.NewHead(0, blockResult, nil, test.chainID) + assert.Equal(t, test.want, head.HasChainID()) + }) + } + + t.Run("HasChainID returns false when Head is nil", func(t *testing.T) { + var head *types.Head + assert.False(t, head.HasChainID()) + }) +}