From 0f8973ec33ab0b5aba09448e6bf50232ab5b245e Mon Sep 17 00:00:00 2001 From: Dave Friedel Date: Sun, 21 Dec 2025 06:00:02 -0500 Subject: [PATCH] Initial commit - Tutus Consensus (dBFT 2.0) --- .github/CODEOWNERS | 1 + .github/workflows/build.yml | 44 + .github/workflows/changelog_update.yml | 33 + .github/workflows/dco.yml | 10 + .github/workflows/go.yml | 98 ++ .gitignore | 5 + CHANGELOG.md | 147 ++ LICENSE.md | 10 + README.md | 51 + block.go | 30 + change_view.go | 10 + change_view_reason.go | 18 + change_view_reason_string.go | 38 + check.go | 180 +++ commit.go | 9 + config.go | 465 ++++++ consensus_message.go | 26 + consensus_message_type.go | 39 + consensus_payload.go | 19 + context.go | 445 ++++++ dbft.go | 751 ++++++++++ dbft_test.go | 1268 +++++++++++++++++ docs/labels.md | 9 + docs/release-instruction.md | 64 + formal-models/.github/dbft.drawio | 1 + formal-models/.github/dbft.png | Bin 0 -> 43622 bytes .../.github/dbft2.1_centralizedCV.drawio | 1 + .../.github/dbft2.1_centralizedCV.png | Bin 0 -> 113626 bytes .../.github/dbft2.1_threeStagedCV.drawio | 1 + .../.github/dbft2.1_threeStagedCV.png | Bin 0 -> 110022 bytes formal-models/.github/dbft_antiMEV.drawio | 111 ++ formal-models/.github/dbft_antiMEV.png | Bin 0 -> 70101 bytes formal-models/README.md | 456 ++++++ formal-models/dbft/dbft.tla | 388 +++++ formal-models/dbft/dbft___AllGoodModel.launch | 42 + .../dbftCentralizedCV.tla | 507 +++++++ .../dbftCentralizedCV___AllGoodModel.launch | 42 + .../dbft2.1_threeStagedCV/dbftCV3.tla | 427 ++++++ .../dbftCV3___AllGoodModel.launch | 42 + formal-models/dbftMultipool/dbftMultipool.tla | 466 ++++++ .../dbftMultipool___AllGoodModel.launch | 44 + formal-models/dbft_antiMEV/dbft.tla | 430 ++++++ .../dbft_antiMEV/dbft___AllGoodModel.launch | 42 + go.mod | 15 + go.sum | 16 + helpers.go | 63 + helpers_test.go | 115 ++ identity.go | 28 + internal/consensus/amev_block.go | 128 ++ internal/consensus/amev_commit.go | 44 + internal/consensus/amev_preBlock.go | 79 + internal/consensus/amev_preCommit.go | 45 + internal/consensus/block.go | 151 ++ internal/consensus/block_test.go | 80 ++ internal/consensus/change_view.go | 47 + internal/consensus/commit.go | 43 + internal/consensus/compact.go | 69 + internal/consensus/consensus.go | 72 + internal/consensus/consensus_message.go | 110 ++ internal/consensus/constructors.go | 83 ++ internal/consensus/helpers.go | 9 + internal/consensus/message.go | 121 ++ internal/consensus/message_test.go | 206 +++ internal/consensus/prepare_request.go | 61 + internal/consensus/prepare_response.go | 43 + internal/consensus/recovery_message.go | 236 +++ internal/consensus/recovery_request.go | 42 + internal/consensus/transaction.go | 41 + internal/crypto/crypto.go | 33 + internal/crypto/crypto_test.go | 34 + internal/crypto/ecdsa.go | 85 ++ internal/crypto/ecdsa_test.go | 20 + internal/crypto/hash.go | 46 + internal/crypto/hash_test.go | 50 + internal/merkle/merkle_tree.go | 79 + internal/merkle/merkle_tree_test.go | 59 + internal/simulation/main.go | 314 ++++ pre_block.go | 25 + pre_commit.go | 10 + prepare_request.go | 11 + prepare_response.go | 8 + recovery_message.go | 23 + recovery_request.go | 7 + rtt.go | 26 + send.go | 236 +++ timer.go | 22 + timer/timer.go | 97 ++ timer/timer_test.go | 56 + transaction.go | 8 + 89 files changed, 9966 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/changelog_update.yml create mode 100644 .github/workflows/dco.yml create mode 100644 .github/workflows/go.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 block.go create mode 100644 change_view.go create mode 100644 change_view_reason.go create mode 100644 change_view_reason_string.go create mode 100644 check.go create mode 100644 commit.go create mode 100644 config.go create mode 100644 consensus_message.go create mode 100644 consensus_message_type.go create mode 100644 consensus_payload.go create mode 100644 context.go create mode 100644 dbft.go create mode 100644 dbft_test.go create mode 100644 docs/labels.md create mode 100644 docs/release-instruction.md create mode 100644 formal-models/.github/dbft.drawio create mode 100644 formal-models/.github/dbft.png create mode 100644 formal-models/.github/dbft2.1_centralizedCV.drawio create mode 100644 formal-models/.github/dbft2.1_centralizedCV.png create mode 100644 formal-models/.github/dbft2.1_threeStagedCV.drawio create mode 100644 formal-models/.github/dbft2.1_threeStagedCV.png create mode 100644 formal-models/.github/dbft_antiMEV.drawio create mode 100644 formal-models/.github/dbft_antiMEV.png create mode 100644 formal-models/README.md create mode 100644 formal-models/dbft/dbft.tla create mode 100644 formal-models/dbft/dbft___AllGoodModel.launch create mode 100644 formal-models/dbft2.1_centralizedCV/dbftCentralizedCV.tla create mode 100644 formal-models/dbft2.1_centralizedCV/dbftCentralizedCV___AllGoodModel.launch create mode 100644 formal-models/dbft2.1_threeStagedCV/dbftCV3.tla create mode 100644 formal-models/dbft2.1_threeStagedCV/dbftCV3___AllGoodModel.launch create mode 100644 formal-models/dbftMultipool/dbftMultipool.tla create mode 100644 formal-models/dbftMultipool/dbftMultipool___AllGoodModel.launch create mode 100644 formal-models/dbft_antiMEV/dbft.tla create mode 100644 formal-models/dbft_antiMEV/dbft___AllGoodModel.launch create mode 100644 go.mod create mode 100644 go.sum create mode 100644 helpers.go create mode 100644 helpers_test.go create mode 100644 identity.go create mode 100644 internal/consensus/amev_block.go create mode 100644 internal/consensus/amev_commit.go create mode 100644 internal/consensus/amev_preBlock.go create mode 100644 internal/consensus/amev_preCommit.go create mode 100644 internal/consensus/block.go create mode 100644 internal/consensus/block_test.go create mode 100644 internal/consensus/change_view.go create mode 100644 internal/consensus/commit.go create mode 100644 internal/consensus/compact.go create mode 100644 internal/consensus/consensus.go create mode 100644 internal/consensus/consensus_message.go create mode 100644 internal/consensus/constructors.go create mode 100644 internal/consensus/helpers.go create mode 100644 internal/consensus/message.go create mode 100644 internal/consensus/message_test.go create mode 100644 internal/consensus/prepare_request.go create mode 100644 internal/consensus/prepare_response.go create mode 100644 internal/consensus/recovery_message.go create mode 100644 internal/consensus/recovery_request.go create mode 100644 internal/consensus/transaction.go create mode 100644 internal/crypto/crypto.go create mode 100644 internal/crypto/crypto_test.go create mode 100644 internal/crypto/ecdsa.go create mode 100644 internal/crypto/ecdsa_test.go create mode 100644 internal/crypto/hash.go create mode 100644 internal/crypto/hash_test.go create mode 100644 internal/merkle/merkle_tree.go create mode 100644 internal/merkle/merkle_tree_test.go create mode 100644 internal/simulation/main.go create mode 100644 pre_block.go create mode 100644 pre_commit.go create mode 100644 prepare_request.go create mode 100644 prepare_response.go create mode 100644 recovery_message.go create mode 100644 recovery_request.go create mode 100644 rtt.go create mode 100644 send.go create mode 100644 timer.go create mode 100644 timer/timer.go create mode 100644 timer/timer_test.go create mode 100644 transaction.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..50e5f83 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @AnnaShaleva @roman-khimov diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b50dccd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,44 @@ +name: Build + +on: + pull_request: + branches: + - master + types: [opened, synchronize] + paths-ignore: + - 'scripts/**' + - '**/*.md' + push: + # Build for the master branch. + branches: + - master + release: + # Publish released commit as Docker `latest` and `git_revision` images. + types: + - published + workflow_dispatch: + inputs: + ref: + description: 'Ref to build dBFT [default: latest master; examples: v0.1.0, 0a4ff9d3e4a9ab432fd5812eb18c98e03b5a7432]' + required: false + default: '' + +jobs: + run: + name: Run simulation + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: true + + - name: Run simulation + run: | + cd ./internal/simulation + go run main.go diff --git a/.github/workflows/changelog_update.yml b/.github/workflows/changelog_update.yml new file mode 100644 index 0000000..ec5b107 --- /dev/null +++ b/.github/workflows/changelog_update.yml @@ -0,0 +1,33 @@ +name: CHANGELOG check + +on: + pull_request: + branches: + - master + paths-ignore: + - '**/*.md' + - '**/*.yml' + - '.github/workflows/**' + - 'formal-models/**' + +jobs: + check: + name: Check for CHANGELOG updates + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get changed CHANGELOG + id: changelog-diff + uses: tj-actions/changed-files@v46 + with: + files: CHANGELOG.md + + - name: Fail if changelog not updated + if: steps.changelog-diff.outputs.any_changed == 'false' + uses: actions/github-script@v7 + with: + script: | + core.setFailed('CHANGELOG.md has not been updated') diff --git a/.github/workflows/dco.yml b/.github/workflows/dco.yml new file mode 100644 index 0000000..1a69a58 --- /dev/null +++ b/.github/workflows/dco.yml @@ -0,0 +1,10 @@ +name: DCO check + +on: + pull_request: + branches: + - master + +jobs: + dco: + uses: nspcc-dev/.github/.github/workflows/dco.yml@master diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..40a6782 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,98 @@ +name: Go +on: + push: + branches: [ master ] + pull_request: + branches: + - master + types: [opened, synchronize] + paths-ignore: + - '**/*.md' + workflow_dispatch: + +jobs: + lint: + name: Lint + uses: nspcc-dev/.github/.github/workflows/go-linter.yml@master + + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + go: [ '1.24', '1.25'] + os: [ubuntu-latest, windows-latest, macos-latest] + exclude: + # Only latest Go version for Windows and MacOS. + - os: windows-latest + go: '1.24' + - os: macos-latest + go: '1.24' + # Exclude latest Go version for Ubuntu as Coverage uses it. + - os: ubuntu-latest + go: '1.25' + steps: + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + + - name: Check out code into the Go module directory + uses: actions/checkout@v4 + + - name: Tests + run: go test -race ./... + + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: 1.25 + + - name: Check out + uses: actions/checkout@v4 + + - name: Collect coverage + run: go test -coverprofile=coverage.txt -covermode=atomic ./... + + - name: Upload coverage results to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + files: ./coverage.txt + slug: nspcc-dev/dbft + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + + codeql: + name: CodeQL + runs-on: ubuntu-slim + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9d9f0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor +.golangci.yml + +# TLC Model Checker files +formal-models/*/*.toolbox/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..50a8717 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,147 @@ +# Changelog + +This document outlines major changes between releases. + +## [Unreleased] + +New features: + +Behaviour changes: + +Improvements: + * minimum required Go version is 1.24 (#144) + +Bugs fixed: + +## [0.4.0] (17 July 2025) + +This release contains two major changes. First one introduces an ability to +change block generation time every block. This change is triggered by the +transfer of `TimePerBlock` setting to native Policy contract on N3 protocol +which makes this value variable throughout the network lifetime. The second +change allows to vary block generation time from `TimePerBlock` (when there are +some transactions in the network and hence, it's beneficial to accept block as +soon as possible) to `MaxTimePerBlock` (when there are no transactions, but +consensus still needs to take care of the network heartbeat). This change is +beneficial for custom networks with small `TimePerBlock` values to prevent the +chain size explosion. Also, this release contains Go version upgrade to 1.23. + +New features: + * `MaxTimePerBlock` and `SubscribeForTxs` configuration parameters are added + to support dynamic block time extension (#150) + +Behaviour changes: + * `SecondsPerBlock` config parameter is replaced with `TimePerBlock` function (#147) + +Improvements: + * minimum required Go version is 1.23 now (#145) + +## [0.3.2] (30 January 2025) + +Important dBFT timer adjustments are included into this patch-release. The first one +is the reference time point for dBFT timer which is moved to the moment of +PrepareRequest receiving. Another one is evaluated network roundtrip time which is now +taken into account every time on dBFT timer reset. These adjustments lead to the fact +that actual block producing time is extremely close to the configuration value. Other +than that, a couple of minor bug fixes are included. + +Improvements: + * timer adjustment for most of the consensus time, more accurate block + intervals (#56) + * timer adjustment for network roundtrip time (#140) + +Bugs fixed: + * inappropriate log on attempt to construct Commit for anti-MEV enabled WatchOnly + (#139) + * empty PreCommit/Commit can be relayed (#142) + +## [0.3.1] (29 November 2024) + +This patch version mostly includes a set of library API extensions made to fit the +needs of developing MEV-resistant blockchain node. Also, this release bumps minimum +required Go version up to 1.22 and contains a set of bug fixes critical for the +library functioning. + +Minor user-side code adjustments are required to adapt new ProcessBlock callback +signature, whereas the rest of APIs stay compatible with the old implementation. +This version also includes a simplification of PrivateKey interface which may be +adopted by removing extra wrappers around PrivateKey implementation on the user code +side. + +Behaviour changes: + * adjust behaviour of ProcessPreBlock callback (#129) + * (*DBFT).Header() and (*DBFT).PreHeader() are moved to (*Context) receiver (#133) + * support error handling for ProcessBlock callback if anti-MEV extension is enabled + (#134) + * remove Sign method from PrivateKey interface (#137) + +Improvements: + * minimum required Go version is 1.22 (#122, #126) + * log Commit signature verification error (#134) + * add Commit message verification callback (#134) + +Bugs fixed: + * context-bound PreBlock and PreHeader are not reset properly (#127) + * PreHeader is constructed instead of PreBlock to create PreCommit message (#128) + * enable anti-MEV extension with respect to the current block index (#132) + * (*Context).PreBlock() method returns PreHeader instead of PreBlock (#133) + * WatchOnly node may send RecoveryMessage on RecoveryRequest (#135) + * invalid PreCommit message is not removed from cache (#134) + +## [0.3.0] (01 August 2024) + +New features: + * TLA+ model for MEV-resistant dBFT extension (#116) + * support for additional phase of MEV-resistant dBFT (#118) + +Behaviour changes: + * simplify PublicKey interface (#114) + * remove WithKeyPair callback from dBFT (#114) + +## [0.2.0] (01 April 2024) + +We're rolling out an update for dBFT that contains a substantial library interface +refactoring. Starting from this version dBFT is shipped as a generic package with +a wide range of generic interfaces, callbacks and parameters. No default payload +implementations are supplied anymore, the library itself works only with payload +interfaces, and thus users are expected to implement the minimum required set of +payload interfaces by themselves. A lot of outdated and unused APIs were removed, +some of the internal APIs were renamed, so that the resulting library interface +is much more clear and lightweight. Also, the minimum required Go version was +upgraded to Go 1.20. + +Please note that no consensus-level behaviour changes introduced, this release +focuses only on the library APIs improvement, so it shouldn't be hard for the users +to migrate to the new interface. + +Behaviour changes: + * add generic Hash/Address parameters to `DBFT` service (#94) + * remove custom payloads implementation from default `DBFT` service configuration + (#94) + * rename `InitializeConsensus` dBFT method to `Reset` (#95) + * drop outdated dBFT `Service` interface (#95) + * move all default implementations to `internal` package (#97) + * remove unused APIs of dBFT and payload interfaces (#104) + * timer interface refactoring (#105) + * constructor returns some meaningful error on failed dBFT instance creation (#107) + +Improvements: + * add MIT License (#78, #79) + * documentation updates (#80, #86, #95) + * dependencies upgrades (#82, #85) + * minimum required Go version upgrade to Go 1.19 (#83) + * log messages adjustment (#88) + * untie `dbft` module from `github.com/nspcc-dev/neo-go` dependency (#94) + * minimum required Go version upgrade to Go 1.20 (#100) + +## [0.1.0] (15 May 2023) + +Stable dbft 2.0 implementation. + +[Unreleased]: https://github.com/nspcc-dev/dbft/compare/v0.3.2...master +[0.4.0]: https://github.com/nspcc-dev/dbft/releases/v0.4.0 +[0.3.2]: https://github.com/nspcc-dev/dbft/releases/v0.3.2 +[0.3.1]: https://github.com/nspcc-dev/dbft/releases/v0.3.1 +[0.3.0]: https://github.com/nspcc-dev/dbft/releases/v0.3.0 +[0.2.0]: https://github.com/nspcc-dev/dbft/releases/v0.2.0 +[0.1.0]: https://github.com/nspcc-dev/dbft/releases/v0.1.0 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c508ed6 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +MIT License + +Copyright (c) 2018-2023 NeoSPCC (@nspcc-dev) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c78b49b --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Tutus Consensus + +Go implementation of the dBFT 2.0 (delegated Byzantine Fault Tolerant) consensus algorithm for the Tutus blockchain. + +## Overview + +This library provides the core consensus mechanism for Tutus blockchain nodes, enabling deterministic finality with 1-second block times. + +## Features + +- **Immediate Finality** - No forks, no rollbacks +- **Byzantine Fault Tolerant** - Tolerates up to f = (n-1)/3 faulty nodes +- **Configurable Validators** - Support for dynamic validator sets +- **Formal Verification** - TLA+ specifications in formal-models/ + +## Installation + +```go +import "github.com/tutus-one/tutus-consensus" +``` + +## Design + +1. **Interface-Driven** - Cryptography, hashing, and block types are provided via interfaces +2. **Block & Transaction Abstractions** - Located in block.go and transaction.go +3. **Timer Abstraction** - timer package provides production-ready timer implementation +4. **Callback Architecture** - Event-driven design for integration flexibility + +## Usage + +Implement your event loop and call the provided callbacks: + +- `Start()` - Initialize consensus +- `Reset()` - Reinitialize for new height +- `OnTransaction()` - Process proposed transactions +- `OnReceive()` - Handle incoming payloads +- `OnTimer()` - Handle timer events + +See internal/simulation/main.go for a complete 6-node example. + +## Documentation + +- [dBFT Research Paper](https://github.com/NeoResearch/yellowpaper/blob/master/releases/08_dBFT.pdf) + +## License + +MIT License - see [LICENSE.md](LICENSE.md) + +--- + +Part of the [Tutus](https://github.com/tutus-one/tutus-chain) blockchain infrastructure. diff --git a/block.go b/block.go new file mode 100644 index 0000000..4705673 --- /dev/null +++ b/block.go @@ -0,0 +1,30 @@ +package dbft + +// Block is a generic interface for a block used by dbft. +type Block[H Hash] interface { + // Hash returns block hash. + Hash() H + // PrevHash returns previous block hash. + PrevHash() H + // MerkleRoot returns a merkle root of the transaction hashes. + MerkleRoot() H + // Index returns block index. + Index() uint32 + + // Signature returns block's signature. + Signature() []byte + // Sign signs block and sets it's signature. + Sign(key PrivateKey) error + // Verify checks if signature is correct. + Verify(key PublicKey, sign []byte) error + + // Transactions returns block's transaction list. + Transactions() []Transaction[H] + // SetTransactions sets block's transaction list. For anti-MEV extension + // transactions provided via this call are taken directly from PreBlock level + // and thus, may be out-of-date. Thus, with anti-MEV extension enabled it's + // suggested to use this method as a Block finalizer since it will be called + // right before the block approval. Do not rely on this with anti-MEV extension + // disabled. + SetTransactions([]Transaction[H]) +} diff --git a/change_view.go b/change_view.go new file mode 100644 index 0000000..5152c63 --- /dev/null +++ b/change_view.go @@ -0,0 +1,10 @@ +package dbft + +// ChangeView represents dBFT ChangeView message. +type ChangeView interface { + // NewViewNumber returns proposed view number. + NewViewNumber() byte + + // Reason returns change view reason. + Reason() ChangeViewReason +} diff --git a/change_view_reason.go b/change_view_reason.go new file mode 100644 index 0000000..4895420 --- /dev/null +++ b/change_view_reason.go @@ -0,0 +1,18 @@ +package dbft + +//go:generate stringer -type=ChangeViewReason -linecomment + +// ChangeViewReason represents a view change reason code. +type ChangeViewReason byte + +// These constants define various reasons for view changing. They're following +// Neo 3 except the Unknown value which is left for compatibility with Neo 2. +const ( + CVTimeout ChangeViewReason = 0x0 // Timeout + CVChangeAgreement ChangeViewReason = 0x1 // ChangeAgreement + CVTxNotFound ChangeViewReason = 0x2 // TxNotFound + CVTxRejectedByPolicy ChangeViewReason = 0x3 // TxRejectedByPolicy + CVTxInvalid ChangeViewReason = 0x4 // TxInvalid + CVBlockRejectedByPolicy ChangeViewReason = 0x5 // BlockRejectedByPolicy + CVUnknown ChangeViewReason = 0xff // Unknown +) diff --git a/change_view_reason_string.go b/change_view_reason_string.go new file mode 100644 index 0000000..0921ea7 --- /dev/null +++ b/change_view_reason_string.go @@ -0,0 +1,38 @@ +// Code generated by "stringer -type=ChangeViewReason -linecomment"; DO NOT EDIT. + +package dbft + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[CVTimeout-0] + _ = x[CVChangeAgreement-1] + _ = x[CVTxNotFound-2] + _ = x[CVTxRejectedByPolicy-3] + _ = x[CVTxInvalid-4] + _ = x[CVBlockRejectedByPolicy-5] + _ = x[CVUnknown-255] +} + +const ( + _ChangeViewReason_name_0 = "TimeoutChangeAgreementTxNotFoundTxRejectedByPolicyTxInvalidBlockRejectedByPolicy" + _ChangeViewReason_name_1 = "Unknown" +) + +var ( + _ChangeViewReason_index_0 = [...]uint8{0, 7, 22, 32, 50, 59, 80} +) + +func (i ChangeViewReason) String() string { + switch { + case 0 <= i && i <= 5: + return _ChangeViewReason_name_0[_ChangeViewReason_index_0[i]:_ChangeViewReason_index_0[i+1]] + case i == 255: + return _ChangeViewReason_name_1 + default: + return "ChangeViewReason(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/check.go b/check.go new file mode 100644 index 0000000..6f8aea8 --- /dev/null +++ b/check.go @@ -0,0 +1,180 @@ +package dbft + +import ( + "go.uber.org/zap" +) + +func (d *DBFT[H]) checkPrepare() { + if d.lastBlockIndex != d.BlockIndex || d.lastBlockView != d.ViewNumber { + // Notice that lastBlockTimestamp is left unchanged because + // this must be the value from the last header. + d.lastBlockTime = d.Timer.Now() + d.lastBlockIndex = d.BlockIndex + d.lastBlockView = d.ViewNumber + } + if !d.hasAllTransactions() { + d.Logger.Debug("check prepare: some transactions are missing", zap.Any("hashes", d.MissingTransactions)) + return + } + + count := 0 + hasRequest := false + + for _, msg := range d.PreparationPayloads { + if msg != nil { + if msg.ViewNumber() == d.ViewNumber { + count++ + } + + if msg.Type() == PrepareRequestType { + hasRequest = true + } + } + } + + d.Logger.Debug("check preparations", zap.Bool("hasReq", hasRequest), + zap.Int("count", count), + zap.Int("M", d.M())) + + if hasRequest && count >= d.M() { + if d.isAntiMEVExtensionEnabled() { + d.sendPreCommit() + d.changeTimer(d.timePerBlock) + d.checkPreCommit() + } else { + d.sendCommit() + d.changeTimer(d.timePerBlock) + d.checkCommit() + } + } +} + +func (d *DBFT[H]) checkPreCommit() { + if !d.hasAllTransactions() { + d.Logger.Debug("check preCommit: some transactions are missing", zap.Any("hashes", d.MissingTransactions)) + return + } + + count := 0 + for _, msg := range d.PreCommitPayloads { + if msg != nil && msg.ViewNumber() == d.ViewNumber { + count++ + } + } + + if count < d.M() { + d.Logger.Debug("not enough PreCommits to process PreBlock", zap.Int("count", count)) + return + } + + d.preBlock = d.CreatePreBlock() + + if !d.preBlockProcessed { + d.Logger.Info("processing PreBlock", + zap.Uint32("height", d.BlockIndex), + zap.Uint("view", uint(d.ViewNumber)), + zap.Int("tx_count", len(d.preBlock.Transactions())), + zap.Int("preCommit_count", count)) + + err := d.ProcessPreBlock(d.preBlock) + if err != nil { + d.Logger.Info("can't process PreBlock, waiting for more PreCommits to be collected", + zap.Error(err), + zap.Int("count", count)) + return + } + d.preBlockProcessed = true + } + + // Require PreCommit sent by self for reliability. This condition must not be + // removed because: + // 1) we need to filter out WatchOnly nodes; + // 2) CNs that have not sent PreCommit must not skip this stage (although it's OK + // from the DKG/TPKE side to build final Block based only on other CN's data). + if d.PreCommitSent() { + d.verifyCommitPayloadsAgainstHeader() + d.sendCommit() + d.changeTimer(d.timePerBlock) + d.checkCommit() + } else { + if !d.Context.WatchOnly() { + d.Logger.Debug("can't send commit since self preCommit not yet sent") + } + } +} + +func (d *DBFT[H]) checkCommit() { + if !d.hasAllTransactions() { + d.Logger.Debug("check commit: some transactions are missing", zap.Any("hashes", d.MissingTransactions)) + return + } + + // return if we received commits from other nodes + // before receiving PrepareRequest from Speaker + count := 0 + + for _, msg := range d.CommitPayloads { + if msg != nil && msg.ViewNumber() == d.ViewNumber { + count++ + } + } + + if count < d.M() { + d.Logger.Debug("not enough to commit", zap.Int("count", count)) + return + } + + d.block = d.CreateBlock() + hash := d.block.Hash() + + d.Logger.Info("approving block", + zap.Uint32("height", d.BlockIndex), + zap.Stringer("hash", hash), + zap.Int("tx_count", len(d.block.Transactions())), + zap.Stringer("merkle", d.block.MerkleRoot()), + zap.Stringer("prev", d.block.PrevHash())) + + err := d.ProcessBlock(d.block) + if err != nil { + if d.isAntiMEVExtensionEnabled() { + d.Logger.Info("can't process Block, waiting for more Commits to be collected", + zap.Error(err), + zap.Int("count", count)) + return + } + d.Logger.Fatal("block processing failed", zap.Error(err)) + } + + d.blockProcessed = true + + // Do not initialize consensus process immediately. It's the caller's duty to + // start the new block acceptance process and call Reset at the + // new height. +} + +func (d *DBFT[H]) checkChangeView(view byte) { + if d.ViewNumber >= view { + return + } + + count := 0 + + for _, msg := range d.ChangeViewPayloads { + if msg != nil && msg.GetChangeView().NewViewNumber() >= view { + count++ + } + } + + if count < d.M() { + return + } + + if !d.Context.WatchOnly() { + msg := d.ChangeViewPayloads[d.MyIndex] + if msg != nil && msg.GetChangeView().NewViewNumber() < view { + d.broadcast(d.makeChangeView(uint64(d.Timer.Now().UnixNano()), CVChangeAgreement)) + } + } + + d.initializeConsensus(view, d.lastBlockTimestamp) +} diff --git a/commit.go b/commit.go new file mode 100644 index 0000000..1b14004 --- /dev/null +++ b/commit.go @@ -0,0 +1,9 @@ +package dbft + +// Commit is an interface for dBFT Commit message. +type Commit interface { + // Signature returns commit's signature field + // which is a final block signature for the current epoch for both dBFT 2.0 and + // for anti-MEV extension. + Signature() []byte +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..11aedb6 --- /dev/null +++ b/config.go @@ -0,0 +1,465 @@ +package dbft + +import ( + "errors" + "time" + + "go.uber.org/zap" +) + +// Config contains initialization and working parameters for dBFT. +type Config[H Hash] struct { + // Logger + Logger *zap.Logger + // Timer + Timer Timer + // TimePerBlock is the minimum time that needs to pass before another block + // will be accepted even if there are pending transactions in the node's + // mempool. This value may be updated every block. + TimePerBlock func() time.Duration + // MaxTimePerBlock is the maximum time that may pass before another block is + // accepted if there are no pending transactions in the node's mempool. This + // value may be updated every block. If set, enables dynamic block time + // extension: blocks are accepted with interval from TimePerBlock to + // MaxTimePerBlock (in CV-less scenario) depending on the presence of + // transactions in the node's pool, ref. + // https://github.com/neo-project/neo/issues/4018. + MaxTimePerBlock func() time.Duration + // TimestampIncrement increment is the amount of units to add to timestamp + // if current time is less than that of previous context. + // By default use millisecond precision. + TimestampIncrement uint64 + // AntiMEVExtensionEnablingHeight denotes the height starting from which dBFT + // Anti-MEV extensions should be enabled. -1 means no extension is enabled. + AntiMEVExtensionEnablingHeight int64 + // GetKeyPair returns an index of the node in the list of validators + // together with it's key pair. + GetKeyPair func([]PublicKey) (int, PrivateKey, PublicKey) + // NewPreBlockFromContext should allocate, fill from Context and return new block.PreBlock. + NewPreBlockFromContext func(ctx *Context[H]) PreBlock[H] + // NewBlockFromContext should allocate, fill from Context and return new block.Block. + NewBlockFromContext func(ctx *Context[H]) Block[H] + // RequestTx is a callback which is called when transaction contained + // in current block can't be found in memory pool. The slice received by + // this callback MUST NOT be changed. + RequestTx func(h ...H) + // SubscribeForTxs is a callback which is called when dBFT needs to track incoming + // mempool transactions. Subscription is supposed to be single-use, no unsubscription + // is initiated by dBFT, hence it's the user's duty to manage and release resources. + // This callback is active iff MaxTimePerBlock is set. + SubscribeForTxs func() + // StopTxFlow is a callback which is called when the process no longer needs + // any transactions. + StopTxFlow func() + // GetTx returns a transaction from memory pool. + GetTx func(h H) Transaction[H] + // GetVerified returns a slice of verified transactions + // to be proposed in a new block. + GetVerified func() []Transaction[H] + // VerifyPreBlock verifies if preBlock is valid. + VerifyPreBlock func(b PreBlock[H]) bool + // VerifyBlock verifies if block is valid. + VerifyBlock func(b Block[H]) bool + // Broadcast should broadcast payload m to the consensus nodes. + Broadcast func(m ConsensusPayload[H]) + // ProcessBlock is called every time new preBlock is accepted. + ProcessPreBlock func(b PreBlock[H]) error + // ProcessBlock is called every time new block is accepted. + ProcessBlock func(b Block[H]) error + // GetBlock should return block with hash. + GetBlock func(h H) Block[H] + // WatchOnly tells if a node should only watch. + WatchOnly func() bool + // CurrentHeight returns index of the last accepted block. + CurrentHeight func() uint32 + // CurrentBlockHash returns hash of the last accepted block. + CurrentBlockHash func() H + // GetValidators returns list of the validators. + // When called with a transaction list it must return + // list of the validators of the next block. + // If this function ever returns 0-length slice, dbft will panic. + GetValidators func(...Transaction[H]) []PublicKey + // NewConsensusPayload is a constructor for payload.ConsensusPayload. + NewConsensusPayload func(*Context[H], MessageType, any) ConsensusPayload[H] + // NewPrepareRequest is a constructor for payload.PrepareRequest. + NewPrepareRequest func(ts uint64, nonce uint64, transactionHashes []H) PrepareRequest[H] + // NewPrepareResponse is a constructor for payload.PrepareResponse. + NewPrepareResponse func(preparationHash H) PrepareResponse[H] + // NewChangeView is a constructor for payload.ChangeView. + NewChangeView func(newViewNumber byte, reason ChangeViewReason, timestamp uint64) ChangeView + // NewPreCommit is a constructor for payload.PreCommit. + NewPreCommit func(data []byte) PreCommit + // NewCommit is a constructor for payload.Commit. + NewCommit func(signature []byte) Commit + // NewRecoveryRequest is a constructor for payload.RecoveryRequest. + NewRecoveryRequest func(ts uint64) RecoveryRequest + // NewRecoveryMessage is a constructor for payload.RecoveryMessage. + NewRecoveryMessage func() RecoveryMessage[H] + // VerifyPrepareRequest can perform external payload verification and returns true iff it was successful. + VerifyPrepareRequest func(p ConsensusPayload[H]) error + // VerifyPrepareResponse performs external PrepareResponse verification and returns nil if it's successful. + VerifyPrepareResponse func(p ConsensusPayload[H]) error + // VerifyPreCommit performs external PreCommit verification and returns nil if it's successful. + // Note that PreBlock-dependent PreCommit verification should be performed inside PreBlock.Verify + // callback. + VerifyPreCommit func(p ConsensusPayload[H]) error + // VerifyCommit performs external Commit verification and returns nil if it's successful. + // Note that Block-dependent Commit verification should be performed inside Block.Verify + // callback. + VerifyCommit func(p ConsensusPayload[H]) error +} + +const defaultSecondsPerBlock = time.Second * 15 + +const defaultTimestampIncrement = uint64(time.Millisecond / time.Nanosecond) + +func defaultConfig[H Hash]() *Config[H] { + // fields which are set to nil must be provided from client + return &Config[H]{ + Logger: zap.NewNop(), + TimePerBlock: func() time.Duration { return defaultSecondsPerBlock }, + TimestampIncrement: defaultTimestampIncrement, + GetKeyPair: nil, + RequestTx: func(...H) {}, + StopTxFlow: func() {}, + GetTx: func(H) Transaction[H] { return nil }, + GetVerified: func() []Transaction[H] { return make([]Transaction[H], 0) }, + VerifyBlock: func(Block[H]) bool { return true }, + Broadcast: func(ConsensusPayload[H]) {}, + ProcessBlock: func(Block[H]) error { return nil }, + GetBlock: func(H) Block[H] { return nil }, + WatchOnly: func() bool { return false }, + CurrentHeight: nil, + CurrentBlockHash: nil, + GetValidators: nil, + + VerifyPrepareRequest: func(ConsensusPayload[H]) error { return nil }, + VerifyPrepareResponse: func(ConsensusPayload[H]) error { return nil }, + VerifyCommit: func(ConsensusPayload[H]) error { return nil }, + + AntiMEVExtensionEnablingHeight: -1, + VerifyPreBlock: func(PreBlock[H]) bool { return true }, + VerifyPreCommit: func(ConsensusPayload[H]) error { return nil }, + } +} + +func checkConfig[H Hash](cfg *Config[H]) error { + if cfg.GetKeyPair == nil { + return errors.New("private key is nil") + } + if cfg.Timer == nil { + return errors.New("Timer is nil") + } + if cfg.CurrentHeight == nil { + return errors.New("CurrentHeight is nil") + } + if cfg.CurrentBlockHash == nil { + return errors.New("CurrentBlockHash is nil") + } + if cfg.GetValidators == nil { + return errors.New("GetValidators is nil") + } + if cfg.NewBlockFromContext == nil { + return errors.New("NewBlockFromContext is nil") + } + if cfg.NewConsensusPayload == nil { + return errors.New("NewConsensusPayload is nil") + } + if cfg.NewPrepareRequest == nil { + return errors.New("NewPrepareRequest is nil") + } + if cfg.NewPrepareResponse == nil { + return errors.New("NewPrepareResponse is nil") + } + if cfg.NewChangeView == nil { + return errors.New("NewChangeView is nil") + } + if cfg.NewCommit == nil { + return errors.New("NewCommit is nil") + } + if cfg.NewRecoveryRequest == nil { + return errors.New("NewRecoveryRequest is nil") + } + if cfg.NewRecoveryMessage == nil { + return errors.New("NewRecoveryMessage is nil") + } + if cfg.AntiMEVExtensionEnablingHeight >= 0 { + if cfg.NewPreBlockFromContext == nil { + return errors.New("NewPreBlockFromContext is nil") + } + if cfg.ProcessPreBlock == nil { + return errors.New("ProcessPreBlock is nil") + } + if cfg.NewPreCommit == nil { + return errors.New("NewPreCommit is nil") + } + } else { + if cfg.NewPreBlockFromContext != nil { + return errors.New("NewPreBlockFromContext is set, but AntiMEVExtensionEnablingHeight is not specified") + } + if cfg.ProcessPreBlock != nil { + return errors.New("ProcessPreBlock is set, but AntiMEVExtensionEnablingHeight is not specified") + } + if cfg.NewPreCommit != nil { + return errors.New("NewPreCommit is set, but AntiMEVExtensionEnablingHeight is not specified") + } + } + if (cfg.MaxTimePerBlock == nil) != (cfg.SubscribeForTxs == nil) { + return errors.New("MaxTimePerBlock and SubscribeForTxs should be specified/not specified at the same time") + } + + return nil +} + +// WithGetKeyPair sets GetKeyPair. +func WithGetKeyPair[H Hash](f func(pubs []PublicKey) (int, PrivateKey, PublicKey)) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.GetKeyPair = f + } +} + +// WithLogger sets Logger. +func WithLogger[H Hash](log *zap.Logger) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.Logger = log + } +} + +// WithTimer sets Timer. +func WithTimer[H Hash](t Timer) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.Timer = t + } +} + +// WithTimePerBlock sets TimePerBlock. +func WithTimePerBlock[H Hash](f func() time.Duration) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.TimePerBlock = f + } +} + +// WithMaxTimePerBlock sets MaxTimePerBlock. +func WithMaxTimePerBlock[H Hash](f func() time.Duration) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.MaxTimePerBlock = f + } +} + +// WithAntiMEVExtensionEnablingHeight sets AntiMEVExtensionEnablingHeight. +func WithAntiMEVExtensionEnablingHeight[H Hash](h int64) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.AntiMEVExtensionEnablingHeight = h + } +} + +// WithTimestampIncrement sets TimestampIncrement. +func WithTimestampIncrement[H Hash](u uint64) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.TimestampIncrement = u + } +} + +// WithNewPreBlockFromContext sets NewPreBlockFromContext. +func WithNewPreBlockFromContext[H Hash](f func(ctx *Context[H]) PreBlock[H]) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewPreBlockFromContext = f + } +} + +// WithNewBlockFromContext sets NewBlockFromContext. +func WithNewBlockFromContext[H Hash](f func(ctx *Context[H]) Block[H]) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewBlockFromContext = f + } +} + +// WithRequestTx sets RequestTx. +func WithRequestTx[H Hash](f func(h ...H)) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.RequestTx = f + } +} + +// WithSubscribeForTxs sets SubscribeForTxs. +func WithSubscribeForTxs[H Hash](f func()) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.SubscribeForTxs = f + } +} + +// WithStopTxFlow sets StopTxFlow. +func WithStopTxFlow[H Hash](f func()) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.StopTxFlow = f + } +} + +// WithGetTx sets GetTx. +func WithGetTx[H Hash](f func(h H) Transaction[H]) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.GetTx = f + } +} + +// WithGetVerified sets GetVerified. +func WithGetVerified[H Hash](f func() []Transaction[H]) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.GetVerified = f + } +} + +// WithVerifyPreBlock sets VerifyPreBlock. +func WithVerifyPreBlock[H Hash](f func(b PreBlock[H]) bool) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.VerifyPreBlock = f + } +} + +// WithVerifyBlock sets VerifyBlock. +func WithVerifyBlock[H Hash](f func(b Block[H]) bool) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.VerifyBlock = f + } +} + +// WithBroadcast sets Broadcast. +func WithBroadcast[H Hash](f func(m ConsensusPayload[H])) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.Broadcast = f + } +} + +// WithProcessBlock sets ProcessBlock callback. Note that for anti-MEV extension +// disabled non-nil error return is a no-op. +func WithProcessBlock[H Hash](f func(b Block[H]) error) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.ProcessBlock = f + } +} + +// WithProcessPreBlock sets ProcessPreBlock. +func WithProcessPreBlock[H Hash](f func(b PreBlock[H]) error) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.ProcessPreBlock = f + } +} + +// WithGetBlock sets GetBlock. +func WithGetBlock[H Hash](f func(h H) Block[H]) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.GetBlock = f + } +} + +// WithWatchOnly sets WatchOnly. +func WithWatchOnly[H Hash](f func() bool) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.WatchOnly = f + } +} + +// WithCurrentHeight sets CurrentHeight. +func WithCurrentHeight[H Hash](f func() uint32) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.CurrentHeight = f + } +} + +// WithCurrentBlockHash sets CurrentBlockHash. +func WithCurrentBlockHash[H Hash](f func() H) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.CurrentBlockHash = f + } +} + +// WithGetValidators sets GetValidators. +func WithGetValidators[H Hash](f func(txs ...Transaction[H]) []PublicKey) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.GetValidators = f + } +} + +// WithNewConsensusPayload sets NewConsensusPayload. +func WithNewConsensusPayload[H Hash](f func(ctx *Context[H], typ MessageType, msg any) ConsensusPayload[H]) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewConsensusPayload = f + } +} + +// WithNewPrepareRequest sets NewPrepareRequest. +func WithNewPrepareRequest[H Hash](f func(ts uint64, nonce uint64, transactionsHashes []H) PrepareRequest[H]) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewPrepareRequest = f + } +} + +// WithNewPrepareResponse sets NewPrepareResponse. +func WithNewPrepareResponse[H Hash](f func(preparationHash H) PrepareResponse[H]) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewPrepareResponse = f + } +} + +// WithNewChangeView sets NewChangeView. +func WithNewChangeView[H Hash](f func(newViewNumber byte, reason ChangeViewReason, ts uint64) ChangeView) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewChangeView = f + } +} + +// WithNewCommit sets NewCommit. +func WithNewCommit[H Hash](f func(signature []byte) Commit) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewCommit = f + } +} + +// WithNewPreCommit sets NewPreCommit. +func WithNewPreCommit[H Hash](f func(signature []byte) PreCommit) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewPreCommit = f + } +} + +// WithNewRecoveryRequest sets NewRecoveryRequest. +func WithNewRecoveryRequest[H Hash](f func(ts uint64) RecoveryRequest) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewRecoveryRequest = f + } +} + +// WithNewRecoveryMessage sets NewRecoveryMessage. +func WithNewRecoveryMessage[H Hash](f func() RecoveryMessage[H]) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewRecoveryMessage = f + } +} + +// WithVerifyPrepareRequest sets VerifyPrepareRequest. +func WithVerifyPrepareRequest[H Hash](f func(prepareReq ConsensusPayload[H]) error) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.VerifyPrepareRequest = f + } +} + +// WithVerifyPrepareResponse sets VerifyPrepareResponse. +func WithVerifyPrepareResponse[H Hash](f func(prepareResp ConsensusPayload[H]) error) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.VerifyPrepareResponse = f + } +} + +// WithVerifyPreCommit sets VerifyPreCommit. +func WithVerifyPreCommit[H Hash](f func(preCommit ConsensusPayload[H]) error) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.VerifyPreCommit = f + } +} + +// WithVerifyCommit sets VerifyCommit. +func WithVerifyCommit[H Hash](f func(commit ConsensusPayload[H]) error) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.VerifyCommit = f + } +} diff --git a/consensus_message.go b/consensus_message.go new file mode 100644 index 0000000..01c9d2b --- /dev/null +++ b/consensus_message.go @@ -0,0 +1,26 @@ +package dbft + +// ConsensusMessage is an interface for generic dBFT message. +type ConsensusMessage[H Hash] interface { + // ViewNumber returns view number when this message was originated. + ViewNumber() byte + // Type returns type of this message. + Type() MessageType + // Payload returns this message's actual payload. + Payload() any + + // GetChangeView returns payload as if it was ChangeView. + GetChangeView() ChangeView + // GetPrepareRequest returns payload as if it was PrepareRequest. + GetPrepareRequest() PrepareRequest[H] + // GetPrepareResponse returns payload as if it was PrepareResponse. + GetPrepareResponse() PrepareResponse[H] + // GetPreCommit returns payload as if it was PreCommit. + GetPreCommit() PreCommit + // GetCommit returns payload as if it was Commit. + GetCommit() Commit + // GetRecoveryRequest returns payload as if it was RecoveryRequest. + GetRecoveryRequest() RecoveryRequest + // GetRecoveryMessage returns payload as if it was RecoveryMessage. + GetRecoveryMessage() RecoveryMessage[H] +} diff --git a/consensus_message_type.go b/consensus_message_type.go new file mode 100644 index 0000000..f7a4f53 --- /dev/null +++ b/consensus_message_type.go @@ -0,0 +1,39 @@ +package dbft + +import "fmt" + +// MessageType is a type for dBFT consensus messages. +type MessageType byte + +// 7 following constants enumerate all possible type of consensus message. +const ( + ChangeViewType MessageType = 0x00 + PrepareRequestType MessageType = 0x20 + PrepareResponseType MessageType = 0x21 + PreCommitType MessageType = 0x31 + CommitType MessageType = 0x30 + RecoveryRequestType MessageType = 0x40 + RecoveryMessageType MessageType = 0x41 +) + +// String implements fmt.Stringer interface. +func (m MessageType) String() string { + switch m { + case ChangeViewType: + return "ChangeView" + case PrepareRequestType: + return "PrepareRequest" + case PrepareResponseType: + return "PrepareResponse" + case CommitType: + return "Commit" + case PreCommitType: + return "PreCommit" + case RecoveryRequestType: + return "RecoveryRequest" + case RecoveryMessageType: + return "RecoveryMessage" + default: + return fmt.Sprintf("UNKNOWN(%02x)", byte(m)) + } +} diff --git a/consensus_payload.go b/consensus_payload.go new file mode 100644 index 0000000..c24699e --- /dev/null +++ b/consensus_payload.go @@ -0,0 +1,19 @@ +package dbft + +// ConsensusPayload is a generic payload type which is exchanged +// between the nodes. +type ConsensusPayload[H Hash] interface { + ConsensusMessage[H] + + // ValidatorIndex returns index of validator from which + // payload was originated from. + ValidatorIndex() uint16 + + // SetValidatorIndex sets validator index. + SetValidatorIndex(i uint16) + + Height() uint32 + + // Hash returns 32-byte checksum of the payload. + Hash() H +} diff --git a/context.go b/context.go new file mode 100644 index 0000000..2891844 --- /dev/null +++ b/context.go @@ -0,0 +1,445 @@ +package dbft + +import ( + "crypto/rand" + "encoding/binary" + "time" +) + +// HeightView is a block height/consensus view pair. +type HeightView struct { + Height uint32 + View byte +} + +// Context is a main dBFT structure which +// contains all information needed for performing transitions. +type Context[H Hash] struct { + // Config is dBFT's Config instance. + Config *Config[H] + + // Priv is node's private key. + Priv PrivateKey + // Pub is node's public key. + Pub PublicKey + + preBlock PreBlock[H] + preHeader PreBlock[H] + block Block[H] + header Block[H] + // blockProcessed denotes whether Config.ProcessBlock callback was called for the current + // height. If so, then no second call must happen. After new block is received by the user, + // dBFT stops any new transaction or messages processing as far as timeouts handling till + // the next call to Reset. + blockProcessed bool + // preBlockProcessed is true when Config.ProcessPreBlock callback was + // invoked for the current height. This happens once and dbft continues + // to march towards proper commit after that. + preBlockProcessed bool + + // BlockIndex is current block index. + BlockIndex uint32 + // ViewNumber is current view number. + ViewNumber byte + // Validators is a current validator list. + Validators []PublicKey + // MyIndex is an index of the current node in the Validators array. + // It is equal to -1 if node is not a validator or is WatchOnly. + MyIndex int + // PrimaryIndex is an index of the primary node in the current epoch. + PrimaryIndex uint + + // PrevHash is a hash of the previous block. + PrevHash H + + // Timestamp is a nanosecond-precision timestamp + Timestamp uint64 + Nonce uint64 + // TransactionHashes is a slice of hashes of proposed transactions in the current block. + TransactionHashes []H + // MissingTransactions is a slice of hashes containing missing transactions for the current block. + MissingTransactions []H + // Transactions is a map containing actual transactions for the current block. + Transactions map[H]Transaction[H] + + // PreparationPayloads stores consensus Prepare* payloads for the current epoch. + PreparationPayloads []ConsensusPayload[H] + // PreCommitPayloads stores consensus PreCommit payloads sent through all epochs + // as a part of anti-MEV dBFT extension. It is assumed that valid PreCommit + // payloads can only be sent once by a single node per the whole set of consensus + // epochs for particular block. Invalid PreCommit payloads are kicked off this + // list immediately (if PrepareRequest was received for the current round, so + // it's possible to verify PreCommit against PreBlock built on PrepareRequest) + // or stored till the corresponding PrepareRequest receiving. + PreCommitPayloads []ConsensusPayload[H] + // CommitPayloads stores consensus Commit payloads sent throughout all epochs. It + // is assumed that valid Commit payload can only be sent once by a single node per + // the whole set of consensus epochs for particular block. Invalid commit payloads + // are kicked off this list immediately (if PrepareRequest was received for the + // current round, so it's possible to verify Commit against it) or stored till + // the corresponding PrepareRequest receiving. + CommitPayloads []ConsensusPayload[H] + // ChangeViewPayloads stores consensus ChangeView payloads for the current epoch. + ChangeViewPayloads []ConsensusPayload[H] + // LastChangeViewPayloads stores consensus ChangeView payloads for the last epoch. + LastChangeViewPayloads []ConsensusPayload[H] + // LastSeenMessage array stores the height and view of the last seen message, for each validator. + // If this node never heard a thing from validator i, LastSeenMessage[i] will be nil. + LastSeenMessage []*HeightView + + lastBlockTimestamp uint64 // ns-precision timestamp from the last header (used for the next block timestamp calculations). + lastBlockTime time.Time // Wall clock time of when we started (as in PrepareRequest) creating the last block (used for timer adjustments). + lastBlockIndex uint32 + lastBlockView byte + timePerBlock time.Duration // minimum amount of time that need to pass before the pending block will be accepted if there are some transactions in the proposal. + maxTimePerBlock time.Duration // maximum amount of time that allowed to pass before the pending block will be accepted even if there's no transactions in the proposal. + txSubscriptionOn bool + + prepareSentTime time.Time + rttEstimates rtt +} + +// N returns total number of validators. +func (c *Context[H]) N() int { return len(c.Validators) } + +// F returns number of validators which can be faulty. +func (c *Context[H]) F() int { return (len(c.Validators) - 1) / 3 } + +// M returns number of validators which must function correctly. +func (c *Context[H]) M() int { return len(c.Validators) - c.F() } + +// GetPrimaryIndex returns index of a primary node for the specified view. +func (c *Context[H]) GetPrimaryIndex(viewNumber byte) uint { + p := (int(c.BlockIndex) - int(viewNumber)) % len(c.Validators) + if p >= 0 { + return uint(p) + } + + return uint(p + len(c.Validators)) +} + +// IsPrimary returns true iff node is primary for current height and view. +func (c *Context[H]) IsPrimary() bool { return c.MyIndex == int(c.PrimaryIndex) } + +// IsBackup returns true iff node is backup for current height and view. +func (c *Context[H]) IsBackup() bool { + return c.MyIndex >= 0 && !c.IsPrimary() +} + +// WatchOnly returns true iff node takes no active part in consensus. +func (c *Context[H]) WatchOnly() bool { return c.MyIndex < 0 || c.Config.WatchOnly() } + +// CountCommitted returns number of received Commit (or PreCommit for anti-MEV +// extension) messages not only for the current epoch but also for any other epoch. +func (c *Context[H]) CountCommitted() (count int) { + for i := range c.CommitPayloads { + // Consider both Commit and PreCommit payloads since both Commit and PreCommit + // phases are one-directional (do not impose view change). + if c.CommitPayloads[i] != nil || c.PreCommitPayloads[i] != nil { + count++ + } + } + + return +} + +// CountFailed returns number of nodes with which no communication was performed +// for this view and that hasn't sent the Commit message at the previous views. +func (c *Context[H]) CountFailed() (count int) { + for i, hv := range c.LastSeenMessage { + if (c.CommitPayloads[i] == nil && c.PreCommitPayloads[i] == nil) && + (hv == nil || hv.Height < c.BlockIndex || hv.View < c.ViewNumber) { + count++ + } + } + + return +} + +// RequestSentOrReceived returns true iff PrepareRequest +// was sent or received for the current epoch. +func (c *Context[H]) RequestSentOrReceived() bool { + return c.PreparationPayloads[c.PrimaryIndex] != nil +} + +// ResponseSent returns true iff Prepare* message was sent for the current epoch. +func (c *Context[H]) ResponseSent() bool { + return !c.WatchOnly() && c.PreparationPayloads[c.MyIndex] != nil +} + +// PreCommitSent returns true iff PreCommit message was sent for the current epoch +// assuming that the node can't go further than current epoch after PreCommit was sent. +func (c *Context[H]) PreCommitSent() bool { + return !c.WatchOnly() && c.PreCommitPayloads[c.MyIndex] != nil +} + +// CommitSent returns true iff Commit message was sent for the current epoch +// assuming that the node can't go further than current epoch after commit was sent. +func (c *Context[H]) CommitSent() bool { + return !c.WatchOnly() && c.CommitPayloads[c.MyIndex] != nil +} + +// BlockSent returns true iff block was formed AND sent for the current height. +// Once block is sent, the consensus stops new transactions and messages processing +// as far as timeouts handling. +// +// Implementation note: the implementation of BlockSent differs from the C#'s one. +// In C# algorithm they use ConsensusContext's Block.Transactions null check to define +// whether block was formed, and the only place where the block can be formed is +// in the ConsensusContext's CreateBlock function right after enough Commits receiving. +// On the contrary, in our implementation we don't have access to the block's +// Transactions field as far as we can't use block null check, because there are +// several places where the call to CreateBlock happens (one of them is right after +// PrepareRequest receiving). Thus, we have a separate Context.blockProcessed field +// for the described purpose. +func (c *Context[H]) BlockSent() bool { return c.blockProcessed } + +// ViewChanging returns true iff node is in a process of changing view. +func (c *Context[H]) ViewChanging() bool { + if c.WatchOnly() { + return false + } + + cv := c.ChangeViewPayloads[c.MyIndex] + + return cv != nil && cv.GetChangeView().NewViewNumber() > c.ViewNumber +} + +// NotAcceptingPayloadsDueToViewChanging returns true if node should not accept new payloads. +func (c *Context[H]) NotAcceptingPayloadsDueToViewChanging() bool { + return c.ViewChanging() && !c.MoreThanFNodesCommittedOrLost() +} + +// MoreThanFNodesCommittedOrLost returns true iff a number of nodes which either committed +// or are faulty is more than maximum amount of allowed faulty nodes. +// A possible attack can happen if the last node to commit is malicious and either sends change view after his +// commit to stall nodes in a higher view, or if he refuses to send recovery messages. In addition, if a node +// asking change views loses network or crashes and comes back when nodes are committed in more than one higher +// numbered view, it is possible for the node accepting recovery to commit in any of the higher views, thus +// potentially splitting nodes among views and stalling the network. +func (c *Context[H]) MoreThanFNodesCommittedOrLost() bool { + return c.CountCommitted()+c.CountFailed() > c.F() +} + +// Header returns current header from context. May be nil in case if no +// header is constructed yet. Do not change the resulting header. +func (c *Context[H]) Header() Block[H] { + return c.header +} + +// PreHeader returns current preHeader from context. May be nil in case if no +// preHeader is constructed yet. Do not change the resulting preHeader. +func (c *Context[H]) PreHeader() PreBlock[H] { + return c.preHeader +} + +// PreBlock returns current PreBlock from context. May be nil in case if no +// PreBlock is constructed yet (even if PreHeader is already constructed). +// External changes in the PreBlock will be seen by dBFT. +func (c *Context[H]) PreBlock() PreBlock[H] { + return c.preBlock +} + +func (c *Context[H]) reset(view byte, ts uint64) { + c.MyIndex = -1 + c.prepareSentTime = time.Time{} + c.lastBlockTimestamp = ts + c.unsubscribeFromTransactions() + + if view == 0 { + c.PrevHash = c.Config.CurrentBlockHash() + c.BlockIndex = c.Config.CurrentHeight() + 1 + c.Validators = c.Config.GetValidators() + c.timePerBlock = c.Config.TimePerBlock() + if c.Config.MaxTimePerBlock != nil { + c.maxTimePerBlock = c.Config.MaxTimePerBlock() + } + + n := len(c.Validators) + c.LastChangeViewPayloads = emptyReusableSlice(c.LastChangeViewPayloads, n) + + c.LastSeenMessage = emptyReusableSlice(c.LastSeenMessage, n) + c.blockProcessed = false + c.preBlockProcessed = false + } else { + for i := range c.Validators { + m := c.ChangeViewPayloads[i] + if m != nil && m.GetChangeView().NewViewNumber() >= view { + c.LastChangeViewPayloads[i] = m + } else { + c.LastChangeViewPayloads[i] = nil + } + } + } + + c.MyIndex, c.Priv, c.Pub = c.Config.GetKeyPair(c.Validators) + + c.block = nil + c.preBlock = nil + c.header = nil + c.preHeader = nil + + n := len(c.Validators) + c.ChangeViewPayloads = emptyReusableSlice(c.ChangeViewPayloads, n) + if view == 0 { + c.PreCommitPayloads = emptyReusableSlice(c.PreCommitPayloads, n) + c.CommitPayloads = emptyReusableSlice(c.CommitPayloads, n) + } + c.PreparationPayloads = emptyReusableSlice(c.PreparationPayloads, n) + + if c.Transactions == nil { // Init. + c.Transactions = make(map[H]Transaction[H]) + } else { // Regular use. + clear(c.Transactions) + } + c.TransactionHashes = nil + if c.MissingTransactions != nil { + c.MissingTransactions = c.MissingTransactions[:0] + } + c.PrimaryIndex = c.GetPrimaryIndex(view) + c.ViewNumber = view + + if c.MyIndex >= 0 { + c.LastSeenMessage[c.MyIndex] = &HeightView{c.BlockIndex, c.ViewNumber} + } +} + +func emptyReusableSlice[E any](s []E, n int) []E { + if len(s) == n { + clear(s) + return s + } + return make([]E, n) +} + +// Fill initializes consensus when node is a speaker. It doesn't perform any +// context modifications if MaxTimePerBlock extension is enabled and there are +// no transactions in the memory pool and force is not set. +func (c *Context[H]) Fill(force bool) bool { + txx := c.Config.GetVerified() + if c.Config.MaxTimePerBlock != nil && !force && len(txx) == 0 { + return false + } + + b := make([]byte, 8) + _, _ = rand.Read(b) + + c.Nonce = binary.LittleEndian.Uint64(b) + c.TransactionHashes = make([]H, len(txx)) + + for i := range txx { + h := txx[i].Hash() + c.TransactionHashes[i] = h + c.Transactions[h] = txx[i] + } + + c.Timestamp = c.lastBlockTimestamp + c.Config.TimestampIncrement + if now := c.getTimestamp(); now > c.Timestamp { + c.Timestamp = now + } + return true +} + +// getTimestamp returns nanoseconds-precision timestamp using +// current context config. +func (c *Context[H]) getTimestamp() uint64 { + return uint64(c.Config.Timer.Now().UnixNano()) / c.Config.TimestampIncrement * c.Config.TimestampIncrement +} + +// CreateBlock returns resulting block for the current epoch. +func (c *Context[H]) CreateBlock() Block[H] { + if c.block == nil { + if c.block = c.MakeHeader(); c.block == nil { + return nil + } + + txx := make([]Transaction[H], len(c.TransactionHashes)) + + for i, h := range c.TransactionHashes { + txx[i] = c.Transactions[h] + } + + // Anti-MEV extension properly sets PreBlock transactions once during PreBlock + // construction and then never updates these transactions in the dBFT context. + // Thus, user must not reuse txx if anti-MEV extension is enabled. However, + // we don't skip a call to Block.SetTransactions since it may be used as a + // signal to the user's code to finalize the block. + c.block.SetTransactions(txx) + } + + return c.block +} + +// CreatePreBlock returns PreBlock for the current epoch. +func (c *Context[H]) CreatePreBlock() PreBlock[H] { + if c.preBlock == nil { + if c.preBlock = c.MakePreHeader(); c.preBlock == nil { + return nil + } + + txx := make([]Transaction[H], len(c.TransactionHashes)) + + for i, h := range c.TransactionHashes { + txx[i] = c.Transactions[h] + } + + c.preBlock.SetTransactions(txx) + } + + return c.preBlock +} + +// isAntiMEVExtensionEnabled returns whether Anti-MEV dBFT extension is enabled +// at the currently processing block height. +func (c *Context[H]) isAntiMEVExtensionEnabled() bool { + return c.Config.AntiMEVExtensionEnablingHeight >= 0 && uint32(c.Config.AntiMEVExtensionEnablingHeight) <= c.BlockIndex +} + +// MakeHeader returns half-filled block for the current epoch. +// All hashable fields will be filled. +func (c *Context[H]) MakeHeader() Block[H] { + if c.header == nil { + if !c.RequestSentOrReceived() { + return nil + } + // For anti-MEV dBFT extension it's important to have PreBlock processed and + // all envelopes decrypted, because a single PrepareRequest is not enough to + // construct proper Block. + if c.isAntiMEVExtensionEnabled() { + if !c.preBlockProcessed { + return nil + } + } + c.header = c.Config.NewBlockFromContext(c) + } + + return c.header +} + +// MakePreHeader returns half-filled block for the current epoch. +// All hashable fields will be filled. +func (c *Context[H]) MakePreHeader() PreBlock[H] { + if c.preHeader == nil { + if !c.RequestSentOrReceived() { + return nil + } + c.preHeader = c.Config.NewPreBlockFromContext(c) + } + + return c.preHeader +} + +// hasAllTransactions returns true iff all transactions were received +// for the proposed block. +func (c *Context[H]) hasAllTransactions() bool { + return len(c.TransactionHashes) == len(c.Transactions) +} + +func (c *Context[H]) subscribeForTransactions() { + c.txSubscriptionOn = true + c.Config.SubscribeForTxs() +} + +func (c *Context[H]) unsubscribeFromTransactions() { + c.txSubscriptionOn = false +} diff --git a/dbft.go b/dbft.go new file mode 100644 index 0000000..c97c016 --- /dev/null +++ b/dbft.go @@ -0,0 +1,751 @@ +package dbft + +import ( + "fmt" + "slices" + "sync" + "time" + + "go.uber.org/zap" +) + +type ( + // DBFT is a dBFT implementation, it includes [Context] (main state) + // and [Config] (service configuration). Data exposed from these fields + // is supposed to be read-only, state is changed via methods of this + // structure. + DBFT[H Hash] struct { + Context[H] + Config[H] + + *sync.Mutex + cache cache[H] + recovering bool + } +) + +// New returns new DBFT instance with specified H and A generic parameters +// using provided options or nil and error if some of the options are missing or invalid. +// H and A generic parameters are used as hash and address representation for +// dBFT consensus messages, blocks and transactions. +func New[H Hash](options ...func(config *Config[H])) (*DBFT[H], error) { + cfg := defaultConfig[H]() + + for _, option := range options { + option(cfg) + } + + if err := checkConfig(cfg); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + d := &DBFT[H]{ + Mutex: new(sync.Mutex), + Config: *cfg, + Context: Context[H]{ + Config: cfg, + }, + } + + return d, nil +} + +func (d *DBFT[H]) addTransaction(tx Transaction[H]) { + d.Transactions[tx.Hash()] = tx + if d.hasAllTransactions() { + if d.IsPrimary() || d.Context.WatchOnly() { + return + } + + if !d.createAndCheckBlock() { + return + } + + d.verifyPreCommitPayloadsAgainstPreBlock() + + d.extendTimer(2) + d.sendPrepareResponse() + d.checkPrepare() + } +} + +// Start initializes dBFT instance and starts the protocol if node is primary. +// It accepts the timestamp of the previous block. It should be called once +// per DBFT lifetime. +func (d *DBFT[H]) Start(ts uint64) { + d.cache = newCache[H]() + d.initializeConsensus(0, ts) + if d.IsPrimary() { + d.sendPrepareRequest(true) + } +} + +// Reset reinitializes dBFT instance with the given timestamp of the previous +// block. It's used if the current consensus state is outdated which happens +// after new block is processed by ledger (the block can come from dBFT or be +// received by other means). The height is to be derived from the configured +// CurrentHeight callback and view will be set to 0. +func (d *DBFT[H]) Reset(ts uint64) { + d.initializeConsensus(0, ts) +} + +func (d *DBFT[H]) initializeConsensus(view byte, ts uint64) { + d.reset(view, ts) + + var role string + + switch { + case d.IsPrimary(): + role = "Primary" + case d.Context.WatchOnly(): + role = "WatchOnly" + default: + role = "Backup" + } + + var logMsg = "initializing dbft" + if view > 0 { + logMsg = "changing dbft view" + } + + d.StopTxFlow() + d.Logger.Info(logMsg, + zap.Uint32("height", d.BlockIndex), + zap.Uint("view", uint(view)), + zap.Int("index", d.MyIndex), + zap.String("role", role)) + + // Process cached messages if any. + if msgs := d.cache.getHeight(d.BlockIndex); msgs != nil { + for _, m := range msgs.prepare { + d.OnReceive(m) + } + + for _, m := range msgs.chViews { + d.OnReceive(m) + } + + for _, m := range msgs.preCommit { + d.OnReceive(m) + } + + for _, m := range msgs.commit { + d.OnReceive(m) + } + } + + if d.Context.WatchOnly() { + return + } + + var timeout time.Duration + if d.IsPrimary() && !d.recovering { + // Initializing to view 0 means we have just persisted previous block or are starting consensus first time. + // In both cases we should wait full timeout value. + // Having non-zero view means we have to start immediately. + if view == 0 { + timeout = d.timePerBlock + } + } else { + timeout = d.timePerBlock << (d.ViewNumber + 1) + } + if d.lastBlockIndex+1 == d.BlockIndex { + var ts = d.Timer.Now() + var diff = ts.Sub(d.lastBlockTime) + timeout -= diff + timeout -= d.rttEstimates.avg / 2 + timeout = max(0, timeout) + } + d.changeTimer(timeout) +} + +// OnTransaction notifies service about receiving new transaction from the +// proposed list of transactions. +func (d *DBFT[H]) OnTransaction(tx Transaction[H]) { + // d.Logger.Debug("OnTransaction", + // zap.Bool("backup", d.IsBackup()), + // zap.Bool("not_accepting", d.NotAcceptingPayloadsDueToViewChanging()), + // zap.Bool("request_ok", d.RequestSentOrReceived()), + // zap.Bool("response_sent", d.ResponseSent()), + // zap.Bool("block_sent", d.BlockSent())) + if !d.IsBackup() || d.NotAcceptingPayloadsDueToViewChanging() || + !d.RequestSentOrReceived() || d.ResponseSent() || d.PreCommitSent() || + d.CommitSent() || d.BlockSent() || len(d.MissingTransactions) == 0 { + return + } + + i := slices.Index(d.MissingTransactions, tx.Hash()) + if i < 0 { + return + } + d.addTransaction(tx) + // `addTransaction` checks for responses and commits. If this was the last transaction + // Context could be initialized on a new height, clearing this field. + if len(d.MissingTransactions) == 0 { + return + } + d.MissingTransactions = slices.Delete(d.MissingTransactions, i, i+1) +} + +// OnTimeout advances state machine as if timeout was fired. +func (d *DBFT[H]) OnTimeout(height uint32, view byte) { + d.onTimeout(height, view, false) +} + +// OnNewTransaction advances state machine if transactions subscription is active +// and there's a new transaction added to the node pool. +func (d *DBFT[H]) OnNewTransaction() { + if !d.txSubscriptionOn { + return + } + d.onTimeout(d.Timer.Height(), d.Timer.View(), true) +} + +func (d *DBFT[H]) onTimeout(height uint32, view byte, force bool) { + if d.Context.WatchOnly() || d.BlockSent() { + return + } + + if height != d.BlockIndex || view != d.ViewNumber { + d.Logger.Debug("timeout: ignore old timer", + zap.Uint32("height", height), + zap.Uint("view", uint(view))) + + return + } + + d.Logger.Debug("timeout", + zap.Uint32("height", height), + zap.Uint("view", uint(view))) + + if d.IsPrimary() && !d.RequestSentOrReceived() { + d.sendPrepareRequest(d.ViewNumber != 0 || d.txSubscriptionOn || force) + } else if (d.IsPrimary() && d.RequestSentOrReceived()) || d.IsBackup() { + if d.CommitSent() || d.PreCommitSent() { + d.Logger.Debug("send recovery to resend commit") + d.sendRecoveryMessage() + d.changeTimer(d.timePerBlock << 1) + } else { + if d.ViewNumber == 0 && d.MaxTimePerBlock != nil && d.IsBackup() { + if force { + delay := d.timePerBlock << 1 + d.changeTimer(delay) + d.unsubscribeFromTransactions() + return + } + if !d.txSubscriptionOn && len(d.GetVerified()) == 0 { + d.subscribeForTransactions() + delay := d.maxTimePerBlock<<1 - d.timePerBlock<<1 + d.changeTimer(delay) + return + } + } + d.sendChangeView(CVTimeout) + } + } +} + +// OnReceive advances state machine in accordance with msg. +func (d *DBFT[H]) OnReceive(msg ConsensusPayload[H]) { + if int(msg.ValidatorIndex()) >= len(d.Validators) { + d.Logger.Error("too big validator index", zap.Uint16("from", msg.ValidatorIndex())) + return + } + + if msg.Payload() == nil { + d.Logger.DPanic("invalid message") + return + } + + d.Logger.Debug("received message", + zap.Stringer("type", msg.Type()), + zap.Uint16("from", msg.ValidatorIndex()), + zap.Uint32("height", msg.Height()), + zap.Uint("view", uint(msg.ViewNumber())), + zap.Uint32("my_height", d.BlockIndex), + zap.Uint("my_view", uint(d.ViewNumber))) + + if msg.Height() < d.BlockIndex { + d.Logger.Debug("ignoring old height", zap.Uint32("height", msg.Height())) + return + } else if msg.Height() > d.BlockIndex || + (msg.ViewNumber() > d.ViewNumber && + msg.Type() != ChangeViewType && + msg.Type() != RecoveryMessageType) { + d.Logger.Debug("caching message from future", + zap.Uint32("height", msg.Height()), + zap.Uint("view", uint(msg.ViewNumber())), + zap.Any("cache", d.cache.mail[msg.Height()])) + d.cache.addMessage(msg) + return + } + + hv := d.LastSeenMessage[msg.ValidatorIndex()] + if hv == nil || hv.Height < msg.Height() || hv.View < msg.ViewNumber() { + d.LastSeenMessage[msg.ValidatorIndex()] = &HeightView{msg.Height(), msg.ViewNumber()} + } + + if d.BlockSent() && msg.Type() != RecoveryRequestType { + // We've already collected the block, only recovery request must be handled. + return + } + + switch msg.Type() { + case ChangeViewType: + d.onChangeView(msg) + case PrepareRequestType: + d.onPrepareRequest(msg) + case PrepareResponseType: + d.onPrepareResponse(msg) + case CommitType: + d.onCommit(msg) + case PreCommitType: + if !d.isAntiMEVExtensionEnabled() { + d.Logger.Error(fmt.Sprintf("%s message received but AntiMEVExtension is disabled", PreCommitType), + zap.Uint16("from", msg.ValidatorIndex()), + ) + return + } + d.onPreCommit(msg) + case RecoveryRequestType: + d.onRecoveryRequest(msg) + case RecoveryMessageType: + d.onRecoveryMessage(msg) + default: + d.Logger.DPanic("wrong message type") + } +} + +func (d *DBFT[H]) onPrepareRequest(msg ConsensusPayload[H]) { + // ignore prepareRequest if we had already received it or + // are in process of changing view + if d.RequestSentOrReceived() { // || (d.ViewChanging() && !d.MoreThanFNodesCommittedOrLost()) { + d.Logger.Debug("ignoring PrepareRequest", + zap.Bool("sor", d.RequestSentOrReceived()), + zap.Bool("viewChanging", d.ViewChanging()), + zap.Bool("moreThanF", d.MoreThanFNodesCommittedOrLost())) + + return + } + + if d.ViewNumber != msg.ViewNumber() { + d.Logger.Debug("ignoring wrong view number", zap.Uint("view", uint(msg.ViewNumber()))) + return + } else if uint(msg.ValidatorIndex()) != d.GetPrimaryIndex(d.ViewNumber) { + d.Logger.Info("ignoring PrepareRequest from wrong node", zap.Uint16("from", msg.ValidatorIndex())) + return + } + + if err := d.VerifyPrepareRequest(msg); err != nil { + // We should change view if we receive signed PrepareRequest from the expected validator but it is invalid. + d.Logger.Warn("invalid PrepareRequest", zap.Uint16("from", msg.ValidatorIndex()), zap.String("error", err.Error())) + d.sendChangeView(CVBlockRejectedByPolicy) + return + } + + d.extendTimer(2) + + p := msg.GetPrepareRequest() + + d.Timestamp = p.Timestamp() + d.Nonce = p.Nonce() + d.TransactionHashes = p.TransactionHashes() + + d.Logger.Info("received PrepareRequest", zap.Uint16("validator", msg.ValidatorIndex()), zap.Int("tx", len(d.TransactionHashes))) + d.processMissingTx() + d.updateExistingPayloads(msg) + d.PreparationPayloads[msg.ValidatorIndex()] = msg + + if !d.hasAllTransactions() || !d.createAndCheckBlock() || d.Context.WatchOnly() { + return + } + + d.sendPrepareResponse() + d.checkPrepare() +} + +func (d *DBFT[H]) processMissingTx() { + for _, h := range d.TransactionHashes { + if _, ok := d.Transactions[h]; ok { + continue + } + if tx := d.GetTx(h); tx == nil { + d.MissingTransactions = append(d.MissingTransactions, h) + } else { + d.Transactions[h] = tx + } + } + + if len(d.MissingTransactions) != 0 { + d.Logger.Info("missing tx", + zap.Int("count", len(d.MissingTransactions))) + d.RequestTx(d.MissingTransactions...) + } +} + +// createAndCheckBlock is a prepareRequest-level helper that creates and checks +// the new proposed block, if it's fine it returns true, if something is wrong +// with it, it sends a changeView request and returns false. It's only valid to +// call it when all transactions for this block are already collected. +func (d *DBFT[H]) createAndCheckBlock() bool { + var blockOK bool + if d.isAntiMEVExtensionEnabled() { + b := d.CreatePreBlock() + blockOK = d.VerifyPreBlock(b) + if !blockOK { + d.Logger.Warn("proposed preBlock fails verification") + } + } else { + b := d.CreateBlock() + blockOK = d.VerifyBlock(b) + if !blockOK { + d.Logger.Warn("proposed block fails verification") + } + } + if !blockOK { + d.sendChangeView(CVTxInvalid) + return false + } + return true +} + +// updateExistingPayloads is called _only_ from onPrepareRequest, it validates +// payloads we may have received before PrepareRequest. +func (d *DBFT[H]) updateExistingPayloads(msg ConsensusPayload[H]) { + for i, m := range d.PreparationPayloads { + if m != nil && m.Type() == PrepareResponseType { + resp := m.GetPrepareResponse() + if resp != nil && resp.PreparationHash() != msg.Hash() { + d.PreparationPayloads[i] = nil + } + } + } + + if d.isAntiMEVExtensionEnabled() { + d.verifyPreCommitPayloadsAgainstPreBlock() + // Commits can't be verified, we have no idea what's the header. + } else { + d.verifyCommitPayloadsAgainstHeader() + } +} + +// verifyPreCommitPayloadsAgainstPreBlock performs verification of PreCommit payloads +// against generated PreBlock. +func (d *DBFT[H]) verifyPreCommitPayloadsAgainstPreBlock() { + if !d.hasAllTransactions() { + return + } + for i, m := range d.PreCommitPayloads { + if m != nil && m.ViewNumber() == d.ViewNumber { + if preBlock := d.CreatePreBlock(); preBlock != nil { + pub := d.Validators[m.ValidatorIndex()] + if err := preBlock.Verify(pub, m.GetPreCommit().Data()); err != nil { + d.PreCommitPayloads[i] = nil + d.Logger.Warn("PreCommit verification failed", + zap.Uint16("from", m.ValidatorIndex()), + zap.Error(err)) + } + } + } + } +} + +// verifyCommitPayloadsAgainstHeader performs verification of commit payloads +// against generated header. +func (d *DBFT[H]) verifyCommitPayloadsAgainstHeader() { + for i, m := range d.CommitPayloads { + if m != nil && m.ViewNumber() == d.ViewNumber { + if header := d.MakeHeader(); header != nil { + pub := d.Validators[m.ValidatorIndex()] + if header.Verify(pub, m.GetCommit().Signature()) != nil { + d.CommitPayloads[i] = nil + d.Logger.Warn("can't validate commit signature") + } + } + } + } +} + +func (d *DBFT[H]) onPrepareResponse(msg ConsensusPayload[H]) { + if d.ViewNumber != msg.ViewNumber() { + d.Logger.Debug("ignoring wrong view number", zap.Uint("view", uint(msg.ViewNumber()))) + return + } else if uint(msg.ValidatorIndex()) == d.GetPrimaryIndex(d.ViewNumber) { + d.Logger.Debug("ignoring PrepareResponse from primary node", zap.Uint16("from", msg.ValidatorIndex())) + return + } + + // ignore PrepareResponse if in process of changing view + m := d.PreparationPayloads[msg.ValidatorIndex()] + if m != nil || d.ViewChanging() && !d.MoreThanFNodesCommittedOrLost() { + d.Logger.Debug("ignoring PrepareResponse", + zap.Bool("dup", m != nil), + zap.Bool("sor", d.RequestSentOrReceived()), + zap.Bool("viewChanging", d.ViewChanging()), + zap.Bool("moreThanF", d.MoreThanFNodesCommittedOrLost())) + return + } + + if err := d.VerifyPrepareResponse(msg); err != nil { + d.Logger.Warn("invalid PrepareResponse", zap.Uint16("from", msg.ValidatorIndex()), zap.String("error", err.Error())) + return + } + d.Logger.Info("received PrepareResponse", zap.Uint16("validator", msg.ValidatorIndex())) + d.PreparationPayloads[msg.ValidatorIndex()] = msg + + if m = d.PreparationPayloads[d.GetPrimaryIndex(d.ViewNumber)]; m != nil { + req := m.GetPrepareRequest() + if req == nil { + d.Logger.DPanic("unexpected nil prepare request") + return + } + + prepHash := msg.GetPrepareResponse().PreparationHash() + if h := m.Hash(); prepHash != h { + d.PreparationPayloads[msg.ValidatorIndex()] = nil + d.Logger.Debug("hash mismatch", + zap.Stringer("primary", h), + zap.Stringer("received", prepHash)) + + return + } + } + + if d.IsPrimary() && !d.prepareSentTime.IsZero() && !d.recovering { + d.rttEstimates.addTime(time.Since(d.prepareSentTime)) + } + + d.extendTimer(2) + + if !d.Context.WatchOnly() && !d.CommitSent() && (!d.isAntiMEVExtensionEnabled() || !d.PreCommitSent()) && d.RequestSentOrReceived() { + d.checkPrepare() + } +} + +func (d *DBFT[H]) onChangeView(msg ConsensusPayload[H]) { + p := msg.GetChangeView() + + if p.NewViewNumber() <= d.ViewNumber { + d.Logger.Debug("ignoring old ChangeView", zap.Uint("new_view", uint(p.NewViewNumber()))) + d.onRecoveryRequest(msg) + + return + } + + if d.CommitSent() || d.PreCommitSent() { + d.Logger.Debug("ignoring ChangeView: preCommit or commit sent") + d.sendRecoveryMessage() + return + } + + m := d.ChangeViewPayloads[msg.ValidatorIndex()] + if m != nil && p.NewViewNumber() < m.GetChangeView().NewViewNumber() { + return + } + + d.Logger.Info("received ChangeView", + zap.Uint("validator", uint(msg.ValidatorIndex())), + zap.Stringer("reason", p.Reason()), + zap.Uint("new view", uint(p.NewViewNumber())), + ) + + d.ChangeViewPayloads[msg.ValidatorIndex()] = msg + d.checkChangeView(p.NewViewNumber()) +} + +func (d *DBFT[H]) onPreCommit(msg ConsensusPayload[H]) { + existing := d.PreCommitPayloads[msg.ValidatorIndex()] + if existing != nil { + if existing.Hash() != msg.Hash() { + d.Logger.Warn("rejecting preCommit due to existing", + zap.Uint("validator", uint(msg.ValidatorIndex())), + zap.Uint("existing view", uint(existing.ViewNumber())), + zap.Uint("view", uint(msg.ViewNumber())), + zap.Stringer("existing hash", existing.Hash()), + zap.Stringer("hash", msg.Hash()), + ) + } + return + } + d.PreCommitPayloads[msg.ValidatorIndex()] = msg + if d.ViewNumber == msg.ViewNumber() { + if err := d.VerifyPreCommit(msg); err != nil { + d.PreCommitPayloads[msg.ValidatorIndex()] = nil + d.Logger.Warn("invalid PreCommit", zap.Uint16("from", msg.ValidatorIndex()), zap.String("error", err.Error())) + return + } + + d.Logger.Info("received PreCommit", zap.Uint("validator", uint(msg.ValidatorIndex()))) + d.extendTimer(4) + + if !d.hasAllTransactions() { + return + } + preBlock := d.CreatePreBlock() + if preBlock != nil { + pub := d.Validators[msg.ValidatorIndex()] + if err := preBlock.Verify(pub, msg.GetPreCommit().Data()); err == nil { + d.checkPreCommit() + } else { + d.PreCommitPayloads[msg.ValidatorIndex()] = nil + d.Logger.Warn("invalid preCommit data", + zap.Uint("validator", uint(msg.ValidatorIndex())), + zap.Error(err), + ) + } + } + return + } + + d.Logger.Info("received preCommit for different view", + zap.Uint("validator", uint(msg.ValidatorIndex())), + zap.Uint("view", uint(msg.ViewNumber())), + ) +} + +func (d *DBFT[H]) onCommit(msg ConsensusPayload[H]) { + existing := d.CommitPayloads[msg.ValidatorIndex()] + if existing != nil { + if existing.Hash() != msg.Hash() { + d.Logger.Warn("rejecting commit due to existing", + zap.Uint("validator", uint(msg.ValidatorIndex())), + zap.Uint("existing view", uint(existing.ViewNumber())), + zap.Uint("view", uint(msg.ViewNumber())), + zap.Stringer("existing hash", existing.Hash()), + zap.Stringer("hash", msg.Hash()), + ) + } + return + } + d.CommitPayloads[msg.ValidatorIndex()] = msg + if d.ViewNumber == msg.ViewNumber() { + if err := d.VerifyCommit(msg); err != nil { + d.CommitPayloads[msg.ValidatorIndex()] = nil + d.Logger.Warn("invalid Commit", zap.Uint16("from", msg.ValidatorIndex()), zap.String("error", err.Error())) + return + } + + d.Logger.Info("received Commit", zap.Uint("validator", uint(msg.ValidatorIndex()))) + d.extendTimer(4) + header := d.MakeHeader() + if header != nil { + pub := d.Validators[msg.ValidatorIndex()] + if err := header.Verify(pub, msg.GetCommit().Signature()); err == nil { + d.checkCommit() + } else { + d.CommitPayloads[msg.ValidatorIndex()] = nil + d.Logger.Warn("invalid commit signature", + zap.Uint("validator", uint(msg.ValidatorIndex())), + zap.Error(err), + ) + } + } + + return + } + + d.Logger.Info("received commit for different view", + zap.Uint("validator", uint(msg.ValidatorIndex())), + zap.Uint("view", uint(msg.ViewNumber())), + ) +} + +func (d *DBFT[H]) onRecoveryRequest(msg ConsensusPayload[H]) { + // Only validators are allowed to send consensus messages. + if d.Context.WatchOnly() { + return + } + + if !d.CommitSent() && (!d.isAntiMEVExtensionEnabled() || !d.PreCommitSent()) { + // Ignore the message if our index is not in F+1 range of the + // next (%N) ones from the sender. This limits recovery + // messages to be broadcasted through the network and F+1 + // guarantees that at least one node responds. + + if (d.MyIndex-int(msg.ValidatorIndex())+d.N()-1)%d.N() > d.F() { + return + } + } + + d.sendRecoveryMessage() +} + +func (d *DBFT[H]) onRecoveryMessage(msg ConsensusPayload[H]) { + d.Logger.Debug("recovery message received", zap.Any("dump", msg)) + + var ( + validPrepResp, validChViews int + validPreCommits, validCommits int + validPrepReq, totalPrepReq int + recovery = msg.GetRecoveryMessage() + total = len(d.Validators) + ) + + // isRecovering is always set to false again after OnRecoveryMessageReceived + d.recovering = true + + defer func() { + d.Logger.Sugar().Debugf("recovering finished cv=%d/%d preq=%d/%d presp=%d/%d pco=%d/%d co=%d/%d", + validChViews, total, + validPrepReq, totalPrepReq, + validPrepResp, total, + validPreCommits, total, + validCommits, total) + d.recovering = false + }() + + if msg.ViewNumber() > d.ViewNumber { + if d.CommitSent() || d.PreCommitSent() { + return + } + + for _, m := range recovery.GetChangeViews(msg, d.Validators) { + validChViews++ + d.OnReceive(m) + } + } + + if msg.ViewNumber() == d.ViewNumber && (!d.ViewChanging() || d.MoreThanFNodesCommittedOrLost()) && !d.CommitSent() && (!d.isAntiMEVExtensionEnabled() || !d.PreCommitSent()) { + if !d.RequestSentOrReceived() { + prepReq := recovery.GetPrepareRequest(msg, d.Validators, uint16(d.PrimaryIndex)) + if prepReq != nil { + totalPrepReq, validPrepReq = 1, 1 + d.OnReceive(prepReq) + } + // If the node is primary, then wait until timer fires to send PrepareRequest + // to avoid rush in blocks submission, #74. + } + + for _, m := range recovery.GetPrepareResponses(msg, d.Validators) { + validPrepResp++ + d.OnReceive(m) + } + } + + // Ensure we know about all (pre) commits from lower view numbers. + if msg.ViewNumber() <= d.ViewNumber { + for _, m := range recovery.GetPreCommits(msg, d.Validators) { + validPreCommits++ + d.OnReceive(m) + } + + for _, m := range recovery.GetCommits(msg, d.Validators) { + validCommits++ + d.OnReceive(m) + } + } +} + +func (d *DBFT[H]) changeTimer(delay time.Duration) { + d.Logger.Debug("reset timer", + zap.Uint32("h", d.BlockIndex), + zap.Int("v", int(d.ViewNumber)), + zap.Duration("delay", delay)) + d.Timer.Reset(d.BlockIndex, d.ViewNumber, delay) +} + +func (d *DBFT[H]) extendTimer(count int) { + if !d.CommitSent() && (!d.isAntiMEVExtensionEnabled() || !d.PreCommitSent()) && !d.ViewChanging() { + d.Timer.Extend(time.Duration(count) * d.timePerBlock / time.Duration(d.M())) + } +} diff --git a/dbft_test.go b/dbft_test.go new file mode 100644 index 0000000..76d595a --- /dev/null +++ b/dbft_test.go @@ -0,0 +1,1268 @@ +package dbft_test + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + "testing" + "time" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" + "github.com/tutus-one/tutus-consensus/timer" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type Payload = dbft.ConsensusPayload[crypto.Uint256] + +type testState struct { + myIndex int + count int + privs []dbft.PrivateKey + pubs []dbft.PublicKey + ch []Payload + currHeight uint32 + currHash crypto.Uint256 + pool *testPool + preBlocks []dbft.PreBlock[crypto.Uint256] + blocks []dbft.Block[crypto.Uint256] + verify func(b dbft.Block[crypto.Uint256]) bool +} + +type ( + testTx uint64 + testPool struct { + storage map[crypto.Uint256]testTx + } +) + +const debugTests = false + +func TestDBFT_OnStartPrimarySendPrepareRequest(t *testing.T) { + s := newTestState(2, 7) + + t.Run("backup sends nothing on start", func(t *testing.T) { + s.currHeight = 0 + service, err := dbft.New[crypto.Uint256](s.getOptions()...) + require.NoError(t, err) + + service.Start(0) + require.Nil(t, s.tryRecv()) + }) + + t.Run("primary send PrepareRequest on start", func(t *testing.T) { + s.currHeight = 1 + service, _ := dbft.New[crypto.Uint256](s.getOptions()...) + + service.Start(0) + p := s.tryRecv() + require.NotNil(t, p) + require.Equal(t, dbft.PrepareRequestType, p.Type()) + require.EqualValues(t, 2, p.Height()) + require.EqualValues(t, 0, p.ViewNumber()) + require.NotNil(t, p.Payload()) + require.EqualValues(t, 2, p.ValidatorIndex()) + + t.Run("primary send ChangeView on timeout", func(t *testing.T) { + service.OnTimeout(s.currHeight+1, 0) + + // if there are many faulty must send RecoveryRequest + cv := s.tryRecv() + require.NotNil(t, cv) + require.Equal(t, dbft.RecoveryRequestType, cv.Type()) + require.Nil(t, s.tryRecv()) + + // if all nodes are up must send ChangeView + for i := range service.LastSeenMessage { + service.LastSeenMessage[i] = &dbft.HeightView{s.currHeight + 1, 0} + } + service.OnTimeout(s.currHeight+1, 0) + + cv = s.tryRecv() + require.NotNil(t, cv) + require.Equal(t, dbft.ChangeViewType, cv.Type()) + require.EqualValues(t, 1, cv.GetChangeView().NewViewNumber()) + require.Nil(t, s.tryRecv()) + }) + }) +} + +func TestDBFT_SingleNode(t *testing.T) { + for _, amev := range []bool{false, true} { + t.Run(fmt.Sprintf("AMEV %t", amev), func(t *testing.T) { + s := newTestState(0, 1) + + s.currHeight = 2 + opts := s.getOptions() + if amev { + opts = s.getAMEVOptions() + } + service, _ := dbft.New[crypto.Uint256](opts...) + + service.Start(0) + p := s.tryRecv() + require.NotNil(t, p) + require.Equal(t, dbft.PrepareRequestType, p.Type()) + require.EqualValues(t, 3, p.Height()) + require.EqualValues(t, 0, p.ViewNumber()) + require.NotNil(t, p.Payload()) + require.EqualValues(t, 0, p.ValidatorIndex()) + + if amev { + cm := s.tryRecv() + require.NotNil(t, cm) + require.Equal(t, dbft.PreCommitType, cm.Type()) + require.EqualValues(t, s.currHeight+1, cm.Height()) + require.EqualValues(t, 0, cm.ViewNumber()) + require.NotNil(t, cm.Payload()) + require.EqualValues(t, 0, cm.ValidatorIndex()) + } + cm := s.tryRecv() + require.NotNil(t, cm) + require.Equal(t, dbft.CommitType, cm.Type()) + require.EqualValues(t, s.currHeight+1, cm.Height()) + require.EqualValues(t, 0, cm.ViewNumber()) + require.NotNil(t, cm.Payload()) + require.EqualValues(t, 0, cm.ValidatorIndex()) + + b := s.nextBlock() + require.NotNil(t, b) + require.Equal(t, s.currHeight+1, b.Index()) + }) + } +} + +func TestDBFT_OnReceiveRequestSendResponse(t *testing.T) { + s := newTestState(2, 7) + s.verify = func(b dbft.Block[crypto.Uint256]) bool { + for _, tx := range b.Transactions() { + if tx.(testTx)%10 == 0 { + return false + } + } + + return true + } + + t.Run("receive request from primary", func(t *testing.T) { + s.currHeight = 4 + service, _ := dbft.New[crypto.Uint256](s.getOptions()...) + txs := []testTx{1} + s.pool.Add(txs[0]) + + p := s.getPrepareRequest(5, txs[0].Hash()) + + service.Start(0) + service.OnReceive(p) + + resp := s.tryRecv() + require.NotNil(t, resp) + require.Equal(t, dbft.PrepareResponseType, resp.Type()) + require.EqualValues(t, s.currHeight+1, resp.Height()) + require.EqualValues(t, 0, resp.ViewNumber()) + require.EqualValues(t, s.myIndex, resp.ValidatorIndex()) + require.NotNil(t, resp.Payload()) + require.Equal(t, p.Hash(), resp.GetPrepareResponse().PreparationHash()) + + // do nothing on second receive + service.OnReceive(p) + require.Nil(t, s.tryRecv()) + + t.Run("receive response from primary", func(t *testing.T) { + resp := s.getPrepareResponse(5, p.Hash(), 0) + + service.OnReceive(resp) + require.Nil(t, s.tryRecv()) + }) + }) + + t.Run("change view on invalid tx", func(t *testing.T) { + s.currHeight = 4 + service, _ := dbft.New[crypto.Uint256](s.getOptions()...) + txs := []testTx{10} + + service.Start(0) + + for i := range service.LastSeenMessage { + service.LastSeenMessage[i] = &dbft.HeightView{s.currHeight + 1, 0} + } + + p := s.getPrepareRequest(5, txs[0].Hash()) + + service.OnReceive(p) + require.Nil(t, s.tryRecv()) + + service.OnTransaction(testTx(10)) + + cv := s.tryRecv() + require.NotNil(t, cv) + require.Equal(t, dbft.ChangeViewType, cv.Type()) + require.EqualValues(t, s.currHeight+1, cv.Height()) + require.EqualValues(t, 0, cv.ViewNumber()) + require.EqualValues(t, s.myIndex, cv.ValidatorIndex()) + require.NotNil(t, cv.Payload()) + require.EqualValues(t, 1, cv.GetChangeView().NewViewNumber()) + }) + + t.Run("receive invalid prepare request", func(t *testing.T) { + s.currHeight = 4 + service, _ := dbft.New[crypto.Uint256](s.getOptions()...) + txs := []testTx{1, 2} + s.pool.Add(txs[0]) + + service.Start(0) + + t.Run("wrong primary index", func(t *testing.T) { + p := s.getPrepareRequest(4, txs[0].Hash()) + service.OnReceive(p) + require.Nil(t, s.tryRecv()) + }) + + t.Run("old height", func(t *testing.T) { + p := s.getPrepareRequestWithHeight(5, 3, txs[0].Hash()) + service.OnReceive(p) + require.Nil(t, s.tryRecv()) + }) + + t.Run("does not have all transactions", func(t *testing.T) { + p := s.getPrepareRequest(5, txs[0].Hash(), txs[1].Hash()) + service.OnReceive(p) + require.Nil(t, s.tryRecv()) + + // do nothing with already present transaction + service.OnTransaction(txs[0]) + require.Nil(t, s.tryRecv()) + + service.OnTransaction(txs[1]) + resp := s.tryRecv() + require.NotNil(t, resp) + require.Equal(t, dbft.PrepareResponseType, resp.Type()) + require.EqualValues(t, s.currHeight+1, resp.Height()) + require.EqualValues(t, 0, resp.ViewNumber()) + require.EqualValues(t, s.myIndex, resp.ValidatorIndex()) + require.NotNil(t, resp.Payload()) + require.Equal(t, p.Hash(), resp.GetPrepareResponse().PreparationHash()) + + // do not send response twice + service.OnTransaction(txs[1]) + require.Nil(t, s.tryRecv()) + }) + }) +} + +func TestDBFT_CommitOnTransaction(t *testing.T) { + s := newTestState(0, 4) + s.currHeight = 1 + + srv, _ := dbft.New[crypto.Uint256](s.getOptions()...) + srv.Start(0) + require.Nil(t, s.tryRecv()) + + tx := testTx(42) + req := s.getPrepareRequest(2, tx.Hash()) + srv.OnReceive(req) + srv.OnReceive(s.getPrepareResponse(1, req.Hash(), 0)) + srv.OnReceive(s.getPrepareResponse(3, req.Hash(), 0)) + require.Nil(t, srv.Header()) // missing transaction. + + // Test state for forming header. + s1 := &testState{ + count: s.count, + pool: newTestPool(), + currHeight: 1, + pubs: s.pubs, + privs: s.privs, + } + s1.pool.Add(tx) + srv1, _ := dbft.New[crypto.Uint256](s1.getOptions()...) + srv1.Start(0) + srv1.OnReceive(req) + srv1.OnReceive(s1.getPrepareResponse(1, req.Hash(), 0)) + srv1.OnReceive(s1.getPrepareResponse(3, req.Hash(), 0)) + require.NotNil(t, srv1.Header()) + + for _, i := range []uint16{1, 2, 3} { + require.NoError(t, srv1.Header().Sign(s1.privs[i])) + c := s1.getCommit(i, srv1.Header().Signature(), 0) + srv.OnReceive(c) + } + + require.Nil(t, s.nextBlock()) + srv.OnTransaction(tx) + require.NotNil(t, s.nextBlock()) +} + +func TestDBFT_OnReceiveCommit(t *testing.T) { + s := newTestState(2, 4) + t.Run("send commit after enough responses", func(t *testing.T) { + s.currHeight = 1 + service, _ := dbft.New[crypto.Uint256](s.getOptions()...) + service.Start(0) + + req := s.tryRecv() + require.NotNil(t, req) + + resp := s.getPrepareResponse(1, req.Hash(), 0) + service.OnReceive(resp) + require.Nil(t, s.tryRecv()) + + resp = s.getPrepareResponse(0, req.Hash(), 0) + service.OnReceive(resp) + + cm := s.tryRecv() + require.NotNil(t, cm) + require.Equal(t, dbft.CommitType, cm.Type()) + require.EqualValues(t, s.currHeight+1, cm.Height()) + require.EqualValues(t, 0, cm.ViewNumber()) + require.EqualValues(t, s.myIndex, cm.ValidatorIndex()) + require.NotNil(t, cm.Payload()) + + pub := s.pubs[s.myIndex] + require.NoError(t, service.Header().Verify(pub, cm.GetCommit().Signature())) + + t.Run("send recovery message on timeout", func(t *testing.T) { + service.OnTimeout(1, 0) + require.Nil(t, s.tryRecv()) + + service.OnTimeout(s.currHeight+1, 0) + + r := s.tryRecv() + require.NotNil(t, r) + require.Equal(t, dbft.RecoveryMessageType, r.Type()) + }) + + t.Run("process block after enough commits", func(t *testing.T) { + s0 := s.copyWithIndex(0) + require.NoError(t, service.Header().Sign(s0.privs[0])) + c0 := s0.getCommit(0, service.Header().Signature(), 0) + service.OnReceive(c0) + require.Nil(t, s.tryRecv()) + require.Nil(t, s.nextBlock()) + + s1 := s.copyWithIndex(1) + require.NoError(t, service.Header().Sign(s1.privs[1])) + c1 := s1.getCommit(1, service.Header().Signature(), 0) + service.OnReceive(c1) + require.Nil(t, s.tryRecv()) + + b := s.nextBlock() + require.NotNil(t, b) + require.Equal(t, s.currHeight+1, b.Index()) + }) + }) +} + +func TestDBFT_OnReceiveRecoveryRequest(t *testing.T) { + s := newTestState(2, 4) + t.Run("send recovery message", func(t *testing.T) { + s.currHeight = 1 + service, _ := dbft.New[crypto.Uint256](s.getOptions()...) + service.Start(0) + + req := s.tryRecv() + require.NotNil(t, req) + + resp := s.getPrepareResponse(1, req.Hash(), 0) + service.OnReceive(resp) + require.Nil(t, s.tryRecv()) + + resp = s.getPrepareResponse(0, req.Hash(), 0) + service.OnReceive(resp) + cm := s.tryRecv() + require.NotNil(t, cm) + + rr := s.getRecoveryRequest(3) + service.OnReceive(rr) + rm := s.tryRecv() + require.NotNil(t, rm) + require.Equal(t, dbft.RecoveryMessageType, rm.Type()) + + other := s.copyWithIndex(3) + srv2, _ := dbft.New[crypto.Uint256](other.getOptions()...) + srv2.Start(0) + srv2.OnReceive(rm) + + r2 := other.tryRecv() + require.NotNil(t, r2) + require.Equal(t, dbft.PrepareResponseType, r2.Type()) + + cm2 := other.tryRecv() + require.NotNil(t, cm2) + require.Equal(t, dbft.CommitType, cm2.Type()) + pub := other.pubs[other.myIndex] + require.NoError(t, service.Header().Verify(pub, cm2.GetCommit().Signature())) + + // send commit once during recovery + require.Nil(t, s.tryRecv()) + }) +} + +func TestDBFT_OnReceiveRecoveryRequestResponds(t *testing.T) { + type recoveryset struct { + nodes int + sender int + receiver int + replies bool + } + var params []recoveryset + + for _, nodes := range []int{4, 5, 7, 10} { // 5 is a bad BFT number, but we want to test the logic anyway. + for sender := range nodes { + for recv := range nodes { + params = append(params, recoveryset{nodes, sender, recv, false}) + + for i := 1; i <= ((nodes-1)/3)+1; i++ { + ind := (sender + i) % nodes + if ind == recv { + params[len(params)-1].replies = true + break + } + } + } + } + } + + for _, param := range params { + t.Run(fmt.Sprintf("%d nodes, %d sender, %d receiver", param.nodes, param.sender, param.receiver), func(t *testing.T) { + s := newTestState(param.receiver, param.nodes) + s.currHeight = 1 + service, _ := dbft.New[crypto.Uint256](s.getOptions()...) + service.Start(uint64(param.receiver)) + + _ = s.tryRecv() // Flush the queue if primary. + + rr := s.getRecoveryRequest(uint16(param.sender)) + service.OnReceive(rr) + rm := s.tryRecv() + if param.replies { + require.NotNil(t, rm) + require.Equal(t, dbft.RecoveryMessageType, rm.Type()) + } else { + require.Nil(t, rm) + } + }) + } +} + +func TestDBFT_OnReceiveChangeView(t *testing.T) { + s := newTestState(2, 4) + t.Run("change view correctly", func(t *testing.T) { + s.currHeight = 6 + service, _ := dbft.New[crypto.Uint256](s.getOptions()...) + service.Start(0) + + resp := s.getChangeView(1, 1) + service.OnReceive(resp) + require.Nil(t, s.tryRecv()) + + resp = s.getChangeView(0, 1) + service.OnReceive(resp) + require.Nil(t, s.tryRecv()) + + service.OnTimeout(s.currHeight+1, 0) + cv := s.tryRecv() + require.NotNil(t, cv) + require.Equal(t, dbft.ChangeViewType, cv.Type()) + + t.Run("primary sends prepare request after timeout", func(t *testing.T) { + service.OnTimeout(s.currHeight+1, 1) + pr := s.tryRecv() + require.NotNil(t, pr) + require.Equal(t, dbft.PrepareRequestType, pr.Type()) + }) + }) +} + +func TestDBFT_Invalid(t *testing.T) { + t.Run("without keys", func(t *testing.T) { + _, err := dbft.New[crypto.Uint256]() + require.Error(t, err) + }) + + priv, pub := crypto.Generate(rand.Reader) + require.NotNil(t, priv) + require.NotNil(t, pub) + + opts := []func(*dbft.Config[crypto.Uint256]){dbft.WithGetKeyPair[crypto.Uint256](func(_ []dbft.PublicKey) (int, dbft.PrivateKey, dbft.PublicKey) { + return -1, nil, nil + })} + t.Run("without Timer", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithTimer[crypto.Uint256](timer.New())) + t.Run("without CurrentHeight", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithCurrentHeight[crypto.Uint256](func() uint32 { return 0 })) + t.Run("without CurrentBlockHash", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithCurrentBlockHash[crypto.Uint256](func() crypto.Uint256 { return crypto.Uint256{} })) + t.Run("without GetValidators", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithGetValidators[crypto.Uint256](func(...dbft.Transaction[crypto.Uint256]) []dbft.PublicKey { + return []dbft.PublicKey{pub} + })) + t.Run("without NewBlockFromContext", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithNewBlockFromContext[crypto.Uint256](func(_ *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { + return nil + })) + t.Run("without NewConsensusPayload", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithNewConsensusPayload[crypto.Uint256](func(_ *dbft.Context[crypto.Uint256], _ dbft.MessageType, _ any) dbft.ConsensusPayload[crypto.Uint256] { + return nil + })) + t.Run("without NewPrepareRequest", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithNewPrepareRequest[crypto.Uint256](func(uint64, uint64, []crypto.Uint256) dbft.PrepareRequest[crypto.Uint256] { + return nil + })) + t.Run("without NewPrepareResponse", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithNewPrepareResponse[crypto.Uint256](func(crypto.Uint256) dbft.PrepareResponse[crypto.Uint256] { + return nil + })) + t.Run("without NewChangeView", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithNewChangeView[crypto.Uint256](func(byte, dbft.ChangeViewReason, uint64) dbft.ChangeView { + return nil + })) + t.Run("without NewCommit", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithNewCommit[crypto.Uint256](func([]byte) dbft.Commit { + return nil + })) + t.Run("without NewRecoveryRequest", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithNewRecoveryRequest[crypto.Uint256](func(uint64) dbft.RecoveryRequest { + return nil + })) + t.Run("without NewRecoveryMessage", func(t *testing.T) { + _, err := dbft.New(opts...) + require.Error(t, err) + }) + + opts = append(opts, dbft.WithNewRecoveryMessage[crypto.Uint256](func() dbft.RecoveryMessage[crypto.Uint256] { + return nil + }), dbft.WithMaxTimePerBlock[crypto.Uint256](func() time.Duration { + return 0 + })) + t.Run("MaxTimePerBlock without SubscribeForTxs", func(t *testing.T) { + _, err := dbft.New(opts...) + require.ErrorContains(t, err, "MaxTimePerBlock and SubscribeForTxs should be specified/not specified at the same time") + }) + + opts = append(opts, dbft.WithSubscribeForTxs[crypto.Uint256](func() {})) + t.Run("with all defaults", func(t *testing.T) { + d, err := dbft.New(opts...) + require.NoError(t, err) + require.NotNil(t, d) + require.NotNil(t, d.RequestTx) + require.NotNil(t, d.GetTx) + require.NotNil(t, d.GetVerified) + require.NotNil(t, d.VerifyBlock) + require.NotNil(t, d.Broadcast) + require.NotNil(t, d.ProcessBlock) + require.NotNil(t, d.GetBlock) + require.NotNil(t, d.Config.WatchOnly) + }) +} + +// TestDBFT_FourGoodNodesDeadlock checks that the following liveness lock is not really +// a liveness lock and there's a way to accept block in this situation. +// 0 :> [type |-> "cv", view |-> 1] <--- this is the primary at view 1 +// 1 :> [type |-> "cv", view |-> 1] <--- this is the primary at view 0 +// 2 :> [type |-> "commitSent", view |-> 0] +// 3 :> [type |-> "commitSent", view |-> 1] +// +// Test structure note: the test is organized to reproduce the liveness lock scenario +// described in https://github.com/neo-project/neo-modules/issues/792#issue-1609058923 +// at the section named "1. Liveness lock with four non-faulty nodes". However, some +// steps are rearranged so that it's possible to reach the target network state described +// above. It is done because dbft implementation contains additional constraints comparing +// to the TLA+ model. +func TestDBFT_FourGoodNodesDeadlock(t *testing.T) { + r0 := newTestState(0, 4) + r0.currHeight = 4 + s0, _ := dbft.New[crypto.Uint256](r0.getOptions()...) + s0.Start(0) + + r1 := r0.copyWithIndex(1) + s1, _ := dbft.New[crypto.Uint256](r1.getOptions()...) + s1.Start(0) + + r2 := r0.copyWithIndex(2) + s2, _ := dbft.New[crypto.Uint256](r2.getOptions()...) + s2.Start(0) + + r3 := r0.copyWithIndex(3) + s3, _ := dbft.New[crypto.Uint256](r3.getOptions()...) + s3.Start(0) + + // Step 1. The primary (at view 0) replica 1 sends the PrepareRequest message. + reqV0 := r1.tryRecv() + require.NotNil(t, reqV0) + require.Equal(t, dbft.PrepareRequestType, reqV0.Type()) + + // Step 2 will be performed later, see the comment to Step 2. + + // Step 3. The backup (at view 0) replica 0 receives the PrepareRequest of + // view 0 and broadcasts its PrepareResponse. + s0.OnReceive(reqV0) + resp0V0 := r0.tryRecv() + require.NotNil(t, resp0V0) + require.Equal(t, dbft.PrepareResponseType, resp0V0.Type()) + + // Step 4 will be performed later, see the comment to Step 4. + + // Step 5. The backup (at view 0) replica 2 receives the PrepareRequest of + // view 0 and broadcasts its PrepareResponse. + s2.OnReceive(reqV0) + resp2V0 := r2.tryRecv() + require.NotNil(t, resp2V0) + require.Equal(t, dbft.PrepareResponseType, resp2V0.Type()) + + // Step 6. The backup (at view 0) replica 2 collects M prepare messages (from + // itself and replicas 0, 1) and broadcasts the Commit message for view 0. + s2.OnReceive(resp0V0) + cm2V0 := r2.tryRecv() + require.NotNil(t, cm2V0) + require.Equal(t, dbft.CommitType, cm2V0.Type()) + + // Step 7. The backup (at view 0) replica 3 decides to change its view + // (possible on timeout) and sends the ChangeView message. + s3.OnReceive(resp0V0) + s3.OnReceive(resp2V0) + s3.OnTimeout(r3.currHeight+1, 0) + cv3V0 := r3.tryRecv() + require.NotNil(t, cv3V0) + require.Equal(t, dbft.ChangeViewType, cv3V0.Type()) + + // Step 2. The primary (at view 0) replica 1 decides to change its view + // (possible on timeout after receiving at least M non-commit messages from the + // current view) and sends the ChangeView message. + s1.OnReceive(resp0V0) + s1.OnReceive(cv3V0) + s1.OnTimeout(r1.currHeight+1, 0) + cv1V0 := r1.tryRecv() + require.NotNil(t, cv1V0) + require.Equal(t, dbft.ChangeViewType, cv1V0.Type()) + + // Step 4. The backup (at view 0) replica 0 decides to change its view + // (possible on timeout after receiving at least M non-commit messages from the + // current view) and sends the ChangeView message. + s0.OnReceive(cv3V0) + s0.OnTimeout(r0.currHeight+1, 0) + cv0V0 := r0.tryRecv() + require.NotNil(t, cv0V0) + require.Equal(t, dbft.ChangeViewType, cv0V0.Type()) + + // Step 8. The primary (at view 0) replica 1 collects M ChangeView messages + // (from itself and replicas 1, 3) and changes its view to 1. + s1.OnReceive(cv0V0) + require.Equal(t, uint8(1), s1.ViewNumber) + + // Step 9. The backup (at view 0) replica 0 collects M ChangeView messages + // (from itself and replicas 0, 3) and changes its view to 1. + s0.OnReceive(cv1V0) + require.Equal(t, uint8(1), s0.ViewNumber) + + // Step 10. The primary (at view 1) replica 0 sends the PrepareRequest message. + s0.OnTimeout(r0.currHeight+1, 1) + reqV1 := r0.tryRecv() + require.NotNil(t, reqV1) + require.Equal(t, dbft.PrepareRequestType, reqV1.Type()) + + // Step 11. The backup (at view 1) replica 1 receives the PrepareRequest of + // view 1 and sends the PrepareResponse. + s1.OnReceive(reqV1) + resp1V1 := r1.tryRecv() + require.NotNil(t, resp1V1) + require.Equal(t, dbft.PrepareResponseType, resp1V1.Type()) + + // Steps 12, 13 will be performed later, see the comments to Step 12, 13. + + // Step 14. The backup (at view 0) replica 3 collects M ChangeView messages + // (from itself and replicas 0, 1) and changes its view to 1. + s3.OnReceive(cv0V0) + s3.OnReceive(cv1V0) + require.Equal(t, uint8(1), s3.ViewNumber) + + // Intermediate step A. It is added to make step 14 possible. The backup (at + // view 1) replica 3 doesn't receive anything for a long time and sends + // RecoveryRequest. + s3.OnTimeout(r3.currHeight+1, 1) + rcvr3V1 := r3.tryRecv() + require.NotNil(t, rcvr3V1) + require.Equal(t, dbft.RecoveryRequestType, rcvr3V1.Type()) + + // Intermediate step B. The backup (at view 1) replica 1 should receive any + // message from replica 3 to be able to change view. However, it couldn't be + // PrepareResponse because replica 1 will immediately commit then. Thus, the + // only thing that remains is to receive RecoveryRequest from replica 3. + // Replica 1 then should answer with Recovery message. + s1.OnReceive(rcvr3V1) + rcvrResp1V1 := r1.tryRecv() + require.NotNil(t, rcvrResp1V1) + require.Equal(t, dbft.RecoveryMessageType, rcvrResp1V1.Type()) + + // Intermediate step C. The primary (at view 1) replica 0 should receive + // RecoveryRequest from replica 3. The purpose of this step is the same as + // in Intermediate step B. + s0.OnReceive(rcvr3V1) + rcvrResp0V1 := r0.tryRecv() + require.NotNil(t, rcvrResp0V1) + require.Equal(t, dbft.RecoveryMessageType, rcvrResp0V1.Type()) + + // Step 12. According to the neo-project/neo#792, at this step the backup (at view 1) + // replica 1 decides to change its view (possible on timeout) and sends the + // ChangeView message. However, the recovery message will be broadcast instead + // of CV, because there's additional condition: too much (>F) "lost" or committed + // nodes are present, see https://github.com/roman-khimov/dbft/blob/b769eb3e0f070d6eabb9443a5931eb4a2e46c538/send.go#L68. + // Replica 1 aware of replica 0 that has sent the PrepareRequest for view 1. + // It can also be aware of replica 2 that has committed at view 0, but it won't + // change the situation. The final way to allow CV is to receive something + // except from PrepareResponse from replica 3 to remove replica 3 from the list + // of "lost" nodes. That's why we'he added Intermediate steps A and B. + // + // After that replica 1 is allowed to send the CV message. + s1.OnTimeout(r1.currHeight+1, 1) + cv1V1 := r1.tryRecv() + require.NotNil(t, cv1V1) + require.Equal(t, dbft.ChangeViewType, cv1V1.Type()) + + // Step 13. The primary (at view 1) replica 0 decides to change its view + // (possible on timeout) and sends the ChangeView message. + s0.OnReceive(resp1V1) + s0.OnTimeout(r0.currHeight+1, 1) + cv0V1 := r0.tryRecv() + require.NotNil(t, cv0V1) + require.Equal(t, dbft.ChangeViewType, cv0V1.Type()) + + // Step 15. The backup (at view 1) replica 3 receives PrepareRequest of view + // 1 and broadcasts its PrepareResponse. + s3.OnReceive(reqV1) + resp3V1 := r3.tryRecv() + require.NotNil(t, resp3V1) + require.Equal(t, dbft.PrepareResponseType, resp3V1.Type()) + + // Step 16. The backup (at view 1) replica 3 collects M prepare messages and + // broadcasts the Commit message for view 1. + s3.OnReceive(resp1V1) + cm3V1 := r3.tryRecv() + require.NotNil(t, cm3V1) + require.Equal(t, dbft.CommitType, cm3V1.Type()) + + // Intermediate step D. It is needed to enable step 17 and to check that + // MoreThanFNodesCommittedOrLost works properly and counts Commit messages from + // any view. + s0.OnReceive(cm2V0) + s0.OnReceive(cm3V1) + + // Step 17. The issue says that "The rest of undelivered messages eventually + // reaches their receivers, but it doesn't change the node's states.", but it's + // not true, the aim of the test is to show that replicas 0 and 1 still can + // commit at view 1 even after CV sent. + s0.OnReceive(resp3V1) + cm0V1 := r0.tryRecv() + require.NotNil(t, cm0V1) + require.Equal(t, dbft.CommitType, cm0V1.Type()) + + s1.OnReceive(cm0V1) + s1.OnReceive(resp3V1) + cm1V1 := r1.tryRecv() + require.NotNil(t, cm1V1) + require.Equal(t, dbft.CommitType, cm1V1.Type()) + + // Finally, send missing Commit message to replicas 0 and 1, they should accept + // the block. + require.Nil(t, r0.nextBlock()) + s0.OnReceive(cm1V1) + require.NotNil(t, r0.nextBlock()) + + require.Nil(t, r1.nextBlock()) + s1.OnReceive(cm3V1) + require.NotNil(t, r1.nextBlock()) +} + +func TestDBFT_OnReceiveCommitAMEV(t *testing.T) { + s := newTestState(2, 4) + t.Run("send preCommit after enough responses", func(t *testing.T) { + s.currHeight = 1 + service, _ := dbft.New[crypto.Uint256](s.getAMEVOptions()...) + service.Start(0) + + req := s.tryRecv() + require.NotNil(t, req) + + resp := s.getPrepareResponse(1, req.Hash(), 0) + service.OnReceive(resp) + require.Nil(t, s.tryRecv()) + + resp = s.getPrepareResponse(0, req.Hash(), 0) + service.OnReceive(resp) + + cm := s.tryRecv() + require.NotNil(t, cm) + require.Equal(t, dbft.PreCommitType, cm.Type()) + require.EqualValues(t, s.currHeight+1, cm.Height()) + require.EqualValues(t, 0, cm.ViewNumber()) + require.EqualValues(t, s.myIndex, cm.ValidatorIndex()) + require.NotNil(t, cm.Payload()) + + pub := s.pubs[s.myIndex] + require.NoError(t, service.PreHeader().Verify(pub, cm.GetPreCommit().Data())) + + t.Run("send commit after enough preCommits", func(t *testing.T) { + s0 := s.copyWithIndex(0) + require.NoError(t, service.PreHeader().SetData(s0.privs[0])) + preC0 := s0.getPreCommit(0, service.PreHeader().Data(), 0) + service.OnReceive(preC0) + require.Nil(t, s.tryRecv()) + require.Nil(t, s.nextPreBlock()) + require.Nil(t, s.nextBlock()) + + s1 := s.copyWithIndex(1) + require.NoError(t, service.PreHeader().SetData(s1.privs[1])) + preC1 := s1.getPreCommit(1, service.PreHeader().Data(), 0) + service.OnReceive(preC1) + + b := s.nextPreBlock() + require.NotNil(t, b) + require.Equal(t, []byte{0, 0, 0, 2}, b.Data()) // After SetData it's equal to node index. + require.Nil(t, s.nextBlock()) + + c := s.tryRecv() + require.NotNil(t, c) + require.Equal(t, dbft.CommitType, c.Type()) + require.EqualValues(t, s.currHeight+1, c.Height()) + require.EqualValues(t, 0, c.ViewNumber()) + require.EqualValues(t, s.myIndex, c.ValidatorIndex()) + require.NotNil(t, c.Payload()) + + t.Run("process block a after enough commitAcks", func(t *testing.T) { + s0 := s.copyWithIndex(0) + require.NoError(t, service.Header().Sign(s0.privs[0])) + c0 := s0.getAMEVCommit(0, service.Header().Signature()) + service.OnReceive(c0) + require.Nil(t, s.tryRecv()) + require.Nil(t, s.nextPreBlock()) + require.Nil(t, s.nextBlock()) + + s1 := s.copyWithIndex(1) + require.NoError(t, service.Header().Sign(s1.privs[1])) + c1 := s1.getAMEVCommit(1, service.Header().Signature()) + service.OnReceive(c1) + require.Nil(t, s.tryRecv()) + require.Nil(t, s.nextPreBlock()) + + b := s.nextBlock() + require.NotNil(t, b) + require.Equal(t, s.currHeight+1, b.Index()) + }) + }) + }) +} + +func TestDBFT_CachedMessages(t *testing.T) { + for _, amev := range []bool{false, true} { + t.Run(fmt.Sprintf("AMEV %t", amev), func(t *testing.T) { + s2 := newTestState(2, 4) + s2.currHeight = 1 + s1 := newTestState(1, 4) + s1.currHeight = 1 + + opts := s2.getOptions() + if amev { + opts = s2.getAMEVOptions() + } + service2, _ := dbft.New[crypto.Uint256](opts...) + service2.Start(0) + + opts = s1.getOptions() + if amev { + opts = s1.getAMEVOptions() + } + service1, _ := dbft.New[crypto.Uint256](opts...) + service1.Start(0) + + req := s2.tryRecv() + require.NotNil(t, req) // Primary sends a request. + require.Equal(t, dbft.PrepareRequestType, req.Type()) + + require.Nil(t, s1.tryRecv()) // Backup waits. + + cv0 := s1.getChangeView(0, 1) + cv3 := s1.getChangeView(3, 1) + service1.OnReceive(cv0) + service1.OnReceive(cv3) + service1.OnTimeout(s1.currHeight+1, 0) + + cv := s1.tryRecv() + require.NotNil(t, cv) + require.Equal(t, dbft.ChangeViewType, cv.Type()) + + service1.OnTimeout(s1.currHeight+1, 1) + req = s1.tryRecv() + require.NotNil(t, req) + require.Equal(t, dbft.PrepareRequestType, req.Type()) + + resp := s1.getPrepareResponse(3, req.Hash(), 1) + service1.OnReceive(resp) + require.Nil(t, s1.tryRecv()) + service2.OnReceive(resp) // From the future. + require.Nil(t, s2.tryRecv()) + + resp = s1.getPrepareResponse(0, req.Hash(), 1) + service2.OnReceive(resp) // From the future. + require.Nil(t, s2.tryRecv()) + + service1.OnReceive(resp) + cm := s1.tryRecv() + require.NotNil(t, cm) + + service2.OnReceive(cm) + require.Nil(t, s2.tryRecv()) + + if amev { + require.Equal(t, dbft.PreCommitType, cm.Type()) + require.EqualValues(t, s1.currHeight+1, cm.Height()) + require.EqualValues(t, 1, cm.ViewNumber()) + require.EqualValues(t, s1.myIndex, cm.ValidatorIndex()) + require.NotNil(t, cm.Payload()) + pub := s1.pubs[s1.myIndex] + require.NoError(t, service1.PreHeader().Verify(pub, cm.GetPreCommit().Data())) + } else { + require.Equal(t, dbft.CommitType, cm.Type()) + require.EqualValues(t, s1.currHeight+1, cm.Height()) + require.EqualValues(t, 1, cm.ViewNumber()) + require.EqualValues(t, s1.myIndex, cm.ValidatorIndex()) + require.NotNil(t, cm.Payload()) + } + + service2.OnReceive(cv0) + service2.OnReceive(cv3) + service2.OnTimeout(s2.currHeight+1, 0) + cv = s2.tryRecv() + require.NotNil(t, cv) + require.Equal(t, dbft.ChangeViewType, cv.Type()) + + require.Equal(t, 1, int(service2.ViewNumber)) + + // s2 has some PrepareResponses, but doesn't have a request. + service2.OnReceive(req) + + resp = s2.tryRecv() + require.NotNil(t, resp) + require.Equal(t, dbft.PrepareResponseType, resp.Type()) + + cm = s2.tryRecv() + require.NotNil(t, cm) + + if amev { + require.Equal(t, dbft.PreCommitType, cm.Type()) + require.EqualValues(t, s2.currHeight+1, cm.Height()) + require.EqualValues(t, 1, cm.ViewNumber()) + require.EqualValues(t, s2.myIndex, cm.ValidatorIndex()) + require.NotNil(t, cm.Payload()) + pub := s1.pubs[s1.myIndex] + require.NoError(t, service1.PreHeader().Verify(pub, cm.GetPreCommit().Data())) + + service2.OnReceive(s2.getPreCommit(0, service2.PreHeader().Data(), 1)) + cm = s2.tryRecv() + require.NotNil(t, cm) + require.Equal(t, dbft.CommitType, cm.Type()) + } else { + require.Equal(t, dbft.CommitType, cm.Type()) + require.EqualValues(t, s2.currHeight+1, cm.Height()) + require.EqualValues(t, 1, cm.ViewNumber()) + require.EqualValues(t, s2.myIndex, cm.ValidatorIndex()) + require.NotNil(t, cm.Payload()) + + require.NoError(t, service2.Header().Sign(s2.privs[0])) + service2.OnReceive(s2.getCommit(0, service2.Header().Signature(), 1)) + require.Nil(t, s2.tryRecv()) + b := s2.nextBlock() + require.NotNil(t, b) + require.Equal(t, s2.currHeight+1, b.Index()) + } + }) + } +} + +func (s testState) getChangeView(from uint16, view byte) Payload { + cv := consensus.NewChangeView(view, 0, 0) + + p := consensus.NewConsensusPayload(dbft.ChangeViewType, s.currHeight+1, from, 0, cv) + return p +} + +func (s testState) getRecoveryRequest(from uint16) Payload { + p := consensus.NewConsensusPayload(dbft.RecoveryRequestType, s.currHeight+1, from, 0, consensus.NewRecoveryRequest(0)) + return p +} + +func (s testState) getCommit(from uint16, sign []byte, view byte) Payload { + c := consensus.NewCommit(sign) + p := consensus.NewConsensusPayload(dbft.CommitType, s.currHeight+1, from, view, c) + return p +} + +func (s testState) getAMEVCommit(from uint16, sign []byte) Payload { + c := consensus.NewAMEVCommit(sign) + p := consensus.NewConsensusPayload(dbft.CommitType, s.currHeight+1, from, 0, c) + return p +} + +func (s testState) getPreCommit(from uint16, data []byte, view byte) Payload { + c := consensus.NewPreCommit(data) + p := consensus.NewConsensusPayload(dbft.PreCommitType, s.currHeight+1, from, view, c) + return p +} + +func (s testState) getPrepareResponse(from uint16, phash crypto.Uint256, view byte) Payload { + resp := consensus.NewPrepareResponse(phash) + + p := consensus.NewConsensusPayload(dbft.PrepareResponseType, s.currHeight+1, from, view, resp) + return p +} + +func (s testState) getPrepareRequest(from uint16, hashes ...crypto.Uint256) Payload { + return s.getPrepareRequestWithHeight(from, s.currHeight+1, hashes...) +} + +func (s testState) getPrepareRequestWithHeight(from uint16, height uint32, hashes ...crypto.Uint256) Payload { + req := consensus.NewPrepareRequest(0, 0, hashes) + + p := consensus.NewConsensusPayload(dbft.PrepareRequestType, height, from, 0, req) + return p +} + +func newTestState(myIndex int, count int) *testState { + s := &testState{ + myIndex: myIndex, + count: count, + pool: newTestPool(), + } + + s.privs, s.pubs = getTestValidators(count) + + return s +} + +func (s *testState) tryRecv() Payload { + if len(s.ch) == 0 { + return nil + } + + p := s.ch[0] + s.ch = s.ch[1:] + + return p +} + +func (s *testState) nextBlock() dbft.Block[crypto.Uint256] { + if len(s.blocks) == 0 { + return nil + } + + b := s.blocks[0] + s.blocks = s.blocks[1:] + + return b +} + +func (s *testState) nextPreBlock() dbft.PreBlock[crypto.Uint256] { + if len(s.preBlocks) == 0 { + return nil + } + + b := s.preBlocks[0] + s.preBlocks = s.preBlocks[1:] + + return b +} + +func (s testState) copyWithIndex(myIndex int) *testState { + return &testState{ + myIndex: myIndex, + count: s.count, + privs: s.privs, + pubs: s.pubs, + currHeight: s.currHeight, + currHash: s.currHash, + pool: newTestPool(), + } +} + +func (s *testState) getOptions() []func(*dbft.Config[crypto.Uint256]) { + opts := []func(*dbft.Config[crypto.Uint256]){ + dbft.WithTimer[crypto.Uint256](timer.New()), + dbft.WithCurrentHeight[crypto.Uint256](func() uint32 { return s.currHeight }), + dbft.WithCurrentBlockHash[crypto.Uint256](func() crypto.Uint256 { return s.currHash }), + dbft.WithGetValidators[crypto.Uint256](func(...dbft.Transaction[crypto.Uint256]) []dbft.PublicKey { return s.pubs }), + dbft.WithGetKeyPair[crypto.Uint256](func(_ []dbft.PublicKey) (int, dbft.PrivateKey, dbft.PublicKey) { + return s.myIndex, s.privs[s.myIndex], s.pubs[s.myIndex] + }), + dbft.WithBroadcast[crypto.Uint256](func(p Payload) { s.ch = append(s.ch, p) }), + dbft.WithGetTx[crypto.Uint256](s.pool.Get), + dbft.WithProcessBlock[crypto.Uint256](func(b dbft.Block[crypto.Uint256]) error { s.blocks = append(s.blocks, b); return nil }), + dbft.WithWatchOnly[crypto.Uint256](func() bool { return false }), + dbft.WithGetBlock[crypto.Uint256](func(crypto.Uint256) dbft.Block[crypto.Uint256] { return nil }), + dbft.WithTimer[crypto.Uint256](timer.New()), + dbft.WithLogger[crypto.Uint256](zap.NewNop()), + dbft.WithNewBlockFromContext[crypto.Uint256](newBlockFromContext), + dbft.WithTimePerBlock[crypto.Uint256](func() time.Duration { + return time.Second * 10 + }), + dbft.WithRequestTx[crypto.Uint256](func(...crypto.Uint256) {}), + dbft.WithGetVerified[crypto.Uint256](func() []dbft.Transaction[crypto.Uint256] { return []dbft.Transaction[crypto.Uint256]{} }), + + dbft.WithNewConsensusPayload[crypto.Uint256](newConsensusPayload), + dbft.WithNewPrepareRequest[crypto.Uint256](consensus.NewPrepareRequest), + dbft.WithNewPrepareResponse[crypto.Uint256](consensus.NewPrepareResponse), + dbft.WithNewChangeView[crypto.Uint256](consensus.NewChangeView), + dbft.WithNewCommit[crypto.Uint256](consensus.NewCommit), + dbft.WithNewRecoveryRequest[crypto.Uint256](consensus.NewRecoveryRequest), + dbft.WithNewRecoveryMessage[crypto.Uint256](func() dbft.RecoveryMessage[crypto.Uint256] { + return consensus.NewRecoveryMessage(nil) + }), + dbft.WithVerifyCommit[crypto.Uint256](func(p dbft.ConsensusPayload[crypto.Uint256]) error { return nil }), + } + + verify := s.verify + if verify == nil { + verify = func(dbft.Block[crypto.Uint256]) bool { return true } + } + + opts = append(opts, dbft.WithVerifyBlock(verify)) + + if debugTests { + cfg := zap.NewDevelopmentConfig() + cfg.DisableStacktrace = true + logger, _ := cfg.Build() + opts = append(opts, dbft.WithLogger[crypto.Uint256](logger)) + } + + return opts +} + +func (s *testState) getAMEVOptions() []func(*dbft.Config[crypto.Uint256]) { + opts := s.getOptions() + opts = append(opts, + dbft.WithAntiMEVExtensionEnablingHeight[crypto.Uint256](0), + dbft.WithNewPreCommit[crypto.Uint256](consensus.NewPreCommit), + dbft.WithNewCommit[crypto.Uint256](consensus.NewAMEVCommit), + dbft.WithNewPreBlockFromContext[crypto.Uint256](newPreBlockFromContext), + dbft.WithNewBlockFromContext[crypto.Uint256](newAMEVBlockFromContext), + dbft.WithProcessPreBlock(func(b dbft.PreBlock[crypto.Uint256]) error { + s.preBlocks = append(s.preBlocks, b) + return nil + }), + ) + + return opts +} + +func newBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { + if ctx.TransactionHashes == nil { + return nil + } + block := consensus.NewBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, ctx.TransactionHashes) + return block +} + +func newPreBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.PreBlock[crypto.Uint256] { + if ctx.TransactionHashes == nil { + return nil + } + pre := consensus.NewPreBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, ctx.TransactionHashes) + return pre +} + +func newAMEVBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { + if ctx.TransactionHashes == nil { + return nil + } + var data [][]byte + for _, c := range ctx.PreCommitPayloads { + if c != nil && c.ViewNumber() == ctx.ViewNumber { + data = append(data, c.GetPreCommit().Data()) + } + } + pre := consensus.NewAMEVBlock(ctx.PreBlock(), data, ctx.M()) + return pre +} + +// newConsensusPayload is a function for creating consensus payload of specific +// type. +func newConsensusPayload(c *dbft.Context[crypto.Uint256], t dbft.MessageType, msg any) dbft.ConsensusPayload[crypto.Uint256] { + cp := consensus.NewConsensusPayload(t, c.BlockIndex, uint16(c.MyIndex), c.ViewNumber, msg) + return cp +} + +func getTestValidators(n int) (privs []dbft.PrivateKey, pubs []dbft.PublicKey) { + for range n { + priv, pub := crypto.Generate(rand.Reader) + privs = append(privs, priv) + pubs = append(pubs, pub) + } + + return +} + +func (tx testTx) Hash() (h crypto.Uint256) { + binary.LittleEndian.PutUint64(h[:], uint64(tx)) + return +} + +func newTestPool() *testPool { + return &testPool{ + storage: make(map[crypto.Uint256]testTx), + } +} + +func (p *testPool) Add(tx testTx) { + p.storage[tx.Hash()] = tx +} + +func (p *testPool) Get(h crypto.Uint256) dbft.Transaction[crypto.Uint256] { + if tx, ok := p.storage[h]; ok { + return tx + } + + return nil +} diff --git a/docs/labels.md b/docs/labels.md new file mode 100644 index 0000000..bc7797a --- /dev/null +++ b/docs/labels.md @@ -0,0 +1,9 @@ +# Project-specific labels + +## Component + +Currently only these ones are used, but the list can be extended in future: + +- tla+ + + Related to the TLA+ algorithm specification diff --git a/docs/release-instruction.md b/docs/release-instruction.md new file mode 100644 index 0000000..0cb6d03 --- /dev/null +++ b/docs/release-instruction.md @@ -0,0 +1,64 @@ +# Release instructions + +This document outlines the dbft release process. It can be used as a todo +list for a new release. + +## Check the state + +These should run successfully: + * build + * unit-tests + * lint + * simulation with default settings + +## Update CHANGELOG and ROADMAP + +Add an entry to the CHANGELOG.md following the style established there. Add a +codename, version and release date in the heading. Write a paragraph +describing the most significant changes done in this release. In case if the dBFT +configuration was changed, some API was marked as deprecated, any experimental +changes were made in the user-facing code and the users' feedback is needed or +if there's any other information that requires user's response, write +another separate paragraph for those who uses dbft package. Then, add sections +with release content describing each change in detail and with a reference to +GitHub issues and/or PRs. Minor issues that doesn't affect the package end-user may +be grouped under a single label. + * "New features" section should include new abilities that were added to the + dBFT/API, are directly visible or available to the user and are large + enough to be treated as a feature. Do not include minor user-facing + improvements and changes that don't affect the user-facing functionality + even if they are new. + * "Behaviour changes" section should include any incompatible changes in default + settings or in API that are available to the user. Add a note about changes + user needs to make if he uses the affected code. + * "Improvements" section should include user-facing changes that are too + insignificant to be treated as a feature and are not directly visible to the + package end-user, such as performance optimizations, refactoring and internal + API changes. + * "Bugs fixed" section should include a set of bugs fixed since the previous + release with optional bug cause or consequences description. + +Create a PR with CHANGELOG changes, review/merge it. + +## Create a GitHub release and a tag + +Use "Draft a new release" button in the "Releases" section. Create a new +`vX.Y.Z` tag for it following the semantic versioning standard. Put change log +for this release into the description. Do not attach any binaries. +Set the "Set as the latest release" checkbox if this is the latest stable +release or "Set as a pre-release" if this is an unstable pre-release. +Press the "Publish release" button. + +## Close GitHub milestone + +Close corresponding X.Y.Z GitHub milestone. + +## Announcements + +Copy the GitHub release page link to: + * Element channel + +## Dependant projects update + +Create an issue or PR to fetch the updated package version in the dependant +repositories. diff --git a/formal-models/.github/dbft.drawio b/formal-models/.github/dbft.drawio new file mode 100644 index 0000000..978abb0 --- /dev/null +++ b/formal-models/.github/dbft.drawio @@ -0,0 +1 @@ +7Vvfk5s2EP5rPJM8+Aawwdyj7btrH5LpTd1e2qeMDDKoB4gI+df99ZUsCRAYG9c+2815MgnWarWS2NX3rSTS6Y3j1S8EpOFX7MOoYxkBQX6n99CxLJP9ZYIUBFATcI0JelNCQ0rnyIeZpkgxjihKdaGHkwR6VJMBQvBSV5vhqD6MiQciWJN+Qz4NhdS1jUL+K0RBqDoyDVkTA6UsBVkIfLwsiXqPnd6YYEzFr3g1hhF/M+q9iHZPDbX5wAhMaJsGb785w+/hiw+sYJ0+/GmPR8lL1+wLMwsQzeWM5WjpWr0C6LM3IouY0BAHOAHRYyEdETxPfMj7MVnpn3mcSr+5rFg0+YJxqnQgpWupBOYUM1FI40jWZpTg1/x9szc1muGEPoEYRWsmGOMYeWyME5Bk7PF1IhWkPdOR5TGOMNnModc3+J+NaUDokMcBq0hwApXsCUW8e64DE19peBHIMuT9EaJEVEg1UzWTffZFrVaiZP0Xt3hnq+LfeQes8LDSSuty6RkSFEMKiRQKn3BHNDpfrQVAAkh3eNzJQ48tSIhZL2TN2hEYAYoWun0g10qQ6+VNnzFiPVvGSmj0B7LJWpR7tm4hw3PiQdmoHKQVO/a9odkxHUM3JOZXM8R+lKZTiDZr4JD1YG9ZD07EXujIRwttXTg/5nzpbiKtm20cP2QKZj9dbTym6tmvQD4jpX+UoYwFGqt//h3+YI9PKQsVQNaflX02b9GF3i0Tb2agS69mUmyh/q/HXzglS7lXpsB7nae5U6Zky3zaTHMLFn8BU8agGmCCCAUJhysGBxw0RgtIKGIsNpQVMfJ9AdWQTQZMN/Y4sqR8JW3Wlj3q2A8NSLoXerdBrcbDssuCDvkI4aoRznawVR28pBnjzhzYOgyJ0pHg1u1pRpVVZQDPZhl8FzTqNYJR63h1avFqzICnt6k7tRbeKEEUsSh7YyzfFMCVUK2TeJnhQ5ByvXgV8KTwbhbhpRcyPr1j8RujBFAeTqNliCicpGLAS6ZZjUf2kkmAwJawFd2X4vJx6IwcJx/pnvgzd0YaC3KdpmRxWaSIZk/KwlJ6aBvNsacFz6GRomLyomnbSVOeI3PA/jXkgO3TNpEf7UvbWqR3gyPTu+OicMtuYjAuchUFHdy3SdCMXzPpZY5gCab4O4HBPAJE4JcnXMorSTD9xCGf91E8P0ucq0ChkdLtOcCj1XHdznBYGiGbvhqkUFLELuazaXLfGdo89PJZpSC5mjmV5rEZ1nXkE2pVXkk+IVCzEeW7xp2KaInzXfMkCYVu1D1XOuHUVmdKIH9JEyh3Ux+Fv22rYZt5OQK3bwT+UxP4oCWBuxcl8DpECMJj3ooRbeK/G6nUVvIuUjFd09VpxTqOVqQZ51xEMqhFibeJjw/PI72LbwTdmmumEfZeh54HU8o37x/YO/3Bpb1jmnV8PZb2/wPPH3ybUiQGWlpQZAmHJAY6xVYJuJLF1ONTI3+d2ss5gKnnADJLKMj/sGuVvfR+35LeG3YcR24orMq5p1u5fnnvWxNrS1if6GCdtX7Cbc8cP/jx+G445NtZ09ID5cj97PunGuaWjNRyQMw5JplmaSlCNGEnv3a/jg3VCXDzXBuq4zD1VBuqC11k1072q1Da9iY7TzaaDL03Jg+uKv5vBwqnPlBom3GcZJ2wmYN1SUHyYuuExOobu1eLaxylb+7Rt117lz77IWZ40iWokvsd3LX9dEUpySTHeGrLbh/31MVsuPgqch/90P00B/nds53dWzc4v8H5yTaQh8J5fj9SSWaa4Laq361+m1qFc8Pepf9O8Fw/K2uA55dOw8E3F/FPY/ir5P8kcMUfCwSXN8jee1C+G7LZbtVyHT0sTvM9V3+gW+2fC8TvawHnLWpB8TOfyOYfSp3h3pUVi4/rhQeL/3/Qe/wX \ No newline at end of file diff --git a/formal-models/.github/dbft.png b/formal-models/.github/dbft.png new file mode 100644 index 0000000000000000000000000000000000000000..22d3d8d7ffe3e49f96047711bd5e9ccde0da94e4 GIT binary patch literal 43622 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 literal 0 HcmV?d00001 diff --git a/formal-models/.github/dbft2.1_centralizedCV.drawio b/formal-models/.github/dbft2.1_centralizedCV.drawio new file mode 100644 index 0000000..5ee0269 --- /dev/null +++ b/formal-models/.github/dbft2.1_centralizedCV.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/formal-models/.github/dbft2.1_centralizedCV.png b/formal-models/.github/dbft2.1_centralizedCV.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5bde40edbf19871b1075959c587e1c89152265 GIT binary patch literal 113626 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%0A~>0y&WS%Asj7SCa3>loTwEmK0&p@k*HI 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

y=z`}0Z}DfZ1r*H{#dO zp(%7_W|p?NFsqpp)$&19gvsFK*j5=}_s2*RmTe@@7BVdR(Hlzh8pafN-{Iu#@HMEB zmIcgzO}NEe^x`$|y>gGqsjcQ3*rp8mc58yB3M3tI6)6*aMRaRHD)k-*K%u-@zm#zN z`_|6j_g~*>FdEV#UrM&t<1HBHsJ-%opYt|WS|OBp;_?0^!Q0SFD%B z!|V!&o^r-PD9gdIFh8l-zD!T7j{_~q>3a%1=$qU-S2aIlUx>sn3_v(NJa=)|f(nZO zvd=$pVCKaWYrwQzqUz538sE1v5oBvFQpEaG1Uh^^8S@pk&XF5~*oG$3)EOM37qkUV fpN&E%@G<1nE!f5UJwv)P;Gcr5%ELlwWB>mLBZ&4! literal 0 HcmV?d00001 diff --git a/formal-models/.github/dbft2.1_threeStagedCV.drawio b/formal-models/.github/dbft2.1_threeStagedCV.drawio new file mode 100644 index 0000000..4aeeeef --- /dev/null +++ b/formal-models/.github/dbft2.1_threeStagedCV.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/formal-models/.github/dbft2.1_threeStagedCV.png b/formal-models/.github/dbft2.1_threeStagedCV.png new file mode 100644 index 0000000000000000000000000000000000000000..d0df26d044f51eaecfbc389519e3ece88dfa2184 GIT binary patch literal 110022 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%0AL1fXM{r@O>WZ?U1L*J3??1s%Oyo}R6o1QB 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 JDgM;s{{TS#*7^Vd literal 0 HcmV?d00001 diff --git a/formal-models/.github/dbft_antiMEV.drawio b/formal-models/.github/dbft_antiMEV.drawio new file mode 100644 index 0000000..3852ba0 --- /dev/null +++ b/formal-models/.github/dbft_antiMEV.drawio @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/formal-models/.github/dbft_antiMEV.png b/formal-models/.github/dbft_antiMEV.png new file mode 100644 index 0000000000000000000000000000000000000000..a2a2b6ef4920d3649ba992a80e6f88a4a2997688 GIT binary patch literal 70101 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 `M` but not including +a whole `PrepareRequest`). This quickly synchronizes nodes still at the preparation +stage when someone else collects enough preparations. It at the same time prevents +malicious/byzantine nodes from sending spoofed `Commit` messages. The `Commit` +message size becomes a little bigger, but since it's just hashes it still fits +into a single packet in the vast majority of the cases, so it doesn't really matter. + +### dBFT 2.1 stages-based model + +The basic idea of this model is to split the consensus process into three subsequent +stages marked as `I`, `II` and `III` at the scheme. To perform a transition between +two subsequent stages each consensus node should wait for a set of messages from +at least `M` consensus nodes to be received so that it's possible to complete a full +picture of the neighbours' decisions in the current consensus round. In other words, +no transition can happen unless we have `M` number of messages from the subsequent round, +timers are only set up after we have this number of messages, just to wait for +(potentially) a whole set of them. At the same time, each of the stages has +its own `ChangeView[1,2,3]` message to exit to the next consensus round (view) if +something goes wrong in the current one and there's definitely no ability to +continue consensus process in the current view. Below there's a short description +of each stage. Please, refer to the model scheme and specification for further +details. + +#### Stage I + +Once initialized, consensus node has two ways: +1. Send its `PrepareRequest`/`PrepareResponse` message (and transmit to the + `prepareSent` state). +2. Decide to go to the next view on timeout or any other valid reason (like + transaction missing in the node's mempool or wrong proposal) via sending + `ChangeView1` message (and transmit to the `cv1` state). + +This scheme is quite similar to the basic dBFT 2.0 model except the new type of +`ChangeView` message. After that the node enters stage `I` and waits for consensus +messages of stage `I` (`PrepareRequest` or `PrepareResponse` or `ChangeView1`) +from at least `M` neighbours which is needed to decide about the next actions. +The set of received messages can be arranged in the following way: + +* `M` messages of `ChangeView1` type denote that `M` nodes have decided to change + their view directly after initialization due to invalid/missing `PrepareRequest` + which leads to immediate view changing. This is a "fail fast" route that is the + same as with dBFT 2.0 for the widespread case of missing primary. No additional + delay is added, everything works as usual. +* `M` preparation messages (of type `PrepareRequest` or `PrepareResponse`) with + missing `ChangeView3` denote that the majority of nodes have decided to commit which + denotes the safe transition to stage `II` can be performed and `Commit` message + can safely be sent even if there's `ChangeView1` message in the network. Notice + that `ChangeView3` check is just a protection against node seriously lagging + behind. +* `M` messages each of the type `PrepareRequest` or `PrepareResponse` or `ChangeView1` + where at least one message is of the type `ChangeView1` denote that at least `M` + nodes have reached the stage `I` and the node can safely take further steps. + The additional `| Commit | ≤ F ∪ | CV3 | > 0` condition requires the majority of + nodes not to have the `Commit` message to be sent so that it's still possible to + collect enough `ChangeView[2,3]` messages to change the view in further stages. + If so, then the safe transition to stage `II` can be performed and `ChangeView2` + message can safely be sent. + +#### Stage II + +Once the node has `Commit` or `ChangeView2` message sent, it enters the stage `II` +of the consensus process and waits for at least `M` messages of stage `II` +(`Commit` or `ChangeView2`) to perform the transition to the next stage. The set +of accepted messages can be arranged in the following way: + +* `M` messages of `ChangeView2` type denote that `M` nodes have decided to change + their view directly after entering the stage `II` due to timeout while waiting + for the `Commit` messages which leads to immediate view changing. +* `M` messages of type `Commit` denote that the majority of nodes have decided to + commit which denotes the block can be accepted immediately without entering the + stage `III`. Notice that this is the regular flow of normal dBFT 2.0 consensus, + it also hasn't been changed and proceeds the way it was before. +* `M` messages each of the type `Commit` or `ChangeView2` where not more than `F` + messages are of the type `Commit` denotes that the majority of nodes decided to + change their view after entering the stage `II` and there's not enough `Commit` + messages to create the block (and produce the fork), thus, the safe transition + to stage `III` can be performed and `ChangeView3` message can safely be sent + even if there's `Commit` message in the network. + +In addition, the direct transition from `cv2` state to the `commitSent` state is +added in case if it's clear that there's more than `F` nodes have decided to +commit and no `ChangeView3` message has been received which means that it's possible +to produce block in the current view. This path handles a corner case of missing +stage `I` messages, in fact, because `Commit` messages prove that there are at +least `M` preparation messages exist, but the node went `cv2` path just because +it missed some of them. + +#### Stage III + +Unlike the basic dBFT 2.0 model where consensus node locks on the commit phase, +stage `III` gives the ability to escape from the commit phase via collecting the set +of `M` `ChangeView3` messages. This phase is reachable as soon as at least `M` nodes +has reached the phase `II` (have `ChangeView2` or `Commit` messages sent) and if +there's not enough `Commit` messages (<=`F`) to accept the block. This stage is +added to avoid situation when the node is being locked on the `commitSent` state +whereas the rest of the nodes (>`F`) is willing to go to the next view. + +Here's the scheme of transitions between consensus node states for the improved +dBFT 2.1 stages-based model: + +![dBFT 2.1stages-based model](./.github/dbft2.1_threeStagedCV.png) + +Here you can find the specification file and the basic TLC Model Checker launch +configuration: + +* [TLA⁺ specification](./dbft2.1_threeStagedCV/dbftCV3.tla) +* [TLC Model Checker configuration](./dbft2.1_threeStagedCV/dbftCV3___AllGoodModel.launch) + +### dBFT 2.1 model with the centralized view changes + +The improvement which was taken as a base for this model is taken from the pBFT +algorithm and is as follows. +The consensus process is split into two stages with the following meaning: + +* Stage `I` holds the node states from which it's allowed to transmit to the subsequent + view under assumption that the *new* proposal will be generated (as the basic dBFT 2.0 + model does). +* Stage `II` holds the node states from which it's allowed to perform view change + *preserving* the proposal from the previous view. + +Another vital difference from the basic dBFT 2.0 model is that view changes are +being performed by the node on the `DoCV[1,2]` command (consensus message) sent by the +leader of the target view specified via `DoCV[1,2]` parameters. Aside from the target view +parameter, `DoCV[1,2]` message contains the set of all related pre-received consensus messages +so that the receivers of `DoCV[1,2]` are able to check its validness before the subsequent +view change. + +Below presented the short description of the proposed consensus process. Please, refer to the +model scheme and specification for further details. + +#### Stage I + +Once initialized at view `v`, the consensus node has two ways: + +1. Send its `PrepareRequest`/`PrepareResponse` message (and transmit to the + `prepareSent` state). +2. Decide to go to the next view `v+1` on timeout or any other valid reason like + transaction missing in the node's mempool via sending `ChangeView1(v+1)` message + (and transmitting to the `cv1 to v'=v+1` state). + +After that the node enters stage `I` and perform as follows: + +* If the node has its `PrepareRequest` or `PrepareResponse` sent: + * If at least `M` preparation messages (including its own) collected, then + it's clear that the majority has proposal for view `v` being accepted as + valid and the node can safely send the `Commit` message and transmit to the + phase `II` (`commitSent` state). + * If there's not enough preparation payloads received from the neighbours for a + long time, then the node is allowed to transmit to the stage `II` via sending + its `ChangeView2` message (and changing its state to the `cv2 to v'=v+1`). It + denotes the node's desire to change view to the next one with the current proposal + to be preserved. +* If the node entered the `cv1 to v'=v+1` state: + * If there's a majority (>=`M`) of `ChangeView1(v+1)` messages and the node is + primary in the view `v+1` then it should send the signal (`DoCV1(v+1)` message) + to the rest of the group to change their view to `v+1` with the new proposal + generated. The rest of the group (backup on `v+1` view that have sent their + `ChangeVeiew1(v+1)` messages) should change their view on `DoCV1(v+1)` receiving. + * If there's a majority (>=`M`) of `ChangeView1(v+1)` messages collected, but + `DoCV1(v+1)` is missing for a long time, then the node is able to "skip" view + `v+1` and send the `ChangeView1(v+2)` message hoping that the primary of `v+2` + will be faster enough to send the `DoCV1(v+2)` signal. The process can be repeated + on timeout for view `v+3`, etc. + * If there's more than `F` nodes that have sent their preparation messages + (and, consequently, announced their desire to transmit to the stage `II` of the + current view rather than to change view), then it's clear that it won't be more than `F` messages + of type `ChangeView1` to perform transition to the next view from the stage `II`. + Thus, the node is allowed to send its `ChangeView2` message (and change its + state to the `cv2 to v'=v+1`). Such situation may happen if the node haven't + proposal received in time (consider valid proposal). + +#### Stage II + +Once the node has entered the stage `II`, the proposal of the current round is +considered to be valid. Depending on the node's state the following decisions are +possible: + +* If the node has its `Commit` sent and is in the `commitSent` state: + * If the majority (>=`M`) of the `Commit` messages has been received, then the + block may be safely accepted for the current proposal. + * If there's not enough `Commit` messages for a long time, then it's legal to + send the `ChangeView2(v+1)` message, transmit to the `cv2 to v'=v+1` state + and decide to go to the next view `v+1` preserving the current proposal + and hoping that it would be possible to collect enough `Commit` messages + for it in the view `v+1`. +* If the node is in the `cv2 to v'=v+1` state then: + * If there's a majority (>=`M`) of `ChangeView2(v+1)` messages and the node is + primary in the view `v+1` then it should send the signal (`DoCV2(v+1)` message) + to the rest of the group to change their view to `v+1` with the old proposal of view `v` + preserved. The rest of the group (backup on `v+1` view that have sent their + `ChangeVeiew2(v+1)` messages) should change their view on `DoCV2(v+1)` receiving. + * If there's a majority (>=`M`) of `ChangeView2(v+1)` messages collected, but + `DoCV2(v+1)` is missing for a long time, then the node is able to "skip" view + `v+1` and send the `ChangeView2(v+2)` message hoping that the primary of `v+2` + will be faster enough to send the `DoCV2(v+2)` signal. The process can be repeated + on timeout for view `v+3`, etc. + * Finally, if the node receives at least `M` messages from the stage `I` at max + `F` of which are preparations (the blue dotted arrow from `cv2 to v'=v+1` to + `cv1 to v'=v+1` state), it has the ability to go back to the `cv1` state to start + the new consensus round with the new proposal. This case is kind of special, + it allows to escape from the deadlock situation when the node is locked on `cv2` + state unable to perform any further steps whereas the rest of the network are + waiting in the `cv1` state. Consider the case of four-nodes network where the + first node is permanently "dead", the primary have sent its `PrepareRequest` + and went to the `cv2` state on the timeout and the rest two nodes are waiting + in the `cv1` state not able to move further. + +It should be noted that "preserving the proposal of view `v` in view `v+1`" means +that the primary of view `v+1` broadcasts the `PrepareRequest` message at view `v+1` +that contains the same set of block's fields (transactions, timestamp, primary, etc) as +the `PrepareRequest` proposed in the view `v` has. + +Here's the scheme of transitions between consensus node states for the improved +dBFT 2.1 model with the centralized view changes process: + +![dBFT 2.1 model with the centralized view changes](./.github/dbft2.1_centralizedCV.png) + +Here you can find the specification file and the basic TLC Model Checker launch +configuration: + +* [TLA⁺ specification](./dbft2.1_centralizedCV/dbftCentralizedCV.tla) +* [TLC Model Checker configuration](./dbft2.1_centralizedCV/dbftCentralizedCV___AllGoodModel.launch) + +## MEV-resistant dBFT models + +[Neo X chain](https://docs.banelabs.org/) uses dBFT 2.0 algorithm as a consensus engine. As a part of +the Neo X anti-MEV feature implementation, dBFT 2.0 extension was designed to +provide single-block finality for encrypted transactions (a.k.a. envelope +transactions). Compared to dBFT 2.0, MEV-resistant dBFT algorithm includes an +additional `post-Commit` phase that is required to be passed through by consensus +nodes before every block acceptance. This phase allows consensus nodes to exchange +some additional data related to encrypted transactions and to the final state of +accepting block using a new type of consensus messages. The improved protocol based +on dBFT 2.0 with an additional phase will be referred below as MEV-resistant dBFT. + +We've checked MEV-resistant dBFT model with the TLC Model Checker against the same +set of launch configurations that was used to reveal the liveness problems of the +[basic dBFT 2.0 model](#basic-dbft-20-model). MEV-resistant dBFT model brings no extra problems to the +protocol, but it has been proved that this model has exactly the same +[liveness bug](https://github.com/neo-project/neo-modules/issues/792) that the +original dBFT 2.0 model has which is expected. + +### Basic MEV-resistant dBFT model + +This specification is an extension of the +[basic dBFT 2.0 model](#basic-dbft-20-model). Compared to the base model, +MEV-resistant dBFT specification additionally includes: + +1. New message type `CommitAck` aimed to reflect an additional protocol + message that should be sent by resource manager if at least `M` `Commit` + messages were collected by the node (that confirms a.k.a. "PreBlock" + final acceptance). +2. New resource manager state `commitAckSent` aimed to reflect the additional phase + of the protocol needed for consensus nodes to exchange some data that was not + available at the time of the first commit. This RM state represents a consensus + node state when it has sent these additional post-commit data but has not accepted + the final block yet. +3. New specification step `RMSendCommitAck` describing the transition between + `commitSent` and `commitAckSent` phases of the protocol, or, which is the same, + corresponding resource managers states. This step allows the resource manager to + send `CommitAck` message if at least `M` valid `Commit` messages are collected. +4. Adjusted behaviour of `RMAcceptBlock` step: block acceptance is possible iff the + node has sent the `CommitAck` message and there are at least `M` `CommitAck` + messages collected by the node. +5. Adjusted behaviour of "faulty" resource managers: allow malicious nodes to send an + `CommitAck` message via `RMFaultySendCommitAck` step. + +It should be noted that, in comparison with the dBFT 2.0 protocol where the node is +being locked in the `commitSent` state until the block acceptance, MEV-resistant dBFT +does not allow to accept the block right after the `commitSent` state. However, it +allows the node to move from `commitSent` phase further to the `commitAckSent` state +and locks the node at this state until the block acceptance. No view change may be +initiated or accepted by a node entered the `commitAckSent` state. + +Here's the scheme of transitions between consensus node states for MEV-resistant dBFT +algorithm: + +![Basic MEV-resistant dBFT model transitions scheme](./.github/dbft_antiMEV.png) + +Here you can find the specification file and the basic MEV-resistant dBFT TLC Model +Checker launch configuration for the four "honest" consensus nodes scenario: + +* [TLA⁺ specification](dbft_antiMEV/dbft.tla) +* [TLC Model Checker configuration](dbft_antiMEV/dbft___AllGoodModel.launch) + +## How to run/check the TLA⁺ specification + +### Prerequirements + +1. Download and install the TLA⁺ Toolbox following the + [official guide](http://lamport.azurewebsites.net/tla/toolbox.html). +2. Read the brief introduction to the TLA⁺ language and TLC Model Checker at the + [official site](http://lamport.azurewebsites.net/tla/high-level-view.html). +3. Download and take a look at the + [TLA⁺ cheat sheet](https://lamport.azurewebsites.net/tla/summary-standalone.pdf). +4. For a proficient learning watch the + [TLA⁺ Video Course](https://lamport.azurewebsites.net/video/videos.html) and + read the [Specifying Systems book](http://lamport.azurewebsites.net/tla/book.html?back-link=tools.html#documentation). + +### Running the TLC model checker + +1. Clone the [repository](https://github.com/nspcc-dev/dbft.git). +2. Open the TLA⁺ Toolbox, open new specification and provide path to the desired + `*.tla` file that contains the specification description. +3. Create the model named `AllGoodModel` in the TLA⁺ Toolbox. +4. Copy the corresponding `*___AllGoodModel.launch` file to the `*.toolbox` + folder. Reload/refresh the model in the TLA⁺ Toolbox. +5. Open the `Model Overview` window in the TLA⁺ Toolbox and check that behaviour + specification, declared constants, invariants and properties of the model are + filled in with some values. +6. Press `Run TLC on the model` button to start the model checking process and + explore the progress in the `Model Checkng Results` window. + +### Model checking note + +It should be noted that all TLA⁺ specifications provided in this repo can be perfectly checked +with `MaxView` model constraint set to be 1 for the four-nodes network scenario. Larger +`MaxView` values produces too many behaviours to be checked, so TLC Model Checker is likely +to fail with OOM during the checking process. However, `MaxView` set to be 1 is enough to check +the model liveness properties for the four-nodes scenario as there are two views to be checked +in this case (0 and 1). \ No newline at end of file diff --git a/formal-models/dbft/dbft.tla b/formal-models/dbft/dbft.tla new file mode 100644 index 0000000..bd3cef0 --- /dev/null +++ b/formal-models/dbft/dbft.tla @@ -0,0 +1,388 @@ +-------------------------------- MODULE dbft -------------------------------- + +EXTENDS + Integers, + FiniteSets + +CONSTANTS + \* RM is the set of consensus node indexes starting from 0. + \* Example: {0, 1, 2, 3} + RM, + + \* RMFault is a set of consensus node indexes that are allowed to become + \* FAULT in the middle of every considered behavior and to send any + \* consensus message afterwards. RMFault must be a subset of RM. An empty + \* set means that all nodes are good in every possible behaviour. + \* Examples: {0} + \* {1, 3} + \* {} + RMFault, + + \* RMDead is a set of consensus node indexes that are allowed to die in the + \* middle of every behaviour and do not send any message afterwards. RMDead + \* must be a subset of RM. An empty set means that all nodes are alive and + \* responding in in every possible behaviour. RMDead may intersect the + \* RMFault set which means that node which is in both RMDead and RMFault + \* may become FAULT and send any message starting from some step of the + \* particular behaviour and may also die in the same behaviour which will + \* prevent it from sending any message. + \* Examples: {0} + \* {3, 2} + \* {} + RMDead, + + \* MaxView is the maximum allowed view to be considered (starting from 0, + \* including the MaxView itself). This constraint was introduced to reduce + \* the number of possible model states to be checked. It is recommended to + \* keep this setting not too high (< N is highly recommended). + \* Example: 2 + MaxView + +VARIABLES + \* rmState is a set of consensus node states. It is represented by the + \* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is + \* the state of the r-th consensus node at the current step. + rmState, + + \* msgs is the shared pool of messages sent to the network by consensus nodes. + \* It is represented by a subset of Messages set. + msgs + +\* vars is a tuple of all variables used in the specification. It is needed to +\* simplify fairness conditions definition. +vars == <> + +\* N is the number of validators. +N == Cardinality(RM) + +\* F is the number of validators that are allowed to be malicious. +F == (N - 1) \div 3 + +\* M is the number of validators that must function correctly. +M == N - F + +\* These assumptions are checked by the TLC model checker once at the start of +\* the model checking process. All the input data (declared constants) specified +\* in the "Model Overview" section must satisfy these constraints. +ASSUME + /\ RM \subseteq Nat + /\ N >= 4 + /\ 0 \in RM + /\ RMFault \subseteq RM + /\ RMDead \subseteq RM + /\ Cardinality(RMFault) <= F + /\ Cardinality(RMDead) <= F + /\ Cardinality(RMFault \cup RMDead) <= F + /\ MaxView \in Nat + /\ MaxView <= 2 + +\* RMStates is a set of records where each record holds the node state and +\* the node current view. +RMStates == [ + type: {"initialized", "prepareSent", "commitSent", "cv", "blockAccepted", "bad", "dead"}, + view : Nat + ] + +\* Messages is a set of records where each record holds the message type, +\* the message sender and sender's view by the moment when message was sent. +Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView"}, rm : RM, view : Nat] + +\* -------------- Useful operators -------------- + +\* IsPrimary is an operator defining whether provided node r is primary +\* for the current round from the r's point of view. It is a mapping +\* from RM to the set of {TRUE, FALSE}. +IsPrimary(r) == rmState[r].view % N = r + +\* GetPrimary is an operator defining mapping from round index to the RM that +\* is primary in this round. +GetPrimary(view) == CHOOSE r \in RM : view % N = r + +\* GetNewView returns new view number based on the previous node view value. +\* Current specifications only allows to increment view. +GetNewView(oldView) == oldView + 1 + +\* CountCommitted returns the number of nodes that have sent the Commit message +\* in the current round or in some other round. +CountCommitted(r) == Cardinality({rm \in RM : Cardinality({msg \in msgs : msg.rm = rm /\ msg.type = "Commit"}) /= 0}) + +\* MoreThanFNodesCommitted returns whether more than F nodes have been committed +\* in the current round (as the node r sees it). +\* +\* IMPORTANT NOTE: we intentionally do not add the "lost" nodes calculation to the specification, and here's +\* the reason: from the node's point of view we can't reliably check that some neighbour is completely +\* out of the network. It is possible that the node doesn't receive consensus messages from some other member +\* due to network delays. On the other hand, real nodes can go down at any time. The absence of the +\* member's message doesn't mean that the member is out of the network, we never can be sure about +\* that, thus, this information is unreliable and can't be trusted during the consensus process. +\* What can be trusted is whether there's a Commit message from some member was received by the node. +MoreThanFNodesCommitted(r) == CountCommitted(r) > F + +\* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest +\* message received from the current round's speaker (as the node r sees it). +PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in msgs + +\* -------------- Safety temporal formula -------------- + +\* Init is the initial predicate initializing values at the start of every +\* behaviour. +Init == + /\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]] + /\ msgs = {} + +\* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest. +RMSendPrepareRequest(r) == + /\ rmState[r].type = "initialized" + /\ IsPrimary(r) + /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] + /\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]} + /\ UNCHANGED <<>> + +\* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from +\* the primary node of the current round (view) and broadcasting PrepareResponse. +\* This step assumes that PrepareRequest always contains valid transactions and +\* signatures. +RMSendPrepareResponse(r) == + /\ \/ rmState[r].type = "initialized" + \* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage + \* as it is done in the code-level dBFT implementation by checking the NotAcceptingPayloadsDueToViewChanging + \* condition (see + \* https://github.com/nspcc-dev/dbft/blob/31c1bbdc74f2faa32ec9025062e3a4e2ccfd4214/dbft.go#L419 + \* and + \* https://github.com/neo-project/neo-modules/blob/d00d90b9c27b3d0c3c57e9ca1f560a09975df241/src/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs#L79). + \* However, we can't easily count the number of "lost" nodes in this specification to match precisely + \* the implementation. Moreover, we don't need it to be counted as the RMSendPrepareResponse enabling + \* condition specifies only the thing that may happen given some particular set of enabling conditions. + \* Thus, we've extended the NotAcceptingPayloadsDueToViewChanging condition to consider only MoreThanFNodesCommitted. + \* It should be noted that the logic of MoreThanFNodesCommittedOrLost can't be reliable in detecting lost nodes + \* (even with neo-project/neo#2057), because real nodes can go down at any time. See the comment above the MoreThanFNodesCommitted. + \/ /\ rmState[r].type = "cv" + /\ MoreThanFNodesCommitted(r) + /\ \neg IsPrimary(r) + /\ PrepareRequestSentOrReceived(r) + /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] + /\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]} + /\ UNCHANGED <<>> + +\* RMSendCommit describes node r sending Commit if there's enough PrepareResponse +\* messages. +RMSendCommit(r) == + /\ \/ rmState[r].type = "prepareSent" + \* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage, + \* see the related comment inside the RMSendPrepareResponse definition. + \/ /\ rmState[r].type = "cv" + /\ MoreThanFNodesCommitted(r) + /\ Cardinality({ + msg \in msgs : /\ (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest") + /\ msg.view = rmState[r].view + }) >= M + /\ PrepareRequestSentOrReceived(r) + /\ rmState' = [rmState EXCEPT ![r].type = "commitSent"] + /\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view]} + /\ UNCHANGED <<>> + +\* RMAcceptBlock describes node r collecting enough Commit messages and accepting +\* the block. +RMAcceptBlock(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ PrepareRequestSentOrReceived(r) + /\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) >= M + /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"] + /\ UNCHANGED <> + +\* RMSendChangeView describes node r sending ChangeView message on timeout. +RMSendChangeView(r) == + /\ \/ (rmState[r].type = "initialized" /\ \neg IsPrimary(r)) + \/ rmState[r].type = "prepareSent" + /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] + IN /\ cv \notin msgs + /\ rmState' = [rmState EXCEPT ![r].type = "cv"] + /\ msgs' = msgs \cup {[type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]} + +\* RMReceiveChangeView describes node r receiving enough ChangeView messages for +\* view changing. +RMReceiveChangeView(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ rmState[r].type /= "commitSent" + /\ Cardinality({ + rm \in RM : Cardinality({ + msg \in msgs : /\ msg.type = "ChangeView" + /\ msg.rm = rm + /\ GetNewView(msg.view) >= GetNewView(rmState[r].view) + }) /= 0 + }) >= M + /\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(rmState[r].view)] + /\ UNCHANGED <> + +\* RMBeBad describes the faulty node r that will send any kind of consensus message starting +\* from the step it's gone wild. This step is enabled only when RMFault is non-empty set. +RMBeBad(r) == + /\ r \in RMFault + /\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F + /\ rmState' = [rmState EXCEPT ![r].type = "bad"] + /\ UNCHANGED <> + +\* RMFaultySendCV describes sending CV message by the faulty node r. +RMFaultySendCV(r) == + /\ rmState[r].type = "bad" + /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] + IN /\ cv \notin msgs + /\ msgs' = msgs \cup {cv} + /\ UNCHANGED <> + +\* RMFaultyDoCV describes view changing by the faulty node r. +RMFaultyDoCV(r) == + /\ rmState[r].type = "bad" + /\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)] + /\ UNCHANGED <> + +\* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r. +RMFaultySendPReq(r) == + /\ rmState[r].type = "bad" + /\ IsPrimary(r) + /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] + IN /\ pReq \notin msgs + /\ msgs' = msgs \cup {pReq} + /\ UNCHANGED <> + +\* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r. +RMFaultySendPResp(r) == + /\ rmState[r].type = "bad" + /\ \neg IsPrimary(r) + /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] + IN /\ pResp \notin msgs + /\ msgs' = msgs \cup {pResp} + /\ UNCHANGED <> + +\* RMFaultySendCommit describes sending Commit message by the faulty node r. +RMFaultySendCommit(r) == + /\ rmState[r].type = "bad" + /\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] + IN /\ commit \notin msgs + /\ msgs' = msgs \cup {commit} + /\ UNCHANGED <> + +\* RMDie describes node r that was removed from the network at the particular step +\* of the behaviour. After this node r can't change its state and accept/send messages. +RMDie(r) == + /\ r \in RMDead + /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F + /\ rmState' = [rmState EXCEPT ![r].type = "dead"] + /\ UNCHANGED <> + +\* Terminating is an action that allows infinite stuttering to prevent deadlock on +\* behaviour termination. We consider termination to be valid if at least M nodes +\* has the block being accepted. +Terminating == + /\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M + /\ UNCHANGED <> + +\* Next is the next-state action describing the transition from the current state +\* to the next state of the behaviour. +Next == + \/ Terminating + \/ \E r \in RM: + RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r) + \/ RMAcceptBlock(r) \/ RMSendChangeView(r) \/ RMReceiveChangeView(r) + \/ RMDie(r) \/ RMBeBad(r) + \/ RMFaultySendCV(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) + +\* Safety is a temporal formula that describes the whole set of allowed +\* behaviours. It specifies only what the system MAY do (i.e. the set of +\* possible allowed behaviours for the system). It asserts only what may +\* happen; any behaviour that violates it does so at some point and +\* nothing past that point makes difference. +\* +\* E.g. this safety formula (applied standalone) allows the behaviour to end +\* with an infinite set of stuttering steps (those steps that DO NOT change +\* neither msgs nor rmState) and never reach the state where at least one +\* node is committed or accepted the block. +\* +\* To forbid such behaviours we must specify what the system MUST +\* do. It will be specified below with the help of fairness conditions in +\* the Fairness formula. +Safety == Init /\ [][Next]_vars + +\* -------------- Fairness temporal formula -------------- + +\* Fairness is a temporal assumptions under which the model is working. +\* Usually it specifies different kind of assumptions for each/some +\* subactions of the Next's state action, but the only think that bothers +\* us is preventing infinite stuttering at those steps where some of Next's +\* subactions are enabled. Thus, the only thing that we require from the +\* system is to keep take the steps until it's impossible to take them. +\* That's exactly how the weak fairness condition works: if some action +\* remains continuously enabled, it must eventually happen. +Fairness == WF_vars(Next) + +\* -------------- Specification -------------- + +\* The complete specification of the protocol written as a temporal formula. +Spec == Safety /\ Fairness + +\* -------------- Liveness temporal formula -------------- + +\* For every possible behaviour it's true that eventually (i.e. at least once +\* through the behaviour) block will be accepted. It is something that dBFT +\* must guarantee (an in practice this condition is violated). +TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M) + +\* A liveness temporal formula asserts only what must happen (i.e. specifies +\* what the system MUST do). Any behaviour can NOT violate it at ANY point; +\* there's always the rest of the behaviour that can always make the liveness +\* formula true; if there's no such behaviour than the liveness formula is +\* violated. The liveness formula is supposed to be checked as a property +\* by the TLC model checker. +Liveness == TerminationRequirement + +\* -------------- ModelConstraints -------------- + +\* MaxViewConstraint is a state predicate restricting the number of possible +\* behaviour states. It is needed to reduce model checking time and prevent +\* the model graph size explosion. This formulae must be specified at the +\* "State constraint" section of the "Additional Spec Options" section inside +\* the model overview. +MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView + /\ \A msg \in msgs : msg.view <= MaxView + +\* -------------- Invariants of the specification -------------- + +\* Model invariant is a state predicate (statement) that must be true for +\* every step of every reachable behaviour. Model invariant is supposed to +\* be checked as an Invariant by the TLC Model Checker. + +\* TypeOK is a type-correctness invariant. It states that all elements of +\* specification variables must have the proper type throughout the behaviour. +TypeOK == + /\ rmState \in [RM -> RMStates] + /\ msgs \subseteq Messages + +\* InvTwoBlocksAccepted states that there can't be two different blocks accepted in +\* the two different views, i.e. dBFT must not allow forks. +InvTwoBlocksAccepted == \A r1 \in RM: + \A r2 \in RM \ {r1}: + \/ rmState[r1].type /= "blockAccepted" + \/ rmState[r2].type /= "blockAccepted" + \/ rmState[r1].view = rmState[r2].view + +\* InvFaultNodesCount states that there can be F faulty or dead nodes at max. +InvFaultNodesCount == Cardinality({ + r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead" + }) <= F + +\* This theorem asserts the truth of the temporal formula whose meaning is that +\* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are +\* the invariants of the specification Spec. This theorem is not supposed to be +\* checked by the TLC model checker, it's here for the reader's understanding of +\* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount. +THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvFaultNodesCount) + +============================================================================= +\* Modification History +\* Last modified Mon Mar 06 15:36:57 MSK 2023 by root +\* Last modified Fri Feb 17 15:47:41 MSK 2023 by anna +\* Last modified Sat Jan 21 01:26:16 MSK 2023 by rik +\* Created Thu Dec 15 16:06:17 MSK 2022 by anna diff --git a/formal-models/dbft/dbft___AllGoodModel.launch b/formal-models/dbft/dbft___AllGoodModel.launch new file mode 100644 index 0000000..52b9984 --- /dev/null +++ b/formal-models/dbft/dbft___AllGoodModel.launch @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/formal-models/dbft2.1_centralizedCV/dbftCentralizedCV.tla b/formal-models/dbft2.1_centralizedCV/dbftCentralizedCV.tla new file mode 100644 index 0000000..5e1d626 --- /dev/null +++ b/formal-models/dbft2.1_centralizedCV/dbftCentralizedCV.tla @@ -0,0 +1,507 @@ +-------------------------------- MODULE dbftCentralizedCV -------------------------------- + +EXTENDS + Integers, + FiniteSets + +CONSTANTS + \* RM is the set of consensus node indexes starting from 0. + \* Example: {0, 1, 2, 3} + RM, + + \* RMFault is a set of consensus node indexes that are allowed to become + \* FAULT in the middle of every considered behavior and to send any + \* consensus message afterwards. RMFault must be a subset of RM. An empty + \* set means that all nodes are good in every possible behaviour. + \* Examples: {0} + \* {1, 3} + \* {} + RMFault, + + \* RMDead is a set of consensus node indexes that are allowed to die in the + \* middle of every behaviour and do not send any message afterwards. RMDead + \* must be a subset of RM. An empty set means that all nodes are alive and + \* responding in in every possible behaviour. RMDead may intersect the + \* RMFault set which means that node which is in both RMDead and RMFault + \* may become FAULT and send any message starting from some step of the + \* particular behaviour and may also die in the same behaviour which will + \* prevent it from sending any message. + \* Examples: {0} + \* {3, 2} + \* {} + RMDead, + + \* MaxView is the maximum allowed view to be considered (starting from 0, + \* including the MaxView itself). This constraint was introduced to reduce + \* the number of possible model states to be checked. It is recommended to + \* keep this setting not too high (< N is highly recommended). + \* Example: 2 + MaxView + +VARIABLES + \* rmState is a set of consensus node states. It is represented by the + \* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is + \* the state of the r-th consensus node at the current step. + rmState, + + \* msgs is the shared pool of messages sent to the network by consensus nodes. + \* It is represented by a subset of Messages set. + msgs, + + \* blockAccepted holds the view number when the accepted block's proposal + \* was firstly created for each consensus node. It is represented by a + \* mapping RM -> Nat and needed for InvTwoBlocksAcceptedAdvanced invariant + \* evaluation. + blockAccepted + +\* vars is a tuple of all variables used in the specification. It is needed to +\* simplify fairness conditions definition. +vars == <> + +\* N is the number of validators. +N == Cardinality(RM) + +\* F is the number of validators that are allowed to be malicious. +F == (N - 1) \div 3 + +\* M is the number of validators that must function correctly. +M == N - F + +\* These assumptions are checked by the TLC model checker once at the start of +\* the model checking process. All the input data (declared constants) specified +\* in the "Model Overview" section must satisfy these constraints. +ASSUME + /\ RM \subseteq Nat + /\ N >= 4 + /\ 0 \in RM + /\ RMFault \subseteq RM + /\ RMDead \subseteq RM + /\ Cardinality(RMFault) <= F + /\ Cardinality(RMDead) <= F + /\ Cardinality(RMFault \cup RMDead) <= F + /\ MaxView \in Nat + /\ MaxView <= 2 + +\* RMStates is a set of records where each record holds the node state and +\* the node current view. +RMStates == [ + type: {"initialized", "prepareSent", "commitSent", "blockAccepted", "cv1", "cv2", "bad", "dead"}, + view : Nat + ] +\* Messages is a set of records where each record holds the message type, +\* the message sender, sender's view by the moment when message was sent +\* (view field), sender's view by the first moment the PrepareRequest was +\* originally proposed (sourceView field) and the target view for +\* ChangeView[1,2]/DoChangeView[1,2] messages (targetView field). +Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView1", "ChangeView2", "DoChangeView1", "DoChangeView2"}, rm : RM, view : Nat, targetView : Nat, sourceView : Nat] + +\* -------------- Useful operators -------------- + +\* IsPrimaryInTheView is an operator defining whether provided node r +\* is primary in the consensus round view from the r's point of view. +\* It is a mapping from RM to the set of {TRUE, FALSE}. +IsPrimaryInTheView(r, view) == view % N = r + +\* IsPrimary is an operator defining whether provided node r is primary +\* for the current round from the r's point of view. It is a mapping +\* from RM to the set of {TRUE, FALSE}. +IsPrimary(r) == IsPrimaryInTheView(r, rmState[r].view) + +\* GetPrimary is an operator defining mapping from round index to the RM that +\* is primary in this round. +GetPrimary(view) == CHOOSE r \in RM : view % N = r + +\* GetNewView returns new view number based on the previous node view value. +\* Current specifications only allows to increment view. +GetNewView(oldView) == oldView + 1 + +\* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest +\* message received from the current round's speaker (as the node r sees it). +PrepareRequestSentOrReceived(r) == Cardinality({msg \in msgs : msg.type = "PrepareRequest"/\ msg.rm = GetPrimary(rmState[r].view) /\ msg.view = rmState[r].view}) /= 0 + +\* -------------- Safety temporal formula -------------- + +\* Init is the initial predicate initializing values at the start of every +\* behaviour. +Init == + /\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]] + /\ msgs = {} + /\ blockAccepted = [r \in RM |-> 0] + +\* RMSendPrepareRequest describes the primary node r originally broadcasting PrepareRequest. +RMSendPrepareRequest(r) == + /\ rmState[r].type = "initialized" + /\ IsPrimary(r) + /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] + /\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view, targetView |-> 0, sourceView |-> rmState[r].view]} + /\ UNCHANGED <> + +\* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from +\* the primary node of the current round (view) and broadcasting PrepareResponse. +\* This step assumes that PrepareRequest always contains valid transactions and +\* signatures. +RMSendPrepareResponse(r) == + /\ rmState[r].type = "initialized" + /\ \neg IsPrimary(r) + /\ PrepareRequestSentOrReceived(r) + /\ LET pReq == CHOOSE msg \in msgs : msg.type = "PrepareRequest" /\ msg.rm = GetPrimary(rmState[r].view) /\ msg.view = rmState[r].view + IN /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] + /\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view, targetView |-> 0, sourceView |-> pReq.sourceView]} + /\ UNCHANGED <> + + +\* RMSendCommit describes node r sending Commit if there's enough PrepareResponse +\* messages. +RMSendCommit(r) == + /\ rmState[r].type = "prepareSent" + /\ Cardinality({ + msg \in msgs : (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest") /\ msg.view = rmState[r].view + }) >= M + /\ PrepareRequestSentOrReceived(r) + /\ LET pReq == CHOOSE msg \in msgs : msg.type = "PrepareRequest" /\ msg.rm = GetPrimary(rmState[r].view) /\ msg.view = rmState[r].view + IN /\ rmState' = [rmState EXCEPT ![r].type = "commitSent"] + /\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view, targetView |-> 0, sourceView |-> pReq.sourceView]} + /\ UNCHANGED <> + +\* RMAcceptBlock describes node r collecting enough Commit messages and accepting +\* the block. +RMAcceptBlock(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ PrepareRequestSentOrReceived(r) + /\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) >= M + /\ LET pReq == CHOOSE msg \in msgs : msg.type = "PrepareRequest" /\ msg.rm = GetPrimary(rmState[r].view) /\ msg.view = rmState[r].view + IN /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"] + /\ blockAccepted' = [blockAccepted EXCEPT ![r] = pReq.sourceView] + /\ UNCHANGED <> + +\* FetchBlock describes node r that fetches the accepted block from some other node. +RMFetchBlock(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ \E rmAccepted \in RM : /\ rmState[rmAccepted].type = "blockAccepted" + /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].view = rmState[rmAccepted].view] + /\ blockAccepted' = [blockAccepted EXCEPT ![r] = blockAccepted[rmAccepted]] + /\ UNCHANGED <> + +\* RMSendChangeView1 describes node r sending ChangeView1 message on timeout +\* during waiting for PrepareResponse or node r in "cv2" state receiving M +\* messages from the I stage not more than F of them are preparations. +RMSendChangeView1(r) == + /\ \/ /\ rmState[r].type = "initialized" + /\ \neg IsPrimary(r) + \/ /\ rmState[r].type = "cv2" + /\ Cardinality({msg \in msgs : /\ (msg.type = "ChangeView1" \/ msg.type = "PrepareRequest" \/ msg.type = "PrepareResponse") + /\ msg.view = rmState[r].view + }) >= M + /\ Cardinality({msg \in msgs : /\ (msg.type = "PrepareRequest" \/ msg.type = "PrepareResponse") + /\ msg.view = rmState[r].view + }) <= F + /\ rmState' = [rmState EXCEPT ![r].type = "cv1"] + /\ msgs' = msgs \cup {[type |-> "ChangeView1", rm |-> r, view |-> rmState[r].view, targetView |-> GetNewView(rmState[r].view), sourceView |-> rmState[r].view]} + /\ UNCHANGED <> + +\* RMSendChangeView1FromCV1 describes node r sending ChangeView1 message on timeout +\* during waiting for DoCV1 signal from the next primary. +RMSendChangeView1FromCV1(r) == + /\ rmState[r].type = "cv1" + /\ LET cv1s == {msg \in msgs : msg.type = "ChangeView1" /\ msg.view = rmState[r].view} + myCV1s == {msg \in cv1s : msg.rm = r} + myBestCV1 == CHOOSE msg \in myCV1s : \A other \in myCV1s : msg.targetView >= other.targetView + IN /\ Cardinality({msg \in cv1s : msg.targetView = myBestCV1.targetView}) >= M + /\ \neg IsPrimaryInTheView(r, myBestCV1.targetView) + /\ msgs' = msgs \cup {[type |-> "ChangeView1", rm |-> r, view |-> rmState[r].view, targetView |-> GetNewView(myBestCV1.targetView), sourceView |-> rmState[r].view]} + /\ UNCHANGED <> + +\* RMSendChangeView2 describes node r sending ChangeView message on timeout +\* during waiting for enough Prepare messages OR from CV1 after not receiving +\* enough ChangeView1 messages. +RMSendChangeView2(r) == + /\ \/ rmState[r].type = "prepareSent" + \/ rmState[r].type = "commitSent" + \/ /\ rmState[r].type = "cv1" + /\ Cardinality({msg \in msgs : /\ (msg.type = "PrepareRequest" \/ msg.type = "PrepareResponse" \/ msg.type = "ChangeView1") + /\ msg.view = rmState[r].view + /\ (msg.targetView = 0 \/ msg.targetView = GetNewView(rmState[r].view)) + }) >= M + /\ Cardinality({msg \in msgs : /\ (msg.type = "PrepareRequest" \/ msg.type = "PrepareResponse") + /\ msg.view = rmState[r].view + /\ (msg.targetView = 0 \/ msg.targetView = GetNewView(rmState[r].view)) + }) > F + /\ LET pReq == CHOOSE msg \in msgs : msg.type = "PrepareRequest" /\ msg.rm = GetPrimary(rmState[r].view) /\ msg.view = rmState[r].view + IN /\ rmState' = [rmState EXCEPT ![r].type = "cv2"] + /\ msgs' = msgs \cup {[type |-> "ChangeView2", rm |-> r, view |-> rmState[r].view, targetView |-> GetNewView(rmState[r].view), sourceView |-> pReq.sourceView]} + /\ UNCHANGED <> + +\* RMSendChangeView2FromCV2 describes node r sending ChangeView2 message on timeout +\* during waiting for DoCV2 signal from the next primary. +RMSendChangeView2FromCV2(r) == + /\ rmState[r].type = "cv2" + /\ LET cv2s == {msg \in msgs : msg.type = "ChangeView2" /\ msg.view = rmState[r].view} + myCV2s == {msg \in cv2s : msg.rm = r} + myBestCV2 == CHOOSE msg \in myCV2s : \A other \in myCV2s : msg.targetView >= other.targetView + pReq == CHOOSE msg \in msgs : msg.type = "PrepareRequest" /\ msg.rm = GetPrimary(rmState[r].view) /\ msg.view = rmState[r].view + IN /\ Cardinality({msg \in cv2s : msg.targetView = myBestCV2.targetView}) >= M + /\ \neg IsPrimaryInTheView(r, myBestCV2.targetView) + /\ msgs' = msgs \cup {[type |-> "ChangeView2", rm |-> r, view |-> rmState[r].view, targetView |-> GetNewView(myBestCV2.targetView), sourceView |-> pReq.sourceView]} + /\ UNCHANGED <> + +\* RMSendDoCV1ByLeader describes node r that collects enough ChangeView1 messages +\* with target view such that the node r is leader in this view. The leader r +\* broadcasts DoChangeView1 message and the newly-created PrepareRequest message +\* for this view. +RMSendDoCV1ByLeader(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ LET cv1s == {msg \in msgs : msg.type = "ChangeView1" /\ msg.view = rmState[r].view} + followersCV1s == {msg \in cv1s : IsPrimaryInTheView(r, msg.targetView)} \* TODO: this condition won't work starting from N+1 view! + targetView == CHOOSE x \in 1..(MaxView+1) : IsPrimaryInTheView(r, x) + IN /\ Cardinality(followersCV1s) >= M + /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent", ![r].view = targetView] + /\ msgs' = msgs \cup {[type |-> "DoChangeView1", rm |-> r, view |-> rmState[r].view, targetView |-> targetView, sourceView |-> targetView], [type |-> "PrepareRequest", rm |-> r, view |-> targetView, targetView |-> 0, sourceView |-> targetView]} + /\ UNCHANGED <> + +\* RMSendDoCV2ByLeader describes node r that collects enough ChangeView2 messages +\* with target view such that the node r is leader in this view. The leader r +\* broadcasts DoChangeView2 message and the old PrepareRequest message that +\* was migrated from the previous view without changes. +RMSendDoCV2ByLeader(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ LET cv2s == {msg \in msgs : msg.type = "ChangeView2" /\ msg.view = rmState[r].view} + followersCV2s == {msg \in cv2s : IsPrimaryInTheView(r, msg.targetView)} + targetView == CHOOSE x \in 1..(MaxView+1) : IsPrimaryInTheView(r, x) + IN /\ Cardinality(followersCV2s) >= M + /\ LET pReq == CHOOSE msg \in msgs : msg.type = "PrepareRequest" /\ msg.rm = GetPrimary(rmState[r].view) /\ msg.view = rmState[r].view + IN /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent", ![r].view = targetView] + /\ msgs' = msgs \cup {[type |-> "DoChangeView2", rm |-> r, view |-> rmState[r].view, targetView |-> targetView, sourceView |-> pReq.sourceView], [type |-> "PrepareRequest", rm |-> r, view |-> targetView, targetView |-> 0, sourceView |-> pReq.sourceView]} + /\ UNCHANGED <> + +\* RMReceiveDoCV1FromLeader descibes node r that receives DoChangeView1 message from +\* the leader of the target DoCV1's view and changes its view. +RMReceiveDoCV1FromLeader(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "blockAccepted" + /\ rmState[r].type /= "dead" + /\ Cardinality({msg \in msgs : msg.type = "DoChangeView1" /\ msg.targetView > rmState[r].view}) /= 0 + /\ LET doCV1s == {msg \in msgs : msg.type = "DoChangeView1" /\ msg.targetView > rmState[r].view} + latestDoCV1 == CHOOSE msg \in doCV1s : \A other \in doCV1s : msg.targetView >= other.targetView + IN /\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = latestDoCV1.targetView] + /\ UNCHANGED <> + +\* RMReceiveDoCV2FromLeader descibes node r that receives DoChangeView2 message from +\* the leader of the target DoCV2's view and changes its view. +RMReceiveDoCV2FromLeader(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "blockAccepted" + /\ rmState[r].type /= "dead" + /\ Cardinality({msg \in msgs : msg.type = "DoChangeView2" /\ msg.targetView > rmState[r].view}) /= 0 + /\ LET doCV2s == {msg \in msgs : msg.type = "DoChangeView2" /\ msg.targetView > rmState[r].view} + latestDoCV2 == CHOOSE msg \in doCV2s : \A other \in doCV2s : msg.targetView >= other.targetView + IN /\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = latestDoCV2.targetView] + /\ UNCHANGED <> + +\* RMBeBad describes the faulty node r that will send any kind of consensus message starting +\* from the step it's gone wild. This step is enabled only when RMFault is non-empty set. +RMBeBad(r) == + /\ r \in RMFault + /\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F + /\ rmState' = [rmState EXCEPT ![r].type = "bad"] + /\ UNCHANGED <> + +\* RMFaultySendCV1 describes sending CV1 message by the faulty node r. To reduce +\* the number of reachable states, the target view of this message is restricted by +\* the next one. +RMFaultySendCV1(r) == + /\ rmState[r].type = "bad" + /\ LET cv == [type |-> "ChangeView1", rm |-> r, view |-> rmState[r].view, targetView |-> GetNewView(rmState[r].view), sourceView |-> rmState[r].view] + IN /\ cv \notin msgs + /\ msgs' = msgs \cup {cv} + /\ UNCHANGED <> + +\* RMFaultySendCV2 describes sending CV2 message by the faulty node r. To reduce +\* the number of reachable states, the target view of this message is restricted by +\* the next one; the source view of this message is restricted by the current one. +RMFaultySendCV2(r) == + /\ rmState[r].type = "bad" + /\ LET cv == [type |-> "ChangeView2", rm |-> r, view |-> rmState[r].view, targetView |-> GetNewView(rmState[r].view), sourceView |-> rmState[r].view] + IN /\ cv \notin msgs + /\ msgs' = msgs \cup {cv} + /\ UNCHANGED <> + +\* RMFaultyDoCV describes view changing by the faulty node r. +RMFaultyDoCV(r) == + /\ rmState[r].type = "bad" + /\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)] + /\ UNCHANGED <> + +\* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r. +\* To reduce the number of reachable states, the sourceView is always restricted by the +\* current r's view. +RMFaultySendPReq(r) == + /\ rmState[r].type = "bad" + /\ IsPrimary(r) + /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view, targetView |-> 0, sourceView |-> rmState[r].view] + IN /\ pReq \notin msgs + /\ msgs' = msgs \cup {pReq} + /\ UNCHANGED <> + +\* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r. +\* To reduce the number of reachable states, the sourceView is always restricted by the +\* current r's view. +RMFaultySendPResp(r) == + /\ rmState[r].type = "bad" + /\ \neg IsPrimary(r) + /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view, targetView |-> 0, sourceView |-> rmState[r].view] + IN /\ pResp \notin msgs + /\ msgs' = msgs \cup {pResp} + /\ UNCHANGED <> + +\* RMFaultySendCommit describes sending Commit message by the faulty node r. +\* To reduce the number of reachable states, the sourceView is always restricted by the +\* current r's view. +RMFaultySendCommit(r) == + /\ rmState[r].type = "bad" + /\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view, targetView |-> 0, sourceView |-> rmState[r].view] + IN /\ commit \notin msgs + /\ msgs' = msgs \cup {commit} + /\ UNCHANGED <> + +\* We don't describe sending DoCV messages by faulty node, because it can't +\* actually produce other than valid message, and valid message sending is described +\* in the "good" node specification. We also don't describe receiving the DoCV message +\* by the faulty node because it has a separate RMFaultyDoCV action enabled. + +\* RMDie describes node r that was removed from the network at the particular step +\* of the behaviour. After this node r can't change its state and accept/send messages. +RMDie(r) == + /\ r \in RMDead + /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F + /\ rmState' = [rmState EXCEPT ![r].type = "dead"] + /\ UNCHANGED <> + +\* Terminating is an action that allows infinite stuttering to prevent deadlock on +\* behaviour termination. We consider termination to be valid if at least M nodes +\* have the block being accepted. +Terminating == + /\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M + /\ UNCHANGED vars + + +\* Next is the next-state action describing the transition from the current state +\* to the next state of the behaviour. +Next == + \/ Terminating + \/ \E r \in RM: + RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r) + \/ RMAcceptBlock(r) + \/ RMSendChangeView1(r) \/ RMSendChangeView1FromCV1(r) + \/ RMSendChangeView2(r) \/ RMSendChangeView2FromCV2(r) + \/ RMSendDoCV1ByLeader(r) \/ RMReceiveDoCV1FromLeader(r) + \/ RMSendDoCV2ByLeader(r) \/ RMReceiveDoCV2FromLeader(r) + \/ RMBeBad(r) + \/ RMFaultySendCV1(r) \/ RMFaultySendCV2(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) + \/ RMDie(r) \/ RMFetchBlock(r) + +\* Safety is a temporal formula that describes the whole set of allowed +\* behaviours. It specifies only what the system MAY do (i.e. the set of +\* possible allowed behaviours for the system). It asserts only what may +\* happen; any behaviour that violates it does so at some point and +\* nothing past that point makes difference. +\* +\* E.g. this safety formula (applied standalone) allows the behaviour to end +\* with an infinite set of stuttering steps (those steps that DO NOT change +\* neither msgs nor rmState) and never reach the state where at least one +\* node is committed or accepted the block. +\* +\* To forbid such behaviours we must specify what the system MUST +\* do. It will be specified below with the help of fairness conditions in +\* the Fairness formula. +Safety == Init /\ [][Next]_vars + + +\* -------------- Fairness temporal formula -------------- + +\* Fairness is a temporal assumptions under which the model is working. +\* Usually it specifies different kind of assumptions for each/some +\* subactions of the Next's state action, but the only think that bothers +\* us is preventing infinite stuttering at those steps where some of Next's +\* subactions are enabled. Thus, the only thing that we require from the +\* system is to keep take the steps until it's impossible to take them. +\* That's exactly how the weak fairness condition works: if some action +\* remains continuously enabled, it must eventually happen. +Fairness == WF_vars(Next) + +\* -------------- Specification -------------- + +\* The complete specification of the protocol written as a temporal formula. +Spec == Safety /\ Fairness + +\* -------------- Liveness temporal formula -------------- + +\* For every possible behaviour it's true that eventually (i.e. at least once +\* through the behaviour) block will be accepted. It is something that dBFT +\* must guarantee (an in practice this condition is violated). +TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M) + +\* A liveness temporal formula asserts only what must happen (i.e. specifies +\* what the system MUST do). Any behaviour can NOT violate it at ANY point; +\* there's always the rest of the behaviour that can always make the liveness +\* formula true; if there's no such behaviour than the liveness formula is +\* violated. The liveness formula is supposed to be checked as a property +\* by the TLC model checker. +Liveness == TerminationRequirement + +\* -------------- ModelConstraints -------------- + +\* MaxViewConstraint is a state predicate restricting the number of possible +\* behaviour states. It is needed to reduce model checking time and prevent +\* the model graph size explosion. This formulae must be specified at the +\* "State constraint" section of the "Additional Spec Options" section inside +\* the model overview. +MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView + /\ \A msg \in msgs : msg.targetView = 0 \/ msg.targetView <= MaxView + 1 + +\* -------------- Invariants of the specification -------------- + +\* Model invariant is a state predicate (statement) that must be true for +\* every step of every reachable behaviour. Model invariant is supposed to +\* be checked as an Invariant by the TLC Model Checker. + +\* TypeOK is a type-correctness invariant. It states that all elements of +\* specification variables must have the proper type throughout the behaviour. +TypeOK == + /\ rmState \in [RM -> RMStates] + /\ msgs \subseteq Messages + /\ blockAccepted \in [RM -> Nat] + +\* InvTwoBlocksAcceptedAdvanced ensures that the proposed and accepted block +\* originally comes from the same view for every node that has the block +\* being accepted. +InvTwoBlocksAcceptedAdvanced == \A r1 \in RM: + \A r2 \in RM \ {r1}: + \/ rmState[r1].type /= "blockAccepted" + \/ rmState[r2].type /= "blockAccepted" + \/ blockAccepted[r1] = blockAccepted[r2] + +\* InvFaultNodesCount states that there can be F faulty or dead nodes at max. +InvFaultNodesCount == Cardinality({ + r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead" + }) <= F + +\* This theorem asserts the truth of the temporal formula whose meaning is that +\* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are +\* the invariants of the specification Spec. This theorem is not supposed to be +\* checked by the TLC model checker, it's here for the reader's understanding of +\* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount. +THEOREM Spec => [](TypeOK /\ InvTwoBlocksAcceptedAdvanced /\ InvFaultNodesCount) +============================================================================= +\* Modification History +\* Last modified Fri Mar 03 10:51:05 MSK 2023 by root +\* Last modified Wed Feb 15 15:43:25 MSK 2023 by anna +\* Last modified Mon Jan 23 21:49:06 MSK 2023 by rik +\* Created Thu Dec 15 16:06:17 MSK 2022 by anna \ No newline at end of file diff --git a/formal-models/dbft2.1_centralizedCV/dbftCentralizedCV___AllGoodModel.launch b/formal-models/dbft2.1_centralizedCV/dbftCentralizedCV___AllGoodModel.launch new file mode 100644 index 0000000..f7a19d4 --- /dev/null +++ b/formal-models/dbft2.1_centralizedCV/dbftCentralizedCV___AllGoodModel.launch @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/formal-models/dbft2.1_threeStagedCV/dbftCV3.tla b/formal-models/dbft2.1_threeStagedCV/dbftCV3.tla new file mode 100644 index 0000000..536934c --- /dev/null +++ b/formal-models/dbft2.1_threeStagedCV/dbftCV3.tla @@ -0,0 +1,427 @@ +-------------------------------- MODULE dbftCV3 -------------------------------- + +EXTENDS + Integers, + FiniteSets + +CONSTANTS + \* RM is the set of consensus node indexes starting from 0. + \* Example: {0, 1, 2, 3} + RM, + + \* RMFault is a set of consensus node indexes that are allowed to become + \* FAULT in the middle of every considered behavior and to send any + \* consensus message afterwards. RMFault must be a subset of RM. An empty + \* set means that all nodes are good in every possible behaviour. + \* Examples: {0} + \* {1, 3} + \* {} + RMFault, + + \* RMDead is a set of consensus node indexes that are allowed to die in the + \* middle of every behaviour and do not send any message afterwards. RMDead + \* must be a subset of RM. An empty set means that all nodes are alive and + \* responding in in every possible behaviour. RMDead may intersect the + \* RMFault set which means that node which is in both RMDead and RMFault + \* may become FAULT and send any message starting from some step of the + \* particular behaviour and may also die in the same behaviour which will + \* prevent it from sending any message. + \* Examples: {0} + \* {3, 2} + \* {} + RMDead, + + \* MaxView is the maximum allowed view to be considered (starting from 0, + \* including the MaxView itself). This constraint was introduced to reduce + \* the number of possible model states to be checked. It is recommended to + \* keep this setting not too high (< N is highly recommended). + \* Example: 2 + MaxView + +VARIABLES + \* rmState is a set of consensus node states. It is represented by the + \* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is + \* the state of the r-th consensus node at the current step. + rmState, + + \* msgs is the shared pool of messages sent to the network by consensus nodes. + \* It is represented by a subset of Messages set. + msgs + +\* vars is a tuple of all variables used in the specification. It is needed to +\* simplify fairness conditions definition. +vars == <> + +\* N is the number of validators. +N == Cardinality(RM) + +\* F is the number of validators that are allowed to be malicious. +F == (N - 1) \div 3 + +\* M is the number of validators that must function correctly. +M == N - F + +\* These assumptions are checked by the TLC model checker once at the start of +\* the model checking process. All the input data (declared constants) specified +\* in the "Model Overview" section must satisfy these constraints. +ASSUME + /\ RM \subseteq Nat + /\ N >= 4 + /\ 0 \in RM + /\ RMFault \subseteq RM + /\ RMDead \subseteq RM + /\ Cardinality(RMFault) <= F + /\ Cardinality(RMDead) <= F + /\ Cardinality(RMFault \cup RMDead) <= F + /\ MaxView \in Nat + /\ MaxView <= 2 + +\* RMStates is a set of records where each record holds the node state and +\* the node current view. +RMStates == [ + type: {"initialized", "prepareSent", "commitSent", "blockAccepted", "cv1", "cv2", "cv3", "bad", "dead"}, + view : Nat + ] + +\* Messages is a set of records where each record holds the message type, +\* the message sender and sender's view by the moment when message was sent. +Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView1", "ChangeView2", "ChangeView3"}, rm : RM, view : Nat] + +\* -------------- Useful operators -------------- + +\* IsPrimary is an operator defining whether provided node r is primary +\* for the current round from the r's point of view. It is a mapping +\* from RM to the set of {TRUE, FALSE}. +IsPrimary(r) == rmState[r].view % N = r + +\* GetPrimary is an operator defining mapping from round index to the RM that +\* is primary in this round. +GetPrimary(view) == CHOOSE r \in RM : view % N = r + +\* GetNewView returns new view number based on the previous node view value. +\* Current specifications only allows to increment view. +GetNewView(oldView) == oldView + 1 + +\* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest +\* message received from the current round's speaker (as the node r sees it). +PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in msgs + +\* -------------- Safety temporal formula -------------- + +\* Init is the initial predicate initializing values at the start of every +\* behaviour. +Init == + /\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]] + /\ msgs = {} + +\* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest. +RMSendPrepareRequest(r) == + /\ rmState[r].type = "initialized" + /\ IsPrimary(r) + /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] + /\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]} + /\ UNCHANGED <<>> + +\* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from +\* the primary node of the current round (view) and broadcasting PrepareResponse. +\* This step assumes that PrepareRequest always contains valid transactions and +\* signatures. +RMSendPrepareResponse(r) == + /\ rmState[r].type = "initialized" + /\ \neg IsPrimary(r) + /\ PrepareRequestSentOrReceived(r) + /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] + /\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]} + /\ UNCHANGED <<>> + +\* RMSendCommit describes node r sending Commit if there's enough PrepareRequest/PrepareResponse +\* messages and no node has sent the ChangeView3 message. It is possible to send the Commit after +\* the ChangeView1 or ChangeView2 message was sent with additional constraints. +RMSendCommit(r) == + /\ \/ rmState[r].type = "prepareSent" + \/ rmState[r].type = "cv1" + \/ /\ rmState[r].type = "cv2" + /\ Cardinality({ + msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view + }) > F + /\ Cardinality({ + msg \in msgs : (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest") /\ msg.view = rmState[r].view + }) >= M + /\ Cardinality({ + msg \in msgs : msg.type = "ChangeView3" /\ msg.view = rmState[r].view + }) = 0 + /\ PrepareRequestSentOrReceived(r) + /\ rmState' = [rmState EXCEPT ![r].type = "commitSent"] + /\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view]} + /\ UNCHANGED <<>> + +\* RMAcceptBlock describes node r collecting enough Commit messages and accepting +\* the block. +RMAcceptBlock(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ PrepareRequestSentOrReceived(r) + /\ Cardinality({ + msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view + }) >= M + /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"] + /\ UNCHANGED <> + +\* FetchBlock describes node r that fetches the accepted block from some other node. +RMFetchBlock(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ \E rmAccepted \in RM : /\ rmState[rmAccepted].type = "blockAccepted" + /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].view = rmState[rmAccepted].view] + /\ UNCHANGED <> + +\* RMSendChangeView1 describes node r sending ChangeView1 message on timeout. +\* Only non-primary node is allowed to send ChangeView1 message, as the primary +\* must send the PrepareRequest if the timer fires. +RMSendChangeView1(r) == + /\ rmState[r].type = "initialized" + /\ \neg IsPrimary(r) + /\ rmState' = [rmState EXCEPT ![r].type = "cv1"] + /\ msgs' = msgs \cup {[type |-> "ChangeView1", rm |-> r, view |-> rmState[r].view]} + +\* RMSendChangeView2 describes node r sending ChangeView2 message on timeout either from +\* "cv1" state or after the node has sent the PrepareRequest or PrepareResponse message. +RMSendChangeView2(r) == + /\ \/ /\ rmState[r].type = "prepareSent" + /\ Cardinality({ + msg \in msgs : msg.type = "ChangeView1" /\ msg.view = rmState[r].view + }) > 0 + \/ rmState[r].type = "cv1" + /\ Cardinality({ + msg \in msgs : (msg.type = "ChangeView1" \/ msg.type = "PrepareRequest" \/ msg.type = "PrepareResponse") /\ msg.view = rmState[r].view + }) >= M + /\ \/ Cardinality({ + msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view + }) <= F + \/ Cardinality({ + msg \in msgs : msg.type = "ChangeView3" /\ msg.view = rmState[r].view + }) > 0 + /\ rmState' = [rmState EXCEPT ![r].type = "cv2"] + /\ msgs' = msgs \cup {[type |-> "ChangeView2", rm |-> r, view |-> rmState[r].view]} + +\* RMSendChangeView3 describes node r sending ChangeView3 message on timeout either from +\* "cv2" state or after the node has sent the Commit message. +RMSendChangeView3(r) == + /\ \/ rmState[r].type = "cv2" + \/ rmState[r].type = "commitSent" + /\ Cardinality({msg \in msgs : (msg.type = "ChangeView2" \/ msg.type = "Commit") /\ msg.view = rmState[r].view}) >= M + /\ Cardinality({msg \in msgs : (msg.type = "ChangeView2") /\ msg.view = rmState[r].view}) > 0 + /\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) <= F + /\ rmState' = [rmState EXCEPT ![r].type = "cv3"] + /\ msgs' = msgs \cup {[type |-> "ChangeView3", rm |-> r, view |-> rmState[r].view]} + +\* RMReceiveChangeView describes node r receiving enough ChangeView[1,2,3] messages for +\* view changing. +RMReceiveChangeView(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ \/ Cardinality({rm \in RM : Cardinality({msg \in msgs : /\ msg.rm = rm + /\ msg.type = "ChangeView1" + /\ GetNewView(msg.view) >= GetNewView(rmState[r].view) + }) # 0 + }) >= M + \/ Cardinality({rm \in RM : Cardinality({msg \in msgs : /\ msg.rm = rm + /\ msg.type = "ChangeView2" + /\ GetNewView(msg.view) >= GetNewView(rmState[r].view) + }) # 0 + }) >= M + \/ Cardinality({rm \in RM : Cardinality({msg \in msgs : /\ msg.rm = rm + /\ msg.type = "ChangeView3" + /\ GetNewView(msg.view) >= GetNewView(rmState[r].view) + }) # 0 + }) >= M + /\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(rmState[r].view)] + /\ UNCHANGED <> + +\* RMBeBad describes the faulty node r that will send any kind of consensus message starting +\* from the step it's gone wild. This step is enabled only when RMFault is non-empty set. +RMBeBad(r) == + /\ r \in RMFault + /\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F + /\ rmState' = [rmState EXCEPT ![r].type = "bad"] + /\ UNCHANGED <> + +\* RMFaultySendCV describes sending CV1 message by the faulty node r. +RMFaultySendCV1(r) == + /\ rmState[r].type = "bad" + /\ LET cv == [type |-> "ChangeView1", rm |-> r, view |-> rmState[r].view] + IN /\ cv \notin msgs + /\ msgs' = msgs \cup {cv} + /\ UNCHANGED <> + +\* RMFaultySendCV2 describes sending CV2 message by the faulty node r. +RMFaultySendCV2(r) == + /\ rmState[r].type = "bad" + /\ LET cv == [type |-> "ChangeView2", rm |-> r, view |-> rmState[r].view] + IN /\ cv \notin msgs + /\ msgs' = msgs \cup {cv} + /\ UNCHANGED <> + +\* RMFaultySendCV3 describes sending CV3 message by the faulty node r. +RMFaultySendCV3(r) == + /\ rmState[r].type = "bad" + /\ LET cv == [type |-> "ChangeView3", rm |-> r, view |-> rmState[r].view] + IN /\ cv \notin msgs + /\ msgs' = msgs \cup {cv} + /\ UNCHANGED <> + +\* RMFaultyDoCV describes view changing by the faulty node r. +RMFaultyDoCV(r) == + /\ rmState[r].type = "bad" + /\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)] + /\ UNCHANGED <> + +\* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r. +RMFaultySendPReq(r) == + /\ rmState[r].type = "bad" + /\ IsPrimary(r) + /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] + IN /\ pReq \notin msgs + /\ msgs' = msgs \cup {pReq} + /\ UNCHANGED <> + +\* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r. +RMFaultySendPResp(r) == + /\ rmState[r].type = "bad" + /\ \neg IsPrimary(r) + /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] + IN /\ pResp \notin msgs + /\ msgs' = msgs \cup {pResp} + /\ UNCHANGED <> + +\* RMFaultySendCommit describes sending Commit message by the faulty node r. +RMFaultySendCommit(r) == + /\ rmState[r].type = "bad" + /\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] + IN /\ commit \notin msgs + /\ msgs' = msgs \cup {commit} + /\ UNCHANGED <> + +\* RMDie describes node r that was removed from the network at the particular step +\* of the behaviour. After this node r can't change its state and accept/send messages. +RMDie(r) == + /\ r \in RMDead + /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F + /\ rmState' = [rmState EXCEPT ![r].type = "dead"] + /\ UNCHANGED <> + +\* Terminating is an action that allows infinite stuttering to prevent deadlock on +\* behaviour termination. We consider termination to be valid if at least M nodes +\* has the block being accepted. +Terminating == + /\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M + /\ UNCHANGED <> + +\* The next-state action. +Next == + \/ Terminating + \/ \E r \in RM: + RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r) + \/ RMAcceptBlock(r) \/ RMSendChangeView1(r) \/ RMReceiveChangeView(r) \/ RMBeBad(r) \/ RMSendChangeView2(r) \/ RMSendChangeView3(r) + \/ RMFaultySendCV1(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) \/ RMFaultySendCV2(r) \/ RMFaultySendCV3(r) + \/ RMDie(r) \/ RMFetchBlock(r) + +\* Safety is a temporal formula that describes the whole set of allowed +\* behaviours. It specifies only what the system MAY do (i.e. the set of +\* possible allowed behaviours for the system). It asserts only what may +\* happen; any behaviour that violates it does so at some point and +\* nothing past that point makes difference. +\* +\* E.g. this safety formula (applied standalone) allows the behaviour to end +\* with an infinite set of stuttering steps (those steps that DO NOT change +\* neither msgs nor rmState) and never reach the state where at least one +\* node is committed or accepted the block. +\* +\* To forbid such behaviours we must specify what the system MUST +\* do. It will be specified below with the help of fairness conditions in +\* the Fairness formula. +Safety == Init /\ [][Next]_vars + +\* -------------- Fairness temporal formula -------------- + +\* Fairness is a temporal assumptions under which the model is working. +\* Usually it specifies different kind of assumptions for each/some +\* subactions of the Next's state action, but the only think that bothers +\* us is preventing infinite stuttering at those steps where some of Next's +\* subactions are enabled. Thus, the only thing that we require from the +\* system is to keep take the steps until it's impossible to take them. +\* That's exactly how the weak fairness condition works: if some action +\* remains continuously enabled, it must eventually happen. +Fairness == WF_vars(Next) + +\* -------------- Specification -------------- + +\* The complete specification of the protocol written as a temporal formula. +Spec == Safety /\ Fairness + +\* -------------- Liveness temporal formula -------------- + +\* For every possible behaviour it's true that eventually (i.e. at least once +\* through the behaviour) block will be accepted. It is something that dBFT +\* must guarantee (an in practice this condition is violated). +TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M) + +\* A liveness temporal formula asserts only what must happen (i.e. specifies +\* what the system MUST do). Any behaviour can NOT violate it at ANY point; +\* there's always the rest of the behaviour that can always make the liveness +\* formula true; if there's no such behaviour than the liveness formula is +\* violated. The liveness formula is supposed to be checked as a property +\* by the TLC model checker. +Liveness == TerminationRequirement + +\* -------------- ModelConstraints -------------- + +\* MaxViewConstraint is a state predicate restricting the number of possible +\* behaviour states. It is needed to reduce model checking time and prevent +\* the model graph size explosion. This formulae must be specified at the +\* "State constraint" section of the "Additional Spec Options" section inside +\* the model overview. +MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView + /\ \A msg \in msgs : msg.view <= MaxView + +\* -------------- Invariants of the specification -------------- + +\* Model invariant is a state predicate (statement) that must be true for +\* every step of every reachable behaviour. Model invariant is supposed to +\* be checked as an Invariant by the TLC Model Checker. + +\* TypeOK is a type-correctness invariant. It states that all elements of +\* specification variables must have the proper type throughout the behaviour. +TypeOK == + /\ rmState \in [RM -> RMStates] + /\ msgs \subseteq Messages + +\* InvTwoBlocksAccepted states that there can't be two different blocks accepted in +\* the two different views, i.e. dBFT must not allow forks. +InvTwoBlocksAccepted == \A r1 \in RM: + \A r2 \in RM \ {r1}: + \/ rmState[r1].type /= "blockAccepted" + \/ rmState[r2].type /= "blockAccepted" + \/ rmState[r1].view = rmState[r2].view + +\* InvFaultNodesCount states that there can be F faulty or dead nodes at max. +InvFaultNodesCount == Cardinality({ + r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead" + }) <= F + +\* This theorem asserts the truth of the temporal formula whose meaning is that +\* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are +\* the invariants of the specification Spec. This theorem is not supposed to be +\* checked by the TLC model checker, it's here for the reader's understanding of +\* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount. +THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvFaultNodesCount) + +============================================================================= +\* Modification History +\* Last modified Wed Mar 01 12:11:07 MSK 2023 by root +\* Last modified Tue Feb 07 23:11:19 MSK 2023 by rik +\* Last modified Fri Feb 03 18:09:33 MSK 2023 by anna +\* Created Thu Dec 15 16:06:17 MSK 2022 by anna diff --git a/formal-models/dbft2.1_threeStagedCV/dbftCV3___AllGoodModel.launch b/formal-models/dbft2.1_threeStagedCV/dbftCV3___AllGoodModel.launch new file mode 100644 index 0000000..8f82824 --- /dev/null +++ b/formal-models/dbft2.1_threeStagedCV/dbftCV3___AllGoodModel.launch @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/formal-models/dbftMultipool/dbftMultipool.tla b/formal-models/dbftMultipool/dbftMultipool.tla new file mode 100644 index 0000000..3eb395e --- /dev/null +++ b/formal-models/dbftMultipool/dbftMultipool.tla @@ -0,0 +1,466 @@ +---------------------------- MODULE dbftMultipool ---------------------------- + +EXTENDS + Integers, + FiniteSets + +CONSTANTS + \* RM is the set of consensus node indexes starting from 0. + \* Example: {0, 1, 2, 3} + RM, + + \* RMFault is a set of consensus node indexes that are allowed to become + \* FAULT in the middle of every considered behavior and to send any + \* consensus message afterwards. RMFault must be a subset of RM. An empty + \* set means that all nodes are good in every possible behaviour. + \* Examples: {0} + \* {1, 3} + \* {} + RMFault, + + \* RMDead is a set of consensus node indexes that are allowed to die in the + \* middle of every behaviour and do not send any message afterwards. RMDead + \* must be a subset of RM. An empty set means that all nodes are alive and + \* responding in in every possible behaviour. RMDead may intersect the + \* RMFault set which means that node which is in both RMDead and RMFault + \* may become FAULT and send any message starting from some step of the + \* particular behaviour and may also die in the same behaviour which will + \* prevent it from sending any message. + \* Examples: {0} + \* {3, 2} + \* {} + RMDead, + + \* MaxView is the maximum allowed view to be considered (starting from 0, + \* including the MaxView itself). This constraint was introduced to reduce + \* the number of possible model states to be checked. It is recommended to + \* keep this setting not too high (< N is highly recommended). + \* Example: 2 + MaxView, + + \* MaxUndeliveredMessages is the maximum number of messages in the common + \* messages pool (msgs) that were not received and handled by all consensus + \* nodes. It must not be too small (>= 3) in order to allow model taking + \* at least any steps. At the same time it must not be too high (<= 6 is + \* recommended) in order to avoid states graph size explosion. + MaxUndeliveredMessages + +VARIABLES + \* rmState is a set of consensus node states. It is represented by the + \* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is + \* the state of the r-th consensus node at the current step. + rmState, + + \* msgs is the shared pool of messages sent to the network by consensus nodes. + \* It is represented by a subset of Messages set. + msgs + +\* vars is a tuple of all variables used in the specification. It is needed to +\* simplify fairness conditions definition. +vars == <> + +\* N is the number of validators. +N == Cardinality(RM) + +\* F is the number of validators that are allowed to be malicious. +F == (N - 1) \div 3 + +\* M is the number of validators that must function correctly. +M == N - F + +\* These assumptions are checked by the TLC model checker once at the start of +\* the model checking process. All the input data (declared constants) specified +\* in the "Model Overview" section must satisfy these constraints. +ASSUME + /\ RM \subseteq Nat + /\ N >= 4 + /\ 0 \in RM + /\ RMFault \subseteq RM + /\ RMDead \subseteq RM + /\ Cardinality(RMFault) <= F + /\ Cardinality(RMDead) <= F + /\ Cardinality(RMFault \cup RMDead) <= F + /\ MaxView \in Nat + /\ MaxView <= 2 + /\ MaxUndeliveredMessages \in Nat + /\ MaxUndeliveredMessages >= 3 \* First value when block can be accepted in some behaviours. + +\* Messages is a set of records where each record holds the message type, +\* the message sender and sender's view by the moment when message was sent. +Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView"}, rm : RM, view : Nat] + +\* RMStates is a set of records where each record holds the node state, the node current view +\* and the pool of messages the nde has been sent or received and handled. +RMStates == [ + type: {"initialized", "prepareSent", "commitSent", "blockAccepted", "bad", "dead"}, + view : Nat, + pool : SUBSET Messages + ] + +\* -------------- Useful operators -------------- + +\* IsPrimary is an operator defining whether provided node r is primary +\* for the current round from the r's point of view. It is a mapping +\* from RM to the set of {TRUE, FALSE}. +IsPrimary(r) == rmState[r].view % N = r + +\* GetPrimary is an operator defining mapping from round index to the RM that +\* is primary in this round. +GetPrimary(view) == CHOOSE r \in RM : view % N = r + +\* GetNewView returns new view number based on the previous node view value. +\* Current specifications only allows to increment view. +GetNewView(oldView) == oldView + 1 + +\* IsViewChanging denotes whether node r have sent ChangeView message for the +\* current (or later) round. +IsViewChanging(r) == Cardinality({msg \in rmState[r].pool : msg.type = "ChangeView" /\ msg.view >= rmState[r].view /\ msg.rm = r}) /= 0 + +\* CountCommitted returns the number of nodes that have sent the Commit message +\* in the current round (as the node r sees it). +CountCommitted(r) == Cardinality({rm \in RM : Cardinality({msg \in rmState[r].pool : msg.rm = rm /\ msg.type = "Commit"}) /= 0}) + +\* CountFailed returns the number of nodes that haven't sent any message since +\* the last round (as the node r sees it from the point of its pool). +CountFailed(r) == Cardinality({rm \in RM : Cardinality({msg \in rmState[r].pool : msg.rm = rm /\ msg.view >= rmState[r].view}) = 0 }) + +\* MoreThanFNodesCommittedOrLost denotes whether more than F nodes committed or +\* failed to communicate in the current round. +MoreThanFNodesCommittedOrLost(r) == CountCommitted(r) + CountFailed(r) > F + +\* NotAcceptingPayloadsDueToViewChanging returns whether the node doesn't accept +\* payloads in the current step. +NotAcceptingPayloadsDueToViewChanging(r) == + /\ IsViewChanging(r) + /\ \neg MoreThanFNodesCommittedOrLost(r) + +\* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest +\* message received from the current round's speaker (as the node r sees it). +PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in rmState[r].pool + +\* CommitSent returns whether the node has its commit sent for the current block. +CommitSent(r) == Cardinality({msg \in rmState[r].pool : msg.rm = r /\ msg.type = "Commit"}) > 0 + +\* -------------- Safety temporal formula -------------- + +\* Init is the initial predicate initializing values at the start of every +\* behaviour. +Init == + /\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 1, pool |-> {}]] + /\ msgs = {} + +\* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest. +RMSendPrepareRequest(r) == + /\ rmState[r].type = "initialized" + /\ IsPrimary(r) + /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] + commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] + IN /\ pReq \notin msgs + /\ IF Cardinality({m \in rmState[r].pool : m.type = "PrepareResponse" /\ m.view = rmState[r].view}) < M - 1 \* -1 is for the current PrepareRequest. + THEN /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent", ![r].pool = rmState[r].pool \cup {pReq}] + /\ msgs' = msgs \cup {pReq} + ELSE /\ msgs' = msgs \cup {pReq, commit} + /\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the current Commit + THEN rmState' = [rmState EXCEPT ![r].type = "commitSent", ![r].pool = rmState[r].pool \cup {pReq, commit}] + ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {pReq, commit}] + /\ UNCHANGED <<>> + +\* RMSendChangeView describes node r sending ChangeView message on timeout. +RMSendChangeView(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ \/ (IsPrimary(r) /\ PrepareRequestSentOrReceived(r)) + \/ (\neg IsPrimary(r) /\ \neg CommitSent(r)) + /\ LET msg == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] + IN /\ msg \notin msgs + /\ msgs' = msgs \cup {msg} + /\ IF Cardinality({m \in rmState[r].pool : m.type = "ChangeView" /\ GetNewView(m.view) >= GetNewView(msg.view)}) >= M-1 \* -1 is for the currently sent CV + THEN rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(msg.view), ![r].pool = rmState[r].pool \cup {msg}] + ELSE rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}] + +\* OnTimeout describes two actions the node can take on timeout for waiting any event. +OnTimeout(r) == + \/ RMSendPrepareRequest(r) + \/ RMSendChangeView(r) + +\* RMOnPrepareRequest describes non-primary node r receiving PrepareRequest from the +\* primary node of the current round (view) and broadcasts PrepareResponse. +\* This step assumes that PrepareRequest always contains valid transactions and +\* signatures. +RMOnPrepareRequest(r) == + /\ rmState[r].type = "initialized" + /\ \E msg \in msgs \ rmState[r].pool: + /\ msg.rm /= r + /\ msg.type = "PrepareRequest" + /\ msg.view = rmState[r].view + /\ \neg IsPrimary(r) + /\ \neg NotAcceptingPayloadsDueToViewChanging(r) \* dbft.go -L296, in C# node, but not in ours + /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] + commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] + IN IF Cardinality({m \in rmState[r].pool : m.type = "PrepareResponse" /\ m.view = rmState[r].view}) < M - 1 - 1 \* -1 is for reveived PrepareRequest; -1 is for current PrepareResponse + THEN /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent", ![r].pool = rmState[r].pool \cup {msg, pResp}] + /\ msgs' = msgs \cup {pResp} + ELSE /\ msgs' = msgs \cup {msg, pResp, commit} + /\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the current Commit + THEN rmState' = [rmState EXCEPT ![r].type = "commitSent", ![r].pool = rmState[r].pool \cup {msg, pResp, commit}] + ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {msg, pResp, commit}] + /\ UNCHANGED <<>> + +\* RMOnPrepareResponse describes node r accepting PrepareResponse message and handling it. +\* If there's enough PrepareResponses collected it will send the Commit; in case if there's +\* enough Commits it will accept the block. +RMOnPrepareResponse(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ \E msg \in msgs \ rmState[r].pool: + /\ msg.rm /= r + /\ msg.type = "PrepareResponse" + /\ msg.view = rmState[r].view + /\ \neg NotAcceptingPayloadsDueToViewChanging(r) + /\ IF \/ Cardinality({m \in rmState[r].pool : (m.type = "PrepareRequest" \/ m.type = "PrepareResponse") /\ m.view = rmState[r].view}) < M - 1 \* -1 is for the currently received PrepareResponse. + \/ CommitSent(r) + \/ \neg PrepareRequestSentOrReceived(r) + THEN /\ rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}] + /\ UNCHANGED <> + ELSE LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] + IN /\ msgs' = msgs \cup {msg, commit} + /\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the current Commit + THEN rmState' = [rmState EXCEPT ![r].type = "commitSent", ![r].pool = rmState[r].pool \cup {msg, commit}] + ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {msg, commit}] + +\* RMOnCommit describes node r accepting Commit message and (in case if there's enough Commits) +\* accepting the block. +RMOnCommit(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ \E msg \in msgs \ rmState[r].pool: + /\ msg.rm /= r + /\ msg.type = "Commit" + /\ msg.view = rmState[r].view + /\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the currently accepting commit + THEN rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}] + ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {msg}] + /\ UNCHANGED <> + +\* RMOnChangeView describes node r receiving ChangeView message and (in case if enough ChangeViews +\* is collected) changing its view. +RMOnChangeView(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ \E msg \in msgs \ rmState[r].pool: + /\ msg.rm /= r + /\ msg.type = "ChangeView" + /\ msg.view = rmState[r].view + /\ \neg CommitSent(r) + /\ Cardinality({m \in rmState[r].pool : m.type = "ChangeView" /\ m.rm = msg.rm /\ m.view > msg.view}) = 0 + /\ IF Cardinality({m \in rmState[r].pool : m.type = "ChangeView" /\ GetNewView(m.view) >= GetNewView(msg.view)}) < M-1 \* -1 is for the currently accepting CV + THEN rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}] + ELSE rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(msg.view), ![r].pool = rmState[r].pool \cup {msg}] + /\ UNCHANGED <> + +\* RMBeBad describes the faulty node r that will send any kind of consensus message starting +\* from the step it's gone wild. This step is enabled only when RMFault is non-empty set. +RMBeBad(r) == + /\ r \in RMFault + /\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F + /\ rmState' = [rmState EXCEPT ![r].type = "bad"] + /\ UNCHANGED <> + +\* RMFaultySendCV describes sending CV message by the faulty node r. +RMFaultySendCV(r) == + /\ rmState[r].type = "bad" + /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] + IN /\ cv \notin msgs + /\ msgs' = msgs \cup {cv} + /\ UNCHANGED <> + +\* RMFaultyDoCV describes view changing by the faulty node r. +RMFaultyDoCV(r) == + /\ rmState[r].type = "bad" + /\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)] + /\ UNCHANGED <> + +\* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r. +RMFaultySendPReq(r) == + /\ rmState[r].type = "bad" + /\ IsPrimary(r) + /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] + IN /\ pReq \notin msgs + /\ msgs' = msgs \cup {pReq} + /\ UNCHANGED <> + +\* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r. +RMFaultySendPResp(r) == + /\ rmState[r].type = "bad" + /\ \neg IsPrimary(r) + /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] + IN /\ pResp \notin msgs + /\ msgs' = msgs \cup {pResp} + /\ UNCHANGED <> + +\* RMFaultySendCommit describes sending Commit message by the faulty node r. +RMFaultySendCommit(r) == + /\ rmState[r].type = "bad" + /\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] + IN /\ commit \notin msgs + /\ msgs' = msgs \cup {commit} + /\ UNCHANGED <> + +\* RMDie describes node r that was removed from the network at the particular step +\* of the behaviour. After this node r can't change its state and accept/send messages. +RMDie(r) == + /\ r \in RMDead + /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F + /\ rmState' = [rmState EXCEPT ![r].type = "dead"] + /\ UNCHANGED <> + +\* Terminating is an action that allows infinite stuttering to prevent deadlock on +\* behaviour termination. We consider termination to be valid if at least M nodes +\* has the block being accepted. +Terminating == + /\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >=1 + /\ UNCHANGED <> + +\* Next is the next-state action describing the transition from the current state +\* to the next state of the behaviour. +Next == + \/ Terminating + \/ \E r \in RM : + \/ OnTimeout(r) + \/ RMOnPrepareRequest(r) \/ RMOnPrepareResponse(r) \/ RMOnCommit(r) \/ RMOnChangeView(r) + \/ RMDie(r) \/ RMBeBad(r) + \/ RMFaultySendCV(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) + +\* Safety is a temporal formula that describes the whole set of allowed +\* behaviours. It specifies only what the system MAY do (i.e. the set of +\* possible allowed behaviours for the system). It asserts only what may +\* happen; any behaviour that violates it does so at some point and +\* nothing past that point makes difference. +\* +\* E.g. this safety formula (applied standalone) allows the behaviour to end +\* with an infinite set of stuttering steps (those steps that DO NOT change +\* neither msgs nor rmState) and never reach the state where at least one +\* node is committed or accepted the block. +\* +\* To forbid such behaviours we must specify what the system MUST +\* do. It will be specified below with the help of fairness conditions in +\* the Fairness formula. +Safety == Init /\ [][Next]_vars + +\* -------------- Fairness temporal formula -------------- + +\* Fairness is a temporal assumptions under which the model is working. +\* Usually it specifies different kind of assumptions for each/some +\* subactions of the Next's state action, but the only think that bothers +\* us is preventing infinite stuttering at those steps where some of Next's +\* subactions are enabled. Thus, the only thing that we require from the +\* system is to keep take the steps until it's impossible to take them. +\* That's exactly how the weak fairness condition works: if some action +\* remains continuously enabled, it must eventually happen. +Fairness == WF_vars(Next) + +\* -------------- Specification -------------- + +\* The complete specification of the protocol written as a temporal formula. +Spec == Safety /\ Fairness + +\* -------------- Liveness temporal formula -------------- + +\* For every possible behaviour it's true that there's at least one PrepareRequest +\* message from the speaker, there's at lest one PrepareResponse message and at +\* least one Commit message. +PrepareRequestSentRequirement == <>(\E msg \in msgs : msg.type = "PrepareRequest") +PrepareResponseSentRequirement == <>(\E msg \in msgs : msg.type = "PrepareResponse") +CommitSentRequirement == <>(\E msg \in msgs : msg.type = "Commit") + +\* For every possible behaviour it's true that eventually (i.e. at least once +\* through the behaviour) block will be accepted. It is something that dBFT +\* must guarantee (an in practice this condition is violated). +TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M) + +\* A liveness temporal formula asserts only what must happen (i.e. specifies +\* what the system MUST do). Any behaviour can NOT violate it at ANY point; +\* there's always the rest of the behaviour that can always make the liveness +\* formula true; if there's no such behaviour than the liveness formula is +\* violated. The liveness formula is supposed to be checked as a property +\* by the TLC model checker. +Liveness == /\ PrepareRequestSentRequirement + /\ PrepareResponseSentRequirement + /\ CommitSentRequirement + /\ TerminationRequirement + +\* -------------- Model constraints -------------- + +\* Model constraints are a set of state predicates restricting the number of possible +\* behaviour states. It is needed to reduce model checking time and prevent +\* the model graph size explosion. These formulaes must be specified at the +\* "State constraint" section of the "Additional Spec Options" section inside +\* the model overview. + +\* MaxViewConstraint is a state predicate restricting the maximum view of messages +\* and consensus nodes state. +MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView + /\ \A msg \in msgs : msg.view <= MaxView + +\* MaxUndeliveredMessageConstraint is a state predicate restricting the maximum +\* number of messages undelivered to any of the consensus nodes. +MaxUndeliveredMessageConstraint == Cardinality({msg \in msgs : \E rm \in RM : msg \notin rmState[rm].pool}) <= MaxUndeliveredMessages + +\* ModelConstraint is overall model constraint rule. +ModelConstraint == MaxViewConstraint /\ MaxUndeliveredMessageConstraint + +\* -------------- Invariants of the specification -------------- + +\* Model invariant is a state predicate (statement) that must be true for +\* every step of every reachable behaviour. Model invariant is supposed to +\* be checked as an Invariant by the TLC Model Checker. + +\* TypeOK is a type-correctness invariant. It states that all elements of +\* specification variables must have the proper type throughout the behaviour. +TypeOK == + /\ rmState \in [RM -> RMStates] + /\ msgs \subseteq Messages + +\* InvTwoBlocksAccepted states that there can't be two different blocks accepted in +\* the two different views, i.e. dBFT must not allow forks. +InvTwoBlocksAccepted == \A r1 \in RM: + \A r2 \in RM \ {r1}: + \/ rmState[r1].type /= "blockAccepted" + \/ rmState[r2].type /= "blockAccepted" + \/ rmState[r1].view = rmState[r2].view + +\* InvDeadlock is a deadlock invariant, it states that the following situation expected +\* never to happen: one node is committed in a single view, two others are committed in +\* a larger view, and the last one has its view changing. +InvDeadlock == \A r1 \in RM : + \A r2 \in RM \ {r1} : + \A r3 \in RM \ {r1, r2} : + \A r4 \in RM \ {r1, r2, r3} : + \/ rmState[r1].type /= "commitSent" + \/ rmState[r2].type /= "commitSent" + \/ rmState[r3].type /= "commitSent" + \/ \neg IsViewChanging(r4) + \/ rmState[r1].view >= rmState[r2].view + \/ rmState[r2].view /= rmState[r3].view + \/ rmState[r3].view /= rmState[r4].view + +\* InvFaultNodesCount states that there can be F faulty or dead nodes at max. +InvFaultNodesCount == Cardinality({ + r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead" + }) <= F + +\* This theorem asserts the truth of the temporal formula whose meaning is that +\* the state predicates TypeOK, InvTwoBlocksAccepted, InvDeadlock and InvFaultNodesCount are +\* the invariants of the specification Spec. This theorem is not supposed to be +\* checked by the TLC model checker, it's here for the reader's understanding of +\* the purpose of TypeOK, InvTwoBlocksAccepted, InvDeadlock and InvFaultNodesCount. +THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvDeadlock /\ InvFaultNodesCount) + +============================================================================= +\* Modification History +\* Last modified Fri Feb 17 15:51:19 MSK 2023 by anna +\* Created Tue Jan 10 12:28:45 MSK 2023 by anna diff --git a/formal-models/dbftMultipool/dbftMultipool___AllGoodModel.launch b/formal-models/dbftMultipool/dbftMultipool___AllGoodModel.launch new file mode 100644 index 0000000..1171d27 --- /dev/null +++ b/formal-models/dbftMultipool/dbftMultipool___AllGoodModel.launch @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/formal-models/dbft_antiMEV/dbft.tla b/formal-models/dbft_antiMEV/dbft.tla new file mode 100644 index 0000000..21cbb9b --- /dev/null +++ b/formal-models/dbft_antiMEV/dbft.tla @@ -0,0 +1,430 @@ +-------------------------------- MODULE dbft -------------------------------- + +EXTENDS + Integers, + FiniteSets + +CONSTANTS + \* RM is the set of consensus node indexes starting from 0. + \* Example: {0, 1, 2, 3} + RM, + + \* RMFault is a set of consensus node indexes that are allowed to become + \* FAULT in the middle of every considered behavior and to send any + \* consensus message afterwards. RMFault must be a subset of RM. An empty + \* set means that all nodes are good in every possible behaviour. + \* Examples: {0} + \* {1, 3} + \* {} + RMFault, + + \* RMDead is a set of consensus node indexes that are allowed to die in the + \* middle of every behaviour and do not send any message afterwards. RMDead + \* must be a subset of RM. An empty set means that all nodes are alive and + \* responding in in every possible behaviour. RMDead may intersect the + \* RMFault set which means that node which is in both RMDead and RMFault + \* may become FAULT and send any message starting from some step of the + \* particular behaviour and may also die in the same behaviour which will + \* prevent it from sending any message. + \* Examples: {0} + \* {3, 2} + \* {} + RMDead, + + \* MaxView is the maximum allowed view to be considered (starting from 0, + \* including the MaxView itself). This constraint was introduced to reduce + \* the number of possible model states to be checked. It is recommended to + \* keep this setting not too high (< N is highly recommended). + \* Example: 2 + MaxView + +VARIABLES + \* rmState is a set of consensus node states. It is represented by the + \* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is + \* the state of the r-th consensus node at the current step. + rmState, + + \* msgs is the shared pool of messages sent to the network by consensus nodes. + \* It is represented by a subset of Messages set. + msgs + +\* vars is a tuple of all variables used in the specification. It is needed to +\* simplify fairness conditions definition. +vars == <> + +\* N is the number of validators. +N == Cardinality(RM) + +\* F is the number of validators that are allowed to be malicious. +F == (N - 1) \div 3 + +\* M is the number of validators that must function correctly. +M == N - F + +\* These assumptions are checked by the TLC model checker once at the start of +\* the model checking process. All the input data (declared constants) specified +\* in the "Model Overview" section must satisfy these constraints. +ASSUME + /\ RM \subseteq Nat + /\ N >= 4 + /\ 0 \in RM + /\ RMFault \subseteq RM + /\ RMDead \subseteq RM + /\ Cardinality(RMFault) <= F + /\ Cardinality(RMDead) <= F + /\ Cardinality(RMFault \cup RMDead) <= F + /\ MaxView \in Nat + /\ MaxView <= 2 + +\* RMStates is a set of records where each record holds the node state and +\* the node current view. +RMStates == [ + type: {"initialized", "prepareSent", "commitSent", "cv", "commitAckSent", "blockAccepted", "bad", "dead"}, + view : Nat + ] + +\* Messages is a set of records where each record holds the message type, +\* the message sender and sender's view by the moment when message was sent. +Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "CommitAck", "ChangeView"}, rm : RM, view : Nat] + +\* -------------- Useful operators -------------- + +\* IsPrimary is an operator defining whether provided node r is primary +\* for the current round from the r's point of view. It is a mapping +\* from RM to the set of {TRUE, FALSE}. +IsPrimary(r) == rmState[r].view % N = r + +\* GetPrimary is an operator defining mapping from round index to the RM that +\* is primary in this round. +GetPrimary(view) == CHOOSE r \in RM : view % N = r + +\* GetNewView returns new view number based on the previous node view value. +\* Current specifications only allows to increment view. +GetNewView(oldView) == oldView + 1 + +\* CountCommitted returns the number of nodes that have sent the Commit message +\* in the current round or in some other round. +CountCommitted(r) == Cardinality({rm \in RM : Cardinality({msg \in msgs : msg.rm = rm /\ msg.type = "Commit"}) /= 0}) + +\* MoreThanFNodesCommitted returns whether more than F nodes have been committed +\* in the current round (as the node r sees it). +\* +\* IMPORTANT NOTE: we intentionally do not add the "lost" nodes calculation to the specification, and here's +\* the reason: from the node's point of view we can't reliably check that some neighbour is completely +\* out of the network. It is possible that the node doesn't receive consensus messages from some other member +\* due to network delays. On the other hand, real nodes can go down at any time. The absence of the +\* member's message doesn't mean that the member is out of the network, we never can be sure about +\* that, thus, this information is unreliable and can't be trusted during the consensus process. +\* What can be trusted is whether there's a Commit message from some member was received by the node. +MoreThanFNodesCommitted(r) == CountCommitted(r) > F + +\* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest +\* message received from the current round's speaker (as the node r sees it). +PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in msgs + +\* -------------- Safety temporal formula -------------- + +\* Init is the initial predicate initializing values at the start of every +\* behaviour. +Init == + /\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]] + /\ msgs = {} + +\* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest. +RMSendPrepareRequest(r) == + /\ rmState[r].type = "initialized" + /\ IsPrimary(r) + /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] + /\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]} + /\ UNCHANGED <<>> + +\* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from +\* the primary node of the current round (view) and broadcasting PrepareResponse. +\* This step assumes that PrepareRequest always contains valid transactions and +\* signatures. +RMSendPrepareResponse(r) == + /\ \/ rmState[r].type = "initialized" + \* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage + \* as it is done in the code-level dBFT implementation by checking the NotAcceptingPayloadsDueToViewChanging + \* condition (see + \* https://github.com/nspcc-dev/dbft/blob/31c1bbdc74f2faa32ec9025062e3a4e2ccfd4214/dbft.go#L419 + \* and + \* https://github.com/neo-project/neo-modules/blob/d00d90b9c27b3d0c3c57e9ca1f560a09975df241/src/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs#L79). + \* However, we can't easily count the number of "lost" nodes in this specification to match precisely + \* the implementation. Moreover, we don't need it to be counted as the RMSendPrepareResponse enabling + \* condition specifies only the thing that may happen given some particular set of enabling conditions. + \* Thus, we've extended the NotAcceptingPayloadsDueToViewChanging condition to consider only MoreThanFNodesCommitted. + \* It should be noted that the logic of MoreThanFNodesCommittedOrLost can't be reliable in detecting lost nodes + \* (even with neo-project/neo#2057), because real nodes can go down at any time. See the comment above the MoreThanFNodesCommitted. + \/ /\ rmState[r].type = "cv" + /\ MoreThanFNodesCommitted(r) + /\ \neg IsPrimary(r) + /\ PrepareRequestSentOrReceived(r) + /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] + /\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]} + /\ UNCHANGED <<>> + +\* RMSendCommit describes node r sending Commit if there's enough PrepareResponse +\* messages. +RMSendCommit(r) == + /\ \/ rmState[r].type = "prepareSent" + \* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage, + \* see the related comment inside the RMSendPrepareResponse definition. + \/ /\ rmState[r].type = "cv" + /\ MoreThanFNodesCommitted(r) + /\ Cardinality({ + msg \in msgs : /\ (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest") + /\ msg.view = rmState[r].view + }) >= M + /\ PrepareRequestSentOrReceived(r) + /\ rmState' = [rmState EXCEPT ![r].type = "commitSent"] + /\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view]} + /\ UNCHANGED <<>> + +\* RMSendCommitAck describes node r collecting enough Commit messages and sending +\* the CommitAck message. +RMSendCommitAck(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "commitAckSent" + /\ rmState[r].type /= "blockAccepted" + /\ PrepareRequestSentOrReceived(r) + /\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) >= M + /\ rmState' = [rmState EXCEPT ![r].type = "commitAckSent"] + /\ msgs' = msgs \cup {[type |-> "CommitAck", rm |-> r, view |-> rmState[r].view]} + /\ UNCHANGED <<>> + +\* RMAcceptBlock describes node r collecting enough CommitAck messages and accepting +\* the block. +RMAcceptBlock(r) == + /\ rmState[r].type = "commitAckSent" + /\ Cardinality({msg \in msgs : msg.type = "CommitAck" /\ msg.view = rmState[r].view}) >= M + /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"] + /\ UNCHANGED <> + +\* RMSendChangeView describes node r sending ChangeView message on timeout. +RMSendChangeView(r) == + /\ \/ (rmState[r].type = "initialized" /\ \neg IsPrimary(r)) + \/ rmState[r].type = "prepareSent" + /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] + IN /\ cv \notin msgs + /\ rmState' = [rmState EXCEPT ![r].type = "cv"] + /\ msgs' = msgs \cup {[type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]} + +\* RMReceiveChangeView describes node r receiving enough ChangeView messages for +\* view changing. +RMReceiveChangeView(r) == + /\ rmState[r].type /= "bad" + /\ rmState[r].type /= "dead" + /\ rmState[r].type /= "blockAccepted" + /\ rmState[r].type /= "commitSent" + /\ rmState[r].type /= "commitAckSent" + /\ Cardinality({ + rm \in RM : Cardinality({ + msg \in msgs : /\ msg.type = "ChangeView" + /\ msg.rm = rm + /\ GetNewView(msg.view) >= GetNewView(rmState[r].view) + }) /= 0 + }) >= M + /\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(rmState[r].view)] + /\ UNCHANGED <> + +\* RMBeBad describes the faulty node r that will send any kind of consensus message starting +\* from the step it's gone wild. This step is enabled only when RMFault is non-empty set. +RMBeBad(r) == + /\ r \in RMFault + /\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F + /\ rmState' = [rmState EXCEPT ![r].type = "bad"] + /\ UNCHANGED <> + +\* RMFaultySendCV describes sending CV message by the faulty node r. +RMFaultySendCV(r) == + /\ rmState[r].type = "bad" + /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] + IN /\ cv \notin msgs + /\ msgs' = msgs \cup {cv} + /\ UNCHANGED <> + +\* RMFaultyDoCV describes view changing by the faulty node r. +RMFaultyDoCV(r) == + /\ rmState[r].type = "bad" + /\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)] + /\ UNCHANGED <> + +\* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r. +RMFaultySendPReq(r) == + /\ rmState[r].type = "bad" + /\ IsPrimary(r) + /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] + IN /\ pReq \notin msgs + /\ msgs' = msgs \cup {pReq} + /\ UNCHANGED <> + +\* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r. +RMFaultySendPResp(r) == + /\ rmState[r].type = "bad" + /\ \neg IsPrimary(r) + /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] + IN /\ pResp \notin msgs + /\ msgs' = msgs \cup {pResp} + /\ UNCHANGED <> + +\* RMFaultySendCommit describes sending Commit message by the faulty node r. +RMFaultySendCommit(r) == + /\ rmState[r].type = "bad" + /\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] + IN /\ commit \notin msgs + /\ msgs' = msgs \cup {commit} + /\ UNCHANGED <> + +\* RMFaultySendCommitAck describes sending CommitAck message by the faulty node r. +RMFaultySendCommitAck(r) == + /\ rmState[r].type = "bad" + /\ LET ack == [type |-> "CommitAck", rm |-> r, view |-> rmState[r].view] + IN /\ ack \notin msgs + /\ msgs' = msgs \cup {ack} + /\ UNCHANGED <> + +\* RMDie describes node r that was removed from the network at the particular step +\* of the behaviour. After this node r can't change its state and accept/send messages. +RMDie(r) == + /\ r \in RMDead + /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F + /\ rmState' = [rmState EXCEPT ![r].type = "dead"] + /\ UNCHANGED <> + +\* Terminating is an action that allows infinite stuttering to prevent deadlock on +\* behaviour termination. We consider termination to be valid if at least M nodes +\* has the block being accepted. +Terminating == + /\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M + /\ UNCHANGED <> + +\* TerminatingFourNodesDeadlock describes node r that is in the state where dBFT +\* stucks in a four nodes scenario with one dead node allowed. Allow infinite +\* stuttering to prevent TLC deadlock recognition. +\* Note that this step is unused in the current specification, however, it may be +\* used for further investigations of this deadlock. +TerminatingFourNodesDeadlockSameView(r) == + /\ Cardinality({rm \in RM : rmState[rm].type = "dead" /\ rmState[rm].view = rmState[r].view}) = 1 + /\ Cardinality({rm \in RM : rmState[rm].type = "cv" /\ rmState[rm].view = rmState[r].view}) = 2 + /\ Cardinality({rm \in RM : rmState[rm].type = "commitSent" /\ rmState[rm].view = rmState[r].view}) = 1 + /\ UNCHANGED <> + +\* TerminatingFourNodesDeadlock describes node r that is in the state where dBFT +\* stucks in a four nodes scenario and the same view. Allow infinite stuttering +\* to prevent TLC deadlock recognition. +\* Note that this step is unused in the current specification, however, it may be +\* used for further investigations of this deadlock. +TerminatingFourNodesDeadlock == + /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) = 1 + /\ Cardinality({rm \in RM : rmState[rm].type = "cv"}) = 2 + /\ Cardinality({rm \in RM : rmState[rm].type = "commitSent"}) = 1 + /\ UNCHANGED <> + +\* Next is the next-state action describing the transition from the current state +\* to the next state of the behaviour. +Next == + \/ Terminating + \/ \E r \in RM: + RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r) \/ RMSendCommitAck(r) + \/ RMAcceptBlock(r) \/ RMSendChangeView(r) \/ RMReceiveChangeView(r) + \/ RMDie(r) \/ RMBeBad(r) + \/ RMFaultySendCV(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendCommitAck(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) + +\* Safety is a temporal formula that describes the whole set of allowed +\* behaviours. It specifies only what the system MAY do (i.e. the set of +\* possible allowed behaviours for the system). It asserts only what may +\* happen; any behaviour that violates it does so at some point and +\* nothing past that point makes difference. +\* +\* E.g. this safety formula (applied standalone) allows the behaviour to end +\* with an infinite set of stuttering steps (those steps that DO NOT change +\* neither msgs nor rmState) and never reach the state where at least one +\* node is committed or accepted the block. +\* +\* To forbid such behaviours we must specify what the system MUST +\* do. It will be specified below with the help of fairness conditions in +\* the Fairness formula. +Safety == Init /\ [][Next]_vars + +\* -------------- Fairness temporal formula -------------- + +\* Fairness is a temporal assumptions under which the model is working. +\* Usually it specifies different kind of assumptions for each/some +\* subactions of the Next's state action, but the only think that bothers +\* us is preventing infinite stuttering at those steps where some of Next's +\* subactions are enabled. Thus, the only thing that we require from the +\* system is to keep take the steps until it's impossible to take them. +\* That's exactly how the weak fairness condition works: if some action +\* remains continuously enabled, it must eventually happen. +Fairness == WF_vars(Next) + +\* -------------- Specification -------------- + +\* The complete specification of the protocol written as a temporal formula. +Spec == Safety /\ Fairness + +\* -------------- Liveness temporal formula -------------- + +\* For every possible behaviour it's true that eventually (i.e. at least once +\* through the behaviour) block will be accepted. It is something that dBFT +\* must guarantee (an in practice this condition is violated). +TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M) + +\* A liveness temporal formula asserts only what must happen (i.e. specifies +\* what the system MUST do). Any behaviour can NOT violate it at ANY point; +\* there's always the rest of the behaviour that can always make the liveness +\* formula true; if there's no such behaviour than the liveness formula is +\* violated. The liveness formula is supposed to be checked as a property +\* by the TLC model checker. +Liveness == TerminationRequirement + +\* -------------- ModelConstraints -------------- + +\* MaxViewConstraint is a state predicate restricting the number of possible +\* behaviour states. It is needed to reduce model checking time and prevent +\* the model graph size explosion. This formulae must be specified at the +\* "State constraint" section of the "Additional Spec Options" section inside +\* the model overview. +MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView + /\ \A msg \in msgs : msg.view <= MaxView + +\* -------------- Invariants of the specification -------------- + +\* Model invariant is a state predicate (statement) that must be true for +\* every step of every reachable behaviour. Model invariant is supposed to +\* be checked as an Invariant by the TLC Model Checker. + +\* TypeOK is a type-correctness invariant. It states that all elements of +\* specification variables must have the proper type throughout the behaviour. +TypeOK == + /\ rmState \in [RM -> RMStates] + /\ msgs \subseteq Messages + +\* InvTwoBlocksAccepted states that there can't be two different blocks accepted in +\* the two different views, i.e. dBFT must not allow forks. +InvTwoBlocksAccepted == \A r1 \in RM: + \A r2 \in RM \ {r1}: + \/ rmState[r1].type /= "blockAccepted" + \/ rmState[r2].type /= "blockAccepted" + \/ rmState[r1].view = rmState[r2].view + +\* InvFaultNodesCount states that there can be F faulty or dead nodes at max. +InvFaultNodesCount == Cardinality({ + r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead" + }) <= F + +\* This theorem asserts the truth of the temporal formula whose meaning is that +\* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are +\* the invariants of the specification Spec. This theorem is not supposed to be +\* checked by the TLC model checker, it's here for the reader's understanding of +\* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount. +THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvFaultNodesCount) + +============================================================================= +\* Modification History +\* Last modified Wed Jun 19 17:51:15 MSK 2024 by anna +\* Last modified Mon Mar 06 15:36:57 MSK 2023 by root +\* Last modified Sat Jan 21 01:26:16 MSK 2023 by rik +\* Created Thu Dec 15 16:06:17 MSK 2022 by anna diff --git a/formal-models/dbft_antiMEV/dbft___AllGoodModel.launch b/formal-models/dbft_antiMEV/dbft___AllGoodModel.launch new file mode 100644 index 0000000..52b9984 --- /dev/null +++ b/formal-models/dbft_antiMEV/dbft___AllGoodModel.launch @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5a67b50 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/tutus-one/tutus-consensus + +go 1.24 + +require ( + github.com/stretchr/testify v1.11.1 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..92630fb --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..9f2ab93 --- /dev/null +++ b/helpers.go @@ -0,0 +1,63 @@ +package dbft + +type ( + // inbox is a structure storing messages from a single epoch. + inbox[H Hash] struct { + prepare map[uint16]ConsensusPayload[H] + chViews map[uint16]ConsensusPayload[H] + preCommit map[uint16]ConsensusPayload[H] + commit map[uint16]ConsensusPayload[H] + } + + // cache is an auxiliary structure storing messages + // from future epochs. + cache[H Hash] struct { + mail map[uint32]*inbox[H] + } +) + +func newInbox[H Hash]() *inbox[H] { + return &inbox[H]{ + prepare: make(map[uint16]ConsensusPayload[H]), + chViews: make(map[uint16]ConsensusPayload[H]), + preCommit: make(map[uint16]ConsensusPayload[H]), + commit: make(map[uint16]ConsensusPayload[H]), + } +} + +func newCache[H Hash]() cache[H] { + return cache[H]{ + mail: make(map[uint32]*inbox[H]), + } +} + +func (c *cache[H]) getHeight(h uint32) *inbox[H] { + if m, ok := c.mail[h]; ok { + delete(c.mail, h) + return m + } + + return nil +} + +func (c *cache[H]) addMessage(m ConsensusPayload[H]) { + msgs, ok := c.mail[m.Height()] + if !ok { + msgs = newInbox[H]() + c.mail[m.Height()] = msgs + } + + switch m.Type() { + case PrepareRequestType, PrepareResponseType: + msgs.prepare[m.ValidatorIndex()] = m + case ChangeViewType: + msgs.chViews[m.ValidatorIndex()] = m + case PreCommitType: + msgs.preCommit[m.ValidatorIndex()] = m + case CommitType: + msgs.commit[m.ValidatorIndex()] = m + default: + // Others are recoveries and we don't currently use them. + // Theoretically messages could be extracted. + } +} diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..0c62421 --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,115 @@ +package dbft + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// Structures used for type-specific dBFT/payloads implementation to avoid cyclic +// dependency. +type ( + hash struct{} + payloadStub struct { + height uint32 + typ MessageType + validatorIndex uint16 + } +) + +func (hash) String() string { + return "" +} + +func (p payloadStub) ViewNumber() byte { + panic("TODO") +} +func (p payloadStub) SetViewNumber(byte) { + panic("TODO") +} +func (p payloadStub) Type() MessageType { + return p.typ +} +func (p payloadStub) SetType(MessageType) { + panic("TODO") +} +func (p payloadStub) Payload() any { + panic("TODO") +} +func (p payloadStub) SetPayload(any) { + panic("TODO") +} +func (p payloadStub) GetChangeView() ChangeView { + panic("TODO") +} +func (p payloadStub) GetPrepareRequest() PrepareRequest[hash] { + panic("TODO") +} +func (p payloadStub) GetPrepareResponse() PrepareResponse[hash] { + panic("TODO") +} +func (p payloadStub) GetCommit() Commit { + panic("TODO") +} +func (p payloadStub) GetPreCommit() PreCommit { panic("TODO") } +func (p payloadStub) GetRecoveryRequest() RecoveryRequest { + panic("TODO") +} +func (p payloadStub) GetRecoveryMessage() RecoveryMessage[hash] { + panic("TODO") +} +func (p payloadStub) ValidatorIndex() uint16 { + return p.validatorIndex +} +func (p payloadStub) SetValidatorIndex(uint16) { + panic("TODO") +} +func (p payloadStub) Height() uint32 { + return p.height +} +func (p payloadStub) SetHeight(uint32) { + panic("TODO") +} +func (p payloadStub) Hash() hash { + panic("TODO") +} + +func TestMessageCache(t *testing.T) { + c := newCache[hash]() + + p1 := payloadStub{ + height: 3, + typ: PrepareRequestType, + } + c.addMessage(p1) + + p2 := payloadStub{ + height: 4, + typ: ChangeViewType, + } + c.addMessage(p2) + + p3 := payloadStub{ + height: 4, + typ: CommitType, + } + c.addMessage(p3) + + p4 := payloadStub{ + height: 3, + typ: PreCommitType, + } + c.addMessage(p4) + + box := c.getHeight(3) + require.Len(t, box.chViews, 0) + require.Len(t, box.prepare, 1) + require.Len(t, box.preCommit, 1) + require.Len(t, box.commit, 0) + + box = c.getHeight(4) + require.Len(t, box.chViews, 1) + require.Len(t, box.prepare, 0) + require.Len(t, box.preCommit, 0) + require.Len(t, box.commit, 1) +} diff --git a/identity.go b/identity.go new file mode 100644 index 0000000..8f1ec68 --- /dev/null +++ b/identity.go @@ -0,0 +1,28 @@ +package dbft + +import ( + "fmt" +) + +type ( + // PublicKey is a generic public key interface used by dbft. + PublicKey any + + // PrivateKey is a generic private key interface used by dbft. PrivateKey is used + // only by [PreBlock] and [Block] signing callbacks ([PreBlock.SetData] and + // [Block.Sign]) to grant access to the private key abstraction to Block and + // PreBlock signing code. PrivateKey does not contain any methods, hence user + // supposed to perform type assertion before the PrivateKey usage. + PrivateKey any + + // Hash is a generic hash interface used by dbft for payloads, blocks and + // transactions identification. It is recommended to implement this interface + // using hash functions with low hash collision probability. The following + // requirements must be met: + // 1. Hashes of two equal payloads/blocks/transactions are equal. + // 2. Hashes of two different payloads/blocks/transactions are different. + Hash interface { + comparable + fmt.Stringer + } +) diff --git a/internal/consensus/amev_block.go b/internal/consensus/amev_block.go new file mode 100644 index 0000000..7c97ee8 --- /dev/null +++ b/internal/consensus/amev_block.go @@ -0,0 +1,128 @@ +package consensus + +import ( + "bytes" + "encoding/binary" + "encoding/gob" + "math" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" + "github.com/tutus-one/tutus-consensus/internal/merkle" +) + +type amevBlock struct { + base + + transactions []dbft.Transaction[crypto.Uint256] + signature []byte + hash *crypto.Uint256 +} + +var _ dbft.Block[crypto.Uint256] = new(amevBlock) + +// NewAMEVBlock returns new block based on PreBlock and additional Commit-level data +// collected from M consensus nodes. +func NewAMEVBlock(pre dbft.PreBlock[crypto.Uint256], cnData [][]byte, m int) dbft.Block[crypto.Uint256] { + preB := pre.(*preBlock) + res := new(amevBlock) + res.base = preB.base + + // Based on the provided cnData we'll add one more transaction to the resulting block. + // Some artificial rules of new tx creation are invented here, but in Neo X there will + // be well-defined custom rules for Envelope transactions. + var sum uint32 + for i := range m { + sum += binary.BigEndian.Uint32(cnData[i]) + } + tx := Tx64(math.MaxInt64 - int64(sum)) + res.transactions = append(preB.initialTransactions, &tx) + + // Rebuild Merkle root for the new set of transactions. + txHashes := make([]crypto.Uint256, len(res.transactions)) + for i := range txHashes { + txHashes[i] = res.transactions[i].Hash() + } + mt := merkle.NewMerkleTree(txHashes...) + res.base.MerkleRoot = mt.Root().Hash + + return res +} + +// PrevHash implements Block interface. +func (b *amevBlock) PrevHash() crypto.Uint256 { + return b.base.PrevHash +} + +// Index implements Block interface. +func (b *amevBlock) Index() uint32 { + return b.base.Index +} + +// MerkleRoot implements Block interface. +func (b *amevBlock) MerkleRoot() crypto.Uint256 { + return b.base.MerkleRoot +} + +// Transactions implements Block interface. +func (b *amevBlock) Transactions() []dbft.Transaction[crypto.Uint256] { + return b.transactions +} + +// SetTransactions implements Block interface. This method is special since it's +// left for dBFT 2.0 compatibility and transactions from this method must not be +// reused to fill final Block's transactions. +func (b *amevBlock) SetTransactions(_ []dbft.Transaction[crypto.Uint256]) { +} + +// Signature implements Block interface. +func (b *amevBlock) Signature() []byte { + return b.signature +} + +// GetHashData returns data for hashing and signing. +// It must be an injection of the set of blocks to the set +// of byte slices, i.e: +// 1. It must have only one valid result for one block. +// 2. Two different blocks must have different hash data. +func (b *amevBlock) GetHashData() []byte { + buf := bytes.Buffer{} + w := gob.NewEncoder(&buf) + _ = b.EncodeBinary(w) + + return buf.Bytes() +} + +// Sign implements Block interface. +func (b *amevBlock) Sign(key dbft.PrivateKey) error { + data := b.GetHashData() + + sign, err := key.(*crypto.ECDSAPriv).Sign(data) + if err != nil { + return err + } + + b.signature = sign + + return nil +} + +// Verify implements Block interface. +func (b *amevBlock) Verify(pub dbft.PublicKey, sign []byte) error { + data := b.GetHashData() + return pub.(*crypto.ECDSAPub).Verify(data, sign) +} + +// Hash implements Block interface. +func (b *amevBlock) Hash() (h crypto.Uint256) { + if b.hash != nil { + return *b.hash + } else if b.transactions == nil { + return + } + + hash := crypto.Hash256(b.GetHashData()) + b.hash = &hash + + return hash +} diff --git a/internal/consensus/amev_commit.go b/internal/consensus/amev_commit.go new file mode 100644 index 0000000..8177c91 --- /dev/null +++ b/internal/consensus/amev_commit.go @@ -0,0 +1,44 @@ +package consensus + +import ( + "encoding/gob" + + "github.com/tutus-one/tutus-consensus" +) + +type ( + // amevCommit implements dbft.Commit. + amevCommit struct { + data [dataSize]byte + } + // amevCommitAux is an auxiliary structure for amevCommit encoding. + amevCommitAux struct { + Data [dataSize]byte + } +) + +const dataSize = 64 + +var _ dbft.Commit = (*amevCommit)(nil) + +// EncodeBinary implements Serializable interface. +func (c amevCommit) EncodeBinary(w *gob.Encoder) error { + return w.Encode(amevCommitAux{ + Data: c.data, + }) +} + +// DecodeBinary implements Serializable interface. +func (c *amevCommit) DecodeBinary(r *gob.Decoder) error { + aux := new(amevCommitAux) + if err := r.Decode(aux); err != nil { + return err + } + c.data = aux.Data + return nil +} + +// Signature implements Commit interface. +func (c amevCommit) Signature() []byte { + return c.data[:] +} diff --git a/internal/consensus/amev_preBlock.go b/internal/consensus/amev_preBlock.go new file mode 100644 index 0000000..874b946 --- /dev/null +++ b/internal/consensus/amev_preBlock.go @@ -0,0 +1,79 @@ +package consensus + +import ( + "encoding/binary" + "errors" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" + "github.com/tutus-one/tutus-consensus/internal/merkle" +) + +type preBlock struct { + base + + // A magic number CN nodes should exchange during Commit phase + // and used to construct the final list of transactions for amevBlock. + data uint32 + + initialTransactions []dbft.Transaction[crypto.Uint256] +} + +var _ dbft.PreBlock[crypto.Uint256] = new(preBlock) + +// NewPreBlock returns new preBlock. +func NewPreBlock(timestamp uint64, index uint32, prevHash crypto.Uint256, nonce uint64, txHashes []crypto.Uint256) dbft.PreBlock[crypto.Uint256] { + pre := new(preBlock) + pre.Timestamp = uint32(timestamp / 1000000000) + pre.Index = index + + // NextConsensus and Version information is not provided by dBFT context, + // these are implementation-specific fields, and thus, should be managed outside the + // dBFT library. For simulation simplicity, let's assume that these fields are filled + // by every CN separately and is not verified. + pre.NextConsensus = crypto.Uint160{1, 2, 3} + pre.Version = 0 + + pre.PrevHash = prevHash + pre.ConsensusData = nonce + + // Canary default value. + pre.data = 0xff + + if len(txHashes) != 0 { + mt := merkle.NewMerkleTree(txHashes...) + pre.MerkleRoot = mt.Root().Hash + } + return pre +} + +func (pre *preBlock) Data() []byte { + var res = make([]byte, 4) + binary.BigEndian.PutUint32(res, pre.data) + return res +} + +func (pre *preBlock) SetData(_ dbft.PrivateKey) error { + // Just an artificial rule for data construction, it can be anything, and in Neo X + // it will be decrypted transactions fragments. + pre.data = pre.Index + return nil +} + +func (pre *preBlock) Verify(_ dbft.PublicKey, data []byte) error { + if len(data) != 4 { + return errors.New("invalid data len") + } + if binary.BigEndian.Uint32(data) != pre.Index { // Just an artificial verification rule, and for NeoX it should be decrypted transactions fragments verification. + return errors.New("invalid data") + } + return nil +} + +func (pre *preBlock) Transactions() []dbft.Transaction[crypto.Uint256] { + return pre.initialTransactions +} + +func (pre *preBlock) SetTransactions(txs []dbft.Transaction[crypto.Uint256]) { + pre.initialTransactions = txs +} diff --git a/internal/consensus/amev_preCommit.go b/internal/consensus/amev_preCommit.go new file mode 100644 index 0000000..0c6206f --- /dev/null +++ b/internal/consensus/amev_preCommit.go @@ -0,0 +1,45 @@ +package consensus + +import ( + "encoding/binary" + "encoding/gob" + + "github.com/tutus-one/tutus-consensus" +) + +type ( + // preCommit implements dbft.PreCommit. + preCommit struct { + magic uint32 // some magic data CN have to exchange to properly construct final amevBlock. + } + // preCommitAux is an auxiliary structure for preCommit encoding. + preCommitAux struct { + Magic uint32 + } +) + +var _ dbft.PreCommit = (*preCommit)(nil) + +// EncodeBinary implements Serializable interface. +func (c preCommit) EncodeBinary(w *gob.Encoder) error { + return w.Encode(preCommitAux{ + Magic: c.magic, + }) +} + +// DecodeBinary implements Serializable interface. +func (c *preCommit) DecodeBinary(r *gob.Decoder) error { + aux := new(preCommitAux) + if err := r.Decode(aux); err != nil { + return err + } + c.magic = aux.Magic + return nil +} + +// Data implements PreCommit interface. +func (c preCommit) Data() []byte { + res := make([]byte, 4) + binary.BigEndian.PutUint32(res, c.magic) + return res +} diff --git a/internal/consensus/block.go b/internal/consensus/block.go new file mode 100644 index 0000000..3201673 --- /dev/null +++ b/internal/consensus/block.go @@ -0,0 +1,151 @@ +package consensus + +import ( + "bytes" + "encoding/gob" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" + "github.com/tutus-one/tutus-consensus/internal/merkle" +) + +type ( + // base is a structure containing all + // hashable and signable fields of the block. + base struct { + ConsensusData uint64 + Index uint32 + Timestamp uint32 + Version uint32 + MerkleRoot crypto.Uint256 + PrevHash crypto.Uint256 + NextConsensus crypto.Uint160 + } + + neoBlock struct { + base + + transactions []dbft.Transaction[crypto.Uint256] + signature []byte + hash *crypto.Uint256 + } + + // signable is an interface used within consensus package to abstract private key + // functionality. This interface is used instead of direct structure usage to be + // able to mock private key implementation in unit tests. + signable interface { + Sign([]byte) ([]byte, error) + } +) + +var _ dbft.Block[crypto.Uint256] = new(neoBlock) + +// PrevHash implements Block interface. +func (b *neoBlock) PrevHash() crypto.Uint256 { + return b.base.PrevHash +} + +// Index implements Block interface. +func (b *neoBlock) Index() uint32 { + return b.base.Index +} + +// MerkleRoot implements Block interface. +func (b *neoBlock) MerkleRoot() crypto.Uint256 { + return b.base.MerkleRoot +} + +// Transactions implements Block interface. +func (b *neoBlock) Transactions() []dbft.Transaction[crypto.Uint256] { + return b.transactions +} + +// SetTransactions implements Block interface. +func (b *neoBlock) SetTransactions(txx []dbft.Transaction[crypto.Uint256]) { + b.transactions = txx +} + +// NewBlock returns new block. +func NewBlock(timestamp uint64, index uint32, prevHash crypto.Uint256, nonce uint64, txHashes []crypto.Uint256) dbft.Block[crypto.Uint256] { + block := new(neoBlock) + block.Timestamp = uint32(timestamp / 1000000000) + block.base.Index = index + + // NextConsensus and Version information is not provided by dBFT context, + // these are implementation-specific fields, and thus, should be managed outside the + // dBFT library. For simulation simplicity, let's assume that these fields are filled + // by every CN separately and is not verified. + block.NextConsensus = crypto.Uint160{1, 2, 3} + block.Version = 0 + + block.base.PrevHash = prevHash + block.ConsensusData = nonce + + if len(txHashes) != 0 { + mt := merkle.NewMerkleTree(txHashes...) + block.base.MerkleRoot = mt.Root().Hash + } + return block +} + +// Signature implements Block interface. +func (b *neoBlock) Signature() []byte { + return b.signature +} + +// GetHashData returns data for hashing and signing. +// It must be an injection of the set of blocks to the set +// of byte slices, i.e: +// 1. It must have only one valid result for one block. +// 2. Two different blocks must have different hash data. +func (b *neoBlock) GetHashData() []byte { + buf := bytes.Buffer{} + w := gob.NewEncoder(&buf) + _ = b.EncodeBinary(w) + + return buf.Bytes() +} + +// Sign implements Block interface. +func (b *neoBlock) Sign(key dbft.PrivateKey) error { + data := b.GetHashData() + + sign, err := key.(signable).Sign(data) + if err != nil { + return err + } + + b.signature = sign + + return nil +} + +// Verify implements Block interface. +func (b *neoBlock) Verify(pub dbft.PublicKey, sign []byte) error { + data := b.GetHashData() + return pub.(*crypto.ECDSAPub).Verify(data, sign) +} + +// Hash implements Block interface. +func (b *neoBlock) Hash() (h crypto.Uint256) { + if b.hash != nil { + return *b.hash + } else if b.transactions == nil { + return + } + + hash := crypto.Hash256(b.GetHashData()) + b.hash = &hash + + return hash +} + +// EncodeBinary implements Serializable interface. +func (b base) EncodeBinary(w *gob.Encoder) error { + return w.Encode(b) +} + +// DecodeBinary implements Serializable interface. +func (b *base) DecodeBinary(r *gob.Decoder) error { + return r.Decode(b) +} diff --git a/internal/consensus/block_test.go b/internal/consensus/block_test.go new file mode 100644 index 0000000..e0c1698 --- /dev/null +++ b/internal/consensus/block_test.go @@ -0,0 +1,80 @@ +package consensus + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "encoding/gob" + "errors" + "testing" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNeoBlock_Setters(t *testing.T) { + b := new(neoBlock) + + require.Equal(t, crypto.Uint256{}, b.Hash()) + + txs := []dbft.Transaction[crypto.Uint256]{testTx(1), testTx(2)} + b.SetTransactions(txs) + assert.Equal(t, txs, b.Transactions()) + + b.base.PrevHash = crypto.Uint256{3, 7} + assert.Equal(t, crypto.Uint256{3, 7}, b.PrevHash()) + + b.base.MerkleRoot = crypto.Uint256{13} + assert.Equal(t, crypto.Uint256{13}, b.MerkleRoot()) + + b.base.Index = 100 + assert.EqualValues(t, 100, b.Index()) + + t.Run("marshal block", func(t *testing.T) { + buf := bytes.Buffer{} + w := gob.NewEncoder(&buf) + err := b.EncodeBinary(w) + require.NoError(t, err) + + r := gob.NewDecoder(bytes.NewReader(buf.Bytes())) + newb := new(neoBlock) + err = newb.DecodeBinary(r) + require.NoError(t, err) + require.Equal(t, b.base, newb.base) + }) + + t.Run("hash does not change after signature", func(t *testing.T) { + priv, pub := crypto.Generate(rand.Reader) + require.NotNil(t, priv) + require.NotNil(t, pub) + + h := b.Hash() + require.NoError(t, b.Sign(priv)) + require.NotEmpty(t, b.Signature()) + require.Equal(t, h, b.Hash()) + require.NoError(t, b.Verify(pub, b.Signature())) + }) + + t.Run("sign with invalid private key", func(t *testing.T) { + require.Error(t, b.Sign(testKey{})) + }) +} + +type testKey struct{} + +var _ signable = testKey{} + +func (t testKey) MarshalBinary() ([]byte, error) { return []byte{}, nil } +func (t testKey) UnmarshalBinary([]byte) error { return nil } +func (t testKey) Sign([]byte) ([]byte, error) { + return nil, errors.New("can't sign") +} + +type testTx uint64 + +func (tx testTx) Hash() (h crypto.Uint256) { + binary.LittleEndian.PutUint64(h[:], uint64(tx)) + return +} diff --git a/internal/consensus/change_view.go b/internal/consensus/change_view.go new file mode 100644 index 0000000..ec733d5 --- /dev/null +++ b/internal/consensus/change_view.go @@ -0,0 +1,47 @@ +package consensus + +import ( + "encoding/gob" + + "github.com/tutus-one/tutus-consensus" +) + +type ( + changeView struct { + newViewNumber byte + timestamp uint32 + } + // changeViewAux is an auxiliary structure for changeView encoding. + changeViewAux struct { + Timestamp uint32 + } +) + +var _ dbft.ChangeView = (*changeView)(nil) + +// EncodeBinary implements Serializable interface. +func (c changeView) EncodeBinary(w *gob.Encoder) error { + return w.Encode(&changeViewAux{ + Timestamp: c.timestamp, + }) +} + +// DecodeBinary implements Serializable interface. +func (c *changeView) DecodeBinary(r *gob.Decoder) error { + aux := new(changeViewAux) + if err := r.Decode(aux); err != nil { + return err + } + c.timestamp = aux.Timestamp + return nil +} + +// NewViewNumber implements ChangeView interface. +func (c changeView) NewViewNumber() byte { + return c.newViewNumber +} + +// Reason implements ChangeView interface. +func (c changeView) Reason() dbft.ChangeViewReason { + return dbft.CVUnknown +} diff --git a/internal/consensus/commit.go b/internal/consensus/commit.go new file mode 100644 index 0000000..9da169b --- /dev/null +++ b/internal/consensus/commit.go @@ -0,0 +1,43 @@ +package consensus + +import ( + "encoding/gob" + + "github.com/tutus-one/tutus-consensus" +) + +type ( + commit struct { + signature [signatureSize]byte + } + // commitAux is an auxiliary structure for commit encoding. + commitAux struct { + Signature [signatureSize]byte + } +) + +const signatureSize = 64 + +var _ dbft.Commit = (*commit)(nil) + +// EncodeBinary implements Serializable interface. +func (c commit) EncodeBinary(w *gob.Encoder) error { + return w.Encode(commitAux{ + Signature: c.signature, + }) +} + +// DecodeBinary implements Serializable interface. +func (c *commit) DecodeBinary(r *gob.Decoder) error { + aux := new(commitAux) + if err := r.Decode(aux); err != nil { + return err + } + c.signature = aux.Signature + return nil +} + +// Signature implements Commit interface. +func (c commit) Signature() []byte { + return c.signature[:] +} diff --git a/internal/consensus/compact.go b/internal/consensus/compact.go new file mode 100644 index 0000000..533a89f --- /dev/null +++ b/internal/consensus/compact.go @@ -0,0 +1,69 @@ +package consensus + +import ( + "encoding/gob" +) + +type ( + changeViewCompact struct { + ValidatorIndex uint16 + OriginalViewNumber byte + Timestamp uint32 + } + + preCommitCompact struct { + ViewNumber byte + ValidatorIndex uint16 + Data []byte + } + + commitCompact struct { + ViewNumber byte + ValidatorIndex uint16 + Signature [signatureSize]byte + } + + preparationCompact struct { + ValidatorIndex uint16 + } +) + +// EncodeBinary implements Serializable interface. +func (p changeViewCompact) EncodeBinary(w *gob.Encoder) error { + return w.Encode(p) +} + +// DecodeBinary implements Serializable interface. +func (p *changeViewCompact) DecodeBinary(r *gob.Decoder) error { + return r.Decode(p) +} + +// EncodeBinary implements Serializable interface. +func (p preCommitCompact) EncodeBinary(w *gob.Encoder) error { + return w.Encode(p) +} + +// DecodeBinary implements Serializable interface. +func (p *preCommitCompact) DecodeBinary(r *gob.Decoder) error { + return r.Decode(p) +} + +// EncodeBinary implements Serializable interface. +func (p commitCompact) EncodeBinary(w *gob.Encoder) error { + return w.Encode(p) +} + +// DecodeBinary implements Serializable interface. +func (p *commitCompact) DecodeBinary(r *gob.Decoder) error { + return r.Decode(p) +} + +// EncodeBinary implements Serializable interface. +func (p preparationCompact) EncodeBinary(w *gob.Encoder) error { + return w.Encode(p) +} + +// DecodeBinary implements Serializable interface. +func (p *preparationCompact) DecodeBinary(r *gob.Decoder) error { + return r.Decode(p) +} diff --git a/internal/consensus/consensus.go b/internal/consensus/consensus.go new file mode 100644 index 0000000..0a9943b --- /dev/null +++ b/internal/consensus/consensus.go @@ -0,0 +1,72 @@ +package consensus + +import ( + "time" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" + "github.com/tutus-one/tutus-consensus/timer" + "go.uber.org/zap" +) + +func New(logger *zap.Logger, key dbft.PrivateKey, pub dbft.PublicKey, + getTx func(uint256 crypto.Uint256) dbft.Transaction[crypto.Uint256], + getVerified func() []dbft.Transaction[crypto.Uint256], + broadcast func(dbft.ConsensusPayload[crypto.Uint256]), + processBlock func(dbft.Block[crypto.Uint256]) error, + currentHeight func() uint32, + currentBlockHash func() crypto.Uint256, + getValidators func(...dbft.Transaction[crypto.Uint256]) []dbft.PublicKey, + verifyPayload func(consensusPayload dbft.ConsensusPayload[crypto.Uint256]) error) (*dbft.DBFT[crypto.Uint256], error) { + return dbft.New[crypto.Uint256]( + dbft.WithTimer[crypto.Uint256](timer.New()), + dbft.WithLogger[crypto.Uint256](logger), + dbft.WithTimePerBlock[crypto.Uint256](func() time.Duration { + return time.Second * 5 + }), + dbft.WithGetKeyPair[crypto.Uint256](func(pubs []dbft.PublicKey) (int, dbft.PrivateKey, dbft.PublicKey) { + for i := range pubs { + if pub.(*crypto.ECDSAPub).Equals(pubs[i]) { + return i, key, pub + } + } + + return -1, nil, nil + }), + dbft.WithGetTx[crypto.Uint256](getTx), + dbft.WithGetVerified[crypto.Uint256](getVerified), + dbft.WithBroadcast[crypto.Uint256](broadcast), + dbft.WithProcessBlock[crypto.Uint256](processBlock), + dbft.WithCurrentHeight[crypto.Uint256](currentHeight), + dbft.WithCurrentBlockHash[crypto.Uint256](currentBlockHash), + dbft.WithGetValidators[crypto.Uint256](getValidators), + dbft.WithVerifyPrepareRequest[crypto.Uint256](verifyPayload), + dbft.WithVerifyPrepareResponse[crypto.Uint256](verifyPayload), + dbft.WithVerifyCommit[crypto.Uint256](verifyPayload), + + dbft.WithNewBlockFromContext[crypto.Uint256](newBlockFromContext), + dbft.WithNewConsensusPayload[crypto.Uint256](defaultNewConsensusPayload), + dbft.WithNewPrepareRequest[crypto.Uint256](NewPrepareRequest), + dbft.WithNewPrepareResponse[crypto.Uint256](NewPrepareResponse), + dbft.WithNewChangeView[crypto.Uint256](NewChangeView), + dbft.WithNewCommit[crypto.Uint256](NewCommit), + dbft.WithNewRecoveryMessage[crypto.Uint256](func() dbft.RecoveryMessage[crypto.Uint256] { + return NewRecoveryMessage(nil) + }), + dbft.WithNewRecoveryRequest[crypto.Uint256](NewRecoveryRequest), + ) +} + +func newBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { + if ctx.TransactionHashes == nil { + return nil + } + block := NewBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, ctx.TransactionHashes) + return block +} + +// defaultNewConsensusPayload is default function for creating +// consensus payload of specific type. +func defaultNewConsensusPayload(c *dbft.Context[crypto.Uint256], t dbft.MessageType, msg any) dbft.ConsensusPayload[crypto.Uint256] { + return NewConsensusPayload(t, c.BlockIndex, uint16(c.MyIndex), c.ViewNumber, msg) +} diff --git a/internal/consensus/consensus_message.go b/internal/consensus/consensus_message.go new file mode 100644 index 0000000..8b2147d --- /dev/null +++ b/internal/consensus/consensus_message.go @@ -0,0 +1,110 @@ +package consensus + +import ( + "bytes" + "encoding/gob" + "fmt" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" +) + +type ( + // Serializable is an interface for serializing consensus messages. + Serializable interface { + EncodeBinary(encoder *gob.Encoder) error + DecodeBinary(decoder *gob.Decoder) error + } + + message struct { + cmType dbft.MessageType + viewNumber byte + + payload any + } + + // messageAux is an auxiliary structure for message marshalling. + messageAux struct { + CMType byte + ViewNumber byte + Payload []byte + } +) + +var _ dbft.ConsensusMessage[crypto.Uint256] = (*message)(nil) + +// EncodeBinary implements Serializable interface. +func (m message) EncodeBinary(w *gob.Encoder) error { + ww := bytes.Buffer{} + enc := gob.NewEncoder(&ww) + if err := m.payload.(Serializable).EncodeBinary(enc); err != nil { + return err + } + return w.Encode(&messageAux{ + CMType: byte(m.cmType), + ViewNumber: m.viewNumber, + Payload: ww.Bytes(), + }) +} + +// DecodeBinary implements Serializable interface. +func (m *message) DecodeBinary(r *gob.Decoder) error { + aux := new(messageAux) + if err := r.Decode(aux); err != nil { + return err + } + m.cmType = dbft.MessageType(aux.CMType) + m.viewNumber = aux.ViewNumber + + switch m.cmType { + case dbft.ChangeViewType: + cv := new(changeView) + cv.newViewNumber = m.viewNumber + 1 + m.payload = cv + case dbft.PrepareRequestType: + m.payload = new(prepareRequest) + case dbft.PrepareResponseType: + m.payload = new(prepareResponse) + case dbft.CommitType: + m.payload = new(commit) + case dbft.RecoveryRequestType: + m.payload = new(recoveryRequest) + case dbft.RecoveryMessageType: + m.payload = new(recoveryMessage) + default: + return fmt.Errorf("invalid type: 0x%02x", byte(m.cmType)) + } + + rr := bytes.NewReader(aux.Payload) + dec := gob.NewDecoder(rr) + return m.payload.(Serializable).DecodeBinary(dec) +} + +func (m message) GetChangeView() dbft.ChangeView { return m.payload.(dbft.ChangeView) } +func (m message) GetPrepareRequest() dbft.PrepareRequest[crypto.Uint256] { + return m.payload.(dbft.PrepareRequest[crypto.Uint256]) +} +func (m message) GetPrepareResponse() dbft.PrepareResponse[crypto.Uint256] { + return m.payload.(dbft.PrepareResponse[crypto.Uint256]) +} +func (m message) GetCommit() dbft.Commit { return m.payload.(dbft.Commit) } +func (m message) GetPreCommit() dbft.PreCommit { return m.payload.(dbft.PreCommit) } +func (m message) GetRecoveryRequest() dbft.RecoveryRequest { return m.payload.(dbft.RecoveryRequest) } +func (m message) GetRecoveryMessage() dbft.RecoveryMessage[crypto.Uint256] { + return m.payload.(dbft.RecoveryMessage[crypto.Uint256]) +} + +// ViewNumber implements ConsensusMessage interface. +func (m message) ViewNumber() byte { + return m.viewNumber +} + +// Type implements ConsensusMessage interface. +func (m message) Type() dbft.MessageType { + return m.cmType +} + +// Payload implements ConsensusMessage interface. +func (m message) Payload() any { + return m.payload +} diff --git a/internal/consensus/constructors.go b/internal/consensus/constructors.go new file mode 100644 index 0000000..b0f30a5 --- /dev/null +++ b/internal/consensus/constructors.go @@ -0,0 +1,83 @@ +package consensus + +import ( + "encoding/binary" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" +) + +// NewConsensusPayload returns minimal ConsensusPayload implementation. +func NewConsensusPayload(t dbft.MessageType, height uint32, validatorIndex uint16, viewNumber byte, consensusMessage any) dbft.ConsensusPayload[crypto.Uint256] { + return &Payload{ + message: message{ + cmType: t, + viewNumber: viewNumber, + payload: consensusMessage, + }, + validatorIndex: validatorIndex, + height: height, + } +} + +// NewPrepareRequest returns minimal prepareRequest implementation. +func NewPrepareRequest(ts uint64, nonce uint64, transactionsHashes []crypto.Uint256) dbft.PrepareRequest[crypto.Uint256] { + return &prepareRequest{ + transactionHashes: transactionsHashes, + nonce: nonce, + timestamp: nanoSecToSec(ts), + } +} + +// NewPrepareResponse returns minimal PrepareResponse implementation. +func NewPrepareResponse(preparationHash crypto.Uint256) dbft.PrepareResponse[crypto.Uint256] { + return &prepareResponse{ + preparationHash: preparationHash, + } +} + +// NewChangeView returns minimal ChangeView implementation. +func NewChangeView(newViewNumber byte, _ dbft.ChangeViewReason, ts uint64) dbft.ChangeView { + return &changeView{ + newViewNumber: newViewNumber, + timestamp: nanoSecToSec(ts), + } +} + +// NewCommit returns minimal Commit implementation. +func NewCommit(signature []byte) dbft.Commit { + c := new(commit) + copy(c.signature[:], signature) + return c +} + +// NewPreCommit returns minimal dbft.PreCommit implementation. +func NewPreCommit(data []byte) dbft.PreCommit { + c := new(preCommit) + c.magic = binary.BigEndian.Uint32(data) + return c +} + +// NewAMEVCommit returns minimal dbft.Commit implementation for anti-MEV extension. +func NewAMEVCommit(data []byte) dbft.Commit { + c := new(amevCommit) + copy(c.data[:], data) + return c +} + +// NewRecoveryRequest returns minimal RecoveryRequest implementation. +func NewRecoveryRequest(ts uint64) dbft.RecoveryRequest { + return &recoveryRequest{ + timestamp: nanoSecToSec(ts), + } +} + +// NewRecoveryMessage returns minimal RecoveryMessage implementation. +func NewRecoveryMessage(preparationHash *crypto.Uint256) dbft.RecoveryMessage[crypto.Uint256] { + return &recoveryMessage{ + preparationHash: preparationHash, + preparationPayloads: make([]preparationCompact, 0), + commitPayloads: make([]commitCompact, 0), + changeViewPayloads: make([]changeViewCompact, 0), + } +} diff --git a/internal/consensus/helpers.go b/internal/consensus/helpers.go new file mode 100644 index 0000000..e794c41 --- /dev/null +++ b/internal/consensus/helpers.go @@ -0,0 +1,9 @@ +package consensus + +func secToNanoSec(s uint32) uint64 { + return uint64(s) * 1000000000 +} + +func nanoSecToSec(ns uint64) uint32 { + return uint32(ns / 1000000000) +} diff --git a/internal/consensus/message.go b/internal/consensus/message.go new file mode 100644 index 0000000..14bc709 --- /dev/null +++ b/internal/consensus/message.go @@ -0,0 +1,121 @@ +package consensus + +import ( + "bytes" + "encoding/gob" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" +) + +type ( + // Payload represents minimal payload containing all necessary fields. + Payload struct { + message + + version uint32 + validatorIndex uint16 + prevHash crypto.Uint256 + height uint32 + + hash *crypto.Uint256 + } + + // payloadAux is an auxiliary structure for Payload encoding. + payloadAux struct { + Version uint32 + ValidatorIndex uint16 + PrevHash crypto.Uint256 + Height uint32 + + Data []byte + } +) + +var _ dbft.ConsensusPayload[crypto.Uint256] = (*Payload)(nil) + +// EncodeBinary implements Serializable interface. +func (p Payload) EncodeBinary(w *gob.Encoder) error { + ww := bytes.Buffer{} + enc := gob.NewEncoder(&ww) + if err := p.message.EncodeBinary(enc); err != nil { + return err + } + + return w.Encode(&payloadAux{ + Version: p.version, + ValidatorIndex: p.validatorIndex, + PrevHash: p.prevHash, + Height: p.height, + Data: ww.Bytes(), + }) +} + +// DecodeBinary implements Serializable interface. +func (p *Payload) DecodeBinary(r *gob.Decoder) error { + aux := new(payloadAux) + if err := r.Decode(aux); err != nil { + return err + } + + p.version = aux.Version + p.prevHash = aux.PrevHash + p.height = aux.Height + p.validatorIndex = aux.ValidatorIndex + + rr := bytes.NewReader(aux.Data) + dec := gob.NewDecoder(rr) + return p.message.DecodeBinary(dec) +} + +// MarshalUnsigned implements ConsensusPayload interface. +func (p Payload) MarshalUnsigned() []byte { + buf := bytes.Buffer{} + enc := gob.NewEncoder(&buf) + _ = p.EncodeBinary(enc) + + return buf.Bytes() +} + +// UnmarshalUnsigned implements ConsensusPayload interface. +func (p *Payload) UnmarshalUnsigned(data []byte) error { + r := bytes.NewReader(data) + dec := gob.NewDecoder(r) + return p.DecodeBinary(dec) +} + +// Hash implements ConsensusPayload interface. +func (p *Payload) Hash() crypto.Uint256 { + if p.hash != nil { + return *p.hash + } + + data := p.MarshalUnsigned() + + return crypto.Hash256(data) +} + +// Version implements ConsensusPayload interface. +func (p Payload) Version() uint32 { + return p.version +} + +// ValidatorIndex implements ConsensusPayload interface. +func (p Payload) ValidatorIndex() uint16 { + return p.validatorIndex +} + +// SetValidatorIndex implements ConsensusPayload interface. +func (p *Payload) SetValidatorIndex(i uint16) { + p.validatorIndex = i +} + +// PrevHash implements ConsensusPayload interface. +func (p Payload) PrevHash() crypto.Uint256 { + return p.prevHash +} + +// Height implements ConsensusPayload interface. +func (p Payload) Height() uint32 { + return p.height +} diff --git a/internal/consensus/message_test.go b/internal/consensus/message_test.go new file mode 100644 index 0000000..0f29489 --- /dev/null +++ b/internal/consensus/message_test.go @@ -0,0 +1,206 @@ +package consensus + +import ( + "bytes" + "crypto/rand" + "encoding/gob" + "testing" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPayload_EncodeDecode(t *testing.T) { + generateMessage := func(typ dbft.MessageType, payload any) *Payload { + return NewConsensusPayload(typ, 77, 10, 3, payload).(*Payload) + } + + t.Run("PrepareRequest", func(t *testing.T) { + m := generateMessage(dbft.PrepareRequestType, &prepareRequest{ + nonce: 123, + timestamp: 345, + transactionHashes: []crypto.Uint256{ + {1, 2, 3}, + {5, 6, 7}, + }, + }) + + testEncodeDecode(t, m, new(Payload)) + testMarshalUnmarshal(t, m, new(Payload)) + }) + + t.Run("PrepareResponse", func(t *testing.T) { + m := generateMessage(dbft.PrepareResponseType, &prepareResponse{ + preparationHash: crypto.Uint256{3}, + }) + + testEncodeDecode(t, m, new(Payload)) + testMarshalUnmarshal(t, m, new(Payload)) + }) + + t.Run("Commit", func(t *testing.T) { + var cc commit + fillRandom(t, cc.signature[:]) + m := generateMessage(dbft.CommitType, &cc) + + testEncodeDecode(t, m, new(Payload)) + testMarshalUnmarshal(t, m, new(Payload)) + }) + + t.Run("ChangeView", func(t *testing.T) { + m := generateMessage(dbft.ChangeViewType, &changeView{ + timestamp: 12345, + newViewNumber: 4, + }) + + testEncodeDecode(t, m, new(Payload)) + testMarshalUnmarshal(t, m, new(Payload)) + }) + + t.Run("RecoveryMessage", func(t *testing.T) { + m := generateMessage(dbft.RecoveryMessageType, &recoveryMessage{ + changeViewPayloads: []changeViewCompact{ + { + Timestamp: 123, + ValidatorIndex: 1, + OriginalViewNumber: 3, + }, + }, + commitPayloads: []commitCompact{}, + preparationPayloads: []preparationCompact{ + 1: {ValidatorIndex: 1}, + 3: {ValidatorIndex: 3}, + 4: {ValidatorIndex: 4}, + }, + prepareRequest: &prepareRequest{ + nonce: 123, + timestamp: 345, + transactionHashes: []crypto.Uint256{ + {1, 2, 3}, + {5, 6, 7}, + }, + }, + }) + + testEncodeDecode(t, m, new(Payload)) + testMarshalUnmarshal(t, m, new(Payload)) + }) + + t.Run("RecoveryRequest", func(t *testing.T) { + m := generateMessage(dbft.RecoveryRequestType, &recoveryRequest{ + timestamp: 17334, + }) + + testEncodeDecode(t, m, new(Payload)) + testMarshalUnmarshal(t, m, new(Payload)) + }) +} + +func TestRecoveryMessage_NoPayloads(t *testing.T) { + m := NewConsensusPayload(dbft.RecoveryRequestType, 77, 0, 3, &recoveryMessage{}).(*Payload) + + validators := make([]dbft.PublicKey, 1) + _, validators[0] = crypto.Generate(rand.Reader) + + rec := m.GetRecoveryMessage() + require.NotNil(t, rec) + + var p dbft.ConsensusPayload[crypto.Uint256] + require.NotPanics(t, func() { p = rec.GetPrepareRequest(p, validators, 0) }) + require.Nil(t, p) + + var ps []dbft.ConsensusPayload[crypto.Uint256] + require.NotPanics(t, func() { ps = rec.GetPrepareResponses(p, validators) }) + require.Len(t, ps, 0) + + require.NotPanics(t, func() { ps = rec.GetCommits(p, validators) }) + require.Len(t, ps, 0) + + require.NotPanics(t, func() { ps = rec.GetChangeViews(p, validators) }) + require.Len(t, ps, 0) +} + +func TestCompact_EncodeDecode(t *testing.T) { + t.Run("ChangeView", func(t *testing.T) { + p := &changeViewCompact{ + ValidatorIndex: 10, + OriginalViewNumber: 31, + Timestamp: 98765, + } + + testEncodeDecode(t, p, new(changeViewCompact)) + }) + + t.Run("Preparation", func(t *testing.T) { + p := &preparationCompact{ + ValidatorIndex: 10, + } + + testEncodeDecode(t, p, new(preparationCompact)) + }) + + t.Run("Commit", func(t *testing.T) { + p := &commitCompact{ + ValidatorIndex: 10, + ViewNumber: 77, + } + fillRandom(t, p.Signature[:]) + + testEncodeDecode(t, p, new(commitCompact)) + }) +} + +func TestPayload_Setters(t *testing.T) { + t.Run("ChangeView", func(t *testing.T) { + cv := NewChangeView(4, 0, secToNanoSec(1234)) + + assert.EqualValues(t, 4, cv.NewViewNumber()) + }) + + t.Run("RecoveryRequest", func(t *testing.T) { + r := NewRecoveryRequest(secToNanoSec(321)) + + require.EqualValues(t, secToNanoSec(321), r.Timestamp()) + }) + + t.Run("RecoveryMessage", func(t *testing.T) { + r := NewRecoveryMessage(&crypto.Uint256{1, 2, 3}) + + require.Equal(t, &crypto.Uint256{1, 2, 3}, r.PreparationHash()) + }) +} + +func TestMessageType_String(t *testing.T) { + require.Equal(t, "ChangeView", dbft.ChangeViewType.String()) + require.Equal(t, "PrepareRequest", dbft.PrepareRequestType.String()) + require.Equal(t, "PrepareResponse", dbft.PrepareResponseType.String()) + require.Equal(t, "Commit", dbft.CommitType.String()) + require.Equal(t, "RecoveryRequest", dbft.RecoveryRequestType.String()) + require.Equal(t, "RecoveryMessage", dbft.RecoveryMessageType.String()) +} + +func testEncodeDecode(t *testing.T, expected, actual Serializable) { + var buf bytes.Buffer + w := gob.NewEncoder(&buf) + err := expected.EncodeBinary(w) + require.NoError(t, err) + + b := buf.Bytes() + r := gob.NewDecoder(bytes.NewReader(b)) + + err = actual.DecodeBinary(r) + require.NoError(t, err) + require.Equal(t, expected, actual) +} + +func testMarshalUnmarshal(t *testing.T, expected, actual *Payload) { + data := expected.MarshalUnsigned() + require.NoError(t, actual.UnmarshalUnsigned(data)) + require.Equal(t, expected.Hash(), actual.Hash()) +} + +func fillRandom(t *testing.T, arr []byte) { + _, _ = rand.Read(arr) +} diff --git a/internal/consensus/prepare_request.go b/internal/consensus/prepare_request.go new file mode 100644 index 0000000..d5becc0 --- /dev/null +++ b/internal/consensus/prepare_request.go @@ -0,0 +1,61 @@ +package consensus + +import ( + "encoding/gob" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" +) + +type ( + prepareRequest struct { + transactionHashes []crypto.Uint256 + nonce uint64 + timestamp uint32 + } + // prepareRequestAux is an auxiliary structure for prepareRequest encoding. + prepareRequestAux struct { + TransactionHashes []crypto.Uint256 + Nonce uint64 + Timestamp uint32 + } +) + +var _ dbft.PrepareRequest[crypto.Uint256] = (*prepareRequest)(nil) + +// EncodeBinary implements Serializable interface. +func (p prepareRequest) EncodeBinary(w *gob.Encoder) error { + return w.Encode(&prepareRequestAux{ + TransactionHashes: p.transactionHashes, + Nonce: p.nonce, + Timestamp: p.timestamp, + }) +} + +// DecodeBinary implements Serializable interface. +func (p *prepareRequest) DecodeBinary(r *gob.Decoder) error { + aux := new(prepareRequestAux) + if err := r.Decode(aux); err != nil { + return err + } + + p.timestamp = aux.Timestamp + p.nonce = aux.Nonce + p.transactionHashes = aux.TransactionHashes + return nil +} + +// Timestamp implements PrepareRequest interface. +func (p prepareRequest) Timestamp() uint64 { + return secToNanoSec(p.timestamp) +} + +// Nonce implements PrepareRequest interface. +func (p prepareRequest) Nonce() uint64 { + return p.nonce +} + +// TransactionHashes implements PrepareRequest interface. +func (p prepareRequest) TransactionHashes() []crypto.Uint256 { + return p.transactionHashes +} diff --git a/internal/consensus/prepare_response.go b/internal/consensus/prepare_response.go new file mode 100644 index 0000000..4261d19 --- /dev/null +++ b/internal/consensus/prepare_response.go @@ -0,0 +1,43 @@ +package consensus + +import ( + "encoding/gob" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" +) + +type ( + prepareResponse struct { + preparationHash crypto.Uint256 + } + // prepareResponseAux is an auxiliary structure for prepareResponse encoding. + prepareResponseAux struct { + PreparationHash crypto.Uint256 + } +) + +var _ dbft.PrepareResponse[crypto.Uint256] = (*prepareResponse)(nil) + +// EncodeBinary implements Serializable interface. +func (p prepareResponse) EncodeBinary(w *gob.Encoder) error { + return w.Encode(prepareResponseAux{ + PreparationHash: p.preparationHash, + }) +} + +// DecodeBinary implements Serializable interface. +func (p *prepareResponse) DecodeBinary(r *gob.Decoder) error { + aux := new(prepareResponseAux) + if err := r.Decode(aux); err != nil { + return err + } + + p.preparationHash = aux.PreparationHash + return nil +} + +// PreparationHash implements PrepareResponse interface. +func (p *prepareResponse) PreparationHash() crypto.Uint256 { + return p.preparationHash +} diff --git a/internal/consensus/recovery_message.go b/internal/consensus/recovery_message.go new file mode 100644 index 0000000..a9af7c4 --- /dev/null +++ b/internal/consensus/recovery_message.go @@ -0,0 +1,236 @@ +package consensus + +import ( + "encoding/binary" + "encoding/gob" + "errors" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" +) + +type ( + recoveryMessage struct { + preparationHash *crypto.Uint256 + preparationPayloads []preparationCompact + preCommitPayloads []preCommitCompact + commitPayloads []commitCompact + changeViewPayloads []changeViewCompact + prepareRequest dbft.PrepareRequest[crypto.Uint256] + } + // recoveryMessageAux is an auxiliary structure for recoveryMessage encoding. + recoveryMessageAux struct { + PreparationPayloads []preparationCompact + PreCommitPayloads []preCommitCompact + CommitPayloads []commitCompact + ChangeViewPayloads []changeViewCompact + } +) + +var _ dbft.RecoveryMessage[crypto.Uint256] = (*recoveryMessage)(nil) + +// PreparationHash implements RecoveryMessage interface. +func (m *recoveryMessage) PreparationHash() *crypto.Uint256 { + return m.preparationHash +} + +// AddPayload implements RecoveryMessage interface. +func (m *recoveryMessage) AddPayload(p dbft.ConsensusPayload[crypto.Uint256]) { + switch p.Type() { + case dbft.PrepareRequestType: + m.prepareRequest = p.GetPrepareRequest() + prepHash := p.Hash() + m.preparationHash = &prepHash + case dbft.PrepareResponseType: + m.preparationPayloads = append(m.preparationPayloads, preparationCompact{ + ValidatorIndex: p.ValidatorIndex(), + }) + case dbft.ChangeViewType: + m.changeViewPayloads = append(m.changeViewPayloads, changeViewCompact{ + ValidatorIndex: p.ValidatorIndex(), + OriginalViewNumber: p.ViewNumber(), + Timestamp: 0, + }) + case dbft.PreCommitType: + pcc := preCommitCompact{ + ViewNumber: p.ViewNumber(), + ValidatorIndex: p.ValidatorIndex(), + Data: p.GetPreCommit().Data(), + } + m.preCommitPayloads = append(m.preCommitPayloads, pcc) + case dbft.CommitType: + cc := commitCompact{ + ViewNumber: p.ViewNumber(), + ValidatorIndex: p.ValidatorIndex(), + } + copy(cc.Signature[:], p.GetCommit().Signature()) + m.commitPayloads = append(m.commitPayloads, cc) + default: + // Other types (recoveries) can't be packed into recovery. + } +} + +func fromPayload(t dbft.MessageType, recovery dbft.ConsensusPayload[crypto.Uint256], p Serializable) *Payload { + return &Payload{ + message: message{ + cmType: t, + viewNumber: recovery.ViewNumber(), + payload: p, + }, + height: recovery.Height(), + } +} + +// GetPrepareRequest implements RecoveryMessage interface. +func (m *recoveryMessage) GetPrepareRequest(p dbft.ConsensusPayload[crypto.Uint256], _ []dbft.PublicKey, ind uint16) dbft.ConsensusPayload[crypto.Uint256] { + if m.prepareRequest == nil { + return nil + } + + req := fromPayload(dbft.PrepareRequestType, p, &prepareRequest{ + // prepareRequest.Timestamp() here returns nanoseconds-precision value, so convert it to seconds again + timestamp: nanoSecToSec(m.prepareRequest.Timestamp()), + nonce: m.prepareRequest.Nonce(), + transactionHashes: m.prepareRequest.TransactionHashes(), + }) + req.SetValidatorIndex(ind) + + return req +} + +// GetPrepareResponses implements RecoveryMessage interface. +func (m *recoveryMessage) GetPrepareResponses(p dbft.ConsensusPayload[crypto.Uint256], _ []dbft.PublicKey) []dbft.ConsensusPayload[crypto.Uint256] { + if m.preparationHash == nil { + return nil + } + + payloads := make([]dbft.ConsensusPayload[crypto.Uint256], len(m.preparationPayloads)) + + for i, resp := range m.preparationPayloads { + payloads[i] = fromPayload(dbft.PrepareResponseType, p, &prepareResponse{ + preparationHash: *m.preparationHash, + }) + payloads[i].SetValidatorIndex(resp.ValidatorIndex) + } + + return payloads +} + +// GetChangeViews implements RecoveryMessage interface. +func (m *recoveryMessage) GetChangeViews(p dbft.ConsensusPayload[crypto.Uint256], _ []dbft.PublicKey) []dbft.ConsensusPayload[crypto.Uint256] { + payloads := make([]dbft.ConsensusPayload[crypto.Uint256], len(m.changeViewPayloads)) + + for i, cv := range m.changeViewPayloads { + payloads[i] = fromPayload(dbft.ChangeViewType, p, &changeView{ + newViewNumber: cv.OriginalViewNumber + 1, + timestamp: cv.Timestamp, + }) + payloads[i].SetValidatorIndex(cv.ValidatorIndex) + } + + return payloads +} + +// GetPreCommits implements RecoveryMessage interface. +func (m *recoveryMessage) GetPreCommits(p dbft.ConsensusPayload[crypto.Uint256], _ []dbft.PublicKey) []dbft.ConsensusPayload[crypto.Uint256] { + payloads := make([]dbft.ConsensusPayload[crypto.Uint256], len(m.preCommitPayloads)) + + for i, c := range m.preCommitPayloads { + payloads[i] = fromPayload(dbft.PreCommitType, p, &preCommit{magic: binary.BigEndian.Uint32(c.Data)}) + payloads[i].SetValidatorIndex(c.ValidatorIndex) + } + + return payloads +} + +// GetCommits implements RecoveryMessage interface. +func (m *recoveryMessage) GetCommits(p dbft.ConsensusPayload[crypto.Uint256], _ []dbft.PublicKey) []dbft.ConsensusPayload[crypto.Uint256] { + payloads := make([]dbft.ConsensusPayload[crypto.Uint256], len(m.commitPayloads)) + + for i, c := range m.commitPayloads { + payloads[i] = fromPayload(dbft.CommitType, p, &commit{signature: c.Signature}) + payloads[i].SetValidatorIndex(c.ValidatorIndex) + } + + return payloads +} + +// EncodeBinary implements Serializable interface. +func (m recoveryMessage) EncodeBinary(w *gob.Encoder) error { + hasReq := m.prepareRequest != nil + if err := w.Encode(hasReq); err != nil { + return err + } + if hasReq { + if err := m.prepareRequest.(Serializable).EncodeBinary(w); err != nil { + return err + } + } else { + if m.preparationHash == nil { + if err := w.Encode(0); err != nil { + return err + } + } else { + if err := w.Encode(crypto.Uint256Size); err != nil { + return err + } + if err := w.Encode(m.preparationHash); err != nil { + return err + } + } + } + return w.Encode(&recoveryMessageAux{ + PreparationPayloads: m.preparationPayloads, + CommitPayloads: m.commitPayloads, + ChangeViewPayloads: m.changeViewPayloads, + }) +} + +// DecodeBinary implements Serializable interface. +func (m *recoveryMessage) DecodeBinary(r *gob.Decoder) error { + var hasReq bool + if err := r.Decode(&hasReq); err != nil { + return err + } + if hasReq { + m.prepareRequest = new(prepareRequest) + if err := m.prepareRequest.(Serializable).DecodeBinary(r); err != nil { + return err + } + } else { + var l int + if err := r.Decode(&l); err != nil { + return err + } + if l != 0 { + if l == crypto.Uint256Size { + m.preparationHash = new(crypto.Uint256) + if err := r.Decode(m.preparationHash); err != nil { + return err + } + } else { + return errors.New("wrong crypto.Uint256 length") + } + } else { + m.preparationHash = nil + } + } + + aux := new(recoveryMessageAux) + if err := r.Decode(aux); err != nil { + return err + } + m.preparationPayloads = aux.PreparationPayloads + if m.preparationPayloads == nil { + m.preparationPayloads = []preparationCompact{} + } + m.commitPayloads = aux.CommitPayloads + if m.commitPayloads == nil { + m.commitPayloads = []commitCompact{} + } + m.changeViewPayloads = aux.ChangeViewPayloads + if m.changeViewPayloads == nil { + m.changeViewPayloads = []changeViewCompact{} + } + return nil +} diff --git a/internal/consensus/recovery_request.go b/internal/consensus/recovery_request.go new file mode 100644 index 0000000..212a447 --- /dev/null +++ b/internal/consensus/recovery_request.go @@ -0,0 +1,42 @@ +package consensus + +import ( + "encoding/gob" + + "github.com/tutus-one/tutus-consensus" +) + +type ( + recoveryRequest struct { + timestamp uint32 + } + // recoveryRequestAux is an auxiliary structure for recoveryRequest encoding. + recoveryRequestAux struct { + Timestamp uint32 + } +) + +var _ dbft.RecoveryRequest = (*recoveryRequest)(nil) + +// EncodeBinary implements Serializable interface. +func (m recoveryRequest) EncodeBinary(w *gob.Encoder) error { + return w.Encode(&recoveryRequestAux{ + Timestamp: m.timestamp, + }) +} + +// DecodeBinary implements Serializable interface. +func (m *recoveryRequest) DecodeBinary(r *gob.Decoder) error { + aux := new(recoveryRequestAux) + if err := r.Decode(aux); err != nil { + return err + } + + m.timestamp = aux.Timestamp + return nil +} + +// Timestamp implements RecoveryRequest interface. +func (m *recoveryRequest) Timestamp() uint64 { + return secToNanoSec(m.timestamp) +} diff --git a/internal/consensus/transaction.go b/internal/consensus/transaction.go new file mode 100644 index 0000000..7abcaf2 --- /dev/null +++ b/internal/consensus/transaction.go @@ -0,0 +1,41 @@ +package consensus + +import ( + "encoding/binary" + "errors" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" +) + +// ============================= +// Small transaction. +// ============================= + +type Tx64 uint64 + +var _ dbft.Transaction[crypto.Uint256] = (*Tx64)(nil) + +func (t *Tx64) Hash() (h crypto.Uint256) { + binary.LittleEndian.PutUint64(h[:], uint64(*t)) + return +} + +// MarshalBinary implements encoding.BinaryMarshaler interface. +func (t *Tx64) MarshalBinary() ([]byte, error) { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(*t)) + + return b, nil +} + +// UnmarshalBinary implements encoding.BinaryUnarshaler interface. +func (t *Tx64) UnmarshalBinary(data []byte) error { + if len(data) != 8 { + return errors.New("length must equal 8 bytes") + } + + *t = Tx64(binary.LittleEndian.Uint64(data)) + + return nil +} diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go new file mode 100644 index 0000000..8813201 --- /dev/null +++ b/internal/crypto/crypto.go @@ -0,0 +1,33 @@ +package crypto + +import ( + "io" + + "github.com/tutus-one/tutus-consensus" +) + +type suiteType byte + +const ( + // SuiteECDSA is a ECDSA suite over P-256 curve + // with 64-byte uncompressed signatures. + SuiteECDSA suiteType = 1 + iota +) + +const defaultSuite = SuiteECDSA + +// Generate generates new key pair using r +// as a source of entropy. +func Generate(r io.Reader) (dbft.PrivateKey, dbft.PublicKey) { + return GenerateWith(defaultSuite, r) +} + +// GenerateWith generates new key pair for suite t +// using r as a source of entropy. +func GenerateWith(t suiteType, r io.Reader) (dbft.PrivateKey, dbft.PublicKey) { + if t == SuiteECDSA { + return generateECDSA(r) + } + + return nil, nil +} diff --git a/internal/crypto/crypto_test.go b/internal/crypto/crypto_test.go new file mode 100644 index 0000000..c83f8fa --- /dev/null +++ b/internal/crypto/crypto_test.go @@ -0,0 +1,34 @@ +package crypto + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVerifySignature(t *testing.T) { + const dataSize = 1000 + + priv, pub := Generate(rand.Reader) + data := make([]byte, dataSize) + _, err := rand.Reader.Read(data) + require.NoError(t, err) + + sign, err := priv.(*ECDSAPriv).Sign(data) + require.NoError(t, err) + require.Equal(t, 64, len(sign)) + + err = pub.(*ECDSAPub).Verify(data, sign) + require.NoError(t, err) +} + +func TestGenerateWith(t *testing.T) { + priv, pub := GenerateWith(defaultSuite, rand.Reader) + require.NotNil(t, priv) + require.NotNil(t, pub) + + priv, pub = GenerateWith(suiteType(0xFF), rand.Reader) + require.Nil(t, priv) + require.Nil(t, pub) +} diff --git a/internal/crypto/ecdsa.go b/internal/crypto/ecdsa.go new file mode 100644 index 0000000..155250f --- /dev/null +++ b/internal/crypto/ecdsa.go @@ -0,0 +1,85 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "errors" + "io" + "math/big" + + "github.com/tutus-one/tutus-consensus" +) + +type ( + // ECDSAPub is a wrapper over *ecsda.PublicKey. + ECDSAPub struct { + *ecdsa.PublicKey + } + + // ECDSAPriv is a wrapper over *ecdsa.PrivateKey. + ECDSAPriv struct { + *ecdsa.PrivateKey + } +) + +func generateECDSA(r io.Reader) (dbft.PrivateKey, dbft.PublicKey) { + key, err := ecdsa.GenerateKey(elliptic.P256(), r) + if err != nil { + return nil, nil + } + + return NewECDSAPrivateKey(key), NewECDSAPublicKey(&key.PublicKey) +} + +// NewECDSAPublicKey returns new PublicKey from *ecdsa.PublicKey. +func NewECDSAPublicKey(pub *ecdsa.PublicKey) dbft.PublicKey { + return &ECDSAPub{ + PublicKey: pub, + } +} + +// NewECDSAPrivateKey returns new PublicKey from *ecdsa.PrivateKey. +func NewECDSAPrivateKey(key *ecdsa.PrivateKey) dbft.PrivateKey { + return &ECDSAPriv{ + PrivateKey: key, + } +} + +// Sign signs message using P-256 curve. +func (e ECDSAPriv) Sign(msg []byte) ([]byte, error) { + h := sha256.Sum256(msg) + r, s, err := ecdsa.Sign(rand.Reader, e.PrivateKey, h[:]) + if err != nil { + return nil, err + } + + sig := make([]byte, 32*2) + _ = r.FillBytes(sig[:32]) + _ = s.FillBytes(sig[32:]) + + return sig, nil +} + +// Equals implements dbft.PublicKey interface. +func (e *ECDSAPub) Equals(other dbft.PublicKey) bool { + return e.Equal(other.(*ECDSAPub).PublicKey) +} + +// Compare does three-way comparison of ECDSAPub. +func (e *ECDSAPub) Compare(p *ECDSAPub) int { + return e.X.Cmp(p.X) +} + +// Verify verifies signature using P-256 curve. +func (e ECDSAPub) Verify(msg, sig []byte) error { + h := sha256.Sum256(msg) + rBytes := new(big.Int).SetBytes(sig[0:32]) + sBytes := new(big.Int).SetBytes(sig[32:64]) + res := ecdsa.Verify(e.PublicKey, h[:], rBytes, sBytes) + if !res { + return errors.New("bad signature") + } + return nil +} diff --git a/internal/crypto/ecdsa_test.go b/internal/crypto/ecdsa_test.go new file mode 100644 index 0000000..4aa06be --- /dev/null +++ b/internal/crypto/ecdsa_test.go @@ -0,0 +1,20 @@ +package crypto + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +// Do not generate keys with not enough entropy. +func TestECDSA_Generate(t *testing.T) { + rd := &errorReader{} + priv, pub := GenerateWith(SuiteECDSA, rd) + require.Nil(t, priv) + require.Nil(t, pub) +} + +type errorReader struct{} + +func (r *errorReader) Read(_ []byte) (int, error) { return 0, errors.New("error on read") } diff --git a/internal/crypto/hash.go b/internal/crypto/hash.go new file mode 100644 index 0000000..aa2b377 --- /dev/null +++ b/internal/crypto/hash.go @@ -0,0 +1,46 @@ +package crypto + +import ( + "crypto/sha256" + "encoding/hex" +) + +const ( + Uint256Size = 32 + Uint160Size = 20 +) + +type ( + Uint256 [Uint256Size]byte + Uint160 [Uint160Size]byte +) + +// String implements fmt.Stringer interface. +func (h Uint256) String() string { + return hex.EncodeToString(h[:]) +} + +// String implements fmt.Stringer interface. +func (h Uint160) String() string { + return hex.EncodeToString(h[:]) +} + +// Hash256 returns double sha-256 of data. +func Hash256(data []byte) Uint256 { + h1 := sha256.Sum256(data) + h2 := sha256.Sum256(h1[:]) + + return h2 +} + +// Hash160 returns ripemd160 from sha256 of data. +func Hash160(data []byte) Uint160 { + var ( + h1 = sha256.Sum256(data) + h Uint160 + ) + + copy(h[:], h1[:Uint160Size]) + + return h +} diff --git a/internal/crypto/hash_test.go b/internal/crypto/hash_test.go new file mode 100644 index 0000000..9610b61 --- /dev/null +++ b/internal/crypto/hash_test.go @@ -0,0 +1,50 @@ +package crypto + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" +) + +var hash256tc = []struct { + data []byte + hash Uint256 +}{ + {[]byte{}, parse256("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456")}, + {[]byte{0, 1, 2, 3}, parse256("f7a355c00c89a08c80636bed35556a210b51786f6803a494f28fc5ba05959fc2")}, +} + +var hash160tc = []struct { + data []byte + hash Uint160 +}{ + {[]byte{}, Uint160{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4}}, + {[]byte{0, 1, 2, 3}, Uint160{0x5, 0x4e, 0xde, 0xc1, 0xd0, 0x21, 0x1f, 0x62, 0x4f, 0xed, 0xc, 0xbc, 0xa9, 0xd4, 0xf9, 0x40, 0xb, 0xe, 0x49, 0x1c}}, +} + +func TestHash256(t *testing.T) { + for _, tc := range hash256tc { + require.Equal(t, tc.hash, Hash256(tc.data)) + } +} + +func TestHash160(t *testing.T) { + for _, tc := range hash160tc { + require.Equal(t, tc.hash, Hash160(tc.data)) + } +} + +func parse256(s string) (h Uint256) { + parseHex(h[:], s) + return +} + +func parseHex(b []byte, s string) { + buf, err := hex.DecodeString(s) + if err != nil || len(buf) != len(b) { + panic("invalid test data") + } + + copy(b, buf) +} diff --git a/internal/merkle/merkle_tree.go b/internal/merkle/merkle_tree.go new file mode 100644 index 0000000..24e7dae --- /dev/null +++ b/internal/merkle/merkle_tree.go @@ -0,0 +1,79 @@ +package merkle + +import ( + "github.com/tutus-one/tutus-consensus/internal/crypto" +) + +type ( + // Tree represents a merkle tree with specified depth. + Tree struct { + Depth int + + root *TreeNode + } + + // TreeNode represents inner node of a merkle tree. + TreeNode struct { + Hash crypto.Uint256 + Parent *TreeNode + Left *TreeNode + Right *TreeNode + } +) + +// NewMerkleTree returns new merkle tree built on hashes. +func NewMerkleTree(hashes ...crypto.Uint256) *Tree { + if len(hashes) == 0 { + return nil + } + + nodes := make([]TreeNode, len(hashes)) + for i := range nodes { + nodes[i].Hash = hashes[i] + } + + mt := &Tree{root: buildTree(nodes...)} + mt.Depth = 1 + + for node := mt.root; node.Left != nil; node = node.Left { + mt.Depth++ + } + + return mt +} + +// Root returns m's root. +func (m *Tree) Root() *TreeNode { + return m.root +} + +func buildTree(leaves ...TreeNode) *TreeNode { + l := len(leaves) + if l == 1 { + return &leaves[0] + } + + parents := make([]TreeNode, (l+1)/2) + for i := range parents { + parents[i].Left = &leaves[i*2] + leaves[i*2].Parent = &parents[i] + + if i*2+1 == l { + parents[i].Right = parents[i].Left + } else { + parents[i].Right = &leaves[i*2+1] + leaves[i*2+1].Parent = &parents[i] + } + + data := append(parents[i].Left.Hash[:], parents[i].Right.Hash[:]...) + parents[i].Hash = crypto.Hash256(data) + } + + return buildTree(parents...) +} + +// IsLeaf returns true iff n is a leaf. +func (n *TreeNode) IsLeaf() bool { return n.Left == nil && n.Right == nil } + +// IsRoot returns true iff n is a root. +func (n *TreeNode) IsRoot() bool { return n.Parent == nil } diff --git a/internal/merkle/merkle_tree_test.go b/internal/merkle/merkle_tree_test.go new file mode 100644 index 0000000..d007e48 --- /dev/null +++ b/internal/merkle/merkle_tree_test.go @@ -0,0 +1,59 @@ +package merkle + +import ( + "crypto/sha256" + "encoding/hex" + "testing" + + "github.com/tutus-one/tutus-consensus/internal/crypto" + "github.com/stretchr/testify/require" +) + +func TestNewMerkleTree(t *testing.T) { + t.Run("empty tree must be nil", func(t *testing.T) { + require.Nil(t, NewMerkleTree()) + }) + + t.Run("merkle tree on 1 leave", func(t *testing.T) { + h := crypto.Uint256{1, 2, 3, 4} + mt := NewMerkleTree(h) + require.NotNil(t, mt) + require.Equal(t, 1, mt.Depth) + require.Equal(t, h, mt.Root().Hash) + require.True(t, mt.Root().IsLeaf()) + }) + + t.Run("predefined tree on 4 leaves", func(t *testing.T) { + hashes := make([]crypto.Uint256, 5) + for i := range hashes { + hashes[i] = sha256.Sum256([]byte{byte(i)}) + } + + mt := NewMerkleTree(hashes...) + require.NotNil(t, mt) + require.Equal(t, 4, mt.Depth) + + expected, err := hex.DecodeString("f570734e3e3e401dad09b8f51499dfb2f631c803b88487ef65b88baa069430d0") + require.NoError(t, err) + require.Equal(t, expected, mt.Root().Hash[:]) + }) +} + +func TestTreeNode_IsLeaf(t *testing.T) { + hashes := []crypto.Uint256{{1}, {2}, {3}} + + mt := NewMerkleTree(hashes...) + require.NotNil(t, mt) + require.True(t, mt.Root().IsRoot()) + require.False(t, mt.Root().IsLeaf()) + + left := mt.Root().Left + require.NotNil(t, left) + require.False(t, left.IsRoot()) + require.False(t, left.IsLeaf()) + + lleft := left.Left + require.NotNil(t, lleft) + require.False(t, lleft.IsRoot()) + require.True(t, lleft.IsLeaf()) +} diff --git a/internal/simulation/main.go b/internal/simulation/main.go new file mode 100644 index 0000000..10d43fb --- /dev/null +++ b/internal/simulation/main.go @@ -0,0 +1,314 @@ +package main + +import ( + "context" + "crypto/rand" + "flag" + "fmt" + "net/http" + "net/http/pprof" + "os" + "os/signal" + "slices" + "sync" + "syscall" + "time" + + "github.com/tutus-one/tutus-consensus" + "github.com/tutus-one/tutus-consensus/internal/consensus" + "github.com/tutus-one/tutus-consensus/internal/crypto" + "go.uber.org/zap" +) + +type ( + simNode struct { + id int + d *dbft.DBFT[crypto.Uint256] + messages chan dbft.ConsensusPayload[crypto.Uint256] + key dbft.PrivateKey + pub dbft.PublicKey + pool *memPool + cluster []*simNode + log *zap.Logger + + height uint32 + lastHash crypto.Uint256 + validators []dbft.PublicKey + } +) + +const ( + defaultChanSize = 100 +) + +var ( + nodebug = flag.Bool("nodebug", false, "disable debug logging") + count = flag.Int("count", 7, "node count") + watchers = flag.Int("watchers", 7, "watch-only node count") + blocked = flag.Int("blocked", -1, "blocked validator (payloads from him/her are dropped)") + txPerBlock = flag.Int("txblock", 1, "transactions per block") + txCount = flag.Int("txcount", 100000, "transactions on every node") + duration = flag.Duration("duration", time.Second*20, "duration of simulation (infinite by default)") +) + +func main() { + flag.Parse() + + initDebugger() + + logger := initLogger() + clusterSize := *count + watchOnly := *watchers + nodes := make([]*simNode, clusterSize+watchOnly) + + initNodes(nodes, logger) + updatePublicKeys(nodes, clusterSize) + + ctx, cancel := initContext(*duration) + defer cancel() + + wg := new(sync.WaitGroup) + wg.Add(len(nodes)) + + for i := range nodes { + go func(i int) { + defer wg.Done() + + nodes[i].Run(ctx) + }(i) + } + + wg.Wait() +} + +// Run implements simple event loop. +func (n *simNode) Run(ctx context.Context) { + n.d.Start(0) + + for { + select { + case <-ctx.Done(): + n.log.Info("context cancelled") + return + case <-n.d.Timer.C(): + n.d.OnTimeout(n.d.Timer.Height(), n.d.Timer.View()) + case msg := <-n.messages: + n.d.OnReceive(msg) + } + } +} + +func initNodes(nodes []*simNode, log *zap.Logger) { + for i := range nodes { + if err := initSimNode(nodes, i, log); err != nil { + panic(err) + } + } +} + +func initSimNode(nodes []*simNode, i int, log *zap.Logger) error { + key, pub := crypto.Generate(rand.Reader) + nodes[i] = &simNode{ + id: i, + messages: make(chan dbft.ConsensusPayload[crypto.Uint256], defaultChanSize), + key: key, + pub: pub, + pool: newMemoryPool(), + log: log.With(zap.Int("id", i)), + cluster: nodes, + } + + var err error + nodes[i].d, err = consensus.New(nodes[i].log, key, pub, nodes[i].pool.Get, + nodes[i].pool.GetVerified, + nodes[i].Broadcast, + nodes[i].ProcessBlock, + nodes[i].CurrentHeight, + nodes[i].CurrentBlockHash, + nodes[i].GetValidators, + nodes[i].VerifyPayload, + ) + if err != nil { + return fmt.Errorf("failed to initialize dBFT: %w", err) + } + + nodes[i].addTx(*txCount) + + return nil +} + +func updatePublicKeys(nodes []*simNode, n int) { + pubs := make([]dbft.PublicKey, n) + for i := range pubs { + pubs[i] = nodes[i].pub + } + + sortValidators(pubs) + + for i := range nodes { + nodes[i].validators = pubs + } +} + +func sortValidators(pubs []dbft.PublicKey) { + slices.SortFunc(pubs, func(a, b dbft.PublicKey) int { + x := a.(*crypto.ECDSAPub) + y := b.(*crypto.ECDSAPub) + return x.Compare(y) + }) +} + +func (n *simNode) Broadcast(m dbft.ConsensusPayload[crypto.Uint256]) { + for i, node := range n.cluster { + if i != n.id { + select { + case node.messages <- m: + default: + n.log.Warn("can't broadcast message: channel is full") + } + } + } +} + +func (n *simNode) CurrentHeight() uint32 { return n.height } +func (n *simNode) CurrentBlockHash() crypto.Uint256 { return n.lastHash } + +// GetValidators always returns the same list of validators. +func (n *simNode) GetValidators(...dbft.Transaction[crypto.Uint256]) []dbft.PublicKey { + return n.validators +} + +func (n *simNode) ProcessBlock(b dbft.Block[crypto.Uint256]) error { + n.d.Logger.Debug("received block", zap.Uint32("height", b.Index())) + + for _, tx := range b.Transactions() { + n.pool.Delete(tx.Hash()) + } + + n.height = b.Index() + n.lastHash = b.Hash() + return nil +} + +// VerifyPayload verifies that payload was received from a good validator. +func (n *simNode) VerifyPayload(p dbft.ConsensusPayload[crypto.Uint256]) error { + if *blocked != -1 && p.ValidatorIndex() == uint16(*blocked) { + return fmt.Errorf("message from blocked validator: %d", *blocked) + } + return nil +} + +func (n *simNode) addTx(count int) { + for i := range count { + tx := consensus.Tx64(uint64(i)) + n.pool.Add(&tx) + } +} + +// ============================= +// Memory pool for transactions. +// ============================= + +type memPool struct { + mtx *sync.RWMutex + store map[crypto.Uint256]dbft.Transaction[crypto.Uint256] +} + +func newMemoryPool() *memPool { + return &memPool{ + mtx: new(sync.RWMutex), + store: make(map[crypto.Uint256]dbft.Transaction[crypto.Uint256]), + } +} + +func (p *memPool) Add(tx dbft.Transaction[crypto.Uint256]) { + p.mtx.Lock() + + h := tx.Hash() + if _, ok := p.store[h]; !ok { + p.store[h] = tx + } + + p.mtx.Unlock() +} + +func (p *memPool) Get(h crypto.Uint256) (tx dbft.Transaction[crypto.Uint256]) { + p.mtx.RLock() + tx = p.store[h] + p.mtx.RUnlock() + + return +} + +func (p *memPool) Delete(h crypto.Uint256) { + p.mtx.Lock() + delete(p.store, h) + p.mtx.Unlock() +} + +func (p *memPool) GetVerified() (txx []dbft.Transaction[crypto.Uint256]) { + n := *txPerBlock + if n == 0 { + return + } + + txx = make([]dbft.Transaction[crypto.Uint256], 0, n) + for _, tx := range p.store { + txx = append(txx, tx) + + if n--; n == 0 { + return + } + } + + return +} + +// initDebugger initializes pprof debug facilities. +func initDebugger() { + r := http.NewServeMux() + r.HandleFunc("/debug/pprof/", pprof.Index) + r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + r.HandleFunc("/debug/pprof/profile", pprof.Profile) + r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + r.HandleFunc("/debug/pprof/trace", pprof.Trace) + + go func() { + err := http.ListenAndServe("localhost:6060", r) + if err != nil { + panic(err) + } + }() +} + +// initLogger initializes new logger. +func initLogger() *zap.Logger { + if *nodebug { + return zap.L() + } + + logger, err := zap.NewDevelopment() + if err != nil { + panic("can't init logger") + } + + return logger +} + +// initContext creates new context which will be cancelled by Ctrl+C. +func initContext(d time.Duration) (ctx context.Context, cancel func()) { + // exit by Ctrl+C + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-c + cancel() + }() + + if d != 0 { + return context.WithTimeout(context.Background(), *duration) + } + + return context.WithCancel(context.Background()) +} diff --git a/pre_block.go b/pre_block.go new file mode 100644 index 0000000..a109700 --- /dev/null +++ b/pre_block.go @@ -0,0 +1,25 @@ +package dbft + +// PreBlock is a generic interface for a PreBlock used by anti-MEV dBFT extension. +// It holds a "draft" of block that should be converted to a final block with the +// help of additional data held by PreCommit messages. +type PreBlock[H Hash] interface { + // Data returns PreBlock's data CNs need to exchange during PreCommit phase. + // Data represents additional information not related to a final block signature. + Data() []byte + // SetData generates and sets PreBlock's data CNs need to exchange during + // PreCommit phase. + SetData(key PrivateKey) error + // Verify checks if data related to PreCommit phase is correct. This method is + // refined on PreBlock rather than on PreCommit message since PreBlock itself is + // required for PreCommit's data verification. It's guaranteed that all + // proposed transactions are collected by the moment of call to Verify. + Verify(key PublicKey, data []byte) error + + // Transactions returns PreBlock's transaction list. This list may be different + // comparing to the final set of Block's transactions. + Transactions() []Transaction[H] + // SetTransactions sets PreBlock's transaction list. This list may be different + // comparing to the final set of Block's transactions. + SetTransactions([]Transaction[H]) +} diff --git a/pre_commit.go b/pre_commit.go new file mode 100644 index 0000000..24d0a50 --- /dev/null +++ b/pre_commit.go @@ -0,0 +1,10 @@ +package dbft + +// PreCommit is an interface for dBFT PreCommit message. This message is used right +// before the Commit phase to exchange additional information required for the final +// block construction in anti-MEV dBFT extension. +type PreCommit interface { + // Data returns PreCommit's data that should be used for the final + // Block construction in anti-MEV dBFT extension. + Data() []byte +} diff --git a/prepare_request.go b/prepare_request.go new file mode 100644 index 0000000..3ba594c --- /dev/null +++ b/prepare_request.go @@ -0,0 +1,11 @@ +package dbft + +// PrepareRequest represents dBFT PrepareRequest message. +type PrepareRequest[H Hash] interface { + // Timestamp returns this message's timestamp. + Timestamp() uint64 + // Nonce is a random nonce. + Nonce() uint64 + // TransactionHashes returns hashes of all transaction in a proposed block. + TransactionHashes() []H +} diff --git a/prepare_response.go b/prepare_response.go new file mode 100644 index 0000000..1675d18 --- /dev/null +++ b/prepare_response.go @@ -0,0 +1,8 @@ +package dbft + +// PrepareResponse represents dBFT PrepareResponse message. +type PrepareResponse[H Hash] interface { + // PreparationHash returns the hash of PrepareRequest payload + // for this epoch. + PreparationHash() H +} diff --git a/recovery_message.go b/recovery_message.go new file mode 100644 index 0000000..02e6201 --- /dev/null +++ b/recovery_message.go @@ -0,0 +1,23 @@ +package dbft + +// RecoveryMessage represents dBFT Recovery message. +type RecoveryMessage[H Hash] interface { + // AddPayload adds payload from this epoch to be recovered. + AddPayload(p ConsensusPayload[H]) + // GetPrepareRequest returns PrepareRequest to be processed. + GetPrepareRequest(p ConsensusPayload[H], validators []PublicKey, primary uint16) ConsensusPayload[H] + // GetPrepareResponses returns a slice of PrepareResponse in any order. + GetPrepareResponses(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] + // GetChangeViews returns a slice of ChangeView in any order. + GetChangeViews(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] + // GetPreCommits returns a slice of PreCommit messages in any order. + // If implemented on networks with no AntiMEV extension it can just + // always return nil. + GetPreCommits(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] + // GetCommits returns a slice of Commit in any order. + GetCommits(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] + + // PreparationHash returns has of PrepareRequest payload for this epoch. + // It can be useful in case only PrepareResponse payloads were received. + PreparationHash() *H +} diff --git a/recovery_request.go b/recovery_request.go new file mode 100644 index 0000000..232b9cf --- /dev/null +++ b/recovery_request.go @@ -0,0 +1,7 @@ +package dbft + +// RecoveryRequest represents dBFT RecoveryRequest message. +type RecoveryRequest interface { + // Timestamp returns this message's timestamp. + Timestamp() uint64 +} diff --git a/rtt.go b/rtt.go new file mode 100644 index 0000000..351332c --- /dev/null +++ b/rtt.go @@ -0,0 +1,26 @@ +package dbft + +import ( + "time" +) + +const rttLength = 7 * 10 // 10 rounds with 7 nodes + +type rtt struct { + times [rttLength]time.Duration + idx int + avg time.Duration +} + +func (r *rtt) addTime(t time.Duration) { + var old = r.times[r.idx] + + if old != 0 { + t = min(t, 2*old) // Too long delays should be normalized, we don't want to overshoot. + } + + r.avg = r.avg + (t-old)/time.Duration(len(r.times)) + r.avg = max(0, r.avg) // Can't be less than zero. + r.times[r.idx] = t + r.idx = (r.idx + 1) % len(r.times) +} diff --git a/send.go b/send.go new file mode 100644 index 0000000..6c40d95 --- /dev/null +++ b/send.go @@ -0,0 +1,236 @@ +package dbft + +import ( + "fmt" + + "go.uber.org/zap" +) + +func (d *DBFT[H]) broadcast(msg ConsensusPayload[H]) { + d.Logger.Debug("broadcasting message", + zap.Stringer("type", msg.Type()), + zap.Uint32("height", d.BlockIndex), + zap.Uint("view", uint(d.ViewNumber))) + + msg.SetValidatorIndex(uint16(d.MyIndex)) + d.Broadcast(msg) +} + +func (c *Context[H]) makePrepareRequest(force bool) ConsensusPayload[H] { + if !c.Fill(force) { + return nil + } + + req := c.Config.NewPrepareRequest(c.Timestamp, c.Nonce, c.TransactionHashes) + + return c.Config.NewConsensusPayload(c, PrepareRequestType, req) +} + +func (d *DBFT[H]) sendPrepareRequest(force bool) { + msg := d.makePrepareRequest(force) + if msg == ConsensusPayload[H](nil) { + d.subscribeForTransactions() + + // Try one more time since there's a tiny race between an attempt to + // construct prepare request and transactions subscription. + msg = d.makePrepareRequest(force) + if msg == ConsensusPayload[H](nil) { + delay := d.maxTimePerBlock - d.timePerBlock + d.changeTimer(delay) + return + } + } + d.unsubscribeFromTransactions() + + d.PreparationPayloads[d.MyIndex] = msg + d.broadcast(msg) + + d.prepareSentTime = d.Timer.Now() + + delay := d.timePerBlock << (d.ViewNumber + 1) + if d.ViewNumber == 0 { + delay -= d.timePerBlock + } + + d.Logger.Info("sending PrepareRequest", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) + d.changeTimer(delay) + d.checkPrepare() +} + +func (c *Context[H]) makeChangeView(ts uint64, reason ChangeViewReason) ConsensusPayload[H] { + cv := c.Config.NewChangeView(c.ViewNumber+1, reason, ts) + + msg := c.Config.NewConsensusPayload(c, ChangeViewType, cv) + c.ChangeViewPayloads[c.MyIndex] = msg + + return msg +} + +func (d *DBFT[H]) sendChangeView(reason ChangeViewReason) { + if d.Context.WatchOnly() { + return + } + + newView := d.ViewNumber + 1 + d.changeTimer(d.timePerBlock << (newView + 1)) + + nc := d.CountCommitted() + nf := d.CountFailed() + + if reason == CVTimeout && nc+nf > d.F() { + d.Logger.Info("skip change view", zap.Int("nc", nc), zap.Int("nf", nf)) + d.sendRecoveryRequest() + + return + } + + // Timeout while missing transactions, set the real reason. + if !d.hasAllTransactions() && reason == CVTimeout { + reason = CVTxNotFound + } + + d.Logger.Info("request change view", + zap.Int("view", int(d.ViewNumber)), + zap.Uint32("height", d.BlockIndex), + zap.Stringer("reason", reason), + zap.Int("new_view", int(newView)), + zap.Int("nc", nc), + zap.Int("nf", nf)) + + msg := d.makeChangeView(uint64(d.Timer.Now().UnixNano()), reason) + d.StopTxFlow() + d.broadcast(msg) + d.checkChangeView(newView) +} + +func (c *Context[H]) makePrepareResponse() ConsensusPayload[H] { + resp := c.Config.NewPrepareResponse(c.PreparationPayloads[c.PrimaryIndex].Hash()) + + msg := c.Config.NewConsensusPayload(c, PrepareResponseType, resp) + c.PreparationPayloads[c.MyIndex] = msg + + return msg +} + +func (d *DBFT[H]) sendPrepareResponse() { + msg := d.makePrepareResponse() + d.Logger.Info("sending PrepareResponse", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) + d.StopTxFlow() + d.broadcast(msg) +} + +func (c *Context[H]) makePreCommit() (ConsensusPayload[H], error) { + if msg := c.PreCommitPayloads[c.MyIndex]; msg != nil { + return msg, nil + } + + if preB := c.CreatePreBlock(); preB != nil { + var preData []byte + if err := preB.SetData(c.Priv); err == nil { + preData = preB.Data() + } else { + return nil, fmt.Errorf("PreCommit data construction failed: %w", err) + } + + preCommit := c.Config.NewPreCommit(preData) + + return c.Config.NewConsensusPayload(c, PreCommitType, preCommit), nil + } + + return nil, fmt.Errorf("failed to construct PreBlock") +} + +func (c *Context[H]) makeCommit() (ConsensusPayload[H], error) { + if msg := c.CommitPayloads[c.MyIndex]; msg != nil { + return msg, nil + } + + if b := c.MakeHeader(); b != nil { + var sign []byte + if err := b.Sign(c.Priv); err == nil { + sign = b.Signature() + } else { + return nil, fmt.Errorf("header signing failed: %w", err) + } + + commit := c.Config.NewCommit(sign) + + return c.Config.NewConsensusPayload(c, CommitType, commit), nil + } + + return nil, fmt.Errorf("failed to construct Header") +} + +func (d *DBFT[H]) sendPreCommit() { + msg, err := d.makePreCommit() + if err != nil { + d.Logger.Error("failed to construct PreCommit", zap.Error(err)) + return + } + d.PreCommitPayloads[d.MyIndex] = msg + d.Logger.Info("sending PreCommit", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) + d.broadcast(msg) +} + +func (d *DBFT[H]) sendCommit() { + msg, err := d.makeCommit() + if err != nil { + d.Logger.Error("failed to construct Commit", zap.Error(err)) + return + } + d.CommitPayloads[d.MyIndex] = msg + d.Logger.Info("sending Commit", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) + d.broadcast(msg) +} + +func (d *DBFT[H]) sendRecoveryRequest() { + // If we're here, something is wrong, we either missing some messages or + // transactions or both, so re-request missing transactions here too. + if d.RequestSentOrReceived() && !d.hasAllTransactions() { + d.processMissingTx() + } + req := d.NewRecoveryRequest(uint64(d.Timer.Now().UnixNano())) + d.broadcast(d.NewConsensusPayload(&d.Context, RecoveryRequestType, req)) +} + +func (c *Context[H]) makeRecoveryMessage() ConsensusPayload[H] { + recovery := c.Config.NewRecoveryMessage() + + for _, p := range c.PreparationPayloads { + if p != nil { + recovery.AddPayload(p) + } + } + + cv := c.LastChangeViewPayloads + // if byte(msg.ViewNumber) == c.ViewNumber { + // cv = c.changeViewPayloads + // } + for _, p := range cv { + if p != nil { + recovery.AddPayload(p) + } + } + + if c.PreCommitSent() { + for _, p := range c.PreCommitPayloads { + if p != nil { + recovery.AddPayload(p) + } + } + } + + if c.CommitSent() { + for _, p := range c.CommitPayloads { + if p != nil { + recovery.AddPayload(p) + } + } + } + + return c.Config.NewConsensusPayload(c, RecoveryMessageType, recovery) +} + +func (d *DBFT[H]) sendRecoveryMessage() { + d.broadcast(d.makeRecoveryMessage()) +} diff --git a/timer.go b/timer.go new file mode 100644 index 0000000..1c91c26 --- /dev/null +++ b/timer.go @@ -0,0 +1,22 @@ +package dbft + +import ( + "time" +) + +// Timer is an interface which implements all time-related +// functions. It can be mocked for testing. +type Timer interface { + // Now returns current time. + Now() time.Time + // Reset resets timer to the specified block height and view. + Reset(height uint32, view byte, d time.Duration) + // Extend extends current timer with duration d. + Extend(d time.Duration) + // Height returns current height set for the timer. + Height() uint32 + // View returns current view set for the timer. + View() byte + // C returns channel for timer events. + C() <-chan time.Time +} diff --git a/timer/timer.go b/timer/timer.go new file mode 100644 index 0000000..0f8157d --- /dev/null +++ b/timer/timer.go @@ -0,0 +1,97 @@ +/* +Package timer contains default implementation of [dbft.Timer] interface and provides +all necessary timer-related functionality to [dbft.DBFT] service. +*/ +package timer + +import ( + "time" +) + +type ( + // Timer is a default [dbft.Timer] implementation. + Timer struct { + height uint32 + view byte + s time.Time + d time.Duration + tt *time.Timer + ch chan time.Time + } +) + +// New returns default Timer implementation. +func New() *Timer { + t := &Timer{ + ch: make(chan time.Time, 1), + } + + return t +} + +// C implements Timer interface. +func (t *Timer) C() <-chan time.Time { + if t.tt == nil { + return t.ch + } + + return t.tt.C +} + +// Height returns current timer height. +func (t *Timer) Height() uint32 { + return t.height +} + +// View return current timer view. +func (t *Timer) View() byte { + return t.view +} + +// Reset implements Timer interface. +func (t *Timer) Reset(height uint32, view byte, d time.Duration) { + t.stop() + + t.s = t.Now() + t.d = d + t.height = height + t.view = view + + if t.d != 0 { + t.tt = time.NewTimer(t.d) + } else { + t.tt = nil + drain(t.ch) + t.ch <- t.s + } +} + +func drain(ch <-chan time.Time) { + select { + case <-ch: + default: + } +} + +// stop stops the Timer. +func (t *Timer) stop() { + if t.tt != nil { + t.tt.Stop() + t.tt = nil + } +} + +// Extend implements Timer interface. +func (t *Timer) Extend(d time.Duration) { + t.d += d + + if elapsed := time.Since(t.s); t.d > elapsed { + t.stop() + t.tt = time.NewTimer(t.d - elapsed) + } +} + +// Now implements Timer interface. +func (t *Timer) Now() time.Time { + return time.Now() +} diff --git a/timer/timer_test.go b/timer/timer_test.go new file mode 100644 index 0000000..6b3c8b6 --- /dev/null +++ b/timer/timer_test.go @@ -0,0 +1,56 @@ +package timer + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestTimer_Reset(t *testing.T) { + tt := New() + + tt.Reset(1, 2, time.Millisecond*100) + time.Sleep(time.Millisecond * 200) + shouldReceive(t, tt, 1, 2, "no value in timer") + + tt.Reset(1, 2, time.Second) + tt.Reset(2, 3, 0) + shouldReceive(t, tt, 2, 3, "no value in timer after reset(0)") + + tt.Reset(1, 2, time.Millisecond*100) + time.Sleep(time.Millisecond * 200) + tt.Reset(1, 3, time.Millisecond*100) + time.Sleep(time.Millisecond * 200) + shouldReceive(t, tt, 1, 3, "invalid value after reset") + + tt.Reset(3, 1, time.Millisecond*100) + shouldNotReceive(t, tt, "value arrived too early") + + tt.Extend(time.Millisecond * 300) + time.Sleep(time.Millisecond * 200) + shouldNotReceive(t, tt, "value arrived too early after extend") + + time.Sleep(time.Millisecond * 300) + shouldReceive(t, tt, 3, 1, "no value in timer after extend") +} + +func shouldReceive(t *testing.T, tt *Timer, height uint32, view byte, msg string) { + select { + case <-tt.C(): + gotHeight := tt.Height() + gotView := tt.View() + require.Equal(t, height, gotHeight) + require.Equal(t, view, gotView) + default: + require.Fail(t, msg) + } +} + +func shouldNotReceive(t *testing.T, tt *Timer, msg string) { + select { + case <-tt.C(): + require.Fail(t, msg) + default: + } +} diff --git a/transaction.go b/transaction.go new file mode 100644 index 0000000..ae3277c --- /dev/null +++ b/transaction.go @@ -0,0 +1,8 @@ +package dbft + +// Transaction is a generic transaction interface. +type Transaction[H Hash] interface { + // Hash must return cryptographic hash of the transaction. + // Transactions which have equal hashes are considered equal. + Hash() H +}