├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── hyperdrive.go ├── hyperdrive_suite_test.go ├── hyperdrive_test.go ├── mq ├── mq.go ├── mq_suite_test.go ├── mq_test.go ├── opt.go └── opt_test.go ├── process ├── message.go ├── message_test.go ├── process.go ├── process_suite_test.go ├── process_test.go ├── processutil │ ├── processutil.go │ ├── processutil_suite_test.go │ └── processutil_test.go ├── state.go └── state_test.go ├── replica ├── .env ├── opt.go ├── opt_test.go ├── replica.go ├── replica_suite_test.go └── replica_test.go ├── scheduler ├── scheduler.go ├── scheduler_suite_test.go └── scheduler_test.go └── timer ├── opt.go ├── opt_test.go ├── timer.go ├── timer_suite_test.go └── timer_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: go 2 | on: [push] 3 | jobs: 4 | 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | 9 | - name: Set up Go 1.13 10 | uses: actions/setup-go@v1 11 | with: 12 | go-version: 1.13 13 | id: go 14 | 15 | - name: Check out code into the Go module directory 16 | uses: actions/checkout@v1 17 | 18 | - name: Caching modules 19 | uses: actions/cache@v1 20 | with: 21 | path: ~/go/pkg/mod 22 | key: ${{ runner.os }}-go-aw-${{ hashFiles('**/go.sum') }} 23 | 24 | - name: Get dependencies 25 | run: | 26 | export PATH=$PATH:$(go env GOPATH)/bin 27 | go get -u github.com/onsi/ginkgo/ginkgo 28 | go get -u github.com/onsi/gomega/... 29 | go get -u golang.org/x/lint/golint 30 | go get -u github.com/loongy/covermerge 31 | go get -u github.com/mattn/goveralls 32 | 33 | - name: Run vetting 34 | run: | 35 | cd $GITHUB_WORKSPACE 36 | export PATH=$PATH:$(go env GOPATH)/bin 37 | go vet ./... 38 | 39 | - name: Run linting 40 | run: | 41 | cd $GITHUB_WORKSPACE 42 | export PATH=$PATH:$(go env GOPATH)/bin 43 | go get -u golang.org/x/lint/golint 44 | golint ./... 45 | 46 | - name: Run tests and report test coverage 47 | env: 48 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | run: | 50 | export PATH=$PATH:$(go env GOPATH)/bin 51 | export REPLAY_MODE=false 52 | cd $GITHUB_WORKSPACE 53 | CI=true ginkgo --v --race --cover --coverprofile coverprofile.out ./... 54 | covermerge \ 55 | scheduler/coverprofile.out \ 56 | timer/coverprofile.out \ 57 | mq/coverprofile.out \ 58 | process/coverprofile.out \ 59 | replica/coverprofile.out \ 60 | coverprofile.out > coverprofile.out 61 | goveralls -coverprofile=coverprofile.out -service=github 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # IDE files 12 | .idea 13 | .vscode 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | *.coverprofile 18 | *.dump 19 | 20 | .direnv.* 21 | .envrc 22 | shell.nix 23 | 24 | # Binaries 25 | releases 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `🤖 hyperdrive` 2 | 3 | [![GoDoc](https://godoc.org/github.com/renproject/hyperdrive?status.svg)](https://godoc.org/github.com/renproject/hyperdrive) 4 | [![CircleCI](https://circleci.com/gh/renproject/hyperdrive/tree/master.svg?style=shield)](https://circleci.com/gh/renproject/hyperdrive/tree/master) 5 | ![Go Report](https://goreportcard.com/badge/github.com/renproject/hyperdrive) 6 | [![Coverage Status](https://coveralls.io/repos/github/renproject/hyperdrive/badge.svg?branch=master)](https://coveralls.io/github/renproject/hyperdrive?branch=master) 7 | [![License: GPL v3](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 8 | 9 | A Byzantine fault tolerant consensus algorithm for secure multiparty computation protocols. For more information, [checkout the Wiki](https://github.com/renproject/hyperdrive/wiki). 10 | 11 | Built with ❤ by Ren. 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/renproject/hyperdrive 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/ethereum/go-ethereum v1.9.5 7 | github.com/onsi/ginkgo v1.12.3 8 | github.com/onsi/gomega v1.10.1 9 | github.com/renproject/id v0.4.2 10 | github.com/renproject/surge v1.2.5 11 | go.uber.org/zap v1.15.0 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 4 | github.com/Shopify/sarama v1.23.1/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs= 5 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 6 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 9 | github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= 10 | github.com/aristanetworks/fsnotify v1.4.2/go.mod h1:D/rtu7LpjYM8tRJphJ0hUBYpjai8SfX+aSNsWDTq/Ks= 11 | github.com/aristanetworks/glog v0.0.0-20180419172825-c15b03b3054f/go.mod h1:KASm+qXFKs/xjSoWn30NrWBBvdTTQq+UjkhjEJHfSFA= 12 | github.com/aristanetworks/goarista v0.0.0-20200224203130-895b4c57c44d/go.mod h1:fc4cJJjY+PlmFYIjSFJ/OPWG8R2B/ue7+q2YbMkirTo= 13 | github.com/aristanetworks/splunk-hec-go v0.3.3/go.mod h1:1VHO9r17b0K7WmOlLb9nTk/2YanvOEnLMUgsFrxBROc= 14 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 15 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 16 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 17 | github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= 18 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 19 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 20 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 21 | github.com/btcsuite/btcutil v1.0.1/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= 22 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 23 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 24 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 25 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 26 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 27 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 28 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 33 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 34 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 35 | github.com/elastic/gosigar v0.10.5/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs= 36 | github.com/ethereum/go-ethereum v1.9.5 h1:4oxsF+/3N/sTgda9XTVG4r+wMVLsveziSMcK83hPbsk= 37 | github.com/ethereum/go-ethereum v1.9.5/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY= 38 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 39 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 40 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 41 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 42 | github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= 43 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 44 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 45 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 46 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 47 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 48 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 49 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 50 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 51 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 52 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 53 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 54 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 55 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 56 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 57 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 58 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 59 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 60 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 61 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 62 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 63 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 64 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 65 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 66 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 67 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 68 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 69 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 70 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 71 | github.com/influxdata/influxdb1-client v0.0.0-20190809212627-fc22c7df067e/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= 72 | github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= 73 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 74 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 75 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 76 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 77 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 78 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 79 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 80 | github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 81 | github.com/klauspost/reedsolomon v1.9.2/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= 82 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 83 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 84 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 85 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 86 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 87 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 88 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 89 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 90 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 91 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 92 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 93 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 94 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 95 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 96 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 97 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 98 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 99 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 100 | github.com/onsi/ginkgo v1.9.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 101 | github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= 102 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 103 | github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= 104 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 105 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 106 | github.com/onsi/ginkgo v1.12.3 h1:+RYp9QczoWz9zfUyLP/5SLXQVhfr6gZOoKGfQqHuLZQ= 107 | github.com/onsi/ginkgo v1.12.3/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 108 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 109 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 110 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 111 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 112 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 113 | github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= 114 | github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= 115 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 116 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 117 | github.com/openconfig/gnmi v0.0.0-20190823184014-89b2bf29312c/go.mod h1:t+O9It+LKzfOAhKTT5O0ehDix+MTqbtT0T9t+7zzOvc= 118 | github.com/openconfig/reference v0.0.0-20190727015836-8dfd928c9696/go.mod h1:ym2A+zigScwkSEb/cVQB0/ZMpU3rqiH6X7WRRsxgOGw= 119 | github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= 120 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 121 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 122 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 123 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 124 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 125 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 126 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 127 | github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= 128 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 129 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 130 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 131 | github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= 132 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 133 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 134 | github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 135 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 136 | github.com/renproject/abi v0.3.0 h1:Vny6SbbmltWSA49b8cQxcgPMZcmq2K0AN9L4l8CJVJ0= 137 | github.com/renproject/abi v0.3.0/go.mod h1:Ug37KRS/um6elthcIgCRrzDfyJquVE+4RYBS6ykbOQI= 138 | github.com/renproject/abi v0.4.0 h1:G9mPrLbgP9P8EFxPyyMaDMDKTiOs78SY4neK3shu9b8= 139 | github.com/renproject/abi v0.4.0/go.mod h1:Ug37KRS/um6elthcIgCRrzDfyJquVE+4RYBS6ykbOQI= 140 | github.com/renproject/abi v0.4.1 h1:UbrC6CSGi7T2VBhKZO9EzRyvCpmTDV+gHCb+WVVTZiQ= 141 | github.com/renproject/abi v0.4.1/go.mod h1:Ug37KRS/um6elthcIgCRrzDfyJquVE+4RYBS6ykbOQI= 142 | github.com/renproject/hyperdrive v1.0.0/go.mod h1:kFiXW7enjQ5jzwprSr64B5HSCNvnXHfkLzz84R2gA/w= 143 | github.com/renproject/id v0.1.1/go.mod h1:i4OJzgjl4XLcU7nfU9UshX7PaBVpnTk3gEVj8dKa6f8= 144 | github.com/renproject/id v0.2.1 h1:bNQStliAf/QUS8LH4bSN+GFoSDuguFCD5nzE9jlAqqM= 145 | github.com/renproject/id v0.2.1/go.mod h1:a3IoJpN44tsb2PD4QEFiu6wZy+UuanOkMtGcnxdLikk= 146 | github.com/renproject/id v0.2.2 h1:hQH74EK5GjzG588EVJ7m5nnXfJd36mmq/ipkYcL8vAc= 147 | github.com/renproject/id v0.2.2/go.mod h1:xEoepH7Jze4l+gJxzSh9yRt634XyiM1j9si+WS2Emsc= 148 | github.com/renproject/id v0.3.1 h1:92CbN8sQTlMIXid69fOJXEkES67uJDORkobRvqoVvJs= 149 | github.com/renproject/id v0.3.1/go.mod h1:xEoepH7Jze4l+gJxzSh9yRt634XyiM1j9si+WS2Emsc= 150 | github.com/renproject/id v0.3.3 h1:IiJR1mJ8PvAds+zRz1gxukbWKJJrYQSUnNdihOsaGAY= 151 | github.com/renproject/id v0.3.3/go.mod h1:BmNHJVfkLsDcvQFHAAPxhhv2KUvWhT4xXFo1Phmp8Kw= 152 | github.com/renproject/id v0.4.2 h1:XseNDPPCJtsZjIWR7Qgf+zxy0Gt5xsLrfwpQxJt5wFQ= 153 | github.com/renproject/id v0.4.2/go.mod h1:bCzV4zZkyWetf0GvhJxMT9HQNnGUwzQpImtXOUXqq0k= 154 | github.com/renproject/phi v0.1.0 h1:ZOn7QeDribk/uV46OhQWcTLxyuLg7P+xR1Hfl5cOQuI= 155 | github.com/renproject/phi v0.1.0/go.mod h1:Hrxx2ONVpfByficRjyRd1trecalYr0lo7Z0akx8UXqg= 156 | github.com/renproject/surge v1.1.1 h1:dpgMRBR1raPWw8TUmICjAVsyl88EmPnUq+o4CZqAm9g= 157 | github.com/renproject/surge v1.1.1/go.mod h1:UnnFYpLSD0T9MzCcyHjbNdmxiQsDVyBDCuqcbhcaLCY= 158 | github.com/renproject/surge v1.1.2 h1:Yy3pTlRyaMJGLfn64JHgCnWs3cWbRJjE+aFxZXRGfWU= 159 | github.com/renproject/surge v1.1.2/go.mod h1:UnnFYpLSD0T9MzCcyHjbNdmxiQsDVyBDCuqcbhcaLCY= 160 | github.com/renproject/surge v1.1.3 h1:nCN3yWUbIbSDWyMaU6aCIidCE15yEcZb8Bcuziog/wU= 161 | github.com/renproject/surge v1.1.3/go.mod h1:UnnFYpLSD0T9MzCcyHjbNdmxiQsDVyBDCuqcbhcaLCY= 162 | github.com/renproject/surge v1.1.5 h1:uoup398vYr7NYYWFwes4Hmsa6AMFujfUEhYZKZ0nuCs= 163 | github.com/renproject/surge v1.1.5/go.mod h1:UnnFYpLSD0T9MzCcyHjbNdmxiQsDVyBDCuqcbhcaLCY= 164 | github.com/renproject/surge v1.2.2 h1:dN5TA82TIzKBJThHPPVaRIUix+PZwKSVTGIk4+xi5AA= 165 | github.com/renproject/surge v1.2.2/go.mod h1:jNVsKCM3/2PAllkc2cx7g2saG9NrHRX5x20I/TDMXOs= 166 | github.com/renproject/surge v1.2.5 h1:P2qKZxWiKrC8hw7in/hXVtic+dGkhd1M0H/1Lj+fJnw= 167 | github.com/renproject/surge v1.2.5/go.mod h1:jNVsKCM3/2PAllkc2cx7g2saG9NrHRX5x20I/TDMXOs= 168 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 169 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 170 | github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570/go.mod h1:8OR4w3TdeIHIh1g6EMY5p0gVNOovcWC+1vpc7naMuAw= 171 | github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3/go.mod h1:hpGUWaI9xL8pRQCTXQgocU38Qw1g0Us7n5PxxTwTCYU= 172 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 173 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 174 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 175 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 176 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 177 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 178 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 179 | github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= 180 | github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= 181 | github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc= 182 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 183 | github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 184 | github.com/xtaci/kcp-go v5.4.5+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE= 185 | github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= 186 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 187 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 188 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 189 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 190 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 191 | go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= 192 | go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 193 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 194 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 195 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 196 | golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 197 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 198 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 199 | golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU= 200 | golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 201 | golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI= 202 | golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 203 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= 204 | golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88= 205 | golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 206 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 207 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 208 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 209 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 210 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 211 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 212 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 213 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 214 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 215 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 216 | golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2 h1:4dVFTC832rPn4pomLSz1vA+are2+dU19w1H8OngV7nc= 217 | golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 218 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= 219 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 220 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 221 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 222 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 223 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 226 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 227 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 228 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 229 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 230 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 231 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 232 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 233 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8 h1:41hwlulw1prEMBxLQSlMSux1zxJf07B3WPsdjJlKZxE= 237 | golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 240 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 241 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= 242 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 243 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 244 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 245 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 246 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 247 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 248 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 249 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 250 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 251 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 252 | golang.org/x/tools v0.0.0-20190912185636-87d9f09c5d89/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 253 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 254 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 255 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= 256 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 257 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 258 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 259 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 260 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 261 | google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 262 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 263 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 264 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 265 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 266 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 267 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 268 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 269 | gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610= 270 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 271 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 272 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 273 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 274 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 275 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 276 | gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= 277 | gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= 278 | gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= 279 | gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= 280 | gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= 281 | gopkg.in/redis.v4 v4.2.4/go.mod h1:8KREHdypkCEojGKQcjMqAODMICIVwZAONWq8RowTITA= 282 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 283 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 284 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 285 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 286 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 287 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 288 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 289 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 290 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 291 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 292 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 293 | -------------------------------------------------------------------------------- /hyperdrive.go: -------------------------------------------------------------------------------- 1 | package hyperdrive 2 | -------------------------------------------------------------------------------- /hyperdrive_suite_test.go: -------------------------------------------------------------------------------- 1 | package hyperdrive_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestHyperdrive(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Hyperdrive Suite") 13 | } 14 | -------------------------------------------------------------------------------- /hyperdrive_test.go: -------------------------------------------------------------------------------- 1 | package hyperdrive_test 2 | -------------------------------------------------------------------------------- /mq/mq.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/renproject/hyperdrive/process" 8 | "github.com/renproject/id" 9 | ) 10 | 11 | // A MessageQueue is used to sort incoming messages by their height and round, 12 | // where messages with lower heights/rounds are found at the beginning of the 13 | // queue. Every sender, identified by their pid, has their own dedicated queue 14 | // with its own dedicated maximum capacity. This limits how far in the future 15 | // the MessageQueue will buffer messages, to prevent running out of memory. 16 | // However, this also means that explicit resynchronisation is needed, because 17 | // not all messages that are received are guaranteed to be kept. MessageQueues 18 | // do not handle de-duplication, and are not safe for concurrent use. 19 | type MessageQueue struct { 20 | opts Options 21 | queuesByPid map[id.Signatory][]interface{} 22 | } 23 | 24 | // New returns an empty MessageQueue. 25 | func New(opts Options) MessageQueue { 26 | return MessageQueue{ 27 | opts: opts, 28 | queuesByPid: make(map[id.Signatory][]interface{}), 29 | } 30 | } 31 | 32 | // Consume Propose, Prevote, and Precommit messages from the MessageQueue that 33 | // have heights up to (and including) the given height. The appropriate callback 34 | // will be called for every message that is consumed. All consumed messages will 35 | // be dropped from the MessageQueue. 36 | func (mq *MessageQueue) Consume(h process.Height, propose func(process.Propose), prevote func(process.Prevote), precommit func(process.Precommit), procsAllowed map[id.Signatory]bool) (n int) { 37 | for from, q := range mq.queuesByPid { 38 | for len(q) > 0 { 39 | if q[0] == nil || height(q[0]) > h { 40 | break 41 | } 42 | 43 | func() { 44 | defer func() { 45 | n++ 46 | q = q[1:] 47 | }() 48 | 49 | if ok := procsAllowed[from]; !ok { 50 | return 51 | } 52 | 53 | switch msg := q[0].(type) { 54 | case process.Propose: 55 | propose(msg) 56 | case process.Prevote: 57 | prevote(msg) 58 | case process.Precommit: 59 | precommit(msg) 60 | } 61 | }() 62 | } 63 | mq.queuesByPid[from] = q 64 | } 65 | return 66 | } 67 | 68 | // DropMessagesBelowHeight removes all messages from the internal message 69 | // queues that have height less than the given height. 70 | func (mq *MessageQueue) DropMessagesBelowHeight(h process.Height) { 71 | for from, q := range mq.queuesByPid { 72 | lastIndexBelowHeight := 0 73 | for _, m := range q { 74 | if m == nil { 75 | break 76 | } 77 | if height(m) < h { 78 | lastIndexBelowHeight++ 79 | } 80 | } 81 | mq.queuesByPid[from] = q[lastIndexBelowHeight:] 82 | } 83 | } 84 | 85 | // InsertPropose message into the MessageQueue. This method assumes that the 86 | // sender has already been authenticated and filtered. 87 | func (mq *MessageQueue) InsertPropose(propose process.Propose) { 88 | mq.insert(propose) 89 | } 90 | 91 | // InsertPrevote message into the MessageQueue. This method assumes that the 92 | // sender has already been authenticated and filtered. 93 | func (mq *MessageQueue) InsertPrevote(prevote process.Prevote) { 94 | mq.insert(prevote) 95 | } 96 | 97 | // InsertPrecommit message into the MessageQueue. This method assumes that the 98 | // sender has already been authenticated and filtered. 99 | func (mq *MessageQueue) InsertPrecommit(precommit process.Precommit) { 100 | mq.insert(precommit) 101 | } 102 | 103 | func (mq *MessageQueue) insert(msg interface{}) { 104 | // Initialise the queue for the sender of the message, to avoid nil-pointer 105 | // errors. This makes the assumption that messages that have not already 106 | // passed authentication checks will not be placed into the MessageQueue. 107 | msgFrom := from(msg) 108 | if _, ok := mq.queuesByPid[msgFrom]; !ok { 109 | mq.queuesByPid[msgFrom] = make([]interface{}, mq.opts.MaxCapacity) 110 | } 111 | 112 | // Load the queue from the map, and defer saving it back to the map. 113 | q := mq.queuesByPid[msgFrom] 114 | defer func() { mq.queuesByPid[msgFrom] = q }() 115 | 116 | // Find the index at which the message should be inserted to maintain 117 | // height/round ordering. 118 | msgHeight := height(msg) 119 | msgRound := round(msg) 120 | insertAt := sort.Search(len(q), func(i int) bool { 121 | if q[i] == nil { 122 | return true 123 | } 124 | 125 | height := height(q[i]) 126 | round := round(q[i]) 127 | return height > msgHeight || (height == msgHeight && round > msgRound) 128 | }) 129 | 130 | // Insert into the slice using the trick described at 131 | // https://github.com/golang/go/wiki/SliceTricks (which minimises 132 | // allocations and copying). 133 | q = append(q, nil) 134 | copy(q[insertAt+1:], q[insertAt:]) 135 | q[insertAt] = msg 136 | 137 | // If the queue for this sender has exceeded its maximum capacity, then we 138 | // drop excess elements. This protects against adversaries that might seek 139 | // to cause an OOM by sending messages "from the far future". 140 | if len(q) > mq.opts.MaxCapacity { 141 | q = q[:mq.opts.MaxCapacity] 142 | } 143 | } 144 | 145 | func height(msg interface{}) process.Height { 146 | switch msg := msg.(type) { 147 | case process.Propose: 148 | return msg.Height 149 | case process.Prevote: 150 | return msg.Height 151 | case process.Precommit: 152 | return msg.Height 153 | default: 154 | panic(fmt.Errorf("non-exhaustive pattern: %T", msg)) 155 | } 156 | } 157 | 158 | func round(msg interface{}) process.Round { 159 | switch msg := msg.(type) { 160 | case process.Propose: 161 | return msg.Round 162 | case process.Prevote: 163 | return msg.Round 164 | case process.Precommit: 165 | return msg.Round 166 | default: 167 | panic(fmt.Errorf("non-exhaustive pattern: %T", msg)) 168 | } 169 | } 170 | 171 | func from(msg interface{}) id.Signatory { 172 | switch msg := msg.(type) { 173 | case process.Propose: 174 | return msg.From 175 | case process.Prevote: 176 | return msg.From 177 | case process.Precommit: 178 | return msg.From 179 | default: 180 | panic(fmt.Errorf("non-exhaustive pattern: %T", msg)) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /mq/mq_suite_test.go: -------------------------------------------------------------------------------- 1 | package mq_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestMq(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Message Queue Suite") 13 | } 14 | -------------------------------------------------------------------------------- /mq/mq_test.go: -------------------------------------------------------------------------------- 1 | package mq_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing/quick" 6 | "time" 7 | 8 | "github.com/renproject/hyperdrive/mq" 9 | "github.com/renproject/hyperdrive/process" 10 | "github.com/renproject/hyperdrive/process/processutil" 11 | 12 | "github.com/renproject/id" 13 | 14 | . "github.com/onsi/ginkgo" 15 | . "github.com/onsi/gomega" 16 | ) 17 | 18 | var _ = Describe("MQ", func() { 19 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 20 | 21 | randomMsg := func(r *rand.Rand, from id.Signatory, height process.Height, round process.Round) interface{} { 22 | switch r.Int() % 3 { 23 | case 0: 24 | // propose msg 25 | msg := processutil.RandomPropose(r) 26 | msg.From = from 27 | msg.Height = height 28 | msg.Round = round 29 | return msg 30 | case 1: 31 | // prevote msg 32 | msg := processutil.RandomPrevote(r) 33 | msg.From = from 34 | msg.Height = height 35 | msg.Round = round 36 | return msg 37 | case 2: 38 | // precommit msg 39 | msg := processutil.RandomPrecommit(r) 40 | msg.From = from 41 | msg.Height = height 42 | msg.Round = round 43 | return msg 44 | default: 45 | panic("this should not happen") 46 | } 47 | } 48 | 49 | insertRandomMessages := func(queue *mq.MessageQueue, sender id.Signatory) (process.Height, process.Height, int) { 50 | // at the most 20 heights and rounds in increasing order 51 | heights := make([]process.Height, 1+r.Intn(10)) 52 | nextHeight := 1 53 | nextRound := 0 54 | for s := 0; s < cap(heights); s++ { 55 | nextHeight = nextHeight + r.Intn(10) 56 | heights[s] = process.Height(nextHeight) 57 | } 58 | rounds := make([]process.Round, 1+r.Intn(10)) 59 | for t := 0; t < cap(rounds); t++ { 60 | nextRound = nextRound + r.Intn(10) 61 | rounds[t] = process.Round(nextRound) 62 | } 63 | 64 | // append all messages and shuffle them 65 | msgsCount := cap(heights) * cap(rounds) 66 | msgs := make([]interface{}, 0, msgsCount) 67 | for s := range heights { 68 | for t := range rounds { 69 | msg := randomMsg(r, sender, heights[s], rounds[t]) 70 | msgs = append(msgs, msg) 71 | } 72 | } 73 | r.Shuffle(len(msgs), func(i, j int) { msgs[i], msgs[j] = msgs[j], msgs[i] }) 74 | 75 | // insert all msgs 76 | for _, msg := range msgs { 77 | switch msg := msg.(type) { 78 | case process.Propose: 79 | queue.InsertPropose(msg) 80 | case process.Prevote: 81 | queue.InsertPrevote(msg) 82 | case process.Precommit: 83 | queue.InsertPrecommit(msg) 84 | } 85 | } 86 | 87 | return heights[0], heights[len(heights)-1], msgsCount 88 | } 89 | 90 | Context("when we instantiate a new message queue", func() { 91 | It("should return an empty mq with the given options", func() { 92 | opts := mq.DefaultOptions() 93 | queue := mq.New(opts) 94 | 95 | // since the queue is empty, we don't expect any message 96 | proposeCallback := func(propose process.Propose) { 97 | Expect(true).ToNot(BeTrue()) 98 | } 99 | prevoteCallback := func(prevote process.Prevote) { 100 | Expect(true).ToNot(BeTrue()) 101 | } 102 | precommitCallback := func(precommit process.Precommit) { 103 | Expect(true).ToNot(BeTrue()) 104 | } 105 | 106 | n := queue.Consume( 107 | process.Height(9223372036854775807), 108 | proposeCallback, 109 | prevoteCallback, 110 | precommitCallback, 111 | map[id.Signatory]bool{}, 112 | ) 113 | 114 | Expect(n).To(Equal(0)) 115 | }) 116 | }) 117 | 118 | Context("when we can insert new messages", func() { 119 | Context("when filtering the sender against the whitelist", func() { 120 | Context("when the sender is whitelisted", func() { 121 | It("should process the message", func() { 122 | opts := mq.DefaultOptions() 123 | queue := mq.New(opts) 124 | 125 | loop := func() bool { 126 | sender := id.NewPrivKey().Signatory() 127 | height := process.Height(r.Int63()) 128 | round := process.Round(r.Int63()) 129 | procsAllowed := map[id.Signatory]bool{} 130 | procsAllowed[sender] = true 131 | 132 | msg := randomMsg(r, sender, height, round) 133 | switch msg := msg.(type) { 134 | case process.Propose: 135 | queue.InsertPropose(msg) 136 | case process.Prevote: 137 | queue.InsertPrevote(msg) 138 | case process.Precommit: 139 | queue.InsertPrecommit(msg) 140 | } 141 | 142 | processed := false 143 | proposeCallback := func(propose process.Propose) { 144 | processed = true 145 | } 146 | prevoteCallback := func(prevote process.Prevote) { 147 | processed = true 148 | } 149 | precommitCallback := func(precommit process.Precommit) { 150 | processed = true 151 | } 152 | 153 | // cannot consume msgs of height less than lowerHeight 154 | n := queue.Consume(height, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 155 | Expect(n).To(Equal(1)) 156 | Expect(processed).Should(BeTrue()) 157 | 158 | return true 159 | } 160 | Expect(quick.Check(loop, nil)).To(Succeed()) 161 | }) 162 | }) 163 | 164 | Context("when the sender is not whitelisted", func() { 165 | It("should reject the message", func() { 166 | opts := mq.DefaultOptions() 167 | queue := mq.New(opts) 168 | 169 | loop := func() bool { 170 | sender := id.NewPrivKey().Signatory() 171 | height := process.Height(r.Int63()) 172 | round := process.Round(r.Int63()) 173 | procsAllowed := map[id.Signatory]bool{} 174 | 175 | msg := randomMsg(r, sender, height, round) 176 | switch msg := msg.(type) { 177 | case process.Propose: 178 | queue.InsertPropose(msg) 179 | case process.Prevote: 180 | queue.InsertPrevote(msg) 181 | case process.Precommit: 182 | queue.InsertPrecommit(msg) 183 | } 184 | 185 | processed := false 186 | proposeCallback := func(propose process.Propose) { 187 | processed = true 188 | } 189 | prevoteCallback := func(prevote process.Prevote) { 190 | processed = true 191 | } 192 | precommitCallback := func(precommit process.Precommit) { 193 | processed = true 194 | } 195 | 196 | // It should filter out the message 197 | n := queue.Consume(height, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 198 | Expect(n).To(Equal(1)) 199 | Expect(processed).Should(BeFalse()) 200 | 201 | return true 202 | } 203 | Expect(quick.Check(loop, nil)).To(Succeed()) 204 | }) 205 | }) 206 | 207 | Context("when sender is removed from the whitelist in the future", func() { 208 | It("should process message from the sender until it's removed from the whitelist", func() { 209 | opts := mq.DefaultOptions() 210 | queue := mq.New(opts) 211 | 212 | loop := func() bool { 213 | sender := id.NewPrivKey().Signatory() 214 | height := process.Height(r.Int63()) 215 | higherHeight := height + 1 + process.Height(r.Intn(100)) 216 | round := process.Round(r.Int63()) 217 | procsAllowed := map[id.Signatory]bool{} 218 | procsAllowed[sender] = true 219 | 220 | msg := randomMsg(r, sender, height, round) 221 | switch msg := msg.(type) { 222 | case process.Propose: 223 | queue.InsertPropose(msg) 224 | case process.Prevote: 225 | queue.InsertPrevote(msg) 226 | case process.Precommit: 227 | queue.InsertPrecommit(msg) 228 | } 229 | 230 | msgWithHigherHeight := randomMsg(r, sender, higherHeight, round) 231 | switch msgWithHigherHeight := msgWithHigherHeight.(type) { 232 | case process.Propose: 233 | queue.InsertPropose(msgWithHigherHeight) 234 | case process.Prevote: 235 | queue.InsertPrevote(msgWithHigherHeight) 236 | case process.Precommit: 237 | queue.InsertPrecommit(msgWithHigherHeight) 238 | } 239 | 240 | processed := false 241 | proposeCallback := func(propose process.Propose) { 242 | processed = true 243 | } 244 | prevoteCallback := func(prevote process.Prevote) { 245 | processed = true 246 | } 247 | precommitCallback := func(precommit process.Precommit) { 248 | processed = true 249 | } 250 | 251 | // It should process the message when consuming current height 252 | n := queue.Consume(height, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 253 | Expect(n).To(Equal(1)) 254 | Expect(processed).Should(BeTrue()) 255 | 256 | // Remove the sender from whitelist 257 | delete(procsAllowed, sender) 258 | processed = false 259 | 260 | // It should not process the message when consuming future height 261 | n = queue.Consume(higherHeight, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 262 | Expect(n).To(Equal(1)) 263 | Expect(processed).Should(BeFalse()) 264 | 265 | return true 266 | } 267 | Expect(quick.Check(loop, nil)).To(Succeed()) 268 | }) 269 | }) 270 | 271 | Context("when sender is added to the whitelist in the future", func() { 272 | It("should not process message from the sender until it's added to the whitelist", func() { 273 | opts := mq.DefaultOptions() 274 | queue := mq.New(opts) 275 | 276 | loop := func() bool { 277 | sender := id.NewPrivKey().Signatory() 278 | height := process.Height(r.Int63()) 279 | higherHeight := height + 1 + process.Height(r.Intn(100)) 280 | round := process.Round(r.Int63()) 281 | procsAllowed := map[id.Signatory]bool{} 282 | 283 | msg := randomMsg(r, sender, height, round) 284 | switch msg := msg.(type) { 285 | case process.Propose: 286 | queue.InsertPropose(msg) 287 | case process.Prevote: 288 | queue.InsertPrevote(msg) 289 | case process.Precommit: 290 | queue.InsertPrecommit(msg) 291 | } 292 | 293 | msgWithHigherHeight := randomMsg(r, sender, higherHeight, round) 294 | switch msgWithHigherHeight := msgWithHigherHeight.(type) { 295 | case process.Propose: 296 | queue.InsertPropose(msgWithHigherHeight) 297 | case process.Prevote: 298 | queue.InsertPrevote(msgWithHigherHeight) 299 | case process.Precommit: 300 | queue.InsertPrecommit(msgWithHigherHeight) 301 | } 302 | 303 | processed := false 304 | proposeCallback := func(propose process.Propose) { 305 | processed = true 306 | } 307 | prevoteCallback := func(prevote process.Prevote) { 308 | processed = true 309 | } 310 | precommitCallback := func(precommit process.Precommit) { 311 | processed = true 312 | } 313 | 314 | // It should not process the message when consuming current height 315 | n := queue.Consume(height, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 316 | Expect(n).To(Equal(1)) 317 | Expect(processed).Should(BeFalse()) 318 | 319 | // Add the sender to whitelist 320 | procsAllowed[sender] = true 321 | 322 | // It should process the message when consuming future height 323 | n = queue.Consume(higherHeight, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 324 | Expect(n).To(Equal(1)) 325 | Expect(processed).Should(BeTrue()) 326 | 327 | return true 328 | } 329 | Expect(quick.Check(loop, nil)).To(Succeed()) 330 | }) 331 | }) 332 | }) 333 | 334 | Context("when two messages have different heights", func() { 335 | It("should correctly sort the messages based on height", func() { 336 | opts := mq.DefaultOptions() 337 | queue := mq.New(opts) 338 | 339 | loop := func() bool { 340 | sender := id.NewPrivKey().Signatory() 341 | lowerHeight := process.Height(r.Int63()) 342 | higherHeight := lowerHeight + 1 + process.Height(r.Intn(100)) 343 | procsAllowed := map[id.Signatory]bool{} 344 | procsAllowed[sender] = true 345 | 346 | // send msg1 347 | msg1 := randomMsg(r, sender, lowerHeight, processutil.RandomRound(r)) 348 | switch msg1 := msg1.(type) { 349 | case process.Propose: 350 | queue.InsertPropose(msg1) 351 | case process.Prevote: 352 | queue.InsertPrevote(msg1) 353 | case process.Precommit: 354 | queue.InsertPrecommit(msg1) 355 | } 356 | 357 | // send msg2 358 | msg2 := randomMsg(r, sender, higherHeight, processutil.RandomRound(r)) 359 | switch msg2 := msg2.(type) { 360 | case process.Propose: 361 | queue.InsertPropose(msg2) 362 | case process.Prevote: 363 | queue.InsertPrevote(msg2) 364 | case process.Precommit: 365 | queue.InsertPrecommit(msg2) 366 | } 367 | 368 | // we should first consume msg1 and then msg2 369 | i := 0 370 | proposeCallback := func(propose process.Propose) { 371 | switch i { 372 | case 0: 373 | Expect(propose.From.Equal(&sender)).To(BeTrue()) 374 | Expect(propose.Height).To(Equal(lowerHeight)) 375 | case 1: 376 | Expect(propose.From.Equal(&sender)).To(BeTrue()) 377 | Expect(propose.Height).To(Equal(higherHeight)) 378 | case 2: 379 | // we have only 2 msgs 380 | Expect(true).ToNot(BeTrue()) 381 | } 382 | 383 | i++ 384 | } 385 | 386 | prevoteCallback := func(prevote process.Prevote) { 387 | switch i { 388 | case 0: 389 | Expect(prevote.From.Equal(&sender)).To(BeTrue()) 390 | Expect(prevote.Height).To(Equal(lowerHeight)) 391 | case 1: 392 | Expect(prevote.From.Equal(&sender)).To(BeTrue()) 393 | Expect(prevote.Height).To(Equal(higherHeight)) 394 | case 2: 395 | // we have only 2 msgs 396 | Expect(true).ToNot(BeTrue()) 397 | } 398 | 399 | i++ 400 | } 401 | 402 | precommitCallback := func(precommit process.Precommit) { 403 | switch i { 404 | case 0: 405 | Expect(precommit.From.Equal(&sender)).To(BeTrue()) 406 | Expect(precommit.Height).To(Equal(lowerHeight)) 407 | case 1: 408 | Expect(precommit.From.Equal(&sender)).To(BeTrue()) 409 | Expect(precommit.Height).To(Equal(higherHeight)) 410 | case 2: 411 | // we have only 2 msgs 412 | Expect(true).ToNot(BeTrue()) 413 | } 414 | 415 | i++ 416 | } 417 | 418 | // cannot consume msgs of height less than lowerHeight 419 | evenLowerHeight := lowerHeight - 1 - process.Height(r.Intn(100)) 420 | n := queue.Consume(evenLowerHeight, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 421 | Expect(n).To(Equal(0)) 422 | Expect(i).To(Equal(0)) 423 | 424 | // consume all messages 425 | n = queue.Consume(higherHeight, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 426 | Expect(n).To(Equal(2)) 427 | Expect(i).To(Equal(2)) 428 | 429 | return true 430 | } 431 | Expect(quick.Check(loop, nil)).To(Succeed()) 432 | }) 433 | }) 434 | 435 | Context("when two messages have the same height", func() { 436 | It("should correctly sort the messages based on round", func() { 437 | opts := mq.DefaultOptions() 438 | queue := mq.New(opts) 439 | 440 | loop := func() bool { 441 | sender := id.NewPrivKey().Signatory() 442 | height := process.Height(r.Int63()) 443 | procsAllowed := map[id.Signatory]bool{} 444 | procsAllowed[sender] = true 445 | 446 | // at the most 20 rounds 447 | rounds := make([]process.Round, 1+r.Intn(20)) 448 | for t := 0; t < cap(rounds); t++ { 449 | rounds[t] = process.Round(t) 450 | } 451 | 452 | for t := range rounds { 453 | msg := randomMsg(r, sender, height, rounds[t]) 454 | switch msg := msg.(type) { 455 | case process.Propose: 456 | queue.InsertPropose(msg) 457 | case process.Prevote: 458 | queue.InsertPrevote(msg) 459 | case process.Precommit: 460 | queue.InsertPrecommit(msg) 461 | } 462 | } 463 | 464 | // we should first consume msg1 and then msg2 465 | t := 0 466 | proposeCallback := func(propose process.Propose) { 467 | if t > cap(rounds) { 468 | Expect(true).ToNot(BeTrue()) 469 | } 470 | 471 | Expect(propose.From.Equal(&sender)).To(BeTrue()) 472 | Expect(propose.Height).To(Equal(height)) 473 | Expect(propose.Round).To(Equal(rounds[t])) 474 | 475 | t++ 476 | } 477 | 478 | prevoteCallback := func(prevote process.Prevote) { 479 | if t > cap(rounds) { 480 | Expect(true).ToNot(BeTrue()) 481 | } 482 | 483 | Expect(prevote.From.Equal(&sender)).To(BeTrue()) 484 | Expect(prevote.Height).To(Equal(height)) 485 | Expect(prevote.Round).To(Equal(rounds[t])) 486 | 487 | t++ 488 | } 489 | 490 | precommitCallback := func(precommit process.Precommit) { 491 | if t > cap(rounds) { 492 | Expect(true).ToNot(BeTrue()) 493 | } 494 | 495 | Expect(precommit.From.Equal(&sender)).To(BeTrue()) 496 | Expect(precommit.Height).To(Equal(height)) 497 | Expect(precommit.Round).To(Equal(rounds[t])) 498 | 499 | t++ 500 | } 501 | 502 | // cannot consume msgs of height less than lowerHeight 503 | lowerHeight := height - 1 - process.Height(r.Intn(100)) 504 | n := queue.Consume(lowerHeight, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 505 | Expect(n).To(Equal(0)) 506 | Expect(t).To(Equal(0)) 507 | 508 | // consume all messages 509 | n = queue.Consume(height, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 510 | Expect(n).To(Equal(cap(rounds))) 511 | Expect(t).To(Equal(cap(rounds))) 512 | 513 | return true 514 | } 515 | Expect(quick.Check(loop, nil)).To(Succeed()) 516 | }) 517 | }) 518 | 519 | Context("when messages with different heights and rounds are inserted", func() { 520 | It("should correctly sort the messages, first by height, then by round", func() { 521 | opts := mq.DefaultOptions() 522 | queue := mq.New(opts) 523 | 524 | loop := func() bool { 525 | sender := id.NewPrivKey().Signatory() 526 | minHeight, maxHeight, msgsCount := insertRandomMessages(&queue, sender) 527 | procsAllowed := map[id.Signatory]bool{} 528 | procsAllowed[sender] = true 529 | 530 | // we should first consume msg1 and then msg2 531 | prevHeight := process.Height(-1) 532 | prevRound := process.Round(-1) 533 | i := 0 534 | proposeCallback := func(propose process.Propose) { 535 | // if we're starting an increased height 536 | if propose.Height > prevHeight { 537 | prevRound = process.Round(-1) 538 | } 539 | 540 | if i > msgsCount { 541 | Expect(true).ToNot(BeTrue()) 542 | } 543 | 544 | Expect(propose.From.Equal(&sender)).To(BeTrue()) 545 | Expect(propose.Height >= prevHeight).To(BeTrue()) 546 | Expect(propose.Round >= prevRound).To(BeTrue()) 547 | 548 | prevHeight = propose.Height 549 | prevRound = propose.Round 550 | 551 | i++ 552 | } 553 | 554 | prevoteCallback := func(prevote process.Prevote) { 555 | // if we're starting an increased height 556 | if prevote.Height > prevHeight { 557 | prevRound = process.Round(-1) 558 | } 559 | 560 | if i > msgsCount { 561 | Expect(true).ToNot(BeTrue()) 562 | } 563 | 564 | Expect(prevote.From.Equal(&sender)).To(BeTrue()) 565 | Expect(prevote.Height >= prevHeight).To(BeTrue()) 566 | Expect(prevote.Round >= prevRound).To(BeTrue()) 567 | 568 | prevHeight = prevote.Height 569 | prevRound = prevote.Round 570 | 571 | i++ 572 | } 573 | 574 | precommitCallback := func(precommit process.Precommit) { 575 | // if we're starting an increased height 576 | if precommit.Height > prevHeight { 577 | prevRound = process.Round(-1) 578 | } 579 | 580 | if i > msgsCount { 581 | Expect(true).ToNot(BeTrue()) 582 | } 583 | 584 | Expect(precommit.From.Equal(&sender)).To(BeTrue()) 585 | Expect(precommit.Height >= prevHeight).To(BeTrue()) 586 | Expect(precommit.Round >= prevRound).To(BeTrue()) 587 | 588 | prevHeight = precommit.Height 589 | prevRound = precommit.Round 590 | 591 | i++ 592 | } 593 | 594 | // cannot consume msgs of height less than the min height 595 | n := queue.Consume(minHeight-1, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 596 | Expect(n).To(Equal(0)) 597 | Expect(i).To(Equal(0)) 598 | 599 | // consume all messages 600 | n = queue.Consume(maxHeight, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 601 | Expect(n).To(Equal(msgsCount)) 602 | Expect(i).To(Equal(msgsCount)) 603 | 604 | return true 605 | } 606 | Expect(quick.Check(loop, nil)).To(Succeed()) 607 | }) 608 | }) 609 | }) 610 | 611 | Context("when dropping all messages below a certain height", func() { 612 | It("should remove all corresponding messages from the queues", func() { 613 | opts := mq.DefaultOptions() 614 | queue := mq.New(opts) 615 | 616 | loop := func() bool { 617 | sender := id.NewPrivKey().Signatory() 618 | procsAllowed := map[id.Signatory]bool{} 619 | procsAllowed[sender] = true 620 | _, maxHeight, _ := insertRandomMessages(&queue, sender) 621 | thresholdHeight := process.Height(r.Intn(int(maxHeight))) 622 | queue.DropMessagesBelowHeight(thresholdHeight) 623 | 624 | proposeCallback := func(propose process.Propose) { 625 | Expect(propose.Height >= thresholdHeight).To(BeTrue()) 626 | } 627 | prevoteCallback := func(prevote process.Prevote) { 628 | Expect(prevote.Height >= thresholdHeight).To(BeTrue()) 629 | } 630 | precommitCallback := func(precommit process.Precommit) { 631 | Expect(precommit.Height >= thresholdHeight).To(BeTrue()) 632 | } 633 | 634 | _ = queue.Consume(maxHeight, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 635 | return true 636 | } 637 | Expect(quick.Check(loop, nil)).To(Succeed()) 638 | }) 639 | }) 640 | 641 | Context("when we have reached the queue's max capacity", func() { 642 | It("trivial case when max capacity is 1", func() { 643 | loop := func() bool { 644 | opts := mq.DefaultOptions().WithMaxCapacity(1) 645 | queue := mq.New(opts) 646 | procsAllowed := map[id.Signatory]bool{} 647 | 648 | // insert a msg 649 | originalSender := id.NewPrivKey().Signatory() 650 | originalMsg := processutil.RandomPropose(r) 651 | originalMsg.From = originalSender 652 | originalMsg.Height = process.Height(1) 653 | originalMsg.Round = process.Round(1) 654 | procsAllowed[originalMsg.From] = true 655 | queue.InsertPropose(originalMsg) 656 | 657 | // any message in height > 1 or (height = 1 || round > 1) will be dropped 658 | // since every sender has a separate max capacity sized queue 659 | // this msg should be consumed 660 | msg := processutil.RandomPropose(r) 661 | msg.From = id.NewPrivKey().Signatory() 662 | msg.Height = process.Height(1) 663 | msg.Round = process.Round(2) 664 | procsAllowed[msg.From] = true 665 | queue.InsertPropose(msg) 666 | 667 | // so consuming will only return the first msg 668 | proposeCallback := func(propose process.Propose) {} 669 | n := queue.Consume(process.Height(1), proposeCallback, nil, nil, procsAllowed) 670 | Expect(n).To(Equal(2)) 671 | 672 | // re-insert the original msg 673 | queue.InsertPropose(originalMsg) 674 | 675 | // any message in height > 1 or (height = 1 || round > 1) will be dropped 676 | // since this msg has the same original sender, the max capacity is 677 | // applicable and this msg is dropped 678 | msg = processutil.RandomPropose(r) 679 | msg.From = originalSender 680 | msg.Height = process.Height(1) 681 | msg.Round = process.Round(2) 682 | queue.InsertPropose(msg) 683 | 684 | // so consuming will only return the original msg 685 | proposeCallback = func(propose process.Propose) { 686 | Expect(propose.Round).To(Equal(originalMsg.Round)) 687 | Expect(propose.From).To(Equal(originalSender)) 688 | } 689 | n = queue.Consume(process.Height(1), proposeCallback, nil, nil, procsAllowed) 690 | Expect(n).To(Equal(1)) 691 | 692 | // re-insert the original msg 693 | queue.InsertPropose(originalMsg) 694 | 695 | // any message in height <= 1 or (height = 1 && round < 1) will drop 696 | // the original msg 697 | msg = processutil.RandomPropose(r) 698 | msg.From = originalSender 699 | msg.Height = process.Height(1) 700 | msg.Round = process.Round(0) 701 | queue.InsertPropose(msg) 702 | 703 | // so consuming will only return the new msg 704 | proposeCallback = func(propose process.Propose) { 705 | Expect(propose.Round).To(Equal(msg.Round)) 706 | Expect(propose.From).To(Equal(originalSender)) 707 | } 708 | n = queue.Consume(process.Height(1), proposeCallback, nil, nil, procsAllowed) 709 | Expect(n).To(Equal(1)) 710 | 711 | return true 712 | } 713 | Expect(quick.Check(loop, nil)).To(Succeed()) 714 | }) 715 | 716 | It("should drop the excess messages", func() { 717 | loop := func() bool { 718 | // max capacity 719 | c := 5 + r.Intn(20) 720 | opts := mq.DefaultOptions().WithMaxCapacity(c) 721 | queue := mq.New(opts) 722 | 723 | // construct msgs 724 | // more messages than the queue's capacity 725 | // msgsCount > c 726 | sender := id.NewPrivKey().Signatory() 727 | height := process.Height(1) 728 | procsAllowed := map[id.Signatory]bool{} 729 | procsAllowed[sender] = true 730 | msgsCount := c + 5 + r.Intn(20) 731 | rounds := make([]process.Round, msgsCount) 732 | msgs := make([]interface{}, msgsCount) 733 | for i := range rounds { 734 | rounds[i] = process.Round(i) 735 | msgs[i] = randomMsg(r, sender, height, rounds[i]) 736 | } 737 | 738 | // insert all msgs 739 | for _, msg := range msgs { 740 | switch msg := msg.(type) { 741 | case process.Propose: 742 | queue.InsertPropose(msg) 743 | case process.Prevote: 744 | queue.InsertPrevote(msg) 745 | case process.Precommit: 746 | queue.InsertPrecommit(msg) 747 | } 748 | } 749 | 750 | // at the end of insertion, only the lowest round msgs should be in 751 | // the queue. The ones above the capacity will have been dropped 752 | // msg.Round < c 753 | // total msgs consumed = total capacity of queue 754 | i := 0 755 | maxMsgRound := process.Round(c) 756 | proposeCallback := func(propose process.Propose) { 757 | if i > msgsCount { 758 | Expect(true).ToNot(BeTrue()) 759 | } 760 | 761 | Expect(propose.Round < maxMsgRound).To(BeTrue()) 762 | 763 | i++ 764 | } 765 | 766 | prevoteCallback := func(prevote process.Prevote) { 767 | if i > msgsCount { 768 | Expect(true).ToNot(BeTrue()) 769 | } 770 | 771 | Expect(prevote.Round < maxMsgRound).To(BeTrue()) 772 | 773 | i++ 774 | } 775 | 776 | precommitCallback := func(precommit process.Precommit) { 777 | if i > msgsCount { 778 | Expect(true).ToNot(BeTrue()) 779 | } 780 | 781 | Expect(precommit.Round < maxMsgRound).To(BeTrue()) 782 | 783 | i++ 784 | } 785 | 786 | n := queue.Consume(height, proposeCallback, prevoteCallback, precommitCallback, procsAllowed) 787 | Expect(n).To(Equal(c)) 788 | Expect(i).To(Equal(c)) 789 | 790 | return true 791 | } 792 | Expect(quick.Check(loop, nil)).To(Succeed()) 793 | }) 794 | }) 795 | }) 796 | -------------------------------------------------------------------------------- /mq/opt.go: -------------------------------------------------------------------------------- 1 | package mq 2 | 3 | import "go.uber.org/zap" 4 | 5 | // Options define the Message Queue options 6 | type Options struct { 7 | Logger *zap.Logger 8 | MaxCapacity int 9 | } 10 | 11 | // DefaultOptions returns the default options as used by the Message Queue 12 | func DefaultOptions() Options { 13 | logger, err := zap.NewDevelopment() 14 | if err != nil { 15 | panic(err) 16 | } 17 | return Options{ 18 | Logger: logger, 19 | MaxCapacity: 1000, 20 | } 21 | } 22 | 23 | // WithLogger updates the logger used in the Message Queue 24 | func (opts Options) WithLogger(logger *zap.Logger) Options { 25 | opts.Logger = logger 26 | return opts 27 | } 28 | 29 | // WithMaxCapacity updates the maximum capacity of the Message Queue 30 | func (opts Options) WithMaxCapacity(capacity int) Options { 31 | opts.MaxCapacity = capacity 32 | return opts 33 | } 34 | -------------------------------------------------------------------------------- /mq/opt_test.go: -------------------------------------------------------------------------------- 1 | package mq_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing/quick" 6 | "time" 7 | 8 | "github.com/renproject/hyperdrive/mq" 9 | 10 | "go.uber.org/zap" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("MQ Opts", func() { 17 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 18 | 19 | Context("MQ Opts", func() { 20 | Specify("with default opts", func() { 21 | opts := mq.DefaultOptions() 22 | Expect(opts.MaxCapacity).To(Equal(1000)) 23 | }) 24 | 25 | Specify("with logger", func() { 26 | logger := zap.NewExample() 27 | _ = mq.DefaultOptions().WithLogger(logger) 28 | }) 29 | 30 | Specify("with max capacity", func() { 31 | loop := func() bool { 32 | capacity := int(r.Int63()) 33 | opts := mq.DefaultOptions().WithMaxCapacity(capacity) 34 | Expect(opts.MaxCapacity).To(Equal(capacity)) 35 | 36 | return true 37 | } 38 | Expect(quick.Check(loop, nil)).To(Succeed()) 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /process/message.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/renproject/id" 7 | "github.com/renproject/surge" 8 | ) 9 | 10 | // MessageType enumerates the various types of Hyperdrive messages. 11 | type MessageType int8 12 | 13 | const ( 14 | // MessageTypePropose is the message type for a propose message. 15 | MessageTypePropose MessageType = 1 16 | // MessageTypePrevote is the message type for a prevote message. 17 | MessageTypePrevote MessageType = 2 18 | // MessageTypePrecommit is the message type for a precommit message. 19 | MessageTypePrecommit MessageType = 3 20 | // MessageTypeTimeout is the message type for a timeout message. 21 | MessageTypeTimeout MessageType = 4 22 | ) 23 | 24 | // String implements the Stringer interface. 25 | func (ty MessageType) String() string { 26 | switch ty { 27 | case MessageTypePropose: 28 | return "Propose" 29 | case MessageTypePrevote: 30 | return "Prevote" 31 | case MessageTypePrecommit: 32 | return "Precommit" 33 | case MessageTypeTimeout: 34 | return "Timeout" 35 | default: 36 | return "Unknown" 37 | } 38 | } 39 | 40 | // A Propose message is sent by the proposer Process at most once per Round. The 41 | // Scheduler interfaces determines which Process is the proposer at any given 42 | // Height and Round. 43 | type Propose struct { 44 | Height Height `json:"height"` 45 | Round Round `json:"round"` 46 | ValidRound Round `json:"validRound"` 47 | Value Value `json:"value"` 48 | 49 | From id.Signatory `json:"from"` 50 | } 51 | 52 | // NewProposeHash receives fields of a propose message and hashes the message 53 | func NewProposeHash(height Height, round Round, validRound Round, value Value) (id.Hash, error) { 54 | sizeHint := surge.SizeHint(height) + surge.SizeHint(round) + surge.SizeHint(validRound) + surge.SizeHint(value) 55 | buf := make([]byte, sizeHint) 56 | return NewProposeHashWithBuffer(height, round, validRound, value, buf) 57 | } 58 | 59 | // NewProposeHashWithBuffer receives fields of a propose message, with a bytes buffer and hashes the message 60 | func NewProposeHashWithBuffer(height Height, round Round, validRound Round, value Value, data []byte) (id.Hash, error) { 61 | buf, rem, err := surge.Marshal(height, data, surge.MaxBytes) 62 | if err != nil { 63 | return id.Hash{}, fmt.Errorf("marshaling height=%v: %v", height, err) 64 | } 65 | buf, rem, err = surge.Marshal(round, buf, rem) 66 | if err != nil { 67 | return id.Hash{}, fmt.Errorf("marshaling round=%v: %v", round, err) 68 | } 69 | buf, rem, err = surge.Marshal(validRound, buf, rem) 70 | if err != nil { 71 | return id.Hash{}, fmt.Errorf("marshaling valid round=%v: %v", validRound, err) 72 | } 73 | buf, rem, err = surge.Marshal(value, buf, rem) 74 | if err != nil { 75 | return id.Hash{}, fmt.Errorf("marshaling value=%v: %v", value, err) 76 | } 77 | return id.NewHash(data), nil 78 | } 79 | 80 | // Equal compares two Proposes. If they are equal, then it return true, 81 | // otherwise it returns false. The signatures are not checked for equality, 82 | // because signatures include randomness. 83 | func (propose Propose) Equal(other *Propose) bool { 84 | return propose.Height == other.Height && 85 | propose.Round == other.Round && 86 | propose.ValidRound == other.ValidRound && 87 | propose.Value.Equal(&other.Value) && 88 | propose.From.Equal(&other.From) 89 | } 90 | 91 | // SizeHint returns the number of bytes required to represent this message in 92 | // binary. 93 | func (propose Propose) SizeHint() int { 94 | return surge.SizeHint(propose.Height) + 95 | surge.SizeHint(propose.Round) + 96 | surge.SizeHint(propose.ValidRound) + 97 | surge.SizeHint(propose.Value) + 98 | surge.SizeHint(propose.From) 99 | } 100 | 101 | // Marshal this message into binary. 102 | func (propose Propose) Marshal(buf []byte, rem int) ([]byte, int, error) { 103 | buf, rem, err := surge.Marshal(propose.Height, buf, rem) 104 | if err != nil { 105 | return buf, rem, fmt.Errorf("marshaling height=%v: %v", propose.Height, err) 106 | } 107 | buf, rem, err = surge.Marshal(propose.Round, buf, rem) 108 | if err != nil { 109 | return buf, rem, fmt.Errorf("marshaling round=%v: %v", propose.Round, err) 110 | } 111 | buf, rem, err = surge.Marshal(propose.ValidRound, buf, rem) 112 | if err != nil { 113 | return buf, rem, fmt.Errorf("marshaling valid round=%v: %v", propose.ValidRound, err) 114 | } 115 | buf, rem, err = surge.Marshal(propose.Value, buf, rem) 116 | if err != nil { 117 | return buf, rem, fmt.Errorf("marshaling value=%v: %v", propose.Value, err) 118 | } 119 | buf, rem, err = surge.Marshal(propose.From, buf, rem) 120 | if err != nil { 121 | return buf, rem, fmt.Errorf("marshaling from=%v: %v", propose.From, err) 122 | } 123 | return buf, rem, nil 124 | } 125 | 126 | // Unmarshal binary into this message. 127 | func (propose *Propose) Unmarshal(buf []byte, rem int) ([]byte, int, error) { 128 | buf, rem, err := surge.Unmarshal(&propose.Height, buf, rem) 129 | if err != nil { 130 | return buf, rem, fmt.Errorf("unmarshaling height: %v", err) 131 | } 132 | buf, rem, err = surge.Unmarshal(&propose.Round, buf, rem) 133 | if err != nil { 134 | return buf, rem, fmt.Errorf("unmarshaling round: %v", err) 135 | } 136 | buf, rem, err = surge.Unmarshal(&propose.ValidRound, buf, rem) 137 | if err != nil { 138 | return buf, rem, fmt.Errorf("unmarshaling valid round: %v", err) 139 | } 140 | buf, rem, err = surge.Unmarshal(&propose.Value, buf, rem) 141 | if err != nil { 142 | return buf, rem, fmt.Errorf("unmarshaling value: %v", err) 143 | } 144 | buf, rem, err = surge.Unmarshal(&propose.From, buf, rem) 145 | if err != nil { 146 | return buf, rem, fmt.Errorf("unmarshaling from: %v", err) 147 | } 148 | return buf, rem, nil 149 | } 150 | 151 | // A Prevote is sent by every correct Process at most once per Round. It is the 152 | // first step of reaching consensus. Informally, if a correct Process receives 153 | // 2F+1 Precommits for a Value, then it will Precommit to that Value. However, 154 | // there are many other conditions which can cause a Process to Prevote. See the 155 | // Process for more information. 156 | type Prevote struct { 157 | Height Height `json:"height"` 158 | Round Round `json:"round"` 159 | Value Value `json:"value"` 160 | 161 | From id.Signatory `json:"from"` 162 | } 163 | 164 | // NewPrevoteHash receives fields of a prevote message and hashes the message 165 | func NewPrevoteHash(height Height, round Round, value Value) (id.Hash, error) { 166 | sizeHint := surge.SizeHint(height) + surge.SizeHint(round) + surge.SizeHint(value) 167 | buf := make([]byte, sizeHint) 168 | return NewPrevoteHashWithBuffer(height, round, value, buf) 169 | } 170 | 171 | // NewPrevoteHashWithBuffer receives fields of a prevote message, with a bytes buffer and hashes the message 172 | func NewPrevoteHashWithBuffer(height Height, round Round, value Value, data []byte) (id.Hash, error) { 173 | buf, rem, err := surge.Marshal(height, data, surge.MaxBytes) 174 | if err != nil { 175 | return id.Hash{}, fmt.Errorf("marshaling height=%v: %v", height, err) 176 | } 177 | buf, rem, err = surge.Marshal(round, buf, rem) 178 | if err != nil { 179 | return id.Hash{}, fmt.Errorf("marshaling round=%v: %v", round, err) 180 | } 181 | buf, rem, err = surge.Marshal(value, buf, rem) 182 | if err != nil { 183 | return id.Hash{}, fmt.Errorf("marshaling value=%v: %v", value, err) 184 | } 185 | return id.NewHash(data), nil 186 | } 187 | 188 | // Equal compares two Prevotes. If they are equal, then it return true, 189 | // otherwise it returns false. The signatures are not checked for equality, 190 | // because signatures include randomness. 191 | func (prevote Prevote) Equal(other *Prevote) bool { 192 | return prevote.Height == other.Height && 193 | prevote.Round == other.Round && 194 | prevote.Value.Equal(&other.Value) && 195 | prevote.From.Equal(&other.From) 196 | } 197 | 198 | // SizeHint returns the number of bytes required to represent this message in 199 | // binary. 200 | func (prevote Prevote) SizeHint() int { 201 | return surge.SizeHint(prevote.Height) + 202 | surge.SizeHint(prevote.Round) + 203 | surge.SizeHint(prevote.Value) + 204 | surge.SizeHint(prevote.From) 205 | } 206 | 207 | // Marshal this message into binary. 208 | func (prevote Prevote) Marshal(buf []byte, rem int) ([]byte, int, error) { 209 | buf, rem, err := surge.Marshal(prevote.Height, buf, rem) 210 | if err != nil { 211 | return buf, rem, fmt.Errorf("marshaling height=%v: %v", prevote.Height, err) 212 | } 213 | buf, rem, err = surge.Marshal(prevote.Round, buf, rem) 214 | if err != nil { 215 | return buf, rem, fmt.Errorf("marshaling round=%v: %v", prevote.Round, err) 216 | } 217 | buf, rem, err = surge.Marshal(prevote.Value, buf, rem) 218 | if err != nil { 219 | return buf, rem, fmt.Errorf("marshaling value=%v: %v", prevote.Value, err) 220 | } 221 | buf, rem, err = surge.Marshal(prevote.From, buf, rem) 222 | if err != nil { 223 | return buf, rem, fmt.Errorf("marshaling from=%v: %v", prevote.From, err) 224 | } 225 | return buf, rem, nil 226 | } 227 | 228 | // Unmarshal binary into this message. 229 | func (prevote *Prevote) Unmarshal(buf []byte, rem int) ([]byte, int, error) { 230 | buf, rem, err := surge.Unmarshal(&prevote.Height, buf, rem) 231 | if err != nil { 232 | return buf, rem, fmt.Errorf("unmarshaling height: %v", err) 233 | } 234 | buf, rem, err = surge.Unmarshal(&prevote.Round, buf, rem) 235 | if err != nil { 236 | return buf, rem, fmt.Errorf("unmarshaling round: %v", err) 237 | } 238 | buf, rem, err = surge.Unmarshal(&prevote.Value, buf, rem) 239 | if err != nil { 240 | return buf, rem, fmt.Errorf("unmarshaling value: %v", err) 241 | } 242 | buf, rem, err = surge.Unmarshal(&prevote.From, buf, rem) 243 | if err != nil { 244 | return buf, rem, fmt.Errorf("unmarshaling from: %v", err) 245 | } 246 | return buf, rem, nil 247 | } 248 | 249 | // A Precommit is sent by every correct Process at most once per Round. It is 250 | // the second step of reaching consensus. Informally, if a correct Process 251 | // receives 2F+1 Precommits for a Value, then it will commit to that Value and 252 | // progress to the next Height. However, there are many other conditions which 253 | // can cause a Process to Precommit. See the Process for more information. 254 | type Precommit struct { 255 | Height Height `json:"height"` 256 | Round Round `json:"round"` 257 | Value Value `json:"value"` 258 | 259 | From id.Signatory `json:"from"` 260 | } 261 | 262 | // NewPrecommitHash receives fields of a precommit message and hashes the message 263 | func NewPrecommitHash(height Height, round Round, value Value) (id.Hash, error) { 264 | sizeHint := surge.SizeHint(height) + surge.SizeHint(round) + surge.SizeHint(value) 265 | buf := make([]byte, sizeHint) 266 | return NewPrecommitHashWithBuffer(height, round, value, buf) 267 | } 268 | 269 | // NewPrecommitHashWithBuffer receives fields of a precommit message, with a bytes buffer and hashes the message 270 | func NewPrecommitHashWithBuffer(height Height, round Round, value Value, data []byte) (id.Hash, error) { 271 | buf, rem, err := surge.Marshal(height, data, surge.MaxBytes) 272 | if err != nil { 273 | return id.Hash{}, fmt.Errorf("marshaling height=%v: %v", height, err) 274 | } 275 | buf, rem, err = surge.Marshal(round, buf, rem) 276 | if err != nil { 277 | return id.Hash{}, fmt.Errorf("marshaling round=%v: %v", round, err) 278 | } 279 | buf, rem, err = surge.Marshal(value, buf, rem) 280 | if err != nil { 281 | return id.Hash{}, fmt.Errorf("marshaling value=%v: %v", value, err) 282 | } 283 | return id.NewHash(data), nil 284 | } 285 | 286 | // Equal compares two Precommits. If they are equal, then it return true, 287 | // otherwise it returns false. The signatures are not checked for equality, 288 | // because signatures include randomness. 289 | func (precommit Precommit) Equal(other *Precommit) bool { 290 | return precommit.Height == other.Height && 291 | precommit.Round == other.Round && 292 | precommit.Value.Equal(&other.Value) && 293 | precommit.From.Equal(&other.From) 294 | } 295 | 296 | // SizeHint returns the number of bytes required to represent this message in 297 | // binary. 298 | func (precommit Precommit) SizeHint() int { 299 | return surge.SizeHint(precommit.Height) + 300 | surge.SizeHint(precommit.Round) + 301 | surge.SizeHint(precommit.Value) + 302 | surge.SizeHint(precommit.From) 303 | } 304 | 305 | // Marshal this message into binary. 306 | func (precommit Precommit) Marshal(buf []byte, rem int) ([]byte, int, error) { 307 | buf, rem, err := surge.Marshal(precommit.Height, buf, rem) 308 | if err != nil { 309 | return buf, rem, fmt.Errorf("marshaling height=%v: %v", precommit.Height, err) 310 | } 311 | buf, rem, err = surge.Marshal(precommit.Round, buf, rem) 312 | if err != nil { 313 | return buf, rem, fmt.Errorf("marshaling round=%v: %v", precommit.Round, err) 314 | } 315 | buf, rem, err = surge.Marshal(precommit.Value, buf, rem) 316 | if err != nil { 317 | return buf, rem, fmt.Errorf("marshaling value=%v: %v", precommit.Value, err) 318 | } 319 | buf, rem, err = surge.Marshal(precommit.From, buf, rem) 320 | if err != nil { 321 | return buf, rem, fmt.Errorf("marshaling from=%v: %v", precommit.From, err) 322 | } 323 | return buf, rem, nil 324 | } 325 | 326 | // Unmarshal binary into this message. 327 | func (precommit *Precommit) Unmarshal(buf []byte, rem int) ([]byte, int, error) { 328 | buf, rem, err := surge.Unmarshal(&precommit.Height, buf, rem) 329 | if err != nil { 330 | return buf, rem, fmt.Errorf("unmarshaling height: %v", err) 331 | } 332 | buf, rem, err = surge.Unmarshal(&precommit.Round, buf, rem) 333 | if err != nil { 334 | return buf, rem, fmt.Errorf("unmarshaling round: %v", err) 335 | } 336 | buf, rem, err = surge.Unmarshal(&precommit.Value, buf, rem) 337 | if err != nil { 338 | return buf, rem, fmt.Errorf("unmarshaling value: %v", err) 339 | } 340 | buf, rem, err = surge.Unmarshal(&precommit.From, buf, rem) 341 | if err != nil { 342 | return buf, rem, fmt.Errorf("unmarshaling from: %v", err) 343 | } 344 | return buf, rem, nil 345 | } 346 | -------------------------------------------------------------------------------- /process/message_test.go: -------------------------------------------------------------------------------- 1 | package process_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing/quick" 6 | "time" 7 | 8 | "github.com/renproject/hyperdrive/process" 9 | "github.com/renproject/hyperdrive/process/processutil" 10 | "github.com/renproject/id" 11 | "github.com/renproject/surge" 12 | 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var _ = Describe("Propose", func() { 18 | Context("when unmarshaling fuzz", func() { 19 | It("should not panic", func() { 20 | f := func(fuzz []byte) bool { 21 | msg := process.Propose{} 22 | Expect(surge.FromBinary(&msg, fuzz)).ToNot(Succeed()) 23 | return true 24 | } 25 | Expect(quick.Check(f, nil)).To(Succeed()) 26 | }) 27 | }) 28 | 29 | Context("when marshaling and then unmarshaling", func() { 30 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 31 | 32 | It("should equal itself", func() { 33 | f := func(height process.Height, round, validRound process.Round, value process.Value, from id.Signatory) bool { 34 | expected := process.Propose{ 35 | Height: height, 36 | Round: round, 37 | ValidRound: validRound, 38 | Value: value, 39 | From: from, 40 | } 41 | data, err := surge.ToBinary(expected) 42 | Expect(err).ToNot(HaveOccurred()) 43 | got := process.Propose{} 44 | err = surge.FromBinary(&got, data) 45 | Expect(err).ToNot(HaveOccurred()) 46 | Expect(got.Equal(&expected)).To(BeTrue()) 47 | return true 48 | } 49 | Expect(quick.Check(f, nil)).To(Succeed()) 50 | }) 51 | 52 | randomMsg := func(r *rand.Rand) interface{} { 53 | switch r.Int() % 3 { 54 | case 0: 55 | return processutil.RandomPropose(r) 56 | case 1: 57 | return processutil.RandomPrevote(r) 58 | case 2: 59 | return processutil.RandomPrecommit(r) 60 | default: 61 | panic("this should not happen") 62 | } 63 | } 64 | 65 | It("when enough size is not available (marshaling)", func() { 66 | loop := func() bool { 67 | msg := randomMsg(r) 68 | switch msg := msg.(type) { 69 | case process.Propose: 70 | buf := make([]byte, msg.SizeHint()) 71 | sizeAvailable := r.Intn(msg.SizeHint()) 72 | _, _, err := msg.Marshal(buf, sizeAvailable) 73 | Expect(err).To(HaveOccurred()) 74 | case process.Prevote: 75 | buf := make([]byte, msg.SizeHint()) 76 | sizeAvailable := r.Intn(msg.SizeHint()) 77 | _, _, err := msg.Marshal(buf, sizeAvailable) 78 | Expect(err).To(HaveOccurred()) 79 | case process.Precommit: 80 | buf := make([]byte, msg.SizeHint()) 81 | sizeAvailable := r.Intn(msg.SizeHint()) 82 | _, _, err := msg.Marshal(buf, sizeAvailable) 83 | Expect(err).To(HaveOccurred()) 84 | } 85 | 86 | return true 87 | } 88 | Expect(quick.Check(loop, nil)).To(Succeed()) 89 | }) 90 | 91 | It("when enough size is not available (unmarshaling)", func() { 92 | loop := func() bool { 93 | msg := randomMsg(r) 94 | switch msg := msg.(type) { 95 | case process.Propose: 96 | sizeHint := msg.SizeHint() 97 | buf := make([]byte, sizeHint) 98 | _, _, err := msg.Marshal(buf, sizeHint) 99 | Expect(err).ToNot(HaveOccurred()) 100 | var unmarshalled process.Propose 101 | sizeAvailable := r.Intn(sizeHint) 102 | _, _, err = unmarshalled.Unmarshal(buf, sizeAvailable) 103 | Expect(err).To(HaveOccurred()) 104 | case process.Prevote: 105 | sizeHint := msg.SizeHint() 106 | buf := make([]byte, sizeHint) 107 | _, _, err := msg.Marshal(buf, sizeHint) 108 | Expect(err).ToNot(HaveOccurred()) 109 | var unmarshalled process.Prevote 110 | sizeAvailable := r.Intn(sizeHint) 111 | _, _, err = unmarshalled.Unmarshal(buf, sizeAvailable) 112 | Expect(err).To(HaveOccurred()) 113 | case process.Precommit: 114 | sizeHint := msg.SizeHint() 115 | buf := make([]byte, sizeHint) 116 | _, _, err := msg.Marshal(buf, sizeHint) 117 | Expect(err).ToNot(HaveOccurred()) 118 | var unmarshalled process.Precommit 119 | sizeAvailable := r.Intn(sizeHint) 120 | _, _, err = unmarshalled.Unmarshal(buf, sizeAvailable) 121 | Expect(err).To(HaveOccurred()) 122 | } 123 | 124 | return true 125 | } 126 | Expect(quick.Check(loop, nil)).To(Succeed()) 127 | }) 128 | }) 129 | 130 | Context("when compute the hash", func() { 131 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 132 | 133 | It("should not be random", func() { 134 | f := func(height process.Height, round, validRound process.Round, value process.Value) bool { 135 | expected, err := process.NewProposeHash(height, round, validRound, value) 136 | Expect(err).ToNot(HaveOccurred()) 137 | got, err := process.NewProposeHash(height, round, validRound, value) 138 | Expect(err).ToNot(HaveOccurred()) 139 | Expect(got.Equal(&expected)).To(BeTrue()) 140 | return true 141 | } 142 | Expect(quick.Check(f, nil)).To(Succeed()) 143 | }) 144 | 145 | It("should the expected signatory", func() { 146 | f := func(height process.Height, round, validRound process.Round, value process.Value) bool { 147 | privKey := id.NewPrivKey() 148 | hash, err := process.NewProposeHash(height, round, validRound, value) 149 | Expect(err).ToNot(HaveOccurred()) 150 | signature, err := privKey.Sign(&hash) 151 | Expect(err).ToNot(HaveOccurred()) 152 | signatory, err := signature.Signatory(&hash) 153 | Expect(err).ToNot(HaveOccurred()) 154 | Expect(privKey.Signatory().Equal(&signatory)).To(BeTrue()) 155 | return true 156 | } 157 | Expect(quick.Check(f, nil)).To(Succeed()) 158 | }) 159 | 160 | It("should fail when not enough bytes available (propose)", func() { 161 | loop := func() bool { 162 | propose := processutil.RandomPropose(r) 163 | sizeHint := surge.SizeHint(propose.Height) + 164 | surge.SizeHint(propose.Round) + 165 | surge.SizeHint(propose.ValidRound) + 166 | surge.SizeHint(propose.Value) 167 | sizeAvailable := r.Intn(sizeHint) 168 | buf := make([]byte, sizeAvailable) 169 | _, err := process.NewProposeHashWithBuffer(propose.Height, propose.Round, propose.ValidRound, propose.Value, buf) 170 | Expect(err).To(HaveOccurred()) 171 | return true 172 | } 173 | Expect(quick.Check(loop, nil)).To(Succeed()) 174 | }) 175 | 176 | It("should fail when not enough bytes available (prevote)", func() { 177 | loop := func() bool { 178 | prevote := processutil.RandomPrevote(r) 179 | sizeHint := surge.SizeHint(prevote.Height) + 180 | surge.SizeHint(prevote.Round) + 181 | surge.SizeHint(prevote.Value) 182 | sizeAvailable := r.Intn(sizeHint) 183 | buf := make([]byte, sizeAvailable) 184 | _, err := process.NewPrevoteHashWithBuffer(prevote.Height, prevote.Round, prevote.Value, buf) 185 | Expect(err).To(HaveOccurred()) 186 | return true 187 | } 188 | Expect(quick.Check(loop, nil)).To(Succeed()) 189 | }) 190 | 191 | It("should fail when not enough bytes available (precommit)", func() { 192 | loop := func() bool { 193 | precommit := processutil.RandomPrecommit(r) 194 | sizeHint := surge.SizeHint(precommit.Height) + 195 | surge.SizeHint(precommit.Round) + 196 | surge.SizeHint(precommit.Value) 197 | sizeAvailable := r.Intn(sizeHint) 198 | buf := make([]byte, sizeAvailable) 199 | _, err := process.NewPrecommitHashWithBuffer(precommit.Height, precommit.Round, precommit.Value, buf) 200 | Expect(err).To(HaveOccurred()) 201 | return true 202 | } 203 | Expect(quick.Check(loop, nil)).To(Succeed()) 204 | }) 205 | }) 206 | }) 207 | 208 | var _ = Describe("Prevote", func() { 209 | Context("when unmarshaling fuzz", func() { 210 | It("should not panic", func() { 211 | f := func(fuzz []byte) bool { 212 | msg := process.Prevote{} 213 | Expect(surge.FromBinary(&msg, fuzz)).ToNot(Succeed()) 214 | return true 215 | } 216 | Expect(quick.Check(f, nil)).To(Succeed()) 217 | }) 218 | }) 219 | 220 | Context("when marshaling and then unmarshaling", func() { 221 | It("should equal itself", func() { 222 | f := func(height process.Height, round process.Round, value process.Value, from id.Signatory, signature id.Signature) bool { 223 | expected := process.Prevote{ 224 | Height: height, 225 | Round: round, 226 | Value: value, 227 | From: from, 228 | } 229 | data, err := surge.ToBinary(expected) 230 | Expect(err).ToNot(HaveOccurred()) 231 | got := process.Prevote{} 232 | err = surge.FromBinary(&got, data) 233 | Expect(err).ToNot(HaveOccurred()) 234 | Expect(got.Equal(&expected)).To(BeTrue()) 235 | return true 236 | } 237 | Expect(quick.Check(f, nil)).To(Succeed()) 238 | }) 239 | }) 240 | 241 | Context("when compute the hash", func() { 242 | It("should not be random", func() { 243 | f := func(height process.Height, round process.Round, value process.Value) bool { 244 | expected, err := process.NewPrevoteHash(height, round, value) 245 | Expect(err).ToNot(HaveOccurred()) 246 | got, err := process.NewPrevoteHash(height, round, value) 247 | Expect(err).ToNot(HaveOccurred()) 248 | Expect(got.Equal(&expected)).To(BeTrue()) 249 | return true 250 | } 251 | Expect(quick.Check(f, nil)).To(Succeed()) 252 | }) 253 | 254 | It("should the expected signatory", func() { 255 | f := func(height process.Height, round process.Round, value process.Value) bool { 256 | privKey := id.NewPrivKey() 257 | hash, err := process.NewPrevoteHash(height, round, value) 258 | Expect(err).ToNot(HaveOccurred()) 259 | signature, err := privKey.Sign(&hash) 260 | Expect(err).ToNot(HaveOccurred()) 261 | signatory, err := signature.Signatory(&hash) 262 | Expect(err).ToNot(HaveOccurred()) 263 | Expect(privKey.Signatory().Equal(&signatory)).To(BeTrue()) 264 | return true 265 | } 266 | Expect(quick.Check(f, nil)).To(Succeed()) 267 | }) 268 | }) 269 | }) 270 | 271 | var _ = Describe("Precommit", func() { 272 | Context("when unmarshaling fuzz", func() { 273 | It("should not panic", func() { 274 | f := func(fuzz []byte) bool { 275 | msg := process.Precommit{} 276 | Expect(surge.FromBinary(&msg, fuzz)).ToNot(Succeed()) 277 | return true 278 | } 279 | Expect(quick.Check(f, nil)).To(Succeed()) 280 | }) 281 | }) 282 | 283 | Context("when marshaling and then unmarshaling", func() { 284 | It("should equal itself", func() { 285 | f := func(height process.Height, round process.Round, value process.Value, from id.Signatory, signature id.Signature) bool { 286 | expected := process.Precommit{ 287 | Height: height, 288 | Round: round, 289 | Value: value, 290 | From: from, 291 | } 292 | data, err := surge.ToBinary(expected) 293 | Expect(err).ToNot(HaveOccurred()) 294 | got := process.Precommit{} 295 | err = surge.FromBinary(&got, data) 296 | Expect(err).ToNot(HaveOccurred()) 297 | Expect(got.Equal(&expected)).To(BeTrue()) 298 | return true 299 | } 300 | Expect(quick.Check(f, nil)).To(Succeed()) 301 | }) 302 | }) 303 | 304 | Context("when compute the hash", func() { 305 | It("should not be random", func() { 306 | f := func(height process.Height, round process.Round, value process.Value) bool { 307 | expected, err := process.NewPrecommitHash(height, round, value) 308 | Expect(err).ToNot(HaveOccurred()) 309 | got, err := process.NewPrecommitHash(height, round, value) 310 | Expect(err).ToNot(HaveOccurred()) 311 | Expect(got.Equal(&expected)).To(BeTrue()) 312 | return true 313 | } 314 | Expect(quick.Check(f, nil)).To(Succeed()) 315 | }) 316 | 317 | It("should the expected signatory", func() { 318 | f := func(height process.Height, round process.Round, value process.Value) bool { 319 | privKey := id.NewPrivKey() 320 | hash, err := process.NewPrecommitHash(height, round, value) 321 | Expect(err).ToNot(HaveOccurred()) 322 | signature, err := privKey.Sign(&hash) 323 | Expect(err).ToNot(HaveOccurred()) 324 | signatory, err := signature.Signatory(&hash) 325 | Expect(err).ToNot(HaveOccurred()) 326 | Expect(privKey.Signatory().Equal(&signatory)).To(BeTrue()) 327 | return true 328 | } 329 | Expect(quick.Check(f, nil)).To(Succeed()) 330 | }) 331 | }) 332 | }) 333 | -------------------------------------------------------------------------------- /process/process.go: -------------------------------------------------------------------------------- 1 | // Package process implements the Byzantine fault tolerant consensus algorithm 2 | // described by "The latest gossip of BFT consensus" (Buchman et al.), which can 3 | // be found at https://arxiv.org/pdf/1807.04938.pdf. It makes extensive use of 4 | // dependency injection, and concrete implementations must be careful to meet all 5 | // of the requirements specified by the interface, otherwise the correctness of 6 | // the consensus algorithm can be broken. 7 | package process 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/renproject/id" 13 | "github.com/renproject/surge" 14 | ) 15 | 16 | // A Timer is used to schedule timeout events. 17 | type Timer interface { 18 | // TimeoutPropose is called when the Process needs its OnTimeoutPropose 19 | // method called after a timeout. The timeout should be proportional to the 20 | // Round. 21 | TimeoutPropose(Height, Round) 22 | // TimeoutPrevote is called when the Process needs its OnTimeoutPrevote 23 | // method called after a timeout. The timeout should be proportional to the 24 | // Round. 25 | TimeoutPrevote(Height, Round) 26 | // TimeoutPrecommit is called when the Process needs its OnTimeoutPrecommit 27 | // method called after a timeout. The timeout should be proportional to the 28 | // Round. 29 | TimeoutPrecommit(Height, Round) 30 | } 31 | 32 | // A Scheduler is used to determine which Process should be proposing a Value at 33 | // the given Height and Round. A Scheduler must be derived solely from the 34 | // Height, Round, and Values on which all correct Processes have already 35 | // achieved consensus. 36 | type Scheduler interface { 37 | Schedule(height Height, round Round) id.Signatory 38 | } 39 | 40 | // A Proposer is used to propose new Values for consensus. A Proposer must only 41 | // ever return a valid Value, and once it returns a Value, it must never return 42 | // a different Value for the same Height and Round. 43 | type Proposer interface { 44 | Propose(Height, Round) Value 45 | } 46 | 47 | // A Broadcaster is used to broadcast Propose, Prevote, and Precommit messages 48 | // to all Processes in the consensus algorithm, including the Process that 49 | // initiated the broadcast. It is assumed that all messages between correct 50 | // Processes are eventually delivered, although no specific order is assumed. 51 | // 52 | // Once a Value has been broadcast as part of a Propose, Prevote, or Precommit 53 | // message, different Values must not be broadcast for that same message type 54 | // with the same Height and Round. The same restriction applies to valid Rounds 55 | // broadcast with a Propose message. 56 | type Broadcaster interface { 57 | BroadcastPropose(Propose) 58 | BroadcastPrevote(Prevote) 59 | BroadcastPrecommit(Precommit) 60 | } 61 | 62 | // A Validator is used to validate a proposed Value. Processes are not required 63 | // to agree on the validity of a Value. 64 | type Validator interface { 65 | Valid(Height, Round, Value) bool 66 | } 67 | 68 | // A Committer is used to emit Values that are committed. The commitment of a 69 | // new Value implies that all correct Processes agree on this Value at this 70 | // Height, and will never revert. 71 | type Committer interface { 72 | Commit(Height, Value) (uint64, Scheduler) 73 | } 74 | 75 | // A Catcher is used to catch bad behaviour in other Processes. For example, 76 | // when the same Process sends two different Proposes at the same Height and 77 | // Round. Not all instances of bad behaviour are caught by the Process. For 78 | // example, when a Process moves to a new height, it will stop processing 79 | // messages from previous heights, and so malicious behaviour that occurs in 80 | // those dropped messages will not be caught. If it is required that all bad 81 | // behaviour is caught, then additional checks must be made — outside the 82 | // context of Hyperdrive — before passing messages to the Process. 83 | type Catcher interface { 84 | CatchDoublePropose(Propose, Propose) 85 | CatchDoublePrevote(Prevote, Prevote) 86 | CatchDoublePrecommit(Precommit, Precommit) 87 | CatchOutOfTurnPropose(Propose) 88 | } 89 | 90 | // A Process is a deterministic finite state automaton that communicates with 91 | // other Processes to implement a Byzantine fault tolerant consensus algorithm. 92 | // It is intended to be used as part of a larger component that implements a 93 | // Byzantine fault tolerant replicated state machine. 94 | // 95 | // All messages from previous and future Heights will be ignored. The component 96 | // using the Process should buffer all messages from future Heights so that they 97 | // are not lost. It is assumed that this component will also handle the 98 | // authentication and rate-limiting of messages. 99 | // 100 | // Processes are not safe for concurrent use. All methods must be called by the 101 | // same goroutine that allocates and starts the Process. 102 | type Process struct { 103 | // whoami represents the identity of this Process. It is assumed that the 104 | // ECDSA private key required to prove ownership of this identity is known. 105 | whoami id.Signatory 106 | // f is the maximum number of malicious adversaries that the Process can 107 | // withstand while still maintaining safety and liveliness. 108 | f uint64 109 | 110 | // Input interface that provide data to the Process. 111 | timer Timer 112 | scheduler Scheduler 113 | proposer Proposer 114 | validator Validator 115 | 116 | // Output interfaces that received data from the Process. 117 | broadcaster Broadcaster 118 | committer Committer 119 | catcher Catcher 120 | 121 | // State of the Process. 122 | State `json:"state"` 123 | } 124 | 125 | // New returns a new Process that is in the default State with empty message 126 | // logs. 127 | func New( 128 | whoami id.Signatory, 129 | f int, 130 | timer Timer, 131 | scheduler Scheduler, 132 | proposer Proposer, 133 | validator Validator, 134 | broadcaster Broadcaster, 135 | committer Committer, 136 | catcher Catcher, 137 | ) Process { 138 | return NewWithCurrentHeight( 139 | whoami, 140 | DefaultHeight, 141 | f, 142 | timer, 143 | scheduler, 144 | proposer, 145 | validator, 146 | broadcaster, 147 | committer, 148 | catcher, 149 | ) 150 | } 151 | 152 | // NewWithCurrentHeight returns a new Process that starts at the given height 153 | // with empty message logs. 154 | func NewWithCurrentHeight( 155 | whoami id.Signatory, 156 | height Height, 157 | f int, 158 | timer Timer, 159 | scheduler Scheduler, 160 | proposer Proposer, 161 | validator Validator, 162 | broadcaster Broadcaster, 163 | committer Committer, 164 | catcher Catcher, 165 | ) Process { 166 | return Process{ 167 | whoami: whoami, 168 | f: uint64(f), 169 | 170 | timer: timer, 171 | scheduler: scheduler, 172 | proposer: proposer, 173 | validator: validator, 174 | 175 | broadcaster: broadcaster, 176 | committer: committer, 177 | catcher: catcher, 178 | 179 | State: DefaultState().WithCurrentHeight(height), 180 | } 181 | } 182 | 183 | // SizeHint returns the number of bytes required to represent this Process in 184 | // binary. 185 | func (p Process) SizeHint() int { 186 | return p.whoami.SizeHint() + 187 | surge.SizeHint(p.f) + 188 | surge.SizeHint(p.State) 189 | } 190 | 191 | // Marshal this Process into binary. 192 | func (p Process) Marshal(buf []byte, rem int) ([]byte, int, error) { 193 | buf, rem, err := p.whoami.Marshal(buf, rem) 194 | if err != nil { 195 | return buf, rem, fmt.Errorf("marshaling whoami: %v", err) 196 | } 197 | buf, rem, err = surge.Marshal(p.f, buf, rem) 198 | if err != nil { 199 | return buf, rem, fmt.Errorf("marshaling f: %v", err) 200 | } 201 | buf, rem, err = surge.Marshal(p.State, buf, rem) 202 | if err != nil { 203 | return buf, rem, fmt.Errorf("marshaling state: %v", err) 204 | } 205 | return buf, rem, nil 206 | } 207 | 208 | // Unmarshal from binary into this Process. 209 | func (p *Process) Unmarshal(buf []byte, rem int) ([]byte, int, error) { 210 | buf, rem, err := p.whoami.Unmarshal(buf, rem) 211 | if err != nil { 212 | return buf, rem, fmt.Errorf("unmarshaling whoami: %v", err) 213 | } 214 | buf, rem, err = surge.Unmarshal(&p.f, buf, rem) 215 | if err != nil { 216 | return buf, rem, fmt.Errorf("unmarshaling f: %v", err) 217 | } 218 | buf, rem, err = surge.Unmarshal(&p.State, buf, rem) 219 | if err != nil { 220 | return buf, rem, fmt.Errorf("unmarshaling state: %v", err) 221 | } 222 | return buf, rem, nil 223 | } 224 | 225 | // Propose is used to notify the Process that a Propose message has been 226 | // received (this includes Propose messages that the Process itself has 227 | // broadcast). All conditions that could be opened by the receipt of a Propose 228 | // message will be tried. 229 | func (p *Process) Propose(propose Propose) { 230 | if !p.insertPropose(propose) { 231 | return 232 | } 233 | 234 | p.trySkipToFutureRound(propose.Round) 235 | p.tryCommitUponSufficientPrecommits(propose.Round) 236 | p.tryPrecommitUponSufficientPrevotes() 237 | p.tryPrevoteUponPropose() 238 | p.tryPrevoteUponSufficientPrevotes() 239 | } 240 | 241 | // Prevote is used to notify the Process that a Prevote message has been 242 | // received (this includes Prevote messages that the Process itself has 243 | // broadcast). All conditions that could be opened by the receipt of a Prevote 244 | // message will be tried. 245 | func (p *Process) Prevote(prevote Prevote) { 246 | if !p.insertPrevote(prevote) { 247 | return 248 | } 249 | 250 | p.trySkipToFutureRound(prevote.Round) 251 | p.tryPrecommitUponSufficientPrevotes() 252 | p.tryPrecommitNilUponSufficientPrevotes() 253 | p.tryPrevoteUponSufficientPrevotes() 254 | p.tryTimeoutPrevoteUponSufficientPrevotes() 255 | } 256 | 257 | // Precommit is used to notify the Process that a Precommit message has been 258 | // received (this includes Precommit messages that the Process itself has 259 | // broadcast). All conditions that could be opened by the receipt of a Precommit 260 | // message will be tried. 261 | func (p *Process) Precommit(precommit Precommit) { 262 | if !p.insertPrecommit(precommit) { 263 | return 264 | } 265 | 266 | p.trySkipToFutureRound(precommit.Round) 267 | p.tryCommitUponSufficientPrecommits(precommit.Round) 268 | p.tryTimeoutPrecommitUponSufficientPrecommits() 269 | } 270 | 271 | // Start the Process. 272 | // 273 | // L10: 274 | // upon start do 275 | // StartRound(0) 276 | // 277 | func (p *Process) Start() { 278 | p.StartRound(0) 279 | } 280 | 281 | func (p *Process) StartWithNewSignatories(f uint64, scheduler Scheduler) { 282 | p.f = f 283 | p.scheduler = scheduler 284 | p.StartRound(0) 285 | } 286 | 287 | // StartRound will progress the Process to a new Round. It does not assume that 288 | // the Height has changed. Since this changes the current Round and the current 289 | // Step, most of the condition methods will be retried at the end (by way of 290 | // defer). 291 | // 292 | // L11: 293 | // Function StartRound(round) 294 | // currentRound ← round 295 | // currentStep ← propose 296 | // if proposer(currentHeight, currentRound) = p then 297 | // if validValue != nil then 298 | // proposal ← validValue 299 | // else 300 | // proposal ← getValue() 301 | // broadcast〈PROPOSAL, currentHeight, currentRound, proposal, validRound〉 302 | // else 303 | // schedule OnTimeoutPropose(currentHeight, currentRound) to be executed after timeoutPropose(currentRound) 304 | func (p *Process) StartRound(round Round) { 305 | defer func() { 306 | p.tryPrecommitUponSufficientPrevotes() 307 | p.tryPrecommitNilUponSufficientPrevotes() 308 | p.tryPrevoteUponPropose() 309 | p.tryPrevoteUponSufficientPrevotes() 310 | p.tryTimeoutPrecommitUponSufficientPrecommits() 311 | p.tryTimeoutPrevoteUponSufficientPrevotes() 312 | }() 313 | 314 | // Set the state the new round, and set the step to the first step in the 315 | // sequence. We do not have special methods dedicated to change the current 316 | // Round, or changing the current Step to Proposing, because StartRound is 317 | // the only location where this logic happens. 318 | p.CurrentRound = round 319 | p.CurrentStep = Proposing 320 | 321 | // If we are not the proposer, then we trigger the propose timeout. 322 | // We proceed only if we have a scheduler impl, because if not, we never 323 | // know who the scheduled proposer is. 324 | if p.scheduler != nil { 325 | proposer := p.scheduler.Schedule(p.CurrentHeight, p.CurrentRound) 326 | if !p.whoami.Equal(&proposer) { 327 | if p.timer != nil { 328 | p.timer.TimeoutPropose(p.CurrentHeight, p.CurrentRound) 329 | } 330 | return 331 | } 332 | 333 | // If we are the proposer, then we emit a propose. 334 | proposeValue := p.ValidValue 335 | if proposeValue.Equal(&NilValue) { 336 | if p.proposer != nil { 337 | proposeValue = p.proposer.Propose(p.CurrentHeight, p.CurrentRound) 338 | } 339 | } 340 | if p.broadcaster != nil { 341 | p.broadcaster.BroadcastPropose(Propose{ 342 | Height: p.CurrentHeight, 343 | Round: p.CurrentRound, 344 | ValidRound: p.ValidRound, 345 | Value: proposeValue, 346 | From: p.whoami, 347 | }) 348 | } 349 | } 350 | } 351 | 352 | // OnTimeoutPropose is used to notify the Process that a timeout has been 353 | // activated. It must only be called after the TimeoutPropose method in the 354 | // Timer has been called. 355 | // 356 | // L57: 357 | // Function OnTimeoutPropose(height, round) 358 | // if height = currentHeight ∧ round = currentRound ∧ currentStep = propose then 359 | // broadcast〈PREVOTE, currentHeight, currentRound, nil 360 | // currentStep ← prevote 361 | func (p *Process) OnTimeoutPropose(height Height, round Round) { 362 | if height == p.CurrentHeight && round == p.CurrentRound && p.CurrentStep == Proposing { 363 | if p.broadcaster != nil { 364 | p.broadcaster.BroadcastPrevote(Prevote{ 365 | Height: p.CurrentHeight, 366 | Round: p.CurrentRound, 367 | Value: NilValue, 368 | From: p.whoami, 369 | }) 370 | } 371 | p.stepToPrevoting() 372 | } 373 | } 374 | 375 | // OnTimeoutPrevote is used to notify the Process that a timeout has been 376 | // activated. It must only be called after the TimeoutPrevote method in the 377 | // Timer has been called. 378 | // 379 | // L61: 380 | // Function OnTimeoutPrevote(height, round) 381 | // if height = currentHeight ∧ round = currentRound ∧ currentStep = prevote then 382 | // broadcast〈PRECOMMIT, currentHeight, currentRound, nil 383 | // currentStep ← precommitting 384 | func (p *Process) OnTimeoutPrevote(height Height, round Round) { 385 | if height == p.CurrentHeight && round == p.CurrentRound && p.CurrentStep == Prevoting { 386 | if p.broadcaster != nil { 387 | p.broadcaster.BroadcastPrecommit(Precommit{ 388 | Height: p.CurrentHeight, 389 | Round: p.CurrentRound, 390 | Value: NilValue, 391 | From: p.whoami, 392 | }) 393 | } 394 | p.stepToPrecommitting() 395 | } 396 | } 397 | 398 | // OnTimeoutPrecommit is used to notify the Process that a timeout has been 399 | // activated. It must only be called after the TimeoutPrecommit method in the 400 | // Timer has been called. 401 | // 402 | // L65: 403 | // Function OnTimeoutPrecommit(height, round) 404 | // if height = currentHeight ∧ round = currentRound then 405 | // StartRound(currentRound + 1) 406 | func (p *Process) OnTimeoutPrecommit(height Height, round Round) { 407 | if height == p.CurrentHeight && round == p.CurrentRound { 408 | p.StartRound(round + 1) 409 | } 410 | } 411 | 412 | // L22: 413 | // upon〈PROPOSAL, currentHeight, currentRound, v, −1〉from proposer(currentHeight, currentRound) 414 | // while currentStep = propose do 415 | // if valid(v) ∧ (lockedRound = −1 ∨ lockedValue = v) then 416 | // broadcast〈PREVOTE, currentHeight, currentRound, id(v) 417 | // else 418 | // broadcast〈PREVOTE, currentHeight, currentRound, nil 419 | // currentStep ← prevote 420 | // 421 | // This method must be tried whenever a Propose is received at the current 422 | // Round, the current Round changes, the current Step changes to Prevote, the 423 | // LockedRound changes, or the the LockedValue changes. 424 | func (p *Process) tryPrevoteUponPropose() { 425 | if p.CurrentStep != Proposing { 426 | return 427 | } 428 | 429 | propose, ok := p.ProposeLogs[p.CurrentRound] 430 | if !ok { 431 | return 432 | } 433 | if propose.ValidRound != InvalidRound { 434 | return 435 | } 436 | proposeIsValid, _ := p.ProposeIsValid[p.CurrentRound] 437 | 438 | if p.broadcaster != nil { 439 | if (p.LockedRound == InvalidRound || p.LockedValue.Equal(&propose.Value)) && proposeIsValid { 440 | p.broadcaster.BroadcastPrevote(Prevote{ 441 | Height: p.CurrentHeight, 442 | Round: p.CurrentRound, 443 | Value: propose.Value, 444 | From: p.whoami, 445 | }) 446 | } else { 447 | p.broadcaster.BroadcastPrevote(Prevote{ 448 | Height: p.CurrentHeight, 449 | Round: p.CurrentRound, 450 | Value: NilValue, 451 | From: p.whoami, 452 | }) 453 | } 454 | } 455 | 456 | p.stepToPrevoting() 457 | } 458 | 459 | // L28: 460 | // 461 | // upon〈PROPOSAL, currentHeight, currentRound, v, vr〉from proposer(currentHeight, currentRound) AND 2f+ 1〈PREVOTE, currentHeight, vr, id(v)〉 462 | // while currentStep = propose ∧ (vr ≥ 0 ∧ vr < currentRound) do 463 | // if valid(v) ∧ (lockedRound ≤ vr ∨ lockedValue = v) then 464 | // broadcast〈PREVOTE, currentHeight, currentRound, id(v)〉 465 | // else 466 | // broadcast〈PREVOTE, currentHeight, currentRound, nil〉 467 | // currentStep ← prevote 468 | // 469 | // This method must be tried whenever a Propose is received at the current Rond, 470 | // a Prevote is received (at any Round), the current Round changes, the 471 | // LockedRound changes, or the the LockedValue changes. 472 | func (p *Process) tryPrevoteUponSufficientPrevotes() { 473 | if p.CurrentStep != Proposing { 474 | return 475 | } 476 | 477 | propose, ok := p.ProposeLogs[p.CurrentRound] 478 | if !ok { 479 | return 480 | } 481 | if propose.ValidRound <= InvalidRound || propose.ValidRound >= p.CurrentRound { 482 | return 483 | } 484 | proposeIsValid, _ := p.ProposeIsValid[p.CurrentRound] 485 | 486 | prevotesInValidRound := 0 487 | for _, prevote := range p.PrevoteLogs[propose.ValidRound] { 488 | if prevote.Value.Equal(&propose.Value) { 489 | prevotesInValidRound++ 490 | } 491 | } 492 | if prevotesInValidRound < int(2*p.f+1) { 493 | return 494 | } 495 | 496 | if p.broadcaster != nil { 497 | if (p.LockedRound <= propose.ValidRound || p.LockedValue.Equal(&propose.Value)) && proposeIsValid { 498 | p.broadcaster.BroadcastPrevote(Prevote{ 499 | Height: p.CurrentHeight, 500 | Round: p.CurrentRound, 501 | Value: propose.Value, 502 | From: p.whoami, 503 | }) 504 | } else { 505 | p.broadcaster.BroadcastPrevote(Prevote{ 506 | Height: p.CurrentHeight, 507 | Round: p.CurrentRound, 508 | Value: NilValue, 509 | From: p.whoami, 510 | }) 511 | } 512 | } 513 | 514 | p.stepToPrevoting() 515 | } 516 | 517 | // L34: 518 | // 519 | // upon 2f+ 1〈PREVOTE, currentHeight, currentRound, ∗〉 520 | // while currentStep = prevote for the first time do 521 | // scheduleOnTimeoutPrevote(currentHeight, currentRound) to be executed after timeoutPrevote(currentRound) 522 | // 523 | // This method must be tried whenever a Prevote is received at the current 524 | // Round, the current Round changes, or the current Step changes to Prevoting. 525 | // It assumes that the Timer will eventually call the OnTimeoutPrevote method. 526 | // This method must only succeed once in any current Round. 527 | func (p *Process) tryTimeoutPrevoteUponSufficientPrevotes() { 528 | if p.checkOnceFlag(p.CurrentRound, OnceFlagTimeoutPrevoteUponSufficientPrevotes) { 529 | return 530 | } 531 | if p.CurrentStep != Prevoting { 532 | return 533 | } 534 | if len(p.PrevoteLogs[p.CurrentRound]) >= int(2*p.f+1) { 535 | if p.timer != nil { 536 | p.timer.TimeoutPrevote(p.CurrentHeight, p.CurrentRound) 537 | p.setOnceFlag(p.CurrentRound, OnceFlagTimeoutPrevoteUponSufficientPrevotes) 538 | } 539 | } 540 | } 541 | 542 | // L36: 543 | // 544 | // upon〈PROPOSAL, currentHeight, currentRound, v, ∗〉from proposer(currentHeight, currentRound) AND 2f+ 1〈PREVOTE, currentHeight, currentRound, id(v)〉 545 | // while valid(v) ∧ currentStep ≥ prevote for the first time do 546 | // if currentStep = prevote then 547 | // lockedValue ← v 548 | // lockedRound ← currentRound 549 | // broadcast〈PRECOMMIT, currentHeight, currentRound, id(v))〉 550 | // currentStep ← precommit 551 | // validValue ← v 552 | // validRound ← currentRound 553 | // 554 | // This method must be tried whenever a Propose is received at the current 555 | // Round, a Prevote is received at the current Round, the current Round changes, 556 | // or the current Step changes to Prevoting or Precommitting. This method must 557 | // only succeed once in any current Round. 558 | func (p *Process) tryPrecommitUponSufficientPrevotes() { 559 | if p.checkOnceFlag(p.CurrentRound, OnceFlagPrecommitUponSufficientPrevotes) { 560 | return 561 | } 562 | if p.CurrentStep < Prevoting { 563 | return 564 | } 565 | 566 | propose, ok := p.ProposeLogs[p.CurrentRound] 567 | if !ok { 568 | return 569 | } 570 | proposeIsValid, _ := p.ProposeIsValid[p.CurrentRound] 571 | if !proposeIsValid { 572 | return 573 | } 574 | prevotesForValue := 0 575 | for _, prevote := range p.PrevoteLogs[p.CurrentRound] { 576 | if prevote.Value.Equal(&propose.Value) { 577 | prevotesForValue++ 578 | } 579 | } 580 | if prevotesForValue < int(2*p.f+1) { 581 | return 582 | } 583 | 584 | if p.CurrentStep == Prevoting { 585 | p.LockedValue = propose.Value 586 | p.LockedRound = p.CurrentRound 587 | if p.broadcaster != nil { 588 | p.broadcaster.BroadcastPrecommit(Precommit{ 589 | Height: p.CurrentHeight, 590 | Round: p.CurrentRound, 591 | Value: propose.Value, 592 | From: p.whoami, 593 | }) 594 | } 595 | 596 | // We defer this call, so that the once flag is set before we exit this 597 | // method. 598 | defer p.stepToPrecommitting() 599 | 600 | // Because the LockedValue and LockedRound have changed, we need to try 601 | // this condition again. We defer this call, so that the once flag is 602 | // set before we exit this method. 603 | defer func() { 604 | p.tryPrevoteUponPropose() 605 | p.tryPrevoteUponSufficientPrevotes() 606 | }() 607 | } 608 | p.ValidValue = propose.Value 609 | p.ValidRound = p.CurrentRound 610 | p.setOnceFlag(p.CurrentRound, OnceFlagPrecommitUponSufficientPrevotes) 611 | } 612 | 613 | // L44: 614 | // 615 | // upon 2f+ 1〈PREVOTE, currentHeight, currentRound, nil〉 616 | // while currentStep = prevote do 617 | // broadcast〈PRECOMMIT, currentHeight, currentRound, nil〉 618 | // currentStep ← precommit 619 | // 620 | // This method must be tried whenever a Prevote is received at the current 621 | // Round, the current Round changes, or the Step changes to Prevoting. 622 | func (p *Process) tryPrecommitNilUponSufficientPrevotes() { 623 | if p.CurrentStep != Prevoting { 624 | return 625 | } 626 | prevotesForNil := 0 627 | for _, prevote := range p.PrevoteLogs[p.CurrentRound] { 628 | if prevote.Value.Equal(&NilValue) { 629 | prevotesForNil++ 630 | } 631 | } 632 | if prevotesForNil >= int(2*p.f+1) { 633 | if p.broadcaster != nil { 634 | p.broadcaster.BroadcastPrecommit(Precommit{ 635 | Height: p.CurrentHeight, 636 | Round: p.CurrentRound, 637 | Value: NilValue, 638 | From: p.whoami, 639 | }) 640 | } 641 | p.stepToPrecommitting() 642 | } 643 | } 644 | 645 | // L47: 646 | // 647 | // upon 2f+ 1〈PRECOMMIT, currentHeight, currentRound, ∗〉for the first time do 648 | // scheduleOnTimeoutPrecommit(currentHeight, currentRound) to be executed after timeoutPrecommit(currentRound) 649 | // 650 | // This method must be tried whenever a Precommit is received at the current 651 | // Round, or the current Round changes. It assumes that the Timer will 652 | // eventually call the OnTimeoutPrecommit method. This method must only succeed 653 | // once in any current Round. 654 | func (p *Process) tryTimeoutPrecommitUponSufficientPrecommits() { 655 | if p.checkOnceFlag(p.CurrentRound, OnceFlagTimeoutPrecommitUponSufficientPrecommits) { 656 | return 657 | } 658 | if len(p.PrecommitLogs[p.CurrentRound]) == int(2*p.f+1) { 659 | if p.timer != nil { 660 | p.timer.TimeoutPrecommit(p.CurrentHeight, p.CurrentRound) 661 | p.setOnceFlag(p.CurrentRound, OnceFlagTimeoutPrecommitUponSufficientPrecommits) 662 | } 663 | } 664 | } 665 | 666 | // L49: 667 | // 668 | // upon〈PROPOSAL, currentHeight, r, v, ∗〉from proposer(currentHeight, r) AND 2f+ 1〈PRECOMMIT, currentHeight, r, id(v)〉 669 | // while decision[currentHeight] = nil do 670 | // if valid(v) then 671 | // decision[currentHeight] = v 672 | // currentHeight ← currentHeight + 1 673 | // reset 674 | // StartRound(0) 675 | // 676 | // This method must be tried whenever a Propose is received, or a Precommit is 677 | // received. Because this method checks whichever Round is relevant (i.e. the 678 | // Round of the Propose/Precommit), it does not need to be tried whenever the 679 | // current Round changes. 680 | // 681 | // We can avoid checking for a nil-decision at the current Height, because the 682 | // only condition under which this would not be true is when the Process has 683 | // progressed passed the Height in question (put another way, the fact that this 684 | // method causes the Height to be incremented prevents it from being triggered 685 | // multiple times). 686 | func (p *Process) tryCommitUponSufficientPrecommits(round Round) { 687 | propose, ok := p.ProposeLogs[round] 688 | if !ok { 689 | return 690 | } 691 | proposeIsValid, _ := p.ProposeIsValid[round] 692 | if !proposeIsValid { 693 | return 694 | } 695 | 696 | precommitsForValue := 0 697 | for _, precommit := range p.PrecommitLogs[round] { 698 | if precommit.Value.Equal(&propose.Value) { 699 | precommitsForValue++ 700 | } 701 | } 702 | if precommitsForValue >= int(2*p.f+1) { 703 | f, scheduler := p.committer.Commit(p.CurrentHeight, propose.Value) 704 | if f != 0 { 705 | p.f = f 706 | } 707 | if scheduler != nil { 708 | p.scheduler = scheduler 709 | } 710 | p.CurrentHeight++ 711 | 712 | // Reset lockedRound, lockedValue, validRound, and validValue to initial 713 | // values. 714 | p.LockedValue = NilValue 715 | p.LockedRound = InvalidRound 716 | p.ValidValue = NilValue 717 | p.ValidRound = InvalidRound 718 | 719 | // Empty message logs in preparation for the new Height. 720 | p.ProposeLogs = map[Round]Propose{} 721 | p.ProposeIsValid = map[Round]bool{} 722 | p.PrevoteLogs = map[Round]map[id.Signatory]Prevote{} 723 | p.PrecommitLogs = map[Round]map[id.Signatory]Precommit{} 724 | p.OnceFlags = map[Round]OnceFlag{} 725 | p.TraceLogs = map[Round]map[id.Signatory]bool{} 726 | 727 | // Start from the first Round in the new Height. 728 | p.StartRound(0) 729 | } 730 | } 731 | 732 | // L55: 733 | // 734 | // upon f+ 1〈∗, currentHeight, r, ∗, ∗〉with r > currentRound do 735 | // StartRound(r) 736 | // 737 | // This method must be tried whenever a Propose is received, a Prevote is 738 | // received, or a Precommit is received. Because this method checks whichever 739 | // Round is relevant (i.e. the Round of the Propose/Prevote/Precommit), and an 740 | // increase in the current Round can only cause this condition to be closed, it 741 | // does not need to be tried whenever the current Round changes. The f+1 742 | // messages (one propose and f prevotes/precommits) must be from f+1 unique 743 | // signatories. 744 | func (p *Process) trySkipToFutureRound(round Round) { 745 | if round <= p.CurrentRound { 746 | return 747 | } 748 | 749 | // count of unique signatories that we have received any message from in the 750 | // given round and at the current height 751 | if len(p.TraceLogs[round]) >= int(p.f+1) { 752 | p.StartRound(round) 753 | } 754 | } 755 | 756 | // insertPropose after validating it and checking for duplicates. If the Propose 757 | // was accepted and inserted, then it return true, otherwise it returns false. 758 | func (p *Process) insertPropose(propose Propose) bool { 759 | if propose.Height != p.CurrentHeight { 760 | return false 761 | } 762 | 763 | if propose.Round <= InvalidRound { 764 | return false 765 | } 766 | 767 | // It is important to check the schedule (here), before checking for 768 | // duplicate proposals (below), because duplicate proposals are only 769 | // relevant if they come from the scheduled proposer. 770 | if p.scheduler != nil { 771 | proposer := p.scheduler.Schedule(propose.Height, propose.Round) 772 | if !proposer.Equal(&propose.From) { 773 | // We have caught a Process attempting to broadcast a propose when it was 774 | // not the scheduled proposer for that height and round. This is caught 775 | // as an out of turn propose 776 | if p.catcher != nil { 777 | p.catcher.CatchOutOfTurnPropose(propose) 778 | } 779 | return false 780 | } 781 | } 782 | 783 | // We have caught a Process attempting to broadcast two different Proposes at 784 | // the same Height and Round. Even though we only explicitly check the Round, 785 | // we know that the Proposes will have the same Height, because we only keep 786 | // message logs for message with the same Height as the current Height of the 787 | // Process. 788 | if existingPropose, ok := p.ProposeLogs[propose.Round]; ok { 789 | if !propose.Equal(&existingPropose) { 790 | if p.catcher != nil { 791 | p.catcher.CatchDoublePropose(propose, existingPropose) 792 | } 793 | } 794 | return false 795 | } 796 | 797 | // We discard a nil value proposal. If a validator implementation is provided 798 | // we check and store the proposal's validity. In the case of an invalid 799 | // proposal, we broadcast a nil prevote, and avoid adding this message to the 800 | // trace logs as it is an invalid proposal. We return true as we have in fact 801 | // inserted the propose message to our propose logs, while explicitly marking 802 | // it as invalid. 803 | if propose.Value == NilValue || (p.validator != nil && !p.validator.Valid(propose.Height, propose.Round, propose.Value)) { 804 | p.ProposeLogs[propose.Round] = propose 805 | p.ProposeIsValid[propose.Round] = false 806 | return true 807 | } 808 | 809 | // If we're here, it means that the proposal is valid. We add the proposer to 810 | // the appropriate round's trace logs 811 | p.ProposeLogs[propose.Round] = propose 812 | p.ProposeIsValid[propose.Round] = true 813 | if _, ok := p.TraceLogs[propose.Round]; !ok { 814 | p.TraceLogs[propose.Round] = map[id.Signatory]bool{} 815 | } 816 | p.TraceLogs[propose.Round][propose.From] = true 817 | 818 | return true 819 | } 820 | 821 | // insertPrevote after validating it and checking for duplicates. If the Prevote 822 | // was accepted and inserted, then it return true, otherwise it returns false. 823 | func (p *Process) insertPrevote(prevote Prevote) bool { 824 | if prevote.Height != p.CurrentHeight { 825 | return false 826 | } 827 | if _, ok := p.PrevoteLogs[prevote.Round]; !ok { 828 | p.PrevoteLogs[prevote.Round] = map[id.Signatory]Prevote{} 829 | } 830 | 831 | existingPrevote, ok := p.PrevoteLogs[prevote.Round][prevote.From] 832 | if ok { 833 | // We have caught a Process attempting to broadcast two different 834 | // Prevotes at the same Height and Round. Even though we only explicitly 835 | // check the Round, we know that the Prevotes will have the same Height, 836 | // because we only keep message logs for message with the same Height as 837 | // the current Height of the Process. 838 | if !prevote.Equal(&existingPrevote) { 839 | if p.catcher != nil { 840 | p.catcher.CatchDoublePrevote(prevote, existingPrevote) 841 | } 842 | } 843 | return false 844 | } 845 | 846 | p.PrevoteLogs[prevote.Round][prevote.From] = prevote 847 | 848 | // add the prevoter to the appropriate round's trace logs 849 | if _, ok := p.TraceLogs[prevote.Round]; !ok { 850 | p.TraceLogs[prevote.Round] = map[id.Signatory]bool{} 851 | } 852 | p.TraceLogs[prevote.Round][prevote.From] = true 853 | 854 | return true 855 | } 856 | 857 | // insertPrecommit after validating it and checking for duplicates. If the 858 | // Precommit was accepted and inserted, then it return true, otherwise it 859 | // returns false. 860 | func (p *Process) insertPrecommit(precommit Precommit) bool { 861 | if precommit.Height != p.CurrentHeight { 862 | return false 863 | } 864 | if _, ok := p.PrecommitLogs[precommit.Round]; !ok { 865 | p.PrecommitLogs[precommit.Round] = map[id.Signatory]Precommit{} 866 | } 867 | 868 | existingPrecommit, ok := p.PrecommitLogs[precommit.Round][precommit.From] 869 | if ok { 870 | // We have caught a Process attempting to broadcast two different 871 | // Precommits at the same Height and Round. Even though we only 872 | // explicitly check the Round, we know that the Precommits will have the 873 | // same Height, because we only keep message logs for message with the 874 | // same Height as the current Height of the Process. 875 | if !precommit.Equal(&existingPrecommit) { 876 | if p.catcher != nil { 877 | p.catcher.CatchDoublePrecommit(precommit, existingPrecommit) 878 | } 879 | } 880 | return false 881 | } 882 | 883 | p.PrecommitLogs[precommit.Round][precommit.From] = precommit 884 | 885 | // add the precommitter to the appropriate round's trace logs 886 | if _, ok := p.TraceLogs[precommit.Round]; !ok { 887 | p.TraceLogs[precommit.Round] = map[id.Signatory]bool{} 888 | } 889 | p.TraceLogs[precommit.Round][precommit.From] = true 890 | 891 | return true 892 | } 893 | 894 | // stepToPrevoting puts the Process into the Prevoting Step. This will also try 895 | // other methods that might now have passing conditions. 896 | func (p *Process) stepToPrevoting() { 897 | p.CurrentStep = Prevoting 898 | 899 | // Because the current Step of the Process has changed, new conditions might 900 | // be open, so we try the relevant ones. Once flags protect us against 901 | // double-tries where necessary. 902 | p.tryPrecommitUponSufficientPrevotes() 903 | p.tryPrecommitNilUponSufficientPrevotes() 904 | p.tryTimeoutPrevoteUponSufficientPrevotes() 905 | } 906 | 907 | // stepToPrecommitting puts the Process into the Precommitting Step. This will 908 | // also try other methods that might now have passing conditions. 909 | func (p *Process) stepToPrecommitting() { 910 | p.CurrentStep = Precommitting 911 | 912 | // Because the current Step of the Process has changed, new conditions might 913 | // be open, so we try the relevant ones. Once flags protect us against 914 | // double-tries where necessary. 915 | p.tryPrecommitUponSufficientPrevotes() 916 | } 917 | 918 | // checkOnceFlag returns true if the OnceFlag has already been set for the given 919 | // Round. Otherwise, it returns false. 920 | func (p *Process) checkOnceFlag(round Round, flag OnceFlag) bool { 921 | return p.OnceFlags[round]&flag == flag 922 | } 923 | 924 | // setOnceFlag set the OnceFlag for the given Round. 925 | func (p *Process) setOnceFlag(round Round, flag OnceFlag) { 926 | p.OnceFlags[round] |= flag 927 | } 928 | 929 | // A OnceFlag is used to guarantee that events only happen once in any given 930 | // Round. 931 | type OnceFlag uint16 932 | 933 | // Enumerate all OnceFlag values. 934 | const ( 935 | OnceFlagTimeoutPrecommitUponSufficientPrecommits = OnceFlag(1) 936 | OnceFlagTimeoutPrevoteUponSufficientPrevotes = OnceFlag(2) 937 | OnceFlagPrecommitUponSufficientPrevotes = OnceFlag(4) 938 | ) 939 | -------------------------------------------------------------------------------- /process/process_suite_test.go: -------------------------------------------------------------------------------- 1 | package process_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestProc(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Process Suite") 13 | } 14 | -------------------------------------------------------------------------------- /process/processutil/processutil.go: -------------------------------------------------------------------------------- 1 | package processutil 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/renproject/hyperdrive/process" 7 | "github.com/renproject/id" 8 | ) 9 | 10 | // BroadcasterCallbacks provide callback functions to test the broadcaster 11 | // behaviour required by a Process 12 | type BroadcasterCallbacks struct { 13 | BroadcastProposeCallback func(process.Propose) 14 | BroadcastPrevoteCallback func(process.Prevote) 15 | BroadcastPrecommitCallback func(process.Precommit) 16 | } 17 | 18 | // BroadcastPropose passes the propose message to the propose callback, if present 19 | func (broadcaster BroadcasterCallbacks) BroadcastPropose(propose process.Propose) { 20 | if broadcaster.BroadcastProposeCallback == nil { 21 | return 22 | } 23 | broadcaster.BroadcastProposeCallback(propose) 24 | } 25 | 26 | // BroadcastPrevote passes the prevote message to the prevote callback, if present 27 | func (broadcaster BroadcasterCallbacks) BroadcastPrevote(prevote process.Prevote) { 28 | if broadcaster.BroadcastPrevoteCallback == nil { 29 | return 30 | } 31 | broadcaster.BroadcastPrevoteCallback(prevote) 32 | } 33 | 34 | // BroadcastPrecommit passes the precommit message to the precommit callback, if present 35 | func (broadcaster BroadcasterCallbacks) BroadcastPrecommit(precommit process.Precommit) { 36 | if broadcaster.BroadcastPrecommitCallback == nil { 37 | return 38 | } 39 | broadcaster.BroadcastPrecommitCallback(precommit) 40 | } 41 | 42 | // CommitterCallback provides a callback function to test the Committer 43 | // behaviour required by a Process 44 | type CommitterCallback struct { 45 | Callback func(process.Height, process.Value) (uint64, process.Scheduler) 46 | } 47 | 48 | // Commit passes the commitment parameters height and round to the commit callback, if present 49 | func (committer CommitterCallback) Commit(height process.Height, value process.Value) (uint64, process.Scheduler) { 50 | if committer.Callback == nil { 51 | return 0, nil 52 | } 53 | return committer.Callback(height, value) 54 | } 55 | 56 | // MockProposer is a mock implementation of the Proposer interface 57 | // It always proposes the value MockValue 58 | type MockProposer struct { 59 | MockValue func() process.Value 60 | } 61 | 62 | // Propose implements the propose behaviour as required by the Proposer interface 63 | // The MockProposer's propose method does not take into consideration the 64 | // consensus parameters height and round, but simply returns the value MockValue 65 | func (p MockProposer) Propose(height process.Height, round process.Round) process.Value { 66 | return p.MockValue() 67 | } 68 | 69 | // MockValidator is a mock implementation of the Validator interface 70 | // It always returns the MockValid value as its validation check 71 | type MockValidator struct { 72 | MockValid func(height process.Height, round process.Round, value process.Value) bool 73 | } 74 | 75 | // Valid implements the validation behaviour as required by the Validator interface 76 | // The MockValidator's valid method does not take into consideration the 77 | // received propose message, but simply returns the MockValid value as its 78 | // validation check 79 | func (v MockValidator) Valid(height process.Height, round process.Round, value process.Value) bool { 80 | return v.MockValid(height, round, value) 81 | } 82 | 83 | // CatcherCallbacks provide callback functions to test the Catcher interface 84 | // required by a Process 85 | type CatcherCallbacks struct { 86 | CatchDoubleProposeCallback func(process.Propose, process.Propose) 87 | CatchDoublePrevoteCallback func(process.Prevote, process.Prevote) 88 | CatchDoublePrecommitCallback func(process.Precommit, process.Precommit) 89 | CatchOutOfTurnProposeCallback func(process.Propose) 90 | } 91 | 92 | // CatchDoublePropose implements the interface method of handling the event when 93 | // two different propose messages were received from the same process. 94 | // In this case, it simply passes those to the appropriate callback function 95 | func (catcher CatcherCallbacks) CatchDoublePropose(propose1 process.Propose, propose2 process.Propose) { 96 | if catcher.CatchDoubleProposeCallback == nil { 97 | return 98 | } 99 | catcher.CatchDoubleProposeCallback(propose1, propose2) 100 | } 101 | 102 | // CatchDoublePrevote implements the interface method of handling the event when 103 | // two different prevote messages were received from the same process. 104 | // In this case, it simply passes those to the appropriate callback function 105 | func (catcher CatcherCallbacks) CatchDoublePrevote(prevote1 process.Prevote, prevote2 process.Prevote) { 106 | if catcher.CatchDoublePrevoteCallback == nil { 107 | return 108 | } 109 | catcher.CatchDoublePrevoteCallback(prevote1, prevote2) 110 | } 111 | 112 | // CatchDoublePrecommit implements the interface method of handling the event when 113 | // two different precommit messages were received from the same process. 114 | // In this case, it simply passes those to the appropriate callback function 115 | func (catcher CatcherCallbacks) CatchDoublePrecommit(precommit1 process.Precommit, precommit2 process.Precommit) { 116 | if catcher.CatchDoublePrecommitCallback == nil { 117 | return 118 | } 119 | catcher.CatchDoublePrecommitCallback(precommit1, precommit2) 120 | } 121 | 122 | // CatchOutOfTurnPropose implements the interface method of handling the event when 123 | // a process not scheduled to propose for the current height/round broadcasts a propose. 124 | // In this case, it simply passes those to the appropriate callback function 125 | func (catcher CatcherCallbacks) CatchOutOfTurnPropose(propose process.Propose) { 126 | if catcher.CatchOutOfTurnProposeCallback == nil { 127 | return 128 | } 129 | catcher.CatchOutOfTurnProposeCallback(propose) 130 | } 131 | 132 | // RandomHeight consumes a source of randomness and returns a random height 133 | // for the consensus mechanism. It returns a truly random height 70% of the times, 134 | // whereas for the other 30% of the times it returns heights for edge scenarios 135 | func RandomHeight(r *rand.Rand) process.Height { 136 | switch r.Int() % 10 { 137 | case 0: 138 | return process.Height(-1) 139 | case 1: 140 | return process.Height(0) 141 | case 2: 142 | return process.Height(9223372036854775807) 143 | default: 144 | return process.Height(r.Int63()) 145 | } 146 | } 147 | 148 | // RandomRound consumes a source of randomness and returns a random round 149 | // for the consensus mechanism. It returns a truly random round 70% of the times, 150 | // whereas for the other 30% of the times it returns rounds for edge scenarios 151 | func RandomRound(r *rand.Rand) process.Round { 152 | switch r.Int() % 10 { 153 | case 0: 154 | return process.Round(-1) 155 | case 1: 156 | return process.Round(0) 157 | case 2: 158 | return process.Round(9223372036854775807) 159 | default: 160 | return process.Round(r.Int63()) 161 | } 162 | } 163 | 164 | // RandomStep consumes a source of randomness and returns a random step 165 | // for the consensus mechanism. A random step could be a valid or invalid step 166 | func RandomStep(r *rand.Rand) process.Step { 167 | switch r.Int() % 10 { 168 | case 0: 169 | return process.Proposing 170 | case 1: 171 | return process.Prevoting 172 | case 2: 173 | return process.Prevoting 174 | case 3: 175 | return process.Step(255) 176 | default: 177 | return process.Step(uint8(r.Int63())) 178 | } 179 | } 180 | 181 | // RandomGoodValue consumes a source of randomness and returns a truly random 182 | // value which is proposed by the proposer, on which consensus can be reached 183 | func RandomGoodValue(r *rand.Rand) process.Value { 184 | v := process.Value{} 185 | for i := range v { 186 | v[i] = byte(r.Int()) 187 | } 188 | return v 189 | } 190 | 191 | // RandomValue consumes a source of randomness and returns a random value 192 | // for the consensus mechanism. It returns a truly random round 80% of the times, 193 | // whereas for the other 20% of the times it returns rounds for edge scenarios 194 | func RandomValue(r *rand.Rand) process.Value { 195 | switch r.Int() % 10 { 196 | case 0: 197 | return process.Value{} 198 | case 1: 199 | return process.Value{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} 200 | default: 201 | return RandomGoodValue(r) 202 | } 203 | } 204 | 205 | // RandomState consumes a source of randomness and returns a random state of 206 | // a process in the consensus mechanism. 207 | func RandomState(r *rand.Rand) process.State { 208 | switch r.Int() % 10 { 209 | case 0: 210 | return process.DefaultState() 211 | default: 212 | return process.State{ 213 | CurrentHeight: RandomHeight(r), 214 | CurrentRound: RandomRound(r), 215 | CurrentStep: RandomStep(r), 216 | LockedRound: RandomRound(r), 217 | LockedValue: RandomValue(r), 218 | ValidRound: RandomRound(r), 219 | ValidValue: RandomValue(r), 220 | 221 | ProposeLogs: make(map[process.Round]process.Propose), 222 | PrevoteLogs: make(map[process.Round]map[id.Signatory]process.Prevote), 223 | PrecommitLogs: make(map[process.Round]map[id.Signatory]process.Precommit), 224 | OnceFlags: make(map[process.Round]process.OnceFlag), 225 | } 226 | } 227 | } 228 | 229 | // RandomPropose consumes a source of randomness and returns a random propose 230 | // message. The message is a valid message 70% of the times, and other times 231 | // this function returns some edge scenarios, including empty message 232 | func RandomPropose(r *rand.Rand) process.Propose { 233 | switch r.Int() % 10 { 234 | case 0: 235 | return process.Propose{} 236 | case 1: 237 | return process.Propose{ 238 | Height: RandomHeight(r), 239 | Round: RandomRound(r), 240 | ValidRound: RandomRound(r), 241 | Value: RandomValue(r), 242 | From: id.Signatory{}, 243 | } 244 | case 2: 245 | signatory := id.Signatory{} 246 | for i := range signatory { 247 | signatory[i] = byte(r.Int()) 248 | } 249 | signature := id.Signature{} 250 | for i := range signature { 251 | signature[i] = byte(r.Int()) 252 | } 253 | return process.Propose{ 254 | Height: RandomHeight(r), 255 | Round: RandomRound(r), 256 | ValidRound: RandomRound(r), 257 | Value: RandomValue(r), 258 | From: signatory, 259 | } 260 | default: 261 | msg := process.Propose{ 262 | Height: RandomHeight(r), 263 | Round: RandomRound(r), 264 | ValidRound: RandomRound(r), 265 | Value: RandomValue(r), 266 | } 267 | privKey := id.NewPrivKey() 268 | msg.From = privKey.Signatory() 269 | return msg 270 | } 271 | } 272 | 273 | // RandomPrevote consumes a source of randomness and returns a random prevote 274 | // message. The message is a valid message 70% of the times, and other times 275 | // this function returns some edge scenarios, including empty message 276 | func RandomPrevote(r *rand.Rand) process.Prevote { 277 | switch r.Int() % 10 { 278 | case 0: 279 | return process.Prevote{} 280 | case 1: 281 | return process.Prevote{ 282 | Height: RandomHeight(r), 283 | Round: RandomRound(r), 284 | Value: RandomValue(r), 285 | From: id.Signatory{}, 286 | } 287 | case 2: 288 | signatory := id.Signatory{} 289 | for i := range signatory { 290 | signatory[i] = byte(r.Int()) 291 | } 292 | signature := id.Signature{} 293 | for i := range signature { 294 | signature[i] = byte(r.Int()) 295 | } 296 | return process.Prevote{ 297 | Height: RandomHeight(r), 298 | Round: RandomRound(r), 299 | Value: RandomValue(r), 300 | From: signatory, 301 | } 302 | default: 303 | msg := process.Prevote{ 304 | Height: RandomHeight(r), 305 | Round: RandomRound(r), 306 | Value: RandomValue(r), 307 | } 308 | privKey := id.NewPrivKey() 309 | msg.From = privKey.Signatory() 310 | return msg 311 | } 312 | } 313 | 314 | // RandomPrecommit consumes a source of randomness and returns a random precommit 315 | // message. The message is a valid message 70% of the times, and other times 316 | // this function returns some edge scenarios, including empty message 317 | func RandomPrecommit(r *rand.Rand) process.Precommit { 318 | switch r.Int() % 10 { 319 | case 0: 320 | return process.Precommit{} 321 | case 1: 322 | return process.Precommit{ 323 | Height: RandomHeight(r), 324 | Round: RandomRound(r), 325 | Value: RandomValue(r), 326 | From: id.Signatory{}, 327 | } 328 | case 2: 329 | signatory := id.Signatory{} 330 | for i := range signatory { 331 | signatory[i] = byte(r.Int()) 332 | } 333 | signature := id.Signature{} 334 | for i := range signature { 335 | signature[i] = byte(r.Int()) 336 | } 337 | return process.Precommit{ 338 | Height: RandomHeight(r), 339 | Round: RandomRound(r), 340 | Value: RandomValue(r), 341 | From: signatory, 342 | } 343 | default: 344 | msg := process.Precommit{ 345 | Height: RandomHeight(r), 346 | Round: RandomRound(r), 347 | Value: RandomValue(r), 348 | } 349 | privKey := id.NewPrivKey() 350 | msg.From = privKey.Signatory() 351 | return msg 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /process/processutil/processutil_suite_test.go: -------------------------------------------------------------------------------- 1 | package processutil_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestProcessutil(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Process Utilities Suite") 13 | } 14 | -------------------------------------------------------------------------------- /process/processutil/processutil_test.go: -------------------------------------------------------------------------------- 1 | package processutil_test 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/renproject/hyperdrive/process" 8 | "github.com/renproject/hyperdrive/process/processutil" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("Process utilities", func() { 15 | Context("when generating random proposes", func() { 16 | It("should not generate the same propose multiple times", func() { 17 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 18 | proposes := make([]process.Propose, 100) 19 | for i := range proposes { 20 | proposes[i] = processutil.RandomPropose(r) 21 | } 22 | all := true 23 | for i := range proposes { 24 | if !proposes[0].Equal(&proposes[i]) { 25 | all = false 26 | break 27 | } 28 | } 29 | Expect(all).To(BeFalse()) 30 | }) 31 | }) 32 | 33 | Context("when generating random prevotes", func() { 34 | It("should not generate the same prevote multiple times", func() { 35 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 36 | prevotes := make([]process.Prevote, 100) 37 | for i := range prevotes { 38 | prevotes[i] = processutil.RandomPrevote(r) 39 | } 40 | all := true 41 | for i := range prevotes { 42 | if !prevotes[0].Equal(&prevotes[i]) { 43 | all = false 44 | break 45 | } 46 | } 47 | Expect(all).To(BeFalse()) 48 | }) 49 | }) 50 | 51 | Context("when generating random precommits", func() { 52 | It("should not generate the same precommit multiple times", func() { 53 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 54 | precommits := make([]process.Precommit, 100) 55 | for i := range precommits { 56 | precommits[i] = processutil.RandomPrecommit(r) 57 | } 58 | all := true 59 | for i := range precommits { 60 | if !precommits[0].Equal(&precommits[i]) { 61 | all = false 62 | break 63 | } 64 | } 65 | Expect(all).To(BeFalse()) 66 | }) 67 | }) 68 | 69 | Context("when generating random states", func() { 70 | It("should not generate the same state multiple times", func() { 71 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 72 | states := make([]process.State, 100) 73 | for i := range states { 74 | states[i] = processutil.RandomState(r) 75 | } 76 | all := true 77 | for i := range states { 78 | if !states[0].Equal(&states[i]) { 79 | all = false 80 | break 81 | } 82 | } 83 | Expect(all).To(BeFalse()) 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /process/state.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/renproject/id" 8 | "github.com/renproject/surge" 9 | ) 10 | 11 | var ( 12 | // DefaulHeight is set to 1, because the genesis block is assumed to exist 13 | // at Height 0. 14 | DefaultHeight = Height(1) 15 | DefaultRound = Round(0) 16 | ) 17 | 18 | // The State of a Process. It should be saved after every method call on the 19 | // Process, but should not be saved during method calls (interacting with the 20 | // State concurrently is unsafe). It is worth noting that the State does not 21 | // contain a decision array, because it delegates this responsibility to the 22 | // Committer interface. 23 | // 24 | // L1: 25 | // 26 | // Initialization: 27 | // currentHeight := 0 /* current height, or consensus instance we are currently executing */ 28 | // currentRound := 0 /* current round number */ 29 | // currentStep ∈ {propose, prevote, precommit} 30 | // decision[] := nil 31 | // lockedValue := nil 32 | // lockedRound := −1 33 | // validValue := nil 34 | // validRound := −1 35 | type State struct { 36 | CurrentHeight Height `json:"currentHeight"` 37 | CurrentRound Round `json:"currentRound"` 38 | CurrentStep Step `json:"currentStep"` 39 | LockedValue Value `json:"lockedValue"` // The most recent value for which a precommit message has been sent. 40 | LockedRound Round `json:"lockedRound"` // The last round in which the process sent a precommit message that is not nil. 41 | ValidValue Value `json:"validValue"` // The most recent possible decision value. 42 | ValidRound Round `json:"validRound"` // The last round in which valid value is updated. 43 | 44 | // ProposeLogs store the Proposes for all Rounds. 45 | ProposeLogs map[Round]Propose `json:"proposeLogs"` 46 | // ProposeIsValid is a map that stores whether the received proposal for the 47 | // consensus round is valid or not 48 | ProposeIsValid map[Round]bool 49 | // PrevoteLogs store the Prevotes for all Processes in all Rounds. 50 | PrevoteLogs map[Round]map[id.Signatory]Prevote `json:"prevoteLogs"` 51 | // PrecommitLogs store the Precommits for all Processes in all Rounds. 52 | PrecommitLogs map[Round]map[id.Signatory]Precommit `json:"precommitLogs"` 53 | // OnceFlags prevents events from happening more than once. 54 | OnceFlags map[Round]OnceFlag `json:"onceFlags"` 55 | // TraceLogs store the unique signatories from which we have received a msg 56 | // (propose/prevote/precommit) in a specific round for the current height 57 | TraceLogs map[Round]map[id.Signatory]bool 58 | } 59 | 60 | // DefaultState returns a State with all fields set to their default values. 61 | func DefaultState() State { 62 | return State{ 63 | CurrentHeight: DefaultHeight, 64 | CurrentRound: DefaultRound, 65 | CurrentStep: Proposing, 66 | LockedValue: NilValue, 67 | LockedRound: InvalidRound, 68 | ValidValue: NilValue, 69 | ValidRound: InvalidRound, 70 | 71 | ProposeLogs: make(map[Round]Propose), 72 | ProposeIsValid: make(map[Round]bool), 73 | PrevoteLogs: make(map[Round]map[id.Signatory]Prevote), 74 | PrecommitLogs: make(map[Round]map[id.Signatory]Precommit), 75 | TraceLogs: make(map[Round]map[id.Signatory]bool), 76 | OnceFlags: make(map[Round]OnceFlag), 77 | } 78 | } 79 | 80 | // WithCurrentHeight returns a process state having modified its current height 81 | // with the given height 82 | func (state State) WithCurrentHeight(height Height) State { 83 | state.CurrentHeight = height 84 | return state 85 | } 86 | 87 | // Clone the State into another copy that can be modified without affecting the 88 | // original. 89 | func (state State) Clone() State { 90 | cloned := State{ 91 | CurrentHeight: state.CurrentHeight, 92 | CurrentRound: state.CurrentRound, 93 | CurrentStep: state.CurrentStep, 94 | LockedValue: state.LockedValue, 95 | LockedRound: state.LockedRound, 96 | ValidValue: state.ValidValue, 97 | ValidRound: state.ValidRound, 98 | 99 | ProposeLogs: make(map[Round]Propose), 100 | ProposeIsValid: make(map[Round]bool), 101 | PrevoteLogs: make(map[Round]map[id.Signatory]Prevote), 102 | PrecommitLogs: make(map[Round]map[id.Signatory]Precommit), 103 | TraceLogs: make(map[Round]map[id.Signatory]bool), 104 | OnceFlags: make(map[Round]OnceFlag), 105 | } 106 | for round, propose := range state.ProposeLogs { 107 | cloned.ProposeLogs[round] = propose 108 | } 109 | for round, proposeIsValid := range state.ProposeIsValid { 110 | cloned.ProposeIsValid[round] = proposeIsValid 111 | } 112 | for round, prevotes := range state.PrevoteLogs { 113 | cloned.PrevoteLogs[round] = make(map[id.Signatory]Prevote) 114 | for signatory, prevote := range prevotes { 115 | cloned.PrevoteLogs[round][signatory] = prevote 116 | } 117 | } 118 | for round, precommits := range state.PrecommitLogs { 119 | cloned.PrecommitLogs[round] = make(map[id.Signatory]Precommit) 120 | for signatory, precommit := range precommits { 121 | cloned.PrecommitLogs[round][signatory] = precommit 122 | } 123 | } 124 | for round, onceFlag := range state.OnceFlags { 125 | cloned.OnceFlags[round] = onceFlag 126 | } 127 | for round, traces := range state.TraceLogs { 128 | cloned.TraceLogs[round] = make(map[id.Signatory]bool) 129 | for signatory, trace := range traces { 130 | cloned.TraceLogs[round][signatory] = trace 131 | } 132 | } 133 | return cloned 134 | } 135 | 136 | // Equal compares two States. If they are equal, then it returns true, otherwise 137 | // it returns false. Message logs and once-flags are ignored for the purpose of 138 | // equality. 139 | func (state State) Equal(other *State) bool { 140 | return state.CurrentHeight == other.CurrentHeight && 141 | state.CurrentRound == other.CurrentRound && 142 | state.CurrentStep == other.CurrentStep && 143 | state.LockedValue.Equal(&other.LockedValue) && 144 | state.LockedRound == other.LockedRound && 145 | state.ValidValue.Equal(&other.ValidValue) && 146 | state.ValidRound == other.ValidRound 147 | } 148 | 149 | // SizeHint implements the Surge SizeHinter interface, and returns the byte size 150 | // of the state instance 151 | func (state State) SizeHint() int { 152 | return surge.SizeHint(state.CurrentHeight) + 153 | surge.SizeHint(state.CurrentRound) + 154 | surge.SizeHint(state.CurrentStep) + 155 | surge.SizeHint(state.LockedValue) + 156 | surge.SizeHint(state.LockedRound) + 157 | surge.SizeHint(state.ValidValue) + 158 | surge.SizeHint(state.ValidRound) + 159 | surge.SizeHint(state.ProposeLogs) + 160 | surge.SizeHint(state.ProposeIsValid) + 161 | surge.SizeHint(state.PrevoteLogs) + 162 | surge.SizeHint(state.PrecommitLogs) + 163 | surge.SizeHint(state.OnceFlags) + 164 | surge.SizeHint(state.TraceLogs) 165 | } 166 | 167 | // Marshal implements the Surge Marshaler interface 168 | func (state State) Marshal(buf []byte, rem int) ([]byte, int, error) { 169 | buf, rem, err := surge.Marshal(state.CurrentHeight, buf, rem) 170 | if err != nil { 171 | return buf, rem, fmt.Errorf("marshaling current height=%v: %v", state.CurrentHeight, err) 172 | } 173 | buf, rem, err = surge.Marshal(state.CurrentRound, buf, rem) 174 | if err != nil { 175 | return buf, rem, fmt.Errorf("marshaling current round=%v: %v", state.CurrentRound, err) 176 | } 177 | buf, rem, err = surge.Marshal(state.CurrentStep, buf, rem) 178 | if err != nil { 179 | return buf, rem, fmt.Errorf("marshaling current step=%v: %v", state.CurrentStep, err) 180 | } 181 | buf, rem, err = surge.Marshal(state.LockedValue, buf, rem) 182 | if err != nil { 183 | return buf, rem, fmt.Errorf("marshaling locked value=%v: %v", state.LockedValue, err) 184 | } 185 | buf, rem, err = surge.Marshal(state.LockedRound, buf, rem) 186 | if err != nil { 187 | return buf, rem, fmt.Errorf("marshaling locked round=%v: %v", state.LockedRound, err) 188 | } 189 | buf, rem, err = surge.Marshal(state.ValidValue, buf, rem) 190 | if err != nil { 191 | return buf, rem, fmt.Errorf("marshaling valid value=%v: %v", state.ValidValue, err) 192 | } 193 | buf, rem, err = surge.Marshal(state.ValidRound, buf, rem) 194 | if err != nil { 195 | return buf, rem, fmt.Errorf("marshaling valid round=%v: %v", state.ValidRound, err) 196 | } 197 | buf, rem, err = surge.Marshal(state.ProposeLogs, buf, rem) 198 | if err != nil { 199 | return buf, rem, fmt.Errorf("marshaling %v propose logs: %v", len(state.ProposeLogs), err) 200 | } 201 | buf, rem, err = surge.Marshal(state.ProposeIsValid, buf, rem) 202 | if err != nil { 203 | return buf, rem, fmt.Errorf("marshaling %v propose is valid: %v", len(state.ProposeIsValid), err) 204 | } 205 | buf, rem, err = surge.Marshal(state.PrevoteLogs, buf, rem) 206 | if err != nil { 207 | return buf, rem, fmt.Errorf("marshaling %v prevote logs: %v", len(state.PrevoteLogs), err) 208 | } 209 | buf, rem, err = surge.Marshal(state.PrecommitLogs, buf, rem) 210 | if err != nil { 211 | return buf, rem, fmt.Errorf("marshaling %v precommit logs: %v", len(state.PrecommitLogs), err) 212 | } 213 | buf, rem, err = surge.Marshal(state.OnceFlags, buf, rem) 214 | if err != nil { 215 | return buf, rem, fmt.Errorf("marshaling %v once flags: %v", len(state.OnceFlags), err) 216 | } 217 | buf, rem, err = surge.Marshal(state.TraceLogs, buf, rem) 218 | if err != nil { 219 | return buf, rem, fmt.Errorf("marshaling %v trace logs: %v", len(state.TraceLogs), err) 220 | } 221 | return buf, rem, nil 222 | } 223 | 224 | // Unmarshal implements the Surge Unmarshaler interface 225 | func (state *State) Unmarshal(buf []byte, rem int) ([]byte, int, error) { 226 | buf, rem, err := surge.Unmarshal(&state.CurrentHeight, buf, rem) 227 | if err != nil { 228 | return buf, rem, fmt.Errorf("unmarshaling current height: %v", err) 229 | } 230 | buf, rem, err = surge.Unmarshal(&state.CurrentRound, buf, rem) 231 | if err != nil { 232 | return buf, rem, fmt.Errorf("unmarshaling current round: %v", err) 233 | } 234 | buf, rem, err = surge.Unmarshal(&state.CurrentStep, buf, rem) 235 | if err != nil { 236 | return buf, rem, fmt.Errorf("unmarshaling current step: %v", err) 237 | } 238 | buf, rem, err = surge.Unmarshal(&state.LockedValue, buf, rem) 239 | if err != nil { 240 | return buf, rem, fmt.Errorf("unmarshaling locked value: %v", err) 241 | } 242 | buf, rem, err = surge.Unmarshal(&state.LockedRound, buf, rem) 243 | if err != nil { 244 | return buf, rem, fmt.Errorf("unmarshaling locked round: %v", err) 245 | } 246 | buf, rem, err = surge.Unmarshal(&state.ValidValue, buf, rem) 247 | if err != nil { 248 | return buf, rem, fmt.Errorf("unmarshaling valid value: %v", err) 249 | } 250 | buf, rem, err = surge.Unmarshal(&state.ValidRound, buf, rem) 251 | if err != nil { 252 | return buf, rem, fmt.Errorf("unmarshaling valid round: %v", err) 253 | } 254 | buf, rem, err = surge.Unmarshal(&state.ProposeLogs, buf, rem) 255 | if err != nil { 256 | return buf, rem, fmt.Errorf("unmarshaling propose logs: %v", err) 257 | } 258 | buf, rem, err = surge.Unmarshal(&state.ProposeIsValid, buf, rem) 259 | if err != nil { 260 | return buf, rem, fmt.Errorf("unmarshaling propose is valid: %v", err) 261 | } 262 | buf, rem, err = surge.Unmarshal(&state.PrevoteLogs, buf, rem) 263 | if err != nil { 264 | return buf, rem, fmt.Errorf("unmarshaling prevote logs: %v", err) 265 | } 266 | buf, rem, err = surge.Unmarshal(&state.PrecommitLogs, buf, rem) 267 | if err != nil { 268 | return buf, rem, fmt.Errorf("unmarshaling precommit logs: %v", err) 269 | } 270 | buf, rem, err = surge.Unmarshal(&state.OnceFlags, buf, rem) 271 | if err != nil { 272 | return buf, rem, fmt.Errorf("unmarshaling once flags: %v", err) 273 | } 274 | buf, rem, err = surge.Unmarshal(&state.TraceLogs, buf, rem) 275 | if err != nil { 276 | return buf, rem, fmt.Errorf("unmarshaling trace logs: %v", err) 277 | } 278 | return buf, rem, nil 279 | } 280 | 281 | // Step defines a typedef for uint8 values that represent the step of the state 282 | // of a Process partaking in the consensus algorithm. 283 | type Step uint8 284 | 285 | // Enumerate step values. 286 | const ( 287 | Proposing = Step(0) 288 | Prevoting = Step(1) 289 | Precommitting = Step(2) 290 | ) 291 | 292 | // Height defines a typedef for int64 values that represent the height of a 293 | // Value at which the consensus algorithm is attempting to reach consensus. 294 | type Height int64 295 | 296 | // Round defines a typedef for int64 values that represent the round of a Value 297 | // at which the consensus algorithm is attempting to reach consensus. 298 | type Round int64 299 | 300 | const ( 301 | // InvalidRound is a reserved int64 that represents an invalid Round. It is 302 | // used when a Process is trying to represent that it does have have a 303 | // LockedRound or ValidRound. 304 | InvalidRound = Round(-1) 305 | ) 306 | 307 | // Value defines a typedef for hashes that represent the hashes of proposed 308 | // values in the consensus algorithm. In the context of a blockchain, a Value 309 | // would be a block. 310 | type Value id.Hash 311 | 312 | // Equal compares two Values. If they are equal, then it returns true, otherwise 313 | // it returns false. 314 | func (v *Value) Equal(other *Value) bool { 315 | return bytes.Equal(v[:], other[:]) 316 | } 317 | 318 | // MarshalJSON serialises a process value to JSON format 319 | func (v Value) MarshalJSON() ([]byte, error) { 320 | return id.Hash(v).MarshalJSON() 321 | } 322 | 323 | // UnmarshalJSON deserialises a JSON format to process value 324 | func (v *Value) UnmarshalJSON(data []byte) error { 325 | return (*id.Hash)(v).UnmarshalJSON(data) 326 | } 327 | 328 | // String implements the Stringer interface for process value 329 | func (v Value) String() string { 330 | return id.Hash(v).String() 331 | } 332 | 333 | var ( 334 | // NilValue is a reserved hash that represents when a Process is 335 | // prevoting/precommitting to nothing (i.e. the Process wants to progress to 336 | // the next Round). 337 | NilValue = Value(id.Hash{}) 338 | ) 339 | -------------------------------------------------------------------------------- /process/state_test.go: -------------------------------------------------------------------------------- 1 | package process_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing/quick" 6 | "time" 7 | 8 | "github.com/renproject/hyperdrive/process" 9 | "github.com/renproject/hyperdrive/process/processutil" 10 | "github.com/renproject/id" 11 | "github.com/renproject/surge" 12 | 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var _ = Describe("State", func() { 18 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 19 | 20 | Context("when unmarshaling fuzz", func() { 21 | It("should not panic", func() { 22 | f := func(fuzz []byte) bool { 23 | msg := process.State{} 24 | Expect(surge.FromBinary(&msg, fuzz)).ToNot(Succeed()) 25 | return true 26 | } 27 | Expect(quick.Check(f, nil)).To(Succeed()) 28 | }) 29 | }) 30 | 31 | Context("when marshaling and then unmarshaling", func() { 32 | It("should equal itself", func() { 33 | f := func(currentHeight process.Height, currentRound process.Round, currentStep process.Step, lockedRound process.Round, lockedValue process.Value, validRound process.Round, validValue process.Value, proposeLogs map[process.Round]process.Propose, prevoteLogs map[process.Round]map[id.Signatory]process.Prevote, precommitLogs map[process.Round]map[id.Signatory]process.Precommit, onceFlags map[process.Round]process.OnceFlag) bool { 34 | expected := process.State{ 35 | CurrentHeight: currentHeight, 36 | CurrentRound: currentRound, 37 | CurrentStep: currentStep, 38 | LockedRound: lockedRound, 39 | LockedValue: lockedValue, 40 | ValidRound: validRound, 41 | ValidValue: validValue, 42 | 43 | ProposeLogs: proposeLogs, 44 | PrevoteLogs: prevoteLogs, 45 | PrecommitLogs: precommitLogs, 46 | OnceFlags: onceFlags, 47 | } 48 | data, err := surge.ToBinary(expected) 49 | Expect(err).ToNot(HaveOccurred()) 50 | got := process.State{} 51 | err = surge.FromBinary(&got, data) 52 | Expect(err).ToNot(HaveOccurred()) 53 | Expect(got.Equal(&expected)).To(BeTrue()) 54 | return true 55 | } 56 | Expect(quick.Check(f, nil)).To(Succeed()) 57 | }) 58 | 59 | It("should return an error when not enough bytes (marshaling)", func() { 60 | loop := func() bool { 61 | expected := processutil.RandomState(r) 62 | sizeAvailable := r.Intn(expected.SizeHint()) 63 | buf := make([]byte, sizeAvailable) 64 | _, _, err := expected.Marshal(buf, sizeAvailable) 65 | Expect(err).To(HaveOccurred()) 66 | 67 | return true 68 | } 69 | Expect(quick.Check(loop, nil)).To(Succeed()) 70 | }) 71 | 72 | It("should return an error when not enough bytes (unmarshaling)", func() { 73 | loop := func() bool { 74 | expected := processutil.RandomState(r) 75 | sizeHint := expected.SizeHint() 76 | buf := make([]byte, sizeHint) 77 | _, _, err := expected.Marshal(buf, sizeHint) 78 | Expect(err).ToNot(HaveOccurred()) 79 | 80 | var unmarshalled process.State 81 | sizeAvailable := r.Intn(sizeHint) 82 | _, _, err = unmarshalled.Unmarshal(buf, sizeAvailable) 83 | Expect(err).To(HaveOccurred()) 84 | 85 | return true 86 | } 87 | Expect(quick.Check(loop, nil)).To(Succeed()) 88 | }) 89 | }) 90 | 91 | Context("when initialising the default state", func() { 92 | It("should have height=1", func() { 93 | Expect(process.DefaultState().CurrentHeight).To(Equal(process.Height(1))) 94 | }) 95 | 96 | It("should have round=0", func() { 97 | Expect(process.DefaultState().CurrentRound).To(Equal(process.Round(0))) 98 | }) 99 | 100 | It("should have step=proposing", func() { 101 | Expect(process.DefaultState().CurrentStep).To(Equal(process.Proposing)) 102 | }) 103 | 104 | It("should have locked round=invalid", func() { 105 | Expect(process.DefaultState().LockedRound).To(Equal(process.InvalidRound)) 106 | }) 107 | 108 | It("should have locked value=nil", func() { 109 | Expect(process.DefaultState().LockedValue).To(Equal(process.NilValue)) 110 | }) 111 | 112 | It("should have valid round=invalid", func() { 113 | Expect(process.DefaultState().ValidRound).To(Equal(process.InvalidRound)) 114 | }) 115 | 116 | It("should have valid value=nil", func() { 117 | Expect(process.DefaultState().ValidValue).To(Equal(process.NilValue)) 118 | }) 119 | }) 120 | 121 | Context("when cloned", func() { 122 | It("should clone correctly", func() { 123 | loop := func() bool { 124 | original := processutil.RandomState(r) 125 | duplicate := original.Clone() 126 | Expect(duplicate.Equal(&original)).To(BeTrue()) 127 | 128 | return true 129 | } 130 | Expect(quick.Check(loop, nil)).To(Succeed()) 131 | }) 132 | }) 133 | }) 134 | -------------------------------------------------------------------------------- /replica/.env: -------------------------------------------------------------------------------- 1 | export REPLAY_MODE=false 2 | -------------------------------------------------------------------------------- /replica/opt.go: -------------------------------------------------------------------------------- 1 | package replica 2 | 3 | import ( 4 | "github.com/renproject/hyperdrive/mq" 5 | "github.com/renproject/hyperdrive/process" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | // Options represent the options for a Hyperdrive Replica 11 | type Options struct { 12 | Logger *zap.Logger 13 | StartingHeight process.Height 14 | MessageQueueOpts mq.Options 15 | } 16 | 17 | // DefaultOptions returns the default options for a Hyperdrive Replica 18 | func DefaultOptions() Options { 19 | logger, err := zap.NewDevelopment() 20 | if err != nil { 21 | panic(err) 22 | } 23 | return Options{ 24 | Logger: logger, 25 | StartingHeight: process.DefaultHeight, 26 | MessageQueueOpts: mq.DefaultOptions(), 27 | } 28 | } 29 | 30 | // WithLogger updates the logger used in the Replica with the provided logger 31 | func (opts Options) WithLogger(logger *zap.Logger) Options { 32 | opts.Logger = logger 33 | return opts 34 | } 35 | 36 | // WithStartingHeight updates the height that the Replica will start at 37 | func (opts Options) WithStartingHeight(height process.Height) Options { 38 | opts.StartingHeight = height 39 | return opts 40 | } 41 | 42 | // WithMqOptions updates the Replica's message queue options 43 | func (opts Options) WithMqOptions(mqOpts mq.Options) Options { 44 | opts.MessageQueueOpts = mqOpts 45 | return opts 46 | } 47 | -------------------------------------------------------------------------------- /replica/opt_test.go: -------------------------------------------------------------------------------- 1 | package replica_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing/quick" 6 | "time" 7 | 8 | "github.com/renproject/hyperdrive/mq" 9 | "github.com/renproject/hyperdrive/replica" 10 | 11 | "go.uber.org/zap" 12 | 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var _ = Describe("Replica Opts", func() { 18 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 19 | 20 | Context("Replica Opts", func() { 21 | Specify("with default opts", func() { 22 | opts := replica.DefaultOptions() 23 | 24 | Expect(opts.MessageQueueOpts.MaxCapacity).To(Equal(1000)) 25 | }) 26 | 27 | Specify("with logger", func() { 28 | logger := zap.NewExample() 29 | _ = replica.DefaultOptions().WithLogger(logger) 30 | }) 31 | 32 | Specify("with message queue opts", func() { 33 | loop := func() bool { 34 | capacity := int(r.Int63()) 35 | mqOpts := mq.DefaultOptions().WithMaxCapacity(capacity) 36 | 37 | opts := replica.DefaultOptions().WithMqOptions(mqOpts) 38 | Expect(opts.MessageQueueOpts.MaxCapacity).To(Equal(capacity)) 39 | 40 | return true 41 | } 42 | Expect(quick.Check(loop, nil)).To(Succeed()) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /replica/replica.go: -------------------------------------------------------------------------------- 1 | package replica 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/renproject/hyperdrive/mq" 7 | "github.com/renproject/hyperdrive/process" 8 | "github.com/renproject/hyperdrive/scheduler" 9 | "github.com/renproject/hyperdrive/timer" 10 | "github.com/renproject/id" 11 | ) 12 | 13 | // DidHandleMessage is called by the Replica after it has finished handling an 14 | // input message (i.e. Propose, Prevote, or Precommit), or timeout. The message 15 | // could have been either accepted and inserted into the processing queue, or 16 | // filtered out and dropped. The callback is also called when the context 17 | // within which the Replica runs gets cancelled. 18 | type DidHandleMessage func() 19 | 20 | // A Replica represents a process in a replicated state machine that 21 | // participates in the Hyperdrive Consensus Algorithm. It encapsulates a 22 | // Hyperdrive Process and exposes an interface for the Hyperdrive user to 23 | // insert messages (propose, prevote, precommit, timeouts). A Replica then 24 | // handles these messages asynchronously, after sorting them in an increasing 25 | // order of height and round. A Replica is instantiated by passing in the set 26 | // of signatories participating in the consensus mechanism, and it filters out 27 | // messages that have not been sent by one of the known set of allowed 28 | // signatories. 29 | type Replica struct { 30 | opts Options 31 | 32 | proc process.Process 33 | procsAllowed map[id.Signatory]bool 34 | 35 | mch chan interface{} 36 | mq mq.MessageQueue 37 | 38 | didHandleMessage DidHandleMessage 39 | } 40 | 41 | // New instantiates and returns a pointer to a new Hyperdrive replica machine 42 | func New( 43 | opts Options, 44 | whoami id.Signatory, 45 | signatories []id.Signatory, 46 | linearTimer process.Timer, 47 | propose process.Proposer, 48 | validate process.Validator, 49 | commit process.Committer, 50 | catch process.Catcher, 51 | broadcast process.Broadcaster, 52 | didHandleMessage DidHandleMessage, 53 | ) *Replica { 54 | f := len(signatories) / 3 55 | scheduler := scheduler.NewRoundRobin(signatories) 56 | proc := process.NewWithCurrentHeight( 57 | whoami, 58 | opts.StartingHeight, 59 | f, 60 | linearTimer, 61 | scheduler, 62 | propose, 63 | validate, 64 | broadcast, 65 | commit, 66 | catch, 67 | ) 68 | 69 | procsAllowed := make(map[id.Signatory]bool) 70 | for _, signatory := range signatories { 71 | procsAllowed[signatory] = true 72 | } 73 | 74 | return &Replica{ 75 | opts: opts, 76 | 77 | proc: proc, 78 | procsAllowed: procsAllowed, 79 | 80 | mch: make(chan interface{}, opts.MessageQueueOpts.MaxCapacity), 81 | mq: mq.New(opts.MessageQueueOpts), 82 | 83 | didHandleMessage: didHandleMessage, 84 | } 85 | } 86 | 87 | // Run starts the Hyperdrive replica's process 88 | func (replica *Replica) Run(ctx context.Context) { 89 | replica.proc.Start() 90 | 91 | isRunning := true 92 | for isRunning { 93 | func() { 94 | defer func() { 95 | if replica.didHandleMessage != nil { 96 | replica.didHandleMessage() 97 | } 98 | }() 99 | 100 | select { 101 | case <-ctx.Done(): 102 | isRunning = false 103 | return 104 | case m := <-replica.mch: 105 | switch m := m.(type) { 106 | case timer.Timeout: 107 | switch m.MessageType { 108 | case process.MessageTypePropose: 109 | replica.proc.OnTimeoutPropose(m.Height, m.Round) 110 | case process.MessageTypePrevote: 111 | replica.proc.OnTimeoutPrevote(m.Height, m.Round) 112 | case process.MessageTypePrecommit: 113 | replica.proc.OnTimeoutPrecommit(m.Height, m.Round) 114 | default: 115 | return 116 | } 117 | case process.Propose: 118 | if !replica.filterHeight(m.Height) { 119 | return 120 | } 121 | replica.mq.InsertPropose(m) 122 | case process.Prevote: 123 | if !replica.filterHeight(m.Height) { 124 | return 125 | } 126 | replica.mq.InsertPrevote(m) 127 | case process.Precommit: 128 | if !replica.filterHeight(m.Height) { 129 | return 130 | } 131 | replica.mq.InsertPrecommit(m) 132 | case ResetHeightMessage: 133 | replica.proc.State = process.DefaultState().WithCurrentHeight(m.height) 134 | replica.mq.DropMessagesBelowHeight(m.height) 135 | 136 | // If the signatories change in the new height 137 | if len(m.signatories) != 0 { 138 | f := len(m.signatories) / 3 139 | replica.proc.StartWithNewSignatories(uint64(f), m.scheduler) 140 | replica.procsAllowed = map[id.Signatory]bool{} 141 | for _, sig := range m.signatories { 142 | replica.procsAllowed[sig] = true 143 | } 144 | } 145 | } 146 | } 147 | 148 | replica.flush() 149 | }() 150 | } 151 | } 152 | 153 | // Propose adds a propose message to the replica. This message will be 154 | // asynchronously inserted into the replica's message queue asynchronously, 155 | // and consumed when the replica does not have any immediate task to do 156 | func (replica *Replica) Propose(ctx context.Context, propose process.Propose) { 157 | select { 158 | case <-ctx.Done(): 159 | case replica.mch <- propose: 160 | } 161 | } 162 | 163 | // Prevote adds a prevote message to the replica. This message will be 164 | // asynchronously inserted into the replica's message queue asynchronously, 165 | // and consumed when the replica does not have any immediate task to do 166 | func (replica *Replica) Prevote(ctx context.Context, prevote process.Prevote) { 167 | select { 168 | case <-ctx.Done(): 169 | case replica.mch <- prevote: 170 | } 171 | } 172 | 173 | // Precommit adds a precommit message to the replica. This message will be 174 | // asynchronously inserted into the replica's message queue asynchronously, 175 | // and consumed when the replica does not have any immediate task to do 176 | func (replica *Replica) Precommit(ctx context.Context, precommit process.Precommit) { 177 | select { 178 | case <-ctx.Done(): 179 | case replica.mch <- precommit: 180 | } 181 | } 182 | 183 | // TimeoutPropose adds a propose timeout message to the replica. This message 184 | // will be filtered based on the replica's consensus height, and inserted 185 | // asynchronously into the replica's message queue. It will be consumed when 186 | // the replica does not have any immediate task to do 187 | func (replica *Replica) TimeoutPropose(ctx context.Context, timeout timer.Timeout) { 188 | select { 189 | case <-ctx.Done(): 190 | case replica.mch <- timeout: 191 | } 192 | } 193 | 194 | // TimeoutPrevote adds a prevote timeout message to the replica. This message 195 | // will be filtered based on the replica's consensus height, and inserted 196 | // asynchronously into the replica's message queue. It will be consumed when 197 | // the replica does not have any immediate task to do 198 | func (replica *Replica) TimeoutPrevote(ctx context.Context, timeout timer.Timeout) { 199 | select { 200 | case <-ctx.Done(): 201 | case replica.mch <- timeout: 202 | } 203 | } 204 | 205 | // TimeoutPrecommit adds a precommit timeout message to the replica. This message 206 | // will be filtered based on the replica's consensus height, and inserted 207 | // asynchronously into the replica's message queue. It will be consumed when 208 | // the replica does not have any immediate task to do 209 | func (replica *Replica) TimeoutPrecommit(ctx context.Context, timeout timer.Timeout) { 210 | select { 211 | case <-ctx.Done(): 212 | case replica.mch <- timeout: 213 | } 214 | } 215 | 216 | // ResetHeight of the underlying process to a future height. This is should only 217 | // be used when resynchronising the chain. If the given height is less than or 218 | // equal to the current height, nothing happens. 219 | // 220 | // NOTE: All messages that are currently in the message queue for heights less 221 | // than the given height will be dropped. 222 | func (replica *Replica) ResetHeight(ctx context.Context, newHeight process.Height, signatories []id.Signatory) { 223 | if newHeight <= replica.proc.State.CurrentHeight { 224 | return 225 | } 226 | message := ResetHeightMessage{ 227 | height: newHeight, 228 | signatories: signatories, 229 | scheduler: scheduler.NewRoundRobin(signatories), 230 | } 231 | select { 232 | case <-ctx.Done(): 233 | case replica.mch <- message: 234 | } 235 | } 236 | 237 | // State returns the current height, round and step of the underlying process. 238 | func (replica Replica) State() (process.Height, process.Round, process.Step) { 239 | return replica.proc.CurrentHeight, replica.proc.CurrentRound, replica.proc.CurrentStep 240 | } 241 | 242 | // CurrentHeight returns the current height of the underlying process. 243 | func (replica Replica) CurrentHeight() process.Height { 244 | return replica.proc.CurrentHeight 245 | } 246 | 247 | func (replica *Replica) filterHeight(height process.Height) bool { 248 | return height >= replica.proc.CurrentHeight 249 | } 250 | 251 | func (replica *Replica) flush() { 252 | for { 253 | n := replica.mq.Consume( 254 | replica.proc.CurrentHeight, 255 | replica.proc.Propose, 256 | replica.proc.Prevote, 257 | replica.proc.Precommit, 258 | replica.procsAllowed, 259 | ) 260 | if n == 0 { 261 | return 262 | } 263 | } 264 | } 265 | 266 | type ResetHeightMessage struct { 267 | height process.Height 268 | signatories []id.Signatory 269 | scheduler process.Scheduler 270 | } 271 | -------------------------------------------------------------------------------- /replica/replica_suite_test.go: -------------------------------------------------------------------------------- 1 | package replica 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestReplica(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Replica Suite") 13 | } 14 | -------------------------------------------------------------------------------- /scheduler/scheduler.go: -------------------------------------------------------------------------------- 1 | // Package scheduler defines interfaces and implementations for scheduling 2 | // different processes as value proposers. At any given height and round, 3 | // exactly one process is expected to take responsibility for proposing a value, 4 | // and this is determined by the Scheduler. 5 | // 6 | // It is important that all processes agree on the schedule. That is, at any 7 | // given height and round, all processes must arrive at the same decision 8 | // regarding which process is expected to be the proposer. This is most commonly 9 | // done by making the schedule deterministic and locally computable. This means 10 | // that processes do not have to invoke a consensus algorithm in order to agree 11 | // on the schedule (although, this is possible to do, using values from height N 12 | // to agree on the schedule for height N+1). 13 | package scheduler 14 | 15 | import ( 16 | "github.com/renproject/hyperdrive/process" 17 | "github.com/renproject/id" 18 | ) 19 | 20 | // RoundRobin holds a list of signatories that will participate in the round 21 | // robin scheduling 22 | type RoundRobin struct { 23 | signatories []id.Signatory 24 | } 25 | 26 | // NewRoundRobin returns a Scheduler that uses a simple round-robin scheduling 27 | // algorithm to select a proposer. Round-robin scheduling has the advantage of 28 | // being very easy to implement and understand, but has the disadvantage of 29 | // being unfair. As such, it should be avoided when the proposer is expected to 30 | // receive a reward. 31 | func NewRoundRobin(signatories []id.Signatory) process.Scheduler { 32 | copied := make([]id.Signatory, len(signatories)) 33 | copy(copied[:], signatories) 34 | return &RoundRobin{ 35 | signatories: copied, 36 | } 37 | } 38 | 39 | // Schedule a proposer using the sum of the height and round, modulo the number 40 | // of candidate processes, as an index into the current slice of candidate 41 | // processes. 42 | func (rr *RoundRobin) Schedule(height process.Height, round process.Round) id.Signatory { 43 | if len(rr.signatories) == 0 { 44 | panic("no processes to schedule") 45 | } 46 | if height <= 0 { 47 | panic("invalid height") 48 | } 49 | if round <= process.InvalidRound { 50 | panic("invalid round") 51 | } 52 | return rr.signatories[(uint64(height)+uint64(round))%uint64(len(rr.signatories))] 53 | } 54 | -------------------------------------------------------------------------------- /scheduler/scheduler_suite_test.go: -------------------------------------------------------------------------------- 1 | package scheduler_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSchedule(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Scheduler Suite") 13 | } 14 | -------------------------------------------------------------------------------- /scheduler/scheduler_test.go: -------------------------------------------------------------------------------- 1 | package scheduler_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing/quick" 6 | "time" 7 | 8 | "github.com/renproject/hyperdrive/process" 9 | "github.com/renproject/hyperdrive/scheduler" 10 | "github.com/renproject/id" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("Scheduler", func() { 17 | rand.Seed(int64(time.Now().Nanosecond())) 18 | 19 | Context("when scheduling", func() { 20 | var first, second, third id.Signatory 21 | var roundRobinScheduler process.Scheduler 22 | 23 | BeforeEach(func() { 24 | first = id.NewPrivKey().Signatory() 25 | second = id.NewPrivKey().Signatory() 26 | third = id.NewPrivKey().Signatory() 27 | roundRobinScheduler = scheduler.NewRoundRobin( 28 | []id.Signatory{ 29 | first, 30 | second, 31 | third, 32 | }, 33 | ) 34 | }) 35 | 36 | It("should panic for an invalid height", func() { 37 | loop := func() bool { 38 | invalidHeight := process.Height(-rand.Int63()) 39 | round := process.Round(rand.Int63()) 40 | Expect(func() { 41 | roundRobinScheduler.Schedule(invalidHeight, round) 42 | }).To(PanicWith("invalid height")) 43 | 44 | return true 45 | } 46 | Expect(quick.Check(loop, nil)).To(Succeed()) 47 | }) 48 | 49 | It("should panic for an invalid round", func() { 50 | loop := func() bool { 51 | height := process.Height(rand.Int63()) 52 | invalidRound := process.Round(-rand.Int63()) 53 | Expect(func() { 54 | roundRobinScheduler.Schedule(height, invalidRound) 55 | }).To(PanicWith("invalid round")) 56 | 57 | return true 58 | } 59 | Expect(quick.Check(loop, nil)).To(Succeed()) 60 | 61 | height := process.Height(rand.Int63()) 62 | Expect(func() { 63 | roundRobinScheduler.Schedule(height, process.InvalidRound) 64 | }).To(PanicWith("invalid round")) 65 | }) 66 | 67 | It("should panic for no signatories", func() { 68 | loop := func() bool { 69 | roundRobinScheduler = scheduler.NewRoundRobin([]id.Signatory{}) 70 | height := process.Height(rand.Int63()) 71 | round := process.Round(rand.Int63()) 72 | Expect(func() { 73 | roundRobinScheduler.Schedule(height, round) 74 | }).To(PanicWith("no processes to schedule")) 75 | 76 | return true 77 | } 78 | Expect(quick.Check(loop, nil)).To(Succeed()) 79 | }) 80 | 81 | It("should schedule correctly for a single signatory", func() { 82 | loop := func() bool { 83 | onlyOne := id.NewPrivKey().Signatory() 84 | roundRobinScheduler = scheduler.NewRoundRobin( 85 | []id.Signatory{onlyOne}, 86 | ) 87 | 88 | for t := 0; t <= 20; t++ { 89 | height := process.Height(rand.Int63()) 90 | round := process.Round(rand.Int63()) 91 | Expect(roundRobinScheduler.Schedule(height, round)).To(Equal(onlyOne)) 92 | } 93 | 94 | return true 95 | } 96 | Expect(quick.Check(loop, nil)).To(Succeed()) 97 | }) 98 | 99 | It("should schedule correctly for more than one signatories", func() { 100 | loop := func() bool { 101 | // create a list of signatories 102 | n := 2 + rand.Intn(12) 103 | signatories := make([]id.Signatory, n) 104 | for i := 0; i < n; i++ { 105 | signatories[i] = id.NewPrivKey().Signatory() 106 | } 107 | 108 | // instantiate the round robin scheduler 109 | roundRobinScheduler = scheduler.NewRoundRobin(signatories) 110 | 111 | // increment height while round is the same 112 | maxHeight := 13 + rand.Intn(28) 113 | for i := 1; i <= maxHeight; i++ { 114 | Expect(roundRobinScheduler.Schedule(process.Height(i), process.Round(0))). 115 | To(Equal(signatories[i%n])) 116 | } 117 | 118 | // increment round while height is the same 119 | maxRound := 13 + rand.Intn(28) 120 | for i := 0; i < maxRound; i++ { 121 | Expect(roundRobinScheduler.Schedule(process.Height(1), process.Round(i))). 122 | To(Equal(signatories[(i+1)%n])) 123 | } 124 | 125 | // increment both height and round 126 | for i := 2; i <= maxHeight; i++ { 127 | for j := 1; j < maxRound; j++ { 128 | Expect(roundRobinScheduler.Schedule(process.Height(i), process.Round(j))). 129 | To(Equal(signatories[(i+j)%n])) 130 | } 131 | } 132 | 133 | return true 134 | } 135 | Expect(quick.Check(loop, nil)).To(Succeed()) 136 | }) 137 | }) 138 | }) 139 | -------------------------------------------------------------------------------- /timer/opt.go: -------------------------------------------------------------------------------- 1 | package timer 2 | 3 | import ( 4 | "time" 5 | 6 | "go.uber.org/zap" 7 | ) 8 | 9 | const ( 10 | // DefaultTimeout is the timeout in seconds set by default 11 | DefaultTimeout = 20 * time.Second 12 | 13 | // DefaultTimeoutScaling is the timeout scaling factor set by default 14 | DefaultTimeoutScaling = 0.5 15 | ) 16 | 17 | // Options represent the options for a Linear Timer 18 | type Options struct { 19 | Logger *zap.Logger 20 | Timeout time.Duration 21 | TimeoutScaling float64 22 | } 23 | 24 | // DefaultOptions returns the default options for a Linear Timer 25 | func DefaultOptions() Options { 26 | logger, err := zap.NewDevelopment() 27 | if err != nil { 28 | panic(err) 29 | } 30 | return Options{ 31 | Logger: logger, 32 | Timeout: DefaultTimeout, 33 | TimeoutScaling: DefaultTimeoutScaling, 34 | } 35 | } 36 | 37 | // WithLogger updates the logger used in the Linear Timer 38 | func (opts Options) WithLogger(logger *zap.Logger) Options { 39 | opts.Logger = logger 40 | return opts 41 | } 42 | 43 | // WithTimeout updates the timeout of the Linear Timer 44 | func (opts Options) WithTimeout(timeout time.Duration) Options { 45 | opts.Timeout = timeout 46 | return opts 47 | } 48 | 49 | // WithTimeoutScaling updates the timeout scaling factor of the Linear Timer 50 | func (opts Options) WithTimeoutScaling(timeoutScaling float64) Options { 51 | opts.TimeoutScaling = timeoutScaling 52 | return opts 53 | } 54 | -------------------------------------------------------------------------------- /timer/opt_test.go: -------------------------------------------------------------------------------- 1 | package timer_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing/quick" 6 | "time" 7 | 8 | "github.com/renproject/hyperdrive/timer" 9 | 10 | "go.uber.org/zap" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("Timer Opts", func() { 17 | rand.Seed(int64(time.Now().Nanosecond())) 18 | 19 | Context("Linear Timer", func() { 20 | Specify("with default options", func() { 21 | defaultOpts := timer.DefaultOptions() 22 | Expect(defaultOpts.Timeout).To(Equal(20 * time.Second)) 23 | Expect(defaultOpts.TimeoutScaling).To(Equal(0.5)) 24 | }) 25 | 26 | Specify("with logger", func() { 27 | logger := zap.NewExample() 28 | _ = timer.DefaultOptions().WithLogger(logger) 29 | }) 30 | 31 | Specify("with timeout", func() { 32 | loop := func() bool { 33 | timeout := time.Duration(rand.Intn(100)) * time.Second 34 | opts := timer.DefaultOptions().WithTimeout(timeout) 35 | Expect(opts.Timeout).To(Equal(timeout)) 36 | return true 37 | } 38 | Expect(quick.Check(loop, nil)).To(Succeed()) 39 | }) 40 | 41 | Specify("with timeout scaling", func() { 42 | loop := func() bool { 43 | timeoutScaling := rand.Float64() 44 | opts := timer.DefaultOptions().WithTimeoutScaling(timeoutScaling) 45 | Expect(opts.TimeoutScaling).To(Equal(timeoutScaling)) 46 | return true 47 | } 48 | Expect(quick.Check(loop, nil)).To(Succeed()) 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /timer/timer.go: -------------------------------------------------------------------------------- 1 | package timer 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/renproject/hyperdrive/process" 8 | 9 | "github.com/renproject/surge" 10 | ) 11 | 12 | // Timeout represents an event emitted by the Linear Timer whenever 13 | // a scheduled timeout is triggered 14 | type Timeout struct { 15 | MessageType process.MessageType 16 | Height process.Height 17 | Round process.Round 18 | } 19 | 20 | // SizeHint implements surge SizeHinter for Timeout 21 | func (timeout Timeout) SizeHint() int { 22 | return surge.SizeHintI8 + 23 | surge.SizeHint(timeout.Height) + 24 | surge.SizeHint(timeout.Round) 25 | } 26 | 27 | // Marshal implements surge Marshaler for Timeout 28 | func (timeout Timeout) Marshal(buf []byte, rem int) ([]byte, int, error) { 29 | buf, rem, err := surge.MarshalI8(int8(timeout.MessageType), buf, rem) 30 | if err != nil { 31 | return buf, rem, fmt.Errorf("marshaling MeesageType=%v: %v", timeout.MessageType, err) 32 | } 33 | buf, rem, err = surge.Marshal(timeout.Height, buf, rem) 34 | if err != nil { 35 | return buf, rem, fmt.Errorf("marshaling Height=%v: %v", timeout.Height, err) 36 | } 37 | buf, rem, err = surge.Marshal(timeout.Round, buf, rem) 38 | if err != nil { 39 | return buf, rem, fmt.Errorf("marshaling Round=%v: %v", timeout.Round, err) 40 | } 41 | 42 | return buf, rem, nil 43 | } 44 | 45 | // Unmarshal implements surge Unmarshaler for Timeout 46 | func (timeout *Timeout) Unmarshal(buf []byte, rem int) ([]byte, int, error) { 47 | buf, rem, err := surge.UnmarshalI8((*int8)(&timeout.MessageType), buf, rem) 48 | if err != nil { 49 | return buf, rem, fmt.Errorf("unmarshaling MessageType: %v", err) 50 | } 51 | buf, rem, err = surge.Unmarshal(&timeout.Height, buf, rem) 52 | if err != nil { 53 | return buf, rem, fmt.Errorf("unmarshaling Height: %v", err) 54 | } 55 | buf, rem, err = surge.Unmarshal(&timeout.Round, buf, rem) 56 | if err != nil { 57 | return buf, rem, fmt.Errorf("unmarshaling Round: %v", err) 58 | } 59 | 60 | return buf, rem, nil 61 | } 62 | 63 | // LinearTimer defines a timer that implements a timing out functionality. 64 | // The timeouts for different contexts (Propose, Prevote and Precommit) are 65 | // provided as callback functions that handle the corresponding timeouts. The 66 | // timeout scales linearly with the consensus round 67 | type LinearTimer struct { 68 | opts Options 69 | handleTimeoutPropose func(Timeout) 70 | handleTimeoutPrevote func(Timeout) 71 | handleTimeoutPrecommit func(Timeout) 72 | } 73 | 74 | // NewLinearTimer constructs a new Linear Timer from the input options and channels 75 | func NewLinearTimer(opts Options, handleTimeoutPropose, handleTimeoutPrevote, handleTimeoutPrecommit func(Timeout)) *LinearTimer { 76 | return &LinearTimer{ 77 | opts: opts, 78 | handleTimeoutPropose: handleTimeoutPropose, 79 | handleTimeoutPrevote: handleTimeoutPrevote, 80 | handleTimeoutPrecommit: handleTimeoutPrecommit, 81 | } 82 | } 83 | 84 | // TimeoutPropose schedules a propose timeout with a timeout period appropriately 85 | // calculated for the consensus height and round 86 | func (t *LinearTimer) TimeoutPropose(height process.Height, round process.Round) { 87 | if t.handleTimeoutPropose != nil { 88 | go func() { 89 | time.Sleep(t.DurationAtHeightAndRound(height, round)) 90 | t.handleTimeoutPropose(Timeout{MessageType: process.MessageTypePropose, Height: height, Round: round}) 91 | }() 92 | } 93 | } 94 | 95 | // TimeoutPrevote schedules a prevote timeout with a timeout period appropriately 96 | // calculated for the consensus height and round 97 | func (t *LinearTimer) TimeoutPrevote(height process.Height, round process.Round) { 98 | if t.handleTimeoutPrevote != nil { 99 | go func() { 100 | time.Sleep(t.DurationAtHeightAndRound(height, round)) 101 | t.handleTimeoutPrevote(Timeout{MessageType: process.MessageTypePrevote, Height: height, Round: round}) 102 | }() 103 | } 104 | } 105 | 106 | // TimeoutPrecommit schedules a precommit timeout with a timeout period appropriately 107 | // calculated for the consensus height and round 108 | func (t *LinearTimer) TimeoutPrecommit(height process.Height, round process.Round) { 109 | if t.handleTimeoutPrecommit != nil { 110 | go func() { 111 | time.Sleep(t.DurationAtHeightAndRound(height, round)) 112 | t.handleTimeoutPrecommit(Timeout{MessageType: process.MessageTypePrecommit, Height: height, Round: round}) 113 | }() 114 | } 115 | } 116 | 117 | // DurationAtHeightAndRound returns the duration of the timeout at the given 118 | // height and round. This is the duration that the other methods will wait 119 | // before scheduling their respective timeout events. 120 | func (t *LinearTimer) DurationAtHeightAndRound(height process.Height, round process.Round) time.Duration { 121 | return t.opts.Timeout + t.opts.Timeout*time.Duration(float64(round)*t.opts.TimeoutScaling) 122 | } 123 | -------------------------------------------------------------------------------- /timer/timer_suite_test.go: -------------------------------------------------------------------------------- 1 | package timer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestTimer(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Timer Suite") 13 | } 14 | -------------------------------------------------------------------------------- /timer/timer_test.go: -------------------------------------------------------------------------------- 1 | package timer_test 2 | 3 | import ( 4 | "math/rand" 5 | "reflect" 6 | "testing/quick" 7 | "time" 8 | 9 | "github.com/renproject/hyperdrive/process" 10 | "github.com/renproject/hyperdrive/process/processutil" 11 | "github.com/renproject/hyperdrive/timer" 12 | "github.com/renproject/surge/surgeutil" 13 | 14 | . "github.com/onsi/ginkgo" 15 | . "github.com/onsi/gomega" 16 | ) 17 | 18 | var _ = Describe("Timer", func() { 19 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 20 | 21 | Context("Timer", func() { 22 | Context("marshaling and unmarshaling", func() { 23 | t := reflect.TypeOf(timer.Timeout{}) 24 | 25 | It("should be the same after marshalling and unmarshalling", func() { 26 | loop := func() bool { 27 | Expect(surgeutil.MarshalUnmarshalCheck(t)).To(Succeed()) 28 | return true 29 | } 30 | Expect(quick.Check(loop, nil)).To(Succeed()) 31 | }) 32 | 33 | It("should not panic when fuzzing", func() { 34 | loop := func() bool { 35 | Expect(func() { surgeutil.Fuzz(t) }).ToNot(Panic()) 36 | return true 37 | } 38 | Expect(quick.Check(loop, nil)).To(Succeed()) 39 | }) 40 | 41 | Context("marshalling", func() { 42 | It("should return an error when the buffer is too small", func() { 43 | loop := func() bool { 44 | Expect(surgeutil.MarshalBufTooSmall(t)).To(Succeed()) 45 | return true 46 | } 47 | Expect(quick.Check(loop, nil)).To(Succeed()) 48 | }) 49 | 50 | It("should return an error when the memory quota is too small", func() { 51 | loop := func() bool { 52 | Expect(surgeutil.MarshalRemTooSmall(t)).To(Succeed()) 53 | return true 54 | } 55 | Expect(quick.Check(loop, nil)).To(Succeed()) 56 | }) 57 | }) 58 | 59 | Context("unmarshalling", func() { 60 | It("should return an error when the buffer is too small", func() { 61 | loop := func() bool { 62 | Expect(surgeutil.UnmarshalBufTooSmall(t)).To(Succeed()) 63 | return true 64 | } 65 | Expect(quick.Check(loop, nil)).To(Succeed()) 66 | }) 67 | 68 | It("should return an error when the memory quota is too small", func() { 69 | loop := func() bool { 70 | Expect(surgeutil.UnmarshalRemTooSmall(t)).To(Succeed()) 71 | return true 72 | } 73 | Expect(quick.Check(loop, nil)).To(Succeed()) 74 | }) 75 | }) 76 | }) 77 | 78 | Context("with nil handlers", func() { 79 | It("should ignore the timeout calls", func() { 80 | opts := timer.DefaultOptions(). 81 | WithTimeout(1 * time.Millisecond). 82 | WithTimeoutScaling(0.0) 83 | 84 | constTimer := timer.NewLinearTimer(opts, nil, nil, nil) 85 | 86 | height := processutil.RandomHeight(r) 87 | round := processutil.RandomRound(r) 88 | 89 | constTimer.TimeoutPropose(height, round) 90 | constTimer.TimeoutPrevote(height, round) 91 | constTimer.TimeoutPrecommit(height, round) 92 | }) 93 | }) 94 | 95 | Context("without a timeout scaling factor", func() { 96 | Specify("on timeout propose", func() { 97 | loop := func() bool { 98 | // 5 millisecond <= timeout <= 20 millisecond 99 | timeout := time.Duration(5+r.Intn(16)) * time.Millisecond 100 | timeoutScaling := 0.0 101 | opts := timer.DefaultOptions(). 102 | WithTimeout(timeout). 103 | WithTimeoutScaling(timeoutScaling) 104 | onProposeTimeoutChan := make(chan timer.Timeout, 1) 105 | onPrevoteTimeoutChan := make(chan timer.Timeout, 1) 106 | onPrecommitTimeoutChan := make(chan timer.Timeout, 1) 107 | handleProposeTimeout := func(timeout timer.Timeout) { 108 | onProposeTimeoutChan <- timeout 109 | } 110 | handlePrevoteTimeout := func(timeout timer.Timeout) { 111 | onPrevoteTimeoutChan <- timeout 112 | } 113 | handlePrecommitTimeout := func(timeout timer.Timeout) { 114 | onPrecommitTimeoutChan <- timeout 115 | } 116 | constantTimer := timer.NewLinearTimer(opts, handleProposeTimeout, handlePrevoteTimeout, handlePrecommitTimeout) 117 | 118 | // timeout should be the same for any round/height 119 | height := processutil.RandomHeight(r) 120 | round := processutil.RandomRound(r) 121 | 122 | constantTimer.TimeoutPropose(height, round) 123 | 124 | // message will be received at least by that time 125 | time.Sleep(timeout - (10 * time.Millisecond)) 126 | select { 127 | case _ = <-onProposeTimeoutChan: 128 | // the channel is empty, so should not reach here 129 | Expect(true).ToNot(BeTrue()) 130 | default: 131 | Expect(true).To(BeTrue()) 132 | } 133 | 134 | // message will be received at least by that time 135 | time.Sleep(20 * time.Millisecond) 136 | select { 137 | case timeoutFor := <-onProposeTimeoutChan: 138 | Expect(timeoutFor.Height).To(Equal(height)) 139 | Expect(timeoutFor.Round).To(Equal(round)) 140 | default: 141 | // this should not happen 142 | Expect(true).ToNot(BeTrue()) 143 | } 144 | 145 | // no other channel should have received any message 146 | select { 147 | case _ = <-onPrevoteTimeoutChan: 148 | Expect(true).ToNot(BeTrue()) 149 | case _ = <-onPrecommitTimeoutChan: 150 | Expect(true).ToNot(BeTrue()) 151 | default: 152 | Expect(true).To(BeTrue()) 153 | } 154 | 155 | return true 156 | } 157 | Expect(quick.Check(loop, nil)).To(Succeed()) 158 | }) 159 | 160 | Specify("on timeout prevote", func() { 161 | loop := func() bool { 162 | // 5 millisecond <= timeout <= 20 millisecond 163 | timeout := time.Duration(5+r.Intn(16)) * time.Millisecond 164 | timeoutScaling := 0.0 165 | opts := timer.DefaultOptions(). 166 | WithTimeout(timeout). 167 | WithTimeoutScaling(timeoutScaling) 168 | onProposeTimeoutChan := make(chan timer.Timeout, 1) 169 | onPrevoteTimeoutChan := make(chan timer.Timeout, 1) 170 | onPrecommitTimeoutChan := make(chan timer.Timeout, 1) 171 | handleProposeTimeout := func(timeout timer.Timeout) { 172 | onProposeTimeoutChan <- timeout 173 | } 174 | handlePrevoteTimeout := func(timeout timer.Timeout) { 175 | onPrevoteTimeoutChan <- timeout 176 | } 177 | handlePrecommitTimeout := func(timeout timer.Timeout) { 178 | onPrecommitTimeoutChan <- timeout 179 | } 180 | constantTimer := timer.NewLinearTimer(opts, handleProposeTimeout, handlePrevoteTimeout, handlePrecommitTimeout) 181 | 182 | // timeout should be the same for any round/height 183 | height := processutil.RandomHeight(r) 184 | round := processutil.RandomRound(r) 185 | 186 | constantTimer.TimeoutPrevote(height, round) 187 | 188 | // message will be received at least by that time 189 | time.Sleep(timeout - (10 * time.Millisecond)) 190 | select { 191 | case _ = <-onPrevoteTimeoutChan: 192 | // the channel is empty, so should not reach here 193 | Expect(true).ToNot(BeTrue()) 194 | default: 195 | Expect(true).To(BeTrue()) 196 | } 197 | 198 | // message will be received at least by that time 199 | time.Sleep(20 * time.Millisecond) 200 | select { 201 | case timeoutFor := <-onPrevoteTimeoutChan: 202 | Expect(timeoutFor.Height).To(Equal(height)) 203 | Expect(timeoutFor.Round).To(Equal(round)) 204 | default: 205 | // this should not happen 206 | Expect(true).ToNot(BeTrue()) 207 | } 208 | 209 | // no other channel should have received any message 210 | select { 211 | case _ = <-onProposeTimeoutChan: 212 | Expect(true).ToNot(BeTrue()) 213 | case _ = <-onPrecommitTimeoutChan: 214 | Expect(true).ToNot(BeTrue()) 215 | default: 216 | Expect(true).To(BeTrue()) 217 | } 218 | 219 | return true 220 | } 221 | Expect(quick.Check(loop, nil)).To(Succeed()) 222 | }) 223 | 224 | Specify("on timeout precommit", func() { 225 | loop := func() bool { 226 | // 5 millisecond <= timeout <= 20 millisecond 227 | timeout := time.Duration(5+r.Intn(16)) * time.Millisecond 228 | timeoutScaling := 0.0 229 | opts := timer.DefaultOptions(). 230 | WithTimeout(timeout). 231 | WithTimeoutScaling(timeoutScaling) 232 | onProposeTimeoutChan := make(chan timer.Timeout, 1) 233 | onPrevoteTimeoutChan := make(chan timer.Timeout, 1) 234 | onPrecommitTimeoutChan := make(chan timer.Timeout, 1) 235 | handleProposeTimeout := func(timeout timer.Timeout) { 236 | onProposeTimeoutChan <- timeout 237 | } 238 | handlePrevoteTimeout := func(timeout timer.Timeout) { 239 | onPrevoteTimeoutChan <- timeout 240 | } 241 | handlePrecommitTimeout := func(timeout timer.Timeout) { 242 | onPrecommitTimeoutChan <- timeout 243 | } 244 | constantTimer := timer.NewLinearTimer(opts, handleProposeTimeout, handlePrevoteTimeout, handlePrecommitTimeout) 245 | 246 | // timeout should be the same for any round/height 247 | height := processutil.RandomHeight(r) 248 | round := processutil.RandomRound(r) 249 | 250 | constantTimer.TimeoutPrecommit(height, round) 251 | 252 | // message will be received at least by that time 253 | time.Sleep(timeout - (10 * time.Millisecond)) 254 | select { 255 | case _ = <-onPrecommitTimeoutChan: 256 | // the channel is empty, so should not reach here 257 | Expect(true).ToNot(BeTrue()) 258 | default: 259 | Expect(true).To(BeTrue()) 260 | } 261 | 262 | // message will be received at least by that time 263 | time.Sleep(20 * time.Millisecond) 264 | select { 265 | case timeoutFor := <-onPrecommitTimeoutChan: 266 | Expect(timeoutFor.Height).To(Equal(height)) 267 | Expect(timeoutFor.Round).To(Equal(round)) 268 | default: 269 | // this should not happen 270 | Expect(true).ToNot(BeTrue()) 271 | } 272 | 273 | // no other channel should have received any message 274 | select { 275 | case _ = <-onProposeTimeoutChan: 276 | Expect(true).ToNot(BeTrue()) 277 | case _ = <-onPrevoteTimeoutChan: 278 | Expect(true).ToNot(BeTrue()) 279 | default: 280 | Expect(true).To(BeTrue()) 281 | } 282 | 283 | return true 284 | } 285 | Expect(quick.Check(loop, nil)).To(Succeed()) 286 | }) 287 | }) 288 | 289 | Context("with a timeout scaling factor", func() { 290 | Specify("on timeout propose", func() { 291 | loop := func() bool { 292 | timeout := 5 * time.Millisecond 293 | timeoutScaling := r.Float64() / 2.0 294 | opts := timer.DefaultOptions(). 295 | WithTimeout(timeout). 296 | WithTimeoutScaling(timeoutScaling) 297 | onProposeTimeoutChan := make(chan timer.Timeout, 1) 298 | onPrevoteTimeoutChan := make(chan timer.Timeout, 1) 299 | onPrecommitTimeoutChan := make(chan timer.Timeout, 1) 300 | handleProposeTimeout := func(timeout timer.Timeout) { 301 | onProposeTimeoutChan <- timeout 302 | } 303 | handlePrevoteTimeout := func(timeout timer.Timeout) { 304 | onPrevoteTimeoutChan <- timeout 305 | } 306 | handlePrecommitTimeout := func(timeout timer.Timeout) { 307 | onPrecommitTimeoutChan <- timeout 308 | } 309 | constantTimer := timer.NewLinearTimer(opts, handleProposeTimeout, handlePrevoteTimeout, handlePrecommitTimeout) 310 | 311 | // timeout should scale up linearly with round 312 | height := processutil.RandomHeight(r) 313 | round := process.Round(r.Intn(20)) 314 | 315 | expectedTimeout := timeout + (timeout * (time.Duration((float64(round) * timeoutScaling)))) 316 | 317 | constantTimer.TimeoutPropose(height, round) 318 | 319 | // message will not be received by that time 320 | time.Sleep(expectedTimeout - (10 * time.Millisecond)) 321 | select { 322 | case _ = <-onProposeTimeoutChan: 323 | // the channel is empty, so should not reach here 324 | Expect(true).ToNot(BeTrue()) 325 | default: 326 | Expect(true).To(BeTrue()) 327 | } 328 | 329 | // message will be received at least by that time 330 | time.Sleep(20 * time.Millisecond) 331 | select { 332 | case timeoutFor := <-onProposeTimeoutChan: 333 | Expect(timeoutFor.Height).To(Equal(height)) 334 | Expect(timeoutFor.Round).To(Equal(round)) 335 | default: 336 | // this should not happen 337 | Expect(true).ToNot(BeTrue()) 338 | } 339 | 340 | // no other channel should have received any message 341 | select { 342 | case _ = <-onPrevoteTimeoutChan: 343 | Expect(true).ToNot(BeTrue()) 344 | case _ = <-onPrecommitTimeoutChan: 345 | Expect(true).ToNot(BeTrue()) 346 | default: 347 | Expect(true).To(BeTrue()) 348 | } 349 | 350 | return true 351 | } 352 | Expect(quick.Check(loop, nil)).To(Succeed()) 353 | }) 354 | 355 | Specify("on timeout prevote", func() { 356 | loop := func() bool { 357 | timeout := 5 * time.Millisecond 358 | timeoutScaling := r.Float64() / 2.0 359 | opts := timer.DefaultOptions(). 360 | WithTimeout(timeout). 361 | WithTimeoutScaling(timeoutScaling) 362 | onProposeTimeoutChan := make(chan timer.Timeout, 1) 363 | onPrevoteTimeoutChan := make(chan timer.Timeout, 1) 364 | onPrecommitTimeoutChan := make(chan timer.Timeout, 1) 365 | handleProposeTimeout := func(timeout timer.Timeout) { 366 | onProposeTimeoutChan <- timeout 367 | } 368 | handlePrevoteTimeout := func(timeout timer.Timeout) { 369 | onPrevoteTimeoutChan <- timeout 370 | } 371 | handlePrecommitTimeout := func(timeout timer.Timeout) { 372 | onPrecommitTimeoutChan <- timeout 373 | } 374 | constantTimer := timer.NewLinearTimer(opts, handleProposeTimeout, handlePrevoteTimeout, handlePrecommitTimeout) 375 | 376 | // timeout should scale up linearly with round 377 | height := processutil.RandomHeight(r) 378 | round := process.Round(r.Intn(20)) 379 | 380 | expectedTimeout := timeout + (timeout * (time.Duration((float64(round) * timeoutScaling)))) 381 | 382 | constantTimer.TimeoutPrevote(height, round) 383 | 384 | // message will not be received by that time 385 | time.Sleep(expectedTimeout - (10 * time.Millisecond)) 386 | select { 387 | case _ = <-onPrevoteTimeoutChan: 388 | // the channel is empty, so should not reach here 389 | Expect(true).ToNot(BeTrue()) 390 | default: 391 | Expect(true).To(BeTrue()) 392 | } 393 | 394 | // message will be received at least by that time 395 | time.Sleep(20 * time.Millisecond) 396 | select { 397 | case timeoutFor := <-onPrevoteTimeoutChan: 398 | Expect(timeoutFor.Height).To(Equal(height)) 399 | Expect(timeoutFor.Round).To(Equal(round)) 400 | default: 401 | // this should not happen 402 | Expect(true).ToNot(BeTrue()) 403 | } 404 | 405 | // no other channel should have received any message 406 | select { 407 | case _ = <-onProposeTimeoutChan: 408 | Expect(true).ToNot(BeTrue()) 409 | case _ = <-onPrecommitTimeoutChan: 410 | Expect(true).ToNot(BeTrue()) 411 | default: 412 | Expect(true).To(BeTrue()) 413 | } 414 | 415 | return true 416 | } 417 | Expect(quick.Check(loop, nil)).To(Succeed()) 418 | }) 419 | 420 | Specify("on timeout precommit", func() { 421 | loop := func() bool { 422 | timeout := 5 * time.Millisecond 423 | timeoutScaling := r.Float64() / 2.0 424 | opts := timer.DefaultOptions(). 425 | WithTimeout(timeout). 426 | WithTimeoutScaling(timeoutScaling) 427 | onProposeTimeoutChan := make(chan timer.Timeout, 1) 428 | onPrevoteTimeoutChan := make(chan timer.Timeout, 1) 429 | onPrecommitTimeoutChan := make(chan timer.Timeout, 1) 430 | handleProposeTimeout := func(timeout timer.Timeout) { 431 | onProposeTimeoutChan <- timeout 432 | } 433 | handlePrevoteTimeout := func(timeout timer.Timeout) { 434 | onPrevoteTimeoutChan <- timeout 435 | } 436 | handlePrecommitTimeout := func(timeout timer.Timeout) { 437 | onPrecommitTimeoutChan <- timeout 438 | } 439 | constantTimer := timer.NewLinearTimer(opts, handleProposeTimeout, handlePrevoteTimeout, handlePrecommitTimeout) 440 | 441 | // timeout should scale up linearly with round 442 | height := processutil.RandomHeight(r) 443 | round := process.Round(r.Intn(20)) 444 | 445 | expectedTimeout := timeout + (timeout * (time.Duration((float64(round) * timeoutScaling)))) 446 | 447 | constantTimer.TimeoutPrecommit(height, round) 448 | 449 | // message will not be received by that time 450 | time.Sleep(expectedTimeout - (10 * time.Millisecond)) 451 | select { 452 | case _ = <-onPrecommitTimeoutChan: 453 | // the channel is empty, so should not reach here 454 | Expect(true).ToNot(BeTrue()) 455 | default: 456 | Expect(true).To(BeTrue()) 457 | } 458 | 459 | // message will be received at least by that time 460 | time.Sleep(20 * time.Millisecond) 461 | select { 462 | case timeoutFor := <-onPrecommitTimeoutChan: 463 | Expect(timeoutFor.Height).To(Equal(height)) 464 | Expect(timeoutFor.Round).To(Equal(round)) 465 | default: 466 | // this should not happen 467 | Expect(true).ToNot(BeTrue()) 468 | } 469 | 470 | // no other channel should have received any message 471 | select { 472 | case _ = <-onPrevoteTimeoutChan: 473 | Expect(true).ToNot(BeTrue()) 474 | case _ = <-onProposeTimeoutChan: 475 | Expect(true).ToNot(BeTrue()) 476 | default: 477 | Expect(true).To(BeTrue()) 478 | } 479 | 480 | return true 481 | } 482 | Expect(quick.Check(loop, nil)).To(Succeed()) 483 | }) 484 | }) 485 | }) 486 | }) 487 | --------------------------------------------------------------------------------