├── .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 | [](https://pkg.go.dev/github.com/nspcc-dev/dbft/)
2 | 
3 | [](https://goreportcard.com/report/github.com/nspcc-dev/dbft)
4 | 
5 | 
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 |
--------------------------------------------------------------------------------