From 323eb0c7dd7204521e662a3a355fe78a0e19c0be Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Tue, 29 Oct 2024 12:09:21 +0300 Subject: [PATCH 1/6] Use acp 118 handler (#1359) * Bump avalanchego to master * always sign uptime messages (testing branch) * nits * cleanup * assign to correct `err` * fix handler * move ValidatorUptime type to subnet-evm * disable always signing * implement on the type itself * remove unneeded code * fix ut * implement acp118 signer and verifier * avoid revalidating in sign * refactor warp backend to use acp118 handler * prune warp db before backend init * add cache tests * remove uptime msg type * add cache test * fix linter * add validator uptimes * use signature cache * bump avago version * nits * rename pkg imports * review fixes * reviews * bump avago --------- Co-authored-by: Darioush Jalali --- go.mod | 28 +-- go.sum | 61 +++-- plugin/evm/vm.go | 51 ++-- plugin/evm/vm_warp_test.go | 114 +++++++-- warp/backend.go | 135 ++++------- warp/backend_test.go | 72 ++---- warp/handlers/signature_request.go | 11 +- warp/handlers/signature_request_p2p.go | 151 ------------ warp/handlers/signature_request_p2p_test.go | 231 ------------------ warp/handlers/signature_request_test.go | 15 +- warp/handlers/stats.go | 26 +- warp/service.go | 4 +- warp/verifier_backend.go | 64 +++++ warp/verifier_backend_test.go | 255 ++++++++++++++++++++ warp/verifier_stats.go | 29 +++ 15 files changed, 594 insertions(+), 653 deletions(-) delete mode 100644 warp/handlers/signature_request_p2p.go delete mode 100644 warp/handlers/signature_request_p2p_test.go create mode 100644 warp/verifier_backend.go create mode 100644 warp/verifier_backend_test.go create mode 100644 warp/verifier_stats.go diff --git a/go.mod b/go.mod index 30297efb0d..8a074d32ca 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.8 require ( github.com/VictoriaMetrics/fastcache v1.12.1 github.com/antithesishq/antithesis-sdk-go v0.3.8 - github.com/ava-labs/avalanchego v1.11.12 + github.com/ava-labs/avalanchego v1.11.13-0.20241026214739-acb3d7d102a0 github.com/cespare/cp v0.1.0 github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 github.com/davecgh/go-spew v1.1.1 @@ -36,16 +36,16 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.12.0 github.com/status-im/keycard-go v0.2.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/tyler-smith/go-bip39 v1.1.0 github.com/urfave/cli/v2 v2.25.7 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.4.0 - golang.org/x/crypto v0.22.0 + golang.org/x/crypto v0.26.0 golang.org/x/exp v0.0.0-20231127185646-65229373498e - golang.org/x/sync v0.7.0 - golang.org/x/sys v0.19.0 - golang.org/x/text v0.14.0 + golang.org/x/sync v0.8.0 + golang.org/x/sys v0.24.0 + golang.org/x/text v0.17.0 golang.org/x/time v0.3.0 google.golang.org/protobuf v1.34.2 gopkg.in/natefinch/lumberjack.v2 v2.0.0 @@ -60,7 +60,7 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/errors v1.9.1 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 // indirect @@ -117,7 +117,7 @@ require ( github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rs/cors v1.7.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect @@ -140,13 +140,13 @@ require ( go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/term v0.19.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gonum.org/v1/gonum v0.11.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect - google.golang.org/grpc v1.62.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/grpc v1.66.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index ef187eaf4c..dd6bf4e4f6 100644 --- a/go.sum +++ b/go.sum @@ -58,8 +58,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/antithesishq/antithesis-sdk-go v0.3.8 h1:OvGoHxIcOXFJLyn9IJQ5DzByZ3YVAWNBc394ObzDRb8= github.com/antithesishq/antithesis-sdk-go v0.3.8/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/ava-labs/avalanchego v1.11.12 h1:fpGs7xsHYjswIik3tdlGcDaHXh22DLcuf5Ri5+u4RNM= -github.com/ava-labs/avalanchego v1.11.12/go.mod h1:qSHmog3wMVjo/ruIAQo0ppXAilyni07NIu5K88RyhWE= +github.com/ava-labs/avalanchego v1.11.13-0.20241026214739-acb3d7d102a0 h1:1T9OnvZP6XZ62EVWlfmrI8rrudyE6bM2Zt51pCHfS5o= +github.com/ava-labs/avalanchego v1.11.13-0.20241026214739-acb3d7d102a0/go.mod h1:gYlTU42Q4b29hzhUN22yclym5qwB3Si0jh4+LTn7DZM= github.com/ava-labs/coreth v0.13.8 h1:f14X3KgwHl9LwzfxlN6S4bbn5VA2rhEsNnHaRLSTo/8= github.com/ava-labs/coreth v0.13.8/go.mod h1:t3BSv/eQv0AlDPMfEDCMMoD/jq1RkUsbFzQAFg5qBcE= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -96,8 +96,9 @@ github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -239,8 +240,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= -github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -506,8 +507,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -554,8 +555,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.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= @@ -644,8 +645,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 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= @@ -728,8 +729,8 @@ golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 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= @@ -751,8 +752,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -820,12 +821,12 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 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= @@ -836,8 +837,8 @@ 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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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= @@ -899,8 +900,8 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= @@ -972,12 +973,10 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= -google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU= -google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU= +google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -996,8 +995,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= -google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 63f4b3931e..7d4cf631fc 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -17,7 +17,10 @@ import ( "sync" "time" + "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/cache/metercacher" "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/network/p2p/gossip" "github.com/prometheus/client_golang/prometheus" @@ -46,7 +49,6 @@ import ( statesyncclient "github.com/ava-labs/subnet-evm/sync/client" "github.com/ava-labs/subnet-evm/sync/client/stats" "github.com/ava-labs/subnet-evm/warp" - "github.com/ava-labs/subnet-evm/warp/handlers" // Force-load tracer engine to trigger registration // @@ -494,33 +496,48 @@ func (vm *VM) Initialize( for i, hexMsg := range vm.config.WarpOffChainMessages { offchainWarpMessages[i] = []byte(hexMsg) } + warpSignatureCache := &cache.LRU[ids.ID, []byte]{Size: warpSignatureCacheSize} + meteredCache, err := metercacher.New("warp_signature_cache", vm.sdkMetrics, warpSignatureCache) + if err != nil { + return fmt.Errorf("failed to create warp signature cache: %w", err) + } + + // clear warpdb on initialization if config enabled + if vm.config.PruneWarpDB { + if err := database.Clear(vm.warpDB, ethdb.IdealBatchSize); err != nil { + return fmt.Errorf("failed to prune warpDB: %w", err) + } + } + vm.warpBackend, err = warp.NewBackend( vm.ctx.NetworkID, vm.ctx.ChainID, vm.ctx.WarpSigner, vm, vm.warpDB, - warpSignatureCacheSize, + meteredCache, offchainWarpMessages, ) if err != nil { return err } - // clear warpdb on initialization if config enabled - if vm.config.PruneWarpDB { - if err := vm.warpBackend.Clear(); err != nil { - return fmt.Errorf("failed to prune warpDB: %w", err) - } - } - if err := vm.initializeChain(lastAcceptedHash, vm.ethConfig); err != nil { return err } go vm.ctx.Log.RecoverAndPanic(vm.startContinuousProfiler) - vm.initializeHandlers() + // Add p2p warp message warpHandler + warpHandler := acp118.NewCachedHandler(meteredCache, vm.warpBackend, vm.ctx.WarpSigner) + vm.Network.AddHandler(p2p.SignatureRequestHandlerID, warpHandler) + + vm.setAppRequestHandlers() + + vm.StateSyncServer = NewStateSyncServer(&stateSyncServerConfig{ + Chain: vm.blockChain, + SyncableInterval: vm.config.StateSyncCommitInterval, + }) return vm.initializeStateSyncClient(lastAcceptedHeight) } @@ -624,20 +641,6 @@ func (vm *VM) initializeStateSyncClient(lastAcceptedHeight uint64) error { return nil } -// initializeHandlers should be called after [vm.chain] is initialized. -func (vm *VM) initializeHandlers() { - vm.StateSyncServer = NewStateSyncServer(&stateSyncServerConfig{ - Chain: vm.blockChain, - SyncableInterval: vm.config.StateSyncCommitInterval, - }) - - // Add p2p warp message warpHandler - warpHandler := handlers.NewSignatureRequestHandlerP2P(vm.warpBackend, vm.networkCodec) - vm.Network.AddHandler(p2p.SignatureRequestHandlerID, warpHandler) - - vm.setAppRequestHandlers() -} - func (vm *VM) initChainState(lastAcceptedBlock *types.Block) error { block := vm.newBlock(lastAcceptedBlock) diff --git a/plugin/evm/vm_warp_test.go b/plugin/evm/vm_warp_test.go index abaf68f4fc..b80af6fdd5 100644 --- a/plugin/evm/vm_warp_test.go +++ b/plugin/evm/vm_warp_test.go @@ -14,6 +14,7 @@ import ( "github.com/ava-labs/avalanchego/ids" commonEng "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/engine/enginetest" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/snow/validators/validatorstest" @@ -32,9 +33,10 @@ import ( "github.com/ava-labs/subnet-evm/params" "github.com/ava-labs/subnet-evm/plugin/evm/message" "github.com/ava-labs/subnet-evm/precompile/contract" - "github.com/ava-labs/subnet-evm/precompile/contracts/warp" + warpcontract "github.com/ava-labs/subnet-evm/precompile/contracts/warp" "github.com/ava-labs/subnet-evm/predicate" "github.com/ava-labs/subnet-evm/utils" + "github.com/ava-labs/subnet-evm/warp" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" @@ -66,7 +68,7 @@ func TestSendWarpMessage(t *testing.T) { genesis := &core.Genesis{} require.NoError(genesis.UnmarshalJSON([]byte(genesisJSONDurango))) genesis.Config.GenesisPrecompiles = params.Precompiles{ - warp.ConfigKey: warp.NewDefaultConfig(utils.TimeToNewUint64(upgrade.InitiallyActiveTime)), + warpcontract.ConfigKey: warpcontract.NewDefaultConfig(utils.TimeToNewUint64(upgrade.InitiallyActiveTime)), } genesisJSON, err := genesis.MarshalJSON() require.NoError(err) @@ -82,7 +84,7 @@ func TestSendWarpMessage(t *testing.T) { payloadData := avagoUtils.RandomBytes(100) - warpSendMessageInput, err := warp.PackSendWarpMessage(payloadData) + warpSendMessageInput, err := warpcontract.PackSendWarpMessage(payloadData) require.NoError(err) addressedPayload, err := payload.NewAddressedCall( testEthAddrs[0].Bytes(), @@ -97,7 +99,7 @@ func TestSendWarpMessage(t *testing.T) { require.NoError(err) // Submit a transaction to trigger sending a warp message - tx0 := types.NewTransaction(uint64(0), warp.ContractAddress, big.NewInt(1), 100_000, big.NewInt(testMinGasPrice), warpSendMessageInput) + tx0 := types.NewTransaction(uint64(0), warpcontract.ContractAddress, big.NewInt(1), 100_000, big.NewInt(testMinGasPrice), warpSendMessageInput) signedTx0, err := types.SignTx(tx0, types.LatestSignerForChainID(vm.chainConfig.ChainID), testKeys[0]) require.NoError(err) @@ -118,19 +120,19 @@ func TestSendWarpMessage(t *testing.T) { require.Len(receipts[0].Logs, 1) expectedTopics := []common.Hash{ - warp.WarpABI.Events["SendWarpMessage"].ID, + warpcontract.WarpABI.Events["SendWarpMessage"].ID, common.BytesToHash(testEthAddrs[0].Bytes()), common.Hash(expectedUnsignedMessage.ID()), } require.Equal(expectedTopics, receipts[0].Logs[0].Topics) logData := receipts[0].Logs[0].Data - unsignedMessage, err := warp.UnpackSendWarpEventDataToMessage(logData) + unsignedMessage, err := warpcontract.UnpackSendWarpEventDataToMessage(logData) require.NoError(err) // Verify the signature cannot be fetched before the block is accepted - _, err = vm.warpBackend.GetMessageSignature(unsignedMessage) + _, err = vm.warpBackend.GetMessageSignature(context.TODO(), unsignedMessage) require.Error(err) - _, err = vm.warpBackend.GetBlockSignature(blk.ID()) + _, err = vm.warpBackend.GetBlockSignature(context.TODO(), blk.ID()) require.Error(err) require.NoError(vm.SetPreference(context.Background(), blk.ID())) @@ -138,7 +140,7 @@ func TestSendWarpMessage(t *testing.T) { vm.blockChain.DrainAcceptorQueue() // Verify the message signature after accepting the block. - rawSignatureBytes, err := vm.warpBackend.GetMessageSignature(unsignedMessage) + rawSignatureBytes, err := vm.warpBackend.GetMessageSignature(context.TODO(), unsignedMessage) require.NoError(err) blsSignature, err := bls.SignatureFromBytes(rawSignatureBytes[:]) require.NoError(err) @@ -155,7 +157,7 @@ func TestSendWarpMessage(t *testing.T) { require.True(bls.Verify(vm.ctx.PublicKey, blsSignature, unsignedMessage.Bytes())) // Verify the blockID will now be signed by the backend and produces a valid signature. - rawSignatureBytes, err = vm.warpBackend.GetBlockSignature(blk.ID()) + rawSignatureBytes, err = vm.warpBackend.GetBlockSignature(context.TODO(), blk.ID()) require.NoError(err) blsSignature, err = bls.SignatureFromBytes(rawSignatureBytes[:]) require.NoError(err) @@ -263,7 +265,7 @@ func testWarpVMTransaction(t *testing.T, unsignedMessage *avalancheWarp.Unsigned genesis := &core.Genesis{} require.NoError(genesis.UnmarshalJSON([]byte(genesisJSONDurango))) genesis.Config.GenesisPrecompiles = params.Precompiles{ - warp.ConfigKey: warp.NewDefaultConfig(utils.TimeToNewUint64(upgrade.InitiallyActiveTime)), + warpcontract.ConfigKey: warpcontract.NewDefaultConfig(utils.TimeToNewUint64(upgrade.InitiallyActiveTime)), } genesisJSON, err := genesis.MarshalJSON() require.NoError(err) @@ -355,7 +357,7 @@ func testWarpVMTransaction(t *testing.T, unsignedMessage *avalancheWarp.Unsigned common.Big0, txPayload, types.AccessList{}, - warp.ContractAddress, + warpcontract.ContractAddress, signedMessage.Bytes(), ), types.LatestSignerForChainID(vm.chainConfig.ChainID), @@ -421,18 +423,18 @@ func TestReceiveWarpMessage(t *testing.T) { genesis.Config.GenesisPrecompiles = params.Precompiles{ // Note that warp is enabled without RequirePrimaryNetworkSigners // by default in the genesis configuration. - warp.ConfigKey: warp.NewDefaultConfig(utils.TimeToNewUint64(upgrade.InitiallyActiveTime)), + warpcontract.ConfigKey: warpcontract.NewDefaultConfig(utils.TimeToNewUint64(upgrade.InitiallyActiveTime)), } genesisJSON, err := genesis.MarshalJSON() require.NoError(err) // disable warp so we can re-enable it with RequirePrimaryNetworkSigners disableTime := upgrade.InitiallyActiveTime.Add(10 * time.Second) - disableConfig := warp.NewDisableConfig(utils.TimeToNewUint64(disableTime)) + disableConfig := warpcontract.NewDisableConfig(utils.TimeToNewUint64(disableTime)) // re-enable warp with RequirePrimaryNetworkSigners reEnableTime := disableTime.Add(10 * time.Second) - reEnableConfig := warp.NewConfig( + reEnableConfig := warpcontract.NewConfig( utils.TimeToNewUint64(reEnableTime), 0, // QuorumNumerator true, // RequirePrimaryNetworkSigners @@ -628,20 +630,20 @@ func testReceiveWarpMessage( ) require.NoError(err) - getWarpMsgInput, err := warp.PackGetVerifiedWarpMessage(0) + getWarpMsgInput, err := warpcontract.PackGetVerifiedWarpMessage(0) require.NoError(err) getVerifiedWarpMessageTx, err := types.SignTx( predicate.NewPredicateTx( vm.chainConfig.ChainID, vm.txPool.Nonce(testEthAddrs[0]), - &warp.Module.Address, + &warpcontract.Module.Address, 1_000_000, big.NewInt(225*params.GWei), big.NewInt(params.GWei), common.Big0, getWarpMsgInput, types.AccessList{}, - warp.ContractAddress, + warpcontract.ContractAddress, signedMessage.Bytes(), ), types.LatestSignerForChainID(vm.chainConfig.ChainID), @@ -674,7 +676,7 @@ func testReceiveWarpMessage( // An empty bitset indicates success. txResultsBytes := results.GetResults( getVerifiedWarpMessageTx.Hash(), - warp.ContractAddress, + warpcontract.ContractAddress, ) bitset := set.BitsFromBytes(txResultsBytes) require.Zero(bitset.Len()) // Empty bitset indicates success @@ -707,8 +709,8 @@ func testReceiveWarpMessage( verifiedMessageTxReceipt := verifiedMessageReceipts[0] require.Equal(types.ReceiptStatusSuccessful, verifiedMessageTxReceipt.Status) - expectedOutput, err := warp.PackGetVerifiedWarpMessageOutput(warp.GetVerifiedWarpMessageOutput{ - Message: warp.WarpMessage{ + expectedOutput, err := warpcontract.PackGetVerifiedWarpMessageOutput(warpcontract.GetVerifiedWarpMessageOutput{ + Message: warpcontract.WarpMessage{ SourceChainID: common.Hash(sourceChainID), OriginSenderAddress: testEthAddrs[0], Payload: payloadData, @@ -749,8 +751,10 @@ func TestMessageSignatureRequestsToVM(t *testing.T) { // Add the known message and get its signature to confirm. err = vm.warpBackend.AddMessage(warpMessage) require.NoError(t, err) - signature, err := vm.warpBackend.GetMessageSignature(warpMessage) + signature, err := vm.warpBackend.GetMessageSignature(context.TODO(), warpMessage) require.NoError(t, err) + var knownSignature [bls.SignatureLen]byte + copy(knownSignature[:], signature) tests := map[string]struct { messageID ids.ID @@ -758,7 +762,7 @@ func TestMessageSignatureRequestsToVM(t *testing.T) { }{ "known": { messageID: warpMessage.ID(), - expectedResponse: signature, + expectedResponse: knownSignature, }, "unknown": { messageID: ids.GenerateTestID(), @@ -805,8 +809,10 @@ func TestBlockSignatureRequestsToVM(t *testing.T) { lastAcceptedID, err := vm.LastAccepted(context.Background()) require.NoError(t, err) - signature, err := vm.warpBackend.GetBlockSignature(lastAcceptedID) + signature, err := vm.warpBackend.GetBlockSignature(context.TODO(), lastAcceptedID) require.NoError(t, err) + var knownSignature [bls.SignatureLen]byte + copy(knownSignature[:], signature) tests := map[string]struct { blockID ids.ID @@ -814,7 +820,7 @@ func TestBlockSignatureRequestsToVM(t *testing.T) { }{ "known": { blockID: lastAcceptedID, - expectedResponse: signature, + expectedResponse: knownSignature, }, "unknown": { blockID: ids.GenerateTestID(), @@ -849,3 +855,61 @@ func TestBlockSignatureRequestsToVM(t *testing.T) { }) } } + +func TestClearWarpDB(t *testing.T) { + ctx, db, genesisBytes, issuer, _ := setupGenesis(t, genesisJSONLatest) + vm := &VM{} + err := vm.Initialize(context.Background(), ctx, db, genesisBytes, []byte{}, []byte{}, issuer, []*commonEng.Fx{}, &enginetest.Sender{}) + require.NoError(t, err) + + // use multiple messages to test that all messages get cleared + payloads := [][]byte{[]byte("test1"), []byte("test2"), []byte("test3"), []byte("test4"), []byte("test5")} + messages := []*avalancheWarp.UnsignedMessage{} + + // add all messages + for _, payload := range payloads { + unsignedMsg, err := avalancheWarp.NewUnsignedMessage(vm.ctx.NetworkID, vm.ctx.ChainID, payload) + require.NoError(t, err) + err = vm.warpBackend.AddMessage(unsignedMsg) + require.NoError(t, err) + // ensure that the message was added + _, err = vm.warpBackend.GetMessageSignature(context.TODO(), unsignedMsg) + require.NoError(t, err) + messages = append(messages, unsignedMsg) + } + + require.NoError(t, vm.Shutdown(context.Background())) + + // Restart VM with the same database default should not prune the warp db + vm = &VM{} + // we need new context since the previous one has registered metrics. + ctx, _, _, _, _ = setupGenesis(t, genesisJSONLatest) + err = vm.Initialize(context.Background(), ctx, db, genesisBytes, []byte{}, []byte{}, issuer, []*commonEng.Fx{}, &enginetest.Sender{}) + require.NoError(t, err) + + // check messages are still present + for _, message := range messages { + bytes, err := vm.warpBackend.GetMessageSignature(context.TODO(), message) + require.NoError(t, err) + require.NotEmpty(t, bytes) + } + + require.NoError(t, vm.Shutdown(context.Background())) + + // restart the VM with pruning enabled + vm = &VM{} + config := `{"prune-warp-db-enabled": true}` + ctx, _, _, _, _ = setupGenesis(t, genesisJSONLatest) + err = vm.Initialize(context.Background(), ctx, db, genesisBytes, []byte{}, []byte(config), issuer, []*commonEng.Fx{}, &enginetest.Sender{}) + require.NoError(t, err) + + it := vm.warpDB.NewIterator() + require.False(t, it.Next()) + it.Release() + + // ensure all messages have been deleted + for _, message := range messages { + _, err := vm.warpBackend.GetMessageSignature(context.TODO(), message) + require.ErrorIs(t, err, &commonEng.AppError{Code: warp.ParseErrCode}) + } +} diff --git a/warp/backend.go b/warp/backend.go index 360161a336..6e1f6a9553 100644 --- a/warp/backend.go +++ b/warp/backend.go @@ -11,21 +11,19 @@ import ( "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p/acp118" "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/utils/crypto/bls" avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - "github.com/ava-labs/subnet-evm/warp/messages" - "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" ) var ( _ Backend = &backend{} errParsingOffChainMessage = errors.New("failed to parse off-chain message") -) -const batchSize = ethdb.IdealBatchSize + messageCacheSize = 500 +) type BlockClient interface { GetAcceptedBlock(ctx context.Context, blockID ids.ID) (snowman.Block, error) @@ -37,19 +35,18 @@ type Backend interface { // AddMessage signs [unsignedMessage] and adds it to the warp backend database AddMessage(unsignedMessage *avalancheWarp.UnsignedMessage) error - // GetMessageSignature returns the signature of the requested message. - GetMessageSignature(message *avalancheWarp.UnsignedMessage) ([bls.SignatureLen]byte, error) + // GetMessageSignature validates the message and returns the signature of the requested message. + GetMessageSignature(ctx context.Context, message *avalancheWarp.UnsignedMessage) ([]byte, error) - // GetBlockSignature returns the signature of the requested message hash. - GetBlockSignature(blockID ids.ID) ([bls.SignatureLen]byte, error) + // GetBlockSignature returns the signature of a hash payload containing blockID if it's the ID of an accepted block. + GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) // GetMessage retrieves the [unsignedMessage] from the warp backend database if available - // TODO: After E-Upgrade, the backend no longer needs to store the mapping from messageHash + // TODO: After Etna, the backend no longer needs to store the mapping from messageHash // to unsignedMessage (and this method can be removed). GetMessage(messageHash ids.ID) (*avalancheWarp.UnsignedMessage, error) - // Clear clears the entire db - Clear() error + acp118.Verifier } // backend implements Backend, keeps track of warp messages, and generates message signatures. @@ -59,10 +56,10 @@ type backend struct { db database.Database warpSigner avalancheWarp.Signer blockClient BlockClient - messageSignatureCache *cache.LRU[ids.ID, [bls.SignatureLen]byte] - blockSignatureCache *cache.LRU[ids.ID, [bls.SignatureLen]byte] + signatureCache cache.Cacher[ids.ID, []byte] messageCache *cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage] offchainAddressedCallMsgs map[ids.ID]*avalancheWarp.UnsignedMessage + stats *verifierStats } // NewBackend creates a new Backend, and initializes the signature cache and message tracking database. @@ -72,7 +69,7 @@ func NewBackend( warpSigner avalancheWarp.Signer, blockClient BlockClient, db database.Database, - cacheSize int, + signatureCache cache.Cacher[ids.ID, []byte], offchainMessages [][]byte, ) (Backend, error) { b := &backend{ @@ -81,9 +78,9 @@ func NewBackend( db: db, warpSigner: warpSigner, blockClient: blockClient, - messageSignatureCache: &cache.LRU[ids.ID, [bls.SignatureLen]byte]{Size: cacheSize}, - blockSignatureCache: &cache.LRU[ids.ID, [bls.SignatureLen]byte]{Size: cacheSize}, - messageCache: &cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage]{Size: cacheSize}, + signatureCache: signatureCache, + messageCache: &cache.LRU[ids.ID, *avalancheWarp.UnsignedMessage]{Size: messageCacheSize}, + stats: newVerifierStats(), offchainAddressedCallMsgs: make(map[ids.ID]*avalancheWarp.UnsignedMessage), } return b, b.initOffChainMessages(offchainMessages) @@ -114,15 +111,9 @@ func (b *backend) initOffChainMessages(offchainMessages [][]byte) error { return nil } -func (b *backend) Clear() error { - b.messageSignatureCache.Flush() - b.blockSignatureCache.Flush() - b.messageCache.Flush() - return database.Clear(b.db, batchSize) -} - func (b *backend) AddMessage(unsignedMessage *avalancheWarp.UnsignedMessage) error { messageID := unsignedMessage.ID() + log.Debug("Adding warp message to backend", "messageID", messageID) // In the case when a node restarts, and possibly changes its bls key, the cache gets emptied but the database does not. // So to avoid having incorrect signatures saved in the database after a bls key change, we save the full message in the database. @@ -131,100 +122,52 @@ func (b *backend) AddMessage(unsignedMessage *avalancheWarp.UnsignedMessage) err return fmt.Errorf("failed to put warp signature in db: %w", err) } - var signature [bls.SignatureLen]byte - sig, err := b.warpSigner.Sign(unsignedMessage) - if err != nil { + if _, err := b.signMessage(unsignedMessage); err != nil { return fmt.Errorf("failed to sign warp message: %w", err) } - - copy(signature[:], sig) - b.messageSignatureCache.Put(messageID, signature) - log.Debug("Adding warp message to backend", "messageID", messageID) return nil } -func (b *backend) GetMessageSignature(unsignedMessage *avalancheWarp.UnsignedMessage) ([bls.SignatureLen]byte, error) { +func (b *backend) GetMessageSignature(ctx context.Context, unsignedMessage *avalancheWarp.UnsignedMessage) ([]byte, error) { messageID := unsignedMessage.ID() log.Debug("Getting warp message from backend", "messageID", messageID) - if sig, ok := b.messageSignatureCache.Get(messageID); ok { + if sig, ok := b.signatureCache.Get(messageID); ok { return sig, nil } - if err := b.ValidateMessage(unsignedMessage); err != nil { - return [bls.SignatureLen]byte{}, fmt.Errorf("failed to validate warp message: %w", err) - } - - var signature [bls.SignatureLen]byte - sig, err := b.warpSigner.Sign(unsignedMessage) - if err != nil { - return [bls.SignatureLen]byte{}, fmt.Errorf("failed to sign warp message: %w", err) + if err := b.Verify(ctx, unsignedMessage, nil); err != nil { + return nil, fmt.Errorf("failed to validate warp message: %w", err) } - - copy(signature[:], sig) - b.messageSignatureCache.Put(messageID, signature) - return signature, nil + return b.signMessage(unsignedMessage) } -func (b *backend) ValidateMessage(unsignedMessage *avalancheWarp.UnsignedMessage) error { - // Known on-chain messages should be signed - if _, err := b.GetMessage(unsignedMessage.ID()); err == nil { - return nil - } +func (b *backend) GetBlockSignature(ctx context.Context, blockID ids.ID) ([]byte, error) { + log.Debug("Getting block from backend", "blockID", blockID) - // Try to parse the payload as an AddressedCall - addressedCall, err := payload.ParseAddressedCall(unsignedMessage.Payload) + blockHashPayload, err := payload.NewHash(blockID) if err != nil { - return fmt.Errorf("failed to parse unknown message as AddressedCall: %w", err) + return nil, fmt.Errorf("failed to create new block hash payload: %w", err) } - // Further, parse the payload to see if it is a known type. - parsed, err := messages.Parse(addressedCall.Payload) + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(b.networkID, b.sourceChainID, blockHashPayload.Bytes()) if err != nil { - return fmt.Errorf("failed to parse unknown message: %w", err) - } - - // Check if the message is a known type that can be signed on demand - signable, ok := parsed.(messages.Signable) - if !ok { - return fmt.Errorf("parsed message is not Signable: %T", signable) + return nil, fmt.Errorf("failed to create new unsigned warp message: %w", err) } - // Check if the message should be signed according to its type - if err := signable.VerifyMesssage(addressedCall.SourceAddress); err != nil { - return fmt.Errorf("failed to verify Signable message: %w", err) - } - return nil -} - -func (b *backend) GetBlockSignature(blockID ids.ID) ([bls.SignatureLen]byte, error) { - log.Debug("Getting block from backend", "blockID", blockID) - if sig, ok := b.blockSignatureCache.Get(blockID); ok { + if sig, ok := b.signatureCache.Get(unsignedMessage.ID()); ok { return sig, nil } - _, err := b.blockClient.GetAcceptedBlock(context.TODO(), blockID) - if err != nil { - return [bls.SignatureLen]byte{}, fmt.Errorf("failed to get block %s: %w", blockID, err) + if err := b.verifyBlockMessage(ctx, blockHashPayload); err != nil { + return nil, fmt.Errorf("failed to validate block message: %w", err) } - var signature [bls.SignatureLen]byte - blockHashPayload, err := payload.NewHash(blockID) - if err != nil { - return [bls.SignatureLen]byte{}, fmt.Errorf("failed to create new block hash payload: %w", err) - } - unsignedMessage, err := avalancheWarp.NewUnsignedMessage(b.networkID, b.sourceChainID, blockHashPayload.Bytes()) + sig, err := b.signMessage(unsignedMessage) if err != nil { - return [bls.SignatureLen]byte{}, fmt.Errorf("failed to create new unsigned warp message: %w", err) + return nil, fmt.Errorf("failed to sign block message: %w", err) } - sig, err := b.warpSigner.Sign(unsignedMessage) - if err != nil { - return [bls.SignatureLen]byte{}, fmt.Errorf("failed to sign warp message: %w", err) - } - - copy(signature[:], sig) - b.blockSignatureCache.Put(blockID, signature) - return signature, nil + return sig, nil } func (b *backend) GetMessage(messageID ids.ID) (*avalancheWarp.UnsignedMessage, error) { @@ -248,3 +191,13 @@ func (b *backend) GetMessage(messageID ids.ID) (*avalancheWarp.UnsignedMessage, return unsignedMessage, nil } + +func (b *backend) signMessage(unsignedMessage *avalancheWarp.UnsignedMessage) ([]byte, error) { + sig, err := b.warpSigner.Sign(unsignedMessage) + if err != nil { + return nil, fmt.Errorf("failed to sign warp message: %w", err) + } + + b.signatureCache.Put(unsignedMessage.ID(), sig) + return sig, nil +} diff --git a/warp/backend_test.go b/warp/backend_test.go index 21013dfc24..cae9c14bc6 100644 --- a/warp/backend_test.go +++ b/warp/backend_test.go @@ -4,8 +4,10 @@ package warp import ( + "context" "testing" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils" @@ -35,56 +37,14 @@ func init() { } } -func TestClearDB(t *testing.T) { - db := memdb.New() - - sk, err := bls.NewSecretKey() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backendIntf, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500, nil) - require.NoError(t, err) - backend, ok := backendIntf.(*backend) - require.True(t, ok) - - // use multiple messages to test that all messages get cleared - payloads := [][]byte{[]byte("test1"), []byte("test2"), []byte("test3"), []byte("test4"), []byte("test5")} - messages := make([]*avalancheWarp.UnsignedMessage, 0, len(payloads)) - - // add all messages - for _, payload := range payloads { - unsignedMsg, err := avalancheWarp.NewUnsignedMessage(networkID, sourceChainID, payload) - require.NoError(t, err) - messages = append(messages, unsignedMsg) - err = backend.AddMessage(unsignedMsg) - require.NoError(t, err) - // ensure that the message was added - _, err = backend.GetMessageSignature(unsignedMsg) - require.NoError(t, err) - } - - err = backend.Clear() - require.NoError(t, err) - require.Zero(t, backend.messageCache.Len()) - require.Zero(t, backend.messageSignatureCache.Len()) - require.Zero(t, backend.blockSignatureCache.Len()) - it := db.NewIterator() - defer it.Release() - require.False(t, it.Next()) - - // ensure all messages have been deleted - for _, message := range messages { - _, err := backend.GetMessageSignature(message) - require.ErrorContains(t, err, "failed to validate warp message") - } -} - func TestAddAndGetValidMessage(t *testing.T) { db := memdb.New() sk, err := bls.NewSecretKey() require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500, nil) + messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 500} + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, nil) require.NoError(t, err) // Add testUnsignedMessage to the warp backend @@ -92,7 +52,7 @@ func TestAddAndGetValidMessage(t *testing.T) { require.NoError(t, err) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := backend.GetMessageSignature(testUnsignedMessage) + signature, err := backend.GetMessageSignature(context.TODO(), testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -106,11 +66,12 @@ func TestAddAndGetUnknownMessage(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(t, err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 500, nil) + messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 500} + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, nil) require.NoError(t, err) // Try getting a signature for a message that was not added. - _, err = backend.GetMessageSignature(testUnsignedMessage) + _, err = backend.GetMessageSignature(context.TODO(), testUnsignedMessage) require.Error(t, err) } @@ -124,7 +85,8 @@ func TestGetBlockSignature(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(err) warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) - backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, db, 500, nil) + messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 500} + backend, err := NewBackend(networkID, sourceChainID, warpSigner, blockClient, db, messageSignatureCache, nil) require.NoError(err) blockHashPayload, err := payload.NewHash(blkID) @@ -134,11 +96,11 @@ func TestGetBlockSignature(t *testing.T) { expectedSig, err := warpSigner.Sign(unsignedMessage) require.NoError(err) - signature, err := backend.GetBlockSignature(blkID) + signature, err := backend.GetBlockSignature(context.TODO(), blkID) require.NoError(err) require.Equal(expectedSig, signature[:]) - _, err = backend.GetBlockSignature(ids.GenerateTestID()) + _, err = backend.GetBlockSignature(context.TODO(), ids.GenerateTestID()) require.Error(err) } @@ -150,7 +112,8 @@ func TestZeroSizedCache(t *testing.T) { warpSigner := avalancheWarp.NewSigner(sk, networkID, sourceChainID) // Verify zero sized cache works normally, because the lru cache will be initialized to size 1 for any size parameter <= 0. - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 0, nil) + messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 0} + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, nil) require.NoError(t, err) // Add testUnsignedMessage to the warp backend @@ -158,7 +121,7 @@ func TestZeroSizedCache(t *testing.T) { require.NoError(t, err) // Verify that a signature is returned successfully, and compare to expected signature. - signature, err := backend.GetMessageSignature(testUnsignedMessage) + signature, err := backend.GetMessageSignature(context.TODO(), testUnsignedMessage) require.NoError(t, err) expectedSig, err := warpSigner.Sign(testUnsignedMessage) @@ -187,7 +150,7 @@ func TestOffChainMessages(t *testing.T) { require.NoError(err) require.Equal(testUnsignedMessage.Bytes(), msg.Bytes()) - signature, err := b.GetMessageSignature(testUnsignedMessage) + signature, err := b.GetMessageSignature(context.TODO(), testUnsignedMessage) require.NoError(err) expectedSignatureBytes, err := warpSigner.Sign(msg) require.NoError(err) @@ -203,7 +166,8 @@ func TestOffChainMessages(t *testing.T) { require := require.New(t) db := memdb.New() - backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, 0, test.offchainMessages) + messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 0} + backend, err := NewBackend(networkID, sourceChainID, warpSigner, nil, db, messageSignatureCache, test.offchainMessages) require.ErrorIs(err, test.err) if test.check != nil { test.check(require, backend) diff --git a/warp/handlers/signature_request.go b/warp/handlers/signature_request.go index 3a28cd994e..4b8b76c565 100644 --- a/warp/handlers/signature_request.go +++ b/warp/handlers/signature_request.go @@ -16,7 +16,7 @@ import ( ) // SignatureRequestHandler serves warp signature requests. It is a peer.RequestHandler for message.MessageSignatureRequest. -// TODO: After E-Upgrade, this handler can be removed and SignatureRequestHandlerP2P is sufficient. +// TODO: After Etna, this handler can be removed and SignatureRequestHandlerP2P is sufficient. type SignatureRequestHandler struct { backend warp.Backend codec codec.Manager @@ -51,13 +51,13 @@ func (s *SignatureRequestHandler) OnMessageSignatureRequest(ctx context.Context, log.Debug("Unknown warp message requested", "messageID", signatureRequest.MessageID) s.stats.IncMessageSignatureMiss() } else { - signature, err = s.backend.GetMessageSignature(unsignedMessage) + sig, err := s.backend.GetMessageSignature(ctx, unsignedMessage) if err != nil { log.Debug("Unknown warp signature requested", "messageID", signatureRequest.MessageID) s.stats.IncMessageSignatureMiss() - signature = [bls.SignatureLen]byte{} } else { s.stats.IncMessageSignatureHit() + copy(signature[:], sig) } } @@ -80,13 +80,14 @@ func (s *SignatureRequestHandler) OnBlockSignatureRequest(ctx context.Context, n s.stats.UpdateBlockSignatureRequestTime(time.Since(startTime)) }() - signature, err := s.backend.GetBlockSignature(request.BlockID) + var signature [bls.SignatureLen]byte + sig, err := s.backend.GetBlockSignature(ctx, request.BlockID) if err != nil { log.Debug("Unknown warp signature requested", "blockID", request.BlockID) s.stats.IncBlockSignatureMiss() - signature = [bls.SignatureLen]byte{} } else { s.stats.IncBlockSignatureHit() + copy(signature[:], sig) } response := message.SignatureResponse{Signature: signature} diff --git a/warp/handlers/signature_request_p2p.go b/warp/handlers/signature_request_p2p.go deleted file mode 100644 index cb711974b2..0000000000 --- a/warp/handlers/signature_request_p2p.go +++ /dev/null @@ -1,151 +0,0 @@ -// (c) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package handlers - -import ( - "context" - "fmt" - "time" - - "github.com/ava-labs/avalanchego/codec" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/network/p2p" - "github.com/ava-labs/avalanchego/proto/pb/sdk" - "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/utils/crypto/bls" - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - "github.com/ava-labs/subnet-evm/warp" - "google.golang.org/protobuf/proto" -) - -var _ p2p.Handler = (*SignatureRequestHandlerP2P)(nil) - -const ( - ErrFailedToParse = iota - ErrFailedToGetSig - ErrFailedToMarshal -) - -type AddressedCallHandler interface { - GetMessageSignature(*avalancheWarp.UnsignedMessage) ([bls.SignatureLen]byte, error) -} - -// SignatureRequestHandlerP2P serves warp signature requests using the p2p -// framework from avalanchego. It is a peer.RequestHandler for -// message.MessageSignatureRequest. -type SignatureRequestHandlerP2P struct { - backend warp.Backend - codec codec.Manager - stats *handlerStats -} - -func NewSignatureRequestHandlerP2P(backend warp.Backend, codec codec.Manager) *SignatureRequestHandlerP2P { - return &SignatureRequestHandlerP2P{ - backend: backend, - codec: codec, - stats: newStats(), - } -} - -func (s *SignatureRequestHandlerP2P) AppRequest( - ctx context.Context, - nodeID ids.NodeID, - deadline time.Time, - requestBytes []byte, -) ([]byte, *common.AppError) { - // Per ACP-118, the requestBytes are the serialized form of - // sdk.SignatureRequest. - req := new(sdk.SignatureRequest) - if err := proto.Unmarshal(requestBytes, req); err != nil { - return nil, &common.AppError{ - Code: ErrFailedToParse, - Message: "failed to unmarshal request: " + err.Error(), - } - } - - unsignedMessage, err := avalancheWarp.ParseUnsignedMessage(req.Message) - if err != nil { - return nil, &common.AppError{ - Code: ErrFailedToParse, - Message: "failed to parse unsigned message: " + err.Error(), - } - } - parsed, err := payload.Parse(unsignedMessage.Payload) - if err != nil { - return nil, &common.AppError{ - Code: ErrFailedToParse, - Message: "failed to parse payload: " + err.Error(), - } - } - - var sig [bls.SignatureLen]byte - switch p := parsed.(type) { - case *payload.AddressedCall: - sig, err = s.GetMessageSignature(unsignedMessage) - if err != nil { - s.stats.IncMessageSignatureMiss() - } else { - s.stats.IncMessageSignatureHit() - } - case *payload.Hash: - sig, err = s.GetBlockSignature(p.Hash) - if err != nil { - s.stats.IncBlockSignatureMiss() - } else { - s.stats.IncBlockSignatureHit() - } - default: - return nil, &common.AppError{ - Code: ErrFailedToParse, - Message: fmt.Sprintf("unknown payload type: %T", p), - } - } - if err != nil { - return nil, &common.AppError{ - Code: ErrFailedToGetSig, - Message: "failed to get signature: " + err.Error(), - } - } - - // Per ACP-118, the responseBytes are the serialized form of - // sdk.SignatureResponse. - resp := &sdk.SignatureResponse{Signature: sig[:]} - respBytes, err := proto.Marshal(resp) - if err != nil { - return nil, &common.AppError{ - Code: ErrFailedToMarshal, - Message: "failed to marshal response: " + err.Error(), - } - } - return respBytes, nil -} - -func (s *SignatureRequestHandlerP2P) GetMessageSignature(message *avalancheWarp.UnsignedMessage) ([bls.SignatureLen]byte, error) { - startTime := time.Now() - s.stats.IncMessageSignatureRequest() - - // Always report signature request time - defer func() { - s.stats.UpdateMessageSignatureRequestTime(time.Since(startTime)) - }() - - return s.backend.GetMessageSignature(message) -} - -func (s *SignatureRequestHandlerP2P) GetBlockSignature(blockID ids.ID) ([bls.SignatureLen]byte, error) { - startTime := time.Now() - s.stats.IncBlockSignatureRequest() - - // Always report signature request time - defer func() { - s.stats.UpdateBlockSignatureRequestTime(time.Since(startTime)) - }() - - return s.backend.GetBlockSignature(blockID) -} - -func (s *SignatureRequestHandlerP2P) AppGossip( - ctx context.Context, nodeID ids.NodeID, gossipBytes []byte) { -} diff --git a/warp/handlers/signature_request_p2p_test.go b/warp/handlers/signature_request_p2p_test.go deleted file mode 100644 index 3104fe59b3..0000000000 --- a/warp/handlers/signature_request_p2p_test.go +++ /dev/null @@ -1,231 +0,0 @@ -// (c) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package handlers - -import ( - "context" - "testing" - "time" - - "github.com/ava-labs/avalanchego/database/memdb" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/proto/pb/sdk" - "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/utils/crypto/bls" - avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" - "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" - "github.com/ava-labs/subnet-evm/plugin/evm/message" - "github.com/ava-labs/subnet-evm/utils" - "github.com/ava-labs/subnet-evm/warp" - "github.com/ava-labs/subnet-evm/warp/warptest" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" -) - -func TestMessageSignatureHandlerP2P(t *testing.T) { - database := memdb.New() - snowCtx := utils.TestSnowContext() - blsSecretKey, err := bls.NewSecretKey() - require.NoError(t, err) - warpSigner := avalancheWarp.NewSigner(blsSecretKey, snowCtx.NetworkID, snowCtx.ChainID) - - addressedPayload, err := payload.NewAddressedCall([]byte{1, 2, 3}, []byte{1, 2, 3}) - require.NoError(t, err) - offchainMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedPayload.Bytes()) - require.NoError(t, err) - - backend, err := warp.NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, database, 100, [][]byte{offchainMessage.Bytes()}) - require.NoError(t, err) - - offchainPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) - require.NoError(t, err) - msg, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, offchainPayload.Bytes()) - require.NoError(t, err) - require.NoError(t, backend.AddMessage(msg)) - signature, err := backend.GetMessageSignature(msg) - require.NoError(t, err) - offchainSignature, err := backend.GetMessageSignature(offchainMessage) - require.NoError(t, err) - - unknownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("unknown message")) - require.NoError(t, err) - unknownMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) - require.NoError(t, err) - - tests := map[string]struct { - setup func() (request sdk.SignatureRequest, expectedResponse []byte) - verifyStats func(t *testing.T, stats *handlerStats) - err error - }{ - "known message": { - setup: func() (request sdk.SignatureRequest, expectedResponse []byte) { - return sdk.SignatureRequest{Message: msg.Bytes()}, signature[:] - }, - verifyStats: func(t *testing.T, stats *handlerStats) { - require.EqualValues(t, 1, stats.messageSignatureRequest.Snapshot().Count()) - require.EqualValues(t, 1, stats.messageSignatureHit.Snapshot().Count()) - require.EqualValues(t, 0, stats.messageSignatureMiss.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureRequest.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureHit.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureMiss.Snapshot().Count()) - }, - }, - "offchain message": { - setup: func() (request sdk.SignatureRequest, expectedResponse []byte) { - return sdk.SignatureRequest{Message: offchainMessage.Bytes()}, offchainSignature[:] - }, - verifyStats: func(t *testing.T, stats *handlerStats) { - require.EqualValues(t, 1, stats.messageSignatureRequest.Snapshot().Count()) - require.EqualValues(t, 1, stats.messageSignatureHit.Snapshot().Count()) - require.EqualValues(t, 0, stats.messageSignatureMiss.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureRequest.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureHit.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureMiss.Snapshot().Count()) - }, - }, - "unknown message": { - setup: func() (request sdk.SignatureRequest, expectedResponse []byte) { - return sdk.SignatureRequest{Message: unknownMessage.Bytes()}, nil - }, - verifyStats: func(t *testing.T, stats *handlerStats) { - require.EqualValues(t, 1, stats.messageSignatureRequest.Snapshot().Count()) - require.EqualValues(t, 0, stats.messageSignatureHit.Snapshot().Count()) - require.EqualValues(t, 1, stats.messageSignatureMiss.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureRequest.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureHit.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureMiss.Snapshot().Count()) - }, - err: &common.AppError{Code: ErrFailedToGetSig}, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - handler := NewSignatureRequestHandlerP2P(backend, message.Codec) - handler.stats.Clear() - - request, expectedResponse := test.setup() - requestBytes, err := proto.Marshal(&request) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, requestBytes) - if test.err != nil { - require.ErrorIs(t, appErr, test.err) - } else { - require.Nil(t, appErr) - } - - test.verifyStats(t, handler.stats) - - // If the expected response is empty, assert that the handler returns an empty response and return early. - if len(expectedResponse) == 0 { - require.Len(t, responseBytes, 0, "expected response to be empty") - return - } - var response sdk.SignatureResponse - err = proto.Unmarshal(responseBytes, &response) - require.NoError(t, err, "error unmarshalling SignatureResponse") - - require.Equal(t, expectedResponse, response.Signature) - }) - } -} - -func TestBlockSignatureHandlerP2P(t *testing.T) { - database := memdb.New() - snowCtx := utils.TestSnowContext() - blsSecretKey, err := bls.NewSecretKey() - require.NoError(t, err) - - warpSigner := avalancheWarp.NewSigner(blsSecretKey, snowCtx.NetworkID, snowCtx.ChainID) - blkID := ids.GenerateTestID() - blockClient := warptest.MakeBlockClient(blkID) - backend, err := warp.NewBackend( - snowCtx.NetworkID, - snowCtx.ChainID, - warpSigner, - blockClient, - database, - 100, - nil, - ) - require.NoError(t, err) - - signature, err := backend.GetBlockSignature(blkID) - require.NoError(t, err) - unknownBlockID := ids.GenerateTestID() - - toMessageBytes := func(id ids.ID) []byte { - idPayload, err := payload.NewHash(id) - require.NoError(t, err) - - msg, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, idPayload.Bytes()) - require.NoError(t, err) - - return msg.Bytes() - } - - tests := map[string]struct { - setup func() (request sdk.SignatureRequest, expectedResponse []byte) - verifyStats func(t *testing.T, stats *handlerStats) - err error - }{ - "known block": { - setup: func() (request sdk.SignatureRequest, expectedResponse []byte) { - return sdk.SignatureRequest{Message: toMessageBytes(blkID)}, signature[:] - }, - verifyStats: func(t *testing.T, stats *handlerStats) { - require.EqualValues(t, 0, stats.messageSignatureRequest.Snapshot().Count()) - require.EqualValues(t, 0, stats.messageSignatureHit.Snapshot().Count()) - require.EqualValues(t, 0, stats.messageSignatureMiss.Snapshot().Count()) - require.EqualValues(t, 1, stats.blockSignatureRequest.Snapshot().Count()) - require.EqualValues(t, 1, stats.blockSignatureHit.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureMiss.Snapshot().Count()) - }, - }, - "unknown block": { - setup: func() (request sdk.SignatureRequest, expectedResponse []byte) { - return sdk.SignatureRequest{Message: toMessageBytes(unknownBlockID)}, nil - }, - verifyStats: func(t *testing.T, stats *handlerStats) { - require.EqualValues(t, 0, stats.messageSignatureRequest.Snapshot().Count()) - require.EqualValues(t, 0, stats.messageSignatureHit.Snapshot().Count()) - require.EqualValues(t, 0, stats.messageSignatureMiss.Snapshot().Count()) - require.EqualValues(t, 1, stats.blockSignatureRequest.Snapshot().Count()) - require.EqualValues(t, 0, stats.blockSignatureHit.Snapshot().Count()) - require.EqualValues(t, 1, stats.blockSignatureMiss.Snapshot().Count()) - }, - err: &common.AppError{Code: ErrFailedToGetSig}, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - handler := NewSignatureRequestHandlerP2P(backend, message.Codec) - handler.stats.Clear() - - request, expectedResponse := test.setup() - requestBytes, err := proto.Marshal(&request) - require.NoError(t, err) - responseBytes, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, requestBytes) - if test.err != nil { - require.ErrorIs(t, appErr, test.err) - } else { - require.Nil(t, appErr) - } - - test.verifyStats(t, handler.stats) - - // If the expected response is empty, assert that the handler returns an empty response and return early. - if len(expectedResponse) == 0 { - require.Len(t, responseBytes, 0, "expected response to be empty") - return - } - var response sdk.SignatureResponse - err = proto.Unmarshal(responseBytes, &response) - require.NoError(t, err, "error unmarshalling SignatureResponse") - - require.Equal(t, expectedResponse, response.Signature) - }) - } -} diff --git a/warp/handlers/signature_request_test.go b/warp/handlers/signature_request_test.go index 1f699324cc..3189478106 100644 --- a/warp/handlers/signature_request_test.go +++ b/warp/handlers/signature_request_test.go @@ -7,6 +7,7 @@ import ( "context" "testing" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/bls" @@ -31,16 +32,17 @@ func TestMessageSignatureHandler(t *testing.T) { offchainMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, addressedPayload.Bytes()) require.NoError(t, err) - backend, err := warp.NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, database, 100, [][]byte{offchainMessage.Bytes()}) + messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 100} + backend, err := warp.NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, database, messageSignatureCache, [][]byte{offchainMessage.Bytes()}) require.NoError(t, err) msg, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, []byte("test")) require.NoError(t, err) messageID := msg.ID() require.NoError(t, backend.AddMessage(msg)) - signature, err := backend.GetMessageSignature(msg) + signature, err := backend.GetMessageSignature(context.TODO(), msg) require.NoError(t, err) - offchainSignature, err := backend.GetMessageSignature(offchainMessage) + offchainSignature, err := backend.GetMessageSignature(context.TODO(), offchainMessage) require.NoError(t, err) unknownMessageID := ids.GenerateTestID() @@ -101,7 +103,6 @@ func TestMessageSignatureHandler(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { handler := NewSignatureRequestHandler(backend, message.Codec) - handler.stats.Clear() request, expectedResponse := test.setup() responseBytes, err := handler.OnMessageSignatureRequest(context.Background(), ids.GenerateTestNodeID(), 1, request) @@ -132,18 +133,19 @@ func TestBlockSignatureHandler(t *testing.T) { warpSigner := avalancheWarp.NewSigner(blsSecretKey, snowCtx.NetworkID, snowCtx.ChainID) blkID := ids.GenerateTestID() blockClient := warptest.MakeBlockClient(blkID) + messageSignatureCache := &cache.LRU[ids.ID, []byte]{Size: 100} backend, err := warp.NewBackend( snowCtx.NetworkID, snowCtx.ChainID, warpSigner, blockClient, database, - 100, + messageSignatureCache, nil, ) require.NoError(t, err) - signature, err := backend.GetBlockSignature(blkID) + signature, err := backend.GetBlockSignature(context.TODO(), blkID) require.NoError(t, err) unknownMessageID := ids.GenerateTestID() @@ -188,7 +190,6 @@ func TestBlockSignatureHandler(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { handler := NewSignatureRequestHandler(backend, message.Codec) - handler.stats.Clear() request, expectedResponse := test.setup() responseBytes, err := handler.OnBlockSignatureRequest(context.Background(), ids.GenerateTestNodeID(), 1, request) diff --git a/warp/handlers/stats.go b/warp/handlers/stats.go index 481f2aaac0..1c7a854e97 100644 --- a/warp/handlers/stats.go +++ b/warp/handlers/stats.go @@ -24,14 +24,14 @@ type handlerStats struct { func newStats() *handlerStats { return &handlerStats{ - messageSignatureRequest: metrics.GetOrRegisterCounter("message_signature_request_count", nil), - messageSignatureHit: metrics.GetOrRegisterCounter("message_signature_request_hit", nil), - messageSignatureMiss: metrics.GetOrRegisterCounter("message_signature_request_miss", nil), - messageSignatureRequestDuration: metrics.GetOrRegisterGauge("message_signature_request_duration", nil), - blockSignatureRequest: metrics.GetOrRegisterCounter("block_signature_request_count", nil), - blockSignatureHit: metrics.GetOrRegisterCounter("block_signature_request_hit", nil), - blockSignatureMiss: metrics.GetOrRegisterCounter("block_signature_request_miss", nil), - blockSignatureRequestDuration: metrics.GetOrRegisterGauge("block_signature_request_duration", nil), + messageSignatureRequest: metrics.NewRegisteredCounter("message_signature_request_count", nil), + messageSignatureHit: metrics.NewRegisteredCounter("message_signature_request_hit", nil), + messageSignatureMiss: metrics.NewRegisteredCounter("message_signature_request_miss", nil), + messageSignatureRequestDuration: metrics.NewRegisteredGauge("message_signature_request_duration", nil), + blockSignatureRequest: metrics.NewRegisteredCounter("block_signature_request_count", nil), + blockSignatureHit: metrics.NewRegisteredCounter("block_signature_request_hit", nil), + blockSignatureMiss: metrics.NewRegisteredCounter("block_signature_request_miss", nil), + blockSignatureRequestDuration: metrics.NewRegisteredGauge("block_signature_request_duration", nil), } } @@ -47,13 +47,3 @@ func (h *handlerStats) IncBlockSignatureMiss() { h.blockSignatureMiss.Inc(1) func (h *handlerStats) UpdateBlockSignatureRequestTime(duration time.Duration) { h.blockSignatureRequestDuration.Inc(int64(duration)) } -func (h *handlerStats) Clear() { - h.messageSignatureRequest.Clear() - h.messageSignatureHit.Clear() - h.messageSignatureMiss.Clear() - h.messageSignatureRequestDuration.Update(0) - h.blockSignatureRequest.Clear() - h.blockSignatureHit.Clear() - h.blockSignatureMiss.Clear() - h.blockSignatureRequestDuration.Update(0) -} diff --git a/warp/service.go b/warp/service.go index 2472c63b09..bcb54ccbbc 100644 --- a/warp/service.go +++ b/warp/service.go @@ -58,7 +58,7 @@ func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexuti if err != nil { return nil, fmt.Errorf("failed to get message %s with error %w", messageID, err) } - signature, err := a.backend.GetMessageSignature(unsignedMessage) + signature, err := a.backend.GetMessageSignature(ctx, unsignedMessage) if err != nil { return nil, fmt.Errorf("failed to get signature for message %s with error %w", messageID, err) } @@ -67,7 +67,7 @@ func (a *API) GetMessageSignature(ctx context.Context, messageID ids.ID) (hexuti // GetBlockSignature returns the BLS signature associated with a blockID. func (a *API) GetBlockSignature(ctx context.Context, blockID ids.ID) (hexutil.Bytes, error) { - signature, err := a.backend.GetBlockSignature(blockID) + signature, err := a.backend.GetBlockSignature(ctx, blockID) if err != nil { return nil, fmt.Errorf("failed to get signature for block %s with error %w", blockID, err) } diff --git a/warp/verifier_backend.go b/warp/verifier_backend.go new file mode 100644 index 0000000000..c70563c585 --- /dev/null +++ b/warp/verifier_backend.go @@ -0,0 +1,64 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "fmt" + + "github.com/ava-labs/avalanchego/snow/engine/common" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +const ( + ParseErrCode = iota + 1 + VerifyErrCode +) + +// Verify verifies the signature of the message +// It also implements the acp118.Verifier interface +func (b *backend) Verify(ctx context.Context, unsignedMessage *avalancheWarp.UnsignedMessage, _ []byte) *common.AppError { + messageID := unsignedMessage.ID() + // Known on-chain messages should be signed + if _, err := b.GetMessage(messageID); err == nil { + return nil + } + + parsed, err := payload.Parse(unsignedMessage.Payload) + if err != nil { + b.stats.IncMessageParseFail() + return &common.AppError{ + Code: ParseErrCode, + Message: "failed to parse payload: " + err.Error(), + } + } + + switch p := parsed.(type) { + case *payload.Hash: + return b.verifyBlockMessage(ctx, p) + default: + b.stats.IncMessageParseFail() + return &common.AppError{ + Code: ParseErrCode, + Message: fmt.Sprintf("unknown payload type: %T", p), + } + } +} + +// verifyBlockMessage returns nil if blockHashPayload contains the ID +// of an accepted block indicating it should be signed by the VM. +func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) *common.AppError { + blockID := blockHashPayload.Hash + _, err := b.blockClient.GetAcceptedBlock(ctx, blockID) + if err != nil { + b.stats.IncBlockSignatureValidationFail() + return &common.AppError{ + Code: VerifyErrCode, + Message: fmt.Sprintf("failed to get block %s: %s", blockID, err.Error()), + } + } + + return nil +} diff --git a/warp/verifier_backend_test.go b/warp/verifier_backend_test.go new file mode 100644 index 0000000000..54fd8dbf19 --- /dev/null +++ b/warp/verifier_backend_test.go @@ -0,0 +1,255 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "context" + "testing" + "time" + + "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p/acp118" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + avalancheWarp "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ava-labs/subnet-evm/warp/warptest" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestAddressedCallSignatures(t *testing.T) { + database := memdb.New() + snowCtx := utils.TestSnowContext() + blsSecretKey, err := bls.NewSecretKey() + require.NoError(t, err) + warpSigner := avalancheWarp.NewSigner(blsSecretKey, snowCtx.NetworkID, snowCtx.ChainID) + + offChainPayload, err := payload.NewAddressedCall([]byte{1, 2, 3}, []byte{1, 2, 3}) + require.NoError(t, err) + offchainMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, offChainPayload.Bytes()) + require.NoError(t, err) + offchainSignature, err := warpSigner.Sign(offchainMessage) + require.NoError(t, err) + + tests := map[string]struct { + setup func(backend Backend) (request []byte, expectedResponse []byte) + verifyStats func(t *testing.T, stats *verifierStats) + err error + }{ + "known message": { + setup: func(backend Backend) (request []byte, expectedResponse []byte) { + knownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("test")) + require.NoError(t, err) + msg, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, knownPayload.Bytes()) + require.NoError(t, err) + signature, err := warpSigner.Sign(msg) + require.NoError(t, err) + + backend.AddMessage(msg) + return msg.Bytes(), signature[:] + }, + verifyStats: func(t *testing.T, stats *verifierStats) { + require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count()) + require.EqualValues(t, 0, stats.blockSignatureValidationFail.Snapshot().Count()) + }, + }, + "offchain message": { + setup: func(_ Backend) (request []byte, expectedResponse []byte) { + return offchainMessage.Bytes(), offchainSignature[:] + }, + verifyStats: func(t *testing.T, stats *verifierStats) { + require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count()) + require.EqualValues(t, 0, stats.blockSignatureValidationFail.Snapshot().Count()) + }, + }, + "unknown message": { + setup: func(_ Backend) (request []byte, expectedResponse []byte) { + unknownPayload, err := payload.NewAddressedCall([]byte{0, 0, 0}, []byte("unknown message")) + require.NoError(t, err) + unknownMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, unknownPayload.Bytes()) + require.NoError(t, err) + return unknownMessage.Bytes(), nil + }, + verifyStats: func(t *testing.T, stats *verifierStats) { + require.EqualValues(t, 1, stats.messageParseFail.Snapshot().Count()) + require.EqualValues(t, 0, stats.blockSignatureValidationFail.Snapshot().Count()) + }, + err: &common.AppError{Code: ParseErrCode}, + }, + } + + for name, test := range tests { + for _, withCache := range []bool{true, false} { + if withCache { + name += "_with_cache" + } else { + name += "_no_cache" + } + t.Run(name, func(t *testing.T) { + var sigCache cache.Cacher[ids.ID, []byte] + if withCache { + sigCache = &cache.LRU[ids.ID, []byte]{Size: 100} + } else { + sigCache = &cache.Empty[ids.ID, []byte]{} + } + warpBackend, err := NewBackend(snowCtx.NetworkID, snowCtx.ChainID, warpSigner, warptest.EmptyBlockClient, database, sigCache, [][]byte{offchainMessage.Bytes()}) + require.NoError(t, err) + handler := acp118.NewCachedHandler(sigCache, warpBackend, warpSigner) + + requestBytes, expectedResponse := test.setup(warpBackend) + protoMsg := &sdk.SignatureRequest{Message: requestBytes} + protoBytes, err := proto.Marshal(protoMsg) + require.NoError(t, err) + responseBytes, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + if test.err != nil { + require.Error(t, appErr) + require.ErrorIs(t, appErr, test.err) + } else { + require.Nil(t, appErr) + } + + test.verifyStats(t, warpBackend.(*backend).stats) + + // If the expected response is empty, assert that the handler returns an empty response and return early. + if len(expectedResponse) == 0 { + require.Len(t, responseBytes, 0, "expected response to be empty") + return + } + // check cache is populated + if withCache { + require.NotZero(t, warpBackend.(*backend).signatureCache.Len()) + } else { + require.Zero(t, warpBackend.(*backend).signatureCache.Len()) + } + response := &sdk.SignatureResponse{} + require.NoError(t, proto.Unmarshal(responseBytes, response)) + require.NoError(t, err, "error unmarshalling SignatureResponse") + + require.Equal(t, expectedResponse, response.Signature) + }) + } + } +} + +func TestBlockSignatures(t *testing.T) { + database := memdb.New() + snowCtx := utils.TestSnowContext() + blsSecretKey, err := bls.NewSecretKey() + require.NoError(t, err) + + warpSigner := avalancheWarp.NewSigner(blsSecretKey, snowCtx.NetworkID, snowCtx.ChainID) + knownBlkID := ids.GenerateTestID() + blockClient := warptest.MakeBlockClient(knownBlkID) + + toMessageBytes := func(id ids.ID) []byte { + idPayload, err := payload.NewHash(id) + if err != nil { + panic(err) + } + + msg, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, idPayload.Bytes()) + if err != nil { + panic(err) + } + + return msg.Bytes() + } + + tests := map[string]struct { + setup func() (request []byte, expectedResponse []byte) + verifyStats func(t *testing.T, stats *verifierStats) + err error + }{ + "known block": { + setup: func() (request []byte, expectedResponse []byte) { + hashPayload, err := payload.NewHash(knownBlkID) + require.NoError(t, err) + unsignedMessage, err := avalancheWarp.NewUnsignedMessage(snowCtx.NetworkID, snowCtx.ChainID, hashPayload.Bytes()) + require.NoError(t, err) + signature, err := warpSigner.Sign(unsignedMessage) + require.NoError(t, err) + return toMessageBytes(knownBlkID), signature[:] + }, + verifyStats: func(t *testing.T, stats *verifierStats) { + require.EqualValues(t, 0, stats.blockSignatureValidationFail.Snapshot().Count()) + require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count()) + }, + }, + "unknown block": { + setup: func() (request []byte, expectedResponse []byte) { + unknownBlockID := ids.GenerateTestID() + return toMessageBytes(unknownBlockID), nil + }, + verifyStats: func(t *testing.T, stats *verifierStats) { + require.EqualValues(t, 1, stats.blockSignatureValidationFail.Snapshot().Count()) + require.EqualValues(t, 0, stats.messageParseFail.Snapshot().Count()) + }, + err: &common.AppError{Code: VerifyErrCode}, + }, + } + + for name, test := range tests { + for _, withCache := range []bool{true, false} { + if withCache { + name += "_with_cache" + } else { + name += "_no_cache" + } + t.Run(name, func(t *testing.T) { + var sigCache cache.Cacher[ids.ID, []byte] + if withCache { + sigCache = &cache.LRU[ids.ID, []byte]{Size: 100} + } else { + sigCache = &cache.Empty[ids.ID, []byte]{} + } + warpBackend, err := NewBackend( + snowCtx.NetworkID, + snowCtx.ChainID, + warpSigner, + blockClient, + database, + sigCache, + nil, + ) + require.NoError(t, err) + handler := acp118.NewCachedHandler(sigCache, warpBackend, warpSigner) + + requestBytes, expectedResponse := test.setup() + protoMsg := &sdk.SignatureRequest{Message: requestBytes} + protoBytes, err := proto.Marshal(protoMsg) + require.NoError(t, err) + responseBytes, appErr := handler.AppRequest(context.Background(), ids.GenerateTestNodeID(), time.Time{}, protoBytes) + if test.err != nil { + require.NotNil(t, appErr) + require.ErrorIs(t, test.err, appErr) + } else { + require.Nil(t, appErr) + } + + test.verifyStats(t, warpBackend.(*backend).stats) + + // If the expected response is empty, assert that the handler returns an empty response and return early. + if len(expectedResponse) == 0 { + require.Len(t, responseBytes, 0, "expected response to be empty") + return + } + // check cache is populated + if withCache { + require.NotZero(t, warpBackend.(*backend).signatureCache.Len()) + } else { + require.Zero(t, warpBackend.(*backend).signatureCache.Len()) + } + var response sdk.SignatureResponse + err = proto.Unmarshal(responseBytes, &response) + require.NoError(t, err, "error unmarshalling SignatureResponse") + require.Equal(t, expectedResponse, response.Signature) + }) + } + } +} diff --git a/warp/verifier_stats.go b/warp/verifier_stats.go new file mode 100644 index 0000000000..bc56d725a5 --- /dev/null +++ b/warp/verifier_stats.go @@ -0,0 +1,29 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package warp + +import ( + "github.com/ava-labs/subnet-evm/metrics" +) + +type verifierStats struct { + messageParseFail metrics.Counter + // BlockRequest metrics + blockSignatureValidationFail metrics.Counter +} + +func newVerifierStats() *verifierStats { + return &verifierStats{ + messageParseFail: metrics.NewRegisteredCounter("message_parse_fail", nil), + blockSignatureValidationFail: metrics.NewRegisteredCounter("block_signature_validation_fail", nil), + } +} + +func (h *verifierStats) IncBlockSignatureValidationFail() { + h.blockSignatureValidationFail.Inc(1) +} + +func (h *verifierStats) IncMessageParseFail() { + h.messageParseFail.Inc(1) +} From d9c210ab05ed170a1ec938ff79f9490d1f744d6c Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Wed, 30 Oct 2024 19:05:35 +0300 Subject: [PATCH 2/6] Validator state (#1371) * add validator state * remove stuttering name * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * use update enum * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * respond to comments * update avalanchego dep branch * fix test changes * fix upgrades after deactivating latest in context * use branch commit for ava version * add listener mock --------- Signed-off-by: Ceyhun Onur Co-authored-by: Darioush Jalali --- plugin/evm/validators/codec.go | 34 +++ plugin/evm/validators/mock_listener.go | 76 ++++++ plugin/evm/validators/state.go | 327 +++++++++++++++++++++++++ plugin/evm/validators/state_test.go | 250 +++++++++++++++++++ scripts/mocks.mockgen.txt | 1 + 5 files changed, 688 insertions(+) create mode 100644 plugin/evm/validators/codec.go create mode 100644 plugin/evm/validators/mock_listener.go create mode 100644 plugin/evm/validators/state.go create mode 100644 plugin/evm/validators/state_test.go diff --git a/plugin/evm/validators/codec.go b/plugin/evm/validators/codec.go new file mode 100644 index 0000000000..dadba8b273 --- /dev/null +++ b/plugin/evm/validators/codec.go @@ -0,0 +1,34 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators + +import ( + "math" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" + "github.com/ava-labs/avalanchego/utils/wrappers" +) + +const ( + codecVersion = uint16(0) +) + +var vdrCodec codec.Manager + +func init() { + vdrCodec = codec.NewManager(math.MaxInt32) + c := linearcodec.NewDefault() + + errs := wrappers.Errs{} + errs.Add( + c.RegisterType(validatorData{}), + + vdrCodec.RegisterCodec(codecVersion, c), + ) + + if errs.Errored() { + panic(errs.Err) + } +} diff --git a/plugin/evm/validators/mock_listener.go b/plugin/evm/validators/mock_listener.go new file mode 100644 index 0000000000..d67703007d --- /dev/null +++ b/plugin/evm/validators/mock_listener.go @@ -0,0 +1,76 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/subnet-evm/plugin/evm/validators (interfaces: StateCallbackListener) +// +// Generated by this command: +// +// mockgen -package=validators -destination=plugin/evm/validators/mock_listener.go github.com/ava-labs/subnet-evm/plugin/evm/validators StateCallbackListener +// + +// Package validators is a generated GoMock package. +package validators + +import ( + reflect "reflect" + + ids "github.com/ava-labs/avalanchego/ids" + gomock "go.uber.org/mock/gomock" +) + +// MockStateCallbackListener is a mock of StateCallbackListener interface. +type MockStateCallbackListener struct { + ctrl *gomock.Controller + recorder *MockStateCallbackListenerMockRecorder +} + +// MockStateCallbackListenerMockRecorder is the mock recorder for MockStateCallbackListener. +type MockStateCallbackListenerMockRecorder struct { + mock *MockStateCallbackListener +} + +// NewMockStateCallbackListener creates a new mock instance. +func NewMockStateCallbackListener(ctrl *gomock.Controller) *MockStateCallbackListener { + mock := &MockStateCallbackListener{ctrl: ctrl} + mock.recorder = &MockStateCallbackListenerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStateCallbackListener) EXPECT() *MockStateCallbackListenerMockRecorder { + return m.recorder +} + +// OnValidatorAdded mocks base method. +func (m *MockStateCallbackListener) OnValidatorAdded(arg0 ids.ID, arg1 ids.NodeID, arg2 uint64, arg3 bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "OnValidatorAdded", arg0, arg1, arg2, arg3) +} + +// OnValidatorAdded indicates an expected call of OnValidatorAdded. +func (mr *MockStateCallbackListenerMockRecorder) OnValidatorAdded(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnValidatorAdded", reflect.TypeOf((*MockStateCallbackListener)(nil).OnValidatorAdded), arg0, arg1, arg2, arg3) +} + +// OnValidatorRemoved mocks base method. +func (m *MockStateCallbackListener) OnValidatorRemoved(arg0 ids.ID, arg1 ids.NodeID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "OnValidatorRemoved", arg0, arg1) +} + +// OnValidatorRemoved indicates an expected call of OnValidatorRemoved. +func (mr *MockStateCallbackListenerMockRecorder) OnValidatorRemoved(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnValidatorRemoved", reflect.TypeOf((*MockStateCallbackListener)(nil).OnValidatorRemoved), arg0, arg1) +} + +// OnValidatorStatusUpdated mocks base method. +func (m *MockStateCallbackListener) OnValidatorStatusUpdated(arg0 ids.ID, arg1 ids.NodeID, arg2 bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "OnValidatorStatusUpdated", arg0, arg1, arg2) +} + +// OnValidatorStatusUpdated indicates an expected call of OnValidatorStatusUpdated. +func (mr *MockStateCallbackListenerMockRecorder) OnValidatorStatusUpdated(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnValidatorStatusUpdated", reflect.TypeOf((*MockStateCallbackListener)(nil).OnValidatorStatusUpdated), arg0, arg1, arg2) +} diff --git a/plugin/evm/validators/state.go b/plugin/evm/validators/state.go new file mode 100644 index 0000000000..f30418c220 --- /dev/null +++ b/plugin/evm/validators/state.go @@ -0,0 +1,327 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators + +import ( + "fmt" + "time" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/uptime" + "github.com/ava-labs/avalanchego/utils/set" +) + +var _ uptime.State = &state{} + +type dbUpdateStatus bool + +var ErrAlreadyExists = fmt.Errorf("validator already exists") + +const ( + updated dbUpdateStatus = true + deleted dbUpdateStatus = false +) + +type State interface { + uptime.State + // AddValidator adds a new validator to the state + AddValidator(vID ids.ID, nodeID ids.NodeID, startTimestamp uint64, isActive bool) error + // DeleteValidator deletes the validator from the state + DeleteValidator(vID ids.ID) error + // WriteState writes the validator state to the disk + WriteState() error + + // SetStatus sets the active status of the validator with the given vID + SetStatus(vID ids.ID, isActive bool) error + // GetStatus returns the active status of the validator with the given vID + GetStatus(vID ids.ID) (bool, error) + + // GetValidationIDs returns the validation IDs in the state + GetValidationIDs() set.Set[ids.ID] + // GetValidatorIDs returns the validator node IDs in the state + GetValidatorIDs() set.Set[ids.NodeID] + + // RegisterListener registers a listener to the state + RegisterListener(StateCallbackListener) +} + +// StateCallbackListener is a listener for the validator state +type StateCallbackListener interface { + // OnValidatorAdded is called when a new validator is added + OnValidatorAdded(vID ids.ID, nodeID ids.NodeID, startTime uint64, isActive bool) + // OnValidatorRemoved is called when a validator is removed + OnValidatorRemoved(vID ids.ID, nodeID ids.NodeID) + // OnValidatorStatusUpdated is called when a validator status is updated + OnValidatorStatusUpdated(vID ids.ID, nodeID ids.NodeID, isActive bool) +} + +type validatorData struct { + UpDuration time.Duration `serialize:"true"` + LastUpdated uint64 `serialize:"true"` + NodeID ids.NodeID `serialize:"true"` + StartTime uint64 `serialize:"true"` + IsActive bool `serialize:"true"` + + validationID ids.ID // database key +} + +type state struct { + data map[ids.ID]*validatorData // vID -> validatorData + index map[ids.NodeID]ids.ID // nodeID -> vID + // updatedData tracks the updates since WriteValidator was last called + updatedData map[ids.ID]dbUpdateStatus // vID -> updated status + db database.Database + + listeners []StateCallbackListener +} + +// NewState creates a new State, it also loads the data from the disk +func NewState(db database.Database) (State, error) { + s := &state{ + index: make(map[ids.NodeID]ids.ID), + data: make(map[ids.ID]*validatorData), + updatedData: make(map[ids.ID]dbUpdateStatus), + db: db, + } + if err := s.loadFromDisk(); err != nil { + return nil, fmt.Errorf("failed to load data from disk: %w", err) + } + return s, nil +} + +// GetUptime returns the uptime of the validator with the given nodeID +func (s *state) GetUptime( + nodeID ids.NodeID, +) (time.Duration, time.Time, error) { + data, err := s.getData(nodeID) + if err != nil { + return 0, time.Time{}, err + } + return data.UpDuration, data.getLastUpdated(), nil +} + +// SetUptime sets the uptime of the validator with the given nodeID +func (s *state) SetUptime( + nodeID ids.NodeID, + upDuration time.Duration, + lastUpdated time.Time, +) error { + data, err := s.getData(nodeID) + if err != nil { + return err + } + data.UpDuration = upDuration + data.setLastUpdated(lastUpdated) + + s.updatedData[data.validationID] = updated + return nil +} + +// GetStartTime returns the start time of the validator with the given nodeID +func (s *state) GetStartTime(nodeID ids.NodeID) (time.Time, error) { + data, err := s.getData(nodeID) + if err != nil { + return time.Time{}, err + } + return data.getStartTime(), nil +} + +// AddValidator adds a new validator to the state +// the new validator is marked as updated and will be written to the disk when WriteState is called +func (s *state) AddValidator(vID ids.ID, nodeID ids.NodeID, startTimestamp uint64, isActive bool) error { + data := &validatorData{ + NodeID: nodeID, + validationID: vID, + IsActive: isActive, + StartTime: startTimestamp, + UpDuration: 0, + LastUpdated: startTimestamp, + } + if err := s.addData(vID, data); err != nil { + return err + } + + s.updatedData[vID] = updated + + for _, listener := range s.listeners { + listener.OnValidatorAdded(vID, nodeID, startTimestamp, isActive) + } + return nil +} + +// DeleteValidator marks the validator as deleted +// marked validator will be deleted from disk when WriteState is called +func (s *state) DeleteValidator(vID ids.ID) error { + data, exists := s.data[vID] + if !exists { + return database.ErrNotFound + } + delete(s.data, data.validationID) + delete(s.index, data.NodeID) + + // mark as deleted for WriteValidator + s.updatedData[data.validationID] = deleted + + for _, listener := range s.listeners { + listener.OnValidatorRemoved(vID, data.NodeID) + } + return nil +} + +// WriteState writes the updated state to the disk +func (s *state) WriteState() error { + // TODO: consider adding batch size + batch := s.db.NewBatch() + for vID, updateStatus := range s.updatedData { + switch updateStatus { + case updated: + data := s.data[vID] + + dataBytes, err := vdrCodec.Marshal(codecVersion, data) + if err != nil { + return err + } + if err := batch.Put(vID[:], dataBytes); err != nil { + return err + } + case deleted: + if err := batch.Delete(vID[:]); err != nil { + return err + } + default: + return fmt.Errorf("unknown update status for %s", vID) + } + // we're done, remove the updated marker + delete(s.updatedData, vID) + } + return batch.Write() +} + +// SetStatus sets the active status of the validator with the given vID +func (s *state) SetStatus(vID ids.ID, isActive bool) error { + data, exists := s.data[vID] + if !exists { + return database.ErrNotFound + } + data.IsActive = isActive + s.updatedData[vID] = updated + + for _, listener := range s.listeners { + listener.OnValidatorStatusUpdated(vID, data.NodeID, isActive) + } + return nil +} + +// GetStatus returns the active status of the validator with the given vID +func (s *state) GetStatus(vID ids.ID) (bool, error) { + data, exists := s.data[vID] + if !exists { + return false, database.ErrNotFound + } + return data.IsActive, nil +} + +// GetValidationIDs returns the validation IDs in the state +func (s *state) GetValidationIDs() set.Set[ids.ID] { + ids := set.NewSet[ids.ID](len(s.data)) + for vID := range s.data { + ids.Add(vID) + } + return ids +} + +// GetValidatorIDs returns the validator IDs in the state +func (s *state) GetValidatorIDs() set.Set[ids.NodeID] { + ids := set.NewSet[ids.NodeID](len(s.index)) + for nodeID := range s.index { + ids.Add(nodeID) + } + return ids +} + +// RegisterListener registers a listener to the state +// OnValidatorAdded is called for all current validators on the provided listener before this function returns +func (s *state) RegisterListener(listener StateCallbackListener) { + s.listeners = append(s.listeners, listener) + + // notify the listener of the current state + for vID, data := range s.data { + listener.OnValidatorAdded(vID, data.NodeID, data.StartTime, data.IsActive) + } +} + +// parseValidatorData parses the data from the bytes into given validatorData +func parseValidatorData(bytes []byte, data *validatorData) error { + if len(bytes) != 0 { + if _, err := vdrCodec.Unmarshal(bytes, data); err != nil { + return err + } + } + return nil +} + +// Load the state from the disk +func (s *state) loadFromDisk() error { + it := s.db.NewIterator() + defer it.Release() + for it.Next() { + vIDBytes := it.Key() + vID, err := ids.ToID(vIDBytes) + if err != nil { + return fmt.Errorf("failed to parse validator ID: %w", err) + } + vdr := &validatorData{ + validationID: vID, + } + if err := parseValidatorData(it.Value(), vdr); err != nil { + return fmt.Errorf("failed to parse validator data: %w", err) + } + if err := s.addData(vID, vdr); err != nil { + return err + } + } + return it.Error() +} + +// addData adds the data to the state +// returns an error if the data already exists +func (s *state) addData(vID ids.ID, data *validatorData) error { + if _, exists := s.data[vID]; exists { + return fmt.Errorf("%w, vID: %s", ErrAlreadyExists, vID) + } + if _, exists := s.index[data.NodeID]; exists { + return fmt.Errorf("%w, nodeID: %s", ErrAlreadyExists, data.NodeID) + } + + s.data[vID] = data + s.index[data.NodeID] = vID + return nil +} + +// getData returns the data for the validator with the given nodeID +// returns database.ErrNotFound if the data does not exist +func (s *state) getData(nodeID ids.NodeID) (*validatorData, error) { + vID, exists := s.index[nodeID] + if !exists { + return nil, database.ErrNotFound + } + data, exists := s.data[vID] + if !exists { + return nil, database.ErrNotFound + } + return data, nil +} + +func (v *validatorData) setLastUpdated(t time.Time) { + v.LastUpdated = uint64(t.Unix()) +} + +func (v *validatorData) getLastUpdated() time.Time { + return time.Unix(int64(v.LastUpdated), 0) +} + +func (v *validatorData) getStartTime() time.Time { + return time.Unix(int64(v.StartTime), 0) +} diff --git a/plugin/evm/validators/state_test.go b/plugin/evm/validators/state_test.go new file mode 100644 index 0000000000..ecfd7d34a9 --- /dev/null +++ b/plugin/evm/validators/state_test.go @@ -0,0 +1,250 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/wrappers" +) + +func TestState(t *testing.T) { + require := require.New(t) + db := memdb.New() + state, err := NewState(db) + require.NoError(err) + + // get non-existent uptime + nodeID := ids.GenerateTestNodeID() + vID := ids.GenerateTestID() + _, _, err = state.GetUptime(nodeID) + require.ErrorIs(err, database.ErrNotFound) + + // set non-existent uptime + startTime := time.Now() + err = state.SetUptime(nodeID, 1, startTime) + require.ErrorIs(err, database.ErrNotFound) + + // add new validator + state.AddValidator(vID, nodeID, uint64(startTime.Unix()), true) + + // adding the same validator should fail + err = state.AddValidator(vID, ids.GenerateTestNodeID(), uint64(startTime.Unix()), true) + require.ErrorIs(err, ErrAlreadyExists) + // adding the same nodeID should fail + err = state.AddValidator(ids.GenerateTestID(), nodeID, uint64(startTime.Unix()), true) + require.ErrorIs(err, ErrAlreadyExists) + + // get uptime + uptime, lastUpdated, err := state.GetUptime(nodeID) + require.NoError(err) + require.Equal(time.Duration(0), uptime) + require.Equal(startTime.Unix(), lastUpdated.Unix()) + + // set uptime + newUptime := 2 * time.Minute + newLastUpdated := lastUpdated.Add(time.Hour) + require.NoError(state.SetUptime(nodeID, newUptime, newLastUpdated)) + // get new uptime + uptime, lastUpdated, err = state.GetUptime(nodeID) + require.NoError(err) + require.Equal(newUptime, uptime) + require.Equal(newLastUpdated, lastUpdated) + + // set status + require.NoError(state.SetStatus(vID, false)) + // get status + status, err := state.GetStatus(vID) + require.NoError(err) + require.False(status) + + // delete uptime + require.NoError(state.DeleteValidator(vID)) + + // get deleted uptime + _, _, err = state.GetUptime(nodeID) + require.ErrorIs(err, database.ErrNotFound) +} + +func TestWriteValidator(t *testing.T) { + require := require.New(t) + db := memdb.New() + state, err := NewState(db) + require.NoError(err) + // write empty uptimes + require.NoError(state.WriteState()) + + // load uptime + nodeID := ids.GenerateTestNodeID() + vID := ids.GenerateTestID() + startTime := time.Now() + require.NoError(state.AddValidator(vID, nodeID, uint64(startTime.Unix()), true)) + + // write state, should reflect to DB + require.NoError(state.WriteState()) + require.True(db.Has(vID[:])) + + // set uptime + newUptime := 2 * time.Minute + newLastUpdated := startTime.Add(time.Hour) + require.NoError(state.SetUptime(nodeID, newUptime, newLastUpdated)) + require.NoError(state.WriteState()) + + // refresh state, should load from DB + state, err = NewState(db) + require.NoError(err) + + // get uptime + uptime, lastUpdated, err := state.GetUptime(nodeID) + require.NoError(err) + require.Equal(newUptime, uptime) + require.Equal(newLastUpdated.Unix(), lastUpdated.Unix()) + + // delete + require.NoError(state.DeleteValidator(vID)) + + // write state, should reflect to DB + require.NoError(state.WriteState()) + require.False(db.Has(vID[:])) +} + +func TestParseValidator(t *testing.T) { + testNodeID, err := ids.NodeIDFromString("NodeID-CaBYJ9kzHvrQFiYWowMkJGAQKGMJqZoat") + require.NoError(t, err) + type test struct { + name string + bytes []byte + expected *validatorData + expectedErr error + } + tests := []test{ + { + name: "nil", + bytes: nil, + expected: &validatorData{ + LastUpdated: 0, + StartTime: 0, + }, + expectedErr: nil, + }, + { + name: "empty", + bytes: []byte{}, + expected: &validatorData{ + LastUpdated: 0, + StartTime: 0, + }, + expectedErr: nil, + }, + { + name: "valid", + bytes: []byte{ + // codec version + 0x00, 0x00, + // up duration + 0x00, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x8D, 0x80, + // last updated + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0xBB, 0xA0, + // node ID + 0x7e, 0xef, 0xe8, 0x8a, 0x45, 0xfb, 0x7a, 0xc4, + 0xb0, 0x59, 0xc9, 0x33, 0x71, 0x0a, 0x57, 0x33, + 0xff, 0x9f, 0x4b, 0xab, + // start time + 0x00, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x8D, 0x80, + // status + 0x01, + }, + expected: &validatorData{ + UpDuration: time.Duration(6000000), + LastUpdated: 900000, + NodeID: testNodeID, + StartTime: 6000000, + IsActive: true, + }, + }, + { + name: "invalid codec version", + bytes: []byte{ + // codec version + 0x00, 0x02, + // up duration + 0x00, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x8D, 0x80, + // last updated + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0xBB, 0xA0, + }, + expected: nil, + expectedErr: codec.ErrUnknownVersion, + }, + { + name: "short byte len", + bytes: []byte{ + // codec version + 0x00, 0x00, + // up duration + 0x00, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x8D, 0x80, + // last updated + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0xBB, 0xA0, + }, + expected: nil, + expectedErr: wrappers.ErrInsufficientLength, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + var data validatorData + err := parseValidatorData(tt.bytes, &data) + require.ErrorIs(err, tt.expectedErr) + if tt.expectedErr != nil { + return + } + require.Equal(tt.expected, &data) + }) + } +} + +func TestStateListener(t *testing.T) { + require := require.New(t) + db := memdb.New() + state, err := NewState(db) + require.NoError(err) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + expectedvID := ids.GenerateTestID() + expectedNodeID := ids.GenerateTestNodeID() + expectedStartTime := time.Now() + mockListener := NewMockStateCallbackListener(ctrl) + // add initial validator to test RegisterListener + initialvID := ids.GenerateTestID() + initialNodeID := ids.GenerateTestNodeID() + initialStartTime := time.Now() + + // add initial validator + require.NoError(state.AddValidator(initialvID, initialNodeID, uint64(initialStartTime.Unix()), true)) + + // register listener + mockListener.EXPECT().OnValidatorAdded(initialvID, initialNodeID, uint64(initialStartTime.Unix()), true) + state.RegisterListener(mockListener) + + // add new validator + mockListener.EXPECT().OnValidatorAdded(expectedvID, expectedNodeID, uint64(expectedStartTime.Unix()), true) + require.NoError(state.AddValidator(expectedvID, expectedNodeID, uint64(expectedStartTime.Unix()), true)) + + // set status + mockListener.EXPECT().OnValidatorStatusUpdated(expectedvID, expectedNodeID, false) + require.NoError(state.SetStatus(expectedvID, false)) + + // remove validator + mockListener.EXPECT().OnValidatorRemoved(expectedvID, expectedNodeID) + require.NoError(state.DeleteValidator(expectedvID)) +} diff --git a/scripts/mocks.mockgen.txt b/scripts/mocks.mockgen.txt index 391dc8e13c..73aa62ccd7 100644 --- a/scripts/mocks.mockgen.txt +++ b/scripts/mocks.mockgen.txt @@ -1,2 +1,3 @@ github.com/ava-labs/subnet-evm/precompile/precompileconfig=Predicater,Config,ChainConfig,Accepter=precompile/precompileconfig/mocks.go github.com/ava-labs/subnet-evm/precompile/contract=BlockContext,AccessibleState,StateDB=precompile/contract/mocks.go +github.com/ava-labs/subnet-evm/plugin/evm/validators=StateCallbackListener=plugin/evm/validators/mock_listener.go From 4e0e5d264a7acfa516775c122936453bea19637b Mon Sep 17 00:00:00 2001 From: Darioush Jalali Date: Mon, 4 Nov 2024 10:13:00 -0800 Subject: [PATCH 3/6] tests: fix use of require.EventuallyWithT (#1376) --- core/test_blockchain.go | 2 +- plugin/evm/gossip_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/test_blockchain.go b/core/test_blockchain.go index 02f98b61cf..54a872f8aa 100644 --- a/core/test_blockchain.go +++ b/core/test_blockchain.go @@ -1685,7 +1685,7 @@ func checkTxIndicesHelper(t *testing.T, expectedTail *uint64, indexedFrom uint64 require.EventuallyWithTf(t, func(c *assert.CollectT) { stored = *rawdb.ReadTxIndexTail(db) - require.Equalf(t, tailValue, stored, "expected tail to be %d, found %d", tailValue, stored) + assert.Equalf(c, tailValue, stored, "expected tail to be %d, found %d", tailValue, stored) }, 30*time.Second, 500*time.Millisecond, "expected tail to be %d eventually", tailValue) } diff --git a/plugin/evm/gossip_test.go b/plugin/evm/gossip_test.go index 551d3a8075..8e0b82ec0f 100644 --- a/plugin/evm/gossip_test.go +++ b/plugin/evm/gossip_test.go @@ -81,7 +81,7 @@ func TestGossipSubscribe(t *testing.T) { defer gossipTxPool.lock.RUnlock() for i, tx := range ethTxs { - require.Truef(gossipTxPool.bloom.Has(&GossipEthTx{Tx: tx}), "expected tx[%d] to be in bloom filter", i) + assert.Truef(c, gossipTxPool.bloom.Has(&GossipEthTx{Tx: tx}), "expected tx[%d] to be in bloom filter", i) } }, 30*time.Second, From 951e87527d5238852dadd0910216e3dfa8f7a42e Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Tue, 5 Nov 2024 03:01:28 +0300 Subject: [PATCH 4/6] use verdb in accepted and metadb (#1375) Co-authored-by: Darioush Jalali --- plugin/evm/vm.go | 4 ++-- plugin/evm/vm_test.go | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 7d4cf631fc..865042c19b 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -1279,8 +1279,8 @@ func (vm *VM) initializeDBs(avaDB database.Database) error { // remains the same regardless of the provided baseDB type. vm.chaindb = rawdb.NewDatabase(Database{prefixdb.NewNested(ethDBPrefix, db)}) vm.db = versiondb.New(db) - vm.acceptedBlockDB = prefixdb.New(acceptedPrefix, db) - vm.metadataDB = prefixdb.New(metadataPrefix, db) + vm.acceptedBlockDB = prefixdb.New(acceptedPrefix, vm.db) + vm.metadataDB = prefixdb.New(metadataPrefix, vm.db) // Note warpDB is not part of versiondb because it is not necessary // that warp signatures are committed to the database atomically with // the last accepted block. diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 3c12de266f..13a22a8e81 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -189,8 +189,10 @@ func setupGenesis( genesisBytes := buildGenesisTest(t, genesisJSON) ctx := NewContext() + baseDB := memdb.New() + // initialize the atomic memory - atomicMemory := atomic.NewMemory(prefixdb.New([]byte{0}, memdb.New())) + atomicMemory := atomic.NewMemory(prefixdb.New([]byte{0}, baseDB)) ctx.SharedMemory = atomicMemory.NewSharedMemory(ctx.ChainID) // NB: this lock is intentionally left locked when this function returns. @@ -204,7 +206,8 @@ func setupGenesis( ctx.Keystore = userKeystore.NewBlockchainKeyStore(ctx.ChainID) issuer := make(chan commonEng.Message, 1) - return ctx, memdb.New(), genesisBytes, issuer, atomicMemory + prefixedDB := prefixdb.New([]byte{1}, baseDB) + return ctx, prefixedDB, genesisBytes, issuer, atomicMemory } // GenesisVM creates a VM instance with the genesis test bytes and returns From 8a6df9e5de4523d310656e1cf05cdf7e3a3ecbd4 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Wed, 6 Nov 2024 00:48:41 +0300 Subject: [PATCH 5/6] Pausable uptime manager (#1372) * add validator state * add pausable uptime manager * remove stuttering name * rename state listener * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * use update enum * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * respond to comments * update avalanchego dep branch * reviews * reword errs * fix test changes * fix upgrades after deactivating latest in context * use branch commit for ava version * reviews * add listener mock * remove errs from resume and pause * check after stopping * use expectedTime in tests * reviews * fix requires * underscore unused params --------- Signed-off-by: Ceyhun Onur Co-authored-by: Darioush Jalali --- plugin/evm/uptime/pausable_manager.go | 144 ++++++++++++ plugin/evm/uptime/pausable_manager_test.go | 241 +++++++++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 plugin/evm/uptime/pausable_manager.go create mode 100644 plugin/evm/uptime/pausable_manager_test.go diff --git a/plugin/evm/uptime/pausable_manager.go b/plugin/evm/uptime/pausable_manager.go new file mode 100644 index 0000000000..2b62f7de16 --- /dev/null +++ b/plugin/evm/uptime/pausable_manager.go @@ -0,0 +1,144 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package uptime + +import ( + "errors" + + "github.com/ava-labs/subnet-evm/plugin/evm/validators" + "github.com/ethereum/go-ethereum/log" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/uptime" + "github.com/ava-labs/avalanchego/utils/set" +) + +var _ validators.StateCallbackListener = &pausableManager{} + +var errPausedDisconnect = errors.New("paused node cannot be disconnected") + +type PausableManager interface { + uptime.Manager + validators.StateCallbackListener + IsPaused(nodeID ids.NodeID) bool +} + +type pausableManager struct { + uptime.Manager + pausedVdrs set.Set[ids.NodeID] + // connectedVdrs is a set of nodes that are connected to the manager. + // This is used to immediately connect nodes when they are unpaused. + connectedVdrs set.Set[ids.NodeID] +} + +// NewPausableManager takes an uptime.Manager and returns a PausableManager +func NewPausableManager(manager uptime.Manager) PausableManager { + return &pausableManager{ + pausedVdrs: make(set.Set[ids.NodeID]), + connectedVdrs: make(set.Set[ids.NodeID]), + Manager: manager, + } +} + +// Connect connects the node with the given ID to the uptime.Manager +// If the node is paused, it will not be connected +func (p *pausableManager) Connect(nodeID ids.NodeID) error { + p.connectedVdrs.Add(nodeID) + if !p.IsPaused(nodeID) && !p.Manager.IsConnected(nodeID) { + return p.Manager.Connect(nodeID) + } + return nil +} + +// Disconnect disconnects the node with the given ID from the uptime.Manager +// If the node is paused, it will not be disconnected +// Invariant: we should never have a connected paused node that is disconnecting +func (p *pausableManager) Disconnect(nodeID ids.NodeID) error { + p.connectedVdrs.Remove(nodeID) + if p.Manager.IsConnected(nodeID) { + if p.IsPaused(nodeID) { + // We should never see this case + return errPausedDisconnect + } + return p.Manager.Disconnect(nodeID) + } + return nil +} + +// StartTracking starts tracking uptime for the nodes with the given IDs +// If a node is paused, it will not be tracked +func (p *pausableManager) StartTracking(nodeIDs []ids.NodeID) error { + activeNodeIDs := make([]ids.NodeID, 0, len(nodeIDs)) + for _, nodeID := range nodeIDs { + if !p.IsPaused(nodeID) { + activeNodeIDs = append(activeNodeIDs, nodeID) + } + } + return p.Manager.StartTracking(activeNodeIDs) +} + +// OnValidatorAdded is called when a validator is added. +// If the node is inactive, it will be paused. +func (p *pausableManager) OnValidatorAdded(_ ids.ID, nodeID ids.NodeID, _ uint64, isActive bool) { + if !isActive { + err := p.pause(nodeID) + if err != nil { + log.Error("failed to handle added validator %s: %s", nodeID, err) + } + } +} + +// OnValidatorRemoved is called when a validator is removed. +// If the node is already paused, it will be resumed. +func (p *pausableManager) OnValidatorRemoved(_ ids.ID, nodeID ids.NodeID) { + if p.IsPaused(nodeID) { + err := p.resume(nodeID) + if err != nil { + log.Error("failed to handle validator removed %s: %s", nodeID, err) + } + } +} + +// OnValidatorStatusUpdated is called when the status of a validator is updated. +// If the node is active, it will be resumed. If the node is inactive, it will be paused. +func (p *pausableManager) OnValidatorStatusUpdated(_ ids.ID, nodeID ids.NodeID, isActive bool) { + var err error + if isActive { + err = p.resume(nodeID) + } else { + err = p.pause(nodeID) + } + if err != nil { + log.Error("failed to update status for node %s: %s", nodeID, err) + } +} + +// IsPaused returns true if the node with the given ID is paused. +func (p *pausableManager) IsPaused(nodeID ids.NodeID) bool { + return p.pausedVdrs.Contains(nodeID) +} + +// pause pauses uptime tracking for the node with the given ID +// pause can disconnect the node from the uptime.Manager if it is connected. +func (p *pausableManager) pause(nodeID ids.NodeID) error { + p.pausedVdrs.Add(nodeID) + if p.Manager.IsConnected(nodeID) { + // If the node is connected, then we need to disconnect it from + // manager + // This should be fine in case tracking has not started yet since + // the inner manager should handle disconnects accordingly + return p.Manager.Disconnect(nodeID) + } + return nil +} + +// resume resumes uptime tracking for the node with the given ID +// resume can connect the node to the uptime.Manager if it was connected. +func (p *pausableManager) resume(nodeID ids.NodeID) error { + p.pausedVdrs.Remove(nodeID) + if p.connectedVdrs.Contains(nodeID) && !p.Manager.IsConnected(nodeID) { + return p.Manager.Connect(nodeID) + } + return nil +} diff --git a/plugin/evm/uptime/pausable_manager_test.go b/plugin/evm/uptime/pausable_manager_test.go new file mode 100644 index 0000000000..c93a8bcad4 --- /dev/null +++ b/plugin/evm/uptime/pausable_manager_test.go @@ -0,0 +1,241 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package uptime + +import ( + "testing" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/uptime" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/stretchr/testify/require" +) + +func TestPausableManager(t *testing.T) { + vID := ids.GenerateTestID() + nodeID0 := ids.GenerateTestNodeID() + startTime := time.Now() + + tests := []struct { + name string + testFunc func(t *testing.T, up PausableManager, clk *mockable.Clock, s uptime.State) + }{ + { + name: "Case 1: Connect, pause, start tracking", + testFunc: func(t *testing.T, up PausableManager, clk *mockable.Clock, s uptime.State) { + require := require.New(t) + + // Connect before tracking + require.NoError(up.Connect(nodeID0)) + addTime(clk, time.Second) + + // Pause before tracking + up.OnValidatorStatusUpdated(vID, nodeID0, false) + require.True(up.IsPaused(nodeID0)) + + // Elapse Time + addTime(clk, time.Second) + + // Start tracking + require.NoError(up.StartTracking([]ids.NodeID{nodeID0})) + currentTime := addTime(clk, time.Second) + // Uptime should not have increased since the node was paused + expectedUptime := 0 * time.Second + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Disconnect + require.NoError(up.Disconnect(nodeID0)) + // Uptime should not have increased + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + }, + }, + { + name: "Case 2: Start tracking, connect, pause, re-connect, resume", + testFunc: func(t *testing.T, up PausableManager, clk *mockable.Clock, s uptime.State) { + require := require.New(t) + + // Start tracking + require.NoError(up.StartTracking([]ids.NodeID{nodeID0})) + + // Connect + addTime(clk, 1*time.Second) + require.NoError(up.Connect(nodeID0)) + + // Pause + addTime(clk, 1*time.Second) + up.OnValidatorStatusUpdated(vID, nodeID0, false) + require.True(up.IsPaused(nodeID0)) + + // Elapse time + currentTime := addTime(clk, 2*time.Second) + // Uptime should be 1 second since the node was paused after 1 sec + expectedUptime := 1 * time.Second + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Disconnect and check uptime + currentTime = addTime(clk, 3*time.Second) + require.NoError(up.Disconnect(nodeID0)) + // Uptime should not have increased since the node was paused + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Connect again and check uptime + addTime(clk, 4*time.Second) + require.NoError(up.Connect(nodeID0)) + currentTime = addTime(clk, 5*time.Second) + // Uptime should not have increased since the node was paused + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Resume and check uptime + currentTime = addTime(clk, 6*time.Second) + up.OnValidatorStatusUpdated(vID, nodeID0, true) + require.False(up.IsPaused(nodeID0)) + // Uptime should not have increased since the node was paused + // and we just resumed it + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Elapsed time check + currentTime = addTime(clk, 7*time.Second) + // Uptime should increase by 7 seconds above since the node was resumed + expectedUptime += 7 * time.Second + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + }, + }, + { + name: "Case 3: Pause, start tracking, connect, re-connect, resume", + testFunc: func(t *testing.T, up PausableManager, clk *mockable.Clock, s uptime.State) { + require := require.New(t) + + // Pause before tracking + up.OnValidatorStatusUpdated(vID, nodeID0, false) + require.True(up.IsPaused(nodeID0)) + + // Start tracking + addTime(clk, time.Second) + require.NoError(up.StartTracking([]ids.NodeID{nodeID0})) + + // Connect and check uptime + addTime(clk, 1*time.Second) + require.NoError(up.Connect(nodeID0)) + + currentTime := addTime(clk, 2*time.Second) + // Uptime should not have increased since the node was paused + expectedUptime := 0 * time.Second + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Disconnect and check uptime + currentTime = addTime(clk, 3*time.Second) + require.NoError(up.Disconnect(nodeID0)) + // Uptime should not have increased since the node was paused + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Connect again and resume + addTime(clk, 4*time.Second) + require.NoError(up.Connect(nodeID0)) + addTime(clk, 5*time.Second) + up.OnValidatorStatusUpdated(vID, nodeID0, true) + require.False(up.IsPaused(nodeID0)) + + // Check uptime after resume + currentTime = addTime(clk, 6*time.Second) + // Uptime should have increased by 6 seconds since the node was resumed + expectedUptime += 6 * time.Second + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + }, + }, + { + name: "Case 4: Start tracking, connect, pause, stop tracking, resume tracking", + testFunc: func(t *testing.T, up PausableManager, clk *mockable.Clock, s uptime.State) { + require := require.New(t) + + // Start tracking and connect + require.NoError(up.StartTracking([]ids.NodeID{nodeID0})) + addTime(clk, time.Second) + require.NoError(up.Connect(nodeID0)) + + // Pause and check uptime + currentTime := addTime(clk, 2*time.Second) + up.OnValidatorStatusUpdated(vID, nodeID0, false) + require.True(up.IsPaused(nodeID0)) + // Uptime should be 2 seconds since the node was paused after 2 seconds + expectedUptime := 2 * time.Second + + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Stop tracking and reinitialize manager + currentTime = addTime(clk, 3*time.Second) + require.NoError(up.StopTracking([]ids.NodeID{nodeID0})) + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + up = NewPausableManager(uptime.NewManager(s, clk)) + + // Uptime should not have increased since the node was paused + // and we have not started tracking again + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Pause and check uptime + up.OnValidatorStatusUpdated(vID, nodeID0, false) + require.True(up.IsPaused(nodeID0)) + // Uptime should not have increased since the node was paused + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Resume and check uptime + currentTime = addTime(clk, 5*time.Second) + up.OnValidatorStatusUpdated(vID, nodeID0, true) + require.False(up.IsPaused(nodeID0)) + // Uptime should have increased by 5 seconds since the node was resumed + expectedUptime += 5 * time.Second + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Start tracking and check elapsed time + currentTime = addTime(clk, 6*time.Second) + require.NoError(up.StartTracking([]ids.NodeID{nodeID0})) + // Uptime should have increased by 6 seconds since we started tracking + // and node was resumed (we assume the node was online until we started tracking) + expectedUptime += 6 * time.Second + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Elapsed time + currentTime = addTime(clk, 7*time.Second) + // Uptime should not have increased since the node was not connected + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + + // Connect and final uptime check + require.NoError(up.Connect(nodeID0)) + currentTime = addTime(clk, 8*time.Second) + // Uptime should have increased by 8 seconds since the node was connected + expectedUptime += 8 * time.Second + checkUptime(t, up, nodeID0, expectedUptime, currentTime) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + up, clk, s := setupTestEnv(nodeID0, startTime) + test.testFunc(t, up, clk, s) + }) + } +} + +func setupTestEnv(nodeID ids.NodeID, startTime time.Time) (PausableManager, *mockable.Clock, uptime.State) { + clk := mockable.Clock{} + clk.Set(startTime) + s := uptime.NewTestState() + s.AddNode(nodeID, startTime) + up := NewPausableManager(uptime.NewManager(s, &clk)) + return up, &clk, s +} + +func addTime(clk *mockable.Clock, duration time.Duration) time.Time { + clk.Set(clk.Time().Add(duration)) + return clk.Time() +} + +func checkUptime(t *testing.T, up PausableManager, nodeID ids.NodeID, expectedUptime time.Duration, expectedLastUpdate time.Time) { + t.Helper() + uptime, lastUpdated, err := up.CalculateUptime(nodeID) + require.NoError(t, err) + require.Equal(t, expectedLastUpdate.Unix(), lastUpdated.Unix()) + require.Equal(t, expectedUptime, uptime) +} From 05f179ca6f35070e34c9e457f7ad6bf0ee2c1299 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Wed, 6 Nov 2024 01:17:08 +0300 Subject: [PATCH 6/6] move interfaces to separate pkgs (#1379) * add validator state * add pausable uptime manager * remove stuttering name * rename state listener * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * use update enum * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * Update plugin/evm/validators/state.go Co-authored-by: Darioush Jalali Signed-off-by: Ceyhun Onur * respond to comments * update avalanchego dep branch * reviews * reword errs * fix test changes * fix upgrades after deactivating latest in context * use branch commit for ava version * reviews * add listener mock * remove errs from resume and pause * check after stopping * use expectedTime in tests * reviews * move interfaces to separate pkgs * regen mock --------- Signed-off-by: Ceyhun Onur Co-authored-by: Darioush Jalali --- plugin/evm/uptime/interfaces/interface.go | 16 +++++++ plugin/evm/uptime/pausable_manager.go | 12 +----- plugin/evm/uptime/pausable_manager_test.go | 15 ++++--- plugin/evm/validators/interfaces/interface.go | 43 +++++++++++++++++++ .../{ => interfaces}/mock_listener.go | 8 ++-- plugin/evm/validators/state.go | 40 ++--------------- plugin/evm/validators/state_test.go | 3 +- scripts/mocks.mockgen.txt | 2 +- 8 files changed, 80 insertions(+), 59 deletions(-) create mode 100644 plugin/evm/uptime/interfaces/interface.go create mode 100644 plugin/evm/validators/interfaces/interface.go rename plugin/evm/validators/{ => interfaces}/mock_listener.go (88%) diff --git a/plugin/evm/uptime/interfaces/interface.go b/plugin/evm/uptime/interfaces/interface.go new file mode 100644 index 0000000000..13e6b7abba --- /dev/null +++ b/plugin/evm/uptime/interfaces/interface.go @@ -0,0 +1,16 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interfaces + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/uptime" + validatorsinterfaces "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" +) + +type PausableManager interface { + uptime.Manager + validatorsinterfaces.StateCallbackListener + IsPaused(nodeID ids.NodeID) bool +} diff --git a/plugin/evm/uptime/pausable_manager.go b/plugin/evm/uptime/pausable_manager.go index 2b62f7de16..3976fcae6c 100644 --- a/plugin/evm/uptime/pausable_manager.go +++ b/plugin/evm/uptime/pausable_manager.go @@ -6,7 +6,7 @@ package uptime import ( "errors" - "github.com/ava-labs/subnet-evm/plugin/evm/validators" + "github.com/ava-labs/subnet-evm/plugin/evm/uptime/interfaces" "github.com/ethereum/go-ethereum/log" "github.com/ava-labs/avalanchego/ids" @@ -14,16 +14,8 @@ import ( "github.com/ava-labs/avalanchego/utils/set" ) -var _ validators.StateCallbackListener = &pausableManager{} - var errPausedDisconnect = errors.New("paused node cannot be disconnected") -type PausableManager interface { - uptime.Manager - validators.StateCallbackListener - IsPaused(nodeID ids.NodeID) bool -} - type pausableManager struct { uptime.Manager pausedVdrs set.Set[ids.NodeID] @@ -33,7 +25,7 @@ type pausableManager struct { } // NewPausableManager takes an uptime.Manager and returns a PausableManager -func NewPausableManager(manager uptime.Manager) PausableManager { +func NewPausableManager(manager uptime.Manager) interfaces.PausableManager { return &pausableManager{ pausedVdrs: make(set.Set[ids.NodeID]), connectedVdrs: make(set.Set[ids.NodeID]), diff --git a/plugin/evm/uptime/pausable_manager_test.go b/plugin/evm/uptime/pausable_manager_test.go index c93a8bcad4..e1f4f4a6f8 100644 --- a/plugin/evm/uptime/pausable_manager_test.go +++ b/plugin/evm/uptime/pausable_manager_test.go @@ -10,6 +10,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/subnet-evm/plugin/evm/uptime/interfaces" "github.com/stretchr/testify/require" ) @@ -20,11 +21,11 @@ func TestPausableManager(t *testing.T) { tests := []struct { name string - testFunc func(t *testing.T, up PausableManager, clk *mockable.Clock, s uptime.State) + testFunc func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) }{ { name: "Case 1: Connect, pause, start tracking", - testFunc: func(t *testing.T, up PausableManager, clk *mockable.Clock, s uptime.State) { + testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) { require := require.New(t) // Connect before tracking @@ -53,7 +54,7 @@ func TestPausableManager(t *testing.T) { }, { name: "Case 2: Start tracking, connect, pause, re-connect, resume", - testFunc: func(t *testing.T, up PausableManager, clk *mockable.Clock, s uptime.State) { + testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) { require := require.New(t) // Start tracking @@ -104,7 +105,7 @@ func TestPausableManager(t *testing.T) { }, { name: "Case 3: Pause, start tracking, connect, re-connect, resume", - testFunc: func(t *testing.T, up PausableManager, clk *mockable.Clock, s uptime.State) { + testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) { require := require.New(t) // Pause before tracking @@ -146,7 +147,7 @@ func TestPausableManager(t *testing.T) { }, { name: "Case 4: Start tracking, connect, pause, stop tracking, resume tracking", - testFunc: func(t *testing.T, up PausableManager, clk *mockable.Clock, s uptime.State) { + testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) { require := require.New(t) // Start tracking and connect @@ -218,7 +219,7 @@ func TestPausableManager(t *testing.T) { } } -func setupTestEnv(nodeID ids.NodeID, startTime time.Time) (PausableManager, *mockable.Clock, uptime.State) { +func setupTestEnv(nodeID ids.NodeID, startTime time.Time) (interfaces.PausableManager, *mockable.Clock, uptime.State) { clk := mockable.Clock{} clk.Set(startTime) s := uptime.NewTestState() @@ -232,7 +233,7 @@ func addTime(clk *mockable.Clock, duration time.Duration) time.Time { return clk.Time() } -func checkUptime(t *testing.T, up PausableManager, nodeID ids.NodeID, expectedUptime time.Duration, expectedLastUpdate time.Time) { +func checkUptime(t *testing.T, up interfaces.PausableManager, nodeID ids.NodeID, expectedUptime time.Duration, expectedLastUpdate time.Time) { t.Helper() uptime, lastUpdated, err := up.CalculateUptime(nodeID) require.NoError(t, err) diff --git a/plugin/evm/validators/interfaces/interface.go b/plugin/evm/validators/interfaces/interface.go new file mode 100644 index 0000000000..197ab553a1 --- /dev/null +++ b/plugin/evm/validators/interfaces/interface.go @@ -0,0 +1,43 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package interfaces + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/uptime" + "github.com/ava-labs/avalanchego/utils/set" +) + +type State interface { + uptime.State + // AddValidator adds a new validator to the state + AddValidator(vID ids.ID, nodeID ids.NodeID, startTimestamp uint64, isActive bool) error + // DeleteValidator deletes the validator from the state + DeleteValidator(vID ids.ID) error + // WriteState writes the validator state to the disk + WriteState() error + + // SetStatus sets the active status of the validator with the given vID + SetStatus(vID ids.ID, isActive bool) error + // GetStatus returns the active status of the validator with the given vID + GetStatus(vID ids.ID) (bool, error) + + // GetValidationIDs returns the validation IDs in the state + GetValidationIDs() set.Set[ids.ID] + // GetValidatorIDs returns the validator node IDs in the state + GetValidatorIDs() set.Set[ids.NodeID] + + // RegisterListener registers a listener to the state + RegisterListener(StateCallbackListener) +} + +// StateCallbackListener is a listener for the validator state +type StateCallbackListener interface { + // OnValidatorAdded is called when a new validator is added + OnValidatorAdded(vID ids.ID, nodeID ids.NodeID, startTime uint64, isActive bool) + // OnValidatorRemoved is called when a validator is removed + OnValidatorRemoved(vID ids.ID, nodeID ids.NodeID) + // OnValidatorStatusUpdated is called when a validator status is updated + OnValidatorStatusUpdated(vID ids.ID, nodeID ids.NodeID, isActive bool) +} diff --git a/plugin/evm/validators/mock_listener.go b/plugin/evm/validators/interfaces/mock_listener.go similarity index 88% rename from plugin/evm/validators/mock_listener.go rename to plugin/evm/validators/interfaces/mock_listener.go index d67703007d..8cf1903729 100644 --- a/plugin/evm/validators/mock_listener.go +++ b/plugin/evm/validators/interfaces/mock_listener.go @@ -1,13 +1,13 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ava-labs/subnet-evm/plugin/evm/validators (interfaces: StateCallbackListener) +// Source: github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces (interfaces: StateCallbackListener) // // Generated by this command: // -// mockgen -package=validators -destination=plugin/evm/validators/mock_listener.go github.com/ava-labs/subnet-evm/plugin/evm/validators StateCallbackListener +// mockgen -package=interfaces -destination=plugin/evm/validators/interfaces/mock_listener.go github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces StateCallbackListener // -// Package validators is a generated GoMock package. -package validators +// Package interfaces is a generated GoMock package. +package interfaces import ( reflect "reflect" diff --git a/plugin/evm/validators/state.go b/plugin/evm/validators/state.go index f30418c220..0be101f6bf 100644 --- a/plugin/evm/validators/state.go +++ b/plugin/evm/validators/state.go @@ -11,6 +11,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/uptime" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" ) var _ uptime.State = &state{} @@ -24,39 +25,6 @@ const ( deleted dbUpdateStatus = false ) -type State interface { - uptime.State - // AddValidator adds a new validator to the state - AddValidator(vID ids.ID, nodeID ids.NodeID, startTimestamp uint64, isActive bool) error - // DeleteValidator deletes the validator from the state - DeleteValidator(vID ids.ID) error - // WriteState writes the validator state to the disk - WriteState() error - - // SetStatus sets the active status of the validator with the given vID - SetStatus(vID ids.ID, isActive bool) error - // GetStatus returns the active status of the validator with the given vID - GetStatus(vID ids.ID) (bool, error) - - // GetValidationIDs returns the validation IDs in the state - GetValidationIDs() set.Set[ids.ID] - // GetValidatorIDs returns the validator node IDs in the state - GetValidatorIDs() set.Set[ids.NodeID] - - // RegisterListener registers a listener to the state - RegisterListener(StateCallbackListener) -} - -// StateCallbackListener is a listener for the validator state -type StateCallbackListener interface { - // OnValidatorAdded is called when a new validator is added - OnValidatorAdded(vID ids.ID, nodeID ids.NodeID, startTime uint64, isActive bool) - // OnValidatorRemoved is called when a validator is removed - OnValidatorRemoved(vID ids.ID, nodeID ids.NodeID) - // OnValidatorStatusUpdated is called when a validator status is updated - OnValidatorStatusUpdated(vID ids.ID, nodeID ids.NodeID, isActive bool) -} - type validatorData struct { UpDuration time.Duration `serialize:"true"` LastUpdated uint64 `serialize:"true"` @@ -74,11 +42,11 @@ type state struct { updatedData map[ids.ID]dbUpdateStatus // vID -> updated status db database.Database - listeners []StateCallbackListener + listeners []interfaces.StateCallbackListener } // NewState creates a new State, it also loads the data from the disk -func NewState(db database.Database) (State, error) { +func NewState(db database.Database) (interfaces.State, error) { s := &state{ index: make(map[ids.NodeID]ids.ID), data: make(map[ids.ID]*validatorData), @@ -243,7 +211,7 @@ func (s *state) GetValidatorIDs() set.Set[ids.NodeID] { // RegisterListener registers a listener to the state // OnValidatorAdded is called for all current validators on the provided listener before this function returns -func (s *state) RegisterListener(listener StateCallbackListener) { +func (s *state) RegisterListener(listener interfaces.StateCallbackListener) { s.listeners = append(s.listeners, listener) // notify the listener of the current state diff --git a/plugin/evm/validators/state_test.go b/plugin/evm/validators/state_test.go index ecfd7d34a9..e5e8244027 100644 --- a/plugin/evm/validators/state_test.go +++ b/plugin/evm/validators/state_test.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/wrappers" + "github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces" ) func TestState(t *testing.T) { @@ -223,7 +224,7 @@ func TestStateListener(t *testing.T) { expectedvID := ids.GenerateTestID() expectedNodeID := ids.GenerateTestNodeID() expectedStartTime := time.Now() - mockListener := NewMockStateCallbackListener(ctrl) + mockListener := interfaces.NewMockStateCallbackListener(ctrl) // add initial validator to test RegisterListener initialvID := ids.GenerateTestID() initialNodeID := ids.GenerateTestNodeID() diff --git a/scripts/mocks.mockgen.txt b/scripts/mocks.mockgen.txt index 73aa62ccd7..43a9d60cad 100644 --- a/scripts/mocks.mockgen.txt +++ b/scripts/mocks.mockgen.txt @@ -1,3 +1,3 @@ github.com/ava-labs/subnet-evm/precompile/precompileconfig=Predicater,Config,ChainConfig,Accepter=precompile/precompileconfig/mocks.go github.com/ava-labs/subnet-evm/precompile/contract=BlockContext,AccessibleState,StateDB=precompile/contract/mocks.go -github.com/ava-labs/subnet-evm/plugin/evm/validators=StateCallbackListener=plugin/evm/validators/mock_listener.go +github.com/ava-labs/subnet-evm/plugin/evm/validators/interfaces=StateCallbackListener=plugin/evm/validators/interfaces/mock_listener.go