diff --git a/go.mod b/go.mod index 10673fd8..43264eae 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/relab/hotstuff -go 1.25.4 +go 1.25.6 require ( - cuelang.org/go v0.15.0 + cuelang.org/go v0.15.4 github.com/felixge/fgprof v0.9.5 github.com/google/go-cmp v0.7.0 github.com/kilic/bls12-381 v0.1.1-0.20210208205449-6045b0235e36 @@ -11,16 +11,16 @@ require ( github.com/relab/gorums v0.10.0 github.com/relab/iago v0.0.0-20251028232537-e5b08eb0c08b github.com/relab/wrfs v0.0.0-20220416082020-a641cd350078 - github.com/spf13/cobra v1.10.1 + github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 go-hep.org/x/hep v0.38.1 - go.uber.org/zap v1.27.0 - golang.org/x/term v0.37.0 + go.uber.org/zap v1.27.1 + golang.org/x/term v0.39.0 golang.org/x/time v0.14.0 gonum.org/v1/plot v0.16.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba - google.golang.org/grpc v1.76.0 - google.golang.org/protobuf v1.36.10 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260126211449-d11affda4bed + google.golang.org/grpc v1.78.0 + google.golang.org/protobuf v1.36.11 ) require ( @@ -34,9 +34,9 @@ require ( github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/emicklei/proto v1.14.2 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect - github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect + github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect @@ -47,8 +47,8 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/sftp v1.13.10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 // indirect - github.com/relab/container v0.0.0-20251028224705-baa7b7c5c895 // indirect + github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b // indirect + github.com/relab/container v0.0.0-20260109140004-4adfae874bb5 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect @@ -58,16 +58,16 @@ require ( github.com/tetratelabs/wazero v1.10.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect - golang.org/x/image v0.33.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/image v0.35.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.39.0 // indirect - gonum.org/v1/gonum v0.16.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/tools v0.40.0 // indirect + gonum.org/v1/gonum v0.17.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3242603b..a3b7fe71 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ codeberg.org/gonuts/binary v0.3.2 h1:7kSBmdRwbUv5fI8LaGp/gV+ow2OTi7EnRKO/pQ6YBJo codeberg.org/gonuts/binary v0.3.2/go.mod h1:hf+kigzXMZzpPTDOuSnTz+ppy5p037QluUFVtJ3OjWI= cuelabs.dev/go/oci/ociregistry v0.0.0-20251112093024-b12090c4dee0 h1:j1eQEmO1XkBX+KkWCTgiOfdLTNfzX0VxYR3NLvx76yM= cuelabs.dev/go/oci/ociregistry v0.0.0-20251112093024-b12090c4dee0/go.mod h1:4WWeZNxUO1vRoZWAHIG0KZOd6dA25ypyWuwD3ti0Tdc= -cuelang.org/go v0.15.0 h1:0jlWNxLp1In6dWJtywTXei7w0cqfHSTiCk/6Z+FUvxI= -cuelang.org/go v0.15.0/go.mod h1:NYw6n4akZcTjA7QQwJ1/gqWrrhsN4aZwhcAL0jv9rZE= +cuelang.org/go v0.15.4 h1:lrkTDhqy8dveHgX1ZLQ6WmgbhD8+rXa0fD25hxEKYhw= +cuelang.org/go v0.15.4/go.mod h1:NYw6n4akZcTjA7QQwJ1/gqWrrhsN4aZwhcAL0jv9rZE= git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= git.sr.ht/~sbinet/epok v0.5.0 h1:eQcocQpGQVYWLiA93dkIgngH0jjjDiTdj7rS3vQLp6w= @@ -51,8 +51,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= @@ -63,8 +63,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0= -github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -111,10 +111,10 @@ github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1Hbe github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 h1:s1LvMaU6mVwoFtbxv/rCZKE7/fwDmDY684FfUe4c1Io= -github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= -github.com/relab/container v0.0.0-20251028224705-baa7b7c5c895 h1:IiS1KzQwmZsL5fnrvSpdMypSKqu7wcqSO7DGjWTr6bs= -github.com/relab/container v0.0.0-20251028224705-baa7b7c5c895/go.mod h1:oLZXG1NirJWzF2fMEeMUC6OLiMn6RtCChzUz1jtF/qs= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b h1:fPVI9E6QNFYI0Ph3XpKUDrcAvbCifHvqYJcntFLPog8= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= +github.com/relab/container v0.0.0-20260109140004-4adfae874bb5 h1:ImfKSqvvsCWtQ2ibKkZZMgE8+incQzgfdRKEcxTW3g4= +github.com/relab/container v0.0.0-20260109140004-4adfae874bb5/go.mod h1:oLZXG1NirJWzF2fMEeMUC6OLiMn6RtCChzUz1jtF/qs= github.com/relab/gorums v0.10.0 h1:kerc6DAD7n4NNghoyXlyA6QzZzyIDJ7HJz7xwCTvsX0= github.com/relab/gorums v0.10.0/go.mod h1:9cov4XXpxDY0Lz8Orc9P+R2DuSWr2RPzwsgiy67xZTM= github.com/relab/iago v0.0.0-20251028232537-e5b08eb0c08b h1:pSBDurPN1xX6YlS8Sg/oh7PeMjPuGzKToKbpLRNmghE= @@ -130,8 +130,8 @@ github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -152,47 +152,47 @@ github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuy github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go-hep.org/x/hep v0.38.1 h1:GdTcYD8aJdmyr+IExx6S99iNcbqNnIwf3nJKEuR6FRE= go-hep.org/x/hep v0.38.1/go.mod h1:raiZKDRcnG57o6DQSB7s2mSGyvZF+OSGSzN56akTnPg= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= -golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= -golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= +golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -200,34 +200,34 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/plot v0.16.0 h1:dK28Qx/Ky4VmPUN/2zeW0ELyM6ucDnBAj5yun7M9n1g= gonum.org/v1/plot v0.16.0/go.mod h1:Xz6U1yDMi6Ni6aaXILqmVIb6Vro8E+K7Q/GeeH+Pn0c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260126211449-d11affda4bed h1:Yyog7dFpq0nVFnxj1NymkvC4RDIzc7KILL6vNAgLbCs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260126211449-d11affda4bed/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/proto/clientpb/client.pb.go b/internal/proto/clientpb/client.pb.go index b38b8d7d..6ed28beb 100644 --- a/internal/proto/clientpb/client.pb.go +++ b/internal/proto/clientpb/client.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc-gen-go v1.36.11 +// protoc v6.33.4 // source: internal/proto/clientpb/client.proto package clientpb diff --git a/internal/proto/clientpb/client_gorums.pb.go b/internal/proto/clientpb/client_gorums.pb.go index 534bfd2e..cd75440d 100644 --- a/internal/proto/clientpb/client_gorums.pb.go +++ b/internal/proto/clientpb/client_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.10.0-devel -// protoc v6.33.0 +// protoc v6.33.4 // source: internal/proto/clientpb/client.proto package clientpb diff --git a/internal/proto/hotstuffpb/hotstuff.pb.go b/internal/proto/hotstuffpb/hotstuff.pb.go index a01e409a..dad5fc92 100644 --- a/internal/proto/hotstuffpb/hotstuff.pb.go +++ b/internal/proto/hotstuffpb/hotstuff.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc-gen-go v1.36.11 +// protoc v6.33.4 // source: internal/proto/hotstuffpb/hotstuff.proto package hotstuffpb diff --git a/internal/proto/hotstuffpb/hotstuff_gorums.pb.go b/internal/proto/hotstuffpb/hotstuff_gorums.pb.go index e3779e4b..f0545f18 100644 --- a/internal/proto/hotstuffpb/hotstuff_gorums.pb.go +++ b/internal/proto/hotstuffpb/hotstuff_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.10.0-devel -// protoc v6.33.0 +// protoc v6.33.4 // source: internal/proto/hotstuffpb/hotstuff.proto package hotstuffpb diff --git a/internal/proto/kauripb/kauri.pb.go b/internal/proto/kauripb/kauri.pb.go index 3c7ed731..9d9b9214 100644 --- a/internal/proto/kauripb/kauri.pb.go +++ b/internal/proto/kauripb/kauri.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc-gen-go v1.36.11 +// protoc v6.33.4 // source: internal/proto/kauripb/kauri.proto package kauripb diff --git a/internal/proto/kauripb/kauri_gorums.pb.go b/internal/proto/kauripb/kauri_gorums.pb.go index a33ac252..8e2a2690 100644 --- a/internal/proto/kauripb/kauri_gorums.pb.go +++ b/internal/proto/kauripb/kauri_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.10.0-devel -// protoc v6.33.0 +// protoc v6.33.4 // source: internal/proto/kauripb/kauri.proto package kauripb diff --git a/internal/proto/orchestrationpb/orchestration.pb.go b/internal/proto/orchestrationpb/orchestration.pb.go index 286fd826..5d026aac 100644 --- a/internal/proto/orchestrationpb/orchestration.pb.go +++ b/internal/proto/orchestrationpb/orchestration.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc-gen-go v1.36.11 +// protoc v6.33.4 // source: internal/proto/orchestrationpb/orchestration.proto package orchestrationpb diff --git a/internal/testutil/mocksender.go b/internal/testutil/mocksender.go index 3134ddec..0e9bbf8c 100644 --- a/internal/testutil/mocksender.go +++ b/internal/testutil/mocksender.go @@ -10,11 +10,18 @@ import ( "github.com/relab/hotstuff/security/blockchain" ) +// ContributionMsg represents a contribution sent to parent in Kauri protocol. +type ContributionMsg struct { + View hotstuff.View + QC hotstuff.QuorumSignature +} + type MockSender struct { - id hotstuff.ID - recipients []hotstuff.ID - messagesSent []any - blockChains []*blockchain.Blockchain + id hotstuff.ID + recipients []hotstuff.ID + messagesSent []any + contributions []ContributionMsg + blockChains []*blockchain.Blockchain } // NewMockSender returns a mock implementation of core.Sender that @@ -111,7 +118,18 @@ func (m *MockSender) Sub(ids []hotstuff.ID) (core.Sender, error) { }, nil } +// SendContributionToParent stores a contribution message for Kauri protocol testing. +func (m *MockSender) SendContributionToParent(view hotstuff.View, qc hotstuff.QuorumSignature) { + m.contributions = append(m.contributions, ContributionMsg{View: view, QC: qc}) +} + +// ContributionsSent returns a slice of contributions sent by the mock sender. +func (m *MockSender) ContributionsSent() []ContributionMsg { + return m.contributions +} + var _ core.Sender = (*MockSender)(nil) +var _ core.KauriSender = (*MockSender)(nil) func isSubset(a, b []hotstuff.ID) bool { set := make(map[hotstuff.ID]struct{}, len(a)) diff --git a/metrics/types/types.pb.go b/metrics/types/types.pb.go index 3d2c9526..51fba661 100644 --- a/metrics/types/types.pb.go +++ b/metrics/types/types.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc-gen-go v1.36.11 +// protoc v6.33.4 // source: metrics/types/types.proto package types diff --git a/protocol/comm/kauri.go b/protocol/comm/kauri.go index fffda5a4..329834b2 100644 --- a/protocol/comm/kauri.go +++ b/protocol/comm/kauri.go @@ -9,6 +9,7 @@ import ( "github.com/relab/hotstuff/core/eventloop" "github.com/relab/hotstuff/core/logging" "github.com/relab/hotstuff/internal/proto/hotstuffpb" + "github.com/relab/hotstuff/internal/proto/kauripb" "github.com/relab/hotstuff/internal/tree" "github.com/relab/hotstuff/network" "github.com/relab/hotstuff/protocol/comm/kauri" @@ -61,12 +62,15 @@ func NewKauri( eventloop.Register(el, func(_ hotstuff.ReplicaConnectedEvent) { k.initDone = true // signal that we are connected }) - eventloop.Register(el, func(event kauri.ContributionRecvEvent) { + eventloop.Register(el, func(event *kauripb.Contribution) { k.onContributionRecv(event) }) eventloop.Register(el, func(event WaitTimerExpiredEvent) { k.onWaitTimerExpired(event) }) + eventloop.Register(el, func(event WaitForConnectedEvent) { + k.onWaitForConnected(event) + }) return k } @@ -83,11 +87,9 @@ func (k *Kauri) Aggregate(proposal *hotstuff.ProposeMsg, pc hotstuff.PartialCert // begin starts dissemination of proposal and aggregation of votes. func (k *Kauri) begin(p *hotstuff.ProposeMsg, pc hotstuff.PartialCert) error { if !k.initDone { - // TODO(meling): This is not correct use of DelayUntil, see issue #267 - eventloop.DelayUntil[network.ConnectedEvent](k.eventLoop, func() { - if err := k.begin(p, pc); err != nil { - k.logger.Error(err) - } + eventloop.DelayUntil[network.ConnectedEvent](k.eventLoop, WaitForConnectedEvent{ + pc: pc, + p: p, }) return nil } @@ -128,12 +130,24 @@ func (k *Kauri) waitToAggregate() { k.eventLoop.AddEvent(WaitTimerExpiredEvent{currentView: view}) } +// onWaitForConnected is invoked when begin is called before the replica is connected. +func (k *Kauri) onWaitForConnected(event WaitForConnectedEvent) { + k.logger.Debugf("WaitForConnectedEvent: %v", event) + if k.currentView > hotstuff.View(event.p.Block.View()) { + k.logger.Debug("Current view is higher than event view, not starting kauri") + return + } + err := k.begin(event.p, event.pc) + if err != nil { + k.logger.Errorf("Failed to begin kauri after connection: %v", err) + } +} + // onContributionRecv is invoked upon receiving the vote for aggregation. -func (k *Kauri) onContributionRecv(event kauri.ContributionRecvEvent) { - if k.currentView != hotstuff.View(event.Contribution.View) { +func (k *Kauri) onContributionRecv(contribution *kauripb.Contribution) { + if k.currentView != hotstuff.View(contribution.View) { return } - contribution := event.Contribution k.logger.Debugf("Processing the contribution from %d", contribution.ID) currentSignature := hotstuffpb.QuorumSignatureFromProto(contribution.Signature) err := k.mergeContribution(currentSignature) @@ -196,4 +210,9 @@ type WaitTimerExpiredEvent struct { currentView hotstuff.View } +type WaitForConnectedEvent struct { + pc hotstuff.PartialCert + p *hotstuff.ProposeMsg +} + var _ Communication = (*Kauri)(nil) diff --git a/protocol/comm/kauri/service.go b/protocol/comm/kauri/service.go index d55ffabb..5a9bd40c 100644 --- a/protocol/comm/kauri/service.go +++ b/protocol/comm/kauri/service.go @@ -20,10 +20,5 @@ func RegisterService( } func (i kauriServiceImpl) SendContribution(_ gorums.ServerCtx, request *kauripb.Contribution) { - i.eventLoop.AddEvent(ContributionRecvEvent{Contribution: request}) -} - -// ContributionRecvEvent is raised when a contribution is received. -type ContributionRecvEvent struct { - Contribution *kauripb.Contribution + i.eventLoop.AddEvent(request) } diff --git a/protocol/comm/kauri_test.go b/protocol/comm/kauri_test.go new file mode 100644 index 00000000..a5b4fef7 --- /dev/null +++ b/protocol/comm/kauri_test.go @@ -0,0 +1,534 @@ +package comm_test + +import ( + "context" + "testing" + "time" + + "github.com/relab/hotstuff" + "github.com/relab/hotstuff/core" + "github.com/relab/hotstuff/core/eventloop" + "github.com/relab/hotstuff/internal/proto/clientpb" + "github.com/relab/hotstuff/internal/test" + "github.com/relab/hotstuff/internal/testutil" + "github.com/relab/hotstuff/internal/tree" + "github.com/relab/hotstuff/network" + "github.com/relab/hotstuff/protocol/comm" + "github.com/relab/hotstuff/security/crypto" +) + +// fullTreeSize calculates the number of nodes in a full tree with the given +// branch factor and 3 levels (root, intermediate, leaves). +func fullTreeSize(bf int) int { + return 1 + bf + bf*bf // level 0 + level 1 + level 2 +} + +// treeConfig represents a tree configuration for table-driven tests. +type treeConfig struct { + branchFactor int + size int +} + +// standardTreeConfigs returns tree configurations for full 3-level trees +// with branch factors 2, 3, 4, and 5. +func standardTreeConfigs() []treeConfig { + return []treeConfig{ + {branchFactor: 2, size: fullTreeSize(2)}, // 7 nodes + {branchFactor: 3, size: fullTreeSize(3)}, // 13 nodes + {branchFactor: 4, size: fullTreeSize(4)}, // 21 nodes + {branchFactor: 5, size: fullTreeSize(5)}, // 31 nodes + } +} + +// kauriTestSetup contains all components needed for Kauri tests. +type kauriTestSetup struct { + essentials *testutil.Essentials + kauri *comm.Kauri + tree *tree.Tree + sender *testutil.MockSender +} + +// wireUpKauri creates a Kauri instance with the specified configuration. +// It returns a setup struct containing the Kauri instance and related components. +func wireUpKauri( + t testing.TB, + id hotstuff.ID, + treeSize, branchFactor int, +) *kauriTestSetup { + t.Helper() + + treePositions := tree.DefaultTreePos(treeSize) + tr := tree.NewSimple(id, branchFactor, treePositions) + tr.SetTreeHeightWaitTime(0) // disable wait time for tests + + essentials := testutil.WireUpEssentials( + t, id, crypto.NameECDSA, + core.WithKauriTree(tr), + ) + + // Add all replicas to the configuration + for _, pos := range treePositions { + if pos != id { + essentials.RuntimeCfg().AddReplica(&hotstuff.ReplicaInfo{ID: pos}) + } + } + + // Create a mock sender with all replica IDs as potential recipients + sender := testutil.NewMockSender(id, treePositions...) + + k := comm.NewKauri( + essentials.Logger(), + essentials.EventLoop(), + essentials.RuntimeCfg(), + essentials.Blockchain(), + essentials.Authority(), + sender, + ) + + return &kauriTestSetup{ + essentials: essentials, + kauri: k, + tree: tr, + sender: sender, + } +} + +// simulateConnection simulates the replica being connected by triggering +// the ReplicaConnectedEvent through the event loop. +func simulateConnection(t testing.TB, setup *kauriTestSetup) { + t.Helper() + el := setup.essentials.EventLoop() + el.AddEvent(hotstuff.ReplicaConnectedEvent{}) + el.Tick(context.Background()) +} + +// createProposal creates a test proposal with a block based on genesis. +func createProposal(t testing.TB, essentials *testutil.Essentials, proposerID hotstuff.ID) *hotstuff.ProposeMsg { + t.Helper() + block := testutil.CreateBlock(t, essentials.Authority()) + return &hotstuff.ProposeMsg{ + ID: proposerID, + Block: block, + } +} + +// waitForEvent registers a handler for event type T and ticks the event loop +// until the event is received or timeout occurs. This is preferred over +// arbitrary tick counts. +func waitForEvent[T any](t testing.TB, el *eventloop.EventLoop, timeout time.Duration) { + t.Helper() + received := make(chan struct{}) + unregister := eventloop.Register(el, func(_ T) { + close(received) + }) + defer unregister() + + deadline := time.Now().Add(timeout) + for { + select { + case <-received: + return + default: + if time.Now().After(deadline) { + t.Fatalf("timeout waiting for event %T", *new(T)) + } + el.Tick(context.Background()) + time.Sleep(time.Millisecond) // yield to allow goroutines to run + } + } +} + +// TestNewKauriPanic tests that NewKauri panics when no tree is configured. +func TestNewKauriPanic(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("NewKauri should panic when no tree is configured") + } + }() + + essentials := testutil.WireUpEssentials(t, 1, crypto.NameECDSA) + // This should panic because no tree is configured + _ = comm.NewKauri( + essentials.Logger(), + essentials.EventLoop(), + essentials.RuntimeCfg(), + essentials.Blockchain(), + essentials.Authority(), + essentials.MockSender(), + ) +} + +// TestDisseminateFromLeaf tests that a leaf node sends its contribution +// directly to the parent without forwarding to children. +func TestDisseminateFromLeaf(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + // Use the last node (a leaf) as the test replica + leafID := hotstuff.ID(tc.size) + setup := wireUpKauri(t, leafID, tc.size, tc.branchFactor) + simulateConnection(t, setup) + + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + // Leaf should send contribution to parent, not propose to children + contributions := setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution from leaf, got %d", len(contributions)) + } + + // Verify no proposals were sent (leaf has no children) + messages := setup.sender.MessagesSent() + for _, msg := range messages { + if _, ok := msg.(hotstuff.ProposeMsg); ok { + t.Error("leaf node should not send proposals") + } + } + }) + } +} + +// TestDisseminateFromIntermediate tests that an intermediate node with children +// starts the aggregation timer and eventually sends its contribution to parent. +func TestDisseminateFromIntermediate(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + // Use node 2 (first child of root, has its own children) + intermediateID := hotstuff.ID(2) + setup := wireUpKauri(t, intermediateID, tc.size, tc.branchFactor) + simulateConnection(t, setup) + + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + // Verify this node has children (precondition) + children := setup.tree.ReplicaChildren() + if len(children) == 0 { + t.Error("intermediate node should have children") + } + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + // No contribution yet (waiting for children) + contributions := setup.sender.ContributionsSent() + if len(contributions) != 0 { + t.Errorf("expected no contributions immediately, got %d", len(contributions)) + } + + // Wait for WaitTimerExpiredEvent to be processed + waitForEvent[comm.WaitTimerExpiredEvent](t, setup.essentials.EventLoop(), 100*time.Millisecond) + + // Now contribution should be sent (timer expired) + contributions = setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution after timer expiry, got %d", len(contributions)) + } + }) + } +} + +// TestDisseminateFromRoot tests that the root node with children +// starts the aggregation timer and eventually sends its contribution to parent. +func TestDisseminateFromRoot(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + rootID := hotstuff.ID(1) + setup := wireUpKauri(t, rootID, tc.size, tc.branchFactor) + simulateConnection(t, setup) + + proposal := createProposal(t, setup.essentials, rootID) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + // Verify root has children (precondition for multi-node trees) + children := setup.tree.ReplicaChildren() + if len(children) == 0 { + t.Error("root should have children in multi-node tree") + } + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + // No contribution yet (waiting for children) + contributions := setup.sender.ContributionsSent() + if len(contributions) != 0 { + t.Errorf("expected no contributions immediately, got %d", len(contributions)) + } + + // Wait for WaitTimerExpiredEvent to be processed + waitForEvent[comm.WaitTimerExpiredEvent](t, setup.essentials.EventLoop(), 100*time.Millisecond) + + // Now contribution should be sent (timer expired) + contributions = setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution after timer expiry, got %d", len(contributions)) + } + }) + } +} + +// TestAggregateFromLeaf tests that a leaf node sends its vote contribution +// to its parent. +func TestAggregateFromLeaf(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + leafID := hotstuff.ID(tc.size) + setup := wireUpKauri(t, leafID, tc.size, tc.branchFactor) + simulateConnection(t, setup) + + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + err := setup.kauri.Aggregate(proposal, pc) + if err != nil { + t.Fatalf("Aggregate failed: %v", err) + } + + // Leaf should send contribution to parent + contributions := setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution from leaf, got %d", len(contributions)) + } + + // Verify the contribution has the correct view + if contributions[0].View != proposal.Block.View() { + t.Errorf("contribution view = %d, want %d", contributions[0].View, proposal.Block.View()) + } + }) + } +} + +// TestDisseminateDelayedUntilConnected tests that Disseminate waits for +// connection before processing. +func TestDisseminateDelayedUntilConnected(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + leafID := hotstuff.ID(tc.size) + setup := wireUpKauri(t, leafID, tc.size, tc.branchFactor) + + // Do NOT simulate connection yet + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + // Nothing should be sent yet + contributions := setup.sender.ContributionsSent() + if len(contributions) != 0 { + t.Errorf("expected no contributions before connection, got %d", len(contributions)) + } + + // Simulate connection: ReplicaConnectedEvent first (sets initDone=true), + // then ConnectedEvent (triggers the delayed WaitForConnectedEvent) + el := setup.essentials.EventLoop() + el.AddEvent(hotstuff.ReplicaConnectedEvent{}) + el.Tick(context.Background()) // Process ReplicaConnectedEvent + + el.AddEvent(network.ConnectedEvent{}) + // Wait for WaitForConnectedEvent to be processed + waitForEvent[comm.WaitForConnectedEvent](t, el, 100*time.Millisecond) + + // Now contribution should be sent + contributions = setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution after connection, got %d", len(contributions)) + } + }) + } +} + +// TestAggregateDelayedUntilConnected tests that Aggregate waits for +// connection before processing. +func TestAggregateDelayedUntilConnected(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + leafID := hotstuff.ID(tc.size) + setup := wireUpKauri(t, leafID, tc.size, tc.branchFactor) + + // Do NOT simulate connection yet + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + err := setup.kauri.Aggregate(proposal, pc) + if err != nil { + t.Fatalf("Aggregate failed: %v", err) + } + + // Nothing should be sent yet + contributions := setup.sender.ContributionsSent() + if len(contributions) != 0 { + t.Errorf("expected no contributions before connection, got %d", len(contributions)) + } + + // Simulate connection: ReplicaConnectedEvent first (sets initDone=true), + // then ConnectedEvent (triggers the delayed WaitForConnectedEvent) + el := setup.essentials.EventLoop() + el.AddEvent(hotstuff.ReplicaConnectedEvent{}) + el.Tick(context.Background()) // Process ReplicaConnectedEvent + + el.AddEvent(network.ConnectedEvent{}) + // Wait for WaitForConnectedEvent to be processed + waitForEvent[comm.WaitForConnectedEvent](t, el, 100*time.Millisecond) + + // Now contribution should be sent + contributions = setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution after connection, got %d", len(contributions)) + } + }) + } +} + +// TestSingleNodeTree tests behavior when there is only one node in the tree. +func TestSingleNodeTree(t *testing.T) { + setup := wireUpKauri(t, 1, 1, 2) + simulateConnection(t, setup) + + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + // Single node has no children, should send contribution immediately + contributions := setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution from single node, got %d", len(contributions)) + } +} + +// TestChildrenCount tests that the correct number of children receive proposals +// for different tree configurations. +func TestChildrenCount(t *testing.T) { + tests := []struct { + branchFactor int + size int + replicaID hotstuff.ID + wantChildren int + }{ + // Root nodes + {branchFactor: 2, size: 7, replicaID: 1, wantChildren: 2}, + {branchFactor: 3, size: 13, replicaID: 1, wantChildren: 3}, + {branchFactor: 4, size: 21, replicaID: 1, wantChildren: 4}, + {branchFactor: 5, size: 31, replicaID: 1, wantChildren: 5}, + // Intermediate nodes (node 2) + {branchFactor: 2, size: 7, replicaID: 2, wantChildren: 2}, + {branchFactor: 3, size: 13, replicaID: 2, wantChildren: 3}, + {branchFactor: 4, size: 21, replicaID: 2, wantChildren: 4}, + // Leaf nodes (last node) + {branchFactor: 2, size: 7, replicaID: 7, wantChildren: 0}, + {branchFactor: 3, size: 13, replicaID: 13, wantChildren: 0}, + {branchFactor: 4, size: 21, replicaID: 21, wantChildren: 0}, + } + + for _, tt := range tests { + t.Run(test.Name("bf", tt.branchFactor, "id", int(tt.replicaID)), func(t *testing.T) { + setup := wireUpKauri(t, tt.replicaID, tt.size, tt.branchFactor) + children := setup.tree.ReplicaChildren() + if len(children) != tt.wantChildren { + t.Errorf("ReplicaChildren() = %d, want %d", len(children), tt.wantChildren) + } + }) + } +} + +// TestTreeNodePositions verifies that tree positions work correctly +// for different node roles. +func TestTreeNodePositions(t *testing.T) { + tests := []struct { + branchFactor int + size int + replicaID hotstuff.ID + wantIsRoot bool + wantIsLeaf bool + }{ + // bf=2, size=7 + {branchFactor: 2, size: 7, replicaID: 1, wantIsRoot: true, wantIsLeaf: false}, + {branchFactor: 2, size: 7, replicaID: 2, wantIsRoot: false, wantIsLeaf: false}, + {branchFactor: 2, size: 7, replicaID: 7, wantIsRoot: false, wantIsLeaf: true}, + // bf=4, size=21 + {branchFactor: 4, size: 21, replicaID: 1, wantIsRoot: true, wantIsLeaf: false}, + {branchFactor: 4, size: 21, replicaID: 5, wantIsRoot: false, wantIsLeaf: false}, + {branchFactor: 4, size: 21, replicaID: 21, wantIsRoot: false, wantIsLeaf: true}, + } + + for _, tt := range tests { + t.Run(test.Name("bf", tt.branchFactor, "id", int(tt.replicaID)), func(t *testing.T) { + setup := wireUpKauri(t, tt.replicaID, tt.size, tt.branchFactor) + + isRoot := setup.tree.IsRoot(tt.replicaID) + if isRoot != tt.wantIsRoot { + t.Errorf("IsRoot(%d) = %v, want %v", tt.replicaID, isRoot, tt.wantIsRoot) + } + + // A node is a leaf if it has no children + isLeaf := len(setup.tree.ReplicaChildren()) == 0 + if isLeaf != tt.wantIsLeaf { + t.Errorf("IsLeaf(%d) = %v, want %v", tt.replicaID, isLeaf, tt.wantIsLeaf) + } + }) + } +} + +// TestProposalViewTracking ensures that the view is correctly tracked +// across dissemination and aggregation. +func TestProposalViewTracking(t *testing.T) { + views := []hotstuff.View{1, 5, 10, 100} + + for _, view := range views { + t.Run(test.Name("view", int(view)), func(t *testing.T) { + leafID := hotstuff.ID(7) // leaf in bf=2, size=7 tree + setup := wireUpKauri(t, leafID, 7, 2) + simulateConnection(t, setup) + + // Create block with specific view + qc := testutil.CreateQC(t, hotstuff.GetGenesis(), setup.essentials.Authority()) + block := hotstuff.NewBlock( + hotstuff.GetGenesis().Hash(), + qc, + &clientpb.Batch{}, + view, + 1, + ) + setup.essentials.Blockchain().Store(block) + + proposal := &hotstuff.ProposeMsg{ID: 1, Block: block} + pc := testutil.CreatePC(t, block, setup.essentials.Authority()) + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + contributions := setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Fatalf("expected 1 contribution, got %d", len(contributions)) + } + + if contributions[0].View != view { + t.Errorf("contribution view = %d, want %d", contributions[0].View, view) + } + }) + } +}