Rebrand to Tutus - update license, workflows, and dependencies

This commit is contained in:
Tutus Development 2025-12-27 15:45:22 +00:00
parent bea7e7cdcb
commit fb604ad460
56 changed files with 6425 additions and 6425 deletions

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @AnnaShaleva @roman-khimov * @AnnaShaleva @roman-khimov

View File

@ -1,44 +1,44 @@
name: Build name: Build
on: on:
pull_request: pull_request:
branches: branches:
- master - master
types: [opened, synchronize] types: [opened, synchronize]
paths-ignore: paths-ignore:
- 'scripts/**' - 'scripts/**'
- '**/*.md' - '**/*.md'
push: push:
# Build for the master branch. # Build for the master branch.
branches: branches:
- master - master
release: release:
# Publish released commit as Docker `latest` and `git_revision` images. # Publish released commit as Docker `latest` and `git_revision` images.
types: types:
- published - published
workflow_dispatch: workflow_dispatch:
inputs: inputs:
ref: ref:
description: 'Ref to build dBFT [default: latest master; examples: v0.1.0, 0a4ff9d3e4a9ab432fd5812eb18c98e03b5a7432]' description: 'Ref to build dBFT [default: latest master; examples: v0.1.0, 0a4ff9d3e4a9ab432fd5812eb18c98e03b5a7432]'
required: false required: false
default: '' default: ''
jobs: jobs:
run: run:
name: Run simulation name: Run simulation
runs-on: ubuntu-slim runs-on: ubuntu-slim
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ github.event.inputs.ref }} ref: ${{ github.event.inputs.ref }}
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
cache: true cache: true
- name: Run simulation - name: Run simulation
run: | run: |
cd ./internal/simulation cd ./internal/simulation
go run main.go go run main.go

View File

@ -1,33 +1,33 @@
name: CHANGELOG check name: CHANGELOG check
on: on:
pull_request: pull_request:
branches: branches:
- master - master
paths-ignore: paths-ignore:
- '**/*.md' - '**/*.md'
- '**/*.yml' - '**/*.yml'
- '.github/workflows/**' - '.github/workflows/**'
- 'formal-models/**' - 'formal-models/**'
jobs: jobs:
check: check:
name: Check for CHANGELOG updates name: Check for CHANGELOG updates
runs-on: ubuntu-slim runs-on: ubuntu-slim
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Get changed CHANGELOG - name: Get changed CHANGELOG
id: changelog-diff id: changelog-diff
uses: tj-actions/changed-files@v46 uses: tj-actions/changed-files@v46
with: with:
files: CHANGELOG.md files: CHANGELOG.md
- name: Fail if changelog not updated - name: Fail if changelog not updated
if: steps.changelog-diff.outputs.any_changed == 'false' if: steps.changelog-diff.outputs.any_changed == 'false'
uses: actions/github-script@v7 uses: actions/github-script@v7
with: with:
script: | script: |
core.setFailed('CHANGELOG.md has not been updated') core.setFailed('CHANGELOG.md has not been updated')

View File

@ -1,10 +1,10 @@
name: DCO check name: DCO check
on: on:
pull_request: pull_request:
branches: branches:
- master - master
jobs: jobs:
dco: dco:
uses: nspcc-dev/.github/.github/workflows/dco.yml@master uses: nspcc-dev/.github/.github/workflows/dco.yml@master

View File

@ -1,98 +1,98 @@
name: Go name: Go
on: on:
push: push:
branches: [ master ] branches: [ master ]
pull_request: pull_request:
branches: branches:
- master - master
types: [opened, synchronize] types: [opened, synchronize]
paths-ignore: paths-ignore:
- '**/*.md' - '**/*.md'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
lint: lint:
name: Lint name: Lint
uses: nspcc-dev/.github/.github/workflows/go-linter.yml@master uses: nspcc-dev/.github/.github/workflows/go-linter.yml@master
test: test:
name: Test name: Test
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
go: [ '1.24', '1.25'] go: [ '1.24', '1.25']
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
exclude: exclude:
# Only latest Go version for Windows and MacOS. # Only latest Go version for Windows and MacOS.
- os: windows-latest - os: windows-latest
go: '1.24' go: '1.24'
- os: macos-latest - os: macos-latest
go: '1.24' go: '1.24'
# Exclude latest Go version for Ubuntu as Coverage uses it. # Exclude latest Go version for Ubuntu as Coverage uses it.
- os: ubuntu-latest - os: ubuntu-latest
go: '1.25' go: '1.25'
steps: steps:
- name: Setup go - name: Setup go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Tests - name: Tests
run: go test -race ./... run: go test -race ./...
coverage: coverage:
name: Coverage name: Coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 1.25 go-version: 1.25
- name: Check out - name: Check out
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Collect coverage - name: Collect coverage
run: go test -coverprofile=coverage.txt -covermode=atomic ./... run: go test -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage results to Codecov - name: Upload coverage results to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
with: with:
fail_ci_if_error: true fail_ci_if_error: true
files: ./coverage.txt files: ./coverage.txt
slug: nspcc-dev/dbft slug: nspcc-dev/dbft
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
verbose: true verbose: true
codeql: codeql:
name: CodeQL name: CodeQL
runs-on: ubuntu-slim runs-on: ubuntu-slim
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'go' ] language: [ 'go' ]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v3

10
.gitignore vendored
View File

@ -1,5 +1,5 @@
/vendor /vendor
.golangci.yml .golangci.yml
# TLC Model Checker files # TLC Model Checker files
formal-models/*/*.toolbox/ formal-models/*/*.toolbox/

View File

@ -1,147 +1,147 @@
# Changelog # Changelog
This document outlines major changes between releases. This document outlines major changes between releases.
## [Unreleased] ## [Unreleased]
New features: New features:
Behaviour changes: Behaviour changes:
Improvements: Improvements:
* minimum required Go version is 1.24 (#144) * minimum required Go version is 1.24 (#144)
Bugs fixed: Bugs fixed:
## [0.4.0] (17 July 2025) ## [0.4.0] (17 July 2025)
This release contains two major changes. First one introduces an ability to This release contains two major changes. First one introduces an ability to
change block generation time every block. This change is triggered by the change block generation time every block. This change is triggered by the
transfer of `TimePerBlock` setting to native Policy contract on N3 protocol transfer of `TimePerBlock` setting to native Policy contract on N3 protocol
which makes this value variable throughout the network lifetime. The second which makes this value variable throughout the network lifetime. The second
change allows to vary block generation time from `TimePerBlock` (when there are change allows to vary block generation time from `TimePerBlock` (when there are
some transactions in the network and hence, it's beneficial to accept block as some transactions in the network and hence, it's beneficial to accept block as
soon as possible) to `MaxTimePerBlock` (when there are no transactions, but soon as possible) to `MaxTimePerBlock` (when there are no transactions, but
consensus still needs to take care of the network heartbeat). This change is consensus still needs to take care of the network heartbeat). This change is
beneficial for custom networks with small `TimePerBlock` values to prevent the beneficial for custom networks with small `TimePerBlock` values to prevent the
chain size explosion. Also, this release contains Go version upgrade to 1.23. chain size explosion. Also, this release contains Go version upgrade to 1.23.
New features: New features:
* `MaxTimePerBlock` and `SubscribeForTxs` configuration parameters are added * `MaxTimePerBlock` and `SubscribeForTxs` configuration parameters are added
to support dynamic block time extension (#150) to support dynamic block time extension (#150)
Behaviour changes: Behaviour changes:
* `SecondsPerBlock` config parameter is replaced with `TimePerBlock` function (#147) * `SecondsPerBlock` config parameter is replaced with `TimePerBlock` function (#147)
Improvements: Improvements:
* minimum required Go version is 1.23 now (#145) * minimum required Go version is 1.23 now (#145)
## [0.3.2] (30 January 2025) ## [0.3.2] (30 January 2025)
Important dBFT timer adjustments are included into this patch-release. The first one Important dBFT timer adjustments are included into this patch-release. The first one
is the reference time point for dBFT timer which is moved to the moment of is the reference time point for dBFT timer which is moved to the moment of
PrepareRequest receiving. Another one is evaluated network roundtrip time which is now PrepareRequest receiving. Another one is evaluated network roundtrip time which is now
taken into account every time on dBFT timer reset. These adjustments lead to the fact taken into account every time on dBFT timer reset. These adjustments lead to the fact
that actual block producing time is extremely close to the configuration value. Other that actual block producing time is extremely close to the configuration value. Other
than that, a couple of minor bug fixes are included. than that, a couple of minor bug fixes are included.
Improvements: Improvements:
* timer adjustment for most of the consensus time, more accurate block * timer adjustment for most of the consensus time, more accurate block
intervals (#56) intervals (#56)
* timer adjustment for network roundtrip time (#140) * timer adjustment for network roundtrip time (#140)
Bugs fixed: Bugs fixed:
* inappropriate log on attempt to construct Commit for anti-MEV enabled WatchOnly * inappropriate log on attempt to construct Commit for anti-MEV enabled WatchOnly
(#139) (#139)
* empty PreCommit/Commit can be relayed (#142) * empty PreCommit/Commit can be relayed (#142)
## [0.3.1] (29 November 2024) ## [0.3.1] (29 November 2024)
This patch version mostly includes a set of library API extensions made to fit the This patch version mostly includes a set of library API extensions made to fit the
needs of developing MEV-resistant blockchain node. Also, this release bumps minimum needs of developing MEV-resistant blockchain node. Also, this release bumps minimum
required Go version up to 1.22 and contains a set of bug fixes critical for the required Go version up to 1.22 and contains a set of bug fixes critical for the
library functioning. library functioning.
Minor user-side code adjustments are required to adapt new ProcessBlock callback Minor user-side code adjustments are required to adapt new ProcessBlock callback
signature, whereas the rest of APIs stay compatible with the old implementation. signature, whereas the rest of APIs stay compatible with the old implementation.
This version also includes a simplification of PrivateKey interface which may be This version also includes a simplification of PrivateKey interface which may be
adopted by removing extra wrappers around PrivateKey implementation on the user code adopted by removing extra wrappers around PrivateKey implementation on the user code
side. side.
Behaviour changes: Behaviour changes:
* adjust behaviour of ProcessPreBlock callback (#129) * adjust behaviour of ProcessPreBlock callback (#129)
* (*DBFT).Header() and (*DBFT).PreHeader() are moved to (*Context) receiver (#133) * (*DBFT).Header() and (*DBFT).PreHeader() are moved to (*Context) receiver (#133)
* support error handling for ProcessBlock callback if anti-MEV extension is enabled * support error handling for ProcessBlock callback if anti-MEV extension is enabled
(#134) (#134)
* remove Sign method from PrivateKey interface (#137) * remove Sign method from PrivateKey interface (#137)
Improvements: Improvements:
* minimum required Go version is 1.22 (#122, #126) * minimum required Go version is 1.22 (#122, #126)
* log Commit signature verification error (#134) * log Commit signature verification error (#134)
* add Commit message verification callback (#134) * add Commit message verification callback (#134)
Bugs fixed: Bugs fixed:
* context-bound PreBlock and PreHeader are not reset properly (#127) * context-bound PreBlock and PreHeader are not reset properly (#127)
* PreHeader is constructed instead of PreBlock to create PreCommit message (#128) * PreHeader is constructed instead of PreBlock to create PreCommit message (#128)
* enable anti-MEV extension with respect to the current block index (#132) * enable anti-MEV extension with respect to the current block index (#132)
* (*Context).PreBlock() method returns PreHeader instead of PreBlock (#133) * (*Context).PreBlock() method returns PreHeader instead of PreBlock (#133)
* WatchOnly node may send RecoveryMessage on RecoveryRequest (#135) * WatchOnly node may send RecoveryMessage on RecoveryRequest (#135)
* invalid PreCommit message is not removed from cache (#134) * invalid PreCommit message is not removed from cache (#134)
## [0.3.0] (01 August 2024) ## [0.3.0] (01 August 2024)
New features: New features:
* TLA+ model for MEV-resistant dBFT extension (#116) * TLA+ model for MEV-resistant dBFT extension (#116)
* support for additional phase of MEV-resistant dBFT (#118) * support for additional phase of MEV-resistant dBFT (#118)
Behaviour changes: Behaviour changes:
* simplify PublicKey interface (#114) * simplify PublicKey interface (#114)
* remove WithKeyPair callback from dBFT (#114) * remove WithKeyPair callback from dBFT (#114)
## [0.2.0] (01 April 2024) ## [0.2.0] (01 April 2024)
We're rolling out an update for dBFT that contains a substantial library interface We're rolling out an update for dBFT that contains a substantial library interface
refactoring. Starting from this version dBFT is shipped as a generic package with refactoring. Starting from this version dBFT is shipped as a generic package with
a wide range of generic interfaces, callbacks and parameters. No default payload a wide range of generic interfaces, callbacks and parameters. No default payload
implementations are supplied anymore, the library itself works only with payload implementations are supplied anymore, the library itself works only with payload
interfaces, and thus users are expected to implement the minimum required set of interfaces, and thus users are expected to implement the minimum required set of
payload interfaces by themselves. A lot of outdated and unused APIs were removed, payload interfaces by themselves. A lot of outdated and unused APIs were removed,
some of the internal APIs were renamed, so that the resulting library interface some of the internal APIs were renamed, so that the resulting library interface
is much more clear and lightweight. Also, the minimum required Go version was is much more clear and lightweight. Also, the minimum required Go version was
upgraded to Go 1.20. upgraded to Go 1.20.
Please note that no consensus-level behaviour changes introduced, this release Please note that no consensus-level behaviour changes introduced, this release
focuses only on the library APIs improvement, so it shouldn't be hard for the users focuses only on the library APIs improvement, so it shouldn't be hard for the users
to migrate to the new interface. to migrate to the new interface.
Behaviour changes: Behaviour changes:
* add generic Hash/Address parameters to `DBFT` service (#94) * add generic Hash/Address parameters to `DBFT` service (#94)
* remove custom payloads implementation from default `DBFT` service configuration * remove custom payloads implementation from default `DBFT` service configuration
(#94) (#94)
* rename `InitializeConsensus` dBFT method to `Reset` (#95) * rename `InitializeConsensus` dBFT method to `Reset` (#95)
* drop outdated dBFT `Service` interface (#95) * drop outdated dBFT `Service` interface (#95)
* move all default implementations to `internal` package (#97) * move all default implementations to `internal` package (#97)
* remove unused APIs of dBFT and payload interfaces (#104) * remove unused APIs of dBFT and payload interfaces (#104)
* timer interface refactoring (#105) * timer interface refactoring (#105)
* constructor returns some meaningful error on failed dBFT instance creation (#107) * constructor returns some meaningful error on failed dBFT instance creation (#107)
Improvements: Improvements:
* add MIT License (#78, #79) * add MIT License (#78, #79)
* documentation updates (#80, #86, #95) * documentation updates (#80, #86, #95)
* dependencies upgrades (#82, #85) * dependencies upgrades (#82, #85)
* minimum required Go version upgrade to Go 1.19 (#83) * minimum required Go version upgrade to Go 1.19 (#83)
* log messages adjustment (#88) * log messages adjustment (#88)
* untie `dbft` module from `github.com/nspcc-dev/neo-go` dependency (#94) * untie `dbft` module from `github.com/nspcc-dev/neo-go` dependency (#94)
* minimum required Go version upgrade to Go 1.20 (#100) * minimum required Go version upgrade to Go 1.20 (#100)
## [0.1.0] (15 May 2023) ## [0.1.0] (15 May 2023)
Stable dbft 2.0 implementation. Stable dbft 2.0 implementation.
[Unreleased]: https://github.com/nspcc-dev/dbft/compare/v0.3.2...master [Unreleased]: https://github.com/nspcc-dev/dbft/compare/v0.3.2...master
[0.4.0]: https://github.com/nspcc-dev/dbft/releases/v0.4.0 [0.4.0]: https://github.com/nspcc-dev/dbft/releases/v0.4.0
[0.3.2]: https://github.com/nspcc-dev/dbft/releases/v0.3.2 [0.3.2]: https://github.com/nspcc-dev/dbft/releases/v0.3.2
[0.3.1]: https://github.com/nspcc-dev/dbft/releases/v0.3.1 [0.3.1]: https://github.com/nspcc-dev/dbft/releases/v0.3.1
[0.3.0]: https://github.com/nspcc-dev/dbft/releases/v0.3.0 [0.3.0]: https://github.com/nspcc-dev/dbft/releases/v0.3.0
[0.2.0]: https://github.com/nspcc-dev/dbft/releases/v0.2.0 [0.2.0]: https://github.com/nspcc-dev/dbft/releases/v0.2.0
[0.1.0]: https://github.com/nspcc-dev/dbft/releases/v0.1.0 [0.1.0]: https://github.com/nspcc-dev/dbft/releases/v0.1.0

View File

@ -1,10 +1,10 @@
MIT License MIT License
Copyright (c) 2018-2023 NeoSPCC (@nspcc-dev) Copyright (c) 2018-2023 NeoSPCC (@nspcc-dev)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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.

View File

@ -1,30 +1,30 @@
package dbft package dbft
// Block is a generic interface for a block used by dbft. // Block is a generic interface for a block used by dbft.
type Block[H Hash] interface { type Block[H Hash] interface {
// Hash returns block hash. // Hash returns block hash.
Hash() H Hash() H
// PrevHash returns previous block hash. // PrevHash returns previous block hash.
PrevHash() H PrevHash() H
// MerkleRoot returns a merkle root of the transaction hashes. // MerkleRoot returns a merkle root of the transaction hashes.
MerkleRoot() H MerkleRoot() H
// Index returns block index. // Index returns block index.
Index() uint32 Index() uint32
// Signature returns block's signature. // Signature returns block's signature.
Signature() []byte Signature() []byte
// Sign signs block and sets it's signature. // Sign signs block and sets it's signature.
Sign(key PrivateKey) error Sign(key PrivateKey) error
// Verify checks if signature is correct. // Verify checks if signature is correct.
Verify(key PublicKey, sign []byte) error Verify(key PublicKey, sign []byte) error
// Transactions returns block's transaction list. // Transactions returns block's transaction list.
Transactions() []Transaction[H] Transactions() []Transaction[H]
// SetTransactions sets block's transaction list. For anti-MEV extension // SetTransactions sets block's transaction list. For anti-MEV extension
// transactions provided via this call are taken directly from PreBlock level // transactions provided via this call are taken directly from PreBlock level
// and thus, may be out-of-date. Thus, with anti-MEV extension enabled it's // and thus, may be out-of-date. Thus, with anti-MEV extension enabled it's
// suggested to use this method as a Block finalizer since it will be called // suggested to use this method as a Block finalizer since it will be called
// right before the block approval. Do not rely on this with anti-MEV extension // right before the block approval. Do not rely on this with anti-MEV extension
// disabled. // disabled.
SetTransactions([]Transaction[H]) SetTransactions([]Transaction[H])
} }

View File

@ -1,10 +1,10 @@
package dbft package dbft
// ChangeView represents dBFT ChangeView message. // ChangeView represents dBFT ChangeView message.
type ChangeView interface { type ChangeView interface {
// NewViewNumber returns proposed view number. // NewViewNumber returns proposed view number.
NewViewNumber() byte NewViewNumber() byte
// Reason returns change view reason. // Reason returns change view reason.
Reason() ChangeViewReason Reason() ChangeViewReason
} }

View File

@ -1,18 +1,18 @@
package dbft package dbft
//go:generate stringer -type=ChangeViewReason -linecomment //go:generate stringer -type=ChangeViewReason -linecomment
// ChangeViewReason represents a view change reason code. // ChangeViewReason represents a view change reason code.
type ChangeViewReason byte type ChangeViewReason byte
// These constants define various reasons for view changing. They're following // These constants define various reasons for view changing. They're following
// Neo 3 except the Unknown value which is left for compatibility with Neo 2. // Neo 3 except the Unknown value which is left for compatibility with Neo 2.
const ( const (
CVTimeout ChangeViewReason = 0x0 // Timeout CVTimeout ChangeViewReason = 0x0 // Timeout
CVChangeAgreement ChangeViewReason = 0x1 // ChangeAgreement CVChangeAgreement ChangeViewReason = 0x1 // ChangeAgreement
CVTxNotFound ChangeViewReason = 0x2 // TxNotFound CVTxNotFound ChangeViewReason = 0x2 // TxNotFound
CVTxRejectedByPolicy ChangeViewReason = 0x3 // TxRejectedByPolicy CVTxRejectedByPolicy ChangeViewReason = 0x3 // TxRejectedByPolicy
CVTxInvalid ChangeViewReason = 0x4 // TxInvalid CVTxInvalid ChangeViewReason = 0x4 // TxInvalid
CVBlockRejectedByPolicy ChangeViewReason = 0x5 // BlockRejectedByPolicy CVBlockRejectedByPolicy ChangeViewReason = 0x5 // BlockRejectedByPolicy
CVUnknown ChangeViewReason = 0xff // Unknown CVUnknown ChangeViewReason = 0xff // Unknown
) )

View File

@ -1,38 +1,38 @@
// Code generated by "stringer -type=ChangeViewReason -linecomment"; DO NOT EDIT. // Code generated by "stringer -type=ChangeViewReason -linecomment"; DO NOT EDIT.
package dbft package dbft
import "strconv" import "strconv"
func _() { func _() {
// An "invalid array index" compiler error signifies that the constant values have changed. // An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again. // Re-run the stringer command to generate them again.
var x [1]struct{} var x [1]struct{}
_ = x[CVTimeout-0] _ = x[CVTimeout-0]
_ = x[CVChangeAgreement-1] _ = x[CVChangeAgreement-1]
_ = x[CVTxNotFound-2] _ = x[CVTxNotFound-2]
_ = x[CVTxRejectedByPolicy-3] _ = x[CVTxRejectedByPolicy-3]
_ = x[CVTxInvalid-4] _ = x[CVTxInvalid-4]
_ = x[CVBlockRejectedByPolicy-5] _ = x[CVBlockRejectedByPolicy-5]
_ = x[CVUnknown-255] _ = x[CVUnknown-255]
} }
const ( const (
_ChangeViewReason_name_0 = "TimeoutChangeAgreementTxNotFoundTxRejectedByPolicyTxInvalidBlockRejectedByPolicy" _ChangeViewReason_name_0 = "TimeoutChangeAgreementTxNotFoundTxRejectedByPolicyTxInvalidBlockRejectedByPolicy"
_ChangeViewReason_name_1 = "Unknown" _ChangeViewReason_name_1 = "Unknown"
) )
var ( var (
_ChangeViewReason_index_0 = [...]uint8{0, 7, 22, 32, 50, 59, 80} _ChangeViewReason_index_0 = [...]uint8{0, 7, 22, 32, 50, 59, 80}
) )
func (i ChangeViewReason) String() string { func (i ChangeViewReason) String() string {
switch { switch {
case 0 <= i && i <= 5: case 0 <= i && i <= 5:
return _ChangeViewReason_name_0[_ChangeViewReason_index_0[i]:_ChangeViewReason_index_0[i+1]] return _ChangeViewReason_name_0[_ChangeViewReason_index_0[i]:_ChangeViewReason_index_0[i+1]]
case i == 255: case i == 255:
return _ChangeViewReason_name_1 return _ChangeViewReason_name_1
default: default:
return "ChangeViewReason(" + strconv.FormatInt(int64(i), 10) + ")" return "ChangeViewReason(" + strconv.FormatInt(int64(i), 10) + ")"
} }
} }

360
check.go
View File

@ -1,180 +1,180 @@
package dbft package dbft
import ( import (
"go.uber.org/zap" "go.uber.org/zap"
) )
func (d *DBFT[H]) checkPrepare() { func (d *DBFT[H]) checkPrepare() {
if d.lastBlockIndex != d.BlockIndex || d.lastBlockView != d.ViewNumber { if d.lastBlockIndex != d.BlockIndex || d.lastBlockView != d.ViewNumber {
// Notice that lastBlockTimestamp is left unchanged because // Notice that lastBlockTimestamp is left unchanged because
// this must be the value from the last header. // this must be the value from the last header.
d.lastBlockTime = d.Timer.Now() d.lastBlockTime = d.Timer.Now()
d.lastBlockIndex = d.BlockIndex d.lastBlockIndex = d.BlockIndex
d.lastBlockView = d.ViewNumber d.lastBlockView = d.ViewNumber
} }
if !d.hasAllTransactions() { if !d.hasAllTransactions() {
d.Logger.Debug("check prepare: some transactions are missing", zap.Any("hashes", d.MissingTransactions)) d.Logger.Debug("check prepare: some transactions are missing", zap.Any("hashes", d.MissingTransactions))
return return
} }
count := 0 count := 0
hasRequest := false hasRequest := false
for _, msg := range d.PreparationPayloads { for _, msg := range d.PreparationPayloads {
if msg != nil { if msg != nil {
if msg.ViewNumber() == d.ViewNumber { if msg.ViewNumber() == d.ViewNumber {
count++ count++
} }
if msg.Type() == PrepareRequestType { if msg.Type() == PrepareRequestType {
hasRequest = true hasRequest = true
} }
} }
} }
d.Logger.Debug("check preparations", zap.Bool("hasReq", hasRequest), d.Logger.Debug("check preparations", zap.Bool("hasReq", hasRequest),
zap.Int("count", count), zap.Int("count", count),
zap.Int("M", d.M())) zap.Int("M", d.M()))
if hasRequest && count >= d.M() { if hasRequest && count >= d.M() {
if d.isAntiMEVExtensionEnabled() { if d.isAntiMEVExtensionEnabled() {
d.sendPreCommit() d.sendPreCommit()
d.changeTimer(d.timePerBlock) d.changeTimer(d.timePerBlock)
d.checkPreCommit() d.checkPreCommit()
} else { } else {
d.sendCommit() d.sendCommit()
d.changeTimer(d.timePerBlock) d.changeTimer(d.timePerBlock)
d.checkCommit() d.checkCommit()
} }
} }
} }
func (d *DBFT[H]) checkPreCommit() { func (d *DBFT[H]) checkPreCommit() {
if !d.hasAllTransactions() { if !d.hasAllTransactions() {
d.Logger.Debug("check preCommit: some transactions are missing", zap.Any("hashes", d.MissingTransactions)) d.Logger.Debug("check preCommit: some transactions are missing", zap.Any("hashes", d.MissingTransactions))
return return
} }
count := 0 count := 0
for _, msg := range d.PreCommitPayloads { for _, msg := range d.PreCommitPayloads {
if msg != nil && msg.ViewNumber() == d.ViewNumber { if msg != nil && msg.ViewNumber() == d.ViewNumber {
count++ count++
} }
} }
if count < d.M() { if count < d.M() {
d.Logger.Debug("not enough PreCommits to process PreBlock", zap.Int("count", count)) d.Logger.Debug("not enough PreCommits to process PreBlock", zap.Int("count", count))
return return
} }
d.preBlock = d.CreatePreBlock() d.preBlock = d.CreatePreBlock()
if !d.preBlockProcessed { if !d.preBlockProcessed {
d.Logger.Info("processing PreBlock", d.Logger.Info("processing PreBlock",
zap.Uint32("height", d.BlockIndex), zap.Uint32("height", d.BlockIndex),
zap.Uint("view", uint(d.ViewNumber)), zap.Uint("view", uint(d.ViewNumber)),
zap.Int("tx_count", len(d.preBlock.Transactions())), zap.Int("tx_count", len(d.preBlock.Transactions())),
zap.Int("preCommit_count", count)) zap.Int("preCommit_count", count))
err := d.ProcessPreBlock(d.preBlock) err := d.ProcessPreBlock(d.preBlock)
if err != nil { if err != nil {
d.Logger.Info("can't process PreBlock, waiting for more PreCommits to be collected", d.Logger.Info("can't process PreBlock, waiting for more PreCommits to be collected",
zap.Error(err), zap.Error(err),
zap.Int("count", count)) zap.Int("count", count))
return return
} }
d.preBlockProcessed = true d.preBlockProcessed = true
} }
// Require PreCommit sent by self for reliability. This condition must not be // Require PreCommit sent by self for reliability. This condition must not be
// removed because: // removed because:
// 1) we need to filter out WatchOnly nodes; // 1) we need to filter out WatchOnly nodes;
// 2) CNs that have not sent PreCommit must not skip this stage (although it's OK // 2) CNs that have not sent PreCommit must not skip this stage (although it's OK
// from the DKG/TPKE side to build final Block based only on other CN's data). // from the DKG/TPKE side to build final Block based only on other CN's data).
if d.PreCommitSent() { if d.PreCommitSent() {
d.verifyCommitPayloadsAgainstHeader() d.verifyCommitPayloadsAgainstHeader()
d.sendCommit() d.sendCommit()
d.changeTimer(d.timePerBlock) d.changeTimer(d.timePerBlock)
d.checkCommit() d.checkCommit()
} else { } else {
if !d.Context.WatchOnly() { if !d.Context.WatchOnly() {
d.Logger.Debug("can't send commit since self preCommit not yet sent") d.Logger.Debug("can't send commit since self preCommit not yet sent")
} }
} }
} }
func (d *DBFT[H]) checkCommit() { func (d *DBFT[H]) checkCommit() {
if !d.hasAllTransactions() { if !d.hasAllTransactions() {
d.Logger.Debug("check commit: some transactions are missing", zap.Any("hashes", d.MissingTransactions)) d.Logger.Debug("check commit: some transactions are missing", zap.Any("hashes", d.MissingTransactions))
return return
} }
// return if we received commits from other nodes // return if we received commits from other nodes
// before receiving PrepareRequest from Speaker // before receiving PrepareRequest from Speaker
count := 0 count := 0
for _, msg := range d.CommitPayloads { for _, msg := range d.CommitPayloads {
if msg != nil && msg.ViewNumber() == d.ViewNumber { if msg != nil && msg.ViewNumber() == d.ViewNumber {
count++ count++
} }
} }
if count < d.M() { if count < d.M() {
d.Logger.Debug("not enough to commit", zap.Int("count", count)) d.Logger.Debug("not enough to commit", zap.Int("count", count))
return return
} }
d.block = d.CreateBlock() d.block = d.CreateBlock()
hash := d.block.Hash() hash := d.block.Hash()
d.Logger.Info("approving block", d.Logger.Info("approving block",
zap.Uint32("height", d.BlockIndex), zap.Uint32("height", d.BlockIndex),
zap.Stringer("hash", hash), zap.Stringer("hash", hash),
zap.Int("tx_count", len(d.block.Transactions())), zap.Int("tx_count", len(d.block.Transactions())),
zap.Stringer("merkle", d.block.MerkleRoot()), zap.Stringer("merkle", d.block.MerkleRoot()),
zap.Stringer("prev", d.block.PrevHash())) zap.Stringer("prev", d.block.PrevHash()))
err := d.ProcessBlock(d.block) err := d.ProcessBlock(d.block)
if err != nil { if err != nil {
if d.isAntiMEVExtensionEnabled() { if d.isAntiMEVExtensionEnabled() {
d.Logger.Info("can't process Block, waiting for more Commits to be collected", d.Logger.Info("can't process Block, waiting for more Commits to be collected",
zap.Error(err), zap.Error(err),
zap.Int("count", count)) zap.Int("count", count))
return return
} }
d.Logger.Fatal("block processing failed", zap.Error(err)) d.Logger.Fatal("block processing failed", zap.Error(err))
} }
d.blockProcessed = true d.blockProcessed = true
// Do not initialize consensus process immediately. It's the caller's duty to // Do not initialize consensus process immediately. It's the caller's duty to
// start the new block acceptance process and call Reset at the // start the new block acceptance process and call Reset at the
// new height. // new height.
} }
func (d *DBFT[H]) checkChangeView(view byte) { func (d *DBFT[H]) checkChangeView(view byte) {
if d.ViewNumber >= view { if d.ViewNumber >= view {
return return
} }
count := 0 count := 0
for _, msg := range d.ChangeViewPayloads { for _, msg := range d.ChangeViewPayloads {
if msg != nil && msg.GetChangeView().NewViewNumber() >= view { if msg != nil && msg.GetChangeView().NewViewNumber() >= view {
count++ count++
} }
} }
if count < d.M() { if count < d.M() {
return return
} }
if !d.Context.WatchOnly() { if !d.Context.WatchOnly() {
msg := d.ChangeViewPayloads[d.MyIndex] msg := d.ChangeViewPayloads[d.MyIndex]
if msg != nil && msg.GetChangeView().NewViewNumber() < view { if msg != nil && msg.GetChangeView().NewViewNumber() < view {
d.broadcast(d.makeChangeView(uint64(d.Timer.Now().UnixNano()), CVChangeAgreement)) d.broadcast(d.makeChangeView(uint64(d.Timer.Now().UnixNano()), CVChangeAgreement))
} }
} }
d.initializeConsensus(view, d.lastBlockTimestamp) d.initializeConsensus(view, d.lastBlockTimestamp)
} }

View File

@ -1,9 +1,9 @@
package dbft package dbft
// Commit is an interface for dBFT Commit message. // Commit is an interface for dBFT Commit message.
type Commit interface { type Commit interface {
// Signature returns commit's signature field // Signature returns commit's signature field
// which is a final block signature for the current epoch for both dBFT 2.0 and // which is a final block signature for the current epoch for both dBFT 2.0 and
// for anti-MEV extension. // for anti-MEV extension.
Signature() []byte Signature() []byte
} }

930
config.go
View File

@ -1,465 +1,465 @@
package dbft package dbft
import ( import (
"errors" "errors"
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
) )
// Config contains initialization and working parameters for dBFT. // Config contains initialization and working parameters for dBFT.
type Config[H Hash] struct { type Config[H Hash] struct {
// Logger // Logger
Logger *zap.Logger Logger *zap.Logger
// Timer // Timer
Timer Timer Timer Timer
// TimePerBlock is the minimum time that needs to pass before another block // TimePerBlock is the minimum time that needs to pass before another block
// will be accepted even if there are pending transactions in the node's // will be accepted even if there are pending transactions in the node's
// mempool. This value may be updated every block. // mempool. This value may be updated every block.
TimePerBlock func() time.Duration TimePerBlock func() time.Duration
// MaxTimePerBlock is the maximum time that may pass before another block is // MaxTimePerBlock is the maximum time that may pass before another block is
// accepted if there are no pending transactions in the node's mempool. This // accepted if there are no pending transactions in the node's mempool. This
// value may be updated every block. If set, enables dynamic block time // value may be updated every block. If set, enables dynamic block time
// extension: blocks are accepted with interval from TimePerBlock to // extension: blocks are accepted with interval from TimePerBlock to
// MaxTimePerBlock (in CV-less scenario) depending on the presence of // MaxTimePerBlock (in CV-less scenario) depending on the presence of
// transactions in the node's pool, ref. // transactions in the node's pool, ref.
// https://github.com/neo-project/neo/issues/4018. // https://github.com/neo-project/neo/issues/4018.
MaxTimePerBlock func() time.Duration MaxTimePerBlock func() time.Duration
// TimestampIncrement increment is the amount of units to add to timestamp // TimestampIncrement increment is the amount of units to add to timestamp
// if current time is less than that of previous context. // if current time is less than that of previous context.
// By default use millisecond precision. // By default use millisecond precision.
TimestampIncrement uint64 TimestampIncrement uint64
// AntiMEVExtensionEnablingHeight denotes the height starting from which dBFT // AntiMEVExtensionEnablingHeight denotes the height starting from which dBFT
// Anti-MEV extensions should be enabled. -1 means no extension is enabled. // Anti-MEV extensions should be enabled. -1 means no extension is enabled.
AntiMEVExtensionEnablingHeight int64 AntiMEVExtensionEnablingHeight int64
// GetKeyPair returns an index of the node in the list of validators // GetKeyPair returns an index of the node in the list of validators
// together with it's key pair. // together with it's key pair.
GetKeyPair func([]PublicKey) (int, PrivateKey, PublicKey) GetKeyPair func([]PublicKey) (int, PrivateKey, PublicKey)
// NewPreBlockFromContext should allocate, fill from Context and return new block.PreBlock. // NewPreBlockFromContext should allocate, fill from Context and return new block.PreBlock.
NewPreBlockFromContext func(ctx *Context[H]) PreBlock[H] NewPreBlockFromContext func(ctx *Context[H]) PreBlock[H]
// NewBlockFromContext should allocate, fill from Context and return new block.Block. // NewBlockFromContext should allocate, fill from Context and return new block.Block.
NewBlockFromContext func(ctx *Context[H]) Block[H] NewBlockFromContext func(ctx *Context[H]) Block[H]
// RequestTx is a callback which is called when transaction contained // RequestTx is a callback which is called when transaction contained
// in current block can't be found in memory pool. The slice received by // in current block can't be found in memory pool. The slice received by
// this callback MUST NOT be changed. // this callback MUST NOT be changed.
RequestTx func(h ...H) RequestTx func(h ...H)
// SubscribeForTxs is a callback which is called when dBFT needs to track incoming // SubscribeForTxs is a callback which is called when dBFT needs to track incoming
// mempool transactions. Subscription is supposed to be single-use, no unsubscription // mempool transactions. Subscription is supposed to be single-use, no unsubscription
// is initiated by dBFT, hence it's the user's duty to manage and release resources. // is initiated by dBFT, hence it's the user's duty to manage and release resources.
// This callback is active iff MaxTimePerBlock is set. // This callback is active iff MaxTimePerBlock is set.
SubscribeForTxs func() SubscribeForTxs func()
// StopTxFlow is a callback which is called when the process no longer needs // StopTxFlow is a callback which is called when the process no longer needs
// any transactions. // any transactions.
StopTxFlow func() StopTxFlow func()
// GetTx returns a transaction from memory pool. // GetTx returns a transaction from memory pool.
GetTx func(h H) Transaction[H] GetTx func(h H) Transaction[H]
// GetVerified returns a slice of verified transactions // GetVerified returns a slice of verified transactions
// to be proposed in a new block. // to be proposed in a new block.
GetVerified func() []Transaction[H] GetVerified func() []Transaction[H]
// VerifyPreBlock verifies if preBlock is valid. // VerifyPreBlock verifies if preBlock is valid.
VerifyPreBlock func(b PreBlock[H]) bool VerifyPreBlock func(b PreBlock[H]) bool
// VerifyBlock verifies if block is valid. // VerifyBlock verifies if block is valid.
VerifyBlock func(b Block[H]) bool VerifyBlock func(b Block[H]) bool
// Broadcast should broadcast payload m to the consensus nodes. // Broadcast should broadcast payload m to the consensus nodes.
Broadcast func(m ConsensusPayload[H]) Broadcast func(m ConsensusPayload[H])
// ProcessBlock is called every time new preBlock is accepted. // ProcessBlock is called every time new preBlock is accepted.
ProcessPreBlock func(b PreBlock[H]) error ProcessPreBlock func(b PreBlock[H]) error
// ProcessBlock is called every time new block is accepted. // ProcessBlock is called every time new block is accepted.
ProcessBlock func(b Block[H]) error ProcessBlock func(b Block[H]) error
// GetBlock should return block with hash. // GetBlock should return block with hash.
GetBlock func(h H) Block[H] GetBlock func(h H) Block[H]
// WatchOnly tells if a node should only watch. // WatchOnly tells if a node should only watch.
WatchOnly func() bool WatchOnly func() bool
// CurrentHeight returns index of the last accepted block. // CurrentHeight returns index of the last accepted block.
CurrentHeight func() uint32 CurrentHeight func() uint32
// CurrentBlockHash returns hash of the last accepted block. // CurrentBlockHash returns hash of the last accepted block.
CurrentBlockHash func() H CurrentBlockHash func() H
// GetValidators returns list of the validators. // GetValidators returns list of the validators.
// When called with a transaction list it must return // When called with a transaction list it must return
// list of the validators of the next block. // list of the validators of the next block.
// If this function ever returns 0-length slice, dbft will panic. // If this function ever returns 0-length slice, dbft will panic.
GetValidators func(...Transaction[H]) []PublicKey GetValidators func(...Transaction[H]) []PublicKey
// NewConsensusPayload is a constructor for payload.ConsensusPayload. // NewConsensusPayload is a constructor for payload.ConsensusPayload.
NewConsensusPayload func(*Context[H], MessageType, any) ConsensusPayload[H] NewConsensusPayload func(*Context[H], MessageType, any) ConsensusPayload[H]
// NewPrepareRequest is a constructor for payload.PrepareRequest. // NewPrepareRequest is a constructor for payload.PrepareRequest.
NewPrepareRequest func(ts uint64, nonce uint64, transactionHashes []H) PrepareRequest[H] NewPrepareRequest func(ts uint64, nonce uint64, transactionHashes []H) PrepareRequest[H]
// NewPrepareResponse is a constructor for payload.PrepareResponse. // NewPrepareResponse is a constructor for payload.PrepareResponse.
NewPrepareResponse func(preparationHash H) PrepareResponse[H] NewPrepareResponse func(preparationHash H) PrepareResponse[H]
// NewChangeView is a constructor for payload.ChangeView. // NewChangeView is a constructor for payload.ChangeView.
NewChangeView func(newViewNumber byte, reason ChangeViewReason, timestamp uint64) ChangeView NewChangeView func(newViewNumber byte, reason ChangeViewReason, timestamp uint64) ChangeView
// NewPreCommit is a constructor for payload.PreCommit. // NewPreCommit is a constructor for payload.PreCommit.
NewPreCommit func(data []byte) PreCommit NewPreCommit func(data []byte) PreCommit
// NewCommit is a constructor for payload.Commit. // NewCommit is a constructor for payload.Commit.
NewCommit func(signature []byte) Commit NewCommit func(signature []byte) Commit
// NewRecoveryRequest is a constructor for payload.RecoveryRequest. // NewRecoveryRequest is a constructor for payload.RecoveryRequest.
NewRecoveryRequest func(ts uint64) RecoveryRequest NewRecoveryRequest func(ts uint64) RecoveryRequest
// NewRecoveryMessage is a constructor for payload.RecoveryMessage. // NewRecoveryMessage is a constructor for payload.RecoveryMessage.
NewRecoveryMessage func() RecoveryMessage[H] NewRecoveryMessage func() RecoveryMessage[H]
// VerifyPrepareRequest can perform external payload verification and returns true iff it was successful. // VerifyPrepareRequest can perform external payload verification and returns true iff it was successful.
VerifyPrepareRequest func(p ConsensusPayload[H]) error VerifyPrepareRequest func(p ConsensusPayload[H]) error
// VerifyPrepareResponse performs external PrepareResponse verification and returns nil if it's successful. // VerifyPrepareResponse performs external PrepareResponse verification and returns nil if it's successful.
VerifyPrepareResponse func(p ConsensusPayload[H]) error VerifyPrepareResponse func(p ConsensusPayload[H]) error
// VerifyPreCommit performs external PreCommit verification and returns nil if it's successful. // VerifyPreCommit performs external PreCommit verification and returns nil if it's successful.
// Note that PreBlock-dependent PreCommit verification should be performed inside PreBlock.Verify // Note that PreBlock-dependent PreCommit verification should be performed inside PreBlock.Verify
// callback. // callback.
VerifyPreCommit func(p ConsensusPayload[H]) error VerifyPreCommit func(p ConsensusPayload[H]) error
// VerifyCommit performs external Commit verification and returns nil if it's successful. // VerifyCommit performs external Commit verification and returns nil if it's successful.
// Note that Block-dependent Commit verification should be performed inside Block.Verify // Note that Block-dependent Commit verification should be performed inside Block.Verify
// callback. // callback.
VerifyCommit func(p ConsensusPayload[H]) error VerifyCommit func(p ConsensusPayload[H]) error
} }
const defaultSecondsPerBlock = time.Second * 15 const defaultSecondsPerBlock = time.Second * 15
const defaultTimestampIncrement = uint64(time.Millisecond / time.Nanosecond) const defaultTimestampIncrement = uint64(time.Millisecond / time.Nanosecond)
func defaultConfig[H Hash]() *Config[H] { func defaultConfig[H Hash]() *Config[H] {
// fields which are set to nil must be provided from client // fields which are set to nil must be provided from client
return &Config[H]{ return &Config[H]{
Logger: zap.NewNop(), Logger: zap.NewNop(),
TimePerBlock: func() time.Duration { return defaultSecondsPerBlock }, TimePerBlock: func() time.Duration { return defaultSecondsPerBlock },
TimestampIncrement: defaultTimestampIncrement, TimestampIncrement: defaultTimestampIncrement,
GetKeyPair: nil, GetKeyPair: nil,
RequestTx: func(...H) {}, RequestTx: func(...H) {},
StopTxFlow: func() {}, StopTxFlow: func() {},
GetTx: func(H) Transaction[H] { return nil }, GetTx: func(H) Transaction[H] { return nil },
GetVerified: func() []Transaction[H] { return make([]Transaction[H], 0) }, GetVerified: func() []Transaction[H] { return make([]Transaction[H], 0) },
VerifyBlock: func(Block[H]) bool { return true }, VerifyBlock: func(Block[H]) bool { return true },
Broadcast: func(ConsensusPayload[H]) {}, Broadcast: func(ConsensusPayload[H]) {},
ProcessBlock: func(Block[H]) error { return nil }, ProcessBlock: func(Block[H]) error { return nil },
GetBlock: func(H) Block[H] { return nil }, GetBlock: func(H) Block[H] { return nil },
WatchOnly: func() bool { return false }, WatchOnly: func() bool { return false },
CurrentHeight: nil, CurrentHeight: nil,
CurrentBlockHash: nil, CurrentBlockHash: nil,
GetValidators: nil, GetValidators: nil,
VerifyPrepareRequest: func(ConsensusPayload[H]) error { return nil }, VerifyPrepareRequest: func(ConsensusPayload[H]) error { return nil },
VerifyPrepareResponse: func(ConsensusPayload[H]) error { return nil }, VerifyPrepareResponse: func(ConsensusPayload[H]) error { return nil },
VerifyCommit: func(ConsensusPayload[H]) error { return nil }, VerifyCommit: func(ConsensusPayload[H]) error { return nil },
AntiMEVExtensionEnablingHeight: -1, AntiMEVExtensionEnablingHeight: -1,
VerifyPreBlock: func(PreBlock[H]) bool { return true }, VerifyPreBlock: func(PreBlock[H]) bool { return true },
VerifyPreCommit: func(ConsensusPayload[H]) error { return nil }, VerifyPreCommit: func(ConsensusPayload[H]) error { return nil },
} }
} }
func checkConfig[H Hash](cfg *Config[H]) error { func checkConfig[H Hash](cfg *Config[H]) error {
if cfg.GetKeyPair == nil { if cfg.GetKeyPair == nil {
return errors.New("private key is nil") return errors.New("private key is nil")
} }
if cfg.Timer == nil { if cfg.Timer == nil {
return errors.New("Timer is nil") return errors.New("Timer is nil")
} }
if cfg.CurrentHeight == nil { if cfg.CurrentHeight == nil {
return errors.New("CurrentHeight is nil") return errors.New("CurrentHeight is nil")
} }
if cfg.CurrentBlockHash == nil { if cfg.CurrentBlockHash == nil {
return errors.New("CurrentBlockHash is nil") return errors.New("CurrentBlockHash is nil")
} }
if cfg.GetValidators == nil { if cfg.GetValidators == nil {
return errors.New("GetValidators is nil") return errors.New("GetValidators is nil")
} }
if cfg.NewBlockFromContext == nil { if cfg.NewBlockFromContext == nil {
return errors.New("NewBlockFromContext is nil") return errors.New("NewBlockFromContext is nil")
} }
if cfg.NewConsensusPayload == nil { if cfg.NewConsensusPayload == nil {
return errors.New("NewConsensusPayload is nil") return errors.New("NewConsensusPayload is nil")
} }
if cfg.NewPrepareRequest == nil { if cfg.NewPrepareRequest == nil {
return errors.New("NewPrepareRequest is nil") return errors.New("NewPrepareRequest is nil")
} }
if cfg.NewPrepareResponse == nil { if cfg.NewPrepareResponse == nil {
return errors.New("NewPrepareResponse is nil") return errors.New("NewPrepareResponse is nil")
} }
if cfg.NewChangeView == nil { if cfg.NewChangeView == nil {
return errors.New("NewChangeView is nil") return errors.New("NewChangeView is nil")
} }
if cfg.NewCommit == nil { if cfg.NewCommit == nil {
return errors.New("NewCommit is nil") return errors.New("NewCommit is nil")
} }
if cfg.NewRecoveryRequest == nil { if cfg.NewRecoveryRequest == nil {
return errors.New("NewRecoveryRequest is nil") return errors.New("NewRecoveryRequest is nil")
} }
if cfg.NewRecoveryMessage == nil { if cfg.NewRecoveryMessage == nil {
return errors.New("NewRecoveryMessage is nil") return errors.New("NewRecoveryMessage is nil")
} }
if cfg.AntiMEVExtensionEnablingHeight >= 0 { if cfg.AntiMEVExtensionEnablingHeight >= 0 {
if cfg.NewPreBlockFromContext == nil { if cfg.NewPreBlockFromContext == nil {
return errors.New("NewPreBlockFromContext is nil") return errors.New("NewPreBlockFromContext is nil")
} }
if cfg.ProcessPreBlock == nil { if cfg.ProcessPreBlock == nil {
return errors.New("ProcessPreBlock is nil") return errors.New("ProcessPreBlock is nil")
} }
if cfg.NewPreCommit == nil { if cfg.NewPreCommit == nil {
return errors.New("NewPreCommit is nil") return errors.New("NewPreCommit is nil")
} }
} else { } else {
if cfg.NewPreBlockFromContext != nil { if cfg.NewPreBlockFromContext != nil {
return errors.New("NewPreBlockFromContext is set, but AntiMEVExtensionEnablingHeight is not specified") return errors.New("NewPreBlockFromContext is set, but AntiMEVExtensionEnablingHeight is not specified")
} }
if cfg.ProcessPreBlock != nil { if cfg.ProcessPreBlock != nil {
return errors.New("ProcessPreBlock is set, but AntiMEVExtensionEnablingHeight is not specified") return errors.New("ProcessPreBlock is set, but AntiMEVExtensionEnablingHeight is not specified")
} }
if cfg.NewPreCommit != nil { if cfg.NewPreCommit != nil {
return errors.New("NewPreCommit is set, but AntiMEVExtensionEnablingHeight is not specified") return errors.New("NewPreCommit is set, but AntiMEVExtensionEnablingHeight is not specified")
} }
} }
if (cfg.MaxTimePerBlock == nil) != (cfg.SubscribeForTxs == nil) { if (cfg.MaxTimePerBlock == nil) != (cfg.SubscribeForTxs == nil) {
return errors.New("MaxTimePerBlock and SubscribeForTxs should be specified/not specified at the same time") return errors.New("MaxTimePerBlock and SubscribeForTxs should be specified/not specified at the same time")
} }
return nil return nil
} }
// WithGetKeyPair sets GetKeyPair. // WithGetKeyPair sets GetKeyPair.
func WithGetKeyPair[H Hash](f func(pubs []PublicKey) (int, PrivateKey, PublicKey)) func(config *Config[H]) { func WithGetKeyPair[H Hash](f func(pubs []PublicKey) (int, PrivateKey, PublicKey)) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.GetKeyPair = f cfg.GetKeyPair = f
} }
} }
// WithLogger sets Logger. // WithLogger sets Logger.
func WithLogger[H Hash](log *zap.Logger) func(config *Config[H]) { func WithLogger[H Hash](log *zap.Logger) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.Logger = log cfg.Logger = log
} }
} }
// WithTimer sets Timer. // WithTimer sets Timer.
func WithTimer[H Hash](t Timer) func(config *Config[H]) { func WithTimer[H Hash](t Timer) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.Timer = t cfg.Timer = t
} }
} }
// WithTimePerBlock sets TimePerBlock. // WithTimePerBlock sets TimePerBlock.
func WithTimePerBlock[H Hash](f func() time.Duration) func(config *Config[H]) { func WithTimePerBlock[H Hash](f func() time.Duration) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.TimePerBlock = f cfg.TimePerBlock = f
} }
} }
// WithMaxTimePerBlock sets MaxTimePerBlock. // WithMaxTimePerBlock sets MaxTimePerBlock.
func WithMaxTimePerBlock[H Hash](f func() time.Duration) func(config *Config[H]) { func WithMaxTimePerBlock[H Hash](f func() time.Duration) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.MaxTimePerBlock = f cfg.MaxTimePerBlock = f
} }
} }
// WithAntiMEVExtensionEnablingHeight sets AntiMEVExtensionEnablingHeight. // WithAntiMEVExtensionEnablingHeight sets AntiMEVExtensionEnablingHeight.
func WithAntiMEVExtensionEnablingHeight[H Hash](h int64) func(config *Config[H]) { func WithAntiMEVExtensionEnablingHeight[H Hash](h int64) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.AntiMEVExtensionEnablingHeight = h cfg.AntiMEVExtensionEnablingHeight = h
} }
} }
// WithTimestampIncrement sets TimestampIncrement. // WithTimestampIncrement sets TimestampIncrement.
func WithTimestampIncrement[H Hash](u uint64) func(config *Config[H]) { func WithTimestampIncrement[H Hash](u uint64) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.TimestampIncrement = u cfg.TimestampIncrement = u
} }
} }
// WithNewPreBlockFromContext sets NewPreBlockFromContext. // WithNewPreBlockFromContext sets NewPreBlockFromContext.
func WithNewPreBlockFromContext[H Hash](f func(ctx *Context[H]) PreBlock[H]) func(config *Config[H]) { func WithNewPreBlockFromContext[H Hash](f func(ctx *Context[H]) PreBlock[H]) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.NewPreBlockFromContext = f cfg.NewPreBlockFromContext = f
} }
} }
// WithNewBlockFromContext sets NewBlockFromContext. // WithNewBlockFromContext sets NewBlockFromContext.
func WithNewBlockFromContext[H Hash](f func(ctx *Context[H]) Block[H]) func(config *Config[H]) { func WithNewBlockFromContext[H Hash](f func(ctx *Context[H]) Block[H]) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.NewBlockFromContext = f cfg.NewBlockFromContext = f
} }
} }
// WithRequestTx sets RequestTx. // WithRequestTx sets RequestTx.
func WithRequestTx[H Hash](f func(h ...H)) func(config *Config[H]) { func WithRequestTx[H Hash](f func(h ...H)) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.RequestTx = f cfg.RequestTx = f
} }
} }
// WithSubscribeForTxs sets SubscribeForTxs. // WithSubscribeForTxs sets SubscribeForTxs.
func WithSubscribeForTxs[H Hash](f func()) func(config *Config[H]) { func WithSubscribeForTxs[H Hash](f func()) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.SubscribeForTxs = f cfg.SubscribeForTxs = f
} }
} }
// WithStopTxFlow sets StopTxFlow. // WithStopTxFlow sets StopTxFlow.
func WithStopTxFlow[H Hash](f func()) func(config *Config[H]) { func WithStopTxFlow[H Hash](f func()) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.StopTxFlow = f cfg.StopTxFlow = f
} }
} }
// WithGetTx sets GetTx. // WithGetTx sets GetTx.
func WithGetTx[H Hash](f func(h H) Transaction[H]) func(config *Config[H]) { func WithGetTx[H Hash](f func(h H) Transaction[H]) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.GetTx = f cfg.GetTx = f
} }
} }
// WithGetVerified sets GetVerified. // WithGetVerified sets GetVerified.
func WithGetVerified[H Hash](f func() []Transaction[H]) func(config *Config[H]) { func WithGetVerified[H Hash](f func() []Transaction[H]) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.GetVerified = f cfg.GetVerified = f
} }
} }
// WithVerifyPreBlock sets VerifyPreBlock. // WithVerifyPreBlock sets VerifyPreBlock.
func WithVerifyPreBlock[H Hash](f func(b PreBlock[H]) bool) func(config *Config[H]) { func WithVerifyPreBlock[H Hash](f func(b PreBlock[H]) bool) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.VerifyPreBlock = f cfg.VerifyPreBlock = f
} }
} }
// WithVerifyBlock sets VerifyBlock. // WithVerifyBlock sets VerifyBlock.
func WithVerifyBlock[H Hash](f func(b Block[H]) bool) func(config *Config[H]) { func WithVerifyBlock[H Hash](f func(b Block[H]) bool) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.VerifyBlock = f cfg.VerifyBlock = f
} }
} }
// WithBroadcast sets Broadcast. // WithBroadcast sets Broadcast.
func WithBroadcast[H Hash](f func(m ConsensusPayload[H])) func(config *Config[H]) { func WithBroadcast[H Hash](f func(m ConsensusPayload[H])) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.Broadcast = f cfg.Broadcast = f
} }
} }
// WithProcessBlock sets ProcessBlock callback. Note that for anti-MEV extension // WithProcessBlock sets ProcessBlock callback. Note that for anti-MEV extension
// disabled non-nil error return is a no-op. // disabled non-nil error return is a no-op.
func WithProcessBlock[H Hash](f func(b Block[H]) error) func(config *Config[H]) { func WithProcessBlock[H Hash](f func(b Block[H]) error) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.ProcessBlock = f cfg.ProcessBlock = f
} }
} }
// WithProcessPreBlock sets ProcessPreBlock. // WithProcessPreBlock sets ProcessPreBlock.
func WithProcessPreBlock[H Hash](f func(b PreBlock[H]) error) func(config *Config[H]) { func WithProcessPreBlock[H Hash](f func(b PreBlock[H]) error) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.ProcessPreBlock = f cfg.ProcessPreBlock = f
} }
} }
// WithGetBlock sets GetBlock. // WithGetBlock sets GetBlock.
func WithGetBlock[H Hash](f func(h H) Block[H]) func(config *Config[H]) { func WithGetBlock[H Hash](f func(h H) Block[H]) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.GetBlock = f cfg.GetBlock = f
} }
} }
// WithWatchOnly sets WatchOnly. // WithWatchOnly sets WatchOnly.
func WithWatchOnly[H Hash](f func() bool) func(config *Config[H]) { func WithWatchOnly[H Hash](f func() bool) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.WatchOnly = f cfg.WatchOnly = f
} }
} }
// WithCurrentHeight sets CurrentHeight. // WithCurrentHeight sets CurrentHeight.
func WithCurrentHeight[H Hash](f func() uint32) func(config *Config[H]) { func WithCurrentHeight[H Hash](f func() uint32) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.CurrentHeight = f cfg.CurrentHeight = f
} }
} }
// WithCurrentBlockHash sets CurrentBlockHash. // WithCurrentBlockHash sets CurrentBlockHash.
func WithCurrentBlockHash[H Hash](f func() H) func(config *Config[H]) { func WithCurrentBlockHash[H Hash](f func() H) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.CurrentBlockHash = f cfg.CurrentBlockHash = f
} }
} }
// WithGetValidators sets GetValidators. // WithGetValidators sets GetValidators.
func WithGetValidators[H Hash](f func(txs ...Transaction[H]) []PublicKey) func(config *Config[H]) { func WithGetValidators[H Hash](f func(txs ...Transaction[H]) []PublicKey) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.GetValidators = f cfg.GetValidators = f
} }
} }
// WithNewConsensusPayload sets NewConsensusPayload. // WithNewConsensusPayload sets NewConsensusPayload.
func WithNewConsensusPayload[H Hash](f func(ctx *Context[H], typ MessageType, msg any) ConsensusPayload[H]) func(config *Config[H]) { func WithNewConsensusPayload[H Hash](f func(ctx *Context[H], typ MessageType, msg any) ConsensusPayload[H]) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.NewConsensusPayload = f cfg.NewConsensusPayload = f
} }
} }
// WithNewPrepareRequest sets NewPrepareRequest. // WithNewPrepareRequest sets NewPrepareRequest.
func WithNewPrepareRequest[H Hash](f func(ts uint64, nonce uint64, transactionsHashes []H) PrepareRequest[H]) func(config *Config[H]) { func WithNewPrepareRequest[H Hash](f func(ts uint64, nonce uint64, transactionsHashes []H) PrepareRequest[H]) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.NewPrepareRequest = f cfg.NewPrepareRequest = f
} }
} }
// WithNewPrepareResponse sets NewPrepareResponse. // WithNewPrepareResponse sets NewPrepareResponse.
func WithNewPrepareResponse[H Hash](f func(preparationHash H) PrepareResponse[H]) func(config *Config[H]) { func WithNewPrepareResponse[H Hash](f func(preparationHash H) PrepareResponse[H]) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.NewPrepareResponse = f cfg.NewPrepareResponse = f
} }
} }
// WithNewChangeView sets NewChangeView. // WithNewChangeView sets NewChangeView.
func WithNewChangeView[H Hash](f func(newViewNumber byte, reason ChangeViewReason, ts uint64) ChangeView) func(config *Config[H]) { func WithNewChangeView[H Hash](f func(newViewNumber byte, reason ChangeViewReason, ts uint64) ChangeView) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.NewChangeView = f cfg.NewChangeView = f
} }
} }
// WithNewCommit sets NewCommit. // WithNewCommit sets NewCommit.
func WithNewCommit[H Hash](f func(signature []byte) Commit) func(config *Config[H]) { func WithNewCommit[H Hash](f func(signature []byte) Commit) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.NewCommit = f cfg.NewCommit = f
} }
} }
// WithNewPreCommit sets NewPreCommit. // WithNewPreCommit sets NewPreCommit.
func WithNewPreCommit[H Hash](f func(signature []byte) PreCommit) func(config *Config[H]) { func WithNewPreCommit[H Hash](f func(signature []byte) PreCommit) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.NewPreCommit = f cfg.NewPreCommit = f
} }
} }
// WithNewRecoveryRequest sets NewRecoveryRequest. // WithNewRecoveryRequest sets NewRecoveryRequest.
func WithNewRecoveryRequest[H Hash](f func(ts uint64) RecoveryRequest) func(config *Config[H]) { func WithNewRecoveryRequest[H Hash](f func(ts uint64) RecoveryRequest) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.NewRecoveryRequest = f cfg.NewRecoveryRequest = f
} }
} }
// WithNewRecoveryMessage sets NewRecoveryMessage. // WithNewRecoveryMessage sets NewRecoveryMessage.
func WithNewRecoveryMessage[H Hash](f func() RecoveryMessage[H]) func(config *Config[H]) { func WithNewRecoveryMessage[H Hash](f func() RecoveryMessage[H]) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.NewRecoveryMessage = f cfg.NewRecoveryMessage = f
} }
} }
// WithVerifyPrepareRequest sets VerifyPrepareRequest. // WithVerifyPrepareRequest sets VerifyPrepareRequest.
func WithVerifyPrepareRequest[H Hash](f func(prepareReq ConsensusPayload[H]) error) func(config *Config[H]) { func WithVerifyPrepareRequest[H Hash](f func(prepareReq ConsensusPayload[H]) error) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.VerifyPrepareRequest = f cfg.VerifyPrepareRequest = f
} }
} }
// WithVerifyPrepareResponse sets VerifyPrepareResponse. // WithVerifyPrepareResponse sets VerifyPrepareResponse.
func WithVerifyPrepareResponse[H Hash](f func(prepareResp ConsensusPayload[H]) error) func(config *Config[H]) { func WithVerifyPrepareResponse[H Hash](f func(prepareResp ConsensusPayload[H]) error) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.VerifyPrepareResponse = f cfg.VerifyPrepareResponse = f
} }
} }
// WithVerifyPreCommit sets VerifyPreCommit. // WithVerifyPreCommit sets VerifyPreCommit.
func WithVerifyPreCommit[H Hash](f func(preCommit ConsensusPayload[H]) error) func(config *Config[H]) { func WithVerifyPreCommit[H Hash](f func(preCommit ConsensusPayload[H]) error) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.VerifyPreCommit = f cfg.VerifyPreCommit = f
} }
} }
// WithVerifyCommit sets VerifyCommit. // WithVerifyCommit sets VerifyCommit.
func WithVerifyCommit[H Hash](f func(commit ConsensusPayload[H]) error) func(config *Config[H]) { func WithVerifyCommit[H Hash](f func(commit ConsensusPayload[H]) error) func(config *Config[H]) {
return func(cfg *Config[H]) { return func(cfg *Config[H]) {
cfg.VerifyCommit = f cfg.VerifyCommit = f
} }
} }

View File

@ -1,26 +1,26 @@
package dbft package dbft
// ConsensusMessage is an interface for generic dBFT message. // ConsensusMessage is an interface for generic dBFT message.
type ConsensusMessage[H Hash] interface { type ConsensusMessage[H Hash] interface {
// ViewNumber returns view number when this message was originated. // ViewNumber returns view number when this message was originated.
ViewNumber() byte ViewNumber() byte
// Type returns type of this message. // Type returns type of this message.
Type() MessageType Type() MessageType
// Payload returns this message's actual payload. // Payload returns this message's actual payload.
Payload() any Payload() any
// GetChangeView returns payload as if it was ChangeView. // GetChangeView returns payload as if it was ChangeView.
GetChangeView() ChangeView GetChangeView() ChangeView
// GetPrepareRequest returns payload as if it was PrepareRequest. // GetPrepareRequest returns payload as if it was PrepareRequest.
GetPrepareRequest() PrepareRequest[H] GetPrepareRequest() PrepareRequest[H]
// GetPrepareResponse returns payload as if it was PrepareResponse. // GetPrepareResponse returns payload as if it was PrepareResponse.
GetPrepareResponse() PrepareResponse[H] GetPrepareResponse() PrepareResponse[H]
// GetPreCommit returns payload as if it was PreCommit. // GetPreCommit returns payload as if it was PreCommit.
GetPreCommit() PreCommit GetPreCommit() PreCommit
// GetCommit returns payload as if it was Commit. // GetCommit returns payload as if it was Commit.
GetCommit() Commit GetCommit() Commit
// GetRecoveryRequest returns payload as if it was RecoveryRequest. // GetRecoveryRequest returns payload as if it was RecoveryRequest.
GetRecoveryRequest() RecoveryRequest GetRecoveryRequest() RecoveryRequest
// GetRecoveryMessage returns payload as if it was RecoveryMessage. // GetRecoveryMessage returns payload as if it was RecoveryMessage.
GetRecoveryMessage() RecoveryMessage[H] GetRecoveryMessage() RecoveryMessage[H]
} }

View File

@ -1,39 +1,39 @@
package dbft package dbft
import "fmt" import "fmt"
// MessageType is a type for dBFT consensus messages. // MessageType is a type for dBFT consensus messages.
type MessageType byte type MessageType byte
// 7 following constants enumerate all possible type of consensus message. // 7 following constants enumerate all possible type of consensus message.
const ( const (
ChangeViewType MessageType = 0x00 ChangeViewType MessageType = 0x00
PrepareRequestType MessageType = 0x20 PrepareRequestType MessageType = 0x20
PrepareResponseType MessageType = 0x21 PrepareResponseType MessageType = 0x21
PreCommitType MessageType = 0x31 PreCommitType MessageType = 0x31
CommitType MessageType = 0x30 CommitType MessageType = 0x30
RecoveryRequestType MessageType = 0x40 RecoveryRequestType MessageType = 0x40
RecoveryMessageType MessageType = 0x41 RecoveryMessageType MessageType = 0x41
) )
// String implements fmt.Stringer interface. // String implements fmt.Stringer interface.
func (m MessageType) String() string { func (m MessageType) String() string {
switch m { switch m {
case ChangeViewType: case ChangeViewType:
return "ChangeView" return "ChangeView"
case PrepareRequestType: case PrepareRequestType:
return "PrepareRequest" return "PrepareRequest"
case PrepareResponseType: case PrepareResponseType:
return "PrepareResponse" return "PrepareResponse"
case CommitType: case CommitType:
return "Commit" return "Commit"
case PreCommitType: case PreCommitType:
return "PreCommit" return "PreCommit"
case RecoveryRequestType: case RecoveryRequestType:
return "RecoveryRequest" return "RecoveryRequest"
case RecoveryMessageType: case RecoveryMessageType:
return "RecoveryMessage" return "RecoveryMessage"
default: default:
return fmt.Sprintf("UNKNOWN(%02x)", byte(m)) return fmt.Sprintf("UNKNOWN(%02x)", byte(m))
} }
} }

View File

@ -1,19 +1,19 @@
package dbft package dbft
// ConsensusPayload is a generic payload type which is exchanged // ConsensusPayload is a generic payload type which is exchanged
// between the nodes. // between the nodes.
type ConsensusPayload[H Hash] interface { type ConsensusPayload[H Hash] interface {
ConsensusMessage[H] ConsensusMessage[H]
// ValidatorIndex returns index of validator from which // ValidatorIndex returns index of validator from which
// payload was originated from. // payload was originated from.
ValidatorIndex() uint16 ValidatorIndex() uint16
// SetValidatorIndex sets validator index. // SetValidatorIndex sets validator index.
SetValidatorIndex(i uint16) SetValidatorIndex(i uint16)
Height() uint32 Height() uint32
// Hash returns 32-byte checksum of the payload. // Hash returns 32-byte checksum of the payload.
Hash() H Hash() H
} }

View File

@ -1,445 +1,445 @@
package dbft package dbft
import ( import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"time" "time"
) )
// HeightView is a block height/consensus view pair. // HeightView is a block height/consensus view pair.
type HeightView struct { type HeightView struct {
Height uint32 Height uint32
View byte View byte
} }
// Context is a main dBFT structure which // Context is a main dBFT structure which
// contains all information needed for performing transitions. // contains all information needed for performing transitions.
type Context[H Hash] struct { type Context[H Hash] struct {
// Config is dBFT's Config instance. // Config is dBFT's Config instance.
Config *Config[H] Config *Config[H]
// Priv is node's private key. // Priv is node's private key.
Priv PrivateKey Priv PrivateKey
// Pub is node's public key. // Pub is node's public key.
Pub PublicKey Pub PublicKey
preBlock PreBlock[H] preBlock PreBlock[H]
preHeader PreBlock[H] preHeader PreBlock[H]
block Block[H] block Block[H]
header Block[H] header Block[H]
// blockProcessed denotes whether Config.ProcessBlock callback was called for the current // blockProcessed denotes whether Config.ProcessBlock callback was called for the current
// height. If so, then no second call must happen. After new block is received by the user, // height. If so, then no second call must happen. After new block is received by the user,
// dBFT stops any new transaction or messages processing as far as timeouts handling till // dBFT stops any new transaction or messages processing as far as timeouts handling till
// the next call to Reset. // the next call to Reset.
blockProcessed bool blockProcessed bool
// preBlockProcessed is true when Config.ProcessPreBlock callback was // preBlockProcessed is true when Config.ProcessPreBlock callback was
// invoked for the current height. This happens once and dbft continues // invoked for the current height. This happens once and dbft continues
// to march towards proper commit after that. // to march towards proper commit after that.
preBlockProcessed bool preBlockProcessed bool
// BlockIndex is current block index. // BlockIndex is current block index.
BlockIndex uint32 BlockIndex uint32
// ViewNumber is current view number. // ViewNumber is current view number.
ViewNumber byte ViewNumber byte
// Validators is a current validator list. // Validators is a current validator list.
Validators []PublicKey Validators []PublicKey
// MyIndex is an index of the current node in the Validators array. // MyIndex is an index of the current node in the Validators array.
// It is equal to -1 if node is not a validator or is WatchOnly. // It is equal to -1 if node is not a validator or is WatchOnly.
MyIndex int MyIndex int
// PrimaryIndex is an index of the primary node in the current epoch. // PrimaryIndex is an index of the primary node in the current epoch.
PrimaryIndex uint PrimaryIndex uint
// PrevHash is a hash of the previous block. // PrevHash is a hash of the previous block.
PrevHash H PrevHash H
// Timestamp is a nanosecond-precision timestamp // Timestamp is a nanosecond-precision timestamp
Timestamp uint64 Timestamp uint64
Nonce uint64 Nonce uint64
// TransactionHashes is a slice of hashes of proposed transactions in the current block. // TransactionHashes is a slice of hashes of proposed transactions in the current block.
TransactionHashes []H TransactionHashes []H
// MissingTransactions is a slice of hashes containing missing transactions for the current block. // MissingTransactions is a slice of hashes containing missing transactions for the current block.
MissingTransactions []H MissingTransactions []H
// Transactions is a map containing actual transactions for the current block. // Transactions is a map containing actual transactions for the current block.
Transactions map[H]Transaction[H] Transactions map[H]Transaction[H]
// PreparationPayloads stores consensus Prepare* payloads for the current epoch. // PreparationPayloads stores consensus Prepare* payloads for the current epoch.
PreparationPayloads []ConsensusPayload[H] PreparationPayloads []ConsensusPayload[H]
// PreCommitPayloads stores consensus PreCommit payloads sent through all epochs // PreCommitPayloads stores consensus PreCommit payloads sent through all epochs
// as a part of anti-MEV dBFT extension. It is assumed that valid PreCommit // as a part of anti-MEV dBFT extension. It is assumed that valid PreCommit
// payloads can only be sent once by a single node per the whole set of consensus // payloads can only be sent once by a single node per the whole set of consensus
// epochs for particular block. Invalid PreCommit payloads are kicked off this // epochs for particular block. Invalid PreCommit payloads are kicked off this
// list immediately (if PrepareRequest was received for the current round, so // list immediately (if PrepareRequest was received for the current round, so
// it's possible to verify PreCommit against PreBlock built on PrepareRequest) // it's possible to verify PreCommit against PreBlock built on PrepareRequest)
// or stored till the corresponding PrepareRequest receiving. // or stored till the corresponding PrepareRequest receiving.
PreCommitPayloads []ConsensusPayload[H] PreCommitPayloads []ConsensusPayload[H]
// CommitPayloads stores consensus Commit payloads sent throughout all epochs. It // CommitPayloads stores consensus Commit payloads sent throughout all epochs. It
// is assumed that valid Commit payload can only be sent once by a single node per // is assumed that valid Commit payload can only be sent once by a single node per
// the whole set of consensus epochs for particular block. Invalid commit payloads // the whole set of consensus epochs for particular block. Invalid commit payloads
// are kicked off this list immediately (if PrepareRequest was received for the // are kicked off this list immediately (if PrepareRequest was received for the
// current round, so it's possible to verify Commit against it) or stored till // current round, so it's possible to verify Commit against it) or stored till
// the corresponding PrepareRequest receiving. // the corresponding PrepareRequest receiving.
CommitPayloads []ConsensusPayload[H] CommitPayloads []ConsensusPayload[H]
// ChangeViewPayloads stores consensus ChangeView payloads for the current epoch. // ChangeViewPayloads stores consensus ChangeView payloads for the current epoch.
ChangeViewPayloads []ConsensusPayload[H] ChangeViewPayloads []ConsensusPayload[H]
// LastChangeViewPayloads stores consensus ChangeView payloads for the last epoch. // LastChangeViewPayloads stores consensus ChangeView payloads for the last epoch.
LastChangeViewPayloads []ConsensusPayload[H] LastChangeViewPayloads []ConsensusPayload[H]
// LastSeenMessage array stores the height and view of the last seen message, for each validator. // LastSeenMessage array stores the height and view of the last seen message, for each validator.
// If this node never heard a thing from validator i, LastSeenMessage[i] will be nil. // If this node never heard a thing from validator i, LastSeenMessage[i] will be nil.
LastSeenMessage []*HeightView LastSeenMessage []*HeightView
lastBlockTimestamp uint64 // ns-precision timestamp from the last header (used for the next block timestamp calculations). lastBlockTimestamp uint64 // ns-precision timestamp from the last header (used for the next block timestamp calculations).
lastBlockTime time.Time // Wall clock time of when we started (as in PrepareRequest) creating the last block (used for timer adjustments). lastBlockTime time.Time // Wall clock time of when we started (as in PrepareRequest) creating the last block (used for timer adjustments).
lastBlockIndex uint32 lastBlockIndex uint32
lastBlockView byte lastBlockView byte
timePerBlock time.Duration // minimum amount of time that need to pass before the pending block will be accepted if there are some transactions in the proposal. timePerBlock time.Duration // minimum amount of time that need to pass before the pending block will be accepted if there are some transactions in the proposal.
maxTimePerBlock time.Duration // maximum amount of time that allowed to pass before the pending block will be accepted even if there's no transactions in the proposal. maxTimePerBlock time.Duration // maximum amount of time that allowed to pass before the pending block will be accepted even if there's no transactions in the proposal.
txSubscriptionOn bool txSubscriptionOn bool
prepareSentTime time.Time prepareSentTime time.Time
rttEstimates rtt rttEstimates rtt
} }
// N returns total number of validators. // N returns total number of validators.
func (c *Context[H]) N() int { return len(c.Validators) } func (c *Context[H]) N() int { return len(c.Validators) }
// F returns number of validators which can be faulty. // F returns number of validators which can be faulty.
func (c *Context[H]) F() int { return (len(c.Validators) - 1) / 3 } func (c *Context[H]) F() int { return (len(c.Validators) - 1) / 3 }
// M returns number of validators which must function correctly. // M returns number of validators which must function correctly.
func (c *Context[H]) M() int { return len(c.Validators) - c.F() } func (c *Context[H]) M() int { return len(c.Validators) - c.F() }
// GetPrimaryIndex returns index of a primary node for the specified view. // GetPrimaryIndex returns index of a primary node for the specified view.
func (c *Context[H]) GetPrimaryIndex(viewNumber byte) uint { func (c *Context[H]) GetPrimaryIndex(viewNumber byte) uint {
p := (int(c.BlockIndex) - int(viewNumber)) % len(c.Validators) p := (int(c.BlockIndex) - int(viewNumber)) % len(c.Validators)
if p >= 0 { if p >= 0 {
return uint(p) return uint(p)
} }
return uint(p + len(c.Validators)) return uint(p + len(c.Validators))
} }
// IsPrimary returns true iff node is primary for current height and view. // IsPrimary returns true iff node is primary for current height and view.
func (c *Context[H]) IsPrimary() bool { return c.MyIndex == int(c.PrimaryIndex) } func (c *Context[H]) IsPrimary() bool { return c.MyIndex == int(c.PrimaryIndex) }
// IsBackup returns true iff node is backup for current height and view. // IsBackup returns true iff node is backup for current height and view.
func (c *Context[H]) IsBackup() bool { func (c *Context[H]) IsBackup() bool {
return c.MyIndex >= 0 && !c.IsPrimary() return c.MyIndex >= 0 && !c.IsPrimary()
} }
// WatchOnly returns true iff node takes no active part in consensus. // WatchOnly returns true iff node takes no active part in consensus.
func (c *Context[H]) WatchOnly() bool { return c.MyIndex < 0 || c.Config.WatchOnly() } func (c *Context[H]) WatchOnly() bool { return c.MyIndex < 0 || c.Config.WatchOnly() }
// CountCommitted returns number of received Commit (or PreCommit for anti-MEV // CountCommitted returns number of received Commit (or PreCommit for anti-MEV
// extension) messages not only for the current epoch but also for any other epoch. // extension) messages not only for the current epoch but also for any other epoch.
func (c *Context[H]) CountCommitted() (count int) { func (c *Context[H]) CountCommitted() (count int) {
for i := range c.CommitPayloads { for i := range c.CommitPayloads {
// Consider both Commit and PreCommit payloads since both Commit and PreCommit // Consider both Commit and PreCommit payloads since both Commit and PreCommit
// phases are one-directional (do not impose view change). // phases are one-directional (do not impose view change).
if c.CommitPayloads[i] != nil || c.PreCommitPayloads[i] != nil { if c.CommitPayloads[i] != nil || c.PreCommitPayloads[i] != nil {
count++ count++
} }
} }
return return
} }
// CountFailed returns number of nodes with which no communication was performed // CountFailed returns number of nodes with which no communication was performed
// for this view and that hasn't sent the Commit message at the previous views. // for this view and that hasn't sent the Commit message at the previous views.
func (c *Context[H]) CountFailed() (count int) { func (c *Context[H]) CountFailed() (count int) {
for i, hv := range c.LastSeenMessage { for i, hv := range c.LastSeenMessage {
if (c.CommitPayloads[i] == nil && c.PreCommitPayloads[i] == nil) && if (c.CommitPayloads[i] == nil && c.PreCommitPayloads[i] == nil) &&
(hv == nil || hv.Height < c.BlockIndex || hv.View < c.ViewNumber) { (hv == nil || hv.Height < c.BlockIndex || hv.View < c.ViewNumber) {
count++ count++
} }
} }
return return
} }
// RequestSentOrReceived returns true iff PrepareRequest // RequestSentOrReceived returns true iff PrepareRequest
// was sent or received for the current epoch. // was sent or received for the current epoch.
func (c *Context[H]) RequestSentOrReceived() bool { func (c *Context[H]) RequestSentOrReceived() bool {
return c.PreparationPayloads[c.PrimaryIndex] != nil return c.PreparationPayloads[c.PrimaryIndex] != nil
} }
// ResponseSent returns true iff Prepare* message was sent for the current epoch. // ResponseSent returns true iff Prepare* message was sent for the current epoch.
func (c *Context[H]) ResponseSent() bool { func (c *Context[H]) ResponseSent() bool {
return !c.WatchOnly() && c.PreparationPayloads[c.MyIndex] != nil return !c.WatchOnly() && c.PreparationPayloads[c.MyIndex] != nil
} }
// PreCommitSent returns true iff PreCommit message was sent for the current epoch // PreCommitSent returns true iff PreCommit message was sent for the current epoch
// assuming that the node can't go further than current epoch after PreCommit was sent. // assuming that the node can't go further than current epoch after PreCommit was sent.
func (c *Context[H]) PreCommitSent() bool { func (c *Context[H]) PreCommitSent() bool {
return !c.WatchOnly() && c.PreCommitPayloads[c.MyIndex] != nil return !c.WatchOnly() && c.PreCommitPayloads[c.MyIndex] != nil
} }
// CommitSent returns true iff Commit message was sent for the current epoch // CommitSent returns true iff Commit message was sent for the current epoch
// assuming that the node can't go further than current epoch after commit was sent. // assuming that the node can't go further than current epoch after commit was sent.
func (c *Context[H]) CommitSent() bool { func (c *Context[H]) CommitSent() bool {
return !c.WatchOnly() && c.CommitPayloads[c.MyIndex] != nil return !c.WatchOnly() && c.CommitPayloads[c.MyIndex] != nil
} }
// BlockSent returns true iff block was formed AND sent for the current height. // BlockSent returns true iff block was formed AND sent for the current height.
// Once block is sent, the consensus stops new transactions and messages processing // Once block is sent, the consensus stops new transactions and messages processing
// as far as timeouts handling. // as far as timeouts handling.
// //
// Implementation note: the implementation of BlockSent differs from the C#'s one. // Implementation note: the implementation of BlockSent differs from the C#'s one.
// In C# algorithm they use ConsensusContext's Block.Transactions null check to define // In C# algorithm they use ConsensusContext's Block.Transactions null check to define
// whether block was formed, and the only place where the block can be formed is // whether block was formed, and the only place where the block can be formed is
// in the ConsensusContext's CreateBlock function right after enough Commits receiving. // in the ConsensusContext's CreateBlock function right after enough Commits receiving.
// On the contrary, in our implementation we don't have access to the block's // On the contrary, in our implementation we don't have access to the block's
// Transactions field as far as we can't use block null check, because there are // Transactions field as far as we can't use block null check, because there are
// several places where the call to CreateBlock happens (one of them is right after // several places where the call to CreateBlock happens (one of them is right after
// PrepareRequest receiving). Thus, we have a separate Context.blockProcessed field // PrepareRequest receiving). Thus, we have a separate Context.blockProcessed field
// for the described purpose. // for the described purpose.
func (c *Context[H]) BlockSent() bool { return c.blockProcessed } func (c *Context[H]) BlockSent() bool { return c.blockProcessed }
// ViewChanging returns true iff node is in a process of changing view. // ViewChanging returns true iff node is in a process of changing view.
func (c *Context[H]) ViewChanging() bool { func (c *Context[H]) ViewChanging() bool {
if c.WatchOnly() { if c.WatchOnly() {
return false return false
} }
cv := c.ChangeViewPayloads[c.MyIndex] cv := c.ChangeViewPayloads[c.MyIndex]
return cv != nil && cv.GetChangeView().NewViewNumber() > c.ViewNumber return cv != nil && cv.GetChangeView().NewViewNumber() > c.ViewNumber
} }
// NotAcceptingPayloadsDueToViewChanging returns true if node should not accept new payloads. // NotAcceptingPayloadsDueToViewChanging returns true if node should not accept new payloads.
func (c *Context[H]) NotAcceptingPayloadsDueToViewChanging() bool { func (c *Context[H]) NotAcceptingPayloadsDueToViewChanging() bool {
return c.ViewChanging() && !c.MoreThanFNodesCommittedOrLost() return c.ViewChanging() && !c.MoreThanFNodesCommittedOrLost()
} }
// MoreThanFNodesCommittedOrLost returns true iff a number of nodes which either committed // MoreThanFNodesCommittedOrLost returns true iff a number of nodes which either committed
// or are faulty is more than maximum amount of allowed faulty nodes. // or are faulty is more than maximum amount of allowed faulty nodes.
// A possible attack can happen if the last node to commit is malicious and either sends change view after his // A possible attack can happen if the last node to commit is malicious and either sends change view after his
// commit to stall nodes in a higher view, or if he refuses to send recovery messages. In addition, if a node // commit to stall nodes in a higher view, or if he refuses to send recovery messages. In addition, if a node
// asking change views loses network or crashes and comes back when nodes are committed in more than one higher // asking change views loses network or crashes and comes back when nodes are committed in more than one higher
// numbered view, it is possible for the node accepting recovery to commit in any of the higher views, thus // numbered view, it is possible for the node accepting recovery to commit in any of the higher views, thus
// potentially splitting nodes among views and stalling the network. // potentially splitting nodes among views and stalling the network.
func (c *Context[H]) MoreThanFNodesCommittedOrLost() bool { func (c *Context[H]) MoreThanFNodesCommittedOrLost() bool {
return c.CountCommitted()+c.CountFailed() > c.F() return c.CountCommitted()+c.CountFailed() > c.F()
} }
// Header returns current header from context. May be nil in case if no // Header returns current header from context. May be nil in case if no
// header is constructed yet. Do not change the resulting header. // header is constructed yet. Do not change the resulting header.
func (c *Context[H]) Header() Block[H] { func (c *Context[H]) Header() Block[H] {
return c.header return c.header
} }
// PreHeader returns current preHeader from context. May be nil in case if no // PreHeader returns current preHeader from context. May be nil in case if no
// preHeader is constructed yet. Do not change the resulting preHeader. // preHeader is constructed yet. Do not change the resulting preHeader.
func (c *Context[H]) PreHeader() PreBlock[H] { func (c *Context[H]) PreHeader() PreBlock[H] {
return c.preHeader return c.preHeader
} }
// PreBlock returns current PreBlock from context. May be nil in case if no // PreBlock returns current PreBlock from context. May be nil in case if no
// PreBlock is constructed yet (even if PreHeader is already constructed). // PreBlock is constructed yet (even if PreHeader is already constructed).
// External changes in the PreBlock will be seen by dBFT. // External changes in the PreBlock will be seen by dBFT.
func (c *Context[H]) PreBlock() PreBlock[H] { func (c *Context[H]) PreBlock() PreBlock[H] {
return c.preBlock return c.preBlock
} }
func (c *Context[H]) reset(view byte, ts uint64) { func (c *Context[H]) reset(view byte, ts uint64) {
c.MyIndex = -1 c.MyIndex = -1
c.prepareSentTime = time.Time{} c.prepareSentTime = time.Time{}
c.lastBlockTimestamp = ts c.lastBlockTimestamp = ts
c.unsubscribeFromTransactions() c.unsubscribeFromTransactions()
if view == 0 { if view == 0 {
c.PrevHash = c.Config.CurrentBlockHash() c.PrevHash = c.Config.CurrentBlockHash()
c.BlockIndex = c.Config.CurrentHeight() + 1 c.BlockIndex = c.Config.CurrentHeight() + 1
c.Validators = c.Config.GetValidators() c.Validators = c.Config.GetValidators()
c.timePerBlock = c.Config.TimePerBlock() c.timePerBlock = c.Config.TimePerBlock()
if c.Config.MaxTimePerBlock != nil { if c.Config.MaxTimePerBlock != nil {
c.maxTimePerBlock = c.Config.MaxTimePerBlock() c.maxTimePerBlock = c.Config.MaxTimePerBlock()
} }
n := len(c.Validators) n := len(c.Validators)
c.LastChangeViewPayloads = emptyReusableSlice(c.LastChangeViewPayloads, n) c.LastChangeViewPayloads = emptyReusableSlice(c.LastChangeViewPayloads, n)
c.LastSeenMessage = emptyReusableSlice(c.LastSeenMessage, n) c.LastSeenMessage = emptyReusableSlice(c.LastSeenMessage, n)
c.blockProcessed = false c.blockProcessed = false
c.preBlockProcessed = false c.preBlockProcessed = false
} else { } else {
for i := range c.Validators { for i := range c.Validators {
m := c.ChangeViewPayloads[i] m := c.ChangeViewPayloads[i]
if m != nil && m.GetChangeView().NewViewNumber() >= view { if m != nil && m.GetChangeView().NewViewNumber() >= view {
c.LastChangeViewPayloads[i] = m c.LastChangeViewPayloads[i] = m
} else { } else {
c.LastChangeViewPayloads[i] = nil c.LastChangeViewPayloads[i] = nil
} }
} }
} }
c.MyIndex, c.Priv, c.Pub = c.Config.GetKeyPair(c.Validators) c.MyIndex, c.Priv, c.Pub = c.Config.GetKeyPair(c.Validators)
c.block = nil c.block = nil
c.preBlock = nil c.preBlock = nil
c.header = nil c.header = nil
c.preHeader = nil c.preHeader = nil
n := len(c.Validators) n := len(c.Validators)
c.ChangeViewPayloads = emptyReusableSlice(c.ChangeViewPayloads, n) c.ChangeViewPayloads = emptyReusableSlice(c.ChangeViewPayloads, n)
if view == 0 { if view == 0 {
c.PreCommitPayloads = emptyReusableSlice(c.PreCommitPayloads, n) c.PreCommitPayloads = emptyReusableSlice(c.PreCommitPayloads, n)
c.CommitPayloads = emptyReusableSlice(c.CommitPayloads, n) c.CommitPayloads = emptyReusableSlice(c.CommitPayloads, n)
} }
c.PreparationPayloads = emptyReusableSlice(c.PreparationPayloads, n) c.PreparationPayloads = emptyReusableSlice(c.PreparationPayloads, n)
if c.Transactions == nil { // Init. if c.Transactions == nil { // Init.
c.Transactions = make(map[H]Transaction[H]) c.Transactions = make(map[H]Transaction[H])
} else { // Regular use. } else { // Regular use.
clear(c.Transactions) clear(c.Transactions)
} }
c.TransactionHashes = nil c.TransactionHashes = nil
if c.MissingTransactions != nil { if c.MissingTransactions != nil {
c.MissingTransactions = c.MissingTransactions[:0] c.MissingTransactions = c.MissingTransactions[:0]
} }
c.PrimaryIndex = c.GetPrimaryIndex(view) c.PrimaryIndex = c.GetPrimaryIndex(view)
c.ViewNumber = view c.ViewNumber = view
if c.MyIndex >= 0 { if c.MyIndex >= 0 {
c.LastSeenMessage[c.MyIndex] = &HeightView{c.BlockIndex, c.ViewNumber} c.LastSeenMessage[c.MyIndex] = &HeightView{c.BlockIndex, c.ViewNumber}
} }
} }
func emptyReusableSlice[E any](s []E, n int) []E { func emptyReusableSlice[E any](s []E, n int) []E {
if len(s) == n { if len(s) == n {
clear(s) clear(s)
return s return s
} }
return make([]E, n) return make([]E, n)
} }
// Fill initializes consensus when node is a speaker. It doesn't perform any // Fill initializes consensus when node is a speaker. It doesn't perform any
// context modifications if MaxTimePerBlock extension is enabled and there are // context modifications if MaxTimePerBlock extension is enabled and there are
// no transactions in the memory pool and force is not set. // no transactions in the memory pool and force is not set.
func (c *Context[H]) Fill(force bool) bool { func (c *Context[H]) Fill(force bool) bool {
txx := c.Config.GetVerified() txx := c.Config.GetVerified()
if c.Config.MaxTimePerBlock != nil && !force && len(txx) == 0 { if c.Config.MaxTimePerBlock != nil && !force && len(txx) == 0 {
return false return false
} }
b := make([]byte, 8) b := make([]byte, 8)
_, _ = rand.Read(b) _, _ = rand.Read(b)
c.Nonce = binary.LittleEndian.Uint64(b) c.Nonce = binary.LittleEndian.Uint64(b)
c.TransactionHashes = make([]H, len(txx)) c.TransactionHashes = make([]H, len(txx))
for i := range txx { for i := range txx {
h := txx[i].Hash() h := txx[i].Hash()
c.TransactionHashes[i] = h c.TransactionHashes[i] = h
c.Transactions[h] = txx[i] c.Transactions[h] = txx[i]
} }
c.Timestamp = c.lastBlockTimestamp + c.Config.TimestampIncrement c.Timestamp = c.lastBlockTimestamp + c.Config.TimestampIncrement
if now := c.getTimestamp(); now > c.Timestamp { if now := c.getTimestamp(); now > c.Timestamp {
c.Timestamp = now c.Timestamp = now
} }
return true return true
} }
// getTimestamp returns nanoseconds-precision timestamp using // getTimestamp returns nanoseconds-precision timestamp using
// current context config. // current context config.
func (c *Context[H]) getTimestamp() uint64 { func (c *Context[H]) getTimestamp() uint64 {
return uint64(c.Config.Timer.Now().UnixNano()) / c.Config.TimestampIncrement * c.Config.TimestampIncrement return uint64(c.Config.Timer.Now().UnixNano()) / c.Config.TimestampIncrement * c.Config.TimestampIncrement
} }
// CreateBlock returns resulting block for the current epoch. // CreateBlock returns resulting block for the current epoch.
func (c *Context[H]) CreateBlock() Block[H] { func (c *Context[H]) CreateBlock() Block[H] {
if c.block == nil { if c.block == nil {
if c.block = c.MakeHeader(); c.block == nil { if c.block = c.MakeHeader(); c.block == nil {
return nil return nil
} }
txx := make([]Transaction[H], len(c.TransactionHashes)) txx := make([]Transaction[H], len(c.TransactionHashes))
for i, h := range c.TransactionHashes { for i, h := range c.TransactionHashes {
txx[i] = c.Transactions[h] txx[i] = c.Transactions[h]
} }
// Anti-MEV extension properly sets PreBlock transactions once during PreBlock // Anti-MEV extension properly sets PreBlock transactions once during PreBlock
// construction and then never updates these transactions in the dBFT context. // construction and then never updates these transactions in the dBFT context.
// Thus, user must not reuse txx if anti-MEV extension is enabled. However, // Thus, user must not reuse txx if anti-MEV extension is enabled. However,
// we don't skip a call to Block.SetTransactions since it may be used as a // we don't skip a call to Block.SetTransactions since it may be used as a
// signal to the user's code to finalize the block. // signal to the user's code to finalize the block.
c.block.SetTransactions(txx) c.block.SetTransactions(txx)
} }
return c.block return c.block
} }
// CreatePreBlock returns PreBlock for the current epoch. // CreatePreBlock returns PreBlock for the current epoch.
func (c *Context[H]) CreatePreBlock() PreBlock[H] { func (c *Context[H]) CreatePreBlock() PreBlock[H] {
if c.preBlock == nil { if c.preBlock == nil {
if c.preBlock = c.MakePreHeader(); c.preBlock == nil { if c.preBlock = c.MakePreHeader(); c.preBlock == nil {
return nil return nil
} }
txx := make([]Transaction[H], len(c.TransactionHashes)) txx := make([]Transaction[H], len(c.TransactionHashes))
for i, h := range c.TransactionHashes { for i, h := range c.TransactionHashes {
txx[i] = c.Transactions[h] txx[i] = c.Transactions[h]
} }
c.preBlock.SetTransactions(txx) c.preBlock.SetTransactions(txx)
} }
return c.preBlock return c.preBlock
} }
// isAntiMEVExtensionEnabled returns whether Anti-MEV dBFT extension is enabled // isAntiMEVExtensionEnabled returns whether Anti-MEV dBFT extension is enabled
// at the currently processing block height. // at the currently processing block height.
func (c *Context[H]) isAntiMEVExtensionEnabled() bool { func (c *Context[H]) isAntiMEVExtensionEnabled() bool {
return c.Config.AntiMEVExtensionEnablingHeight >= 0 && uint32(c.Config.AntiMEVExtensionEnablingHeight) <= c.BlockIndex return c.Config.AntiMEVExtensionEnablingHeight >= 0 && uint32(c.Config.AntiMEVExtensionEnablingHeight) <= c.BlockIndex
} }
// MakeHeader returns half-filled block for the current epoch. // MakeHeader returns half-filled block for the current epoch.
// All hashable fields will be filled. // All hashable fields will be filled.
func (c *Context[H]) MakeHeader() Block[H] { func (c *Context[H]) MakeHeader() Block[H] {
if c.header == nil { if c.header == nil {
if !c.RequestSentOrReceived() { if !c.RequestSentOrReceived() {
return nil return nil
} }
// For anti-MEV dBFT extension it's important to have PreBlock processed and // For anti-MEV dBFT extension it's important to have PreBlock processed and
// all envelopes decrypted, because a single PrepareRequest is not enough to // all envelopes decrypted, because a single PrepareRequest is not enough to
// construct proper Block. // construct proper Block.
if c.isAntiMEVExtensionEnabled() { if c.isAntiMEVExtensionEnabled() {
if !c.preBlockProcessed { if !c.preBlockProcessed {
return nil return nil
} }
} }
c.header = c.Config.NewBlockFromContext(c) c.header = c.Config.NewBlockFromContext(c)
} }
return c.header return c.header
} }
// MakePreHeader returns half-filled block for the current epoch. // MakePreHeader returns half-filled block for the current epoch.
// All hashable fields will be filled. // All hashable fields will be filled.
func (c *Context[H]) MakePreHeader() PreBlock[H] { func (c *Context[H]) MakePreHeader() PreBlock[H] {
if c.preHeader == nil { if c.preHeader == nil {
if !c.RequestSentOrReceived() { if !c.RequestSentOrReceived() {
return nil return nil
} }
c.preHeader = c.Config.NewPreBlockFromContext(c) c.preHeader = c.Config.NewPreBlockFromContext(c)
} }
return c.preHeader return c.preHeader
} }
// hasAllTransactions returns true iff all transactions were received // hasAllTransactions returns true iff all transactions were received
// for the proposed block. // for the proposed block.
func (c *Context[H]) hasAllTransactions() bool { func (c *Context[H]) hasAllTransactions() bool {
return len(c.TransactionHashes) == len(c.Transactions) return len(c.TransactionHashes) == len(c.Transactions)
} }
func (c *Context[H]) subscribeForTransactions() { func (c *Context[H]) subscribeForTransactions() {
c.txSubscriptionOn = true c.txSubscriptionOn = true
c.Config.SubscribeForTxs() c.Config.SubscribeForTxs()
} }
func (c *Context[H]) unsubscribeFromTransactions() { func (c *Context[H]) unsubscribeFromTransactions() {
c.txSubscriptionOn = false c.txSubscriptionOn = false
} }

1502
dbft.go

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
# Project-specific labels # Project-specific labels
## Component ## Component
Currently only these ones are used, but the list can be extended in future: Currently only these ones are used, but the list can be extended in future:
- tla+ - tla+
Related to the TLA+ algorithm specification Related to the TLA+ algorithm specification

View File

@ -1,64 +1,64 @@
# Release instructions # Release instructions
This document outlines the dbft release process. It can be used as a todo This document outlines the dbft release process. It can be used as a todo
list for a new release. list for a new release.
## Check the state ## Check the state
These should run successfully: These should run successfully:
* build * build
* unit-tests * unit-tests
* lint * lint
* simulation with default settings * simulation with default settings
## Update CHANGELOG and ROADMAP ## Update CHANGELOG and ROADMAP
Add an entry to the CHANGELOG.md following the style established there. Add a Add an entry to the CHANGELOG.md following the style established there. Add a
codename, version and release date in the heading. Write a paragraph codename, version and release date in the heading. Write a paragraph
describing the most significant changes done in this release. In case if the dBFT describing the most significant changes done in this release. In case if the dBFT
configuration was changed, some API was marked as deprecated, any experimental configuration was changed, some API was marked as deprecated, any experimental
changes were made in the user-facing code and the users' feedback is needed or changes were made in the user-facing code and the users' feedback is needed or
if there's any other information that requires user's response, write if there's any other information that requires user's response, write
another separate paragraph for those who uses dbft package. Then, add sections another separate paragraph for those who uses dbft package. Then, add sections
with release content describing each change in detail and with a reference to with release content describing each change in detail and with a reference to
GitHub issues and/or PRs. Minor issues that doesn't affect the package end-user may GitHub issues and/or PRs. Minor issues that doesn't affect the package end-user may
be grouped under a single label. be grouped under a single label.
* "New features" section should include new abilities that were added to the * "New features" section should include new abilities that were added to the
dBFT/API, are directly visible or available to the user and are large dBFT/API, are directly visible or available to the user and are large
enough to be treated as a feature. Do not include minor user-facing enough to be treated as a feature. Do not include minor user-facing
improvements and changes that don't affect the user-facing functionality improvements and changes that don't affect the user-facing functionality
even if they are new. even if they are new.
* "Behaviour changes" section should include any incompatible changes in default * "Behaviour changes" section should include any incompatible changes in default
settings or in API that are available to the user. Add a note about changes settings or in API that are available to the user. Add a note about changes
user needs to make if he uses the affected code. user needs to make if he uses the affected code.
* "Improvements" section should include user-facing changes that are too * "Improvements" section should include user-facing changes that are too
insignificant to be treated as a feature and are not directly visible to the insignificant to be treated as a feature and are not directly visible to the
package end-user, such as performance optimizations, refactoring and internal package end-user, such as performance optimizations, refactoring and internal
API changes. API changes.
* "Bugs fixed" section should include a set of bugs fixed since the previous * "Bugs fixed" section should include a set of bugs fixed since the previous
release with optional bug cause or consequences description. release with optional bug cause or consequences description.
Create a PR with CHANGELOG changes, review/merge it. Create a PR with CHANGELOG changes, review/merge it.
## Create a GitHub release and a tag ## Create a GitHub release and a tag
Use "Draft a new release" button in the "Releases" section. Create a new Use "Draft a new release" button in the "Releases" section. Create a new
`vX.Y.Z` tag for it following the semantic versioning standard. Put change log `vX.Y.Z` tag for it following the semantic versioning standard. Put change log
for this release into the description. Do not attach any binaries. for this release into the description. Do not attach any binaries.
Set the "Set as the latest release" checkbox if this is the latest stable Set the "Set as the latest release" checkbox if this is the latest stable
release or "Set as a pre-release" if this is an unstable pre-release. release or "Set as a pre-release" if this is an unstable pre-release.
Press the "Publish release" button. Press the "Publish release" button.
## Close GitHub milestone ## Close GitHub milestone
Close corresponding X.Y.Z GitHub milestone. Close corresponding X.Y.Z GitHub milestone.
## Announcements ## Announcements
Copy the GitHub release page link to: Copy the GitHub release page link to:
* Element channel * Element channel
## Dependant projects update ## Dependant projects update
Create an issue or PR to fetch the updated package version in the dependant Create an issue or PR to fetch the updated package version in the dependant
repositories. repositories.

View File

@ -1,111 +1,111 @@
<mxfile host="app.diagrams.net" modified="2024-06-24T11:45:32.984Z" agent="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0" etag="L3zGyTC-LyAz5kXq654f" version="24.5.5" type="google"> <mxfile host="app.diagrams.net" modified="2024-06-24T11:45:32.984Z" agent="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0" etag="L3zGyTC-LyAz5kXq654f" version="24.5.5" type="google">
<diagram name="Page-1" id="gx1AT7QsytIHyGW8taHa"> <diagram name="Page-1" id="gx1AT7QsytIHyGW8taHa">
<mxGraphModel grid="1" page="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"> <mxGraphModel grid="1" page="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root> <root>
<mxCell id="0" /> <mxCell id="0" />
<mxCell id="1" parent="0" /> <mxCell id="1" parent="0" />
<mxCell id="zO6A_hVda2gypDU5CBnV-14" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=16;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="zO6A_hVda2gypDU5CBnV-6"> <mxCell id="zO6A_hVda2gypDU5CBnV-14" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=16;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="zO6A_hVda2gypDU5CBnV-6">
<mxGeometry relative="1" as="geometry"> <mxGeometry relative="1" as="geometry">
<mxPoint x="471" y="35" as="sourcePoint" /> <mxPoint x="471" y="35" as="sourcePoint" />
<mxPoint x="590" y="160" as="targetPoint" /> <mxPoint x="590" y="160" as="targetPoint" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-15" value="&lt;div style=&quot;font-size: 14px;&quot;&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;send PReq (primary)&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 14px;&quot;&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;or&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 14px;&quot;&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;send PResp (backup)&lt;br&gt;&lt;/font&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-14"> <mxCell id="zO6A_hVda2gypDU5CBnV-15" value="&lt;div style=&quot;font-size: 14px;&quot;&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;send PReq (primary)&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 14px;&quot;&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;or&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 14px;&quot;&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;send PResp (backup)&lt;br&gt;&lt;/font&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-14">
<mxGeometry x="0.175" y="3" relative="1" as="geometry"> <mxGeometry x="0.175" y="3" relative="1" as="geometry">
<mxPoint x="-3" y="23" as="offset" /> <mxPoint x="-3" y="23" as="offset" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-3" value="&lt;font style=&quot;font-size: 16px;&quot; face=&quot;Comic Sans MS&quot;&gt;initialized&lt;/font&gt;" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1"> <mxCell id="zO6A_hVda2gypDU5CBnV-3" value="&lt;font style=&quot;font-size: 16px;&quot; face=&quot;Comic Sans MS&quot;&gt;initialized&lt;/font&gt;" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1">
<mxGeometry x="340" y="10" width="130" height="50" as="geometry" /> <mxGeometry x="340" y="10" width="130" height="50" as="geometry" />
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-23" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;" edge="1" parent="1" source="zO6A_hVda2gypDU5CBnV-6" target="zO6A_hVda2gypDU5CBnV-7"> <mxCell id="zO6A_hVda2gypDU5CBnV-23" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;" edge="1" parent="1" source="zO6A_hVda2gypDU5CBnV-6" target="zO6A_hVda2gypDU5CBnV-7">
<mxGeometry relative="1" as="geometry" /> <mxGeometry relative="1" as="geometry" />
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-24" value="| PReq &lt;strong style=&quot;font-family: noto_regular; color: rgb(75, 75, 75); font-size: 10pt;&quot;&gt; &lt;/strong&gt;PResp | ≥ M&lt;span style=&quot;font-family: noto_regular; color: rgb(75, 75, 75); font-size: 10pt;&quot;&gt;&lt;/span&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-23"> <mxCell id="zO6A_hVda2gypDU5CBnV-24" value="| PReq &lt;strong style=&quot;font-family: noto_regular; color: rgb(75, 75, 75); font-size: 10pt;&quot;&gt; &lt;/strong&gt;PResp | ≥ M&lt;span style=&quot;font-family: noto_regular; color: rgb(75, 75, 75); font-size: 10pt;&quot;&gt;&lt;/span&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-23">
<mxGeometry x="-0.24" y="-1" relative="1" as="geometry"> <mxGeometry x="-0.24" y="-1" relative="1" as="geometry">
<mxPoint x="4" y="8" as="offset" /> <mxPoint x="4" y="8" as="offset" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-6" value="prepareSent" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1"> <mxCell id="zO6A_hVda2gypDU5CBnV-6" value="prepareSent" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1">
<mxGeometry x="520" y="160" width="130" height="50" as="geometry" /> <mxGeometry x="520" y="160" width="130" height="50" as="geometry" />
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-25" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;" edge="1" parent="1" source="zO6A_hVda2gypDU5CBnV-7" target="zO6A_hVda2gypDU5CBnV-8"> <mxCell id="zO6A_hVda2gypDU5CBnV-25" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;" edge="1" parent="1" source="zO6A_hVda2gypDU5CBnV-7" target="zO6A_hVda2gypDU5CBnV-8">
<mxGeometry relative="1" as="geometry" /> <mxGeometry relative="1" as="geometry" />
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-26" value="| Commit | ≥ M" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-25"> <mxCell id="zO6A_hVda2gypDU5CBnV-26" value="| Commit | ≥ M" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-25">
<mxGeometry x="-0.1818" y="-2" relative="1" as="geometry"> <mxGeometry x="-0.1818" y="-2" relative="1" as="geometry">
<mxPoint y="6" as="offset" /> <mxPoint y="6" as="offset" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-7" value="commitSent" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1"> <mxCell id="zO6A_hVda2gypDU5CBnV-7" value="commitSent" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1">
<mxGeometry x="520" y="310" width="130" height="50" as="geometry" /> <mxGeometry x="520" y="310" width="130" height="50" as="geometry" />
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-8" value="commitAckSent" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1"> <mxCell id="zO6A_hVda2gypDU5CBnV-8" value="commitAckSent" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1">
<mxGeometry x="520" y="470" width="130" height="50" as="geometry" /> <mxGeometry x="520" y="470" width="130" height="50" as="geometry" />
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-11" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;fontFamily=Comic Sans MS;fontSize=16;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;endSize=4;startSize=4;jumpSize=8;strokeWidth=2;startArrow=classicThin;startFill=1;endArrow=none;endFill=0;" edge="1" parent="1" source="zO6A_hVda2gypDU5CBnV-9" target="zO6A_hVda2gypDU5CBnV-3"> <mxCell id="zO6A_hVda2gypDU5CBnV-11" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;fontFamily=Comic Sans MS;fontSize=16;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;endSize=4;startSize=4;jumpSize=8;strokeWidth=2;startArrow=classicThin;startFill=1;endArrow=none;endFill=0;" edge="1" parent="1" source="zO6A_hVda2gypDU5CBnV-9" target="zO6A_hVda2gypDU5CBnV-3">
<mxGeometry relative="1" as="geometry"> <mxGeometry relative="1" as="geometry">
<mxPoint x="275" y="85" as="targetPoint" /> <mxPoint x="275" y="85" as="targetPoint" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-12" value="&lt;font style=&quot;font-size: 14px;&quot;&gt;t/o&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-11"> <mxCell id="zO6A_hVda2gypDU5CBnV-12" value="&lt;font style=&quot;font-size: 14px;&quot;&gt;t/o&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-11">
<mxGeometry x="-0.2125" y="1" relative="1" as="geometry"> <mxGeometry x="-0.2125" y="1" relative="1" as="geometry">
<mxPoint as="offset" /> <mxPoint as="offset" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-16" value="&amp;nbsp;t/o&amp;nbsp; " style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=classicThin;startFill=1;endArrow=none;endFill=0;startSize=4;endSize=4;" edge="1" parent="1" target="zO6A_hVda2gypDU5CBnV-6"> <mxCell id="zO6A_hVda2gypDU5CBnV-16" value="&amp;nbsp;t/o&amp;nbsp; " style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=classicThin;startFill=1;endArrow=none;endFill=0;startSize=4;endSize=4;" edge="1" parent="1" target="zO6A_hVda2gypDU5CBnV-6">
<mxGeometry relative="1" as="geometry"> <mxGeometry relative="1" as="geometry">
<mxPoint x="340" y="185" as="sourcePoint" /> <mxPoint x="340" y="185" as="sourcePoint" />
<mxPoint x="470" y="185" as="targetPoint" /> <mxPoint x="470" y="185" as="targetPoint" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-17" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;" edge="1" parent="1" source="zO6A_hVda2gypDU5CBnV-9" target="zO6A_hVda2gypDU5CBnV-6"> <mxCell id="zO6A_hVda2gypDU5CBnV-17" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;" edge="1" parent="1" source="zO6A_hVda2gypDU5CBnV-9" target="zO6A_hVda2gypDU5CBnV-6">
<mxGeometry relative="1" as="geometry"> <mxGeometry relative="1" as="geometry">
<Array as="points"> <Array as="points">
<mxPoint x="275" y="240" /> <mxPoint x="275" y="240" />
<mxPoint x="480" y="240" /> <mxPoint x="480" y="240" />
<mxPoint x="480" y="140" /> <mxPoint x="480" y="140" />
<mxPoint x="585" y="140" /> <mxPoint x="585" y="140" />
</Array> </Array>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-22" value="&amp;nbsp;| Commit | &amp;gt; F&amp;nbsp; " style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-17"> <mxCell id="zO6A_hVda2gypDU5CBnV-22" value="&amp;nbsp;| Commit | &amp;gt; F&amp;nbsp; " style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-17">
<mxGeometry x="-0.4" y="1" relative="1" as="geometry"> <mxGeometry x="-0.4" y="1" relative="1" as="geometry">
<mxPoint x="-8" as="offset" /> <mxPoint x="-8" as="offset" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-27" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;" edge="1" parent="1" source="zO6A_hVda2gypDU5CBnV-9" target="zO6A_hVda2gypDU5CBnV-3"> <mxCell id="zO6A_hVda2gypDU5CBnV-27" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;" edge="1" parent="1" source="zO6A_hVda2gypDU5CBnV-9" target="zO6A_hVda2gypDU5CBnV-3">
<mxGeometry relative="1" as="geometry"> <mxGeometry relative="1" as="geometry">
<Array as="points"> <Array as="points">
<mxPoint x="160" y="185" /> <mxPoint x="160" y="185" />
<mxPoint x="160" y="-20" /> <mxPoint x="160" y="-20" />
<mxPoint x="405" y="-20" /> <mxPoint x="405" y="-20" />
</Array> </Array>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-28" value="&amp;nbsp;| CV | ≥ M, init at next view&amp;nbsp; " style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-27"> <mxCell id="zO6A_hVda2gypDU5CBnV-28" value="&amp;nbsp;| CV | ≥ M, init at next view&amp;nbsp; " style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="zO6A_hVda2gypDU5CBnV-27">
<mxGeometry x="0.2286" y="-3" relative="1" as="geometry"> <mxGeometry x="0.2286" y="-3" relative="1" as="geometry">
<mxPoint x="47" y="-4" as="offset" /> <mxPoint x="47" y="-4" as="offset" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="zO6A_hVda2gypDU5CBnV-9" value="cv" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1"> <mxCell id="zO6A_hVda2gypDU5CBnV-9" value="cv" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1">
<mxGeometry x="210" y="160" width="130" height="50" as="geometry" /> <mxGeometry x="210" y="160" width="130" height="50" as="geometry" />
</mxCell> </mxCell>
<mxCell id="bSvnLd7m7FiDBpnTT2Ld-1" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;" edge="1" parent="1"> <mxCell id="bSvnLd7m7FiDBpnTT2Ld-1" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=8;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;strokeWidth=2;fontFamily=Comic Sans MS;fontSize=14;fontColor=#404040;startArrow=none;startFill=0;endArrow=classicThin;endFill=1;startSize=4;endSize=4;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry"> <mxGeometry relative="1" as="geometry">
<mxPoint x="584.5" y="520" as="sourcePoint" /> <mxPoint x="584.5" y="520" as="sourcePoint" />
<mxPoint x="584.5" y="630" as="targetPoint" /> <mxPoint x="584.5" y="630" as="targetPoint" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="bSvnLd7m7FiDBpnTT2Ld-2" value="| Ack | ≥ M" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="bSvnLd7m7FiDBpnTT2Ld-1"> <mxCell id="bSvnLd7m7FiDBpnTT2Ld-2" value="| Ack | ≥ M" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontFamily=Comic Sans MS;fontColor=#404040;" connectable="0" vertex="1" parent="bSvnLd7m7FiDBpnTT2Ld-1">
<mxGeometry x="-0.1818" y="-2" relative="1" as="geometry"> <mxGeometry x="-0.1818" y="-2" relative="1" as="geometry">
<mxPoint x="3" y="6" as="offset" /> <mxPoint x="3" y="6" as="offset" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="bSvnLd7m7FiDBpnTT2Ld-3" value="blockAccepted" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1"> <mxCell id="bSvnLd7m7FiDBpnTT2Ld-3" value="blockAccepted" style="strokeWidth=2;html=1;shape=mxgraph.flowchart.terminator;whiteSpace=wrap;fontFamily=Georgia;fontSize=16;strokeColor=#EA6B66;" vertex="1" parent="1">
<mxGeometry x="520" y="630" width="130" height="50" as="geometry" /> <mxGeometry x="520" y="630" width="130" height="50" as="geometry" />
</mxCell> </mxCell>
</root> </root>
</mxGraphModel> </mxGraphModel>
</diagram> </diagram>
</mxfile> </mxfile>

View File

@ -1,456 +1,456 @@
# dBFT formal models # dBFT formal models
This section contains a set of dBFT's formal specifications written in This section contains a set of dBFT's formal specifications written in
[TLA⁺](https://lamport.azurewebsites.net/tla/tla.html) language. The models [TLA⁺](https://lamport.azurewebsites.net/tla/tla.html) language. The models
describe the core algorithm logic represented in a high-level way and can be used describe the core algorithm logic represented in a high-level way and can be used
to illustrate some basic dBFT concepts and to validate the algorithm in terms of to illustrate some basic dBFT concepts and to validate the algorithm in terms of
liveness and fairness. It should be noted that presented models do not precisely liveness and fairness. It should be noted that presented models do not precisely
follow the dBFT implementation presented in the repository and may omit some follow the dBFT implementation presented in the repository and may omit some
implementation details in favor of the specification simplicity and the implementation details in favor of the specification simplicity and the
fundamental philosophy of the TLA⁺. However, the presented models directly fundamental philosophy of the TLA⁺. However, the presented models directly
reflect some liveness problems dBFT 2.0 has; the models can and are aimed to be reflect some liveness problems dBFT 2.0 has; the models can and are aimed to be
used for the dBFT 2.0 liveness evaluation and further algorithm improvements. used for the dBFT 2.0 liveness evaluation and further algorithm improvements.
Any contributions, questions and discussions on the presented models are highly Any contributions, questions and discussions on the presented models are highly
appreciated. appreciated.
## dBFT 2.0 models ## dBFT 2.0 models
### Basic dBFT 2.0 model ### Basic dBFT 2.0 model
This specification is a basis that was taken for the further algorithm This specification is a basis that was taken for the further algorithm
investigation. We recommend to begin acquaintance with the dBFT models from this investigation. We recommend to begin acquaintance with the dBFT models from this
one. one.
The specification describes the process of a single block acceptance: the set of The specification describes the process of a single block acceptance: the set of
resource managers `RM` (which is effectively a set of consensus nodes) resource managers `RM` (which is effectively a set of consensus nodes)
communicating via the shared consensus message pool `msgs` and taking the communicating via the shared consensus message pool `msgs` and taking the
decision in a few consensus rounds (views). Each consensus node has its own state decision in a few consensus rounds (views). Each consensus node has its own state
at each step of the behaviour. Consensus node may send a consensus message by at each step of the behaviour. Consensus node may send a consensus message by
adding it to the `msgs` pool. To perform the transition between states the adding it to the `msgs` pool. To perform the transition between states the
consensus node must send a consensus message or there must be a particular set of consensus node must send a consensus message or there must be a particular set of
consensus messages in the shared message pool required for a particular consensus messages in the shared message pool required for a particular
transition. transition.
Here's the scheme of transitions between consensus node states: Here's the scheme of transitions between consensus node states:
![Basic dBFT model transitions scheme](./.github/dbft.png) ![Basic dBFT model transitions scheme](./.github/dbft.png)
The specification also describes two kinds of malicious nodes behaviour that can The specification also describes two kinds of malicious nodes behaviour that can
be combined, i.e. enabled or disabled independently for each particular node: be combined, i.e. enabled or disabled independently for each particular node:
1. "Dead" nodes. "Dead" node is completely excluded from the consensus process 1. "Dead" nodes. "Dead" node is completely excluded from the consensus process
and not able to send the consensus messages and to perform state transitions. and not able to send the consensus messages and to perform state transitions.
The node may become "dead" at any step in the middle of the consensus process. The node may become "dead" at any step in the middle of the consensus process.
Once the node becomes "dead" there's no way for it to rejoin the consensus Once the node becomes "dead" there's no way for it to rejoin the consensus
process. process.
2. "Faulty" nodes. "Faulty" node is allowed to send consensus messages of *any* 2. "Faulty" nodes. "Faulty" node is allowed to send consensus messages of *any*
type at *any* step and to change its view without regarding the dBFT view type at *any* step and to change its view without regarding the dBFT view
changing rules. The node may become "faulty" at any step in the middle of the changing rules. The node may become "faulty" at any step in the middle of the
consensus process. Once the node becomes "faulty" there's no way for it to consensus process. Once the node becomes "faulty" there's no way for it to
become "good" again. become "good" again.
The specification contains several invariants and liveness properties that must The specification contains several invariants and liveness properties that must
be checked by the TLC Model Checker. These formulas mostly describe two basic be checked by the TLC Model Checker. These formulas mostly describe two basic
concepts that dBFT algorithm expected to guarantee: concepts that dBFT algorithm expected to guarantee:
1. No fork must happen. There must be no situation such that two different 1. No fork must happen. There must be no situation such that two different
blocks are accepted at two different consensus rounds (views). blocks are accepted at two different consensus rounds (views).
2. The block must always be accepted. There must be no situation such that nodes 2. The block must always be accepted. There must be no situation such that nodes
are stuck in the middle of consensus process and can't take any further steps. are stuck in the middle of consensus process and can't take any further steps.
The specification is written and working under several assumptions: The specification is written and working under several assumptions:
1. All consensus messages are valid. In real life it is guaranteed by verifiable 1. All consensus messages are valid. In real life it is guaranteed by verifiable
message signatures. In case if malicious or corrupted message is received it message signatures. In case if malicious or corrupted message is received it
won't be handled by the node. won't be handled by the node.
2. The exact timeouts (e.g. t/o on waiting a particular consensus message, etc.) 2. The exact timeouts (e.g. t/o on waiting a particular consensus message, etc.)
are not included into the model. However, the model covers timeouts in are not included into the model. However, the model covers timeouts in
general, i.e. the timeout is just the possibility to perform a particular general, i.e. the timeout is just the possibility to perform a particular
state transition. state transition.
3. All consensus messages must eventually be delivered to all nodes, but the 3. All consensus messages must eventually be delivered to all nodes, but the
exact order of delivering isn't guaranteed. exact order of delivering isn't guaranteed.
4. The maximum number of consensus rounds (views) is restricted. This constraint 4. The maximum number of consensus rounds (views) is restricted. This constraint
was introduced to reduce the number of possible model states to be checked. was introduced to reduce the number of possible model states to be checked.
The threshold may be specified via model configuration, and it is highly The threshold may be specified via model configuration, and it is highly
recommended to keep this setting less or equal to the number of consensus recommended to keep this setting less or equal to the number of consensus
nodes. nodes.
Here you can find the specification file and the TLC Model Checker launch Here you can find the specification file and the TLC Model Checker launch
configuration: configuration:
* [TLA⁺ specification](./dbft/dbft.tla) * [TLA⁺ specification](./dbft/dbft.tla)
* [TLC Model Checker configuration](./dbft/dbft___AllGoodModel.launch) * [TLC Model Checker configuration](./dbft/dbft___AllGoodModel.launch)
### Extended dBFT 2.0 model ### Extended dBFT 2.0 model
This is an experimental dBFT 2.0 specification that extends the This is an experimental dBFT 2.0 specification that extends the
[basic model](#basic-dbft-20-model) in the following way: besides the shared pool [basic model](#basic-dbft-20-model) in the following way: besides the shared pool
of consensus messages `msgs` each consensus node has its own local pool of of consensus messages `msgs` each consensus node has its own local pool of
received and handled messages. Decisions on transmission between the node states received and handled messages. Decisions on transmission between the node states
are taken by the node based on the state of the local message pool. This approach are taken by the node based on the state of the local message pool. This approach
allows to create more accurate low-leveled model which is extremely close to the allows to create more accurate low-leveled model which is extremely close to the
dBFT implementation presented in this repository. At the same time such approach dBFT implementation presented in this repository. At the same time such approach
*significantly* increases the number of considered model states which leads to *significantly* increases the number of considered model states which leads to
abnormally long TLC Model Checker runs. Thus, we do not recommend to use this abnormally long TLC Model Checker runs. Thus, we do not recommend to use this
model in development and place it here as an example of alternative (and more model in development and place it here as an example of alternative (and more
detailed) dBFT specification. These two models are expected to be equivalent in detailed) dBFT specification. These two models are expected to be equivalent in
terms of the liveness locks that can be discovered by both of them, and, speaking terms of the liveness locks that can be discovered by both of them, and, speaking
the TLA⁺ language, the Extended dBFT specification implements the the TLA⁺ language, the Extended dBFT specification implements the
[basic one](#basic-dbft-20-model) (which can be proven and written in TLA⁺, but [basic one](#basic-dbft-20-model) (which can be proven and written in TLA⁺, but
stays out of the task scope). stays out of the task scope).
Except for this remark and a couple of minor differences all the Except for this remark and a couple of minor differences all the
[basic model](#basic-dbft-20-model) description, constraints and assumptions are [basic model](#basic-dbft-20-model) description, constraints and assumptions are
valid for the Extended specification as far. Thus, we highly recommend to valid for the Extended specification as far. Thus, we highly recommend to
consider the [basic model](#basic-dbft-20-model) before going to the Extended consider the [basic model](#basic-dbft-20-model) before going to the Extended
one. one.
Here you can find the specification file and the TLC Model Checker launch Here you can find the specification file and the TLC Model Checker launch
configuration: configuration:
* [TLA⁺ specification](./dbftMultipool/dbftMultipool.tla) * [TLA⁺ specification](./dbftMultipool/dbftMultipool.tla)
* [TLC Model Checker configuration](./dbftMultipool/dbftMultipool___AllGoodModel.launch) * [TLC Model Checker configuration](./dbftMultipool/dbftMultipool___AllGoodModel.launch)
## Proposed dBFT 2.1 models ## Proposed dBFT 2.1 models
Based on the liveness locks scenarios found by the TLC model checker in the Based on the liveness locks scenarios found by the TLC model checker in the
[basic dBFT 2.0 model](#basic-dbft-20-model) we've developed two extensions of [basic dBFT 2.0 model](#basic-dbft-20-model) we've developed two extensions of
dBFT 2.0 protocol that allow to avoid the liveness lock problem and to preserve dBFT 2.0 protocol that allow to avoid the liveness lock problem and to preserve
the safety properties of the algorithm. The extensions currently don't have the safety properties of the algorithm. The extensions currently don't have
any code-level implementation and presented as a TLA⁺ specifications ready to be any code-level implementation and presented as a TLA⁺ specifications ready to be
reviewed and discussed. The improved protocol presented in the extensions will reviewed and discussed. The improved protocol presented in the extensions will
be referred below as dBFT 2.1. be referred below as dBFT 2.1.
We've checked both dBFT 2.1 models with the TLC Model Checker against the same We've checked both dBFT 2.1 models with the TLC Model Checker against the same
set of launch configurations that was used to reveal the liveness problems of the set of launch configurations that was used to reveal the liveness problems of the
[basic dBFT 2.0 model](#basic-dbft-20-model). The improved models have larger [basic dBFT 2.0 model](#basic-dbft-20-model). The improved models have larger
set of states, thus, the TLC Model Checker wasn't able to finish the liveness set of states, thus, the TLC Model Checker wasn't able to finish the liveness
requirements checks for *all* possible states. However, the checks have passed for requirements checks for *all* possible states. However, the checks have passed for
a state graph diameter that was large enough to believe the presented models a state graph diameter that was large enough to believe the presented models
solve the dBFT 2.0 liveness lock problems. solve the dBFT 2.0 liveness lock problems.
### Common `Commit` message improvement note ### Common `Commit` message improvement note
Here and below we assume that `Commit` messages should bear preparation hashes Here and below we assume that `Commit` messages should bear preparation hashes
from all nodes that have sent the preparation message (>= `M` but not including from all nodes that have sent the preparation message (>= `M` but not including
a whole `PrepareRequest`). This quickly synchronizes nodes still at the preparation a whole `PrepareRequest`). This quickly synchronizes nodes still at the preparation
stage when someone else collects enough preparations. It at the same time prevents stage when someone else collects enough preparations. It at the same time prevents
malicious/byzantine nodes from sending spoofed `Commit` messages. The `Commit` malicious/byzantine nodes from sending spoofed `Commit` messages. The `Commit`
message size becomes a little bigger, but since it's just hashes it still fits message size becomes a little bigger, but since it's just hashes it still fits
into a single packet in the vast majority of the cases, so it doesn't really matter. into a single packet in the vast majority of the cases, so it doesn't really matter.
### dBFT 2.1 stages-based model ### dBFT 2.1 stages-based model
The basic idea of this model is to split the consensus process into three subsequent The basic idea of this model is to split the consensus process into three subsequent
stages marked as `I`, `II` and `III` at the scheme. To perform a transition between stages marked as `I`, `II` and `III` at the scheme. To perform a transition between
two subsequent stages each consensus node should wait for a set of messages from two subsequent stages each consensus node should wait for a set of messages from
at least `M` consensus nodes to be received so that it's possible to complete a full at least `M` consensus nodes to be received so that it's possible to complete a full
picture of the neighbours' decisions in the current consensus round. In other words, picture of the neighbours' decisions in the current consensus round. In other words,
no transition can happen unless we have `M` number of messages from the subsequent round, no transition can happen unless we have `M` number of messages from the subsequent round,
timers are only set up after we have this number of messages, just to wait for timers are only set up after we have this number of messages, just to wait for
(potentially) a whole set of them. At the same time, each of the stages has (potentially) a whole set of them. At the same time, each of the stages has
its own `ChangeView[1,2,3]` message to exit to the next consensus round (view) if its own `ChangeView[1,2,3]` message to exit to the next consensus round (view) if
something goes wrong in the current one and there's definitely no ability to something goes wrong in the current one and there's definitely no ability to
continue consensus process in the current view. Below there's a short description continue consensus process in the current view. Below there's a short description
of each stage. Please, refer to the model scheme and specification for further of each stage. Please, refer to the model scheme and specification for further
details. details.
#### Stage I #### Stage I
Once initialized, consensus node has two ways: Once initialized, consensus node has two ways:
1. Send its `PrepareRequest`/`PrepareResponse` message (and transmit to the 1. Send its `PrepareRequest`/`PrepareResponse` message (and transmit to the
`prepareSent` state). `prepareSent` state).
2. Decide to go to the next view on timeout or any other valid reason (like 2. Decide to go to the next view on timeout or any other valid reason (like
transaction missing in the node's mempool or wrong proposal) via sending transaction missing in the node's mempool or wrong proposal) via sending
`ChangeView1` message (and transmit to the `cv1` state). `ChangeView1` message (and transmit to the `cv1` state).
This scheme is quite similar to the basic dBFT 2.0 model except the new type of This scheme is quite similar to the basic dBFT 2.0 model except the new type of
`ChangeView` message. After that the node enters stage `I` and waits for consensus `ChangeView` message. After that the node enters stage `I` and waits for consensus
messages of stage `I` (`PrepareRequest` or `PrepareResponse` or `ChangeView1`) messages of stage `I` (`PrepareRequest` or `PrepareResponse` or `ChangeView1`)
from at least `M` neighbours which is needed to decide about the next actions. from at least `M` neighbours which is needed to decide about the next actions.
The set of received messages can be arranged in the following way: The set of received messages can be arranged in the following way:
* `M` messages of `ChangeView1` type denote that `M` nodes have decided to change * `M` messages of `ChangeView1` type denote that `M` nodes have decided to change
their view directly after initialization due to invalid/missing `PrepareRequest` their view directly after initialization due to invalid/missing `PrepareRequest`
which leads to immediate view changing. This is a "fail fast" route that is the which leads to immediate view changing. This is a "fail fast" route that is the
same as with dBFT 2.0 for the widespread case of missing primary. No additional same as with dBFT 2.0 for the widespread case of missing primary. No additional
delay is added, everything works as usual. delay is added, everything works as usual.
* `M` preparation messages (of type `PrepareRequest` or `PrepareResponse`) with * `M` preparation messages (of type `PrepareRequest` or `PrepareResponse`) with
missing `ChangeView3` denote that the majority of nodes have decided to commit which missing `ChangeView3` denote that the majority of nodes have decided to commit which
denotes the safe transition to stage `II` can be performed and `Commit` message denotes the safe transition to stage `II` can be performed and `Commit` message
can safely be sent even if there's `ChangeView1` message in the network. Notice can safely be sent even if there's `ChangeView1` message in the network. Notice
that `ChangeView3` check is just a protection against node seriously lagging that `ChangeView3` check is just a protection against node seriously lagging
behind. behind.
* `M` messages each of the type `PrepareRequest` or `PrepareResponse` or `ChangeView1` * `M` messages each of the type `PrepareRequest` or `PrepareResponse` or `ChangeView1`
where at least one message is of the type `ChangeView1` denote that at least `M` where at least one message is of the type `ChangeView1` denote that at least `M`
nodes have reached the stage `I` and the node can safely take further steps. nodes have reached the stage `I` and the node can safely take further steps.
The additional `| Commit | ≤ F | CV3 | > 0` condition requires the majority of The additional `| Commit | ≤ F | CV3 | > 0` condition requires the majority of
nodes not to have the `Commit` message to be sent so that it's still possible to nodes not to have the `Commit` message to be sent so that it's still possible to
collect enough `ChangeView[2,3]` messages to change the view in further stages. collect enough `ChangeView[2,3]` messages to change the view in further stages.
If so, then the safe transition to stage `II` can be performed and `ChangeView2` If so, then the safe transition to stage `II` can be performed and `ChangeView2`
message can safely be sent. message can safely be sent.
#### Stage II #### Stage II
Once the node has `Commit` or `ChangeView2` message sent, it enters the stage `II` Once the node has `Commit` or `ChangeView2` message sent, it enters the stage `II`
of the consensus process and waits for at least `M` messages of stage `II` of the consensus process and waits for at least `M` messages of stage `II`
(`Commit` or `ChangeView2`) to perform the transition to the next stage. The set (`Commit` or `ChangeView2`) to perform the transition to the next stage. The set
of accepted messages can be arranged in the following way: of accepted messages can be arranged in the following way:
* `M` messages of `ChangeView2` type denote that `M` nodes have decided to change * `M` messages of `ChangeView2` type denote that `M` nodes have decided to change
their view directly after entering the stage `II` due to timeout while waiting their view directly after entering the stage `II` due to timeout while waiting
for the `Commit` messages which leads to immediate view changing. for the `Commit` messages which leads to immediate view changing.
* `M` messages of type `Commit` denote that the majority of nodes have decided to * `M` messages of type `Commit` denote that the majority of nodes have decided to
commit which denotes the block can be accepted immediately without entering the commit which denotes the block can be accepted immediately without entering the
stage `III`. Notice that this is the regular flow of normal dBFT 2.0 consensus, stage `III`. Notice that this is the regular flow of normal dBFT 2.0 consensus,
it also hasn't been changed and proceeds the way it was before. it also hasn't been changed and proceeds the way it was before.
* `M` messages each of the type `Commit` or `ChangeView2` where not more than `F` * `M` messages each of the type `Commit` or `ChangeView2` where not more than `F`
messages are of the type `Commit` denotes that the majority of nodes decided to messages are of the type `Commit` denotes that the majority of nodes decided to
change their view after entering the stage `II` and there's not enough `Commit` change their view after entering the stage `II` and there's not enough `Commit`
messages to create the block (and produce the fork), thus, the safe transition messages to create the block (and produce the fork), thus, the safe transition
to stage `III` can be performed and `ChangeView3` message can safely be sent to stage `III` can be performed and `ChangeView3` message can safely be sent
even if there's `Commit` message in the network. even if there's `Commit` message in the network.
In addition, the direct transition from `cv2` state to the `commitSent` state is In addition, the direct transition from `cv2` state to the `commitSent` state is
added in case if it's clear that there's more than `F` nodes have decided to added in case if it's clear that there's more than `F` nodes have decided to
commit and no `ChangeView3` message has been received which means that it's possible commit and no `ChangeView3` message has been received which means that it's possible
to produce block in the current view. This path handles a corner case of missing to produce block in the current view. This path handles a corner case of missing
stage `I` messages, in fact, because `Commit` messages prove that there are at stage `I` messages, in fact, because `Commit` messages prove that there are at
least `M` preparation messages exist, but the node went `cv2` path just because least `M` preparation messages exist, but the node went `cv2` path just because
it missed some of them. it missed some of them.
#### Stage III #### Stage III
Unlike the basic dBFT 2.0 model where consensus node locks on the commit phase, Unlike the basic dBFT 2.0 model where consensus node locks on the commit phase,
stage `III` gives the ability to escape from the commit phase via collecting the set stage `III` gives the ability to escape from the commit phase via collecting the set
of `M` `ChangeView3` messages. This phase is reachable as soon as at least `M` nodes of `M` `ChangeView3` messages. This phase is reachable as soon as at least `M` nodes
has reached the phase `II` (have `ChangeView2` or `Commit` messages sent) and if has reached the phase `II` (have `ChangeView2` or `Commit` messages sent) and if
there's not enough `Commit` messages (<=`F`) to accept the block. This stage is there's not enough `Commit` messages (<=`F`) to accept the block. This stage is
added to avoid situation when the node is being locked on the `commitSent` state added to avoid situation when the node is being locked on the `commitSent` state
whereas the rest of the nodes (>`F`) is willing to go to the next view. whereas the rest of the nodes (>`F`) is willing to go to the next view.
Here's the scheme of transitions between consensus node states for the improved Here's the scheme of transitions between consensus node states for the improved
dBFT 2.1 stages-based model: dBFT 2.1 stages-based model:
![dBFT 2.1stages-based model](./.github/dbft2.1_threeStagedCV.png) ![dBFT 2.1stages-based model](./.github/dbft2.1_threeStagedCV.png)
Here you can find the specification file and the basic TLC Model Checker launch Here you can find the specification file and the basic TLC Model Checker launch
configuration: configuration:
* [TLA⁺ specification](./dbft2.1_threeStagedCV/dbftCV3.tla) * [TLA⁺ specification](./dbft2.1_threeStagedCV/dbftCV3.tla)
* [TLC Model Checker configuration](./dbft2.1_threeStagedCV/dbftCV3___AllGoodModel.launch) * [TLC Model Checker configuration](./dbft2.1_threeStagedCV/dbftCV3___AllGoodModel.launch)
### dBFT 2.1 model with the centralized view changes ### dBFT 2.1 model with the centralized view changes
The improvement which was taken as a base for this model is taken from the pBFT The improvement which was taken as a base for this model is taken from the pBFT
algorithm and is as follows. algorithm and is as follows.
The consensus process is split into two stages with the following meaning: The consensus process is split into two stages with the following meaning:
* Stage `I` holds the node states from which it's allowed to transmit to the subsequent * Stage `I` holds the node states from which it's allowed to transmit to the subsequent
view under assumption that the *new* proposal will be generated (as the basic dBFT 2.0 view under assumption that the *new* proposal will be generated (as the basic dBFT 2.0
model does). model does).
* Stage `II` holds the node states from which it's allowed to perform view change * Stage `II` holds the node states from which it's allowed to perform view change
*preserving* the proposal from the previous view. *preserving* the proposal from the previous view.
Another vital difference from the basic dBFT 2.0 model is that view changes are Another vital difference from the basic dBFT 2.0 model is that view changes are
being performed by the node on the `DoCV[1,2]` command (consensus message) sent by the being performed by the node on the `DoCV[1,2]` command (consensus message) sent by the
leader of the target view specified via `DoCV[1,2]` parameters. Aside from the target view leader of the target view specified via `DoCV[1,2]` parameters. Aside from the target view
parameter, `DoCV[1,2]` message contains the set of all related pre-received consensus messages parameter, `DoCV[1,2]` message contains the set of all related pre-received consensus messages
so that the receivers of `DoCV[1,2]` are able to check its validness before the subsequent so that the receivers of `DoCV[1,2]` are able to check its validness before the subsequent
view change. view change.
Below presented the short description of the proposed consensus process. Please, refer to the Below presented the short description of the proposed consensus process. Please, refer to the
model scheme and specification for further details. model scheme and specification for further details.
#### Stage I #### Stage I
Once initialized at view `v`, the consensus node has two ways: Once initialized at view `v`, the consensus node has two ways:
1. Send its `PrepareRequest`/`PrepareResponse` message (and transmit to the 1. Send its `PrepareRequest`/`PrepareResponse` message (and transmit to the
`prepareSent` state). `prepareSent` state).
2. Decide to go to the next view `v+1` on timeout or any other valid reason like 2. Decide to go to the next view `v+1` on timeout or any other valid reason like
transaction missing in the node's mempool via sending `ChangeView1(v+1)` message transaction missing in the node's mempool via sending `ChangeView1(v+1)` message
(and transmitting to the `cv1 to v'=v+1` state). (and transmitting to the `cv1 to v'=v+1` state).
After that the node enters stage `I` and perform as follows: After that the node enters stage `I` and perform as follows:
* If the node has its `PrepareRequest` or `PrepareResponse` sent: * If the node has its `PrepareRequest` or `PrepareResponse` sent:
* If at least `M` preparation messages (including its own) collected, then * If at least `M` preparation messages (including its own) collected, then
it's clear that the majority has proposal for view `v` being accepted as it's clear that the majority has proposal for view `v` being accepted as
valid and the node can safely send the `Commit` message and transmit to the valid and the node can safely send the `Commit` message and transmit to the
phase `II` (`commitSent` state). phase `II` (`commitSent` state).
* If there's not enough preparation payloads received from the neighbours for a * If there's not enough preparation payloads received from the neighbours for a
long time, then the node is allowed to transmit to the stage `II` via sending long time, then the node is allowed to transmit to the stage `II` via sending
its `ChangeView2` message (and changing its state to the `cv2 to v'=v+1`). It its `ChangeView2` message (and changing its state to the `cv2 to v'=v+1`). It
denotes the node's desire to change view to the next one with the current proposal denotes the node's desire to change view to the next one with the current proposal
to be preserved. to be preserved.
* If the node entered the `cv1 to v'=v+1` state: * If the node entered the `cv1 to v'=v+1` state:
* If there's a majority (>=`M`) of `ChangeView1(v+1)` messages and the node is * If there's a majority (>=`M`) of `ChangeView1(v+1)` messages and the node is
primary in the view `v+1` then it should send the signal (`DoCV1(v+1)` message) primary in the view `v+1` then it should send the signal (`DoCV1(v+1)` message)
to the rest of the group to change their view to `v+1` with the new proposal to the rest of the group to change their view to `v+1` with the new proposal
generated. The rest of the group (backup on `v+1` view that have sent their generated. The rest of the group (backup on `v+1` view that have sent their
`ChangeVeiew1(v+1)` messages) should change their view on `DoCV1(v+1)` receiving. `ChangeVeiew1(v+1)` messages) should change their view on `DoCV1(v+1)` receiving.
* If there's a majority (>=`M`) of `ChangeView1(v+1)` messages collected, but * If there's a majority (>=`M`) of `ChangeView1(v+1)` messages collected, but
`DoCV1(v+1)` is missing for a long time, then the node is able to "skip" view `DoCV1(v+1)` is missing for a long time, then the node is able to "skip" view
`v+1` and send the `ChangeView1(v+2)` message hoping that the primary of `v+2` `v+1` and send the `ChangeView1(v+2)` message hoping that the primary of `v+2`
will be faster enough to send the `DoCV1(v+2)` signal. The process can be repeated will be faster enough to send the `DoCV1(v+2)` signal. The process can be repeated
on timeout for view `v+3`, etc. on timeout for view `v+3`, etc.
* If there's more than `F` nodes that have sent their preparation messages * If there's more than `F` nodes that have sent their preparation messages
(and, consequently, announced their desire to transmit to the stage `II` of the (and, consequently, announced their desire to transmit to the stage `II` of the
current view rather than to change view), then it's clear that it won't be more than `F` messages current view rather than to change view), then it's clear that it won't be more than `F` messages
of type `ChangeView1` to perform transition to the next view from the stage `II`. of type `ChangeView1` to perform transition to the next view from the stage `II`.
Thus, the node is allowed to send its `ChangeView2` message (and change its Thus, the node is allowed to send its `ChangeView2` message (and change its
state to the `cv2 to v'=v+1`). Such situation may happen if the node haven't state to the `cv2 to v'=v+1`). Such situation may happen if the node haven't
proposal received in time (consider valid proposal). proposal received in time (consider valid proposal).
#### Stage II #### Stage II
Once the node has entered the stage `II`, the proposal of the current round is Once the node has entered the stage `II`, the proposal of the current round is
considered to be valid. Depending on the node's state the following decisions are considered to be valid. Depending on the node's state the following decisions are
possible: possible:
* If the node has its `Commit` sent and is in the `commitSent` state: * If the node has its `Commit` sent and is in the `commitSent` state:
* If the majority (>=`M`) of the `Commit` messages has been received, then the * If the majority (>=`M`) of the `Commit` messages has been received, then the
block may be safely accepted for the current proposal. block may be safely accepted for the current proposal.
* If there's not enough `Commit` messages for a long time, then it's legal to * If there's not enough `Commit` messages for a long time, then it's legal to
send the `ChangeView2(v+1)` message, transmit to the `cv2 to v'=v+1` state send the `ChangeView2(v+1)` message, transmit to the `cv2 to v'=v+1` state
and decide to go to the next view `v+1` preserving the current proposal and decide to go to the next view `v+1` preserving the current proposal
and hoping that it would be possible to collect enough `Commit` messages and hoping that it would be possible to collect enough `Commit` messages
for it in the view `v+1`. for it in the view `v+1`.
* If the node is in the `cv2 to v'=v+1` state then: * If the node is in the `cv2 to v'=v+1` state then:
* If there's a majority (>=`M`) of `ChangeView2(v+1)` messages and the node is * If there's a majority (>=`M`) of `ChangeView2(v+1)` messages and the node is
primary in the view `v+1` then it should send the signal (`DoCV2(v+1)` message) primary in the view `v+1` then it should send the signal (`DoCV2(v+1)` message)
to the rest of the group to change their view to `v+1` with the old proposal of view `v` to the rest of the group to change their view to `v+1` with the old proposal of view `v`
preserved. The rest of the group (backup on `v+1` view that have sent their preserved. The rest of the group (backup on `v+1` view that have sent their
`ChangeVeiew2(v+1)` messages) should change their view on `DoCV2(v+1)` receiving. `ChangeVeiew2(v+1)` messages) should change their view on `DoCV2(v+1)` receiving.
* If there's a majority (>=`M`) of `ChangeView2(v+1)` messages collected, but * If there's a majority (>=`M`) of `ChangeView2(v+1)` messages collected, but
`DoCV2(v+1)` is missing for a long time, then the node is able to "skip" view `DoCV2(v+1)` is missing for a long time, then the node is able to "skip" view
`v+1` and send the `ChangeView2(v+2)` message hoping that the primary of `v+2` `v+1` and send the `ChangeView2(v+2)` message hoping that the primary of `v+2`
will be faster enough to send the `DoCV2(v+2)` signal. The process can be repeated will be faster enough to send the `DoCV2(v+2)` signal. The process can be repeated
on timeout for view `v+3`, etc. on timeout for view `v+3`, etc.
* Finally, if the node receives at least `M` messages from the stage `I` at max * Finally, if the node receives at least `M` messages from the stage `I` at max
`F` of which are preparations (the blue dotted arrow from `cv2 to v'=v+1` to `F` of which are preparations (the blue dotted arrow from `cv2 to v'=v+1` to
`cv1 to v'=v+1` state), it has the ability to go back to the `cv1` state to start `cv1 to v'=v+1` state), it has the ability to go back to the `cv1` state to start
the new consensus round with the new proposal. This case is kind of special, the new consensus round with the new proposal. This case is kind of special,
it allows to escape from the deadlock situation when the node is locked on `cv2` it allows to escape from the deadlock situation when the node is locked on `cv2`
state unable to perform any further steps whereas the rest of the network are state unable to perform any further steps whereas the rest of the network are
waiting in the `cv1` state. Consider the case of four-nodes network where the waiting in the `cv1` state. Consider the case of four-nodes network where the
first node is permanently "dead", the primary have sent its `PrepareRequest` first node is permanently "dead", the primary have sent its `PrepareRequest`
and went to the `cv2` state on the timeout and the rest two nodes are waiting and went to the `cv2` state on the timeout and the rest two nodes are waiting
in the `cv1` state not able to move further. in the `cv1` state not able to move further.
It should be noted that "preserving the proposal of view `v` in view `v+1`" means It should be noted that "preserving the proposal of view `v` in view `v+1`" means
that the primary of view `v+1` broadcasts the `PrepareRequest` message at view `v+1` that the primary of view `v+1` broadcasts the `PrepareRequest` message at view `v+1`
that contains the same set of block's fields (transactions, timestamp, primary, etc) as that contains the same set of block's fields (transactions, timestamp, primary, etc) as
the `PrepareRequest` proposed in the view `v` has. the `PrepareRequest` proposed in the view `v` has.
Here's the scheme of transitions between consensus node states for the improved Here's the scheme of transitions between consensus node states for the improved
dBFT 2.1 model with the centralized view changes process: dBFT 2.1 model with the centralized view changes process:
![dBFT 2.1 model with the centralized view changes](./.github/dbft2.1_centralizedCV.png) ![dBFT 2.1 model with the centralized view changes](./.github/dbft2.1_centralizedCV.png)
Here you can find the specification file and the basic TLC Model Checker launch Here you can find the specification file and the basic TLC Model Checker launch
configuration: configuration:
* [TLA⁺ specification](./dbft2.1_centralizedCV/dbftCentralizedCV.tla) * [TLA⁺ specification](./dbft2.1_centralizedCV/dbftCentralizedCV.tla)
* [TLC Model Checker configuration](./dbft2.1_centralizedCV/dbftCentralizedCV___AllGoodModel.launch) * [TLC Model Checker configuration](./dbft2.1_centralizedCV/dbftCentralizedCV___AllGoodModel.launch)
## MEV-resistant dBFT models ## MEV-resistant dBFT models
[Neo X chain](https://docs.banelabs.org/) uses dBFT 2.0 algorithm as a consensus engine. As a part of [Neo X chain](https://docs.banelabs.org/) uses dBFT 2.0 algorithm as a consensus engine. As a part of
the Neo X anti-MEV feature implementation, dBFT 2.0 extension was designed to the Neo X anti-MEV feature implementation, dBFT 2.0 extension was designed to
provide single-block finality for encrypted transactions (a.k.a. envelope provide single-block finality for encrypted transactions (a.k.a. envelope
transactions). Compared to dBFT 2.0, MEV-resistant dBFT algorithm includes an transactions). Compared to dBFT 2.0, MEV-resistant dBFT algorithm includes an
additional `post-Commit` phase that is required to be passed through by consensus additional `post-Commit` phase that is required to be passed through by consensus
nodes before every block acceptance. This phase allows consensus nodes to exchange nodes before every block acceptance. This phase allows consensus nodes to exchange
some additional data related to encrypted transactions and to the final state of some additional data related to encrypted transactions and to the final state of
accepting block using a new type of consensus messages. The improved protocol based accepting block using a new type of consensus messages. The improved protocol based
on dBFT 2.0 with an additional phase will be referred below as MEV-resistant dBFT. on dBFT 2.0 with an additional phase will be referred below as MEV-resistant dBFT.
We've checked MEV-resistant dBFT model with the TLC Model Checker against the same We've checked MEV-resistant dBFT model with the TLC Model Checker against the same
set of launch configurations that was used to reveal the liveness problems of the set of launch configurations that was used to reveal the liveness problems of the
[basic dBFT 2.0 model](#basic-dbft-20-model). MEV-resistant dBFT model brings no extra problems to the [basic dBFT 2.0 model](#basic-dbft-20-model). MEV-resistant dBFT model brings no extra problems to the
protocol, but it has been proved that this model has exactly the same protocol, but it has been proved that this model has exactly the same
[liveness bug](https://github.com/neo-project/neo-modules/issues/792) that the [liveness bug](https://github.com/neo-project/neo-modules/issues/792) that the
original dBFT 2.0 model has which is expected. original dBFT 2.0 model has which is expected.
### Basic MEV-resistant dBFT model ### Basic MEV-resistant dBFT model
This specification is an extension of the This specification is an extension of the
[basic dBFT 2.0 model](#basic-dbft-20-model). Compared to the base model, [basic dBFT 2.0 model](#basic-dbft-20-model). Compared to the base model,
MEV-resistant dBFT specification additionally includes: MEV-resistant dBFT specification additionally includes:
1. New message type `CommitAck` aimed to reflect an additional protocol 1. New message type `CommitAck` aimed to reflect an additional protocol
message that should be sent by resource manager if at least `M` `Commit` message that should be sent by resource manager if at least `M` `Commit`
messages were collected by the node (that confirms a.k.a. "PreBlock" messages were collected by the node (that confirms a.k.a. "PreBlock"
final acceptance). final acceptance).
2. New resource manager state `commitAckSent` aimed to reflect the additional phase 2. New resource manager state `commitAckSent` aimed to reflect the additional phase
of the protocol needed for consensus nodes to exchange some data that was not of the protocol needed for consensus nodes to exchange some data that was not
available at the time of the first commit. This RM state represents a consensus available at the time of the first commit. This RM state represents a consensus
node state when it has sent these additional post-commit data but has not accepted node state when it has sent these additional post-commit data but has not accepted
the final block yet. the final block yet.
3. New specification step `RMSendCommitAck` describing the transition between 3. New specification step `RMSendCommitAck` describing the transition between
`commitSent` and `commitAckSent` phases of the protocol, or, which is the same, `commitSent` and `commitAckSent` phases of the protocol, or, which is the same,
corresponding resource managers states. This step allows the resource manager to corresponding resource managers states. This step allows the resource manager to
send `CommitAck` message if at least `M` valid `Commit` messages are collected. send `CommitAck` message if at least `M` valid `Commit` messages are collected.
4. Adjusted behaviour of `RMAcceptBlock` step: block acceptance is possible iff the 4. Adjusted behaviour of `RMAcceptBlock` step: block acceptance is possible iff the
node has sent the `CommitAck` message and there are at least `M` `CommitAck` node has sent the `CommitAck` message and there are at least `M` `CommitAck`
messages collected by the node. messages collected by the node.
5. Adjusted behaviour of "faulty" resource managers: allow malicious nodes to send an 5. Adjusted behaviour of "faulty" resource managers: allow malicious nodes to send an
`CommitAck` message via `RMFaultySendCommitAck` step. `CommitAck` message via `RMFaultySendCommitAck` step.
It should be noted that, in comparison with the dBFT 2.0 protocol where the node is It should be noted that, in comparison with the dBFT 2.0 protocol where the node is
being locked in the `commitSent` state until the block acceptance, MEV-resistant dBFT being locked in the `commitSent` state until the block acceptance, MEV-resistant dBFT
does not allow to accept the block right after the `commitSent` state. However, it does not allow to accept the block right after the `commitSent` state. However, it
allows the node to move from `commitSent` phase further to the `commitAckSent` state allows the node to move from `commitSent` phase further to the `commitAckSent` state
and locks the node at this state until the block acceptance. No view change may be and locks the node at this state until the block acceptance. No view change may be
initiated or accepted by a node entered the `commitAckSent` state. initiated or accepted by a node entered the `commitAckSent` state.
Here's the scheme of transitions between consensus node states for MEV-resistant dBFT Here's the scheme of transitions between consensus node states for MEV-resistant dBFT
algorithm: algorithm:
![Basic MEV-resistant dBFT model transitions scheme](./.github/dbft_antiMEV.png) ![Basic MEV-resistant dBFT model transitions scheme](./.github/dbft_antiMEV.png)
Here you can find the specification file and the basic MEV-resistant dBFT TLC Model Here you can find the specification file and the basic MEV-resistant dBFT TLC Model
Checker launch configuration for the four "honest" consensus nodes scenario: Checker launch configuration for the four "honest" consensus nodes scenario:
* [TLA⁺ specification](dbft_antiMEV/dbft.tla) * [TLA⁺ specification](dbft_antiMEV/dbft.tla)
* [TLC Model Checker configuration](dbft_antiMEV/dbft___AllGoodModel.launch) * [TLC Model Checker configuration](dbft_antiMEV/dbft___AllGoodModel.launch)
## How to run/check the TLA⁺ specification ## How to run/check the TLA⁺ specification
### Prerequirements ### Prerequirements
1. Download and install the TLA⁺ Toolbox following the 1. Download and install the TLA⁺ Toolbox following the
[official guide](http://lamport.azurewebsites.net/tla/toolbox.html). [official guide](http://lamport.azurewebsites.net/tla/toolbox.html).
2. Read the brief introduction to the TLA⁺ language and TLC Model Checker at the 2. Read the brief introduction to the TLA⁺ language and TLC Model Checker at the
[official site](http://lamport.azurewebsites.net/tla/high-level-view.html). [official site](http://lamport.azurewebsites.net/tla/high-level-view.html).
3. Download and take a look at the 3. Download and take a look at the
[TLA⁺ cheat sheet](https://lamport.azurewebsites.net/tla/summary-standalone.pdf). [TLA⁺ cheat sheet](https://lamport.azurewebsites.net/tla/summary-standalone.pdf).
4. For a proficient learning watch the 4. For a proficient learning watch the
[TLA⁺ Video Course](https://lamport.azurewebsites.net/video/videos.html) and [TLA⁺ Video Course](https://lamport.azurewebsites.net/video/videos.html) and
read the [Specifying Systems book](http://lamport.azurewebsites.net/tla/book.html?back-link=tools.html#documentation). read the [Specifying Systems book](http://lamport.azurewebsites.net/tla/book.html?back-link=tools.html#documentation).
### Running the TLC model checker ### Running the TLC model checker
1. Clone the [repository](https://github.com/nspcc-dev/dbft.git). 1. Clone the [repository](https://github.com/nspcc-dev/dbft.git).
2. Open the TLA⁺ Toolbox, open new specification and provide path to the desired 2. Open the TLA⁺ Toolbox, open new specification and provide path to the desired
`*.tla` file that contains the specification description. `*.tla` file that contains the specification description.
3. Create the model named `AllGoodModel` in the TLA⁺ Toolbox. 3. Create the model named `AllGoodModel` in the TLA⁺ Toolbox.
4. Copy the corresponding `*___AllGoodModel.launch` file to the `*.toolbox` 4. Copy the corresponding `*___AllGoodModel.launch` file to the `*.toolbox`
folder. Reload/refresh the model in the TLA⁺ Toolbox. folder. Reload/refresh the model in the TLA⁺ Toolbox.
5. Open the `Model Overview` window in the TLA⁺ Toolbox and check that behaviour 5. Open the `Model Overview` window in the TLA⁺ Toolbox and check that behaviour
specification, declared constants, invariants and properties of the model are specification, declared constants, invariants and properties of the model are
filled in with some values. filled in with some values.
6. Press `Run TLC on the model` button to start the model checking process and 6. Press `Run TLC on the model` button to start the model checking process and
explore the progress in the `Model Checkng Results` window. explore the progress in the `Model Checkng Results` window.
### Model checking note ### Model checking note
It should be noted that all TLA⁺ specifications provided in this repo can be perfectly checked It should be noted that all TLA⁺ specifications provided in this repo can be perfectly checked
with `MaxView` model constraint set to be 1 for the four-nodes network scenario. Larger with `MaxView` model constraint set to be 1 for the four-nodes network scenario. Larger
`MaxView` values produces too many behaviours to be checked, so TLC Model Checker is likely `MaxView` values produces too many behaviours to be checked, so TLC Model Checker is likely
to fail with OOM during the checking process. However, `MaxView` set to be 1 is enough to check to fail with OOM during the checking process. However, `MaxView` set to be 1 is enough to check
the model liveness properties for the four-nodes scenario as there are two views to be checked the model liveness properties for the four-nodes scenario as there are two views to be checked
in this case (0 and 1). in this case (0 and 1).

View File

@ -1,388 +1,388 @@
-------------------------------- MODULE dbft -------------------------------- -------------------------------- MODULE dbft --------------------------------
EXTENDS EXTENDS
Integers, Integers,
FiniteSets FiniteSets
CONSTANTS CONSTANTS
\* RM is the set of consensus node indexes starting from 0. \* RM is the set of consensus node indexes starting from 0.
\* Example: {0, 1, 2, 3} \* Example: {0, 1, 2, 3}
RM, RM,
\* RMFault is a set of consensus node indexes that are allowed to become \* RMFault is a set of consensus node indexes that are allowed to become
\* FAULT in the middle of every considered behavior and to send any \* FAULT in the middle of every considered behavior and to send any
\* consensus message afterwards. RMFault must be a subset of RM. An empty \* consensus message afterwards. RMFault must be a subset of RM. An empty
\* set means that all nodes are good in every possible behaviour. \* set means that all nodes are good in every possible behaviour.
\* Examples: {0} \* Examples: {0}
\* {1, 3} \* {1, 3}
\* {} \* {}
RMFault, RMFault,
\* RMDead is a set of consensus node indexes that are allowed to die in the \* RMDead is a set of consensus node indexes that are allowed to die in the
\* middle of every behaviour and do not send any message afterwards. RMDead \* middle of every behaviour and do not send any message afterwards. RMDead
\* must be a subset of RM. An empty set means that all nodes are alive and \* must be a subset of RM. An empty set means that all nodes are alive and
\* responding in in every possible behaviour. RMDead may intersect the \* responding in in every possible behaviour. RMDead may intersect the
\* RMFault set which means that node which is in both RMDead and RMFault \* RMFault set which means that node which is in both RMDead and RMFault
\* may become FAULT and send any message starting from some step of the \* may become FAULT and send any message starting from some step of the
\* particular behaviour and may also die in the same behaviour which will \* particular behaviour and may also die in the same behaviour which will
\* prevent it from sending any message. \* prevent it from sending any message.
\* Examples: {0} \* Examples: {0}
\* {3, 2} \* {3, 2}
\* {} \* {}
RMDead, RMDead,
\* MaxView is the maximum allowed view to be considered (starting from 0, \* MaxView is the maximum allowed view to be considered (starting from 0,
\* including the MaxView itself). This constraint was introduced to reduce \* including the MaxView itself). This constraint was introduced to reduce
\* the number of possible model states to be checked. It is recommended to \* the number of possible model states to be checked. It is recommended to
\* keep this setting not too high (< N is highly recommended). \* keep this setting not too high (< N is highly recommended).
\* Example: 2 \* Example: 2
MaxView MaxView
VARIABLES VARIABLES
\* rmState is a set of consensus node states. It is represented by the \* rmState is a set of consensus node states. It is represented by the
\* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is \* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is
\* the state of the r-th consensus node at the current step. \* the state of the r-th consensus node at the current step.
rmState, rmState,
\* msgs is the shared pool of messages sent to the network by consensus nodes. \* msgs is the shared pool of messages sent to the network by consensus nodes.
\* It is represented by a subset of Messages set. \* It is represented by a subset of Messages set.
msgs msgs
\* vars is a tuple of all variables used in the specification. It is needed to \* vars is a tuple of all variables used in the specification. It is needed to
\* simplify fairness conditions definition. \* simplify fairness conditions definition.
vars == <<rmState, msgs>> vars == <<rmState, msgs>>
\* N is the number of validators. \* N is the number of validators.
N == Cardinality(RM) N == Cardinality(RM)
\* F is the number of validators that are allowed to be malicious. \* F is the number of validators that are allowed to be malicious.
F == (N - 1) \div 3 F == (N - 1) \div 3
\* M is the number of validators that must function correctly. \* M is the number of validators that must function correctly.
M == N - F M == N - F
\* These assumptions are checked by the TLC model checker once at the start of \* These assumptions are checked by the TLC model checker once at the start of
\* the model checking process. All the input data (declared constants) specified \* the model checking process. All the input data (declared constants) specified
\* in the "Model Overview" section must satisfy these constraints. \* in the "Model Overview" section must satisfy these constraints.
ASSUME ASSUME
/\ RM \subseteq Nat /\ RM \subseteq Nat
/\ N >= 4 /\ N >= 4
/\ 0 \in RM /\ 0 \in RM
/\ RMFault \subseteq RM /\ RMFault \subseteq RM
/\ RMDead \subseteq RM /\ RMDead \subseteq RM
/\ Cardinality(RMFault) <= F /\ Cardinality(RMFault) <= F
/\ Cardinality(RMDead) <= F /\ Cardinality(RMDead) <= F
/\ Cardinality(RMFault \cup RMDead) <= F /\ Cardinality(RMFault \cup RMDead) <= F
/\ MaxView \in Nat /\ MaxView \in Nat
/\ MaxView <= 2 /\ MaxView <= 2
\* RMStates is a set of records where each record holds the node state and \* RMStates is a set of records where each record holds the node state and
\* the node current view. \* the node current view.
RMStates == [ RMStates == [
type: {"initialized", "prepareSent", "commitSent", "cv", "blockAccepted", "bad", "dead"}, type: {"initialized", "prepareSent", "commitSent", "cv", "blockAccepted", "bad", "dead"},
view : Nat view : Nat
] ]
\* Messages is a set of records where each record holds the message type, \* Messages is a set of records where each record holds the message type,
\* the message sender and sender's view by the moment when message was sent. \* the message sender and sender's view by the moment when message was sent.
Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView"}, rm : RM, view : Nat] Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView"}, rm : RM, view : Nat]
\* -------------- Useful operators -------------- \* -------------- Useful operators --------------
\* IsPrimary is an operator defining whether provided node r is primary \* IsPrimary is an operator defining whether provided node r is primary
\* for the current round from the r's point of view. It is a mapping \* for the current round from the r's point of view. It is a mapping
\* from RM to the set of {TRUE, FALSE}. \* from RM to the set of {TRUE, FALSE}.
IsPrimary(r) == rmState[r].view % N = r IsPrimary(r) == rmState[r].view % N = r
\* GetPrimary is an operator defining mapping from round index to the RM that \* GetPrimary is an operator defining mapping from round index to the RM that
\* is primary in this round. \* is primary in this round.
GetPrimary(view) == CHOOSE r \in RM : view % N = r GetPrimary(view) == CHOOSE r \in RM : view % N = r
\* GetNewView returns new view number based on the previous node view value. \* GetNewView returns new view number based on the previous node view value.
\* Current specifications only allows to increment view. \* Current specifications only allows to increment view.
GetNewView(oldView) == oldView + 1 GetNewView(oldView) == oldView + 1
\* CountCommitted returns the number of nodes that have sent the Commit message \* CountCommitted returns the number of nodes that have sent the Commit message
\* in the current round or in some other round. \* in the current round or in some other round.
CountCommitted(r) == Cardinality({rm \in RM : Cardinality({msg \in msgs : msg.rm = rm /\ msg.type = "Commit"}) /= 0}) CountCommitted(r) == Cardinality({rm \in RM : Cardinality({msg \in msgs : msg.rm = rm /\ msg.type = "Commit"}) /= 0})
\* MoreThanFNodesCommitted returns whether more than F nodes have been committed \* MoreThanFNodesCommitted returns whether more than F nodes have been committed
\* in the current round (as the node r sees it). \* in the current round (as the node r sees it).
\* \*
\* IMPORTANT NOTE: we intentionally do not add the "lost" nodes calculation to the specification, and here's \* IMPORTANT NOTE: we intentionally do not add the "lost" nodes calculation to the specification, and here's
\* the reason: from the node's point of view we can't reliably check that some neighbour is completely \* the reason: from the node's point of view we can't reliably check that some neighbour is completely
\* out of the network. It is possible that the node doesn't receive consensus messages from some other member \* out of the network. It is possible that the node doesn't receive consensus messages from some other member
\* due to network delays. On the other hand, real nodes can go down at any time. The absence of the \* due to network delays. On the other hand, real nodes can go down at any time. The absence of the
\* member's message doesn't mean that the member is out of the network, we never can be sure about \* member's message doesn't mean that the member is out of the network, we never can be sure about
\* that, thus, this information is unreliable and can't be trusted during the consensus process. \* that, thus, this information is unreliable and can't be trusted during the consensus process.
\* What can be trusted is whether there's a Commit message from some member was received by the node. \* What can be trusted is whether there's a Commit message from some member was received by the node.
MoreThanFNodesCommitted(r) == CountCommitted(r) > F MoreThanFNodesCommitted(r) == CountCommitted(r) > F
\* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest \* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest
\* message received from the current round's speaker (as the node r sees it). \* message received from the current round's speaker (as the node r sees it).
PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in msgs PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in msgs
\* -------------- Safety temporal formula -------------- \* -------------- Safety temporal formula --------------
\* Init is the initial predicate initializing values at the start of every \* Init is the initial predicate initializing values at the start of every
\* behaviour. \* behaviour.
Init == Init ==
/\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]] /\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]]
/\ msgs = {} /\ msgs = {}
\* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest. \* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest.
RMSendPrepareRequest(r) == RMSendPrepareRequest(r) ==
/\ rmState[r].type = "initialized" /\ rmState[r].type = "initialized"
/\ IsPrimary(r) /\ IsPrimary(r)
/\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"]
/\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]}
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from \* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from
\* the primary node of the current round (view) and broadcasting PrepareResponse. \* the primary node of the current round (view) and broadcasting PrepareResponse.
\* This step assumes that PrepareRequest always contains valid transactions and \* This step assumes that PrepareRequest always contains valid transactions and
\* signatures. \* signatures.
RMSendPrepareResponse(r) == RMSendPrepareResponse(r) ==
/\ \/ rmState[r].type = "initialized" /\ \/ rmState[r].type = "initialized"
\* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage \* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage
\* as it is done in the code-level dBFT implementation by checking the NotAcceptingPayloadsDueToViewChanging \* as it is done in the code-level dBFT implementation by checking the NotAcceptingPayloadsDueToViewChanging
\* condition (see \* condition (see
\* https://github.com/nspcc-dev/dbft/blob/31c1bbdc74f2faa32ec9025062e3a4e2ccfd4214/dbft.go#L419 \* https://github.com/nspcc-dev/dbft/blob/31c1bbdc74f2faa32ec9025062e3a4e2ccfd4214/dbft.go#L419
\* and \* and
\* https://github.com/neo-project/neo-modules/blob/d00d90b9c27b3d0c3c57e9ca1f560a09975df241/src/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs#L79). \* https://github.com/neo-project/neo-modules/blob/d00d90b9c27b3d0c3c57e9ca1f560a09975df241/src/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs#L79).
\* However, we can't easily count the number of "lost" nodes in this specification to match precisely \* However, we can't easily count the number of "lost" nodes in this specification to match precisely
\* the implementation. Moreover, we don't need it to be counted as the RMSendPrepareResponse enabling \* the implementation. Moreover, we don't need it to be counted as the RMSendPrepareResponse enabling
\* condition specifies only the thing that may happen given some particular set of enabling conditions. \* condition specifies only the thing that may happen given some particular set of enabling conditions.
\* Thus, we've extended the NotAcceptingPayloadsDueToViewChanging condition to consider only MoreThanFNodesCommitted. \* Thus, we've extended the NotAcceptingPayloadsDueToViewChanging condition to consider only MoreThanFNodesCommitted.
\* It should be noted that the logic of MoreThanFNodesCommittedOrLost can't be reliable in detecting lost nodes \* It should be noted that the logic of MoreThanFNodesCommittedOrLost can't be reliable in detecting lost nodes
\* (even with neo-project/neo#2057), because real nodes can go down at any time. See the comment above the MoreThanFNodesCommitted. \* (even with neo-project/neo#2057), because real nodes can go down at any time. See the comment above the MoreThanFNodesCommitted.
\/ /\ rmState[r].type = "cv" \/ /\ rmState[r].type = "cv"
/\ MoreThanFNodesCommitted(r) /\ MoreThanFNodesCommitted(r)
/\ \neg IsPrimary(r) /\ \neg IsPrimary(r)
/\ PrepareRequestSentOrReceived(r) /\ PrepareRequestSentOrReceived(r)
/\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"]
/\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]}
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMSendCommit describes node r sending Commit if there's enough PrepareResponse \* RMSendCommit describes node r sending Commit if there's enough PrepareResponse
\* messages. \* messages.
RMSendCommit(r) == RMSendCommit(r) ==
/\ \/ rmState[r].type = "prepareSent" /\ \/ rmState[r].type = "prepareSent"
\* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage, \* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage,
\* see the related comment inside the RMSendPrepareResponse definition. \* see the related comment inside the RMSendPrepareResponse definition.
\/ /\ rmState[r].type = "cv" \/ /\ rmState[r].type = "cv"
/\ MoreThanFNodesCommitted(r) /\ MoreThanFNodesCommitted(r)
/\ Cardinality({ /\ Cardinality({
msg \in msgs : /\ (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest") msg \in msgs : /\ (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest")
/\ msg.view = rmState[r].view /\ msg.view = rmState[r].view
}) >= M }) >= M
/\ PrepareRequestSentOrReceived(r) /\ PrepareRequestSentOrReceived(r)
/\ rmState' = [rmState EXCEPT ![r].type = "commitSent"] /\ rmState' = [rmState EXCEPT ![r].type = "commitSent"]
/\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view]}
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMAcceptBlock describes node r collecting enough Commit messages and accepting \* RMAcceptBlock describes node r collecting enough Commit messages and accepting
\* the block. \* the block.
RMAcceptBlock(r) == RMAcceptBlock(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ PrepareRequestSentOrReceived(r) /\ PrepareRequestSentOrReceived(r)
/\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) >= M /\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) >= M
/\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"] /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMSendChangeView describes node r sending ChangeView message on timeout. \* RMSendChangeView describes node r sending ChangeView message on timeout.
RMSendChangeView(r) == RMSendChangeView(r) ==
/\ \/ (rmState[r].type = "initialized" /\ \neg IsPrimary(r)) /\ \/ (rmState[r].type = "initialized" /\ \neg IsPrimary(r))
\/ rmState[r].type = "prepareSent" \/ rmState[r].type = "prepareSent"
/\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]
IN /\ cv \notin msgs IN /\ cv \notin msgs
/\ rmState' = [rmState EXCEPT ![r].type = "cv"] /\ rmState' = [rmState EXCEPT ![r].type = "cv"]
/\ msgs' = msgs \cup {[type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]}
\* RMReceiveChangeView describes node r receiving enough ChangeView messages for \* RMReceiveChangeView describes node r receiving enough ChangeView messages for
\* view changing. \* view changing.
RMReceiveChangeView(r) == RMReceiveChangeView(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ rmState[r].type /= "blockAccepted" /\ rmState[r].type /= "blockAccepted"
/\ rmState[r].type /= "commitSent" /\ rmState[r].type /= "commitSent"
/\ Cardinality({ /\ Cardinality({
rm \in RM : Cardinality({ rm \in RM : Cardinality({
msg \in msgs : /\ msg.type = "ChangeView" msg \in msgs : /\ msg.type = "ChangeView"
/\ msg.rm = rm /\ msg.rm = rm
/\ GetNewView(msg.view) >= GetNewView(rmState[r].view) /\ GetNewView(msg.view) >= GetNewView(rmState[r].view)
}) /= 0 }) /= 0
}) >= M }) >= M
/\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(rmState[r].view)] /\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(rmState[r].view)]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMBeBad describes the faulty node r that will send any kind of consensus message starting \* RMBeBad describes the faulty node r that will send any kind of consensus message starting
\* from the step it's gone wild. This step is enabled only when RMFault is non-empty set. \* from the step it's gone wild. This step is enabled only when RMFault is non-empty set.
RMBeBad(r) == RMBeBad(r) ==
/\ r \in RMFault /\ r \in RMFault
/\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F /\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F
/\ rmState' = [rmState EXCEPT ![r].type = "bad"] /\ rmState' = [rmState EXCEPT ![r].type = "bad"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMFaultySendCV describes sending CV message by the faulty node r. \* RMFaultySendCV describes sending CV message by the faulty node r.
RMFaultySendCV(r) == RMFaultySendCV(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]
IN /\ cv \notin msgs IN /\ cv \notin msgs
/\ msgs' = msgs \cup {cv} /\ msgs' = msgs \cup {cv}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultyDoCV describes view changing by the faulty node r. \* RMFaultyDoCV describes view changing by the faulty node r.
RMFaultyDoCV(r) == RMFaultyDoCV(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)] /\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r. \* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r.
RMFaultySendPReq(r) == RMFaultySendPReq(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ IsPrimary(r) /\ IsPrimary(r)
/\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]
IN /\ pReq \notin msgs IN /\ pReq \notin msgs
/\ msgs' = msgs \cup {pReq} /\ msgs' = msgs \cup {pReq}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r. \* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r.
RMFaultySendPResp(r) == RMFaultySendPResp(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ \neg IsPrimary(r) /\ \neg IsPrimary(r)
/\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]
IN /\ pResp \notin msgs IN /\ pResp \notin msgs
/\ msgs' = msgs \cup {pResp} /\ msgs' = msgs \cup {pResp}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendCommit describes sending Commit message by the faulty node r. \* RMFaultySendCommit describes sending Commit message by the faulty node r.
RMFaultySendCommit(r) == RMFaultySendCommit(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] /\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view]
IN /\ commit \notin msgs IN /\ commit \notin msgs
/\ msgs' = msgs \cup {commit} /\ msgs' = msgs \cup {commit}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMDie describes node r that was removed from the network at the particular step \* RMDie describes node r that was removed from the network at the particular step
\* of the behaviour. After this node r can't change its state and accept/send messages. \* of the behaviour. After this node r can't change its state and accept/send messages.
RMDie(r) == RMDie(r) ==
/\ r \in RMDead /\ r \in RMDead
/\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F
/\ rmState' = [rmState EXCEPT ![r].type = "dead"] /\ rmState' = [rmState EXCEPT ![r].type = "dead"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* Terminating is an action that allows infinite stuttering to prevent deadlock on \* Terminating is an action that allows infinite stuttering to prevent deadlock on
\* behaviour termination. We consider termination to be valid if at least M nodes \* behaviour termination. We consider termination to be valid if at least M nodes
\* has the block being accepted. \* has the block being accepted.
Terminating == Terminating ==
/\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M /\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M
/\ UNCHANGED <<msgs, rmState>> /\ UNCHANGED <<msgs, rmState>>
\* Next is the next-state action describing the transition from the current state \* Next is the next-state action describing the transition from the current state
\* to the next state of the behaviour. \* to the next state of the behaviour.
Next == Next ==
\/ Terminating \/ Terminating
\/ \E r \in RM: \/ \E r \in RM:
RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r) RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r)
\/ RMAcceptBlock(r) \/ RMSendChangeView(r) \/ RMReceiveChangeView(r) \/ RMAcceptBlock(r) \/ RMSendChangeView(r) \/ RMReceiveChangeView(r)
\/ RMDie(r) \/ RMBeBad(r) \/ RMDie(r) \/ RMBeBad(r)
\/ RMFaultySendCV(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) \/ RMFaultySendCV(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r)
\* Safety is a temporal formula that describes the whole set of allowed \* Safety is a temporal formula that describes the whole set of allowed
\* behaviours. It specifies only what the system MAY do (i.e. the set of \* behaviours. It specifies only what the system MAY do (i.e. the set of
\* possible allowed behaviours for the system). It asserts only what may \* possible allowed behaviours for the system). It asserts only what may
\* happen; any behaviour that violates it does so at some point and \* happen; any behaviour that violates it does so at some point and
\* nothing past that point makes difference. \* nothing past that point makes difference.
\* \*
\* E.g. this safety formula (applied standalone) allows the behaviour to end \* E.g. this safety formula (applied standalone) allows the behaviour to end
\* with an infinite set of stuttering steps (those steps that DO NOT change \* with an infinite set of stuttering steps (those steps that DO NOT change
\* neither msgs nor rmState) and never reach the state where at least one \* neither msgs nor rmState) and never reach the state where at least one
\* node is committed or accepted the block. \* node is committed or accepted the block.
\* \*
\* To forbid such behaviours we must specify what the system MUST \* To forbid such behaviours we must specify what the system MUST
\* do. It will be specified below with the help of fairness conditions in \* do. It will be specified below with the help of fairness conditions in
\* the Fairness formula. \* the Fairness formula.
Safety == Init /\ [][Next]_vars Safety == Init /\ [][Next]_vars
\* -------------- Fairness temporal formula -------------- \* -------------- Fairness temporal formula --------------
\* Fairness is a temporal assumptions under which the model is working. \* Fairness is a temporal assumptions under which the model is working.
\* Usually it specifies different kind of assumptions for each/some \* Usually it specifies different kind of assumptions for each/some
\* subactions of the Next's state action, but the only think that bothers \* subactions of the Next's state action, but the only think that bothers
\* us is preventing infinite stuttering at those steps where some of Next's \* us is preventing infinite stuttering at those steps where some of Next's
\* subactions are enabled. Thus, the only thing that we require from the \* subactions are enabled. Thus, the only thing that we require from the
\* system is to keep take the steps until it's impossible to take them. \* system is to keep take the steps until it's impossible to take them.
\* That's exactly how the weak fairness condition works: if some action \* That's exactly how the weak fairness condition works: if some action
\* remains continuously enabled, it must eventually happen. \* remains continuously enabled, it must eventually happen.
Fairness == WF_vars(Next) Fairness == WF_vars(Next)
\* -------------- Specification -------------- \* -------------- Specification --------------
\* The complete specification of the protocol written as a temporal formula. \* The complete specification of the protocol written as a temporal formula.
Spec == Safety /\ Fairness Spec == Safety /\ Fairness
\* -------------- Liveness temporal formula -------------- \* -------------- Liveness temporal formula --------------
\* For every possible behaviour it's true that eventually (i.e. at least once \* For every possible behaviour it's true that eventually (i.e. at least once
\* through the behaviour) block will be accepted. It is something that dBFT \* through the behaviour) block will be accepted. It is something that dBFT
\* must guarantee (an in practice this condition is violated). \* must guarantee (an in practice this condition is violated).
TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M) TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M)
\* A liveness temporal formula asserts only what must happen (i.e. specifies \* A liveness temporal formula asserts only what must happen (i.e. specifies
\* what the system MUST do). Any behaviour can NOT violate it at ANY point; \* what the system MUST do). Any behaviour can NOT violate it at ANY point;
\* there's always the rest of the behaviour that can always make the liveness \* there's always the rest of the behaviour that can always make the liveness
\* formula true; if there's no such behaviour than the liveness formula is \* formula true; if there's no such behaviour than the liveness formula is
\* violated. The liveness formula is supposed to be checked as a property \* violated. The liveness formula is supposed to be checked as a property
\* by the TLC model checker. \* by the TLC model checker.
Liveness == TerminationRequirement Liveness == TerminationRequirement
\* -------------- ModelConstraints -------------- \* -------------- ModelConstraints --------------
\* MaxViewConstraint is a state predicate restricting the number of possible \* MaxViewConstraint is a state predicate restricting the number of possible
\* behaviour states. It is needed to reduce model checking time and prevent \* behaviour states. It is needed to reduce model checking time and prevent
\* the model graph size explosion. This formulae must be specified at the \* the model graph size explosion. This formulae must be specified at the
\* "State constraint" section of the "Additional Spec Options" section inside \* "State constraint" section of the "Additional Spec Options" section inside
\* the model overview. \* the model overview.
MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView
/\ \A msg \in msgs : msg.view <= MaxView /\ \A msg \in msgs : msg.view <= MaxView
\* -------------- Invariants of the specification -------------- \* -------------- Invariants of the specification --------------
\* Model invariant is a state predicate (statement) that must be true for \* Model invariant is a state predicate (statement) that must be true for
\* every step of every reachable behaviour. Model invariant is supposed to \* every step of every reachable behaviour. Model invariant is supposed to
\* be checked as an Invariant by the TLC Model Checker. \* be checked as an Invariant by the TLC Model Checker.
\* TypeOK is a type-correctness invariant. It states that all elements of \* TypeOK is a type-correctness invariant. It states that all elements of
\* specification variables must have the proper type throughout the behaviour. \* specification variables must have the proper type throughout the behaviour.
TypeOK == TypeOK ==
/\ rmState \in [RM -> RMStates] /\ rmState \in [RM -> RMStates]
/\ msgs \subseteq Messages /\ msgs \subseteq Messages
\* InvTwoBlocksAccepted states that there can't be two different blocks accepted in \* InvTwoBlocksAccepted states that there can't be two different blocks accepted in
\* the two different views, i.e. dBFT must not allow forks. \* the two different views, i.e. dBFT must not allow forks.
InvTwoBlocksAccepted == \A r1 \in RM: InvTwoBlocksAccepted == \A r1 \in RM:
\A r2 \in RM \ {r1}: \A r2 \in RM \ {r1}:
\/ rmState[r1].type /= "blockAccepted" \/ rmState[r1].type /= "blockAccepted"
\/ rmState[r2].type /= "blockAccepted" \/ rmState[r2].type /= "blockAccepted"
\/ rmState[r1].view = rmState[r2].view \/ rmState[r1].view = rmState[r2].view
\* InvFaultNodesCount states that there can be F faulty or dead nodes at max. \* InvFaultNodesCount states that there can be F faulty or dead nodes at max.
InvFaultNodesCount == Cardinality({ InvFaultNodesCount == Cardinality({
r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead" r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead"
}) <= F }) <= F
\* This theorem asserts the truth of the temporal formula whose meaning is that \* This theorem asserts the truth of the temporal formula whose meaning is that
\* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are \* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are
\* the invariants of the specification Spec. This theorem is not supposed to be \* the invariants of the specification Spec. This theorem is not supposed to be
\* checked by the TLC model checker, it's here for the reader's understanding of \* checked by the TLC model checker, it's here for the reader's understanding of
\* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount. \* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount.
THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvFaultNodesCount) THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvFaultNodesCount)
============================================================================= =============================================================================
\* Modification History \* Modification History
\* Last modified Mon Mar 06 15:36:57 MSK 2023 by root \* Last modified Mon Mar 06 15:36:57 MSK 2023 by root
\* Last modified Fri Feb 17 15:47:41 MSK 2023 by anna \* Last modified Fri Feb 17 15:47:41 MSK 2023 by anna
\* Last modified Sat Jan 21 01:26:16 MSK 2023 by rik \* Last modified Sat Jan 21 01:26:16 MSK 2023 by rik
\* Created Thu Dec 15 16:06:17 MSK 2022 by anna \* Created Thu Dec 15 16:06:17 MSK 2022 by anna

View File

@ -1,42 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.lamport.tla.toolbox.tool.tlc.modelCheck"> <launchConfiguration type="org.lamport.tla.toolbox.tool.tlc.modelCheck">
<stringAttribute key="configurationName" value="AllGoodModel"/> <stringAttribute key="configurationName" value="AllGoodModel"/>
<intAttribute key="distributedFPSetCount" value="0"/> <intAttribute key="distributedFPSetCount" value="0"/>
<stringAttribute key="distributedNetworkInterface" value="172.200.0.254"/> <stringAttribute key="distributedNetworkInterface" value="172.200.0.254"/>
<intAttribute key="distributedNodesCount" value="1"/> <intAttribute key="distributedNodesCount" value="1"/>
<stringAttribute key="distributedTLC" value="off"/> <stringAttribute key="distributedTLC" value="off"/>
<intAttribute key="fpIndex" value="47"/> <intAttribute key="fpIndex" value="47"/>
<intAttribute key="maxHeapSize" value="50"/> <intAttribute key="maxHeapSize" value="50"/>
<stringAttribute key="modelBehaviorInit" value=""/> <stringAttribute key="modelBehaviorInit" value=""/>
<stringAttribute key="modelBehaviorNext" value=""/> <stringAttribute key="modelBehaviorNext" value=""/>
<stringAttribute key="modelBehaviorSpec" value="Spec"/> <stringAttribute key="modelBehaviorSpec" value="Spec"/>
<intAttribute key="modelBehaviorSpecType" value="1"/> <intAttribute key="modelBehaviorSpecType" value="1"/>
<stringAttribute key="modelBehaviorVars" value="msgs, rmState"/> <stringAttribute key="modelBehaviorVars" value="msgs, rmState"/>
<stringAttribute key="modelComments" value=""/> <stringAttribute key="modelComments" value=""/>
<booleanAttribute key="modelCorrectnessCheckDeadlock" value="true"/> <booleanAttribute key="modelCorrectnessCheckDeadlock" value="true"/>
<listAttribute key="modelCorrectnessInvariants"> <listAttribute key="modelCorrectnessInvariants">
<listEntry value="1TypeOK"/> <listEntry value="1TypeOK"/>
<listEntry value="1InvTwoBlocksAccepted"/> <listEntry value="1InvTwoBlocksAccepted"/>
<listEntry value="1InvFaultNodesCount"/> <listEntry value="1InvFaultNodesCount"/>
</listAttribute> </listAttribute>
<listAttribute key="modelCorrectnessProperties"> <listAttribute key="modelCorrectnessProperties">
<listEntry value="1Liveness"/> <listEntry value="1Liveness"/>
</listAttribute> </listAttribute>
<intAttribute key="modelEditorOpenTabs" value="10"/> <intAttribute key="modelEditorOpenTabs" value="10"/>
<stringAttribute key="modelParameterActionConstraint" value=""/> <stringAttribute key="modelParameterActionConstraint" value=""/>
<listAttribute key="modelParameterConstants"> <listAttribute key="modelParameterConstants">
<listEntry value="RMFault;;{};0;0"/> <listEntry value="RMFault;;{};0;0"/>
<listEntry value="MaxView;;1;0;0"/> <listEntry value="MaxView;;1;0;0"/>
<listEntry value="RMDead;;{};0;0"/> <listEntry value="RMDead;;{};0;0"/>
<listEntry value="RM;;{0, 1, 2, 3};0;0"/> <listEntry value="RM;;{0, 1, 2, 3};0;0"/>
</listAttribute> </listAttribute>
<stringAttribute key="modelParameterContraint" value="MaxViewConstraint"/> <stringAttribute key="modelParameterContraint" value="MaxViewConstraint"/>
<listAttribute key="modelParameterDefinitions"/> <listAttribute key="modelParameterDefinitions"/>
<stringAttribute key="modelParameterModelValues" value="{}"/> <stringAttribute key="modelParameterModelValues" value="{}"/>
<stringAttribute key="modelParameterNewDefinitions" value=""/> <stringAttribute key="modelParameterNewDefinitions" value=""/>
<intAttribute key="modelVersion" value="20191005"/> <intAttribute key="modelVersion" value="20191005"/>
<intAttribute key="numberOfWorkers" value="8"/> <intAttribute key="numberOfWorkers" value="8"/>
<stringAttribute key="result.mail.address" value=""/> <stringAttribute key="result.mail.address" value=""/>
<stringAttribute key="specName" value="dbft"/> <stringAttribute key="specName" value="dbft"/>
<stringAttribute key="tlcResourcesProfile" value="local custom"/> <stringAttribute key="tlcResourcesProfile" value="local custom"/>
</launchConfiguration> </launchConfiguration>

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.lamport.tla.toolbox.tool.tlc.modelCheck"> <launchConfiguration type="org.lamport.tla.toolbox.tool.tlc.modelCheck">
<stringAttribute key="configurationName" value="AllGoodModel"/> <stringAttribute key="configurationName" value="AllGoodModel"/>
<intAttribute key="distributedFPSetCount" value="0"/> <intAttribute key="distributedFPSetCount" value="0"/>
<stringAttribute key="distributedNetworkInterface" value="172.200.0.254"/> <stringAttribute key="distributedNetworkInterface" value="172.200.0.254"/>
<intAttribute key="distributedNodesCount" value="1"/> <intAttribute key="distributedNodesCount" value="1"/>
<stringAttribute key="distributedTLC" value="off"/> <stringAttribute key="distributedTLC" value="off"/>
<intAttribute key="fpIndex" value="105"/> <intAttribute key="fpIndex" value="105"/>
<intAttribute key="maxHeapSize" value="25"/> <intAttribute key="maxHeapSize" value="25"/>
<stringAttribute key="modelBehaviorInit" value=""/> <stringAttribute key="modelBehaviorInit" value=""/>
<stringAttribute key="modelBehaviorNext" value=""/> <stringAttribute key="modelBehaviorNext" value=""/>
<stringAttribute key="modelBehaviorSpec" value="Spec"/> <stringAttribute key="modelBehaviorSpec" value="Spec"/>
<intAttribute key="modelBehaviorSpecType" value="1"/> <intAttribute key="modelBehaviorSpecType" value="1"/>
<stringAttribute key="modelBehaviorVars" value="msgs, rmState, blockAccepted"/> <stringAttribute key="modelBehaviorVars" value="msgs, rmState, blockAccepted"/>
<stringAttribute key="modelComments" value=""/> <stringAttribute key="modelComments" value=""/>
<booleanAttribute key="modelCorrectnessCheckDeadlock" value="true"/> <booleanAttribute key="modelCorrectnessCheckDeadlock" value="true"/>
<listAttribute key="modelCorrectnessInvariants"> <listAttribute key="modelCorrectnessInvariants">
<listEntry value="1TypeOK"/> <listEntry value="1TypeOK"/>
<listEntry value="1InvTwoBlocksAcceptedAdvanced"/> <listEntry value="1InvTwoBlocksAcceptedAdvanced"/>
<listEntry value="1InvFaultNodesCount"/> <listEntry value="1InvFaultNodesCount"/>
</listAttribute> </listAttribute>
<listAttribute key="modelCorrectnessProperties"> <listAttribute key="modelCorrectnessProperties">
<listEntry value="1Liveness"/> <listEntry value="1Liveness"/>
</listAttribute> </listAttribute>
<intAttribute key="modelEditorOpenTabs" value="10"/> <intAttribute key="modelEditorOpenTabs" value="10"/>
<stringAttribute key="modelParameterActionConstraint" value=""/> <stringAttribute key="modelParameterActionConstraint" value=""/>
<listAttribute key="modelParameterConstants"> <listAttribute key="modelParameterConstants">
<listEntry value="RMFault;;{};0;0"/> <listEntry value="RMFault;;{};0;0"/>
<listEntry value="MaxView;;1;0;0"/> <listEntry value="MaxView;;1;0;0"/>
<listEntry value="RMDead;;{};0;0"/> <listEntry value="RMDead;;{};0;0"/>
<listEntry value="RM;;{0, 1, 2, 3};0;0"/> <listEntry value="RM;;{0, 1, 2, 3};0;0"/>
</listAttribute> </listAttribute>
<stringAttribute key="modelParameterContraint" value="MaxViewConstraint"/> <stringAttribute key="modelParameterContraint" value="MaxViewConstraint"/>
<listAttribute key="modelParameterDefinitions"/> <listAttribute key="modelParameterDefinitions"/>
<stringAttribute key="modelParameterModelValues" value="{}"/> <stringAttribute key="modelParameterModelValues" value="{}"/>
<stringAttribute key="modelParameterNewDefinitions" value=""/> <stringAttribute key="modelParameterNewDefinitions" value=""/>
<intAttribute key="modelVersion" value="20191005"/> <intAttribute key="modelVersion" value="20191005"/>
<intAttribute key="numberOfWorkers" value="4"/> <intAttribute key="numberOfWorkers" value="4"/>
<stringAttribute key="result.mail.address" value=""/> <stringAttribute key="result.mail.address" value=""/>
<stringAttribute key="specName" value="dbftCentralizedCV"/> <stringAttribute key="specName" value="dbftCentralizedCV"/>
<stringAttribute key="tlcResourcesProfile" value="local custom"/> <stringAttribute key="tlcResourcesProfile" value="local custom"/>
</launchConfiguration> </launchConfiguration>

View File

@ -1,427 +1,427 @@
-------------------------------- MODULE dbftCV3 -------------------------------- -------------------------------- MODULE dbftCV3 --------------------------------
EXTENDS EXTENDS
Integers, Integers,
FiniteSets FiniteSets
CONSTANTS CONSTANTS
\* RM is the set of consensus node indexes starting from 0. \* RM is the set of consensus node indexes starting from 0.
\* Example: {0, 1, 2, 3} \* Example: {0, 1, 2, 3}
RM, RM,
\* RMFault is a set of consensus node indexes that are allowed to become \* RMFault is a set of consensus node indexes that are allowed to become
\* FAULT in the middle of every considered behavior and to send any \* FAULT in the middle of every considered behavior and to send any
\* consensus message afterwards. RMFault must be a subset of RM. An empty \* consensus message afterwards. RMFault must be a subset of RM. An empty
\* set means that all nodes are good in every possible behaviour. \* set means that all nodes are good in every possible behaviour.
\* Examples: {0} \* Examples: {0}
\* {1, 3} \* {1, 3}
\* {} \* {}
RMFault, RMFault,
\* RMDead is a set of consensus node indexes that are allowed to die in the \* RMDead is a set of consensus node indexes that are allowed to die in the
\* middle of every behaviour and do not send any message afterwards. RMDead \* middle of every behaviour and do not send any message afterwards. RMDead
\* must be a subset of RM. An empty set means that all nodes are alive and \* must be a subset of RM. An empty set means that all nodes are alive and
\* responding in in every possible behaviour. RMDead may intersect the \* responding in in every possible behaviour. RMDead may intersect the
\* RMFault set which means that node which is in both RMDead and RMFault \* RMFault set which means that node which is in both RMDead and RMFault
\* may become FAULT and send any message starting from some step of the \* may become FAULT and send any message starting from some step of the
\* particular behaviour and may also die in the same behaviour which will \* particular behaviour and may also die in the same behaviour which will
\* prevent it from sending any message. \* prevent it from sending any message.
\* Examples: {0} \* Examples: {0}
\* {3, 2} \* {3, 2}
\* {} \* {}
RMDead, RMDead,
\* MaxView is the maximum allowed view to be considered (starting from 0, \* MaxView is the maximum allowed view to be considered (starting from 0,
\* including the MaxView itself). This constraint was introduced to reduce \* including the MaxView itself). This constraint was introduced to reduce
\* the number of possible model states to be checked. It is recommended to \* the number of possible model states to be checked. It is recommended to
\* keep this setting not too high (< N is highly recommended). \* keep this setting not too high (< N is highly recommended).
\* Example: 2 \* Example: 2
MaxView MaxView
VARIABLES VARIABLES
\* rmState is a set of consensus node states. It is represented by the \* rmState is a set of consensus node states. It is represented by the
\* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is \* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is
\* the state of the r-th consensus node at the current step. \* the state of the r-th consensus node at the current step.
rmState, rmState,
\* msgs is the shared pool of messages sent to the network by consensus nodes. \* msgs is the shared pool of messages sent to the network by consensus nodes.
\* It is represented by a subset of Messages set. \* It is represented by a subset of Messages set.
msgs msgs
\* vars is a tuple of all variables used in the specification. It is needed to \* vars is a tuple of all variables used in the specification. It is needed to
\* simplify fairness conditions definition. \* simplify fairness conditions definition.
vars == <<rmState, msgs>> vars == <<rmState, msgs>>
\* N is the number of validators. \* N is the number of validators.
N == Cardinality(RM) N == Cardinality(RM)
\* F is the number of validators that are allowed to be malicious. \* F is the number of validators that are allowed to be malicious.
F == (N - 1) \div 3 F == (N - 1) \div 3
\* M is the number of validators that must function correctly. \* M is the number of validators that must function correctly.
M == N - F M == N - F
\* These assumptions are checked by the TLC model checker once at the start of \* These assumptions are checked by the TLC model checker once at the start of
\* the model checking process. All the input data (declared constants) specified \* the model checking process. All the input data (declared constants) specified
\* in the "Model Overview" section must satisfy these constraints. \* in the "Model Overview" section must satisfy these constraints.
ASSUME ASSUME
/\ RM \subseteq Nat /\ RM \subseteq Nat
/\ N >= 4 /\ N >= 4
/\ 0 \in RM /\ 0 \in RM
/\ RMFault \subseteq RM /\ RMFault \subseteq RM
/\ RMDead \subseteq RM /\ RMDead \subseteq RM
/\ Cardinality(RMFault) <= F /\ Cardinality(RMFault) <= F
/\ Cardinality(RMDead) <= F /\ Cardinality(RMDead) <= F
/\ Cardinality(RMFault \cup RMDead) <= F /\ Cardinality(RMFault \cup RMDead) <= F
/\ MaxView \in Nat /\ MaxView \in Nat
/\ MaxView <= 2 /\ MaxView <= 2
\* RMStates is a set of records where each record holds the node state and \* RMStates is a set of records where each record holds the node state and
\* the node current view. \* the node current view.
RMStates == [ RMStates == [
type: {"initialized", "prepareSent", "commitSent", "blockAccepted", "cv1", "cv2", "cv3", "bad", "dead"}, type: {"initialized", "prepareSent", "commitSent", "blockAccepted", "cv1", "cv2", "cv3", "bad", "dead"},
view : Nat view : Nat
] ]
\* Messages is a set of records where each record holds the message type, \* Messages is a set of records where each record holds the message type,
\* the message sender and sender's view by the moment when message was sent. \* the message sender and sender's view by the moment when message was sent.
Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView1", "ChangeView2", "ChangeView3"}, rm : RM, view : Nat] Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView1", "ChangeView2", "ChangeView3"}, rm : RM, view : Nat]
\* -------------- Useful operators -------------- \* -------------- Useful operators --------------
\* IsPrimary is an operator defining whether provided node r is primary \* IsPrimary is an operator defining whether provided node r is primary
\* for the current round from the r's point of view. It is a mapping \* for the current round from the r's point of view. It is a mapping
\* from RM to the set of {TRUE, FALSE}. \* from RM to the set of {TRUE, FALSE}.
IsPrimary(r) == rmState[r].view % N = r IsPrimary(r) == rmState[r].view % N = r
\* GetPrimary is an operator defining mapping from round index to the RM that \* GetPrimary is an operator defining mapping from round index to the RM that
\* is primary in this round. \* is primary in this round.
GetPrimary(view) == CHOOSE r \in RM : view % N = r GetPrimary(view) == CHOOSE r \in RM : view % N = r
\* GetNewView returns new view number based on the previous node view value. \* GetNewView returns new view number based on the previous node view value.
\* Current specifications only allows to increment view. \* Current specifications only allows to increment view.
GetNewView(oldView) == oldView + 1 GetNewView(oldView) == oldView + 1
\* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest \* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest
\* message received from the current round's speaker (as the node r sees it). \* message received from the current round's speaker (as the node r sees it).
PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in msgs PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in msgs
\* -------------- Safety temporal formula -------------- \* -------------- Safety temporal formula --------------
\* Init is the initial predicate initializing values at the start of every \* Init is the initial predicate initializing values at the start of every
\* behaviour. \* behaviour.
Init == Init ==
/\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]] /\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]]
/\ msgs = {} /\ msgs = {}
\* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest. \* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest.
RMSendPrepareRequest(r) == RMSendPrepareRequest(r) ==
/\ rmState[r].type = "initialized" /\ rmState[r].type = "initialized"
/\ IsPrimary(r) /\ IsPrimary(r)
/\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"]
/\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]}
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from \* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from
\* the primary node of the current round (view) and broadcasting PrepareResponse. \* the primary node of the current round (view) and broadcasting PrepareResponse.
\* This step assumes that PrepareRequest always contains valid transactions and \* This step assumes that PrepareRequest always contains valid transactions and
\* signatures. \* signatures.
RMSendPrepareResponse(r) == RMSendPrepareResponse(r) ==
/\ rmState[r].type = "initialized" /\ rmState[r].type = "initialized"
/\ \neg IsPrimary(r) /\ \neg IsPrimary(r)
/\ PrepareRequestSentOrReceived(r) /\ PrepareRequestSentOrReceived(r)
/\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"]
/\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]}
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMSendCommit describes node r sending Commit if there's enough PrepareRequest/PrepareResponse \* RMSendCommit describes node r sending Commit if there's enough PrepareRequest/PrepareResponse
\* messages and no node has sent the ChangeView3 message. It is possible to send the Commit after \* messages and no node has sent the ChangeView3 message. It is possible to send the Commit after
\* the ChangeView1 or ChangeView2 message was sent with additional constraints. \* the ChangeView1 or ChangeView2 message was sent with additional constraints.
RMSendCommit(r) == RMSendCommit(r) ==
/\ \/ rmState[r].type = "prepareSent" /\ \/ rmState[r].type = "prepareSent"
\/ rmState[r].type = "cv1" \/ rmState[r].type = "cv1"
\/ /\ rmState[r].type = "cv2" \/ /\ rmState[r].type = "cv2"
/\ Cardinality({ /\ Cardinality({
msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view
}) > F }) > F
/\ Cardinality({ /\ Cardinality({
msg \in msgs : (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest") /\ msg.view = rmState[r].view msg \in msgs : (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest") /\ msg.view = rmState[r].view
}) >= M }) >= M
/\ Cardinality({ /\ Cardinality({
msg \in msgs : msg.type = "ChangeView3" /\ msg.view = rmState[r].view msg \in msgs : msg.type = "ChangeView3" /\ msg.view = rmState[r].view
}) = 0 }) = 0
/\ PrepareRequestSentOrReceived(r) /\ PrepareRequestSentOrReceived(r)
/\ rmState' = [rmState EXCEPT ![r].type = "commitSent"] /\ rmState' = [rmState EXCEPT ![r].type = "commitSent"]
/\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view]}
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMAcceptBlock describes node r collecting enough Commit messages and accepting \* RMAcceptBlock describes node r collecting enough Commit messages and accepting
\* the block. \* the block.
RMAcceptBlock(r) == RMAcceptBlock(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ rmState[r].type /= "blockAccepted" /\ rmState[r].type /= "blockAccepted"
/\ PrepareRequestSentOrReceived(r) /\ PrepareRequestSentOrReceived(r)
/\ Cardinality({ /\ Cardinality({
msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view
}) >= M }) >= M
/\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"] /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* FetchBlock describes node r that fetches the accepted block from some other node. \* FetchBlock describes node r that fetches the accepted block from some other node.
RMFetchBlock(r) == RMFetchBlock(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ rmState[r].type /= "blockAccepted" /\ rmState[r].type /= "blockAccepted"
/\ \E rmAccepted \in RM : /\ rmState[rmAccepted].type = "blockAccepted" /\ \E rmAccepted \in RM : /\ rmState[rmAccepted].type = "blockAccepted"
/\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].view = rmState[rmAccepted].view] /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].view = rmState[rmAccepted].view]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMSendChangeView1 describes node r sending ChangeView1 message on timeout. \* RMSendChangeView1 describes node r sending ChangeView1 message on timeout.
\* Only non-primary node is allowed to send ChangeView1 message, as the primary \* Only non-primary node is allowed to send ChangeView1 message, as the primary
\* must send the PrepareRequest if the timer fires. \* must send the PrepareRequest if the timer fires.
RMSendChangeView1(r) == RMSendChangeView1(r) ==
/\ rmState[r].type = "initialized" /\ rmState[r].type = "initialized"
/\ \neg IsPrimary(r) /\ \neg IsPrimary(r)
/\ rmState' = [rmState EXCEPT ![r].type = "cv1"] /\ rmState' = [rmState EXCEPT ![r].type = "cv1"]
/\ msgs' = msgs \cup {[type |-> "ChangeView1", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "ChangeView1", rm |-> r, view |-> rmState[r].view]}
\* RMSendChangeView2 describes node r sending ChangeView2 message on timeout either from \* RMSendChangeView2 describes node r sending ChangeView2 message on timeout either from
\* "cv1" state or after the node has sent the PrepareRequest or PrepareResponse message. \* "cv1" state or after the node has sent the PrepareRequest or PrepareResponse message.
RMSendChangeView2(r) == RMSendChangeView2(r) ==
/\ \/ /\ rmState[r].type = "prepareSent" /\ \/ /\ rmState[r].type = "prepareSent"
/\ Cardinality({ /\ Cardinality({
msg \in msgs : msg.type = "ChangeView1" /\ msg.view = rmState[r].view msg \in msgs : msg.type = "ChangeView1" /\ msg.view = rmState[r].view
}) > 0 }) > 0
\/ rmState[r].type = "cv1" \/ rmState[r].type = "cv1"
/\ Cardinality({ /\ Cardinality({
msg \in msgs : (msg.type = "ChangeView1" \/ msg.type = "PrepareRequest" \/ msg.type = "PrepareResponse") /\ msg.view = rmState[r].view msg \in msgs : (msg.type = "ChangeView1" \/ msg.type = "PrepareRequest" \/ msg.type = "PrepareResponse") /\ msg.view = rmState[r].view
}) >= M }) >= M
/\ \/ Cardinality({ /\ \/ Cardinality({
msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view
}) <= F }) <= F
\/ Cardinality({ \/ Cardinality({
msg \in msgs : msg.type = "ChangeView3" /\ msg.view = rmState[r].view msg \in msgs : msg.type = "ChangeView3" /\ msg.view = rmState[r].view
}) > 0 }) > 0
/\ rmState' = [rmState EXCEPT ![r].type = "cv2"] /\ rmState' = [rmState EXCEPT ![r].type = "cv2"]
/\ msgs' = msgs \cup {[type |-> "ChangeView2", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "ChangeView2", rm |-> r, view |-> rmState[r].view]}
\* RMSendChangeView3 describes node r sending ChangeView3 message on timeout either from \* RMSendChangeView3 describes node r sending ChangeView3 message on timeout either from
\* "cv2" state or after the node has sent the Commit message. \* "cv2" state or after the node has sent the Commit message.
RMSendChangeView3(r) == RMSendChangeView3(r) ==
/\ \/ rmState[r].type = "cv2" /\ \/ rmState[r].type = "cv2"
\/ rmState[r].type = "commitSent" \/ rmState[r].type = "commitSent"
/\ Cardinality({msg \in msgs : (msg.type = "ChangeView2" \/ msg.type = "Commit") /\ msg.view = rmState[r].view}) >= M /\ Cardinality({msg \in msgs : (msg.type = "ChangeView2" \/ msg.type = "Commit") /\ msg.view = rmState[r].view}) >= M
/\ Cardinality({msg \in msgs : (msg.type = "ChangeView2") /\ msg.view = rmState[r].view}) > 0 /\ Cardinality({msg \in msgs : (msg.type = "ChangeView2") /\ msg.view = rmState[r].view}) > 0
/\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) <= F /\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) <= F
/\ rmState' = [rmState EXCEPT ![r].type = "cv3"] /\ rmState' = [rmState EXCEPT ![r].type = "cv3"]
/\ msgs' = msgs \cup {[type |-> "ChangeView3", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "ChangeView3", rm |-> r, view |-> rmState[r].view]}
\* RMReceiveChangeView describes node r receiving enough ChangeView[1,2,3] messages for \* RMReceiveChangeView describes node r receiving enough ChangeView[1,2,3] messages for
\* view changing. \* view changing.
RMReceiveChangeView(r) == RMReceiveChangeView(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ rmState[r].type /= "blockAccepted" /\ rmState[r].type /= "blockAccepted"
/\ \/ Cardinality({rm \in RM : Cardinality({msg \in msgs : /\ msg.rm = rm /\ \/ Cardinality({rm \in RM : Cardinality({msg \in msgs : /\ msg.rm = rm
/\ msg.type = "ChangeView1" /\ msg.type = "ChangeView1"
/\ GetNewView(msg.view) >= GetNewView(rmState[r].view) /\ GetNewView(msg.view) >= GetNewView(rmState[r].view)
}) # 0 }) # 0
}) >= M }) >= M
\/ Cardinality({rm \in RM : Cardinality({msg \in msgs : /\ msg.rm = rm \/ Cardinality({rm \in RM : Cardinality({msg \in msgs : /\ msg.rm = rm
/\ msg.type = "ChangeView2" /\ msg.type = "ChangeView2"
/\ GetNewView(msg.view) >= GetNewView(rmState[r].view) /\ GetNewView(msg.view) >= GetNewView(rmState[r].view)
}) # 0 }) # 0
}) >= M }) >= M
\/ Cardinality({rm \in RM : Cardinality({msg \in msgs : /\ msg.rm = rm \/ Cardinality({rm \in RM : Cardinality({msg \in msgs : /\ msg.rm = rm
/\ msg.type = "ChangeView3" /\ msg.type = "ChangeView3"
/\ GetNewView(msg.view) >= GetNewView(rmState[r].view) /\ GetNewView(msg.view) >= GetNewView(rmState[r].view)
}) # 0 }) # 0
}) >= M }) >= M
/\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(rmState[r].view)] /\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(rmState[r].view)]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMBeBad describes the faulty node r that will send any kind of consensus message starting \* RMBeBad describes the faulty node r that will send any kind of consensus message starting
\* from the step it's gone wild. This step is enabled only when RMFault is non-empty set. \* from the step it's gone wild. This step is enabled only when RMFault is non-empty set.
RMBeBad(r) == RMBeBad(r) ==
/\ r \in RMFault /\ r \in RMFault
/\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F /\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F
/\ rmState' = [rmState EXCEPT ![r].type = "bad"] /\ rmState' = [rmState EXCEPT ![r].type = "bad"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMFaultySendCV describes sending CV1 message by the faulty node r. \* RMFaultySendCV describes sending CV1 message by the faulty node r.
RMFaultySendCV1(r) == RMFaultySendCV1(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET cv == [type |-> "ChangeView1", rm |-> r, view |-> rmState[r].view] /\ LET cv == [type |-> "ChangeView1", rm |-> r, view |-> rmState[r].view]
IN /\ cv \notin msgs IN /\ cv \notin msgs
/\ msgs' = msgs \cup {cv} /\ msgs' = msgs \cup {cv}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendCV2 describes sending CV2 message by the faulty node r. \* RMFaultySendCV2 describes sending CV2 message by the faulty node r.
RMFaultySendCV2(r) == RMFaultySendCV2(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET cv == [type |-> "ChangeView2", rm |-> r, view |-> rmState[r].view] /\ LET cv == [type |-> "ChangeView2", rm |-> r, view |-> rmState[r].view]
IN /\ cv \notin msgs IN /\ cv \notin msgs
/\ msgs' = msgs \cup {cv} /\ msgs' = msgs \cup {cv}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendCV3 describes sending CV3 message by the faulty node r. \* RMFaultySendCV3 describes sending CV3 message by the faulty node r.
RMFaultySendCV3(r) == RMFaultySendCV3(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET cv == [type |-> "ChangeView3", rm |-> r, view |-> rmState[r].view] /\ LET cv == [type |-> "ChangeView3", rm |-> r, view |-> rmState[r].view]
IN /\ cv \notin msgs IN /\ cv \notin msgs
/\ msgs' = msgs \cup {cv} /\ msgs' = msgs \cup {cv}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultyDoCV describes view changing by the faulty node r. \* RMFaultyDoCV describes view changing by the faulty node r.
RMFaultyDoCV(r) == RMFaultyDoCV(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)] /\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r. \* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r.
RMFaultySendPReq(r) == RMFaultySendPReq(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ IsPrimary(r) /\ IsPrimary(r)
/\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]
IN /\ pReq \notin msgs IN /\ pReq \notin msgs
/\ msgs' = msgs \cup {pReq} /\ msgs' = msgs \cup {pReq}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r. \* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r.
RMFaultySendPResp(r) == RMFaultySendPResp(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ \neg IsPrimary(r) /\ \neg IsPrimary(r)
/\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]
IN /\ pResp \notin msgs IN /\ pResp \notin msgs
/\ msgs' = msgs \cup {pResp} /\ msgs' = msgs \cup {pResp}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendCommit describes sending Commit message by the faulty node r. \* RMFaultySendCommit describes sending Commit message by the faulty node r.
RMFaultySendCommit(r) == RMFaultySendCommit(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] /\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view]
IN /\ commit \notin msgs IN /\ commit \notin msgs
/\ msgs' = msgs \cup {commit} /\ msgs' = msgs \cup {commit}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMDie describes node r that was removed from the network at the particular step \* RMDie describes node r that was removed from the network at the particular step
\* of the behaviour. After this node r can't change its state and accept/send messages. \* of the behaviour. After this node r can't change its state and accept/send messages.
RMDie(r) == RMDie(r) ==
/\ r \in RMDead /\ r \in RMDead
/\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F
/\ rmState' = [rmState EXCEPT ![r].type = "dead"] /\ rmState' = [rmState EXCEPT ![r].type = "dead"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* Terminating is an action that allows infinite stuttering to prevent deadlock on \* Terminating is an action that allows infinite stuttering to prevent deadlock on
\* behaviour termination. We consider termination to be valid if at least M nodes \* behaviour termination. We consider termination to be valid if at least M nodes
\* has the block being accepted. \* has the block being accepted.
Terminating == Terminating ==
/\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M /\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M
/\ UNCHANGED <<msgs, rmState>> /\ UNCHANGED <<msgs, rmState>>
\* The next-state action. \* The next-state action.
Next == Next ==
\/ Terminating \/ Terminating
\/ \E r \in RM: \/ \E r \in RM:
RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r) RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r)
\/ RMAcceptBlock(r) \/ RMSendChangeView1(r) \/ RMReceiveChangeView(r) \/ RMBeBad(r) \/ RMSendChangeView2(r) \/ RMSendChangeView3(r) \/ RMAcceptBlock(r) \/ RMSendChangeView1(r) \/ RMReceiveChangeView(r) \/ RMBeBad(r) \/ RMSendChangeView2(r) \/ RMSendChangeView3(r)
\/ RMFaultySendCV1(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) \/ RMFaultySendCV2(r) \/ RMFaultySendCV3(r) \/ RMFaultySendCV1(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) \/ RMFaultySendCV2(r) \/ RMFaultySendCV3(r)
\/ RMDie(r) \/ RMFetchBlock(r) \/ RMDie(r) \/ RMFetchBlock(r)
\* Safety is a temporal formula that describes the whole set of allowed \* Safety is a temporal formula that describes the whole set of allowed
\* behaviours. It specifies only what the system MAY do (i.e. the set of \* behaviours. It specifies only what the system MAY do (i.e. the set of
\* possible allowed behaviours for the system). It asserts only what may \* possible allowed behaviours for the system). It asserts only what may
\* happen; any behaviour that violates it does so at some point and \* happen; any behaviour that violates it does so at some point and
\* nothing past that point makes difference. \* nothing past that point makes difference.
\* \*
\* E.g. this safety formula (applied standalone) allows the behaviour to end \* E.g. this safety formula (applied standalone) allows the behaviour to end
\* with an infinite set of stuttering steps (those steps that DO NOT change \* with an infinite set of stuttering steps (those steps that DO NOT change
\* neither msgs nor rmState) and never reach the state where at least one \* neither msgs nor rmState) and never reach the state where at least one
\* node is committed or accepted the block. \* node is committed or accepted the block.
\* \*
\* To forbid such behaviours we must specify what the system MUST \* To forbid such behaviours we must specify what the system MUST
\* do. It will be specified below with the help of fairness conditions in \* do. It will be specified below with the help of fairness conditions in
\* the Fairness formula. \* the Fairness formula.
Safety == Init /\ [][Next]_vars Safety == Init /\ [][Next]_vars
\* -------------- Fairness temporal formula -------------- \* -------------- Fairness temporal formula --------------
\* Fairness is a temporal assumptions under which the model is working. \* Fairness is a temporal assumptions under which the model is working.
\* Usually it specifies different kind of assumptions for each/some \* Usually it specifies different kind of assumptions for each/some
\* subactions of the Next's state action, but the only think that bothers \* subactions of the Next's state action, but the only think that bothers
\* us is preventing infinite stuttering at those steps where some of Next's \* us is preventing infinite stuttering at those steps where some of Next's
\* subactions are enabled. Thus, the only thing that we require from the \* subactions are enabled. Thus, the only thing that we require from the
\* system is to keep take the steps until it's impossible to take them. \* system is to keep take the steps until it's impossible to take them.
\* That's exactly how the weak fairness condition works: if some action \* That's exactly how the weak fairness condition works: if some action
\* remains continuously enabled, it must eventually happen. \* remains continuously enabled, it must eventually happen.
Fairness == WF_vars(Next) Fairness == WF_vars(Next)
\* -------------- Specification -------------- \* -------------- Specification --------------
\* The complete specification of the protocol written as a temporal formula. \* The complete specification of the protocol written as a temporal formula.
Spec == Safety /\ Fairness Spec == Safety /\ Fairness
\* -------------- Liveness temporal formula -------------- \* -------------- Liveness temporal formula --------------
\* For every possible behaviour it's true that eventually (i.e. at least once \* For every possible behaviour it's true that eventually (i.e. at least once
\* through the behaviour) block will be accepted. It is something that dBFT \* through the behaviour) block will be accepted. It is something that dBFT
\* must guarantee (an in practice this condition is violated). \* must guarantee (an in practice this condition is violated).
TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M) TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M)
\* A liveness temporal formula asserts only what must happen (i.e. specifies \* A liveness temporal formula asserts only what must happen (i.e. specifies
\* what the system MUST do). Any behaviour can NOT violate it at ANY point; \* what the system MUST do). Any behaviour can NOT violate it at ANY point;
\* there's always the rest of the behaviour that can always make the liveness \* there's always the rest of the behaviour that can always make the liveness
\* formula true; if there's no such behaviour than the liveness formula is \* formula true; if there's no such behaviour than the liveness formula is
\* violated. The liveness formula is supposed to be checked as a property \* violated. The liveness formula is supposed to be checked as a property
\* by the TLC model checker. \* by the TLC model checker.
Liveness == TerminationRequirement Liveness == TerminationRequirement
\* -------------- ModelConstraints -------------- \* -------------- ModelConstraints --------------
\* MaxViewConstraint is a state predicate restricting the number of possible \* MaxViewConstraint is a state predicate restricting the number of possible
\* behaviour states. It is needed to reduce model checking time and prevent \* behaviour states. It is needed to reduce model checking time and prevent
\* the model graph size explosion. This formulae must be specified at the \* the model graph size explosion. This formulae must be specified at the
\* "State constraint" section of the "Additional Spec Options" section inside \* "State constraint" section of the "Additional Spec Options" section inside
\* the model overview. \* the model overview.
MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView
/\ \A msg \in msgs : msg.view <= MaxView /\ \A msg \in msgs : msg.view <= MaxView
\* -------------- Invariants of the specification -------------- \* -------------- Invariants of the specification --------------
\* Model invariant is a state predicate (statement) that must be true for \* Model invariant is a state predicate (statement) that must be true for
\* every step of every reachable behaviour. Model invariant is supposed to \* every step of every reachable behaviour. Model invariant is supposed to
\* be checked as an Invariant by the TLC Model Checker. \* be checked as an Invariant by the TLC Model Checker.
\* TypeOK is a type-correctness invariant. It states that all elements of \* TypeOK is a type-correctness invariant. It states that all elements of
\* specification variables must have the proper type throughout the behaviour. \* specification variables must have the proper type throughout the behaviour.
TypeOK == TypeOK ==
/\ rmState \in [RM -> RMStates] /\ rmState \in [RM -> RMStates]
/\ msgs \subseteq Messages /\ msgs \subseteq Messages
\* InvTwoBlocksAccepted states that there can't be two different blocks accepted in \* InvTwoBlocksAccepted states that there can't be two different blocks accepted in
\* the two different views, i.e. dBFT must not allow forks. \* the two different views, i.e. dBFT must not allow forks.
InvTwoBlocksAccepted == \A r1 \in RM: InvTwoBlocksAccepted == \A r1 \in RM:
\A r2 \in RM \ {r1}: \A r2 \in RM \ {r1}:
\/ rmState[r1].type /= "blockAccepted" \/ rmState[r1].type /= "blockAccepted"
\/ rmState[r2].type /= "blockAccepted" \/ rmState[r2].type /= "blockAccepted"
\/ rmState[r1].view = rmState[r2].view \/ rmState[r1].view = rmState[r2].view
\* InvFaultNodesCount states that there can be F faulty or dead nodes at max. \* InvFaultNodesCount states that there can be F faulty or dead nodes at max.
InvFaultNodesCount == Cardinality({ InvFaultNodesCount == Cardinality({
r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead" r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead"
}) <= F }) <= F
\* This theorem asserts the truth of the temporal formula whose meaning is that \* This theorem asserts the truth of the temporal formula whose meaning is that
\* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are \* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are
\* the invariants of the specification Spec. This theorem is not supposed to be \* the invariants of the specification Spec. This theorem is not supposed to be
\* checked by the TLC model checker, it's here for the reader's understanding of \* checked by the TLC model checker, it's here for the reader's understanding of
\* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount. \* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount.
THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvFaultNodesCount) THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvFaultNodesCount)
============================================================================= =============================================================================
\* Modification History \* Modification History
\* Last modified Wed Mar 01 12:11:07 MSK 2023 by root \* Last modified Wed Mar 01 12:11:07 MSK 2023 by root
\* Last modified Tue Feb 07 23:11:19 MSK 2023 by rik \* Last modified Tue Feb 07 23:11:19 MSK 2023 by rik
\* Last modified Fri Feb 03 18:09:33 MSK 2023 by anna \* Last modified Fri Feb 03 18:09:33 MSK 2023 by anna
\* Created Thu Dec 15 16:06:17 MSK 2022 by anna \* Created Thu Dec 15 16:06:17 MSK 2022 by anna

View File

@ -1,42 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.lamport.tla.toolbox.tool.tlc.modelCheck"> <launchConfiguration type="org.lamport.tla.toolbox.tool.tlc.modelCheck">
<stringAttribute key="configurationName" value="AllGoodModel"/> <stringAttribute key="configurationName" value="AllGoodModel"/>
<intAttribute key="distributedFPSetCount" value="0"/> <intAttribute key="distributedFPSetCount" value="0"/>
<stringAttribute key="distributedNetworkInterface" value="172.200.0.254"/> <stringAttribute key="distributedNetworkInterface" value="172.200.0.254"/>
<intAttribute key="distributedNodesCount" value="1"/> <intAttribute key="distributedNodesCount" value="1"/>
<stringAttribute key="distributedTLC" value="off"/> <stringAttribute key="distributedTLC" value="off"/>
<intAttribute key="fpIndex" value="96"/> <intAttribute key="fpIndex" value="96"/>
<intAttribute key="maxHeapSize" value="25"/> <intAttribute key="maxHeapSize" value="25"/>
<stringAttribute key="modelBehaviorInit" value=""/> <stringAttribute key="modelBehaviorInit" value=""/>
<stringAttribute key="modelBehaviorNext" value=""/> <stringAttribute key="modelBehaviorNext" value=""/>
<stringAttribute key="modelBehaviorSpec" value="Spec"/> <stringAttribute key="modelBehaviorSpec" value="Spec"/>
<intAttribute key="modelBehaviorSpecType" value="1"/> <intAttribute key="modelBehaviorSpecType" value="1"/>
<stringAttribute key="modelBehaviorVars" value="msgs, rmState"/> <stringAttribute key="modelBehaviorVars" value="msgs, rmState"/>
<stringAttribute key="modelComments" value=""/> <stringAttribute key="modelComments" value=""/>
<booleanAttribute key="modelCorrectnessCheckDeadlock" value="true"/> <booleanAttribute key="modelCorrectnessCheckDeadlock" value="true"/>
<listAttribute key="modelCorrectnessInvariants"> <listAttribute key="modelCorrectnessInvariants">
<listEntry value="1TypeOK"/> <listEntry value="1TypeOK"/>
<listEntry value="1InvTwoBlocksAccepted"/> <listEntry value="1InvTwoBlocksAccepted"/>
<listEntry value="1InvFaultNodesCount"/> <listEntry value="1InvFaultNodesCount"/>
</listAttribute> </listAttribute>
<listAttribute key="modelCorrectnessProperties"> <listAttribute key="modelCorrectnessProperties">
<listEntry value="1Liveness"/> <listEntry value="1Liveness"/>
</listAttribute> </listAttribute>
<intAttribute key="modelEditorOpenTabs" value="10"/> <intAttribute key="modelEditorOpenTabs" value="10"/>
<stringAttribute key="modelParameterActionConstraint" value=""/> <stringAttribute key="modelParameterActionConstraint" value=""/>
<listAttribute key="modelParameterConstants"> <listAttribute key="modelParameterConstants">
<listEntry value="RMFault;;{};0;0"/> <listEntry value="RMFault;;{};0;0"/>
<listEntry value="MaxView;;1;0;0"/> <listEntry value="MaxView;;1;0;0"/>
<listEntry value="RMDead;;{};0;0"/> <listEntry value="RMDead;;{};0;0"/>
<listEntry value="RM;;{0, 1, 2, 3};0;0"/> <listEntry value="RM;;{0, 1, 2, 3};0;0"/>
</listAttribute> </listAttribute>
<stringAttribute key="modelParameterContraint" value="MaxViewConstraint"/> <stringAttribute key="modelParameterContraint" value="MaxViewConstraint"/>
<listAttribute key="modelParameterDefinitions"/> <listAttribute key="modelParameterDefinitions"/>
<stringAttribute key="modelParameterModelValues" value="{}"/> <stringAttribute key="modelParameterModelValues" value="{}"/>
<stringAttribute key="modelParameterNewDefinitions" value=""/> <stringAttribute key="modelParameterNewDefinitions" value=""/>
<intAttribute key="modelVersion" value="20191005"/> <intAttribute key="modelVersion" value="20191005"/>
<intAttribute key="numberOfWorkers" value="4"/> <intAttribute key="numberOfWorkers" value="4"/>
<stringAttribute key="result.mail.address" value=""/> <stringAttribute key="result.mail.address" value=""/>
<stringAttribute key="specName" value="dbftCV3"/> <stringAttribute key="specName" value="dbftCV3"/>
<stringAttribute key="tlcResourcesProfile" value="local custom"/> <stringAttribute key="tlcResourcesProfile" value="local custom"/>
</launchConfiguration> </launchConfiguration>

View File

@ -1,466 +1,466 @@
---------------------------- MODULE dbftMultipool ---------------------------- ---------------------------- MODULE dbftMultipool ----------------------------
EXTENDS EXTENDS
Integers, Integers,
FiniteSets FiniteSets
CONSTANTS CONSTANTS
\* RM is the set of consensus node indexes starting from 0. \* RM is the set of consensus node indexes starting from 0.
\* Example: {0, 1, 2, 3} \* Example: {0, 1, 2, 3}
RM, RM,
\* RMFault is a set of consensus node indexes that are allowed to become \* RMFault is a set of consensus node indexes that are allowed to become
\* FAULT in the middle of every considered behavior and to send any \* FAULT in the middle of every considered behavior and to send any
\* consensus message afterwards. RMFault must be a subset of RM. An empty \* consensus message afterwards. RMFault must be a subset of RM. An empty
\* set means that all nodes are good in every possible behaviour. \* set means that all nodes are good in every possible behaviour.
\* Examples: {0} \* Examples: {0}
\* {1, 3} \* {1, 3}
\* {} \* {}
RMFault, RMFault,
\* RMDead is a set of consensus node indexes that are allowed to die in the \* RMDead is a set of consensus node indexes that are allowed to die in the
\* middle of every behaviour and do not send any message afterwards. RMDead \* middle of every behaviour and do not send any message afterwards. RMDead
\* must be a subset of RM. An empty set means that all nodes are alive and \* must be a subset of RM. An empty set means that all nodes are alive and
\* responding in in every possible behaviour. RMDead may intersect the \* responding in in every possible behaviour. RMDead may intersect the
\* RMFault set which means that node which is in both RMDead and RMFault \* RMFault set which means that node which is in both RMDead and RMFault
\* may become FAULT and send any message starting from some step of the \* may become FAULT and send any message starting from some step of the
\* particular behaviour and may also die in the same behaviour which will \* particular behaviour and may also die in the same behaviour which will
\* prevent it from sending any message. \* prevent it from sending any message.
\* Examples: {0} \* Examples: {0}
\* {3, 2} \* {3, 2}
\* {} \* {}
RMDead, RMDead,
\* MaxView is the maximum allowed view to be considered (starting from 0, \* MaxView is the maximum allowed view to be considered (starting from 0,
\* including the MaxView itself). This constraint was introduced to reduce \* including the MaxView itself). This constraint was introduced to reduce
\* the number of possible model states to be checked. It is recommended to \* the number of possible model states to be checked. It is recommended to
\* keep this setting not too high (< N is highly recommended). \* keep this setting not too high (< N is highly recommended).
\* Example: 2 \* Example: 2
MaxView, MaxView,
\* MaxUndeliveredMessages is the maximum number of messages in the common \* MaxUndeliveredMessages is the maximum number of messages in the common
\* messages pool (msgs) that were not received and handled by all consensus \* messages pool (msgs) that were not received and handled by all consensus
\* nodes. It must not be too small (>= 3) in order to allow model taking \* nodes. It must not be too small (>= 3) in order to allow model taking
\* at least any steps. At the same time it must not be too high (<= 6 is \* at least any steps. At the same time it must not be too high (<= 6 is
\* recommended) in order to avoid states graph size explosion. \* recommended) in order to avoid states graph size explosion.
MaxUndeliveredMessages MaxUndeliveredMessages
VARIABLES VARIABLES
\* rmState is a set of consensus node states. It is represented by the \* rmState is a set of consensus node states. It is represented by the
\* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is \* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is
\* the state of the r-th consensus node at the current step. \* the state of the r-th consensus node at the current step.
rmState, rmState,
\* msgs is the shared pool of messages sent to the network by consensus nodes. \* msgs is the shared pool of messages sent to the network by consensus nodes.
\* It is represented by a subset of Messages set. \* It is represented by a subset of Messages set.
msgs msgs
\* vars is a tuple of all variables used in the specification. It is needed to \* vars is a tuple of all variables used in the specification. It is needed to
\* simplify fairness conditions definition. \* simplify fairness conditions definition.
vars == <<rmState, msgs>> vars == <<rmState, msgs>>
\* N is the number of validators. \* N is the number of validators.
N == Cardinality(RM) N == Cardinality(RM)
\* F is the number of validators that are allowed to be malicious. \* F is the number of validators that are allowed to be malicious.
F == (N - 1) \div 3 F == (N - 1) \div 3
\* M is the number of validators that must function correctly. \* M is the number of validators that must function correctly.
M == N - F M == N - F
\* These assumptions are checked by the TLC model checker once at the start of \* These assumptions are checked by the TLC model checker once at the start of
\* the model checking process. All the input data (declared constants) specified \* the model checking process. All the input data (declared constants) specified
\* in the "Model Overview" section must satisfy these constraints. \* in the "Model Overview" section must satisfy these constraints.
ASSUME ASSUME
/\ RM \subseteq Nat /\ RM \subseteq Nat
/\ N >= 4 /\ N >= 4
/\ 0 \in RM /\ 0 \in RM
/\ RMFault \subseteq RM /\ RMFault \subseteq RM
/\ RMDead \subseteq RM /\ RMDead \subseteq RM
/\ Cardinality(RMFault) <= F /\ Cardinality(RMFault) <= F
/\ Cardinality(RMDead) <= F /\ Cardinality(RMDead) <= F
/\ Cardinality(RMFault \cup RMDead) <= F /\ Cardinality(RMFault \cup RMDead) <= F
/\ MaxView \in Nat /\ MaxView \in Nat
/\ MaxView <= 2 /\ MaxView <= 2
/\ MaxUndeliveredMessages \in Nat /\ MaxUndeliveredMessages \in Nat
/\ MaxUndeliveredMessages >= 3 \* First value when block can be accepted in some behaviours. /\ MaxUndeliveredMessages >= 3 \* First value when block can be accepted in some behaviours.
\* Messages is a set of records where each record holds the message type, \* Messages is a set of records where each record holds the message type,
\* the message sender and sender's view by the moment when message was sent. \* the message sender and sender's view by the moment when message was sent.
Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView"}, rm : RM, view : Nat] Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "ChangeView"}, rm : RM, view : Nat]
\* RMStates is a set of records where each record holds the node state, the node current view \* RMStates is a set of records where each record holds the node state, the node current view
\* and the pool of messages the nde has been sent or received and handled. \* and the pool of messages the nde has been sent or received and handled.
RMStates == [ RMStates == [
type: {"initialized", "prepareSent", "commitSent", "blockAccepted", "bad", "dead"}, type: {"initialized", "prepareSent", "commitSent", "blockAccepted", "bad", "dead"},
view : Nat, view : Nat,
pool : SUBSET Messages pool : SUBSET Messages
] ]
\* -------------- Useful operators -------------- \* -------------- Useful operators --------------
\* IsPrimary is an operator defining whether provided node r is primary \* IsPrimary is an operator defining whether provided node r is primary
\* for the current round from the r's point of view. It is a mapping \* for the current round from the r's point of view. It is a mapping
\* from RM to the set of {TRUE, FALSE}. \* from RM to the set of {TRUE, FALSE}.
IsPrimary(r) == rmState[r].view % N = r IsPrimary(r) == rmState[r].view % N = r
\* GetPrimary is an operator defining mapping from round index to the RM that \* GetPrimary is an operator defining mapping from round index to the RM that
\* is primary in this round. \* is primary in this round.
GetPrimary(view) == CHOOSE r \in RM : view % N = r GetPrimary(view) == CHOOSE r \in RM : view % N = r
\* GetNewView returns new view number based on the previous node view value. \* GetNewView returns new view number based on the previous node view value.
\* Current specifications only allows to increment view. \* Current specifications only allows to increment view.
GetNewView(oldView) == oldView + 1 GetNewView(oldView) == oldView + 1
\* IsViewChanging denotes whether node r have sent ChangeView message for the \* IsViewChanging denotes whether node r have sent ChangeView message for the
\* current (or later) round. \* current (or later) round.
IsViewChanging(r) == Cardinality({msg \in rmState[r].pool : msg.type = "ChangeView" /\ msg.view >= rmState[r].view /\ msg.rm = r}) /= 0 IsViewChanging(r) == Cardinality({msg \in rmState[r].pool : msg.type = "ChangeView" /\ msg.view >= rmState[r].view /\ msg.rm = r}) /= 0
\* CountCommitted returns the number of nodes that have sent the Commit message \* CountCommitted returns the number of nodes that have sent the Commit message
\* in the current round (as the node r sees it). \* in the current round (as the node r sees it).
CountCommitted(r) == Cardinality({rm \in RM : Cardinality({msg \in rmState[r].pool : msg.rm = rm /\ msg.type = "Commit"}) /= 0}) CountCommitted(r) == Cardinality({rm \in RM : Cardinality({msg \in rmState[r].pool : msg.rm = rm /\ msg.type = "Commit"}) /= 0})
\* CountFailed returns the number of nodes that haven't sent any message since \* CountFailed returns the number of nodes that haven't sent any message since
\* the last round (as the node r sees it from the point of its pool). \* the last round (as the node r sees it from the point of its pool).
CountFailed(r) == Cardinality({rm \in RM : Cardinality({msg \in rmState[r].pool : msg.rm = rm /\ msg.view >= rmState[r].view}) = 0 }) CountFailed(r) == Cardinality({rm \in RM : Cardinality({msg \in rmState[r].pool : msg.rm = rm /\ msg.view >= rmState[r].view}) = 0 })
\* MoreThanFNodesCommittedOrLost denotes whether more than F nodes committed or \* MoreThanFNodesCommittedOrLost denotes whether more than F nodes committed or
\* failed to communicate in the current round. \* failed to communicate in the current round.
MoreThanFNodesCommittedOrLost(r) == CountCommitted(r) + CountFailed(r) > F MoreThanFNodesCommittedOrLost(r) == CountCommitted(r) + CountFailed(r) > F
\* NotAcceptingPayloadsDueToViewChanging returns whether the node doesn't accept \* NotAcceptingPayloadsDueToViewChanging returns whether the node doesn't accept
\* payloads in the current step. \* payloads in the current step.
NotAcceptingPayloadsDueToViewChanging(r) == NotAcceptingPayloadsDueToViewChanging(r) ==
/\ IsViewChanging(r) /\ IsViewChanging(r)
/\ \neg MoreThanFNodesCommittedOrLost(r) /\ \neg MoreThanFNodesCommittedOrLost(r)
\* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest \* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest
\* message received from the current round's speaker (as the node r sees it). \* message received from the current round's speaker (as the node r sees it).
PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in rmState[r].pool PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in rmState[r].pool
\* CommitSent returns whether the node has its commit sent for the current block. \* CommitSent returns whether the node has its commit sent for the current block.
CommitSent(r) == Cardinality({msg \in rmState[r].pool : msg.rm = r /\ msg.type = "Commit"}) > 0 CommitSent(r) == Cardinality({msg \in rmState[r].pool : msg.rm = r /\ msg.type = "Commit"}) > 0
\* -------------- Safety temporal formula -------------- \* -------------- Safety temporal formula --------------
\* Init is the initial predicate initializing values at the start of every \* Init is the initial predicate initializing values at the start of every
\* behaviour. \* behaviour.
Init == Init ==
/\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 1, pool |-> {}]] /\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 1, pool |-> {}]]
/\ msgs = {} /\ msgs = {}
\* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest. \* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest.
RMSendPrepareRequest(r) == RMSendPrepareRequest(r) ==
/\ rmState[r].type = "initialized" /\ rmState[r].type = "initialized"
/\ IsPrimary(r) /\ IsPrimary(r)
/\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]
commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view]
IN /\ pReq \notin msgs IN /\ pReq \notin msgs
/\ IF Cardinality({m \in rmState[r].pool : m.type = "PrepareResponse" /\ m.view = rmState[r].view}) < M - 1 \* -1 is for the current PrepareRequest. /\ IF Cardinality({m \in rmState[r].pool : m.type = "PrepareResponse" /\ m.view = rmState[r].view}) < M - 1 \* -1 is for the current PrepareRequest.
THEN /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent", ![r].pool = rmState[r].pool \cup {pReq}] THEN /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent", ![r].pool = rmState[r].pool \cup {pReq}]
/\ msgs' = msgs \cup {pReq} /\ msgs' = msgs \cup {pReq}
ELSE /\ msgs' = msgs \cup {pReq, commit} ELSE /\ msgs' = msgs \cup {pReq, commit}
/\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the current Commit /\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the current Commit
THEN rmState' = [rmState EXCEPT ![r].type = "commitSent", ![r].pool = rmState[r].pool \cup {pReq, commit}] THEN rmState' = [rmState EXCEPT ![r].type = "commitSent", ![r].pool = rmState[r].pool \cup {pReq, commit}]
ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {pReq, commit}] ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {pReq, commit}]
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMSendChangeView describes node r sending ChangeView message on timeout. \* RMSendChangeView describes node r sending ChangeView message on timeout.
RMSendChangeView(r) == RMSendChangeView(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ rmState[r].type /= "blockAccepted" /\ rmState[r].type /= "blockAccepted"
/\ \/ (IsPrimary(r) /\ PrepareRequestSentOrReceived(r)) /\ \/ (IsPrimary(r) /\ PrepareRequestSentOrReceived(r))
\/ (\neg IsPrimary(r) /\ \neg CommitSent(r)) \/ (\neg IsPrimary(r) /\ \neg CommitSent(r))
/\ LET msg == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] /\ LET msg == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]
IN /\ msg \notin msgs IN /\ msg \notin msgs
/\ msgs' = msgs \cup {msg} /\ msgs' = msgs \cup {msg}
/\ IF Cardinality({m \in rmState[r].pool : m.type = "ChangeView" /\ GetNewView(m.view) >= GetNewView(msg.view)}) >= M-1 \* -1 is for the currently sent CV /\ IF Cardinality({m \in rmState[r].pool : m.type = "ChangeView" /\ GetNewView(m.view) >= GetNewView(msg.view)}) >= M-1 \* -1 is for the currently sent CV
THEN rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(msg.view), ![r].pool = rmState[r].pool \cup {msg}] THEN rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(msg.view), ![r].pool = rmState[r].pool \cup {msg}]
ELSE rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}] ELSE rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}]
\* OnTimeout describes two actions the node can take on timeout for waiting any event. \* OnTimeout describes two actions the node can take on timeout for waiting any event.
OnTimeout(r) == OnTimeout(r) ==
\/ RMSendPrepareRequest(r) \/ RMSendPrepareRequest(r)
\/ RMSendChangeView(r) \/ RMSendChangeView(r)
\* RMOnPrepareRequest describes non-primary node r receiving PrepareRequest from the \* RMOnPrepareRequest describes non-primary node r receiving PrepareRequest from the
\* primary node of the current round (view) and broadcasts PrepareResponse. \* primary node of the current round (view) and broadcasts PrepareResponse.
\* This step assumes that PrepareRequest always contains valid transactions and \* This step assumes that PrepareRequest always contains valid transactions and
\* signatures. \* signatures.
RMOnPrepareRequest(r) == RMOnPrepareRequest(r) ==
/\ rmState[r].type = "initialized" /\ rmState[r].type = "initialized"
/\ \E msg \in msgs \ rmState[r].pool: /\ \E msg \in msgs \ rmState[r].pool:
/\ msg.rm /= r /\ msg.rm /= r
/\ msg.type = "PrepareRequest" /\ msg.type = "PrepareRequest"
/\ msg.view = rmState[r].view /\ msg.view = rmState[r].view
/\ \neg IsPrimary(r) /\ \neg IsPrimary(r)
/\ \neg NotAcceptingPayloadsDueToViewChanging(r) \* dbft.go -L296, in C# node, but not in ours /\ \neg NotAcceptingPayloadsDueToViewChanging(r) \* dbft.go -L296, in C# node, but not in ours
/\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]
commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view]
IN IF Cardinality({m \in rmState[r].pool : m.type = "PrepareResponse" /\ m.view = rmState[r].view}) < M - 1 - 1 \* -1 is for reveived PrepareRequest; -1 is for current PrepareResponse IN IF Cardinality({m \in rmState[r].pool : m.type = "PrepareResponse" /\ m.view = rmState[r].view}) < M - 1 - 1 \* -1 is for reveived PrepareRequest; -1 is for current PrepareResponse
THEN /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent", ![r].pool = rmState[r].pool \cup {msg, pResp}] THEN /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent", ![r].pool = rmState[r].pool \cup {msg, pResp}]
/\ msgs' = msgs \cup {pResp} /\ msgs' = msgs \cup {pResp}
ELSE /\ msgs' = msgs \cup {msg, pResp, commit} ELSE /\ msgs' = msgs \cup {msg, pResp, commit}
/\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the current Commit /\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the current Commit
THEN rmState' = [rmState EXCEPT ![r].type = "commitSent", ![r].pool = rmState[r].pool \cup {msg, pResp, commit}] THEN rmState' = [rmState EXCEPT ![r].type = "commitSent", ![r].pool = rmState[r].pool \cup {msg, pResp, commit}]
ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {msg, pResp, commit}] ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {msg, pResp, commit}]
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMOnPrepareResponse describes node r accepting PrepareResponse message and handling it. \* RMOnPrepareResponse describes node r accepting PrepareResponse message and handling it.
\* If there's enough PrepareResponses collected it will send the Commit; in case if there's \* If there's enough PrepareResponses collected it will send the Commit; in case if there's
\* enough Commits it will accept the block. \* enough Commits it will accept the block.
RMOnPrepareResponse(r) == RMOnPrepareResponse(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ rmState[r].type /= "blockAccepted" /\ rmState[r].type /= "blockAccepted"
/\ \E msg \in msgs \ rmState[r].pool: /\ \E msg \in msgs \ rmState[r].pool:
/\ msg.rm /= r /\ msg.rm /= r
/\ msg.type = "PrepareResponse" /\ msg.type = "PrepareResponse"
/\ msg.view = rmState[r].view /\ msg.view = rmState[r].view
/\ \neg NotAcceptingPayloadsDueToViewChanging(r) /\ \neg NotAcceptingPayloadsDueToViewChanging(r)
/\ IF \/ Cardinality({m \in rmState[r].pool : (m.type = "PrepareRequest" \/ m.type = "PrepareResponse") /\ m.view = rmState[r].view}) < M - 1 \* -1 is for the currently received PrepareResponse. /\ IF \/ Cardinality({m \in rmState[r].pool : (m.type = "PrepareRequest" \/ m.type = "PrepareResponse") /\ m.view = rmState[r].view}) < M - 1 \* -1 is for the currently received PrepareResponse.
\/ CommitSent(r) \/ CommitSent(r)
\/ \neg PrepareRequestSentOrReceived(r) \/ \neg PrepareRequestSentOrReceived(r)
THEN /\ rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}] THEN /\ rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
ELSE LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] ELSE LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view]
IN /\ msgs' = msgs \cup {msg, commit} IN /\ msgs' = msgs \cup {msg, commit}
/\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the current Commit /\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the current Commit
THEN rmState' = [rmState EXCEPT ![r].type = "commitSent", ![r].pool = rmState[r].pool \cup {msg, commit}] THEN rmState' = [rmState EXCEPT ![r].type = "commitSent", ![r].pool = rmState[r].pool \cup {msg, commit}]
ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {msg, commit}] ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {msg, commit}]
\* RMOnCommit describes node r accepting Commit message and (in case if there's enough Commits) \* RMOnCommit describes node r accepting Commit message and (in case if there's enough Commits)
\* accepting the block. \* accepting the block.
RMOnCommit(r) == RMOnCommit(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ rmState[r].type /= "blockAccepted" /\ rmState[r].type /= "blockAccepted"
/\ \E msg \in msgs \ rmState[r].pool: /\ \E msg \in msgs \ rmState[r].pool:
/\ msg.rm /= r /\ msg.rm /= r
/\ msg.type = "Commit" /\ msg.type = "Commit"
/\ msg.view = rmState[r].view /\ msg.view = rmState[r].view
/\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the currently accepting commit /\ IF Cardinality({m \in rmState[r].pool : m.type = "Commit" /\ m.view = rmState[r].view}) < M-1 \* -1 is for the currently accepting commit
THEN rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}] THEN rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}]
ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {msg}] ELSE rmState' = [rmState EXCEPT ![r].type = "blockAccepted", ![r].pool = rmState[r].pool \cup {msg}]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMOnChangeView describes node r receiving ChangeView message and (in case if enough ChangeViews \* RMOnChangeView describes node r receiving ChangeView message and (in case if enough ChangeViews
\* is collected) changing its view. \* is collected) changing its view.
RMOnChangeView(r) == RMOnChangeView(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ rmState[r].type /= "blockAccepted" /\ rmState[r].type /= "blockAccepted"
/\ \E msg \in msgs \ rmState[r].pool: /\ \E msg \in msgs \ rmState[r].pool:
/\ msg.rm /= r /\ msg.rm /= r
/\ msg.type = "ChangeView" /\ msg.type = "ChangeView"
/\ msg.view = rmState[r].view /\ msg.view = rmState[r].view
/\ \neg CommitSent(r) /\ \neg CommitSent(r)
/\ Cardinality({m \in rmState[r].pool : m.type = "ChangeView" /\ m.rm = msg.rm /\ m.view > msg.view}) = 0 /\ Cardinality({m \in rmState[r].pool : m.type = "ChangeView" /\ m.rm = msg.rm /\ m.view > msg.view}) = 0
/\ IF Cardinality({m \in rmState[r].pool : m.type = "ChangeView" /\ GetNewView(m.view) >= GetNewView(msg.view)}) < M-1 \* -1 is for the currently accepting CV /\ IF Cardinality({m \in rmState[r].pool : m.type = "ChangeView" /\ GetNewView(m.view) >= GetNewView(msg.view)}) < M-1 \* -1 is for the currently accepting CV
THEN rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}] THEN rmState' = [rmState EXCEPT ![r].pool = rmState[r].pool \cup {msg}]
ELSE rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(msg.view), ![r].pool = rmState[r].pool \cup {msg}] ELSE rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(msg.view), ![r].pool = rmState[r].pool \cup {msg}]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMBeBad describes the faulty node r that will send any kind of consensus message starting \* RMBeBad describes the faulty node r that will send any kind of consensus message starting
\* from the step it's gone wild. This step is enabled only when RMFault is non-empty set. \* from the step it's gone wild. This step is enabled only when RMFault is non-empty set.
RMBeBad(r) == RMBeBad(r) ==
/\ r \in RMFault /\ r \in RMFault
/\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F /\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F
/\ rmState' = [rmState EXCEPT ![r].type = "bad"] /\ rmState' = [rmState EXCEPT ![r].type = "bad"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMFaultySendCV describes sending CV message by the faulty node r. \* RMFaultySendCV describes sending CV message by the faulty node r.
RMFaultySendCV(r) == RMFaultySendCV(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]
IN /\ cv \notin msgs IN /\ cv \notin msgs
/\ msgs' = msgs \cup {cv} /\ msgs' = msgs \cup {cv}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultyDoCV describes view changing by the faulty node r. \* RMFaultyDoCV describes view changing by the faulty node r.
RMFaultyDoCV(r) == RMFaultyDoCV(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)] /\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r. \* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r.
RMFaultySendPReq(r) == RMFaultySendPReq(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ IsPrimary(r) /\ IsPrimary(r)
/\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]
IN /\ pReq \notin msgs IN /\ pReq \notin msgs
/\ msgs' = msgs \cup {pReq} /\ msgs' = msgs \cup {pReq}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r. \* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r.
RMFaultySendPResp(r) == RMFaultySendPResp(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ \neg IsPrimary(r) /\ \neg IsPrimary(r)
/\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]
IN /\ pResp \notin msgs IN /\ pResp \notin msgs
/\ msgs' = msgs \cup {pResp} /\ msgs' = msgs \cup {pResp}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendCommit describes sending Commit message by the faulty node r. \* RMFaultySendCommit describes sending Commit message by the faulty node r.
RMFaultySendCommit(r) == RMFaultySendCommit(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] /\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view]
IN /\ commit \notin msgs IN /\ commit \notin msgs
/\ msgs' = msgs \cup {commit} /\ msgs' = msgs \cup {commit}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMDie describes node r that was removed from the network at the particular step \* RMDie describes node r that was removed from the network at the particular step
\* of the behaviour. After this node r can't change its state and accept/send messages. \* of the behaviour. After this node r can't change its state and accept/send messages.
RMDie(r) == RMDie(r) ==
/\ r \in RMDead /\ r \in RMDead
/\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F
/\ rmState' = [rmState EXCEPT ![r].type = "dead"] /\ rmState' = [rmState EXCEPT ![r].type = "dead"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* Terminating is an action that allows infinite stuttering to prevent deadlock on \* Terminating is an action that allows infinite stuttering to prevent deadlock on
\* behaviour termination. We consider termination to be valid if at least M nodes \* behaviour termination. We consider termination to be valid if at least M nodes
\* has the block being accepted. \* has the block being accepted.
Terminating == Terminating ==
/\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >=1 /\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >=1
/\ UNCHANGED <<msgs, rmState>> /\ UNCHANGED <<msgs, rmState>>
\* Next is the next-state action describing the transition from the current state \* Next is the next-state action describing the transition from the current state
\* to the next state of the behaviour. \* to the next state of the behaviour.
Next == Next ==
\/ Terminating \/ Terminating
\/ \E r \in RM : \/ \E r \in RM :
\/ OnTimeout(r) \/ OnTimeout(r)
\/ RMOnPrepareRequest(r) \/ RMOnPrepareResponse(r) \/ RMOnCommit(r) \/ RMOnChangeView(r) \/ RMOnPrepareRequest(r) \/ RMOnPrepareResponse(r) \/ RMOnCommit(r) \/ RMOnChangeView(r)
\/ RMDie(r) \/ RMBeBad(r) \/ RMDie(r) \/ RMBeBad(r)
\/ RMFaultySendCV(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) \/ RMFaultySendCV(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r)
\* Safety is a temporal formula that describes the whole set of allowed \* Safety is a temporal formula that describes the whole set of allowed
\* behaviours. It specifies only what the system MAY do (i.e. the set of \* behaviours. It specifies only what the system MAY do (i.e. the set of
\* possible allowed behaviours for the system). It asserts only what may \* possible allowed behaviours for the system). It asserts only what may
\* happen; any behaviour that violates it does so at some point and \* happen; any behaviour that violates it does so at some point and
\* nothing past that point makes difference. \* nothing past that point makes difference.
\* \*
\* E.g. this safety formula (applied standalone) allows the behaviour to end \* E.g. this safety formula (applied standalone) allows the behaviour to end
\* with an infinite set of stuttering steps (those steps that DO NOT change \* with an infinite set of stuttering steps (those steps that DO NOT change
\* neither msgs nor rmState) and never reach the state where at least one \* neither msgs nor rmState) and never reach the state where at least one
\* node is committed or accepted the block. \* node is committed or accepted the block.
\* \*
\* To forbid such behaviours we must specify what the system MUST \* To forbid such behaviours we must specify what the system MUST
\* do. It will be specified below with the help of fairness conditions in \* do. It will be specified below with the help of fairness conditions in
\* the Fairness formula. \* the Fairness formula.
Safety == Init /\ [][Next]_vars Safety == Init /\ [][Next]_vars
\* -------------- Fairness temporal formula -------------- \* -------------- Fairness temporal formula --------------
\* Fairness is a temporal assumptions under which the model is working. \* Fairness is a temporal assumptions under which the model is working.
\* Usually it specifies different kind of assumptions for each/some \* Usually it specifies different kind of assumptions for each/some
\* subactions of the Next's state action, but the only think that bothers \* subactions of the Next's state action, but the only think that bothers
\* us is preventing infinite stuttering at those steps where some of Next's \* us is preventing infinite stuttering at those steps where some of Next's
\* subactions are enabled. Thus, the only thing that we require from the \* subactions are enabled. Thus, the only thing that we require from the
\* system is to keep take the steps until it's impossible to take them. \* system is to keep take the steps until it's impossible to take them.
\* That's exactly how the weak fairness condition works: if some action \* That's exactly how the weak fairness condition works: if some action
\* remains continuously enabled, it must eventually happen. \* remains continuously enabled, it must eventually happen.
Fairness == WF_vars(Next) Fairness == WF_vars(Next)
\* -------------- Specification -------------- \* -------------- Specification --------------
\* The complete specification of the protocol written as a temporal formula. \* The complete specification of the protocol written as a temporal formula.
Spec == Safety /\ Fairness Spec == Safety /\ Fairness
\* -------------- Liveness temporal formula -------------- \* -------------- Liveness temporal formula --------------
\* For every possible behaviour it's true that there's at least one PrepareRequest \* For every possible behaviour it's true that there's at least one PrepareRequest
\* message from the speaker, there's at lest one PrepareResponse message and at \* message from the speaker, there's at lest one PrepareResponse message and at
\* least one Commit message. \* least one Commit message.
PrepareRequestSentRequirement == <>(\E msg \in msgs : msg.type = "PrepareRequest") PrepareRequestSentRequirement == <>(\E msg \in msgs : msg.type = "PrepareRequest")
PrepareResponseSentRequirement == <>(\E msg \in msgs : msg.type = "PrepareResponse") PrepareResponseSentRequirement == <>(\E msg \in msgs : msg.type = "PrepareResponse")
CommitSentRequirement == <>(\E msg \in msgs : msg.type = "Commit") CommitSentRequirement == <>(\E msg \in msgs : msg.type = "Commit")
\* For every possible behaviour it's true that eventually (i.e. at least once \* For every possible behaviour it's true that eventually (i.e. at least once
\* through the behaviour) block will be accepted. It is something that dBFT \* through the behaviour) block will be accepted. It is something that dBFT
\* must guarantee (an in practice this condition is violated). \* must guarantee (an in practice this condition is violated).
TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M) TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M)
\* A liveness temporal formula asserts only what must happen (i.e. specifies \* A liveness temporal formula asserts only what must happen (i.e. specifies
\* what the system MUST do). Any behaviour can NOT violate it at ANY point; \* what the system MUST do). Any behaviour can NOT violate it at ANY point;
\* there's always the rest of the behaviour that can always make the liveness \* there's always the rest of the behaviour that can always make the liveness
\* formula true; if there's no such behaviour than the liveness formula is \* formula true; if there's no such behaviour than the liveness formula is
\* violated. The liveness formula is supposed to be checked as a property \* violated. The liveness formula is supposed to be checked as a property
\* by the TLC model checker. \* by the TLC model checker.
Liveness == /\ PrepareRequestSentRequirement Liveness == /\ PrepareRequestSentRequirement
/\ PrepareResponseSentRequirement /\ PrepareResponseSentRequirement
/\ CommitSentRequirement /\ CommitSentRequirement
/\ TerminationRequirement /\ TerminationRequirement
\* -------------- Model constraints -------------- \* -------------- Model constraints --------------
\* Model constraints are a set of state predicates restricting the number of possible \* Model constraints are a set of state predicates restricting the number of possible
\* behaviour states. It is needed to reduce model checking time and prevent \* behaviour states. It is needed to reduce model checking time and prevent
\* the model graph size explosion. These formulaes must be specified at the \* the model graph size explosion. These formulaes must be specified at the
\* "State constraint" section of the "Additional Spec Options" section inside \* "State constraint" section of the "Additional Spec Options" section inside
\* the model overview. \* the model overview.
\* MaxViewConstraint is a state predicate restricting the maximum view of messages \* MaxViewConstraint is a state predicate restricting the maximum view of messages
\* and consensus nodes state. \* and consensus nodes state.
MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView
/\ \A msg \in msgs : msg.view <= MaxView /\ \A msg \in msgs : msg.view <= MaxView
\* MaxUndeliveredMessageConstraint is a state predicate restricting the maximum \* MaxUndeliveredMessageConstraint is a state predicate restricting the maximum
\* number of messages undelivered to any of the consensus nodes. \* number of messages undelivered to any of the consensus nodes.
MaxUndeliveredMessageConstraint == Cardinality({msg \in msgs : \E rm \in RM : msg \notin rmState[rm].pool}) <= MaxUndeliveredMessages MaxUndeliveredMessageConstraint == Cardinality({msg \in msgs : \E rm \in RM : msg \notin rmState[rm].pool}) <= MaxUndeliveredMessages
\* ModelConstraint is overall model constraint rule. \* ModelConstraint is overall model constraint rule.
ModelConstraint == MaxViewConstraint /\ MaxUndeliveredMessageConstraint ModelConstraint == MaxViewConstraint /\ MaxUndeliveredMessageConstraint
\* -------------- Invariants of the specification -------------- \* -------------- Invariants of the specification --------------
\* Model invariant is a state predicate (statement) that must be true for \* Model invariant is a state predicate (statement) that must be true for
\* every step of every reachable behaviour. Model invariant is supposed to \* every step of every reachable behaviour. Model invariant is supposed to
\* be checked as an Invariant by the TLC Model Checker. \* be checked as an Invariant by the TLC Model Checker.
\* TypeOK is a type-correctness invariant. It states that all elements of \* TypeOK is a type-correctness invariant. It states that all elements of
\* specification variables must have the proper type throughout the behaviour. \* specification variables must have the proper type throughout the behaviour.
TypeOK == TypeOK ==
/\ rmState \in [RM -> RMStates] /\ rmState \in [RM -> RMStates]
/\ msgs \subseteq Messages /\ msgs \subseteq Messages
\* InvTwoBlocksAccepted states that there can't be two different blocks accepted in \* InvTwoBlocksAccepted states that there can't be two different blocks accepted in
\* the two different views, i.e. dBFT must not allow forks. \* the two different views, i.e. dBFT must not allow forks.
InvTwoBlocksAccepted == \A r1 \in RM: InvTwoBlocksAccepted == \A r1 \in RM:
\A r2 \in RM \ {r1}: \A r2 \in RM \ {r1}:
\/ rmState[r1].type /= "blockAccepted" \/ rmState[r1].type /= "blockAccepted"
\/ rmState[r2].type /= "blockAccepted" \/ rmState[r2].type /= "blockAccepted"
\/ rmState[r1].view = rmState[r2].view \/ rmState[r1].view = rmState[r2].view
\* InvDeadlock is a deadlock invariant, it states that the following situation expected \* InvDeadlock is a deadlock invariant, it states that the following situation expected
\* never to happen: one node is committed in a single view, two others are committed in \* never to happen: one node is committed in a single view, two others are committed in
\* a larger view, and the last one has its view changing. \* a larger view, and the last one has its view changing.
InvDeadlock == \A r1 \in RM : InvDeadlock == \A r1 \in RM :
\A r2 \in RM \ {r1} : \A r2 \in RM \ {r1} :
\A r3 \in RM \ {r1, r2} : \A r3 \in RM \ {r1, r2} :
\A r4 \in RM \ {r1, r2, r3} : \A r4 \in RM \ {r1, r2, r3} :
\/ rmState[r1].type /= "commitSent" \/ rmState[r1].type /= "commitSent"
\/ rmState[r2].type /= "commitSent" \/ rmState[r2].type /= "commitSent"
\/ rmState[r3].type /= "commitSent" \/ rmState[r3].type /= "commitSent"
\/ \neg IsViewChanging(r4) \/ \neg IsViewChanging(r4)
\/ rmState[r1].view >= rmState[r2].view \/ rmState[r1].view >= rmState[r2].view
\/ rmState[r2].view /= rmState[r3].view \/ rmState[r2].view /= rmState[r3].view
\/ rmState[r3].view /= rmState[r4].view \/ rmState[r3].view /= rmState[r4].view
\* InvFaultNodesCount states that there can be F faulty or dead nodes at max. \* InvFaultNodesCount states that there can be F faulty or dead nodes at max.
InvFaultNodesCount == Cardinality({ InvFaultNodesCount == Cardinality({
r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead" r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead"
}) <= F }) <= F
\* This theorem asserts the truth of the temporal formula whose meaning is that \* This theorem asserts the truth of the temporal formula whose meaning is that
\* the state predicates TypeOK, InvTwoBlocksAccepted, InvDeadlock and InvFaultNodesCount are \* the state predicates TypeOK, InvTwoBlocksAccepted, InvDeadlock and InvFaultNodesCount are
\* the invariants of the specification Spec. This theorem is not supposed to be \* the invariants of the specification Spec. This theorem is not supposed to be
\* checked by the TLC model checker, it's here for the reader's understanding of \* checked by the TLC model checker, it's here for the reader's understanding of
\* the purpose of TypeOK, InvTwoBlocksAccepted, InvDeadlock and InvFaultNodesCount. \* the purpose of TypeOK, InvTwoBlocksAccepted, InvDeadlock and InvFaultNodesCount.
THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvDeadlock /\ InvFaultNodesCount) THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvDeadlock /\ InvFaultNodesCount)
============================================================================= =============================================================================
\* Modification History \* Modification History
\* Last modified Fri Feb 17 15:51:19 MSK 2023 by anna \* Last modified Fri Feb 17 15:51:19 MSK 2023 by anna
\* Created Tue Jan 10 12:28:45 MSK 2023 by anna \* Created Tue Jan 10 12:28:45 MSK 2023 by anna

View File

@ -1,44 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.lamport.tla.toolbox.tool.tlc.modelCheck"> <launchConfiguration type="org.lamport.tla.toolbox.tool.tlc.modelCheck">
<stringAttribute key="configurationName" value="AllGoodModel"/> <stringAttribute key="configurationName" value="AllGoodModel"/>
<intAttribute key="distributedFPSetCount" value="0"/> <intAttribute key="distributedFPSetCount" value="0"/>
<stringAttribute key="distributedNetworkInterface" value="172.200.0.254"/> <stringAttribute key="distributedNetworkInterface" value="172.200.0.254"/>
<intAttribute key="distributedNodesCount" value="1"/> <intAttribute key="distributedNodesCount" value="1"/>
<stringAttribute key="distributedTLC" value="off"/> <stringAttribute key="distributedTLC" value="off"/>
<intAttribute key="fpIndex" value="70"/> <intAttribute key="fpIndex" value="70"/>
<intAttribute key="maxHeapSize" value="50"/> <intAttribute key="maxHeapSize" value="50"/>
<stringAttribute key="modelBehaviorInit" value=""/> <stringAttribute key="modelBehaviorInit" value=""/>
<stringAttribute key="modelBehaviorNext" value=""/> <stringAttribute key="modelBehaviorNext" value=""/>
<stringAttribute key="modelBehaviorSpec" value="Spec"/> <stringAttribute key="modelBehaviorSpec" value="Spec"/>
<intAttribute key="modelBehaviorSpecType" value="1"/> <intAttribute key="modelBehaviorSpecType" value="1"/>
<stringAttribute key="modelBehaviorVars" value="msgs, rmState"/> <stringAttribute key="modelBehaviorVars" value="msgs, rmState"/>
<stringAttribute key="modelComments" value=""/> <stringAttribute key="modelComments" value=""/>
<booleanAttribute key="modelCorrectnessCheckDeadlock" value="true"/> <booleanAttribute key="modelCorrectnessCheckDeadlock" value="true"/>
<listAttribute key="modelCorrectnessInvariants"> <listAttribute key="modelCorrectnessInvariants">
<listEntry value="1TypeOK"/> <listEntry value="1TypeOK"/>
<listEntry value="1InvTwoBlocksAccepted"/> <listEntry value="1InvTwoBlocksAccepted"/>
<listEntry value="1InvDeadlock"/> <listEntry value="1InvDeadlock"/>
<listEntry value="1InvFaultNodesCount"/> <listEntry value="1InvFaultNodesCount"/>
</listAttribute> </listAttribute>
<listAttribute key="modelCorrectnessProperties"> <listAttribute key="modelCorrectnessProperties">
<listEntry value="1Liveness"/> <listEntry value="1Liveness"/>
</listAttribute> </listAttribute>
<intAttribute key="modelEditorOpenTabs" value="10"/> <intAttribute key="modelEditorOpenTabs" value="10"/>
<stringAttribute key="modelParameterActionConstraint" value=""/> <stringAttribute key="modelParameterActionConstraint" value=""/>
<listAttribute key="modelParameterConstants"> <listAttribute key="modelParameterConstants">
<listEntry value="RMFault;;{};0;0"/> <listEntry value="RMFault;;{};0;0"/>
<listEntry value="MaxView;;1;0;0"/> <listEntry value="MaxView;;1;0;0"/>
<listEntry value="RMDead;;{};0;0"/> <listEntry value="RMDead;;{};0;0"/>
<listEntry value="RM;;{0, 1, 2, 3};0;0"/> <listEntry value="RM;;{0, 1, 2, 3};0;0"/>
<listEntry value="MaxUndeliveredMessages;;6;0;0"/> <listEntry value="MaxUndeliveredMessages;;6;0;0"/>
</listAttribute> </listAttribute>
<stringAttribute key="modelParameterContraint" value="ModelConstraint"/> <stringAttribute key="modelParameterContraint" value="ModelConstraint"/>
<listAttribute key="modelParameterDefinitions"/> <listAttribute key="modelParameterDefinitions"/>
<stringAttribute key="modelParameterModelValues" value="{}"/> <stringAttribute key="modelParameterModelValues" value="{}"/>
<stringAttribute key="modelParameterNewDefinitions" value=""/> <stringAttribute key="modelParameterNewDefinitions" value=""/>
<intAttribute key="modelVersion" value="20191005"/> <intAttribute key="modelVersion" value="20191005"/>
<intAttribute key="numberOfWorkers" value="8"/> <intAttribute key="numberOfWorkers" value="8"/>
<stringAttribute key="result.mail.address" value=""/> <stringAttribute key="result.mail.address" value=""/>
<stringAttribute key="specName" value="dbftMultipool"/> <stringAttribute key="specName" value="dbftMultipool"/>
<stringAttribute key="tlcResourcesProfile" value="local custom"/> <stringAttribute key="tlcResourcesProfile" value="local custom"/>
</launchConfiguration> </launchConfiguration>

View File

@ -1,430 +1,430 @@
-------------------------------- MODULE dbft -------------------------------- -------------------------------- MODULE dbft --------------------------------
EXTENDS EXTENDS
Integers, Integers,
FiniteSets FiniteSets
CONSTANTS CONSTANTS
\* RM is the set of consensus node indexes starting from 0. \* RM is the set of consensus node indexes starting from 0.
\* Example: {0, 1, 2, 3} \* Example: {0, 1, 2, 3}
RM, RM,
\* RMFault is a set of consensus node indexes that are allowed to become \* RMFault is a set of consensus node indexes that are allowed to become
\* FAULT in the middle of every considered behavior and to send any \* FAULT in the middle of every considered behavior and to send any
\* consensus message afterwards. RMFault must be a subset of RM. An empty \* consensus message afterwards. RMFault must be a subset of RM. An empty
\* set means that all nodes are good in every possible behaviour. \* set means that all nodes are good in every possible behaviour.
\* Examples: {0} \* Examples: {0}
\* {1, 3} \* {1, 3}
\* {} \* {}
RMFault, RMFault,
\* RMDead is a set of consensus node indexes that are allowed to die in the \* RMDead is a set of consensus node indexes that are allowed to die in the
\* middle of every behaviour and do not send any message afterwards. RMDead \* middle of every behaviour and do not send any message afterwards. RMDead
\* must be a subset of RM. An empty set means that all nodes are alive and \* must be a subset of RM. An empty set means that all nodes are alive and
\* responding in in every possible behaviour. RMDead may intersect the \* responding in in every possible behaviour. RMDead may intersect the
\* RMFault set which means that node which is in both RMDead and RMFault \* RMFault set which means that node which is in both RMDead and RMFault
\* may become FAULT and send any message starting from some step of the \* may become FAULT and send any message starting from some step of the
\* particular behaviour and may also die in the same behaviour which will \* particular behaviour and may also die in the same behaviour which will
\* prevent it from sending any message. \* prevent it from sending any message.
\* Examples: {0} \* Examples: {0}
\* {3, 2} \* {3, 2}
\* {} \* {}
RMDead, RMDead,
\* MaxView is the maximum allowed view to be considered (starting from 0, \* MaxView is the maximum allowed view to be considered (starting from 0,
\* including the MaxView itself). This constraint was introduced to reduce \* including the MaxView itself). This constraint was introduced to reduce
\* the number of possible model states to be checked. It is recommended to \* the number of possible model states to be checked. It is recommended to
\* keep this setting not too high (< N is highly recommended). \* keep this setting not too high (< N is highly recommended).
\* Example: 2 \* Example: 2
MaxView MaxView
VARIABLES VARIABLES
\* rmState is a set of consensus node states. It is represented by the \* rmState is a set of consensus node states. It is represented by the
\* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is \* mapping (function) with domain RM and range RMStates. I.e. rmState[r] is
\* the state of the r-th consensus node at the current step. \* the state of the r-th consensus node at the current step.
rmState, rmState,
\* msgs is the shared pool of messages sent to the network by consensus nodes. \* msgs is the shared pool of messages sent to the network by consensus nodes.
\* It is represented by a subset of Messages set. \* It is represented by a subset of Messages set.
msgs msgs
\* vars is a tuple of all variables used in the specification. It is needed to \* vars is a tuple of all variables used in the specification. It is needed to
\* simplify fairness conditions definition. \* simplify fairness conditions definition.
vars == <<rmState, msgs>> vars == <<rmState, msgs>>
\* N is the number of validators. \* N is the number of validators.
N == Cardinality(RM) N == Cardinality(RM)
\* F is the number of validators that are allowed to be malicious. \* F is the number of validators that are allowed to be malicious.
F == (N - 1) \div 3 F == (N - 1) \div 3
\* M is the number of validators that must function correctly. \* M is the number of validators that must function correctly.
M == N - F M == N - F
\* These assumptions are checked by the TLC model checker once at the start of \* These assumptions are checked by the TLC model checker once at the start of
\* the model checking process. All the input data (declared constants) specified \* the model checking process. All the input data (declared constants) specified
\* in the "Model Overview" section must satisfy these constraints. \* in the "Model Overview" section must satisfy these constraints.
ASSUME ASSUME
/\ RM \subseteq Nat /\ RM \subseteq Nat
/\ N >= 4 /\ N >= 4
/\ 0 \in RM /\ 0 \in RM
/\ RMFault \subseteq RM /\ RMFault \subseteq RM
/\ RMDead \subseteq RM /\ RMDead \subseteq RM
/\ Cardinality(RMFault) <= F /\ Cardinality(RMFault) <= F
/\ Cardinality(RMDead) <= F /\ Cardinality(RMDead) <= F
/\ Cardinality(RMFault \cup RMDead) <= F /\ Cardinality(RMFault \cup RMDead) <= F
/\ MaxView \in Nat /\ MaxView \in Nat
/\ MaxView <= 2 /\ MaxView <= 2
\* RMStates is a set of records where each record holds the node state and \* RMStates is a set of records where each record holds the node state and
\* the node current view. \* the node current view.
RMStates == [ RMStates == [
type: {"initialized", "prepareSent", "commitSent", "cv", "commitAckSent", "blockAccepted", "bad", "dead"}, type: {"initialized", "prepareSent", "commitSent", "cv", "commitAckSent", "blockAccepted", "bad", "dead"},
view : Nat view : Nat
] ]
\* Messages is a set of records where each record holds the message type, \* Messages is a set of records where each record holds the message type,
\* the message sender and sender's view by the moment when message was sent. \* the message sender and sender's view by the moment when message was sent.
Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "CommitAck", "ChangeView"}, rm : RM, view : Nat] Messages == [type : {"PrepareRequest", "PrepareResponse", "Commit", "CommitAck", "ChangeView"}, rm : RM, view : Nat]
\* -------------- Useful operators -------------- \* -------------- Useful operators --------------
\* IsPrimary is an operator defining whether provided node r is primary \* IsPrimary is an operator defining whether provided node r is primary
\* for the current round from the r's point of view. It is a mapping \* for the current round from the r's point of view. It is a mapping
\* from RM to the set of {TRUE, FALSE}. \* from RM to the set of {TRUE, FALSE}.
IsPrimary(r) == rmState[r].view % N = r IsPrimary(r) == rmState[r].view % N = r
\* GetPrimary is an operator defining mapping from round index to the RM that \* GetPrimary is an operator defining mapping from round index to the RM that
\* is primary in this round. \* is primary in this round.
GetPrimary(view) == CHOOSE r \in RM : view % N = r GetPrimary(view) == CHOOSE r \in RM : view % N = r
\* GetNewView returns new view number based on the previous node view value. \* GetNewView returns new view number based on the previous node view value.
\* Current specifications only allows to increment view. \* Current specifications only allows to increment view.
GetNewView(oldView) == oldView + 1 GetNewView(oldView) == oldView + 1
\* CountCommitted returns the number of nodes that have sent the Commit message \* CountCommitted returns the number of nodes that have sent the Commit message
\* in the current round or in some other round. \* in the current round or in some other round.
CountCommitted(r) == Cardinality({rm \in RM : Cardinality({msg \in msgs : msg.rm = rm /\ msg.type = "Commit"}) /= 0}) CountCommitted(r) == Cardinality({rm \in RM : Cardinality({msg \in msgs : msg.rm = rm /\ msg.type = "Commit"}) /= 0})
\* MoreThanFNodesCommitted returns whether more than F nodes have been committed \* MoreThanFNodesCommitted returns whether more than F nodes have been committed
\* in the current round (as the node r sees it). \* in the current round (as the node r sees it).
\* \*
\* IMPORTANT NOTE: we intentionally do not add the "lost" nodes calculation to the specification, and here's \* IMPORTANT NOTE: we intentionally do not add the "lost" nodes calculation to the specification, and here's
\* the reason: from the node's point of view we can't reliably check that some neighbour is completely \* the reason: from the node's point of view we can't reliably check that some neighbour is completely
\* out of the network. It is possible that the node doesn't receive consensus messages from some other member \* out of the network. It is possible that the node doesn't receive consensus messages from some other member
\* due to network delays. On the other hand, real nodes can go down at any time. The absence of the \* due to network delays. On the other hand, real nodes can go down at any time. The absence of the
\* member's message doesn't mean that the member is out of the network, we never can be sure about \* member's message doesn't mean that the member is out of the network, we never can be sure about
\* that, thus, this information is unreliable and can't be trusted during the consensus process. \* that, thus, this information is unreliable and can't be trusted during the consensus process.
\* What can be trusted is whether there's a Commit message from some member was received by the node. \* What can be trusted is whether there's a Commit message from some member was received by the node.
MoreThanFNodesCommitted(r) == CountCommitted(r) > F MoreThanFNodesCommitted(r) == CountCommitted(r) > F
\* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest \* PrepareRequestSentOrReceived denotes whether there's a PrepareRequest
\* message received from the current round's speaker (as the node r sees it). \* message received from the current round's speaker (as the node r sees it).
PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in msgs PrepareRequestSentOrReceived(r) == [type |-> "PrepareRequest", rm |-> GetPrimary(rmState[r].view), view |-> rmState[r].view] \in msgs
\* -------------- Safety temporal formula -------------- \* -------------- Safety temporal formula --------------
\* Init is the initial predicate initializing values at the start of every \* Init is the initial predicate initializing values at the start of every
\* behaviour. \* behaviour.
Init == Init ==
/\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]] /\ rmState = [r \in RM |-> [type |-> "initialized", view |-> 0]]
/\ msgs = {} /\ msgs = {}
\* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest. \* RMSendPrepareRequest describes the primary node r broadcasting PrepareRequest.
RMSendPrepareRequest(r) == RMSendPrepareRequest(r) ==
/\ rmState[r].type = "initialized" /\ rmState[r].type = "initialized"
/\ IsPrimary(r) /\ IsPrimary(r)
/\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"]
/\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]}
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from \* RMSendPrepareResponse describes non-primary node r receiving PrepareRequest from
\* the primary node of the current round (view) and broadcasting PrepareResponse. \* the primary node of the current round (view) and broadcasting PrepareResponse.
\* This step assumes that PrepareRequest always contains valid transactions and \* This step assumes that PrepareRequest always contains valid transactions and
\* signatures. \* signatures.
RMSendPrepareResponse(r) == RMSendPrepareResponse(r) ==
/\ \/ rmState[r].type = "initialized" /\ \/ rmState[r].type = "initialized"
\* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage \* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage
\* as it is done in the code-level dBFT implementation by checking the NotAcceptingPayloadsDueToViewChanging \* as it is done in the code-level dBFT implementation by checking the NotAcceptingPayloadsDueToViewChanging
\* condition (see \* condition (see
\* https://github.com/nspcc-dev/dbft/blob/31c1bbdc74f2faa32ec9025062e3a4e2ccfd4214/dbft.go#L419 \* https://github.com/nspcc-dev/dbft/blob/31c1bbdc74f2faa32ec9025062e3a4e2ccfd4214/dbft.go#L419
\* and \* and
\* https://github.com/neo-project/neo-modules/blob/d00d90b9c27b3d0c3c57e9ca1f560a09975df241/src/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs#L79). \* https://github.com/neo-project/neo-modules/blob/d00d90b9c27b3d0c3c57e9ca1f560a09975df241/src/DBFTPlugin/Consensus/ConsensusService.OnMessage.cs#L79).
\* However, we can't easily count the number of "lost" nodes in this specification to match precisely \* However, we can't easily count the number of "lost" nodes in this specification to match precisely
\* the implementation. Moreover, we don't need it to be counted as the RMSendPrepareResponse enabling \* the implementation. Moreover, we don't need it to be counted as the RMSendPrepareResponse enabling
\* condition specifies only the thing that may happen given some particular set of enabling conditions. \* condition specifies only the thing that may happen given some particular set of enabling conditions.
\* Thus, we've extended the NotAcceptingPayloadsDueToViewChanging condition to consider only MoreThanFNodesCommitted. \* Thus, we've extended the NotAcceptingPayloadsDueToViewChanging condition to consider only MoreThanFNodesCommitted.
\* It should be noted that the logic of MoreThanFNodesCommittedOrLost can't be reliable in detecting lost nodes \* It should be noted that the logic of MoreThanFNodesCommittedOrLost can't be reliable in detecting lost nodes
\* (even with neo-project/neo#2057), because real nodes can go down at any time. See the comment above the MoreThanFNodesCommitted. \* (even with neo-project/neo#2057), because real nodes can go down at any time. See the comment above the MoreThanFNodesCommitted.
\/ /\ rmState[r].type = "cv" \/ /\ rmState[r].type = "cv"
/\ MoreThanFNodesCommitted(r) /\ MoreThanFNodesCommitted(r)
/\ \neg IsPrimary(r) /\ \neg IsPrimary(r)
/\ PrepareRequestSentOrReceived(r) /\ PrepareRequestSentOrReceived(r)
/\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"] /\ rmState' = [rmState EXCEPT ![r].type = "prepareSent"]
/\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]}
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMSendCommit describes node r sending Commit if there's enough PrepareResponse \* RMSendCommit describes node r sending Commit if there's enough PrepareResponse
\* messages. \* messages.
RMSendCommit(r) == RMSendCommit(r) ==
/\ \/ rmState[r].type = "prepareSent" /\ \/ rmState[r].type = "prepareSent"
\* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage, \* We do allow the transition from the "cv" state to the "prepareSent" or "commitSent" stage,
\* see the related comment inside the RMSendPrepareResponse definition. \* see the related comment inside the RMSendPrepareResponse definition.
\/ /\ rmState[r].type = "cv" \/ /\ rmState[r].type = "cv"
/\ MoreThanFNodesCommitted(r) /\ MoreThanFNodesCommitted(r)
/\ Cardinality({ /\ Cardinality({
msg \in msgs : /\ (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest") msg \in msgs : /\ (msg.type = "PrepareResponse" \/ msg.type = "PrepareRequest")
/\ msg.view = rmState[r].view /\ msg.view = rmState[r].view
}) >= M }) >= M
/\ PrepareRequestSentOrReceived(r) /\ PrepareRequestSentOrReceived(r)
/\ rmState' = [rmState EXCEPT ![r].type = "commitSent"] /\ rmState' = [rmState EXCEPT ![r].type = "commitSent"]
/\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "Commit", rm |-> r, view |-> rmState[r].view]}
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMSendCommitAck describes node r collecting enough Commit messages and sending \* RMSendCommitAck describes node r collecting enough Commit messages and sending
\* the CommitAck message. \* the CommitAck message.
RMSendCommitAck(r) == RMSendCommitAck(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ rmState[r].type /= "commitAckSent" /\ rmState[r].type /= "commitAckSent"
/\ rmState[r].type /= "blockAccepted" /\ rmState[r].type /= "blockAccepted"
/\ PrepareRequestSentOrReceived(r) /\ PrepareRequestSentOrReceived(r)
/\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) >= M /\ Cardinality({msg \in msgs : msg.type = "Commit" /\ msg.view = rmState[r].view}) >= M
/\ rmState' = [rmState EXCEPT ![r].type = "commitAckSent"] /\ rmState' = [rmState EXCEPT ![r].type = "commitAckSent"]
/\ msgs' = msgs \cup {[type |-> "CommitAck", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "CommitAck", rm |-> r, view |-> rmState[r].view]}
/\ UNCHANGED <<>> /\ UNCHANGED <<>>
\* RMAcceptBlock describes node r collecting enough CommitAck messages and accepting \* RMAcceptBlock describes node r collecting enough CommitAck messages and accepting
\* the block. \* the block.
RMAcceptBlock(r) == RMAcceptBlock(r) ==
/\ rmState[r].type = "commitAckSent" /\ rmState[r].type = "commitAckSent"
/\ Cardinality({msg \in msgs : msg.type = "CommitAck" /\ msg.view = rmState[r].view}) >= M /\ Cardinality({msg \in msgs : msg.type = "CommitAck" /\ msg.view = rmState[r].view}) >= M
/\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"] /\ rmState' = [rmState EXCEPT ![r].type = "blockAccepted"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMSendChangeView describes node r sending ChangeView message on timeout. \* RMSendChangeView describes node r sending ChangeView message on timeout.
RMSendChangeView(r) == RMSendChangeView(r) ==
/\ \/ (rmState[r].type = "initialized" /\ \neg IsPrimary(r)) /\ \/ (rmState[r].type = "initialized" /\ \neg IsPrimary(r))
\/ rmState[r].type = "prepareSent" \/ rmState[r].type = "prepareSent"
/\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]
IN /\ cv \notin msgs IN /\ cv \notin msgs
/\ rmState' = [rmState EXCEPT ![r].type = "cv"] /\ rmState' = [rmState EXCEPT ![r].type = "cv"]
/\ msgs' = msgs \cup {[type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]} /\ msgs' = msgs \cup {[type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]}
\* RMReceiveChangeView describes node r receiving enough ChangeView messages for \* RMReceiveChangeView describes node r receiving enough ChangeView messages for
\* view changing. \* view changing.
RMReceiveChangeView(r) == RMReceiveChangeView(r) ==
/\ rmState[r].type /= "bad" /\ rmState[r].type /= "bad"
/\ rmState[r].type /= "dead" /\ rmState[r].type /= "dead"
/\ rmState[r].type /= "blockAccepted" /\ rmState[r].type /= "blockAccepted"
/\ rmState[r].type /= "commitSent" /\ rmState[r].type /= "commitSent"
/\ rmState[r].type /= "commitAckSent" /\ rmState[r].type /= "commitAckSent"
/\ Cardinality({ /\ Cardinality({
rm \in RM : Cardinality({ rm \in RM : Cardinality({
msg \in msgs : /\ msg.type = "ChangeView" msg \in msgs : /\ msg.type = "ChangeView"
/\ msg.rm = rm /\ msg.rm = rm
/\ GetNewView(msg.view) >= GetNewView(rmState[r].view) /\ GetNewView(msg.view) >= GetNewView(rmState[r].view)
}) /= 0 }) /= 0
}) >= M }) >= M
/\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(rmState[r].view)] /\ rmState' = [rmState EXCEPT ![r].type = "initialized", ![r].view = GetNewView(rmState[r].view)]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMBeBad describes the faulty node r that will send any kind of consensus message starting \* RMBeBad describes the faulty node r that will send any kind of consensus message starting
\* from the step it's gone wild. This step is enabled only when RMFault is non-empty set. \* from the step it's gone wild. This step is enabled only when RMFault is non-empty set.
RMBeBad(r) == RMBeBad(r) ==
/\ r \in RMFault /\ r \in RMFault
/\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F /\ Cardinality({rm \in RM : rmState[rm].type = "bad"}) < F
/\ rmState' = [rmState EXCEPT ![r].type = "bad"] /\ rmState' = [rmState EXCEPT ![r].type = "bad"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMFaultySendCV describes sending CV message by the faulty node r. \* RMFaultySendCV describes sending CV message by the faulty node r.
RMFaultySendCV(r) == RMFaultySendCV(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view] /\ LET cv == [type |-> "ChangeView", rm |-> r, view |-> rmState[r].view]
IN /\ cv \notin msgs IN /\ cv \notin msgs
/\ msgs' = msgs \cup {cv} /\ msgs' = msgs \cup {cv}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultyDoCV describes view changing by the faulty node r. \* RMFaultyDoCV describes view changing by the faulty node r.
RMFaultyDoCV(r) == RMFaultyDoCV(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)] /\ rmState' = [rmState EXCEPT ![r].view = GetNewView(rmState[r].view)]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r. \* RMFaultySendPReq describes sending PrepareRequest message by the primary faulty node r.
RMFaultySendPReq(r) == RMFaultySendPReq(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ IsPrimary(r) /\ IsPrimary(r)
/\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view] /\ LET pReq == [type |-> "PrepareRequest", rm |-> r, view |-> rmState[r].view]
IN /\ pReq \notin msgs IN /\ pReq \notin msgs
/\ msgs' = msgs \cup {pReq} /\ msgs' = msgs \cup {pReq}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r. \* RMFaultySendPResp describes sending PrepareResponse message by the non-primary faulty node r.
RMFaultySendPResp(r) == RMFaultySendPResp(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ \neg IsPrimary(r) /\ \neg IsPrimary(r)
/\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view] /\ LET pResp == [type |-> "PrepareResponse", rm |-> r, view |-> rmState[r].view]
IN /\ pResp \notin msgs IN /\ pResp \notin msgs
/\ msgs' = msgs \cup {pResp} /\ msgs' = msgs \cup {pResp}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendCommit describes sending Commit message by the faulty node r. \* RMFaultySendCommit describes sending Commit message by the faulty node r.
RMFaultySendCommit(r) == RMFaultySendCommit(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view] /\ LET commit == [type |-> "Commit", rm |-> r, view |-> rmState[r].view]
IN /\ commit \notin msgs IN /\ commit \notin msgs
/\ msgs' = msgs \cup {commit} /\ msgs' = msgs \cup {commit}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMFaultySendCommitAck describes sending CommitAck message by the faulty node r. \* RMFaultySendCommitAck describes sending CommitAck message by the faulty node r.
RMFaultySendCommitAck(r) == RMFaultySendCommitAck(r) ==
/\ rmState[r].type = "bad" /\ rmState[r].type = "bad"
/\ LET ack == [type |-> "CommitAck", rm |-> r, view |-> rmState[r].view] /\ LET ack == [type |-> "CommitAck", rm |-> r, view |-> rmState[r].view]
IN /\ ack \notin msgs IN /\ ack \notin msgs
/\ msgs' = msgs \cup {ack} /\ msgs' = msgs \cup {ack}
/\ UNCHANGED <<rmState>> /\ UNCHANGED <<rmState>>
\* RMDie describes node r that was removed from the network at the particular step \* RMDie describes node r that was removed from the network at the particular step
\* of the behaviour. After this node r can't change its state and accept/send messages. \* of the behaviour. After this node r can't change its state and accept/send messages.
RMDie(r) == RMDie(r) ==
/\ r \in RMDead /\ r \in RMDead
/\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) < F
/\ rmState' = [rmState EXCEPT ![r].type = "dead"] /\ rmState' = [rmState EXCEPT ![r].type = "dead"]
/\ UNCHANGED <<msgs>> /\ UNCHANGED <<msgs>>
\* Terminating is an action that allows infinite stuttering to prevent deadlock on \* Terminating is an action that allows infinite stuttering to prevent deadlock on
\* behaviour termination. We consider termination to be valid if at least M nodes \* behaviour termination. We consider termination to be valid if at least M nodes
\* has the block being accepted. \* has the block being accepted.
Terminating == Terminating ==
/\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M /\ Cardinality({rm \in RM : rmState[rm].type = "blockAccepted"}) >= M
/\ UNCHANGED <<msgs, rmState>> /\ UNCHANGED <<msgs, rmState>>
\* TerminatingFourNodesDeadlock describes node r that is in the state where dBFT \* TerminatingFourNodesDeadlock describes node r that is in the state where dBFT
\* stucks in a four nodes scenario with one dead node allowed. Allow infinite \* stucks in a four nodes scenario with one dead node allowed. Allow infinite
\* stuttering to prevent TLC deadlock recognition. \* stuttering to prevent TLC deadlock recognition.
\* Note that this step is unused in the current specification, however, it may be \* Note that this step is unused in the current specification, however, it may be
\* used for further investigations of this deadlock. \* used for further investigations of this deadlock.
TerminatingFourNodesDeadlockSameView(r) == TerminatingFourNodesDeadlockSameView(r) ==
/\ Cardinality({rm \in RM : rmState[rm].type = "dead" /\ rmState[rm].view = rmState[r].view}) = 1 /\ Cardinality({rm \in RM : rmState[rm].type = "dead" /\ rmState[rm].view = rmState[r].view}) = 1
/\ Cardinality({rm \in RM : rmState[rm].type = "cv" /\ rmState[rm].view = rmState[r].view}) = 2 /\ Cardinality({rm \in RM : rmState[rm].type = "cv" /\ rmState[rm].view = rmState[r].view}) = 2
/\ Cardinality({rm \in RM : rmState[rm].type = "commitSent" /\ rmState[rm].view = rmState[r].view}) = 1 /\ Cardinality({rm \in RM : rmState[rm].type = "commitSent" /\ rmState[rm].view = rmState[r].view}) = 1
/\ UNCHANGED <<msgs, rmState>> /\ UNCHANGED <<msgs, rmState>>
\* TerminatingFourNodesDeadlock describes node r that is in the state where dBFT \* TerminatingFourNodesDeadlock describes node r that is in the state where dBFT
\* stucks in a four nodes scenario and the same view. Allow infinite stuttering \* stucks in a four nodes scenario and the same view. Allow infinite stuttering
\* to prevent TLC deadlock recognition. \* to prevent TLC deadlock recognition.
\* Note that this step is unused in the current specification, however, it may be \* Note that this step is unused in the current specification, however, it may be
\* used for further investigations of this deadlock. \* used for further investigations of this deadlock.
TerminatingFourNodesDeadlock == TerminatingFourNodesDeadlock ==
/\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) = 1 /\ Cardinality({rm \in RM : rmState[rm].type = "dead"}) = 1
/\ Cardinality({rm \in RM : rmState[rm].type = "cv"}) = 2 /\ Cardinality({rm \in RM : rmState[rm].type = "cv"}) = 2
/\ Cardinality({rm \in RM : rmState[rm].type = "commitSent"}) = 1 /\ Cardinality({rm \in RM : rmState[rm].type = "commitSent"}) = 1
/\ UNCHANGED <<msgs, rmState>> /\ UNCHANGED <<msgs, rmState>>
\* Next is the next-state action describing the transition from the current state \* Next is the next-state action describing the transition from the current state
\* to the next state of the behaviour. \* to the next state of the behaviour.
Next == Next ==
\/ Terminating \/ Terminating
\/ \E r \in RM: \/ \E r \in RM:
RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r) \/ RMSendCommitAck(r) RMSendPrepareRequest(r) \/ RMSendPrepareResponse(r) \/ RMSendCommit(r) \/ RMSendCommitAck(r)
\/ RMAcceptBlock(r) \/ RMSendChangeView(r) \/ RMReceiveChangeView(r) \/ RMAcceptBlock(r) \/ RMSendChangeView(r) \/ RMReceiveChangeView(r)
\/ RMDie(r) \/ RMBeBad(r) \/ RMDie(r) \/ RMBeBad(r)
\/ RMFaultySendCV(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendCommitAck(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r) \/ RMFaultySendCV(r) \/ RMFaultyDoCV(r) \/ RMFaultySendCommit(r) \/ RMFaultySendCommitAck(r) \/ RMFaultySendPReq(r) \/ RMFaultySendPResp(r)
\* Safety is a temporal formula that describes the whole set of allowed \* Safety is a temporal formula that describes the whole set of allowed
\* behaviours. It specifies only what the system MAY do (i.e. the set of \* behaviours. It specifies only what the system MAY do (i.e. the set of
\* possible allowed behaviours for the system). It asserts only what may \* possible allowed behaviours for the system). It asserts only what may
\* happen; any behaviour that violates it does so at some point and \* happen; any behaviour that violates it does so at some point and
\* nothing past that point makes difference. \* nothing past that point makes difference.
\* \*
\* E.g. this safety formula (applied standalone) allows the behaviour to end \* E.g. this safety formula (applied standalone) allows the behaviour to end
\* with an infinite set of stuttering steps (those steps that DO NOT change \* with an infinite set of stuttering steps (those steps that DO NOT change
\* neither msgs nor rmState) and never reach the state where at least one \* neither msgs nor rmState) and never reach the state where at least one
\* node is committed or accepted the block. \* node is committed or accepted the block.
\* \*
\* To forbid such behaviours we must specify what the system MUST \* To forbid such behaviours we must specify what the system MUST
\* do. It will be specified below with the help of fairness conditions in \* do. It will be specified below with the help of fairness conditions in
\* the Fairness formula. \* the Fairness formula.
Safety == Init /\ [][Next]_vars Safety == Init /\ [][Next]_vars
\* -------------- Fairness temporal formula -------------- \* -------------- Fairness temporal formula --------------
\* Fairness is a temporal assumptions under which the model is working. \* Fairness is a temporal assumptions under which the model is working.
\* Usually it specifies different kind of assumptions for each/some \* Usually it specifies different kind of assumptions for each/some
\* subactions of the Next's state action, but the only think that bothers \* subactions of the Next's state action, but the only think that bothers
\* us is preventing infinite stuttering at those steps where some of Next's \* us is preventing infinite stuttering at those steps where some of Next's
\* subactions are enabled. Thus, the only thing that we require from the \* subactions are enabled. Thus, the only thing that we require from the
\* system is to keep take the steps until it's impossible to take them. \* system is to keep take the steps until it's impossible to take them.
\* That's exactly how the weak fairness condition works: if some action \* That's exactly how the weak fairness condition works: if some action
\* remains continuously enabled, it must eventually happen. \* remains continuously enabled, it must eventually happen.
Fairness == WF_vars(Next) Fairness == WF_vars(Next)
\* -------------- Specification -------------- \* -------------- Specification --------------
\* The complete specification of the protocol written as a temporal formula. \* The complete specification of the protocol written as a temporal formula.
Spec == Safety /\ Fairness Spec == Safety /\ Fairness
\* -------------- Liveness temporal formula -------------- \* -------------- Liveness temporal formula --------------
\* For every possible behaviour it's true that eventually (i.e. at least once \* For every possible behaviour it's true that eventually (i.e. at least once
\* through the behaviour) block will be accepted. It is something that dBFT \* through the behaviour) block will be accepted. It is something that dBFT
\* must guarantee (an in practice this condition is violated). \* must guarantee (an in practice this condition is violated).
TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M) TerminationRequirement == <>(Cardinality({r \in RM : rmState[r].type = "blockAccepted"}) >= M)
\* A liveness temporal formula asserts only what must happen (i.e. specifies \* A liveness temporal formula asserts only what must happen (i.e. specifies
\* what the system MUST do). Any behaviour can NOT violate it at ANY point; \* what the system MUST do). Any behaviour can NOT violate it at ANY point;
\* there's always the rest of the behaviour that can always make the liveness \* there's always the rest of the behaviour that can always make the liveness
\* formula true; if there's no such behaviour than the liveness formula is \* formula true; if there's no such behaviour than the liveness formula is
\* violated. The liveness formula is supposed to be checked as a property \* violated. The liveness formula is supposed to be checked as a property
\* by the TLC model checker. \* by the TLC model checker.
Liveness == TerminationRequirement Liveness == TerminationRequirement
\* -------------- ModelConstraints -------------- \* -------------- ModelConstraints --------------
\* MaxViewConstraint is a state predicate restricting the number of possible \* MaxViewConstraint is a state predicate restricting the number of possible
\* behaviour states. It is needed to reduce model checking time and prevent \* behaviour states. It is needed to reduce model checking time and prevent
\* the model graph size explosion. This formulae must be specified at the \* the model graph size explosion. This formulae must be specified at the
\* "State constraint" section of the "Additional Spec Options" section inside \* "State constraint" section of the "Additional Spec Options" section inside
\* the model overview. \* the model overview.
MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView MaxViewConstraint == /\ \A r \in RM : rmState[r].view <= MaxView
/\ \A msg \in msgs : msg.view <= MaxView /\ \A msg \in msgs : msg.view <= MaxView
\* -------------- Invariants of the specification -------------- \* -------------- Invariants of the specification --------------
\* Model invariant is a state predicate (statement) that must be true for \* Model invariant is a state predicate (statement) that must be true for
\* every step of every reachable behaviour. Model invariant is supposed to \* every step of every reachable behaviour. Model invariant is supposed to
\* be checked as an Invariant by the TLC Model Checker. \* be checked as an Invariant by the TLC Model Checker.
\* TypeOK is a type-correctness invariant. It states that all elements of \* TypeOK is a type-correctness invariant. It states that all elements of
\* specification variables must have the proper type throughout the behaviour. \* specification variables must have the proper type throughout the behaviour.
TypeOK == TypeOK ==
/\ rmState \in [RM -> RMStates] /\ rmState \in [RM -> RMStates]
/\ msgs \subseteq Messages /\ msgs \subseteq Messages
\* InvTwoBlocksAccepted states that there can't be two different blocks accepted in \* InvTwoBlocksAccepted states that there can't be two different blocks accepted in
\* the two different views, i.e. dBFT must not allow forks. \* the two different views, i.e. dBFT must not allow forks.
InvTwoBlocksAccepted == \A r1 \in RM: InvTwoBlocksAccepted == \A r1 \in RM:
\A r2 \in RM \ {r1}: \A r2 \in RM \ {r1}:
\/ rmState[r1].type /= "blockAccepted" \/ rmState[r1].type /= "blockAccepted"
\/ rmState[r2].type /= "blockAccepted" \/ rmState[r2].type /= "blockAccepted"
\/ rmState[r1].view = rmState[r2].view \/ rmState[r1].view = rmState[r2].view
\* InvFaultNodesCount states that there can be F faulty or dead nodes at max. \* InvFaultNodesCount states that there can be F faulty or dead nodes at max.
InvFaultNodesCount == Cardinality({ InvFaultNodesCount == Cardinality({
r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead" r \in RM : rmState[r].type = "bad" \/ rmState[r].type = "dead"
}) <= F }) <= F
\* This theorem asserts the truth of the temporal formula whose meaning is that \* This theorem asserts the truth of the temporal formula whose meaning is that
\* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are \* the state predicates TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount are
\* the invariants of the specification Spec. This theorem is not supposed to be \* the invariants of the specification Spec. This theorem is not supposed to be
\* checked by the TLC model checker, it's here for the reader's understanding of \* checked by the TLC model checker, it's here for the reader's understanding of
\* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount. \* the purpose of TypeOK, InvTwoBlocksAccepted and InvFaultNodesCount.
THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvFaultNodesCount) THEOREM Spec => [](TypeOK /\ InvTwoBlocksAccepted /\ InvFaultNodesCount)
============================================================================= =============================================================================
\* Modification History \* Modification History
\* Last modified Wed Jun 19 17:51:15 MSK 2024 by anna \* Last modified Wed Jun 19 17:51:15 MSK 2024 by anna
\* Last modified Mon Mar 06 15:36:57 MSK 2023 by root \* Last modified Mon Mar 06 15:36:57 MSK 2023 by root
\* Last modified Sat Jan 21 01:26:16 MSK 2023 by rik \* Last modified Sat Jan 21 01:26:16 MSK 2023 by rik
\* Created Thu Dec 15 16:06:17 MSK 2022 by anna \* Created Thu Dec 15 16:06:17 MSK 2022 by anna

View File

@ -1,42 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.lamport.tla.toolbox.tool.tlc.modelCheck"> <launchConfiguration type="org.lamport.tla.toolbox.tool.tlc.modelCheck">
<stringAttribute key="configurationName" value="AllGoodModel"/> <stringAttribute key="configurationName" value="AllGoodModel"/>
<intAttribute key="distributedFPSetCount" value="0"/> <intAttribute key="distributedFPSetCount" value="0"/>
<stringAttribute key="distributedNetworkInterface" value="172.200.0.254"/> <stringAttribute key="distributedNetworkInterface" value="172.200.0.254"/>
<intAttribute key="distributedNodesCount" value="1"/> <intAttribute key="distributedNodesCount" value="1"/>
<stringAttribute key="distributedTLC" value="off"/> <stringAttribute key="distributedTLC" value="off"/>
<intAttribute key="fpIndex" value="47"/> <intAttribute key="fpIndex" value="47"/>
<intAttribute key="maxHeapSize" value="50"/> <intAttribute key="maxHeapSize" value="50"/>
<stringAttribute key="modelBehaviorInit" value=""/> <stringAttribute key="modelBehaviorInit" value=""/>
<stringAttribute key="modelBehaviorNext" value=""/> <stringAttribute key="modelBehaviorNext" value=""/>
<stringAttribute key="modelBehaviorSpec" value="Spec"/> <stringAttribute key="modelBehaviorSpec" value="Spec"/>
<intAttribute key="modelBehaviorSpecType" value="1"/> <intAttribute key="modelBehaviorSpecType" value="1"/>
<stringAttribute key="modelBehaviorVars" value="msgs, rmState"/> <stringAttribute key="modelBehaviorVars" value="msgs, rmState"/>
<stringAttribute key="modelComments" value=""/> <stringAttribute key="modelComments" value=""/>
<booleanAttribute key="modelCorrectnessCheckDeadlock" value="true"/> <booleanAttribute key="modelCorrectnessCheckDeadlock" value="true"/>
<listAttribute key="modelCorrectnessInvariants"> <listAttribute key="modelCorrectnessInvariants">
<listEntry value="1TypeOK"/> <listEntry value="1TypeOK"/>
<listEntry value="1InvTwoBlocksAccepted"/> <listEntry value="1InvTwoBlocksAccepted"/>
<listEntry value="1InvFaultNodesCount"/> <listEntry value="1InvFaultNodesCount"/>
</listAttribute> </listAttribute>
<listAttribute key="modelCorrectnessProperties"> <listAttribute key="modelCorrectnessProperties">
<listEntry value="1Liveness"/> <listEntry value="1Liveness"/>
</listAttribute> </listAttribute>
<intAttribute key="modelEditorOpenTabs" value="10"/> <intAttribute key="modelEditorOpenTabs" value="10"/>
<stringAttribute key="modelParameterActionConstraint" value=""/> <stringAttribute key="modelParameterActionConstraint" value=""/>
<listAttribute key="modelParameterConstants"> <listAttribute key="modelParameterConstants">
<listEntry value="RMFault;;{};0;0"/> <listEntry value="RMFault;;{};0;0"/>
<listEntry value="MaxView;;1;0;0"/> <listEntry value="MaxView;;1;0;0"/>
<listEntry value="RMDead;;{};0;0"/> <listEntry value="RMDead;;{};0;0"/>
<listEntry value="RM;;{0, 1, 2, 3};0;0"/> <listEntry value="RM;;{0, 1, 2, 3};0;0"/>
</listAttribute> </listAttribute>
<stringAttribute key="modelParameterContraint" value="MaxViewConstraint"/> <stringAttribute key="modelParameterContraint" value="MaxViewConstraint"/>
<listAttribute key="modelParameterDefinitions"/> <listAttribute key="modelParameterDefinitions"/>
<stringAttribute key="modelParameterModelValues" value="{}"/> <stringAttribute key="modelParameterModelValues" value="{}"/>
<stringAttribute key="modelParameterNewDefinitions" value=""/> <stringAttribute key="modelParameterNewDefinitions" value=""/>
<intAttribute key="modelVersion" value="20191005"/> <intAttribute key="modelVersion" value="20191005"/>
<intAttribute key="numberOfWorkers" value="8"/> <intAttribute key="numberOfWorkers" value="8"/>
<stringAttribute key="result.mail.address" value=""/> <stringAttribute key="result.mail.address" value=""/>
<stringAttribute key="specName" value="dbft"/> <stringAttribute key="specName" value="dbft"/>
<stringAttribute key="tlcResourcesProfile" value="local custom"/> <stringAttribute key="tlcResourcesProfile" value="local custom"/>
</launchConfiguration> </launchConfiguration>

32
go.sum
View File

@ -1,16 +1,16 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,63 +1,63 @@
package dbft package dbft
type ( type (
// inbox is a structure storing messages from a single epoch. // inbox is a structure storing messages from a single epoch.
inbox[H Hash] struct { inbox[H Hash] struct {
prepare map[uint16]ConsensusPayload[H] prepare map[uint16]ConsensusPayload[H]
chViews map[uint16]ConsensusPayload[H] chViews map[uint16]ConsensusPayload[H]
preCommit map[uint16]ConsensusPayload[H] preCommit map[uint16]ConsensusPayload[H]
commit map[uint16]ConsensusPayload[H] commit map[uint16]ConsensusPayload[H]
} }
// cache is an auxiliary structure storing messages // cache is an auxiliary structure storing messages
// from future epochs. // from future epochs.
cache[H Hash] struct { cache[H Hash] struct {
mail map[uint32]*inbox[H] mail map[uint32]*inbox[H]
} }
) )
func newInbox[H Hash]() *inbox[H] { func newInbox[H Hash]() *inbox[H] {
return &inbox[H]{ return &inbox[H]{
prepare: make(map[uint16]ConsensusPayload[H]), prepare: make(map[uint16]ConsensusPayload[H]),
chViews: make(map[uint16]ConsensusPayload[H]), chViews: make(map[uint16]ConsensusPayload[H]),
preCommit: make(map[uint16]ConsensusPayload[H]), preCommit: make(map[uint16]ConsensusPayload[H]),
commit: make(map[uint16]ConsensusPayload[H]), commit: make(map[uint16]ConsensusPayload[H]),
} }
} }
func newCache[H Hash]() cache[H] { func newCache[H Hash]() cache[H] {
return cache[H]{ return cache[H]{
mail: make(map[uint32]*inbox[H]), mail: make(map[uint32]*inbox[H]),
} }
} }
func (c *cache[H]) getHeight(h uint32) *inbox[H] { func (c *cache[H]) getHeight(h uint32) *inbox[H] {
if m, ok := c.mail[h]; ok { if m, ok := c.mail[h]; ok {
delete(c.mail, h) delete(c.mail, h)
return m return m
} }
return nil return nil
} }
func (c *cache[H]) addMessage(m ConsensusPayload[H]) { func (c *cache[H]) addMessage(m ConsensusPayload[H]) {
msgs, ok := c.mail[m.Height()] msgs, ok := c.mail[m.Height()]
if !ok { if !ok {
msgs = newInbox[H]() msgs = newInbox[H]()
c.mail[m.Height()] = msgs c.mail[m.Height()] = msgs
} }
switch m.Type() { switch m.Type() {
case PrepareRequestType, PrepareResponseType: case PrepareRequestType, PrepareResponseType:
msgs.prepare[m.ValidatorIndex()] = m msgs.prepare[m.ValidatorIndex()] = m
case ChangeViewType: case ChangeViewType:
msgs.chViews[m.ValidatorIndex()] = m msgs.chViews[m.ValidatorIndex()] = m
case PreCommitType: case PreCommitType:
msgs.preCommit[m.ValidatorIndex()] = m msgs.preCommit[m.ValidatorIndex()] = m
case CommitType: case CommitType:
msgs.commit[m.ValidatorIndex()] = m msgs.commit[m.ValidatorIndex()] = m
default: default:
// Others are recoveries and we don't currently use them. // Others are recoveries and we don't currently use them.
// Theoretically messages could be extracted. // Theoretically messages could be extracted.
} }
} }

View File

@ -1,115 +1,115 @@
package dbft package dbft
import ( import (
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// Structures used for type-specific dBFT/payloads implementation to avoid cyclic // Structures used for type-specific dBFT/payloads implementation to avoid cyclic
// dependency. // dependency.
type ( type (
hash struct{} hash struct{}
payloadStub struct { payloadStub struct {
height uint32 height uint32
typ MessageType typ MessageType
validatorIndex uint16 validatorIndex uint16
} }
) )
func (hash) String() string { func (hash) String() string {
return "" return ""
} }
func (p payloadStub) ViewNumber() byte { func (p payloadStub) ViewNumber() byte {
panic("TODO") panic("TODO")
} }
func (p payloadStub) SetViewNumber(byte) { func (p payloadStub) SetViewNumber(byte) {
panic("TODO") panic("TODO")
} }
func (p payloadStub) Type() MessageType { func (p payloadStub) Type() MessageType {
return p.typ return p.typ
} }
func (p payloadStub) SetType(MessageType) { func (p payloadStub) SetType(MessageType) {
panic("TODO") panic("TODO")
} }
func (p payloadStub) Payload() any { func (p payloadStub) Payload() any {
panic("TODO") panic("TODO")
} }
func (p payloadStub) SetPayload(any) { func (p payloadStub) SetPayload(any) {
panic("TODO") panic("TODO")
} }
func (p payloadStub) GetChangeView() ChangeView { func (p payloadStub) GetChangeView() ChangeView {
panic("TODO") panic("TODO")
} }
func (p payloadStub) GetPrepareRequest() PrepareRequest[hash] { func (p payloadStub) GetPrepareRequest() PrepareRequest[hash] {
panic("TODO") panic("TODO")
} }
func (p payloadStub) GetPrepareResponse() PrepareResponse[hash] { func (p payloadStub) GetPrepareResponse() PrepareResponse[hash] {
panic("TODO") panic("TODO")
} }
func (p payloadStub) GetCommit() Commit { func (p payloadStub) GetCommit() Commit {
panic("TODO") panic("TODO")
} }
func (p payloadStub) GetPreCommit() PreCommit { panic("TODO") } func (p payloadStub) GetPreCommit() PreCommit { panic("TODO") }
func (p payloadStub) GetRecoveryRequest() RecoveryRequest { func (p payloadStub) GetRecoveryRequest() RecoveryRequest {
panic("TODO") panic("TODO")
} }
func (p payloadStub) GetRecoveryMessage() RecoveryMessage[hash] { func (p payloadStub) GetRecoveryMessage() RecoveryMessage[hash] {
panic("TODO") panic("TODO")
} }
func (p payloadStub) ValidatorIndex() uint16 { func (p payloadStub) ValidatorIndex() uint16 {
return p.validatorIndex return p.validatorIndex
} }
func (p payloadStub) SetValidatorIndex(uint16) { func (p payloadStub) SetValidatorIndex(uint16) {
panic("TODO") panic("TODO")
} }
func (p payloadStub) Height() uint32 { func (p payloadStub) Height() uint32 {
return p.height return p.height
} }
func (p payloadStub) SetHeight(uint32) { func (p payloadStub) SetHeight(uint32) {
panic("TODO") panic("TODO")
} }
func (p payloadStub) Hash() hash { func (p payloadStub) Hash() hash {
panic("TODO") panic("TODO")
} }
func TestMessageCache(t *testing.T) { func TestMessageCache(t *testing.T) {
c := newCache[hash]() c := newCache[hash]()
p1 := payloadStub{ p1 := payloadStub{
height: 3, height: 3,
typ: PrepareRequestType, typ: PrepareRequestType,
} }
c.addMessage(p1) c.addMessage(p1)
p2 := payloadStub{ p2 := payloadStub{
height: 4, height: 4,
typ: ChangeViewType, typ: ChangeViewType,
} }
c.addMessage(p2) c.addMessage(p2)
p3 := payloadStub{ p3 := payloadStub{
height: 4, height: 4,
typ: CommitType, typ: CommitType,
} }
c.addMessage(p3) c.addMessage(p3)
p4 := payloadStub{ p4 := payloadStub{
height: 3, height: 3,
typ: PreCommitType, typ: PreCommitType,
} }
c.addMessage(p4) c.addMessage(p4)
box := c.getHeight(3) box := c.getHeight(3)
require.Len(t, box.chViews, 0) require.Len(t, box.chViews, 0)
require.Len(t, box.prepare, 1) require.Len(t, box.prepare, 1)
require.Len(t, box.preCommit, 1) require.Len(t, box.preCommit, 1)
require.Len(t, box.commit, 0) require.Len(t, box.commit, 0)
box = c.getHeight(4) box = c.getHeight(4)
require.Len(t, box.chViews, 1) require.Len(t, box.chViews, 1)
require.Len(t, box.prepare, 0) require.Len(t, box.prepare, 0)
require.Len(t, box.preCommit, 0) require.Len(t, box.preCommit, 0)
require.Len(t, box.commit, 1) require.Len(t, box.commit, 1)
} }

View File

@ -1,28 +1,28 @@
package dbft package dbft
import ( import (
"fmt" "fmt"
) )
type ( type (
// PublicKey is a generic public key interface used by dbft. // PublicKey is a generic public key interface used by dbft.
PublicKey any PublicKey any
// PrivateKey is a generic private key interface used by dbft. PrivateKey is used // PrivateKey is a generic private key interface used by dbft. PrivateKey is used
// only by [PreBlock] and [Block] signing callbacks ([PreBlock.SetData] and // only by [PreBlock] and [Block] signing callbacks ([PreBlock.SetData] and
// [Block.Sign]) to grant access to the private key abstraction to Block and // [Block.Sign]) to grant access to the private key abstraction to Block and
// PreBlock signing code. PrivateKey does not contain any methods, hence user // PreBlock signing code. PrivateKey does not contain any methods, hence user
// supposed to perform type assertion before the PrivateKey usage. // supposed to perform type assertion before the PrivateKey usage.
PrivateKey any PrivateKey any
// Hash is a generic hash interface used by dbft for payloads, blocks and // Hash is a generic hash interface used by dbft for payloads, blocks and
// transactions identification. It is recommended to implement this interface // transactions identification. It is recommended to implement this interface
// using hash functions with low hash collision probability. The following // using hash functions with low hash collision probability. The following
// requirements must be met: // requirements must be met:
// 1. Hashes of two equal payloads/blocks/transactions are equal. // 1. Hashes of two equal payloads/blocks/transactions are equal.
// 2. Hashes of two different payloads/blocks/transactions are different. // 2. Hashes of two different payloads/blocks/transactions are different.
Hash interface { Hash interface {
comparable comparable
fmt.Stringer fmt.Stringer
} }
) )

View File

@ -1,69 +1,69 @@
package consensus package consensus
import ( import (
"encoding/gob" "encoding/gob"
) )
type ( type (
changeViewCompact struct { changeViewCompact struct {
ValidatorIndex uint16 ValidatorIndex uint16
OriginalViewNumber byte OriginalViewNumber byte
Timestamp uint32 Timestamp uint32
} }
preCommitCompact struct { preCommitCompact struct {
ViewNumber byte ViewNumber byte
ValidatorIndex uint16 ValidatorIndex uint16
Data []byte Data []byte
} }
commitCompact struct { commitCompact struct {
ViewNumber byte ViewNumber byte
ValidatorIndex uint16 ValidatorIndex uint16
Signature [signatureSize]byte Signature [signatureSize]byte
} }
preparationCompact struct { preparationCompact struct {
ValidatorIndex uint16 ValidatorIndex uint16
} }
) )
// EncodeBinary implements Serializable interface. // EncodeBinary implements Serializable interface.
func (p changeViewCompact) EncodeBinary(w *gob.Encoder) error { func (p changeViewCompact) EncodeBinary(w *gob.Encoder) error {
return w.Encode(p) return w.Encode(p)
} }
// DecodeBinary implements Serializable interface. // DecodeBinary implements Serializable interface.
func (p *changeViewCompact) DecodeBinary(r *gob.Decoder) error { func (p *changeViewCompact) DecodeBinary(r *gob.Decoder) error {
return r.Decode(p) return r.Decode(p)
} }
// EncodeBinary implements Serializable interface. // EncodeBinary implements Serializable interface.
func (p preCommitCompact) EncodeBinary(w *gob.Encoder) error { func (p preCommitCompact) EncodeBinary(w *gob.Encoder) error {
return w.Encode(p) return w.Encode(p)
} }
// DecodeBinary implements Serializable interface. // DecodeBinary implements Serializable interface.
func (p *preCommitCompact) DecodeBinary(r *gob.Decoder) error { func (p *preCommitCompact) DecodeBinary(r *gob.Decoder) error {
return r.Decode(p) return r.Decode(p)
} }
// EncodeBinary implements Serializable interface. // EncodeBinary implements Serializable interface.
func (p commitCompact) EncodeBinary(w *gob.Encoder) error { func (p commitCompact) EncodeBinary(w *gob.Encoder) error {
return w.Encode(p) return w.Encode(p)
} }
// DecodeBinary implements Serializable interface. // DecodeBinary implements Serializable interface.
func (p *commitCompact) DecodeBinary(r *gob.Decoder) error { func (p *commitCompact) DecodeBinary(r *gob.Decoder) error {
return r.Decode(p) return r.Decode(p)
} }
// EncodeBinary implements Serializable interface. // EncodeBinary implements Serializable interface.
func (p preparationCompact) EncodeBinary(w *gob.Encoder) error { func (p preparationCompact) EncodeBinary(w *gob.Encoder) error {
return w.Encode(p) return w.Encode(p)
} }
// DecodeBinary implements Serializable interface. // DecodeBinary implements Serializable interface.
func (p *preparationCompact) DecodeBinary(r *gob.Decoder) error { func (p *preparationCompact) DecodeBinary(r *gob.Decoder) error {
return r.Decode(p) return r.Decode(p)
} }

View File

@ -1,9 +1,9 @@
package consensus package consensus
func secToNanoSec(s uint32) uint64 { func secToNanoSec(s uint32) uint64 {
return uint64(s) * 1000000000 return uint64(s) * 1000000000
} }
func nanoSecToSec(ns uint64) uint32 { func nanoSecToSec(ns uint64) uint32 {
return uint32(ns / 1000000000) return uint32(ns / 1000000000)
} }

View File

@ -1,34 +1,34 @@
package crypto package crypto
import ( import (
"crypto/rand" "crypto/rand"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestVerifySignature(t *testing.T) { func TestVerifySignature(t *testing.T) {
const dataSize = 1000 const dataSize = 1000
priv, pub := Generate(rand.Reader) priv, pub := Generate(rand.Reader)
data := make([]byte, dataSize) data := make([]byte, dataSize)
_, err := rand.Reader.Read(data) _, err := rand.Reader.Read(data)
require.NoError(t, err) require.NoError(t, err)
sign, err := priv.(*ECDSAPriv).Sign(data) sign, err := priv.(*ECDSAPriv).Sign(data)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 64, len(sign)) require.Equal(t, 64, len(sign))
err = pub.(*ECDSAPub).Verify(data, sign) err = pub.(*ECDSAPub).Verify(data, sign)
require.NoError(t, err) require.NoError(t, err)
} }
func TestGenerateWith(t *testing.T) { func TestGenerateWith(t *testing.T) {
priv, pub := GenerateWith(defaultSuite, rand.Reader) priv, pub := GenerateWith(defaultSuite, rand.Reader)
require.NotNil(t, priv) require.NotNil(t, priv)
require.NotNil(t, pub) require.NotNil(t, pub)
priv, pub = GenerateWith(suiteType(0xFF), rand.Reader) priv, pub = GenerateWith(suiteType(0xFF), rand.Reader)
require.Nil(t, priv) require.Nil(t, priv)
require.Nil(t, pub) require.Nil(t, pub)
} }

View File

@ -1,20 +1,20 @@
package crypto package crypto
import ( import (
"errors" "errors"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// Do not generate keys with not enough entropy. // Do not generate keys with not enough entropy.
func TestECDSA_Generate(t *testing.T) { func TestECDSA_Generate(t *testing.T) {
rd := &errorReader{} rd := &errorReader{}
priv, pub := GenerateWith(SuiteECDSA, rd) priv, pub := GenerateWith(SuiteECDSA, rd)
require.Nil(t, priv) require.Nil(t, priv)
require.Nil(t, pub) require.Nil(t, pub)
} }
type errorReader struct{} type errorReader struct{}
func (r *errorReader) Read(_ []byte) (int, error) { return 0, errors.New("error on read") } func (r *errorReader) Read(_ []byte) (int, error) { return 0, errors.New("error on read") }

View File

@ -1,46 +1,46 @@
package crypto package crypto
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
) )
const ( const (
Uint256Size = 32 Uint256Size = 32
Uint160Size = 20 Uint160Size = 20
) )
type ( type (
Uint256 [Uint256Size]byte Uint256 [Uint256Size]byte
Uint160 [Uint160Size]byte Uint160 [Uint160Size]byte
) )
// String implements fmt.Stringer interface. // String implements fmt.Stringer interface.
func (h Uint256) String() string { func (h Uint256) String() string {
return hex.EncodeToString(h[:]) return hex.EncodeToString(h[:])
} }
// String implements fmt.Stringer interface. // String implements fmt.Stringer interface.
func (h Uint160) String() string { func (h Uint160) String() string {
return hex.EncodeToString(h[:]) return hex.EncodeToString(h[:])
} }
// Hash256 returns double sha-256 of data. // Hash256 returns double sha-256 of data.
func Hash256(data []byte) Uint256 { func Hash256(data []byte) Uint256 {
h1 := sha256.Sum256(data) h1 := sha256.Sum256(data)
h2 := sha256.Sum256(h1[:]) h2 := sha256.Sum256(h1[:])
return h2 return h2
} }
// Hash160 returns ripemd160 from sha256 of data. // Hash160 returns ripemd160 from sha256 of data.
func Hash160(data []byte) Uint160 { func Hash160(data []byte) Uint160 {
var ( var (
h1 = sha256.Sum256(data) h1 = sha256.Sum256(data)
h Uint160 h Uint160
) )
copy(h[:], h1[:Uint160Size]) copy(h[:], h1[:Uint160Size])
return h return h
} }

View File

@ -1,50 +1,50 @@
package crypto package crypto
import ( import (
"encoding/hex" "encoding/hex"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var hash256tc = []struct { var hash256tc = []struct {
data []byte data []byte
hash Uint256 hash Uint256
}{ }{
{[]byte{}, parse256("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456")}, {[]byte{}, parse256("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456")},
{[]byte{0, 1, 2, 3}, parse256("f7a355c00c89a08c80636bed35556a210b51786f6803a494f28fc5ba05959fc2")}, {[]byte{0, 1, 2, 3}, parse256("f7a355c00c89a08c80636bed35556a210b51786f6803a494f28fc5ba05959fc2")},
} }
var hash160tc = []struct { var hash160tc = []struct {
data []byte data []byte
hash Uint160 hash Uint160
}{ }{
{[]byte{}, Uint160{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4}}, {[]byte{}, Uint160{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4}},
{[]byte{0, 1, 2, 3}, Uint160{0x5, 0x4e, 0xde, 0xc1, 0xd0, 0x21, 0x1f, 0x62, 0x4f, 0xed, 0xc, 0xbc, 0xa9, 0xd4, 0xf9, 0x40, 0xb, 0xe, 0x49, 0x1c}}, {[]byte{0, 1, 2, 3}, Uint160{0x5, 0x4e, 0xde, 0xc1, 0xd0, 0x21, 0x1f, 0x62, 0x4f, 0xed, 0xc, 0xbc, 0xa9, 0xd4, 0xf9, 0x40, 0xb, 0xe, 0x49, 0x1c}},
} }
func TestHash256(t *testing.T) { func TestHash256(t *testing.T) {
for _, tc := range hash256tc { for _, tc := range hash256tc {
require.Equal(t, tc.hash, Hash256(tc.data)) require.Equal(t, tc.hash, Hash256(tc.data))
} }
} }
func TestHash160(t *testing.T) { func TestHash160(t *testing.T) {
for _, tc := range hash160tc { for _, tc := range hash160tc {
require.Equal(t, tc.hash, Hash160(tc.data)) require.Equal(t, tc.hash, Hash160(tc.data))
} }
} }
func parse256(s string) (h Uint256) { func parse256(s string) (h Uint256) {
parseHex(h[:], s) parseHex(h[:], s)
return return
} }
func parseHex(b []byte, s string) { func parseHex(b []byte, s string) {
buf, err := hex.DecodeString(s) buf, err := hex.DecodeString(s)
if err != nil || len(buf) != len(b) { if err != nil || len(buf) != len(b) {
panic("invalid test data") panic("invalid test data")
} }
copy(b, buf) copy(b, buf)
} }

View File

@ -1,25 +1,25 @@
package dbft package dbft
// PreBlock is a generic interface for a PreBlock used by anti-MEV dBFT extension. // PreBlock is a generic interface for a PreBlock used by anti-MEV dBFT extension.
// It holds a "draft" of block that should be converted to a final block with the // It holds a "draft" of block that should be converted to a final block with the
// help of additional data held by PreCommit messages. // help of additional data held by PreCommit messages.
type PreBlock[H Hash] interface { type PreBlock[H Hash] interface {
// Data returns PreBlock's data CNs need to exchange during PreCommit phase. // Data returns PreBlock's data CNs need to exchange during PreCommit phase.
// Data represents additional information not related to a final block signature. // Data represents additional information not related to a final block signature.
Data() []byte Data() []byte
// SetData generates and sets PreBlock's data CNs need to exchange during // SetData generates and sets PreBlock's data CNs need to exchange during
// PreCommit phase. // PreCommit phase.
SetData(key PrivateKey) error SetData(key PrivateKey) error
// Verify checks if data related to PreCommit phase is correct. This method is // Verify checks if data related to PreCommit phase is correct. This method is
// refined on PreBlock rather than on PreCommit message since PreBlock itself is // refined on PreBlock rather than on PreCommit message since PreBlock itself is
// required for PreCommit's data verification. It's guaranteed that all // required for PreCommit's data verification. It's guaranteed that all
// proposed transactions are collected by the moment of call to Verify. // proposed transactions are collected by the moment of call to Verify.
Verify(key PublicKey, data []byte) error Verify(key PublicKey, data []byte) error
// Transactions returns PreBlock's transaction list. This list may be different // Transactions returns PreBlock's transaction list. This list may be different
// comparing to the final set of Block's transactions. // comparing to the final set of Block's transactions.
Transactions() []Transaction[H] Transactions() []Transaction[H]
// SetTransactions sets PreBlock's transaction list. This list may be different // SetTransactions sets PreBlock's transaction list. This list may be different
// comparing to the final set of Block's transactions. // comparing to the final set of Block's transactions.
SetTransactions([]Transaction[H]) SetTransactions([]Transaction[H])
} }

View File

@ -1,10 +1,10 @@
package dbft package dbft
// PreCommit is an interface for dBFT PreCommit message. This message is used right // PreCommit is an interface for dBFT PreCommit message. This message is used right
// before the Commit phase to exchange additional information required for the final // before the Commit phase to exchange additional information required for the final
// block construction in anti-MEV dBFT extension. // block construction in anti-MEV dBFT extension.
type PreCommit interface { type PreCommit interface {
// Data returns PreCommit's data that should be used for the final // Data returns PreCommit's data that should be used for the final
// Block construction in anti-MEV dBFT extension. // Block construction in anti-MEV dBFT extension.
Data() []byte Data() []byte
} }

View File

@ -1,11 +1,11 @@
package dbft package dbft
// PrepareRequest represents dBFT PrepareRequest message. // PrepareRequest represents dBFT PrepareRequest message.
type PrepareRequest[H Hash] interface { type PrepareRequest[H Hash] interface {
// Timestamp returns this message's timestamp. // Timestamp returns this message's timestamp.
Timestamp() uint64 Timestamp() uint64
// Nonce is a random nonce. // Nonce is a random nonce.
Nonce() uint64 Nonce() uint64
// TransactionHashes returns hashes of all transaction in a proposed block. // TransactionHashes returns hashes of all transaction in a proposed block.
TransactionHashes() []H TransactionHashes() []H
} }

View File

@ -1,8 +1,8 @@
package dbft package dbft
// PrepareResponse represents dBFT PrepareResponse message. // PrepareResponse represents dBFT PrepareResponse message.
type PrepareResponse[H Hash] interface { type PrepareResponse[H Hash] interface {
// PreparationHash returns the hash of PrepareRequest payload // PreparationHash returns the hash of PrepareRequest payload
// for this epoch. // for this epoch.
PreparationHash() H PreparationHash() H
} }

View File

@ -1,23 +1,23 @@
package dbft package dbft
// RecoveryMessage represents dBFT Recovery message. // RecoveryMessage represents dBFT Recovery message.
type RecoveryMessage[H Hash] interface { type RecoveryMessage[H Hash] interface {
// AddPayload adds payload from this epoch to be recovered. // AddPayload adds payload from this epoch to be recovered.
AddPayload(p ConsensusPayload[H]) AddPayload(p ConsensusPayload[H])
// GetPrepareRequest returns PrepareRequest to be processed. // GetPrepareRequest returns PrepareRequest to be processed.
GetPrepareRequest(p ConsensusPayload[H], validators []PublicKey, primary uint16) ConsensusPayload[H] GetPrepareRequest(p ConsensusPayload[H], validators []PublicKey, primary uint16) ConsensusPayload[H]
// GetPrepareResponses returns a slice of PrepareResponse in any order. // GetPrepareResponses returns a slice of PrepareResponse in any order.
GetPrepareResponses(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] GetPrepareResponses(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H]
// GetChangeViews returns a slice of ChangeView in any order. // GetChangeViews returns a slice of ChangeView in any order.
GetChangeViews(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] GetChangeViews(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H]
// GetPreCommits returns a slice of PreCommit messages in any order. // GetPreCommits returns a slice of PreCommit messages in any order.
// If implemented on networks with no AntiMEV extension it can just // If implemented on networks with no AntiMEV extension it can just
// always return nil. // always return nil.
GetPreCommits(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] GetPreCommits(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H]
// GetCommits returns a slice of Commit in any order. // GetCommits returns a slice of Commit in any order.
GetCommits(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H] GetCommits(p ConsensusPayload[H], validators []PublicKey) []ConsensusPayload[H]
// PreparationHash returns has of PrepareRequest payload for this epoch. // PreparationHash returns has of PrepareRequest payload for this epoch.
// It can be useful in case only PrepareResponse payloads were received. // It can be useful in case only PrepareResponse payloads were received.
PreparationHash() *H PreparationHash() *H
} }

View File

@ -1,7 +1,7 @@
package dbft package dbft
// RecoveryRequest represents dBFT RecoveryRequest message. // RecoveryRequest represents dBFT RecoveryRequest message.
type RecoveryRequest interface { type RecoveryRequest interface {
// Timestamp returns this message's timestamp. // Timestamp returns this message's timestamp.
Timestamp() uint64 Timestamp() uint64
} }

52
rtt.go
View File

@ -1,26 +1,26 @@
package dbft package dbft
import ( import (
"time" "time"
) )
const rttLength = 7 * 10 // 10 rounds with 7 nodes const rttLength = 7 * 10 // 10 rounds with 7 nodes
type rtt struct { type rtt struct {
times [rttLength]time.Duration times [rttLength]time.Duration
idx int idx int
avg time.Duration avg time.Duration
} }
func (r *rtt) addTime(t time.Duration) { func (r *rtt) addTime(t time.Duration) {
var old = r.times[r.idx] var old = r.times[r.idx]
if old != 0 { if old != 0 {
t = min(t, 2*old) // Too long delays should be normalized, we don't want to overshoot. t = min(t, 2*old) // Too long delays should be normalized, we don't want to overshoot.
} }
r.avg = r.avg + (t-old)/time.Duration(len(r.times)) r.avg = r.avg + (t-old)/time.Duration(len(r.times))
r.avg = max(0, r.avg) // Can't be less than zero. r.avg = max(0, r.avg) // Can't be less than zero.
r.times[r.idx] = t r.times[r.idx] = t
r.idx = (r.idx + 1) % len(r.times) r.idx = (r.idx + 1) % len(r.times)
} }

472
send.go
View File

@ -1,236 +1,236 @@
package dbft package dbft
import ( import (
"fmt" "fmt"
"go.uber.org/zap" "go.uber.org/zap"
) )
func (d *DBFT[H]) broadcast(msg ConsensusPayload[H]) { func (d *DBFT[H]) broadcast(msg ConsensusPayload[H]) {
d.Logger.Debug("broadcasting message", d.Logger.Debug("broadcasting message",
zap.Stringer("type", msg.Type()), zap.Stringer("type", msg.Type()),
zap.Uint32("height", d.BlockIndex), zap.Uint32("height", d.BlockIndex),
zap.Uint("view", uint(d.ViewNumber))) zap.Uint("view", uint(d.ViewNumber)))
msg.SetValidatorIndex(uint16(d.MyIndex)) msg.SetValidatorIndex(uint16(d.MyIndex))
d.Broadcast(msg) d.Broadcast(msg)
} }
func (c *Context[H]) makePrepareRequest(force bool) ConsensusPayload[H] { func (c *Context[H]) makePrepareRequest(force bool) ConsensusPayload[H] {
if !c.Fill(force) { if !c.Fill(force) {
return nil return nil
} }
req := c.Config.NewPrepareRequest(c.Timestamp, c.Nonce, c.TransactionHashes) req := c.Config.NewPrepareRequest(c.Timestamp, c.Nonce, c.TransactionHashes)
return c.Config.NewConsensusPayload(c, PrepareRequestType, req) return c.Config.NewConsensusPayload(c, PrepareRequestType, req)
} }
func (d *DBFT[H]) sendPrepareRequest(force bool) { func (d *DBFT[H]) sendPrepareRequest(force bool) {
msg := d.makePrepareRequest(force) msg := d.makePrepareRequest(force)
if msg == ConsensusPayload[H](nil) { if msg == ConsensusPayload[H](nil) {
d.subscribeForTransactions() d.subscribeForTransactions()
// Try one more time since there's a tiny race between an attempt to // Try one more time since there's a tiny race between an attempt to
// construct prepare request and transactions subscription. // construct prepare request and transactions subscription.
msg = d.makePrepareRequest(force) msg = d.makePrepareRequest(force)
if msg == ConsensusPayload[H](nil) { if msg == ConsensusPayload[H](nil) {
delay := d.maxTimePerBlock - d.timePerBlock delay := d.maxTimePerBlock - d.timePerBlock
d.changeTimer(delay) d.changeTimer(delay)
return return
} }
} }
d.unsubscribeFromTransactions() d.unsubscribeFromTransactions()
d.PreparationPayloads[d.MyIndex] = msg d.PreparationPayloads[d.MyIndex] = msg
d.broadcast(msg) d.broadcast(msg)
d.prepareSentTime = d.Timer.Now() d.prepareSentTime = d.Timer.Now()
delay := d.timePerBlock << (d.ViewNumber + 1) delay := d.timePerBlock << (d.ViewNumber + 1)
if d.ViewNumber == 0 { if d.ViewNumber == 0 {
delay -= d.timePerBlock delay -= d.timePerBlock
} }
d.Logger.Info("sending PrepareRequest", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) d.Logger.Info("sending PrepareRequest", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber)))
d.changeTimer(delay) d.changeTimer(delay)
d.checkPrepare() d.checkPrepare()
} }
func (c *Context[H]) makeChangeView(ts uint64, reason ChangeViewReason) ConsensusPayload[H] { func (c *Context[H]) makeChangeView(ts uint64, reason ChangeViewReason) ConsensusPayload[H] {
cv := c.Config.NewChangeView(c.ViewNumber+1, reason, ts) cv := c.Config.NewChangeView(c.ViewNumber+1, reason, ts)
msg := c.Config.NewConsensusPayload(c, ChangeViewType, cv) msg := c.Config.NewConsensusPayload(c, ChangeViewType, cv)
c.ChangeViewPayloads[c.MyIndex] = msg c.ChangeViewPayloads[c.MyIndex] = msg
return msg return msg
} }
func (d *DBFT[H]) sendChangeView(reason ChangeViewReason) { func (d *DBFT[H]) sendChangeView(reason ChangeViewReason) {
if d.Context.WatchOnly() { if d.Context.WatchOnly() {
return return
} }
newView := d.ViewNumber + 1 newView := d.ViewNumber + 1
d.changeTimer(d.timePerBlock << (newView + 1)) d.changeTimer(d.timePerBlock << (newView + 1))
nc := d.CountCommitted() nc := d.CountCommitted()
nf := d.CountFailed() nf := d.CountFailed()
if reason == CVTimeout && nc+nf > d.F() { if reason == CVTimeout && nc+nf > d.F() {
d.Logger.Info("skip change view", zap.Int("nc", nc), zap.Int("nf", nf)) d.Logger.Info("skip change view", zap.Int("nc", nc), zap.Int("nf", nf))
d.sendRecoveryRequest() d.sendRecoveryRequest()
return return
} }
// Timeout while missing transactions, set the real reason. // Timeout while missing transactions, set the real reason.
if !d.hasAllTransactions() && reason == CVTimeout { if !d.hasAllTransactions() && reason == CVTimeout {
reason = CVTxNotFound reason = CVTxNotFound
} }
d.Logger.Info("request change view", d.Logger.Info("request change view",
zap.Int("view", int(d.ViewNumber)), zap.Int("view", int(d.ViewNumber)),
zap.Uint32("height", d.BlockIndex), zap.Uint32("height", d.BlockIndex),
zap.Stringer("reason", reason), zap.Stringer("reason", reason),
zap.Int("new_view", int(newView)), zap.Int("new_view", int(newView)),
zap.Int("nc", nc), zap.Int("nc", nc),
zap.Int("nf", nf)) zap.Int("nf", nf))
msg := d.makeChangeView(uint64(d.Timer.Now().UnixNano()), reason) msg := d.makeChangeView(uint64(d.Timer.Now().UnixNano()), reason)
d.StopTxFlow() d.StopTxFlow()
d.broadcast(msg) d.broadcast(msg)
d.checkChangeView(newView) d.checkChangeView(newView)
} }
func (c *Context[H]) makePrepareResponse() ConsensusPayload[H] { func (c *Context[H]) makePrepareResponse() ConsensusPayload[H] {
resp := c.Config.NewPrepareResponse(c.PreparationPayloads[c.PrimaryIndex].Hash()) resp := c.Config.NewPrepareResponse(c.PreparationPayloads[c.PrimaryIndex].Hash())
msg := c.Config.NewConsensusPayload(c, PrepareResponseType, resp) msg := c.Config.NewConsensusPayload(c, PrepareResponseType, resp)
c.PreparationPayloads[c.MyIndex] = msg c.PreparationPayloads[c.MyIndex] = msg
return msg return msg
} }
func (d *DBFT[H]) sendPrepareResponse() { func (d *DBFT[H]) sendPrepareResponse() {
msg := d.makePrepareResponse() msg := d.makePrepareResponse()
d.Logger.Info("sending PrepareResponse", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) d.Logger.Info("sending PrepareResponse", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber)))
d.StopTxFlow() d.StopTxFlow()
d.broadcast(msg) d.broadcast(msg)
} }
func (c *Context[H]) makePreCommit() (ConsensusPayload[H], error) { func (c *Context[H]) makePreCommit() (ConsensusPayload[H], error) {
if msg := c.PreCommitPayloads[c.MyIndex]; msg != nil { if msg := c.PreCommitPayloads[c.MyIndex]; msg != nil {
return msg, nil return msg, nil
} }
if preB := c.CreatePreBlock(); preB != nil { if preB := c.CreatePreBlock(); preB != nil {
var preData []byte var preData []byte
if err := preB.SetData(c.Priv); err == nil { if err := preB.SetData(c.Priv); err == nil {
preData = preB.Data() preData = preB.Data()
} else { } else {
return nil, fmt.Errorf("PreCommit data construction failed: %w", err) return nil, fmt.Errorf("PreCommit data construction failed: %w", err)
} }
preCommit := c.Config.NewPreCommit(preData) preCommit := c.Config.NewPreCommit(preData)
return c.Config.NewConsensusPayload(c, PreCommitType, preCommit), nil return c.Config.NewConsensusPayload(c, PreCommitType, preCommit), nil
} }
return nil, fmt.Errorf("failed to construct PreBlock") return nil, fmt.Errorf("failed to construct PreBlock")
} }
func (c *Context[H]) makeCommit() (ConsensusPayload[H], error) { func (c *Context[H]) makeCommit() (ConsensusPayload[H], error) {
if msg := c.CommitPayloads[c.MyIndex]; msg != nil { if msg := c.CommitPayloads[c.MyIndex]; msg != nil {
return msg, nil return msg, nil
} }
if b := c.MakeHeader(); b != nil { if b := c.MakeHeader(); b != nil {
var sign []byte var sign []byte
if err := b.Sign(c.Priv); err == nil { if err := b.Sign(c.Priv); err == nil {
sign = b.Signature() sign = b.Signature()
} else { } else {
return nil, fmt.Errorf("header signing failed: %w", err) return nil, fmt.Errorf("header signing failed: %w", err)
} }
commit := c.Config.NewCommit(sign) commit := c.Config.NewCommit(sign)
return c.Config.NewConsensusPayload(c, CommitType, commit), nil return c.Config.NewConsensusPayload(c, CommitType, commit), nil
} }
return nil, fmt.Errorf("failed to construct Header") return nil, fmt.Errorf("failed to construct Header")
} }
func (d *DBFT[H]) sendPreCommit() { func (d *DBFT[H]) sendPreCommit() {
msg, err := d.makePreCommit() msg, err := d.makePreCommit()
if err != nil { if err != nil {
d.Logger.Error("failed to construct PreCommit", zap.Error(err)) d.Logger.Error("failed to construct PreCommit", zap.Error(err))
return return
} }
d.PreCommitPayloads[d.MyIndex] = msg d.PreCommitPayloads[d.MyIndex] = msg
d.Logger.Info("sending PreCommit", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) d.Logger.Info("sending PreCommit", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber)))
d.broadcast(msg) d.broadcast(msg)
} }
func (d *DBFT[H]) sendCommit() { func (d *DBFT[H]) sendCommit() {
msg, err := d.makeCommit() msg, err := d.makeCommit()
if err != nil { if err != nil {
d.Logger.Error("failed to construct Commit", zap.Error(err)) d.Logger.Error("failed to construct Commit", zap.Error(err))
return return
} }
d.CommitPayloads[d.MyIndex] = msg d.CommitPayloads[d.MyIndex] = msg
d.Logger.Info("sending Commit", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber))) d.Logger.Info("sending Commit", zap.Uint32("height", d.BlockIndex), zap.Uint("view", uint(d.ViewNumber)))
d.broadcast(msg) d.broadcast(msg)
} }
func (d *DBFT[H]) sendRecoveryRequest() { func (d *DBFT[H]) sendRecoveryRequest() {
// If we're here, something is wrong, we either missing some messages or // If we're here, something is wrong, we either missing some messages or
// transactions or both, so re-request missing transactions here too. // transactions or both, so re-request missing transactions here too.
if d.RequestSentOrReceived() && !d.hasAllTransactions() { if d.RequestSentOrReceived() && !d.hasAllTransactions() {
d.processMissingTx() d.processMissingTx()
} }
req := d.NewRecoveryRequest(uint64(d.Timer.Now().UnixNano())) req := d.NewRecoveryRequest(uint64(d.Timer.Now().UnixNano()))
d.broadcast(d.NewConsensusPayload(&d.Context, RecoveryRequestType, req)) d.broadcast(d.NewConsensusPayload(&d.Context, RecoveryRequestType, req))
} }
func (c *Context[H]) makeRecoveryMessage() ConsensusPayload[H] { func (c *Context[H]) makeRecoveryMessage() ConsensusPayload[H] {
recovery := c.Config.NewRecoveryMessage() recovery := c.Config.NewRecoveryMessage()
for _, p := range c.PreparationPayloads { for _, p := range c.PreparationPayloads {
if p != nil { if p != nil {
recovery.AddPayload(p) recovery.AddPayload(p)
} }
} }
cv := c.LastChangeViewPayloads cv := c.LastChangeViewPayloads
// if byte(msg.ViewNumber) == c.ViewNumber { // if byte(msg.ViewNumber) == c.ViewNumber {
// cv = c.changeViewPayloads // cv = c.changeViewPayloads
// } // }
for _, p := range cv { for _, p := range cv {
if p != nil { if p != nil {
recovery.AddPayload(p) recovery.AddPayload(p)
} }
} }
if c.PreCommitSent() { if c.PreCommitSent() {
for _, p := range c.PreCommitPayloads { for _, p := range c.PreCommitPayloads {
if p != nil { if p != nil {
recovery.AddPayload(p) recovery.AddPayload(p)
} }
} }
} }
if c.CommitSent() { if c.CommitSent() {
for _, p := range c.CommitPayloads { for _, p := range c.CommitPayloads {
if p != nil { if p != nil {
recovery.AddPayload(p) recovery.AddPayload(p)
} }
} }
} }
return c.Config.NewConsensusPayload(c, RecoveryMessageType, recovery) return c.Config.NewConsensusPayload(c, RecoveryMessageType, recovery)
} }
func (d *DBFT[H]) sendRecoveryMessage() { func (d *DBFT[H]) sendRecoveryMessage() {
d.broadcast(d.makeRecoveryMessage()) d.broadcast(d.makeRecoveryMessage())
} }

View File

@ -1,22 +1,22 @@
package dbft package dbft
import ( import (
"time" "time"
) )
// Timer is an interface which implements all time-related // Timer is an interface which implements all time-related
// functions. It can be mocked for testing. // functions. It can be mocked for testing.
type Timer interface { type Timer interface {
// Now returns current time. // Now returns current time.
Now() time.Time Now() time.Time
// Reset resets timer to the specified block height and view. // Reset resets timer to the specified block height and view.
Reset(height uint32, view byte, d time.Duration) Reset(height uint32, view byte, d time.Duration)
// Extend extends current timer with duration d. // Extend extends current timer with duration d.
Extend(d time.Duration) Extend(d time.Duration)
// Height returns current height set for the timer. // Height returns current height set for the timer.
Height() uint32 Height() uint32
// View returns current view set for the timer. // View returns current view set for the timer.
View() byte View() byte
// C returns channel for timer events. // C returns channel for timer events.
C() <-chan time.Time C() <-chan time.Time
} }

View File

@ -1,97 +1,97 @@
/* /*
Package timer contains default implementation of [dbft.Timer] interface and provides Package timer contains default implementation of [dbft.Timer] interface and provides
all necessary timer-related functionality to [dbft.DBFT] service. all necessary timer-related functionality to [dbft.DBFT] service.
*/ */
package timer package timer
import ( import (
"time" "time"
) )
type ( type (
// Timer is a default [dbft.Timer] implementation. // Timer is a default [dbft.Timer] implementation.
Timer struct { Timer struct {
height uint32 height uint32
view byte view byte
s time.Time s time.Time
d time.Duration d time.Duration
tt *time.Timer tt *time.Timer
ch chan time.Time ch chan time.Time
} }
) )
// New returns default Timer implementation. // New returns default Timer implementation.
func New() *Timer { func New() *Timer {
t := &Timer{ t := &Timer{
ch: make(chan time.Time, 1), ch: make(chan time.Time, 1),
} }
return t return t
} }
// C implements Timer interface. // C implements Timer interface.
func (t *Timer) C() <-chan time.Time { func (t *Timer) C() <-chan time.Time {
if t.tt == nil { if t.tt == nil {
return t.ch return t.ch
} }
return t.tt.C return t.tt.C
} }
// Height returns current timer height. // Height returns current timer height.
func (t *Timer) Height() uint32 { func (t *Timer) Height() uint32 {
return t.height return t.height
} }
// View return current timer view. // View return current timer view.
func (t *Timer) View() byte { func (t *Timer) View() byte {
return t.view return t.view
} }
// Reset implements Timer interface. // Reset implements Timer interface.
func (t *Timer) Reset(height uint32, view byte, d time.Duration) { func (t *Timer) Reset(height uint32, view byte, d time.Duration) {
t.stop() t.stop()
t.s = t.Now() t.s = t.Now()
t.d = d t.d = d
t.height = height t.height = height
t.view = view t.view = view
if t.d != 0 { if t.d != 0 {
t.tt = time.NewTimer(t.d) t.tt = time.NewTimer(t.d)
} else { } else {
t.tt = nil t.tt = nil
drain(t.ch) drain(t.ch)
t.ch <- t.s t.ch <- t.s
} }
} }
func drain(ch <-chan time.Time) { func drain(ch <-chan time.Time) {
select { select {
case <-ch: case <-ch:
default: default:
} }
} }
// stop stops the Timer. // stop stops the Timer.
func (t *Timer) stop() { func (t *Timer) stop() {
if t.tt != nil { if t.tt != nil {
t.tt.Stop() t.tt.Stop()
t.tt = nil t.tt = nil
} }
} }
// Extend implements Timer interface. // Extend implements Timer interface.
func (t *Timer) Extend(d time.Duration) { func (t *Timer) Extend(d time.Duration) {
t.d += d t.d += d
if elapsed := time.Since(t.s); t.d > elapsed { if elapsed := time.Since(t.s); t.d > elapsed {
t.stop() t.stop()
t.tt = time.NewTimer(t.d - elapsed) t.tt = time.NewTimer(t.d - elapsed)
} }
} }
// Now implements Timer interface. // Now implements Timer interface.
func (t *Timer) Now() time.Time { func (t *Timer) Now() time.Time {
return time.Now() return time.Now()
} }

View File

@ -1,56 +1,56 @@
package timer package timer
import ( import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestTimer_Reset(t *testing.T) { func TestTimer_Reset(t *testing.T) {
tt := New() tt := New()
tt.Reset(1, 2, time.Millisecond*100) tt.Reset(1, 2, time.Millisecond*100)
time.Sleep(time.Millisecond * 200) time.Sleep(time.Millisecond * 200)
shouldReceive(t, tt, 1, 2, "no value in timer") shouldReceive(t, tt, 1, 2, "no value in timer")
tt.Reset(1, 2, time.Second) tt.Reset(1, 2, time.Second)
tt.Reset(2, 3, 0) tt.Reset(2, 3, 0)
shouldReceive(t, tt, 2, 3, "no value in timer after reset(0)") shouldReceive(t, tt, 2, 3, "no value in timer after reset(0)")
tt.Reset(1, 2, time.Millisecond*100) tt.Reset(1, 2, time.Millisecond*100)
time.Sleep(time.Millisecond * 200) time.Sleep(time.Millisecond * 200)
tt.Reset(1, 3, time.Millisecond*100) tt.Reset(1, 3, time.Millisecond*100)
time.Sleep(time.Millisecond * 200) time.Sleep(time.Millisecond * 200)
shouldReceive(t, tt, 1, 3, "invalid value after reset") shouldReceive(t, tt, 1, 3, "invalid value after reset")
tt.Reset(3, 1, time.Millisecond*100) tt.Reset(3, 1, time.Millisecond*100)
shouldNotReceive(t, tt, "value arrived too early") shouldNotReceive(t, tt, "value arrived too early")
tt.Extend(time.Millisecond * 300) tt.Extend(time.Millisecond * 300)
time.Sleep(time.Millisecond * 200) time.Sleep(time.Millisecond * 200)
shouldNotReceive(t, tt, "value arrived too early after extend") shouldNotReceive(t, tt, "value arrived too early after extend")
time.Sleep(time.Millisecond * 300) time.Sleep(time.Millisecond * 300)
shouldReceive(t, tt, 3, 1, "no value in timer after extend") shouldReceive(t, tt, 3, 1, "no value in timer after extend")
} }
func shouldReceive(t *testing.T, tt *Timer, height uint32, view byte, msg string) { func shouldReceive(t *testing.T, tt *Timer, height uint32, view byte, msg string) {
select { select {
case <-tt.C(): case <-tt.C():
gotHeight := tt.Height() gotHeight := tt.Height()
gotView := tt.View() gotView := tt.View()
require.Equal(t, height, gotHeight) require.Equal(t, height, gotHeight)
require.Equal(t, view, gotView) require.Equal(t, view, gotView)
default: default:
require.Fail(t, msg) require.Fail(t, msg)
} }
} }
func shouldNotReceive(t *testing.T, tt *Timer, msg string) { func shouldNotReceive(t *testing.T, tt *Timer, msg string) {
select { select {
case <-tt.C(): case <-tt.C():
require.Fail(t, msg) require.Fail(t, msg)
default: default:
} }
} }

View File

@ -1,8 +1,8 @@
package dbft package dbft
// Transaction is a generic transaction interface. // Transaction is a generic transaction interface.
type Transaction[H Hash] interface { type Transaction[H Hash] interface {
// Hash must return cryptographic hash of the transaction. // Hash must return cryptographic hash of the transaction.
// Transactions which have equal hashes are considered equal. // Transactions which have equal hashes are considered equal.
Hash() H Hash() H
} }