├── .github ├── CODEOWNERS └── workflows │ ├── build.yml │ ├── changelog_update.yml │ ├── dco.yml │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── block.go ├── change_view.go ├── change_view_reason.go ├── change_view_reason_string.go ├── check.go ├── commit.go ├── config.go ├── consensus_message.go ├── consensus_message_type.go ├── consensus_payload.go ├── context.go ├── dbft.go ├── dbft_test.go ├── docs ├── labels.md └── release-instruction.md ├── formal-models ├── .github │ ├── dbft.drawio │ ├── dbft.png │ ├── dbft2.1_centralizedCV.drawio │ ├── dbft2.1_centralizedCV.png │ ├── dbft2.1_threeStagedCV.drawio │ ├── dbft2.1_threeStagedCV.png │ ├── dbft_antiMEV.drawio │ └── dbft_antiMEV.png ├── README.md ├── dbft │ ├── dbft.tla │ └── dbft___AllGoodModel.launch ├── dbft2.1_centralizedCV │ ├── dbftCentralizedCV.tla │ └── dbftCentralizedCV___AllGoodModel.launch ├── dbft2.1_threeStagedCV │ ├── dbftCV3.tla │ └── dbftCV3___AllGoodModel.launch ├── dbftMultipool │ ├── dbftMultipool.tla │ └── dbftMultipool___AllGoodModel.launch └── dbft_antiMEV │ ├── dbft.tla │ └── dbft___AllGoodModel.launch ├── go.mod ├── go.sum ├── helpers.go ├── helpers_test.go ├── identity.go ├── internal ├── consensus │ ├── amev_block.go │ ├── amev_commit.go │ ├── amev_preBlock.go │ ├── amev_preCommit.go │ ├── block.go │ ├── block_test.go │ ├── change_view.go │ ├── commit.go │ ├── compact.go │ ├── consensus.go │ ├── consensus_message.go │ ├── constructors.go │ ├── helpers.go │ ├── message.go │ ├── message_test.go │ ├── prepare_request.go │ ├── prepare_response.go │ ├── recovery_message.go │ ├── recovery_request.go │ └── transaction.go ├── crypto │ ├── crypto.go │ ├── crypto_test.go │ ├── ecdsa.go │ ├── ecdsa_test.go │ ├── hash.go │ └── hash_test.go ├── merkle │ ├── merkle_tree.go │ └── merkle_tree_test.go └── simulation │ └── main.go ├── pre_block.go ├── pre_commit.go ├── prepare_request.go ├── prepare_response.go ├── recovery_message.go ├── recovery_request.go ├── rtt.go ├── send.go ├── timer.go ├── timer ├── timer.go └── timer_test.go └── transaction.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AnnaShaleva @roman-khimov 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | types: [opened, synchronize] 8 | paths-ignore: 9 | - 'scripts/**' 10 | - '**/*.md' 11 | push: 12 | # Build for the master branch. 13 | branches: 14 | - master 15 | release: 16 | # Publish released commit as Docker `latest` and `git_revision` images. 17 | types: 18 | - published 19 | workflow_dispatch: 20 | inputs: 21 | ref: 22 | description: 'Ref to build dBFT [default: latest master; examples: v0.1.0, 0a4ff9d3e4a9ab432fd5812eb18c98e03b5a7432]' 23 | required: false 24 | default: '' 25 | 26 | jobs: 27 | run: 28 | name: Run simulation 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | ref: ${{ github.event.inputs.ref }} 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v5 37 | with: 38 | go-version: '1.24' 39 | cache: true 40 | 41 | - name: Run simulation 42 | run: | 43 | cd ./internal/simulation 44 | go run main.go 45 | -------------------------------------------------------------------------------- /.github/workflows/changelog_update.yml: -------------------------------------------------------------------------------- 1 | name: CHANGELOG check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**/*.md' 9 | - '**/*.yml' 10 | - '.github/workflows/**' 11 | - 'formal-models/**' 12 | 13 | jobs: 14 | check: 15 | name: Check for CHANGELOG updates 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Get changed CHANGELOG 23 | id: changelog-diff 24 | uses: tj-actions/changed-files@v46 25 | with: 26 | files: CHANGELOG.md 27 | 28 | - name: Fail if changelog not updated 29 | if: steps.changelog-diff.outputs.any_changed == 'false' 30 | uses: actions/github-script@v7 31 | with: 32 | script: | 33 | core.setFailed('CHANGELOG.md has not been updated') 34 | -------------------------------------------------------------------------------- /.github/workflows/dco.yml: -------------------------------------------------------------------------------- 1 | name: DCO check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | dco: 10 | uses: nspcc-dev/.github/.github/workflows/dco.yml@master 11 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: 7 | - master 8 | types: [opened, synchronize] 9 | paths-ignore: 10 | - '**/*.md' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | lint: 15 | name: Lint 16 | uses: nspcc-dev/.github/.github/workflows/go-linter.yml@master 17 | 18 | test: 19 | name: Test 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | matrix: 23 | go: [ '1.23', '1.24'] 24 | os: [ubuntu-latest, windows-2022, macos-14] 25 | exclude: 26 | # Only latest Go version for Windows and MacOS. 27 | - os: windows-2022 28 | go: '1.23' 29 | - os: macos-14 30 | go: '1.23' 31 | # Exclude latest Go version for Ubuntu as Coverage uses it. 32 | - os: ubuntu-latest 33 | go: '1.24' 34 | steps: 35 | 36 | - name: Setup go 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version: ${{ matrix.go }} 40 | 41 | - name: Check out code into the Go module directory 42 | uses: actions/checkout@v4 43 | 44 | - name: Tests 45 | run: go test -race ./... 46 | 47 | coverage: 48 | name: Coverage 49 | runs-on: ubuntu-latest 50 | steps: 51 | 52 | - name: Setup Go 53 | uses: actions/setup-go@v5 54 | with: 55 | go-version: 1.24 56 | 57 | - name: Check out 58 | uses: actions/checkout@v4 59 | 60 | - name: Collect coverage 61 | run: go test -coverprofile=coverage.txt -covermode=atomic ./... 62 | 63 | - name: Upload coverage results to Codecov 64 | uses: codecov/codecov-action@v4 65 | with: 66 | fail_ci_if_error: true 67 | files: ./coverage.txt 68 | slug: nspcc-dev/dbft 69 | token: ${{ secrets.CODECOV_TOKEN }} 70 | verbose: true 71 | 72 | codeql: 73 | name: CodeQL 74 | runs-on: ubuntu-latest 75 | 76 | strategy: 77 | fail-fast: false 78 | matrix: 79 | language: [ 'go' ] 80 | 81 | steps: 82 | - name: Checkout repository 83 | uses: actions/checkout@v4 84 | 85 | - name: Initialize CodeQL 86 | uses: github/codeql-action/init@v3 87 | with: 88 | languages: ${{ matrix.language }} 89 | 90 | - name: Autobuild 91 | uses: github/codeql-action/autobuild@v3 92 | 93 | - name: Perform CodeQL Analysis 94 | uses: github/codeql-action/analyze@v3 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .golangci.yml 3 | 4 | # TLC Model Checker files 5 | formal-models/*/*.toolbox/ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This document outlines major changes between releases. 4 | 5 | ## [Unreleased] 6 | 7 | New features: 8 | 9 | Behaviour changes: 10 | * `SecondsPerBlock` config parameter is replaced with `TimePerBlock` function (#147) 11 | 12 | Improvements: 13 | * minimum required Go version is 1.23 now (#145) 14 | 15 | Bugs fixed: 16 | 17 | ## [0.3.2] (30 January 2025) 18 | 19 | Important dBFT timer adjustments are included into this patch-release. The first one 20 | is the reference time point for dBFT timer which is moved to the moment of 21 | PrepareRequest receiving. Another one is evaluated network roundtrip time which is now 22 | taken into account every time on dBFT timer reset. These adjustments lead to the fact 23 | that actual block producing time is extremely close to the configuration value. Other 24 | than that, a couple of minor bug fixes are included. 25 | 26 | Improvements: 27 | * timer adjustment for most of the consensus time, more accurate block 28 | intervals (#56) 29 | * timer adjustment for network roundtrip time (#140) 30 | 31 | Bugs fixed: 32 | * inappropriate log on attempt to construct Commit for anti-MEV enabled WatchOnly 33 | (#139) 34 | * empty PreCommit/Commit can be relayed (#142) 35 | 36 | ## [0.3.1] (29 November 2024) 37 | 38 | This patch version mostly includes a set of library API extensions made to fit the 39 | needs of developing MEV-resistant blockchain node. Also, this release bumps minimum 40 | required Go version up to 1.22 and contains a set of bug fixes critical for the 41 | library functioning. 42 | 43 | Minor user-side code adjustments are required to adapt new ProcessBlock callback 44 | signature, whereas the rest of APIs stay compatible with the old implementation. 45 | This version also includes a simplification of PrivateKey interface which may be 46 | adopted by removing extra wrappers around PrivateKey implementation on the user code 47 | side. 48 | 49 | Behaviour changes: 50 | * adjust behaviour of ProcessPreBlock callback (#129) 51 | * (*DBFT).Header() and (*DBFT).PreHeader() are moved to (*Context) receiver (#133) 52 | * support error handling for ProcessBlock callback if anti-MEV extension is enabled 53 | (#134) 54 | * remove Sign method from PrivateKey interface (#137) 55 | 56 | Improvements: 57 | * minimum required Go version is 1.22 (#122, #126) 58 | * log Commit signature verification error (#134) 59 | * add Commit message verification callback (#134) 60 | 61 | Bugs fixed: 62 | * context-bound PreBlock and PreHeader are not reset properly (#127) 63 | * PreHeader is constructed instead of PreBlock to create PreCommit message (#128) 64 | * enable anti-MEV extension with respect to the current block index (#132) 65 | * (*Context).PreBlock() method returns PreHeader instead of PreBlock (#133) 66 | * WatchOnly node may send RecoveryMessage on RecoveryRequest (#135) 67 | * invalid PreCommit message is not removed from cache (#134) 68 | 69 | ## [0.3.0] (01 August 2024) 70 | 71 | New features: 72 | * TLA+ model for MEV-resistant dBFT extension (#116) 73 | * support for additional phase of MEV-resistant dBFT (#118) 74 | 75 | Behaviour changes: 76 | * simplify PublicKey interface (#114) 77 | * remove WithKeyPair callback from dBFT (#114) 78 | 79 | ## [0.2.0] (01 April 2024) 80 | 81 | We're rolling out an update for dBFT that contains a substantial library interface 82 | refactoring. Starting from this version dBFT is shipped as a generic package with 83 | a wide range of generic interfaces, callbacks and parameters. No default payload 84 | implementations are supplied anymore, the library itself works only with payload 85 | interfaces, and thus users are expected to implement the minimum required set of 86 | payload interfaces by themselves. A lot of outdated and unused APIs were removed, 87 | some of the internal APIs were renamed, so that the resulting library interface 88 | is much more clear and lightweight. Also, the minimum required Go version was 89 | upgraded to Go 1.20. 90 | 91 | Please note that no consensus-level behaviour changes introduced, this release 92 | focuses only on the library APIs improvement, so it shouldn't be hard for the users 93 | to migrate to the new interface. 94 | 95 | Behaviour changes: 96 | * add generic Hash/Address parameters to `DBFT` service (#94) 97 | * remove custom payloads implementation from default `DBFT` service configuration 98 | (#94) 99 | * rename `InitializeConsensus` dBFT method to `Reset` (#95) 100 | * drop outdated dBFT `Service` interface (#95) 101 | * move all default implementations to `internal` package (#97) 102 | * remove unused APIs of dBFT and payload interfaces (#104) 103 | * timer interface refactoring (#105) 104 | * constructor returns some meaningful error on failed dBFT instance creation (#107) 105 | 106 | Improvements: 107 | * add MIT License (#78, #79) 108 | * documentation updates (#80, #86, #95) 109 | * dependencies upgrades (#82, #85) 110 | * minimum required Go version upgrade to Go 1.19 (#83) 111 | * log messages adjustment (#88) 112 | * untie `dbft` module from `github.com/nspcc-dev/neo-go` dependency (#94) 113 | * minimum required Go version upgrade to Go 1.20 (#100) 114 | 115 | ## [0.1.0] (15 May 2023) 116 | 117 | Stable dbft 2.0 implementation. 118 | 119 | [Unreleased]: https://github.com/nspcc-dev/dbft/compare/v0.3.2...master 120 | [0.3.2]: https://github.com/nspcc-dev/dbft/releases/v0.3.2 121 | [0.3.1]: https://github.com/nspcc-dev/dbft/releases/v0.3.1 122 | [0.3.0]: https://github.com/nspcc-dev/dbft/releases/v0.3.0 123 | [0.2.0]: https://github.com/nspcc-dev/dbft/releases/v0.2.0 124 | [0.1.0]: https://github.com/nspcc-dev/dbft/releases/v0.1.0 125 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2023 NeoSPCC (@nspcc-dev) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/nspcc-dev/dbft.svg)](https://pkg.go.dev/github.com/nspcc-dev/dbft/) 2 | ![Codecov](https://img.shields.io/codecov/c/github/nspcc-dev/dbft.svg) 3 | [![Report](https://goreportcard.com/badge/github.com/nspcc-dev/dbft)](https://goreportcard.com/report/github.com/nspcc-dev/dbft) 4 | ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nspcc-dev/dbft?sort=semver) 5 | ![License](https://img.shields.io/github/license/nspcc-dev/dbft.svg?style=popout) 6 | 7 | # DBFT 8 | This repo contains Go implementation of the dBFT 2.0 consensus algorithm and its models 9 | written in [TLA⁺](https://lamport.azurewebsites.net/tla/tla.html) language. 10 | 11 | ## Design and structure 12 | 1. All control flow is done in main `dbft` package. Most of the code which communicates with external 13 | world (event time events) is hidden behind interfaces, callbacks and generic parameters. As a 14 | consequence it is highly flexible and extendable. Description of config options can be found 15 | in `config.go`. 16 | 2. `dbft` package contains `PrivateKey`/`PublicKey` interfaces which permits usage of one's own 17 | cryptography for signing blocks on `Commit` stage. Refer to `identity.go` for `PrivateKey`/`PublicKey` 18 | description. No default implementation is provided. 19 | 3. `dbft` package contains `Hash` interface which permits usage of one's own 20 | hash implementation without additional overhead on conversions. Instantiate dBFT with 21 | custom hash implementation that matches requirements specified in the corresponding 22 | documentation. Refer to `identity.go` for `Hash` description. No default implementation is 23 | provided. 24 | 4. `dbft` package contains `Block` and `Transaction` abstractions located at the `block.go` and 25 | `transaction.go` files. Every block must be able to be signed and verified as well as implement getters 26 | for main fields. `Transaction` is an entity which can be hashed. Two entities having 27 | equal hashes are considered equal. No default implementation is provided. 28 | 5. `dbft` contains generic interfaces for payloads. No default implementation is provided. 29 | 6. `dbft` contains generic `Timer` interface for time-related operations. `timer` package contains 30 | default `Timer` provider that can safely be used in production code. The interface itself 31 | is mostly created for tests dealing with dBFT's time-dependant behaviour. 32 | 7. `internal` contains an example of custom identity types and payloads implementation used to implement 33 | an example of dBFT's usage with 6-node consensus. Refer to `internal` subpackages for type-specific dBFT 34 | implementation and tests. Refer to `internal/simulation` for an example of dBFT library usage. 35 | 8. `formal-models` contains the set of dBFT's models written in [TLA⁺](https://lamport.azurewebsites.net/tla/tla.html) 36 | language and instructions on how to run and check them. Please, refer to the [README](./formal-models/README.md) 37 | for more details. 38 | 39 | ## Usage 40 | A client of the library must implement its own event loop. 41 | The library provides 5 callbacks that change the state of the consensus 42 | process: 43 | - `Start()` which initializes internal dBFT structures 44 | - `Reset()` which reinitializes the consensus process 45 | - `OnTransaction()` which must be called everytime new transaction appears 46 | - `OnReceive()` which must be called everytime new payload is received 47 | - `OnTimer()` which must be called everytime timer fires 48 | 49 | A minimal example can be found in `internal/simulation/main.go`. 50 | 51 | ## Links 52 | - dBFT high-level description on NEO website [https://docs.neo.org/docs/en-us/basic/consensus/dbft.html](https://docs.neo.org/docs/en-us/basic/consensus/dbft.html) 53 | - dBFT research paper [https://github.com/NeoResearch/yellowpaper/blob/master/releases/08_dBFT.pdf](https://github.com/NeoResearch/yellowpaper/blob/master/releases/08_dBFT.pdf) 54 | 55 | ## Notes 56 | 1. C# NEO node implementation works with the memory pool model, where only transaction hashes 57 | are proposed in the first step of the consensus and 58 | transactions are synchronized in the background. 59 | Some of the callbacks are in config with sole purpose to support this usecase. However it is 60 | very easy to extend `PrepareRequest` to also include proposed transactions. 61 | 2. NEO has the ability to change the list nodes which verify the block (they are called Validators). This is done through `GetValidators` 62 | callback which is called at the start of every epoch. In the simple case where validators are constant 63 | it can return the same value everytime it is called. 64 | 3. `ProcessBlock` is a callback which is called synchronously every time new block is accepted. 65 | It can or can not persist block; it also may keep the blockchain state unchanged. dBFT will NOT 66 | be initialized at the next height by itself to collect the next block until `Reset` 67 | is called. In other words, it's the caller's responsibility to initialize dBFT at the next height even 68 | after block collection at the current height. It's also the caller's responsibility to update the 69 | blockchain state before the next height initialization so that other callbacks including 70 | `CurrentHeight` and `CurrentHash` return new values. 71 | -------------------------------------------------------------------------------- /block.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // Block is a generic interface for a block used by dbft. 4 | type Block[H Hash] interface { 5 | // Hash returns block hash. 6 | Hash() H 7 | // PrevHash returns previous block hash. 8 | PrevHash() H 9 | // MerkleRoot returns a merkle root of the transaction hashes. 10 | MerkleRoot() H 11 | // Index returns block index. 12 | Index() uint32 13 | 14 | // Signature returns block's signature. 15 | Signature() []byte 16 | // Sign signs block and sets it's signature. 17 | Sign(key PrivateKey) error 18 | // Verify checks if signature is correct. 19 | Verify(key PublicKey, sign []byte) error 20 | 21 | // Transactions returns block's transaction list. 22 | Transactions() []Transaction[H] 23 | // SetTransactions sets block's transaction list. For anti-MEV extension 24 | // transactions provided via this call are taken directly from PreBlock level 25 | // and thus, may be out-of-date. Thus, with anti-MEV extension enabled it's 26 | // suggested to use this method as a Block finalizer since it will be called 27 | // right before the block approval. Do not rely on this with anti-MEV extension 28 | // disabled. 29 | SetTransactions([]Transaction[H]) 30 | } 31 | -------------------------------------------------------------------------------- /change_view.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // ChangeView represents dBFT ChangeView message. 4 | type ChangeView interface { 5 | // NewViewNumber returns proposed view number. 6 | NewViewNumber() byte 7 | 8 | // Reason returns change view reason. 9 | Reason() ChangeViewReason 10 | } 11 | -------------------------------------------------------------------------------- /change_view_reason.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | //go:generate stringer -type=ChangeViewReason -linecomment 4 | 5 | // ChangeViewReason represents a view change reason code. 6 | type ChangeViewReason byte 7 | 8 | // These constants define various reasons for view changing. They're following 9 | // Neo 3 except the Unknown value which is left for compatibility with Neo 2. 10 | const ( 11 | CVTimeout ChangeViewReason = 0x0 // Timeout 12 | CVChangeAgreement ChangeViewReason = 0x1 // ChangeAgreement 13 | CVTxNotFound ChangeViewReason = 0x2 // TxNotFound 14 | CVTxRejectedByPolicy ChangeViewReason = 0x3 // TxRejectedByPolicy 15 | CVTxInvalid ChangeViewReason = 0x4 // TxInvalid 16 | CVBlockRejectedByPolicy ChangeViewReason = 0x5 // BlockRejectedByPolicy 17 | CVUnknown ChangeViewReason = 0xff // Unknown 18 | ) 19 | -------------------------------------------------------------------------------- /change_view_reason_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ChangeViewReason -linecomment"; DO NOT EDIT. 2 | 3 | package dbft 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[CVTimeout-0] 12 | _ = x[CVChangeAgreement-1] 13 | _ = x[CVTxNotFound-2] 14 | _ = x[CVTxRejectedByPolicy-3] 15 | _ = x[CVTxInvalid-4] 16 | _ = x[CVBlockRejectedByPolicy-5] 17 | _ = x[CVUnknown-255] 18 | } 19 | 20 | const ( 21 | _ChangeViewReason_name_0 = "TimeoutChangeAgreementTxNotFoundTxRejectedByPolicyTxInvalidBlockRejectedByPolicy" 22 | _ChangeViewReason_name_1 = "Unknown" 23 | ) 24 | 25 | var ( 26 | _ChangeViewReason_index_0 = [...]uint8{0, 7, 22, 32, 50, 59, 80} 27 | ) 28 | 29 | func (i ChangeViewReason) String() string { 30 | switch { 31 | case 0 <= i && i <= 5: 32 | return _ChangeViewReason_name_0[_ChangeViewReason_index_0[i]:_ChangeViewReason_index_0[i+1]] 33 | case i == 255: 34 | return _ChangeViewReason_name_1 35 | default: 36 | return "ChangeViewReason(" + strconv.FormatInt(int64(i), 10) + ")" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /check.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | func (d *DBFT[H]) checkPrepare() { 8 | if d.lastBlockIndex != d.BlockIndex || d.lastBlockView != d.ViewNumber { 9 | // Notice that lastBlockTimestamp is left unchanged because 10 | // this must be the value from the last header. 11 | d.lastBlockTime = d.Timer.Now() 12 | d.lastBlockIndex = d.BlockIndex 13 | d.lastBlockView = d.ViewNumber 14 | } 15 | if !d.hasAllTransactions() { 16 | d.Logger.Debug("check prepare: some transactions are missing", zap.Any("hashes", d.MissingTransactions)) 17 | return 18 | } 19 | 20 | count := 0 21 | hasRequest := false 22 | 23 | for _, msg := range d.PreparationPayloads { 24 | if msg != nil { 25 | if msg.ViewNumber() == d.ViewNumber { 26 | count++ 27 | } 28 | 29 | if msg.Type() == PrepareRequestType { 30 | hasRequest = true 31 | } 32 | } 33 | } 34 | 35 | d.Logger.Debug("check preparations", zap.Bool("hasReq", hasRequest), 36 | zap.Int("count", count), 37 | zap.Int("M", d.M())) 38 | 39 | if hasRequest && count >= d.M() { 40 | if d.isAntiMEVExtensionEnabled() { 41 | d.sendPreCommit() 42 | d.changeTimer(d.timePerBlock) 43 | d.checkPreCommit() 44 | } else { 45 | d.sendCommit() 46 | d.changeTimer(d.timePerBlock) 47 | d.checkCommit() 48 | } 49 | } 50 | } 51 | 52 | func (d *DBFT[H]) checkPreCommit() { 53 | if !d.hasAllTransactions() { 54 | d.Logger.Debug("check preCommit: some transactions are missing", zap.Any("hashes", d.MissingTransactions)) 55 | return 56 | } 57 | 58 | count := 0 59 | for _, msg := range d.PreCommitPayloads { 60 | if msg != nil && msg.ViewNumber() == d.ViewNumber { 61 | count++ 62 | } 63 | } 64 | 65 | if count < d.M() { 66 | d.Logger.Debug("not enough PreCommits to process PreBlock", zap.Int("count", count)) 67 | return 68 | } 69 | 70 | d.preBlock = d.CreatePreBlock() 71 | 72 | if !d.preBlockProcessed { 73 | d.Logger.Info("processing PreBlock", 74 | zap.Uint32("height", d.BlockIndex), 75 | zap.Uint("view", uint(d.ViewNumber)), 76 | zap.Int("tx_count", len(d.preBlock.Transactions())), 77 | zap.Int("preCommit_count", count)) 78 | 79 | err := d.ProcessPreBlock(d.preBlock) 80 | if err != nil { 81 | d.Logger.Info("can't process PreBlock, waiting for more PreCommits to be collected", 82 | zap.Error(err), 83 | zap.Int("count", count)) 84 | return 85 | } 86 | d.preBlockProcessed = true 87 | } 88 | 89 | // Require PreCommit sent by self for reliability. This condition must not be 90 | // removed because: 91 | // 1) we need to filter out WatchOnly nodes; 92 | // 2) CNs that have not sent PreCommit must not skip this stage (although it's OK 93 | // from the DKG/TPKE side to build final Block based only on other CN's data). 94 | if d.PreCommitSent() { 95 | d.verifyCommitPayloadsAgainstHeader() 96 | d.sendCommit() 97 | d.changeTimer(d.timePerBlock) 98 | d.checkCommit() 99 | } else { 100 | if !d.Context.WatchOnly() { 101 | d.Logger.Debug("can't send commit since self preCommit not yet sent") 102 | } 103 | } 104 | } 105 | 106 | func (d *DBFT[H]) checkCommit() { 107 | if !d.hasAllTransactions() { 108 | d.Logger.Debug("check commit: some transactions are missing", zap.Any("hashes", d.MissingTransactions)) 109 | return 110 | } 111 | 112 | // return if we received commits from other nodes 113 | // before receiving PrepareRequest from Speaker 114 | count := 0 115 | 116 | for _, msg := range d.CommitPayloads { 117 | if msg != nil && msg.ViewNumber() == d.ViewNumber { 118 | count++ 119 | } 120 | } 121 | 122 | if count < d.M() { 123 | d.Logger.Debug("not enough to commit", zap.Int("count", count)) 124 | return 125 | } 126 | 127 | d.block = d.CreateBlock() 128 | hash := d.block.Hash() 129 | 130 | d.Logger.Info("approving block", 131 | zap.Uint32("height", d.BlockIndex), 132 | zap.Stringer("hash", hash), 133 | zap.Int("tx_count", len(d.block.Transactions())), 134 | zap.Stringer("merkle", d.block.MerkleRoot()), 135 | zap.Stringer("prev", d.block.PrevHash())) 136 | 137 | err := d.ProcessBlock(d.block) 138 | if err != nil { 139 | if d.isAntiMEVExtensionEnabled() { 140 | d.Logger.Info("can't process Block, waiting for more Commits to be collected", 141 | zap.Error(err), 142 | zap.Int("count", count)) 143 | return 144 | } 145 | d.Logger.Fatal("block processing failed", zap.Error(err)) 146 | } 147 | 148 | d.blockProcessed = true 149 | 150 | // Do not initialize consensus process immediately. It's the caller's duty to 151 | // start the new block acceptance process and call Reset at the 152 | // new height. 153 | } 154 | 155 | func (d *DBFT[H]) checkChangeView(view byte) { 156 | if d.ViewNumber >= view { 157 | return 158 | } 159 | 160 | count := 0 161 | 162 | for _, msg := range d.ChangeViewPayloads { 163 | if msg != nil && msg.GetChangeView().NewViewNumber() >= view { 164 | count++ 165 | } 166 | } 167 | 168 | if count < d.M() { 169 | return 170 | } 171 | 172 | if !d.Context.WatchOnly() { 173 | msg := d.ChangeViewPayloads[d.MyIndex] 174 | if msg != nil && msg.GetChangeView().NewViewNumber() < view { 175 | d.broadcast(d.makeChangeView(uint64(d.Timer.Now().UnixNano()), CVChangeAgreement)) 176 | } 177 | } 178 | 179 | d.initializeConsensus(view, d.lastBlockTimestamp) 180 | } 181 | -------------------------------------------------------------------------------- /commit.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // Commit is an interface for dBFT Commit message. 4 | type Commit interface { 5 | // Signature returns commit's signature field 6 | // which is a final block signature for the current epoch for both dBFT 2.0 and 7 | // for anti-MEV extension. 8 | Signature() []byte 9 | } 10 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | // Config contains initialization and working parameters for dBFT. 11 | type Config[H Hash] struct { 12 | // Logger 13 | Logger *zap.Logger 14 | // Timer 15 | Timer Timer 16 | // TimePerBlock is the time that need to pass before another block will 17 | // be accepted. This value may be updated every block. 18 | TimePerBlock func() time.Duration 19 | // TimestampIncrement increment is the amount of units to add to timestamp 20 | // if current time is less than that of previous context. 21 | // By default use millisecond precision. 22 | TimestampIncrement uint64 23 | // AntiMEVExtensionEnablingHeight denotes the height starting from which dBFT 24 | // Anti-MEV extensions should be enabled. -1 means no extension is enabled. 25 | AntiMEVExtensionEnablingHeight int64 26 | // GetKeyPair returns an index of the node in the list of validators 27 | // together with it's key pair. 28 | GetKeyPair func([]PublicKey) (int, PrivateKey, PublicKey) 29 | // NewPreBlockFromContext should allocate, fill from Context and return new block.PreBlock. 30 | NewPreBlockFromContext func(ctx *Context[H]) PreBlock[H] 31 | // NewBlockFromContext should allocate, fill from Context and return new block.Block. 32 | NewBlockFromContext func(ctx *Context[H]) Block[H] 33 | // RequestTx is a callback which is called when transaction contained 34 | // in current block can't be found in memory pool. The slice received by 35 | // this callback MUST NOT be changed. 36 | RequestTx func(h ...H) 37 | // StopTxFlow is a callback which is called when the process no longer needs 38 | // any transactions. 39 | StopTxFlow func() 40 | // GetTx returns a transaction from memory pool. 41 | GetTx func(h H) Transaction[H] 42 | // GetVerified returns a slice of verified transactions 43 | // to be proposed in a new block. 44 | GetVerified func() []Transaction[H] 45 | // VerifyPreBlock verifies if preBlock is valid. 46 | VerifyPreBlock func(b PreBlock[H]) bool 47 | // VerifyBlock verifies if block is valid. 48 | VerifyBlock func(b Block[H]) bool 49 | // Broadcast should broadcast payload m to the consensus nodes. 50 | Broadcast func(m ConsensusPayload[H]) 51 | // ProcessBlock is called every time new preBlock is accepted. 52 | ProcessPreBlock func(b PreBlock[H]) error 53 | // ProcessBlock is called every time new block is accepted. 54 | ProcessBlock func(b Block[H]) error 55 | // GetBlock should return block with hash. 56 | GetBlock func(h H) Block[H] 57 | // WatchOnly tells if a node should only watch. 58 | WatchOnly func() bool 59 | // CurrentHeight returns index of the last accepted block. 60 | CurrentHeight func() uint32 61 | // CurrentBlockHash returns hash of the last accepted block. 62 | CurrentBlockHash func() H 63 | // GetValidators returns list of the validators. 64 | // When called with a transaction list it must return 65 | // list of the validators of the next block. 66 | // If this function ever returns 0-length slice, dbft will panic. 67 | GetValidators func(...Transaction[H]) []PublicKey 68 | // NewConsensusPayload is a constructor for payload.ConsensusPayload. 69 | NewConsensusPayload func(*Context[H], MessageType, any) ConsensusPayload[H] 70 | // NewPrepareRequest is a constructor for payload.PrepareRequest. 71 | NewPrepareRequest func(ts uint64, nonce uint64, transactionHashes []H) PrepareRequest[H] 72 | // NewPrepareResponse is a constructor for payload.PrepareResponse. 73 | NewPrepareResponse func(preparationHash H) PrepareResponse[H] 74 | // NewChangeView is a constructor for payload.ChangeView. 75 | NewChangeView func(newViewNumber byte, reason ChangeViewReason, timestamp uint64) ChangeView 76 | // NewPreCommit is a constructor for payload.PreCommit. 77 | NewPreCommit func(data []byte) PreCommit 78 | // NewCommit is a constructor for payload.Commit. 79 | NewCommit func(signature []byte) Commit 80 | // NewRecoveryRequest is a constructor for payload.RecoveryRequest. 81 | NewRecoveryRequest func(ts uint64) RecoveryRequest 82 | // NewRecoveryMessage is a constructor for payload.RecoveryMessage. 83 | NewRecoveryMessage func() RecoveryMessage[H] 84 | // VerifyPrepareRequest can perform external payload verification and returns true iff it was successful. 85 | VerifyPrepareRequest func(p ConsensusPayload[H]) error 86 | // VerifyPrepareResponse performs external PrepareResponse verification and returns nil if it's successful. 87 | VerifyPrepareResponse func(p ConsensusPayload[H]) error 88 | // VerifyPreCommit performs external PreCommit verification and returns nil if it's successful. 89 | // Note that PreBlock-dependent PreCommit verification should be performed inside PreBlock.Verify 90 | // callback. 91 | VerifyPreCommit func(p ConsensusPayload[H]) error 92 | // VerifyCommit performs external Commit verification and returns nil if it's successful. 93 | // Note that Block-dependent Commit verification should be performed inside Block.Verify 94 | // callback. 95 | VerifyCommit func(p ConsensusPayload[H]) error 96 | } 97 | 98 | const defaultSecondsPerBlock = time.Second * 15 99 | 100 | const defaultTimestampIncrement = uint64(time.Millisecond / time.Nanosecond) 101 | 102 | func defaultConfig[H Hash]() *Config[H] { 103 | // fields which are set to nil must be provided from client 104 | return &Config[H]{ 105 | Logger: zap.NewNop(), 106 | TimePerBlock: func() time.Duration { return defaultSecondsPerBlock }, 107 | TimestampIncrement: defaultTimestampIncrement, 108 | GetKeyPair: nil, 109 | RequestTx: func(...H) {}, 110 | StopTxFlow: func() {}, 111 | GetTx: func(H) Transaction[H] { return nil }, 112 | GetVerified: func() []Transaction[H] { return make([]Transaction[H], 0) }, 113 | VerifyBlock: func(Block[H]) bool { return true }, 114 | Broadcast: func(ConsensusPayload[H]) {}, 115 | ProcessBlock: func(Block[H]) error { return nil }, 116 | GetBlock: func(H) Block[H] { return nil }, 117 | WatchOnly: func() bool { return false }, 118 | CurrentHeight: nil, 119 | CurrentBlockHash: nil, 120 | GetValidators: nil, 121 | 122 | VerifyPrepareRequest: func(ConsensusPayload[H]) error { return nil }, 123 | VerifyPrepareResponse: func(ConsensusPayload[H]) error { return nil }, 124 | VerifyCommit: func(ConsensusPayload[H]) error { return nil }, 125 | 126 | AntiMEVExtensionEnablingHeight: -1, 127 | VerifyPreBlock: func(PreBlock[H]) bool { return true }, 128 | VerifyPreCommit: func(ConsensusPayload[H]) error { return nil }, 129 | } 130 | } 131 | 132 | func checkConfig[H Hash](cfg *Config[H]) error { 133 | if cfg.GetKeyPair == nil { 134 | return errors.New("private key is nil") 135 | } else if cfg.Timer == nil { 136 | return errors.New("Timer is nil") 137 | } else if cfg.CurrentHeight == nil { 138 | return errors.New("CurrentHeight is nil") 139 | } else if cfg.CurrentBlockHash == nil { 140 | return errors.New("CurrentBlockHash is nil") 141 | } else if cfg.GetValidators == nil { 142 | return errors.New("GetValidators is nil") 143 | } else if cfg.NewBlockFromContext == nil { 144 | return errors.New("NewBlockFromContext is nil") 145 | } else if cfg.NewConsensusPayload == nil { 146 | return errors.New("NewConsensusPayload is nil") 147 | } else if cfg.NewPrepareRequest == nil { 148 | return errors.New("NewPrepareRequest is nil") 149 | } else if cfg.NewPrepareResponse == nil { 150 | return errors.New("NewPrepareResponse is nil") 151 | } else if cfg.NewChangeView == nil { 152 | return errors.New("NewChangeView is nil") 153 | } else if cfg.NewCommit == nil { 154 | return errors.New("NewCommit is nil") 155 | } else if cfg.NewRecoveryRequest == nil { 156 | return errors.New("NewRecoveryRequest is nil") 157 | } else if cfg.NewRecoveryMessage == nil { 158 | return errors.New("NewRecoveryMessage is nil") 159 | } else if cfg.AntiMEVExtensionEnablingHeight >= 0 { 160 | if cfg.NewPreBlockFromContext == nil { 161 | return errors.New("NewPreBlockFromContext is nil") 162 | } else if cfg.ProcessPreBlock == nil { 163 | return errors.New("ProcessPreBlock is nil") 164 | } else if cfg.NewPreCommit == nil { 165 | return errors.New("NewPreCommit is nil") 166 | } 167 | } else if cfg.NewPreBlockFromContext != nil { 168 | return errors.New("NewPreBlockFromContext is set, but AntiMEVExtensionEnablingHeight is not specified") 169 | } else if cfg.ProcessPreBlock != nil { 170 | return errors.New("ProcessPreBlock is set, but AntiMEVExtensionEnablingHeight is not specified") 171 | } else if cfg.NewPreCommit != nil { 172 | return errors.New("NewPreCommit is set, but AntiMEVExtensionEnablingHeight is not specified") 173 | } 174 | 175 | return nil 176 | } 177 | 178 | // WithGetKeyPair sets GetKeyPair. 179 | func WithGetKeyPair[H Hash](f func(pubs []PublicKey) (int, PrivateKey, PublicKey)) func(config *Config[H]) { 180 | return func(cfg *Config[H]) { 181 | cfg.GetKeyPair = f 182 | } 183 | } 184 | 185 | // WithLogger sets Logger. 186 | func WithLogger[H Hash](log *zap.Logger) func(config *Config[H]) { 187 | return func(cfg *Config[H]) { 188 | cfg.Logger = log 189 | } 190 | } 191 | 192 | // WithTimer sets Timer. 193 | func WithTimer[H Hash](t Timer) func(config *Config[H]) { 194 | return func(cfg *Config[H]) { 195 | cfg.Timer = t 196 | } 197 | } 198 | 199 | // WithTimePerBlock sets TimePerBlock. 200 | func WithTimePerBlock[H Hash](f func() time.Duration) func(config *Config[H]) { 201 | return func(cfg *Config[H]) { 202 | cfg.TimePerBlock = f 203 | } 204 | } 205 | 206 | // WithAntiMEVExtensionEnablingHeight sets AntiMEVExtensionEnablingHeight. 207 | func WithAntiMEVExtensionEnablingHeight[H Hash](h int64) func(config *Config[H]) { 208 | return func(cfg *Config[H]) { 209 | cfg.AntiMEVExtensionEnablingHeight = h 210 | } 211 | } 212 | 213 | // WithTimestampIncrement sets TimestampIncrement. 214 | func WithTimestampIncrement[H Hash](u uint64) func(config *Config[H]) { 215 | return func(cfg *Config[H]) { 216 | cfg.TimestampIncrement = u 217 | } 218 | } 219 | 220 | // WithNewPreBlockFromContext sets NewPreBlockFromContext. 221 | func WithNewPreBlockFromContext[H Hash](f func(ctx *Context[H]) PreBlock[H]) func(config *Config[H]) { 222 | return func(cfg *Config[H]) { 223 | cfg.NewPreBlockFromContext = f 224 | } 225 | } 226 | 227 | // WithNewBlockFromContext sets NewBlockFromContext. 228 | func WithNewBlockFromContext[H Hash](f func(ctx *Context[H]) Block[H]) func(config *Config[H]) { 229 | return func(cfg *Config[H]) { 230 | cfg.NewBlockFromContext = f 231 | } 232 | } 233 | 234 | // WithRequestTx sets RequestTx. 235 | func WithRequestTx[H Hash](f func(h ...H)) func(config *Config[H]) { 236 | return func(cfg *Config[H]) { 237 | cfg.RequestTx = f 238 | } 239 | } 240 | 241 | // WithStopTxFlow sets StopTxFlow. 242 | func WithStopTxFlow[H Hash](f func()) func(config *Config[H]) { 243 | return func(cfg *Config[H]) { 244 | cfg.StopTxFlow = f 245 | } 246 | } 247 | 248 | // WithGetTx sets GetTx. 249 | func WithGetTx[H Hash](f func(h H) Transaction[H]) func(config *Config[H]) { 250 | return func(cfg *Config[H]) { 251 | cfg.GetTx = f 252 | } 253 | } 254 | 255 | // WithGetVerified sets GetVerified. 256 | func WithGetVerified[H Hash](f func() []Transaction[H]) func(config *Config[H]) { 257 | return func(cfg *Config[H]) { 258 | cfg.GetVerified = f 259 | } 260 | } 261 | 262 | // WithVerifyPreBlock sets VerifyPreBlock. 263 | func WithVerifyPreBlock[H Hash](f func(b PreBlock[H]) bool) func(config *Config[H]) { 264 | return func(cfg *Config[H]) { 265 | cfg.VerifyPreBlock = f 266 | } 267 | } 268 | 269 | // WithVerifyBlock sets VerifyBlock. 270 | func WithVerifyBlock[H Hash](f func(b Block[H]) bool) func(config *Config[H]) { 271 | return func(cfg *Config[H]) { 272 | cfg.VerifyBlock = f 273 | } 274 | } 275 | 276 | // WithBroadcast sets Broadcast. 277 | func WithBroadcast[H Hash](f func(m ConsensusPayload[H])) func(config *Config[H]) { 278 | return func(cfg *Config[H]) { 279 | cfg.Broadcast = f 280 | } 281 | } 282 | 283 | // WithProcessBlock sets ProcessBlock callback. Note that for anti-MEV extension 284 | // disabled non-nil error return is a no-op. 285 | func WithProcessBlock[H Hash](f func(b Block[H]) error) func(config *Config[H]) { 286 | return func(cfg *Config[H]) { 287 | cfg.ProcessBlock = f 288 | } 289 | } 290 | 291 | // WithProcessPreBlock sets ProcessPreBlock. 292 | func WithProcessPreBlock[H Hash](f func(b PreBlock[H]) error) func(config *Config[H]) { 293 | return func(cfg *Config[H]) { 294 | cfg.ProcessPreBlock = f 295 | } 296 | } 297 | 298 | // WithGetBlock sets GetBlock. 299 | func WithGetBlock[H Hash](f func(h H) Block[H]) func(config *Config[H]) { 300 | return func(cfg *Config[H]) { 301 | cfg.GetBlock = f 302 | } 303 | } 304 | 305 | // WithWatchOnly sets WatchOnly. 306 | func WithWatchOnly[H Hash](f func() bool) func(config *Config[H]) { 307 | return func(cfg *Config[H]) { 308 | cfg.WatchOnly = f 309 | } 310 | } 311 | 312 | // WithCurrentHeight sets CurrentHeight. 313 | func WithCurrentHeight[H Hash](f func() uint32) func(config *Config[H]) { 314 | return func(cfg *Config[H]) { 315 | cfg.CurrentHeight = f 316 | } 317 | } 318 | 319 | // WithCurrentBlockHash sets CurrentBlockHash. 320 | func WithCurrentBlockHash[H Hash](f func() H) func(config *Config[H]) { 321 | return func(cfg *Config[H]) { 322 | cfg.CurrentBlockHash = f 323 | } 324 | } 325 | 326 | // WithGetValidators sets GetValidators. 327 | func WithGetValidators[H Hash](f func(txs ...Transaction[H]) []PublicKey) func(config *Config[H]) { 328 | return func(cfg *Config[H]) { 329 | cfg.GetValidators = f 330 | } 331 | } 332 | 333 | // WithNewConsensusPayload sets NewConsensusPayload. 334 | func WithNewConsensusPayload[H Hash](f func(ctx *Context[H], typ MessageType, msg any) ConsensusPayload[H]) func(config *Config[H]) { 335 | return func(cfg *Config[H]) { 336 | cfg.NewConsensusPayload = f 337 | } 338 | } 339 | 340 | // WithNewPrepareRequest sets NewPrepareRequest. 341 | func WithNewPrepareRequest[H Hash](f func(ts uint64, nonce uint64, transactionsHashes []H) PrepareRequest[H]) func(config *Config[H]) { 342 | return func(cfg *Config[H]) { 343 | cfg.NewPrepareRequest = f 344 | } 345 | } 346 | 347 | // WithNewPrepareResponse sets NewPrepareResponse. 348 | func WithNewPrepareResponse[H Hash](f func(preparationHash H) PrepareResponse[H]) func(config *Config[H]) { 349 | return func(cfg *Config[H]) { 350 | cfg.NewPrepareResponse = f 351 | } 352 | } 353 | 354 | // WithNewChangeView sets NewChangeView. 355 | func WithNewChangeView[H Hash](f func(newViewNumber byte, reason ChangeViewReason, ts uint64) ChangeView) func(config *Config[H]) { 356 | return func(cfg *Config[H]) { 357 | cfg.NewChangeView = f 358 | } 359 | } 360 | 361 | // WithNewCommit sets NewCommit. 362 | func WithNewCommit[H Hash](f func(signature []byte) Commit) func(config *Config[H]) { 363 | return func(cfg *Config[H]) { 364 | cfg.NewCommit = f 365 | } 366 | } 367 | 368 | // WithNewPreCommit sets NewPreCommit. 369 | func WithNewPreCommit[H Hash](f func(signature []byte) PreCommit) func(config *Config[H]) { 370 | return func(cfg *Config[H]) { 371 | cfg.NewPreCommit = f 372 | } 373 | } 374 | 375 | // WithNewRecoveryRequest sets NewRecoveryRequest. 376 | func WithNewRecoveryRequest[H Hash](f func(ts uint64) RecoveryRequest) func(config *Config[H]) { 377 | return func(cfg *Config[H]) { 378 | cfg.NewRecoveryRequest = f 379 | } 380 | } 381 | 382 | // WithNewRecoveryMessage sets NewRecoveryMessage. 383 | func WithNewRecoveryMessage[H Hash](f func() RecoveryMessage[H]) func(config *Config[H]) { 384 | return func(cfg *Config[H]) { 385 | cfg.NewRecoveryMessage = f 386 | } 387 | } 388 | 389 | // WithVerifyPrepareRequest sets VerifyPrepareRequest. 390 | func WithVerifyPrepareRequest[H Hash](f func(prepareReq ConsensusPayload[H]) error) func(config *Config[H]) { 391 | return func(cfg *Config[H]) { 392 | cfg.VerifyPrepareRequest = f 393 | } 394 | } 395 | 396 | // WithVerifyPrepareResponse sets VerifyPrepareResponse. 397 | func WithVerifyPrepareResponse[H Hash](f func(prepareResp ConsensusPayload[H]) error) func(config *Config[H]) { 398 | return func(cfg *Config[H]) { 399 | cfg.VerifyPrepareResponse = f 400 | } 401 | } 402 | 403 | // WithVerifyPreCommit sets VerifyPreCommit. 404 | func WithVerifyPreCommit[H Hash](f func(preCommit ConsensusPayload[H]) error) func(config *Config[H]) { 405 | return func(cfg *Config[H]) { 406 | cfg.VerifyPreCommit = f 407 | } 408 | } 409 | 410 | // WithVerifyCommit sets VerifyCommit. 411 | func WithVerifyCommit[H Hash](f func(commit ConsensusPayload[H]) error) func(config *Config[H]) { 412 | return func(cfg *Config[H]) { 413 | cfg.VerifyCommit = f 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /consensus_message.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // ConsensusMessage is an interface for generic dBFT message. 4 | type ConsensusMessage[H Hash] interface { 5 | // ViewNumber returns view number when this message was originated. 6 | ViewNumber() byte 7 | // Type returns type of this message. 8 | Type() MessageType 9 | // Payload returns this message's actual payload. 10 | Payload() any 11 | 12 | // GetChangeView returns payload as if it was ChangeView. 13 | GetChangeView() ChangeView 14 | // GetPrepareRequest returns payload as if it was PrepareRequest. 15 | GetPrepareRequest() PrepareRequest[H] 16 | // GetPrepareResponse returns payload as if it was PrepareResponse. 17 | GetPrepareResponse() PrepareResponse[H] 18 | // GetPreCommit returns payload as if it was PreCommit. 19 | GetPreCommit() PreCommit 20 | // GetCommit returns payload as if it was Commit. 21 | GetCommit() Commit 22 | // GetRecoveryRequest returns payload as if it was RecoveryRequest. 23 | GetRecoveryRequest() RecoveryRequest 24 | // GetRecoveryMessage returns payload as if it was RecoveryMessage. 25 | GetRecoveryMessage() RecoveryMessage[H] 26 | } 27 | -------------------------------------------------------------------------------- /consensus_message_type.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | import "fmt" 4 | 5 | // MessageType is a type for dBFT consensus messages. 6 | type MessageType byte 7 | 8 | // 7 following constants enumerate all possible type of consensus message. 9 | const ( 10 | ChangeViewType MessageType = 0x00 11 | PrepareRequestType MessageType = 0x20 12 | PrepareResponseType MessageType = 0x21 13 | PreCommitType MessageType = 0x31 14 | CommitType MessageType = 0x30 15 | RecoveryRequestType MessageType = 0x40 16 | RecoveryMessageType MessageType = 0x41 17 | ) 18 | 19 | // String implements fmt.Stringer interface. 20 | func (m MessageType) String() string { 21 | switch m { 22 | case ChangeViewType: 23 | return "ChangeView" 24 | case PrepareRequestType: 25 | return "PrepareRequest" 26 | case PrepareResponseType: 27 | return "PrepareResponse" 28 | case CommitType: 29 | return "Commit" 30 | case PreCommitType: 31 | return "PreCommit" 32 | case RecoveryRequestType: 33 | return "RecoveryRequest" 34 | case RecoveryMessageType: 35 | return "RecoveryMessage" 36 | default: 37 | return fmt.Sprintf("UNKNOWN(%02x)", byte(m)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /consensus_payload.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // ConsensusPayload is a generic payload type which is exchanged 4 | // between the nodes. 5 | type ConsensusPayload[H Hash] interface { 6 | ConsensusMessage[H] 7 | 8 | // ValidatorIndex returns index of validator from which 9 | // payload was originated from. 10 | ValidatorIndex() uint16 11 | 12 | // SetValidatorIndex sets validator index. 13 | SetValidatorIndex(i uint16) 14 | 15 | Height() uint32 16 | 17 | // Hash returns 32-byte checksum of the payload. 18 | Hash() H 19 | } 20 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | "time" 7 | ) 8 | 9 | // HeightView is a block height/consensus view pair. 10 | type HeightView struct { 11 | Height uint32 12 | View byte 13 | } 14 | 15 | // Context is a main dBFT structure which 16 | // contains all information needed for performing transitions. 17 | type Context[H Hash] struct { 18 | // Config is dBFT's Config instance. 19 | Config *Config[H] 20 | 21 | // Priv is node's private key. 22 | Priv PrivateKey 23 | // Pub is node's public key. 24 | Pub PublicKey 25 | 26 | preBlock PreBlock[H] 27 | preHeader PreBlock[H] 28 | block Block[H] 29 | header Block[H] 30 | // blockProcessed denotes whether Config.ProcessBlock callback was called for the current 31 | // height. If so, then no second call must happen. After new block is received by the user, 32 | // dBFT stops any new transaction or messages processing as far as timeouts handling till 33 | // the next call to Reset. 34 | blockProcessed bool 35 | // preBlockProcessed is true when Config.ProcessPreBlock callback was 36 | // invoked for the current height. This happens once and dbft continues 37 | // to march towards proper commit after that. 38 | preBlockProcessed bool 39 | 40 | // BlockIndex is current block index. 41 | BlockIndex uint32 42 | // ViewNumber is current view number. 43 | ViewNumber byte 44 | // Validators is a current validator list. 45 | Validators []PublicKey 46 | // MyIndex is an index of the current node in the Validators array. 47 | // It is equal to -1 if node is not a validator or is WatchOnly. 48 | MyIndex int 49 | // PrimaryIndex is an index of the primary node in the current epoch. 50 | PrimaryIndex uint 51 | 52 | // PrevHash is a hash of the previous block. 53 | PrevHash H 54 | 55 | // Timestamp is a nanosecond-precision timestamp 56 | Timestamp uint64 57 | Nonce uint64 58 | // TransactionHashes is a slice of hashes of proposed transactions in the current block. 59 | TransactionHashes []H 60 | // MissingTransactions is a slice of hashes containing missing transactions for the current block. 61 | MissingTransactions []H 62 | // Transactions is a map containing actual transactions for the current block. 63 | Transactions map[H]Transaction[H] 64 | 65 | // PreparationPayloads stores consensus Prepare* payloads for the current epoch. 66 | PreparationPayloads []ConsensusPayload[H] 67 | // PreCommitPayloads stores consensus PreCommit payloads sent through all epochs 68 | // as a part of anti-MEV dBFT extension. It is assumed that valid PreCommit 69 | // payloads can only be sent once by a single node per the whole set of consensus 70 | // epochs for particular block. Invalid PreCommit payloads are kicked off this 71 | // list immediately (if PrepareRequest was received for the current round, so 72 | // it's possible to verify PreCommit against PreBlock built on PrepareRequest) 73 | // or stored till the corresponding PrepareRequest receiving. 74 | PreCommitPayloads []ConsensusPayload[H] 75 | // CommitPayloads stores consensus Commit payloads sent throughout all epochs. It 76 | // is assumed that valid Commit payload can only be sent once by a single node per 77 | // the whole set of consensus epochs for particular block. Invalid commit payloads 78 | // are kicked off this list immediately (if PrepareRequest was received for the 79 | // current round, so it's possible to verify Commit against it) or stored till 80 | // the corresponding PrepareRequest receiving. 81 | CommitPayloads []ConsensusPayload[H] 82 | // ChangeViewPayloads stores consensus ChangeView payloads for the current epoch. 83 | ChangeViewPayloads []ConsensusPayload[H] 84 | // LastChangeViewPayloads stores consensus ChangeView payloads for the last epoch. 85 | LastChangeViewPayloads []ConsensusPayload[H] 86 | // LastSeenMessage array stores the height and view of the last seen message, for each validator. 87 | // If this node never heard a thing from validator i, LastSeenMessage[i] will be nil. 88 | LastSeenMessage []*HeightView 89 | 90 | lastBlockTimestamp uint64 // ns-precision timestamp from the last header (used for the next block timestamp calculations). 91 | lastBlockTime time.Time // Wall clock time of when we started (as in PrepareRequest) creating the last block (used for timer adjustments). 92 | lastBlockIndex uint32 93 | lastBlockView byte 94 | timePerBlock time.Duration // amount of time that need to pass before the pending block will be accepted. 95 | 96 | prepareSentTime time.Time 97 | rttEstimates rtt 98 | } 99 | 100 | // N returns total number of validators. 101 | func (c *Context[H]) N() int { return len(c.Validators) } 102 | 103 | // F returns number of validators which can be faulty. 104 | func (c *Context[H]) F() int { return (len(c.Validators) - 1) / 3 } 105 | 106 | // M returns number of validators which must function correctly. 107 | func (c *Context[H]) M() int { return len(c.Validators) - c.F() } 108 | 109 | // GetPrimaryIndex returns index of a primary node for the specified view. 110 | func (c *Context[H]) GetPrimaryIndex(viewNumber byte) uint { 111 | p := (int(c.BlockIndex) - int(viewNumber)) % len(c.Validators) 112 | if p >= 0 { 113 | return uint(p) 114 | } 115 | 116 | return uint(p + len(c.Validators)) 117 | } 118 | 119 | // IsPrimary returns true iff node is primary for current height and view. 120 | func (c *Context[H]) IsPrimary() bool { return c.MyIndex == int(c.PrimaryIndex) } 121 | 122 | // IsBackup returns true iff node is backup for current height and view. 123 | func (c *Context[H]) IsBackup() bool { 124 | return c.MyIndex >= 0 && !c.IsPrimary() 125 | } 126 | 127 | // WatchOnly returns true iff node takes no active part in consensus. 128 | func (c *Context[H]) WatchOnly() bool { return c.MyIndex < 0 || c.Config.WatchOnly() } 129 | 130 | // CountCommitted returns number of received Commit (or PreCommit for anti-MEV 131 | // extension) messages not only for the current epoch but also for any other epoch. 132 | func (c *Context[H]) CountCommitted() (count int) { 133 | for i := range c.CommitPayloads { 134 | // Consider both Commit and PreCommit payloads since both Commit and PreCommit 135 | // phases are one-directional (do not impose view change). 136 | if c.CommitPayloads[i] != nil || c.PreCommitPayloads[i] != nil { 137 | count++ 138 | } 139 | } 140 | 141 | return 142 | } 143 | 144 | // CountFailed returns number of nodes with which no communication was performed 145 | // for this view and that hasn't sent the Commit message at the previous views. 146 | func (c *Context[H]) CountFailed() (count int) { 147 | for i, hv := range c.LastSeenMessage { 148 | if (c.CommitPayloads[i] == nil && c.PreCommitPayloads[i] == nil) && 149 | (hv == nil || hv.Height < c.BlockIndex || hv.View < c.ViewNumber) { 150 | count++ 151 | } 152 | } 153 | 154 | return 155 | } 156 | 157 | // RequestSentOrReceived returns true iff PrepareRequest 158 | // was sent or received for the current epoch. 159 | func (c *Context[H]) RequestSentOrReceived() bool { 160 | return c.PreparationPayloads[c.PrimaryIndex] != nil 161 | } 162 | 163 | // ResponseSent returns true iff Prepare* message was sent for the current epoch. 164 | func (c *Context[H]) ResponseSent() bool { 165 | return !c.WatchOnly() && c.PreparationPayloads[c.MyIndex] != nil 166 | } 167 | 168 | // PreCommitSent returns true iff PreCommit message was sent for the current epoch 169 | // assuming that the node can't go further than current epoch after PreCommit was sent. 170 | func (c *Context[H]) PreCommitSent() bool { 171 | return !c.WatchOnly() && c.PreCommitPayloads[c.MyIndex] != nil 172 | } 173 | 174 | // CommitSent returns true iff Commit message was sent for the current epoch 175 | // assuming that the node can't go further than current epoch after commit was sent. 176 | func (c *Context[H]) CommitSent() bool { 177 | return !c.WatchOnly() && c.CommitPayloads[c.MyIndex] != nil 178 | } 179 | 180 | // BlockSent returns true iff block was formed AND sent for the current height. 181 | // Once block is sent, the consensus stops new transactions and messages processing 182 | // as far as timeouts handling. 183 | // 184 | // Implementation note: the implementation of BlockSent differs from the C#'s one. 185 | // In C# algorithm they use ConsensusContext's Block.Transactions null check to define 186 | // whether block was formed, and the only place where the block can be formed is 187 | // in the ConsensusContext's CreateBlock function right after enough Commits receiving. 188 | // On the contrary, in our implementation we don't have access to the block's 189 | // Transactions field as far as we can't use block null check, because there are 190 | // several places where the call to CreateBlock happens (one of them is right after 191 | // PrepareRequest receiving). Thus, we have a separate Context.blockProcessed field 192 | // for the described purpose. 193 | func (c *Context[H]) BlockSent() bool { return c.blockProcessed } 194 | 195 | // ViewChanging returns true iff node is in a process of changing view. 196 | func (c *Context[H]) ViewChanging() bool { 197 | if c.WatchOnly() { 198 | return false 199 | } 200 | 201 | cv := c.ChangeViewPayloads[c.MyIndex] 202 | 203 | return cv != nil && cv.GetChangeView().NewViewNumber() > c.ViewNumber 204 | } 205 | 206 | // NotAcceptingPayloadsDueToViewChanging returns true if node should not accept new payloads. 207 | func (c *Context[H]) NotAcceptingPayloadsDueToViewChanging() bool { 208 | return c.ViewChanging() && !c.MoreThanFNodesCommittedOrLost() 209 | } 210 | 211 | // MoreThanFNodesCommittedOrLost returns true iff a number of nodes which either committed 212 | // or are faulty is more than maximum amount of allowed faulty nodes. 213 | // A possible attack can happen if the last node to commit is malicious and either sends change view after his 214 | // commit to stall nodes in a higher view, or if he refuses to send recovery messages. In addition, if a node 215 | // asking change views loses network or crashes and comes back when nodes are committed in more than one higher 216 | // numbered view, it is possible for the node accepting recovery to commit in any of the higher views, thus 217 | // potentially splitting nodes among views and stalling the network. 218 | func (c *Context[H]) MoreThanFNodesCommittedOrLost() bool { 219 | return c.CountCommitted()+c.CountFailed() > c.F() 220 | } 221 | 222 | // Header returns current header from context. May be nil in case if no 223 | // header is constructed yet. Do not change the resulting header. 224 | func (c *Context[H]) Header() Block[H] { 225 | return c.header 226 | } 227 | 228 | // PreHeader returns current preHeader from context. May be nil in case if no 229 | // preHeader is constructed yet. Do not change the resulting preHeader. 230 | func (c *Context[H]) PreHeader() PreBlock[H] { 231 | return c.preHeader 232 | } 233 | 234 | // PreBlock returns current PreBlock from context. May be nil in case if no 235 | // PreBlock is constructed yet (even if PreHeader is already constructed). 236 | // External changes in the PreBlock will be seen by dBFT. 237 | func (c *Context[H]) PreBlock() PreBlock[H] { 238 | return c.preBlock 239 | } 240 | 241 | func (c *Context[H]) reset(view byte, ts uint64) { 242 | c.MyIndex = -1 243 | c.prepareSentTime = time.Time{} 244 | c.lastBlockTimestamp = ts 245 | 246 | if view == 0 { 247 | c.PrevHash = c.Config.CurrentBlockHash() 248 | c.BlockIndex = c.Config.CurrentHeight() + 1 249 | c.Validators = c.Config.GetValidators() 250 | c.timePerBlock = c.Config.TimePerBlock() 251 | 252 | n := len(c.Validators) 253 | c.LastChangeViewPayloads = emptyReusableSlice(c.LastChangeViewPayloads, n) 254 | 255 | c.LastSeenMessage = emptyReusableSlice(c.LastSeenMessage, n) 256 | c.blockProcessed = false 257 | c.preBlockProcessed = false 258 | } else { 259 | for i := range c.Validators { 260 | m := c.ChangeViewPayloads[i] 261 | if m != nil && m.GetChangeView().NewViewNumber() >= view { 262 | c.LastChangeViewPayloads[i] = m 263 | } else { 264 | c.LastChangeViewPayloads[i] = nil 265 | } 266 | } 267 | } 268 | 269 | c.MyIndex, c.Priv, c.Pub = c.Config.GetKeyPair(c.Validators) 270 | 271 | c.block = nil 272 | c.preBlock = nil 273 | c.header = nil 274 | c.preHeader = nil 275 | 276 | n := len(c.Validators) 277 | c.ChangeViewPayloads = emptyReusableSlice(c.ChangeViewPayloads, n) 278 | if view == 0 { 279 | c.PreCommitPayloads = emptyReusableSlice(c.PreCommitPayloads, n) 280 | c.CommitPayloads = emptyReusableSlice(c.CommitPayloads, n) 281 | } 282 | c.PreparationPayloads = emptyReusableSlice(c.PreparationPayloads, n) 283 | 284 | if c.Transactions == nil { // Init. 285 | c.Transactions = make(map[H]Transaction[H]) 286 | } else { // Regular use. 287 | clear(c.Transactions) 288 | } 289 | c.TransactionHashes = nil 290 | if c.MissingTransactions != nil { 291 | c.MissingTransactions = c.MissingTransactions[:0] 292 | } 293 | c.PrimaryIndex = c.GetPrimaryIndex(view) 294 | c.ViewNumber = view 295 | 296 | if c.MyIndex >= 0 { 297 | c.LastSeenMessage[c.MyIndex] = &HeightView{c.BlockIndex, c.ViewNumber} 298 | } 299 | } 300 | 301 | func emptyReusableSlice[E any](s []E, n int) []E { 302 | if len(s) == n { 303 | clear(s) 304 | return s 305 | } 306 | return make([]E, n) 307 | } 308 | 309 | // Fill initializes consensus when node is a speaker. 310 | func (c *Context[H]) Fill() { 311 | b := make([]byte, 8) 312 | _, err := rand.Read(b) 313 | if err != nil { 314 | panic(err) 315 | } 316 | 317 | txx := c.Config.GetVerified() 318 | c.Nonce = binary.LittleEndian.Uint64(b) 319 | c.TransactionHashes = make([]H, len(txx)) 320 | 321 | for i := range txx { 322 | h := txx[i].Hash() 323 | c.TransactionHashes[i] = h 324 | c.Transactions[h] = txx[i] 325 | } 326 | 327 | c.Timestamp = c.lastBlockTimestamp + c.Config.TimestampIncrement 328 | if now := c.getTimestamp(); now > c.Timestamp { 329 | c.Timestamp = now 330 | } 331 | } 332 | 333 | // getTimestamp returns nanoseconds-precision timestamp using 334 | // current context config. 335 | func (c *Context[H]) getTimestamp() uint64 { 336 | return uint64(c.Config.Timer.Now().UnixNano()) / c.Config.TimestampIncrement * c.Config.TimestampIncrement 337 | } 338 | 339 | // CreateBlock returns resulting block for the current epoch. 340 | func (c *Context[H]) CreateBlock() Block[H] { 341 | if c.block == nil { 342 | if c.block = c.MakeHeader(); c.block == nil { 343 | return nil 344 | } 345 | 346 | txx := make([]Transaction[H], len(c.TransactionHashes)) 347 | 348 | for i, h := range c.TransactionHashes { 349 | txx[i] = c.Transactions[h] 350 | } 351 | 352 | // Anti-MEV extension properly sets PreBlock transactions once during PreBlock 353 | // construction and then never updates these transactions in the dBFT context. 354 | // Thus, user must not reuse txx if anti-MEV extension is enabled. However, 355 | // we don't skip a call to Block.SetTransactions since it may be used as a 356 | // signal to the user's code to finalize the block. 357 | c.block.SetTransactions(txx) 358 | } 359 | 360 | return c.block 361 | } 362 | 363 | // CreatePreBlock returns PreBlock for the current epoch. 364 | func (c *Context[H]) CreatePreBlock() PreBlock[H] { 365 | if c.preBlock == nil { 366 | if c.preBlock = c.MakePreHeader(); c.preBlock == nil { 367 | return nil 368 | } 369 | 370 | txx := make([]Transaction[H], len(c.TransactionHashes)) 371 | 372 | for i, h := range c.TransactionHashes { 373 | txx[i] = c.Transactions[h] 374 | } 375 | 376 | c.preBlock.SetTransactions(txx) 377 | } 378 | 379 | return c.preBlock 380 | } 381 | 382 | // isAntiMEVExtensionEnabled returns whether Anti-MEV dBFT extension is enabled 383 | // at the currently processing block height. 384 | func (c *Context[H]) isAntiMEVExtensionEnabled() bool { 385 | return c.Config.AntiMEVExtensionEnablingHeight >= 0 && uint32(c.Config.AntiMEVExtensionEnablingHeight) <= c.BlockIndex 386 | } 387 | 388 | // MakeHeader returns half-filled block for the current epoch. 389 | // All hashable fields will be filled. 390 | func (c *Context[H]) MakeHeader() Block[H] { 391 | if c.header == nil { 392 | if !c.RequestSentOrReceived() { 393 | return nil 394 | } 395 | // For anti-MEV dBFT extension it's important to have PreBlock processed and 396 | // all envelopes decrypted, because a single PrepareRequest is not enough to 397 | // construct proper Block. 398 | if c.isAntiMEVExtensionEnabled() { 399 | if !c.preBlockProcessed { 400 | return nil 401 | } 402 | } 403 | c.header = c.Config.NewBlockFromContext(c) 404 | } 405 | 406 | return c.header 407 | } 408 | 409 | // MakePreHeader returns half-filled block for the current epoch. 410 | // All hashable fields will be filled. 411 | func (c *Context[H]) MakePreHeader() PreBlock[H] { 412 | if c.preHeader == nil { 413 | if !c.RequestSentOrReceived() { 414 | return nil 415 | } 416 | c.preHeader = c.Config.NewPreBlockFromContext(c) 417 | } 418 | 419 | return c.preHeader 420 | } 421 | 422 | // hasAllTransactions returns true iff all transactions were received 423 | // for the proposed block. 424 | func (c *Context[H]) hasAllTransactions() bool { 425 | return len(c.TransactionHashes) == len(c.Transactions) 426 | } 427 | -------------------------------------------------------------------------------- /docs/labels.md: -------------------------------------------------------------------------------- 1 | # Project-specific labels 2 | 3 | ## Component 4 | 5 | Currently only these ones are used, but the list can be extended in future: 6 | 7 | - tla+ 8 | 9 | Related to the TLA+ algorithm specification 10 | -------------------------------------------------------------------------------- /docs/release-instruction.md: -------------------------------------------------------------------------------- 1 | # Release instructions 2 | 3 | This document outlines the dbft release process. It can be used as a todo 4 | list for a new release. 5 | 6 | ## Check the state 7 | 8 | These should run successfully: 9 | * build 10 | * unit-tests 11 | * lint 12 | * simulation with default settings 13 | 14 | ## Update CHANGELOG and ROADMAP 15 | 16 | Add an entry to the CHANGELOG.md following the style established there. Add a 17 | codename, version and release date in the heading. Write a paragraph 18 | describing the most significant changes done in this release. In case if the dBFT 19 | configuration was changed, some API was marked as deprecated, any experimental 20 | changes were made in the user-facing code and the users' feedback is needed or 21 | if there's any other information that requires user's response, write 22 | another separate paragraph for those who uses dbft package. Then, add sections 23 | with release content describing each change in detail and with a reference to 24 | GitHub issues and/or PRs. Minor issues that doesn't affect the package end-user may 25 | be grouped under a single label. 26 | * "New features" section should include new abilities that were added to the 27 | dBFT/API, are directly visible or available to the user and are large 28 | enough to be treated as a feature. Do not include minor user-facing 29 | improvements and changes that don't affect the user-facing functionality 30 | even if they are new. 31 | * "Behaviour changes" section should include any incompatible changes in default 32 | settings or in API that are available to the user. Add a note about changes 33 | user needs to make if he uses the affected code. 34 | * "Improvements" section should include user-facing changes that are too 35 | insignificant to be treated as a feature and are not directly visible to the 36 | package end-user, such as performance optimizations, refactoring and internal 37 | API changes. 38 | * "Bugs fixed" section should include a set of bugs fixed since the previous 39 | release with optional bug cause or consequences description. 40 | 41 | Create a PR with CHANGELOG changes, review/merge it. 42 | 43 | ## Create a GitHub release and a tag 44 | 45 | Use "Draft a new release" button in the "Releases" section. Create a new 46 | `vX.Y.Z` tag for it following the semantic versioning standard. Put change log 47 | for this release into the description. Do not attach any binaries. 48 | Set the "Set as the latest release" checkbox if this is the latest stable 49 | release or "Set as a pre-release" if this is an unstable pre-release. 50 | Press the "Publish release" button. 51 | 52 | ## Close GitHub milestone 53 | 54 | Close corresponding X.Y.Z GitHub milestone. 55 | 56 | ## Announcements 57 | 58 | Copy the GitHub release page link to: 59 | * Element channel 60 | 61 | ## Dependant projects update 62 | 63 | Create an issue or PR to fetch the updated package version in the dependant 64 | repositories. 65 | -------------------------------------------------------------------------------- /formal-models/.github/dbft.drawio: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /formal-models/.github/dbft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nspcc-dev/dbft/7462b47e4d2d26be510f6dafdaa4832178234495/formal-models/.github/dbft.png -------------------------------------------------------------------------------- /formal-models/.github/dbft2.1_centralizedCV.drawio: -------------------------------------------------------------------------------- 1 | 7V3rk5u2Fv9rPE06szu8sT+u7d020+y9me69SfMpg21s02CgmH04f30lkAC9MBgB3o3T6awR4iA4R+f8zkNipM92L7/FTrS9D1euP9KUTeytRvp8pGkq+B80RM7GJRpgjwfvB25UUOujt3L3RMckDP3Ei8jGZRgE7jIh2pw4Dp/JbuvQZ4fxsHR8l2n94q2SbdY6NpWi/XfX22zxjVQFndk5uDNq2G+dVfhcatJvR/osDsMk+7V7mbk+fDP4vWTX3QnO5gOL3SCpc8GXtf6f+OPqjwfFU+43D4sv9tq8GmdUnhz/ET0wGmxywG/ADVY38L2Bo6Xv7Pfe8n9bLxjp022y80GjCn7G4WOwclfoaOXst/nBPonD7+4s9MM4pacr4N9slp/BbxW8j+k6DJI7Z+f5B9AwC3feEgzlwQn24M/9A+qAREI1wLHrL9KBPblx4gGmwaZgdef5eGDZ07irDc3O4q2pOS+AhLrhzk3iA+jyXHAbM3tbYjRui13fSbwnkryDZGyTk8vv8Cn0wI015SXrYVvokgMp5ZjCPnyMly66qMzcI3SuxhShxIk3bsIQAj9KT100pbLTQI5UlSNIlg9e1nQBfmzgj19HUEKzRnCTvJ2VOMCuj87C9Ukhc3xvE0ApBKxzgTBNMddv0Imdt1pBGtPY3Xs/nEVKTwHHEXzs9EWY05E5JwVJG9eVvJIQz2ZQjFPJTwD/Q3j7iZI/DFI+aAiFDoAjdl+Eoiieoqx8IirKtW6aFsl7VYpoXpmkZE5IAuF6vXfbylL4wYmmv3/efVv/vX5a3v9pf1tZV7rCkSVKREiF87z1Evchcpbw7DOwMqTgNFYra6BASsy+Vefmrc3osiAMoLBtoFJEgpbr+EIUjrBcoH3QBXgWIx7oFjouKaeJxtFOmqGI2U3wqylztMmZMWc2u7srmZPhmKOqfTLnx3+tm2/bzytH2xyi+f/N2TT4fAXez3FzDpTrAzoM42QbbsLA8W+LVsqY//24i9Drh4qyuORjGEa4j5skB9TJeUxCksMt7bzFal9Dgf+lpJ04wegEsTxtQxBAyQABH7+QSCG9DN3TyM4SR4A/f0GK1yY+/JrfABzMX4ijQ/nokxt7gMHQZpVk7zgoAcg2tdoVHLf4EtpS9Rs2iSZ08zRUYk6oKWJ1hkr488EUopKV90TMC+ufRwjEU0m72qeMvwEdVCN6STmGzxegJaMD+7citAeCBs5/+tP9B/x5FwFRceLD+xJQym5B3hY0p09Atp7NQ4GJ+qrHXzBlH0GuLJzl98coZ8oi5jxPncccFOjmmrQJ0M1VbStgW2GtKpCtapMQVJeCa1VSt9HGWAqu5T6vXq2MThRhixFhZZ1hraKN5TMj8V7gJR4QvB/A8DeRaQeO8slzn0vOnYdPPpWu8YSzgMUHZfCwdSLYb/eygdGj67UfPi+3wFRfg6mx8wIngZLKBZmEqAP+xRvP4cwIJkBxe2NNLUsORtQNygKyGFHVK8IL0iEinkSDIkKpaEpCGGlweFkfEWbQ6xgirIEc7ZbIsZ0U8hyVTFXsIyfgKj5ogzepJF4tM15B/RdvFu80BZADA1E000Q/FOM9HyHYswJs5bcEEhRsxNp2jWQJ3i8Ik/Bb7G4efSfOtC01GBsNofj7HmllSnErUcIf4q02Go9HNzdkuAwPMuuEkUn2POklk9GNCQX86Isc4plKz5EO60jzsDgJq4QzwUmZyhaamCvlGk8nuQFAkqhpd4CT+CEm+xxM1IuX/IVVPPj9tbBW4KiwT/DgUDro0Tq95sDGKWaMKywC+NVW9C0StjF5GUHgAvDDOZS6IT0jvo9JBdkNk5pRGUW584uX9LOcHcTMwWIP/4Ah3oV04yjPZL5OzezDcU5zFMGLIbdS3hXKrEp525ZlS1Xf3XuyFiNAUezCV/Hgovnws/h3JqUoVG1wB888B+t5cfA6c/Dsmg6eIG/ck4PHqog2Dp6G/TpVRz90tcrBAzKx85Jq/+jidFQ6HeYxu6WO1TFptzQpbofWm5/RWlEO6FfkGjZXtyVK7TVsTaVHKjlaBVJ2RIy/bq25Mr/tXE124kC0qqYQa0lOSqBjxclD+63TaQ1zETjwNC83YjXHDFnspTRP0wnCUgNn7zrwR46pdYVS6nKSbldkMIm6vjtPxWYm2DKFBj+do2JrpKOij4d2VNgoxMIPl99vlks3SmAW8ifmjmUOzR1uQW//cKlxpVjheUrARY3gDSufhHdJwqiyk6mSeAu5oQXQwhAw86cNwypBR6iwtcbgUSLMmtT0RgU5jLa+AlUhMqYK1bquL9OE+K11CRIGN3zcMqyvem6FRNXKFSbIVI0KtMtBNVR90rgvWHMeKbJLkI/KpUny7WWr57Zqt2lWi46Jk0mtY93x+g9Rd0Mxq/qTOTO2+o8uyLJPrG2mh1EzQyhNA3CTeJzQAekcZ9HRz6ooNFr4znQ5yoS/dEt4wVgXXDAPwf3fsbWCv/CKBd+nUYtmHr6osLgodIRsSkpkjgyCvduzB9QUQD1bl0clgNWQPDJKFIdRuHf8+uO/BKUZs1dRMQwQILUWTk74wtLIud5F/IIbrjkLO4/MGv59KYXptxSG42L1WQpDB44m9QzdqyiFERvRjstQuXb5zZWl4qfE5u+uwpS/pXRsaVF4a0MoNgtVDq9lG3JcXGr2X/Vm+fLXcx5zsxlmzSYAK+udgW1DEthuNB0HqsjNocfX0pkTMudnHrY4t7qj2jCk7cqS3ssZeVsaMH7j8knl+2hATvmepD4HfaYqb6Yx0+ktJ7eYkEufNZJcEZ2ckzq7eFVde1Vib2mwxJVJwaoJRaGtU5UvbhaEXvP7Noy1TipirbKQnypeKN1zCRRCflpj5DcQ0msy0AvMe0Mwj5fi6lZX8vtpx/xSxbYmUvxSOlykm9T+ZLXzOBQhQ6cICVRxRwCV/17FvnCNZI/Wg1aqSvY00UqSkz00tR6SP3tn556Q/VFEz/SKw2CjNnGvIz4FE5eq2lRNkAomKOAUTw+z2TgnM/vG3Q+ZVlBg3U61ZNhy1dwUrSn2N9U+sLp4h7ULVr9g9TeiRKQutpKukaQA87GpdQPMrZrqrQ6ebptTNo+EO+j9Khv370XlanWcgeWTxkeUVeHqXy7x6lGpwgF7hLiqfLB4taadk7K/IMba+llyKkxQX6tIBpD822hmH3vTDLYxnLSs/yXLfw6QsuP18W9cZbVI85cgZV6M2+1yA41KUuE9V0Wojd6TtWn/SkXYa1iYXd0sq3DhggTTszTnMerrAwlyPwbTHgi20sm6hRebnr749PTNRngfe8qTfMDiEoq+OG6i6Wt+ukqmwmX1bXcruTgJfuM6fSz0TyM149i4PnF5FF3hoNLxdVll47pN3UcuSuXOQm7BGJW2argM2GS3TuHuhYI+PNX+0wfsvvEnErJlERrLIjSRRAhMaEmEVFmENFmEdFmEZH2LQ2NnwImEZEm2JkuytWOSnftyv7J6pDr9veC0CXY4ujQfa34FqfYae9h2/0XCo8kETaWM8iluILvbgk4QNcjrO3Sz6nlZJ8TbL07WiA2397ozFf8DlCzHPzAsAQ+cdKcYoBO2RxxgGIAK/MofQsRNBeM0s44+qS7dacXXCRU/1hWWrzy2dvbpSp1NnH14fXzVlaH5SgVFTIPla9UXk6XHRI5EwYb9ntxJH+NiUGCxbUclojmzFcMoFZItGhasGB7uFTdYbl3zzXT1JR8xfu3wG4oL+ikE2ao+xtK7cPCW4wu4MIBuGFJcz+xtCDYcyIYIaaV7DlSNuPEWPOcOEOo5khKAgE1v3IWXdpSAABfh2c2RADiMQ8j5wrGDvs59uILu+e2/ -------------------------------------------------------------------------------- /formal-models/.github/dbft2.1_centralizedCV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nspcc-dev/dbft/7462b47e4d2d26be510f6dafdaa4832178234495/formal-models/.github/dbft2.1_centralizedCV.png -------------------------------------------------------------------------------- /formal-models/.github/dbft2.1_threeStagedCV.drawio: -------------------------------------------------------------------------------- 1 | 7V1tc6O2Fv41nmk/JMM79kfbiW87t9vuNG3u3U87BGObXYwoxom9v76SkQAJ8WIQGCfuTidBCAE6R+c85zkHZaTOt4f/hFaw+QSWjjdSpHXoLkfqw0hRZPg/bAistUM1oB5P7g/SKOHWvbt0dlTHCAAvcgO60Qa+79gR1WaFIXiju62Al3+MJ9vynFzr/9xltIlbx7qUtv/iuOsNuZEs4TNbi3TGDbuNtQRvmSb1caTOQwCi+LftYe54aGbIvMTXLQrOJg8WOn5U5wLwqxXMfnnefl19W73an/40vy6NO2USD/NqeXv8xvhpoyOZghDs/aWDRpFH6uxt40bOU2DZ6OwblChs20RbD59eAT9aWFvXO8KGOdi6NhzsyfJ38MenJ9wBS1XW0LHreXPggfB0M3U+Xyzmc9i+i0Lw3SFnfODDK2Zrz9oh+UmoA5lPdIDfwgkj51A4P3Iy61AXHbB1ovAIu+ALDCynYyLJ+PgtFbssj3HjJiNzRcONFlatdTJ2Kg74C5YIXzqL2X/BzvsCDru9s5Z2/jPQf7+Tc7JwllA78SEIow1YA9/yHtPWWdr6GwABlso3J4qOeNatfQRomTkHN/o/msh7HR99yZx5OOA5Ph0cMwefndCFL+qEpM2HL50ZCB1+yZ5LhzodHbNH7GC01sXaQFagcrpoOUXLGR7aSCtc+6+N68cnFq7nURdmFEw6/ZeoDJrQcoWB8w/2oY17/fjDmH7dPC8tZX0MHv7W5zP/+Q4vosgK105U0s/kK2DoeFbkvtLPwVMmfOln4MInTBR3zGiuYjL6GD8XvipVSTh91jHTLUAddsX3kVWdvo/OGBym/2Rc2h/+Ej9Buj6SOam1ZLgGTTUGZtBWq5Vi2wMwaEZtgyYrAgwaXzrSwKTzKD/oj+bZ0rFCmwxqdCIs1ejV+3CtFZyvSmFV+yNanN/22wDP3LiZs8p7grN0wMDHGR3QJPTvNLQVRsSpYBU4tWF/Ip3jduBl+J5afJY6Eugo6/uxSv9kdOKfNFOmdVunR4jda8495cbRJwxCM+r5uQauhb8edM56MDw4obOl+0qtC+OfPYL1J027250EP4UdZC04nCRGzsPf1vinR/q3GmgHFQ2e//yn8w/88VMAVcUKjz+T8eF7x7egbwubT29Atw7mpeBCvernT4WyC5BUXiz7+z5IhPISct6nzmtybPFv1guMrimDaXnu2kfmCpoDZDRmyE+5MMKd4hNbd7mMTbUDX8Z6OY2HLAsGhHBwfTbSHwosaaXp5ZnaUTZGx7dMItgqT1rirQr9q3QvmzQcVeOjlsbNUKhBWWcMVqud04k1UsuNUUMVNnIqLK0sm74mL+ecxru+G7lQ8X5Ax99Kp/PePgsFNlaA+m0Pa8Qs3a888GZvoOO9h4q+dX0rQnrHhZCU4kJphGvX4uh3Lnp8nBozQxDkU1l/xoF8Kgfx6V0BPrIkLorvhGKjlmBRGwJY7IXBMGoixLYMRjv95AUkFTYvabO2aN37L7sg02cXWD7XLnLQgDlPgVVyPZSNvy62rCusaci2+iACX0NnvfesEFtWxvhKQcGtH5XReDyaTjN3hhNIbh53Iugifs7TJZPRVEdqnX/7ZBgu9ohnpREkae+BCtEYnoSJ2CfKCnf+rKYTiJ5RKp8ncbdvoI5CxHVZBEnMayME6aHnnEEkvT55IF4KQTzIjD1koUe/k+6JjcI+nVD4LVGmTEfQsqZ1ADP5CSJzCJiAJChGaXoiTVbUT1B0CAeumRdqgg64ylKAd9vyR2x+Q+4mv6HpdIAoGzqzorrIV1RgmiHwFzn8gL2lLAwJ2fGKQifD9ctPKFJHt0h//twKGImDbB0/aAmCu2L+qwm84aA9MbD0WieEvwbBdutGOZ2BNkVaXIXOc4A3eVH82pKUe/VewpdB6En7mKfN3bNmvlg2xYIYTqiB4rnhhholwLs01FAmTGJNDKNNoy1CNHfPaOcrN4LQQfP05GBg91GYYVNmmGHt4tSwPoQw8EYNM/EijotJjNw0LhYYKpo1ieTxRYlkXpFYQ0a4AIKxsJ0GJe+Sd0tqqTrg3fQqZyiP5THttxQh3lBhBu2NeFPHQ7C4Q6wMrqGBQyf6ujW4fH0qWELnrY9zOTW2Blgbl9cMm2Zp/9YcXNkUZryBfTLqHw53ThQadyYVChfDneOcaF48YH+f2rYTRKi45ANJR5Z0WjzJYrmYeEheSmSJcAM3dXYFcOqPKG+UOqdz/BFbrkEbfsYJ5xWUihZol5INGmTa9+CwInU6xPXm3XXioS8RD9T9NKYgsdoWvTHuZMyUHnddMawUxhmtuUF49QIU8oKXDTCGVhpablcRlSYrTO5PSPBwp5q0PzU6iB74AdMg0vY3vobP17StY7hW+3xu9MDgHSYhX9WdfG1W1J3N9zP96VgjX5KjMQ9nNvysRZMY09PvVy1KHmLzq9aKSwB4xNf83KxdedUcKignzW7aL5tYTJotNLm+c0A/Xl3n7UpyVO1oOfFesyIBhfJPY4NZQEK85riHBBSXIhmEzxRE6Q+dARtqqRvHH/ZZ6lZEwLyHUre6jkZQaUz/5WnCC/p7r1vr85OEinKdelX03ZSUib/34Ku3xEq+ZlnX2TVc7wWthSCCbgL4o6qMVocFRpX1RYapiaFBNCYzywzQHQvC29wqt5ztV7mmhr3n5ILJRLN9Vhxx1XMyIDB+++6kczBeDLIvljxg6aQJM4IgLK7St9HkcpKL6X5nnklymSUklyikTyisZpyS0hOnxI4Wc0xSEX3kskNxb5nr9Y5QSysMUu5+FIlR68TZnPGRJDUC2Zijh0qtIfFGbSu1PmxxVV0HNKDiKta2X7q4ij81tUB4qS+44ji4iiro7vMrQi6UhtQDm9JKRqGIQ/rwZFHLh+S8rfjXysO7GyfUY2F9CXYpY4E0g6VvxGT5FHlC47Uudk7j7qqttIZrF8zrCa1czDFKmZKYhvttXyD672tPqrKHrKIYc0GkMmoeM75nHpLdFK/XEnS+icyL+NecSCLEF3TmY5A52WEJ5ASAC9ayG1uTpswH0Hob1yRArjIb4atSXrA8uYrY3povVyUv1+sTrNrqy3YRgpWZFavrecHqfS5YbRDbWApKHdy+nOuM3CH7kA+z9pXZAsCo4Plllj+Vz8wLMBd0QwaRfeuakUGZaoJbYmBIoetIeFyqlW+0CMNSzRBTnGD2Vl5K7E1VrKDewoDcDiiG1uOnjnyN5IQBN7zYBC+O+Z4tK1qjI8ly+aCzdtJvw+M25Uw/8qbTrUj3IWzAJvqJuMkAlkhflE71O9vIu0EO4rx9vntM/NxyHUJyHRV/a43Z4k5jP7mrgRqpAcz2EBIepn8KNu6e/rVc9fFf -------------------------------------------------------------------------------- /formal-models/.github/dbft2.1_threeStagedCV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nspcc-dev/dbft/7462b47e4d2d26be510f6dafdaa4832178234495/formal-models/.github/dbft2.1_threeStagedCV.png -------------------------------------------------------------------------------- /formal-models/.github/dbft_antiMEV.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /formal-models/.github/dbft_antiMEV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nspcc-dev/dbft/7462b47e4d2d26be510f6dafdaa4832178234495/formal-models/.github/dbft_antiMEV.png -------------------------------------------------------------------------------- /formal-models/dbft/dbft.tla: -------------------------------------------------------------------------------- 1 | -------------------------------- MODULE dbft -------------------------------- 2 | 3 | EXTENDS 4 | Integers, 5 | FiniteSets 6 | 7 | CONSTANTS 8 | \* RM is the set of consensus node indexes starting from 0. 9 | \* Example: {0, 1, 2, 3} 10 | RM, 11 | 12 | \* RMFault is a set of consensus node indexes that are allowed to become 13 | \* FAULT in the middle of every considered behavior and to send any 14 | \* consensus message afterwards. RMFault must be a subset of RM. An empty 15 | \* set means that all nodes are good in every possible behaviour. 16 | \* Examples: {0} 17 | \* {1, 3} 18 | \* {} 19 | RMFault, 20 | 21 | \* RMDead is a set of consensus node indexes that are allowed to die in the 22 | \* middle of every behaviour and do not send any message afterwards. RMDead 23 | \* must be a subset of RM. An empty set means that all nodes are alive and 24 | \* responding in in every possible behaviour. RMDead may intersect the 25 | \* RMFault set which means that node which is in both RMDead and RMFault 26 | \* may become FAULT and send any message starting from some step of the 27 | \* particular behaviour and may also die in the same behaviour which will 28 | \* prevent it from sending any message. 29 | \* Examples: {0} 30 | \* {3, 2} 31 | \* {} 32 | RMDead, 33 | 34 | \* MaxView is the maximum allowed view to be considered (starting from 0, 35 | \* including the MaxView itself). This constraint was introduced to reduce 36 | \* the number of possible model states to be checked. It is recommended to 37 | \* keep this setting not too high (< N is highly recommended). 38 | \* Example: 2 39 | MaxView 40 | 41 | VARIABLES 42 | \* rmState is a set of consensus node states. It is represented by the 43 | \* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is 44 | \* the state of the r-th consensus node at the current step. 45 | rmState, 46 | 47 | \* msgs is the shared pool of messages sent to the network by consensus nodes. 48 | \* It is represented by a subset of Messages set. 49 | msgs 50 | 51 | \* vars is a tuple of all variables used in the specification. It is needed to 52 | \* simplify fairness conditions definition. 53 | vars == <> 54 | 55 | \* N is the number of validators. 56 | N == Cardinality(RM) 57 | 58 | \* F is the number of validators that are allowed to be malicious. 59 | F == (N - 1) \div 3 60 | 61 | \* M is the number of validators that must function correctly. 62 | M == N - F 63 | 64 | \* These assumptions are checked by the TLC model checker once at the start of 65 | \* the model checking process. All the input data (declared constants) specified 66 | \* in the "Model Overview" section must satisfy these constraints. 67 | ASSUME 68 | /\ RM \subseteq Nat 69 | /\ N >= 4 70 | /\ 0 \in RM 71 | /\ RMFault \subseteq RM 72 | /\ RMDead \subseteq RM 73 | /\ Cardinality(RMFault) <= F 74 | /\ Cardinality(RMDead) <= F 75 | /\ Cardinality(RMFault \cup RMDead) <= F 76 | /\ MaxView \in Nat 77 | /\ MaxView <= 2 78 | 79 | \* RMStates is a set of records where each record holds the node state and 80 | \* the node current view. 81 | RMStates == [ 82 | type: {"initialized", "prepareSent", "commitSent", "cv", "blockAccepted", "bad", "dead"}, 83 | view : Nat 84 | ] 85 | 86 | \* Messages is a set of records where each record holds the message type, 87 | \* the message sender and sender's view by the moment when message was sent. 88 | Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView"}, rm : RM, view : Nat] 89 | 90 | \* -------------- Useful operators -------------- 91 | 92 | \* IsPrimary is an operator defining whether provided node r is primary 93 | \* for the current round from the r's point of view. It is a mapping 94 | \* from RM to the set of {TRUE, FALSE}. 95 | IsPrimary(r) == rmState[r].view % N = r 96 | 97 | \* GetPrimary is an operator defining mapping from round index to the RM that 98 | \* is primary in this round. 99 | GetPrimary(view) == CHOOSE r \in RM : view % N = r 100 | 101 | \* GetNewView returns new view number based on the previous node view value. 102 | \* Current specifications only allows to increment view. 103 | GetNewView(oldView) == oldView + 1 104 | 105 | \* CountCommitted returns the number of nodes that have sent the Commit message 106 | \* in the current round or in some other round. 107 | CountCommitted(r) == Cardinality({rm \in RM : Cardinality({msg \in msgs : msg.rm = rm /\ msg.type = "Commit"}) /= 0}) 108 | 109 | \* MoreThanFNodesCommitted returns whether more than F nodes have been committed 110 | \* in the current round (as the node r sees it). 111 | \* 112 | \* IMPORTANT NOTE: we intentionally do not add the "lost" nodes calculation to the specification, and here's 113 | \* the reason: from the node's point of view we can't reliably check that some neighbour is completely 114 | \* out of the network. It is possible that the node doesn't receive consensus messages from some other member 115 | \* due to network delays. On the other hand, real nodes can go down at any time. The absence of the 116 | \* member's message doesn't mean that the member is out of the network, we never can be sure about 117 | \* that, thus, this information is unreliable and can't be trusted during the consensus process. 118 | \* What can be trusted is whether there's a Commit message from some member was received by the node. 119 | MoreThanFNodesCommitted(r) == CountCommitted(r) > F 120 | 121 | \* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest 122 | \* message received from the current round's speaker (as the node r sees it). 123 | PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in msgs 124 | 125 | \* -------------- Safety temporal formula -------------- 126 | 127 | \* Init is the initial predicate initializing values at the start of every 128 | \* behaviour. 129 | Init == 130 | /\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]] 131 | /\ msgs = {} 132 | 133 | \* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest. 134 | RMSendPrepareRequest(r) == 135 | /\ rmState[r].type = "initialized" 136 | /\ IsPrimary(r) 137 | /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] 138 | /\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]} 139 | /\ UNCHANGED <<>> 140 | 141 | \* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from 142 | \* the primary node of the current round (view) and broadcasting PrepareResponse. 143 | \* This step assumes that PrepareRequest always contains valid transactions and 144 | \* signatures. 145 | RMSendPrepareResponse(r) == 146 | /\ \/ rmState[r].type = "initialized" 147 | \* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage 148 | \* as it is done in the code-level dBFT implementation by checking the NotAcceptingPayloadsDueToViewChanging 149 | \* condition (see 150 | \* https://github.com/nspcc-dev/dbft/blob/31c1bbdc74f2faa32ec9025062e3a4e2ccfd4214/dbft.go#L419 151 | \* and 152 | \* https://github.com/neo-project/neo-modules/blob/d00d90b9c27b3d0c3c57e9ca1f560a09975df241/src/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs#L79). 153 | \* However, we can't easily count the number of "lost" nodes in this specification to match precisely 154 | \* the implementation. Moreover, we don't need it to be counted as the RMSendPrepareResponse enabling 155 | \* condition specifies only the thing that may happen given some particular set of enabling conditions. 156 | \* Thus, we've extended the NotAcceptingPayloadsDueToViewChanging condition to consider only MoreThanFNodesCommitted. 157 | \* It should be noted that the logic of MoreThanFNodesCommittedOrLost can't be reliable in detecting lost nodes 158 | \* (even with neo-project/neo#2057), because real nodes can go down at any time. See the comment above the MoreThanFNodesCommitted. 159 | \/ /\ rmState[r].type = "cv" 160 | /\ MoreThanFNodesCommitted(r) 161 | /\ \neg IsPrimary(r) 162 | /\ PrepareRequestSentOrReceived(r) 163 | /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] 164 | /\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]} 165 | /\ UNCHANGED <<>> 166 | 167 | \* RMSendCommit describes node r sending Commit if there's enough PrepareResponse 168 | \* messages. 169 | RMSendCommit(r) == 170 | /\ \/ rmState[r].type = "prepareSent" 171 | \* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage, 172 | \* see the related comment inside the RMSendPrepareResponse definition. 173 | \/ /\ rmState[r].type = "cv" 174 | /\ MoreThanFNodesCommitted(r) 175 | /\ Cardinality({ 176 | msg \in msgs : /\ (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest") 177 | /\ msg.view = rmState[r].view 178 | }) >= M 179 | /\ PrepareRequestSentOrReceived(r) 180 | /\ rmState' = [rmState EXCEPT ![r].type = "commitSent"] 181 | /\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view]} 182 | /\ UNCHANGED <<>> 183 | 184 | \* RMAcceptBlock describes node r collecting enough Commit messages and accepting 185 | \* the block. 186 | RMAcceptBlock(r) == 187 | /\ rmState[r].type /= "bad" 188 | /\ rmState[r].type /= "dead" 189 | /\ PrepareRequestSentOrReceived(r) 190 | /\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) >= M 191 | /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"] 192 | /\ UNCHANGED <> 193 | 194 | \* RMSendChangeView describes node r sending ChangeView message on timeout. 195 | RMSendChangeView(r) == 196 | /\ \/ (rmState[r].type = "initialized" /\ \neg IsPrimary(r)) 197 | \/ rmState[r].type = "prepareSent" 198 | /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] 199 | IN /\ cv \notin msgs 200 | /\ rmState' = [rmState EXCEPT ![r].type = "cv"] 201 | /\ msgs' = msgs \cup {[type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]} 202 | 203 | \* RMReceiveChangeView describes node r receiving enough ChangeView messages for 204 | \* view changing. 205 | RMReceiveChangeView(r) == 206 | /\ rmState[r].type /= "bad" 207 | /\ rmState[r].type /= "dead" 208 | /\ rmState[r].type /= "blockAccepted" 209 | /\ rmState[r].type /= "commitSent" 210 | /\ Cardinality({ 211 | rm \in RM : Cardinality({ 212 | msg \in msgs : /\ msg.type = "ChangeView" 213 | /\ msg.rm = rm 214 | /\ GetNewView(msg.view) >= GetNewView(rmState[r].view) 215 | }) /= 0 216 | }) >= M 217 | /\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(rmState[r].view)] 218 | /\ UNCHANGED <> 219 | 220 | \* RMBeBad describes the faulty node r that will send any kind of consensus message starting 221 | \* from the step it's gone wild. This step is enabled only when RMFault is non-empty set. 222 | RMBeBad(r) == 223 | /\ r \in RMFault 224 | /\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F 225 | /\ rmState' = [rmState EXCEPT ![r].type = "bad"] 226 | /\ UNCHANGED <> 227 | 228 | \* RMFaultySendCV describes sending CV message by the faulty node r. 229 | RMFaultySendCV(r) == 230 | /\ rmState[r].type = "bad" 231 | /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] 232 | IN /\ cv \notin msgs 233 | /\ msgs' = msgs \cup {cv} 234 | /\ UNCHANGED <> 235 | 236 | \* RMFaultyDoCV describes view changing by the faulty node r. 237 | RMFaultyDoCV(r) == 238 | /\ rmState[r].type = "bad" 239 | /\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)] 240 | /\ UNCHANGED <> 241 | 242 | \* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r. 243 | RMFaultySendPReq(r) == 244 | /\ rmState[r].type = "bad" 245 | /\ IsPrimary(r) 246 | /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] 247 | IN /\ pReq \notin msgs 248 | /\ msgs' = msgs \cup {pReq} 249 | /\ UNCHANGED <> 250 | 251 | \* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r. 252 | RMFaultySendPResp(r) == 253 | /\ rmState[r].type = "bad" 254 | /\ \neg IsPrimary(r) 255 | /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] 256 | IN /\ pResp \notin msgs 257 | /\ msgs' = msgs \cup {pResp} 258 | /\ UNCHANGED <> 259 | 260 | \* RMFaultySendCommit describes sending Commit message by the faulty node r. 261 | RMFaultySendCommit(r) == 262 | /\ rmState[r].type = "bad" 263 | /\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] 264 | IN /\ commit \notin msgs 265 | /\ msgs' = msgs \cup {commit} 266 | /\ UNCHANGED <> 267 | 268 | \* RMDie describes node r that was removed from the network at the particular step 269 | \* of the behaviour. After this node r can't change its state and accept/send messages. 270 | RMDie(r) == 271 | /\ r \in RMDead 272 | /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F 273 | /\ rmState' = [rmState EXCEPT ![r].type = "dead"] 274 | /\ UNCHANGED <> 275 | 276 | \* Terminating is an action that allows infinite stuttering to prevent deadlock on 277 | \* behaviour termination. We consider termination to be valid if at least M nodes 278 | \* has the block being accepted. 279 | Terminating == 280 | /\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M 281 | /\ UNCHANGED <> 282 | 283 | \* Next is the next-state action describing the transition from the current state 284 | \* to the next state of the behaviour. 285 | Next == 286 | \/ Terminating 287 | \/ \E r \in RM: 288 | RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r) 289 | \/ RMAcceptBlock(r) \/ RMSendChangeView(r) \/ RMReceiveChangeView(r) 290 | \/ RMDie(r) \/ RMBeBad(r) 291 | \/ RMFaultySendCV(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) 292 | 293 | \* Safety is a temporal formula that describes the whole set of allowed 294 | \* behaviours. It specifies only what the system MAY do (i.e. the set of 295 | \* possible allowed behaviours for the system). It asserts only what may 296 | \* happen; any behaviour that violates it does so at some point and 297 | \* nothing past that point makes difference. 298 | \* 299 | \* E.g. this safety formula (applied standalone) allows the behaviour to end 300 | \* with an infinite set of stuttering steps (those steps that DO NOT change 301 | \* neither msgs nor rmState) and never reach the state where at least one 302 | \* node is committed or accepted the block. 303 | \* 304 | \* To forbid such behaviours we must specify what the system MUST 305 | \* do. It will be specified below with the help of fairness conditions in 306 | \* the Fairness formula. 307 | Safety == Init /\ [][Next]_vars 308 | 309 | \* -------------- Fairness temporal formula -------------- 310 | 311 | \* Fairness is a temporal assumptions under which the model is working. 312 | \* Usually it specifies different kind of assumptions for each/some 313 | \* subactions of the Next's state action, but the only think that bothers 314 | \* us is preventing infinite stuttering at those steps where some of Next's 315 | \* subactions are enabled. Thus, the only thing that we require from the 316 | \* system is to keep take the steps until it's impossible to take them. 317 | \* That's exactly how the weak fairness condition works: if some action 318 | \* remains continuously enabled, it must eventually happen. 319 | Fairness == WF_vars(Next) 320 | 321 | \* -------------- Specification -------------- 322 | 323 | \* The complete specification of the protocol written as a temporal formula. 324 | Spec == Safety /\ Fairness 325 | 326 | \* -------------- Liveness temporal formula -------------- 327 | 328 | \* For every possible behaviour it's true that eventually (i.e. at least once 329 | \* through the behaviour) block will be accepted. It is something that dBFT 330 | \* must guarantee (an in practice this condition is violated). 331 | TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M) 332 | 333 | \* A liveness temporal formula asserts only what must happen (i.e. specifies 334 | \* what the system MUST do). Any behaviour can NOT violate it at ANY point; 335 | \* there's always the rest of the behaviour that can always make the liveness 336 | \* formula true; if there's no such behaviour than the liveness formula is 337 | \* violated. The liveness formula is supposed to be checked as a property 338 | \* by the TLC model checker. 339 | Liveness == TerminationRequirement 340 | 341 | \* -------------- ModelConstraints -------------- 342 | 343 | \* MaxViewConstraint is a state predicate restricting the number of possible 344 | \* behaviour states. It is needed to reduce model checking time and prevent 345 | \* the model graph size explosion. This formulae must be specified at the 346 | \* "State constraint" section of the "Additional Spec Options" section inside 347 | \* the model overview. 348 | MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView 349 | /\ \A msg \in msgs : msg.view <= MaxView 350 | 351 | \* -------------- Invariants of the specification -------------- 352 | 353 | \* Model invariant is a state predicate (statement) that must be true for 354 | \* every step of every reachable behaviour. Model invariant is supposed to 355 | \* be checked as an Invariant by the TLC Model Checker. 356 | 357 | \* TypeOK is a type-correctness invariant. It states that all elements of 358 | \* specification variables must have the proper type throughout the behaviour. 359 | TypeOK == 360 | /\ rmState \in [RM -> RMStates] 361 | /\ msgs \subseteq Messages 362 | 363 | \* InvTwoBlocksAccepted states that there can't be two different blocks accepted in 364 | \* the two different views, i.e. dBFT must not allow forks. 365 | InvTwoBlocksAccepted == \A r1 \in RM: 366 | \A r2 \in RM \ {r1}: 367 | \/ rmState[r1].type /= "blockAccepted" 368 | \/ rmState[r2].type /= "blockAccepted" 369 | \/ rmState[r1].view = rmState[r2].view 370 | 371 | \* InvFaultNodesCount states that there can be F faulty or dead nodes at max. 372 | InvFaultNodesCount == Cardinality({ 373 | r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead" 374 | }) <= F 375 | 376 | \* This theorem asserts the truth of the temporal formula whose meaning is that 377 | \* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are 378 | \* the invariants of the specification Spec. This theorem is not supposed to be 379 | \* checked by the TLC model checker, it's here for the reader's understanding of 380 | \* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount. 381 | THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvFaultNodesCount) 382 | 383 | ============================================================================= 384 | \* Modification History 385 | \* Last modified Mon Mar 06 15:36:57 MSK 2023 by root 386 | \* Last modified Fri Feb 17 15:47:41 MSK 2023 by anna 387 | \* Last modified Sat Jan 21 01:26:16 MSK 2023 by rik 388 | \* Created Thu Dec 15 16:06:17 MSK 2022 by anna 389 | -------------------------------------------------------------------------------- /formal-models/dbft/dbft___AllGoodModel.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /formal-models/dbft2.1_centralizedCV/dbftCentralizedCV___AllGoodModel.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /formal-models/dbft2.1_threeStagedCV/dbftCV3___AllGoodModel.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /formal-models/dbftMultipool/dbftMultipool___AllGoodModel.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /formal-models/dbft_antiMEV/dbft___AllGoodModel.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nspcc-dev/dbft 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/stretchr/testify v1.9.0 7 | go.uber.org/zap v1.27.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | go.uber.org/multierr v1.10.0 // indirect 14 | gopkg.in/yaml.v3 v3.0.1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 8 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 9 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 10 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 11 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 12 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | type ( 4 | // inbox is a structure storing messages from a single epoch. 5 | inbox[H Hash] struct { 6 | prepare map[uint16]ConsensusPayload[H] 7 | chViews map[uint16]ConsensusPayload[H] 8 | preCommit map[uint16]ConsensusPayload[H] 9 | commit map[uint16]ConsensusPayload[H] 10 | } 11 | 12 | // cache is an auxiliary structure storing messages 13 | // from future epochs. 14 | cache[H Hash] struct { 15 | mail map[uint32]*inbox[H] 16 | } 17 | ) 18 | 19 | func newInbox[H Hash]() *inbox[H] { 20 | return &inbox[H]{ 21 | prepare: make(map[uint16]ConsensusPayload[H]), 22 | chViews: make(map[uint16]ConsensusPayload[H]), 23 | preCommit: make(map[uint16]ConsensusPayload[H]), 24 | commit: make(map[uint16]ConsensusPayload[H]), 25 | } 26 | } 27 | 28 | func newCache[H Hash]() cache[H] { 29 | return cache[H]{ 30 | mail: make(map[uint32]*inbox[H]), 31 | } 32 | } 33 | 34 | func (c *cache[H]) getHeight(h uint32) *inbox[H] { 35 | if m, ok := c.mail[h]; ok { 36 | delete(c.mail, h) 37 | return m 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (c *cache[H]) addMessage(m ConsensusPayload[H]) { 44 | msgs, ok := c.mail[m.Height()] 45 | if !ok { 46 | msgs = newInbox[H]() 47 | c.mail[m.Height()] = msgs 48 | } 49 | 50 | switch m.Type() { 51 | case PrepareRequestType, PrepareResponseType: 52 | msgs.prepare[m.ValidatorIndex()] = m 53 | case ChangeViewType: 54 | msgs.chViews[m.ValidatorIndex()] = m 55 | case PreCommitType: 56 | msgs.preCommit[m.ValidatorIndex()] = m 57 | case CommitType: 58 | msgs.commit[m.ValidatorIndex()] = m 59 | default: 60 | // Others are recoveries and we don't currently use them. 61 | // Theoretically messages could be extracted. 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | // Structures used for type-specific dBFT/payloads implementation to avoid cyclic 10 | // dependency. 11 | type ( 12 | hash struct{} 13 | payloadStub struct { 14 | height uint32 15 | typ MessageType 16 | validatorIndex uint16 17 | } 18 | ) 19 | 20 | func (hash) String() string { 21 | return "" 22 | } 23 | 24 | func (p payloadStub) ViewNumber() byte { 25 | panic("TODO") 26 | } 27 | func (p payloadStub) SetViewNumber(byte) { 28 | panic("TODO") 29 | } 30 | func (p payloadStub) Type() MessageType { 31 | return p.typ 32 | } 33 | func (p payloadStub) SetType(MessageType) { 34 | panic("TODO") 35 | } 36 | func (p payloadStub) Payload() any { 37 | panic("TODO") 38 | } 39 | func (p payloadStub) SetPayload(any) { 40 | panic("TODO") 41 | } 42 | func (p payloadStub) GetChangeView() ChangeView { 43 | panic("TODO") 44 | } 45 | func (p payloadStub) GetPrepareRequest() PrepareRequest[hash] { 46 | panic("TODO") 47 | } 48 | func (p payloadStub) GetPrepareResponse() PrepareResponse[hash] { 49 | panic("TODO") 50 | } 51 | func (p payloadStub) GetCommit() Commit { 52 | panic("TODO") 53 | } 54 | func (p payloadStub) GetPreCommit() PreCommit { panic("TODO") } 55 | func (p payloadStub) GetRecoveryRequest() RecoveryRequest { 56 | panic("TODO") 57 | } 58 | func (p payloadStub) GetRecoveryMessage() RecoveryMessage[hash] { 59 | panic("TODO") 60 | } 61 | func (p payloadStub) ValidatorIndex() uint16 { 62 | return p.validatorIndex 63 | } 64 | func (p payloadStub) SetValidatorIndex(uint16) { 65 | panic("TODO") 66 | } 67 | func (p payloadStub) Height() uint32 { 68 | return p.height 69 | } 70 | func (p payloadStub) SetHeight(uint32) { 71 | panic("TODO") 72 | } 73 | func (p payloadStub) Hash() hash { 74 | panic("TODO") 75 | } 76 | 77 | func TestMessageCache(t *testing.T) { 78 | c := newCache[hash]() 79 | 80 | p1 := payloadStub{ 81 | height: 3, 82 | typ: PrepareRequestType, 83 | } 84 | c.addMessage(p1) 85 | 86 | p2 := payloadStub{ 87 | height: 4, 88 | typ: ChangeViewType, 89 | } 90 | c.addMessage(p2) 91 | 92 | p3 := payloadStub{ 93 | height: 4, 94 | typ: CommitType, 95 | } 96 | c.addMessage(p3) 97 | 98 | p4 := payloadStub{ 99 | height: 3, 100 | typ: PreCommitType, 101 | } 102 | c.addMessage(p4) 103 | 104 | box := c.getHeight(3) 105 | require.Len(t, box.chViews, 0) 106 | require.Len(t, box.prepare, 1) 107 | require.Len(t, box.preCommit, 1) 108 | require.Len(t, box.commit, 0) 109 | 110 | box = c.getHeight(4) 111 | require.Len(t, box.chViews, 1) 112 | require.Len(t, box.prepare, 0) 113 | require.Len(t, box.preCommit, 0) 114 | require.Len(t, box.commit, 1) 115 | } 116 | -------------------------------------------------------------------------------- /identity.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ( 8 | // PublicKey is a generic public key interface used by dbft. 9 | PublicKey any 10 | 11 | // PrivateKey is a generic private key interface used by dbft. PrivateKey is used 12 | // only by [PreBlock] and [Block] signing callbacks ([PreBlock.SetData] and 13 | // [Block.Sign]) to grant access to the private key abstraction to Block and 14 | // PreBlock signing code. PrivateKey does not contain any methods, hence user 15 | // supposed to perform type assertion before the PrivateKey usage. 16 | PrivateKey any 17 | 18 | // Hash is a generic hash interface used by dbft for payloads, blocks and 19 | // transactions identification. It is recommended to implement this interface 20 | // using hash functions with low hash collision probability. The following 21 | // requirements must be met: 22 | // 1. Hashes of two equal payloads/blocks/transactions are equal. 23 | // 2. Hashes of two different payloads/blocks/transactions are different. 24 | Hash interface { 25 | comparable 26 | fmt.Stringer 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /internal/consensus/amev_block.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/gob" 7 | "math" 8 | 9 | "github.com/nspcc-dev/dbft" 10 | "github.com/nspcc-dev/dbft/internal/crypto" 11 | "github.com/nspcc-dev/dbft/internal/merkle" 12 | ) 13 | 14 | type amevBlock struct { 15 | base 16 | 17 | transactions []dbft.Transaction[crypto.Uint256] 18 | signature []byte 19 | hash *crypto.Uint256 20 | } 21 | 22 | var _ dbft.Block[crypto.Uint256] = new(amevBlock) 23 | 24 | // NewAMEVBlock returns new block based on PreBlock and additional Commit-level data 25 | // collected from M consensus nodes. 26 | func NewAMEVBlock(pre dbft.PreBlock[crypto.Uint256], cnData [][]byte, m int) dbft.Block[crypto.Uint256] { 27 | preB := pre.(*preBlock) 28 | res := new(amevBlock) 29 | res.base = preB.base 30 | 31 | // Based on the provided cnData we'll add one more transaction to the resulting block. 32 | // Some artificial rules of new tx creation are invented here, but in Neo X there will 33 | // be well-defined custom rules for Envelope transactions. 34 | var sum uint32 35 | for i := range m { 36 | sum += binary.BigEndian.Uint32(cnData[i]) 37 | } 38 | tx := Tx64(math.MaxInt64 - int64(sum)) 39 | res.transactions = append(preB.initialTransactions, &tx) 40 | 41 | // Rebuild Merkle root for the new set of transactions. 42 | txHashes := make([]crypto.Uint256, len(res.transactions)) 43 | for i := range txHashes { 44 | txHashes[i] = res.transactions[i].Hash() 45 | } 46 | mt := merkle.NewMerkleTree(txHashes...) 47 | res.base.MerkleRoot = mt.Root().Hash 48 | 49 | return res 50 | } 51 | 52 | // PrevHash implements Block interface. 53 | func (b *amevBlock) PrevHash() crypto.Uint256 { 54 | return b.base.PrevHash 55 | } 56 | 57 | // Index implements Block interface. 58 | func (b *amevBlock) Index() uint32 { 59 | return b.base.Index 60 | } 61 | 62 | // MerkleRoot implements Block interface. 63 | func (b *amevBlock) MerkleRoot() crypto.Uint256 { 64 | return b.base.MerkleRoot 65 | } 66 | 67 | // Transactions implements Block interface. 68 | func (b *amevBlock) Transactions() []dbft.Transaction[crypto.Uint256] { 69 | return b.transactions 70 | } 71 | 72 | // SetTransactions implements Block interface. This method is special since it's 73 | // left for dBFT 2.0 compatibility and transactions from this method must not be 74 | // reused to fill final Block's transactions. 75 | func (b *amevBlock) SetTransactions(_ []dbft.Transaction[crypto.Uint256]) { 76 | } 77 | 78 | // Signature implements Block interface. 79 | func (b *amevBlock) Signature() []byte { 80 | return b.signature 81 | } 82 | 83 | // GetHashData returns data for hashing and signing. 84 | // It must be an injection of the set of blocks to the set 85 | // of byte slices, i.e: 86 | // 1. It must have only one valid result for one block. 87 | // 2. Two different blocks must have different hash data. 88 | func (b *amevBlock) GetHashData() []byte { 89 | buf := bytes.Buffer{} 90 | w := gob.NewEncoder(&buf) 91 | _ = b.EncodeBinary(w) 92 | 93 | return buf.Bytes() 94 | } 95 | 96 | // Sign implements Block interface. 97 | func (b *amevBlock) Sign(key dbft.PrivateKey) error { 98 | data := b.GetHashData() 99 | 100 | sign, err := key.(*crypto.ECDSAPriv).Sign(data) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | b.signature = sign 106 | 107 | return nil 108 | } 109 | 110 | // Verify implements Block interface. 111 | func (b *amevBlock) Verify(pub dbft.PublicKey, sign []byte) error { 112 | data := b.GetHashData() 113 | return pub.(*crypto.ECDSAPub).Verify(data, sign) 114 | } 115 | 116 | // Hash implements Block interface. 117 | func (b *amevBlock) Hash() (h crypto.Uint256) { 118 | if b.hash != nil { 119 | return *b.hash 120 | } else if b.transactions == nil { 121 | return 122 | } 123 | 124 | hash := crypto.Hash256(b.GetHashData()) 125 | b.hash = &hash 126 | 127 | return hash 128 | } 129 | -------------------------------------------------------------------------------- /internal/consensus/amev_commit.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/gob" 5 | 6 | "github.com/nspcc-dev/dbft" 7 | ) 8 | 9 | type ( 10 | // amevCommit implements dbft.Commit. 11 | amevCommit struct { 12 | data [dataSize]byte 13 | } 14 | // amevCommitAux is an auxiliary structure for amevCommit encoding. 15 | amevCommitAux struct { 16 | Data [dataSize]byte 17 | } 18 | ) 19 | 20 | const dataSize = 64 21 | 22 | var _ dbft.Commit = (*amevCommit)(nil) 23 | 24 | // EncodeBinary implements Serializable interface. 25 | func (c amevCommit) EncodeBinary(w *gob.Encoder) error { 26 | return w.Encode(amevCommitAux{ 27 | Data: c.data, 28 | }) 29 | } 30 | 31 | // DecodeBinary implements Serializable interface. 32 | func (c *amevCommit) DecodeBinary(r *gob.Decoder) error { 33 | aux := new(amevCommitAux) 34 | if err := r.Decode(aux); err != nil { 35 | return err 36 | } 37 | c.data = aux.Data 38 | return nil 39 | } 40 | 41 | // Signature implements Commit interface. 42 | func (c amevCommit) Signature() []byte { 43 | return c.data[:] 44 | } 45 | -------------------------------------------------------------------------------- /internal/consensus/amev_preBlock.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | 7 | "github.com/nspcc-dev/dbft" 8 | "github.com/nspcc-dev/dbft/internal/crypto" 9 | "github.com/nspcc-dev/dbft/internal/merkle" 10 | ) 11 | 12 | type preBlock struct { 13 | base 14 | 15 | // A magic number CN nodes should exchange during Commit phase 16 | // and used to construct the final list of transactions for amevBlock. 17 | data uint32 18 | 19 | initialTransactions []dbft.Transaction[crypto.Uint256] 20 | } 21 | 22 | var _ dbft.PreBlock[crypto.Uint256] = new(preBlock) 23 | 24 | // NewPreBlock returns new preBlock. 25 | func NewPreBlock(timestamp uint64, index uint32, prevHash crypto.Uint256, nonce uint64, txHashes []crypto.Uint256) dbft.PreBlock[crypto.Uint256] { 26 | pre := new(preBlock) 27 | pre.base.Timestamp = uint32(timestamp / 1000000000) 28 | pre.base.Index = index 29 | 30 | // NextConsensus and Version information is not provided by dBFT context, 31 | // these are implementation-specific fields, and thus, should be managed outside the 32 | // dBFT library. For simulation simplicity, let's assume that these fields are filled 33 | // by every CN separately and is not verified. 34 | pre.base.NextConsensus = crypto.Uint160{1, 2, 3} 35 | pre.base.Version = 0 36 | 37 | pre.base.PrevHash = prevHash 38 | pre.base.ConsensusData = nonce 39 | 40 | // Canary default value. 41 | pre.data = 0xff 42 | 43 | if len(txHashes) != 0 { 44 | mt := merkle.NewMerkleTree(txHashes...) 45 | pre.base.MerkleRoot = mt.Root().Hash 46 | } 47 | return pre 48 | } 49 | 50 | func (pre *preBlock) Data() []byte { 51 | var res = make([]byte, 4) 52 | binary.BigEndian.PutUint32(res, pre.data) 53 | return res 54 | } 55 | 56 | func (pre *preBlock) SetData(_ dbft.PrivateKey) error { 57 | // Just an artificial rule for data construction, it can be anything, and in Neo X 58 | // it will be decrypted transactions fragments. 59 | pre.data = pre.base.Index 60 | return nil 61 | } 62 | 63 | func (pre *preBlock) Verify(_ dbft.PublicKey, data []byte) error { 64 | if len(data) != 4 { 65 | return errors.New("invalid data len") 66 | } 67 | if binary.BigEndian.Uint32(data) != pre.base.Index { // Just an artificial verification rule, and for NeoX it should be decrypted transactions fragments verification. 68 | return errors.New("invalid data") 69 | } 70 | return nil 71 | } 72 | 73 | func (pre *preBlock) Transactions() []dbft.Transaction[crypto.Uint256] { 74 | return pre.initialTransactions 75 | } 76 | 77 | func (pre *preBlock) SetTransactions(txs []dbft.Transaction[crypto.Uint256]) { 78 | pre.initialTransactions = txs 79 | } 80 | -------------------------------------------------------------------------------- /internal/consensus/amev_preCommit.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/gob" 6 | 7 | "github.com/nspcc-dev/dbft" 8 | ) 9 | 10 | type ( 11 | // preCommit implements dbft.PreCommit. 12 | preCommit struct { 13 | magic uint32 // some magic data CN have to exchange to properly construct final amevBlock. 14 | } 15 | // preCommitAux is an auxiliary structure for preCommit encoding. 16 | preCommitAux struct { 17 | Magic uint32 18 | } 19 | ) 20 | 21 | var _ dbft.PreCommit = (*preCommit)(nil) 22 | 23 | // EncodeBinary implements Serializable interface. 24 | func (c preCommit) EncodeBinary(w *gob.Encoder) error { 25 | return w.Encode(preCommitAux{ 26 | Magic: c.magic, 27 | }) 28 | } 29 | 30 | // DecodeBinary implements Serializable interface. 31 | func (c *preCommit) DecodeBinary(r *gob.Decoder) error { 32 | aux := new(preCommitAux) 33 | if err := r.Decode(aux); err != nil { 34 | return err 35 | } 36 | c.magic = aux.Magic 37 | return nil 38 | } 39 | 40 | // Data implements PreCommit interface. 41 | func (c preCommit) Data() []byte { 42 | res := make([]byte, 4) 43 | binary.BigEndian.PutUint32(res, c.magic) 44 | return res 45 | } 46 | -------------------------------------------------------------------------------- /internal/consensus/block.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | 7 | "github.com/nspcc-dev/dbft" 8 | "github.com/nspcc-dev/dbft/internal/crypto" 9 | "github.com/nspcc-dev/dbft/internal/merkle" 10 | ) 11 | 12 | type ( 13 | // base is a structure containing all 14 | // hashable and signable fields of the block. 15 | base struct { 16 | ConsensusData uint64 17 | Index uint32 18 | Timestamp uint32 19 | Version uint32 20 | MerkleRoot crypto.Uint256 21 | PrevHash crypto.Uint256 22 | NextConsensus crypto.Uint160 23 | } 24 | 25 | neoBlock struct { 26 | base 27 | 28 | transactions []dbft.Transaction[crypto.Uint256] 29 | signature []byte 30 | hash *crypto.Uint256 31 | } 32 | 33 | // signable is an interface used within consensus package to abstract private key 34 | // functionality. This interface is used instead of direct structure usage to be 35 | // able to mock private key implementation in unit tests. 36 | signable interface { 37 | Sign([]byte) ([]byte, error) 38 | } 39 | ) 40 | 41 | var _ dbft.Block[crypto.Uint256] = new(neoBlock) 42 | 43 | // PrevHash implements Block interface. 44 | func (b *neoBlock) PrevHash() crypto.Uint256 { 45 | return b.base.PrevHash 46 | } 47 | 48 | // Index implements Block interface. 49 | func (b *neoBlock) Index() uint32 { 50 | return b.base.Index 51 | } 52 | 53 | // MerkleRoot implements Block interface. 54 | func (b *neoBlock) MerkleRoot() crypto.Uint256 { 55 | return b.base.MerkleRoot 56 | } 57 | 58 | // Transactions implements Block interface. 59 | func (b *neoBlock) Transactions() []dbft.Transaction[crypto.Uint256] { 60 | return b.transactions 61 | } 62 | 63 | // SetTransactions implements Block interface. 64 | func (b *neoBlock) SetTransactions(txx []dbft.Transaction[crypto.Uint256]) { 65 | b.transactions = txx 66 | } 67 | 68 | // NewBlock returns new block. 69 | func NewBlock(timestamp uint64, index uint32, prevHash crypto.Uint256, nonce uint64, txHashes []crypto.Uint256) dbft.Block[crypto.Uint256] { 70 | block := new(neoBlock) 71 | block.base.Timestamp = uint32(timestamp / 1000000000) 72 | block.base.Index = index 73 | 74 | // NextConsensus and Version information is not provided by dBFT context, 75 | // these are implementation-specific fields, and thus, should be managed outside the 76 | // dBFT library. For simulation simplicity, let's assume that these fields are filled 77 | // by every CN separately and is not verified. 78 | block.base.NextConsensus = crypto.Uint160{1, 2, 3} 79 | block.base.Version = 0 80 | 81 | block.base.PrevHash = prevHash 82 | block.base.ConsensusData = nonce 83 | 84 | if len(txHashes) != 0 { 85 | mt := merkle.NewMerkleTree(txHashes...) 86 | block.base.MerkleRoot = mt.Root().Hash 87 | } 88 | return block 89 | } 90 | 91 | // Signature implements Block interface. 92 | func (b *neoBlock) Signature() []byte { 93 | return b.signature 94 | } 95 | 96 | // GetHashData returns data for hashing and signing. 97 | // It must be an injection of the set of blocks to the set 98 | // of byte slices, i.e: 99 | // 1. It must have only one valid result for one block. 100 | // 2. Two different blocks must have different hash data. 101 | func (b *neoBlock) GetHashData() []byte { 102 | buf := bytes.Buffer{} 103 | w := gob.NewEncoder(&buf) 104 | _ = b.EncodeBinary(w) 105 | 106 | return buf.Bytes() 107 | } 108 | 109 | // Sign implements Block interface. 110 | func (b *neoBlock) Sign(key dbft.PrivateKey) error { 111 | data := b.GetHashData() 112 | 113 | sign, err := key.(signable).Sign(data) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | b.signature = sign 119 | 120 | return nil 121 | } 122 | 123 | // Verify implements Block interface. 124 | func (b *neoBlock) Verify(pub dbft.PublicKey, sign []byte) error { 125 | data := b.GetHashData() 126 | return pub.(*crypto.ECDSAPub).Verify(data, sign) 127 | } 128 | 129 | // Hash implements Block interface. 130 | func (b *neoBlock) Hash() (h crypto.Uint256) { 131 | if b.hash != nil { 132 | return *b.hash 133 | } else if b.transactions == nil { 134 | return 135 | } 136 | 137 | hash := crypto.Hash256(b.GetHashData()) 138 | b.hash = &hash 139 | 140 | return hash 141 | } 142 | 143 | // EncodeBinary implements Serializable interface. 144 | func (b base) EncodeBinary(w *gob.Encoder) error { 145 | return w.Encode(b) 146 | } 147 | 148 | // DecodeBinary implements Serializable interface. 149 | func (b *base) DecodeBinary(r *gob.Decoder) error { 150 | return r.Decode(b) 151 | } 152 | -------------------------------------------------------------------------------- /internal/consensus/block_test.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/binary" 7 | "encoding/gob" 8 | "errors" 9 | "testing" 10 | 11 | "github.com/nspcc-dev/dbft" 12 | "github.com/nspcc-dev/dbft/internal/crypto" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestNeoBlock_Setters(t *testing.T) { 18 | b := new(neoBlock) 19 | 20 | require.Equal(t, crypto.Uint256{}, b.Hash()) 21 | 22 | txs := []dbft.Transaction[crypto.Uint256]{testTx(1), testTx(2)} 23 | b.SetTransactions(txs) 24 | assert.Equal(t, txs, b.Transactions()) 25 | 26 | b.base.PrevHash = crypto.Uint256{3, 7} 27 | assert.Equal(t, crypto.Uint256{3, 7}, b.PrevHash()) 28 | 29 | b.base.MerkleRoot = crypto.Uint256{13} 30 | assert.Equal(t, crypto.Uint256{13}, b.MerkleRoot()) 31 | 32 | b.base.Index = 100 33 | assert.EqualValues(t, 100, b.Index()) 34 | 35 | t.Run("marshal block", func(t *testing.T) { 36 | buf := bytes.Buffer{} 37 | w := gob.NewEncoder(&buf) 38 | err := b.EncodeBinary(w) 39 | require.NoError(t, err) 40 | 41 | r := gob.NewDecoder(bytes.NewReader(buf.Bytes())) 42 | newb := new(neoBlock) 43 | err = newb.DecodeBinary(r) 44 | require.NoError(t, err) 45 | require.Equal(t, b.base, newb.base) 46 | }) 47 | 48 | t.Run("hash does not change after signature", func(t *testing.T) { 49 | priv, pub := crypto.Generate(rand.Reader) 50 | require.NotNil(t, priv) 51 | require.NotNil(t, pub) 52 | 53 | h := b.Hash() 54 | require.NoError(t, b.Sign(priv)) 55 | require.NotEmpty(t, b.Signature()) 56 | require.Equal(t, h, b.Hash()) 57 | require.NoError(t, b.Verify(pub, b.Signature())) 58 | }) 59 | 60 | t.Run("sign with invalid private key", func(t *testing.T) { 61 | require.Error(t, b.Sign(testKey{})) 62 | }) 63 | } 64 | 65 | type testKey struct{} 66 | 67 | var _ signable = testKey{} 68 | 69 | func (t testKey) MarshalBinary() ([]byte, error) { return []byte{}, nil } 70 | func (t testKey) UnmarshalBinary([]byte) error { return nil } 71 | func (t testKey) Sign([]byte) ([]byte, error) { 72 | return nil, errors.New("can't sign") 73 | } 74 | 75 | type testTx uint64 76 | 77 | func (tx testTx) Hash() (h crypto.Uint256) { 78 | binary.LittleEndian.PutUint64(h[:], uint64(tx)) 79 | return 80 | } 81 | -------------------------------------------------------------------------------- /internal/consensus/change_view.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/gob" 5 | 6 | "github.com/nspcc-dev/dbft" 7 | ) 8 | 9 | type ( 10 | changeView struct { 11 | newViewNumber byte 12 | timestamp uint32 13 | } 14 | // changeViewAux is an auxiliary structure for changeView encoding. 15 | changeViewAux struct { 16 | Timestamp uint32 17 | } 18 | ) 19 | 20 | var _ dbft.ChangeView = (*changeView)(nil) 21 | 22 | // EncodeBinary implements Serializable interface. 23 | func (c changeView) EncodeBinary(w *gob.Encoder) error { 24 | return w.Encode(&changeViewAux{ 25 | Timestamp: c.timestamp, 26 | }) 27 | } 28 | 29 | // DecodeBinary implements Serializable interface. 30 | func (c *changeView) DecodeBinary(r *gob.Decoder) error { 31 | aux := new(changeViewAux) 32 | if err := r.Decode(aux); err != nil { 33 | return err 34 | } 35 | c.timestamp = aux.Timestamp 36 | return nil 37 | } 38 | 39 | // NewViewNumber implements ChangeView interface. 40 | func (c changeView) NewViewNumber() byte { 41 | return c.newViewNumber 42 | } 43 | 44 | // Reason implements ChangeView interface. 45 | func (c changeView) Reason() dbft.ChangeViewReason { 46 | return dbft.CVUnknown 47 | } 48 | -------------------------------------------------------------------------------- /internal/consensus/commit.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/gob" 5 | 6 | "github.com/nspcc-dev/dbft" 7 | ) 8 | 9 | type ( 10 | commit struct { 11 | signature [signatureSize]byte 12 | } 13 | // commitAux is an auxiliary structure for commit encoding. 14 | commitAux struct { 15 | Signature [signatureSize]byte 16 | } 17 | ) 18 | 19 | const signatureSize = 64 20 | 21 | var _ dbft.Commit = (*commit)(nil) 22 | 23 | // EncodeBinary implements Serializable interface. 24 | func (c commit) EncodeBinary(w *gob.Encoder) error { 25 | return w.Encode(commitAux{ 26 | Signature: c.signature, 27 | }) 28 | } 29 | 30 | // DecodeBinary implements Serializable interface. 31 | func (c *commit) DecodeBinary(r *gob.Decoder) error { 32 | aux := new(commitAux) 33 | if err := r.Decode(aux); err != nil { 34 | return err 35 | } 36 | c.signature = aux.Signature 37 | return nil 38 | } 39 | 40 | // Signature implements Commit interface. 41 | func (c commit) Signature() []byte { 42 | return c.signature[:] 43 | } 44 | -------------------------------------------------------------------------------- /internal/consensus/compact.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/gob" 5 | ) 6 | 7 | type ( 8 | changeViewCompact struct { 9 | ValidatorIndex uint16 10 | OriginalViewNumber byte 11 | Timestamp uint32 12 | } 13 | 14 | preCommitCompact struct { 15 | ViewNumber byte 16 | ValidatorIndex uint16 17 | Data []byte 18 | } 19 | 20 | commitCompact struct { 21 | ViewNumber byte 22 | ValidatorIndex uint16 23 | Signature [signatureSize]byte 24 | } 25 | 26 | preparationCompact struct { 27 | ValidatorIndex uint16 28 | } 29 | ) 30 | 31 | // EncodeBinary implements Serializable interface. 32 | func (p changeViewCompact) EncodeBinary(w *gob.Encoder) error { 33 | return w.Encode(p) 34 | } 35 | 36 | // DecodeBinary implements Serializable interface. 37 | func (p *changeViewCompact) DecodeBinary(r *gob.Decoder) error { 38 | return r.Decode(p) 39 | } 40 | 41 | // EncodeBinary implements Serializable interface. 42 | func (p preCommitCompact) EncodeBinary(w *gob.Encoder) error { 43 | return w.Encode(p) 44 | } 45 | 46 | // DecodeBinary implements Serializable interface. 47 | func (p *preCommitCompact) DecodeBinary(r *gob.Decoder) error { 48 | return r.Decode(p) 49 | } 50 | 51 | // EncodeBinary implements Serializable interface. 52 | func (p commitCompact) EncodeBinary(w *gob.Encoder) error { 53 | return w.Encode(p) 54 | } 55 | 56 | // DecodeBinary implements Serializable interface. 57 | func (p *commitCompact) DecodeBinary(r *gob.Decoder) error { 58 | return r.Decode(p) 59 | } 60 | 61 | // EncodeBinary implements Serializable interface. 62 | func (p preparationCompact) EncodeBinary(w *gob.Encoder) error { 63 | return w.Encode(p) 64 | } 65 | 66 | // DecodeBinary implements Serializable interface. 67 | func (p *preparationCompact) DecodeBinary(r *gob.Decoder) error { 68 | return r.Decode(p) 69 | } 70 | -------------------------------------------------------------------------------- /internal/consensus/consensus.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/nspcc-dev/dbft" 7 | "github.com/nspcc-dev/dbft/internal/crypto" 8 | "github.com/nspcc-dev/dbft/timer" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func New(logger *zap.Logger, key dbft.PrivateKey, pub dbft.PublicKey, 13 | getTx func(uint256 crypto.Uint256) dbft.Transaction[crypto.Uint256], 14 | getVerified func() []dbft.Transaction[crypto.Uint256], 15 | broadcast func(dbft.ConsensusPayload[crypto.Uint256]), 16 | processBlock func(dbft.Block[crypto.Uint256]) error, 17 | currentHeight func() uint32, 18 | currentBlockHash func() crypto.Uint256, 19 | getValidators func(...dbft.Transaction[crypto.Uint256]) []dbft.PublicKey, 20 | verifyPayload func(consensusPayload dbft.ConsensusPayload[crypto.Uint256]) error) (*dbft.DBFT[crypto.Uint256], error) { 21 | return dbft.New[crypto.Uint256]( 22 | dbft.WithTimer[crypto.Uint256](timer.New()), 23 | dbft.WithLogger[crypto.Uint256](logger), 24 | dbft.WithTimePerBlock[crypto.Uint256](func() time.Duration { 25 | return time.Second * 5 26 | }), 27 | dbft.WithGetKeyPair[crypto.Uint256](func(pubs []dbft.PublicKey) (int, dbft.PrivateKey, dbft.PublicKey) { 28 | for i := range pubs { 29 | if pub.(*crypto.ECDSAPub).Equals(pubs[i]) { 30 | return i, key, pub 31 | } 32 | } 33 | 34 | return -1, nil, nil 35 | }), 36 | dbft.WithGetTx[crypto.Uint256](getTx), 37 | dbft.WithGetVerified[crypto.Uint256](getVerified), 38 | dbft.WithBroadcast[crypto.Uint256](broadcast), 39 | dbft.WithProcessBlock[crypto.Uint256](processBlock), 40 | dbft.WithCurrentHeight[crypto.Uint256](currentHeight), 41 | dbft.WithCurrentBlockHash[crypto.Uint256](currentBlockHash), 42 | dbft.WithGetValidators[crypto.Uint256](getValidators), 43 | dbft.WithVerifyPrepareRequest[crypto.Uint256](verifyPayload), 44 | dbft.WithVerifyPrepareResponse[crypto.Uint256](verifyPayload), 45 | dbft.WithVerifyCommit[crypto.Uint256](verifyPayload), 46 | 47 | dbft.WithNewBlockFromContext[crypto.Uint256](newBlockFromContext), 48 | dbft.WithNewConsensusPayload[crypto.Uint256](defaultNewConsensusPayload), 49 | dbft.WithNewPrepareRequest[crypto.Uint256](NewPrepareRequest), 50 | dbft.WithNewPrepareResponse[crypto.Uint256](NewPrepareResponse), 51 | dbft.WithNewChangeView[crypto.Uint256](NewChangeView), 52 | dbft.WithNewCommit[crypto.Uint256](NewCommit), 53 | dbft.WithNewRecoveryMessage[crypto.Uint256](func() dbft.RecoveryMessage[crypto.Uint256] { 54 | return NewRecoveryMessage(nil) 55 | }), 56 | dbft.WithNewRecoveryRequest[crypto.Uint256](NewRecoveryRequest), 57 | ) 58 | } 59 | 60 | func newBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { 61 | if ctx.TransactionHashes == nil { 62 | return nil 63 | } 64 | block := NewBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, ctx.TransactionHashes) 65 | return block 66 | } 67 | 68 | // defaultNewConsensusPayload is default function for creating 69 | // consensus payload of specific type. 70 | func defaultNewConsensusPayload(c *dbft.Context[crypto.Uint256], t dbft.MessageType, msg any) dbft.ConsensusPayload[crypto.Uint256] { 71 | return NewConsensusPayload(t, c.BlockIndex, uint16(c.MyIndex), c.ViewNumber, msg) 72 | } 73 | -------------------------------------------------------------------------------- /internal/consensus/consensus_message.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | 8 | "github.com/nspcc-dev/dbft" 9 | "github.com/nspcc-dev/dbft/internal/crypto" 10 | ) 11 | 12 | type ( 13 | // Serializable is an interface for serializing consensus messages. 14 | Serializable interface { 15 | EncodeBinary(encoder *gob.Encoder) error 16 | DecodeBinary(decoder *gob.Decoder) error 17 | } 18 | 19 | message struct { 20 | cmType dbft.MessageType 21 | viewNumber byte 22 | 23 | payload any 24 | } 25 | 26 | // messageAux is an auxiliary structure for message marshalling. 27 | messageAux struct { 28 | CMType byte 29 | ViewNumber byte 30 | Payload []byte 31 | } 32 | ) 33 | 34 | var _ dbft.ConsensusMessage[crypto.Uint256] = (*message)(nil) 35 | 36 | // EncodeBinary implements Serializable interface. 37 | func (m message) EncodeBinary(w *gob.Encoder) error { 38 | ww := bytes.Buffer{} 39 | enc := gob.NewEncoder(&ww) 40 | if err := m.payload.(Serializable).EncodeBinary(enc); err != nil { 41 | return err 42 | } 43 | return w.Encode(&messageAux{ 44 | CMType: byte(m.cmType), 45 | ViewNumber: m.viewNumber, 46 | Payload: ww.Bytes(), 47 | }) 48 | } 49 | 50 | // DecodeBinary implements Serializable interface. 51 | func (m *message) DecodeBinary(r *gob.Decoder) error { 52 | aux := new(messageAux) 53 | if err := r.Decode(aux); err != nil { 54 | return err 55 | } 56 | m.cmType = dbft.MessageType(aux.CMType) 57 | m.viewNumber = aux.ViewNumber 58 | 59 | switch m.cmType { 60 | case dbft.ChangeViewType: 61 | cv := new(changeView) 62 | cv.newViewNumber = m.viewNumber + 1 63 | m.payload = cv 64 | case dbft.PrepareRequestType: 65 | m.payload = new(prepareRequest) 66 | case dbft.PrepareResponseType: 67 | m.payload = new(prepareResponse) 68 | case dbft.CommitType: 69 | m.payload = new(commit) 70 | case dbft.RecoveryRequestType: 71 | m.payload = new(recoveryRequest) 72 | case dbft.RecoveryMessageType: 73 | m.payload = new(recoveryMessage) 74 | default: 75 | return fmt.Errorf("invalid type: 0x%02x", byte(m.cmType)) 76 | } 77 | 78 | rr := bytes.NewReader(aux.Payload) 79 | dec := gob.NewDecoder(rr) 80 | return m.payload.(Serializable).DecodeBinary(dec) 81 | } 82 | 83 | func (m message) GetChangeView() dbft.ChangeView { return m.payload.(dbft.ChangeView) } 84 | func (m message) GetPrepareRequest() dbft.PrepareRequest[crypto.Uint256] { 85 | return m.payload.(dbft.PrepareRequest[crypto.Uint256]) 86 | } 87 | func (m message) GetPrepareResponse() dbft.PrepareResponse[crypto.Uint256] { 88 | return m.payload.(dbft.PrepareResponse[crypto.Uint256]) 89 | } 90 | func (m message) GetCommit() dbft.Commit { return m.payload.(dbft.Commit) } 91 | func (m message) GetPreCommit() dbft.PreCommit { return m.payload.(dbft.PreCommit) } 92 | func (m message) GetRecoveryRequest() dbft.RecoveryRequest { return m.payload.(dbft.RecoveryRequest) } 93 | func (m message) GetRecoveryMessage() dbft.RecoveryMessage[crypto.Uint256] { 94 | return m.payload.(dbft.RecoveryMessage[crypto.Uint256]) 95 | } 96 | 97 | // ViewNumber implements ConsensusMessage interface. 98 | func (m message) ViewNumber() byte { 99 | return m.viewNumber 100 | } 101 | 102 | // Type implements ConsensusMessage interface. 103 | func (m message) Type() dbft.MessageType { 104 | return m.cmType 105 | } 106 | 107 | // Payload implements ConsensusMessage interface. 108 | func (m message) Payload() any { 109 | return m.payload 110 | } 111 | -------------------------------------------------------------------------------- /internal/consensus/constructors.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/nspcc-dev/dbft" 7 | "github.com/nspcc-dev/dbft/internal/crypto" 8 | ) 9 | 10 | // NewConsensusPayload returns minimal ConsensusPayload implementation. 11 | func NewConsensusPayload(t dbft.MessageType, height uint32, validatorIndex uint16, viewNumber byte, consensusMessage any) dbft.ConsensusPayload[crypto.Uint256] { 12 | return &Payload{ 13 | message: message{ 14 | cmType: t, 15 | viewNumber: viewNumber, 16 | payload: consensusMessage, 17 | }, 18 | validatorIndex: validatorIndex, 19 | height: height, 20 | } 21 | } 22 | 23 | // NewPrepareRequest returns minimal prepareRequest implementation. 24 | func NewPrepareRequest(ts uint64, nonce uint64, transactionsHashes []crypto.Uint256) dbft.PrepareRequest[crypto.Uint256] { 25 | return &prepareRequest{ 26 | transactionHashes: transactionsHashes, 27 | nonce: nonce, 28 | timestamp: nanoSecToSec(ts), 29 | } 30 | } 31 | 32 | // NewPrepareResponse returns minimal PrepareResponse implementation. 33 | func NewPrepareResponse(preparationHash crypto.Uint256) dbft.PrepareResponse[crypto.Uint256] { 34 | return &prepareResponse{ 35 | preparationHash: preparationHash, 36 | } 37 | } 38 | 39 | // NewChangeView returns minimal ChangeView implementation. 40 | func NewChangeView(newViewNumber byte, _ dbft.ChangeViewReason, ts uint64) dbft.ChangeView { 41 | return &changeView{ 42 | newViewNumber: newViewNumber, 43 | timestamp: nanoSecToSec(ts), 44 | } 45 | } 46 | 47 | // NewCommit returns minimal Commit implementation. 48 | func NewCommit(signature []byte) dbft.Commit { 49 | c := new(commit) 50 | copy(c.signature[:], signature) 51 | return c 52 | } 53 | 54 | // NewPreCommit returns minimal dbft.PreCommit implementation. 55 | func NewPreCommit(data []byte) dbft.PreCommit { 56 | c := new(preCommit) 57 | c.magic = binary.BigEndian.Uint32(data) 58 | return c 59 | } 60 | 61 | // NewAMEVCommit returns minimal dbft.Commit implementation for anti-MEV extension. 62 | func NewAMEVCommit(data []byte) dbft.Commit { 63 | c := new(amevCommit) 64 | copy(c.data[:], data) 65 | return c 66 | } 67 | 68 | // NewRecoveryRequest returns minimal RecoveryRequest implementation. 69 | func NewRecoveryRequest(ts uint64) dbft.RecoveryRequest { 70 | return &recoveryRequest{ 71 | timestamp: nanoSecToSec(ts), 72 | } 73 | } 74 | 75 | // NewRecoveryMessage returns minimal RecoveryMessage implementation. 76 | func NewRecoveryMessage(preparationHash *crypto.Uint256) dbft.RecoveryMessage[crypto.Uint256] { 77 | return &recoveryMessage{ 78 | preparationHash: preparationHash, 79 | preparationPayloads: make([]preparationCompact, 0), 80 | commitPayloads: make([]commitCompact, 0), 81 | changeViewPayloads: make([]changeViewCompact, 0), 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/consensus/helpers.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | func secToNanoSec(s uint32) uint64 { 4 | return uint64(s) * 1000000000 5 | } 6 | 7 | func nanoSecToSec(ns uint64) uint32 { 8 | return uint32(ns / 1000000000) 9 | } 10 | -------------------------------------------------------------------------------- /internal/consensus/message.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | 7 | "github.com/nspcc-dev/dbft" 8 | "github.com/nspcc-dev/dbft/internal/crypto" 9 | ) 10 | 11 | type ( 12 | // Payload represents minimal payload containing all necessary fields. 13 | Payload struct { 14 | message 15 | 16 | version uint32 17 | validatorIndex uint16 18 | prevHash crypto.Uint256 19 | height uint32 20 | 21 | hash *crypto.Uint256 22 | } 23 | 24 | // payloadAux is an auxiliary structure for Payload encoding. 25 | payloadAux struct { 26 | Version uint32 27 | ValidatorIndex uint16 28 | PrevHash crypto.Uint256 29 | Height uint32 30 | 31 | Data []byte 32 | } 33 | ) 34 | 35 | var _ dbft.ConsensusPayload[crypto.Uint256] = (*Payload)(nil) 36 | 37 | // EncodeBinary implements Serializable interface. 38 | func (p Payload) EncodeBinary(w *gob.Encoder) error { 39 | ww := bytes.Buffer{} 40 | enc := gob.NewEncoder(&ww) 41 | if err := p.message.EncodeBinary(enc); err != nil { 42 | return err 43 | } 44 | 45 | return w.Encode(&payloadAux{ 46 | Version: p.version, 47 | ValidatorIndex: p.validatorIndex, 48 | PrevHash: p.prevHash, 49 | Height: p.height, 50 | Data: ww.Bytes(), 51 | }) 52 | } 53 | 54 | // DecodeBinary implements Serializable interface. 55 | func (p *Payload) DecodeBinary(r *gob.Decoder) error { 56 | aux := new(payloadAux) 57 | if err := r.Decode(aux); err != nil { 58 | return err 59 | } 60 | 61 | p.version = aux.Version 62 | p.prevHash = aux.PrevHash 63 | p.height = aux.Height 64 | p.validatorIndex = aux.ValidatorIndex 65 | 66 | rr := bytes.NewReader(aux.Data) 67 | dec := gob.NewDecoder(rr) 68 | return p.message.DecodeBinary(dec) 69 | } 70 | 71 | // MarshalUnsigned implements ConsensusPayload interface. 72 | func (p Payload) MarshalUnsigned() []byte { 73 | buf := bytes.Buffer{} 74 | enc := gob.NewEncoder(&buf) 75 | _ = p.EncodeBinary(enc) 76 | 77 | return buf.Bytes() 78 | } 79 | 80 | // UnmarshalUnsigned implements ConsensusPayload interface. 81 | func (p *Payload) UnmarshalUnsigned(data []byte) error { 82 | r := bytes.NewReader(data) 83 | dec := gob.NewDecoder(r) 84 | return p.DecodeBinary(dec) 85 | } 86 | 87 | // Hash implements ConsensusPayload interface. 88 | func (p *Payload) Hash() crypto.Uint256 { 89 | if p.hash != nil { 90 | return *p.hash 91 | } 92 | 93 | data := p.MarshalUnsigned() 94 | 95 | return crypto.Hash256(data) 96 | } 97 | 98 | // Version implements ConsensusPayload interface. 99 | func (p Payload) Version() uint32 { 100 | return p.version 101 | } 102 | 103 | // ValidatorIndex implements ConsensusPayload interface. 104 | func (p Payload) ValidatorIndex() uint16 { 105 | return p.validatorIndex 106 | } 107 | 108 | // SetValidatorIndex implements ConsensusPayload interface. 109 | func (p *Payload) SetValidatorIndex(i uint16) { 110 | p.validatorIndex = i 111 | } 112 | 113 | // PrevHash implements ConsensusPayload interface. 114 | func (p Payload) PrevHash() crypto.Uint256 { 115 | return p.prevHash 116 | } 117 | 118 | // Height implements ConsensusPayload interface. 119 | func (p Payload) Height() uint32 { 120 | return p.height 121 | } 122 | -------------------------------------------------------------------------------- /internal/consensus/message_test.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/gob" 7 | "testing" 8 | 9 | "github.com/nspcc-dev/dbft" 10 | "github.com/nspcc-dev/dbft/internal/crypto" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestPayload_EncodeDecode(t *testing.T) { 16 | generateMessage := func(typ dbft.MessageType, payload any) *Payload { 17 | return NewConsensusPayload(typ, 77, 10, 3, payload).(*Payload) 18 | } 19 | 20 | t.Run("PrepareRequest", func(t *testing.T) { 21 | m := generateMessage(dbft.PrepareRequestType, &prepareRequest{ 22 | nonce: 123, 23 | timestamp: 345, 24 | transactionHashes: []crypto.Uint256{ 25 | {1, 2, 3}, 26 | {5, 6, 7}, 27 | }, 28 | }) 29 | 30 | testEncodeDecode(t, m, new(Payload)) 31 | testMarshalUnmarshal(t, m, new(Payload)) 32 | }) 33 | 34 | t.Run("PrepareResponse", func(t *testing.T) { 35 | m := generateMessage(dbft.PrepareResponseType, &prepareResponse{ 36 | preparationHash: crypto.Uint256{3}, 37 | }) 38 | 39 | testEncodeDecode(t, m, new(Payload)) 40 | testMarshalUnmarshal(t, m, new(Payload)) 41 | }) 42 | 43 | t.Run("Commit", func(t *testing.T) { 44 | var cc commit 45 | fillRandom(t, cc.signature[:]) 46 | m := generateMessage(dbft.CommitType, &cc) 47 | 48 | testEncodeDecode(t, m, new(Payload)) 49 | testMarshalUnmarshal(t, m, new(Payload)) 50 | }) 51 | 52 | t.Run("ChangeView", func(t *testing.T) { 53 | m := generateMessage(dbft.ChangeViewType, &changeView{ 54 | timestamp: 12345, 55 | newViewNumber: 4, 56 | }) 57 | 58 | testEncodeDecode(t, m, new(Payload)) 59 | testMarshalUnmarshal(t, m, new(Payload)) 60 | }) 61 | 62 | t.Run("RecoveryMessage", func(t *testing.T) { 63 | m := generateMessage(dbft.RecoveryMessageType, &recoveryMessage{ 64 | changeViewPayloads: []changeViewCompact{ 65 | { 66 | Timestamp: 123, 67 | ValidatorIndex: 1, 68 | OriginalViewNumber: 3, 69 | }, 70 | }, 71 | commitPayloads: []commitCompact{}, 72 | preparationPayloads: []preparationCompact{ 73 | 1: {ValidatorIndex: 1}, 74 | 3: {ValidatorIndex: 3}, 75 | 4: {ValidatorIndex: 4}, 76 | }, 77 | prepareRequest: &prepareRequest{ 78 | nonce: 123, 79 | timestamp: 345, 80 | transactionHashes: []crypto.Uint256{ 81 | {1, 2, 3}, 82 | {5, 6, 7}, 83 | }, 84 | }, 85 | }) 86 | 87 | testEncodeDecode(t, m, new(Payload)) 88 | testMarshalUnmarshal(t, m, new(Payload)) 89 | }) 90 | 91 | t.Run("RecoveryRequest", func(t *testing.T) { 92 | m := generateMessage(dbft.RecoveryRequestType, &recoveryRequest{ 93 | timestamp: 17334, 94 | }) 95 | 96 | testEncodeDecode(t, m, new(Payload)) 97 | testMarshalUnmarshal(t, m, new(Payload)) 98 | }) 99 | } 100 | 101 | func TestRecoveryMessage_NoPayloads(t *testing.T) { 102 | m := NewConsensusPayload(dbft.RecoveryRequestType, 77, 0, 3, &recoveryMessage{}).(*Payload) 103 | 104 | validators := make([]dbft.PublicKey, 1) 105 | _, validators[0] = crypto.Generate(rand.Reader) 106 | 107 | rec := m.GetRecoveryMessage() 108 | require.NotNil(t, rec) 109 | 110 | var p dbft.ConsensusPayload[crypto.Uint256] 111 | require.NotPanics(t, func() { p = rec.GetPrepareRequest(p, validators, 0) }) 112 | require.Nil(t, p) 113 | 114 | var ps []dbft.ConsensusPayload[crypto.Uint256] 115 | require.NotPanics(t, func() { ps = rec.GetPrepareResponses(p, validators) }) 116 | require.Len(t, ps, 0) 117 | 118 | require.NotPanics(t, func() { ps = rec.GetCommits(p, validators) }) 119 | require.Len(t, ps, 0) 120 | 121 | require.NotPanics(t, func() { ps = rec.GetChangeViews(p, validators) }) 122 | require.Len(t, ps, 0) 123 | } 124 | 125 | func TestCompact_EncodeDecode(t *testing.T) { 126 | t.Run("ChangeView", func(t *testing.T) { 127 | p := &changeViewCompact{ 128 | ValidatorIndex: 10, 129 | OriginalViewNumber: 31, 130 | Timestamp: 98765, 131 | } 132 | 133 | testEncodeDecode(t, p, new(changeViewCompact)) 134 | }) 135 | 136 | t.Run("Preparation", func(t *testing.T) { 137 | p := &preparationCompact{ 138 | ValidatorIndex: 10, 139 | } 140 | 141 | testEncodeDecode(t, p, new(preparationCompact)) 142 | }) 143 | 144 | t.Run("Commit", func(t *testing.T) { 145 | p := &commitCompact{ 146 | ValidatorIndex: 10, 147 | ViewNumber: 77, 148 | } 149 | fillRandom(t, p.Signature[:]) 150 | 151 | testEncodeDecode(t, p, new(commitCompact)) 152 | }) 153 | } 154 | 155 | func TestPayload_Setters(t *testing.T) { 156 | t.Run("ChangeView", func(t *testing.T) { 157 | cv := NewChangeView(4, 0, secToNanoSec(1234)) 158 | 159 | assert.EqualValues(t, 4, cv.NewViewNumber()) 160 | }) 161 | 162 | t.Run("RecoveryRequest", func(t *testing.T) { 163 | r := NewRecoveryRequest(secToNanoSec(321)) 164 | 165 | require.EqualValues(t, secToNanoSec(321), r.Timestamp()) 166 | }) 167 | 168 | t.Run("RecoveryMessage", func(t *testing.T) { 169 | r := NewRecoveryMessage(&crypto.Uint256{1, 2, 3}) 170 | 171 | require.Equal(t, &crypto.Uint256{1, 2, 3}, r.PreparationHash()) 172 | }) 173 | } 174 | 175 | func TestMessageType_String(t *testing.T) { 176 | require.Equal(t, "ChangeView", dbft.ChangeViewType.String()) 177 | require.Equal(t, "PrepareRequest", dbft.PrepareRequestType.String()) 178 | require.Equal(t, "PrepareResponse", dbft.PrepareResponseType.String()) 179 | require.Equal(t, "Commit", dbft.CommitType.String()) 180 | require.Equal(t, "RecoveryRequest", dbft.RecoveryRequestType.String()) 181 | require.Equal(t, "RecoveryMessage", dbft.RecoveryMessageType.String()) 182 | } 183 | 184 | func testEncodeDecode(t *testing.T, expected, actual Serializable) { 185 | var buf bytes.Buffer 186 | w := gob.NewEncoder(&buf) 187 | err := expected.EncodeBinary(w) 188 | require.NoError(t, err) 189 | 190 | b := buf.Bytes() 191 | r := gob.NewDecoder(bytes.NewReader(b)) 192 | 193 | err = actual.DecodeBinary(r) 194 | require.NoError(t, err) 195 | require.Equal(t, expected, actual) 196 | } 197 | 198 | func testMarshalUnmarshal(t *testing.T, expected, actual *Payload) { 199 | data := expected.MarshalUnsigned() 200 | require.NoError(t, actual.UnmarshalUnsigned(data)) 201 | require.Equal(t, expected.Hash(), actual.Hash()) 202 | } 203 | 204 | func fillRandom(t *testing.T, arr []byte) { 205 | _, err := rand.Read(arr) 206 | require.NoError(t, err) 207 | } 208 | -------------------------------------------------------------------------------- /internal/consensus/prepare_request.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/gob" 5 | 6 | "github.com/nspcc-dev/dbft" 7 | "github.com/nspcc-dev/dbft/internal/crypto" 8 | ) 9 | 10 | type ( 11 | prepareRequest struct { 12 | transactionHashes []crypto.Uint256 13 | nonce uint64 14 | timestamp uint32 15 | } 16 | // prepareRequestAux is an auxiliary structure for prepareRequest encoding. 17 | prepareRequestAux struct { 18 | TransactionHashes []crypto.Uint256 19 | Nonce uint64 20 | Timestamp uint32 21 | } 22 | ) 23 | 24 | var _ dbft.PrepareRequest[crypto.Uint256] = (*prepareRequest)(nil) 25 | 26 | // EncodeBinary implements Serializable interface. 27 | func (p prepareRequest) EncodeBinary(w *gob.Encoder) error { 28 | return w.Encode(&prepareRequestAux{ 29 | TransactionHashes: p.transactionHashes, 30 | Nonce: p.nonce, 31 | Timestamp: p.timestamp, 32 | }) 33 | } 34 | 35 | // DecodeBinary implements Serializable interface. 36 | func (p *prepareRequest) DecodeBinary(r *gob.Decoder) error { 37 | aux := new(prepareRequestAux) 38 | if err := r.Decode(aux); err != nil { 39 | return err 40 | } 41 | 42 | p.timestamp = aux.Timestamp 43 | p.nonce = aux.Nonce 44 | p.transactionHashes = aux.TransactionHashes 45 | return nil 46 | } 47 | 48 | // Timestamp implements PrepareRequest interface. 49 | func (p prepareRequest) Timestamp() uint64 { 50 | return secToNanoSec(p.timestamp) 51 | } 52 | 53 | // Nonce implements PrepareRequest interface. 54 | func (p prepareRequest) Nonce() uint64 { 55 | return p.nonce 56 | } 57 | 58 | // TransactionHashes implements PrepareRequest interface. 59 | func (p prepareRequest) TransactionHashes() []crypto.Uint256 { 60 | return p.transactionHashes 61 | } 62 | -------------------------------------------------------------------------------- /internal/consensus/prepare_response.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/gob" 5 | 6 | "github.com/nspcc-dev/dbft" 7 | "github.com/nspcc-dev/dbft/internal/crypto" 8 | ) 9 | 10 | type ( 11 | prepareResponse struct { 12 | preparationHash crypto.Uint256 13 | } 14 | // prepareResponseAux is an auxiliary structure for prepareResponse encoding. 15 | prepareResponseAux struct { 16 | PreparationHash crypto.Uint256 17 | } 18 | ) 19 | 20 | var _ dbft.PrepareResponse[crypto.Uint256] = (*prepareResponse)(nil) 21 | 22 | // EncodeBinary implements Serializable interface. 23 | func (p prepareResponse) EncodeBinary(w *gob.Encoder) error { 24 | return w.Encode(prepareResponseAux{ 25 | PreparationHash: p.preparationHash, 26 | }) 27 | } 28 | 29 | // DecodeBinary implements Serializable interface. 30 | func (p *prepareResponse) DecodeBinary(r *gob.Decoder) error { 31 | aux := new(prepareResponseAux) 32 | if err := r.Decode(aux); err != nil { 33 | return err 34 | } 35 | 36 | p.preparationHash = aux.PreparationHash 37 | return nil 38 | } 39 | 40 | // PreparationHash implements PrepareResponse interface. 41 | func (p *prepareResponse) PreparationHash() crypto.Uint256 { 42 | return p.preparationHash 43 | } 44 | -------------------------------------------------------------------------------- /internal/consensus/recovery_message.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/gob" 6 | "errors" 7 | 8 | "github.com/nspcc-dev/dbft" 9 | "github.com/nspcc-dev/dbft/internal/crypto" 10 | ) 11 | 12 | type ( 13 | recoveryMessage struct { 14 | preparationHash *crypto.Uint256 15 | preparationPayloads []preparationCompact 16 | preCommitPayloads []preCommitCompact 17 | commitPayloads []commitCompact 18 | changeViewPayloads []changeViewCompact 19 | prepareRequest dbft.PrepareRequest[crypto.Uint256] 20 | } 21 | // recoveryMessageAux is an auxiliary structure for recoveryMessage encoding. 22 | recoveryMessageAux struct { 23 | PreparationPayloads []preparationCompact 24 | PreCommitPayloads []preCommitCompact 25 | CommitPayloads []commitCompact 26 | ChangeViewPayloads []changeViewCompact 27 | } 28 | ) 29 | 30 | var _ dbft.RecoveryMessage[crypto.Uint256] = (*recoveryMessage)(nil) 31 | 32 | // PreparationHash implements RecoveryMessage interface. 33 | func (m *recoveryMessage) PreparationHash() *crypto.Uint256 { 34 | return m.preparationHash 35 | } 36 | 37 | // AddPayload implements RecoveryMessage interface. 38 | func (m *recoveryMessage) AddPayload(p dbft.ConsensusPayload[crypto.Uint256]) { 39 | switch p.Type() { 40 | case dbft.PrepareRequestType: 41 | m.prepareRequest = p.GetPrepareRequest() 42 | prepHash := p.Hash() 43 | m.preparationHash = &prepHash 44 | case dbft.PrepareResponseType: 45 | m.preparationPayloads = append(m.preparationPayloads, preparationCompact{ 46 | ValidatorIndex: p.ValidatorIndex(), 47 | }) 48 | case dbft.ChangeViewType: 49 | m.changeViewPayloads = append(m.changeViewPayloads, changeViewCompact{ 50 | ValidatorIndex: p.ValidatorIndex(), 51 | OriginalViewNumber: p.ViewNumber(), 52 | Timestamp: 0, 53 | }) 54 | case dbft.PreCommitType: 55 | pcc := preCommitCompact{ 56 | ViewNumber: p.ViewNumber(), 57 | ValidatorIndex: p.ValidatorIndex(), 58 | Data: p.GetPreCommit().Data(), 59 | } 60 | m.preCommitPayloads = append(m.preCommitPayloads, pcc) 61 | case dbft.CommitType: 62 | cc := commitCompact{ 63 | ViewNumber: p.ViewNumber(), 64 | ValidatorIndex: p.ValidatorIndex(), 65 | } 66 | copy(cc.Signature[:], p.GetCommit().Signature()) 67 | m.commitPayloads = append(m.commitPayloads, cc) 68 | default: 69 | // Other types (recoveries) can't be packed into recovery. 70 | } 71 | } 72 | 73 | func fromPayload(t dbft.MessageType, recovery dbft.ConsensusPayload[crypto.Uint256], p Serializable) *Payload { 74 | return &Payload{ 75 | message: message{ 76 | cmType: t, 77 | viewNumber: recovery.ViewNumber(), 78 | payload: p, 79 | }, 80 | height: recovery.Height(), 81 | } 82 | } 83 | 84 | // GetPrepareRequest implements RecoveryMessage interface. 85 | func (m *recoveryMessage) GetPrepareRequest(p dbft.ConsensusPayload[crypto.Uint256], _ []dbft.PublicKey, ind uint16) dbft.ConsensusPayload[crypto.Uint256] { 86 | if m.prepareRequest == nil { 87 | return nil 88 | } 89 | 90 | req := fromPayload(dbft.PrepareRequestType, p, &prepareRequest{ 91 | // prepareRequest.Timestamp() here returns nanoseconds-precision value, so convert it to seconds again 92 | timestamp: nanoSecToSec(m.prepareRequest.Timestamp()), 93 | nonce: m.prepareRequest.Nonce(), 94 | transactionHashes: m.prepareRequest.TransactionHashes(), 95 | }) 96 | req.SetValidatorIndex(ind) 97 | 98 | return req 99 | } 100 | 101 | // GetPrepareResponses implements RecoveryMessage interface. 102 | func (m *recoveryMessage) GetPrepareResponses(p dbft.ConsensusPayload[crypto.Uint256], _ []dbft.PublicKey) []dbft.ConsensusPayload[crypto.Uint256] { 103 | if m.preparationHash == nil { 104 | return nil 105 | } 106 | 107 | payloads := make([]dbft.ConsensusPayload[crypto.Uint256], len(m.preparationPayloads)) 108 | 109 | for i, resp := range m.preparationPayloads { 110 | payloads[i] = fromPayload(dbft.PrepareResponseType, p, &prepareResponse{ 111 | preparationHash: *m.preparationHash, 112 | }) 113 | payloads[i].SetValidatorIndex(resp.ValidatorIndex) 114 | } 115 | 116 | return payloads 117 | } 118 | 119 | // GetChangeViews implements RecoveryMessage interface. 120 | func (m *recoveryMessage) GetChangeViews(p dbft.ConsensusPayload[crypto.Uint256], _ []dbft.PublicKey) []dbft.ConsensusPayload[crypto.Uint256] { 121 | payloads := make([]dbft.ConsensusPayload[crypto.Uint256], len(m.changeViewPayloads)) 122 | 123 | for i, cv := range m.changeViewPayloads { 124 | payloads[i] = fromPayload(dbft.ChangeViewType, p, &changeView{ 125 | newViewNumber: cv.OriginalViewNumber + 1, 126 | timestamp: cv.Timestamp, 127 | }) 128 | payloads[i].SetValidatorIndex(cv.ValidatorIndex) 129 | } 130 | 131 | return payloads 132 | } 133 | 134 | // GetPreCommits implements RecoveryMessage interface. 135 | func (m *recoveryMessage) GetPreCommits(p dbft.ConsensusPayload[crypto.Uint256], _ []dbft.PublicKey) []dbft.ConsensusPayload[crypto.Uint256] { 136 | payloads := make([]dbft.ConsensusPayload[crypto.Uint256], len(m.preCommitPayloads)) 137 | 138 | for i, c := range m.preCommitPayloads { 139 | payloads[i] = fromPayload(dbft.PreCommitType, p, &preCommit{magic: binary.BigEndian.Uint32(c.Data)}) 140 | payloads[i].SetValidatorIndex(c.ValidatorIndex) 141 | } 142 | 143 | return payloads 144 | } 145 | 146 | // GetCommits implements RecoveryMessage interface. 147 | func (m *recoveryMessage) GetCommits(p dbft.ConsensusPayload[crypto.Uint256], _ []dbft.PublicKey) []dbft.ConsensusPayload[crypto.Uint256] { 148 | payloads := make([]dbft.ConsensusPayload[crypto.Uint256], len(m.commitPayloads)) 149 | 150 | for i, c := range m.commitPayloads { 151 | payloads[i] = fromPayload(dbft.CommitType, p, &commit{signature: c.Signature}) 152 | payloads[i].SetValidatorIndex(c.ValidatorIndex) 153 | } 154 | 155 | return payloads 156 | } 157 | 158 | // EncodeBinary implements Serializable interface. 159 | func (m recoveryMessage) EncodeBinary(w *gob.Encoder) error { 160 | hasReq := m.prepareRequest != nil 161 | if err := w.Encode(hasReq); err != nil { 162 | return err 163 | } 164 | if hasReq { 165 | if err := m.prepareRequest.(Serializable).EncodeBinary(w); err != nil { 166 | return err 167 | } 168 | } else { 169 | if m.preparationHash == nil { 170 | if err := w.Encode(0); err != nil { 171 | return err 172 | } 173 | } else { 174 | if err := w.Encode(crypto.Uint256Size); err != nil { 175 | return err 176 | } 177 | if err := w.Encode(m.preparationHash); err != nil { 178 | return err 179 | } 180 | } 181 | } 182 | return w.Encode(&recoveryMessageAux{ 183 | PreparationPayloads: m.preparationPayloads, 184 | CommitPayloads: m.commitPayloads, 185 | ChangeViewPayloads: m.changeViewPayloads, 186 | }) 187 | } 188 | 189 | // DecodeBinary implements Serializable interface. 190 | func (m *recoveryMessage) DecodeBinary(r *gob.Decoder) error { 191 | var hasReq bool 192 | if err := r.Decode(&hasReq); err != nil { 193 | return err 194 | } 195 | if hasReq { 196 | m.prepareRequest = new(prepareRequest) 197 | if err := m.prepareRequest.(Serializable).DecodeBinary(r); err != nil { 198 | return err 199 | } 200 | } else { 201 | var l int 202 | if err := r.Decode(&l); err != nil { 203 | return err 204 | } 205 | if l != 0 { 206 | if l == crypto.Uint256Size { 207 | m.preparationHash = new(crypto.Uint256) 208 | if err := r.Decode(m.preparationHash); err != nil { 209 | return err 210 | } 211 | } else { 212 | return errors.New("wrong crypto.Uint256 length") 213 | } 214 | } else { 215 | m.preparationHash = nil 216 | } 217 | } 218 | 219 | aux := new(recoveryMessageAux) 220 | if err := r.Decode(aux); err != nil { 221 | return err 222 | } 223 | m.preparationPayloads = aux.PreparationPayloads 224 | if m.preparationPayloads == nil { 225 | m.preparationPayloads = []preparationCompact{} 226 | } 227 | m.commitPayloads = aux.CommitPayloads 228 | if m.commitPayloads == nil { 229 | m.commitPayloads = []commitCompact{} 230 | } 231 | m.changeViewPayloads = aux.ChangeViewPayloads 232 | if m.changeViewPayloads == nil { 233 | m.changeViewPayloads = []changeViewCompact{} 234 | } 235 | return nil 236 | } 237 | -------------------------------------------------------------------------------- /internal/consensus/recovery_request.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/gob" 5 | 6 | "github.com/nspcc-dev/dbft" 7 | ) 8 | 9 | type ( 10 | recoveryRequest struct { 11 | timestamp uint32 12 | } 13 | // recoveryRequestAux is an auxiliary structure for recoveryRequest encoding. 14 | recoveryRequestAux struct { 15 | Timestamp uint32 16 | } 17 | ) 18 | 19 | var _ dbft.RecoveryRequest = (*recoveryRequest)(nil) 20 | 21 | // EncodeBinary implements Serializable interface. 22 | func (m recoveryRequest) EncodeBinary(w *gob.Encoder) error { 23 | return w.Encode(&recoveryRequestAux{ 24 | Timestamp: m.timestamp, 25 | }) 26 | } 27 | 28 | // DecodeBinary implements Serializable interface. 29 | func (m *recoveryRequest) DecodeBinary(r *gob.Decoder) error { 30 | aux := new(recoveryRequestAux) 31 | if err := r.Decode(aux); err != nil { 32 | return err 33 | } 34 | 35 | m.timestamp = aux.Timestamp 36 | return nil 37 | } 38 | 39 | // Timestamp implements RecoveryRequest interface. 40 | func (m *recoveryRequest) Timestamp() uint64 { 41 | return secToNanoSec(m.timestamp) 42 | } 43 | -------------------------------------------------------------------------------- /internal/consensus/transaction.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | 7 | "github.com/nspcc-dev/dbft" 8 | "github.com/nspcc-dev/dbft/internal/crypto" 9 | ) 10 | 11 | // ============================= 12 | // Small transaction. 13 | // ============================= 14 | 15 | type Tx64 uint64 16 | 17 | var _ dbft.Transaction[crypto.Uint256] = (*Tx64)(nil) 18 | 19 | func (t *Tx64) Hash() (h crypto.Uint256) { 20 | binary.LittleEndian.PutUint64(h[:], uint64(*t)) 21 | return 22 | } 23 | 24 | // MarshalBinary implements encoding.BinaryMarshaler interface. 25 | func (t *Tx64) MarshalBinary() ([]byte, error) { 26 | b := make([]byte, 8) 27 | binary.LittleEndian.PutUint64(b, uint64(*t)) 28 | 29 | return b, nil 30 | } 31 | 32 | // UnmarshalBinary implements encoding.BinaryUnarshaler interface. 33 | func (t *Tx64) UnmarshalBinary(data []byte) error { 34 | if len(data) != 8 { 35 | return errors.New("length must equal 8 bytes") 36 | } 37 | 38 | *t = Tx64(binary.LittleEndian.Uint64(data)) 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/nspcc-dev/dbft" 7 | ) 8 | 9 | type suiteType byte 10 | 11 | const ( 12 | // SuiteECDSA is a ECDSA suite over P-256 curve 13 | // with 64-byte uncompressed signatures. 14 | SuiteECDSA suiteType = 1 + iota 15 | ) 16 | 17 | const defaultSuite = SuiteECDSA 18 | 19 | // Generate generates new key pair using r 20 | // as a source of entropy. 21 | func Generate(r io.Reader) (dbft.PrivateKey, dbft.PublicKey) { 22 | return GenerateWith(defaultSuite, r) 23 | } 24 | 25 | // GenerateWith generates new key pair for suite t 26 | // using r as a source of entropy. 27 | func GenerateWith(t suiteType, r io.Reader) (dbft.PrivateKey, dbft.PublicKey) { 28 | if t == SuiteECDSA { 29 | return generateECDSA(r) 30 | } 31 | 32 | return nil, nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/rand" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestVerifySignature(t *testing.T) { 11 | const dataSize = 1000 12 | 13 | priv, pub := Generate(rand.Reader) 14 | data := make([]byte, dataSize) 15 | _, err := rand.Reader.Read(data) 16 | require.NoError(t, err) 17 | 18 | sign, err := priv.(*ECDSAPriv).Sign(data) 19 | require.NoError(t, err) 20 | require.Equal(t, 64, len(sign)) 21 | 22 | err = pub.(*ECDSAPub).Verify(data, sign) 23 | require.NoError(t, err) 24 | } 25 | 26 | func TestGenerateWith(t *testing.T) { 27 | priv, pub := GenerateWith(defaultSuite, rand.Reader) 28 | require.NotNil(t, priv) 29 | require.NotNil(t, pub) 30 | 31 | priv, pub = GenerateWith(suiteType(0xFF), rand.Reader) 32 | require.Nil(t, priv) 33 | require.Nil(t, pub) 34 | } 35 | -------------------------------------------------------------------------------- /internal/crypto/ecdsa.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/sha256" 8 | "errors" 9 | "io" 10 | "math/big" 11 | 12 | "github.com/nspcc-dev/dbft" 13 | ) 14 | 15 | type ( 16 | // ECDSAPub is a wrapper over *ecsda.PublicKey. 17 | ECDSAPub struct { 18 | *ecdsa.PublicKey 19 | } 20 | 21 | // ECDSAPriv is a wrapper over *ecdsa.PrivateKey. 22 | ECDSAPriv struct { 23 | *ecdsa.PrivateKey 24 | } 25 | ) 26 | 27 | func generateECDSA(r io.Reader) (dbft.PrivateKey, dbft.PublicKey) { 28 | key, err := ecdsa.GenerateKey(elliptic.P256(), r) 29 | if err != nil { 30 | return nil, nil 31 | } 32 | 33 | return NewECDSAPrivateKey(key), NewECDSAPublicKey(&key.PublicKey) 34 | } 35 | 36 | // NewECDSAPublicKey returns new PublicKey from *ecdsa.PublicKey. 37 | func NewECDSAPublicKey(pub *ecdsa.PublicKey) dbft.PublicKey { 38 | return &ECDSAPub{ 39 | PublicKey: pub, 40 | } 41 | } 42 | 43 | // NewECDSAPrivateKey returns new PublicKey from *ecdsa.PrivateKey. 44 | func NewECDSAPrivateKey(key *ecdsa.PrivateKey) dbft.PrivateKey { 45 | return &ECDSAPriv{ 46 | PrivateKey: key, 47 | } 48 | } 49 | 50 | // Sign signs message using P-256 curve. 51 | func (e ECDSAPriv) Sign(msg []byte) ([]byte, error) { 52 | h := sha256.Sum256(msg) 53 | r, s, err := ecdsa.Sign(rand.Reader, e.PrivateKey, h[:]) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | sig := make([]byte, 32*2) 59 | _ = r.FillBytes(sig[:32]) 60 | _ = s.FillBytes(sig[32:]) 61 | 62 | return sig, nil 63 | } 64 | 65 | // Equals implements dbft.PublicKey interface. 66 | func (e *ECDSAPub) Equals(other dbft.PublicKey) bool { 67 | return e.Equal(other.(*ECDSAPub).PublicKey) 68 | } 69 | 70 | // Compare does three-way comparison of ECDSAPub. 71 | func (e *ECDSAPub) Compare(p *ECDSAPub) int { 72 | return e.X.Cmp(p.X) 73 | } 74 | 75 | // Verify verifies signature using P-256 curve. 76 | func (e ECDSAPub) Verify(msg, sig []byte) error { 77 | h := sha256.Sum256(msg) 78 | rBytes := new(big.Int).SetBytes(sig[0:32]) 79 | sBytes := new(big.Int).SetBytes(sig[32:64]) 80 | res := ecdsa.Verify(e.PublicKey, h[:], rBytes, sBytes) 81 | if !res { 82 | return errors.New("bad signature") 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/crypto/ecdsa_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | // Do not generate keys with not enough entropy. 11 | func TestECDSA_Generate(t *testing.T) { 12 | rd := &errorReader{} 13 | priv, pub := GenerateWith(SuiteECDSA, rd) 14 | require.Nil(t, priv) 15 | require.Nil(t, pub) 16 | } 17 | 18 | type errorReader struct{} 19 | 20 | func (r *errorReader) Read(_ []byte) (int, error) { return 0, errors.New("error on read") } 21 | -------------------------------------------------------------------------------- /internal/crypto/hash.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | ) 7 | 8 | const ( 9 | Uint256Size = 32 10 | Uint160Size = 20 11 | ) 12 | 13 | type ( 14 | Uint256 [Uint256Size]byte 15 | Uint160 [Uint160Size]byte 16 | ) 17 | 18 | // String implements fmt.Stringer interface. 19 | func (h Uint256) String() string { 20 | return hex.EncodeToString(h[:]) 21 | } 22 | 23 | // String implements fmt.Stringer interface. 24 | func (h Uint160) String() string { 25 | return hex.EncodeToString(h[:]) 26 | } 27 | 28 | // Hash256 returns double sha-256 of data. 29 | func Hash256(data []byte) Uint256 { 30 | h1 := sha256.Sum256(data) 31 | h2 := sha256.Sum256(h1[:]) 32 | 33 | return h2 34 | } 35 | 36 | // Hash160 returns ripemd160 from sha256 of data. 37 | func Hash160(data []byte) Uint160 { 38 | var ( 39 | h1 = sha256.Sum256(data) 40 | h Uint160 41 | ) 42 | 43 | copy(h[:], h1[:Uint160Size]) 44 | 45 | return h 46 | } 47 | -------------------------------------------------------------------------------- /internal/crypto/hash_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var hash256tc = []struct { 11 | data []byte 12 | hash Uint256 13 | }{ 14 | {[]byte{}, parse256("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456")}, 15 | {[]byte{0, 1, 2, 3}, parse256("f7a355c00c89a08c80636bed35556a210b51786f6803a494f28fc5ba05959fc2")}, 16 | } 17 | 18 | var hash160tc = []struct { 19 | data []byte 20 | hash Uint160 21 | }{ 22 | {[]byte{}, Uint160{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4}}, 23 | {[]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}}, 24 | } 25 | 26 | func TestHash256(t *testing.T) { 27 | for _, tc := range hash256tc { 28 | require.Equal(t, tc.hash, Hash256(tc.data)) 29 | } 30 | } 31 | 32 | func TestHash160(t *testing.T) { 33 | for _, tc := range hash160tc { 34 | require.Equal(t, tc.hash, Hash160(tc.data)) 35 | } 36 | } 37 | 38 | func parse256(s string) (h Uint256) { 39 | parseHex(h[:], s) 40 | return 41 | } 42 | 43 | func parseHex(b []byte, s string) { 44 | buf, err := hex.DecodeString(s) 45 | if err != nil || len(buf) != len(b) { 46 | panic("invalid test data") 47 | } 48 | 49 | copy(b, buf) 50 | } 51 | -------------------------------------------------------------------------------- /internal/merkle/merkle_tree.go: -------------------------------------------------------------------------------- 1 | package merkle 2 | 3 | import ( 4 | "github.com/nspcc-dev/dbft/internal/crypto" 5 | ) 6 | 7 | type ( 8 | // Tree represents a merkle tree with specified depth. 9 | Tree struct { 10 | Depth int 11 | 12 | root *TreeNode 13 | } 14 | 15 | // TreeNode represents inner node of a merkle tree. 16 | TreeNode struct { 17 | Hash crypto.Uint256 18 | Parent *TreeNode 19 | Left *TreeNode 20 | Right *TreeNode 21 | } 22 | ) 23 | 24 | // NewMerkleTree returns new merkle tree built on hashes. 25 | func NewMerkleTree(hashes ...crypto.Uint256) *Tree { 26 | if len(hashes) == 0 { 27 | return nil 28 | } 29 | 30 | nodes := make([]TreeNode, len(hashes)) 31 | for i := range nodes { 32 | nodes[i].Hash = hashes[i] 33 | } 34 | 35 | mt := &Tree{root: buildTree(nodes...)} 36 | mt.Depth = 1 37 | 38 | for node := mt.root; node.Left != nil; node = node.Left { 39 | mt.Depth++ 40 | } 41 | 42 | return mt 43 | } 44 | 45 | // Root returns m's root. 46 | func (m *Tree) Root() *TreeNode { 47 | return m.root 48 | } 49 | 50 | func buildTree(leaves ...TreeNode) *TreeNode { 51 | l := len(leaves) 52 | if l == 1 { 53 | return &leaves[0] 54 | } 55 | 56 | parents := make([]TreeNode, (l+1)/2) 57 | for i := range parents { 58 | parents[i].Left = &leaves[i*2] 59 | leaves[i*2].Parent = &parents[i] 60 | 61 | if i*2+1 == l { 62 | parents[i].Right = parents[i].Left 63 | } else { 64 | parents[i].Right = &leaves[i*2+1] 65 | leaves[i*2+1].Parent = &parents[i] 66 | } 67 | 68 | data := append(parents[i].Left.Hash[:], parents[i].Right.Hash[:]...) 69 | parents[i].Hash = crypto.Hash256(data) 70 | } 71 | 72 | return buildTree(parents...) 73 | } 74 | 75 | // IsLeaf returns true iff n is a leaf. 76 | func (n *TreeNode) IsLeaf() bool { return n.Left == nil && n.Right == nil } 77 | 78 | // IsRoot returns true iff n is a root. 79 | func (n *TreeNode) IsRoot() bool { return n.Parent == nil } 80 | -------------------------------------------------------------------------------- /internal/merkle/merkle_tree_test.go: -------------------------------------------------------------------------------- 1 | package merkle 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/nspcc-dev/dbft/internal/crypto" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewMerkleTree(t *testing.T) { 13 | t.Run("empty tree must be nil", func(t *testing.T) { 14 | require.Nil(t, NewMerkleTree()) 15 | }) 16 | 17 | t.Run("merkle tree on 1 leave", func(t *testing.T) { 18 | h := crypto.Uint256{1, 2, 3, 4} 19 | mt := NewMerkleTree(h) 20 | require.NotNil(t, mt) 21 | require.Equal(t, 1, mt.Depth) 22 | require.Equal(t, h, mt.Root().Hash) 23 | require.True(t, mt.Root().IsLeaf()) 24 | }) 25 | 26 | t.Run("predefined tree on 4 leaves", func(t *testing.T) { 27 | hashes := make([]crypto.Uint256, 5) 28 | for i := range hashes { 29 | hashes[i] = sha256.Sum256([]byte{byte(i)}) 30 | } 31 | 32 | mt := NewMerkleTree(hashes...) 33 | require.NotNil(t, mt) 34 | require.Equal(t, 4, mt.Depth) 35 | 36 | expected, err := hex.DecodeString("f570734e3e3e401dad09b8f51499dfb2f631c803b88487ef65b88baa069430d0") 37 | require.NoError(t, err) 38 | require.Equal(t, expected, mt.Root().Hash[:]) 39 | }) 40 | } 41 | 42 | func TestTreeNode_IsLeaf(t *testing.T) { 43 | hashes := []crypto.Uint256{{1}, {2}, {3}} 44 | 45 | mt := NewMerkleTree(hashes...) 46 | require.NotNil(t, mt) 47 | require.True(t, mt.Root().IsRoot()) 48 | require.False(t, mt.Root().IsLeaf()) 49 | 50 | left := mt.Root().Left 51 | require.NotNil(t, left) 52 | require.False(t, left.IsRoot()) 53 | require.False(t, left.IsLeaf()) 54 | 55 | lleft := left.Left 56 | require.NotNil(t, lleft) 57 | require.False(t, lleft.IsRoot()) 58 | require.True(t, lleft.IsLeaf()) 59 | } 60 | -------------------------------------------------------------------------------- /internal/simulation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "flag" 7 | "fmt" 8 | "net/http" 9 | "net/http/pprof" 10 | "os" 11 | "os/signal" 12 | "slices" 13 | "sync" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/nspcc-dev/dbft" 18 | "github.com/nspcc-dev/dbft/internal/consensus" 19 | "github.com/nspcc-dev/dbft/internal/crypto" 20 | "go.uber.org/zap" 21 | ) 22 | 23 | type ( 24 | simNode struct { 25 | id int 26 | d *dbft.DBFT[crypto.Uint256] 27 | messages chan dbft.ConsensusPayload[crypto.Uint256] 28 | key dbft.PrivateKey 29 | pub dbft.PublicKey 30 | pool *memPool 31 | cluster []*simNode 32 | log *zap.Logger 33 | 34 | height uint32 35 | lastHash crypto.Uint256 36 | validators []dbft.PublicKey 37 | } 38 | ) 39 | 40 | const ( 41 | defaultChanSize = 100 42 | ) 43 | 44 | var ( 45 | nodebug = flag.Bool("nodebug", false, "disable debug logging") 46 | count = flag.Int("count", 7, "node count") 47 | watchers = flag.Int("watchers", 7, "watch-only node count") 48 | blocked = flag.Int("blocked", -1, "blocked validator (payloads from him/her are dropped)") 49 | txPerBlock = flag.Int("txblock", 1, "transactions per block") 50 | txCount = flag.Int("txcount", 100000, "transactions on every node") 51 | duration = flag.Duration("duration", time.Second*20, "duration of simulation (infinite by default)") 52 | ) 53 | 54 | func main() { 55 | flag.Parse() 56 | 57 | initDebugger() 58 | 59 | logger := initLogger() 60 | clusterSize := *count 61 | watchOnly := *watchers 62 | nodes := make([]*simNode, clusterSize+watchOnly) 63 | 64 | initNodes(nodes, logger) 65 | updatePublicKeys(nodes, clusterSize) 66 | 67 | ctx, cancel := initContext(*duration) 68 | defer cancel() 69 | 70 | wg := new(sync.WaitGroup) 71 | wg.Add(len(nodes)) 72 | 73 | for i := range nodes { 74 | go func(i int) { 75 | defer wg.Done() 76 | 77 | nodes[i].Run(ctx) 78 | }(i) 79 | } 80 | 81 | wg.Wait() 82 | } 83 | 84 | // Run implements simple event loop. 85 | func (n *simNode) Run(ctx context.Context) { 86 | n.d.Start(0) 87 | 88 | for { 89 | select { 90 | case <-ctx.Done(): 91 | n.log.Info("context cancelled") 92 | return 93 | case <-n.d.Timer.C(): 94 | n.d.OnTimeout(n.d.Timer.Height(), n.d.Timer.View()) 95 | case msg := <-n.messages: 96 | n.d.OnReceive(msg) 97 | } 98 | } 99 | } 100 | 101 | func initNodes(nodes []*simNode, log *zap.Logger) { 102 | for i := range nodes { 103 | if err := initSimNode(nodes, i, log); err != nil { 104 | panic(err) 105 | } 106 | } 107 | } 108 | 109 | func initSimNode(nodes []*simNode, i int, log *zap.Logger) error { 110 | key, pub := crypto.Generate(rand.Reader) 111 | nodes[i] = &simNode{ 112 | id: i, 113 | messages: make(chan dbft.ConsensusPayload[crypto.Uint256], defaultChanSize), 114 | key: key, 115 | pub: pub, 116 | pool: newMemoryPool(), 117 | log: log.With(zap.Int("id", i)), 118 | cluster: nodes, 119 | } 120 | 121 | var err error 122 | nodes[i].d, err = consensus.New(nodes[i].log, key, pub, nodes[i].pool.Get, 123 | nodes[i].pool.GetVerified, 124 | nodes[i].Broadcast, 125 | nodes[i].ProcessBlock, 126 | nodes[i].CurrentHeight, 127 | nodes[i].CurrentBlockHash, 128 | nodes[i].GetValidators, 129 | nodes[i].VerifyPayload, 130 | ) 131 | if err != nil { 132 | return fmt.Errorf("failed to initialize dBFT: %w", err) 133 | } 134 | 135 | nodes[i].addTx(*txCount) 136 | 137 | return nil 138 | } 139 | 140 | func updatePublicKeys(nodes []*simNode, n int) { 141 | pubs := make([]dbft.PublicKey, n) 142 | for i := range pubs { 143 | pubs[i] = nodes[i].pub 144 | } 145 | 146 | sortValidators(pubs) 147 | 148 | for i := range nodes { 149 | nodes[i].validators = pubs 150 | } 151 | } 152 | 153 | func sortValidators(pubs []dbft.PublicKey) { 154 | slices.SortFunc(pubs, func(a, b dbft.PublicKey) int { 155 | x := a.(*crypto.ECDSAPub) 156 | y := b.(*crypto.ECDSAPub) 157 | return x.Compare(y) 158 | }) 159 | } 160 | 161 | func (n *simNode) Broadcast(m dbft.ConsensusPayload[crypto.Uint256]) { 162 | for i, node := range n.cluster { 163 | if i != n.id { 164 | select { 165 | case node.messages <- m: 166 | default: 167 | n.log.Warn("can't broadcast message: channel is full") 168 | } 169 | } 170 | } 171 | } 172 | 173 | func (n *simNode) CurrentHeight() uint32 { return n.height } 174 | func (n *simNode) CurrentBlockHash() crypto.Uint256 { return n.lastHash } 175 | 176 | // GetValidators always returns the same list of validators. 177 | func (n *simNode) GetValidators(...dbft.Transaction[crypto.Uint256]) []dbft.PublicKey { 178 | return n.validators 179 | } 180 | 181 | func (n *simNode) ProcessBlock(b dbft.Block[crypto.Uint256]) error { 182 | n.d.Logger.Debug("received block", zap.Uint32("height", b.Index())) 183 | 184 | for _, tx := range b.Transactions() { 185 | n.pool.Delete(tx.Hash()) 186 | } 187 | 188 | n.height = b.Index() 189 | n.lastHash = b.Hash() 190 | return nil 191 | } 192 | 193 | // VerifyPayload verifies that payload was received from a good validator. 194 | func (n *simNode) VerifyPayload(p dbft.ConsensusPayload[crypto.Uint256]) error { 195 | if *blocked != -1 && p.ValidatorIndex() == uint16(*blocked) { 196 | return fmt.Errorf("message from blocked validator: %d", *blocked) 197 | } 198 | return nil 199 | } 200 | 201 | func (n *simNode) addTx(count int) { 202 | for i := range count { 203 | tx := consensus.Tx64(uint64(i)) 204 | n.pool.Add(&tx) 205 | } 206 | } 207 | 208 | // ============================= 209 | // Memory pool for transactions. 210 | // ============================= 211 | 212 | type memPool struct { 213 | mtx *sync.RWMutex 214 | store map[crypto.Uint256]dbft.Transaction[crypto.Uint256] 215 | } 216 | 217 | func newMemoryPool() *memPool { 218 | return &memPool{ 219 | mtx: new(sync.RWMutex), 220 | store: make(map[crypto.Uint256]dbft.Transaction[crypto.Uint256]), 221 | } 222 | } 223 | 224 | func (p *memPool) Add(tx dbft.Transaction[crypto.Uint256]) { 225 | p.mtx.Lock() 226 | 227 | h := tx.Hash() 228 | if _, ok := p.store[h]; !ok { 229 | p.store[h] = tx 230 | } 231 | 232 | p.mtx.Unlock() 233 | } 234 | 235 | func (p *memPool) Get(h crypto.Uint256) (tx dbft.Transaction[crypto.Uint256]) { 236 | p.mtx.RLock() 237 | tx = p.store[h] 238 | p.mtx.RUnlock() 239 | 240 | return 241 | } 242 | 243 | func (p *memPool) Delete(h crypto.Uint256) { 244 | p.mtx.Lock() 245 | delete(p.store, h) 246 | p.mtx.Unlock() 247 | } 248 | 249 | func (p *memPool) GetVerified() (txx []dbft.Transaction[crypto.Uint256]) { 250 | n := *txPerBlock 251 | if n == 0 { 252 | return 253 | } 254 | 255 | txx = make([]dbft.Transaction[crypto.Uint256], 0, n) 256 | for _, tx := range p.store { 257 | txx = append(txx, tx) 258 | 259 | if n--; n == 0 { 260 | return 261 | } 262 | } 263 | 264 | return 265 | } 266 | 267 | // initDebugger initializes pprof debug facilities. 268 | func initDebugger() { 269 | r := http.NewServeMux() 270 | r.HandleFunc("/debug/pprof/", pprof.Index) 271 | r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 272 | r.HandleFunc("/debug/pprof/profile", pprof.Profile) 273 | r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 274 | r.HandleFunc("/debug/pprof/trace", pprof.Trace) 275 | 276 | go func() { 277 | err := http.ListenAndServe("localhost:6060", r) 278 | if err != nil { 279 | panic(err) 280 | } 281 | }() 282 | } 283 | 284 | // initLogger initializes new logger. 285 | func initLogger() *zap.Logger { 286 | if *nodebug { 287 | return zap.L() 288 | } 289 | 290 | logger, err := zap.NewDevelopment() 291 | if err != nil { 292 | panic("can't init logger") 293 | } 294 | 295 | return logger 296 | } 297 | 298 | // initContext creates new context which will be cancelled by Ctrl+C. 299 | func initContext(d time.Duration) (ctx context.Context, cancel func()) { 300 | // exit by Ctrl+C 301 | c := make(chan os.Signal, 1) 302 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 303 | 304 | go func() { 305 | <-c 306 | cancel() 307 | }() 308 | 309 | if d != 0 { 310 | return context.WithTimeout(context.Background(), *duration) 311 | } 312 | 313 | return context.WithCancel(context.Background()) 314 | } 315 | -------------------------------------------------------------------------------- /pre_block.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // PreBlock is a generic interface for a PreBlock used by anti-MEV dBFT extension. 4 | // It holds a "draft" of block that should be converted to a final block with the 5 | // help of additional data held by PreCommit messages. 6 | type PreBlock[H Hash] interface { 7 | // Data returns PreBlock's data CNs need to exchange during PreCommit phase. 8 | // Data represents additional information not related to a final block signature. 9 | Data() []byte 10 | // SetData generates and sets PreBlock's data CNs need to exchange during 11 | // PreCommit phase. 12 | SetData(key PrivateKey) error 13 | // Verify checks if data related to PreCommit phase is correct. This method is 14 | // refined on PreBlock rather than on PreCommit message since PreBlock itself is 15 | // required for PreCommit's data verification. It's guaranteed that all 16 | // proposed transactions are collected by the moment of call to Verify. 17 | Verify(key PublicKey, data []byte) error 18 | 19 | // Transactions returns PreBlock's transaction list. This list may be different 20 | // comparing to the final set of Block's transactions. 21 | Transactions() []Transaction[H] 22 | // SetTransactions sets PreBlock's transaction list. This list may be different 23 | // comparing to the final set of Block's transactions. 24 | SetTransactions([]Transaction[H]) 25 | } 26 | -------------------------------------------------------------------------------- /pre_commit.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // PreCommit is an interface for dBFT PreCommit message. This message is used right 4 | // before the Commit phase to exchange additional information required for the final 5 | // block construction in anti-MEV dBFT extension. 6 | type PreCommit interface { 7 | // Data returns PreCommit's data that should be used for the final 8 | // Block construction in anti-MEV dBFT extension. 9 | Data() []byte 10 | } 11 | -------------------------------------------------------------------------------- /prepare_request.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // PrepareRequest represents dBFT PrepareRequest message. 4 | type PrepareRequest[H Hash] interface { 5 | // Timestamp returns this message's timestamp. 6 | Timestamp() uint64 7 | // Nonce is a random nonce. 8 | Nonce() uint64 9 | // TransactionHashes returns hashes of all transaction in a proposed block. 10 | TransactionHashes() []H 11 | } 12 | -------------------------------------------------------------------------------- /prepare_response.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // PrepareResponse represents dBFT PrepareResponse message. 4 | type PrepareResponse[H Hash] interface { 5 | // PreparationHash returns the hash of PrepareRequest payload 6 | // for this epoch. 7 | PreparationHash() H 8 | } 9 | -------------------------------------------------------------------------------- /recovery_message.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // RecoveryMessage represents dBFT Recovery message. 4 | type RecoveryMessage[H Hash] interface { 5 | // AddPayload adds payload from this epoch to be recovered. 6 | AddPayload(p ConsensusPayload[H]) 7 | // GetPrepareRequest returns PrepareRequest to be processed. 8 | GetPrepareRequest(p ConsensusPayload[H], validators []PublicKey, primary uint16) ConsensusPayload[H] 9 | // GetPrepareResponses returns a slice of PrepareResponse in any order. 10 | GetPrepareResponses(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] 11 | // GetChangeViews returns a slice of ChangeView in any order. 12 | GetChangeViews(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] 13 | // GetPreCommits returns a slice of PreCommit messages in any order. 14 | // If implemented on networks with no AntiMEV extension it can just 15 | // always return nil. 16 | GetPreCommits(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] 17 | // GetCommits returns a slice of Commit in any order. 18 | GetCommits(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] 19 | 20 | // PreparationHash returns has of PrepareRequest payload for this epoch. 21 | // It can be useful in case only PrepareResponse payloads were received. 22 | PreparationHash() *H 23 | } 24 | -------------------------------------------------------------------------------- /recovery_request.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // RecoveryRequest represents dBFT RecoveryRequest message. 4 | type RecoveryRequest interface { 5 | // Timestamp returns this message's timestamp. 6 | Timestamp() uint64 7 | } 8 | -------------------------------------------------------------------------------- /rtt.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const rttLength = 7 * 10 // 10 rounds with 7 nodes 8 | 9 | type rtt struct { 10 | times [rttLength]time.Duration 11 | idx int 12 | avg time.Duration 13 | } 14 | 15 | func (r *rtt) addTime(t time.Duration) { 16 | var old = r.times[r.idx] 17 | 18 | if old != 0 { 19 | t = min(t, 2*old) // Too long delays should be normalized, we don't want to overshoot. 20 | } 21 | 22 | r.avg = r.avg + (t-old)/time.Duration(len(r.times)) 23 | r.avg = max(0, r.avg) // Can't be less than zero. 24 | r.times[r.idx] = t 25 | r.idx = (r.idx + 1) % len(r.times) 26 | } 27 | -------------------------------------------------------------------------------- /send.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.uber.org/zap" 7 | ) 8 | 9 | func (d *DBFT[H]) broadcast(msg ConsensusPayload[H]) { 10 | d.Logger.Debug("broadcasting message", 11 | zap.Stringer("type", msg.Type()), 12 | zap.Uint32("height", d.BlockIndex), 13 | zap.Uint("view", uint(d.ViewNumber))) 14 | 15 | msg.SetValidatorIndex(uint16(d.MyIndex)) 16 | d.Broadcast(msg) 17 | } 18 | 19 | func (c *Context[H]) makePrepareRequest() ConsensusPayload[H] { 20 | c.Fill() 21 | 22 | req := c.Config.NewPrepareRequest(c.Timestamp, c.Nonce, c.TransactionHashes) 23 | 24 | return c.Config.NewConsensusPayload(c, PrepareRequestType, req) 25 | } 26 | 27 | func (d *DBFT[H]) sendPrepareRequest() { 28 | msg := d.makePrepareRequest() 29 | d.PreparationPayloads[d.MyIndex] = msg 30 | d.broadcast(msg) 31 | 32 | d.prepareSentTime = d.Timer.Now() 33 | 34 | delay := d.timePerBlock << (d.ViewNumber + 1) 35 | if d.ViewNumber == 0 { 36 | delay -= d.timePerBlock 37 | } 38 | 39 | d.Logger.Info("sending PrepareRequest", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) 40 | d.changeTimer(delay) 41 | d.checkPrepare() 42 | } 43 | 44 | func (c *Context[H]) makeChangeView(ts uint64, reason ChangeViewReason) ConsensusPayload[H] { 45 | cv := c.Config.NewChangeView(c.ViewNumber+1, reason, ts) 46 | 47 | msg := c.Config.NewConsensusPayload(c, ChangeViewType, cv) 48 | c.ChangeViewPayloads[c.MyIndex] = msg 49 | 50 | return msg 51 | } 52 | 53 | func (d *DBFT[H]) sendChangeView(reason ChangeViewReason) { 54 | if d.Context.WatchOnly() { 55 | return 56 | } 57 | 58 | newView := d.ViewNumber + 1 59 | d.changeTimer(d.timePerBlock << (newView + 1)) 60 | 61 | nc := d.CountCommitted() 62 | nf := d.CountFailed() 63 | 64 | if reason == CVTimeout && nc+nf > d.F() { 65 | d.Logger.Info("skip change view", zap.Int("nc", nc), zap.Int("nf", nf)) 66 | d.sendRecoveryRequest() 67 | 68 | return 69 | } 70 | 71 | // Timeout while missing transactions, set the real reason. 72 | if !d.hasAllTransactions() && reason == CVTimeout { 73 | reason = CVTxNotFound 74 | } 75 | 76 | d.Logger.Info("request change view", 77 | zap.Int("view", int(d.ViewNumber)), 78 | zap.Uint32("height", d.BlockIndex), 79 | zap.Stringer("reason", reason), 80 | zap.Int("new_view", int(newView)), 81 | zap.Int("nc", nc), 82 | zap.Int("nf", nf)) 83 | 84 | msg := d.makeChangeView(uint64(d.Timer.Now().UnixNano()), reason) 85 | d.StopTxFlow() 86 | d.broadcast(msg) 87 | d.checkChangeView(newView) 88 | } 89 | 90 | func (c *Context[H]) makePrepareResponse() ConsensusPayload[H] { 91 | resp := c.Config.NewPrepareResponse(c.PreparationPayloads[c.PrimaryIndex].Hash()) 92 | 93 | msg := c.Config.NewConsensusPayload(c, PrepareResponseType, resp) 94 | c.PreparationPayloads[c.MyIndex] = msg 95 | 96 | return msg 97 | } 98 | 99 | func (d *DBFT[H]) sendPrepareResponse() { 100 | msg := d.makePrepareResponse() 101 | d.Logger.Info("sending PrepareResponse", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) 102 | d.StopTxFlow() 103 | d.broadcast(msg) 104 | } 105 | 106 | func (c *Context[H]) makePreCommit() (ConsensusPayload[H], error) { 107 | if msg := c.PreCommitPayloads[c.MyIndex]; msg != nil { 108 | return msg, nil 109 | } 110 | 111 | if preB := c.CreatePreBlock(); preB != nil { 112 | var preData []byte 113 | if err := preB.SetData(c.Priv); err == nil { 114 | preData = preB.Data() 115 | } else { 116 | return nil, fmt.Errorf("PreCommit data construction failed: %w", err) 117 | } 118 | 119 | preCommit := c.Config.NewPreCommit(preData) 120 | 121 | return c.Config.NewConsensusPayload(c, PreCommitType, preCommit), nil 122 | } 123 | 124 | return nil, fmt.Errorf("failed to construct PreBlock") 125 | } 126 | 127 | func (c *Context[H]) makeCommit() (ConsensusPayload[H], error) { 128 | if msg := c.CommitPayloads[c.MyIndex]; msg != nil { 129 | return msg, nil 130 | } 131 | 132 | if b := c.MakeHeader(); b != nil { 133 | var sign []byte 134 | if err := b.Sign(c.Priv); err == nil { 135 | sign = b.Signature() 136 | } else { 137 | return nil, fmt.Errorf("header signing failed: %w", err) 138 | } 139 | 140 | commit := c.Config.NewCommit(sign) 141 | 142 | return c.Config.NewConsensusPayload(c, CommitType, commit), nil 143 | } 144 | 145 | return nil, fmt.Errorf("failed to construct Header") 146 | } 147 | 148 | func (d *DBFT[H]) sendPreCommit() { 149 | msg, err := d.makePreCommit() 150 | if err != nil { 151 | d.Logger.Error("failed to construct PreCommit", zap.Error(err)) 152 | return 153 | } 154 | d.PreCommitPayloads[d.MyIndex] = msg 155 | d.Logger.Info("sending PreCommit", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) 156 | d.broadcast(msg) 157 | } 158 | 159 | func (d *DBFT[H]) sendCommit() { 160 | msg, err := d.makeCommit() 161 | if err != nil { 162 | d.Logger.Error("failed to construct Commit", zap.Error(err)) 163 | return 164 | } 165 | d.CommitPayloads[d.MyIndex] = msg 166 | d.Logger.Info("sending Commit", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) 167 | d.broadcast(msg) 168 | } 169 | 170 | func (d *DBFT[H]) sendRecoveryRequest() { 171 | // If we're here, something is wrong, we either missing some messages or 172 | // transactions or both, so re-request missing transactions here too. 173 | if d.RequestSentOrReceived() && !d.hasAllTransactions() { 174 | d.processMissingTx() 175 | } 176 | req := d.NewRecoveryRequest(uint64(d.Timer.Now().UnixNano())) 177 | d.broadcast(d.Config.NewConsensusPayload(&d.Context, RecoveryRequestType, req)) 178 | } 179 | 180 | func (c *Context[H]) makeRecoveryMessage() ConsensusPayload[H] { 181 | recovery := c.Config.NewRecoveryMessage() 182 | 183 | for _, p := range c.PreparationPayloads { 184 | if p != nil { 185 | recovery.AddPayload(p) 186 | } 187 | } 188 | 189 | cv := c.LastChangeViewPayloads 190 | // if byte(msg.ViewNumber) == c.ViewNumber { 191 | // cv = c.changeViewPayloads 192 | // } 193 | for _, p := range cv { 194 | if p != nil { 195 | recovery.AddPayload(p) 196 | } 197 | } 198 | 199 | if c.PreCommitSent() { 200 | for _, p := range c.PreCommitPayloads { 201 | if p != nil { 202 | recovery.AddPayload(p) 203 | } 204 | } 205 | } 206 | 207 | if c.CommitSent() { 208 | for _, p := range c.CommitPayloads { 209 | if p != nil { 210 | recovery.AddPayload(p) 211 | } 212 | } 213 | } 214 | 215 | return c.Config.NewConsensusPayload(c, RecoveryMessageType, recovery) 216 | } 217 | 218 | func (d *DBFT[H]) sendRecoveryMessage() { 219 | d.broadcast(d.makeRecoveryMessage()) 220 | } 221 | -------------------------------------------------------------------------------- /timer.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Timer is an interface which implements all time-related 8 | // functions. It can be mocked for testing. 9 | type Timer interface { 10 | // Now returns current time. 11 | Now() time.Time 12 | // Reset resets timer to the specified block height and view. 13 | Reset(height uint32, view byte, d time.Duration) 14 | // Extend extends current timer with duration d. 15 | Extend(d time.Duration) 16 | // Height returns current height set for the timer. 17 | Height() uint32 18 | // View returns current view set for the timer. 19 | View() byte 20 | // C returns channel for timer events. 21 | C() <-chan time.Time 22 | } 23 | -------------------------------------------------------------------------------- /timer/timer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package timer contains default implementation of [dbft.Timer] interface and provides 3 | all necessary timer-related functionality to [dbft.DBFT] service. 4 | */ 5 | package timer 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | type ( 12 | // Timer is a default [dbft.Timer] implementation. 13 | Timer struct { 14 | height uint32 15 | view byte 16 | s time.Time 17 | d time.Duration 18 | tt *time.Timer 19 | ch chan time.Time 20 | } 21 | ) 22 | 23 | // New returns default Timer implementation. 24 | func New() *Timer { 25 | t := &Timer{ 26 | ch: make(chan time.Time, 1), 27 | } 28 | 29 | return t 30 | } 31 | 32 | // C implements Timer interface. 33 | func (t *Timer) C() <-chan time.Time { 34 | if t.tt == nil { 35 | return t.ch 36 | } 37 | 38 | return t.tt.C 39 | } 40 | 41 | // Height returns current timer height. 42 | func (t *Timer) Height() uint32 { 43 | return t.height 44 | } 45 | 46 | // View return current timer view. 47 | func (t *Timer) View() byte { 48 | return t.view 49 | } 50 | 51 | // Reset implements Timer interface. 52 | func (t *Timer) Reset(height uint32, view byte, d time.Duration) { 53 | t.stop() 54 | 55 | t.s = t.Now() 56 | t.d = d 57 | t.height = height 58 | t.view = view 59 | 60 | if t.d != 0 { 61 | t.tt = time.NewTimer(t.d) 62 | } else { 63 | t.tt = nil 64 | drain(t.ch) 65 | t.ch <- t.s 66 | } 67 | } 68 | 69 | func drain(ch <-chan time.Time) { 70 | select { 71 | case <-ch: 72 | default: 73 | } 74 | } 75 | 76 | // stop stops the Timer. 77 | func (t *Timer) stop() { 78 | if t.tt != nil { 79 | t.tt.Stop() 80 | t.tt = nil 81 | } 82 | } 83 | 84 | // Extend implements Timer interface. 85 | func (t *Timer) Extend(d time.Duration) { 86 | t.d += d 87 | 88 | if elapsed := time.Since(t.s); t.d > elapsed { 89 | t.stop() 90 | t.tt = time.NewTimer(t.d - elapsed) 91 | } 92 | } 93 | 94 | // Now implements Timer interface. 95 | func (t *Timer) Now() time.Time { 96 | return time.Now() 97 | } 98 | -------------------------------------------------------------------------------- /timer/timer_test.go: -------------------------------------------------------------------------------- 1 | package timer 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestTimer_Reset(t *testing.T) { 11 | tt := New() 12 | 13 | tt.Reset(1, 2, time.Millisecond*100) 14 | time.Sleep(time.Millisecond * 200) 15 | shouldReceive(t, tt, 1, 2, "no value in timer") 16 | 17 | tt.Reset(1, 2, time.Second) 18 | tt.Reset(2, 3, 0) 19 | shouldReceive(t, tt, 2, 3, "no value in timer after reset(0)") 20 | 21 | tt.Reset(1, 2, time.Millisecond*100) 22 | time.Sleep(time.Millisecond * 200) 23 | tt.Reset(1, 3, time.Millisecond*100) 24 | time.Sleep(time.Millisecond * 200) 25 | shouldReceive(t, tt, 1, 3, "invalid value after reset") 26 | 27 | tt.Reset(3, 1, time.Millisecond*100) 28 | shouldNotReceive(t, tt, "value arrived too early") 29 | 30 | tt.Extend(time.Millisecond * 300) 31 | time.Sleep(time.Millisecond * 200) 32 | shouldNotReceive(t, tt, "value arrived too early after extend") 33 | 34 | time.Sleep(time.Millisecond * 300) 35 | shouldReceive(t, tt, 3, 1, "no value in timer after extend") 36 | } 37 | 38 | func shouldReceive(t *testing.T, tt *Timer, height uint32, view byte, msg string) { 39 | select { 40 | case <-tt.C(): 41 | gotHeight := tt.Height() 42 | gotView := tt.View() 43 | require.Equal(t, height, gotHeight) 44 | require.Equal(t, view, gotView) 45 | default: 46 | require.Fail(t, msg) 47 | } 48 | } 49 | 50 | func shouldNotReceive(t *testing.T, tt *Timer, msg string) { 51 | select { 52 | case <-tt.C(): 53 | require.Fail(t, msg) 54 | default: 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | package dbft 2 | 3 | // Transaction is a generic transaction interface. 4 | type Transaction[H Hash] interface { 5 | // Hash must return cryptographic hash of the transaction. 6 | // Transactions which have equal hashes are considered equal. 7 | Hash() H 8 | } 9 | --------------------------------------------------------------------------------